diff --git a/apps/web/src/components/business/business.css b/apps/web/src/components/business/business.css index 43ae2d92b..8c3239583 100644 --- a/apps/web/src/components/business/business.css +++ b/apps/web/src/components/business/business.css @@ -2,52 +2,52 @@ /* ==================== 按钮 ==================== */ .xx-primary-btn { - background: linear-gradient(135deg, #6366f1, #4f46e5) !important; - color: white !important; + background: var(--gradient-primary) !important; + color: var(--text-inverse) !important; border: none !important; - border-radius: 14px !important; + border-radius: var(--radius-md) !important; padding: 10px 20px !important; - font-weight: 600 !important; - box-shadow: 0 14px 26px rgba(79, 70, 229, 0.22) !important; - transition: all 0.2s !important; + font-weight: var(--font-weight-bold) !important; + box-shadow: var(--shadow-primary) !important; + transition: var(--transition-all) !important; cursor: pointer; height: auto !important; } .xx-primary-btn:hover { - box-shadow: 0 18px 34px rgba(79, 70, 229, 0.28) !important; + box-shadow: var(--shadow-hover) !important; transform: translateY(-1px); } .xx-ghost-btn { background: transparent !important; - color: #4f46e5 !important; - border: 2px solid #4f46e5 !important; - border-radius: 14px !important; - padding: 8px 18px !important; - font-weight: 600 !important; - transition: all 0.2s !important; + color: var(--primary-color) !important; + border: 2px solid var(--primary-color) !important; + border-radius: var(--radius-md) !important; + padding: var(--space-sm) 18px !important; + font-weight: var(--font-weight-bold) !important; + transition: var(--transition-all) !important; cursor: pointer; height: auto !important; } .xx-ghost-btn:hover { - background: #eef2ff !important; + background: var(--primary-soft) !important; } /* ==================== 卡片 ==================== */ .xx-card { - background: rgba(255, 255, 255, 0.94); - border: 1px solid rgba(226, 232, 240, 0.95); - border-radius: 22px; - box-shadow: 0 24px 70px rgba(15, 23, 42, 0.09); - padding: 24px; + background: var(--bg-elevated); + border: 1px solid var(--border-color); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-card); + padding: var(--space-lg); margin-bottom: 20px; - transition: all 0.25s; + transition: all var(--transition-slow); } .xx-card:hover { - box-shadow: 0 26px 64px rgba(15, 23, 42, 0.14); + box-shadow: var(--shadow-md); transform: translateY(-2px); } @@ -55,7 +55,7 @@ .xx-page { max-width: 1200px; margin: 0 auto; - padding: 24px; + padding: var(--space-lg); } .xx-page-head { @@ -68,63 +68,63 @@ .xx-page-head h2 { font-size: 26px; - font-weight: 850; - color: #0f172a; - margin: 0 0 8px; + font-weight: var(--font-weight-extrabold); + color: var(--text-primary); + margin: 0 0 var(--space-sm); } .xx-page-head p { - font-size: 14px; - color: #64748b; + font-size: var(--font-size-base); + color: var(--text-secondary); margin: 0; } /* ==================== 表格样式 ==================== */ .xx-table-card { - background: rgba(255, 255, 255, 0.94); - border: 1px solid rgba(226, 232, 240, 0.95); - border-radius: 22px; - box-shadow: 0 24px 70px rgba(15, 23, 42, 0.09); + background: var(--bg-elevated); + border: 1px solid var(--border-color); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-card); padding: 20px; overflow: hidden; } /* 表格包装器 */ .xx-table-wrapper { - border-radius: 16px; + border-radius: var(--radius-lg); overflow: hidden; } /* ==================== 标签/Tag ==================== */ .xx-tag { - padding: 4px 12px; - border-radius: 10px; + padding: var(--space-xs) 12px; + border-radius: var(--radius-xs); font-size: 13px; - font-weight: 500; + font-weight: var(--font-weight-medium); } .xx-tag-indigo { - background: #eef2ff; - color: #4f46e5; - border: 1px solid #c7d2fe; + background: var(--primary-soft); + color: var(--primary-color); + border: 1px solid var(--color-primary-200); } .xx-tag-success { - background: #f0fdf4; - color: #16a34a; - border: 1px solid #bbf7d0; + background: var(--success-soft); + color: var(--color-secondary-500); + border: 1px solid var(--success-border); } .xx-tag-warning { - background: #fffbeb; - color: #d97706; - border: 1px solid #fde68a; + background: var(--warning-soft); + color: var(--accent-dark); + border: 1px solid var(--color-accent-200); } .xx-tag-error { - background: #fef2f2; - color: #dc2626; - border: 1px solid #fecaca; + background: var(--error-soft); + color: var(--error-color); + border: 1px solid var(--error-border); } /* ==================== 搜索栏 ==================== */ @@ -135,52 +135,52 @@ .xx-search-input { width: 100%; padding: 12px 18px; - border: 2px solid #e2e8f0; - border-radius: 14px; - font-size: 14px; - background: white; - transition: all 0.2s; + border: 2px solid var(--border-color); + border-radius: var(--radius-md); + font-size: var(--font-size-base); + background: var(--bg-primary); + transition: var(--transition-all); outline: none; } .xx-search-input:focus { - border-color: #4f46e5; - box-shadow: 0 0 0 4px rgba(79, 70, 229, 0.1); + border-color: var(--primary-color); + box-shadow: 0 0 0 4px color-mix(in srgb, var(--primary-color) 10%, transparent); } /* ==================== Modal ==================== */ .xx-modal .ant-modal-content { - border-radius: 22px; - padding: 24px; + border-radius: var(--radius-xl); + padding: var(--space-lg); } .xx-modal .ant-modal-header { - border-radius: 22px 22px 0 0; - padding: 20px 24px; - border-bottom: 1px solid #e2e8f0; + border-radius: var(--radius-xl) var(--radius-xl) 0 0; + padding: 20px var(--space-lg); + border-bottom: 1px solid var(--border-color); } .xx-modal .ant-modal-title { - font-size: 18px; - font-weight: 700; - color: #0f172a; + font-size: var(--font-size-lg); + font-weight: var(--font-weight-bold); + color: var(--text-primary); } .xx-modal .ant-modal-footer { - border-top: 1px solid #e2e8f0; - padding: 16px 24px; + border-top: 1px solid var(--border-color); + padding: var(--space-md) var(--space-lg); } /* ==================== 空状态 ==================== */ .xx-empty-state { text-align: center; - padding: 48px 24px; - color: #64748b; + padding: var(--space-3xl) var(--space-lg); + color: var(--text-secondary); } .xx-empty-state-icon { font-size: 48px; - margin-bottom: 16px; + margin-bottom: var(--space-md); } /* ==================== 网格布局 ==================== */ @@ -213,15 +213,15 @@ /* ==================== 配额展示 ==================== */ .xx-quota-item { padding: 20px; - background: white; - border: 1px solid #e2e8f0; - border-radius: 16px; - transition: all 0.2s; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + transition: var(--transition-all); } .xx-quota-item:hover { - border-color: #4f46e5; - box-shadow: 0 8px 24px rgba(79, 70, 229, 0.1); + border-color: var(--primary-color); + box-shadow: 0 8px 24px color-mix(in srgb, var(--primary-color) 10%, transparent); } /* ==================== 进度条 ==================== */ @@ -232,130 +232,130 @@ /* ==================== Ant Design 覆盖样式 ==================== */ /* Table overrides */ .ant-table-wrapper .ant-table-thead > tr > th { - background: #f8fafc !important; - font-weight: 700 !important; - color: #0f172a !important; - border-bottom: 2px solid #e2e8f0 !important; - padding: 14px 16px !important; + background: var(--bg-secondary) !important; + font-weight: var(--font-weight-bold) !important; + color: var(--text-primary) !important; + border-bottom: 2px solid var(--border-color) !important; + padding: 14px var(--space-md) !important; } .ant-table-wrapper .ant-table-tbody > tr > td { - padding: 14px 16px !important; - border-bottom: 1px solid #f1f5f9 !important; + padding: 14px var(--space-md) !important; + border-bottom: 1px solid var(--color-gray-100) !important; } .ant-table-wrapper .ant-table-tbody > tr:hover > td { - background: #fafbfc !important; + background: var(--color-gray-50) !important; } /* Card overrides */ .ant-card { - border-radius: 22px !important; - border: 1px solid rgba(226, 232, 240, 0.95) !important; + border-radius: var(--radius-xl) !important; + border: 1px solid var(--border-color) !important; } .ant-card-head { - border-bottom: 1px solid #e2e8f0 !important; + border-bottom: 1px solid var(--border-color) !important; min-height: 52px !important; - padding: 0 24px !important; + padding: 0 var(--space-lg) !important; } .ant-card-head-title { - font-weight: 700 !important; - font-size: 16px !important; - color: #0f172a !important; + font-weight: var(--font-weight-bold) !important; + font-size: var(--font-size-md) !important; + color: var(--text-primary) !important; } .ant-card-body { - padding: 20px 24px !important; + padding: 20px var(--space-lg) !important; } /* Modal overrides */ .ant-modal-content { - border-radius: 22px !important; + border-radius: var(--radius-xl) !important; overflow: hidden; } .ant-modal-header { - padding: 20px 24px !important; - background: white !important; + padding: 20px var(--space-lg) !important; + background: var(--bg-primary) !important; } .ant-modal-title { - font-weight: 700 !important; - font-size: 18px !important; - color: #0f172a !important; + font-weight: var(--font-weight-bold) !important; + font-size: var(--font-size-lg) !important; + color: var(--text-primary) !important; } .ant-modal-body { - padding: 24px !important; + padding: var(--space-lg) !important; } .ant-modal-footer { - padding: 16px 24px !important; + padding: var(--space-md) var(--space-lg) !important; } /* Button overrides */ .ant-btn-primary { - background: linear-gradient(135deg, #6366f1, #4f46e5) !important; + background: var(--gradient-primary) !important; border: none !important; - border-radius: 14px !important; - box-shadow: 0 14px 26px rgba(79, 70, 229, 0.22) !important; + border-radius: var(--radius-md) !important; + box-shadow: var(--shadow-primary) !important; height: auto !important; padding: 10px 20px !important; - font-weight: 600 !important; + font-weight: var(--font-weight-bold) !important; } .ant-btn-primary:hover { - background: linear-gradient(135deg, #6366f1, #4f46e5) !important; - box-shadow: 0 18px 34px rgba(79, 70, 229, 0.28) !important; + background: var(--gradient-primary) !important; + box-shadow: var(--shadow-hover) !important; transform: translateY(-1px); } /* Tag overrides */ .ant-tag { - border-radius: 10px !important; - padding: 4px 12px !important; - font-weight: 500 !important; + border-radius: var(--radius-xs) !important; + padding: var(--space-xs) 12px !important; + font-weight: var(--font-weight-medium) !important; } /* Select overrides */ .ant-select-selector { - border-radius: 14px !important; - border-color: #e2e8f0 !important; + border-radius: var(--radius-md) !important; + border-color: var(--border-color) !important; } .ant-select:not(.ant-select-disabled):hover .ant-select-selector { - border-color: #4f46e5 !important; + border-color: var(--primary-color) !important; } .ant-select-focused .ant-select-selector { - border-color: #4f46e5 !important; - box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1) !important; + border-color: var(--primary-color) !important; + box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 10%, transparent) !important; } /* Input overrides */ .ant-input { - border-radius: 14px !important; - border-color: #e2e8f0 !important; + border-radius: var(--radius-md) !important; + border-color: var(--border-color) !important; padding: 10px 14px !important; } .ant-input:hover { - border-color: #4f46e5 !important; + border-color: var(--primary-color) !important; } .ant-input:focus { - border-color: #4f46e5 !important; - box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1) !important; + border-color: var(--primary-color) !important; + box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 10%, transparent) !important; } /* Progress overrides */ .ant-progress-inner { - background: #f1f5f9 !important; - border-radius: 10px !important; + background: var(--color-gray-100) !important; + border-radius: var(--radius-xs) !important; } .ant-progress-bg { - border-radius: 10px !important; + border-radius: var(--radius-xs) !important; } diff --git a/apps/web/src/components/layout/Header.css b/apps/web/src/components/layout/Header.css index f6dbaed88..e74730978 100644 --- a/apps/web/src/components/layout/Header.css +++ b/apps/web/src/components/layout/Header.css @@ -3,17 +3,17 @@ height: 68px; position: sticky; top: 0; - z-index: 30; - background: rgba(255, 255, 255, 0.86); + z-index: var(--z-fixed); + background: var(--bg-glass); backdrop-filter: blur(16px); - border-bottom: 1px solid rgba(226, 232, 240, 0.92); + border-bottom: 1px solid var(--border-color); } .xx-top-nav-inner { max-width: 1440px; margin: 0 auto; height: 100%; - padding: 0 24px; + padding: 0 var(--space-lg); display: flex; align-items: center; justify-content: space-between; @@ -23,12 +23,12 @@ display: flex; gap: 10px; align-items: center; - font-size: 20px; - font-weight: 900; + font-size: var(--font-size-xl); + font-weight: var(--font-weight-extrabold); border: none; background: none; cursor: pointer; - color: #0f172a; + color: var(--text-primary); padding: 0; flex-shrink: 0; } @@ -36,41 +36,41 @@ .xx-logo { width: 38px; height: 38px; - border-radius: 14px; - background: linear-gradient(135deg, #818cf8, #4f46e5); + border-radius: var(--radius-md); + background: var(--gradient-primary); display: grid; place-items: center; - color: #fff; - box-shadow: 0 12px 26px rgba(79, 70, 229, 0.24); - font-size: 20px; + color: var(--text-inverse); + box-shadow: var(--shadow-primary); + font-size: var(--font-size-xl); } /* 桌面端导航链接 */ .xx-nav-links { display: flex; - gap: 16px; - color: #64748b; - font-weight: 750; + gap: var(--space-md); + color: var(--text-secondary); + font-weight: var(--font-weight-bold); } .xx-nav-links button { border: none; background: none; cursor: pointer; - color: #64748b; + color: var(--text-secondary); padding: 6px 4px; - font-size: 14px; - font-weight: 750; - transition: color 0.2s; + font-size: var(--font-size-base); + font-weight: var(--font-weight-bold); + transition: color var(--transition-fast); white-space: nowrap; } .xx-nav-links button:hover { - color: #4f46e5; + color: var(--primary-color); } .xx-nav-links button.active { - color: #4f46e5; + color: var(--primary-color); } /* 右侧区域 */ @@ -84,17 +84,17 @@ .xx-user-menu { display: flex; align-items: center; - gap: 8px; + gap: var(--space-sm); cursor: pointer; - color: #0f172a; - font-size: 14px; + color: var(--text-primary); + font-size: var(--font-size-base); } .xx-avatar { width: 34px; height: 34px; - background: #eef2ff; - color: #4338ca; + background: var(--primary-soft); + color: var(--primary-dark); } /* 汉堡菜单按钮(默认隐藏) */ @@ -103,42 +103,42 @@ border: none; background: none; cursor: pointer; - font-size: 20px; - color: #64748b; - padding: 8px; + font-size: var(--font-size-xl); + color: var(--text-secondary); + padding: var(--space-sm); } /* 手机端导航抽屉内容 */ .xx-mobile-nav { display: flex; flex-direction: column; - gap: 4px; + gap: var(--space-xs); } .xx-mobile-nav-item { display: flex; align-items: center; gap: 12px; - padding: 12px 16px; + padding: 12px var(--space-md); border: none; background: none; cursor: pointer; - font-size: 16px; - color: #334155; - border-radius: 8px; - transition: background 0.2s; + font-size: var(--font-size-md); + color: var(--color-gray-700); + border-radius: var(--radius-sm); + transition: background var(--transition-fast); width: 100%; text-align: left; } .xx-mobile-nav-item:hover { - background: #f1f5f9; + background: var(--bg-tertiary); } .xx-mobile-nav-item.active { - background: #eef2ff; - color: #4f46e5; - font-weight: 600; + background: var(--primary-soft); + color: var(--primary-color); + font-weight: var(--font-weight-semibold); } .xx-mobile-nav-icon { @@ -182,7 +182,7 @@ /* 小屏幕平板:导航文字缩小 */ @media (min-width: 769px) and (max-width: 1024px) { .xx-nav-links { - gap: 8px; + gap: var(--space-sm); } .xx-nav-links button { diff --git a/apps/web/src/pages/assets/AssetLibrary.tsx b/apps/web/src/pages/assets/AssetLibrary.tsx index 3c242fcc5..ed9104287 100644 --- a/apps/web/src/pages/assets/AssetLibrary.tsx +++ b/apps/web/src/pages/assets/AssetLibrary.tsx @@ -24,6 +24,7 @@ import { } from "@ant-design/icons"; import { Button, Input, Select } from "@/components/ui"; import "./assets.css"; +import PageHead from "@/components/layout/PageHead"; /* ============================================================ * 类型 diff --git a/apps/web/src/pages/duplication/DuplicationDetail.tsx b/apps/web/src/pages/duplication/DuplicationDetail.tsx index e179f27e6..656f7cc8b 100644 --- a/apps/web/src/pages/duplication/DuplicationDetail.tsx +++ b/apps/web/src/pages/duplication/DuplicationDetail.tsx @@ -13,6 +13,7 @@ import { type DuplicateSegment, } from "@/api/duplication"; import "./duplication.css"; +import PageHead from "@/components/layout/PageHead"; /** 格式化时间(秒 → mm:ss) */ const formatTime = (seconds: number) => { @@ -191,51 +192,41 @@ const DuplicationDetail: React.FC = () => { )} - {/* 返回按钮 */} - - - {/* 页头 */} -
-
-

🎬 {detail.filename}

-
+ {RISK_LABEL[riskLevel]} {formatSize(detail.file_size)} {formatDuration(detail.duration_seconds)} - - 提交于 {new Date(detail.created_at).toLocaleString("zh-CN")} - + 提交于 {new Date(detail.created_at).toLocaleString("zh-CN")}
-
-
- - {detail.status === "failed" && ( + } + actions={ +
- )} -
-
+ {detail.status === "failed" && ( + + )} +
+ } + /> {/* 内容网格 */}
diff --git a/apps/web/src/pages/duplication/DuplicationResults.tsx b/apps/web/src/pages/duplication/DuplicationResults.tsx index e31d072de..daf252a06 100644 --- a/apps/web/src/pages/duplication/DuplicationResults.tsx +++ b/apps/web/src/pages/duplication/DuplicationResults.tsx @@ -13,6 +13,7 @@ import { type DuplicationStatus, } from "@/api/duplication"; import "./duplication.css"; +import PageHead from "@/components/layout/PageHead"; /** 风险等级分类 */ type RiskFilter = "all" | "high" | "medium" | "low"; @@ -131,10 +132,10 @@ const DuplicationResults: React.FC = () => {
)} - {/* 页头 */} -
-

