Pārlūkot izejas kodu

PureAdmin版PC前端优化

skyline 2 nedēļas atpakaļ
vecāks
revīzija
fc913e2226
30 mainītis faili ar 919 papildinājumiem un 643 dzēšanām
  1. 9 7
      PRODUCT.md
  2. 238 0
      admin-web-new/.impeccable/design.json
  3. 36 12
      admin-web-new/DESIGN.md
  4. 1 1
      admin-web-new/public/platform-config.json
  5. 2 2
      admin-web-new/src/components/ReIcon/src/iconifyIconOffline.ts
  6. 1 1
      admin-web-new/src/components/ReIcon/src/iconifyIconOnline.ts
  7. 1 1
      admin-web-new/src/components/ReSelector/src/index.css
  8. 1 2
      admin-web-new/src/layout/components/lay-content/index.vue
  9. 0 11
      admin-web-new/src/layout/components/lay-navbar/index.vue
  10. 11 20
      admin-web-new/src/layout/components/lay-sidebar/NavVertical.vue
  11. 2 0
      admin-web-new/src/layout/components/lay-sidebar/components/SidebarLogo.vue
  12. 1 1
      admin-web-new/src/layout/hooks/useLayout.ts
  13. 1 3
      admin-web-new/src/layout/index.vue
  14. 3 23
      admin-web-new/src/store/modules/app.ts
  15. 1 1
      admin-web-new/src/store/modules/epTheme.ts
  16. 8 0
      admin-web-new/src/style/element-plus.scss
  17. 17 0
      admin-web-new/src/style/index.scss
  18. 5 0
      admin-web-new/src/style/reset.scss
  19. 33 5
      admin-web-new/src/style/sidebar.scss
  20. 4 4
      admin-web-new/src/style/theme.scss
  21. 1 1
      admin-web-new/src/utils/responsive.ts
  22. 253 96
      admin-web-new/src/views/admin/dashboard/index.vue
  23. 2 2
      admin-web-new/src/views/admin/finance/split-record.vue
  24. 3 3
      admin-web-new/src/views/admin/ordering/dialog.vue
  25. 1 1
      admin-web-new/src/views/admin/ordering/index.vue
  26. 2 2
      admin-web-new/src/views/admin/platform/device-config-dialog.vue
  27. 2 2
      admin-web-new/src/views/admin/role/index.vue
  28. 1 1
      admin-web-new/src/views/admin/station/device.vue
  29. 1 1
      admin-web-new/src/views/admin/station/list.vue
  30. 278 440
      admin-web/yarn.lock

+ 9 - 7
PRODUCT.md

@@ -8,29 +8,31 @@ product
 
 - **洗车店经营者**:在PC端查看营收报表、经营数据分析,做经营决策
 - **洗车店员工**:在PC端和手机端处理订单、管理会员、维护库存、设定价格等日常业务操作
+- **洗车服务商/商家**:通过平台接入门店,提供洗车耗材、设备维护、增值服务等,管理自己的供货和结算
 - **车主用户**:通过微信小程序预约洗车、下单、查看订单状态、使用会员权益
 
 ## Product Purpose
 
-为自助洗车门店提供一站式运营管理平台后台管理系统覆盖订单、会员、库存、营收等核心业务,微信小程序为车主提供便捷的预约和下单体验。目标是帮助门店经营者提升运营效率、降低管理成本
+为自助洗车门店提供一站式运营管理平台,同时连接服务商与门店形成完整的服务网络。后台管理系统覆盖订单、会员、库存、营收等核心业务,微信小程序为车主提供便捷的预约和下单体验。平台目标从提升单店运营效率扩展为让门店、服务商、车主三方在一个系统中高效协作
 
 ## Brand Personality
 
-简洁、清晰、现代。像 Notion / Linear 那样,信息层级分明,留白得当,交互克制精准。不花哨,不陈旧,让操作者在日常高频使用中感到清爽和专业
+清晰、亲切、高效。像一个熟悉的老工具:不需要说明书,手感刚好,放在那里就知道该怎么用。不冷冰冰,不咄咄逼人,但在需要的时候精确可靠。对标 Coda、Arc 这类「有温度的工具」——它们首先是好用的工具,但不吝啬在细节里放一点个性和关怀
 
 ## Anti-references
 
 - **花哨营销风**:避免大色块堆砌、过度装饰、多余动画、营销口号式文案
 - **老旧传统后台**:避免密集表格、灰色调泛滥、控件堆叠、缺乏视觉层级和呼吸感的老式ERP风格
 - **AI生成感**:避免SaaS奶油白+单色蓝紫渐变的模板化配色,避免统一尺寸的图标卡片网格
+- **冷漠工具感**:避免为了追求「专业」而牺牲温度——无性格的灰色、机械的间距、缺乏反馈的交互
 
 ## Design Principles
 
-1. **信息层级优先** — 用户的核心操作路径必须一目了然,重要数据和操作权重高于辅助信息
-2. **克制而非贫瘠** — 少即是多,但关键交互需要清晰的视觉反馈,不做为简洁而牺牲可用性
-3. **现代工具感** — 对标一流工具的打磨程度:间距节奏有变化,排版有对比,色彩有策略而非随意
-4. **多端一致** — PC管理后台与手机小程序在视觉语言上保持统一,让使用者在不同设备间切换无认知负担
-5. **效率导向** — 每个元素的存在都有目的,不增加操作者的认知负荷和点击步数
+1. **场景感知** — 界面应该知道操作者当前在做什么任务,把相关的数据和操作浮到表面,收起无关信息。减少操作者的记忆负担和寻找成本
+2. **温润有质** — 在专业工具的框架内注入适度的质感和温度。间距有呼吸,色彩不刺眼,交互有反馈。操作者每天面对这个界面数小时,它不该让人感到疲惫或冷漠
+3. **清晰优先** — 信息层级是可用性的根基。重要数据和主操作路径在任何屏幕上必须一目了然,权重差异通过字号和字重表达,不依赖颜色区分
+4. **亲切高效** — 高效不意味着机械。减少点击步数的同时,交互反馈应当是友好和确定的。错误提示说人话,空状态给引导,让操作者感到被理解而非被训斥
+5. **多端如一** — PC管理后台、服务商端、手机小程序在视觉语言和交互模式上保持统一,让使用者在不同设备间切换时无需重新学习
 
 ## Accessibility & Inclusion
 

+ 238 - 0
admin-web-new/.impeccable/design.json

