diff --git a/web/tenant/DESIGN_SYSTEM.md b/web/tenant/DESIGN_SYSTEM.md new file mode 100644 index 0000000..b87c3f8 --- /dev/null +++ b/web/tenant/DESIGN_SYSTEM.md @@ -0,0 +1,446 @@ +# Tenant Web 设计系统 + +> 适用范围:`web/tenant` +> +> 当前版本:2026-07-27 +> +> 实现来源:`src/styles/main.css`、`src/views/Layout.vue`、`src/views/dashboard/DashboardPage.vue` + +## 1. 设计定位 + +租户端采用一套轻量、明亮、低对比度的现代控制台设计语言,核心视觉方向为: + +- 冷白蓝雾背景,避免传统后台常见的纯灰或纯白大面积铺底。 +- 蓝紫色作为品牌主色,冰青、天蓝、薄荷绿、暖橙和珊瑚粉作为功能辅助色。 +- 参考 macOS Liquid Glass 的视觉特征,以半透明材质、环境色折射、镜面高光和柔和阴影表现层级。 +- 卡片边界尽量通过透明度、内高光和冷色阴影区分,避免使用过深边框。 +- 信息密度保持适中,使用大圆角、充足留白和紧凑的数据排版。 +- 动效服务于状态反馈,不使用持续、强烈或影响阅读的装饰动画。 + +## 2. 核心视觉原则 + +### 2.1 环境先于玻璃 + +液态玻璃必须建立在具有细微颜色变化的环境背景上。纯白背景没有足够的信息可供玻璃层模糊和折射,因此页面背景及侧栏内部均使用低透明度环境光晕。 + +### 2.2 玻璃以中性色为主 + +玻璃主体保持冷白透明,蓝色、紫色和青色只用于边缘折射或局部光晕,不应形成明显的彩色实心区域。 + +### 2.3 层级由材质区分 + +页面层级按以下顺序增强: + +1. 页面背景:冷白底色与极淡 Mesh Gradient。 +2. 大面积容器:低透明度玻璃与背景模糊。 +3. 普通卡片:更高白色透明度、细描边和轻量阴影。 +4. 激活与悬浮状态:镜面高光、内阴影、边缘虹彩和更明显的悬浮阴影。 + +### 2.4 色彩承担语义 + +- 蓝紫:品牌、主要操作、当前状态。 +- 天蓝:请求、数据、信息类功能。 +- 冰青:Token、连接、实时状态。 +- 绿色:成功、安全、余额正常。 +- 暖橙:提醒、费用、需要关注。 +- 红色:错误、危险、超额。 +- 粉色:模型、内容或次级强调。 + +## 3. 色彩系统 + +### 3.1 品牌主色 + +主色使用蓝紫色阶,默认品牌色为 `Primary 500`。 + +| Token | 色值 | 建议用途 | +| --- | --- | --- | +| `primary-50` | `#F4F3FF` | 浅色选中背景、图标底色 | +| `primary-100` | `#ECE9FF` | 聚焦环、浅色悬停 | +| `primary-200` | `#D9D4FF` | 浅色边框 | +| `primary-300` | `#BBB2FF` | 装饰、高亮边缘 | +| `primary-400` | `#9588FF` | 次级品牌强调 | +| `primary-500` | `#7568F8` | 默认品牌色、主要按钮 | +| `primary-600` | `#6558EA` | 按钮悬停、链接 | +| `primary-700` | `#5547D1` | 按压状态、强调文字 | +| `primary-800` | `#463BAB` | 深色品牌元素 | +| `primary-900` | `#3C3588` | 高对比品牌文字 | +| `primary-950` | `#251F5C` | 极深品牌背景 | + +品牌渐变: + +```css +linear-gradient(135deg, #7b72ff, #6457e8) +``` + +### 3.2 功能辅助色 + +| Token | 色值 | 语义 | +| --- | --- | --- | +| `accent-blue` | `#3B82F6` | 数据、请求、信息 | +| `accent-cyan` | `#06B6D4` | Token、连接、实时状态 | +| `accent-green` | `#10B981` | 成功、安全、正常 | +| `accent-amber` | `#F59E0B` | 费用、提醒、关注 | +| `accent-pink` | `#EC4899` | 模型、内容、次级强调 | +| `accent-violet` | `#8B5CF6` | 品牌扩展、智能能力 | + +辅助色应优先用于小面积元素,例如图标磁贴、图表线条、状态点和数据趋势。避免在大面积容器中使用高饱和纯色。 + +### 3.3 中性色 + +中性色沿用 Slate/Gray 体系: + +| 用途 | 推荐颜色 | +| --- | --- | +| 页面主标题 | `slate-900` / `#0F172A` | +| 卡片标题 | `slate-800` / `#1E293B` | +| 正文 | `slate-600` / `#475569` | +| 次级说明 | `slate-400` / `#94A3B8` | +| 禁用与占位 | `slate-300` / `#CBD5E1` | +| 分隔线 | `slate-100` 或白色透明边框 | + +正文不建议使用纯黑 `#000000`,以保持整体柔和度。 + +### 3.4 页面背景 + +基础背景: + +```css +#F6F8FC +``` + +环境 Mesh Gradient: + +```css +radial-gradient(at 12% 8%, rgba(87, 168, 255, 0.08) 0, transparent 36%), +radial-gradient(at 72% 0%, rgba(137, 117, 255, 0.065) 0, transparent 40%), +radial-gradient(at 95% 70%, rgba(70, 213, 219, 0.055) 0, transparent 38%), +radial-gradient(at 45% 100%, rgba(121, 151, 255, 0.045) 0, transparent 42%) +``` + +背景光晕只用于提供环境色,不应成为页面的视觉主体。 + +## 4. 液态玻璃系统 + +### 4.1 基础玻璃令牌 + +```css +--glass-bg: rgba(255, 255, 255, 0.72); +--glass-bg-strong: rgba(255, 255, 255, 0.84); +--glass-bg-subtle: rgba(255, 255, 255, 0.62); +--glass-border: rgba(255, 255, 255, 0.82); +--glass-blur: 28px; +--glass-saturate: 1.18; +--glass-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.9); +``` + +### 4.2 普通玻璃容器 + +适用于顶栏、悬浮工具栏和大面积弱层级容器: + +- 背景:`rgba(255, 255, 255, 0.62)`。 +- 模糊:`blur(28px)`。 +- 饱和度:`saturate(1.18)`。 +- 描边:半透明白色。 +- 阴影:冷色、低透明度、大扩散范围。 + +### 4.3 卡片玻璃 + +普通内容卡片使用较高的不透明度: + +- 背景:`rgba(255, 255, 255, 0.84)`。 +- 圆角:`24px`。 +- 描边:`rgba(255, 255, 255, 0.82)`。 +- 阴影:`0 10px 34px rgba(91, 104, 151, 0.08)`。 +- 内高光:`inset 0 1px 0 rgba(255, 255, 255, 0.9)`。 + +卡片不使用厚重灰色边框。悬停时只允许轻微上移,并适度增强阴影。 + +### 4.4 侧栏环境玻璃 + +侧栏不是纯白色容器,而是带有轻微环境色的独立玻璃层: + +```css +background: + radial-gradient(circle at 4% 7%, rgba(80, 171, 255, 0.16), transparent 31%), + radial-gradient(circle at 105% 62%, rgba(139, 111, 248, 0.13), transparent 35%), + radial-gradient(circle at 28% 106%, rgba(64, 211, 211, 0.10), transparent 30%), + rgba(250, 252, 255, 0.62); +backdrop-filter: blur(32px) saturate(1.35); +``` + +这些环境色为菜单选中项提供可模糊和折射的内容,同时仍保持侧栏接近冷白色。 + +### 4.5 菜单选中玻璃 + +菜单选中态采用接近 macOS Liquid Glass 的纯 CSS 近似效果: + +- 中性半透明白色为主体。 +- 左下角冰青、右下角蓝紫作为极淡折射色。 +- `blur(26px) saturate(1.7) brightness(1.06)` 增强背景折射。 +- 顶部椭圆形高光模拟镜面反射。 +- 底部 `1px` 虹彩线模拟玻璃边缘色散。 +- 多层内阴影模拟玻璃厚度和弧面。 +- 外阴影表现轻微悬浮,不使用强烈光晕。 + +所有菜单项默认保留 `1px` 透明边框,选中时只替换边框颜色,避免状态切换造成布局偏移。 + +纯 CSS 无法实现真实的动态光线折射。若未来需要内容随背景实时扭曲,应使用 SVG displacement filter 或 WebGL shader。 + +## 5. 圆角系统 + +| 层级 | 圆角 | 使用场景 | +| --- | --- | --- | +| 小 | `8px` | 标签、紧凑按钮、序号 | +| 中 | `12px` | 输入框、小图标容器 | +| 常规 | `15px–16px` | 菜单项、普通按钮 | +| 大 | `18px` | 快捷操作、内部面板 | +| 卡片 | `24px` | 数据卡片、图表卡片 | +| 容器 | `28px` | 主侧栏、大型玻璃容器 | +| 胶囊 | `9999px` | 搜索框、状态标签、圆形操作区 | + +同一区域内圆角应呈现明确层级:外层容器圆角最大,内部元素依次减小。 + +## 6. 阴影系统 + +| Token | 配方 | 用途 | +| --- | --- | --- | +| `shadow-glass-sm` | `0 8px 24px rgba(76, 91, 142, 0.08)` | 顶栏、轻量浮层 | +| `shadow-glass` | `0 18px 55px rgba(76, 91, 142, 0.11)` | 大型玻璃容器 | +| `shadow-card` | 冷色外阴影 + 白色内高光 | 普通卡片 | +| `shadow-card-hover` | `0 18px 48px rgba(91, 104, 151, 0.14)` | 卡片悬停 | +| `shadow-glow` | 蓝紫色柔光 | 品牌图标和少量强调 | + +阴影颜色使用蓝灰或品牌色透明值,不使用纯黑重阴影。 + +## 7. 字体与排版 + +字体栈: + +```css +system-ui, +-apple-system, +BlinkMacSystemFont, +'Segoe UI', +Roboto, +'Helvetica Neue', +Arial, +'PingFang SC', +'Hiragino Sans GB', +'Microsoft YaHei', +sans-serif +``` + +推荐排版层级: + +| 类型 | 建议规格 | +| --- | --- | +| 页面标题 | `28px`,`700`,紧凑字距 | +| 区块标题 | `16px`,`600` | +| 卡片关键数据 | `28px–30px`,`700`,等宽数字 | +| 正文 | `14px`,`400–500` | +| 次级说明 | `12px`,`400` | +| 辅助标签 | `10px–11px`,`500–700` | + +金额、百分比、Token 和请求数量应使用 `tabular-nums`,防止数字变化时宽度跳动。 + +## 8. 图标与渐变磁贴 + +图标以线性 SVG 为主,常规尺寸为 `16px–20px`。统计卡片和快捷入口可使用渐变图标磁贴: + +| 类型 | 渐变方向 | +| --- | --- | +| Violet | `#8E7DFF → #6D5CE7` | +| Blue | `#5DB9FF → #3889EB` | +| Cyan | `#48D5D5 → #22B3BD` | +| Green | `#64DCB0 → #2CB887` | +| Amber | `#FFC861 → #F0A335` | +| Pink | `#FF8EB5 → #EB6394` | + +磁贴应使用白色内高光和低透明度同色阴影,避免使用纯色平面图标块。 + +## 9. 图表配色 + +推荐图表序列: + +```text +#7568F8 蓝紫,主数据 +#399CF4 天蓝,次数据 +#2CC5C1 冰青 +#FFB552 暖橙 +#F487A8 珊瑚粉 +#9F74EE 紫色扩展 +#67D49D 薄荷绿 +#FF826F 珊瑚红 +``` + +图表使用规范: + +- 主曲线使用蓝紫,第二曲线使用天蓝。 +- 填充区域透明度控制在 `0.04–0.12`。 +- 网格线使用低透明度 Slate 色。 +- Tooltip 使用深蓝灰半透明背景,不使用纯黑。 +- 环形图扇区之间使用半透明白色描边,保持玻璃环境下的分离度。 + +## 10. 动效规范 + +基础动效时长: + +- 快速反馈:`180ms–200ms`。 +- 页面与卡片进入:`300ms`。 +- 图表加载:约 `650ms`。 +- 持续装饰动画:应谨慎使用,并控制在低频、低幅度范围。 + +推荐缓动: + +```css +ease-out +``` + +允许的悬停变化: + +- 上移 `1px–3px`。 +- 轻微增强阴影。 +- 提升玻璃背景白色透明度。 +- 图标或文字改变为品牌色。 + +禁止通过增加边框宽度、内边距或字体粗细改变元素尺寸,避免页面发生布局抖动。 + +所有非必要动画必须兼容: + +```css +@media (prefers-reduced-motion: reduce) +``` + +## 11. 响应式规范 + +- `< 1024px`:侧栏使用抽屉模式,完全移出视口并显示遮罩。 +- `>= 1024px`:侧栏固定悬浮,主内容根据展开状态调整左边距。 +- 指标卡:手机单列、平板双列、宽屏四列。 +- 图表区:常规屏幕单列,超宽屏采用主趋势图与占比图组合布局。 +- 快捷操作:根据容器宽度使用两列或三列。 +- 顶栏搜索框在小屏幕隐藏,保留关键操作按钮。 + +## 12. 可访问性与降级 + +- 玻璃背景上的正文必须保持足够对比度,正文不使用低于 `slate-500` 的颜色。 +- 状态不能只依赖颜色表达,应同时提供图标、文字或百分比。 +- 交互元素应提供清晰的键盘聚焦环。 +- 不支持 `backdrop-filter` 时,玻璃组件降级为高不透明度白色背景。 +- 禁用状态降低透明度,但仍应保留可读文字。 +- 动画应支持 `prefers-reduced-motion`。 + +## 13. 使用建议 + +### 推荐 + +- 使用透明白色、内高光和冷色阴影建立材质层次。 +- 让背景保留极淡的环境色,为玻璃提供折射内容。 +- 使用蓝紫色表达品牌和主要状态。 +- 使用功能色区分数据类别和业务状态。 +- 保持大圆角、低对比边界和充足留白。 + +### 避免 + +- 避免大面积高饱和蓝紫渐变。 +- 避免纯黑阴影、纯黑正文和深灰粗边框。 +- 避免在同一视图中使用过多发光元素。 +- 避免所有卡片都设置悬浮动画,静态信息卡可保持稳定。 +- 避免只有透明白色、没有环境色的“假玻璃”效果。 +- 避免通过选中态边框改变元素实际尺寸。 + +## 14. 实现位置 + +| 内容 | 文件 | +| --- | --- | +| 全局主题、色彩和玻璃组件 | `src/styles/main.css` | +| 租户端侧栏与顶栏 | `src/views/Layout.vue` | +| 仪表盘信息架构与数据卡片 | `src/views/dashboard/DashboardPage.vue` | +| 趋势图 | `src/components/charts/TokenTrendChart.vue` | +| 模型费用环形图 | `src/components/charts/ModelDistChart.vue` | + +后续新增页面应优先复用全局 `.card`、`.glass`、`.glass-card`、`.sidebar-link`、按钮和输入框样式,避免在页面内部重新定义相似但不一致的颜色与材质配方。 + +## 15. 表格规范 + +租户端表格统一使用 `.table-container` 和 `.table`,列表分页使用 `.table-pagination`。 + +### 15.1 表格容器 + +- 圆角:`20px`。 +- 背景:半透明冷白玻璃,叠加极淡的天蓝和蓝紫环境光。 +- 模糊:`blur(22px) saturate(1.2)`。 +- 描边:半透明白色,不使用传统灰色实线外框。 +- 阴影:冷色轻阴影与顶部内高光。 +- 横向滚动条使用蓝紫渐变滑块,宽表格在移动端保持横向滚动。 + +当表格已经位于带有 `overflow-hidden` 的卡片内部时,使用 `.table-container-flush` 移除重复的圆角、描边和阴影。 + +### 15.2 表头 + +- 高度由 `14px` 上下内边距构成。 +- 字号:`12px`。 +- 字重:`650`。 +- 文字颜色:`#64748B`。 +- 背景:冷白玻璃叠加低透明度蓝紫渐变。 +- 使用顶部镜面高光和底部柔和分隔线表现玻璃厚度。 +- 相邻表头之间使用短竖向渐变分隔,不使用贯穿整行的深色边框。 +- 表头内容默认不换行,长表格通过横向滚动查看。 + +### 15.3 数据行 + +- 单元格默认背景为低透明度白色。 +- 偶数行使用极淡蓝灰背景,形成弱斑马纹。 +- 行分隔线使用低透明度 Slate 色。 +- 悬停或行内元素获得焦点时,整行显示冰蓝至淡紫的液态渐变。 +- 悬停行首显示 `3px` 蓝紫色内侧指示条。 +- 数字统一使用 `tabular-nums`,避免金额和计数变化造成宽度跳动。 +- 表格内普通链接使用 `primary-600`,悬停时切换为 `primary-800`。 + +### 15.4 行内操作 + +- 操作按钮优先使用小尺寸幽灵按钮。 +- 默认背景保持透明或极淡白色。 +- 悬停时增加白色玻璃背景、浅色描边和轻量阴影。 +- 状态使用圆角徽章,不通过纯文本颜色单独表达。 +- 危险操作继续使用红色语义,但避免大面积红色按钮。 + +### 15.5 分页栏 + +- 使用 `.table-pagination` 与表格卡片连接。 +- 背景为冷白半透明渐变,与表头形成上下呼应。 +- 顶部分隔使用白色透明边缘,而不是旧式灰色边框。 +- 翻页按钮使用小型玻璃按钮,悬停时显示品牌色。 +- 禁用按钮降低对比度并移除阴影。 +- 手机端允许分页信息和按钮自动换行。 + +### 15.6 使用示例 + +```vue +
+ + + + + + + + + + + + + + + +
名称状态操作
{{ item.name }}正常
+
+ +
+ 共 {{ total }} 条记录 +
+ + {{ page }} / {{ totalPages }} + +
+
+``` diff --git a/web/tenant/index.html b/web/tenant/index.html index 65016ae..bf202ff 100644 --- a/web/tenant/index.html +++ b/web/tenant/index.html @@ -10,7 +10,7 @@ - + diff --git a/web/tenant/src/components/charts/ModelDistChart.vue b/web/tenant/src/components/charts/ModelDistChart.vue index 13c6dc8..2a1215a 100644 --- a/web/tenant/src/components/charts/ModelDistChart.vue +++ b/web/tenant/src/components/charts/ModelDistChart.vue @@ -2,13 +2,14 @@ import { computed } from 'vue' import { Doughnut } from 'vue-chartjs' import { - Chart as ChartJS, ArcElement, - Tooltip, + Chart as ChartJS, Legend, + Tooltip, + type ChartData, type ChartOptions, - type ChartData } from 'chart.js' +import Icon from '@/components/common/Icon.vue' ChartJS.register(ArcElement, Tooltip, Legend) @@ -22,118 +23,101 @@ const props = withDefaults(defineProps<{ }> loading?: boolean }>(), { - data: () => [] + data: () => [], }) -const COLORS = [ - '#14b8a6', '#3b82f6', '#8b5cf6', '#f59e0b', '#ef4444', - '#ec4899', '#06b6d4', '#84cc16', '#f97316', '#6366f1', - '#10b981', '#e11d48', '#0ea5e9', '#a855f7', '#eab308', - '#d946ef', '#22d3ee', '#4ade80', '#fb923c', '#818cf8' -] +const colors = ['#7568f8', '#42a4f5', '#2cc5c1', '#ffb552', '#f487a8', '#9f74ee', '#67d49d', '#ff826f'] -const formatNumber = (n: number): string => { - if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M' - if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K' - return String(n) +function formatCost(value: number): string { + if (value === 0) return '$0.00' + if (value >= 1) return `$${value.toFixed(2)}` + return `$${value.toFixed(4)}` } +const totalCost = computed(() => props.data.reduce((sum, item) => sum + Number(item.total_cost || 0), 0)) + const chartData = computed>(() => ({ - labels: props.data.map(d => d.model_name), + labels: props.data.map((item) => item.model_name), datasets: [{ - data: props.data.map(d => d.total_cost), - backgroundColor: props.data.map((_, i) => COLORS[i % COLORS.length]), - borderWidth: 2, - borderColor: '#ffffff', - hoverOffset: 6 - }] + data: props.data.map((item) => item.total_cost), + backgroundColor: props.data.map((_, index) => colors[index % colors.length]), + borderWidth: 4, + borderColor: 'rgba(255, 255, 255, 0.92)', + hoverBorderColor: '#ffffff', + hoverOffset: 7, + borderRadius: 4, + }], })) const chartOptions = computed>(() => ({ responsive: true, maintainAspectRatio: true, - cutout: '65%', + cutout: '70%', + animation: { duration: 650, easing: 'easeOutQuart' }, plugins: { legend: { display: false }, tooltip: { - backgroundColor: 'rgba(15, 23, 42, 0.9)', - padding: 10, - cornerRadius: 8, + backgroundColor: 'rgba(30, 36, 61, 0.92)', + padding: 12, + cornerRadius: 12, callbacks: { - label: (ctx) => { - const total = ctx.dataset.data.reduce((s, v) => s + (v as number), 0) - const pct = total > 0 ? ((ctx.parsed / total) * 100).toFixed(1) : '0' - return ` $${(ctx.parsed as number).toFixed(4)} (${pct}%)` - } - } - } - } + label: (context) => { + const value = Number(context.parsed) || 0 + const percent = totalCost.value > 0 ? ((value / totalCost.value) * 100).toFixed(1) : '0' + return ` ${formatCost(value)} · ${percent}%` + }, + }, + }, + }, })) -const totalCost = computed(() => props.data.reduce((s, d) => s + d.total_cost, 0)) - const modelList = computed(() => - props.data.map((d, i) => ({ - ...d, - total_tokens: d.input_tokens + d.output_tokens, - color: COLORS[i % COLORS.length], - percent: totalCost.value > 0 ? ((d.total_cost / totalCost.value) * 100).toFixed(1) : '0' - })) + props.data.slice(0, 5).map((item, index) => ({ + ...item, + color: colors[index % colors.length], + percent: totalCost.value > 0 ? ((item.total_cost / totalCost.value) * 100).toFixed(1) : '0', + })), ) diff --git a/web/tenant/src/components/charts/TokenTrendChart.vue b/web/tenant/src/components/charts/TokenTrendChart.vue index 3650a96..20a94dd 100644 --- a/web/tenant/src/components/charts/TokenTrendChart.vue +++ b/web/tenant/src/components/charts/TokenTrendChart.vue @@ -2,18 +2,19 @@ import { computed } from 'vue' import { Line } from 'vue-chartjs' import { - Chart as ChartJS, CategoryScale, + Chart as ChartJS, + Filler, + Legend, LinearScale, - PointElement, LineElement, + PointElement, Title, Tooltip, - Legend, - Filler, + type ChartData, type ChartOptions, - type ChartData } from 'chart.js' +import Icon from '@/components/common/Icon.vue' ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, Filler) @@ -26,151 +27,163 @@ const props = withDefaults(defineProps<{ requests: number }> loading?: boolean + days?: number }>(), { - data: () => [] + data: () => [], + days: 7, }) -const formatTokenLabel = (value: number): string => { - if (value >= 1_000_000_000) return (value / 1_000_000_000).toFixed(1) + 'B' - if (value >= 1_000_000) return (value / 1_000_000).toFixed(1) + 'M' - if (value >= 1_000) return (value / 1_000).toFixed(1) + 'K' +const emit = defineEmits<{ + changeDays: [days: number] +}>() + +function formatCompact(value: number): string { + if (value >= 1_000_000_000) return `${(value / 1_000_000_000).toFixed(1)}B` + if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M` + if (value >= 1_000) return `${(value / 1_000).toFixed(1)}K` return String(value) } -const formatDateLabel = (dateStr: string): string => { - if (!dateStr) return '' - const parts = dateStr.split('-') - if (parts.length === 3) return `${parts[1]}/${parts[2]}` - return dateStr +function formatDateLabel(date: string): string { + const parts = date.split('-') + return parts.length === 3 ? `${parts[1]}-${parts[2]}` : date } const chartData = computed>(() => ({ - labels: props.data.map(d => formatDateLabel(d.date)), + labels: props.data.map((item) => formatDateLabel(item.date)), datasets: [ { - label: '输入 Token', - data: props.data.map(d => d.input_tokens), - borderColor: '#3b82f6', - backgroundColor: 'rgba(59, 130, 246, 0.08)', + label: 'Token 用量', + data: props.data.map((item) => item.input_tokens + item.output_tokens), + borderColor: '#7568f8', + backgroundColor: 'rgba(117, 104, 248, 0.1)', fill: true, - tension: 0.4, - pointRadius: 2, + tension: 0.42, + pointRadius: 2.5, pointHoverRadius: 5, - borderWidth: 2, - yAxisID: 'y' + pointBackgroundColor: '#ffffff', + pointBorderColor: '#7568f8', + pointBorderWidth: 2, + borderWidth: 2.25, + yAxisID: 'y', }, { - label: '输出 Token', - data: props.data.map(d => d.output_tokens), - borderColor: '#10b981', - backgroundColor: 'rgba(16, 185, 129, 0.08)', - fill: true, - tension: 0.4, - pointRadius: 2, - pointHoverRadius: 5, - borderWidth: 2, - yAxisID: 'y' - }, - { - label: '费用 ($)', - data: props.data.map(d => d.total_cost), - borderColor: '#f59e0b', - backgroundColor: 'transparent', + label: '请求数', + data: props.data.map((item) => item.requests), + borderColor: '#399cf4', + backgroundColor: 'rgba(57, 156, 244, 0.04)', fill: false, - tension: 0.4, - pointRadius: 2, + tension: 0.42, + pointRadius: 2.5, pointHoverRadius: 5, - borderWidth: 2, - borderDash: [5, 5], - yAxisID: 'y1' - } - ] + pointBackgroundColor: '#ffffff', + pointBorderColor: '#399cf4', + pointBorderWidth: 2, + borderWidth: 2.25, + yAxisID: 'y1', + }, + ], })) const chartOptions = computed>(() => ({ responsive: true, maintainAspectRatio: false, - interaction: { - mode: 'index', - intersect: false - }, + interaction: { mode: 'index', intersect: false }, + animation: { duration: 650, easing: 'easeOutQuart' }, plugins: { legend: { position: 'top', - align: 'end', + align: 'start', labels: { usePointStyle: true, pointStyle: 'circle', - boxWidth: 6, - boxHeight: 6, - padding: 16, - font: { size: 12 } - } + boxWidth: 7, + boxHeight: 7, + padding: 18, + color: '#718096', + font: { size: 11, weight: 500 }, + }, }, tooltip: { - backgroundColor: 'rgba(15, 23, 42, 0.9)', - titleFont: { size: 12 }, - bodyFont: { size: 12 }, + backgroundColor: 'rgba(30, 36, 61, 0.92)', + titleColor: '#ffffff', + bodyColor: '#e2e8f0', padding: 12, - cornerRadius: 8, + cornerRadius: 12, + displayColors: true, callbacks: { - label: (ctx) => { - const label = ctx.dataset.label || '' - const value = ctx.parsed.y ?? 0 - if (label.includes('费用')) return `${label}: $${value.toFixed(4)}` - return `${label}: ${formatTokenLabel(value)}` - } - } - } + label: (context) => { + const value = context.parsed.y ?? 0 + return `${context.dataset.label}: ${formatCompact(value)}` + }, + }, + }, }, scales: { x: { + border: { display: false }, grid: { display: false }, - ticks: { - font: { size: 11 }, - color: '#9ca3af', - maxRotation: 0 - } + ticks: { color: '#9aa7bd', font: { size: 10 }, maxRotation: 0 }, }, y: { position: 'left', - grid: { color: 'rgba(0,0,0,0.04)' }, + border: { display: false }, + grid: { color: 'rgba(148, 163, 184, 0.14)', lineWidth: 1 }, ticks: { - font: { size: 11 }, - color: '#9ca3af', - callback: (value) => formatTokenLabel((value as number) ?? 0) + color: '#9aa7bd', + font: { size: 10 }, + padding: 8, + callback: (value) => formatCompact(Number(value) || 0), }, - beginAtZero: true + beginAtZero: true, }, y1: { position: 'right', + border: { display: false }, grid: { display: false }, ticks: { - font: { size: 11 }, - color: '#f59e0b', - callback: (value) => '$' + ((value as number) ?? 0).toFixed(2) + color: '#9aa7bd', + font: { size: 10 }, + padding: 8, + callback: (value) => formatCompact(Number(value) || 0), }, - beginAtZero: true - } - } + beginAtZero: true, + }, + }, })) + + diff --git a/web/tenant/src/views/settings/LoginHistoryPage.vue b/web/tenant/src/views/settings/LoginHistoryPage.vue index 1331b3c..159b58b 100644 --- a/web/tenant/src/views/settings/LoginHistoryPage.vue +++ b/web/tenant/src/views/settings/LoginHistoryPage.vue @@ -180,7 +180,7 @@ const methodLabel: Record = { -