diff --git a/apps/web/src/api/accounts.ts b/apps/web/src/api/accounts.ts index 24209fcd2..301fc3873 100644 --- a/apps/web/src/api/accounts.ts +++ b/apps/web/src/api/accounts.ts @@ -1,7 +1,9 @@ /** - * 账号管理 Mock API + * 账号管理 API — ⚠️ 当前全部为 Mock 实现 + * + * TODO(后端): 后端尚未提供账号管理相关 API(router.py 无 accounts 路由)。 + * 当前所有函数均为前端 mock(内存数据 + 模拟延迟),待后端就绪后需替换为真实 API 调用。 * - * 模拟多平台账号绑定/解绑操作 * 支持平台:抖音、快手、小红书、微信视频号 */ @@ -71,7 +73,7 @@ export const PLATFORMS: Platform[] = [ }, ]; -/* ── Mock 数据 ───────────────────────────────────────────── */ +/* ── Mock 数据(TODO: 待后端提供账号存储后替换) ────────── */ let MOCK_ACCOUNTS: Account[] = [ { @@ -108,11 +110,11 @@ let MOCK_ACCOUNTS: Account[] = [ }, ]; -/* ── 模拟延迟 ───────────────────────────────────────────── */ +/* ── 模拟延迟(TODO: 后端就绪后移除) ─────────────────── */ const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)); -/* ── API 函数 ───────────────────────────────────────────── */ +/* ── Mock API 函数(TODO: 替换为 apiClient 真实调用) ──── */ /** 获取指定平台的账号列表 */ export async function getAccountsByPlatform( diff --git a/apps/web/src/pages/admin/Admin.css b/apps/web/src/pages/admin/Admin.css index 071cb79cc..6eb0fc565 100644 --- a/apps/web/src/pages/admin/Admin.css +++ b/apps/web/src/pages/admin/Admin.css @@ -1,442 +1,20 @@ -/* V21 Admin 页面样式 */ +/* Admin 页面样式(Phase 3 精简) + * + * 原始 477 行 → 精简至仅保留实际使用的 class。 + * 已迁移至 global.css / ui.css 的样式不再重复定义: + * .xx-page-head → global.css + * .xx-primary-btn → global.css + * .xx-tag / .xx-card → ui.css / global.css + * + * 以下 class 仅被 AdminComingSoon.tsx 使用。 + */ -/* 页面容器 */ -.dashboard-page, -.analytics-page, -.user-management-page, -.log-viewer-page, -.system-monitor-page, .admin-coming-soon-page { padding: 32px; max-width: 1400px; margin: 0 auto; } -/* 页面头部 */ -.xx-page-head { - background: rgba(255, 255, 255, 0.94); - border: 1px solid rgba(226, 232, 240, 0.95); - border-radius: var(--radius-xl); - box-shadow: 0 24px 70px rgba(15, 23, 42, 0.09); - padding: 32px 40px; - margin-bottom: 32px; - display: flex; - justify-content: space-between; - align-items: center; -} - -.xx-page-head-content { - display: flex; - flex-direction: column; - gap: 8px; -} - -.xx-page-head h2 { - font-size: 28px; - font-weight: 900; - color: var(--slate, #0f172a); - margin: 0; - letter-spacing: -0.02em; -} - -.xx-page-head p { - font-size: 15px; - color: var(--muted, #64748b); - margin: 0; -} - -.xx-page-head-actions { - display: flex; - gap: 12px; - align-items: center; -} - -/* 简化页面头部(无操作按钮) */ -.xx-page-head-simple { - background: rgba(255, 255, 255, 0.94); - border: 1px solid rgba(226, 232, 240, 0.95); - border-radius: var(--radius-xl); - box-shadow: 0 24px 70px rgba(15, 23, 42, 0.09); - padding: 32px 40px; - margin-bottom: 32px; -} - -.xx-page-head-simple h2 { - font-size: 28px; - font-weight: 900; - color: var(--slate, #0f172a); - margin: 0 0 8px; - letter-spacing: -0.02em; -} - -.xx-page-head-simple p { - font-size: 15px; - color: var(--muted, #64748b); - margin: 0; -} - -/* 统计卡片网格 - 4列 */ -.xx-grid-4 { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 24px; - margin-bottom: 32px; -} - -@media (max-width: 1200px) { - .xx-grid-4 { - grid-template-columns: repeat(2, 1fr); - } -} - -@media (max-width: 768px) { - .xx-grid-4 { - grid-template-columns: 1fr; - } -} - -/* 统计卡片 */ -.xx-stat-card { - background: rgba(255, 255, 255, 0.94); - border: 1px solid rgba(226, 232, 240, 0.95); - border-radius: var(--radius-lg); - box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06); - padding: 24px; - transition: all 0.3s; -} - -.xx-stat-card:hover { - transform: translateY(-2px); - box-shadow: 0 16px 40px rgba(15, 23, 42, 0.08); -} - -.xx-stat-card-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 16px; -} - -.xx-stat-card-icon { - width: 48px; - height: 48px; - border-radius: var(--radius-md); - display: flex; - align-items: center; - justify-content: center; - font-size: 22px; -} - -.xx-stat-card-icon.primary { - background: linear-gradient(135deg, #6366f1, #4f46e5); - color: white; -} - -.xx-stat-card-icon.success { - background: linear-gradient(135deg, #34d399, #10b981); - color: white; -} - -.xx-stat-card-icon.warning { - background: linear-gradient(135deg, #fbbf24, #f59e0b); - color: white; -} - -.xx-stat-card-icon.purple { - background: linear-gradient(135deg, #a78bfa, #8b5cf6); - color: white; -} - -.xx-stat-card-icon.info { - background: linear-gradient(135deg, #60a5fa, #3b82f6); - color: white; -} - -.xx-stat-card-icon.orange { - background: linear-gradient(135deg, #fb923c, #f97316); - color: white; -} - -.xx-stat-card-label { - font-size: 14px; - color: var(--muted, #64748b); - font-weight: 500; - margin-bottom: 8px; -} - -.xx-stat-card-value { - font-size: 32px; - font-weight: 900; - color: var(--slate, #0f172a); - line-height: 1.2; - letter-spacing: -0.02em; -} - -.xx-stat-card-value.primary { - color: var(--indigo, #4f46e5); -} - -.xx-stat-card-value.success { - color: var(--green, #10b981); -} - -.xx-stat-card-value.warning { - color: var(--amber, #f59e0b); -} - -.xx-stat-card-value.purple { - color: #8b5cf6; -} - -.xx-stat-card-growth { - font-size: 13px; - color: var(--green, #10b981); - font-weight: 600; - margin-top: 8px; - display: flex; - align-items: center; - gap: 4px; -} - -/* 数据表格 */ -.xx-table-wrapper { - background: rgba(255, 255, 255, 0.94); - border: 1px solid rgba(226, 232, 240, 0.95); - border-radius: var(--radius-lg); - box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06); - overflow: hidden; -} - -.xx-table-wrapper .ant-table { - background: transparent; -} - -.xx-table-wrapper .ant-table-thead > tr > th { - background: rgba(248, 250, 252, 0.8); - font-weight: 800; - font-size: 13px; - color: var(--slate, #0f172a); - text-transform: uppercase; - letter-spacing: 0.05em; - padding: 16px 20px; -} - -.xx-table-wrapper .ant-table-tbody > tr > td { - padding: 16px 20px; - font-size: 14px; -} - -.xx-table-wrapper .ant-table-tbody > tr:hover > td { - background: rgba(79, 70, 229, 0.03); -} - -/* V21 按钮 */ -.xx-primary-btn { - background: linear-gradient(135deg, #6366f1, #4f46e5) !important; - color: white !important; - border: none !important; - border-radius: var(--radius-md) !important; - font-weight: 700 !important; - box-shadow: 0 8px 20px rgba(79, 70, 229, 0.25) !important; - transition: all 0.2s !important; -} - -.xx-primary-btn:hover { - box-shadow: 0 12px 28px rgba(79, 70, 229, 0.3) !important; - transform: translateY(-1px); -} - -.xx-ghost-btn { - background: white !important; - border: 1px solid rgba(226, 232, 240, 0.95) !important; - color: var(--slate, #0f172a) !important; - border-radius: var(--radius-md) !important; - font-weight: 600 !important; - transition: all 0.2s !important; -} - -.xx-ghost-btn:hover { - border-color: var(--indigo, #4f46e5) !important; - color: var(--indigo, #4f46e5) !important; -} - -/* V21 输入框 */ -.xx-search-input { - border-radius: var(--radius-md) !important; - border: 1px solid rgba(226, 232, 240, 0.95) !important; - padding: 8px 16px !important; -} - -.xx-search-input:hover, -.xx-search-input:focus { - border-color: var(--indigo, #4f46e5) !important; - box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1) !important; -} - -/* V21 Tag */ -.xx-tag { - border-radius: var(--radius-xs) !important; - font-weight: 600 !important; - font-size: 12px !important; - padding: 4px 10px !important; -} - -.xx-tag.info { - background: rgba(59, 130, 246, 0.1) !important; - color: #3b82f6 !important; - border: 1px solid rgba(59, 130, 246, 0.2) !important; -} - -.xx-tag.success { - background: rgba(16, 185, 129, 0.1) !important; - color: #10b981 !important; - border: 1px solid rgba(16, 185, 129, 0.2) !important; -} - -.xx-tag.warning { - background: rgba(245, 158, 11, 0.1) !important; - color: #f59e0b !important; - border: 1px solid rgba(245, 158, 11, 0.2) !important; -} - -.xx-tag.error { - background: rgba(239, 68, 68, 0.1) !important; - color: #ef4444 !important; - border: 1px solid rgba(239, 68, 68, 0.2) !important; -} - -.xx-tag.debug { - background: rgba(100, 116, 139, 0.1) !important; - color: #64748b !important; - border: 1px solid rgba(100, 116, 139, 0.2) !important; -} - -/* V21 Progress */ -.xx-progress-primary .ant-progress-circle .ant-progress-text { - color: var(--indigo, #4f46e5) !important; - font-weight: 700 !important; -} - -.xx-progress-success .ant-progress-circle .ant-progress-text { - color: var(--green, #10b981) !important; - font-weight: 700 !important; -} - -.xx-progress-warning .ant-progress-circle .ant-progress-text { - color: var(--amber, #f59e0b) !important; - font-weight: 700 !important; -} - -/* 图表容器 */ -.xx-chart-container { - background: rgba(255, 255, 255, 0.94); - border: 1px solid rgba(226, 232, 240, 0.95); - border-radius: var(--radius-lg); - box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06); - padding: 24px; -} - -.xx-chart-container .ant-card-head { - border-bottom: 1px solid rgba(226, 232, 240, 0.8); - padding: 20px 24px; -} - -.xx-chart-container .ant-card-head-title { - font-weight: 800; - font-size: 17px; - color: var(--slate, #0f172a); -} - -/* 筛选器区域 */ -.xx-filter-bar { - background: rgba(255, 255, 255, 0.94); - border: 1px solid rgba(226, 232, 240, 0.95); - border-radius: var(--radius-lg); - box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06); - padding: 20px 24px; - margin-bottom: 24px; - display: flex; - flex-wrap: wrap; - gap: 16px; - align-items: center; -} - -/* 资源监控卡片 */ -.xx-resource-card { - background: rgba(255, 255, 255, 0.94); - border: 1px solid rgba(226, 232, 240, 0.95); - border-radius: var(--radius-lg); - box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06); - padding: 24px; - text-align: center; -} - -.xx-resource-card-icon { - width: 64px; - height: 64px; - border-radius: var(--radius-md); - display: flex; - align-items: center; - justify-content: center; - margin: 0 auto 16px; - font-size: 28px; -} - -.xx-resource-card-label { - font-size: 14px; - color: var(--muted, #64748b); - font-weight: 600; - margin-bottom: 16px; -} - -/* 日期选择器 */ -.xx-date-picker { - border-radius: var(--radius-md) !important; -} - -/* Drawer */ -.xx-drawer .ant-drawer-header { - border-bottom: 1px solid rgba(226, 232, 240, 0.8); - padding: 20px 24px; -} - -.xx-drawer .ant-drawer-title { - font-weight: 800; - font-size: 18px; - color: var(--slate, #0f172a); -} - -/* 日志详情 */ -.xx-log-detail { - padding: 4px 0; -} - -.xx-log-detail-label { - font-size: 13px; - font-weight: 700; - color: var(--slate, #0f172a); - margin-bottom: 4px; -} - -.xx-log-detail-value { - font-size: 14px; - color: var(--muted, #64748b); - background: rgba(248, 250, 252, 0.8); - padding: 12px; - border-radius: var(--radius-sm); - margin-bottom: 16px; -} - -.xx-log-detail-code { - font-family: "JetBrains Mono", "Fira Code", monospace; - background: rgba(248, 250, 252, 0.8); - padding: 12px; - border-radius: var(--radius-sm); - white-space: pre-wrap; - word-break: break-all; -} - -/* Result 页面居中 */ .xx-result-center { display: flex; justify-content: center; @@ -447,31 +25,3 @@ .xx-result-center .ant-result { padding: 48px; } - -/* 刷新时间显示 */ -.xx-refresh-time { - font-size: 13px; - color: var(--muted, #64748b); - margin-right: 12px; -} - -/* 图表配色覆盖 */ -.recharts-text { - fill: #64748b !important; - font-size: 12px !important; -} - -.recharts-cartesian-grid-horizontal line, -.recharts-cartesian-grid-vertical line { - stroke: rgba(226, 232, 240, 0.8) !important; -} - -/* 图表 Legend */ -.recharts-legend-wrapper { - padding-top: 16px !important; -} - -.recharts-legend-item-text { - color: #64748b !important; - font-size: 13px !important; -} diff --git a/apps/web/src/pages/titles/TitleLibrary.tsx b/apps/web/src/pages/titles/TitleLibrary.tsx index 36eac804a..740f9f62c 100644 --- a/apps/web/src/pages/titles/TitleLibrary.tsx +++ b/apps/web/src/pages/titles/TitleLibrary.tsx @@ -36,36 +36,24 @@ type TitleType = "hot" | "normal" | "creative"; type Industry = "general" | "food" | "tech" | "beauty" | "education" | "travel"; type Frequency = "all" | "high" | "medium" | "low"; -interface CategoryItem { - id: string; - name: string; - count: number; -} - interface TitleData { id: string; content: string; type: TitleType; industry: Industry; + category: string; usageCount: number; isFavorited: boolean; createdAt: string; } -/* ============================================================ - * Mock 数据 - * ============================================================ */ -// TODO: 分类数据当前为前端硬编码 mock,待后端提供标题分类 API 后替换 -const MOCK_CATEGORIES: CategoryItem[] = [ - { id: "cat-all", name: "全部标题", count: 0 }, -]; - /** 后端 TitleItem → 前端 TitleData 映射 */ const toTitleData = (item: TitleItem): TitleData => ({ id: item.id, content: item.content, type: (item.category as TitleType) || "normal", industry: "general", + category: item.category || "未分类", usageCount: 0, isFavorited: false, createdAt: item.created_at?.slice(0, 10) || "", @@ -246,9 +234,8 @@ const TitleCard: React.FC<{ const TitleLibrary: React.FC = () => { const queryClient = useQueryClient(); - /* 分类数据 */ - const [categories, setCategories] = useState(MOCK_CATEGORIES); - const [activeCatId, setActiveCatId] = useState(MOCK_CATEGORIES[0].id); + /* 分类数据 — 从真实标题数据动态派生 */ + const [activeCatId, setActiveCatId] = useState("cat-all"); /* 标题数据 — 真实 API */ const { data: apiTitles = [] } = useQuery({ @@ -261,6 +248,23 @@ const TitleLibrary: React.FC = () => { [apiTitles], ); + /* 从真实标题数据动态派生分类(无需后端分类 API) */ + const categories = useMemo(() => { + const cats = new Map(); + apiTitles.forEach((t) => { + const cat = t.category || "未分类"; + cats.set(cat, (cats.get(cat) || 0) + 1); + }); + return [ + { id: "cat-all", name: "全部标题", count: apiTitles.length }, + ...Array.from(cats.entries()).map(([name, count]) => ({ + id: `cat-${name}`, + name, + count, + })), + ]; + }, [apiTitles]); + /* CRUD mutations */ const createMutation = useMutation({ mutationFn: (content: string) => createTitle({ content }), @@ -297,10 +301,6 @@ const TitleLibrary: React.FC = () => { const [editingId, setEditingId] = useState(null); const [editText, setEditText] = useState(""); - /* 新建分类 */ - const [createCatModalOpen, setCreateCatModalOpen] = useState(false); - const [newCatName, setNewCatName] = useState(""); - /* 新建标题 */ const [createTitleModalOpen, setCreateTitleModalOpen] = useState(false); const [newTitleContent, setNewTitleContent] = useState(""); @@ -318,19 +318,11 @@ const TitleLibrary: React.FC = () => { const filteredTitles = useMemo(() => { let list = titles; - /* 按分类过滤("全部标题" 不过滤) */ + /* 按分类过滤("全部标题" 不过滤)— 直接匹配后端 category 字段 */ if (activeCatId !== "cat-all") { const catName = activeCategory?.name || ""; - const catToIndustry: Record = { - 美食探店: "food", - 科技数码: "tech", - 生活日常: "general", - 美妆穿搭: "beauty", - 教育学习: "education", - }; - const mappedIndustry = catToIndustry[catName]; - if (mappedIndustry) { - list = list.filter((t) => t.industry === mappedIndustry); + if (catName) { + list = list.filter((t) => t.category === catName); } } @@ -424,32 +416,6 @@ const TitleLibrary: React.FC = () => { [deleteMutation], ); - /* 新建分类 */ - const handleCreateCategory = () => { - if (!newCatName.trim()) { - message.warning("请输入分类名称"); - return; - } - const cat: CategoryItem = { - id: `cat-${Date.now()}`, - name: newCatName.trim(), - count: 0, - }; - setCategories((prev) => [...prev, cat]); - setActiveCatId(cat.id); - setCreateCatModalOpen(false); - setNewCatName(""); - message.success(`分类 "${cat.name}" 创建成功`); - }; - - /* 删除分类 */ - const handleDeleteCategory = (id: string) => { - setCategories((prev) => prev.filter((c) => c.id !== id)); - if (activeCatId === id) { - setActiveCatId("cat-all"); - } - message.success("分类已删除"); - }; /* 新建标题 */ const handleCreateTitle = () => { @@ -537,38 +503,12 @@ const TitleLibrary: React.FC = () => { {cat.count} 条 - {cat.id !== "cat-all" && ( - { - e?.stopPropagation(); - handleDeleteCategory(cat.id); - }} - onCancel={(e) => e?.stopPropagation()} - okText="删除" - cancelText="取消" - > - - - )} + ))} - {/* 新建分类 */} -
setCreateCatModalOpen(true)} - > - - 新建分类 -
+ {/* TODO: 新建分类功能待后端分类 API 就绪后启用 */} {/* ─── 右侧:内容区 ─── */} @@ -674,34 +614,7 @@ const TitleLibrary: React.FC = () => { - {/* ─── 新建分类弹窗 ─── */} - setCreateCatModalOpen(false)} - onOk={handleCreateCategory} - okText="创建" - cancelText="取消" - destroyOnClose - > -
-
- 分类名称 -
- setNewCatName(e.target.value)} - maxLength={30} - /> -
-
+ {/* ─── 新建标题弹窗 ─── */}