@@ -0,0 +1,238 @@
+{
+  "schemaVersion": 2,
+  "generatedAt": "2026-05-14T09:14:00Z",
+  "title": "Design System: 自助洗车运营管理系统",
+  "extensions": {
+    "colorMeta": {
+      "brand-red": {
+        "role": "primary",
+        "displayName": "Brand Red",
+        "canonical": "oklch(52% 0.16 25)",
+        "tonalRamp": [
+          "oklch(15% 0.04 25)",
+          "oklch(25% 0.08 25)",
+          "oklch(35% 0.12 25)",
+          "oklch(45% 0.16 25)",
+          "oklch(52% 0.16 25)",
+          "oklch(65% 0.12 25)",
+          "oklch(78% 0.08 25)",
+          "oklch(92% 0.03 25)"
+        ]
+      },
+      "surface-white": {
+        "role": "neutral",
+        "displayName": "Surface White",
+        "canonical": "oklch(99% 0.002 25)",
+        "tonalRamp": [
+          "oklch(96% 0.001 25)",
+          "oklch(97% 0.001 25)",
+          "oklch(98% 0.002 25)",
+          "oklch(99% 0.002 25)",
+          "oklch(99.5% 0.001 25)",
+          "oklch(99% 0.003 25)",
+          "oklch(98% 0.004 25)",
+          "oklch(97% 0.005 25)"
+        ]
+      },
+      "content-bg": {
+        "role": "neutral",
+        "displayName": "Content Background",
+        "canonical": "oklch(95% 0.005 255)",
+        "tonalRamp": [
+          "oklch(90% 0.003 255)",
+          "oklch(92% 0.004 255)",
+          "oklch(94% 0.005 255)",
+          "oklch(95% 0.005 255)",
+          "oklch(96% 0.004 255)",
+          "oklch(97% 0.003 255)",
+          "oklch(98% 0.002 255)",
+          "oklch(99% 0.001 255)"
+        ]
+      },
+      "text-primary": {
+        "role": "neutral",
+        "displayName": "Text Primary",
+        "canonical": "oklch(27% 0.005 255)"
+      },
+      "text-regular": {
+        "role": "neutral",
+        "displayName": "Text Regular",
+        "canonical": "oklch(45% 0.005 255)"
+      },
+      "text-secondary": {
+        "role": "neutral",
+        "displayName": "Text Secondary",
+        "canonical": "oklch(62% 0.005 255)"
+      },
+      "sidebar-bg": {
+        "role": "neutral",
+        "displayName": "Sidebar Background",
+        "canonical": "oklch(14% 0.03 255)"
+      }
+    },
+    "typographyMeta": {
+      "headline": {
+        "displayName": "Headline",
+        "purpose": "Page-level headings. At most one per page."
+      },
+      "title": {
+        "displayName": "Title",
+        "purpose": "Section headers, card titles, dialog titles."
+      },
+      "body": {
+        "displayName": "Body",
+        "purpose": "Body text, table content, descriptions. Max 75ch line width."
+      },
+      "label": {
+        "displayName": "Label",
+        "purpose": "Form labels and field names. Weight carries hierarchy, not color."
+      },
+      "caption": {
+        "displayName": "Caption",
+        "purpose": "Helper text, timestamps, secondary metadata."
+      }
+    },
+    "shadows": [
+      {
+        "name": "message-float",
+        "value": "0 3px 6px -4px rgba(0,0,0,0.12), 0 6px 16px rgba(0,0,0,0.08), 0 9px 28px 8px rgba(0,0,0,0.05)",
+        "purpose": "Message toasts and notification popups."
+      },
+      {
+        "name": "dialog-footer",
+        "value": "0 -1px 0 0 #e0e3e8, 0 -3px 6px 0 rgb(69 98 155 / 12%)",
+        "purpose": "Bottom divider shadow for dialogs and search overlays."
+      },
+      {
+        "name": "table-fixed-column",
+        "value": "8px 0 10px -5px rgba(0,0,0,0.06)",
+        "purpose": "Horizontal scroll hint for fixed table columns."
+      }
+    ],
+    "motion": [
+      {
+        "name": "ease-standard",
+        "value": "cubic-bezier(0.4, 0, 0.2, 1)",
+        "purpose": "Default transition for hover states and UI element changes."
+      },
+      {
+        "name": "sidebar-transition",
+        "value": "var(--pure-transition-duration, 0.3s)",
+        "purpose": "Sidebar expand/collapse and layout width transitions."
+      }
+    ],
+    "breakpoints": [
+      { "name": "mobile", "value": "760px" },
+      { "name": "collapsed-sidebar", "value": "990px" }
+    ]
+  },
+  "components": [
+    {
+      "name": "Primary Button",
+      "kind": "button",
+      "refersTo": "button-primary",
+      "description": "Main action button: confirm, submit, create. Brand red with white text.",
+      "html": "<button class=\"ds-btn-primary\">确认提交</button>",
+      "css": ".ds-btn-primary { background: #C83A35; color: #fff; border: none; border-radius: 4px; padding: 8px 15px; font-size: 14px; font-family: \"Helvetica Neue\", Helvetica, \"PingFang SC\", \"Hiragino Sans GB\", \"Microsoft YaHei\", sans-serif; cursor: pointer; transition: background-color 0.2s ease; } .ds-btn-primary:hover { background: oklch(45% 0.16 25); } .ds-btn-primary:focus-visible { outline: 2px solid #C83A35; outline-offset: 2px; } .ds-btn-primary:active { background: oklch(38% 0.15 25); }"
+    },
+    {
+      "name": "Secondary Button",
+      "kind": "button",
+      "refersTo": "button-default",
+      "description": "Secondary actions: cancel, back, batch operations.",
+      "html": "<button class=\"ds-btn-secondary\">取消</button>",
+      "css": ".ds-btn-secondary { background: #FEFEFE; color: #303133; border: 1px solid rgb(5 5 5 / 6%); border-radius: 4px; padding: 8px 15px; font-size: 14px; font-family: \"Helvetica Neue\", Helvetica, \"PingFang SC\", \"Hiragino Sans GB\", \"Microsoft YaHei\", sans-serif; cursor: pointer; transition: background-color 0.2s ease; } .ds-btn-secondary:hover { background: #f5f5f5; }"
+    },
+    {
+      "name": "Text Link Button",
+      "kind": "button",
+      "refersTo": "button-text",
+      "description": "Inline table row actions: edit, view, delete.",
+      "html": "<button class=\"ds-btn-text\">编辑</button>",
+      "css": ".ds-btn-text { background: transparent; color: #C83A35; border: none; padding: 0; font-size: 14px; cursor: pointer; transition: opacity 0.2s ease; } .ds-btn-text:hover { opacity: 0.8; }"
+    },
+    {
+      "name": "Active Menu Item",
+      "kind": "nav",
+      "refersTo": "menu-item-active",
+      "description": "Currently active sidebar menu item with brand red background pill.",
+      "html": "<div class=\"ds-menu-item-active\"><span class=\"ds-menu-icon\"><svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/><line x1=\"9\" y1=\"9\" x2=\"15\" y2=\"9\"/><line x1=\"9\" y1=\"13\" x2=\"15\" y2=\"13\"/><line x1=\"9\" y1=\"17\" x2=\"12\" y2=\"17\"/></svg></span><span>订单管理</span></div>",
+      "css": ".ds-menu-item-active { position: relative; display: flex; align-items: center; height: 50px; padding: 0 20px; color: #fff; font-size: 14px; font-family: \"Helvetica Neue\", Helvetica, \"PingFang SC\", \"Hiragino Sans GB\", \"Microsoft YaHei\", sans-serif; cursor: pointer; z-index: 1; } .ds-menu-item-active::before { position: absolute; inset: 0 8px; margin: 4px 0; content: \"\"; background: #C83A35; border-radius: 3px; z-index: -1; } .ds-menu-icon { display: flex; align-items: center; margin-right: 5px; width: 18px; height: 18px; }"
+    },
+    {
+      "name": "Content Card",
+      "kind": "card",
+      "refersTo": "card",
+      "description": "Standard page content container with subtle hover shadow feedback.",
+      "html": "<div class=\"ds-card\"><div class=\"ds-card-body\">卡片内容</div></div>",
+      "css": ".ds-card { background: #FEFEFE; border-radius: 4px; border: 1px solid rgb(5 5 5 / 6%); overflow: hidden; transition: box-shadow 0.2s ease; } .ds-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.08); } .ds-card-body { padding: 20px; font-size: 14px; color: #303133; font-family: \"Helvetica Neue\", Helvetica, \"PingFang SC\", \"Hiragino Sans GB\", \"Microsoft YaHei\", sans-serif; }"
+    },
+    {
+      "name": "Text Input",
+      "kind": "input",
+      "refersTo": "input",
+      "description": "Standard form input with brand red focus ring.",
+      "html": "<div class=\"ds-form-item\"><label class=\"ds-label\">字段名</label><input class=\"ds-input\" type=\"text\" placeholder=\"请输入\" /></div>",
+      "css": ".ds-form-item { display: flex; flex-direction: column; gap: 8px; } .ds-label { font-size: 14px; font-weight: 700; color: #606266; font-family: \"Helvetica Neue\", Helvetica, \"PingFang SC\", \"Hiragino Sans GB\", \"Microsoft YaHei\", sans-serif; } .ds-input { background: #FEFEFE; border: 1px solid rgb(5 5 5 / 6%); border-radius: 4px; padding: 8px 12px; font-size: 14px; color: #303133; font-family: \"Helvetica Neue\", Helvetica, \"PingFang SC\", \"Hiragino Sans GB\", \"Microsoft YaHei\", sans-serif; outline: none; transition: border-color 0.2s ease; } .ds-input:focus { border-color: #C83A35; } .ds-input::placeholder { color: #909399; }"
+    }
+  ],
+  "narrative": {
+    "northStar": "清晨车间",
+    "overview": "走进清晨的洗车车间:地面刚冲过,工具归位整齐,阳光从高窗斜射进来,空气里有微微的温热。这不是无菌实验室般的冰冷秩序,而是一个被用心打理过的、有体温的工作空间。系统继承了 vue-pure-admin 的布局骨架和 Element Plus 的组件体系,但配色从默认蓝转向品牌红,在专业工具感中注入洗车行业的识别度。设计追求的是「用得顺手」而非「看了惊艳」。像一个熟悉的老工具——不需要说明书,手感刚好,放在那里就知道该怎么用。",
+    "keyCharacteristics": [
+      "功能先行的信息架构,核心操作路径不超过两级菜单",
+      "红色作为策略性点缀 (≤10% 页面面积),它的力量来自稀缺而非铺张",
+      "暗色侧边栏 + 亮色内容区的经典双区布局,不跟风全暗模式",
+      "留白和间距有节奏变化,不追求「每处等距」的机械整齐",
+      "每个交互都有确定的反馈,错误提示说人话,空状态给引导"
+    ],
+    "rules": [
+      {
+        "name": "The 10% Rule",
+        "body": "Brand Red 在任何页面上的总面积不超过 10%。按钮、选中态、关键图标——它的力量来自稀缺,而不是铺张。",
+        "section": "colors"
+      },
+      {
+        "name": "The Tinted Neutral Rule",
+        "body": "不在中性色中使用纯黑 (#000) 或纯白 (#fff)。Surface White 向暖色微偏,背景灰向蓝微偏,维持「清晨车间」的微暖而不冰冷。",
+        "section": "colors"
+      },
+      {
+        "name": "The Warm Restraint Rule",
+        "body": "不使用冷调灰色作为大面积底色。Content Background (#F0F2F5) 已经带有微弱的蓝灰偏暖,足以提供对比而不产生距离感。如果某个区域让人觉得「冷」,先检查中性色是否偏向了纯灰。",
+        "section": "colors"
+      },
+      {
+        "name": "The Weight-Only Hierarchy Rule",
+        "body": "文本层级通过字号和字重区分,不依赖颜色变化。同字号下,Regular vs Bold 的信息差足够大。",
+        "section": "typography"
+      },
+      {
+        "name": "The Flat-By-Default Rule",
+        "body": "卡片、表格、表单默认无阴影。阴影只在需要传达「此层浮于彼层之上」时出现。",
+        "section": "elevation"
+      }
+    ],
+    "dos": [
+      "Do 用 Brand Red 做页面唯一强调色,按钮、选中态、关键状态指示",
+      "Do 让信息层级通过字号和字重表达,不依赖多种颜色区分",
+      "Do 保持侧边栏暗色 + 内容区亮色的经典布局,这是操作者熟悉的心智模型",
+      "Do 表单标签加粗 (700),用重量而非颜色建立字段层级",
+      "Do 最大正文行宽控制在 75ch 以内,确保表格和描述文本的可读性",
+      "Do 为 VXE Table 启用斑马纹和 hover 高亮,密集数据场景下这两者是效率刚需",
+      "Do 每个交互都给确定的视觉反馈——按钮的 hover 变色、行的悬停高亮、加载态的状态指示",
+      "Do 空状态和错误提示用友好的人话,给操作者下一步指引而非仅报错"
+    ],
+    "donts": [
+      "Don't 在页面中使用超过 10% 面积的 Brand Red",
+      "Don't 使用花哨的大色块、多余动画、营销口号式文案",
+      "Don't 堆砌密集表格、灰色调泛滥、控件挨控件——避免传统 ERP 式的窒息感",
+      "Don't 追求「专业感」而牺牲温度——无性格的冷灰、机械的等距排列、缺乏反馈的沉默交互",
+      "Don't 使用 border-left 或 border-right 超过 1px 的彩色强调条",
+      "Don't 使用渐变文字 (background-clip: text)",
+      "Don't 默认使用玻璃拟态 (glassmorphism)",
+      "Don't 用统一尺寸的图标卡片网格铺满页面",
+      "Don't 模态弹窗套模态弹窗"
+    ]
+  }
+}

