fix: P1审计修复 - CSS变量替换、PageHead补充、antd组件替换 (14个文件) #138
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
/* ============================================================
|
||||
* 类型
|
||||
|
||||
@@ -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 = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 返回按钮 */}
|
||||
<button
|
||||
className="dup-detail-back"
|
||||
onClick={() => navigate("/duplication/results")}
|
||||
>
|
||||
← 返回查重记录
|
||||
</button>
|
||||
|
||||
{/* 页头 */}
|
||||
<div className="dup-detail-header">
|
||||
<div>
|
||||
<h2>🎬 {detail.filename}</h2>
|
||||
<div className="dup-detail-header-meta">
|
||||
<PageHead
|
||||
title={`🎬 ${detail.filename}`}
|
||||
description={
|
||||
<div className="dup-detail-header-meta" style={{ display: "flex", gap: 12, alignItems: "center", marginTop: 8 }}>
|
||||
<Tag variant={RISK_TAG_VARIANT[riskLevel]}>
|
||||
{RISK_LABEL[riskLevel]}
|
||||
</Tag>
|
||||
<span>{formatSize(detail.file_size)}</span>
|
||||
<span>{formatDuration(detail.duration_seconds)}</span>
|
||||
<span>
|
||||
提交于 {new Date(detail.created_at).toLocaleString("zh-CN")}
|
||||
</span>
|
||||
<span>提交于 {new Date(detail.created_at).toLocaleString("zh-CN")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="dup-detail-actions">
|
||||
<Button
|
||||
buttonType="secondary"
|
||||
buttonSize="md"
|
||||
onClick={() => {
|
||||
/* TODO: 下载报告 */
|
||||
showToast("报告下载功能开发中", "warning");
|
||||
}}
|
||||
>
|
||||
📥 下载报告
|
||||
</Button>
|
||||
{detail.status === "failed" && (
|
||||
}
|
||||
actions={
|
||||
<div className="dup-detail-actions" style={{ display: "flex", gap: 8 }}>
|
||||
<Button
|
||||
buttonType="primary"
|
||||
buttonType="secondary"
|
||||
buttonSize="md"
|
||||
onClick={() => retryMutation.mutate(detail.id)}
|
||||
onClick={() => {
|
||||
showToast("报告下载功能开发中", "warning");
|
||||
}}
|
||||
>
|
||||
🔄 重新查重
|
||||
📥 下载报告
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{detail.status === "failed" && (
|
||||
<Button
|
||||
buttonType="primary"
|
||||
buttonSize="md"
|
||||
onClick={() => retryMutation.mutate(detail.id)}
|
||||
>
|
||||
🔄 重新查重
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 内容网格 */}
|
||||
<div className="dup-detail-grid">
|
||||
|
||||
@@ -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 = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 页头 */}
|
||||
<div className="dup-results-header">
|
||||
<h2>📋 查重记录</h2>
|
||||
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
||||
<PageHead
|
||||
title="查重记录"
|
||||
actions={
|
||||
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
||||
{/* 筛选胶囊 */}
|
||||
<div className="dup-filter">
|
||||
{FILTER_OPTIONS.map((opt) => (
|
||||
@@ -154,8 +155,9 @@ const DuplicationResults: React.FC = () => {
|
||||
>
|
||||
📤 上传查重
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 加载中 */}
|
||||
{isLoading && (
|
||||
|
||||
@@ -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 = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="dup-page-header">
|
||||
<h2>📹 视频查重</h2>
|
||||
<p>
|
||||
上传视频文件,系统将自动检测与已有素材的重复片段,帮助您避免重复内容。
|
||||
</p>
|
||||
</div>
|
||||
<PageHead title="视频查重" description="上传视频文件,系统将自动检测与已有素材的重复片段,帮助您避免重复内容。" />
|
||||
|
||||
<div className="dup-upload-grid">
|
||||
{/* 左侧:上传区域 */}
|
||||
|
||||
@@ -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 (
|
||||
<div className="xx-settings-page">
|
||||
<PageHead title="个人设置" description="管理您的账户信息" />
|
||||
|
||||
<div className="xx-settings-card">
|
||||
<h3>个人信息</h3>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 20 }}
|
||||
message="个人资料编辑暂未开放"
|
||||
description="当前仅展示登录用户信息,资料修改接口接入后再开放保存。"
|
||||
/>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={onFinish}
|
||||
initialValues={{
|
||||
username: user?.username,
|
||||
email: user?.email,
|
||||
display_name: user?.display_name,
|
||||
}}
|
||||
>
|
||||
<Form.Item label="用户名" name="username">
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
<div className="xx-settings-notice">
|
||||
<span className="xx-settings-notice-icon">ℹ️</span>
|
||||
<div>
|
||||
<strong>个人资料编辑暂未开放</strong>
|
||||
<p>当前仅展示登录用户信息,资料修改接口接入后再开放保存。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form.Item label="邮箱" name="email">
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
<div className="xx-settings-form">
|
||||
<div className="xx-settings-field">
|
||||
<label className="xx-settings-label">用户名</label>
|
||||
<Input
|
||||
value={user?.username || ""}
|
||||
disabled
|
||||
placeholder="用户名"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Form.Item label="显示名称" name="display_name">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<div className="xx-settings-field">
|
||||
<label className="xx-settings-label">邮箱</label>
|
||||
<Input
|
||||
value={user?.email || ""}
|
||||
disabled
|
||||
placeholder="邮箱"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" disabled>
|
||||
<div className="xx-settings-field">
|
||||
<label className="xx-settings-label">显示名称</label>
|
||||
<Input
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
placeholder="请输入显示名称"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="xx-settings-field">
|
||||
<Button
|
||||
buttonType="primary"
|
||||
buttonSize="md"
|
||||
onClick={handleSave}
|
||||
disabled
|
||||
>
|
||||
保存暂未开放
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
|
||||
export const Component = Settings;
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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 }) => (
|
||||
<button
|
||||
type="button"
|
||||
className={`xx-toggle-switch ${checked ? "checked" : ""} ${loading ? "loading" : ""}`}
|
||||
onClick={() => !loading && onChange(!checked)}
|
||||
disabled={loading}
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
>
|
||||
<span className="xx-toggle-switch-handle">
|
||||
{loading && <span className="xx-toggle-switch-spinner" />}
|
||||
</span>
|
||||
<span className="xx-toggle-switch-inner">
|
||||
{checked ? checkedChildren : unCheckedChildren}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
/** 自定义 Spinner 组件 */
|
||||
const Spinner: React.FC<{ size?: "small" | "large" }> = ({ size = "large" }) => (
|
||||
<div className={`xx-spinner xx-spinner--${size}`}>
|
||||
<div className="xx-spinner-dot" />
|
||||
<div className="xx-spinner-dot" />
|
||||
<div className="xx-spinner-dot" />
|
||||
</div>
|
||||
);
|
||||
|
||||
const Billing: React.FC = () => {
|
||||
const [subscription, setSubscription] = useState<SubscriptionInfo | null>(
|
||||
null,
|
||||
@@ -62,13 +98,15 @@ const Billing: React.FC = () => {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="xx-billing-page">
|
||||
<Spin size="large" />
|
||||
<Spinner size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="xx-billing-page">
|
||||
<PageHead title="账单管理" description="管理您的订阅和账单信息" />
|
||||
|
||||
{subscription && (
|
||||
<>
|
||||
{/* 当前订阅概览 */}
|
||||
@@ -104,7 +142,7 @@ const Billing: React.FC = () => {
|
||||
开启后,将在每个计费周期结束时自动扣费续期,避免服务中断。
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
<ToggleSwitch
|
||||
checked={autoRenewChecked}
|
||||
onChange={handleToggleAutoRenew}
|
||||
loading={autoRenewLoading}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* V21 定价页面 */
|
||||
/* V21 定价页面 - P1-2/1-5: CSS 变量替换 */
|
||||
.xx-plans-page {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
@@ -14,14 +14,14 @@
|
||||
.xx-page-head h2 {
|
||||
font-size: 42px;
|
||||
font-weight: 900;
|
||||
color: var(--slate, #0f172a);
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 16px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.xx-page-head p {
|
||||
font-size: 18px;
|
||||
color: var(--muted, #64748b);
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
max-width: 560px;
|
||||
margin-left: auto;
|
||||
@@ -53,10 +53,10 @@
|
||||
|
||||
/* V21 定价卡片 */
|
||||
.xx-plan-card {
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
border: 1px solid rgba(226, 232, 240, 0.95);
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 28px;
|
||||
box-shadow: 0 24px 70px rgba(15, 23, 42, 0.09);
|
||||
box-shadow: var(--shadow-card);
|
||||
padding: 32px 28px;
|
||||
position: relative;
|
||||
transition: all 0.3s;
|
||||
@@ -65,16 +65,16 @@
|
||||
}
|
||||
|
||||
.xx-plan-card:hover {
|
||||
border-color: var(--indigo, #4f46e5);
|
||||
box-shadow: 0 32px 90px rgba(79, 70, 229, 0.15);
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 32px 90px color-mix(in srgb, var(--primary-color) 15%, transparent);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.xx-plan-card.featured {
|
||||
border: 2px solid var(--indigo, #4f46e5);
|
||||
border: 2px solid var(--primary-color);
|
||||
box-shadow:
|
||||
0 0 0 4px rgba(79, 70, 229, 0.1),
|
||||
0 32px 90px rgba(79, 70, 229, 0.15);
|
||||
0 0 0 4px color-mix(in srgb, var(--primary-color) 10%, transparent),
|
||||
0 32px 90px color-mix(in srgb, var(--primary-color) 15%, transparent);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
@@ -88,8 +88,8 @@
|
||||
top: -12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: linear-gradient(135deg, #6366f1, #4f46e5);
|
||||
color: white;
|
||||
background: linear-gradient(135deg, var(--color-primary-500), var(--primary-color));
|
||||
color: var(--text-inverse);
|
||||
padding: 6px 16px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
@@ -99,14 +99,14 @@
|
||||
|
||||
/* 企业版定制标签 */
|
||||
.xx-badge.enterprise {
|
||||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||
background: linear-gradient(135deg, var(--accent-color), var(--accent-dark));
|
||||
}
|
||||
|
||||
/* 套餐名称 */
|
||||
.xx-plan-card h3 {
|
||||
font-size: 22px;
|
||||
font-weight: 900;
|
||||
color: var(--slate, #0f172a);
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 20px;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -120,20 +120,20 @@
|
||||
.xx-plan-price .currency {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--muted, #64748b);
|
||||
color: var(--text-secondary);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.xx-plan-price .amount {
|
||||
font-size: 48px;
|
||||
font-weight: 900;
|
||||
color: var(--indigo, #4f46e5);
|
||||
color: var(--primary-color);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.xx-plan-price .period {
|
||||
font-size: 16px;
|
||||
color: var(--muted, #64748b);
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -144,12 +144,12 @@
|
||||
|
||||
/* 描述 */
|
||||
.xx-plan-description {
|
||||
color: var(--muted, #64748b);
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid var(--line, #e2e8f0);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* 功能列表 */
|
||||
@@ -163,14 +163,14 @@
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
color: var(--muted, #64748b);
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.xx-feature span:first-child {
|
||||
color: var(--green, #10b981);
|
||||
color: var(--secondary-color);
|
||||
font-weight: 900;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -182,37 +182,39 @@
|
||||
padding: 14px 24px !important;
|
||||
font-size: 15px !important;
|
||||
font-weight: 850 !important;
|
||||
border-radius: 14px !important;
|
||||
border-radius: var(--radius-md) !important;
|
||||
transition: all 0.2s !important;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.xx-subscribe-btn.primary {
|
||||
background: linear-gradient(135deg, #6366f1, #4f46e5) !important;
|
||||
color: white !important;
|
||||
background: linear-gradient(135deg, var(--color-primary-500), var(--primary-color)) !important;
|
||||
color: var(--text-inverse) !important;
|
||||
border: none !important;
|
||||
box-shadow: 0 14px 26px rgba(79, 70, 229, 0.22) !important;
|
||||
box-shadow: var(--shadow-primary) !important;
|
||||
}
|
||||
|
||||
.xx-subscribe-btn.primary:hover {
|
||||
box-shadow: 0 18px 34px rgba(79, 70, 229, 0.28) !important;
|
||||
box-shadow: var(--shadow-hover) !important;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.xx-subscribe-btn.ghost {
|
||||
background: white !important;
|
||||
border: 1px solid var(--line, #e2e8f0) !important;
|
||||
color: var(--slate, #0f172a) !important;
|
||||
background: var(--bg-primary) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
.xx-subscribe-btn.ghost:hover {
|
||||
border-color: var(--indigo, #4f46e5) !important;
|
||||
color: var(--indigo, #4f46e5) !important;
|
||||
border-color: var(--primary-color) !important;
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
/* 企业版按钮 */
|
||||
.xx-subscribe-btn.enterprise {
|
||||
background: linear-gradient(135deg, #f59e0b, #d97706) !important;
|
||||
color: white !important;
|
||||
background: linear-gradient(135deg, var(--accent-color), var(--accent-dark)) !important;
|
||||
color: var(--text-inverse) !important;
|
||||
border: none !important;
|
||||
box-shadow: 0 14px 26px rgba(245, 158, 11, 0.22) !important;
|
||||
box-shadow: 0 14px 26px color-mix(in srgb, var(--accent-color) 22%, transparent) !important;
|
||||
}
|
||||
|
||||
@@ -1,28 +1,10 @@
|
||||
/* 升级/降级页面 */
|
||||
/* 升级/降级页面 - P1-3: 全部使用 CSS 变量 */
|
||||
.xx-upgrade-page {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 24px;
|
||||
}
|
||||
|
||||
.xx-upgrade-header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.xx-upgrade-header h2 {
|
||||
font-size: 32px;
|
||||
font-weight: 900;
|
||||
color: var(--slate, #0f172a);
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.xx-upgrade-header p {
|
||||
font-size: 16px;
|
||||
color: var(--muted, #64748b);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 套餐卡片 */
|
||||
.xx-upgrade-plans {
|
||||
display: grid;
|
||||
@@ -38,8 +20,8 @@
|
||||
}
|
||||
|
||||
.xx-upgrade-card {
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
border: 2px solid rgba(226, 232, 240, 0.95);
|
||||
background: var(--bg-elevated);
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 20px;
|
||||
padding: 28px 24px;
|
||||
text-align: center;
|
||||
@@ -49,27 +31,27 @@
|
||||
}
|
||||
|
||||
.xx-upgrade-card:hover {
|
||||
border-color: var(--indigo, #4f46e5);
|
||||
box-shadow: 0 8px 30px rgba(79, 70, 229, 0.1);
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 8px 30px color-mix(in srgb, var(--primary-color) 10%, transparent);
|
||||
}
|
||||
|
||||
.xx-upgrade-card.selected {
|
||||
border-color: var(--indigo, #4f46e5);
|
||||
border-color: var(--primary-color);
|
||||
box-shadow:
|
||||
0 0 0 3px rgba(79, 70, 229, 0.12),
|
||||
0 8px 30px rgba(79, 70, 229, 0.1);
|
||||
0 0 0 3px color-mix(in srgb, var(--primary-color) 12%, transparent),
|
||||
0 8px 30px color-mix(in srgb, var(--primary-color) 10%, transparent);
|
||||
}
|
||||
|
||||
.xx-upgrade-card.current {
|
||||
border-color: var(--green, #10b981);
|
||||
border-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.xx-current-badge {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: 16px;
|
||||
background: var(--green, #10b981);
|
||||
color: white;
|
||||
background: var(--secondary-color);
|
||||
color: var(--text-inverse);
|
||||
padding: 3px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
@@ -79,7 +61,7 @@
|
||||
.xx-upgrade-card h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: var(--slate, #0f172a);
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
@@ -87,14 +69,48 @@
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* ── BillingCycleSwitch 组件 ──────────────────────── */
|
||||
.xx-billing-cycle-switch {
|
||||
display: inline-flex;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.xx-billing-cycle-btn {
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.xx-billing-cycle-btn:first-child {
|
||||
border-right: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.xx-billing-cycle-btn.active {
|
||||
background: var(--primary-color);
|
||||
color: var(--text-inverse);
|
||||
}
|
||||
|
||||
.xx-save {
|
||||
display: inline-block;
|
||||
margin-left: 4px;
|
||||
font-size: 11px;
|
||||
color: var(--green, #10b981);
|
||||
color: var(--secondary-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.xx-billing-cycle-btn.active .xx-save {
|
||||
color: var(--text-inverse);
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* 操作区 */
|
||||
.xx-upgrade-actions {
|
||||
text-align: center;
|
||||
@@ -108,10 +124,45 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--muted, #64748b);
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.xx-cancel-btn {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* ── 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; }
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
/**
|
||||
* 升级/降级/续费页面
|
||||
* P1-3: antd Button/Modal/Radio/Spin → 自定义 UI 组件
|
||||
*/
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button, Radio, message, Spin, Modal } from "antd";
|
||||
import { message } from "antd";
|
||||
import { Button, Modal } from "@/components/ui";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
getCurrentSubscription,
|
||||
@@ -15,6 +17,7 @@ import type {
|
||||
PlanType,
|
||||
BillingCycle,
|
||||
} from "@/api/subscription";
|
||||
import PageHead from "@/components/layout/PageHead";
|
||||
import "./UpgradeSubscription.css";
|
||||
|
||||
const PLANS_META: Record<
|
||||
@@ -27,6 +30,45 @@ const PLANS_META: Record<
|
||||
enterprise: { name: "企业版", price: 0, yearlyPrice: 0 },
|
||||
};
|
||||
|
||||
/** 自定义计费周期切换组件 */
|
||||
const BillingCycleSwitch: React.FC<{
|
||||
value: BillingCycle;
|
||||
onChange: (cycle: BillingCycle) => void;
|
||||
monthlyPrice: number;
|
||||
yearlyPrice: number;
|
||||
}> = ({ value, onChange, monthlyPrice, yearlyPrice }) => (
|
||||
<div className="xx-billing-cycle-switch">
|
||||
<button
|
||||
type="button"
|
||||
className={`xx-billing-cycle-btn ${value === "monthly" ? "active" : ""}`}
|
||||
onClick={() => onChange("monthly")}
|
||||
>
|
||||
¥{monthlyPrice}/月
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`xx-billing-cycle-btn ${value === "yearly" ? "active" : ""}`}
|
||||
onClick={() => onChange("yearly")}
|
||||
>
|
||||
¥{yearlyPrice}/年
|
||||
{yearlyPrice > 0 && monthlyPrice > 0 && (
|
||||
<span className="xx-save">
|
||||
省 ¥{monthlyPrice * 12 - yearlyPrice}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
/** 自定义 Spinner 组件 */
|
||||
const Spinner: React.FC<{ size?: "small" | "large" }> = ({ size = "large" }) => (
|
||||
<div className={`xx-spinner xx-spinner--${size}`}>
|
||||
<div className="xx-spinner-dot" />
|
||||
<div className="xx-spinner-dot" />
|
||||
<div className="xx-spinner-dot" />
|
||||
</div>
|
||||
);
|
||||
|
||||
const UpgradeSubscription: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [subscription, setSubscription] = useState<SubscriptionInfo | null>(
|
||||
@@ -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 (
|
||||
<div className="xx-upgrade-page">
|
||||
<Spin size="large" />
|
||||
<Spinner size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -140,10 +181,7 @@ const UpgradeSubscription: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="xx-upgrade-page">
|
||||
<div className="xx-upgrade-header">
|
||||
<h2>变更订阅方案</h2>
|
||||
<p>当前套餐:{PLANS_META[currentPlan]?.name ?? "体验版"}</p>
|
||||
</div>
|
||||
<PageHead title="变更订阅方案" description={`当前套餐:${PLANS_META[currentPlan]?.name ?? "体验版"}`} />
|
||||
|
||||
<div className="xx-upgrade-plans">
|
||||
{(["standard", "pro", "enterprise"] as PlanType[]).map((planId) => {
|
||||
@@ -158,21 +196,12 @@ const UpgradeSubscription: React.FC = () => {
|
||||
{isCurrent && <div className="xx-current-badge">当前</div>}
|
||||
<h3>{plan.name}</h3>
|
||||
<div className="xx-price">
|
||||
<Radio.Group
|
||||
<BillingCycleSwitch
|
||||
value={billingCycle}
|
||||
onChange={(e) => setBillingCycle(e.target.value)}
|
||||
size="small"
|
||||
>
|
||||
<Radio.Button value="monthly">¥{plan.price}/月</Radio.Button>
|
||||
<Radio.Button value="yearly">
|
||||
¥{plan.yearlyPrice}/年
|
||||
{plan.yearlyPrice > 0 && plan.price > 0 && (
|
||||
<span className="xx-save">
|
||||
省 ¥{plan.price * 12 - plan.yearlyPrice}
|
||||
</span>
|
||||
)}
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
onChange={setBillingCycle}
|
||||
monthlyPrice={plan.price}
|
||||
yearlyPrice={plan.yearlyPrice}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -181,13 +210,12 @@ const UpgradeSubscription: React.FC = () => {
|
||||
|
||||
<div className="xx-upgrade-actions">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
loading={submitting}
|
||||
buttonType="primary"
|
||||
buttonSize="lg"
|
||||
disabled={submitting || selectedPlan === currentPlan}
|
||||
onClick={handleUpgrade}
|
||||
disabled={selectedPlan === currentPlan}
|
||||
>
|
||||
确认变更
|
||||
{submitting ? "处理中..." : "确认变更"}
|
||||
</Button>
|
||||
|
||||
{subscription && subscription.status === "active" && (
|
||||
@@ -196,7 +224,8 @@ const UpgradeSubscription: React.FC = () => {
|
||||
自动续费:{subscription.auto_renew ? "已开启" : "已关闭"}
|
||||
</span>
|
||||
<Button
|
||||
type="link"
|
||||
buttonType="text"
|
||||
buttonSize="sm"
|
||||
onClick={() => handleToggleAutoRenew(!subscription.auto_renew)}
|
||||
>
|
||||
{subscription.auto_renew ? "关闭" : "开启"}
|
||||
@@ -208,8 +237,8 @@ const UpgradeSubscription: React.FC = () => {
|
||||
subscription.status === "active" &&
|
||||
currentPlan !== "free" && (
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
buttonType="danger"
|
||||
buttonSize="sm"
|
||||
onClick={handleCancel}
|
||||
className="xx-cancel-btn"
|
||||
>
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from "@ant-design/icons";
|
||||
import { Button, Input, Select } from "@/components/ui";
|
||||
import "./titles.css";
|
||||
import PageHead from "@/components/layout/PageHead";
|
||||
|
||||
/* ============================================================
|
||||
* 类型
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from "@ant-design/icons";
|
||||
import { Button, Input, Select } from "@/components/ui";
|
||||
import "./voices.css";
|
||||
import PageHead from "@/components/layout/PageHead";
|
||||
|
||||
/* ============================================================
|
||||
* 类型
|
||||
|
||||
Reference in New Issue
Block a user