mirror of
https://github.com/Cccc-owo/CheckInApp.git
synced 2026-06-17 05:56:29 +00:00
style: use united style
This commit is contained in:
+4
-3
@@ -1,10 +1,11 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<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" />
|
||||
<title>frontend</title>
|
||||
<title>接龙自动打卡</title>
|
||||
<meta name="description" content="接龙自动打卡系统 - 轻松管理您的打卡任务" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -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
@@ -2,180 +2,211 @@ import { theme } from 'ant-design-vue'
|
||||
|
||||
/**
|
||||
* Ant Design Vue 主题配置
|
||||
* 匹配现有 Material Design 3 色彩系统
|
||||
* 严格遵循 Material Design 3 规范
|
||||
* @param {boolean} isDark - 是否为暗色模式
|
||||
*/
|
||||
export default function getAntdTheme(isDark = false) {
|
||||
return {
|
||||
token: {
|
||||
// 主色调 - 绿色(与 MD3 primary 保持一致)
|
||||
// === Material Design 3 Color System ===
|
||||
|
||||
// Primary - 主色调(绿色)
|
||||
colorPrimary: isDark ? '#81c784' : '#4caf50',
|
||||
|
||||
// 成功色
|
||||
// Secondary colors
|
||||
colorSuccess: isDark ? '#81c784' : '#4caf50',
|
||||
|
||||
// 警告色
|
||||
colorWarning: '#ff9800',
|
||||
|
||||
// 错误色
|
||||
colorError: '#f56c6c',
|
||||
|
||||
// 信息色 - 蓝色(与 MD3 secondary 保持一致)
|
||||
colorError: '#f44336', // MD3 标准错误色
|
||||
colorInfo: isDark ? '#64b5f6' : '#2196f3',
|
||||
|
||||
// 背景色
|
||||
colorBgBase: isDark ? '#121212' : '#ffffff',
|
||||
colorBgContainer: isDark ? '#1c1c1e' : '#ffffff',
|
||||
colorBgElevated: isDark ? '#2c2c2e' : '#ffffff',
|
||||
colorBgLayout: isDark ? '#121212' : '#fafafa',
|
||||
colorBgSpotlight: isDark ? '#2c2c2e' : '#ffffff',
|
||||
// === Surface & Background (MD3 规范) ===
|
||||
colorBgBase: isDark ? '#1c1b1f' : '#ffffff',
|
||||
colorBgContainer: isDark ? '#1c1b1f' : '#ffffff',
|
||||
colorBgElevated: isDark ? '#26252a' : '#ffffff',
|
||||
colorBgLayout: isDark ? '#1c1b1f' : '#fefbff', // MD3 标准背景色
|
||||
colorBgSpotlight: isDark ? '#26252a' : '#ffffff',
|
||||
|
||||
// 文字色
|
||||
colorText: isDark ? '#e5e5e7' : '#1c1b1f',
|
||||
colorTextSecondary: isDark ? '#a0a0a3' : '#64748b',
|
||||
colorTextTertiary: isDark ? '#808083' : '#94a3b8',
|
||||
colorTextQuaternary: isDark ? '#606063' : '#cbd5e1',
|
||||
// === Typography (MD3 规范) ===
|
||||
colorText: isDark ? '#e6e1e5' : '#1c1b1f', // On-surface
|
||||
colorTextSecondary: isDark ? '#cac4d0' : '#49454f', // On-surface-variant
|
||||
colorTextTertiary: isDark ? '#938f99' : '#79747e',
|
||||
colorTextQuaternary: isDark ? '#79747e' : '#938f99',
|
||||
|
||||
// 边框色
|
||||
colorBorder: isDark ? '#3a3a3c' : '#e5e7eb',
|
||||
colorBorderSecondary: isDark ? '#2c2c2e' : '#f3f4f6',
|
||||
// === Borders ===
|
||||
colorBorder: isDark ? '#49454f' : '#d1cdd6',
|
||||
colorBorderSecondary: isDark ? '#3a3740' : '#e3e1e6',
|
||||
colorSplit: isDark ? '#49454f' : '#e3e1e6',
|
||||
|
||||
// 分割线颜色
|
||||
colorSplit: isDark ? '#3a3a3c' : '#e5e7eb',
|
||||
// === Shape System ===
|
||||
borderRadius: 12, // Medium shape
|
||||
borderRadiusLG: 16, // Large shape
|
||||
borderRadiusSM: 8, // Small shape
|
||||
borderRadiusXS: 4, // Extra small shape
|
||||
|
||||
// 边框圆角 - 与 Material Design 3 一致
|
||||
borderRadius: 12,
|
||||
// === Typography ===
|
||||
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
|
||||
|
||||
// 字体家族
|
||||
fontFamily: "'Inter', 'Segoe UI', 'Roboto', system-ui, -apple-system, sans-serif",
|
||||
|
||||
// 链接色
|
||||
// === Links ===
|
||||
colorLink: isDark ? '#64b5f6' : '#2196f3',
|
||||
colorLinkHover: isDark ? '#90caf9' : '#1976d2',
|
||||
colorLinkActive: isDark ? '#42a5f5' : '#1565c0',
|
||||
|
||||
// 字体大小
|
||||
fontSize: 14,
|
||||
|
||||
// 行高
|
||||
lineHeight: 1.5715,
|
||||
|
||||
// 控制组件高度
|
||||
// === Components ===
|
||||
controlHeight: 40,
|
||||
controlHeightLG: 48,
|
||||
controlHeightSM: 32,
|
||||
|
||||
// === Motion (MD3 规范) ===
|
||||
motionDurationSlow: '0.3s',
|
||||
motionDurationMid: '0.2s',
|
||||
motionDurationFast: '0.1s',
|
||||
},
|
||||
|
||||
components: {
|
||||
// Card 组件定制
|
||||
// === Card 组件 (MD3 Elevated Card) ===
|
||||
Card: {
|
||||
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,
|
||||
colorBgContainer: isDark ? '#1c1c1e' : '#ffffff',
|
||||
colorBorderSecondary: isDark ? '#3a3a3c' : '#f0f0f0',
|
||||
colorTextHeading: isDark ? '#e5e5e7' : '#1c1b1f',
|
||||
colorBgContainer: isDark ? '#1c1b1f' : '#ffffff',
|
||||
colorBorderSecondary: isDark ? '#49454f' : '#e3e1e6',
|
||||
colorTextHeading: isDark ? '#e6e1e5' : '#1c1b1f',
|
||||
},
|
||||
|
||||
// Button 组件定制
|
||||
// === Button 组件 (MD3 规范) ===
|
||||
Button: {
|
||||
borderRadius: 24, // 圆角按钮,类似 MD3
|
||||
borderRadius: 20, // MD3 Filled Button 圆角
|
||||
borderRadiusLG: 24,
|
||||
borderRadiusSM: 16,
|
||||
controlHeight: 40,
|
||||
controlHeightLG: 48,
|
||||
controlHeightSM: 32,
|
||||
fontSize: 14,
|
||||
colorText: isDark ? '#e5e5e7' : '#1c1b1f',
|
||||
colorBgContainer: isDark ? '#2c2c2e' : '#ffffff',
|
||||
fontSizeLG: 16,
|
||||
fontSizeSM: 12,
|
||||
paddingContentHorizontal: 24,
|
||||
colorText: isDark ? '#e6e1e5' : '#1c1b1f',
|
||||
colorBgContainer: isDark ? '#26252a' : '#ffffff',
|
||||
},
|
||||
|
||||
// Input 组件定制
|
||||
// === Input 组件 (MD3 Text Field) ===
|
||||
Input: {
|
||||
borderRadius: 12,
|
||||
controlHeight: 40,
|
||||
colorBgContainer: isDark ? '#2c2c2e' : '#ffffff',
|
||||
colorText: isDark ? '#e5e5e7' : '#1c1b1f',
|
||||
colorTextPlaceholder: isDark ? '#808083' : '#94a3b8',
|
||||
colorBorder: isDark ? '#3a3a3c' : '#e5e7eb',
|
||||
colorBgContainer: isDark ? '#26252a' : '#ffffff',
|
||||
colorText: isDark ? '#e6e1e5' : '#1c1b1f',
|
||||
colorTextPlaceholder: isDark ? '#938f99' : '#79747e',
|
||||
colorBorder: isDark ? '#49454f' : '#d1cdd6',
|
||||
},
|
||||
|
||||
// Select 组件定制
|
||||
// === Select 组件 ===
|
||||
Select: {
|
||||
borderRadius: 12,
|
||||
controlHeight: 40,
|
||||
colorBgContainer: isDark ? '#2c2c2e' : '#ffffff',
|
||||
colorBgElevated: isDark ? '#2c2c2e' : '#ffffff',
|
||||
colorText: isDark ? '#e5e5e7' : '#1c1b1f',
|
||||
colorTextPlaceholder: isDark ? '#808083' : '#94a3b8',
|
||||
colorBorder: isDark ? '#3a3a3c' : '#e5e7eb',
|
||||
colorBgContainer: isDark ? '#26252a' : '#ffffff',
|
||||
colorBgElevated: isDark ? '#26252a' : '#ffffff',
|
||||
colorText: isDark ? '#e6e1e5' : '#1c1b1f',
|
||||
colorTextPlaceholder: isDark ? '#938f99' : '#79747e',
|
||||
colorBorder: isDark ? '#49454f' : '#d1cdd6',
|
||||
},
|
||||
|
||||
// Modal 组件定制
|
||||
// === Modal 组件 (MD3 Dialog) ===
|
||||
Modal: {
|
||||
borderRadiusLG: 16,
|
||||
colorBgElevated: isDark ? '#1c1c1e' : '#ffffff',
|
||||
colorText: isDark ? '#e5e5e7' : '#1c1b1f',
|
||||
colorTextHeading: isDark ? '#e5e5e7' : '#1c1b1f',
|
||||
borderRadiusLG: 28, // MD3 Dialog 使用 Extra Large 圆角
|
||||
colorBgElevated: isDark ? '#1c1b1f' : '#ffffff',
|
||||
colorText: isDark ? '#e6e1e5' : '#1c1b1f',
|
||||
colorTextHeading: isDark ? '#e6e1e5' : '#1c1b1f',
|
||||
},
|
||||
|
||||
// Table 组件定制
|
||||
// === Table 组件 ===
|
||||
Table: {
|
||||
borderRadius: 12,
|
||||
colorBgContainer: isDark ? '#1c1c1e' : '#ffffff',
|
||||
colorFillAlter: isDark ? '#2c2c2e' : '#f5f7fa',
|
||||
colorText: isDark ? '#e5e5e7' : '#1c1b1f',
|
||||
colorTextHeading: isDark ? '#e5e5e7' : '#1c1b1f',
|
||||
colorBorderSecondary: isDark ? '#3a3a3c' : '#f0f0f0',
|
||||
colorBgContainer: isDark ? '#1c1b1f' : '#ffffff',
|
||||
colorFillAlter: isDark ? '#26252a' : '#f5f5f5',
|
||||
colorText: isDark ? '#e6e1e5' : '#1c1b1f',
|
||||
colorTextHeading: isDark ? '#e6e1e5' : '#1c1b1f',
|
||||
colorBorderSecondary: isDark ? '#49454f' : '#e3e1e6',
|
||||
},
|
||||
|
||||
// Tabs 组件定制
|
||||
// === Tabs 组件 ===
|
||||
Tabs: {
|
||||
borderRadius: 12,
|
||||
colorText: isDark ? '#e5e5e7' : '#1c1b1f',
|
||||
colorBgContainer: isDark ? '#1c1c1e' : '#ffffff',
|
||||
colorText: isDark ? '#e6e1e5' : '#1c1b1f',
|
||||
colorBgContainer: isDark ? '#1c1b1f' : '#ffffff',
|
||||
},
|
||||
|
||||
// Menu 组件定制
|
||||
// === Menu 组件 ===
|
||||
Menu: {
|
||||
colorItemBg: isDark ? '#1c1c1e' : '#ffffff',
|
||||
colorItemBgHover: isDark ? '#2c2c2e' : '#f5f7fa',
|
||||
colorItemBgSelected: isDark ? '#2c2c2e' : '#e8f5e9',
|
||||
colorItemText: isDark ? '#e5e5e7' : '#1c1b1f',
|
||||
colorItemBg: isDark ? '#1c1b1f' : '#ffffff',
|
||||
colorItemBgHover: isDark ? '#26252a' : '#f5f5f5',
|
||||
colorItemBgSelected: isDark ? '#3a4a3f' : '#e8f5e9',
|
||||
colorItemText: isDark ? '#e6e1e5' : '#1c1b1f',
|
||||
colorItemTextSelected: isDark ? '#81c784' : '#4caf50',
|
||||
borderRadius: 12,
|
||||
},
|
||||
|
||||
// Dropdown 组件定制
|
||||
// === Dropdown 组件 ===
|
||||
Dropdown: {
|
||||
colorBgElevated: isDark ? '#2c2c2e' : '#ffffff',
|
||||
colorText: isDark ? '#e5e5e7' : '#1c1b1f',
|
||||
colorBgElevated: isDark ? '#26252a' : '#ffffff',
|
||||
colorText: isDark ? '#e6e1e5' : '#1c1b1f',
|
||||
borderRadiusLG: 12,
|
||||
},
|
||||
|
||||
// Descriptions 组件定制
|
||||
// === Descriptions 组件 ===
|
||||
Descriptions: {
|
||||
colorText: isDark ? '#e5e5e7' : '#1c1b1f',
|
||||
colorTextSecondary: isDark ? '#a0a0a3' : '#64748b',
|
||||
colorBgContainer: isDark ? '#1c1c1e' : '#ffffff',
|
||||
colorFillAlter: isDark ? '#2c2c2e' : '#f5f7fa',
|
||||
colorText: isDark ? '#e6e1e5' : '#1c1b1f',
|
||||
colorTextSecondary: isDark ? '#cac4d0' : '#49454f',
|
||||
colorBgContainer: isDark ? '#1c1b1f' : '#ffffff',
|
||||
colorFillAlter: isDark ? '#201f24' : '#f3f4f6', // Label 背景色 = surface-container
|
||||
colorSplit: isDark ? '#49454f' : '#e3e1e6',
|
||||
borderRadiusLG: 8, // 设置 Descriptions 容器圆角
|
||||
},
|
||||
|
||||
// Alert 组件定制
|
||||
// === Alert 组件 ===
|
||||
Alert: {
|
||||
borderRadiusLG: 12,
|
||||
colorText: isDark ? '#e5e5e7' : '#1c1b1f',
|
||||
colorText: isDark ? '#e6e1e5' : '#1c1b1f',
|
||||
},
|
||||
|
||||
// Drawer 组件定制
|
||||
// === Drawer 组件 ===
|
||||
Drawer: {
|
||||
colorBgElevated: isDark ? '#1c1c1e' : '#ffffff',
|
||||
colorText: isDark ? '#e5e5e7' : '#1c1b1f',
|
||||
colorBgElevated: isDark ? '#1c1b1f' : '#ffffff',
|
||||
colorText: isDark ? '#e6e1e5' : '#1c1b1f',
|
||||
borderRadiusLG: 16,
|
||||
},
|
||||
|
||||
// Form 组件定制
|
||||
// === Form 组件 ===
|
||||
Form: {
|
||||
colorText: isDark ? '#e5e5e7' : '#1c1b1f',
|
||||
colorTextHeading: isDark ? '#e5e5e7' : '#1c1b1f',
|
||||
colorText: isDark ? '#e6e1e5' : '#1c1b1f',
|
||||
colorTextHeading: isDark ? '#e6e1e5' : '#1c1b1f',
|
||||
},
|
||||
|
||||
// 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,
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -325,191 +325,174 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* === Material Design 3 样式重写 === */
|
||||
|
||||
.crontab-editor {
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
background: #f5f7fa;
|
||||
transition: all 0.3s;
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
background-color: var(--md-sys-color-surface-container-lowest);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.dark .crontab-editor {
|
||||
border-color: #3a3a3c;
|
||||
background: #2c2c2e;
|
||||
.crontab-editor:focus-within {
|
||||
border-color: var(--md-sys-color-primary);
|
||||
box-shadow: 0 0 0 1px var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
/* 模式选择标签 */
|
||||
.mode-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 2px solid #ebeef5;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.dark .mode-tabs {
|
||||
border-bottom-color: #3a3a3c;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.mode-tab {
|
||||
padding: 8px 16px;
|
||||
padding: 10px 20px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #909399;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.1px;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -2px;
|
||||
transition: all 0.3s;
|
||||
margin-bottom: -1px;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dark .mode-tab {
|
||||
color: #a0a0a3;
|
||||
.mode-tab:hover {
|
||||
color: var(--md-sys-color-on-surface);
|
||||
background-color: rgba(76, 175, 80, 0.04);
|
||||
}
|
||||
|
||||
.mode-tab.active {
|
||||
color: #409eff;
|
||||
border-bottom-color: #409eff;
|
||||
}
|
||||
|
||||
.dark .mode-tab.active {
|
||||
color: #81c784;
|
||||
border-bottom-color: #81c784;
|
||||
color: var(--md-sys-color-primary);
|
||||
border-bottom-color: var(--md-sys-color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 模式内容区域 */
|
||||
.mode-content {
|
||||
margin: 16px 0;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
/* 快速选项 */
|
||||
.quick-option {
|
||||
padding: 12px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
transition: background 0.3s;
|
||||
padding: 16px;
|
||||
background-color: var(--md-sys-color-surface);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.dark .quick-option {
|
||||
background: #1c1c1e;
|
||||
.quick-option:hover {
|
||||
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 {
|
||||
font-weight: 600;
|
||||
color: #1c1b1f;
|
||||
}
|
||||
|
||||
.dark .option-label {
|
||||
color: #e5e5e7;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
letter-spacing: 0.1px;
|
||||
}
|
||||
|
||||
.option-desc {
|
||||
margin-left: 12px;
|
||||
color: #909399;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
.dark .option-desc {
|
||||
color: #a0a0a3;
|
||||
}
|
||||
|
||||
/* 表达式输入 */
|
||||
.expression-input {
|
||||
margin: 12px 0;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
margin-top: 8px;
|
||||
color: #909399;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dark .help-text {
|
||||
color: #a0a0a3;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
.help-text a {
|
||||
color: #409eff;
|
||||
color: var(--md-sys-color-secondary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.dark .help-text a {
|
||||
color: #81c784;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.help-text a:hover {
|
||||
color: var(--md-sys-color-primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 预览区域 */
|
||||
.preview-section {
|
||||
margin: 16px 0;
|
||||
padding: 12px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.dark .preview-section {
|
||||
background: #1c1c1e;
|
||||
padding: 16px;
|
||||
background-color: var(--md-sys-color-surface-container-low);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.preview-section h4 {
|
||||
margin: 0 0 8px 0;
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
color: #1c1b1f;
|
||||
}
|
||||
|
||||
.dark .preview-section h4 {
|
||||
color: #e5e5e7;
|
||||
font-weight: 500;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
letter-spacing: 0.1px;
|
||||
}
|
||||
|
||||
.execution-list {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
padding-left: 24px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
}
|
||||
|
||||
.dark .execution-list {
|
||||
color: #a0a0a3;
|
||||
.execution-list li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* 验证消息 */
|
||||
.validation-message {
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
margin-top: 16px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0.25px;
|
||||
border: 1px solid;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.validation-message.success {
|
||||
background: #f0f9ff;
|
||||
color: #67c23a;
|
||||
border: 1px solid #c6e2ff;
|
||||
}
|
||||
|
||||
.dark .validation-message.success {
|
||||
background: rgba(129, 199, 132, 0.1);
|
||||
color: #81c784;
|
||||
border-color: rgba(129, 199, 132, 0.3);
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
border-color: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
.validation-message.error {
|
||||
background: #fef0f0;
|
||||
color: #f56c6c;
|
||||
border: 1px solid #fde7e7;
|
||||
}
|
||||
|
||||
.dark .validation-message.error {
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
color: #ef5350;
|
||||
border-color: rgba(244, 67, 54, 0.3);
|
||||
background-color: var(--md-sys-color-error-container);
|
||||
color: var(--md-sys-color-on-error-container);
|
||||
border-color: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
.validation-message.info {
|
||||
background: #f4f4f5;
|
||||
color: #909399;
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.dark .validation-message.info {
|
||||
background: #2c2c2e;
|
||||
color: #a0a0a3;
|
||||
border-color: #3a3a3c;
|
||||
background-color: var(--md-sys-color-surface-container-high);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
border-color: var(--md-sys-color-outline-variant);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
placeholder="在表单中显示的名称"
|
||||
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 label="字段类型" class="mb-0">
|
||||
@@ -24,7 +24,7 @@
|
||||
<a-select-option label="🔢 数字输入" value="number" />
|
||||
<a-select-option label="📋 下拉选择" value="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>
|
||||
</div>
|
||||
|
||||
@@ -38,22 +38,22 @@
|
||||
class="w-full"
|
||||
>
|
||||
<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 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 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 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 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>
|
||||
<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 label="默认值" class="mb-0">
|
||||
@@ -72,7 +72,7 @@
|
||||
:rows="3"
|
||||
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'">
|
||||
<p>输入JSON对象,会自动序列化为字符串</p>
|
||||
<p>如:{"key1":value1,"key2":value2}</p>
|
||||
@@ -92,15 +92,15 @@
|
||||
placeholder="输入框的灰色提示文本"
|
||||
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>
|
||||
|
||||
<!-- 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>
|
||||
<label class="text-sm font-medium text-gray-700">是否必填</label>
|
||||
<p class="text-xs text-gray-500">用户必须填写此字段</p>
|
||||
<label class="text-sm font-medium text-on-surface">是否必填</label>
|
||||
<p class="text-xs text-on-surface-variant">用户必须填写此字段</p>
|
||||
</div>
|
||||
<a-switch
|
||||
:checked="modelValue.required"
|
||||
@@ -111,8 +111,8 @@
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<label class="text-sm font-medium text-gray-700">是否隐藏</label>
|
||||
<p class="text-xs text-gray-500">直接使用默认值,不在表单中显示</p>
|
||||
<label class="text-sm font-medium text-on-surface">是否隐藏</label>
|
||||
<p class="text-xs text-on-surface-variant">直接使用默认值,不在表单中显示</p>
|
||||
</div>
|
||||
<a-switch
|
||||
:checked="modelValue.hidden"
|
||||
@@ -142,9 +142,9 @@
|
||||
<div
|
||||
v-for="(option, index) in modelValue.options || []"
|
||||
: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
|
||||
:value="option.label"
|
||||
@change="e => updateOption(index, 'label', e.target.value)"
|
||||
@@ -175,8 +175,8 @@
|
||||
添加选项
|
||||
</a-button>
|
||||
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
💡 提示:显示文本是用户看到的内容,选项值是实际保存的数据
|
||||
<p class="text-xs text-on-surface-variant mt-2">
|
||||
💡 提示:显示文本是用户看到的内容,选项值是实际保存的数据
|
||||
</p>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<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 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">
|
||||
<button
|
||||
type="button"
|
||||
@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
|
||||
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 }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
@@ -19,10 +19,10 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</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" />
|
||||
</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>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@@ -45,22 +45,22 @@
|
||||
</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" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数组字段 -->
|
||||
<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">
|
||||
<button
|
||||
type="button"
|
||||
@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
|
||||
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 }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
@@ -69,10 +69,10 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</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" />
|
||||
</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>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@@ -102,8 +102,8 @@
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<p class="text-sm text-gray-500 mb-2">数组为空</p>
|
||||
<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-on-surface-variant mb-2">数组为空</p>
|
||||
<a-button size="small" type="primary" @click="addArrayItem">添加第一个元素</a-button>
|
||||
</div>
|
||||
|
||||
@@ -111,17 +111,17 @@
|
||||
<div
|
||||
v-for="(item, index) in localFieldConfig"
|
||||
: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">
|
||||
<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>
|
||||
</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}`" />
|
||||
</div>
|
||||
|
||||
@@ -164,15 +164,15 @@
|
||||
|
||||
<!-- 对象字段 -->
|
||||
<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">
|
||||
<button
|
||||
type="button"
|
||||
@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
|
||||
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 }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
@@ -181,10 +181,10 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</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" />
|
||||
</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>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@@ -214,12 +214,12 @@
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<p class="text-sm text-gray-500 mb-2">对象为空</p>
|
||||
<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-on-surface-variant mb-2">对象为空</p>
|
||||
<a-button size="small" type="primary" @click="addFieldToObject">添加第一个子字段</a-button>
|
||||
</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
|
||||
v-for="(subConfig, subKey) in localFieldConfig"
|
||||
|
||||
@@ -26,11 +26,7 @@ onMounted(() => {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #e8eef5 100%);
|
||||
}
|
||||
|
||||
.dark .layout-container {
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #0f0f0f 100%);
|
||||
background: linear-gradient(135deg, var(--md-sys-color-surface-container-lowest) 0%, var(--md-sys-color-surface-container-low) 100%);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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">
|
||||
<div class="flex items-center justify-between">
|
||||
<!-- Logo and Brand -->
|
||||
@@ -24,7 +24,7 @@
|
||||
'px-4 py-2 rounded-full font-medium transition-all cursor-pointer',
|
||||
isActive
|
||||
? '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">
|
||||
@@ -45,7 +45,7 @@
|
||||
'px-4 py-2 rounded-full font-medium transition-all cursor-pointer',
|
||||
isActive
|
||||
? '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">
|
||||
@@ -66,7 +66,7 @@
|
||||
'px-4 py-2 rounded-full font-medium transition-all cursor-pointer',
|
||||
isActive
|
||||
? '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">
|
||||
@@ -81,7 +81,7 @@
|
||||
<a
|
||||
:class="[
|
||||
'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 />
|
||||
@@ -121,7 +121,7 @@
|
||||
<!-- Token Status Indicator (Desktop & Mobile) -->
|
||||
<a-tooltip v-if="showTokenStatus" :title="tokenStatusTooltip">
|
||||
<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"
|
||||
>
|
||||
<a-badge :status="tokenBadgeStatus" />
|
||||
@@ -142,32 +142,32 @@
|
||||
</a-tooltip>
|
||||
|
||||
<!-- Theme Toggle Button -->
|
||||
<a-tooltip :title="isDark ? '切换到亮色模式' : '切换到暗色模式'">
|
||||
<a-button
|
||||
type="text"
|
||||
class="!p-2 !flex !items-center !justify-center hover:!bg-gray-100 dark:hover:!bg-gray-700 transition-all"
|
||||
<a-tooltip :title="isDark ? '切换到亮色模式' : '切换到暗色模式'" placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
class="w-10 h-10 rounded-full flex items-center justify-center hover:bg-surface-container transition-all"
|
||||
@click="toggleTheme"
|
||||
>
|
||||
<BulbFilled v-if="isDark" class="text-lg text-yellow-400" />
|
||||
<BulbOutlined v-else class="text-lg text-gray-700 dark:text-gray-300" />
|
||||
</a-button>
|
||||
<BulbFilled v-if="isDark" class="text-xl text-yellow-400" />
|
||||
<BulbOutlined v-else class="text-xl text-on-surface" />
|
||||
</button>
|
||||
</a-tooltip>
|
||||
|
||||
<!-- Desktop User Menu -->
|
||||
<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' }">
|
||||
{{ userInitial }}
|
||||
</a-avatar>
|
||||
<span class="hidden md:block font-medium text-gray-700 dark:text-gray-200">{{ authStore.user?.alias || '用户' }}</span>
|
||||
<DownOutlined class="text-xs text-gray-500 dark:text-gray-400" />
|
||||
<span class="hidden md:block font-medium text-on-surface">{{ authStore.user?.alias || '用户' }}</span>
|
||||
<DownOutlined class="text-xs text-on-surface-variant" />
|
||||
</a>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="info" disabled>
|
||||
<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-xs text-gray-500 dark:text-gray-400 mt-1">{{ authStore.isAdmin ? '管理员' : '普通用户' }}</p>
|
||||
<p class="text-sm font-medium text-on-surface">{{ authStore.user?.alias }}</p>
|
||||
<p class="text-xs text-on-surface-variant mt-1">{{ authStore.isAdmin ? '管理员' : '普通用户' }}</p>
|
||||
</div>
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
@@ -184,14 +184,14 @@
|
||||
</a-dropdown>
|
||||
|
||||
<!-- Mobile Hamburger Button -->
|
||||
<a-button
|
||||
<button
|
||||
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"
|
||||
class="!p-2"
|
||||
>
|
||||
<MenuOutlined class="text-xl text-gray-700 dark:text-gray-300" />
|
||||
</a-button>
|
||||
<MenuOutlined class="text-xl text-on-surface" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -204,14 +204,14 @@
|
||||
title="菜单"
|
||||
>
|
||||
<!-- 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">
|
||||
<a-avatar :size="48" :style="{ backgroundColor: '#f56a00' }">
|
||||
{{ userInitial }}
|
||||
</a-avatar>
|
||||
<div>
|
||||
<p class="font-medium text-gray-900">{{ authStore.user?.alias || '用户' }}</p>
|
||||
<p class="text-xs text-gray-500">{{ authStore.isAdmin ? '管理员' : '普通用户' }}</p>
|
||||
<p class="font-medium text-on-surface">{{ authStore.user?.alias || '用户' }}</p>
|
||||
<p class="text-xs text-on-surface-variant">{{ authStore.isAdmin ? '管理员' : '普通用户' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -364,11 +364,11 @@ const tokenBadgeText = computed(() => {
|
||||
|
||||
const tokenIconClass = computed(() => {
|
||||
const mins = remainingMinutes.value
|
||||
if (mins === null) return 'text-gray-500'
|
||||
if (mins < 0) return 'text-red-500' // 已过期
|
||||
if (mins <= 10) return 'text-red-500 animate-pulse' // 10分钟内,闪烁
|
||||
if (mins <= 30) return 'text-orange-500' // 30分钟内
|
||||
return 'text-blue-500' // 正常
|
||||
if (mins === null) return 'text-on-surface-variant'
|
||||
if (mins < 0) return 'text-red-500 dark:text-red-400' // 已过期
|
||||
if (mins <= 10) return 'text-red-500 dark:text-red-400 animate-pulse' // 10分钟内,闪烁
|
||||
if (mins <= 30) return 'text-orange-500 dark:text-orange-400' // 30分钟内
|
||||
return 'text-blue-500 dark:text-blue-400' // 正常
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+948
-875
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
<a-row :gutter="[20, 20]">
|
||||
<!-- Token 状态卡片 -->
|
||||
<a-col :xs="24" :sm="24" :md="24">
|
||||
<a-card class="status-card">
|
||||
<a-card class="status-card md3-card">
|
||||
<template #title>
|
||||
<div class="card-header">
|
||||
<KeyOutlined />
|
||||
@@ -60,7 +60,7 @@
|
||||
|
||||
<!-- 手动打卡卡片 -->
|
||||
<a-col :xs="24" :sm="24" :md="24">
|
||||
<a-card>
|
||||
<a-card class="md3-card">
|
||||
<template #title>
|
||||
<div class="card-header">
|
||||
<CalendarOutlined />
|
||||
@@ -123,7 +123,7 @@
|
||||
}}
|
||||
</a-tag>
|
||||
</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 || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
@@ -134,7 +134,7 @@
|
||||
|
||||
<!-- 用户信息卡片 -->
|
||||
<a-col :xs="24" :sm="24" :md="24">
|
||||
<a-card>
|
||||
<a-card class="md3-card">
|
||||
<template #title>
|
||||
<div class="card-header">
|
||||
<UserOutlined />
|
||||
@@ -151,10 +151,10 @@
|
||||
{{ authStore.isAdmin ? '管理员' : '普通用户' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="邮箱" :span="2">
|
||||
<a-descriptions-item label="邮箱">
|
||||
{{ authStore.user?.email || '未设置' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="注册时间" :span="2">
|
||||
<a-descriptions-item label="注册时间">
|
||||
{{ formatDateTime(authStore.user?.created_at, false) }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
@@ -335,23 +335,58 @@ onMounted(async () => {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.token-status {
|
||||
padding: 10px 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.token-status .ant-descriptions {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.check-in-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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 {
|
||||
width: 100%;
|
||||
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>
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
<Layout>
|
||||
<div class="settings-view">
|
||||
<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">
|
||||
<h2 class="text-xl font-bold text-gray-800 dark:text-gray-100 mb-4 flex items-center">
|
||||
<a-card class="md3-card mb-6">
|
||||
<h2 class="text-xl font-bold text-on-surface mb-4 flex items-center">
|
||||
<UserOutlined class="mr-2" />
|
||||
基本信息
|
||||
</h2>
|
||||
@@ -28,11 +28,11 @@
|
||||
{{ formatDate(user?.created_at) }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- 修改邮箱 -->
|
||||
<div class="md3-card p-6 mb-6">
|
||||
<h2 class="text-xl font-bold text-gray-800 dark:text-gray-100 mb-4 flex items-center">
|
||||
<a-card class="md3-card mb-6">
|
||||
<h2 class="text-xl font-bold text-on-surface mb-4 flex items-center">
|
||||
<EditOutlined class="mr-2" />
|
||||
修改个人信息
|
||||
</h2>
|
||||
@@ -73,11 +73,11 @@
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- 设置/修改密码 -->
|
||||
<div class="md3-card p-6">
|
||||
<h2 class="text-xl font-bold text-gray-800 dark:text-gray-100 mb-4 flex items-center">
|
||||
<a-card class="md3-card">
|
||||
<h2 class="text-xl font-bold text-on-surface mb-4 flex items-center">
|
||||
<KeyOutlined class="mr-2" />
|
||||
{{ hasPassword ? '修改密码' : '设置密码' }}
|
||||
</h2>
|
||||
@@ -136,7 +136,7 @@
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
返回任务列表
|
||||
</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-1">
|
||||
<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">
|
||||
<NumberOutlined class="mr-1" />
|
||||
接龙 ID: {{ getThreadId(currentTask) }}
|
||||
@@ -35,54 +35,54 @@
|
||||
{{ checkInLoading ? '打卡中...' : '立即打卡' }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<!-- Stats Summary -->
|
||||
<a-row :gutter="[16, 16]" class="mb-6">
|
||||
<a-col :xs="12" :sm="8" :md="4">
|
||||
<div class="fluent-card p-5 animate-slide-up">
|
||||
<p class="text-sm text-gray-600 mb-1">总打卡次数</p>
|
||||
<p class="text-2xl font-bold text-gray-800">{{ recordStats.total }}</p>
|
||||
</div>
|
||||
<a-card class="md3-card animate-slide-up">
|
||||
<p class="text-sm text-on-surface-variant mb-1">总打卡次数</p>
|
||||
<p class="text-2xl font-bold text-on-surface">{{ recordStats.total }}</p>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="12" :sm="8" :md="4">
|
||||
<div class="fluent-card p-5 animate-slide-up" style="animation-delay: 0.05s">
|
||||
<p class="text-sm text-gray-600 mb-1">成功次数</p>
|
||||
<p class="text-2xl font-bold text-green-600">{{ recordStats.success }}</p>
|
||||
</div>
|
||||
<a-card class="md3-card animate-slide-up" style="animation-delay: 0.05s">
|
||||
<p class="text-sm text-on-surface-variant mb-1">成功次数</p>
|
||||
<p class="text-2xl font-bold text-green-600 dark:text-green-400">{{ recordStats.success }}</p>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="12" :sm="8" :md="4">
|
||||
<div class="fluent-card p-5 animate-slide-up" style="animation-delay: 0.1s">
|
||||
<p class="text-sm text-gray-600 mb-1">时间范围外</p>
|
||||
<p class="text-2xl font-bold text-blue-600">{{ recordStats.outOfTime }}</p>
|
||||
</div>
|
||||
<a-card class="md3-card animate-slide-up" style="animation-delay: 0.1s">
|
||||
<p class="text-sm text-on-surface-variant mb-1">时间范围外</p>
|
||||
<p class="text-2xl font-bold text-blue-600 dark:text-blue-400">{{ recordStats.outOfTime }}</p>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="12" :sm="8" :md="4">
|
||||
<div class="fluent-card p-5 animate-slide-up" style="animation-delay: 0.15s">
|
||||
<p class="text-sm text-gray-600 mb-1">失败次数</p>
|
||||
<p class="text-2xl font-bold text-red-600">{{ recordStats.failure }}</p>
|
||||
</div>
|
||||
<a-card class="md3-card animate-slide-up" style="animation-delay: 0.15s">
|
||||
<p class="text-sm text-on-surface-variant mb-1">失败次数</p>
|
||||
<p class="text-2xl font-bold text-red-600 dark:text-red-400">{{ recordStats.failure }}</p>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="12" :sm="8" :md="4">
|
||||
<div class="fluent-card p-5 animate-slide-up" style="animation-delay: 0.2s">
|
||||
<p class="text-sm text-gray-600 mb-1">异常次数</p>
|
||||
<p class="text-2xl font-bold text-orange-600">{{ recordStats.unknown }}</p>
|
||||
</div>
|
||||
<a-card class="md3-card animate-slide-up" style="animation-delay: 0.2s">
|
||||
<p class="text-sm text-on-surface-variant mb-1">异常次数</p>
|
||||
<p class="text-2xl font-bold text-orange-600 dark:text-orange-400">{{ recordStats.unknown }}</p>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="12" :sm="8" :md="4">
|
||||
<div class="fluent-card p-5 animate-slide-up" style="animation-delay: 0.25s">
|
||||
<p class="text-sm text-gray-600 mb-1">成功率</p>
|
||||
<p class="text-2xl font-bold text-purple-600">{{ recordStats.successRate }}%</p>
|
||||
</div>
|
||||
<a-card class="md3-card animate-slide-up" style="animation-delay: 0.25s">
|
||||
<p class="text-sm text-on-surface-variant mb-1">成功率</p>
|
||||
<p class="text-2xl font-bold text-purple-600 dark:text-purple-400">{{ recordStats.successRate }}%</p>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="fluent-card p-4 mb-6">
|
||||
<a-card class="md3-card mb-6">
|
||||
<a-space wrap :size="[16, 16]">
|
||||
<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-button value="">全部</a-radio-button>
|
||||
<a-radio-button value="success">成功</a-radio-button>
|
||||
@@ -93,7 +93,7 @@
|
||||
</div>
|
||||
|
||||
<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-button value="">全部</a-radio-button>
|
||||
<a-radio-button value="scheduler">自动</a-radio-button>
|
||||
@@ -106,7 +106,7 @@
|
||||
刷新
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- Records List -->
|
||||
<div v-if="loading" class="space-y-4">
|
||||
@@ -115,22 +115,22 @@
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<div v-else-if="records.length === 0" class="fluent-card p-12 text-center">
|
||||
<FileTextOutlined class="text-8xl text-gray-300 mb-4" />
|
||||
<h3 class="text-xl font-semibold text-gray-700 mb-2">暂无打卡记录</h3>
|
||||
<p class="text-gray-500">当前筛选条件下没有找到任何打卡记录</p>
|
||||
</div>
|
||||
<a-card v-else-if="records.length === 0" class="md3-card text-center" style="padding: 48px 20px;">
|
||||
<FileTextOutlined class="text-8xl text-on-surface-variant opacity-30 mb-4" />
|
||||
<h3 class="text-xl font-semibold text-on-surface mb-2">暂无打卡记录</h3>
|
||||
<p class="text-on-surface-variant">当前筛选条件下没有找到任何打卡记录</p>
|
||||
</a-card>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<div
|
||||
<a-card
|
||||
v-for="record in records"
|
||||
: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-1">
|
||||
<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 }}
|
||||
</h3>
|
||||
<a-tag
|
||||
@@ -153,7 +153,7 @@
|
||||
{{ record.trigger_type === 'scheduled' ? '自动触发' : '手动触发' }}
|
||||
</a-tag>
|
||||
</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" />
|
||||
{{ formatDateTime(record.check_in_time) }}
|
||||
</div>
|
||||
@@ -161,18 +161,18 @@
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<span class="text-sm font-medium text-gray-700 w-20">响应:</span>
|
||||
<span class="text-sm text-gray-900 flex-1">{{ record.response_text }}</span>
|
||||
<span class="text-sm font-medium text-on-surface-variant w-20">响应:</span>
|
||||
<span class="text-sm text-on-surface flex-1">{{ record.response_text }}</span>
|
||||
</div>
|
||||
|
||||
<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 text-red-600 flex-1">{{ record.error_message }}</span>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-error w-20">错误:</span>
|
||||
<span class="text-sm text-error flex-1">{{ record.error_message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<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>
|
||||
<a-button
|
||||
type="primary"
|
||||
@@ -25,45 +25,45 @@
|
||||
<!-- Stats Cards -->
|
||||
<a-row :gutter="[16, 16]" class="mb-6">
|
||||
<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>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">总任务数</p>
|
||||
<p class="text-3xl font-bold text-primary-600 dark:text-primary-400">{{ taskStore.taskStats.total }}</p>
|
||||
<p class="text-sm text-on-surface-variant mb-1">总任务数</p>
|
||||
<p class="text-3xl font-bold text-primary">{{ taskStore.taskStats.total }}</p>
|
||||
</div>
|
||||
<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" />
|
||||
</div>
|
||||
<FileTextOutlined class="text-2xl text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<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" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<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>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1">已禁用</p>
|
||||
<p class="text-3xl font-bold text-gray-600 dark:text-gray-300">{{ taskStore.taskStats.inactive }}</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-gray-100 dark:bg-gray-700 rounded-md3 flex items-center justify-center">
|
||||
<StopOutlined class="text-2xl text-gray-600 dark:text-gray-300" />
|
||||
<p class="text-sm text-on-surface-variant mb-1">已禁用</p>
|
||||
<p class="text-3xl font-bold text-on-surface-variant">{{ taskStore.taskStats.inactive }}</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-surface-container-high rounded-md3 flex items-center justify-center">
|
||||
<StopOutlined class="text-2xl text-on-surface-variant" />
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
@@ -79,14 +79,14 @@
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<div v-else-if="taskStore.tasks.length === 0" class="fluent-card p-12 text-center">
|
||||
<FileTextOutlined class="text-8xl text-gray-300 mb-4" />
|
||||
<h3 class="text-xl font-semibold text-gray-700 mb-2">暂无任务</h3>
|
||||
<p class="text-gray-500 mb-6">点击右上角的"创建任务"按钮开始添加您的第一个打卡任务</p>
|
||||
<a-card v-else-if="taskStore.tasks.length === 0" class="md3-card text-center" style="padding: 48px 20px;">
|
||||
<FileTextOutlined class="text-8xl text-on-surface-variant opacity-30 mb-4" />
|
||||
<h3 class="text-xl font-semibold text-on-surface mb-2">暂无任务</h3>
|
||||
<p class="text-on-surface-variant mb-6">点击右上角的"创建任务"按钮开始添加您的第一个打卡任务</p>
|
||||
<a-button type="primary" @click="showCreateDialog = true">
|
||||
创建第一个任务
|
||||
</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-row v-else :gutter="[16, 16]">
|
||||
<a-col
|
||||
@@ -94,15 +94,16 @@
|
||||
v-for="task in taskStore.tasks"
|
||||
:key="task.id"
|
||||
>
|
||||
<div
|
||||
class="fluent-card p-6 hover:scale-105 transform transition-all cursor-pointer animate-slide-up"
|
||||
<a-card
|
||||
class="md3-card hover:scale-105 transform transition-all cursor-pointer animate-slide-up"
|
||||
@click="viewTask(task)"
|
||||
>
|
||||
<!-- Task Header -->
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-1">{{ task.name || '未命名任务' }}</h3>
|
||||
<p class="text-sm text-gray-500">任务 ID: {{ task.id }}</p>
|
||||
<h3 class="text-lg font-semibold text-on-surface mb-1">{{ task.name || '未命名任务' }}</h3>
|
||||
<a-divider style="margin: 8px 0;" />
|
||||
<p class="text-sm text-on-surface-variant">任务 ID: {{ task.id }}</p>
|
||||
</div>
|
||||
<a-tag :color="task.is_active ? 'success' : 'default'">
|
||||
{{ task.is_active ? '启用' : '禁用' }}
|
||||
@@ -111,21 +112,21 @@
|
||||
|
||||
<!-- Task Details -->
|
||||
<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" />
|
||||
接龙ID: {{ getThreadId(task) }}
|
||||
</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" />
|
||||
最后打卡: {{ task.last_check_in_time ? formatDateTime(task.last_check_in_time) : '未打卡' }}
|
||||
</div>
|
||||
<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="{
|
||||
'text-green-600 font-medium': task.last_check_in_status === 'success',
|
||||
'text-blue-600 font-medium': task.last_check_in_status === 'out_of_time',
|
||||
'text-red-600 font-medium': task.last_check_in_status === 'failure',
|
||||
'text-yellow-600 font-medium': task.last_check_in_status === 'unknown'
|
||||
'text-green-600 dark:text-green-400 font-medium': task.last_check_in_status === 'success',
|
||||
'text-blue-600 dark:text-blue-400 font-medium': task.last_check_in_status === 'out_of_time',
|
||||
'text-red-600 dark:text-red-400 font-medium': task.last_check_in_status === 'failure',
|
||||
'text-yellow-600 dark:text-yellow-400 font-medium': task.last_check_in_status === 'unknown'
|
||||
}">
|
||||
{{
|
||||
task.last_check_in_status === 'success' ? '✅ 打卡成功' :
|
||||
@@ -134,12 +135,12 @@
|
||||
'❗ 打卡异常'
|
||||
}}
|
||||
</span>
|
||||
<span v-else class="text-gray-500">暂无打卡记录</span>
|
||||
<span v-else class="text-on-surface-variant">暂无打卡记录</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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
|
||||
type="primary"
|
||||
size="small"
|
||||
@@ -174,7 +175,7 @@
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
@@ -192,12 +193,12 @@
|
||||
<div v-if="!editingTask">
|
||||
<div v-if="loadingTemplates" class="text-center py-8">
|
||||
<a-spin size="large" />
|
||||
<p class="text-gray-500 mt-2">加载模板中...</p>
|
||||
<p class="text-on-surface-variant mt-2">加载模板中...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="activeTemplates.length === 0" class="text-center py-8">
|
||||
<p class="text-gray-500">暂无可用模板</p>
|
||||
<p class="text-sm text-gray-400 mt-2">请联系管理员创建模板</p>
|
||||
<p class="text-on-surface-variant">暂无可用模板</p>
|
||||
<p class="text-sm text-on-surface-variant opacity-70 mt-2">请联系管理员创建模板</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
@@ -208,10 +209,10 @@
|
||||
v-for="template in activeTemplates"
|
||||
:key="template.id"
|
||||
@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>
|
||||
<p class="text-sm text-gray-600">{{ template.description || '无描述' }}</p>
|
||||
<h4 class="font-semibold text-on-surface mb-1">{{ template.name }}</h4>
|
||||
<p class="text-sm text-on-surface-variant">{{ template.description || '无描述' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
@@ -281,7 +282,7 @@
|
||||
</a-select-option>
|
||||
</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 }}
|
||||
</span>
|
||||
</a-form-item>
|
||||
@@ -298,7 +299,7 @@
|
||||
|
||||
<a-form-item label="启用状态">
|
||||
<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 ? '启用自动打卡' : '禁用自动打卡(仍可手动打卡)' }}
|
||||
</span>
|
||||
</a-form-item>
|
||||
@@ -312,7 +313,7 @@
|
||||
|
||||
<div class="mb-4">
|
||||
<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
|
||||
size="small"
|
||||
type="primary"
|
||||
@@ -330,7 +331,7 @@
|
||||
class="font-mono text-xs"
|
||||
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>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<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>
|
||||
<button @click="showCreateDialog" class="md3-button-filled">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -20,37 +20,38 @@
|
||||
|
||||
<!-- Templates List -->
|
||||
<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 }" />
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<div v-else-if="templates.length === 0" class="fluent-card p-12 text-center">
|
||||
<svg class="w-20 h-20 mx-auto text-gray-300 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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-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" />
|
||||
</svg>
|
||||
<h3 class="text-xl font-semibold text-gray-700 dark:text-gray-200 mb-2">暂无模板</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400 mb-4">创建第一个模板,让用户更轻松地创建打卡任务</p>
|
||||
<h3 class="text-xl font-semibold text-on-surface mb-2">暂无模板</h3>
|
||||
<p class="text-on-surface-variant mb-4">创建第一个模板,让用户更轻松地创建打卡任务</p>
|
||||
<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
|
||||
<a-card
|
||||
v-for="template in templates"
|
||||
: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">
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-100 mb-2">{{ template.name }}</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3">{{ template.description || '无描述' }}</p>
|
||||
<span :class="template.is_active ? 'status-success' : 'status-info'">
|
||||
<h3 class="text-lg font-semibold text-on-surface mb-2">{{ template.name }}</h3>
|
||||
<a-divider style="margin: 8px 0;" />
|
||||
<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 ? '已启用' : '已禁用' }}
|
||||
</span>
|
||||
</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="flex justify-center">
|
||||
@@ -77,7 +78,7 @@
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div></div>
|
||||
<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">
|
||||
<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>
|
||||
@@ -86,7 +87,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<!-- Create/Edit Dialog -->
|
||||
@@ -156,7 +157,7 @@
|
||||
<!-- 字段配置编辑器 -->
|
||||
<div class="field-config-editor">
|
||||
<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-button type="primary">
|
||||
添加字段
|
||||
@@ -188,12 +189,12 @@
|
||||
</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">
|
||||
<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">
|
||||
<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-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" />
|
||||
</svg>
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-2">暂无字段配置</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">点击上方"添加字段"开始配置模板</p>
|
||||
<h3 class="text-lg font-semibold text-on-surface mb-2">暂无字段配置</h3>
|
||||
<p class="text-sm text-on-surface-variant">点击上方"添加字段"开始配置模板</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-3">
|
||||
@@ -215,7 +216,7 @@
|
||||
<span class="text-lg font-bold">JSON 预览</span>
|
||||
</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>
|
||||
</div>
|
||||
</a-form>
|
||||
@@ -242,7 +243,7 @@
|
||||
placeholder="例如: Id, Group1, DateTarget"
|
||||
@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>
|
||||
</a-form-item>
|
||||
@@ -262,14 +263,14 @@
|
||||
:style="isMobile ? { top: 0, maxWidth: '100vw' } : {}"
|
||||
>
|
||||
<div v-if="previewData" class="space-y-4">
|
||||
<div class="bg-gray-50 dark:bg-gray-800 rounded p-4">
|
||||
<h4 class="font-semibold mb-2 text-gray-800 dark:text-gray-100">生成的 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>
|
||||
<div class="bg-surface-container rounded p-4">
|
||||
<h4 class="font-semibold mb-2 text-on-surface">生成的 Payload(使用默认值):</h4>
|
||||
<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 class="bg-gray-50 dark:bg-gray-800 rounded p-4">
|
||||
<h4 class="font-semibold mb-2 text-gray-800 dark:text-gray-100">字段配置:</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>
|
||||
<div class="bg-surface-container rounded p-4">
|
||||
<h4 class="font-semibold mb-2 text-on-surface">字段配置:</h4>
|
||||
<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>
|
||||
|
||||
|
||||
@@ -59,16 +59,22 @@ export default {
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
'md3': '12px',
|
||||
'md3-lg': '16px',
|
||||
'md3-xl': '28px',
|
||||
// Material Design 3 Shape System
|
||||
'md3-xs': '4px', // Extra Small - chips, small tags
|
||||
'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: {
|
||||
'md3-1': '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
||||
'md3-2': '0 1px 3px 1px rgba(0, 0, 0, 0.08)',
|
||||
'md3-3': '0 4px 8px 3px rgba(0, 0, 0, 0.10)',
|
||||
'md3-4': '0 6px 10px 4px rgba(0, 0, 0, 0.12)',
|
||||
'md3-5': '0 8px 12px 6px rgba(0, 0, 0, 0.14)',
|
||||
// Material Design 3 Elevation System (official spec)
|
||||
'md3-0': 'none',
|
||||
'md3-1': '0px 1px 2px 0px rgba(0, 0, 0, 0.3), 0px 1px 3px 1px rgba(0, 0, 0, 0.15)',
|
||||
'md3-2': '0px 1px 2px 0px rgba(0, 0, 0, 0.3), 0px 2px 6px 2px rgba(0, 0, 0, 0.15)',
|
||||
'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: {
|
||||
'fade-in': 'fadeIn 0.3s ease-in-out',
|
||||
|
||||
Reference in New Issue
Block a user