+ 36 - 12
admin-web-new/DESIGN.md

@@ -1,6 +1,6 @@
 ---
 name: 自助洗车运营管理系统
-description: 自助洗车门店一站式运营管理后台,兼顾效率与清爽的现代工具界面
+description: 自助洗车门店一站式运营管理后台,清晰、亲切、高效的工具界面
 colors:
   brand-red: "#C83A35"
   surface-white: "#FEFEFE"
@@ -13,6 +13,16 @@ colors:
   sidebar-logo-bg: "#002140"
   menu-active-before: "#C83A35"
 typography:
+  headline:
+    fontFamily: '"Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif'
+    fontSize: "20px"
+    fontWeight: 600
+    lineHeight: 1.4
+  title:
+    fontFamily: '"Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif'
+    fontSize: "16px"
+    fontWeight: 600
+    lineHeight: 1.4
   body:
     fontFamily: '"Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif'
     fontSize: "14px"
@@ -22,6 +32,12 @@ typography:
     fontFamily: '"Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif'
     fontSize: "14px"
     fontWeight: 700
+    lineHeight: 1.5
+  caption:
+    fontFamily: '"Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif'
+    fontSize: "12px"
+    fontWeight: 400
+    lineHeight: 1.4
   code:
     fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
     fontSize: "1em"
@@ -53,22 +69,25 @@ components:
 
 **Creative North Star: "清晨车间"**
 
-走进清晨的洗车车间:地面刚冲过,工具归位整齐,阳光从高窗斜射进来,空气里有微微的温热。这个后台管理系统应该给操作者同样的感受 — 一切井然有序、触手可及、不张扬但有温度。
+走进清晨的洗车车间:地面刚冲过,工具归位整齐,阳光从高窗斜射进来,空气里有微微的温热。这不是无菌实验室般的冰冷秩序,而是一个被用心打理过的、有体温的工作空间。这个后台管理系统应该给操作者同样的感受:一切井然有序、触手可及、不张扬但有温度。
+
+系统继承了 `vue-pure-admin` 的布局骨架和 Element Plus 的组件体系,但配色从默认蓝转向品牌红,在专业工具感中注入洗车行业的识别度。设计追求的是「用得顺手」而非「看了惊艳」:信息层级清晰,操作路径短,视觉噪音低。像一个熟悉的老工具——不需要说明书,手感刚好,放在那里就知道该怎么用。
 
-系统继承了 `vue-pure-admin` 的布局骨架和 Element Plus 的组件体系,但配色从默认蓝转向品牌红,在专业工具感中注入洗车行业的识别度。设计追求的是「用得顺手」而非「看了惊艳」:信息层级清晰,操作路径短,视觉噪音低。明确拒绝传统后台的密集表格堆砌和 SaaS 模板的苍白单调。
+明确拒绝传统后台的密集表格堆砌、SaaS 模板的苍白单调,也拒绝为了追求「专业」而牺牲温度——无性格的灰色、机械的间距、缺乏反馈的交互都不是这个系统该有的
 
 **Key Characteristics:**
 - 功能先行的信息架构,核心操作路径不超过两级菜单
-- 红色作为策略性点缀 (≤10% 页面面积),不做大面积色块
+- 红色作为策略性点缀 (≤10% 页面面积),它的力量来自稀缺而非铺张
 - 暗色侧边栏 + 亮色内容区的经典双区布局,不跟风全暗模式
 - 留白和间距有节奏变化,不追求「每处等距」的机械整齐
+- 每个交互都有确定的反馈,错误提示说人话,空状态给引导
 
 ## 2. Colors
 
-品牌红是系统中唯一的强调色。中性色体系来自 Element Plus 的灰度分级,足以覆盖文本、背景、边框的所有场景。
+品牌红是系统中唯一的强调色。中性色体系来自 Element Plus 的灰度分级,足以覆盖文本、背景、边框的所有场景。整体色调微暖,避免冷灰带来的距离感。
 
 ### Primary
