Compare commits

...

3 Commits

Author SHA1 Message Date
xiaoxia 6227aa610c fix: Dashboard CSS 类名对齐已有样式 + 修复路由指向
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 9s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 11s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Failing after 5s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 1m2s
- 所有类名改回 dashboard.css 已有定义(xx-kpi-*/xx-task-*/xx-quick-*/xx-chart-*/xx-announcement-*)
- "查看全部"路由从 /asset-library 改为 /app/history
- vite build 验证通过
2026-07-13 16:57:21 +08:00
CI Test 3ebcc7e066 refactor: 补做 Phase 3 Mock 真替换 — accounts/Dashboard/GeneratePage
CI/CD Pipeline / Unit Tests (pull_request) Failing after 16s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 21s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 39s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Failing after 43s
- 删除 api/accounts.ts(163行全 mock),Accounts.tsx 改为占位状态
- Dashboard.tsx 移除硬编码 mock 数据(kpiData/quickEntries/recentTasks),改用空状态
- GeneratePage.tsx 移除 MOCK_TIMELINE 及 TimelineScene 接口,时间线预览改为占位提示
- 清理所有相关 TODO 注释和未使用的 import
- vite build 验证通过
2026-07-13 16:46:14 +08:00
CI Test 923c6bad1c feat(frontend): Phase 3 前端深度清理 — Mock 数据真替换 + Admin.css 全清
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 1m22s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 2m40s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 1m44s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 3m19s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
1. TitleLibrary.tsx: 移除 MOCK_CATEGORIES,分类从真实 API 数据动态派生
   - 删除 CategoryItem 接口、MOCK_CATEGORIES 常量
   - 用 useMemo 从 apiTitles.category 字段聚合分类列表
   - 分类筛选改为直接匹配 category 字段(移除 catToIndustry 硬编码映射)
   - 移除无后端支持的新建/删除分类功能(保留 TODO 注释)
   - TitleData 新增 category 字段

2. accounts.ts: 全文件标记为 Mock,添加明确 TODO
   - 文件头注明后端无 accounts 路由,待后端就绪后替换
   - 各 section 注释标注 mock 性质

3. Admin.css: 477 行 → 27 行
   - 仅保留 AdminComingSoon.tsx 实际使用的 2 个 class
   - 其余样式已迁移至 global.css / ui.css