📋 查重记录

-
+ {/* 筛选胶囊 */}
{FILTER_OPTIONS.map((opt) => ( @@ -154,8 +155,9 @@ const DuplicationResults: React.FC = () => { > 📤 上传查重 -
-
+
+ } + /> {/* 加载中 */} {isLoading && ( diff --git a/apps/web/src/pages/duplication/DuplicationUpload.tsx b/apps/web/src/pages/duplication/DuplicationUpload.tsx index a6f3ccd0c..847158067 100644 --- a/apps/web/src/pages/duplication/DuplicationUpload.tsx +++ b/apps/web/src/pages/duplication/DuplicationUpload.tsx @@ -9,6 +9,7 @@ import { Button, Card, Tag } from "@/components/ui"; import { uploadForDuplication } from "@/api/duplication"; import { useNavigate } from "react-router-dom"; import "./duplication.css"; +import PageHead from "@/components/layout/PageHead"; /** 支持的视频格式 */ const ACCEPT_FORMATS = ".mp4,.avi,.mov,.mkv,.wmv,.flv,.webm"; @@ -123,12 +124,7 @@ const DuplicationUpload: React.FC = () => { )} -
-

📹 视频查重

-

- 上传视频文件,系统将自动检测与已有素材的重复片段,帮助您避免重复内容。 -

-
+
{/* 左侧:上传区域 */} diff --git a/apps/web/src/pages/profile/Settings.tsx b/apps/web/src/pages/profile/Settings.tsx index e0d27f637..3cfcdc275 100644 --- a/apps/web/src/pages/profile/Settings.tsx +++ b/apps/web/src/pages/profile/Settings.tsx @@ -1,61 +1,83 @@ /** * 个人设置页面 + * P1-2: 添加 PageHead + * P1-3: antd Form/Input/Button/Alert → 自定义 UI 组件 */ -import React from "react"; -import { Form, Input, Button, Alert } from "antd"; +import React, { useState } from "react"; +import { Button, Input, Modal } from "@/components/ui"; import { useAuthStore } from "@/store/authStore"; +import PageHead from "@/components/layout/PageHead"; import "./ProfileSettings.css"; const Settings: React.FC = () => { const user = useAuthStore((state) => state.user); - const [form] = Form.useForm(); + const [displayName, setDisplayName] = useState(user?.display_name || ""); + const [saving, setSaving] = useState(false); - const onFinish = () => undefined; + const handleSave = () => { + Modal.info({ + title: "提示", + content: "个人资料修改接口暂未开放,保存功能即将上线。", + }); + }; return (
+ +

个人信息

- -
- - - +
+ ℹ️ +
+ 个人资料编辑暂未开放 +

当前仅展示登录用户信息,资料修改接口接入后再开放保存。

+
+
- - - +
+
+ + +
- - - +
+ + +
- - - - +
+
); }; export default Settings; - export const Component = Settings; diff --git a/apps/web/src/pages/subscription/Billing.css b/apps/web/src/pages/subscription/Billing.css index 6d62e06bd..e478c1d83 100644 --- a/apps/web/src/pages/subscription/Billing.css +++ b/apps/web/src/pages/subscription/Billing.css @@ -1,128 +1,198 @@ -/* 账单管理页面 */ +/** + * 账单管理页面样式 + * P1-3: 全部使用 CSS 变量 + 新增 ToggleSwitch/Spinner 样式 + */ + .xx-billing-page { - max-width: 960px; + max-width: 800px; margin: 0 auto; padding: 40px 24px; } /* 订阅概览 */ .xx-billing-overview { - background: rgba(255, 255, 255, 0.94); - border: 1px solid rgba(226, 232, 240, 0.95); - border-radius: 20px; + background: var(--bg-elevated); + border: 1px solid var(--border-color); + border-radius: var(--radius-xl); padding: 32px; - margin-bottom: 40px; - box-shadow: 0 8px 30px rgba(15, 23, 42, 0.06); + margin-bottom: 24px; + box-shadow: var(--shadow-sm); } .xx-billing-overview h2 { - font-size: 24px; - font-weight: 800; - color: var(--slate, #0f172a); - margin: 0 0 24px; + font-size: 20px; + font-weight: 700; + color: var(--text-primary); + margin: 0 0 20px; } .xx-overview-details { display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 20px; - margin-bottom: 24px; + grid-template-columns: repeat(3, 1fr); + gap: 24px; } -@media (max-width: 768px) { +@media (max-width: 640px) { .xx-overview-details { - grid-template-columns: repeat(2, 1fr); + grid-template-columns: 1fr; } } .xx-overview-item { display: flex; flex-direction: column; - gap: 6px; + gap: 4px; } .xx-label { font-size: 13px; - color: var(--muted, #64748b); + color: var(--text-secondary); font-weight: 500; } .xx-value { font-size: 16px; - font-weight: 700; - color: var(--slate, #0f172a); + color: var(--text-primary); + font-weight: 600; } -/* 账单记录 */ -.xx-billing-history { - background: rgba(255, 255, 255, 0.94); - border: 1px solid rgba(226, 232, 240, 0.95); - border-radius: 20px; +/* 自动续费 */ +.xx-billing-auto-renew { + background: var(--bg-elevated); + border: 1px solid var(--border-color); + border-radius: var(--radius-xl); padding: 32px; - box-shadow: 0 8px 30px rgba(15, 23, 42, 0.06); + box-shadow: var(--shadow-sm); } -.xx-billing-history h2 { - font-size: 24px; - font-weight: 800; - color: var(--slate, #0f172a); - margin: 0 0 24px; -} - -/* 表格样式 */ -.xx-billing-table { - width: 100%; -} - -.xx-table-header { - display: grid; - grid-template-columns: 1.2fr 1fr 0.8fr 1fr 0.8fr 1fr; - gap: 12px; - padding: 12px 16px; - background: rgba(241, 245, 249, 0.8); - border-radius: 10px; - margin-bottom: 8px; - font-size: 13px; +.xx-billing-auto-renew h2 { + font-size: 20px; font-weight: 700; - color: var(--muted, #64748b); + color: var(--text-primary); + margin: 0 0 20px; } -.xx-table-row { - display: grid; - grid-template-columns: 1.2fr 1fr 0.8fr 1fr 0.8fr 1fr; - gap: 12px; - padding: 14px 16px; +.xx-auto-renew-row { + display: flex; align-items: center; - border-bottom: 1px solid rgba(226, 232, 240, 0.6); - font-size: 14px; - color: var(--slate, #0f172a); - transition: background 0.15s; + justify-content: space-between; + gap: 24px; } -.xx-table-row:hover { - background: rgba(241, 245, 249, 0.4); +.xx-auto-renew-info { + flex: 1; } -.xx-table-row:last-child { - border-bottom: none; +.xx-auto-renew-title { + font-size: 15px; + font-weight: 600; + color: var(--text-primary); + margin: 0 0 4px; } -.xx-amount { - font-weight: 700; - color: var(--indigo, #4f46e5); +.xx-auto-renew-desc { + font-size: 13px; + color: var(--text-secondary); + margin: 0; + line-height: 1.5; } -@media (max-width: 768px) { - .xx-table-header, - .xx-table-row { - grid-template-columns: 1fr 1fr 1fr; - font-size: 12px; - } - - .xx-table-header span:nth-child(4), - .xx-table-header span:nth-child(6), - .xx-table-row span:nth-child(4), - .xx-table-row span:nth-child(6) { - display: none; - } +/* ── ToggleSwitch 组件 ───────────────────────────────── */ +.xx-toggle-switch { + position: relative; + display: inline-flex; + align-items: center; + width: 44px; + height: 24px; + padding: 0; + border: none; + border-radius: 12px; + background: var(--color-gray-300); + cursor: pointer; + transition: background 0.2s ease; + flex-shrink: 0; +} + +.xx-toggle-switch.checked { + background: var(--primary-color); +} + +.xx-toggle-switch.loading { + opacity: 0.65; + cursor: not-allowed; +} + +.xx-toggle-switch-handle { + position: absolute; + top: 2px; + left: 2px; + width: 20px; + height: 20px; + border-radius: 50%; + background: var(--bg-primary); + box-shadow: var(--shadow-xs); + transition: transform 0.2s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.xx-toggle-switch.checked .xx-toggle-switch-handle { + transform: translateX(20px); +} + +.xx-toggle-switch-inner { + position: absolute; + width: 100%; + text-align: center; + font-size: 0; + /* 文字不显示,仅保留 aria 语义 */ +} + +.xx-toggle-switch-spinner { + width: 12px; + height: 12px; + border: 2px solid var(--primary-color); + border-top-color: transparent; + border-radius: 50%; + animation: xx-toggle-spin 0.6s linear infinite; +} + +@keyframes xx-toggle-spin { + to { transform: rotate(360deg); } +} + +/* ── Spinner 组件 ───────────────────────────────── */ +.xx-spinner { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 40px 0; +} + +.xx-spinner-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--primary-color); + animation: xx-spinner-bounce 1.2s ease-in-out infinite; +} + +.xx-spinner-dot:nth-child(2) { + animation-delay: 0.15s; +} + +.xx-spinner-dot:nth-child(3) { + animation-delay: 0.3s; +} + +.xx-spinner--small .xx-spinner-dot { + width: 6px; + height: 6px; +} + +@keyframes xx-spinner-bounce { + 0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; } + 40% { transform: scale(1); opacity: 1; } } diff --git a/apps/web/src/pages/subscription/Billing.tsx b/apps/web/src/pages/subscription/Billing.tsx index 22a769b48..68bfb7b50 100644 --- a/apps/web/src/pages/subscription/Billing.tsx +++ b/apps/web/src/pages/subscription/Billing.tsx @@ -1,11 +1,13 @@ /** * 账单管理页面 * 展示当前订阅信息 + 自动续费开关 + * P1-3: antd Switch→自定义ToggleSwitch, antd Spin→自定义Spinner */ import React, { useState, useEffect } from "react"; -import { Switch, message, Spin } from "antd"; +import { message } from "antd"; import { getCurrentSubscription, toggleAutoRenew } from "@/api/subscription"; import type { SubscriptionInfo } from "@/api/subscription"; +import PageHead from "@/components/layout/PageHead"; import "./Billing.css"; const formatDate = (iso: string): string => { @@ -17,6 +19,40 @@ const formatDate = (iso: string): string => { }); }; +/** 自定义 ToggleSwitch 组件 */ +const ToggleSwitch: React.FC<{ + checked: boolean; + onChange: (checked: boolean) => void; + loading?: boolean; + checkedChildren?: string; + unCheckedChildren?: string; +}> = ({ checked, onChange, loading, checkedChildren, unCheckedChildren }) => ( + +); + +/** 自定义 Spinner 组件 */ +const Spinner: React.FC<{ size?: "small" | "large" }> = ({ size = "large" }) => ( +
+
+
+
+
+); + const Billing: React.FC = () => { const [subscription, setSubscription] = useState( null, @@ -62,13 +98,15 @@ const Billing: React.FC = () => { if (loading) { return (
- +
); } return (
+ + {subscription && ( <> {/* 当前订阅概览 */} @@ -104,7 +142,7 @@ const Billing: React.FC = () => { 开启后,将在每个计费周期结束时自动扣费续期,避免服务中断。

- void; + monthlyPrice: number; + yearlyPrice: number; +}> = ({ value, onChange, monthlyPrice, yearlyPrice }) => ( +
+ + +
+); + +/** 自定义 Spinner 组件 */ +const Spinner: React.FC<{ size?: "small" | "large" }> = ({ size = "large" }) => ( +
+
+
+
+
+); + const UpgradeSubscription: React.FC = () => { const navigate = useNavigate(); const [subscription, setSubscription] = useState( @@ -113,7 +155,6 @@ const UpgradeSubscription: React.FC = () => { title: "确认取消订阅", content: "取消后,当前周期结束前仍可正常使用,到期后降级为体验版。", okText: "确认取消", - okType: "danger", cancelText: "再想想", onOk: async () => { try { @@ -131,7 +172,7 @@ const UpgradeSubscription: React.FC = () => { if (loading) { return (
- +
); } @@ -140,10 +181,7 @@ const UpgradeSubscription: React.FC = () => { return (
-
-

变更订阅方案

-

当前套餐:{PLANS_META[currentPlan]?.name ?? "体验版"}

-
+
{(["standard", "pro", "enterprise"] as PlanType[]).map((planId) => { @@ -158,21 +196,12 @@ const UpgradeSubscription: React.FC = () => { {isCurrent &&
当前
}

{plan.name}

- setBillingCycle(e.target.value)} - size="small" - > - ¥{plan.price}/月 - - ¥{plan.yearlyPrice}/年 - {plan.yearlyPrice > 0 && plan.price > 0 && ( - - 省 ¥{plan.price * 12 - plan.yearlyPrice} - - )} - - + onChange={setBillingCycle} + monthlyPrice={plan.price} + yearlyPrice={plan.yearlyPrice} + />
); @@ -181,13 +210,12 @@ const UpgradeSubscription: React.FC = () => {
{subscription && subscription.status === "active" && ( @@ -196,7 +224,8 @@ const UpgradeSubscription: React.FC = () => { 自动续费:{subscription.auto_renew ? "已开启" : "已关闭"}