-- **Brand Red** (#C83A35 / oklch(52% 0.16 25)): 洗车品牌红精神、醒目但不刺眼。用于主按钮、激活态菜单项、关键状态指示、进度条。仅在需要传达「行动」和「确认」时出现。
+- **Brand Red** (#C83A35 / oklch(52% 0.16 25)): 洗车品牌红——精神、醒目但不刺眼。用于主按钮、激活态菜单项、关键状态指示、进度条。仅在需要传达「行动」和「确认」时出现。
 
 ### Neutral
 - **Surface White** (#FEFEFE): 卡片、弹窗、表格等浮层组件的背景。微暖,不是纯白。
@@ -84,16 +103,18 @@ components:
 - **Sidebar Logo BG** (#002140): 侧边栏顶部 Logo 区域,比主体略亮。
 
 ### Named Rules
-**The 10% Rule.** Brand Red 在任何页面上的总面积不超过 10%。按钮、选中态、关键图标它的力量来自稀缺,而不是铺张。
+**The 10% Rule.** Brand Red 在任何页面上的总面积不超过 10%。按钮、选中态、关键图标——它的力量来自稀缺,而不是铺张。
 
 **The Tinted Neutral Rule.** 不在中性色中使用纯黑 (#000) 或纯白 (#fff)。Surface White 向暖色微偏,背景灰向蓝微偏,维持「清晨车间」的微暖而不冰冷。
 
+**The Warm Restraint Rule.** 不使用冷调灰色作为大面积底色。Content Background (#F0F2F5) 已经带有微弱的蓝灰偏暖,足以提供对比而不产生距离感。如果某个区域让人觉得「冷」,先检查中性色是否偏向了纯灰。
+
 ## 3. Typography
 
 **Body Font:** "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif
 **Code/Mono Font:** ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace
 
-**Character:** 标准中文后台字体栈。PingFang SC / Microsoft YaHei 保证中英文混排的一致性,Helvetica Neue 在前端作为数字和英文的最优渲染选择。
+**Character:** 标准中文后台字体栈。PingFang SC / Microsoft YaHei 保证中英文混排的一致性,Helvetica Neue 在前端作为数字和英文的最优渲染选择。整体气质是「清晰可辨」而非「设计感强烈」——操作者每天看数小时,字体不应当抢夺注意力。
 
 ### Hierarchy
 - **Headline** (600, 20px, 1.4): 页面主标题。一个页面最多一个。
@@ -107,7 +128,7 @@ components:
 
 ## 4. Elevation
 
-系统采用**轻量分层**策略。大部分表面是平的,阴影仅用于浮层组件(弹窗、下拉菜单、消息提示)和悬停反馈。侧边栏和内容区通过颜色对比分层,不走纯阴影路径。
+系统采用**轻量分层**策略。大部分表面是平的,阴影仅用于浮层组件(弹窗、下拉菜单、消息提示)和悬停反馈。侧边栏和内容区通过颜色对比分层,不走纯阴影路径。这种克制的高程策略让界面感觉稳定、可预测——像一个平整的工作台面,工具放在上面而不是飘在空中。
 
 ### Shadow Vocabulary
 - **Message Float** (`0 3px 6px -4px rgba(0,0,0,0.12), 0 6px 16px rgba(0,0,0,0.08), 0 9px 28px 8px rgba(0,0,0,0.05)`): 消息提示、通知弹出。
@@ -124,13 +145,13 @@ components:
 ### Buttons
 - **Shape:** 微圆角,默认 4px。
 - **Primary:** Brand Red 背景 + 白色文字。padding 遵循 Element Plus 默认 (8-15px 横, 5-12px 纵按尺寸)。用于主操作:确认、提交、新建。
-- **Hover:** 背景加深至 oklch(45% 0.16 25),无上移或放大动画
+- **Hover:** 背景加深至 oklch(45% 0.16 25),transition 0.2s ease。不上移、不放大——按钮不是装饰品,保持稳定给人信心
 - **Default/Secondary:** 白色背景 + 灰色边框。用于取消、返回、批量操作中的次要动作。
 - **Text:** 无边框无背景,Brand Red 文字色,用于表格行内操作(编辑、查看、删除)。
 
 ### Sidebar Menu
 - **Style:** 暗色底 + 亮色文字。默认 "default" 主题 (sidebar: #001529)。
-- **Menu Item:** 48px 高,文字 color: rgb(254 254 254 / 65%),hover 时变亮。
+- **Menu Item:** 50px 高,文字 color: rgb(254 254 254 / 65%),hover 时变亮。
 - **Active Item:** Brand Red 背景底 (3px 圆角),白色文字带 z-index 浮于底条之上。
 - **Collapse:** 折叠到 54px 宽时只显示图标,hover 弹出完整菜单项。
 
@@ -167,11 +188,14 @@ components:
 - **Do** 表单标签加粗 (700),用重量而非颜色建立字段层级
 - **Do** 最大正文行宽控制在 75ch 以内,确保表格和描述文本的可读性
 - **Do** 为 VXE Table 启用斑马纹和 hover 高亮,密集数据场景下这两者是效率刚需
+- **Do** 每个交互都给确定的视觉反馈——按钮的 hover 变色、行的悬停高亮、加载态的状态指示
+- **Do** 空状态和错误提示用友好的人话,给操作者下一步指引而非仅报错
 
 ### Don't:
 - **Don't** 在页面中使用超过 10% 面积的 Brand Red。大红色块是营销页面做的事,不是工具后台该有的
 - **Don't** 使用花哨的大色块、多余动画、营销口号式文案
-- **Don't** 堆砌密集表格、灰色调泛滥、控件挨控件 — 避免传统 ERP 式的窒息感
+- **Don't** 堆砌密集表格、灰色调泛滥、控件挨控件——避免传统 ERP 式的窒息感
+- **Don't** 追求「专业感」而牺牲温度——无性格的冷灰、机械的等距排列、缺乏反馈的沉默交互,让操作者感到被工具冷漠对待
 - **Don't** 使用 `border-left` 或 `border-right` 超过 1px 的彩色强调条
 - **Don't** 使用渐变文字 (`background-clip: text`)
 - **Don't** 默认使用玻璃拟态 (glassmorphism)

+ 1 - 1
admin-web-new/public/platform-config.json

@@ -16,7 +16,7 @@
   "HideFooter": false,
   "Stretch": false,
   "SidebarStatus": false,
-  "EpThemeColor": "#409EFF",
+  "EpThemeColor": "#C83A35",
   "ShowLogo": true,
   "ShowModel": "smart",
   "MenuArrowIconNoTransition": false,

+ 2 - 2
admin-web-new/src/components/ReIcon/src/iconifyIconOffline.ts

@@ -18,7 +18,7 @@ export default defineComponent({
         IconifyIcon,
         {
           icon: this.icon,
-          "aria-hidden": false,
+          "aria-hidden": true,
           style: attrs?.style
             ? Object.assign(attrs.style, { outline: "none" })
             : { outline: "none" },
@@ -32,7 +32,7 @@ export default defineComponent({
       return h(
         this.icon,
         {
-          "aria-hidden": false,
+          "aria-hidden": true,
           style: attrs?.style
             ? Object.assign(attrs.style, { outline: "none" })
             : { outline: "none" },

+ 1 - 1
admin-web-new/src/components/ReIcon/src/iconifyIconOnline.ts

@@ -17,7 +17,7 @@ export default defineComponent({
       IconifyIcon,
       {
         icon: `${this.icon}`,
-        "aria-hidden": false,
+        "aria-hidden": true,
         style: attrs?.style
           ? Object.assign(attrs.style, { outline: "none" })
           : { outline: "none" },

+ 1 - 1
admin-web-new/src/components/ReSelector/src/index.css

@@ -11,7 +11,7 @@
 }
 
 .hs-on {
-  background-color: #409eff;
+  background-color: var(--el-color-primary);
   border-radius: 50%;
 }
 

+ 1 - 2
admin-web-new/src/layout/components/lay-content/index.vue

@@ -134,7 +134,6 @@ const transitionMain = defineComponent({
             >
               <el-backtop
                 :title="t('buttons.pureBackTop')"
-                target=".app-main .el-scrollbar__wrap"
               >
                 <BackTopIcon />
               </el-backtop>
@@ -199,7 +198,7 @@ const transitionMain = defineComponent({
   position: relative;
   width: 100%;
   height: 100vh;
-  overflow-x: hidden;
+  overflow-x: auto;
 }
 
 .app-main-nofixed-header {

+ 0 - 11
admin-web-new/src/layout/components/lay-navbar/index.vue

@@ -6,8 +6,6 @@ import LayNavMix from "../lay-sidebar/NavMix.vue";
 import { useTranslationLang } from "@/layout/hooks/useTranslationLang";
 import LaySidebarFullScreen from "../lay-sidebar/components/SidebarFullScreen.vue";
 import LaySidebarBreadCrumb from "../lay-sidebar/components/SidebarBreadCrumb.vue";
-import LaySidebarTopCollapse from "../lay-sidebar/components/SidebarTopCollapse.vue";
-
 import GlobalizationIcon from "@/assets/svg/globalization.svg?component";
 import AccountSettingsIcon from "~icons/ri/user-settings-line";
 import LogoutCircleRLine from "~icons/ri/logout-circle-r-line";
@@ -22,11 +20,9 @@ const {
   device,
   logout,
   onPanel,
-  pureApp,
   username,
   userAvatar,
   avatarsStyle,
-  toggleSideBar,
   toAccountSettings,
   getDropdownItemStyle,
   getDropdownItemClass
@@ -98,13 +94,6 @@ onMounted(() => {
 
 <template>
   <div class="navbar bg-[#fff] shadow-xs shadow-[rgba(0,21,41,0.08)]">
-    <LaySidebarTopCollapse
-      v-if="device === 'mobile'"
-      class="hamburger-container"
-      :is-active="pureApp.sidebar.opened"
-      @toggleClick="toggleSideBar"
-    />
-
     <LaySidebarBreadCrumb
       v-if="layout !== 'mix' && device !== 'mobile'"
       class="breadcrumb-container"

+ 11 - 20
admin-web-new/src/layout/components/lay-sidebar/NavVertical.vue

@@ -9,11 +9,8 @@ import { usePermissionStoreHook } from "@/store/modules/permission";
 import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue";
 import LaySidebarLogo from "../lay-sidebar/components/SidebarLogo.vue";
 import LaySidebarItem from "../lay-sidebar/components/SidebarItem.vue";
-import LaySidebarLeftCollapse from "../lay-sidebar/components/SidebarLeftCollapse.vue";
-import LaySidebarCenterCollapse from "../lay-sidebar/components/SidebarCenterCollapse.vue";
 
 const route = useRoute();
-const isShow = ref(false);
 const showLogo = ref(
   storageLocal().getItem<StorageConfigs>(
     `${responsiveStorageNameSpace()}configure`
@@ -23,10 +20,8 @@ const showLogo = ref(
 const {
   device,
   pureApp,
-  isCollapse,
   tooltipEffect,
-  menuSelect,
-  toggleSideBar
+  menuSelect
 } = useNav();
 
 const subMenuData = ref([]);
@@ -90,10 +85,8 @@ onBeforeUnmount(() => {
   <div
     v-loading="loading"
     :class="['sidebar-container', showLogo ? 'has-logo' : 'no-logo']"
-    @mouseenter.prevent="isShow = true"
-    @mouseleave.prevent="isShow = false"
   >
-    <LaySidebarLogo v-if="showLogo" :collapse="isCollapse" />
+    <LaySidebarLogo v-if="showLogo" :collapse="false" />
     <el-scrollbar
       wrap-class="scrollbar-wrapper"
       :class="[device === 'mobile' ? 'mobile' : 'pc']"
@@ -103,7 +96,7 @@ onBeforeUnmount(() => {
         mode="vertical"
         popper-class="pure-scrollbar"
         class="outer-most select-none"
-        :collapse="isCollapse"
+        :collapse="false"
         :collapse-transition="false"
         :popper-effect="tooltipEffect"
         :default-active="defaultActive"
@@ -117,16 +110,6 @@ onBeforeUnmount(() => {
         />
       </el-menu>
     </el-scrollbar>
-    <LaySidebarCenterCollapse
-      v-if="device !== 'mobile' && (isShow || isCollapse)"
-      :is-active="pureApp.sidebar.opened"
-      @toggleClick="toggleSideBar"
-    />
-    <LaySidebarLeftCollapse
-      v-if="device !== 'mobile'"
-      :is-active="pureApp.sidebar.opened"
-      @toggleClick="toggleSideBar"
-    />
   </div>
 </template>
 
@@ -134,4 +117,12 @@ onBeforeUnmount(() => {
 :deep(.el-loading-mask) {
   opacity: 0.45;
 }
+
+:deep(.el-scrollbar.pc .el-scrollbar__wrap) {
+  padding-top: 20px !important;
+}
+
+:deep(.el-menu.outer-most) {
+  padding-top: 0 !important;
+}
 </style>

+ 2 - 0
admin-web-new/src/layout/components/lay-sidebar/components/SidebarLogo.vue

@@ -42,6 +42,8 @@ const { title, getLogo } = useNav();
   width: 100%;
   height: 48px;
   overflow: hidden;
+  display: flex;
+  align-items: center;
 
   .sidebar-logo-link {
     display: flex;

+ 1 - 1
admin-web-new/src/layout/hooks/useLayout.ts

@@ -27,7 +27,7 @@ export function useLayout() {
         theme: $config?.Theme ?? "light",
         darkMode: $config?.DarkMode ?? false,
         sidebarStatus: $config?.SidebarStatus ?? true,
-        epThemeColor: $config?.EpThemeColor ?? "#409EFF",
+        epThemeColor: $config?.EpThemeColor ?? "#C83A35",
         themeColor: $config?.Theme ?? "light",
         themeMode: $config?.ThemeMode ?? "light"
       };

+ 1 - 3
admin-web-new/src/layout/index.vue

@@ -1,6 +1,5 @@
 <script setup lang="ts">
-import "animate.css";
-// 引入 src/components/ReIcon/src/offlineIcon.ts 文件中所有使用addIcon添加过的本地图标
+// animate.css 已移除,改用 scss transition 统一管理动效
 import "@/components/ReIcon/src/offlineIcon";
 import { setType } from "./types";
 import { useI18n } from "vue-i18n";
@@ -189,7 +188,6 @@ const LayHeader = defineComponent({
       <el-scrollbar v-else>
         <el-backtop
           :title="t('buttons.pureBackTop')"
-          target=".main-container .el-scrollbar__wrap"
         >
           <BackTopIcon />
         </el-backtop>

+ 3 - 23
admin-web-new/src/store/modules/app.ts

@@ -11,10 +11,7 @@ import {
 export const useAppStore = defineStore("pure-app", {
   state: (): appType => ({
     sidebar: {
-      opened:
-        storageLocal().getItem<StorageConfigs>(
-          `${responsiveStorageNameSpace()}layout`
-        )?.sidebarStatus ?? getConfig().SidebarStatus,
+      opened: true,
       withoutAnimation: false,
       isClickCollapse: false
     },
@@ -47,25 +44,8 @@ export const useAppStore = defineStore("pure-app", {
     }
   },
   actions: {
-    TOGGLE_SIDEBAR(opened?: boolean, resize?: string) {
-      const layout = storageLocal().getItem<StorageConfigs>(
-        `${responsiveStorageNameSpace()}layout`
-      );
-      if (opened && resize) {
-        this.sidebar.withoutAnimation = true;
-        this.sidebar.opened = true;
-        layout.sidebarStatus = true;
-      } else if (!opened && resize) {
-        this.sidebar.withoutAnimation = true;
-        this.sidebar.opened = false;
-        layout.sidebarStatus = false;
-      } else if (!opened && !resize) {
-        this.sidebar.withoutAnimation = false;
-        this.sidebar.opened = !this.sidebar.opened;
-        this.sidebar.isClickCollapse = !this.sidebar.opened;
-        layout.sidebarStatus = this.sidebar.opened;
-      }
-      storageLocal().setItem(`${responsiveStorageNameSpace()}layout`, layout);
+    TOGGLE_SIDEBAR(_opened?: boolean, _resize?: string) {
+      this.sidebar.opened = true;
     },
     async toggleSideBar(opened?: boolean, resize?: string) {
       await this.TOGGLE_SIDEBAR(opened, resize);

+ 1 - 1
admin-web-new/src/store/modules/epTheme.ts

@@ -24,7 +24,7 @@ export const useEpThemeStore = defineStore("pure-epTheme", {
     /** 用于mix菜单布局下hamburger-svg的fill属性 */
     fill(state) {
       if (state.epTheme === "light") {
-        return "#409eff";
+        return "#C83A35";
       } else {
         return "#fff";
       }

+ 8 - 0
admin-web-new/src/style/element-plus.scss

@@ -102,6 +102,14 @@
 }
 
 /* 克隆并自定义 ElMessage 样式,不会影响 ElMessage 原本样式,在 src/utils/message.ts 中调用自定义样式 ElMessage 方法即可,整体暗色风格在 src/style/dark.scss 文件进行了适配 */
+
+/* 移动端对话框自适应 */
+@media (max-width: 640px) {
+  .el-dialog {
+    width: 92vw !important;
+    max-width: 92vw;
+  }
+}
 .pure-message {
   background: #fff !important;
   border-width: 0 !important;

+ 17 - 0
admin-web-new/src/style/index.scss

@@ -24,6 +24,23 @@
   --pure-theme-sidebar-logo: none;
   --pure-theme-menu-title-hover: initial;
   --pure-theme-menu-active-before: transparent;
+
+  /* Brand Red — 覆盖 Element Plus 默认蓝色为品牌红 #C83A35 */
+  --el-color-primary: #C83A35;
+  --el-color-primary-light-1: #D1635C;
+  --el-color-primary-light-2: #D57068;
+  --el-color-primary-light-3: #D97B6F;
+  --el-color-primary-light-4: #DF8E81;
+  --el-color-primary-light-5: #E59E94;
+  --el-color-primary-light-6: #EBAFA6;
+  --el-color-primary-light-7: #F1C4BD;
+  --el-color-primary-light-8: #F6D6D1;
+  --el-color-primary-light-9: #FBE9E6;
+  --el-color-primary-dark-1: #B43330;
+  --el-color-primary-dark-2: #A02D29;
+  --el-color-primary-rgb: 200, 58, 53;
+  --el-menu-active-color: #C83A35;
+  --el-menu-hover-bg-color: rgb(200 58 53 / 8%);
 }
 
 /* 灰色模式 */

+ 5 - 0
admin-web-new/src/style/reset.scss

@@ -238,6 +238,11 @@ div:focus {
   outline: none;
 }
 
+div:focus-visible {
+  outline: 2px solid var(--el-color-primary);
+  outline-offset: 2px;
+}
+
 .clearfix {
   &::after {
     display: block;

+ 33 - 5
admin-web-new/src/style/sidebar.scss

@@ -108,11 +108,9 @@
 
     &.has-logo {
       .el-scrollbar.pc {
-        /* logo: 48px、leftCollapse: 40px、leftCollapse-shadow: 4px */
-        height: calc(100% - 92px);
+        height: calc(100% - 48px);
       }
 
-      /* logo: 48px */
       .el-scrollbar.mobile {
         height: calc(100% - 48px);
       }
@@ -120,8 +118,7 @@
 
     &.no-logo {
       .el-scrollbar.pc {
-        /* leftCollapse: 40px、leftCollapse-shadow: 4px */
-        height: calc(100% - 44px);
+        height: 100%;
       }
 
       .el-scrollbar.mobile {
@@ -581,6 +578,37 @@ body[layout="vertical"] {
     background: var(--pure-theme-sidebar-logo);
   }
 
+  .sidebar-container {
+    .outer-most.el-sub-menu:first-of-type
+      > .el-sub-menu__title
+      > .el-sub-menu__icon-arrow {
+      display: none !important;
+    }
+
+    &.has-logo .el-scrollbar.pc {
+      height: calc(100% - 48px) !important;
+    }
+
+    .el-scrollbar {
+      &.pc {
+        .el-scrollbar__wrap {
+          padding-top: 20px !important;
+        }
+      }
+    }
+
+    .el-menu {
+      border: none;
+      padding-top: 0;
+
+      &:not(.el-menu--collapse) {
+        > .outer-first-child {
+          margin-top: 0;
+        }
+      }
+    }
+  }
+
   .hideSidebar {
     .fixed-header {
       width: calc(100% - 54px);

+ 4 - 4
admin-web-new/src/style/theme.scss

@@ -7,19 +7,19 @@ html[data-theme="light"] {
   --pure-theme-menu-text: rgb(0 0 0 / 60%);
   --pure-theme-sidebar-logo: #fff;
   --pure-theme-menu-title-hover: #000;
-  --pure-theme-menu-active-before: #4091f7;
+  --pure-theme-menu-active-before: #C83A35;
 }
 
-/* 道奇蓝 */
+/* 品牌红(默认) */
 html[data-theme="default"] {
   --pure-theme-sub-menu-active-text: #fff;
   --pure-theme-menu-bg: #001529;
-  --pure-theme-menu-hover: rgb(64 145 247 / 15%);
+  --pure-theme-menu-hover: rgb(200 58 53 / 15%);
   --pure-theme-sub-menu-bg: #0f0303;
   --pure-theme-menu-text: rgb(254 254 254 / 65%);
   --pure-theme-sidebar-logo: #002140;
   --pure-theme-menu-title-hover: #fff;
-  --pure-theme-menu-active-before: #4091f7;
+  --pure-theme-menu-active-before: #C83A35;
 }
 
 /* 深紫罗兰色 */

+ 1 - 1
admin-web-new/src/utils/responsive.ts

@@ -18,7 +18,7 @@ export const injectResponsiveStorage = (app: App, config: PlatformConfigs) => {
         theme: config.Theme ?? "light",
         darkMode: config.DarkMode ?? false,
         sidebarStatus: config.SidebarStatus ?? true,
-        epThemeColor: config.EpThemeColor ?? "#409EFF",
+        epThemeColor: config.EpThemeColor ?? "#C83A35",
         themeColor: config.Theme ?? "light", // 主题色(对应系统配置中的主题色,与theme不同的是它不会受到浅色、深色主题模式切换的影响,只会在手动点击主题色时改变)
         themeMode: config.ThemeMode ?? "light" // 主题模式(浅色:light、深色:dark、自动:system)
       },

+ 253 - 96
admin-web-new/src/views/admin/dashboard/index.vue

@@ -1,10 +1,30 @@
 <script setup lang="ts">
-import { markRaw, nextTick, onActivated, onMounted, reactive, ref, watch } from "vue";
-import * as echarts from "echarts";
+import { markRaw, nextTick, onActivated, onMounted, reactive, ref, watch, computed } from "vue";
+import * as echarts from "echarts/core";
+import { PieChart, BarChart, LineChart } from "echarts/charts";
+import {
+  GridComponent,
+  TitleComponent,
+  LegendComponent,
+  TooltipComponent,
+  DataZoomComponent
+} from "echarts/components";
+import { CanvasRenderer } from "echarts/renderers";
+
+echarts.use([
+  PieChart,
+  BarChart,
+  LineChart,
+  GridComponent,
+  TitleComponent,
+  LegendComponent,
+  TooltipComponent,
+  DataZoomComponent,
+  CanvasRenderer
+]);
 import { getDashboard, getTrend, getWashDeviceStatus } from "@/api/stat";
 import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
-import { storageLocal } from "@pureadmin/utils";
-import { userKey, type DataInfo } from "@/utils/auth";
+import { useRenderIcon } from "@/components/ReIcon/src/hooks";
 
 defineOptions({
   name: "Dashboard"
@@ -18,7 +38,6 @@ const end = new Date();
 const start = new Date();
 start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
 
-// 简单的格式化工具
 const fmtMoney = (value: number) => {
   if (!value) return "0";
   return (value / 100).toFixed(2);
@@ -36,7 +55,6 @@ const dateDiff = (start: Date, end: Date) => {
   return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
 };
 
-// Session 工具
 const Session = {
   get: (key: string) => {
     const value = sessionStorage.getItem(key);
@@ -47,6 +65,20 @@ const Session = {
   }
 };
 
+// 品牌衍生图表色板
+const CHART_BAR = "#4DA89D";
+const CHART_LINE = "#C83A35";
+const CHART_GRID = "#EBEBEB";
+
+const PIE_COLORS = [
+  "#C83A35",
+  "#4DA89D",
+  "#E5A350",
+  "#8B7EC8",
+  "#5BA0D9",
+  "#8E8E8E"
+];
+
 const state = reactive({
   currentStationId: null as string | null,
   dateRange: [formatDate(start), formatDate(end)] as [string, string],
@@ -55,14 +87,14 @@ const state = reactive({
     homeChartTwo: null as any,
     dispose: [null, "", undefined]
   },
-  homeOne: [
-    { num1: "0", num3: "今日注册会员数(人)" },
-    { num1: "0", num3: "今日收益金额(元)" },
-    { num1: "0", num3: "今日洗车消费总金额(元)" },
-    { num1: "0", num3: "订单平均消费金额(元)" },
-    { num1: "0", num3: "今日订单数量(笔)" },
-    { num1: "0", num3: "洗车平均时长(分钟)" }
-  ],
+  metrics: {
+    registeredMembers: { value: "0", label: "今日注册会员", icon: "ri/user-add-line" },
+    todayIncome: { value: "0", label: "今日收益金额", icon: "ri/money-dollar-circle-line" },
+    consumptionAmount: { value: "0", label: "今日消费总额", icon: "ri/shopping-cart-2-line" },
+    avgOrderPrice: { value: "0", label: "订单均价", icon: "ri/calculator-line" },
+    todayOrders: { value: "0", label: "今日订单数量", icon: "ri/file-list-3-line" },
+    avgDuration: { value: "0", label: "洗车平均时长", icon: "ri/timer-line" }
+  },
   myCharts: [] as any[],
   charts: {
     theme: "",
@@ -105,13 +137,12 @@ const shortcuts = [
   }
 ];
 
-// 折线图
 const initLineChart = (dataList: Array<any>) => {
   if (!state.global.dispose.some((b: any) => b === state.global.homeChartOne)) {
     state.global.homeChartOne?.dispose();
   }
   state.global.homeChartOne = markRaw(echarts.init(homeLineRef.value, state.charts.theme));
-  
+
   dataList.forEach(item => {
     item.startTime = item.statTime.slice(0, 3).join("-");
     item.seq = Number(item.statTime.join(""));
@@ -119,8 +150,7 @@ const initLineChart = (dataList: Array<any>) => {
 
   state.homeOneExtra.totalIncome = dataList.reduce((k, v) => k + v.totalAmount, 0);
   state.homeOneExtra.totalWashOrders = dataList.reduce((k, v) => k + v.totalOrders, 0);
-  
-  // 排序
+
   dataList.sort((a, b) => a.seq - b.seq);
 
   const xAxis = dataList.map(k => k.startTime);
@@ -142,7 +172,7 @@ const initLineChart = (dataList: Array<any>) => {
         type: "value",
         name: "费用/元  洗车量/次",
         position: "left",
-        splitLine: { show: true, lineStyle: { type: "dashed", color: "#f5f5f5" } }
+        splitLine: { show: true, lineStyle: { type: "dashed", color: CHART_GRID } }
       }
     ],
     series: [
@@ -154,8 +184,8 @@ const initLineChart = (dataList: Array<any>) => {
         symbol: "circle",
         smooth: true,
         data: dataList.map(k => k.totalOrders),
-        lineStyle: { color: "#68a7a0" },
-        itemStyle: { color: "#68a7a0", borderColor: "#68a7a0", barBorderRadius: 5 }
+        lineStyle: { color: CHART_BAR },
+        itemStyle: { color: CHART_BAR, borderColor: CHART_BAR, barBorderRadius: 5 }
       },
       {
         name: "总金额",
@@ -164,8 +194,8 @@ const initLineChart = (dataList: Array<any>) => {
         symbol: "circle",
         smooth: true,
         data: dataList.map(k => fmtMoney(k.totalAmount)),
-        lineStyle: { color: "#3770ff" },
-        itemStyle: { color: "#3770ff", borderColor: "#3770ff" }
+        lineStyle: { color: CHART_LINE },
+        itemStyle: { color: CHART_LINE, borderColor: CHART_LINE }
       }
     ]
   };
@@ -173,13 +203,12 @@ const initLineChart = (dataList: Array<any>) => {
   state.myCharts.push(state.global.homeChartOne);
 };
 
-// 饼图
 const initPieChart = (dataMap: any) => {
   if (!state.global.dispose.some((b: any) => b === state.global.homeChartTwo)) {
     state.global.homeChartTwo?.dispose();
   }
   state.global.homeChartTwo = markRaw(echarts.init(homePieRef.value, state.charts.theme));
-  
+
   const sessionDicts = Session.get("dicts");
   let dicts: any[] = [];
   if (sessionDicts) {
@@ -188,9 +217,8 @@ const initPieChart = (dataMap: any) => {
   if (!dicts.length) return;
 
   const getname = dicts.map(k => k.name);
-  const colorList = ["#6B6F75FF", "#36C78B", "#e9ee8e", "#ffa496", "#E790E8", "#363638FF"];
   const data: any[] = [];
-  
+
   for (let i = 0; i < getname.length; i++) {
     const dict = dicts.find(k => k.name === getname[i]);
     data.push({ name: getname[i], value: dataMap[`${dict?.value}`] || 0 });
@@ -221,7 +249,7 @@ const initPieChart = (dataMap: any) => {
         radius: ["82", dataTheme.value ? "50" : "102"],
         center: ["32%", "50%"],
         itemStyle: {
-          color: (params: any) => colorList[params.dataIndex]
+          color: (params: any) => PIE_COLORS[params.dataIndex % PIE_COLORS.length]
         },
         label: { show: false },
         labelLine: { show: false },
@@ -233,7 +261,6 @@ const initPieChart = (dataMap: any) => {
   state.myCharts.push(state.global.homeChartTwo);
 };
 
-// 批量设置 echarts resize
 const initEchartsResizeFun = () => {
   nextTick(() => {
     for (let i = 0; i < state.myCharts.length; i++) {
@@ -258,7 +285,7 @@ const loadStationStat = () => {
   const start = state.dateRange[0];
   const end = state.dateRange[1];
   const size = dateDiff(new Date(start), new Date(end)) + 1;
-  
+
   getTrend({
     startTime: start,
     endTime: end,
@@ -274,94 +301,119 @@ const loadStationStatToday = () => {
   getDashboard(state.currentStationId || undefined).then((res: any) => {
     const data = res?.data || res;
     if (data) {
-      const {
-        todayIncome,
-        todayConsumptionAmount,
-        todayRegisteredMembers,
-        todayWashOrders,
-        avgOrderPrice,
-        avgOrderDuration
-      } = data;
-      state.homeOne[0].num1 = String(todayRegisteredMembers || 0);
-      state.homeOne[1].num1 = fmtMoney(todayIncome || 0);
-      state.homeOne[2].num1 = fmtMoney(todayConsumptionAmount || 0);
-      state.homeOne[3].num1 = fmtMoney(avgOrderPrice || 0);
-      state.homeOne[4].num1 = String(todayWashOrders || 0);
-      state.homeOne[5].num1 = String(avgOrderDuration || 0);
+      state.metrics.registeredMembers.value = String(data.todayRegisteredMembers || 0);
+      state.metrics.todayIncome.value = fmtMoney(data.todayIncome || 0);
+      state.metrics.consumptionAmount.value = fmtMoney(data.todayConsumptionAmount || 0);
+      state.metrics.avgOrderPrice.value = fmtMoney(data.avgOrderPrice || 0);
+      state.metrics.todayOrders.value = String(data.todayWashOrders || 0);
+      state.metrics.avgDuration.value = String(data.avgOrderDuration || 0);
     }
   });
 };
 
-// 页面加载时
 onMounted(() => {
   const currentStationId = Session.get("currentStationId");
   if (currentStationId) {
     state.currentStationId = currentStationId;
-    // 有站点 ID 时才加载数据
     initEchartsResize();
     loadStationStat();
     loadStationStatToday();
     loadCurrentEquipmentStatus();
   } else {
-    // 提示用户选择站点
-    console.warn('请先选择站点');
-    // 仍然初始化图表和基础数据,只是不加载接口数据
     initEchartsResize();
   }
 });
 
-// 由于页面缓存原因,keep-alive
 onActivated(() => {
   initEchartsResizeFun();
 });
 
-// 监听深色主题变化
 watch(
   () => dataTheme.value,
   (isDark) => {
     nextTick(() => {
       state.charts.theme = isDark ? "dark" : "";
       state.charts.bgColor = isDark ? "transparent" : "";
-      state.charts.color = isDark ? "#dadada" : "#303133";
+      state.charts.color = isDark ? "#C8C8C8" : "#303133";
       setTimeout(() => loadStationStat(), 500);
       setTimeout(() => loadCurrentEquipmentStatus(), 700);
     });
   }
 );
+
+const featuredMetrics = computed(() => [
+  state.metrics.todayIncome,
+  state.metrics.todayOrders
+]);
+
+const secondaryMetrics = computed(() => [
+  state.metrics.consumptionAmount,
+  state.metrics.avgOrderPrice,
+  state.metrics.registeredMembers,
+  state.metrics.avgDuration
+]);
+
+const isMoneyMetric = (label: string) => {
+  return label.includes("金额") || label.includes("收益") || label.includes("均价") || label.includes("消费");
+};
 </script>
 
 <template>
   <div class="dashboard-container">
-    <!-- 未选择站点提示 -->
+    <!-- 未选择站点提 -->
     <el-alert
       v-if="!state.currentStationId"
-      title="请先选择站点"
+      title="选择站点"
       type="warning"
-      description="请在右上角选择要查看的站点,选择后将自动加载统计数据"
+      description="请通过右上角站点选择器切换到要查看的门店,选择后将自动加载统计数据"
       show-icon
       closable
       class="mb-4"
     />
-    
-    <!-- 统计卡片 -->
-    <el-row :gutter="15" class="mb-4">
-      <el-col
-        :xs="24"
-        :sm="12"
-        :md="12"
-        :lg="4"
-        :xl="4"
-        v-for="(item, index) in state.homeOne"
-        :key="index"
+
+    <!-- 核心指标 -->
+    <div class="featured-row">
+      <div
+        v-for="(metric, idx) in featuredMetrics"
+        :key="idx"
+        class="featured-card"
+        :class="idx === 0 ? 'featured-primary' : 'featured-secondary'"
       >
-        <el-card class="stat-card" shadow="hover">
-          <div class="stat-content">
-            <span class="stat-value">{{ item.num1 }}</span>
-            <div class="stat-label">{{ item.num3 }}</div>
+        <div class="featured-icon">
+          <component :is="useRenderIcon(metric.icon)" />
+        </div>
+        <div class="featured-body">
+          <div class="featured-value">
+            {{ metric.value }}
+            <span v-if="isMoneyMetric(metric.label)" class="featured-unit">元</span>
+            <span v-else class="featured-unit">笔</span>
           </div>
-        </el-card>
-      </el-col>
-    </el-row>
+          <div class="featured-label">{{ metric.label }}</div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 次要指标 -->
+    <div class="secondary-row">
+      <div
+        v-for="(metric, idx) in secondaryMetrics"
+        :key="idx"
+        class="secondary-card"
+      >
+        <div class="secondary-icon">
+          <component :is="useRenderIcon(metric.icon)" />
+        </div>
+        <div class="secondary-body">
+          <div class="secondary-value">
+            {{ metric.value }}
+            <span v-if="isMoneyMetric(metric.label)" class="secondary-unit">元</span>
+            <span v-else-if="metric.label.includes('时长')" class="secondary-unit">分钟</span>
+            <span v-else class="secondary-unit">人</span>
+          </div>
+          <div class="secondary-label">{{ metric.label }}</div>
+        </div>
+      </div>
+    </div>
 
     <!-- 图表区域 -->
     <el-row :gutter="15">
@@ -381,19 +433,19 @@ watch(
                 :shortcuts="shortcuts"
               />
               <div class="chart-summary">
-                总收益金额:
-                <el-tag type="success">{{ fmtMoney(state.homeOneExtra.totalIncome) }}元</el-tag>
-                总订单数量:
-                <el-tag type="danger">{{ state.homeOneExtra.totalWashOrders }}笔</el-tag>
+                总收益:
+                <el-tag type="success" size="small">{{ fmtMoney(state.homeOneExtra.totalIncome) }}元</el-tag>
+                总订单:
+                <el-tag type="danger" size="small">{{ state.homeOneExtra.totalWashOrders }}笔</el-tag>
               </div>
             </div>
           </template>
-          <div class="chart-wrapper" ref="homeLineRef"></div>
+          <div class="chart-wrapper" ref="homeLineRef" />
         </el-card>
       </el-col>
       <el-col :xs="24" :sm="10" :md="10" :lg="8" :xl="8">
         <el-card class="chart-card">
-          <div class="chart-wrapper" ref="homePieRef"></div>
+          <div class="chart-wrapper" ref="homePieRef" />
         </el-card>
       </el-col>
     </el-row>
@@ -405,47 +457,144 @@ watch(
   padding: 15px;
 }
 
-.stat-card {
+// 核心指标行
+.featured-row {
+  display: grid;
+  grid-template-columns: 3fr 2fr;
+  gap: 15px;
   margin-bottom: 15px;
-  
-  .stat-content {
-    text-align: center;
-    padding: 10px 0;
-    
-    .stat-value {
-      font-size: 28px;
-      font-weight: 600;
+
+  @media (max-width: 768px) {
+    grid-template-columns: 1fr;
+  }
+}
+
+.featured-card {
+  display: flex;
+  align-items: center;
+  gap: 20px;
+  padding: 24px 28px;
+  border-radius: 4px;
+  background: var(--el-bg-color);
+
+  &.featured-primary {
+    box-shadow: 0 1px 3px rgb(0 0 0 / 6%);
+    border-top: 3px solid var(--el-color-primary);
+  }
+
+  &.featured-secondary {
+    box-shadow: 0 1px 3px rgb(0 0 0 / 6%);
+  }
+
+  .featured-icon {
+    font-size: 40px;
+    color: var(--el-color-primary);
+    opacity: 0.85;
+    flex-shrink: 0;
+  }
+
+  .featured-body {
+    .featured-value {
+      font-size: 36px;
+      font-weight: 700;
+      line-height: 1.2;
       color: var(--el-text-color-primary);
+      letter-spacing: -0.5px;
+    }
+
+    .featured-unit {
+      font-size: 16px;
+      font-weight: 500;
+      color: var(--el-text-color-secondary);
+      margin-left: 4px;
     }
-    
-    .stat-label {
-      margin-top: 8px;
+
+    .featured-label {
+      margin-top: 6px;
       font-size: 14px;
+      font-weight: 500;
       color: var(--el-text-color-secondary);
     }
   }
 }
 
+// 次要指标行
+.secondary-row {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: 15px;
+  margin-bottom: 15px;
+
+  @media (max-width: 992px) {
+    grid-template-columns: repeat(2, 1fr);
+  }
+
+  @media (max-width: 640px) {
+    grid-template-columns: 1fr;
+  }
+}
+
+.secondary-card {
+  display: flex;
+  align-items: center;
+  gap: 14px;
+  padding: 18px 20px;
+  border-radius: 4px;
+  background: var(--el-bg-color);
+  box-shadow: 0 1px 2px rgb(0 0 0 / 4%);
+
+  .secondary-icon {
+    font-size: 28px;
+    color: var(--el-text-color-placeholder);
+    flex-shrink: 0;
+  }
+
+  .secondary-body {
+    min-width: 0;
+
+    .secondary-value {
+      font-size: 22px;
+      font-weight: 600;
+      line-height: 1.3;
+      color: var(--el-text-color-primary);
+    }
+
+    .secondary-unit {
+      font-size: 12px;
+      font-weight: 500;
+      color: var(--el-text-color-secondary);
+      margin-left: 2px;
+    }
+
+    .secondary-label {
+      margin-top: 2px;
+      font-size: 13px;
+      color: var(--el-text-color-secondary);
+    }
+  }
+}
+
+// 图表
 .chart-card {
   margin-bottom: 15px;
-  
+
   .chart-header {
     display: flex;
     justify-content: space-between;
     align-items: center;
     flex-wrap: wrap;
     gap: 10px;
-    
+
     .chart-summary {
       font-size: 14px;
       color: var(--el-text-color-secondary);
-      
+
       .el-tag {
-        margin: 0 5px;
+        margin: 0 4px;
       }
     }
   }
-  
+
   .chart-wrapper {
     height: 350px;
     width: 100%;
@@ -457,5 +606,13 @@ watch(
     flex-direction: column;
     align-items: flex-start;
   }
+
+  .featured-card {
+    padding: 18px 20px;
+
+    .featured-value {
+      font-size: 28px;
+    }
+  }
 }
 </style>

+ 2 - 2
admin-web-new/src/views/admin/finance/split-record.vue

@@ -244,7 +244,7 @@ const formatMoney = (value: number) => {
   text-align: center;
   
   .station-id {
-    color: #909399;
+    color: var(--el-text-color-secondary);
     font-size: 12px;
   }
   
@@ -259,7 +259,7 @@ const formatMoney = (value: number) => {
 
 .money {
   font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif;
-  color: #f56c6c;
+  color: var(--el-color-danger);
   font-weight: 500;
 }
 </style>

+ 3 - 3
admin-web-new/src/views/admin/ordering/dialog.vue

@@ -180,19 +180,19 @@ defineExpose({ open });
   font-weight: 500;
   margin-bottom: 10px;
   padding-left: 10px;
-  border-left: 3px solid #409eff;
+  border-bottom: 2px solid var(--el-color-primary);
 }
 
 .money {
   font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif;
   
   &.highlight {
-    color: #f56c6c;
+    color: var(--el-color-danger);
     font-weight: 600;
   }
   
   &.discount {
-    color: #67c23a;
+    color: var(--el-color-success);
   }
 }
 </style>

+ 1 - 1
admin-web-new/src/views/admin/ordering/index.vue

@@ -312,6 +312,6 @@ const processDetailData = (detail: any[]) => {
 .no-detail {
   padding: 20px;
   text-align: center;
-  color: #999;
+  color: var(--el-text-color-secondary);
 }
 </style>

+ 2 - 2
admin-web-new/src/views/admin/platform/device-config-dialog.vue

@@ -402,10 +402,10 @@ defineExpose({ open });
 .form-group-title {
   font-size: 14px;
   font-weight: 600;
-  color: #409eff;
+  color: var(--el-color-primary);
   margin: 15px 0 10px 0;
   padding-bottom: 5px;
-  border-bottom: 1px solid #e4e7ed;
+  border-bottom: 1px solid var(--el-border-color-light);
 }
 
 .w100 {

+ 2 - 2
admin-web-new/src/views/admin/role/index.vue

@@ -294,7 +294,7 @@ const groupByKey = (arr: any[], key: string) => {
             <template v-if="idx === 0">
               权限
               <span
-                style="cursor: pointer; font-size: 12px; margin-left: 8px; color: #ccc"
+                style="cursor: pointer; font-size: 12px; margin-left: 8px; color: var(--el-text-color-secondary)"
                 @click="handleExpandSwitch"
               >
                 {{ state.expandAll ? "收起" : "展开" }}
@@ -303,7 +303,7 @@ const groupByKey = (arr: any[], key: string) => {
           </template>
           <template #default="scope">
             <template v-if="scope.column.label !== '权限'">
-              <el-icon v-if="scope.row[scope.column.rawColumnKey]" color="#5FB878">
+              <el-icon v-if="scope.row[scope.column.rawColumnKey]" color="var(--el-color-success)">
                 <CircleCheckFilled />
               </el-icon>
               <span v-else>-</span>

+ 1 - 1
admin-web-new/src/views/admin/station/device.vue

@@ -380,7 +380,7 @@ const formatDuration = (ms: number) => {
   text-align: center;
   
   .station-id {
-    color: #909399;
+    color: var(--el-text-color-secondary);
     font-size: 12px;
   }
   

+ 1 - 1
admin-web-new/src/views/admin/station/list.vue

@@ -330,7 +330,7 @@ const handleGotoDevice = (row: any) => {
   text-align: center;
   
   .station-id {
-    color: #909399;
+    color: var(--el-text-color-secondary);
     font-size: 12px;
   }
   

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 278 - 440
admin-web/yarn.lock


Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels