🎨 更新环境配置,添加 Dockerfile,优化代码结构
This commit is contained in:
@@ -1 +1 @@
|
||||
VITE_BASE_API=/api
|
||||
VITE_BASE_API=http://localhost:8888
|
||||
|
||||
@@ -1 +1 @@
|
||||
VITE_BASE_API=/api
|
||||
VITE_BASE_API=https://api.wanjia.ai
|
||||
|
||||
2
web-admin/.gitignore
vendored
2
web-admin/.gitignore
vendored
@@ -1,5 +1,5 @@
|
||||
# Logs
|
||||
logs
|
||||
#logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
|
||||
26
web-admin/Dockerfile
Normal file
26
web-admin/Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
||||
# ---- deps ----
|
||||
FROM node:20-alpine AS deps
|
||||
WORKDIR /app
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# ---- build ----
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
ARG VITE_BASE_API=/api
|
||||
ENV VITE_BASE_API=$VITE_BASE_API
|
||||
|
||||
RUN pnpm exec vite build
|
||||
|
||||
# ---- runtime ----
|
||||
FROM nginx:alpine AS runner
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
33
web-admin/nginx.conf
Normal file
33
web-admin/nginx.conf
Normal file
@@ -0,0 +1,33 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
# 文件上传大小限制 50MB
|
||||
client_max_body_size 50m;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# 将 /api/ 请求反代到后端(去掉 /api 前缀,与 vite dev proxy 行为一致)
|
||||
location /api/ {
|
||||
proxy_pass http://server:8888/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_read_timeout 35s;
|
||||
proxy_send_timeout 35s;
|
||||
proxy_connect_timeout 10s;
|
||||
}
|
||||
|
||||
# SPA fallback:所有非文件路径返回 index.html
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# 静态资源缓存
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
"dayjs": "^1.11.20",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router-dom": "^7.14.0",
|
||||
"recharts": "^3.8.1",
|
||||
"zustand": "^5.0.12"
|
||||
|
||||
3974
web-admin/pnpm-lock.yaml
generated
Normal file
3974
web-admin/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,123 +1,606 @@
|
||||
import { Card, Col, List, Progress, Row, Statistic, Tag, Typography } from 'antd'
|
||||
import { moduleList } from '@/features/discovery/systemInventory'
|
||||
import { flattenMenus } from '@/lib/menu'
|
||||
import { useAuthStore } from '@/store/auth'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Image,
|
||||
Row,
|
||||
Skeleton,
|
||||
Statistic,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
message,
|
||||
Drawer,
|
||||
Input,
|
||||
Space,
|
||||
Upload,
|
||||
} from 'antd'
|
||||
import {
|
||||
AlertOutlined,
|
||||
BarChartOutlined,
|
||||
BookOutlined,
|
||||
MessageOutlined,
|
||||
ReloadOutlined,
|
||||
RiseOutlined,
|
||||
StarOutlined,
|
||||
UploadOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { useState } from 'react'
|
||||
import { dashboardApi, feedbackAdminApi, uploadFile, getMediaUrl } from '@/lib/api'
|
||||
import type { DashboardFeedbackPendingItem, DashboardTopCreator } from '@/types/system'
|
||||
|
||||
export function DashboardPage() {
|
||||
const menus = useAuthStore((state) => state.menus)
|
||||
const user = useAuthStore((state) => state.user)
|
||||
const { Text, Title } = Typography
|
||||
|
||||
const flatMenus = flattenMenus(menus)
|
||||
const implementedCount = moduleList.filter((item) => item.status === 'ready').length
|
||||
const partialCount = moduleList.filter((item) => item.status === 'partial').length
|
||||
const allowedModules = flatMenus.length
|
||||
const allEndpoints = moduleList.reduce((total, item) => total + item.endpoints.length, 0)
|
||||
// ── 快速回复 Drawer ──────────────────────────────────────────
|
||||
const QUICK_REPLIES: { label: string; value: string }[] = [
|
||||
{ label: '正在处理', value: '您好,我们已收到您的反馈,正在为您处理,请耐心等待。' },
|
||||
{ label: '已解决', value: '您好,您反馈的问题已解决,如还有疑问欢迎继续联系我们。' },
|
||||
{ label: '无法复现', value: '您好,我们尝试复现您描述的问题但未能成功,请提供更多截图或操作步骤以便排查。' },
|
||||
]
|
||||
|
||||
function QuickReplyDrawer({
|
||||
item,
|
||||
onClose,
|
||||
onReplied,
|
||||
}: {
|
||||
item: DashboardFeedbackPendingItem | null
|
||||
onClose: () => void
|
||||
onReplied: () => void
|
||||
}) {
|
||||
const [content, setContent] = useState('')
|
||||
const [images, setImages] = useState<string[]>([])
|
||||
const [sending, setSending] = useState(false)
|
||||
const [uploading, setUploading] = useState(false)
|
||||
|
||||
const detailQuery = useQuery({
|
||||
queryKey: ['feedback-detail', item?.id],
|
||||
queryFn: () => feedbackAdminApi.getById(item!.id),
|
||||
enabled: !!item,
|
||||
})
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!item || (!content.trim() && images.length === 0)) return
|
||||
setSending(true)
|
||||
try {
|
||||
await feedbackAdminApi.reply(item.id, {
|
||||
content: content.trim() || undefined,
|
||||
images: images.length > 0 ? images : undefined,
|
||||
})
|
||||
message.success('回复成功')
|
||||
setContent('')
|
||||
setImages([])
|
||||
onReplied()
|
||||
onClose()
|
||||
} catch {
|
||||
message.error('回复失败')
|
||||
} finally {
|
||||
setSending(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page-stack">
|
||||
<Card className="glass-panel page-panel">
|
||||
<div className="hero-grid">
|
||||
<div>
|
||||
<Tag color="processing" style={{ marginBottom: 12 }}>
|
||||
React 管理后台重构中
|
||||
</Tag>
|
||||
<Typography.Title level={2} style={{ marginBottom: 12 }}>
|
||||
{user?.nickName || user?.userName || '管理员'},欢迎回来
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph className="text-muted" style={{ maxWidth: 720, marginBottom: 20 }}>
|
||||
新后台已经基于真实菜单树和后端接口协议接管路由结构。当前仪表盘会直接反映已有菜单权限、模块覆盖进度和需要继续迁移的能力。
|
||||
</Typography.Paragraph>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}>
|
||||
<Card bordered={false} style={{ background: 'var(--accent-soft)' }}>
|
||||
<Statistic title="当前角色默认首页" value={user?.authority?.defaultRouter || 'dashboard'} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card bordered={false} style={{ background: 'var(--success-soft)' }}>
|
||||
<Statistic title="可访问内部菜单" value={allowedModules} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Drawer
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Avatar src={item?.userAvatar} size={28} />
|
||||
<span style={{ fontSize: 14, fontWeight: 600 }}>
|
||||
{item?.userNickname} · {item?.title}
|
||||
</span>
|
||||
{item && <Badge count={item.unreadCount} />}
|
||||
</div>
|
||||
}
|
||||
open={!!item}
|
||||
onClose={onClose}
|
||||
width={520}
|
||||
footer={
|
||||
<div style={{ display: 'flex', gap: 8, flexDirection: 'column' }}>
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||||
{QUICK_REPLIES.map((r) => (
|
||||
<Button
|
||||
key={r.label}
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setContent((prev) => (prev.trim() ? `${prev}\n${r.value}` : r.value))
|
||||
}
|
||||
>
|
||||
{r.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<Card bordered={false} style={{ background: 'rgba(16, 37, 66, 0.04)' }}>
|
||||
<Typography.Title level={4}>重构覆盖率</Typography.Title>
|
||||
<Progress
|
||||
percent={Math.round((implementedCount / moduleList.length) * 100)}
|
||||
strokeColor="#d16f3f"
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Input.TextArea
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder="输入回复内容..."
|
||||
autoSize={{ minRows: 2, maxRows: 5 }}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<List
|
||||
size="small"
|
||||
style={{ marginTop: 16 }}
|
||||
dataSource={[
|
||||
`已接入模块 ${implementedCount} 个`,
|
||||
`待继续迁移 ${partialCount} 个`,
|
||||
`模块登记总数 ${moduleList.length} 个`,
|
||||
`清点出的接口触点 ${allEndpoints} 个`,
|
||||
]}
|
||||
renderItem={(item) => <List.Item>{item}</List.Item>}
|
||||
/>
|
||||
</Card>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<Upload
|
||||
showUploadList={false}
|
||||
accept="image/*"
|
||||
customRequest={async ({ file, onSuccess, onError }) => {
|
||||
setUploading(true)
|
||||
try {
|
||||
const url = await uploadFile(file as File)
|
||||
setImages((prev) => [...prev, url])
|
||||
onSuccess?.(url)
|
||||
} catch {
|
||||
onError?.(new Error('上传失败'))
|
||||
message.error('图片上传失败')
|
||||
} finally {
|
||||
setUploading(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button icon={<UploadOutlined />} loading={uploading} size="small" />
|
||||
</Upload>
|
||||
<Button type="primary" size="small" loading={sending} onClick={handleSend}>
|
||||
发送
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{images.length > 0 && (
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||||
<Image.PreviewGroup>
|
||||
{images.map((url, i) => (
|
||||
<div key={i} style={{ position: 'relative' }}>
|
||||
<Image
|
||||
src={url}
|
||||
width={56}
|
||||
height={56}
|
||||
style={{ objectFit: 'cover', borderRadius: 4 }}
|
||||
/>
|
||||
<button
|
||||
onClick={() => setImages((prev) => prev.filter((_, idx) => idx !== i))}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: -4,
|
||||
right: -4,
|
||||
border: 'none',
|
||||
background: 'rgba(0,0,0,0.5)',
|
||||
color: '#fff',
|
||||
borderRadius: '50%',
|
||||
width: 16,
|
||||
height: 16,
|
||||
cursor: 'pointer',
|
||||
fontSize: 10,
|
||||
lineHeight: '16px',
|
||||
textAlign: 'center',
|
||||
padding: 0,
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</Image.PreviewGroup>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
<div className="metric-grid">
|
||||
<div className="metric-card">
|
||||
<Typography.Text className="text-muted">模块总数</Typography.Text>
|
||||
<Typography.Title level={2} style={{ margin: '8px 0 0' }}>
|
||||
{moduleList.length}
|
||||
</Typography.Title>
|
||||
</div>
|
||||
<div className="metric-card">
|
||||
<Typography.Text className="text-muted">已接入</Typography.Text>
|
||||
<Typography.Title level={2} style={{ margin: '8px 0 0' }}>
|
||||
{implementedCount}
|
||||
</Typography.Title>
|
||||
</div>
|
||||
<div className="metric-card">
|
||||
<Typography.Text className="text-muted">待继续</Typography.Text>
|
||||
<Typography.Title level={2} style={{ margin: '8px 0 0' }}>
|
||||
{partialCount}
|
||||
</Typography.Title>
|
||||
</div>
|
||||
<div className="metric-card">
|
||||
<Typography.Text className="text-muted">已登记接口触点</Typography.Text>
|
||||
<Typography.Title level={2} style={{ margin: '8px 0 0' }}>
|
||||
{allEndpoints}
|
||||
</Typography.Title>
|
||||
</div>
|
||||
</div>
|
||||
<Card className="glass-panel page-panel" title="模块清单">
|
||||
<div className="catalog-grid">
|
||||
{moduleList.map((item) => (
|
||||
<div className="catalog-card" key={item.name}>
|
||||
<div className="section-heading" style={{ marginBottom: 8 }}>
|
||||
<Typography.Title level={5} style={{ margin: 0 }}>
|
||||
{item.title}
|
||||
</Typography.Title>
|
||||
<span className={`status-dot ${item.status}`}></span>
|
||||
}
|
||||
>
|
||||
{detailQuery.isLoading ? (
|
||||
<Skeleton active />
|
||||
) : detailQuery.data?.data ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, maxHeight: 'calc(100vh - 280px)', overflowY: 'auto' }}>
|
||||
{detailQuery.data.data.messages.map((msg) => (
|
||||
<div
|
||||
key={msg.id}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: msg.senderType === 'admin' ? 'row-reverse' : 'row',
|
||||
gap: 8,
|
||||
alignItems: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<Avatar src={msg.senderAvatar ? getMediaUrl(msg.senderAvatar) : undefined} size={32} style={{ flexShrink: 0 }}>
|
||||
{msg.senderName[0]}
|
||||
</Avatar>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: '75%',
|
||||
background: msg.senderType === 'admin' ? 'var(--accent-soft)' : 'rgba(16,37,66,0.06)',
|
||||
borderRadius: 10,
|
||||
padding: '8px 12px',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-secondary)', marginBottom: 4 }}>
|
||||
{msg.senderName}
|
||||
</div>
|
||||
{msg.content && <div style={{ fontSize: 13 }}>{msg.content}</div>}
|
||||
{msg.images && msg.images.length > 0 && (
|
||||
<Image.PreviewGroup>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 4 }}>
|
||||
{msg.images.map((url, i) => (
|
||||
<Image
|
||||
key={i}
|
||||
src={getMediaUrl(url)}
|
||||
width={80}
|
||||
height={80}
|
||||
style={{ objectFit: 'cover', borderRadius: 6 }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Image.PreviewGroup>
|
||||
)}
|
||||
</div>
|
||||
<Typography.Paragraph className="text-muted" style={{ minHeight: 66 }}>
|
||||
{item.summary}
|
||||
</Typography.Paragraph>
|
||||
<Tag>{item.group}</Tag>
|
||||
<Tag bordered={false}>{item.endpoints.length} 个接口触点</Tag>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="glass-panel page-panel" title="当前角色菜单">
|
||||
<List
|
||||
dataSource={flatMenus.filter((item) => !item.hidden)}
|
||||
renderItem={(item) => (
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
title={item.meta.title}
|
||||
description={`${item.name} · ${item.fullPath}`}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
) : null}
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
// ── 通用 Section 标题 ────────────────────────────────────────
|
||||
function SectionHeader({ icon, title, extra }: { icon: React.ReactNode; title: string; extra?: React.ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ color: 'var(--accent)', display: 'flex' }}>{icon}</span>
|
||||
<Title level={5} style={{ margin: 0 }}>
|
||||
{title}
|
||||
</Title>
|
||||
</div>
|
||||
{extra}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── 指标卡 ────────────────────────────────────────────────────
|
||||
function MetricCard({
|
||||
title,
|
||||
value,
|
||||
suffix,
|
||||
loading,
|
||||
highlight,
|
||||
sub,
|
||||
}: {
|
||||
title: string
|
||||
value: number | string
|
||||
suffix?: string
|
||||
loading?: boolean
|
||||
highlight?: boolean
|
||||
sub?: string
|
||||
}) {
|
||||
return (
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
background: highlight ? 'var(--accent-soft)' : 'var(--surface)',
|
||||
borderRadius: 12,
|
||||
height: '100%',
|
||||
}}
|
||||
bodyStyle={{ padding: '16px 20px' }}
|
||||
>
|
||||
{loading ? (
|
||||
<Skeleton active paragraph={{ rows: 1 }} title={false} />
|
||||
) : (
|
||||
<>
|
||||
<Statistic
|
||||
title={<Text style={{ fontSize: 12, color: 'var(--text-secondary)' }}>{title}</Text>}
|
||||
value={value}
|
||||
suffix={suffix}
|
||||
valueStyle={{ fontSize: 26, fontWeight: 700 }}
|
||||
/>
|
||||
{sub && (
|
||||
<Text style={{ fontSize: 12, color: 'var(--text-secondary)' }}>{sub}</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
// ── 主组件 ────────────────────────────────────────────────────
|
||||
export function DashboardPage() {
|
||||
const [selectedFeedback, setSelectedFeedback] = useState<DashboardFeedbackPendingItem | null>(null)
|
||||
|
||||
const { data, isLoading, refetch, isRefetching } = useQuery({
|
||||
queryKey: ['dashboard-stats'],
|
||||
queryFn: () => dashboardApi.getStats(),
|
||||
refetchInterval: 15_000,
|
||||
})
|
||||
|
||||
const stats = data?.data
|
||||
|
||||
const handleReplied = () => {
|
||||
refetch()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page-stack" style={{ gap: 20 }}>
|
||||
{/* 顶部刷新 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Title level={4} style={{ margin: 0 }}>
|
||||
运营大屏
|
||||
</Title>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={isRefetching}
|
||||
onClick={() => refetch()}
|
||||
size="small"
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* ── 用户数据 ── */}
|
||||
<Card className="glass-panel page-panel" bodyStyle={{ padding: '20px 24px' }}>
|
||||
<SectionHeader icon={<UserOutlined />} title="用户数据" />
|
||||
<Row gutter={[12, 12]}>
|
||||
<Col xs={12} sm={8} md={4}>
|
||||
<MetricCard title="注册用户总数" value={stats?.users.total ?? 0} loading={isLoading} />
|
||||
</Col>
|
||||
<Col xs={12} sm={8} md={4}>
|
||||
<MetricCard title="创作者数" value={stats?.users.creatorCount ?? 0} loading={isLoading} />
|
||||
</Col>
|
||||
<Col xs={12} sm={8} md={4}>
|
||||
<MetricCard
|
||||
title="当前在线"
|
||||
value={stats?.users.onlineCount ?? 0}
|
||||
loading={isLoading}
|
||||
highlight
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={12} sm={8} md={4}>
|
||||
<MetricCard
|
||||
title="今日新增"
|
||||
value={stats?.users.todayNew ?? 0}
|
||||
loading={isLoading}
|
||||
sub={`本周 +${stats?.users.weekNew ?? 0}`}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={12} sm={8} md={4}>
|
||||
<MetricCard
|
||||
title="累计消息数"
|
||||
value={stats?.users.totalMessages ?? 0}
|
||||
loading={isLoading}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={12} sm={8} md={4}>
|
||||
<MetricCard
|
||||
title="累计对话数"
|
||||
value={stats?.users.totalChats ?? 0}
|
||||
loading={isLoading}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* ── 角色卡 + 创作者 并排 ── */}
|
||||
<Row gutter={[16, 16]}>
|
||||
{/* 角色卡模块 */}
|
||||
<Col xs={24} lg={10}>
|
||||
<Card className="glass-panel page-panel" style={{ height: '100%' }} bodyStyle={{ padding: '20px 24px' }}>
|
||||
<SectionHeader icon={<BookOutlined />} title="角色卡" />
|
||||
<Row gutter={[12, 12]}>
|
||||
<Col span={8}>
|
||||
<MetricCard title="角色卡总数" value={stats?.characters.total ?? 0} loading={isLoading} />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<MetricCard title="已发布" value={stats?.characters.published ?? 0} loading={isLoading} />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<MetricCard
|
||||
title="待审核"
|
||||
value={stats?.characters.pendingReview ?? 0}
|
||||
loading={isLoading}
|
||||
highlight={!!stats?.characters.pendingReview}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<MetricCard
|
||||
title="今日新增"
|
||||
value={stats?.characters.todayNew ?? 0}
|
||||
loading={isLoading}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<MetricCard
|
||||
title="本周新增"
|
||||
value={stats?.characters.weekNew ?? 0}
|
||||
loading={isLoading}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* 创作者模块 */}
|
||||
<Col xs={24} lg={14}>
|
||||
<Card className="glass-panel page-panel" style={{ height: '100%' }} bodyStyle={{ padding: '20px 24px' }}>
|
||||
<SectionHeader icon={<StarOutlined />} title="创作者" />
|
||||
<Row gutter={[12, 12]}>
|
||||
<Col xs={12} sm={8} md={6}>
|
||||
<MetricCard title="创作者总数" value={stats?.creators.total ?? 0} loading={isLoading} />
|
||||
</Col>
|
||||
<Col xs={12} sm={8} md={6}>
|
||||
<MetricCard title="签约创作者" value={stats?.creators.contracted ?? 0} loading={isLoading} />
|
||||
</Col>
|
||||
<Col xs={12} sm={8} md={6}>
|
||||
<MetricCard
|
||||
title="今日新增"
|
||||
value={stats?.creators.todayNew ?? 0}
|
||||
loading={isLoading}
|
||||
sub={`本周 +${stats?.creators.weekNew ?? 0}`}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={12} sm={8} md={6}>
|
||||
<MetricCard
|
||||
title="待审创作申请"
|
||||
value={stats?.creators.pendingApplications ?? 0}
|
||||
loading={isLoading}
|
||||
highlight={!!stats?.creators.pendingApplications}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* ── 热门创作者排行 ── */}
|
||||
<Card className="glass-panel page-panel" bodyStyle={{ padding: '20px 24px' }}>
|
||||
<SectionHeader icon={<RiseOutlined />} title="热门创作者排行(按角色卡游玩量)" />
|
||||
<Table<DashboardTopCreator>
|
||||
size="small"
|
||||
loading={isLoading}
|
||||
dataSource={stats?.creators.topCreators ?? []}
|
||||
pagination={false}
|
||||
rowKey="userId"
|
||||
columns={[
|
||||
{
|
||||
title: '排名',
|
||||
width: 56,
|
||||
render: (_, __, idx) => (
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 700,
|
||||
color: idx < 3 ? 'var(--accent)' : 'var(--text-secondary)',
|
||||
fontSize: idx < 3 ? 16 : 13,
|
||||
}}
|
||||
>
|
||||
{idx + 1}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '创作者',
|
||||
dataIndex: 'nickName',
|
||||
render: (name: string, row: DashboardTopCreator) => (
|
||||
<Space>
|
||||
<Avatar src={row.avatar ? getMediaUrl(row.avatar) : undefined} size={28}>
|
||||
{name[0]}
|
||||
</Avatar>
|
||||
<Text>{name}</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '角色卡数',
|
||||
dataIndex: 'charCount',
|
||||
width: 100,
|
||||
render: (v: number) => <Tag>{v}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '总游玩量',
|
||||
dataIndex: 'totalUseCount',
|
||||
width: 120,
|
||||
render: (v: number) => (
|
||||
<Text strong style={{ color: 'var(--accent)' }}>
|
||||
{v.toLocaleString()}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* ── 用户反馈 ── */}
|
||||
<Card className="glass-panel page-panel" bodyStyle={{ padding: '20px 24px' }}>
|
||||
<SectionHeader
|
||||
icon={<MessageOutlined />}
|
||||
title="用户反馈"
|
||||
extra={
|
||||
<Space size={4}>
|
||||
<Tag color="processing">待回复 {stats?.feedback.adminUnread ?? 0}</Tag>
|
||||
<Tag color="default">开放 {stats?.feedback.open ?? 0}</Tag>
|
||||
<Tag color="success">今日已关 {stats?.feedback.closedToday ?? 0}</Tag>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
<Table<DashboardFeedbackPendingItem>
|
||||
size="small"
|
||||
loading={isLoading}
|
||||
dataSource={stats?.feedback.pendingItems ?? []}
|
||||
pagination={false}
|
||||
rowKey="id"
|
||||
locale={{ emptyText: '暂无待回复工单' }}
|
||||
columns={[
|
||||
{
|
||||
title: '用户',
|
||||
dataIndex: 'userNickname',
|
||||
width: 140,
|
||||
render: (name: string, row: DashboardFeedbackPendingItem) => (
|
||||
<Space>
|
||||
<Avatar src={row.userAvatar} size={24}>
|
||||
{name[0]}
|
||||
</Avatar>
|
||||
<Text style={{ fontSize: 12 }}>{name}</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '工单标题',
|
||||
dataIndex: 'title',
|
||||
render: (title: string) => <Text style={{ fontSize: 13 }}>{title}</Text>,
|
||||
},
|
||||
{
|
||||
title: '未读',
|
||||
dataIndex: 'unreadCount',
|
||||
width: 70,
|
||||
render: (v: number) => <Badge count={v} />,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
width: 150,
|
||||
render: (v: string) => (
|
||||
<Text style={{ fontSize: 12, color: 'var(--text-secondary)' }}>
|
||||
{new Date(v).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 80,
|
||||
render: (_, row) => (
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<AlertOutlined />}
|
||||
onClick={() => setSelectedFeedback(row)}
|
||||
>
|
||||
回复
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* ── 系统统计 ── */}
|
||||
<Card className="glass-panel page-panel" bodyStyle={{ padding: '20px 24px' }}>
|
||||
<SectionHeader icon={<BarChartOutlined />} title="系统汇总" />
|
||||
<Row gutter={[12, 12]}>
|
||||
<Col xs={12} md={6}>
|
||||
<MetricCard title="总对话会话数" value={stats?.system.totalConversations ?? 0} loading={isLoading} />
|
||||
</Col>
|
||||
<Col xs={12} md={6}>
|
||||
<MetricCard title="反馈工单总数" value={stats?.feedback.total ?? 0} loading={isLoading} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* 快捷回复 Drawer */}
|
||||
<QuickReplyDrawer
|
||||
item={selectedFeedback}
|
||||
onClose={() => setSelectedFeedback(null)}
|
||||
onReplied={handleReplied}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -64,6 +64,159 @@ export const moduleCatalog: Record<string, ModuleDescriptor> = {
|
||||
features: ['用户列表', '新建用户', '重置密码', '角色切换'],
|
||||
endpoints: ['/user/getUserList', '/user/admin_register', '/user/setUserInfo', '/user/resetPassword'],
|
||||
},
|
||||
appManage: {
|
||||
name: 'appManage',
|
||||
title: '应用管理',
|
||||
group: '应用管理',
|
||||
status: 'ready',
|
||||
summary: '聚合用户运营、创作者管理、内容管理和运行配置能力。',
|
||||
features: ['用户运营', '创作者管理', '内容管理', '运行配置', '风险审阅'],
|
||||
endpoints: ['/admin/user/list', '/admin/creator/applications', '/admin/characters', '/admin/conversations', '/admin/risk/users', '/admin/risk/events', '/admin/risk/traces/lookup'],
|
||||
},
|
||||
appUserManage: {
|
||||
name: 'appUserManage',
|
||||
title: '用户运营',
|
||||
group: '应用管理',
|
||||
status: 'ready',
|
||||
summary: '承载前台用户管理、对话留痕和风险审阅入口,用于聚合用户侧运营能力。',
|
||||
features: ['用户管理入口', '对话日志入口', '风险账号审阅', '风险事件审阅', '用户资料维护', '会话留痕审计'],
|
||||
endpoints: ['/admin/user/list', '/admin/user/:id', '/admin/user/:id/reset-password', '/admin/conversations', '/admin/risk/users', '/admin/risk/events', '/admin/risk/traces/lookup'],
|
||||
},
|
||||
appUserList: {
|
||||
name: 'appUserList',
|
||||
title: '用户管理',
|
||||
group: '应用管理',
|
||||
status: 'ready',
|
||||
summary: '管理前台用户资料、启用状态、创作者标识与密码重置。',
|
||||
features: ['关键词检索', '状态切换', '资料编辑', '密码重置'],
|
||||
endpoints: ['/admin/user/list', '/admin/user/:id', '/admin/user/:id/reset-password', '/admin/user/:id/toggle-enable'],
|
||||
},
|
||||
appCreatorManage: {
|
||||
name: 'appCreatorManage',
|
||||
title: '创作者管理分组',
|
||||
group: '应用管理',
|
||||
status: 'ready',
|
||||
summary: '承载创作者申请审核和创作者列表入口。',
|
||||
features: ['创作者申请处理', '创作者列表入口', '创作者身份维护'],
|
||||
endpoints: ['/admin/creator/applications', '/admin/creators', '/admin/creators/:id/revoke'],
|
||||
},
|
||||
appCreatorManageList: {
|
||||
name: 'appCreatorManageList',
|
||||
title: '创作者管理',
|
||||
group: '应用管理',
|
||||
status: 'ready',
|
||||
summary: '管理创作者申请审核、创作者列表和权限撤销。',
|
||||
features: ['申请审核', '创作者列表', '权限撤销'],
|
||||
endpoints: ['/admin/creator/applications', '/admin/creator/applications/:id/approve', '/admin/creator/applications/:id/reject', '/admin/creators'],
|
||||
},
|
||||
appCharacterReview: {
|
||||
name: 'appCharacterReview',
|
||||
title: '角色卡审核(旧分组)',
|
||||
group: '应用管理',
|
||||
status: 'ready',
|
||||
summary: '旧分组占位。角色卡审核入口已并入内容管理。',
|
||||
features: ['旧分组保留', '兼容历史配置'],
|
||||
endpoints: ['/admin/characters', '/admin/characters/:id/approve', '/admin/characters/:id/reject'],
|
||||
},
|
||||
appCharacterReviewList: {
|
||||
name: 'appCharacterReviewList',
|
||||
title: '角色卡审核',
|
||||
group: '应用管理',
|
||||
status: 'ready',
|
||||
summary: '审核角色卡内容并执行通过、拒绝和删除操作。',
|
||||
features: ['待审列表', '审核详情', '通过与拒绝', '删除角色卡'],
|
||||
endpoints: ['/admin/characters', '/admin/characters/:id', '/admin/characters/:id/approve', '/admin/characters/:id/reject'],
|
||||
},
|
||||
appCharacterManage: {
|
||||
name: 'appCharacterManage',
|
||||
title: '内容管理',
|
||||
group: '应用管理',
|
||||
status: 'ready',
|
||||
summary: '承载角色卡审核与角色卡全量管理入口,用于聚合内容治理能力。',
|
||||
features: ['角色卡审核入口', '角色卡管理入口', '内容上架治理', '内容存量治理'],
|
||||
endpoints: ['/admin/characters', '/admin/characters/:id', '/admin/characters/:id/approve', '/admin/characters/:id/reject'],
|
||||
},
|
||||
appCharacterManageList: {
|
||||
name: 'appCharacterManageList',
|
||||
title: '角色卡管理',
|
||||
group: '应用管理',
|
||||
status: 'ready',
|
||||
summary: '查看角色卡全量列表并执行删除等存量治理动作。',
|
||||
features: ['角色卡全量列表', '筛选检索', '删除处理'],
|
||||
endpoints: ['/admin/characters', '/admin/characters/:id', '/admin/characters/:id/delete'],
|
||||
},
|
||||
appAIConfig: {
|
||||
name: 'appAIConfig',
|
||||
title: 'AI模型配置分组',
|
||||
group: '应用管理',
|
||||
status: 'ready',
|
||||
summary: '承载模型供应商配置入口,用于区分应用运行配置。',
|
||||
features: ['模型配置入口', '默认模型治理', '模型同步'],
|
||||
endpoints: ['/admin/ai-config', '/admin/ai-config/:id/test', '/admin/ai-config/:id/fetch-models'],
|
||||
},
|
||||
appAIConfigList: {
|
||||
name: 'appAIConfigList',
|
||||
title: 'AI模型配置',
|
||||
group: '应用管理',
|
||||
status: 'ready',
|
||||
summary: '管理模型供应商、默认模型、请求参数和可用模型同步。',
|
||||
features: ['配置列表', '新增与编辑', '默认配置切换', '模型拉取与连通性测试'],
|
||||
endpoints: ['/admin/ai-config', '/admin/ai-config/:id/set-default', '/admin/ai-config/:id/test', '/admin/ai-config/:id/fetch-models'],
|
||||
},
|
||||
appSystemConfig: {
|
||||
name: 'appSystemConfig',
|
||||
title: '系统配置分组',
|
||||
group: '应用管理',
|
||||
status: 'ready',
|
||||
summary: '承载应用级通知与系统配置入口。',
|
||||
features: ['系统配置入口', '邮件配置入口', '通知能力维护'],
|
||||
endpoints: ['/admin/email-config', '/admin/email-config/test'],
|
||||
},
|
||||
appEmailConfig: {
|
||||
name: 'appEmailConfig',
|
||||
title: '邮件配置',
|
||||
group: '应用管理',
|
||||
status: 'ready',
|
||||
summary: '管理 SMTP 参数、默认收件人和测试邮件发送。',
|
||||
features: ['SMTP 配置', '启用开关', '默认收件人', '测试邮件'],
|
||||
endpoints: ['/admin/email-config', '/admin/email-config/test'],
|
||||
},
|
||||
appConversationLog: {
|
||||
name: 'appConversationLog',
|
||||
title: '对话日志(旧分组)',
|
||||
group: '应用管理',
|
||||
status: 'ready',
|
||||
summary: '旧分组占位。对话日志入口已并入用户运营。',
|
||||
features: ['旧分组保留', '兼容历史配置'],
|
||||
endpoints: ['/admin/conversations', '/admin/conversations/:id'],
|
||||
},
|
||||
appConversationLogList: {
|
||||
name: 'appConversationLogList',
|
||||
title: '对话日志',
|
||||
group: '应用管理',
|
||||
status: 'ready',
|
||||
summary: '查看对话会话、消息内容和删除处理记录。',
|
||||
features: ['会话列表', '消息明细', '删除记录'],
|
||||
endpoints: ['/admin/conversations', '/admin/conversations/:id', '/admin/conversations/:id/delete'],
|
||||
},
|
||||
appRiskAccountList: {
|
||||
name: 'appRiskAccountList',
|
||||
title: '风险账号审阅',
|
||||
group: '应用管理',
|
||||
status: 'ready',
|
||||
summary: '查看触发风险事件的账号摘要,并更新人工审阅状态和处理备注。',
|
||||
features: ['风险账号列表', '风险分排序', '最近风险摘要', '人工状态更新'],
|
||||
endpoints: ['/admin/risk/users', '/admin/risk/users/:id/status'],
|
||||
},
|
||||
appRiskEventList: {
|
||||
name: 'appRiskEventList',
|
||||
title: '风险事件审阅',
|
||||
group: '应用管理',
|
||||
status: 'ready',
|
||||
summary: '查看风险事件明细、命中摘录和来源信息,并记录处理结论。',
|
||||
features: ['风险事件列表', '事件类型筛选', '详情展开', '人工状态更新'],
|
||||
endpoints: ['/admin/risk/events', '/admin/risk/events/:id/status', '/admin/risk/traces/lookup'],
|
||||
},
|
||||
dictionary: {
|
||||
name: 'dictionary',
|
||||
title: '字典管理',
|
||||
|
||||
@@ -31,6 +31,14 @@ const iconMap: Record<string, React.ReactNode> = {
|
||||
loginLog: <AppstoreOutlined />,
|
||||
systemTools: <CodeOutlined />,
|
||||
common: <BookOutlined />,
|
||||
appManage: <AppstoreOutlined />,
|
||||
appUserManage: <UserOutlined />,
|
||||
appCreatorManage: <UserOutlined />,
|
||||
appCharacterReview: <BookOutlined />,
|
||||
appCharacterManage: <BookOutlined />,
|
||||
appAIConfig: <SettingOutlined />,
|
||||
appSystemConfig: <SettingOutlined />,
|
||||
appConversationLog: <BookOutlined />,
|
||||
}
|
||||
|
||||
function buildMenuItems(menus: AppMenu[]): ItemType[] {
|
||||
@@ -89,6 +97,16 @@ export function AdminShell({ children }: Props) {
|
||||
|
||||
const menuItems = useMemo(() => buildMenuItems(menus), [menus])
|
||||
|
||||
const defaultOpenKeys = useMemo(() => {
|
||||
const parts = selectedKey.split('/')
|
||||
const keys: string[] = []
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
keys.push(parts.slice(0, i).join('/'))
|
||||
}
|
||||
return keys
|
||||
}, [selectedKey])
|
||||
|
||||
|
||||
const handleMenuClick = ({ key }: { key: string }) => {
|
||||
const target = flattenMenus(menus).find((menu) => menu.fullPathKey === key)
|
||||
if (!target) {
|
||||
@@ -146,6 +164,8 @@ export function AdminShell({ children }: Props) {
|
||||
theme="dark"
|
||||
inlineIndent={16}
|
||||
selectedKeys={[selectedKey]}
|
||||
defaultOpenKeys={defaultOpenKeys}
|
||||
motion={false}
|
||||
items={menuItems}
|
||||
onClick={handleMenuClick}
|
||||
style={{ width: '100%', background: 'transparent', borderInlineEnd: 'none' }}
|
||||
|
||||
@@ -23,7 +23,7 @@ import type { DataNode } from 'antd/es/tree'
|
||||
import { DeleteOutlined, EditOutlined, FolderAddOutlined, UploadOutlined } from '@ant-design/icons'
|
||||
import { formatDate } from '@/lib/date'
|
||||
import { http } from '@/lib/http'
|
||||
import { mediaLibraryApi } from '@/lib/api'
|
||||
import { getMediaUrl, mediaLibraryApi } from '@/lib/api'
|
||||
import type { AttachmentCategory, MediaFileRecord } from '@/types/system'
|
||||
|
||||
type SearchFormValues = {
|
||||
@@ -257,7 +257,7 @@ export function MediaLibraryPage() {
|
||||
<Image
|
||||
width={56}
|
||||
height={56}
|
||||
src={record.url}
|
||||
src={getMediaUrl(record.url)}
|
||||
style={{ objectFit: 'cover', borderRadius: 12 }}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -17,6 +17,131 @@ const componentOptions: ComponentOption[] = [
|
||||
{ value: 'features/menus/MenuManagementPage', label: 'MenuManagementPage · 菜单管理', routeName: 'menu' },
|
||||
{ value: 'features/apis/ApiManagementPage', label: 'ApiManagementPage · API 管理', routeName: 'api' },
|
||||
{ value: 'features/users/UserManagementPage', label: 'UserManagementPage · 用户管理', routeName: 'user' },
|
||||
{
|
||||
value: 'features/appManage/AppManageLandingPage',
|
||||
label: 'AppManageLandingPage · 应用管理总览',
|
||||
routeName: 'appManage',
|
||||
},
|
||||
{
|
||||
value: 'features/discovery/ModuleLandingPage:appUserManage',
|
||||
label: 'ModuleLandingPage · 用户运营分组',
|
||||
routeName: 'appUserManage',
|
||||
},
|
||||
{
|
||||
value: 'features/appManage/AppUserManagementPage',
|
||||
label: 'AppUserManagementPage · 用户管理',
|
||||
routeName: 'appUserList',
|
||||
},
|
||||
{
|
||||
value: 'features/discovery/ModuleLandingPage:appCreatorManage',
|
||||
label: 'ModuleLandingPage · 创作者管理分组',
|
||||
routeName: 'appCreatorManage',
|
||||
},
|
||||
{
|
||||
value: 'features/appManage/CreatorManagementPage',
|
||||
label: 'CreatorManagementPage · 创作者管理',
|
||||
routeName: 'appCreatorManageList',
|
||||
},
|
||||
{
|
||||
value: 'features/discovery/ModuleLandingPage:appSigningApplication',
|
||||
label: 'ModuleLandingPage · 签约申请分组',
|
||||
routeName: 'appSigningApplication',
|
||||
},
|
||||
{
|
||||
value: 'features/appManage/SigningApplicationPage',
|
||||
label: 'SigningApplicationPage · 签约申请审核',
|
||||
routeName: 'appSigningApplicationList',
|
||||
},
|
||||
{
|
||||
value: 'features/discovery/ModuleLandingPage:appFanLevelConfig',
|
||||
label: 'ModuleLandingPage · 粉丝等级配置分组',
|
||||
routeName: 'appFanLevelConfig',
|
||||
},
|
||||
{
|
||||
value: 'features/appManage/FanLevelConfigPage',
|
||||
label: 'FanLevelConfigPage · 粉丝等级配置',
|
||||
routeName: 'appFanLevelConfigList',
|
||||
},
|
||||
{
|
||||
value: 'features/discovery/ModuleLandingPage:appCreatorLevelConfig',
|
||||
label: 'ModuleLandingPage · 创作者等级配置分组',
|
||||
routeName: 'appCreatorLevelConfig',
|
||||
},
|
||||
{
|
||||
value: 'features/appManage/CreatorLevelConfigPage',
|
||||
label: 'CreatorLevelConfigPage · 创作者等级配置',
|
||||
routeName: 'appCreatorLevelConfigList',
|
||||
},
|
||||
{
|
||||
value: 'features/discovery/ModuleLandingPage:appCharacterReview',
|
||||
label: 'ModuleLandingPage · 角色卡审核旧分组',
|
||||
routeName: 'appCharacterReview',
|
||||
},
|
||||
{
|
||||
value: 'features/appManage/CharacterReviewPage',
|
||||
label: 'CharacterReviewPage · 角色卡审核',
|
||||
routeName: 'appCharacterReviewList',
|
||||
},
|
||||
{
|
||||
value: 'features/discovery/ModuleLandingPage:appCharacterManage',
|
||||
label: 'ModuleLandingPage · 内容管理分组',
|
||||
routeName: 'appCharacterManage',
|
||||
},
|
||||
{
|
||||
value: 'features/appManage/CharacterManagementPage',
|
||||
label: 'CharacterManagementPage · 角色卡管理',
|
||||
routeName: 'appCharacterManageList',
|
||||
},
|
||||
{
|
||||
value: 'features/discovery/ModuleLandingPage:appLeaderboardConfig',
|
||||
label: 'ModuleLandingPage · 榜单配置分组',
|
||||
routeName: 'appLeaderboardConfig',
|
||||
},
|
||||
{
|
||||
value: 'features/appManage/LeaderboardConfigPage',
|
||||
label: 'LeaderboardConfigPage · 榜单配置',
|
||||
routeName: 'appLeaderboardConfigList',
|
||||
},
|
||||
{
|
||||
value: 'features/appManage/AIConfigManagementPage',
|
||||
label: 'AIConfigManagementPage · AI模型配置',
|
||||
routeName: 'appAIConfigList',
|
||||
},
|
||||
{
|
||||
value: 'features/discovery/ModuleLandingPage:appAIConfig',
|
||||
label: 'ModuleLandingPage · AI模型配置分组',
|
||||
routeName: 'appAIConfig',
|
||||
},
|
||||
{
|
||||
value: 'features/discovery/ModuleLandingPage:appSystemConfig',
|
||||
label: 'ModuleLandingPage · 应用系统配置分组',
|
||||
routeName: 'appSystemConfig',
|
||||
},
|
||||
{
|
||||
value: 'features/appManage/EmailConfigPage',
|
||||
label: 'EmailConfigPage · 邮件配置',
|
||||
routeName: 'appEmailConfig',
|
||||
},
|
||||
{
|
||||
value: 'features/discovery/ModuleLandingPage:appConversationLog',
|
||||
label: 'ModuleLandingPage · 对话日志旧分组',
|
||||
routeName: 'appConversationLog',
|
||||
},
|
||||
{
|
||||
value: 'features/appManage/ConversationLogPage',
|
||||
label: 'ConversationLogPage · 对话日志',
|
||||
routeName: 'appConversationLogList',
|
||||
},
|
||||
{
|
||||
value: 'features/appManage/RiskAccountReviewPage',
|
||||
label: 'RiskAccountReviewPage · 风险账号审阅',
|
||||
routeName: 'appRiskAccountList',
|
||||
},
|
||||
{
|
||||
value: 'features/appManage/RiskEventReviewPage',
|
||||
label: 'RiskEventReviewPage · 风险事件审阅',
|
||||
routeName: 'appRiskEventList',
|
||||
},
|
||||
{
|
||||
value: 'features/dictionaries/DictionaryManagementPage',
|
||||
label: 'DictionaryManagementPage · 字典管理',
|
||||
@@ -58,6 +183,26 @@ const componentOptions: ComponentOption[] = [
|
||||
{ value: 'features/mcp/McpTestPage', label: 'McpTestPage · MCP Tools 管理', routeName: 'mcpTest' },
|
||||
{ value: 'features/mcp/McpToolPage', label: 'McpToolPage · MCP Tools 模板', routeName: 'mcpTool' },
|
||||
{ value: 'features/media/MediaLibraryPage', label: 'MediaLibraryPage · 媒体库', routeName: 'upload' },
|
||||
{
|
||||
value: 'features/discovery/ModuleLandingPage:appAnnouncementManage',
|
||||
label: 'ModuleLandingPage · 公告管理分组',
|
||||
routeName: 'appAnnouncementManage',
|
||||
},
|
||||
{
|
||||
value: 'features/appManage/AnnouncementManagePage',
|
||||
label: 'AnnouncementManagePage · 公告管理',
|
||||
routeName: 'appAnnouncementManageList',
|
||||
},
|
||||
{
|
||||
value: 'features/discovery/ModuleLandingPage:appFeedbackManage',
|
||||
label: 'ModuleLandingPage · 反馈工单分组',
|
||||
routeName: 'appFeedbackManage',
|
||||
},
|
||||
{
|
||||
value: 'features/appManage/FeedbackManagePage',
|
||||
label: 'FeedbackManagePage · 反馈工单管理',
|
||||
routeName: 'appFeedbackManageList',
|
||||
},
|
||||
]
|
||||
|
||||
const componentRouteNameMap = new Map(componentOptions.map((item) => [item.value, item.routeName]))
|
||||
|
||||
@@ -124,6 +124,14 @@ const groupMetaMap: Record<string, { title: string; summary: string }> = {
|
||||
title: 'MCP',
|
||||
summary: 'MCP 服务监听、对外地址与鉴权头。',
|
||||
},
|
||||
autocode: {
|
||||
title: '代码生成',
|
||||
summary: '代码生成根目录、模块名与目标路径。',
|
||||
},
|
||||
excel: {
|
||||
title: 'Excel',
|
||||
summary: 'Excel 导出目录。',
|
||||
},
|
||||
'disk-list': {
|
||||
title: '磁盘监控',
|
||||
summary: '服务器状态页磁盘挂载点来源。',
|
||||
|
||||
@@ -145,16 +145,20 @@ img {
|
||||
color: rgba(255, 255, 255, 0.96) !important;
|
||||
}
|
||||
|
||||
.admin-nav-menu.ant-menu-dark .ant-menu-item-selected,
|
||||
.admin-nav-menu.ant-menu-dark .ant-menu-submenu-selected > .ant-menu-submenu-title {
|
||||
.admin-nav-menu.ant-menu-dark .ant-menu-item-selected {
|
||||
background:
|
||||
linear-gradient(135deg, rgba(209, 111, 63, 0.95), rgba(235, 146, 96, 0.86)) !important;
|
||||
color: #fff !important;
|
||||
box-shadow: 0 12px 24px rgba(209, 111, 63, 0.22);
|
||||
}
|
||||
|
||||
.admin-nav-menu.ant-menu-dark .ant-menu-item-selected::after,
|
||||
.admin-nav-menu.ant-menu-dark .ant-menu-submenu-selected > .ant-menu-submenu-title::after {
|
||||
/* 父级 SubMenu 不显示子项选中时的高亮 */
|
||||
.admin-nav-menu.ant-menu-dark .ant-menu-submenu-selected > .ant-menu-submenu-title {
|
||||
color: rgba(255, 255, 255, 0.65) !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.admin-nav-menu.ant-menu-dark .ant-menu-item-selected::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,37 @@
|
||||
import { http } from './http'
|
||||
import type {
|
||||
AIConfigRecord,
|
||||
AIConfigTestResult,
|
||||
AppUserDetail,
|
||||
AppUserRecord,
|
||||
ApiRecord,
|
||||
BannerItem,
|
||||
ApiGroupsPayload,
|
||||
ApiTokenRecord,
|
||||
AttachmentCategory,
|
||||
AuthorityButtonSelection,
|
||||
Authority,
|
||||
CaptchaInfo,
|
||||
CharacterAdminItem,
|
||||
CharacterTraceLookupResult,
|
||||
ConversationDetail,
|
||||
ConversationLogPageResult,
|
||||
CreatorApplicationPageResult,
|
||||
CreatorIdentity,
|
||||
CreatorPageResult,
|
||||
CreatorAdminDetail,
|
||||
CreatorWallet,
|
||||
DashboardStats,
|
||||
Dictionary,
|
||||
HomepageConfig,
|
||||
DictionaryDetail,
|
||||
EmailConfigRecord,
|
||||
InitCheckResult,
|
||||
InitDBPayload,
|
||||
InviteCode,
|
||||
InviteCodeUsageItem,
|
||||
AdminUserExpLogItem,
|
||||
LevelExpConfig,
|
||||
LoginLog,
|
||||
LoginResult,
|
||||
McpContent,
|
||||
@@ -22,11 +43,29 @@ import type {
|
||||
MenuNode,
|
||||
OperationRecord,
|
||||
PagePayload,
|
||||
PointsConfig,
|
||||
PointsTransaction,
|
||||
RechargePackage,
|
||||
RiskAccountPageResult,
|
||||
RiskEventPageResult,
|
||||
ServerState,
|
||||
ShopItem,
|
||||
UserShopItem,
|
||||
FanLevelConfig,
|
||||
CreatorLevelExpConfig,
|
||||
LeaderboardConfig,
|
||||
SigningApplicationPageResult,
|
||||
SyncApiPayload,
|
||||
SysErrorRecord,
|
||||
SysParam,
|
||||
UserInfo,
|
||||
ActivityPageResult,
|
||||
Activity,
|
||||
ContestSubmission,
|
||||
AnnouncementItem,
|
||||
CreateAnnouncementDto,
|
||||
AdminFeedbackListItem,
|
||||
FeedbackDetailResponse,
|
||||
} from '@/types/system'
|
||||
|
||||
export const authApi = {
|
||||
@@ -165,6 +204,114 @@ export const systemApi = {
|
||||
},
|
||||
}
|
||||
|
||||
export const appUserAdminApi = {
|
||||
getUserList(params: { page: number; pageSize: number; keyword?: string; status?: string }) {
|
||||
return http.get<PagePayload<AppUserRecord>>('/admin/user/list', { params })
|
||||
},
|
||||
getUserById(id: number) {
|
||||
return http.get<AppUserRecord>(`/admin/user/${id}`)
|
||||
},
|
||||
updateUser(id: number, payload: { nickName?: string; email?: string; avatar?: string; enable?: boolean }) {
|
||||
return http.put<Record<string, never>>(`/admin/user/${id}`, payload)
|
||||
},
|
||||
deleteUser(id: number) {
|
||||
return http.delete<Record<string, never>>(`/admin/user/${id}`)
|
||||
},
|
||||
resetPassword(id: number, payload: { newPassword: string }) {
|
||||
return http.post<Record<string, never>>(`/admin/user/${id}/reset-password`, payload)
|
||||
},
|
||||
toggleEnable(id: number) {
|
||||
return http.post<Record<string, never>>(`/admin/user/${id}/toggle-enable`)
|
||||
},
|
||||
getUserDetail(id: number) {
|
||||
return http.get<AppUserDetail>(`/admin/user/${id}/detail`)
|
||||
},
|
||||
adjustPoints(userId: number, payload: { amount: number; reason: string; walletType: string }) {
|
||||
return http.post<Record<string, never>>(`/admin/points/adjust/${userId}`, payload)
|
||||
},
|
||||
}
|
||||
|
||||
export const aiConfigApi = {
|
||||
getList() {
|
||||
return http.get<AIConfigRecord[]>('/admin/ai-config')
|
||||
},
|
||||
getById(id: number) {
|
||||
return http.get<AIConfigRecord>(`/admin/ai-config/${id}`)
|
||||
},
|
||||
create(payload: {
|
||||
name: string
|
||||
provider: string
|
||||
baseUrl: string
|
||||
apiKey?: string
|
||||
defaultModel?: string
|
||||
maxTokens?: number
|
||||
contextLimit?: number
|
||||
presetPrompt?: string
|
||||
settings?: Record<string, unknown>
|
||||
extraHeaders?: Record<string, unknown>
|
||||
pointsPerMessage?: number
|
||||
scope?: string
|
||||
}) {
|
||||
return http.post<AIConfigRecord>('/admin/ai-config', payload)
|
||||
},
|
||||
update(
|
||||
id: number,
|
||||
payload: Partial<{
|
||||
name: string
|
||||
base_url: string
|
||||
api_key: string
|
||||
default_model: string
|
||||
max_tokens: number
|
||||
context_limit: number
|
||||
preset_prompt: string
|
||||
settings: Record<string, unknown>
|
||||
extra_headers: Record<string, unknown>
|
||||
is_active: boolean
|
||||
is_default: boolean
|
||||
scope: string
|
||||
points_per_message: number
|
||||
}>,
|
||||
) {
|
||||
return http.put<Record<string, never>>(`/admin/ai-config/${id}`, payload)
|
||||
},
|
||||
delete(id: number) {
|
||||
return http.delete<Record<string, never>>(`/admin/ai-config/${id}`)
|
||||
},
|
||||
setDefault(id: number) {
|
||||
return http.put<Record<string, never>>(`/admin/ai-config/${id}/set-default`)
|
||||
},
|
||||
setActive(id: number, active: boolean) {
|
||||
return http.put<Record<string, never>>(`/admin/ai-config/${id}/set-active`, { active })
|
||||
},
|
||||
test(id: number, payload: {
|
||||
baseUrl: string
|
||||
apiKey?: string
|
||||
provider?: string
|
||||
model?: string
|
||||
mode?: 'connectivity' | 'message'
|
||||
prompt?: string
|
||||
maxTokens?: number
|
||||
extraHeaders?: Record<string, string>
|
||||
}) {
|
||||
return http.post<AIConfigTestResult>(`/admin/ai-config/${id}/test`, payload)
|
||||
},
|
||||
fetchModels(id: number) {
|
||||
return http.get<{ models: string[] }>(`/admin/ai-config/${id}/fetch-models`)
|
||||
},
|
||||
}
|
||||
|
||||
export const emailConfigApi = {
|
||||
get() {
|
||||
return http.get<EmailConfigRecord>('/admin/email-config')
|
||||
},
|
||||
update(payload: EmailConfigRecord) {
|
||||
return http.put<Record<string, never>>('/admin/email-config', payload)
|
||||
},
|
||||
test(to: string) {
|
||||
return http.post<Record<string, never>>('/admin/email-config/test', { to })
|
||||
},
|
||||
}
|
||||
|
||||
export const apiRegistryApi = {
|
||||
getApiList(payload: Record<string, unknown>) {
|
||||
return http.post<PagePayload<ApiRecord>>('/api/getApiList', payload)
|
||||
@@ -382,3 +529,411 @@ export const mcpApi = {
|
||||
return http.post<Record<string, never>>('/mcp/createTool', payload)
|
||||
},
|
||||
}
|
||||
|
||||
export const characterReviewApi = {
|
||||
getList(params: { page: number; pageSize: number; auditStatus?: string; unpublishStatus?: string; keyword?: string }) {
|
||||
return http.get<PagePayload<CharacterAdminItem>>('/admin/characters', { params })
|
||||
},
|
||||
getById(id: number) {
|
||||
return http.get<CharacterAdminItem>(`/admin/characters/${id}`)
|
||||
},
|
||||
approve(id: number) {
|
||||
return http.post<Record<string, never>>(`/admin/characters/${id}/approve`)
|
||||
},
|
||||
reject(id: number, note: string) {
|
||||
return http.post<Record<string, never>>(`/admin/characters/${id}/reject`, { note })
|
||||
},
|
||||
}
|
||||
|
||||
export const creatorAdminApi = {
|
||||
listApplications(params: { page: number; pageSize: number; status?: string; keyword?: string }) {
|
||||
return http.get<CreatorApplicationPageResult>('/admin/creator/applications', { params })
|
||||
},
|
||||
approveApplication(id: number, payload?: { identityId?: number }) {
|
||||
return http.post<Record<string, never>>(`/admin/creator/applications/${id}/approve`, payload)
|
||||
},
|
||||
rejectApplication(id: number, note: string) {
|
||||
return http.post<Record<string, never>>(`/admin/creator/applications/${id}/reject`, { note })
|
||||
},
|
||||
listCreators(params: { page: number; pageSize: number; keyword?: string }) {
|
||||
return http.get<CreatorPageResult>('/admin/creators', { params })
|
||||
},
|
||||
getCreatorDetail(userId: number) {
|
||||
return http.get<CreatorAdminDetail>(`/admin/creators/${userId}/detail`)
|
||||
},
|
||||
revokeCreator(userId: number) {
|
||||
return http.post<Record<string, never>>(`/admin/creators/${userId}/revoke`)
|
||||
},
|
||||
setCreatorMark(userId: number, payload: { markName?: string; clear?: boolean }) {
|
||||
return http.put<Record<string, never>>(`/admin/creators/${userId}/set-mark`, payload)
|
||||
},
|
||||
setCreatorIdentity(userId: number, identityId: number | null) {
|
||||
return http.put<Record<string, never>>(`/admin/creators/${userId}/set-identity`, { identityId })
|
||||
},
|
||||
setContracted(userId: number, contracted: boolean) {
|
||||
return http.put<Record<string, never>>(`/admin/creators/${userId}/set-contracted`, { contracted })
|
||||
},
|
||||
}
|
||||
|
||||
export const characterManageApi = {
|
||||
getList(params: { page: number; pageSize: number; auditStatus?: string; unpublishStatus?: string; keyword?: string }) {
|
||||
return characterReviewApi.getList(params)
|
||||
},
|
||||
getById(id: number) {
|
||||
return characterReviewApi.getById(id)
|
||||
},
|
||||
approve(id: number) {
|
||||
return characterReviewApi.approve(id)
|
||||
},
|
||||
reject(id: number, note: string) {
|
||||
return characterReviewApi.reject(id, note)
|
||||
},
|
||||
deleteCharacter(id: number) {
|
||||
return http.delete<Record<string, never>>(`/admin/characters/${id}`)
|
||||
},
|
||||
setRecommended(id: number, value: boolean) {
|
||||
return http.put<Record<string, never>>(`/admin/characters/${id}/set-recommended`, { value })
|
||||
},
|
||||
setHot(id: number, value: boolean) {
|
||||
return http.put<Record<string, never>>(`/admin/characters/${id}/set-hot`, { value })
|
||||
},
|
||||
approveUnpublish(id: number) {
|
||||
return http.post<Record<string, never>>(`/admin/characters/${id}/approve-unpublish`)
|
||||
},
|
||||
rejectUnpublish(id: number, note: string) {
|
||||
return http.post<Record<string, never>>(`/admin/characters/${id}/reject-unpublish`, { note })
|
||||
},
|
||||
directUnpublish(id: number, note: string) {
|
||||
return http.post<Record<string, never>>(`/admin/characters/${id}/unpublish`, { note })
|
||||
},
|
||||
}
|
||||
|
||||
export const conversationLogApi = {
|
||||
listConversations(params: { page: number; pageSize: number; keyword?: string }) {
|
||||
return http.get<ConversationLogPageResult>('/admin/conversations', { params })
|
||||
},
|
||||
getConversationDetail(id: number) {
|
||||
return http.get<ConversationDetail>(`/admin/conversations/${id}`)
|
||||
},
|
||||
deleteConversation(id: number) {
|
||||
return http.delete<Record<string, never>>(`/admin/conversations/${id}`)
|
||||
},
|
||||
}
|
||||
|
||||
export const riskReviewApi = {
|
||||
getRiskUsers(params: { page: number; pageSize: number; keyword?: string; riskStatus?: string }) {
|
||||
return http.get<RiskAccountPageResult>('/admin/risk/users', { params })
|
||||
},
|
||||
getRiskEvents(params: {
|
||||
page: number
|
||||
pageSize: number
|
||||
keyword?: string
|
||||
riskStatus?: string
|
||||
eventType?: string
|
||||
userId?: number
|
||||
}) {
|
||||
return http.get<RiskEventPageResult>('/admin/risk/events', { params })
|
||||
},
|
||||
updateRiskUserStatus(id: number, payload: { status: string; note?: string }) {
|
||||
return http.put<Record<string, never>>(`/admin/risk/users/${id}/status`, payload)
|
||||
},
|
||||
updateRiskEventStatus(id: number, payload: { status: string; note?: string }) {
|
||||
return http.put<Record<string, never>>(`/admin/risk/events/${id}/status`, payload)
|
||||
},
|
||||
lookupTrace(payload: { traceCode?: string; content?: string }) {
|
||||
return http.post<CharacterTraceLookupResult>('/admin/risk/traces/lookup', payload)
|
||||
},
|
||||
}
|
||||
|
||||
// ── 积分管理 API ──────────────────────────────────────────
|
||||
export const pointsAdminApi = {
|
||||
getConfig() {
|
||||
return http.get<PointsConfig>('/admin/points/config')
|
||||
},
|
||||
updateConfig(payload: PointsConfig) {
|
||||
return http.put<Record<string, never>>('/admin/points/config', payload)
|
||||
},
|
||||
getPackages(params: { page: number; pageSize: number; status?: string }) {
|
||||
return http.get<PagePayload<RechargePackage>>('/admin/points/packages', { params })
|
||||
},
|
||||
createPackage(payload: Omit<RechargePackage, 'id' | 'createdAt' | 'updatedAt'>) {
|
||||
return http.post<RechargePackage>('/admin/points/packages', payload)
|
||||
},
|
||||
updatePackage(id: number, payload: Partial<Omit<RechargePackage, 'id' | 'createdAt' | 'updatedAt'>>) {
|
||||
return http.put<Record<string, never>>(`/admin/points/packages/${id}`, payload)
|
||||
},
|
||||
deletePackage(id: number) {
|
||||
return http.delete<Record<string, never>>(`/admin/points/packages/${id}`)
|
||||
},
|
||||
getTransactions(params: {
|
||||
page: number
|
||||
pageSize: number
|
||||
keyword?: string
|
||||
userId?: string
|
||||
type?: string
|
||||
walletType?: string
|
||||
}) {
|
||||
return http.get<PagePayload<PointsTransaction>>('/admin/points/transactions', { params })
|
||||
},
|
||||
adjustPoints(payload: { userId: number; walletType: string; amount: number; description: string }) {
|
||||
return http.post<Record<string, never>>('/admin/points/adjust', payload)
|
||||
},
|
||||
}
|
||||
|
||||
// ── 邀请码管理 API ────────────────────────────────────────
|
||||
export const inviteCodeAdminApi = {
|
||||
getCodes(params: { page: number; pageSize: number; keyword?: string }) {
|
||||
return http.get<PagePayload<InviteCode>>('/admin/invite/codes', { params })
|
||||
},
|
||||
createCode(payload: { code?: string; maxUsage: number; expiredAt?: string; remark?: string }) {
|
||||
return http.post<InviteCode>('/admin/invite/codes', payload)
|
||||
},
|
||||
updateCode(id: number, payload: { maxUsage?: number; expiredAt?: string; status?: string; remark?: string }) {
|
||||
return http.put<Record<string, never>>(`/admin/invite/codes/${id}`, payload)
|
||||
},
|
||||
deleteCode(id: number) {
|
||||
return http.delete<Record<string, never>>(`/admin/invite/codes/${id}`)
|
||||
},
|
||||
getUsages(params: { page: number; pageSize: number; keyword?: string }) {
|
||||
return http.get<PagePayload<InviteCodeUsageItem>>('/admin/invite/usages', { params })
|
||||
},
|
||||
}
|
||||
|
||||
// ── 用户等级配置 API ──────────────────────────────────────
|
||||
export const levelAdminApi = {
|
||||
// 获取全量等级配置(包含升级经验阈值 + 各事件经验值)
|
||||
getConfig() {
|
||||
return http.get<LevelExpConfig>('/admin/levels/config')
|
||||
},
|
||||
// 更新全量等级配置
|
||||
updateConfig(payload: LevelExpConfig) {
|
||||
return http.put<Record<string, never>>('/admin/levels/config', payload)
|
||||
},
|
||||
// 经验流水记录
|
||||
getExpLogs(params: { page: number; pageSize: number; keyword?: string; userId?: number; sourceType?: string }) {
|
||||
return http.get<PagePayload<AdminUserExpLogItem>>('/admin/levels/exp-logs', { params })
|
||||
},
|
||||
}
|
||||
|
||||
// ── 首页配置 API ─────────────────────────────────────────
|
||||
export const homepageConfigApi = {
|
||||
get() {
|
||||
return http.get<HomepageConfig>('/admin/homepage/config')
|
||||
},
|
||||
update(payload: { banners: BannerItem[]; recommendedCharIds: string; recommendedCreatorIds: string; isEnabled: boolean }) {
|
||||
return http.put<Record<string, never>>('/admin/homepage/config', payload)
|
||||
},
|
||||
}
|
||||
|
||||
// ── 创作者钱包 API ────────────────────────────────────────
|
||||
export const creatorWalletAdminApi = {
|
||||
getWallets(params: { page: number; pageSize: number; keyword?: string }) {
|
||||
return http.get<PagePayload<CreatorWallet>>('/admin/creator/wallets', { params })
|
||||
},
|
||||
adjustWallet(payload: { userId: number; amount: number; description: string }) {
|
||||
return http.post<Record<string, never>>('/admin/creator/wallets/adjust', payload)
|
||||
},
|
||||
}
|
||||
|
||||
// ── 创作者身份管理 API ────────────────────────────────────
|
||||
export const creatorIdentityAdminApi = {
|
||||
list() {
|
||||
return http.get<CreatorIdentity[]>('/admin/creator-identities')
|
||||
},
|
||||
create(payload: { name: string; description?: string; color?: string; sort?: number }) {
|
||||
return http.post<CreatorIdentity>('/admin/creator-identities', payload)
|
||||
},
|
||||
update(id: number, payload: { name: string; description?: string; color?: string; isActive: boolean; sort?: number }) {
|
||||
return http.put<Record<string, never>>(`/admin/creator-identities/${id}`, payload)
|
||||
},
|
||||
delete(id: number) {
|
||||
return http.delete<Record<string, never>>(`/admin/creator-identities/${id}`)
|
||||
},
|
||||
}
|
||||
|
||||
// ── 关于我们配置 API ────────────────────────────────────────
|
||||
export interface AppAboutConfig {
|
||||
id?: number
|
||||
appName: string
|
||||
version: string
|
||||
slogan: string
|
||||
description: string
|
||||
privacyUrl: string
|
||||
termsUrl: string
|
||||
supportEmail: string
|
||||
}
|
||||
|
||||
export const appAboutApi = {
|
||||
get() {
|
||||
return http.get<AppAboutConfig>('/admin/system/about')
|
||||
},
|
||||
update(payload: Omit<AppAboutConfig, 'id'>) {
|
||||
return http.put<Record<string, never>>('/admin/system/about', payload)
|
||||
},
|
||||
}
|
||||
|
||||
// ── 粉丝等级配置 API ──────────────────────────────────────────
|
||||
export const fanLevelAdminApi = {
|
||||
getList() {
|
||||
return http.get<FanLevelConfig[]>('/admin/fan-level-configs')
|
||||
},
|
||||
update(level: number, payload: Omit<FanLevelConfig, 'level'>) {
|
||||
return http.put<Record<string, never>>(`/admin/fan-level-configs/${level}`, payload)
|
||||
},
|
||||
}
|
||||
|
||||
// ── 创作者等级配置 API ────────────────────────────────────────
|
||||
export const creatorLevelAdminApi = {
|
||||
getConfig() {
|
||||
return http.get<CreatorLevelExpConfig>('/admin/creator-level-config')
|
||||
},
|
||||
updateConfig(payload: CreatorLevelExpConfig) {
|
||||
return http.put<Record<string, never>>('/admin/creator-level-config', payload)
|
||||
},
|
||||
}
|
||||
|
||||
// ── 签约申请管理 API ──────────────────────────────────────────
|
||||
export const signingAdminApi = {
|
||||
getList(params: { page: number; pageSize: number; status?: string; keyword?: string }) {
|
||||
return http.get<SigningApplicationPageResult>('/admin/creator/signing-applications', { params })
|
||||
},
|
||||
approve(id: number) {
|
||||
return http.post<Record<string, never>>(`/admin/creator/signing-applications/${id}/approve`)
|
||||
},
|
||||
reject(id: number, note: string) {
|
||||
return http.post<Record<string, never>>(`/admin/creator/signing-applications/${id}/reject`, { note })
|
||||
},
|
||||
}
|
||||
|
||||
// ── 榜单配置 API ────────────────────────────────────────────
|
||||
export const leaderboardAdminApi = {
|
||||
getConfig() {
|
||||
return http.get<LeaderboardConfig>('/admin/characters/leaderboard-config')
|
||||
},
|
||||
updateConfig(payload: LeaderboardConfig) {
|
||||
return http.put<Record<string, never>>('/admin/characters/leaderboard-config', payload)
|
||||
},
|
||||
}
|
||||
|
||||
// ── 仪表盘统计 API ─────────────────────────────────────────
|
||||
export const dashboardApi = {
|
||||
getStats() {
|
||||
return http.get<DashboardStats>('/admin/dashboard/stats')
|
||||
},
|
||||
}
|
||||
export function getMediaUrl(url: string | undefined | null): string {
|
||||
if (!url) return ''
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) return url
|
||||
const baseURL = (import.meta.env.VITE_BASE_API as string) || ''
|
||||
try {
|
||||
const origin = new URL(baseURL).origin
|
||||
return `${origin}/${url.startsWith('/') ? url.slice(1) : url}`
|
||||
} catch {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadFile(file: File): Promise<string> {
|
||||
const form = new FormData()
|
||||
form.append('file', file)
|
||||
const res = await http.post<{ file: { url: string } }>(
|
||||
'/fileUploadAndDownload/upload',
|
||||
form,
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } },
|
||||
)
|
||||
return getMediaUrl(res.data.file.url)
|
||||
}
|
||||
|
||||
// ── 兑换商店管理 API ────────────────────────────────────────
|
||||
export const shopAdminApi = {
|
||||
listItems(params: { page: number; pageSize: number; category?: string; status?: string; keyword?: string }) {
|
||||
return http.get<PagePayload<ShopItem>>('/admin/shop/items', { params })
|
||||
},
|
||||
createItem(payload: Omit<ShopItem, 'id' | 'createdAt'>) {
|
||||
return http.post<ShopItem>('/admin/shop/items', payload)
|
||||
},
|
||||
updateItem(id: number, payload: Partial<Omit<ShopItem, 'id' | 'createdAt'>>) {
|
||||
return http.put<Record<string, never>>(`/admin/shop/items/${id}`, payload)
|
||||
},
|
||||
deleteItem(id: number) {
|
||||
return http.delete<Record<string, never>>(`/admin/shop/items/${id}`)
|
||||
},
|
||||
listUserItems(params: { page: number; pageSize: number; status?: string }) {
|
||||
return http.get<PagePayload<UserShopItem>>('/admin/shop/user-items', { params })
|
||||
},
|
||||
reviewUserItem(id: number, payload: { status: string; reviewNote?: string }) {
|
||||
return http.put<Record<string, never>>(`/admin/shop/user-items/${id}/review`, payload)
|
||||
},
|
||||
}
|
||||
|
||||
// ── 活动管理 API ──────────────────────────────────────────
|
||||
export const activityAdminApi = {
|
||||
list(params: { page: number; pageSize: number; type?: string; status?: string; keyword?: string }) {
|
||||
return http.get<ActivityPageResult>('/admin/activities', { params })
|
||||
},
|
||||
create(payload: Omit<Activity, 'id' | 'createdAt' | 'participantCount'>) {
|
||||
return http.post<Activity>('/admin/activities', payload)
|
||||
},
|
||||
update(id: number, payload: Partial<Activity>) {
|
||||
return http.put<Record<string, never>>(`/admin/activities/${id}`, payload)
|
||||
},
|
||||
delete(id: number) {
|
||||
return http.delete<Record<string, never>>(`/admin/activities/${id}`)
|
||||
},
|
||||
publish(id: number) {
|
||||
return http.post<Record<string, never>>(`/admin/activities/${id}/publish`)
|
||||
},
|
||||
end(id: number) {
|
||||
return http.post<Record<string, never>>(`/admin/activities/${id}/end`)
|
||||
},
|
||||
settle(id: number) {
|
||||
return http.post<Record<string, never>>(`/admin/activities/${id}/settle`)
|
||||
},
|
||||
getParticipants(id: number, params: { page: number; pageSize: number }) {
|
||||
return http.get(`/admin/activities/${id}/participants`, { params })
|
||||
},
|
||||
getSubmissions(id: number, params: { page: number; pageSize: number; status?: string }) {
|
||||
return http.get<{ list: ContestSubmission[]; total: number }>(`/admin/activities/${id}/submissions`, { params })
|
||||
},
|
||||
reviewSubmission(id: number, subId: number, payload: { status: string; reviewNote?: string }) {
|
||||
return http.put<Record<string, never>>(`/admin/activities/${id}/submissions/${subId}/review`, payload)
|
||||
},
|
||||
pinSubmission(id: number, subId: number, pin: boolean) {
|
||||
return http.put<Record<string, never>>(`/admin/activities/${id}/submissions/${subId}/pin`, { pin })
|
||||
},
|
||||
}
|
||||
|
||||
// ── 公告管理 API ──────────────────────────────────────────
|
||||
export const announcementAdminApi = {
|
||||
getList(params: { page: number; pageSize: number; keyword?: string; status?: string }) {
|
||||
return http.get<PagePayload<AnnouncementItem>>('/admin/announcement/list', { params })
|
||||
},
|
||||
getById(id: number) {
|
||||
return http.get<AnnouncementItem>(`/admin/announcement/${id}`)
|
||||
},
|
||||
create(payload: CreateAnnouncementDto) {
|
||||
return http.post<AnnouncementItem>('/admin/announcement', payload)
|
||||
},
|
||||
update(id: number, payload: Partial<CreateAnnouncementDto>) {
|
||||
return http.put<Record<string, never>>(`/admin/announcement/${id}`, payload)
|
||||
},
|
||||
delete(id: number) {
|
||||
return http.delete<Record<string, never>>(`/admin/announcement/${id}`)
|
||||
},
|
||||
}
|
||||
|
||||
// ── 用户反馈工单管理 API ──────────────────────────────────────
|
||||
export const feedbackAdminApi = {
|
||||
getList(params: { page: number; pageSize: number; status?: string; keyword?: string; userId?: number }) {
|
||||
return http.get<PagePayload<AdminFeedbackListItem>>('/admin/feedback/list', { params })
|
||||
},
|
||||
getById(id: number) {
|
||||
return http.get<FeedbackDetailResponse>(`/admin/feedback/${id}`)
|
||||
},
|
||||
reply(id: number, payload: { content?: string; images?: string[] }) {
|
||||
return http.post<Record<string, never>>(`/admin/feedback/${id}/reply`, payload)
|
||||
},
|
||||
close(id: number) {
|
||||
return http.post<Record<string, never>>(`/admin/feedback/${id}/close`)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -39,6 +39,161 @@ export type UserInfo = BaseEntity & {
|
||||
originSetting?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type AppUserRecord = BaseEntity & {
|
||||
uuid?: string
|
||||
username: string
|
||||
nickName?: string
|
||||
email?: string
|
||||
phone?: string
|
||||
avatar?: string
|
||||
status?: string
|
||||
enable: boolean
|
||||
isAdmin?: boolean
|
||||
isCreator?: boolean
|
||||
level?: number
|
||||
experience?: number
|
||||
chatCount?: number
|
||||
messageCount?: number
|
||||
lastLoginAt?: string | null
|
||||
lastLoginIp?: string
|
||||
}
|
||||
|
||||
export type AppUserDetail = {
|
||||
id: number
|
||||
username: string
|
||||
nickName: string
|
||||
email: string
|
||||
avatar: string
|
||||
enable: boolean
|
||||
isCreator: boolean
|
||||
level: number
|
||||
experience: number
|
||||
createdAt: string
|
||||
lastLoginAt: string | null
|
||||
// 积分
|
||||
normalPoints: number
|
||||
paidBalance: number
|
||||
freeBalance: number
|
||||
creatorPoints: number
|
||||
totalRecharged: number
|
||||
totalConsumed: number
|
||||
// 邀请
|
||||
inviteCode: string
|
||||
inviteCount: number
|
||||
invitedBy: string
|
||||
// 统计
|
||||
chatCount: number
|
||||
messageCount: number
|
||||
}
|
||||
|
||||
export type RiskReviewStatus = 'none' | 'pending_review' | 'reviewed'
|
||||
|
||||
export type RiskAccountRecord = {
|
||||
userId: number
|
||||
username: string
|
||||
nickName: string
|
||||
avatar: string
|
||||
enable: boolean
|
||||
riskScore: number
|
||||
riskEventCount: number
|
||||
lastRiskAt: string | null
|
||||
lastRiskType: string
|
||||
lastRiskReason: string
|
||||
riskStatus: RiskReviewStatus
|
||||
riskNote: string
|
||||
}
|
||||
|
||||
export type RiskAccountPageResult = {
|
||||
list: RiskAccountRecord[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export type RiskEventRecord = {
|
||||
id: number
|
||||
userId: number
|
||||
username: string
|
||||
nickName: string
|
||||
characterId?: number | null
|
||||
characterName: string
|
||||
conversationId?: number | null
|
||||
eventType: string
|
||||
riskLabel: string
|
||||
riskScore: number
|
||||
excerpt: string
|
||||
detail: string
|
||||
sourceIp: string
|
||||
userAgent: string
|
||||
status: RiskReviewStatus
|
||||
reviewedBy?: number | null
|
||||
reviewedAt?: string | null
|
||||
reviewNote: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export type RiskEventPageResult = {
|
||||
list: RiskEventRecord[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export type CharacterTraceLookupResult = {
|
||||
traceCode: string
|
||||
traceType: string
|
||||
userId: number
|
||||
username: string
|
||||
nickName: string
|
||||
characterId: number
|
||||
characterName: string
|
||||
conversationId?: number | null
|
||||
sourceIp: string
|
||||
userAgent: string
|
||||
carrierField: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export type AIConfigRecord = BaseEntity & {
|
||||
name: string
|
||||
provider: string
|
||||
baseUrl: string
|
||||
apiKey?: string
|
||||
defaultModel?: string
|
||||
maxTokens?: number
|
||||
contextLimit?: number
|
||||
models?: string[] | null
|
||||
presetPrompt?: string
|
||||
settings?: Record<string, unknown> | null
|
||||
extraHeaders?: Record<string, unknown> | null
|
||||
isActive: boolean
|
||||
isDefault: boolean
|
||||
scope?: string
|
||||
pointsPerMessage?: number
|
||||
}
|
||||
|
||||
export type EmailConfigRecord = {
|
||||
id?: number
|
||||
host?: string
|
||||
port?: number
|
||||
from?: string
|
||||
nickname?: string
|
||||
secret?: string
|
||||
isSsl?: boolean
|
||||
isLoginAuth?: boolean
|
||||
to?: string
|
||||
enabled?: boolean
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
export type AIConfigTestResult = {
|
||||
success: boolean
|
||||
latency: number
|
||||
error?: string
|
||||
content?: string
|
||||
}
|
||||
|
||||
export type MenuMeta = {
|
||||
title: string
|
||||
icon?: string
|
||||
@@ -307,8 +462,403 @@ export type McpToolTemplateRequest = {
|
||||
}>
|
||||
}
|
||||
|
||||
export type CharacterAdminItem = {
|
||||
id: number
|
||||
name: string
|
||||
avatar: string
|
||||
creator: string
|
||||
creatorNickName: string
|
||||
displayIntro: string
|
||||
tags: string[] | null
|
||||
auditStatus: 'none' | 'pending' | 'approved' | 'rejected' | 'update_pending'
|
||||
auditNote: string
|
||||
auditRequestedAt: string | null
|
||||
auditReviewedAt: string | null
|
||||
isPublic: boolean
|
||||
isRecommended: boolean
|
||||
isHot: boolean
|
||||
unpublishStatus: 'none' | 'pending'
|
||||
unpublishNote: string
|
||||
unpublishRequestedAt: string | null
|
||||
deleteStatus: 'none' | 'pending'
|
||||
deleteNote: string
|
||||
deleteRequestedAt: string | null
|
||||
useCount: number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export type CharacterAdminPageResult = {
|
||||
list: CharacterAdminItem[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export type CreatorApplicationItem = {
|
||||
id: number
|
||||
userId: number
|
||||
username: string
|
||||
nickName: string
|
||||
avatar: string
|
||||
status: 'pending' | 'approved' | 'rejected'
|
||||
reason: string
|
||||
note: string
|
||||
reviewedAt: string | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export type CreatorApplicationPageResult = {
|
||||
list: CreatorApplicationItem[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export type CreatorItem = {
|
||||
userId: number
|
||||
username: string
|
||||
nickName: string
|
||||
avatar: string
|
||||
email: string
|
||||
level?: number
|
||||
isContracted: boolean
|
||||
creatorMarkId?: number | null
|
||||
creatorMarkName?: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export type CreatorPageResult = {
|
||||
list: CreatorItem[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export type CreatorAdminDetail = {
|
||||
userId: number
|
||||
username: string
|
||||
nickName: string
|
||||
avatar: string
|
||||
email: string
|
||||
level: number
|
||||
experience: number
|
||||
isCreator: boolean
|
||||
creatorMarkId?: number | null
|
||||
creatorMarkName?: string
|
||||
createdAt: string
|
||||
lastLoginAt: string | null
|
||||
walletBalance: number
|
||||
walletEarned: number
|
||||
walletRedeemed: number
|
||||
normalBalance: number
|
||||
characterCount: number
|
||||
publishedCount: number
|
||||
chatCount: number
|
||||
messageCount: number
|
||||
}
|
||||
|
||||
export type ConversationLogItem = {
|
||||
id: number
|
||||
userId: number
|
||||
username: string
|
||||
nickName: string
|
||||
characterId: number
|
||||
characterName: string
|
||||
title: string
|
||||
aiProvider: string
|
||||
model: string
|
||||
messageCount: number
|
||||
tokenCount: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export type ConversationLogPageResult = {
|
||||
list: ConversationLogItem[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export type MessageItem = {
|
||||
id: number
|
||||
role: 'user' | 'assistant' | 'system'
|
||||
content: string
|
||||
tokenCount: number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export type ConversationDetail = ConversationLogItem & {
|
||||
messages: MessageItem[]
|
||||
}
|
||||
|
||||
export type AutoMcpToolRequest = McpToolTemplateRequest
|
||||
|
||||
// ── 创作者身份 ─────────────────────────────────────────────
|
||||
export type CreatorIdentity = {
|
||||
ID: number
|
||||
name: string
|
||||
description: string
|
||||
color: string
|
||||
icon?: string
|
||||
isActive: boolean
|
||||
sort: number
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
// ── 积分管理 ──────────────────────────────────────────────
|
||||
export type InviteRewardTier = {
|
||||
minCount: number // 邀请人已邀请 >= minCount 人时启用此档(0 = 基础档)
|
||||
inviterReward: number // 邀请人获得积分
|
||||
inviteeReward: number // 被邀请人获得积分
|
||||
}
|
||||
|
||||
export type PointsConfig = {
|
||||
// 基础
|
||||
pointDeductionPerMessage: number
|
||||
rechargeRatio: number
|
||||
normalRegisterReward: number
|
||||
// 邀请(基础固定奖励,被阶梯覆盖时仍作备用)
|
||||
inviteRegisterReward: number
|
||||
inviteRechargeCommission: number
|
||||
inviteRewardTiers: InviteRewardTier[] // 阶梯奖励配置
|
||||
// 创作者
|
||||
creatorRewardConsumePoints: number
|
||||
creatorRewardPoints: number
|
||||
creatorRewardRatio: number
|
||||
// 星光币兑换灵感点
|
||||
creatorToNormalRate: number // 1 星光币 = N 灵感点
|
||||
creatorToNormalMinExchange: number // 最小兑换量(星光币)
|
||||
}
|
||||
|
||||
export type RechargePackage = {
|
||||
id: number
|
||||
name: string
|
||||
price: number
|
||||
points: number
|
||||
bonusPoints: number
|
||||
status: 'active' | 'inactive'
|
||||
sort: number
|
||||
description: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export type PointsTransaction = {
|
||||
id: number
|
||||
userId: number
|
||||
username: string
|
||||
nickName: string
|
||||
walletType: string
|
||||
type: string
|
||||
amount: number
|
||||
balance: number
|
||||
description: string
|
||||
orderId: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
// ── 邀请码管理 ────────────────────────────────────────────
|
||||
export type InviteCode = {
|
||||
id: number
|
||||
code: string
|
||||
creatorId: number
|
||||
creatorUsername: string
|
||||
creatorNickName: string
|
||||
maxUsage: number
|
||||
usedCount: number
|
||||
expiredAt: string | null
|
||||
status: 'active' | 'inactive'
|
||||
remark: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export type InviteRewardDetail = {
|
||||
rewardAmount: number
|
||||
rewardDescription: string
|
||||
status: string
|
||||
}
|
||||
|
||||
export type InviteCodeUsageItem = {
|
||||
id: number
|
||||
inviteCode: string
|
||||
inviterUserId: number
|
||||
inviterUsername: string
|
||||
inviterNickName: string
|
||||
inviteeUserId: number
|
||||
inviteeUsername: string
|
||||
inviteeNickName: string
|
||||
usedAt: string
|
||||
ipAddress: string
|
||||
inviterReward?: InviteRewardDetail
|
||||
inviteeReward?: InviteRewardDetail
|
||||
}
|
||||
|
||||
// ── 用户等级 ──────────────────────────────────────────────
|
||||
// LevelConfig 保留供旧代码引用,不对应实际后端 CRUD(后端已改为统一 config)
|
||||
export type LevelConfig = {
|
||||
id: number
|
||||
level: number
|
||||
requiredExp: number
|
||||
name: string
|
||||
color: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
// LevelExpConfig 与后端 LevelConfigResponse / UpdateLevelConfigRequest 完全对齐
|
||||
export type LevelExpConfig = {
|
||||
levelUpExpRules: number[] // 升级所需经验阈值数组,levelUpExpRules[0] = Lv1→Lv2 所需经验
|
||||
eventExpRegister: number
|
||||
eventExpDailyLogin: number
|
||||
eventExpSendMessage: number
|
||||
eventExpCreateCharacter: number
|
||||
eventExpCreateComment: number
|
||||
eventExpCharacterLiked: number
|
||||
dailyCapSendMessage: number
|
||||
dailyCapCreateComment: number
|
||||
dailyCapCharacterLiked: number
|
||||
}
|
||||
|
||||
export type AdminUserExpLogItem = {
|
||||
id: number
|
||||
userId: number
|
||||
username: string
|
||||
nickName: string
|
||||
sourceType: string
|
||||
sourceRef: string
|
||||
pointTxId?: number
|
||||
pointsDelta: number
|
||||
expDelta: number
|
||||
expBefore: number
|
||||
expAfter: number
|
||||
levelBefore: number
|
||||
levelAfter: number
|
||||
remark: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
// ── 创作者钱包 ────────────────────────────────────────────
|
||||
export type CreatorWallet = {
|
||||
userId: number
|
||||
username: string
|
||||
nickName: string
|
||||
avatar: string
|
||||
balance: number
|
||||
totalEarned: number
|
||||
totalRedeemed: number
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
// ── 首页配置 ──────────────────────────────────────────────
|
||||
export type BannerItem = {
|
||||
imageUrl: string
|
||||
title: string
|
||||
subtitle: string
|
||||
jumpUrl: string
|
||||
sort: number
|
||||
}
|
||||
|
||||
export type HomepageConfig = {
|
||||
id?: number
|
||||
banners: BannerItem[]
|
||||
recommendedCharIds: string
|
||||
recommendedCreatorIds: string
|
||||
isEnabled: boolean
|
||||
}
|
||||
|
||||
// ── 兑换商店 ────────────────────────────────────────────────
|
||||
export type ShopItem = {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
category: string
|
||||
scope: string
|
||||
unlockType: string
|
||||
currencyType: string
|
||||
price: number
|
||||
needReview: boolean
|
||||
status: string
|
||||
sort: number
|
||||
coverImage: string
|
||||
resourceUrl: string
|
||||
minLevel: number
|
||||
stockLimit: number
|
||||
exchangeCount: number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export type UserShopItem = {
|
||||
id: number
|
||||
userId: number
|
||||
shopItemId: number
|
||||
status: string
|
||||
reviewNote: string
|
||||
exchangedAt: string
|
||||
itemName: string
|
||||
coverImage: string
|
||||
}
|
||||
|
||||
// ── 粉丝等级配置 ────────────────────────────────────────────
|
||||
export type FanLevelConfig = {
|
||||
level: number
|
||||
name: string
|
||||
minFollowDays: number
|
||||
minPoints: number
|
||||
minConvCount: number
|
||||
degradeLevel: number
|
||||
degradeCondition: string
|
||||
degradeConvCount: number
|
||||
degradeDays: number
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
// ── 创作者等级配置 ──────────────────────────────────────────
|
||||
export type CreatorLevelExpConfig = {
|
||||
levelUpExpRules: number[]
|
||||
events: {
|
||||
publishCharacter: number
|
||||
characterUsed: number
|
||||
characterLiked: number
|
||||
characterFavorited: number
|
||||
followCreator: number
|
||||
}
|
||||
dailyCaps: {
|
||||
publishCharacter: number
|
||||
characterUsed: number
|
||||
characterLiked: number
|
||||
characterFavorited: number
|
||||
followCreator: number
|
||||
}
|
||||
}
|
||||
|
||||
// ── 榜单配置 ────────────────────────────────────────────────
|
||||
export type LeaderboardConfig = {
|
||||
weeklySortBy: 'likes' | 'plays' | 'favorites'
|
||||
monthlySortBy: 'likes' | 'plays' | 'favorites'
|
||||
}
|
||||
|
||||
// ── 签约申请 ──────────────────────────────────────────────
|
||||
export type SigningApplicationItem = {
|
||||
id: number
|
||||
userId: number
|
||||
username: string
|
||||
nickName: string
|
||||
avatar: string
|
||||
reason: string
|
||||
portfolio: string
|
||||
status: 'pending' | 'approved' | 'rejected'
|
||||
note: string
|
||||
reviewedAt: string | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export type SigningApplicationPageResult = {
|
||||
list: SigningApplicationItem[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export type ModuleStatus = 'ready' | 'partial' | 'planned'
|
||||
|
||||
export type ModuleDescriptor = {
|
||||
@@ -320,3 +870,261 @@ export type ModuleDescriptor = {
|
||||
features: string[]
|
||||
endpoints: string[]
|
||||
}
|
||||
|
||||
// ── 活动管理类型 ──────────────────────────────────────────
|
||||
export type ActivityType = 'task' | 'contest' | 'checkin' | 'ranking' | 'lottery'
|
||||
export type ActivityStatus = 'draft' | 'active' | 'ended' | 'cancelled'
|
||||
|
||||
export interface RewardTier {
|
||||
rankFrom: number
|
||||
rankTo: number
|
||||
amount: number
|
||||
}
|
||||
|
||||
export interface RewardPool {
|
||||
currency: 'inspiration' | 'stardust'
|
||||
amount: number
|
||||
tiers?: RewardTier[]
|
||||
}
|
||||
|
||||
export interface ContestExtraConfig {
|
||||
requireSubmissionReview: boolean
|
||||
voteEnabled: boolean
|
||||
voteCostType: 'free' | 'inspiration' | 'stardust'
|
||||
voteCostAmount: number
|
||||
maxVotesPerUser: number
|
||||
maxSubmissionsPerUser: number
|
||||
}
|
||||
|
||||
export interface RankingDimension {
|
||||
metric: string
|
||||
weight: number
|
||||
label: string
|
||||
}
|
||||
|
||||
export interface RankingExtraConfig {
|
||||
dimensions: RankingDimension[]
|
||||
scoreFormula: string
|
||||
rankBy: 'character' | 'user'
|
||||
}
|
||||
|
||||
export interface LotteryExtraConfig {
|
||||
uiStyle: 'wheel' | 'grid' | 'flip'
|
||||
drawCostType: 'inspiration' | 'stardust' | 'condition'
|
||||
drawCostAmount: number
|
||||
maxDrawsPerUser: number
|
||||
freeDraws: number
|
||||
}
|
||||
|
||||
export interface CheckinMilestone {
|
||||
day: number
|
||||
rewardType: string
|
||||
rewardAmount: number
|
||||
}
|
||||
|
||||
export interface CheckinExtraConfig {
|
||||
cycleDays: number
|
||||
milestones: CheckinMilestone[]
|
||||
dailyRewardType: string
|
||||
dailyRewardAmount: number
|
||||
}
|
||||
|
||||
export interface ActivityTask {
|
||||
id?: number
|
||||
title: string
|
||||
description: string
|
||||
taskType: string
|
||||
targetCount: number
|
||||
rewardType: string
|
||||
rewardAmount: number
|
||||
rewardAmountMin?: number
|
||||
rewardAmountMax?: number
|
||||
rewardExp?: number
|
||||
rewardExpMin?: number
|
||||
rewardExpMax?: number
|
||||
rewardTargetId?: number
|
||||
sortOrder: number
|
||||
}
|
||||
|
||||
export interface LotteryPrize {
|
||||
id?: number
|
||||
name: string
|
||||
description: string
|
||||
prizeType: 'inspiration' | 'stardust' | 'badge' | 'physical'
|
||||
prizeValue: number
|
||||
totalCount: number
|
||||
weight: number
|
||||
image: string
|
||||
sortOrder: number
|
||||
}
|
||||
|
||||
export interface Activity {
|
||||
id: number
|
||||
title: string
|
||||
subtitle: string
|
||||
type: ActivityType
|
||||
status: ActivityStatus
|
||||
coverImage: string
|
||||
description: string
|
||||
rules: string
|
||||
startAt: string
|
||||
endAt: string
|
||||
maxParticipants: number
|
||||
isFeatured: boolean
|
||||
rewardPool: RewardPool
|
||||
extraConfig: ContestExtraConfig | RankingExtraConfig | LotteryExtraConfig | CheckinExtraConfig | null
|
||||
participantCount: number
|
||||
createdAt: string
|
||||
tasks?: ActivityTask[]
|
||||
}
|
||||
|
||||
export interface ActivityPageResult {
|
||||
list: Activity[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface ContestSubmission {
|
||||
id: number
|
||||
activityId: number
|
||||
userId: number
|
||||
nickname: string
|
||||
characterId: number
|
||||
characterName: string
|
||||
characterCover: string
|
||||
voteCount: number
|
||||
isPinned: boolean
|
||||
reviewStatus: 'pending' | 'approved' | 'rejected'
|
||||
reviewNote: string
|
||||
submittedAt: string
|
||||
}
|
||||
|
||||
// ── 公告管理 ──────────────────────────────────────────────
|
||||
export type AnnouncementItem = {
|
||||
id: number
|
||||
title: string
|
||||
content: string
|
||||
contentFormat: 'markdown' | 'html'
|
||||
status: 'on' | 'off'
|
||||
startAt?: string
|
||||
endAt?: string
|
||||
priority: number
|
||||
popupOnHome: boolean
|
||||
allowClose: boolean
|
||||
allowSnoozeToday: boolean
|
||||
createdBy: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export type CreateAnnouncementDto = {
|
||||
title: string
|
||||
content: string
|
||||
contentFormat: 'markdown' | 'html'
|
||||
status: 'on' | 'off'
|
||||
startAt?: string
|
||||
endAt?: string
|
||||
priority: number
|
||||
popupOnHome: boolean
|
||||
allowClose: boolean
|
||||
allowSnoozeToday: boolean
|
||||
}
|
||||
|
||||
// ── 用户反馈工单 ────────────────────────────────────────────
|
||||
export type FeedbackMessageItem = {
|
||||
id: number
|
||||
senderType: 'user' | 'admin'
|
||||
senderId: number
|
||||
senderName: string
|
||||
senderAvatar: string
|
||||
content: string
|
||||
images: string[]
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export type FeedbackDetailResponse = {
|
||||
id: number
|
||||
title: string
|
||||
status: 'open' | 'closed'
|
||||
createdAt: string
|
||||
closedAt?: string
|
||||
messages: FeedbackMessageItem[]
|
||||
}
|
||||
|
||||
export type AdminFeedbackListItem = {
|
||||
id: number
|
||||
title: string
|
||||
status: 'open' | 'closed'
|
||||
userId: number
|
||||
userNickname: string
|
||||
userAvatar: string
|
||||
lastMessageAt: string
|
||||
adminUnreadCount: number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
// ── 仪表盘统计 ──────────────────────────────────────────────
|
||||
export type DashboardUserStats = {
|
||||
total: number
|
||||
creatorCount: number
|
||||
onlineCount: number
|
||||
todayNew: number
|
||||
weekNew: number
|
||||
totalMessages: number
|
||||
totalChats: number
|
||||
}
|
||||
|
||||
export type DashboardCharacterStats = {
|
||||
total: number
|
||||
published: number
|
||||
pendingReview: number
|
||||
todayNew: number
|
||||
weekNew: number
|
||||
}
|
||||
|
||||
export type DashboardTopCreator = {
|
||||
userId: number
|
||||
nickName: string
|
||||
avatar: string
|
||||
charCount: number
|
||||
totalUseCount: number
|
||||
}
|
||||
|
||||
export type DashboardCreatorStats = {
|
||||
total: number
|
||||
contracted: number
|
||||
todayNew: number
|
||||
weekNew: number
|
||||
pendingApplications: number
|
||||
topCreators: DashboardTopCreator[]
|
||||
}
|
||||
|
||||
export type DashboardFeedbackPendingItem = {
|
||||
id: number
|
||||
title: string
|
||||
userNickname: string
|
||||
userAvatar: string
|
||||
unreadCount: number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export type DashboardFeedbackStats = {
|
||||
total: number
|
||||
open: number
|
||||
adminUnread: number
|
||||
closedToday: number
|
||||
pendingItems: DashboardFeedbackPendingItem[]
|
||||
}
|
||||
|
||||
export type DashboardSystemStats = {
|
||||
totalConversations: number
|
||||
}
|
||||
|
||||
export type DashboardStats = {
|
||||
users: DashboardUserStats
|
||||
characters: DashboardCharacterStats
|
||||
creators: DashboardCreatorStats
|
||||
feedback: DashboardFeedbackStats
|
||||
system: DashboardSystemStats
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user