style: use united style

This commit is contained in:
2026-01-03 16:09:22 +08:00
parent 955b09436e
commit c98aa73364
20 changed files with 2099 additions and 1319 deletions
+4 -3
View File
@@ -1,10 +1,11 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title> <title>接龙自动打卡</title>
<meta name="description" content="接龙自动打卡系统 - 轻松管理您的打卡任务" />
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
+20
View File
@@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<!-- 圆形绿色渐变背景 -->
<defs>
<linearGradient id="greenGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#66bb6a;stop-opacity:1" />
<stop offset="100%" style="stop-color:#4caf50;stop-opacity:1" />
</linearGradient>
</defs>
<!-- 圆形背景 -->
<circle cx="50" cy="50" r="48" fill="url(#greenGradient)"/>
<!-- 白色打钩图标 -->
<path d="M 30 50 L 42 62 L 70 34"
stroke="white"
stroke-width="8"
stroke-linecap="round"
stroke-linejoin="round"
fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 650 B

+129 -98
View File
@@ -2,180 +2,211 @@ import { theme } from 'ant-design-vue'
/** /**
* Ant Design Vue 主题配置 * Ant Design Vue 主题配置
* 匹配现有 Material Design 3 色彩系统 * 严格遵循 Material Design 3 规范
* @param {boolean} isDark - 是否为暗色模式 * @param {boolean} isDark - 是否为暗色模式
*/ */
export default function getAntdTheme(isDark = false) { export default function getAntdTheme(isDark = false) {
return { return {
token: { token: {
// 主色调 - 绿色(与 MD3 primary 保持一致) // === Material Design 3 Color System ===
// Primary - 主色调(绿色)
colorPrimary: isDark ? '#81c784' : '#4caf50', colorPrimary: isDark ? '#81c784' : '#4caf50',
// 成功色 // Secondary colors
colorSuccess: isDark ? '#81c784' : '#4caf50', colorSuccess: isDark ? '#81c784' : '#4caf50',
// 警告色
colorWarning: '#ff9800', colorWarning: '#ff9800',
colorError: '#f44336', // MD3 标准错误色
// 错误色
colorError: '#f56c6c',
// 信息色 - 蓝色(与 MD3 secondary 保持一致)
colorInfo: isDark ? '#64b5f6' : '#2196f3', colorInfo: isDark ? '#64b5f6' : '#2196f3',
// 背景色 // === Surface & Background (MD3 规范) ===
colorBgBase: isDark ? '#121212' : '#ffffff', colorBgBase: isDark ? '#1c1b1f' : '#ffffff',
colorBgContainer: isDark ? '#1c1c1e' : '#ffffff', colorBgContainer: isDark ? '#1c1b1f' : '#ffffff',
colorBgElevated: isDark ? '#2c2c2e' : '#ffffff', colorBgElevated: isDark ? '#26252a' : '#ffffff',
colorBgLayout: isDark ? '#121212' : '#fafafa', colorBgLayout: isDark ? '#1c1b1f' : '#fefbff', // MD3 标准背景色
colorBgSpotlight: isDark ? '#2c2c2e' : '#ffffff', colorBgSpotlight: isDark ? '#26252a' : '#ffffff',
// 文字色 // === Typography (MD3 规范) ===
colorText: isDark ? '#e5e5e7' : '#1c1b1f', colorText: isDark ? '#e6e1e5' : '#1c1b1f', // On-surface
colorTextSecondary: isDark ? '#a0a0a3' : '#64748b', colorTextSecondary: isDark ? '#cac4d0' : '#49454f', // On-surface-variant
colorTextTertiary: isDark ? '#808083' : '#94a3b8', colorTextTertiary: isDark ? '#938f99' : '#79747e',
colorTextQuaternary: isDark ? '#606063' : '#cbd5e1', colorTextQuaternary: isDark ? '#79747e' : '#938f99',
// 边框色 // === Borders ===
colorBorder: isDark ? '#3a3a3c' : '#e5e7eb', colorBorder: isDark ? '#49454f' : '#d1cdd6',
colorBorderSecondary: isDark ? '#2c2c2e' : '#f3f4f6', colorBorderSecondary: isDark ? '#3a3740' : '#e3e1e6',
colorSplit: isDark ? '#49454f' : '#e3e1e6',
// 分割线颜色 // === Shape System ===
colorSplit: isDark ? '#3a3a3c' : '#e5e7eb', borderRadius: 12, // Medium shape
borderRadiusLG: 16, // Large shape
borderRadiusSM: 8, // Small shape
borderRadiusXS: 4, // Extra small shape
// 边框圆角 - 与 Material Design 3 一致 // === Typography ===
borderRadius: 12, fontFamily: "'Roboto', 'Inter', system-ui, -apple-system, sans-serif",
fontSize: 14, // Body Medium
fontSizeLG: 16, // Body Large
fontSizeSM: 12, // Body Small
lineHeight: 1.428, // 20/14 = 1.428
lineHeightLG: 1.5, // 24/16 = 1.5
// 字体家族 // === Links ===
fontFamily: "'Inter', 'Segoe UI', 'Roboto', system-ui, -apple-system, sans-serif",
// 链接色
colorLink: isDark ? '#64b5f6' : '#2196f3', colorLink: isDark ? '#64b5f6' : '#2196f3',
colorLinkHover: isDark ? '#90caf9' : '#1976d2', colorLinkHover: isDark ? '#90caf9' : '#1976d2',
colorLinkActive: isDark ? '#42a5f5' : '#1565c0', colorLinkActive: isDark ? '#42a5f5' : '#1565c0',
// 字体大小 // === Components ===
fontSize: 14,
// 行高
lineHeight: 1.5715,
// 控制组件高度
controlHeight: 40, controlHeight: 40,
controlHeightLG: 48,
controlHeightSM: 32,
// === Motion (MD3 规范) ===
motionDurationSlow: '0.3s',
motionDurationMid: '0.2s',
motionDurationFast: '0.1s',
}, },
components: { components: {
// Card 组件定制 // === Card 组件 (MD3 Elevated Card) ===
Card: { Card: {
borderRadiusLG: 16, borderRadiusLG: 16,
boxShadowTertiary: isDark
? '0 1px 3px 1px rgba(0, 0, 0, 0.5)'
: '0 1px 3px 1px rgba(0, 0, 0, 0.08)',
paddingLG: 24, paddingLG: 24,
colorBgContainer: isDark ? '#1c1c1e' : '#ffffff', colorBgContainer: isDark ? '#1c1b1f' : '#ffffff',
colorBorderSecondary: isDark ? '#3a3a3c' : '#f0f0f0', colorBorderSecondary: isDark ? '#49454f' : '#e3e1e6',
colorTextHeading: isDark ? '#e5e5e7' : '#1c1b1f', colorTextHeading: isDark ? '#e6e1e5' : '#1c1b1f',
}, },
// Button 组件定制 // === Button 组件 (MD3 规范) ===
Button: { Button: {
borderRadius: 24, // 圆角按钮,类似 MD3 borderRadius: 20, // MD3 Filled Button 圆角
borderRadiusLG: 24,
borderRadiusSM: 16,
controlHeight: 40, controlHeight: 40,
controlHeightLG: 48,
controlHeightSM: 32,
fontSize: 14, fontSize: 14,
colorText: isDark ? '#e5e5e7' : '#1c1b1f', fontSizeLG: 16,
colorBgContainer: isDark ? '#2c2c2e' : '#ffffff', fontSizeSM: 12,
paddingContentHorizontal: 24,
colorText: isDark ? '#e6e1e5' : '#1c1b1f',
colorBgContainer: isDark ? '#26252a' : '#ffffff',
}, },
// Input 组件定制 // === Input 组件 (MD3 Text Field) ===
Input: { Input: {
borderRadius: 12, borderRadius: 12,
controlHeight: 40, controlHeight: 40,
colorBgContainer: isDark ? '#2c2c2e' : '#ffffff', colorBgContainer: isDark ? '#26252a' : '#ffffff',
colorText: isDark ? '#e5e5e7' : '#1c1b1f', colorText: isDark ? '#e6e1e5' : '#1c1b1f',
colorTextPlaceholder: isDark ? '#808083' : '#94a3b8', colorTextPlaceholder: isDark ? '#938f99' : '#79747e',
colorBorder: isDark ? '#3a3a3c' : '#e5e7eb', colorBorder: isDark ? '#49454f' : '#d1cdd6',
}, },
// Select 组件定制 // === Select 组件 ===
Select: { Select: {
borderRadius: 12, borderRadius: 12,
controlHeight: 40, controlHeight: 40,
colorBgContainer: isDark ? '#2c2c2e' : '#ffffff', colorBgContainer: isDark ? '#26252a' : '#ffffff',
colorBgElevated: isDark ? '#2c2c2e' : '#ffffff', colorBgElevated: isDark ? '#26252a' : '#ffffff',
colorText: isDark ? '#e5e5e7' : '#1c1b1f', colorText: isDark ? '#e6e1e5' : '#1c1b1f',
colorTextPlaceholder: isDark ? '#808083' : '#94a3b8', colorTextPlaceholder: isDark ? '#938f99' : '#79747e',
colorBorder: isDark ? '#3a3a3c' : '#e5e7eb', colorBorder: isDark ? '#49454f' : '#d1cdd6',
}, },
// Modal 组件定制 // === Modal 组件 (MD3 Dialog) ===
Modal: { Modal: {
borderRadiusLG: 16, borderRadiusLG: 28, // MD3 Dialog 使用 Extra Large 圆角
colorBgElevated: isDark ? '#1c1c1e' : '#ffffff', colorBgElevated: isDark ? '#1c1b1f' : '#ffffff',
colorText: isDark ? '#e5e5e7' : '#1c1b1f', colorText: isDark ? '#e6e1e5' : '#1c1b1f',
colorTextHeading: isDark ? '#e5e5e7' : '#1c1b1f', colorTextHeading: isDark ? '#e6e1e5' : '#1c1b1f',
}, },
// Table 组件定制 // === Table 组件 ===
Table: { Table: {
borderRadius: 12, borderRadius: 12,
colorBgContainer: isDark ? '#1c1c1e' : '#ffffff', colorBgContainer: isDark ? '#1c1b1f' : '#ffffff',
colorFillAlter: isDark ? '#2c2c2e' : '#f5f7fa', colorFillAlter: isDark ? '#26252a' : '#f5f5f5',
colorText: isDark ? '#e5e5e7' : '#1c1b1f', colorText: isDark ? '#e6e1e5' : '#1c1b1f',
colorTextHeading: isDark ? '#e5e5e7' : '#1c1b1f', colorTextHeading: isDark ? '#e6e1e5' : '#1c1b1f',
colorBorderSecondary: isDark ? '#3a3a3c' : '#f0f0f0', colorBorderSecondary: isDark ? '#49454f' : '#e3e1e6',
}, },
// Tabs 组件定制 // === Tabs 组件 ===
Tabs: { Tabs: {
borderRadius: 12, borderRadius: 12,
colorText: isDark ? '#e5e5e7' : '#1c1b1f', colorText: isDark ? '#e6e1e5' : '#1c1b1f',
colorBgContainer: isDark ? '#1c1c1e' : '#ffffff', colorBgContainer: isDark ? '#1c1b1f' : '#ffffff',
}, },
// Menu 组件定制 // === Menu 组件 ===
Menu: { Menu: {
colorItemBg: isDark ? '#1c1c1e' : '#ffffff', colorItemBg: isDark ? '#1c1b1f' : '#ffffff',
colorItemBgHover: isDark ? '#2c2c2e' : '#f5f7fa', colorItemBgHover: isDark ? '#26252a' : '#f5f5f5',
colorItemBgSelected: isDark ? '#2c2c2e' : '#e8f5e9', colorItemBgSelected: isDark ? '#3a4a3f' : '#e8f5e9',
colorItemText: isDark ? '#e5e5e7' : '#1c1b1f', colorItemText: isDark ? '#e6e1e5' : '#1c1b1f',
colorItemTextSelected: isDark ? '#81c784' : '#4caf50', colorItemTextSelected: isDark ? '#81c784' : '#4caf50',
borderRadius: 12,
}, },
// Dropdown 组件定制 // === Dropdown 组件 ===
Dropdown: { Dropdown: {
colorBgElevated: isDark ? '#2c2c2e' : '#ffffff', colorBgElevated: isDark ? '#26252a' : '#ffffff',
colorText: isDark ? '#e5e5e7' : '#1c1b1f', colorText: isDark ? '#e6e1e5' : '#1c1b1f',
borderRadiusLG: 12,
}, },
// Descriptions 组件定制 // === Descriptions 组件 ===
Descriptions: { Descriptions: {
colorText: isDark ? '#e5e5e7' : '#1c1b1f', colorText: isDark ? '#e6e1e5' : '#1c1b1f',
colorTextSecondary: isDark ? '#a0a0a3' : '#64748b', colorTextSecondary: isDark ? '#cac4d0' : '#49454f',
colorBgContainer: isDark ? '#1c1c1e' : '#ffffff', colorBgContainer: isDark ? '#1c1b1f' : '#ffffff',
colorFillAlter: isDark ? '#2c2c2e' : '#f5f7fa', colorFillAlter: isDark ? '#201f24' : '#f3f4f6', // Label 背景色 = surface-container
colorSplit: isDark ? '#49454f' : '#e3e1e6',
borderRadiusLG: 8, // 设置 Descriptions 容器圆角
}, },
// Alert 组件定制 // === Alert 组件 ===
Alert: { Alert: {
borderRadiusLG: 12, borderRadiusLG: 12,
colorText: isDark ? '#e5e5e7' : '#1c1b1f', colorText: isDark ? '#e6e1e5' : '#1c1b1f',
}, },
// Drawer 组件定制 // === Drawer 组件 ===
Drawer: { Drawer: {
colorBgElevated: isDark ? '#1c1c1e' : '#ffffff', colorBgElevated: isDark ? '#1c1b1f' : '#ffffff',
colorText: isDark ? '#e5e5e7' : '#1c1b1f', colorText: isDark ? '#e6e1e5' : '#1c1b1f',
borderRadiusLG: 16,
}, },
// Form 组件定制 // === Form 组件 ===
Form: { Form: {
colorText: isDark ? '#e5e5e7' : '#1c1b1f', colorText: isDark ? '#e6e1e5' : '#1c1b1f',
colorTextHeading: isDark ? '#e5e5e7' : '#1c1b1f', colorTextHeading: isDark ? '#e6e1e5' : '#1c1b1f',
}, },
// Empty 组件定制 // === Empty 组件 ===
Empty: { Empty: {
colorTextDescription: isDark ? '#a0a0a3' : '#94a3b8', colorTextDescription: isDark ? '#938f99' : '#79747e',
},
// === Tag 组件 ===
Tag: {
borderRadiusSM: 16, // 药丸形
colorText: isDark ? '#e6e1e5' : '#1c1b1f',
},
// === Switch 组件 ===
Switch: {
colorPrimary: isDark ? '#81c784' : '#4caf50',
colorText: isDark ? '#e6e1e5' : '#1c1b1f',
},
// === Tooltip 组件 ===
Tooltip: {
colorBgSpotlight: isDark ? '#313033' : '#f5f5f5', // Tooltip 背景色(跟随主题)
colorTextLightSolid: isDark ? '#ffffff' : '#1c1b1f', // Tooltip 文本颜色(跟随主题)
borderRadius: 8,
}, },
}, },
+90 -107
View File
@@ -325,191 +325,174 @@ onBeforeUnmount(() => {
</script> </script>
<style scoped> <style scoped>
/* === Material Design 3 样式重写 === */
.crontab-editor { .crontab-editor {
border: 1px solid #dcdfe6; border: 1px solid var(--md-sys-color-outline-variant);
border-radius: 4px; border-radius: 12px;
padding: 16px; padding: 20px;
background: #f5f7fa; background-color: var(--md-sys-color-surface-container-lowest);
transition: all 0.3s; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
} }
.dark .crontab-editor { .crontab-editor:focus-within {
border-color: #3a3a3c; border-color: var(--md-sys-color-primary);
background: #2c2c2e; box-shadow: 0 0 0 1px var(--md-sys-color-primary);
} }
/* 模式选择标签 */
.mode-tabs { .mode-tabs {
display: flex; display: flex;
gap: 8px; gap: 8px;
margin-bottom: 16px; margin-bottom: 20px;
border-bottom: 2px solid #ebeef5; border-bottom: 1px solid var(--md-sys-color-outline-variant);
transition: border-color 0.3s; padding-bottom: 0;
}
.dark .mode-tabs {
border-bottom-color: #3a3a3c;
} }
.mode-tab { .mode-tab {
padding: 8px 16px; padding: 10px 20px;
background: none; background: none;
border: none; border: none;
cursor: pointer; cursor: pointer;
color: #909399; color: var(--md-sys-color-on-surface-variant);
font-size: 14px;
font-weight: 500; font-weight: 500;
letter-spacing: 0.1px;
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
margin-bottom: -2px; margin-bottom: -1px;
transition: all 0.3s; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
} }
.dark .mode-tab { .mode-tab:hover {
color: #a0a0a3; color: var(--md-sys-color-on-surface);
background-color: rgba(76, 175, 80, 0.04);
} }
.mode-tab.active { .mode-tab.active {
color: #409eff; color: var(--md-sys-color-primary);
border-bottom-color: #409eff; border-bottom-color: var(--md-sys-color-primary);
} font-weight: 600;
.dark .mode-tab.active {
color: #81c784;
border-bottom-color: #81c784;
} }
/* 模式内容区域 */
.mode-content { .mode-content {
margin: 16px 0; margin: 20px 0;
} }
/* 快速选项 */
.quick-option { .quick-option {
padding: 12px; padding: 16px;
background: white; background-color: var(--md-sys-color-surface);
border-radius: 4px; border-radius: 12px;
transition: background 0.3s; border: 1px solid var(--md-sys-color-outline-variant);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
} }
.dark .quick-option { .quick-option:hover {
background: #1c1c1e; border-color: var(--md-sys-color-outline);
box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.3),
0px 1px 3px 1px rgba(0, 0, 0, 0.15);
} }
.option-label { .option-label {
font-weight: 600; font-size: 14px;
color: #1c1b1f; font-weight: 500;
} color: var(--md-sys-color-on-surface);
letter-spacing: 0.1px;
.dark .option-label {
color: #e5e5e7;
} }
.option-desc { .option-desc {
margin-left: 12px; margin-left: 12px;
color: #909399; color: var(--md-sys-color-on-surface-variant);
font-size: 12px; font-size: 12px;
letter-spacing: 0.4px;
} }
.dark .option-desc { /* 表达式输入 */
color: #a0a0a3;
}
.expression-input { .expression-input {
margin: 12px 0; margin: 16px 0;
} }
.help-text { .help-text {
margin-top: 8px; margin-top: 8px;
color: #909399; color: var(--md-sys-color-on-surface-variant);
font-size: 12px; font-size: 12px;
} line-height: 16px;
letter-spacing: 0.4px;
.dark .help-text {
color: #a0a0a3;
} }
.help-text a { .help-text a {
color: #409eff; color: var(--md-sys-color-secondary);
text-decoration: none; text-decoration: none;
} font-weight: 500;
transition: color 0.2s;
.dark .help-text a {
color: #81c784;
} }
.help-text a:hover { .help-text a:hover {
color: var(--md-sys-color-primary);
text-decoration: underline; text-decoration: underline;
} }
/* 预览区域 */
.preview-section { .preview-section {
margin: 16px 0; margin: 16px 0;
padding: 12px; padding: 16px;
background: white; background-color: var(--md-sys-color-surface-container-low);
border-radius: 4px; border-radius: 12px;
transition: background 0.3s; border: 1px solid var(--md-sys-color-outline-variant);
} transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
.dark .preview-section {
background: #1c1c1e;
} }
.preview-section h4 { .preview-section h4 {
margin: 0 0 8px 0; margin: 0 0 12px 0;
font-size: 14px; font-size: 14px;
color: #1c1b1f; font-weight: 500;
} color: var(--md-sys-color-on-surface);
letter-spacing: 0.1px;
.dark .preview-section h4 {
color: #e5e5e7;
} }
.execution-list { .execution-list {
margin: 0; margin: 0;
padding-left: 20px; padding-left: 24px;
font-size: 12px; font-size: 13px;
color: #606266; line-height: 20px;
color: var(--md-sys-color-on-surface-variant);
} }
.dark .execution-list { .execution-list li {
color: #a0a0a3; margin-bottom: 4px;
} }
/* 验证消息 */
.validation-message { .validation-message {
padding: 8px 12px; padding: 12px 16px;
border-radius: 4px; border-radius: 12px;
margin-top: 12px; margin-top: 16px;
font-size: 12px; font-size: 13px;
line-height: 20px;
letter-spacing: 0.25px;
border: 1px solid;
display: flex;
align-items: center;
gap: 8px;
} }
.validation-message.success { .validation-message.success {
background: #f0f9ff; background-color: var(--md-sys-color-primary-container);
color: #67c23a; color: var(--md-sys-color-on-primary-container);
border: 1px solid #c6e2ff; border-color: var(--md-sys-color-primary);
}
.dark .validation-message.success {
background: rgba(129, 199, 132, 0.1);
color: #81c784;
border-color: rgba(129, 199, 132, 0.3);
} }
.validation-message.error { .validation-message.error {
background: #fef0f0; background-color: var(--md-sys-color-error-container);
color: #f56c6c; color: var(--md-sys-color-on-error-container);
border: 1px solid #fde7e7; border-color: var(--md-sys-color-error);
}
.dark .validation-message.error {
background: rgba(244, 67, 54, 0.1);
color: #ef5350;
border-color: rgba(244, 67, 54, 0.3);
} }
.validation-message.info { .validation-message.info {
background: #f4f4f5; background-color: var(--md-sys-color-surface-container-high);
color: #909399; color: var(--md-sys-color-on-surface-variant);
border: 1px solid #ebeef5; border-color: var(--md-sys-color-outline-variant);
}
.dark .validation-message.info {
background: #2c2c2e;
color: #a0a0a3;
border-color: #3a3a3c;
} }
</style> </style>
+19 -19
View File
@@ -9,7 +9,7 @@
placeholder="在表单中显示的名称" placeholder="在表单中显示的名称"
allow-clear allow-clear
/> />
<span class="text-xs text-gray-500 mt-1">显示名称</span> <span class="text-xs text-on-surface-variant mt-1">显示名称</span>
</a-form-item> </a-form-item>
<a-form-item label="字段类型" class="mb-0"> <a-form-item label="字段类型" class="mb-0">
@@ -24,7 +24,7 @@
<a-select-option label="🔢 数字输入" value="number" /> <a-select-option label="🔢 数字输入" value="number" />
<a-select-option label="📋 下拉选择" value="select" /> <a-select-option label="📋 下拉选择" value="select" />
</a-select> </a-select>
<span class="text-xs text-gray-500 mt-1">用户填写时使用的输入控件</span> <span class="text-xs text-on-surface-variant mt-1">用户填写时使用的输入控件</span>
</a-form-item> </a-form-item>
</div> </div>
@@ -38,22 +38,22 @@
class="w-full" class="w-full"
> >
<a-select-option label="字符串 (string)" value="string"> <a-select-option label="字符串 (string)" value="string">
<span class="text-xs text-gray-500">字符串 (string)</span> <span class="text-xs text-on-surface-variant">字符串 (string)</span>
</a-select-option> </a-select-option>
<a-select-option label="整数 (int)" value="int"> <a-select-option label="整数 (int)" value="int">
<span class="text-xs text-gray-500">整数 (int)</span> <span class="text-xs text-on-surface-variant">整数 (int)</span>
</a-select-option> </a-select-option>
<a-select-option label="浮点数 (double)" value="double"> <a-select-option label="浮点数 (double)" value="double">
<span class="text-xs text-gray-500">浮点数 (double)</span> <span class="text-xs text-on-surface-variant">浮点数 (double)</span>
</a-select-option> </a-select-option>
<a-select-option label="布尔值 (bool)" value="bool"> <a-select-option label="布尔值 (bool)" value="bool">
<span class="text-xs text-gray-500">布尔值 (bool)</span> <span class="text-xs text-on-surface-variant">布尔值 (bool)</span>
</a-select-option> </a-select-option>
<a-select-option label="JSON对象 (json)" value="json"> <a-select-option label="JSON对象 (json)" value="json">
<span class="text-xs text-gray-500">JSON对象 (json) - 用于Values字段</span> <span class="text-xs text-on-surface-variant">JSON对象 (json) - 用于Values字段</span>
</a-select-option> </a-select-option>
</a-select> </a-select>
<span class="text-xs text-gray-500 mt-1">数据存储时的类型</span> <span class="text-xs text-on-surface-variant mt-1">数据存储时的类型</span>
</a-form-item> </a-form-item>
<a-form-item label="默认值" class="mb-0"> <a-form-item label="默认值" class="mb-0">
@@ -72,7 +72,7 @@
:rows="3" :rows="3"
allow-clear allow-clear
/> />
<span class="text-xs text-gray-500 mt-1"> <span class="text-xs text-on-surface-variant mt-1">
<template v-if="modelValue.value_type === 'json'"> <template v-if="modelValue.value_type === 'json'">
<p>输入JSON对象,会自动序列化为字符串</p> <p>输入JSON对象,会自动序列化为字符串</p>
<p>:{"key1":value1,"key2":value2}</p> <p>:{"key1":value1,"key2":value2}</p>
@@ -92,15 +92,15 @@
placeholder="输入框的灰色提示文本" placeholder="输入框的灰色提示文本"
allow-clear allow-clear
/> />
<span class="text-xs text-gray-500 mt-1">占位符</span> <span class="text-xs text-on-surface-variant mt-1">占位符</span>
</a-form-item> </a-form-item>
<!-- Row 4: Switches --> <!-- Row 4: Switches -->
<div class="grid grid-cols-2 gap-4 p-3 bg-blue-50 rounded-lg"> <div class="grid grid-cols-2 gap-4 p-3 bg-surface-container-low rounded-md3 border border-outline-variant">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<label class="text-sm font-medium text-gray-700">是否必填</label> <label class="text-sm font-medium text-on-surface">是否必填</label>
<p class="text-xs text-gray-500">用户必须填写此字段</p> <p class="text-xs text-on-surface-variant">用户必须填写此字段</p>
</div> </div>
<a-switch <a-switch
:checked="modelValue.required" :checked="modelValue.required"
@@ -111,8 +111,8 @@
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<label class="text-sm font-medium text-gray-700">是否隐藏</label> <label class="text-sm font-medium text-on-surface">是否隐藏</label>
<p class="text-xs text-gray-500">直接使用默认值不在表单中显示</p> <p class="text-xs text-on-surface-variant">直接使用默认值不在表单中显示</p>
</div> </div>
<a-switch <a-switch
:checked="modelValue.hidden" :checked="modelValue.hidden"
@@ -142,9 +142,9 @@
<div <div
v-for="(option, index) in modelValue.options || []" v-for="(option, index) in modelValue.options || []"
:key="index" :key="index"
class="flex items-center gap-2 p-2 bg-gray-50 rounded" class="flex items-center gap-2 p-2 bg-surface-container rounded-md3"
> >
<span class="text-xs text-gray-500 w-8">{{ index + 1 }}.</span> <span class="text-xs text-on-surface-variant w-8">{{ index + 1 }}.</span>
<a-input <a-input
:value="option.label" :value="option.label"
@change="e => updateOption(index, 'label', e.target.value)" @change="e => updateOption(index, 'label', e.target.value)"
@@ -175,8 +175,8 @@
添加选项 添加选项
</a-button> </a-button>
<p class="text-xs text-gray-500 mt-2"> <p class="text-xs text-on-surface-variant mt-2">
💡 提示显示文本是用户看到的内容选项值是实际保存的数据 💡 提示显示文本是用户看到的内容,选项值是实际保存的数据
</p> </p>
</div> </div>
</a-form-item> </a-form-item>
+25 -25
View File
@@ -1,16 +1,16 @@
<template> <template>
<div class="field-tree-node border-2 rounded-lg p-4 bg-white shadow-sm hover:shadow-md transition-shadow"> <div class="field-tree-node border border-outline-variant rounded-md3 p-4 bg-surface shadow-md3-1 hover:shadow-md3-2 transition-shadow">
<!-- 普通字段 --> <!-- 普通字段 -->
<div v-if="isFieldConfig" class="field-config"> <div v-if="isFieldConfig" class="field-config">
<div class="flex items-center justify-between mb-3 pb-2 border-b border-gray-200"> <div class="flex items-center justify-between mb-3 pb-2 border-b border-outline-variant">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button <button
type="button" type="button"
@click="isCollapsed = !isCollapsed" @click="isCollapsed = !isCollapsed"
class="hover:bg-gray-100 rounded p-1 transition-colors" class="hover:bg-surface-container rounded-md3 p-1 transition-colors"
> >
<svg <svg
class="w-4 h-4 text-gray-600 transition-transform" class="w-4 h-4 text-on-surface-variant transition-transform"
:class="{ 'rotate-180': !isCollapsed }" :class="{ 'rotate-180': !isCollapsed }"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
@@ -19,10 +19,10 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg> </svg>
</button> </button>
<svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
</svg> </svg>
<span class="font-mono text-base font-bold text-blue-700">{{ fieldKey }}</span> <span class="font-mono text-base font-bold text-primary">{{ fieldKey }}</span>
<a-tag type="primary" size="small">普通字段</a-tag> <a-tag type="primary" size="small">普通字段</a-tag>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
@@ -45,22 +45,22 @@
</div> </div>
</div> </div>
<div v-show="!isCollapsed" class="bg-gray-50 rounded-lg p-3"> <div v-show="!isCollapsed" class="bg-surface-container-low rounded-md3 p-3">
<FieldConfigEditor v-model="localFieldConfig" :field-key="fieldKey" /> <FieldConfigEditor v-model="localFieldConfig" :field-key="fieldKey" />
</div> </div>
</div> </div>
<!-- 数组字段 --> <!-- 数组字段 -->
<div v-else-if="isArray" class="array-field"> <div v-else-if="isArray" class="array-field">
<div class="flex items-center justify-between mb-3 pb-2 border-b border-purple-200"> <div class="flex items-center justify-between mb-3 pb-2 border-b border-outline-variant">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button <button
type="button" type="button"
@click="isCollapsed = !isCollapsed" @click="isCollapsed = !isCollapsed"
class="hover:bg-gray-100 rounded p-1 transition-colors" class="hover:bg-surface-container rounded-md3 p-1 transition-colors"
> >
<svg <svg
class="w-4 h-4 text-gray-600 transition-transform" class="w-4 h-4 text-on-surface-variant transition-transform"
:class="{ 'rotate-180': !isCollapsed }" :class="{ 'rotate-180': !isCollapsed }"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
@@ -69,10 +69,10 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg> </svg>
</button> </button>
<svg class="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5 text-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16" />
</svg> </svg>
<span class="font-mono text-base font-bold text-purple-700">{{ fieldKey }}</span> <span class="font-mono text-base font-bold text-secondary">{{ fieldKey }}</span>
<a-tag type="warning" size="small">数组字段</a-tag> <a-tag type="warning" size="small">数组字段</a-tag>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
@@ -102,8 +102,8 @@
</div> </div>
<div v-show="!isCollapsed"> <div v-show="!isCollapsed">
<div v-if="localFieldConfig.length === 0" class="text-center py-6 bg-purple-50 rounded-lg border border-dashed border-purple-300"> <div v-if="localFieldConfig.length === 0" class="text-center py-6 bg-surface-container-low rounded-md3 border border-dashed border-outline">
<p class="text-sm text-gray-500 mb-2">数组为空</p> <p class="text-sm text-on-surface-variant mb-2">数组为空</p>
<a-button size="small" type="primary" @click="addArrayItem">添加第一个元素</a-button> <a-button size="small" type="primary" @click="addArrayItem">添加第一个元素</a-button>
</div> </div>
@@ -111,17 +111,17 @@
<div <div
v-for="(item, index) in localFieldConfig" v-for="(item, index) in localFieldConfig"
:key="index" :key="index"
class="border-2 border-purple-200 rounded-lg p-3 bg-purple-50" class="border border-outline-variant rounded-md3 p-3 bg-surface-container"
> >
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">
<span class="text-sm font-semibold text-purple-700">元素 #{{ index + 1 }}</span> <span class="text-sm font-semibold text-secondary">元素 #{{ index + 1 }}</span>
<a-button size="small" type="danger" plain @click="removeArrayItem(index)"> <a-button size="small" type="danger" plain @click="removeArrayItem(index)">
删除元素 删除元素
</a-button> </a-button>
</div> </div>
<!-- 如果数组元素是字段配置对象直接渲染为字段编辑器 --> <!-- 如果数组元素是字段配置对象直接渲染为字段编辑器 -->
<div v-if="typeof item === 'object' && !Array.isArray(item) && 'display_name' in item" class="bg-white rounded-lg p-3"> <div v-if="typeof item === 'object' && !Array.isArray(item) && 'display_name' in item" class="bg-surface rounded-md3 p-3">
<FieldConfigEditor :model-value="item" @update:model-value="updateArrayItemField(index, $event)" :field-key="`元素${index + 1}`" /> <FieldConfigEditor :model-value="item" @update:model-value="updateArrayItemField(index, $event)" :field-key="`元素${index + 1}`" />
</div> </div>
@@ -164,15 +164,15 @@
<!-- 对象字段 --> <!-- 对象字段 -->
<div v-else-if="isObject" class="object-field"> <div v-else-if="isObject" class="object-field">
<div class="flex items-center justify-between mb-3 pb-2 border-b border-green-200"> <div class="flex items-center justify-between mb-3 pb-2 border-b border-outline-variant">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button <button
type="button" type="button"
@click="isCollapsed = !isCollapsed" @click="isCollapsed = !isCollapsed"
class="hover:bg-gray-100 rounded p-1 transition-colors" class="hover:bg-surface-container rounded-md3 p-1 transition-colors"
> >
<svg <svg
class="w-4 h-4 text-gray-600 transition-transform" class="w-4 h-4 text-on-surface-variant transition-transform"
:class="{ 'rotate-180': !isCollapsed }" :class="{ 'rotate-180': !isCollapsed }"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
@@ -181,10 +181,10 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg> </svg>
</button> </button>
<svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5 text-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg> </svg>
<span class="font-mono text-base font-bold text-green-700">{{ fieldKey }}</span> <span class="font-mono text-base font-bold text-accent">{{ fieldKey }}</span>
<a-tag type="success" size="small">对象字段</a-tag> <a-tag type="success" size="small">对象字段</a-tag>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
@@ -214,12 +214,12 @@
</div> </div>
<div v-show="!isCollapsed"> <div v-show="!isCollapsed">
<div v-if="Object.keys(localFieldConfig).length === 0" class="text-center py-6 bg-green-50 rounded-lg border border-dashed border-green-300"> <div v-if="Object.keys(localFieldConfig).length === 0" class="text-center py-6 bg-surface-container-low rounded-md3 border border-dashed border-outline">
<p class="text-sm text-gray-500 mb-2">对象为空</p> <p class="text-sm text-on-surface-variant mb-2">对象为空</p>
<a-button size="small" type="primary" @click="addFieldToObject">添加第一个子字段</a-button> <a-button size="small" type="primary" @click="addFieldToObject">添加第一个子字段</a-button>
</div> </div>
<div v-else class="space-y-3 mt-3 pl-4 border-l-4 border-green-300"> <div v-else class="space-y-3 mt-3 pl-4 border-l-4 border-accent">
<!-- 递归渲染对象中的字段 --> <!-- 递归渲染对象中的字段 -->
<FieldTreeNode <FieldTreeNode
v-for="(subConfig, subKey) in localFieldConfig" v-for="(subConfig, subKey) in localFieldConfig"
+1 -5
View File
@@ -26,11 +26,7 @@ onMounted(() => {
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: linear-gradient(135deg, #f5f7fa 0%, #e8eef5 100%); background: linear-gradient(135deg, var(--md-sys-color-surface-container-lowest) 0%, var(--md-sys-color-surface-container-low) 100%);
}
.dark .layout-container {
background: linear-gradient(135deg, #1a1a1a 0%, #0f0f0f 100%);
} }
.main-content { .main-content {
+31 -31
View File
@@ -1,5 +1,5 @@
<template> <template>
<div class="sticky top-0 z-50 glass-effect border-b border-gray-200/50 dark:border-gray-700/50 shadow-md3-2"> <div class="sticky top-0 z-50 md3-surface elevation-2 border-b border-outline-variant">
<nav class="max-w-7xl mx-auto px-6 py-4"> <nav class="max-w-7xl mx-auto px-6 py-4">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<!-- Logo and Brand --> <!-- Logo and Brand -->
@@ -24,7 +24,7 @@
'px-4 py-2 rounded-full font-medium transition-all cursor-pointer', 'px-4 py-2 rounded-full font-medium transition-all cursor-pointer',
isActive isActive
? 'bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400' ? 'bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700' : 'text-on-surface hover:bg-surface-container'
]" ]"
> >
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
@@ -45,7 +45,7 @@
'px-4 py-2 rounded-full font-medium transition-all cursor-pointer', 'px-4 py-2 rounded-full font-medium transition-all cursor-pointer',
isActive isActive
? 'bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400' ? 'bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700' : 'text-on-surface hover:bg-surface-container'
]" ]"
> >
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
@@ -66,7 +66,7 @@
'px-4 py-2 rounded-full font-medium transition-all cursor-pointer', 'px-4 py-2 rounded-full font-medium transition-all cursor-pointer',
isActive isActive
? 'bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400' ? 'bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700' : 'text-on-surface hover:bg-surface-container'
]" ]"
> >
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
@@ -81,7 +81,7 @@
<a <a
:class="[ :class="[
'px-4 py-2 rounded-full font-medium transition-all flex items-center space-x-2 cursor-pointer', 'px-4 py-2 rounded-full font-medium transition-all flex items-center space-x-2 cursor-pointer',
isAdminPath ? 'bg-secondary-100 dark:bg-secondary-900/30 text-secondary-700 dark:text-secondary-400' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700' isAdminPath ? 'bg-secondary-100 dark:bg-secondary-900/30 text-secondary-700 dark:text-secondary-400' : 'text-on-surface hover:bg-surface-container'
]" ]"
> >
<SettingOutlined /> <SettingOutlined />
@@ -121,7 +121,7 @@
<!-- Token Status Indicator (Desktop & Mobile) --> <!-- Token Status Indicator (Desktop & Mobile) -->
<a-tooltip v-if="showTokenStatus" :title="tokenStatusTooltip"> <a-tooltip v-if="showTokenStatus" :title="tokenStatusTooltip">
<div <div
class="px-2 md:px-3 py-1.5 rounded-full cursor-pointer transition-all hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center space-x-1 md:space-x-2" class="px-2 md:px-3 py-1.5 rounded-full cursor-pointer transition-all hover:bg-surface-container flex items-center space-x-1 md:space-x-2"
@click="handleTokenStatusClick" @click="handleTokenStatusClick"
> >
<a-badge :status="tokenBadgeStatus" /> <a-badge :status="tokenBadgeStatus" />
@@ -142,32 +142,32 @@
</a-tooltip> </a-tooltip>
<!-- Theme Toggle Button --> <!-- Theme Toggle Button -->
<a-tooltip :title="isDark ? '切换到亮色模式' : '切换到暗色模式'"> <a-tooltip :title="isDark ? '切换到亮色模式' : '切换到暗色模式'" placement="bottom">
<a-button <button
type="text" type="button"
class="!p-2 !flex !items-center !justify-center hover:!bg-gray-100 dark:hover:!bg-gray-700 transition-all" class="w-10 h-10 rounded-full flex items-center justify-center hover:bg-surface-container transition-all"
@click="toggleTheme" @click="toggleTheme"
> >
<BulbFilled v-if="isDark" class="text-lg text-yellow-400" /> <BulbFilled v-if="isDark" class="text-xl text-yellow-400" />
<BulbOutlined v-else class="text-lg text-gray-700 dark:text-gray-300" /> <BulbOutlined v-else class="text-xl text-on-surface" />
</a-button> </button>
</a-tooltip> </a-tooltip>
<!-- Desktop User Menu --> <!-- Desktop User Menu -->
<a-dropdown v-if="!isMobile" :trigger="['hover']"> <a-dropdown v-if="!isMobile" :trigger="['hover']">
<a class="flex items-center space-x-3 px-4 py-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-all cursor-pointer"> <a class="flex items-center space-x-3 px-4 py-2 rounded-full hover:bg-surface-container transition-all cursor-pointer">
<a-avatar :style="{ backgroundColor: '#f56a00' }"> <a-avatar :style="{ backgroundColor: '#f56a00' }">
{{ userInitial }} {{ userInitial }}
</a-avatar> </a-avatar>
<span class="hidden md:block font-medium text-gray-700 dark:text-gray-200">{{ authStore.user?.alias || '用户' }}</span> <span class="hidden md:block font-medium text-on-surface">{{ authStore.user?.alias || '用户' }}</span>
<DownOutlined class="text-xs text-gray-500 dark:text-gray-400" /> <DownOutlined class="text-xs text-on-surface-variant" />
</a> </a>
<template #overlay> <template #overlay>
<a-menu> <a-menu>
<a-menu-item key="info" disabled> <a-menu-item key="info" disabled>
<div class="px-2 py-1"> <div class="px-2 py-1">
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ authStore.user?.alias }}</p> <p class="text-sm font-medium text-on-surface">{{ authStore.user?.alias }}</p>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ authStore.isAdmin ? '管理员' : '普通用户' }}</p> <p class="text-xs text-on-surface-variant mt-1">{{ authStore.isAdmin ? '管理员' : '普通用户' }}</p>
</div> </div>
</a-menu-item> </a-menu-item>
<a-menu-divider /> <a-menu-divider />
@@ -184,14 +184,14 @@
</a-dropdown> </a-dropdown>
<!-- Mobile Hamburger Button --> <!-- Mobile Hamburger Button -->
<a-button <button
v-if="isMobile" v-if="isMobile"
type="text" type="button"
class="w-10 h-10 rounded-full flex items-center justify-center hover:bg-surface-container transition-all"
@click="drawerVisible = true" @click="drawerVisible = true"
class="!p-2"
> >
<MenuOutlined class="text-xl text-gray-700 dark:text-gray-300" /> <MenuOutlined class="text-xl text-on-surface" />
</a-button> </button>
</div> </div>
</div> </div>
</nav> </nav>
@@ -204,14 +204,14 @@
title="菜单" title="菜单"
> >
<!-- User Info in Drawer --> <!-- User Info in Drawer -->
<div class="mb-6 pb-4 border-b border-gray-200"> <div class="mb-6 pb-4 border-b border-outline-variant">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<a-avatar :size="48" :style="{ backgroundColor: '#f56a00' }"> <a-avatar :size="48" :style="{ backgroundColor: '#f56a00' }">
{{ userInitial }} {{ userInitial }}
</a-avatar> </a-avatar>
<div> <div>
<p class="font-medium text-gray-900">{{ authStore.user?.alias || '用户' }}</p> <p class="font-medium text-on-surface">{{ authStore.user?.alias || '用户' }}</p>
<p class="text-xs text-gray-500">{{ authStore.isAdmin ? '管理员' : '普通用户' }}</p> <p class="text-xs text-on-surface-variant">{{ authStore.isAdmin ? '管理员' : '普通用户' }}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -364,11 +364,11 @@ const tokenBadgeText = computed(() => {
const tokenIconClass = computed(() => { const tokenIconClass = computed(() => {
const mins = remainingMinutes.value const mins = remainingMinutes.value
if (mins === null) return 'text-gray-500' if (mins === null) return 'text-on-surface-variant'
if (mins < 0) return 'text-red-500' // 已过期 if (mins < 0) return 'text-red-500 dark:text-red-400' // 已过期
if (mins <= 10) return 'text-red-500 animate-pulse' // 10分钟内,闪烁 if (mins <= 10) return 'text-red-500 dark:text-red-400 animate-pulse' // 10分钟内,闪烁
if (mins <= 30) return 'text-orange-500' // 30分钟内 if (mins <= 30) return 'text-orange-500 dark:text-orange-400' // 30分钟内
return 'text-blue-500' // 正常 return 'text-blue-500 dark:text-blue-400' // 正常
}) })
const tokenStatusTooltip = computed(() => { const tokenStatusTooltip = computed(() => {
@@ -0,0 +1,119 @@
<template>
<a-card class="md3-card text-center" style="padding: 48px 20px;">
<!-- 图标 -->
<div v-if="icon" class="mb-6">
<component
:is="icon"
class="text-8xl mx-auto"
:class="iconColorClass"
/>
</div>
<!-- 标题 -->
<h3 class="md3-title-large text-on-surface mb-2">
{{ title || '暂无数据' }}
</h3>
<!-- 描述 -->
<p class="md3-body-medium text-on-surface-variant mb-6">
{{ description || '当前没有内容可显示' }}
</p>
<!-- 操作按钮可选 -->
<div v-if="$slots.action || actionText">
<slot name="action">
<a-button
v-if="actionText"
type="primary"
@click="handleAction"
:loading="loading"
>
<template v-if="actionIcon" #icon>
<component :is="actionIcon" />
</template>
{{ actionText }}
</a-button>
</slot>
</div>
</a-card>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
/**
* 图标组件
*/
icon: {
type: Object,
default: null
},
/**
* 标题文本
*/
title: {
type: String,
default: ''
},
/**
* 描述文本
*/
description: {
type: String,
default: ''
},
/**
* 操作按钮文本
*/
actionText: {
type: String,
default: ''
},
/**
* 操作按钮图标
*/
actionIcon: {
type: Object,
default: null
},
/**
* 加载状态
*/
loading: {
type: Boolean,
default: false
},
/**
* 图标颜色
*/
iconColor: {
type: String,
default: 'neutral',
validator: (v) => ['primary', 'neutral', 'success', 'warning', 'error'].includes(v)
}
})
const emit = defineEmits(['action'])
const handleAction = () => {
emit('action')
}
const iconColorClass = computed(() => {
const colors = {
primary: 'text-primary',
neutral: 'text-on-surface-variant',
success: 'text-green-500',
warning: 'text-orange-500',
error: 'text-error'
}
return colors[props.iconColor]
})
</script>
@@ -0,0 +1,106 @@
<template>
<div v-if="loading" class="loading-state">
<!-- 卡片骨架屏 -->
<div v-if="type === 'card'" class="grid grid-cols-1 gap-4">
<a-card
v-for="i in count"
:key="i"
class="md3-card"
>
<a-skeleton
:active="true"
:paragraph="{ rows: paragraphRows }"
:avatar="showAvatar"
/>
</a-card>
</div>
<!-- 列表骨架屏 -->
<div v-else-if="type === 'list'" class="space-y-4">
<a-card
v-for="i in count"
:key="i"
class="md3-card"
>
<a-skeleton
:active="true"
:paragraph="{ rows: 1 }"
:avatar="showAvatar"
/>
</a-card>
</div>
<!-- 表格骨架屏 -->
<a-card v-else-if="type === 'table'" class="md3-card">
<a-skeleton
:active="true"
:paragraph="{ rows: count * 2 }"
/>
</a-card>
<!-- 默认骨架屏 -->
<a-card v-else class="md3-card">
<a-skeleton
:active="true"
:paragraph="{ rows: paragraphRows }"
:avatar="showAvatar"
/>
</a-card>
</div>
</template>
<script setup>
const props = defineProps({
/**
* 是否显示加载状态
*/
loading: {
type: Boolean,
default: true
},
/**
* 骨架屏类型
*/
type: {
type: String,
default: 'card',
validator: (v) => ['card', 'list', 'table', 'default'].includes(v)
},
/**
* 骨架屏数量
*/
count: {
type: Number,
default: 3
},
/**
* 段落行数
*/
paragraphRows: {
type: Number,
default: 4
},
/**
* 是否显示头像
*/
showAvatar: {
type: Boolean,
default: false
}
})
</script>
<style scoped>
.loading-state {
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
</style>
@@ -0,0 +1,196 @@
<template>
<a-card
class="md3-card animate-slide-up transition-standard hover:elevation-3"
:style="{ animationDelay }"
>
<div class="flex items-center justify-between">
<!-- 数值和标签 -->
<div class="flex-1">
<p class="md3-label-medium text-on-surface-variant mb-1">{{ label }}</p>
<p class="md3-headline-medium" :class="valueColorClass">
{{ formattedValue }}
</p>
<p v-if="subtitle" class="md3-body-small text-on-surface-variant mt-1">
{{ subtitle }}
</p>
</div>
<!-- 图标 -->
<div
v-if="icon"
class="w-12 h-12 rounded-md3 flex items-center justify-center flex-shrink-0 ml-4"
:class="iconBgClass"
>
<component :is="icon" :class="iconColorClass" class="text-2xl" />
</div>
</div>
<!-- 趋势指示器可选 -->
<div v-if="trend !== undefined" class="mt-3 pt-3 border-t border-outline-variant">
<div class="flex items-center text-sm">
<component
:is="trendIcon"
:class="trendColorClass"
class="mr-1"
/>
<span :class="trendColorClass" class="md3-label-small">
{{ trendText }}
</span>
</div>
</div>
</a-card>
</template>
<script setup>
import { computed } from 'vue'
import {
ArrowUpOutlined,
ArrowDownOutlined,
MinusOutlined
} from '@ant-design/icons-vue'
const props = defineProps({
/**
* 卡片标签
*/
label: {
type: String,
required: true
},
/**
* 显示的数值
*/
value: {
type: [String, Number],
required: true
},
/**
* 副标题/描述
*/
subtitle: {
type: String,
default: ''
},
/**
* 图标组件
*/
icon: {
type: Object,
default: null
},
/**
* 颜色主题
*/
color: {
type: String,
default: 'primary',
validator: (v) => ['primary', 'success', 'warning', 'error', 'info', 'neutral'].includes(v)
},
/**
* 动画延迟(秒)
*/
delay: {
type: Number,
default: 0
},
/**
* 格式化函数
*/
formatter: {
type: Function,
default: null
},
/**
* 趋势值(正数上升,负数下降,0持平)
*/
trend: {
type: Number,
default: undefined
},
/**
* 趋势文本
*/
trendText: {
type: String,
default: ''
}
})
// 动画延迟
const animationDelay = computed(() => `${props.delay}s`)
// 格式化数值
const formattedValue = computed(() => {
if (props.formatter) {
return props.formatter(props.value)
}
return props.value
})
// 颜色映射
const colorClasses = {
primary: {
value: 'text-primary',
iconBg: 'bg-primary-100 dark:bg-primary-900/30',
icon: 'text-primary'
},
success: {
value: 'text-green-600 dark:text-green-400',
iconBg: 'bg-green-100 dark:bg-green-900/30',
icon: 'text-green-600 dark:text-green-400'
},
warning: {
value: 'text-orange-600 dark:text-orange-400',
iconBg: 'bg-orange-100 dark:bg-orange-900/30',
icon: 'text-orange-600 dark:text-orange-400'
},
error: {
value: 'text-error',
iconBg: 'bg-red-100 dark:bg-red-900/30',
icon: 'text-error'
},
info: {
value: 'text-secondary',
iconBg: 'bg-blue-100 dark:bg-blue-900/30',
icon: 'text-secondary'
},
neutral: {
value: 'text-on-surface',
iconBg: 'bg-surface-container',
icon: 'text-on-surface-variant'
}
}
const valueColorClass = computed(() => colorClasses[props.color].value)
const iconBgClass = computed(() => colorClasses[props.color].iconBg)
const iconColorClass = computed(() => colorClasses[props.color].icon)
// 趋势图标和颜色
const trendIcon = computed(() => {
if (props.trend === undefined) return null
if (props.trend > 0) return ArrowUpOutlined
if (props.trend < 0) return ArrowDownOutlined
return MinusOutlined
})
const trendColorClass = computed(() => {
if (props.trend === undefined) return ''
if (props.trend > 0) return 'text-green-600 dark:text-green-400'
if (props.trend < 0) return 'text-red-600 dark:text-red-400'
return 'text-on-surface-variant'
})
</script>
<style scoped>
.md3-card:hover {
transform: translateY(-2px);
}
</style>
@@ -0,0 +1,84 @@
/**
* 通用异步操作 Composable
* 统一处理 loading、error 状态和消息提示
*
* @example
* const { loading, error, execute } = useAsyncAction()
*
* const handleSubmit = async () => {
* await execute(
* () => api.createTask(formData),
* { successMsg: '创建成功', errorMsg: '创建失败' }
* )
* }
*/
import { ref } from 'vue'
import { message } from 'ant-design-vue'
export function useAsyncAction(options = {}) {
const loading = ref(false)
const error = ref(null)
/**
* 执行异步操作
* @param {Function} asyncFn - 异步函数
* @param {Object} config - 配置选项
* @param {string} config.successMsg - 成功提示消息
* @param {string} config.errorMsg - 错误提示消息
* @param {boolean} config.throwOnError - 是否抛出错误
* @param {boolean} config.silent - 是否静默模式(不显示消息)
* @returns {Promise} 异步函数的返回值
*/
const execute = async (asyncFn, config = {}) => {
const {
successMsg = options.successMsg,
errorMsg = options.errorMsg,
throwOnError = false,
silent = false
} = config
loading.value = true
error.value = null
try {
const result = await asyncFn()
if (!silent && successMsg) {
message.success(successMsg)
}
return result
} catch (err) {
error.value = err
if (!silent) {
const msg = err.message || err.detail || errorMsg || '操作失败'
message.error(msg)
}
if (throwOnError) {
throw err
}
return null
} finally {
loading.value = false
}
}
/**
* 重置状态
*/
const reset = () => {
loading.value = false
error.value = null
}
return {
loading,
error,
execute,
reset
}
}
+128
View File
@@ -0,0 +1,128 @@
/**
* 状态轮询 Composable
* 支持指数退避、最大重试次数、自动清理
*
* @example
* const { polling, startPolling, stopPolling } = usePollStatus({
* interval: 2000,
* maxRetries: 15,
* backoff: true
* })
*
* startPolling(
* async () => {
* const status = await api.getStatus(id)
* return {
* completed: status.status !== 'pending',
* success: status.status === 'success',
* data: status
* }
* },
* {
* onSuccess: (result) => console.log('完成', result),
* onFailure: (error) => console.error('失败', error),
* onTimeout: () => console.warn('超时')
* }
* )
*/
import { ref, onUnmounted } from 'vue'
export function usePollStatus(options = {}) {
const {
interval = 2000, // 初始轮询间隔(毫秒)
maxRetries = 15, // 最大重试次数
backoff = false, // 是否使用指数退避
maxBackoffInterval = 10000 // 最大退避间隔(毫秒)
} = options
const polling = ref(false)
let pollTimer = null
let retryCount = 0
/**
* 开始轮询
* @param {Function} checkFn - 检查函数,应返回 { completed, success, data }
* @param {Object} callbacks - 回调函数
* @param {Function} callbacks.onSuccess - 成功回调
* @param {Function} callbacks.onFailure - 失败回调
* @param {Function} callbacks.onTimeout - 超时回调
*/
const startPolling = async (checkFn, callbacks = {}) => {
const { onSuccess, onFailure, onTimeout } = callbacks
// 重置状态
stopPolling()
polling.value = true
retryCount = 0
const poll = async () => {
try {
const result = await checkFn()
// 检查是否完成
if (result.completed) {
stopPolling()
if (result.success) {
onSuccess?.(result.data || result)
} else {
onFailure?.(result.data || result)
}
return
}
// 检查是否超时
retryCount++
if (retryCount >= maxRetries) {
stopPolling()
onTimeout?.()
return
}
// 计算下次轮询间隔(支持指数退避)
let nextInterval = interval
if (backoff) {
// 指数退避:2s -> 4s -> 8s -> 最大10s
nextInterval = Math.min(
interval * Math.pow(2, retryCount - 1),
maxBackoffInterval
)
}
// 继续轮询
pollTimer = setTimeout(poll, nextInterval)
} catch (error) {
stopPolling()
onFailure?.(error)
}
}
// 立即执行第一次检查
poll()
}
/**
* 停止轮询
*/
const stopPolling = () => {
if (pollTimer) {
clearTimeout(pollTimer)
pollTimer = null
}
polling.value = false
retryCount = 0
}
// 组件卸载时自动清理
onUnmounted(() => {
stopPolling()
})
return {
polling,
startPolling,
stopPolling
}
}
+958 -885
View File
File diff suppressed because it is too large Load Diff
+43 -8
View File
@@ -4,7 +4,7 @@
<a-row :gutter="[20, 20]"> <a-row :gutter="[20, 20]">
<!-- Token 状态卡片 --> <!-- Token 状态卡片 -->
<a-col :xs="24" :sm="24" :md="24"> <a-col :xs="24" :sm="24" :md="24">
<a-card class="status-card"> <a-card class="status-card md3-card">
<template #title> <template #title>
<div class="card-header"> <div class="card-header">
<KeyOutlined /> <KeyOutlined />
@@ -60,7 +60,7 @@
<!-- 手动打卡卡片 --> <!-- 手动打卡卡片 -->
<a-col :xs="24" :sm="24" :md="24"> <a-col :xs="24" :sm="24" :md="24">
<a-card> <a-card class="md3-card">
<template #title> <template #title>
<div class="card-header"> <div class="card-header">
<CalendarOutlined /> <CalendarOutlined />
@@ -123,7 +123,7 @@
}} }}
</a-tag> </a-tag>
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item label="打卡响应" :span="2"> <a-descriptions-item label="打卡响应" :span="{ xs: 1, sm: 1, md: 2 }">
{{ lastCheckIn.response_text || lastCheckIn.error_message || '-' }} {{ lastCheckIn.response_text || lastCheckIn.error_message || '-' }}
</a-descriptions-item> </a-descriptions-item>
</a-descriptions> </a-descriptions>
@@ -134,7 +134,7 @@
<!-- 用户信息卡片 --> <!-- 用户信息卡片 -->
<a-col :xs="24" :sm="24" :md="24"> <a-col :xs="24" :sm="24" :md="24">
<a-card> <a-card class="md3-card">
<template #title> <template #title>
<div class="card-header"> <div class="card-header">
<UserOutlined /> <UserOutlined />
@@ -151,10 +151,10 @@
{{ authStore.isAdmin ? '管理员' : '普通用户' }} {{ authStore.isAdmin ? '管理员' : '普通用户' }}
</a-tag> </a-tag>
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item label="邮箱" :span="2"> <a-descriptions-item label="邮箱">
{{ authStore.user?.email || '未设置' }} {{ authStore.user?.email || '未设置' }}
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item label="注册时间" :span="2"> <a-descriptions-item label="注册时间">
{{ formatDateTime(authStore.user?.created_at, false) }} {{ formatDateTime(authStore.user?.created_at, false) }}
</a-descriptions-item> </a-descriptions-item>
</a-descriptions> </a-descriptions>
@@ -335,23 +335,58 @@ onMounted(async () => {
margin: 0 auto; margin: 0 auto;
} }
.card-header {
display: flex;
align-items: center;
gap: 12px;
font-size: 18px;
font-weight: 500;
}
.loading-container { .loading-container {
padding: 20px; padding: 20px;
} }
.token-status { .token-status {
padding: 10px 0; padding: 0;
}
.token-status .ant-descriptions {
margin-bottom: 0;
} }
.check-in-container { .check-in-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding: 20px; padding: 16px 20px;
gap: 12px;
}
.check-in-container .hint {
color: var(--md-sys-color-on-surface-variant);
font-size: 14px;
margin: 0 0 4px 0;
text-align: center;
} }
.last-check-in { .last-check-in {
width: 100%; width: 100%;
margin-top: 20px; margin-top: 20px;
} }
.last-check-in .label {
font-size: 14px;
font-weight: 500;
color: var(--md-sys-color-on-surface-variant);
margin: 12px 0 8px 0;
}
.ant-alert {
margin-top: 16px;
}
.ant-select {
margin-bottom: 0;
}
</style> </style>
+10 -10
View File
@@ -2,11 +2,11 @@
<Layout> <Layout>
<div class="settings-view"> <div class="settings-view">
<div class="max-w-4xl mx-auto"> <div class="max-w-4xl mx-auto">
<h1 class="text-3xl font-bold text-gray-800 dark:text-gray-100 mb-6">个人设置</h1> <h1 class="text-3xl font-bold text-on-surface mb-6">个人设置</h1>
<!-- 基本信息卡片 --> <!-- 基本信息卡片 -->
<div class="md3-card p-6 mb-6"> <a-card class="md3-card mb-6">
<h2 class="text-xl font-bold text-gray-800 dark:text-gray-100 mb-4 flex items-center"> <h2 class="text-xl font-bold text-on-surface mb-4 flex items-center">
<UserOutlined class="mr-2" /> <UserOutlined class="mr-2" />
基本信息 基本信息
</h2> </h2>
@@ -28,11 +28,11 @@
{{ formatDate(user?.created_at) }} {{ formatDate(user?.created_at) }}
</a-descriptions-item> </a-descriptions-item>
</a-descriptions> </a-descriptions>
</div> </a-card>
<!-- 修改邮箱 --> <!-- 修改邮箱 -->
<div class="md3-card p-6 mb-6"> <a-card class="md3-card mb-6">
<h2 class="text-xl font-bold text-gray-800 dark:text-gray-100 mb-4 flex items-center"> <h2 class="text-xl font-bold text-on-surface mb-4 flex items-center">
<EditOutlined class="mr-2" /> <EditOutlined class="mr-2" />
修改个人信息 修改个人信息
</h2> </h2>
@@ -73,11 +73,11 @@
</a-space> </a-space>
</a-form-item> </a-form-item>
</a-form> </a-form>
</div> </a-card>
<!-- 设置/修改密码 --> <!-- 设置/修改密码 -->
<div class="md3-card p-6"> <a-card class="md3-card">
<h2 class="text-xl font-bold text-gray-800 dark:text-gray-100 mb-4 flex items-center"> <h2 class="text-xl font-bold text-on-surface mb-4 flex items-center">
<KeyOutlined class="mr-2" /> <KeyOutlined class="mr-2" />
{{ hasPassword ? '修改密码' : '设置密码' }} {{ hasPassword ? '修改密码' : '设置密码' }}
</h2> </h2>
@@ -136,7 +136,7 @@
</a-space> </a-space>
</a-form-item> </a-form-item>
</a-form> </a-form>
</div> </a-card>
</div> </div>
</div> </div>
</Layout> </Layout>
+46 -46
View File
@@ -13,11 +13,11 @@
返回任务列表 返回任务列表
</a-button> </a-button>
<div v-if="currentTask" class="fluent-card p-6"> <a-card v-if="currentTask" class="md3-card">
<div class="flex items-start justify-between"> <div class="flex items-start justify-between">
<div class="flex-1"> <div class="flex-1">
<h1 class="text-3xl font-bold text-gradient mb-2">{{ currentTask.name || '未命名任务' }}</h1> <h1 class="text-3xl font-bold text-gradient mb-2">{{ currentTask.name || '未命名任务' }}</h1>
<div class="flex items-center gap-4 text-sm text-gray-600"> <div class="flex items-center gap-4 text-sm text-on-surface-variant">
<span class="flex items-center"> <span class="flex items-center">
<NumberOutlined class="mr-1" /> <NumberOutlined class="mr-1" />
接龙 ID: {{ getThreadId(currentTask) }} 接龙 ID: {{ getThreadId(currentTask) }}
@@ -35,54 +35,54 @@
{{ checkInLoading ? '打卡中...' : '立即打卡' }} {{ checkInLoading ? '打卡中...' : '立即打卡' }}
</a-button> </a-button>
</div> </div>
</div> </a-card>
</div> </div>
<!-- Stats Summary --> <!-- Stats Summary -->
<a-row :gutter="[16, 16]" class="mb-6"> <a-row :gutter="[16, 16]" class="mb-6">
<a-col :xs="12" :sm="8" :md="4"> <a-col :xs="12" :sm="8" :md="4">
<div class="fluent-card p-5 animate-slide-up"> <a-card class="md3-card animate-slide-up">
<p class="text-sm text-gray-600 mb-1">总打卡次数</p> <p class="text-sm text-on-surface-variant mb-1">总打卡次数</p>
<p class="text-2xl font-bold text-gray-800">{{ recordStats.total }}</p> <p class="text-2xl font-bold text-on-surface">{{ recordStats.total }}</p>
</div> </a-card>
</a-col> </a-col>
<a-col :xs="12" :sm="8" :md="4"> <a-col :xs="12" :sm="8" :md="4">
<div class="fluent-card p-5 animate-slide-up" style="animation-delay: 0.05s"> <a-card class="md3-card animate-slide-up" style="animation-delay: 0.05s">
<p class="text-sm text-gray-600 mb-1">成功次数</p> <p class="text-sm text-on-surface-variant mb-1">成功次数</p>
<p class="text-2xl font-bold text-green-600">{{ recordStats.success }}</p> <p class="text-2xl font-bold text-green-600 dark:text-green-400">{{ recordStats.success }}</p>
</div> </a-card>
</a-col> </a-col>
<a-col :xs="12" :sm="8" :md="4"> <a-col :xs="12" :sm="8" :md="4">
<div class="fluent-card p-5 animate-slide-up" style="animation-delay: 0.1s"> <a-card class="md3-card animate-slide-up" style="animation-delay: 0.1s">
<p class="text-sm text-gray-600 mb-1">时间范围外</p> <p class="text-sm text-on-surface-variant mb-1">时间范围外</p>
<p class="text-2xl font-bold text-blue-600">{{ recordStats.outOfTime }}</p> <p class="text-2xl font-bold text-blue-600 dark:text-blue-400">{{ recordStats.outOfTime }}</p>
</div> </a-card>
</a-col> </a-col>
<a-col :xs="12" :sm="8" :md="4"> <a-col :xs="12" :sm="8" :md="4">
<div class="fluent-card p-5 animate-slide-up" style="animation-delay: 0.15s"> <a-card class="md3-card animate-slide-up" style="animation-delay: 0.15s">
<p class="text-sm text-gray-600 mb-1">失败次数</p> <p class="text-sm text-on-surface-variant mb-1">失败次数</p>
<p class="text-2xl font-bold text-red-600">{{ recordStats.failure }}</p> <p class="text-2xl font-bold text-red-600 dark:text-red-400">{{ recordStats.failure }}</p>
</div> </a-card>
</a-col> </a-col>
<a-col :xs="12" :sm="8" :md="4"> <a-col :xs="12" :sm="8" :md="4">
<div class="fluent-card p-5 animate-slide-up" style="animation-delay: 0.2s"> <a-card class="md3-card animate-slide-up" style="animation-delay: 0.2s">
<p class="text-sm text-gray-600 mb-1">异常次数</p> <p class="text-sm text-on-surface-variant mb-1">异常次数</p>
<p class="text-2xl font-bold text-orange-600">{{ recordStats.unknown }}</p> <p class="text-2xl font-bold text-orange-600 dark:text-orange-400">{{ recordStats.unknown }}</p>
</div> </a-card>
</a-col> </a-col>
<a-col :xs="12" :sm="8" :md="4"> <a-col :xs="12" :sm="8" :md="4">
<div class="fluent-card p-5 animate-slide-up" style="animation-delay: 0.25s"> <a-card class="md3-card animate-slide-up" style="animation-delay: 0.25s">
<p class="text-sm text-gray-600 mb-1">成功率</p> <p class="text-sm text-on-surface-variant mb-1">成功率</p>
<p class="text-2xl font-bold text-purple-600">{{ recordStats.successRate }}%</p> <p class="text-2xl font-bold text-purple-600 dark:text-purple-400">{{ recordStats.successRate }}%</p>
</div> </a-card>
</a-col> </a-col>
</a-row> </a-row>
<!-- Filters --> <!-- Filters -->
<div class="fluent-card p-4 mb-6"> <a-card class="md3-card mb-6">
<a-space wrap :size="[16, 16]"> <a-space wrap :size="[16, 16]">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-sm font-medium text-gray-700">状态筛选:</span> <span class="text-sm font-medium text-on-surface">状态筛选:</span>
<a-radio-group v-model:value="filterStatus" button-style="solid" size="small" @change="handleFilterChange"> <a-radio-group v-model:value="filterStatus" button-style="solid" size="small" @change="handleFilterChange">
<a-radio-button value="">全部</a-radio-button> <a-radio-button value="">全部</a-radio-button>
<a-radio-button value="success">成功</a-radio-button> <a-radio-button value="success">成功</a-radio-button>
@@ -93,7 +93,7 @@
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-sm font-medium text-gray-700">触发方式:</span> <span class="text-sm font-medium text-on-surface">触发方式:</span>
<a-radio-group v-model:value="filterTrigger" button-style="solid" size="small" @change="handleFilterChange"> <a-radio-group v-model:value="filterTrigger" button-style="solid" size="small" @change="handleFilterChange">
<a-radio-button value="">全部</a-radio-button> <a-radio-button value="">全部</a-radio-button>
<a-radio-button value="scheduler">自动</a-radio-button> <a-radio-button value="scheduler">自动</a-radio-button>
@@ -106,7 +106,7 @@
刷新 刷新
</a-button> </a-button>
</a-space> </a-space>
</div> </a-card>
<!-- Records List --> <!-- Records List -->
<div v-if="loading" class="space-y-4"> <div v-if="loading" class="space-y-4">
@@ -115,22 +115,22 @@
</a-card> </a-card>
</div> </div>
<div v-else-if="records.length === 0" class="fluent-card p-12 text-center"> <a-card v-else-if="records.length === 0" class="md3-card text-center" style="padding: 48px 20px;">
<FileTextOutlined class="text-8xl text-gray-300 mb-4" /> <FileTextOutlined class="text-8xl text-on-surface-variant opacity-30 mb-4" />
<h3 class="text-xl font-semibold text-gray-700 mb-2">暂无打卡记录</h3> <h3 class="text-xl font-semibold text-on-surface mb-2">暂无打卡记录</h3>
<p class="text-gray-500">当前筛选条件下没有找到任何打卡记录</p> <p class="text-on-surface-variant">当前筛选条件下没有找到任何打卡记录</p>
</div> </a-card>
<div v-else class="space-y-4"> <div v-else class="space-y-4">
<div <a-card
v-for="record in records" v-for="record in records"
:key="record.id" :key="record.id"
class="fluent-card p-6 hover:shadow-xl transition-all animate-slide-up" class="md3-card hover:shadow-xl transition-all animate-slide-up"
> >
<div class="flex items-start justify-between mb-4"> <div class="flex items-start justify-between mb-4">
<div class="flex-1"> <div class="flex-1">
<div class="flex items-center gap-3 mb-2 flex-wrap"> <div class="flex items-center gap-3 mb-2 flex-wrap">
<h3 class="text-lg font-semibold text-gray-800"> <h3 class="text-lg font-semibold text-on-surface">
打卡记录 #{{ record.id }} 打卡记录 #{{ record.id }}
</h3> </h3>
<a-tag <a-tag
@@ -153,7 +153,7 @@
{{ record.trigger_type === 'scheduled' ? '自动触发' : '手动触发' }} {{ record.trigger_type === 'scheduled' ? '自动触发' : '手动触发' }}
</a-tag> </a-tag>
</div> </div>
<div class="flex items-center text-sm text-gray-600"> <div class="flex items-center text-sm text-on-surface-variant">
<ClockCircleOutlined class="mr-1" /> <ClockCircleOutlined class="mr-1" />
{{ formatDateTime(record.check_in_time) }} {{ formatDateTime(record.check_in_time) }}
</div> </div>
@@ -161,18 +161,18 @@
</div> </div>
<!-- Record Details --> <!-- Record Details -->
<div class="bg-gray-50 rounded-lg p-4 space-y-2"> <div class="bg-surface-container-high dark:bg-surface-container rounded-lg p-4 space-y-2">
<div v-if="record.response_text" class="flex items-start"> <div v-if="record.response_text" class="flex items-start">
<span class="text-sm font-medium text-gray-700 w-20">响应:</span> <span class="text-sm font-medium text-on-surface-variant w-20">响应:</span>
<span class="text-sm text-gray-900 flex-1">{{ record.response_text }}</span> <span class="text-sm text-on-surface flex-1">{{ record.response_text }}</span>
</div> </div>
<div v-if="record.error_message" class="flex items-start"> <div v-if="record.error_message" class="flex items-start">
<span class="text-sm font-medium text-red-700 w-20">错误:</span> <span class="text-sm font-medium text-error w-20">错误:</span>
<span class="text-sm text-red-600 flex-1">{{ record.error_message }}</span> <span class="text-sm text-error flex-1">{{ record.error_message }}</span>
</div> </div>
</div> </div>
</div> </a-card>
</div> </div>
<!-- Pagination --> <!-- Pagination -->
+45 -44
View File
@@ -7,7 +7,7 @@
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<div> <div>
<h1 class="text-4xl font-bold text-gradient mb-2">任务管理</h1> <h1 class="text-4xl font-bold text-gradient mb-2">任务管理</h1>
<p class="text-gray-600 dark:text-gray-400">管理您的自动打卡任务</p> <p class="text-on-surface-variant">管理您的自动打卡任务</p>
</div> </div>
<a-button <a-button
type="primary" type="primary"
@@ -25,45 +25,45 @@
<!-- Stats Cards --> <!-- Stats Cards -->
<a-row :gutter="[16, 16]" class="mb-6"> <a-row :gutter="[16, 16]" class="mb-6">
<a-col :xs="24" :sm="8" :md="8"> <a-col :xs="24" :sm="8" :md="8">
<div class="fluent-card p-6 animate-slide-up"> <a-card class="md3-card animate-slide-up">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">总任务数</p> <p class="text-sm text-on-surface-variant mb-1">总任务数</p>
<p class="text-3xl font-bold text-primary-600 dark:text-primary-400">{{ taskStore.taskStats.total }}</p> <p class="text-3xl font-bold text-primary">{{ taskStore.taskStats.total }}</p>
</div> </div>
<div class="w-12 h-12 bg-primary-100 dark:bg-primary-900/30 rounded-md3 flex items-center justify-center"> <div class="w-12 h-12 bg-primary-100 dark:bg-primary-900/30 rounded-md3 flex items-center justify-center">
<FileTextOutlined class="text-2xl text-primary-600 dark:text-primary-400" /> <FileTextOutlined class="text-2xl text-primary" />
</div> </div>
</div> </div>
</div> </a-card>
</a-col> </a-col>
<a-col :xs="24" :sm="8" :md="8"> <a-col :xs="24" :sm="8" :md="8">
<div class="fluent-card p-6 animate-slide-up" style="animation-delay: 0.1s"> <a-card class="md3-card animate-slide-up" style="animation-delay: 0.1s">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">启用中</p> <p class="text-sm text-on-surface-variant mb-1">启用中</p>
<p class="text-3xl font-bold text-green-600 dark:text-green-400">{{ taskStore.taskStats.active }}</p> <p class="text-3xl font-bold text-green-600 dark:text-green-400">{{ taskStore.taskStats.active }}</p>
</div> </div>
<div class="w-12 h-12 bg-green-100 dark:bg-green-900/30 rounded-md3 flex items-center justify-center"> <div class="w-12 h-12 bg-green-100 dark:bg-green-900/30 rounded-md3 flex items-center justify-center">
<CheckCircleOutlined class="text-2xl text-green-600 dark:text-green-400" /> <CheckCircleOutlined class="text-2xl text-green-600 dark:text-green-400" />
</div> </div>
</div> </div>
</div> </a-card>
</a-col> </a-col>
<a-col :xs="24" :sm="8" :md="8"> <a-col :xs="24" :sm="8" :md="8">
<div class="fluent-card p-6 animate-slide-up" style="animation-delay: 0.2s"> <a-card class="md3-card animate-slide-up" style="animation-delay: 0.2s">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">已禁用</p> <p class="text-sm text-on-surface-variant mb-1">已禁用</p>
<p class="text-3xl font-bold text-gray-600 dark:text-gray-300">{{ taskStore.taskStats.inactive }}</p> <p class="text-3xl font-bold text-on-surface-variant">{{ taskStore.taskStats.inactive }}</p>
</div> </div>
<div class="w-12 h-12 bg-gray-100 dark:bg-gray-700 rounded-md3 flex items-center justify-center"> <div class="w-12 h-12 bg-surface-container-high rounded-md3 flex items-center justify-center">
<StopOutlined class="text-2xl text-gray-600 dark:text-gray-300" /> <StopOutlined class="text-2xl text-on-surface-variant" />
</div> </div>
</div> </div>
</div> </a-card>
</a-col> </a-col>
</a-row> </a-row>
</div> </div>
@@ -79,14 +79,14 @@
</a-row> </a-row>
</div> </div>
<div v-else-if="taskStore.tasks.length === 0" class="fluent-card p-12 text-center"> <a-card v-else-if="taskStore.tasks.length === 0" class="md3-card text-center" style="padding: 48px 20px;">
<FileTextOutlined class="text-8xl text-gray-300 mb-4" /> <FileTextOutlined class="text-8xl text-on-surface-variant opacity-30 mb-4" />
<h3 class="text-xl font-semibold text-gray-700 mb-2">暂无任务</h3> <h3 class="text-xl font-semibold text-on-surface mb-2">暂无任务</h3>
<p class="text-gray-500 mb-6">点击右上角的"创建任务"按钮开始添加您的第一个打卡任务</p> <p class="text-on-surface-variant mb-6">点击右上角的"创建任务"按钮开始添加您的第一个打卡任务</p>
<a-button type="primary" @click="showCreateDialog = true"> <a-button type="primary" @click="showCreateDialog = true">
创建第一个任务 创建第一个任务
</a-button> </a-button>
</div> </a-card>
<a-row v-else :gutter="[16, 16]"> <a-row v-else :gutter="[16, 16]">
<a-col <a-col
@@ -94,15 +94,16 @@
v-for="task in taskStore.tasks" v-for="task in taskStore.tasks"
:key="task.id" :key="task.id"
> >
<div <a-card
class="fluent-card p-6 hover:scale-105 transform transition-all cursor-pointer animate-slide-up" class="md3-card hover:scale-105 transform transition-all cursor-pointer animate-slide-up"
@click="viewTask(task)" @click="viewTask(task)"
> >
<!-- Task Header --> <!-- Task Header -->
<div class="flex items-start justify-between mb-4"> <div class="flex items-start justify-between mb-4">
<div class="flex-1"> <div class="flex-1">
<h3 class="text-lg font-semibold text-gray-800 mb-1">{{ task.name || '未命名任务' }}</h3> <h3 class="text-lg font-semibold text-on-surface mb-1">{{ task.name || '未命名任务' }}</h3>
<p class="text-sm text-gray-500">任务 ID: {{ task.id }}</p> <a-divider style="margin: 8px 0;" />
<p class="text-sm text-on-surface-variant">任务 ID: {{ task.id }}</p>
</div> </div>
<a-tag :color="task.is_active ? 'success' : 'default'"> <a-tag :color="task.is_active ? 'success' : 'default'">
{{ task.is_active ? '启用' : '禁用' }} {{ task.is_active ? '启用' : '禁用' }}
@@ -111,21 +112,21 @@
<!-- Task Details --> <!-- Task Details -->
<div class="space-y-2 mb-4"> <div class="space-y-2 mb-4">
<div class="flex items-center text-sm text-gray-600"> <div class="flex items-center text-sm text-on-surface-variant">
<TagOutlined class="mr-2" /> <TagOutlined class="mr-2" />
接龙ID: {{ getThreadId(task) }} 接龙ID: {{ getThreadId(task) }}
</div> </div>
<div class="flex items-center text-sm text-gray-600"> <div class="flex items-center text-sm text-on-surface-variant">
<ClockCircleOutlined class="mr-2" /> <ClockCircleOutlined class="mr-2" />
最后打卡: {{ task.last_check_in_time ? formatDateTime(task.last_check_in_time) : '未打卡' }} 最后打卡: {{ task.last_check_in_time ? formatDateTime(task.last_check_in_time) : '未打卡' }}
</div> </div>
<div class="flex items-center text-sm"> <div class="flex items-center text-sm">
<CheckCircleOutlined class="mr-2 text-gray-600" /> <CheckCircleOutlined class="mr-2 text-on-surface-variant" />
<span v-if="task.last_check_in_status" :class="{ <span v-if="task.last_check_in_status" :class="{
'text-green-600 font-medium': task.last_check_in_status === 'success', 'text-green-600 dark:text-green-400 font-medium': task.last_check_in_status === 'success',
'text-blue-600 font-medium': task.last_check_in_status === 'out_of_time', 'text-blue-600 dark:text-blue-400 font-medium': task.last_check_in_status === 'out_of_time',
'text-red-600 font-medium': task.last_check_in_status === 'failure', 'text-red-600 dark:text-red-400 font-medium': task.last_check_in_status === 'failure',
'text-yellow-600 font-medium': task.last_check_in_status === 'unknown' 'text-yellow-600 dark:text-yellow-400 font-medium': task.last_check_in_status === 'unknown'
}"> }">
{{ {{
task.last_check_in_status === 'success' ? '✅ 打卡成功' : task.last_check_in_status === 'success' ? '✅ 打卡成功' :
@@ -134,12 +135,12 @@
'❗ 打卡异常' '❗ 打卡异常'
}} }}
</span> </span>
<span v-else class="text-gray-500">暂无打卡记录</span> <span v-else class="text-on-surface-variant">暂无打卡记录</span>
</div> </div>
</div> </div>
<!-- Task Actions --> <!-- Task Actions -->
<div class="flex gap-2 pt-4 border-t border-gray-100"> <div class="flex gap-2 pt-4 border-t border-outline-variant">
<a-button <a-button
type="primary" type="primary"
size="small" size="small"
@@ -174,7 +175,7 @@
<template #icon><DeleteOutlined /></template> <template #icon><DeleteOutlined /></template>
</a-button> </a-button>
</div> </div>
</div> </a-card>
</a-col> </a-col>
</a-row> </a-row>
</div> </div>
@@ -192,12 +193,12 @@
<div v-if="!editingTask"> <div v-if="!editingTask">
<div v-if="loadingTemplates" class="text-center py-8"> <div v-if="loadingTemplates" class="text-center py-8">
<a-spin size="large" /> <a-spin size="large" />
<p class="text-gray-500 mt-2">加载模板中...</p> <p class="text-on-surface-variant mt-2">加载模板中...</p>
</div> </div>
<div v-else-if="activeTemplates.length === 0" class="text-center py-8"> <div v-else-if="activeTemplates.length === 0" class="text-center py-8">
<p class="text-gray-500">暂无可用模板</p> <p class="text-on-surface-variant">暂无可用模板</p>
<p class="text-sm text-gray-400 mt-2">请联系管理员创建模板</p> <p class="text-sm text-on-surface-variant opacity-70 mt-2">请联系管理员创建模板</p>
</div> </div>
<div v-else> <div v-else>
@@ -208,10 +209,10 @@
v-for="template in activeTemplates" v-for="template in activeTemplates"
:key="template.id" :key="template.id"
@click="selectTemplate(template)" @click="selectTemplate(template)"
class="border rounded-lg p-4 cursor-pointer hover:border-primary-500 hover:bg-primary-50 transition-all" class="border border-outline-variant rounded-lg p-4 cursor-pointer hover:border-primary hover:bg-primary-container/10 transition-all"
> >
<h4 class="font-semibold text-gray-800 mb-1">{{ template.name }}</h4> <h4 class="font-semibold text-on-surface mb-1">{{ template.name }}</h4>
<p class="text-sm text-gray-600">{{ template.description || '无描述' }}</p> <p class="text-sm text-on-surface-variant">{{ template.description || '无描述' }}</p>
</div> </div>
</div> </div>
</a-form-item> </a-form-item>
@@ -281,7 +282,7 @@
</a-select-option> </a-select-option>
</a-select> </a-select>
<span v-if="fieldConfig.default_value" class="text-xs text-gray-500 mt-1"> <span v-if="fieldConfig.default_value" class="text-xs text-on-surface-variant mt-1">
默认值: {{ fieldConfig.default_value }} 默认值: {{ fieldConfig.default_value }}
</span> </span>
</a-form-item> </a-form-item>
@@ -298,7 +299,7 @@
<a-form-item label="启用状态"> <a-form-item label="启用状态">
<a-switch v-model:checked="taskForm.is_active" /> <a-switch v-model:checked="taskForm.is_active" />
<span class="ml-2 text-sm text-gray-500"> <span class="ml-2 text-sm text-on-surface-variant">
{{ taskForm.is_active ? '启用自动打卡' : '禁用自动打卡(仍可手动打卡)' }} {{ taskForm.is_active ? '启用自动打卡' : '禁用自动打卡(仍可手动打卡)' }}
</span> </span>
</a-form-item> </a-form-item>
@@ -312,7 +313,7 @@
<div class="mb-4"> <div class="mb-4">
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">
<span class="text-sm text-gray-600">完整的打卡请求配置</span> <span class="text-sm text-on-surface-variant">完整的打卡请求配置</span>
<a-button <a-button
size="small" size="small"
type="primary" type="primary"
@@ -330,7 +331,7 @@
class="font-mono text-xs" class="font-mono text-xs"
style="resize: vertical; min-height: 200px; max-height: 400px;" style="resize: vertical; min-height: 200px; max-height: 400px;"
/> />
<p class="text-xs text-gray-500 mt-1"> <p class="text-xs text-on-surface-variant mt-1">
💡 此配置由模板自动生成如需修改请删除任务后从模板重新创建 💡 此配置由模板自动生成如需修改请删除任务后从模板重新创建
</p> </p>
</div> </div>
+31 -30
View File
@@ -7,7 +7,7 @@
<div class="flex items-center justify-between mb-6"> <div class="flex items-center justify-between mb-6">
<div> <div>
<h1 class="text-3xl font-bold text-gradient mb-2">任务模板管理</h1> <h1 class="text-3xl font-bold text-gradient mb-2">任务模板管理</h1>
<p class="text-gray-600 dark:text-gray-400">JSON 映射架构 - 配置即结构字段名保持原样</p> <p class="text-on-surface-variant">JSON 映射架构 - 配置即结构</p>
</div> </div>
<button @click="showCreateDialog" class="md3-button-filled"> <button @click="showCreateDialog" class="md3-button-filled">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -20,37 +20,38 @@
<!-- Templates List --> <!-- Templates List -->
<div v-if="loading && templates.length === 0" class="space-y-4"> <div v-if="loading && templates.length === 0" class="space-y-4">
<div v-for="i in 3" :key="i" class="fluent-card p-6"> <a-card v-for="i in 3" :key="i" class="md3-card">
<a-skeleton :active="true" :paragraph="{ rows: 2 }" /> <a-skeleton :active="true" :paragraph="{ rows: 2 }" />
</div> </a-card>
</div> </div>
<div v-else-if="templates.length === 0" class="fluent-card p-12 text-center"> <a-card v-else-if="templates.length === 0" class="md3-card text-center" style="padding: 48px 20px;">
<svg class="w-20 h-20 mx-auto text-gray-300 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-20 h-20 mx-auto text-on-surface-variant opacity-30 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg> </svg>
<h3 class="text-xl font-semibold text-gray-700 dark:text-gray-200 mb-2">暂无模板</h3> <h3 class="text-xl font-semibold text-on-surface mb-2">暂无模板</h3>
<p class="text-gray-500 dark:text-gray-400 mb-4">创建第一个模板让用户更轻松地创建打卡任务</p> <p class="text-on-surface-variant mb-4">创建第一个模板让用户更轻松地创建打卡任务</p>
<button @click="showCreateDialog" class="md3-button-filled">新建模板</button> <button @click="showCreateDialog" class="md3-button-filled">新建模板</button>
</div> </a-card>
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div <a-card
v-for="template in templates" v-for="template in templates"
:key="template.id" :key="template.id"
class="fluent-card p-7 hover:shadow-xl transition-all animate-slide-up" class="md3-card hover:shadow-xl transition-all animate-slide-up"
> >
<div class="flex items-start justify-between mb-5"> <div class="flex items-start justify-between mb-3">
<div class="flex-1"> <div class="flex-1">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-100 mb-2">{{ template.name }}</h3> <h3 class="text-lg font-semibold text-on-surface mb-2">{{ template.name }}</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3">{{ template.description || '无描述' }}</p> <a-divider style="margin: 8px 0;" />
<span :class="template.is_active ? 'status-success' : 'status-info'"> <p class="text-sm text-on-surface-variant mb-2">{{ template.description || '无描述' }}</p>
<span :class="template.is_active ? 'md3-badge-success' : 'md3-badge-info'">
{{ template.is_active ? '已启用' : '已禁用' }} {{ template.is_active ? '已启用' : '已禁用' }}
</span> </span>
</div> </div>
</div> </div>
<div class="mt-5 pt-4 border-t border-gray-100 space-y-2"> <div class="mt-3 pt-3 border-t border-outline-variant space-y-2">
<!-- 第一行预览在左半部分居中编辑在右半部分居中 --> <!-- 第一行预览在左半部分居中编辑在右半部分居中 -->
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
<div class="flex justify-center"> <div class="flex justify-center">
@@ -77,7 +78,7 @@
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
<div></div> <div></div>
<div class="flex justify-center"> <div class="flex justify-center">
<button @click="deleteTemplate(template)" class="md3-button-text text-sm !text-red-600 dark:!text-red-500 flex-shrink-0"> <button @click="deleteTemplate(template)" class="md3-button-outlined text-sm !text-red-600 dark:!text-red-500 !border-red-600 dark:!border-red-500 hover:!bg-red-50 dark:hover:!bg-red-900/20 flex-shrink-0">
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg> </svg>
@@ -86,7 +87,7 @@
</div> </div>
</div> </div>
</div> </div>
</div> </a-card>
</div> </div>
<!-- Create/Edit Dialog --> <!-- Create/Edit Dialog -->
@@ -156,7 +157,7 @@
<!-- 字段配置编辑器 --> <!-- 字段配置编辑器 -->
<div class="field-config-editor"> <div class="field-config-editor">
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold text-gray-800 dark:text-gray-100">字段配置</h3> <h3 class="text-lg font-bold text-on-surface">字段配置</h3>
<a-dropdown> <a-dropdown>
<a-button type="primary"> <a-button type="primary">
添加字段 添加字段
@@ -188,12 +189,12 @@
</div> </div>
<!-- 递归渲染字段树 --> <!-- 递归渲染字段树 -->
<div v-if="Object.keys(formData.field_config).length === 0" class="text-center py-12 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg bg-gray-50 dark:bg-gray-800/50"> <div v-if="Object.keys(formData.field_config).length === 0" class="text-center py-12 border-2 border-dashed border-outline-variant rounded-lg bg-surface-container">
<svg class="w-16 h-16 mx-auto text-gray-400 dark:text-gray-500 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-16 h-16 mx-auto text-on-surface-variant opacity-40 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg> </svg>
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-2">暂无字段配置</h3> <h3 class="text-lg font-semibold text-on-surface mb-2">暂无字段配置</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">点击上方"添加字段"开始配置模板</p> <p class="text-sm text-on-surface-variant">点击上方"添加字段"开始配置模板</p>
</div> </div>
<div v-else class="space-y-3"> <div v-else class="space-y-3">
@@ -215,7 +216,7 @@
<span class="text-lg font-bold">JSON 预览</span> <span class="text-lg font-bold">JSON 预览</span>
</a-divider> </a-divider>
<div class="bg-gray-900 dark:bg-black text-green-400 p-4 rounded-lg font-mono text-sm overflow-auto max-h-96"> <div class="bg-surface-container text-green-400 p-4 rounded-lg font-mono text-sm overflow-auto max-h-96">
<pre>{{ JSON.stringify(formData.field_config, null, 2) }}</pre> <pre>{{ JSON.stringify(formData.field_config, null, 2) }}</pre>
</div> </div>
</a-form> </a-form>
@@ -242,7 +243,7 @@
placeholder="例如: Id, Group1, DateTarget" placeholder="例如: Id, Group1, DateTarget"
@keyup.enter="confirmAddField" @keyup.enter="confirmAddField"
/> />
<span class="text-xs text-gray-500 dark:text-gray-400 mt-1 block"> <span class="text-xs text-on-surface-variant mt-1 block">
💡 字段名将保持原样不会进行大小写转换 💡 字段名将保持原样不会进行大小写转换
</span> </span>
</a-form-item> </a-form-item>
@@ -262,14 +263,14 @@
:style="isMobile ? { top: 0, maxWidth: '100vw' } : {}" :style="isMobile ? { top: 0, maxWidth: '100vw' } : {}"
> >
<div v-if="previewData" class="space-y-4"> <div v-if="previewData" class="space-y-4">
<div class="bg-gray-50 dark:bg-gray-800 rounded p-4"> <div class="bg-surface-container rounded p-4">
<h4 class="font-semibold mb-2 text-gray-800 dark:text-gray-100">生成的 Payload使用默认值</h4> <h4 class="font-semibold mb-2 text-on-surface">生成的 Payload使用默认值</h4>
<pre class="text-xs bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200 p-3 rounded border dark:border-gray-700 overflow-auto max-h-96">{{ JSON.stringify(previewData.preview_payload, null, 2) }}</pre> <pre class="text-xs bg-surface text-on-surface p-3 rounded border border-outline-variant overflow-auto max-h-96">{{ JSON.stringify(previewData.preview_payload, null, 2) }}</pre>
</div> </div>
<div class="bg-gray-50 dark:bg-gray-800 rounded p-4"> <div class="bg-surface-container rounded p-4">
<h4 class="font-semibold mb-2 text-gray-800 dark:text-gray-100">字段配置</h4> <h4 class="font-semibold mb-2 text-on-surface">字段配置</h4>
<pre class="text-xs bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200 p-3 rounded border dark:border-gray-700 overflow-auto max-h-96">{{ JSON.stringify(previewData.field_config, null, 2) }}</pre> <pre class="text-xs bg-surface text-on-surface p-3 rounded border border-outline-variant overflow-auto max-h-96">{{ JSON.stringify(previewData.field_config, null, 2) }}</pre>
</div> </div>
</div> </div>
+14 -8
View File
@@ -59,16 +59,22 @@ export default {
}, },
}, },
borderRadius: { borderRadius: {
'md3': '12px', // Material Design 3 Shape System
'md3-lg': '16px', 'md3-xs': '4px', // Extra Small - chips, small tags
'md3-xl': '28px', 'md3-sm': '8px', // Small - text fields, small components
'md3': '12px', // Medium - cards, buttons (default)
'md3-lg': '16px', // Large - large cards, dialogs
'md3-xl': '28px', // Extra Large - fully rounded buttons
'md3-full': '9999px', // Full - pill shape
}, },
boxShadow: { boxShadow: {
'md3-1': '0 1px 2px 0 rgba(0, 0, 0, 0.05)', // Material Design 3 Elevation System (official spec)
'md3-2': '0 1px 3px 1px rgba(0, 0, 0, 0.08)', 'md3-0': 'none',
'md3-3': '0 4px 8px 3px rgba(0, 0, 0, 0.10)', 'md3-1': '0px 1px 2px 0px rgba(0, 0, 0, 0.3), 0px 1px 3px 1px rgba(0, 0, 0, 0.15)',
'md3-4': '0 6px 10px 4px rgba(0, 0, 0, 0.12)', 'md3-2': '0px 1px 2px 0px rgba(0, 0, 0, 0.3), 0px 2px 6px 2px rgba(0, 0, 0, 0.15)',
'md3-5': '0 8px 12px 6px rgba(0, 0, 0, 0.14)', 'md3-3': '0px 1px 3px 0px rgba(0, 0, 0, 0.3), 0px 4px 8px 3px rgba(0, 0, 0, 0.15)',
'md3-4': '0px 2px 3px 0px rgba(0, 0, 0, 0.3), 0px 6px 10px 4px rgba(0, 0, 0, 0.15)',
'md3-5': '0px 4px 4px 0px rgba(0, 0, 0, 0.3), 0px 8px 12px 6px rgba(0, 0, 0, 0.15)',
}, },
animation: { animation: {
'fade-in': 'fadeIn 0.3s ease-in-out', 'fade-in': 'fadeIn 0.3s ease-in-out',