🎨 更新环境配置,添加 Dockerfile,优化代码结构

This commit is contained in:
2026-04-21 22:09:19 +08:00
parent 81d8466f88
commit 5c2a146a44
35 changed files with 6396 additions and 141 deletions

View File

@@ -1 +1 @@
VITE_BASE_API=/api
VITE_BASE_API=http://localhost:8888

View File

@@ -1 +1 @@
VITE_BASE_API=/api
VITE_BASE_API=https://api.wanjia.ai

View File

@@ -1,5 +1,5 @@
# Logs
logs
#logs
*.log
npm-debug.log*
yarn-debug.log*

26
web-admin/Dockerfile Normal file
View 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
View 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";
}
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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>
)
}

View File

@@ -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: '字典管理',

View File

@@ -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' }}

View File

@@ -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 }}
/>
) : (

View File

@@ -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]))

View File

@@ -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: '服务器状态页磁盘挂载点来源。',

View File

@@ -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;
}

View File

@@ -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`)
},
}

View File

@@ -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
}