4. 未使用依赖: 已确认 package.json 全部 26 个依赖均在使用,无需清理
2026-07-13 16:07:26 +08:00
6 changed files with 180 additions and 1382 deletions
-161
View File
@@ -1,161 +0,0 @@
/**
* 账号管理 Mock API
*
* 模拟多平台账号绑定/解绑操作
* 支持平台:抖音、快手、小红书、微信视频号
*/
/* ── 类型定义 ───────────────────────────────────────────── */
/** 平台 ID */
export type PlatformId = "douyin" | "kuaishou" | "xiaohongshu" | "wechat";
/** 账号状态 */
export type AccountStatus = "active" | "expired" | "limited";
/** 已绑定的账号 */
export interface Account {
id: string;
platform_id: PlatformId;
name: string;
avatar?: string;
status: AccountStatus;
bound_at: string;
}
/** 平台信息 */
export interface Platform {
id: PlatformId;
name: string;
subName: string;
icon: string;
gradient: string;
}
/** 绑定账号请求 */
export interface BindAccountRequest {
platform_id: PlatformId;
name: string;
}
/* ── 平台配置 ───────────────────────────────────────────── */
export const PLATFORMS: Platform[] = [
{
id: "douyin",
name: "抖音",
subName: "短视频发布平台",
icon: "📱",
gradient: "linear-gradient(135deg, #fe2c55, #25f4ee)",
},
{
id: "kuaishou",
name: "快手",
subName: "短视频发布平台",
icon: "🎬",
gradient: "linear-gradient(135deg, #ff4906, #ffba00)",
},
{
id: "xiaohongshu",
name: "小红书",
subName: "种草笔记发布平台",
icon: "📕",
gradient: "linear-gradient(135deg, #ff2442, #ff6b6b)",
},
{
id: "wechat",
name: "微信视频号",
subName: "视频号发布平台",
icon: "💬",
gradient: "linear-gradient(135deg, #07c160, #4cd964)",
},
];
/* ── Mock 数据 ───────────────────────────────────────────── */
let MOCK_ACCOUNTS: Account[] = [
{
id: "acc-001",
platform_id: "douyin",
name: "小虾官方号",
avatar: "🦐",
status: "active",
bound_at: "2025-12-01T10:00:00Z",
},
{
id: "acc-002",
platform_id: "douyin",
name: "小虾日常",
avatar: "🐟",
status: "active",
bound_at: "2025-12-15T14:30:00Z",
},
{
id: "acc-003",
platform_id: "kuaishou",
name: "小虾剪辑",
avatar: "🎬",
status: "active",
bound_at: "2026-01-05T09:00:00Z",
},
{
id: "acc-004",
platform_id: "xiaohongshu",
name: "小虾种草",
avatar: "📕",
status: "limited",
bound_at: "2026-02-20T16:00:00Z",
},
];
/* ── 模拟延迟 ───────────────────────────────────────────── */
const delay = (ms: number) => new Promise((r) => setTimeout(r, ms));
/* ── API 函数 ───────────────────────────────────────────── */
/** 获取指定平台的账号列表 */
export async function getAccountsByPlatform(
platformId: PlatformId,
): Promise<Account[]> {
await delay(300);
return MOCK_ACCOUNTS.filter((a) => a.platform_id === platformId);
}
/** 获取所有平台的账号总数 */
export async function getAllAccounts(): Promise<Account[]> {
await delay(200);
return [...MOCK_ACCOUNTS];
}
/** 绑定新账号 */
export async function bindAccount(data: BindAccountRequest): Promise<Account> {
await delay(500);
const newAccount: Account = {
id: `acc-${Date.now()}`,
platform_id: data.platform_id,
name: data.name,
avatar: undefined,
status: "active",
bound_at: new Date().toISOString(),
};
MOCK_ACCOUNTS = [...MOCK_ACCOUNTS, newAccount];
return newAccount;
}
/** 解绑账号 */
export async function unbindAccount(accountId: string): Promise<void> {
await delay(400);
MOCK_ACCOUNTS = MOCK_ACCOUNTS.filter((a) => a.id !== accountId);
}
/* ── 状态配置 ───────────────────────────────────────────── */
export const ACCOUNT_STATUS_CONFIG: Record<
AccountStatus,
{ label: string; className: string }
> = {
active: { label: "正常", className: "acc-status--active" },
expired: { label: "已过期", className: "acc-status--expired" },
limited: { label: "受限", className: "acc-status--limited" },
};
+75 -198
View File
@@ -2,194 +2,66 @@
* 账号管理页面 — V21 Design System
*
* 展示多平台账号绑定状态(抖音/快手/小红书/微信视频号)
* 支持绑定/解绑操作
* 后端账号管理 API 尚未就绪,当前展示占位状态
*
* 零 antd 直接导入,全部使用 CSS 变量
*/
import React, { useState, useCallback } from "react";
import { useQueries, useMutation, useQueryClient } from "@tanstack/react-query";
import React from "react";
import { Button } from "@/components/ui";
import PageHead from "@/components/layout/PageHead";
import {
PLATFORMS,
getAccountsByPlatform,
unbindAccount,
bindAccount,
ACCOUNT_STATUS_CONFIG,
type Platform,
type Account,
type PlatformId,
} from "@/api/accounts";
import "./accounts.css";
/* ── Toast 系统 ─────────────────────────────────────────── */
/* ── 类型定义 ───────────────────────────────────────────── */
interface Toast {
id: number;
message: string;
type: "success" | "error";
export type PlatformId =
| "douyin"
| "kuaishou"
| "xiaohongshu"
| "wechat";
export interface Platform {
id: PlatformId;
name: string;
subName: string;
icon: string;
gradient: string;
}
let toastIdCounter = 0;
/* ── 平台卡片组件 ───────────────────────────────────────── */
interface PlatformCardProps {
platform: Platform;
accounts: Account[];
isLoading: boolean;
onBind: (platformId: PlatformId) => void;
onUnbind: (accountId: string, accountName: string) => void;
}
const PlatformCard: React.FC<PlatformCardProps> = ({
platform,
accounts,
isLoading,
onBind,
onUnbind,
}) => {
return (
<div className="acc-card">
{/* 平台头部 */}
<div className="acc-card-header">
<div
className="acc-card-icon"
style={{ background: platform.gradient }}
>
{platform.icon}
</div>
<div>
<h3 className="acc-card-title">{platform.name}</h3>
<p className="acc-card-subtitle">{platform.subName}</p>
</div>
</div>
{/* 账号列表 */}
<div className="acc-account-list">
{isLoading ? (
<div className="acc-empty">
<p className="acc-empty-text"></p>
</div>
) : accounts.length > 0 ? (
accounts.map((account) => {
const statusCfg = ACCOUNT_STATUS_CONFIG[account.status];
return (
<div key={account.id} className="acc-account-row">
<div
className="acc-account-avatar"
style={{ background: platform.gradient }}
>
{account.avatar || platform.icon}
</div>
<div className="acc-account-info">
<div className="acc-account-name">{account.name}</div>
<span className={`acc-status-pill ${statusCfg.className}`}>
{statusCfg.label}
</span>
</div>
<Button
buttonType="ghost"
buttonSize="sm"
onClick={() => onUnbind(account.id, account.name)}
>
</Button>
</div>
);
})
) : (
<div className="acc-empty">
<div className="acc-empty-icon">🔓</div>
<p className="acc-empty-text"></p>
</div>
)}
</div>
{/* 绑定按钮 */}
<Button buttonType="ghost" onClick={() => onBind(platform.id)}>
+
</Button>
</div>
);
};
/** 支持的平台列表 */
const PLATFORMS: Platform[] = [
{
id: "douyin",
name: "抖音",
subName: "短视频发布",
icon: "🎵",
gradient: "linear-gradient(135deg, #000 0%, #333 100%)",
},
{
id: "kuaishou",
name: "快手",
subName: "短视频发布",
icon: "📹",
gradient: "linear-gradient(135deg, #ff6600 0%, #ff9933 100%)",
},
{
id: "xiaohongshu",
name: "小红书",
subName: "种草笔记 + 视频",
icon: "📕",
gradient: "linear-gradient(135deg, #fe2c55 0%, #ff6680 100%)",
},
{
id: "wechat",
name: "微信视频号",
subName: "视频号发布",
icon: "💬",
gradient: "linear-gradient(135deg, #07c160 0%, #38d97a 100%)",
},
];
/* ── 主页面 ─────────────────────────────────────────────── */
const Accounts: React.FC = () => {
const queryClient = useQueryClient();
const [toasts, setToasts] = useState<Toast[]>([]);
/** 显示 toast */
const showToast = useCallback((message: string, type: Toast["type"]) => {
const id = ++toastIdCounter;
setToasts((prev) => [...prev, { id, message, type }]);
setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, 3000);
}, []);
/** 查询所有平台的账号 */
const _queriesResults = useQueries({
queries: PLATFORMS.map((platform) => ({
queryKey: ["accounts", platform.id] as const,
queryFn: () => getAccountsByPlatform(platform.id),
})),
});
const accountQueries = PLATFORMS.map((platform, i) => ({
platform,
..._queriesResults[i],
}));
/** 解绑 mutation */
const unbindMutation = useMutation({
mutationFn: unbindAccount,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["accounts"] });
showToast("已解绑账号", "success");
},
onError: () => {
showToast("解绑失败", "error");
},
});
/** 绑定 mutation */
const bindMutation = useMutation({
mutationFn: bindAccount,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["accounts"] });
showToast("账号绑定成功", "success");
},
onError: () => {
showToast("绑定失败", "error");
},
});
/** 绑定新账号 */
const handleBind = (platformId: PlatformId) => {
const platform = PLATFORMS.find((p) => p.id === platformId);
if (!platform) return;
const name = window.prompt(`请输入要绑定的${platform.name}账号名称:`);
if (name && name.trim()) {
bindMutation.mutate({ platform_id: platformId, name: name.trim() });
}
};
/** 解绑账号 */
const handleUnbind = (accountId: string, accountName: string) => {
if (window.confirm(`确定解绑账号「${accountName}」吗?`)) {
unbindMutation.mutate(accountId);
}
};
/** 统计已绑定账号数 */
const totalBound = accountQueries.reduce(
(sum, q) => sum + (q.data?.length ?? 0),
0,
);
const totalPlatforms = PLATFORMS.length;
return (
<div className="acc-page">
<PageHead
@@ -197,17 +69,34 @@ const Accounts: React.FC = () => {
description="绑定您的社交平台账号,用于视频一键发布到各平台"
/>
{/* 平台卡片网格 */}
{/* 平台卡片网格 — 占位状态 */}
<div className="acc-grid">
{accountQueries.map(({ platform, data, isLoading }) => (
<PlatformCard
key={platform.id}
platform={platform}
accounts={data ?? []}
isLoading={isLoading}
onBind={handleBind}
onUnbind={handleUnbind}
/>
{PLATFORMS.map((platform) => (
<div key={platform.id} className="acc-card">
<div className="acc-card-header">
<div
className="acc-card-icon"
style={{ background: platform.gradient }}
>
{platform.icon}
</div>
<div>
<h3 className="acc-card-title">{platform.name}</h3>
<p className="acc-card-subtitle">{platform.subName}</p>
</div>
</div>
<div className="acc-account-list">
<div className="acc-empty">
<div className="acc-empty-icon">🔒</div>
<p className="acc-empty-text">线</p>
</div>
</div>
<Button buttonType="ghost" disabled>
</Button>
</div>
))}
</div>
@@ -215,22 +104,10 @@ const Accounts: React.FC = () => {
<div className="acc-stats-bar">
<span className="acc-stats-icon">📊</span>
<span className="acc-stats-text">
<span className="acc-stats-highlight">{totalBound}</span>{" "}
/ {" "}
<span className="acc-stats-highlight">{totalPlatforms}</span>
<span className="acc-stats-highlight">{PLATFORMS.length}</span>{" "}
线
</span>
</div>
{/* Toast 提示 */}
{toasts.length > 0 && (
<div className="vc-toast-container">
{toasts.map((t) => (
<div key={t.id} className={`vc-toast vc-toast--${t.type}`}>
{t.type === "success" ? "✅" : "❌"} {t.message}
</div>
))}
</div>
)}
</div>
);
};
+10 -460
View File
@@ -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;
}
+56 -425
View File
@@ -1,454 +1,85 @@
/**
* 控制台页面 — V21 设计系统
* KPI 卡片网格 + 快速入口 + 最近任务卡片列表 + 使用统计图表 + 公告
* 使用 mock 数据,CSS 变量,V21 组件
* CSS 变量,V21 组件
*/
import React from "react";
import { useNavigate } from "react-router-dom";
import { Button, Tag } from "@/components/ui";
import {
VideoCameraOutlined,
AppstoreOutlined,
ThunderboltOutlined,
DatabaseOutlined,
FileTextOutlined,
} from "@ant-design/icons";
import { DatabaseOutlined } from "@ant-design/icons";
import "./dashboard.css";
/* ============================================================
* Mock 数据
* ============================================================ */
interface KpiItem {
key: string;
icon: string;
iconGradient: string;
value: string;
label: string;
trend: string;
trendDirection: "up" | "down" | "neutral";
accent: string;
}
/* ── 主组件 ─────────────────────────────────────────────── */
// TODO: kpiData 当前使用硬编码 mock 数据,待后端提供 Dashboard 统计 API 后替换
const kpiData: KpiItem[] = [
{
key: "projects",
icon: "video",
iconGradient: "linear-gradient(135deg, #6366f1, #4f46e5)",
value: "12",
label: "项目总数",
trend: "↑ 2 本月新增",
trendDirection: "up",
accent: "#6366f1",
},
{
key: "assets",
icon: "appstore",
iconGradient: "linear-gradient(135deg, #0ea5e9, #0284c7)",
value: "486",
label: "素材总数",
trend: "↑ 38 本月上传",
trendDirection: "up",
accent: "#0ea5e9",
},
{
key: "generations",
icon: "thunderbolt",
iconGradient: "linear-gradient(135deg, #10b981, #059669)",
value: "156",
label: "本月生成数",
trend: "↑ 23% 较上月",
trendDirection: "up",
accent: "#10b981",
},
{
key: "storage",
icon: "database",
iconGradient: "linear-gradient(135deg, #f59e0b, #d97706)",
value: "2.4GB",
label: "存储空间",
trend: "已用 24%",
trendDirection: "neutral",
accent: "#f59e0b",
},
];
interface QuickEntry {
id: string;
icon: string;
iconGradient: string;
title: string;
description: string;
path: string;
}
// TODO: quickEntries 描述中含硬编码计数(如 486个素材),待后端 API 后动态化
const quickEntries: QuickEntry[] = [
{
id: "titles",
icon: "filetext",
iconGradient: "linear-gradient(135deg, #6366f1, #4f46e5)",
title: "标题库",
description: "24条标题 · 5个分类",
path: "/app/titles",
},
{
id: "assets",
icon: "appstore",
iconGradient: "linear-gradient(135deg, #0ea5e9, #0284c7)",
title: "素材库",
description: "486个素材 · 3个素材库",
path: "/app/assets",
},
{
id: "generate",
icon: "thunderbolt",
iconGradient: "linear-gradient(135deg, #10b981, #059669)",
title: "一键生成",
description: "开始创作新视频",
path: "/app/generate",
},
{
id: "products",
icon: "video",
iconGradient: "linear-gradient(135deg, #f59e0b, #d97706)",
title: "成片库",
description: "89个成片 · 3个待复核",
path: "/app/products",
},
];
type TaskStatus = "completed" | "processing" | "pending" | "failed";
interface RecentTask {
id: string;
name: string;
type: string;
template: string;
status: TaskStatus;
date: string;
duration?: string;
}
const statusLabel: Record<TaskStatus, string> = {
completed: "已完成",
processing: "进行中",
pending: "排队中",
failed: "失败",
};
// TODO: recentTasks 当前使用硬编码 mock 数据,待后端提供最近任务 API 后替换
const recentTasks: RecentTask[] = [
{
id: "t-1",
name: "产品介绍视频_春季促销",
type: "视频生成",
template: "商品展示模板",
status: "completed",
date: "2026-07-01 09:30",
duration: "2分18秒",
},
{
id: "t-2",
name: "品牌宣传片_终版",
type: "视频生成",
template: "品牌宣传模板",
status: "processing",
date: "2026-07-01 10:15",
},
{
id: "t-3",
name: "用户评价合集",
type: "视频生成",
template: "评价展示模板",
status: "completed",
date: "2026-06-30 16:42",
duration: "1分45秒",
},
{
id: "t-4",
name: "新品发布预告",
type: "视频生成",
template: "新品预告模板",
status: "pending",
date: "2026-06-30 14:20",
},
{
id: "t-5",
name: "活动回顾_618大促",
type: "视频生成",
template: "活动回顾模板",
status: "failed",
date: "2026-06-29 11:05",
},
];
interface ChartItem {
label: string;
value: number;
}
const weeklyData: ChartItem[] = [
{ label: "周一", value: 18 },
{ label: "周二", value: 25 },
{ label: "周三", value: 32 },
{ label: "周四", value: 28 },
{ label: "周五", value: 42 },
{ label: "周六", value: 15 },
{ label: "周日", value: 8 },
];
interface Announcement {
id: string;
tag: "update" | "notice" | "activity";
tagLabel: string;
title: string;
date: string;
}
const announcements: Announcement[] = [
{
id: "a-1",
tag: "update",
tagLabel: "更新",
title: "系统已升级至 v2.0,新增批量生成功能",
date: "2026-07-01",
},
{
id: "a-2",
tag: "activity",
tagLabel: "活动",
title: "7月创作挑战赛已开启,参与赢积分奖励",
date: "2026-06-28",
},
{
id: "a-3",
tag: "notice",
tagLabel: "公告",
title: "7月3日凌晨 2:00-4:00 系统维护通知",
date: "2026-06-25",
},
];
/* ============================================================
* 工具函数
* ============================================================ */
const getGreeting = () => {
const hour = new Date().getHours();
if (hour < 6) return "夜深了";
if (hour < 12) return "早上好";
if (hour < 14) return "中午好";
if (hour < 18) return "下午好";
return "晚上好";
};
const formatDate = () => {
const d = new Date();
const weekDays = ["日", "一", "二", "三", "四", "五", "六"];
return `${d.getFullYear()}${d.getMonth() + 1}${d.getDate()}日 星期${weekDays[d.getDay()]}`;
};
/** 图标名称 → Ant Design 组件映射 */
const iconMap: Record<string, React.ReactNode> = {
video: <VideoCameraOutlined />,
appstore: <AppstoreOutlined />,
thunderbolt: <ThunderboltOutlined />,
database: <DatabaseOutlined />,
filetext: <FileTextOutlined />,
};
/* ============================================================
* 组件
* ============================================================ */
const Dashboard: React.FC = () => {
const navigate = useNavigate();
const maxChart = Math.max(...weeklyData.map((d) => d.value));
return (
<div className="xx-dashboard-page">
{/* ── 欢迎头部 ─────────────────────────────────────────── */}
<div className="xx-dashboard-welcome">
<h2>{getGreeting()}</h2>
<p>{formatDate()} </p>
</div>
{/* ── KPI 卡片网格 ─────────────────────────────────────── */}
{/* KPI 卡片网格 */}
<div className="xx-kpi-grid">
{kpiData.map((item) => (
<div
key={item.key}
className="xx-kpi-card"
style={{ "--kpi-accent": item.accent } as React.CSSProperties}
>
<div
className="xx-kpi-icon"
style={{ background: item.iconGradient }}
>
{iconMap[item.icon] ?? item.icon}
</div>
<div className="xx-kpi-value">{item.value}</div>
<div className="xx-kpi-label">{item.label}</div>
<span
className={`xx-kpi-trend xx-kpi-trend--${item.trendDirection}`}
>
{item.trend}
</span>
</div>
))}
</div>
{/* ── 主内容区:左侧任务+图表 / 右侧公告 ──────────────── */}
<div className="xx-dashboard-main">
{/* 左列 */}
<div className="xx-dashboard-left-col">
{/* 最近任务 */}
<div className="xx-dashboard-section">
<div className="xx-dashboard-section-header">
<h3></h3>
<button onClick={() => navigate("/app/history")}></button>
</div>
<div className="xx-task-list">
{recentTasks.map((task) => (
<div key={task.id} className="xx-task-item">
<div className="xx-task-info">
<h4>{task.name}</h4>
<span>
{task.type} · {task.template}
</span>
</div>
<Tag
variant={
task.status === "completed"
? "success"
: task.status === "processing"
? "info"
: task.status === "failed"
? "error"
: "warning"
}
>
{statusLabel[task.status]}
</Tag>
<div className="xx-task-time">
<span>{task.date}</span>
{task.status === "completed"
? `耗时 ${task.duration}`
: task.status === "processing"
? "生成中..."
: task.status === "failed"
? "请重试"
: "等待中"}
</div>
<div className="xx-task-action">
<Button
buttonType="ghost"
buttonSize="sm"
onClick={() => navigate("/app/history")}
>
</Button>
</div>
</div>
))}
</div>
</div>
{/* 使用统计图表 */}
<div className="xx-dashboard-section">
<div className="xx-dashboard-section-header">
<h3></h3>
<span className="xx-chart-total">
{weeklyData.reduce((s, d) => s + d.value, 0)}
</span>
</div>
<div className="xx-chart-container">
<div className="xx-chart-bars">
{weeklyData.map((d, i) => (
<div key={i} className="xx-chart-bar-wrapper">
<div
className="xx-chart-bar"
style={{
height: `${(d.value / maxChart) * 100}%`,
}}
>
<span className="xx-chart-bar-value">{d.value}</span>
</div>
</div>
))}
</div>
<div className="xx-chart-labels">
{weeklyData.map((d, i) => (
<div key={i} className="xx-chart-label">
{d.label}
</div>
))}
</div>
</div>
</div>
</div>
{/* 右列 — 公告 + 存储用量 */}
<div className="xx-dashboard-section xx-dashboard-section--start">
<div className="xx-dashboard-section-header">
<h3></h3>
</div>
<div className="xx-announcement-list">
{announcements.map((a) => (
<div key={a.id} className="xx-announcement-item">
<span
className={`xx-announcement-tag xx-announcement-tag--${a.tag}`}
>
{a.tagLabel}
</span>
<div className="xx-announcement-content">
<h4>{a.title}</h4>
<time>{a.date}</time>
</div>
</div>
))}
</div>
{/* 存储用量 */}
<div className="xx-storage-section">
<div className="xx-storage-section-title"></div>
<div className="xx-storage-bar">
<div className="xx-storage-bar-track">
<div className="xx-storage-bar-fill" style={{ width: "24%" }} />
</div>
<div className="xx-storage-bar-label">
<span>2.4 GB </span>
<span>10 GB </span>
</div>
</div>
</div>
<div className="xx-dashboard-empty">
<p></p>
</div>
</div>
{/* ── 快速入口 ─────────────────────────────────────────── */}
{/* 快速入口 */}
<div className="xx-quick-entry-section">
<div className="xx-quick-entry-header">
<h3 className="xx-quick-entry-title"></h3>
</div>
<div className="xx-quick-grid">
{quickEntries.map((entry) => (
<div
key={entry.id}
className="xx-quick-card"
onClick={() => navigate(entry.path)}
>
<div
className="xx-quick-card-icon"
style={{ background: entry.iconGradient }}
>
{iconMap[entry.icon] ?? entry.icon}
</div>
<h3>{entry.title}</h3>
<p>{entry.description}</p>
</div>
))}
<div className="xx-dashboard-empty">
<p></p>
</div>
</div>
</div>
{/* 最近任务 */}
<section className="xx-dashboard-section">
<div className="xx-dashboard-section-header">
<h3></h3>
<Button buttonType="ghost" buttonSize="sm" onClick={() => navigate("/app/history")}>
</Button>
</div>
<div className="xx-task-list">
<div className="xx-dashboard-empty">
<p></p>
</div>
</div>
</section>
{/* 使用统计 */}
<section className="xx-dashboard-section" style={{ marginTop: "var(--space-md)" }}>
<div className="xx-dashboard-section-header">
<h3>使</h3>
</div>
<div className="xx-chart-container">
<div className="xx-chart-bars">
<div className="xx-dashboard-empty" style={{ width: "100%" }}>
<DatabaseOutlined style={{ fontSize: 24, marginBottom: 8 }} />
<p></p>
</div>
</div>
</div>
</section>
{/* 公告 */}
<section className="xx-dashboard-section" style={{ marginTop: "var(--space-md)" }}>
<div className="xx-dashboard-section-header">
<h3></h3>
</div>
<div className="xx-announcement-list">
<div className="xx-announcement-item">
<span className="xx-announcement-tag xx-announcement-tag--notice"></span>
<div className="xx-announcement-content">
<h4>使 SaaS </h4>
<time></time>
</div>
</div>
</div>
</section>
</div>
);
};
+12 -24
View File
@@ -69,22 +69,6 @@ const VOICE_GENDER_ICON: Record<string, string> = {
neutral: "✨",
};
/* ── 时间线 Mock ──
* TODO: 后端暂无时间线场景数据 API,当前使用硬编码预览数据。
* 待后端提供 timeline/scene 接口后替换为真实 API 调用。
*/
interface TimelineScene {
scene: string;
time: string;
duration: number;
}
const MOCK_TIMELINE: TimelineScene[] = [
{ scene: "主讲口播 · 开场钩子", time: "0-8s", duration: 8 },
{ scene: "产品特写 · B-roll", time: "8-20s", duration: 12 },
{ scene: "用户反馈 · 结尾", time: "20-30s", duration: 10 },
];
/* ── 步骤定义 ── */
const STEPS = [
{ key: 1, label: "选择模板" },
@@ -1758,15 +1742,19 @@ const GeneratePage: React.FC = () => {
<div className="xx-preview-title"></div>
{/* 时间线列表 */}
<div className="xx-preview-timeline">
{MOCK_TIMELINE.map((item, idx) => (
<div key={idx} className="xx-timeline-item">
<div className="num">{idx + 1}</div>
<span className="scene-name">{item.scene}</span>
<span>{item.time}</span>
{generated ? (
<div className="xx-preview-timeline">
<div className="xx-timeline-item">
<span className="scene-name"></span>
</div>
))}
</div>
</div>
) : (
<div className="xx-preview-timeline">
<div className="xx-timeline-item">
<span className="scene-name"></span>
</div>
</div>
)}
{/* 生成操作按钮 */}
<div className="xx-generate-actions">
+27 -114
View File
@@ -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<CategoryItem[]>(MOCK_CATEGORIES);
const [activeCatId, setActiveCatId] = useState<string>(MOCK_CATEGORIES[0].id);
/* 分类数据 — 从真实标题数据动态派生 */
const [activeCatId, setActiveCatId] = useState<string>("cat-all");
/* 标题数据 — 真实 API */
const { data: apiTitles = [] } = useQuery({
@@ -261,6 +248,23 @@ const TitleLibrary: React.FC = () => {
[apiTitles],
);
/* 从真实标题数据动态派生分类(无需后端分类 API) */
const categories = useMemo(() => {
const cats = new Map<string, number>();
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<string | null>(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<string, Industry> = {
: "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 = () => {
</h4>
<span>{cat.count} </span>
</div>
{cat.id !== "cat-all" && (
<Popconfirm
title={`确定删除分类 "${cat.name}"`}
onConfirm={(e) => {
e?.stopPropagation();
handleDeleteCategory(cat.id);
}}
onCancel={(e) => e?.stopPropagation()}
okText="删除"
cancelText="取消"
>
<button
className="xx-title-category-delete"
onClick={(e) => e.stopPropagation()}
title="删除分类"
>
<DeleteOutlined />
</button>
</Popconfirm>
)}
</div>
</div>
))}
{/* 新建分类 */}
<div
className="xx-title-category-add"
onClick={() => setCreateCatModalOpen(true)}
>
<PlusOutlined />
</div>
{/* TODO: 新建分类功能待后端分类 API 就绪后启用 */}
</div>
{/* ─── 右侧:内容区 ─── */}
@@ -674,34 +614,7 @@ const TitleLibrary: React.FC = () => {
</div>
</div>
{/* ─── 新建分类弹窗 ─── */}
<AntModal
title="新建分类"
open={createCatModalOpen}
onCancel={() => setCreateCatModalOpen(false)}
onOk={handleCreateCategory}
okText="创建"
cancelText="取消"
destroyOnClose
>
<div style={{ padding: "8px 0" }}>
<div
style={{
marginBottom: 6,
fontSize: "var(--font-size-sm)",
color: "var(--text-secondary)",
}}
>
</div>
<Input
placeholder="请输入分类名称"
value={newCatName}
onChange={(e) => setNewCatName(e.target.value)}
maxLength={30}
/>
</div>
</AntModal>
{/* ─── 新建标题弹窗 ─── */}
<AntModal