Explorar el Código

admin-mp页面调试

skyline hace 1 semana
padre
commit
f8fafe2818
Se han modificado 44 ficheros con 6288 adiciones y 2119 borrados
  1. 307 0
      DESIGN.md
  2. 7 0
      admin-mp/.claude/settings.local.json
  3. 15 0
      admin-mp/index.html
  4. 4 4
      admin-mp/package.json
  5. 148 117
      admin-mp/src/App.vue
  6. 9 0
      admin-mp/src/api/dict.js
  7. 36 0
      admin-mp/src/api/finance.js
  8. 37 0
      admin-mp/src/api/system.js
  9. 9 0
      admin-mp/src/api/user.js
  10. 75 0
      admin-mp/src/components/AppIcon.vue
  11. 69 0
      admin-mp/src/components/NavBar.vue
  12. 87 0
      admin-mp/src/components/dict-label/index.vue
  13. 93 0
      admin-mp/src/components/dict-select/index.vue
  14. 106 0
      admin-mp/src/custom-tab-bar/index.vue
  15. 4 0
      admin-mp/src/main.js
  16. 1 1
      admin-mp/src/manifest.json
  17. 68 5
      admin-mp/src/pages.json
  18. 141 271
      admin-mp/src/pages/device/detail.vue
  19. 249 436
      admin-mp/src/pages/device/list.vue
  20. 85 40
      admin-mp/src/pages/finance/index.vue
  21. 382 0
      admin-mp/src/pages/finance/refund.vue
  22. 536 0
      admin-mp/src/pages/finance/settlement.vue
  23. 345 0
      admin-mp/src/pages/finance/split-record.vue
  24. 121 173
      admin-mp/src/pages/finance/withdraw.vue
  25. 424 506
      admin-mp/src/pages/index/index.vue
  26. 15 35
      admin-mp/src/pages/login/login.vue
  27. 68 121
      admin-mp/src/pages/order/detail.vue
  28. 67 72
      admin-mp/src/pages/order/list.vue
  29. 7 11
      admin-mp/src/pages/setting/device-binding.vue
  30. 7 11
      admin-mp/src/pages/setting/device-config-detail.vue
  31. 15 24
      admin-mp/src/pages/setting/device-config.vue
  32. 76 6
      admin-mp/src/pages/setting/index.vue
  33. 12 69
      admin-mp/src/pages/setting/rate-config-detail.vue
  34. 14 88
      admin-mp/src/pages/setting/rate-config.vue
  35. 287 0
      admin-mp/src/pages/station/detail.vue
  36. 369 0
      admin-mp/src/pages/station/list.vue
  37. 602 0
      admin-mp/src/pages/system/dict.vue
  38. 223 0
      admin-mp/src/pages/system/feedback.vue
  39. 197 0
      admin-mp/src/pages/system/log.vue
  40. 198 0
      admin-mp/src/pages/system/notice.vue
  41. 537 0
      admin-mp/src/pages/user/list.vue
  42. 69 76
      admin-mp/src/uni.scss
  43. 164 0
      admin-mp/src/utils/dict.js
  44. 3 53
      admin-mp/src/utils/index.js

+ 307 - 0
DESIGN.md

@@ -0,0 +1,307 @@
+---
+name: 自助洗车运营管理
+description: 为自助洗车门店提供一站式运营管理平台,连接门店、服务商与车主
+colors:
+  cinnabar-red:
+    hex: "#C6171E"
+    role: primary
+  cinnabar-light:
+    hex: "#E84545"
+    role: primary-light
+  cinnabar-dark:
+    hex: "#A81212"
+    role: primary-dark
+  success-green:
+    hex: "#52C41A"
+    role: semantic
+  warning-orange:
+    hex: "#FF9800"
+    role: semantic
+  error-red:
+    hex: "#F44336"
+    role: semantic
+  info-blue:
+    hex: "#2196F3"
+    role: semantic
+  ink-black:
+    hex: "#1A1A1A"
+    role: text-primary
+  stone-grey:
+    hex: "#666666"
+    role: text-secondary
+  ash-grey:
+    hex: "#999999"
+    role: text-disabled
+  cloud-grey:
+    hex: "#B0B0B0"
+    role: text-placeholder
+  surface-white:
+    hex: "#FFFFFF"
+    role: surface
+  frost-page:
+    hex: "#F5F7FA"
+    role: page-bg
+  hairline:
+    hex: "#E0E0E0"
+    role: border
+  hairline-light:
+    hex: "#F0F0F0"
+    role: border-light
+typography:
+  display:
+    fontFamily: "Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif"
+    fontSize: "32px"
+    fontWeight: 700
+    lineHeight: 1.2
+  headline:
+    fontFamily: "Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif"
+    fontSize: "24px"
+    fontWeight: 600
+    lineHeight: 1.3
+  title:
+    fontFamily: "Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif"
+    fontSize: "20px"
+    fontWeight: 600
+    lineHeight: 1.3
+  body:
+    fontFamily: "Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif"
+    fontSize: "16px"
+    fontWeight: 400
+    lineHeight: 1.5
+  body-sm:
+    fontFamily: "Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif"
+    fontSize: "14px"
+    fontWeight: 400
+    lineHeight: 1.5
+  label:
+    fontFamily: "Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif"
+    fontSize: "12px"
+    fontWeight: 500
+    lineHeight: 1.2
+rounded:
+  sm: "8px"
+  md: "12px"
+  lg: "16px"
+  xl: "24px"
+  pill: "100px"
+spacing:
+  xs: "4px"
+  sm: "8px"
+  base: "12px"
+  lg: "16px"
+  xl: "20px"
+  card-inner: "10px"
+components:
+  button-primary:
+    backgroundColor: "{colors.cinnabar-red}"
+    textColor: "{colors.surface-white}"
+    rounded: "{rounded.lg}"
+    padding: "12px 28px"
+  button-primary-pressed:
+    backgroundColor: "{colors.cinnabar-dark}"
+    textColor: "{colors.surface-white}"
+    rounded: "{rounded.lg}"
+  button-secondary:
+    backgroundColor: "{colors.surface-white}"
+    textColor: "{colors.ink-black}"
+    rounded: "{rounded.sm}"
+    padding: "10px 24px"
+  chip-status:
+    backgroundColor: "var(--chip-bg)"
+    textColor: "var(--chip-color)"
+    rounded: "{rounded.pill}"
+    padding: "3px 12px"
+  card:
+    backgroundColor: "{colors.surface-white}"
+    rounded: "{rounded.xl}"
+    padding: "12px"
+  input-field:
+    backgroundColor: "#F5F5F5"
+    textColor: "{colors.ink-black}"
+    rounded: "{rounded.sm}"
+    padding: "10px 14px"
+  tab-active:
+    backgroundColor: "{colors.cinnabar-red}"
+    textColor: "{colors.surface-white}"
+    rounded: "{rounded.sm}"
+  tab-inactive:
+    backgroundColor: "transparent"
+    textColor: "{colors.stone-grey}"
+    rounded: "{rounded.sm}"
+---
+
+# Design System: 自助洗车运营管理
+
+## 1. Overview
+
+**Creative North Star: "工坊里的红标工具箱"**
+
+朱砂红是工具箱上的那块铭牌——它不抢眼,但你扫一眼就知道该拿哪一把。每一件工具都放在它应该在的位置,手柄的弧度刚好贴合手掌,不需要说明书,不需要思考。界面是安静的,但处处有回应:按下去有反馈,等待时有交代,出错时说人话。
+
+这套设计语言服务于每天面对屏幕数小时的操作者——洗车店经营者、员工、服务商。它不追求惊艳,追求的是长期相处的舒适。温润有质、清晰优先、亲切高效:这三条产品原则直接翻译成了视觉规则。
+
+**Key Characteristics:**
+- 红色是功能性的,不是装饰性的——它标记主操作、激活态、关键信息,只占屏幕的 ≤10%
+- 微妙的触感反馈:静态时平面,交互时元素微升,阴影轻且聚焦
+- Inter 字体承载从 32px 标题到 12px 标签的完整层级,重量对比清晰
+- 字典驱动的状态色系统:红/绿/橙/蓝四色由后端控制,前端不做假设
+- 卡片白底带极轻阴影,页面底色是带有一点蓝调的灰白
+
+## 2. Colors
+
+朱砂红是工具箱的标记色,灰白系是工坊的墙面和桌面。
+
+### Primary
+- **朱砂红 Cinnabar Red** (#C6171E): 主操作按钮、选中态、导航栏背景、品牌焦点。出现在每屏 ≤10% 的面积上——它的稀缺就是它的力量。
+- **浅朱砂 Cinnabar Light** (#E84545): 悬停态、渐变终点、需要比主色轻一级的强调场景。
+- **深朱砂 Cinnabar Dark** (#A81212): 按压态、链接悬停、需要更沉一步的红色。
+
+### Semantic
+- **成功绿 Success Green** (#52C41A): 在线、已完成、收入金额。字典映射的默认成功色。
+- **提醒橙 Warning Orange** (#FF9800): 待处理、忙碌、需注意。非破坏性的警示。
+- **错误红 Error Red** (#F44336): 删除、危险操作、失败状态。与朱砂红保持区分。
+- **信息蓝 Info Blue** (#2196F3): 链接、辅助信息、中性提示。
+
+### Neutral
+- **墨黑 Ink Black** (#1A1A1A): 正文标题,最高对比度文字。
+- **石灰 Stone Grey** (#666666): 次要信息、描述文字、未选中标签。
+- **灰 Ash Grey** (#999999): 禁用态、占位提示、次级辅助信息。
+- **云灰 Cloud Grey** (#B0B0B0): 输入框占位符。
+- **纸白 Surface White** (#FFFFFF): 所有卡片和内容区的背景。
+- **霜白页面 Frost Page** (#F5F7FA): 页面底色,略带蓝调的灰白,与卡片白形成自然层级。
+- **发丝线 Hairline** (#E0E0E0): 标准分割线和边框。
+- **浅发丝 Hairline Light** (#F0F0F0): 轻量分割,卡片内部元素分隔。
+
+### Named Rules
+
+**The 10% Rule.** 朱砂红在任何一屏上的面积不超过 10%。它是标记,不是底色。如果发现自己在给第三个元素涂红,先问问前两个是不是必须的。
+
+**The Red Don't Compete Rule.** 错误红 (#F44336) 和朱砂红 (#C6171E) 永远不同时出现在同一视线区域内。它们是两套独立的信号系统——品牌 vs 状态——混在一起就是噪音。
+
+## 3. Typography
+
+**Display Font:** Inter (with system fallback stack)
+**Body Font:** Inter (with system fallback stack)
+
+**Character:** Inter 是一个「没有口音」的字体——它不暗示任何时代、任何地域、任何行业。它的中性让操作者把注意力完全放在数据上,而不是字体的性格上。但它的 x-height 偏高、开口开阔,在长时间阅读时不容易疲劳。
+
+### Hierarchy
+- **Display** (700, 32px, 1.2): 页面级标题。每页只出现一次。
+- **Headline** (600, 24px, 1.3): 区块标题、弹窗标题。
+- **Title** (600, 20px, 1.3): 卡片标题、列表项主文字。
+- **Body** (400, 16px, 1.5): 正文、表单标签、列表内容。行宽上限 70ch。
+- **Body SM** (400, 14px, 1.5): 辅助描述、时间戳、次要列表项。
+- **Label** (500, 12px, 1.2): 状态标签内文字、表头、元数据。可配合大写。
+
+### Named Rules
+
+**The Weight Contrast Rule.** 相邻层级之间字重差至少 200(Regular→Semibold→Bold),字号差至少 1.25 倍。不要用同字重不同字号来区分层级——它看起来像排版错误。
+
+**The Single Typeface Rule.** 全系统只用 Inter 一个字族。不要引入第二个字体——层级完全由字号和字重区分,不需要换字体来「增加变化」。
+
+## 4. Elevation
+
+**触感微升 Tactile Lift.** 界面在静态时是平的——卡片和页面底色之间只有颜色差异,没有阴影。元素只有在交互时才微微浮起:悬停的卡片、按下的按钮、展开的下拉。阴影是交互的产物,不是默认装饰。
+
+### Shadow Vocabulary
+- **微光 Ambient SM** (`0 1px 2px rgba(0,0,0,0.06), 0 1px 3px rgba(0,0,0,0.04)`): 悬停态,元素刚刚离开表面。
+- **标准 Ambient Base** (`0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06)`): 卡片默认态(当页面需要卡片显式分层时)。
+- **升起 Lift LG** (`0 4px 6px rgba(0,0,0,0.07), 0 2px 4px rgba(0,0,0,0.06)`): 激活卡片、拖拽中元素。
+- **顶层 Elevate XL** (`0 10px 15px rgba(0,0,0,0.08), 0 4px 6px rgba(0,0,0,0.05)`): 模态框、弹出层。
+- **品牌导航 Branded Nav** (`0 2px 8px rgba(198,23,30,0.15)`): 顶部导航栏专属——红色调阴影让它温和地浮在内容之上。
+
+### Named Rules
+
+**The Lift-On-Interaction Rule.** 静态元素没有阴影。阴影只在 hover、focus、active、drag 时出现。如果一张卡在用户碰到它之前就已经浮在那里,问自己它是不是真的需要那么重要。
+
+## 5. Components
+
+### Buttons
+
+**Character:** 温润确定。圆角柔和但不圆胖,按压有下降和颜色加深的双重反馈。
+
+- **Shape:** 主按钮圆角 16px(lg),次级按钮和操作按钮 8px(sm)。提交型全宽按钮使用 44rpx(22px)胶囊形。
+- **Primary:** 朱砂红背景,白色文字,内边距 12px 28px。字体 14px/500。
+- **Hover:** 背景变为浅朱砂 (#E84545),或原色基础上叠加半透明遮罩。
+- **Pressed:** 背景变为深朱砂 (#A81212),同时 `transform: scale(0.97)`——双重确认。
+- **Disabled:** 不透明度 0.5,背景退为 #CCCCCC。交互完全阻断。
+- **Secondary:** 白底,墨黑文字,1px 发丝线边框。悬停时边框加深。
+- **Ghost:** 纯文字,无背景无边框。用于表格行内操作、工具栏次要动作。悬停时文字变色为朱砂红。
+
+### Chips / Status Tags
+
+**Character:** 功能性徽章——一眼知道状态,不需要读文字。
+
+- **Shape:** 胶囊形(pill,100px 半径),内边距 3px 12px,字号 11px/500。
+- **Color:** 背景 = 字典色 + `1A`(10% 不透明度),文字 = 字典色原值。例如在线状态:`background: #52C41A1A; color: #52C41A`。
+- **Fallback:** 当字典数据不可用时,成功态 #52C41A,警告态 #FAAD14,错误态 #F5222D,中性态 #999999。
+- **Size:** 统一高度,文字自适应宽度。不设固定宽度。
+
+### Cards
+
+**Character:** 安静的白纸。圆角柔和(24px/xl),阴影极轻(Ambient Base),只在悬停时升起。
+
+- **Corner Style:** 24px(xl),或页面内使用 20rpx(10px)。
+- **Background:** 纸白 #FFFFFF。
+- **Shadow (at rest):** 可选,取决于页面是否需要显式分层。数据密集页面可默认带阴影;信息流页面可仅靠底色区分。
+- **Shadow (hover):** `0 4rpx 16rpx rgba(0,0,0,0.06)`,配合 `translateY(-2px)`。
+- **Border:** 无。用阴影和间距区分卡片与背景,不加描边。
+- **Internal Padding:** 24rpx(12px)标准,30rpx(15px)宽松。
+
+### Inputs / Fields
+
+**Character:** 低调的输入区,焦点时被朱砂红唤醒。
+
+- **Style:** 背景 #F5F5F5(比霜白页面稍暖的灰),无边框,圆角 8px(sm),内边距 10px 14px。
+- **Focus:** 朱砂红聚焦环 `0 0 0 3px rgba(198,23,30,0.2)`,背景保持 #F5F5F5 不变。
+- **Placeholder:** 云灰 #B0B0B0,14px/400。
+- **Error:** 聚焦环变为错误红,输入框下方不额外显示红色提示框(错误信息在字段旁或下方用文字提示)。
+
+### Segmented Controls / Filter Tabs
+
+**Character:** 快速切换视口的「挡位选择器」。选中态有明确的物理感。
+
+- **Container:** 背景 #F5F5F5,圆角 16rpx(8px),内边距 6rpx(3px),flex 排列。
+- **Active Segment:** 朱砂红背景,白色文字,字重 500。附带品牌色阴影 `0 4rpx 12rpx rgba(198,23,30,0.3)`——这是整个系统里阴影最重的元素,因为它需要确认「你在这里」。
+- **Inactive:** 透明背景,石灰 (#666666) 文字,字重 400。
+- **Transition:** 0.25s cubic-bezier(0.4, 0, 0.2, 1)。
+
+### Search Bars
+
+**Character:** 工具条,不是装饰。输入区和按钮紧密相邻,一眼知道是搜索。
+
+- **Container:** 白底卡片,标准阴影 `0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06)`,圆角匹配所在页面的卡片风格。
+- **Input Wrapper:** 背景 #F5F5F5,圆角 8px,内含搜索图标(AppIcon `search`,云灰色)。
+- **Search Button:** 朱砂红背景,白色文字 "搜索",圆角 8px。紧贴输入区右侧。
+
+### Navigation (NavBar)
+
+**Character:** 朱砂红的顶部标记——告诉操作者「你还在工具里」。白色标题文字,左侧返回箭头。
+
+- **Background:** 朱砂红 #C6171E。
+- **Shadow:** 品牌导航阴影 `0 2px 8px rgba(198,23,30,0.15)`。
+- **Layout:** 三列——左侧 120rpx 固定宽度(返回箭头),中间标题居中,右侧 120rpx 固定宽度(可选操作按钮)。
+- **Title:** 17px/600,白色。
+- **Height:** 44px + 状态栏高度。
+- **Back Arrow:** AppIcon `chevron-left`,白色,24px。
+
+## 6. Do's and Don'ts
+
+### Do:
+- **Do** 用朱砂红标记每屏唯一的主操作路径。如果一屏上出现两个红色按钮,其中一个必须降级为次级。
+- **Do** 用 24px 圆角统一所有卡片。页面上不应该出现两种不同的卡片圆角。
+- **Do** 让阴影只出现在交互时。静态元素保持平坦,页面底色和卡片底色之间的色差已经足够区分层级。
+- **Do** 用字重差异(≥200)和字号差异(≥1.25x)来建立信息层级,不依赖颜色。
+- **Do** 在状态标签上使用字典色 + 10% 不透明度背景的公式。这是系统级的约定,不要为某个标签特判。
+- **Do** 给所有颜色过渡和位置变化使用 `cubic-bezier(0.4, 0, 0.2, 1)` 缓动,0.25s 时长。统一的节奏比花哨的曲线更重要。
+
+### Don't:
+- **Don't** 使用大色块堆砌、过度装饰、多余动画、营销口号式文案。这个系统是为工具使用场景设计的,不是营销页面。
+- **Don't** 出现密集表格、灰色调泛滥、控件堆叠、缺乏视觉层级和呼吸感的老式 ERP 风格。
+- **Don't** 使用 SaaS 奶油白 + 单色蓝紫渐变的模板化配色。这里没有蓝紫渐变的位置。
+- **Don't** 为了追求"专业"而牺牲温度——无性格的灰色、机械的间距、缺乏反馈的交互。
+- **Don't** 使用 `border-left` 或 `border-right` 超过 1px 的彩色侧边条纹来标记卡片或列表项。用完整的边框、背景色差或前置图标代替。
+- **Don't** 使用渐变文字(`background-clip: text`)。标题的强调通过字重和字号实现,不需要渐变。
+- **Don't** 在一屏上同时使用错误红 (#F44336) 和朱砂红 (#C6171E)——它们是两套信号系统,混在一起互相消解。
+- **Don't** 引入第二个字体。Inter 承担所有角色。层级靠字重和字号,不靠换字体。
+- **Don't** 嵌套卡片——卡片里面再放卡片永远是错的。用背景色差或分割线区分内容区块。

+ 7 - 0
admin-mp/.claude/settings.local.json

@@ -0,0 +1,7 @@
+{
+  "permissions": {
+    "allow": [
+      "Bash(node *)"
+    ]
+  }
+}

+ 15 - 0
admin-mp/index.html

@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover" />
+    <title>自助洗车运营管理</title>
+    <link rel="preconnect" href="https://fonts.googleapis.com">
+    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>

+ 4 - 4
admin-mp/package.json

@@ -1,11 +1,11 @@
 {
   "name": "admin-app",
   "version": "1.0.0",
-  "description": "自助洗车运营管理小程序",
+  "description": "自助洗车运营管理H5",
   "main": "main.js",
   "scripts": {
-    "dev:mp-weixin": "uni -p mp-weixin",
-    "build:mp-weixin": "uni build -p mp-weixin",
+    "dev:h5": "uni",
+    "build:h5": "uni build",
     "dev": "uni",
     "build": "uni build"
   },
@@ -43,7 +43,7 @@
     "name": "自助洗车运营管理",
     "versionName": "1.0.0",
     "versionCode": "100",
-    "description": "自助洗车运营管理小程序"
+    "description": "自助洗车运营管理H5"
   },
   "keywords": [],
   "author": "",

+ 148 - 117
admin-mp/src/App.vue

@@ -6,195 +6,226 @@
 
 <script setup>
 import { onLaunch } from '@dcloudio/uni-app'
+import { loadDicts } from './utils/dict.js'
+import { storage } from './utils/index.js'
 
 // 应用启动时执行
 onLaunch(() => {
   console.log('App launched')
+  // 如果已有登录token,预加载字典数据
+  if (storage.get('token')) {
+    loadDicts()
+  }
 })
 </script>
 
 <style lang="scss">
-/* 全局样式 - 基于洗车小程序设计规范 */
+/* 全局样式 — Soft UI Evolution */
 @import './uni.scss';
 
-/* 微信小程序使用page作为根元素 */
 page {
   font-family: $uni-font-family;
   font-size: $uni-font-size-base;
   line-height: $uni-line-height-base;
   color: $uni-text-color;
   background-color: $uni-page-bg-color;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
 }
 
-/* 自定义tabBar点击动效 */
-/* 注意:微信小程序原生tabBar样式自定义能力有限,以下样式可能需要配合自定义tabBar组件才能完全生效 */
-.uni-tabbar {
-  --tabbar-height: 50px;
-  --tabbar-bg-color: #FFFFFF;
-  --tabbar-border-color: #E0E0E0;
-  --tabbar-item-active-color: #C6171E;
-  --tabbar-item-color: #999999;
-  --tabbar-item-font-size: 14px;
-  --tabbar-item-icon-size: 24px;
-  --tabbar-item-spacing: 4px;
+/* ===== Global Keyframes ===== */
+@keyframes app-spin {
+  to { transform: rotate(360deg); }
 }
 
-/* 为tabBar项添加点击动效 */
-.uni-tabbar__item:active {
-  transform: scale(0.95);
-  transition: transform 0.15s ease;
+/* ===== Loading Spinner (unified CSS, replaces emoji 🔄) ===== */
+.loading-spinner {
+  width: 64rpx;
+  height: 64rpx;
+  border: 4rpx solid rgba(198, 23, 30, 0.1);
+  border-top-color: $uni-color-primary;
+  border-radius: 50%;
+  animation: app-spin 0.8s linear infinite;
 }
 
-/* 为tabBar项添加hover效果(如果支持的话) */
-.uni-tabbar__item:hover {
-  opacity: 0.8;
-}
-
-/* 自定义tabBar样式增强 */
-.uni-tabbar__item-icon {
-  font-size: var(--tabbar-item-icon-size);
+/* ===== Loading State Container ===== */
+.loading-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 120rpx 0;
 }
-
-.uni-tabbar__item-text {
-  font-size: var(--tabbar-item-font-size);
-  margin-top: var(--tabbar-item-spacing);
+.loading-text {
+  font-size: $uni-font-size-sm;
+  color: $uni-text-color-light;
+  margin-top: 20rpx;
 }
 
-/* 如果使用自定义tabBar组件,可以添加以下样式 */
-.custom-tabbar {
+/* ===== Empty State ===== */
+.empty-state {
   display: flex;
-  justify-content: space-around;
+  flex-direction: column;
   align-items: center;
-  height: var(--tabbar-height);
-  background-color: var(--tabbar-bg-color);
-  border-top: 1px solid var(--tabbar-border-color);
-  position: fixed;
-  bottom: 0;
-  left: 0;
-  right: 0;
-  z-index: 999;
-}
-
-.custom-tabbar-item {
-  flex: 1;
+  justify-content: center;
+  padding: 100rpx 0;
+}
+.empty-icon-wrapper {
+  width: 120rpx;
+  height: 120rpx;
+  border-radius: 50%;
+  background: #F5F5F5;
   display: flex;
-  flex-direction: column;
   align-items: center;
   justify-content: center;
-  height: 100%;
-  color: var(--tabbar-item-color);
-  transition: all 0.3s ease;
+  margin-bottom: 24rpx;
 }
-
-.custom-tabbar-item.active {
-  color: var(--tabbar-item-active-color);
+.empty-text {
+  font-size: 28rpx;
+  color: $uni-text-color-light;
+  margin-bottom: 32rpx;
 }
-
-.custom-tabbar-item:active {
-  transform: scale(0.95);
-  transition: transform 0.15s ease;
+.empty-refresh-btn {
+  padding: 16rpx 48rpx;
+  background: $uni-color-primary;
+  color: #FFFFFF;
+  border: none;
+  border-radius: $uni-border-radius-sm;
+  font-size: $uni-font-size-sm;
+  font-weight: $uni-font-weight-medium;
+  transition: all $uni-transition-duration-base $uni-transition-timing;
 }
-
-.custom-tabbar-item-icon {
-  font-size: var(--tabbar-item-icon-size);
-  margin-bottom: var(--tabbar-item-spacing);
+.empty-refresh-btn:active {
+  transform: translateY(1px);
 }
 
-.custom-tabbar-item-text {
+/* ===== Tab Bar ===== */
+.uni-tabbar {
+  --tabbar-height: 50px;
+  --tabbar-bg-color: #FFFFFF;
+  --tabbar-border-color: #E0E0E0;
+  --tabbar-item-active-color: #C6171E;
+  --tabbar-item-color: #999999;
+  --tabbar-item-font-size: 14px;
+  --tabbar-item-icon-size: 24px;
+  --tabbar-item-spacing: 4px;
+}
+.uni-tabbar__item:active {
+  transform: scale(0.95);
+  transition: transform $uni-transition-duration-fast $uni-transition-timing;
+}
+.uni-tabbar__item-icon { font-size: var(--tabbar-item-icon-size); }
+.uni-tabbar__item-text {
   font-size: var(--tabbar-item-font-size);
+  margin-top: var(--tabbar-item-spacing);
 }
 
-/* 容器样式 */
+/* ===== Flex Utilities ===== */
+.flex { display: flex; }
+.flex-center { display: flex; align-items: center; justify-content: center; }
+.flex-just-between { display: flex; align-items: center; justify-content: space-between; }
+.flex-column { display: flex; flex-direction: column; }
+
+/* ===== Container ===== */
 .container {
   width: 100%;
   padding: 0 $uni-spacing-base;
 }
 
-/* Flex布局工具类 */
-.flex {
-  display: flex;
-}
-
-.flex-center {
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-
-.flex-just-between {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-}
-
-.flex-column {
-  display: flex;
-  flex-direction: column;
-}
-
-/* 卡片基础样式 */
+/* ===== Card (Soft UI Evolution standard) ===== */
 .card {
   background-color: $uni-card-bg-color;
-  border-radius: $uni-border-radius-base;
-  border: 1px solid $uni-border-color;
-  box-shadow: $uni-box-shadow;
+  border-radius: $uni-border-radius-md;
+  box-shadow: $uni-box-shadow-base;
   padding: $uni-spacing-base;
+  transition: all $uni-transition-duration-base $uni-transition-timing;
+}
+.card:active {
+  transform: translateY(-2px);
+  box-shadow: $uni-box-shadow-lg;
 }
 
-/* 主要按钮样式 */
+/* ===== Buttons (Soft UI) ===== */
 .btn-primary {
   background-color: $uni-color-primary;
-  color: white;
+  color: #FFFFFF;
   border: none;
-  border-radius: $uni-border-radius-base;
-  padding: $uni-spacing-sm $uni-spacing-base;
-  font-size: $uni-font-size-base;
-  font-weight: $uni-font-weight-medium;
-  cursor: pointer;
-  transition: all $uni-transition-duration;
+  border-radius: $uni-border-radius-sm;
+  padding: 12px 24px;
+  font-size: $uni-font-size-sm;
+  font-weight: $uni-font-weight-semibold;
+  box-shadow: $uni-box-shadow-sm;
+  transition: all $uni-transition-duration-base $uni-transition-timing;
 }
-
 .btn-primary:active {
-  opacity: 0.8;
-  transform: scale(0.98);
+  transform: translateY(1px);
+  box-shadow: none;
+}
+.btn-primary:disabled {
+  opacity: 0.5;
+  box-shadow: none;
 }
 
-/* 次要按钮样式 */
 .btn-secondary {
   background-color: $uni-bg-color;
   color: $uni-text-color;
   border: 1px solid $uni-border-color;
-  border-radius: $uni-border-radius-base;
-  padding: $uni-spacing-sm $uni-spacing-base;
-  font-size: $uni-font-size-base;
-  cursor: pointer;
-  transition: all $uni-transition-duration;
+  border-radius: $uni-border-radius-sm;
+  padding: 12px 24px;
+  font-size: $uni-font-size-sm;
+  font-weight: $uni-font-weight-medium;
+  transition: all $uni-transition-duration-base $uni-transition-timing;
 }
-
 .btn-secondary:active {
   background-color: $uni-page-bg-color;
 }
 
-/* 状态标签样式 */
+/* ===== Status Tags (pill shape) ===== */
 .status-tag {
-  padding: 4rpx 12rpx;
-  border-radius: $uni-border-radius-circle;
+  display: inline-flex;
+  align-items: center;
+  padding: 4px 12px;
+  border-radius: $uni-border-radius-pill;
   font-size: $uni-font-size-xs;
   font-weight: $uni-font-weight-medium;
 }
-
-.status-idle {
+.status-tag-success {
   background-color: rgba(76, 175, 80, 0.1);
-  color: $uni-color-idle;
+  color: $uni-color-success;
 }
-
-.status-busy {
+.status-tag-warning {
   background-color: rgba(255, 152, 0, 0.1);
-  color: $uni-color-busy;
+  color: $uni-color-warning;
 }
-
-.status-error {
+.status-tag-error {
   background-color: rgba(244, 67, 54, 0.1);
-  color: $uni-color-error-state;
+  color: $uni-color-error;
+}
+.status-tag-info {
+  background-color: rgba(33, 150, 243, 0.1);
+  color: $uni-color-info;
+}
+
+/* ===== Form Input (Soft UI standard) ===== */
+.input-field {
+  width: 100%;
+  padding: 16rpx 20rpx;
+  background: $uni-page-bg-color;
+  border: 2rpx solid $uni-border-color;
+  border-radius: $uni-border-radius-sm;
+  font-size: $uni-font-size-sm;
+  color: $uni-text-color;
+  transition: border-color $uni-transition-duration-base $uni-transition-timing,
+              box-shadow $uni-transition-duration-base $uni-transition-timing;
+  box-sizing: border-box;
+}
+.input-field:focus {
+  border-color: $uni-color-primary;
+  box-shadow: $uni-focus-ring;
+  background: #FFFFFF;
+  outline: none;
+}
+.input-field::placeholder {
+  color: $uni-text-color-placeholder;
 }
 </style>

+ 9 - 0
admin-mp/src/api/dict.js

@@ -11,3 +11,12 @@ export const getDataDictList = (params = {}) => {
     ...params
   })
 }
+
+/**
+ * 批量保存或更新字典项
+ * @param {Array} data - 字典项数组
+ * @returns {Promise}
+ */
+export const saveOrUpdateDict = (data) => {
+  return post('/dataDict/saveOrUpdate', data, false)
+}

+ 36 - 0
admin-mp/src/api/finance.js

@@ -52,4 +52,40 @@ export const reviewWithdrawn = (params) => {
  */
 export const confirmWithdrawnPayment = (params) => {
   return post('/finance/confirmWithdrawnPayment', params)
+}
+
+/**
+ * 获取退款记录列表
+ * @param {Object} params - 查询参数
+ * @returns {Promise} 退款记录列表
+ */
+export const getRefundLogs = (params) => {
+  return get('/finance/listRefundLog', params)
+}
+
+/**
+ * 处理退款
+ * @param {string|number} id - 退款记录ID
+ * @returns {Promise} 退款处理结果
+ */
+export const processRefund = (id) => {
+  return get(`/finance/customWxRefund/${id}`)
+}
+
+/**
+ * 获取结算记录列表
+ * @param {Object} params - 查询参数
+ * @returns {Promise} 结算记录列表
+ */
+export const getSettlementRecords = (params) => {
+  return post('/finance/settlementRecords', params)
+}
+
+/**
+ * 申请退款
+ * @param {Object} params - { userId, reason }
+ * @returns {Promise} 退款申请结果
+ */
+export const applyRefund = (params) => {
+  return post('/finance/applyRefund', params)
 }

+ 37 - 0
admin-mp/src/api/system.js

@@ -0,0 +1,37 @@
+import { post, get } from '../utils'
+
+/**
+ * 获取反馈列表
+ * @param {Object} params - 查询参数
+ * @returns {Promise} 反馈列表
+ */
+export const getFeedbackList = (params) => {
+  return post('/feedback/list', params)
+}
+
+/**
+ * 获取反馈详情
+ * @param {string|number} id - 反馈ID
+ * @returns {Promise} 反馈详情
+ */
+export const getFeedbackDetail = (id) => {
+  return get(`/feedback/detail/${id}`)
+}
+
+/**
+ * 获取公告列表
+ * @param {Object} params - 查询参数
+ * @returns {Promise} 公告列表
+ */
+export const getNoticeList = (params) => {
+  return get('/notice/list', params)
+}
+
+/**
+ * 获取操作日志列表
+ * @param {Object} params - 查询参数
+ * @returns {Promise} 操作日志列表
+ */
+export const getOptLogList = (params) => {
+  return post('/optLog/list', params)
+}

+ 9 - 0
admin-mp/src/api/user.js

@@ -33,4 +33,13 @@ export const getRoleList = () => {
  */
 export const getUserDetail = (id) => {
   return get(`/admin-user/detail/${id}`)
+}
+
+/**
+ * 获取App用户列表
+ * @param {Object} params - 查询参数
+ * @returns {Promise} App用户列表
+ */
+export const getAppUserList = (params) => {
+  return get('/custom/listUser', params)
 }

+ 75 - 0
admin-mp/src/components/AppIcon.vue

@@ -0,0 +1,75 @@
+<template>
+  <image v-if="svgSrc" :src="svgSrc" :style="iconStyle" class="app-icon" mode="aspectFit" />
+</template>
+
+<script setup>
+import { computed } from 'vue'
+
+const props = defineProps({
+  name: { type: String, required: true },
+  size: { type: [Number, String], default: 24 },
+  color: { type: String, default: 'currentColor' }
+})
+
+const svgPaths = {
+  'user': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
+  'building': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="2" width="16" height="20" rx="2"/><path d="M9 22v-4h6v4M8 6h.01M8 10h.01M12 6h.01M12 10h.01M16 6h.01M16 10h.01"/></svg>',
+  'settings': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>',
+  'clipboard': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><rect x="8" y="2" width="8" height="4" rx="1"/></svg>',
+  'dollar': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>',
+  'users': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/></svg>',
+  'smartphone': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="2" width="14" height="20" rx="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>',
+  'credit-card': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="4" width="22" height="16" rx="2"/><line x1="1" y1="10" x2="23" y2="10"/></svg>',
+  'banknote': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="20" height="12" rx="2"/><circle cx="12" cy="12" r="2"/><path d="M6 12h.01M18 12h.01"/></svg>',
+  'clock': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>',
+  'monitor': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>',
+  'trending-up': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/></svg>',
+  'trending-down': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 18 13.5 8.5 8.5 13.5 1 6"/><polyline points="17 18 23 18 23 12"/></svg>',
+  'inbox': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg>',
+  'chevron-left': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>',
+  'chevron-right': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>',
+  'chevron-down': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>',
+  'x': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
+  'rotate-ccw': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>',
+  'bar-chart': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>',
+  'edit': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.83 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg>',
+  'megaphone': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/><path d="M4 15l10-3v6l-10-3z"/><path d="M14 12v6"/></svg>',
+  'message-circle': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>',
+  'book-open': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>',
+  'home': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>',
+  'lock': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>',
+  'search': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
+  'plus': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>',
+  'arrow-up': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>',
+  'arrow-down': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg>',
+  'refresh-cw': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>',
+  'log-out': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>'
+}
+
+const svgSrc = computed(() => {
+  const svg = svgPaths[props.name]
+  if (!svg) return ''
+  const coloredSvg = svg.replace('stroke="COLOR"', `stroke="${props.color}"`)
+  return 'data:image/svg+xml,' + encodeURIComponent(coloredSvg)
+})
+
+const iconStyle = computed(() => {
+  const sizeNum = Number(props.size) || 24
+  const size = `${sizeNum}rpx`
+  return {
+    width: size,
+    height: size,
+    display: 'inline-block',
+    flexShrink: '0',
+    verticalAlign: 'middle'
+  }
+})
+</script>
+
+<style scoped>
+.app-icon {
+  display: inline-block;
+  flex-shrink: 0;
+  vertical-align: middle;
+}
+</style>

+ 69 - 0
admin-mp/src/components/NavBar.vue

@@ -0,0 +1,69 @@
+<template>
+  <view class="nav-bar">
+    <view class="nav-bar__left" @click="handleBack">
+      <AppIcon v-if="showBack" name="chevron-left" :size="22" color="#FFFFFF" />
+    </view>
+    <text class="nav-bar__title">{{ title }}</text>
+    <view class="nav-bar__right">
+      <slot name="right"></slot>
+    </view>
+  </view>
+</template>
+
+<script setup>
+defineProps({
+  title: { type: String, default: '' },
+  showBack: { type: Boolean, default: true }
+})
+
+const emit = defineEmits(['back'])
+
+const handleBack = () => {
+  if (emit._events && emit._events.back) {
+    emit('back')
+  } else {
+    uni.navigateBack()
+  }
+}
+</script>
+
+<style scoped>
+.nav-bar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  height: 88rpx;
+  padding-top: calc(24rpx + var(--status-bar-height));
+  padding-right: 30rpx;
+  padding-bottom: 24rpx;
+  padding-left: 30rpx;
+  background: #C6171E;
+  box-shadow: 0 2px 8px rgba(198, 23, 30, 0.15);
+  position: relative;
+  z-index: 100;
+}
+
+.nav-bar__left {
+  width: 120rpx;
+  display: flex;
+  align-items: center;
+}
+
+.nav-bar__title {
+  flex: 1;
+  text-align: center;
+  font-size: 34rpx;
+  font-weight: 600;
+  color: #FFFFFF;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.nav-bar__right {
+  width: 120rpx;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+}
+</style>

+ 87 - 0
admin-mp/src/components/dict-label/index.vue

@@ -0,0 +1,87 @@
+<template>
+  <text class="dict-label" :style="labelStyle">{{ displayText || '-' }}</text>
+</template>
+
+<script setup>
+import { computed } from 'vue'
+import dictUtil from '../../utils/dict.js'
+
+const props = defineProps({
+  modelValue: {
+    type: [String, Number, Boolean],
+    default: ''
+  },
+  type: {
+    type: String,
+    required: true
+  },
+  dataRange: {
+    type: Array,
+    default: () => []
+  }
+})
+
+const colorList = [
+  '#FFB800',
+  '#009688',
+  '#1E9FFF',
+  '#00C7D2',
+  '#599CDE',
+  '#FF5722',
+  '#eb2f96',
+  '#4a5055'
+]
+
+const hexToRgb = (hex) => {
+  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
+  return result
+    ? {
+        r: parseInt(result[1], 16),
+        g: parseInt(result[2], 16),
+        b: parseInt(result[3], 16)
+      }
+    : { r: 0, g: 0, b: 0 }
+}
+
+const labelInfo = computed(() => {
+  if (props.dataRange && props.dataRange.length > 0) {
+    const data = props.dataRange.find(k => k.value == props.modelValue)
+    if (data) {
+      return { text: data.label, color: data.color || '#000000' }
+    }
+    return { text: '', color: '' }
+  }
+
+  const dictList = dictUtil.getDictList(props.type)
+  const dict = dictList.find(k => k.value == props.modelValue || k.value === String(props.modelValue))
+  if (dict) {
+    const colorIndex = (dict.value || 0) % 8
+    return { text: dict.name || dict.label, color: dict.color || colorList[colorIndex] }
+  }
+  return { text: '', color: '' }
+})
+
+const displayText = computed(() => labelInfo.value.text)
+
+const labelStyle = computed(() => {
+  const color = labelInfo.value.color || '#000000'
+  const { r, g, b } = hexToRgb(color)
+  return {
+    backgroundColor: `rgba(${r},${g},${b},0.15)`,
+    color: `rgb(${r},${g},${b})`
+  }
+})
+</script>
+
+<style scoped>
+.dict-label {
+  display: inline-block;
+  padding: 4rpx 16rpx;
+  font-size: 22rpx;
+  font-weight: 600;
+  line-height: 1.6;
+  text-align: center;
+  white-space: nowrap;
+  border-radius: 6rpx;
+}
+</style>

+ 93 - 0
admin-mp/src/components/dict-select/index.vue

@@ -0,0 +1,93 @@
+<template>
+  <view class="dict-select" @click="openPicker">
+    <text :class="['select-text', { placeholder: !displayText }]">
+      {{ displayText || placeholder }}
+    </text>
+    <text class="select-arrow">▼</text>
+  </view>
+</template>
+
+<script setup>
+import { ref, computed, watch } from 'vue'
+import dictUtil from '../../utils/dict.js'
+
+const props = defineProps({
+  modelValue: {
+    type: [String, Number],
+    default: ''
+  },
+  type: {
+    type: String,
+    required: true
+  },
+  dataRange: {
+    type: Array,
+    default: () => []
+  },
+  placeholder: {
+    type: String,
+    default: '请选择'
+  },
+  disabled: {
+    type: Boolean,
+    default: false
+  }
+})
+
+const emit = defineEmits(['update:modelValue', 'change'])
+
+const options = computed(() => {
+  if (props.dataRange && props.dataRange.length > 0) {
+    return props.dataRange
+  }
+  return dictUtil.getDictOptions(props.type)
+})
+
+const displayText = computed(() => {
+  const opt = options.value.find(k => k.value == props.modelValue)
+  return opt ? opt.label : ''
+})
+
+const openPicker = () => {
+  if (props.disabled) return
+  if (options.value.length === 0) {
+    uni.showToast({ title: '暂无选项', icon: 'none' })
+    return
+  }
+
+  const range = options.value.map(k => k.label)
+  const currentIndex = options.value.findIndex(k => k.value == props.modelValue)
+
+  uni.showActionSheet({
+    itemList: range,
+    success: (res) => {
+      const selected = options.value[res.tapIndex]
+      emit('update:modelValue', selected.value)
+      emit('change', selected.value)
+    }
+  })
+}
+</script>
+
+<style scoped>
+.dict-select {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16rpx 20rpx;
+  background: #F5F7FA;
+  border: 2rpx solid #E8E8E8;
+  border-radius: 16rpx;
+  font-size: 28rpx;
+}
+.select-text {
+  color: #1A1A1A;
+}
+.select-text.placeholder {
+  color: #CCCCCC;
+}
+.select-arrow {
+  font-size: 20rpx;
+  color: #999999;
+}
+</style>

+ 106 - 0
admin-mp/src/custom-tab-bar/index.vue

@@ -0,0 +1,106 @@
+<template>
+  <view class="tab-bar">
+    <view
+      v-for="(tab, index) in tabs"
+      :key="index"
+      class="tab-item"
+      @click="switchTab(tab)"
+    >
+      <view class="tab-inner" :class="{ active: current === index }">
+        <AppIcon
+          :name="tab.icon"
+          :size="current === index ? 22 : 22"
+          :color="current === index ? activeColor : inactiveColor"
+        />
+        <text
+          class="tab-text"
+          :class="{ active: current === index }"
+        >{{ tab.text }}</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+
+const activeColor = '#C6171E'
+const inactiveColor = '#999999'
+
+const tabs = [
+  { pagePath: '/pages/index/index',   text: '首页', icon: 'home' },
+  { pagePath: '/pages/order/list',    text: '订单', icon: 'clipboard' },
+  { pagePath: '/pages/device/list',   text: '设备', icon: 'monitor' },
+  { pagePath: '/pages/finance/index', text: '财务', icon: 'dollar' },
+]
+
+const current = ref(0)
+
+const syncCurrent = () => {
+  const pages = getCurrentPages()
+  if (pages.length > 0) {
+    const route = '/' + pages[pages.length - 1].route
+    const idx = tabs.findIndex(t => t.pagePath === route)
+    if (idx !== -1) current.value = idx
+  }
+}
+
+onMounted(() => syncCurrent())
+
+const switchTab = (tab) => {
+  const index = tabs.findIndex(t => t.pagePath === tab.pagePath)
+  if (index !== -1) current.value = index
+  uni.switchTab({ url: tab.pagePath })
+}
+</script>
+
+<style scoped>
+.tab-bar {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  display: flex;
+  align-items: stretch;
+  background: #FFFFFF;
+  padding: 8rpx 12rpx;
+  padding-bottom: calc(8rpx + env(safe-area-inset-bottom));
+  box-shadow: 0 -2px 16px rgba(0, 0, 0, 0.06);
+  border-top: 1px solid #F0F0F0;
+  z-index: 999;
+}
+
+.tab-item {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.tab-inner {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 4rpx;
+  padding: 10rpx 24rpx;
+  border-radius: 20rpx;
+  transition: background 0.25s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.tab-inner.active {
+  background: rgba(198, 23, 30, 0.08);
+}
+
+.tab-text {
+  font-size: 20rpx;
+  font-weight: 500;
+  color: #999999;
+  transition: color 0.25s;
+}
+
+.tab-text.active {
+  color: #C6171E;
+  font-weight: 600;
+}
+</style>

+ 4 - 0
admin-mp/src/main.js

@@ -1,5 +1,7 @@
 import { createSSRApp } from 'vue'
 import App from './App.vue'
+import AppIcon from './components/AppIcon.vue'
+import NavBar from './components/NavBar.vue'
 import * as utils from './utils/index.js'
 
 // 将工具函数挂载到全局
@@ -7,6 +9,8 @@ globalThis.utils = utils
 
 export function createApp() {
   const app = createSSRApp(App)
+  app.component('AppIcon', AppIcon)
+  app.component('NavBar', NavBar)
   return {
     app
   }

+ 1 - 1
admin-mp/src/manifest.json

@@ -1,7 +1,7 @@
 {
     "name" : "自助洗车运营管理",
     "appid" : "__UNI__14E0F2A",
-    "description" : "自助洗车运营管理小程序",
+    "description" : "自助洗车运营管理H5",
     "versionName" : "1.0.0",
     "versionCode" : "100",
     "transformPx" : true,

+ 68 - 5
admin-mp/src/pages.json

@@ -11,7 +11,8 @@
       "path": "pages/index/index",
       "style": {
         "navigationBarTitleText": "首页",
-        "enablePullDownRefresh": false
+        "navigationStyle": "custom",
+        "enablePullDownRefresh": true
       }
     },
     {
@@ -52,6 +53,24 @@
         "navigationBarTitleText": "提现管理"
       }
     },
+    {
+      "path": "pages/finance/refund",
+      "style": {
+        "navigationBarTitleText": "退款清单"
+      }
+    },
+    {
+      "path": "pages/finance/settlement",
+      "style": {
+        "navigationBarTitleText": "结算记录"
+      }
+    },
+    {
+      "path": "pages/finance/split-record",
+      "style": {
+        "navigationBarTitleText": "分账记录"
+      }
+    },
     {
       "path": "pages/setting/index",
       "style": {
@@ -93,19 +112,63 @@
         "navigationBarTitleText": "设备绑定管理",
         "navigationStyle": "custom"
       }
+    },
+    {
+      "path": "pages/station/list",
+      "style": {
+        "navigationBarTitleText": "站点清单"
+      }
+    },
+    {
+      "path": "pages/station/detail",
+      "style": {
+        "navigationBarTitleText": "站点详情",
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/user/list",
+      "style": {
+        "navigationBarTitleText": "用户列表"
+      }
+    },
+    {
+      "path": "pages/system/feedback",
+      "style": {
+        "navigationBarTitleText": "反馈上报"
+      }
+    },
+    {
+      "path": "pages/system/notice",
+      "style": {
+        "navigationBarTitleText": "系统公告"
+      }
+    },
+    {
+      "path": "pages/system/log",
+      "style": {
+        "navigationBarTitleText": "操作日志"
+      }
+    },
+    {
+      "path": "pages/system/dict",
+      "style": {
+        "navigationBarTitleText": "数据字典"
+      }
     }
   ],
   "globalStyle": {
     "navigationBarTextStyle": "black",
     "navigationBarTitleText": "自助洗车运营管理",
     "navigationBarBackgroundColor": "#ffffff",
-    "backgroundColor": "#f5f5f5"
+    "backgroundColor": "#F5F7FA"
   },
   "tabBar": {
-    "color": "#666666",
-    "selectedColor": "#007AFF",
+    "custom": true,
+    "color": "#999999",
+    "selectedColor": "#C6171E",
     "backgroundColor": "#ffffff",
-    "borderStyle": "black",
+    "borderStyle": "white",
     "list": [
       {
         "pagePath": "pages/index/index",

+ 141 - 271
admin-mp/src/pages/device/detail.vue

@@ -1,15 +1,7 @@
 <template>
   <view class="device-detail-container">
-    <!-- 顶部紫色导航栏 -->
-    <view class="header-nav">
-      <view class="nav-left">
-        <text class="back-icon" @click="goBack">←</text>
-      </view>
-      <text class="nav-title">设备详情</text>
-      <view class="nav-right"></view>
-    </view>
-    
-    <!-- 设备基本信息卡片 -->
+    <NavBar title="设备详情" @back="goBack" />
+
     <view class="basic-info-card">
       <view class="device-main-info">
         <view class="device-name-section">
@@ -22,9 +14,8 @@
         </view>
       </view>
     </view>
-    
-    <!-- 设备详细信息卡片 -->
-    <view class="detail-info-card">
+
+    <view class="info-card">
       <view class="card-title">基础信息</view>
       <view class="info-list">
         <view class="info-item">
@@ -49,45 +40,43 @@
         </view>
       </view>
     </view>
-    
-    <!-- 设备运行状态卡片 -->
-    <view class="status-card">
+
+    <view class="info-card">
       <view class="card-title">实时状态</view>
-      <view class="status-list">
-        <view class="status-item">
-          <text class="status-label">核心温度</text>
-          <text class="status-value">{{ deviceDetail.temperatureChip ? deviceDetail.temperatureChip + '°C' : '未知' }}</text>
+      <view class="info-list">
+        <view class="info-item">
+          <text class="item-label">核心温度</text>
+          <text class="item-value">{{ deviceDetail.temperatureChip ? deviceDetail.temperatureChip + '°C' : '未知' }}</text>
         </view>
-        <view class="status-item">
-          <text class="status-label">是否有水</text>
-          <text class="status-value">{{ fmtDictName('yes_no', deviceDetail.hasWater) }}</text>
+        <view class="info-item">
+          <text class="item-label">是否有水</text>
+          <text class="item-value">{{ fmtDictName('yes_no', deviceDetail.hasWater) }}</text>
         </view>
-        <view class="status-item">
-          <text class="status-label">是否有泡沫</text>
-          <text class="status-value">{{ fmtDictName('yes_no', deviceDetail.hasFoam) }}</text>
+        <view class="info-item">
+          <text class="item-label">是否有泡沫</text>
+          <text class="item-value">{{ fmtDictName('yes_no', deviceDetail.hasFoam) }}</text>
         </view>
-        <view class="status-item" v-if="deviceDetail.state === 'fault'">
-          <text class="status-label">故障原因</text>
-          <text class="status-value danger-text">{{ deviceDetail.faultReason || '未知故障' }}</text>
+        <view class="info-item" v-if="isFault">
+          <text class="item-label">故障原因</text>
+          <text class="item-value item-value-danger">{{ deviceDetail.faultReason || '未知故障' }}</text>
         </view>
-        <view class="status-item">
-          <text class="status-label">运行时长</text>
-          <text class="status-value">{{ formatDuration(deviceDetail.runningTime) || '0小时' }}</text>
+        <view class="info-item">
+          <text class="item-label">运行时长</text>
+          <text class="item-value">{{ formatDuration(deviceDetail.runningTime) || '0小时' }}</text>
         </view>
       </view>
     </view>
-    
-    <!-- 设备操作按钮 -->
+
     <view class="action-section">
-      <button 
-        v-if="isOnline" 
+      <button
+        v-if="isOnline"
         class="stop-btn"
         @click="handleStopDevice"
       >
         停止设备
       </button>
-      <button 
-        v-if="!isOnline" 
+      <button
+        v-if="!isOnline"
         class="start-btn"
         @click="handleStartDevice"
       >
@@ -97,18 +86,16 @@
         刷新信息
       </button>
     </view>
-    
-    <!-- 加载状态 -->
+
     <view class="loading-overlay" v-if="loading">
-      <text class="loading-spinner">🔄</text>
+      <view class="loading-spinner"></view>
       <text class="loading-text">加载中...</text>
     </view>
-    
-    <!-- 空状态 -->
+
     <view class="empty-state" v-if="!loading && !deviceDetail.id">
-      <text class="empty-icon">📱</text>
+      <AppIcon name="smartphone" size="48" color="#BFBFBF" />
       <text class="empty-text">设备不存在</text>
-      <button class="refresh-btn" @click="loadDeviceDetail">刷新</button>
+      <button class="empty-refresh-btn" @click="loadDeviceDetail">刷新</button>
     </view>
   </view>
 </template>
@@ -116,222 +103,122 @@
 <script setup>
 import { ref, onMounted, computed } from 'vue'
 import { getDeviceDetail, stopDevice } from '../../api/device.js'
-import { formatTime, showToast, storage, fmtDictName, getDictColor } from '../../utils/index.js'
+import { formatTime, showToast, fmtDictName, getDictColor } from '../../utils/index.js'
+import dictUtil from '../../utils/dict.js'
 
 const deviceId = ref('')
 const deviceDetail = ref({})
 const loading = ref(true)
 
-// 计算设备是否在线(通过字典查找在线状态值)
 const isOnline = computed(() => {
   const state = deviceDetail.value.state
   if (state === null || state === undefined) return false
-  const dicts = storage.get('dicts')
-  if (dicts && dicts['WashDevice.status']) {
-    const item = dicts['WashDevice.status'].find(k => k.value == state)
-    if (item && item.color) {
-      return item.color === '#52C41A'
-    }
-  }
-  return false
+  return state == dictUtil.getDictValue('WashDevice.status', '在线')
 })
 
-// 获取设备类型文本
-const getDeviceTypeText = (type) => {
-  if (!type) return '无'
-  
-  try {
-    const dicts = storage.get('dicts')
-    // 尝试不同的设备类型字典键名
-    const possibleDictKeys = ['Device.type', 'WashDevice.type', 'Device.deviceType', 'DeviceType']
-    
-    for (const key of possibleDictKeys) {
-      if (dicts && dicts[key]) {
-        const deviceTypeDict = dicts[key]
-        const dictItem = deviceTypeDict.find(item => item.value == type)
-        if (dictItem) {
-          return dictItem.name
-        }
-      }
-    }
-    
-    return String(type)
-  } catch (error) {
-    console.error('获取设备类型文本失败:', error)
-    return String(type)
-  }
-}
+const isFault = computed(() => {
+  const state = deviceDetail.value.state
+  if (state === null || state === undefined) return false
+  return state == dictUtil.getDictValue('WashDevice.status', '故障')
+})
 
-// 从字典获取设备状态文本
-const getDeviceStatusText = (state) => {
-  return fmtDictName('WashDevice.status', state)
-}
+const getDeviceStatusText = (state) => fmtDictName('WashDevice.status', state)
 
-// 从字典获取设备状态样式
 const getDeviceStatusStyle = (state) => {
   const color = getDictColor('WashDevice.status', state)
   if (color) {
-    return {
-      color: color,
-      backgroundColor: `${color}1A`
-    }
+    return { color: color, backgroundColor: `${color}1A` }
   }
   return {}
 }
 
-// 格式化运行时长
 const formatDuration = (seconds) => {
   if (!seconds || isNaN(seconds)) return '0小时'
-  
   const totalSeconds = parseInt(seconds)
   const hours = Math.floor(totalSeconds / 3600)
   const minutes = Math.floor((totalSeconds % 3600) / 60)
-  
-  if (hours > 0) {
-    return `${hours}小时${minutes}分钟`
-  } else {
-    return `${minutes}分钟`
-  }
+  if (hours > 0) return `${hours}小时${minutes}分钟`
+  return `${minutes}分钟`
 }
 
-// 加载设备详情
 const loadDeviceDetail = async () => {
   if (!deviceId.value) {
-    console.error('设备ID为空')
     showToast('设备ID不存在')
     loading.value = false
     return
   }
-  
   loading.value = true
   try {
     const res = await getDeviceDetail(deviceId.value)
-    console.log('设备详情响应:', res)
-    
     if (res && res.code === 200) {
       deviceDetail.value = res.data || {}
-      console.log('加载的设备详情:', deviceDetail.value)
     } else {
       showToast(res.msg || '获取设备详情失败')
     }
   } catch (error) {
-    console.error('获取设备详情失败:', error)
     showToast('获取设备详情失败')
   } finally {
     loading.value = false
   }
 }
 
-// 停止设备
-const handleStopDevice = async () => {
-  try {
-    await stopDevice(deviceDetail.value.shortId)
-    showToast('设备已停止', 'success')
-    // 刷新设备详情
-    loadDeviceDetail()
-  } catch (error) {
-    console.error('停止设备失败:', error)
-    showToast('停止设备失败')
-  }
+const handleStopDevice = () => {
+  uni.showModal({
+    title: '停止设备',
+    content: `确定要停止设备「${deviceDetail.value.deviceName || deviceDetail.value.shortId}」吗?此操作将中断当前运行。`,
+    success: async (res) => {
+      if (res.confirm) {
+        try {
+          await stopDevice(deviceDetail.value.shortId)
+          showToast('设备已停止', 'success')
+          loadDeviceDetail()
+        } catch (error) {
+          showToast('停止设备失败')
+        }
+      }
+    }
+  })
 }
 
-// 启动设备(预留功能)
 const handleStartDevice = () => {
   showToast('启动设备功能开发中')
 }
 
-// 刷新设备信息
 const refreshDeviceInfo = () => {
   loadDeviceDetail()
 }
 
-// 返回上一页
 const goBack = () => {
   uni.navigateBack()
 }
 
-// 页面加载时获取设备ID并加载详情
 onMounted(() => {
-  // 获取页面参数
   const pages = getCurrentPages()
   const currentPage = pages[pages.length - 1]
   const receivedDeviceId = currentPage.options.id || ''
-  
-  console.log('设备详情页接收到的参数:', currentPage.options)
-  console.log('设备ID:', receivedDeviceId)
-  
   if (!receivedDeviceId) {
-    console.error('未接收到设备ID')
     showToast('设备ID不存在,无法加载详情')
     loading.value = false
     return
   }
-  
   deviceId.value = receivedDeviceId
   loadDeviceDetail()
 })
 </script>
 
 <style scoped>
-/* 容器 */
 .device-detail-container {
   min-height: 100vh;
-  background-color: #F8F9FA;
-  padding-bottom: 120rpx;
-}
-
-/* 顶部紫色导航栏 */
-.header-nav {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  padding-top: calc(24rpx + var(--status-bar-height));
-  padding-right: 30rpx;
-  padding-bottom: 24rpx;
-  padding-left: 30rpx;
-  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
-  box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.2);
-  position: relative;
-  z-index: 10;
-}
-
-.nav-left {
-  width: 180rpx;
-  display: flex;
-  align-items: center;
-}
-
-.back-icon {
-  font-size: 40rpx;
-  color: #FFFFFF;
-  font-weight: 600;
-}
-
-.nav-title {
-  font-size: 34rpx;
-  color: #FFFFFF;
-  font-weight: 600;
-  flex: 1;
-  text-align: center;
-  white-space: nowrap;
-}
-
-.nav-right {
-  width: 180rpx;
-}
-
-.danger-text {
-  color: #F5222D;
-  font-weight: 600;
+  background-color: #F5F7FA;
+  padding-bottom: 160rpx;
 }
 
-/* 设备基本信息卡片 */
+/* ===== Basic Info Card ===== */
 .basic-info-card {
-  margin: 20rpx 30rpx;
-  padding: 24rpx 30rpx;
+  margin: 20rpx 28rpx;
+  padding: 28rpx 28rpx;
   background: #FFFFFF;
-  border-radius: 20rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
+  border-radius: 24rpx;
 }
 
 .device-main-info {
@@ -348,66 +235,60 @@ onMounted(() => {
   font-size: 32rpx;
   font-weight: 600;
   color: #1A1A1A;
-  margin-bottom: 8rpx;
   display: block;
+  margin-bottom: 10rpx;
 }
 
 .device-id {
   font-size: 24rpx;
-  color: #667EEA;
-  font-weight: 600;
-  display: block;
+  color: #999999;
 }
 
-/* 设备状态 */
 .device-status {
   display: flex;
   align-items: center;
-  font-size: 24rpx;
+  font-size: 22rpx;
   font-weight: 600;
-  padding: 8rpx 24rpx;
-  border-radius: 30rpx;
+  padding: 8rpx 20rpx;
+  border-radius: 100px;
+  flex-shrink: 0;
 }
 
 .status-dot {
   display: inline-block;
-  width: 12rpx;
-  height: 12rpx;
+  width: 10rpx;
+  height: 10rpx;
   border-radius: 50%;
   margin-right: 8rpx;
   background-color: currentColor;
 }
 
-
-/* 详情信息卡片 */
-.detail-info-card,
-.status-card {
-  margin: 20rpx 30rpx;
+/* ===== Info Cards ===== */
+.info-card {
+  margin: 0 28rpx 20rpx;
   background: #FFFFFF;
-  border-radius: 20rpx;
+  border-radius: 24rpx;
   overflow: hidden;
-  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
 }
 
 .card-title {
-  padding: 24rpx 30rpx 20rpx;
+  padding: 24rpx 28rpx 20rpx;
   font-size: 28rpx;
   font-weight: 600;
   color: #1A1A1A;
-  border-bottom: 1rpx solid #F0F0F0;
+  border-bottom: 1px solid #F0F0F0;
 }
 
-/* 信息列表 */
 .info-list {
-  padding: 20rpx 30rpx 24rpx;
+  padding: 8rpx 28rpx 16rpx;
 }
 
 .info-item {
   display: flex;
   align-items: center;
   justify-content: space-between;
-  padding: 16rpx 0;
-  border-bottom: 1rpx solid #F8F9FA;
+  padding: 18rpx 0;
+  border-bottom: 1px solid #F5F7FA;
 }
 
 .info-item:last-child {
@@ -426,45 +307,21 @@ onMounted(() => {
   text-align: right;
 }
 
-/* 状态列表 */
-.status-list {
-  padding: 20rpx 30rpx 24rpx;
+.item-value-danger {
+  color: #F44336;
 }
 
-.status-item {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  padding: 16rpx 0;
-  border-bottom: 1rpx solid #F8F9FA;
-}
-
-.status-item:last-child {
-  border-bottom: none;
-}
-
-.status-label {
-  font-size: 26rpx;
-  color: #666666;
-}
-
-.status-value {
-  font-size: 26rpx;
-  color: #1A1A1A;
-  font-weight: 500;
-  text-align: right;
-}
-
-/* 操作按钮区域 */
+/* ===== Action Section ===== */
 .action-section {
   position: fixed;
   bottom: 0;
   left: 0;
   right: 0;
-  padding: 24rpx 30rpx;
+  padding: 20rpx 28rpx;
+  padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
   background: #FFFFFF;
-  border-top: 1rpx solid #F0F0F0;
-  box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.06);
+  border-top: 1px solid #F0F0F0;
+  box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.04);
   z-index: 100;
   display: flex;
   gap: 20rpx;
@@ -472,56 +329,63 @@ onMounted(() => {
 
 .stop-btn {
   flex: 1;
-  padding: 20rpx 0;
-  background: linear-gradient(90deg, #F5222D 0%, #FF4D4F 100%);
+  padding: 22rpx 0;
+  background: #C6171E;
   color: #FFFFFF;
   border: none;
   border-radius: 16rpx;
   font-size: 28rpx;
   font-weight: 600;
-  box-shadow: 0 6rpx 20rpx rgba(245, 34, 45, 0.35);
+  transition: background 0.25s, transform 0.15s;
+}
+
+.stop-btn:active {
+  background: #A81212;
+  transform: scale(0.97);
 }
 
 .start-btn {
   flex: 1;
-  padding: 20rpx 0;
-  background: linear-gradient(90deg, #52C41A 0%, #73D13D 100%);
+  padding: 22rpx 0;
+  background: #52C41A;
   color: #FFFFFF;
   border: none;
   border-radius: 16rpx;
   font-size: 28rpx;
   font-weight: 600;
-  box-shadow: 0 6rpx 20rpx rgba(82, 196, 26, 0.35);
+  transition: background 0.25s, transform 0.15s;
+}
+
+.start-btn:active {
+  opacity: 0.85;
+  transform: scale(0.97);
 }
 
 .refresh-btn {
   flex: 1;
-  padding: 20rpx 0;
-  background: linear-gradient(90deg, #667EEA 0%, #764BA2 100%);
-  color: #FFFFFF;
-  border: none;
+  padding: 22rpx 0;
+  background: #FFFFFF;
+  color: #1A1A1A;
+  border: 1px solid #E0E0E0;
   border-radius: 16rpx;
   font-size: 28rpx;
-  font-weight: 600;
-  box-shadow: 0 6rpx 20rpx rgba(102, 126, 234, 0.35);
+  font-weight: 500;
+  transition: border-color 0.25s, color 0.25s;
 }
 
-.stop-btn:active,
-.start-btn:active,
 .refresh-btn:active {
-  transform: translateY(2rpx);
-  opacity: 0.9;
+  border-color: #C6171E;
+  color: #C6171E;
 }
 
-/* 加载状态 */
+/* ===== Loading ===== */
 .loading-overlay {
   position: fixed;
   top: 0;
   left: 0;
   right: 0;
   bottom: 0;
-  background-color: rgba(0, 0, 0, 0.4);
-  backdrop-filter: blur(2px);
+  background: rgba(0, 0, 0, 0.4);
   display: flex;
   flex-direction: column;
   align-items: center;
@@ -530,40 +394,46 @@ onMounted(() => {
 }
 
 .loading-spinner {
-  font-size: 64rpx;
-  animation: spin 1s linear infinite;
-  margin-bottom: 32rpx;
-  color: #fff;
-}
-
-.loading-text {
-  font-size: 28rpx;
-  color: #fff;
+  width: 64rpx;
+  height: 64rpx;
+  border: 4rpx solid rgba(255, 255, 255, 0.3);
+  border-top-color: #FFFFFF;
+  border-radius: 50%;
+  animation: spin 0.8s linear infinite;
+  margin-bottom: 24rpx;
 }
 
 @keyframes spin {
-  from { transform: rotate(0deg); }
   to { transform: rotate(360deg); }
 }
 
-/* 空状态 */
+.loading-text {
+  font-size: 28rpx;
+  color: #FFFFFF;
+}
+
+/* ===== Empty ===== */
 .empty-state {
   display: flex;
   flex-direction: column;
   align-items: center;
   justify-content: center;
   padding: 120rpx 0;
-  color: #999999;
 }
 
-.empty-icon {
-  font-size: 120rpx;
-  margin-bottom: 32rpx;
-  opacity: 0.5;
+.empty-text {
+  font-size: 28rpx;
+  color: #999999;
+  margin: 24rpx 0 32rpx;
 }
 
-.empty-text {
+.empty-refresh-btn {
+  padding: 16rpx 48rpx;
+  background: #C6171E;
+  color: #FFFFFF;
+  border: none;
+  border-radius: 44rpx;
   font-size: 28rpx;
-  margin-bottom: 40rpx;
+  font-weight: 500;
 }
-</style>
+</style>

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 249 - 436
admin-mp/src/pages/device/list.vue


+ 85 - 40
admin-mp/src/pages/finance/index.vue

@@ -5,7 +5,7 @@
         <text class="stat-title">今日营收</text>
         <text class="stat-value">¥{{ formatAmount(todayRevenue || 0) }}</text>
         <view class="stat-change" :class="getChangeClass(todayRevenueChange)">
-          <text class="change-icon">{{ todayRevenueChange >= 0 ? '↑' : '↓' }}</text>
+          <AppIcon :name="todayRevenueChange >= 0 ? 'arrow-up' : 'arrow-down'" :size="22" :color="todayRevenueChange >= 0 ? '#52C41A' : '#F44336'" />
           <text>{{ Math.abs(todayRevenueChange) }}%</text>
         </view>
       </view>
@@ -13,22 +13,46 @@
         <text class="stat-title">本月营收</text>
         <text class="stat-value">¥{{ formatAmount(monthRevenue || 0) }}</text>
         <view class="stat-change" :class="getChangeClass(monthRevenueChange)">
-          <text class="change-icon">{{ monthRevenueChange >= 0 ? '↑' : '↓' }}</text>
+          <AppIcon :name="monthRevenueChange >= 0 ? 'arrow-up' : 'arrow-down'" :size="22" :color="monthRevenueChange >= 0 ? '#52C41A' : '#F44336'" />
           <text>{{ Math.abs(monthRevenueChange) }}%</text>
         </view>
       </view>
       <view class="stat-card">
         <text class="stat-title">待提现金额</text>
         <text class="stat-value">¥{{ formatAmount(pendingWithdraw || 0) }}</text>
-        <text class="stat-desc">站点待提现</text>
+        <text class="stat-footnote">站点待提现</text>
       </view>
       <view class="stat-card">
         <text class="stat-title">累计分账</text>
         <text class="stat-value">¥{{ formatAmount(totalSplitAmount || 0) }}</text>
-        <text class="stat-desc">平台分账总额</text>
+        <text class="stat-footnote">平台分账总额</text>
       </view>
     </view>
-    
+
+    <!-- 快捷功能入口 -->
+    <view class="finance-menu">
+      <view class="menu-item" @click="navigateTo('/pages/finance/withdraw')">
+        <AppIcon name="credit-card" :size="40" color="#C6171E" class="menu-icon" />
+        <text class="menu-title">提现管理</text>
+        <AppIcon name="chevron-right" :size="28" color="#B0B0B0" />
+      </view>
+      <view class="menu-item" @click="navigateTo('/pages/finance/refund')">
+        <AppIcon name="rotate-ccw" :size="40" color="#C6171E" class="menu-icon" />
+        <text class="menu-title">退款清单</text>
+        <AppIcon name="chevron-right" :size="28" color="#B0B0B0" />
+      </view>
+      <view class="menu-item" @click="navigateTo('/pages/finance/settlement')">
+        <AppIcon name="clipboard" :size="40" color="#C6171E" class="menu-icon" />
+        <text class="menu-title">结算记录</text>
+        <AppIcon name="chevron-right" :size="28" color="#B0B0B0" />
+      </view>
+      <view class="menu-item" @click="navigateTo('/pages/finance/split-record')">
+        <AppIcon name="trending-up" :size="40" color="#C6171E" class="menu-icon" />
+        <text class="menu-title">分账记录</text>
+        <AppIcon name="chevron-right" :size="28" color="#B0B0B0" />
+      </view>
+    </view>
+
     <view class="finance-tabs">
       <view class="segmented-control">
         <view 
@@ -121,7 +145,7 @@
     </view>
     
     <view class="empty-state" v-if="(stationAccounts.length === 0 && activeTab === 0) || (splitRecords.length === 0 && activeTab === 1)">
-      <text class="empty-icon">📭</text>
+      <view class="empty-icon-wrapper"><AppIcon name="inbox" :size="80" color="#B0B0B0" /></view>
       <text>暂无数据</text>
     </view>
     
@@ -129,11 +153,14 @@
     <view class="loading-overlay" v-if="loading">
       <view class="loading-spinner"></view>
     </view>
+
+    <custom-tab-bar />
   </view>
 </template>
 
 <script setup>
 import { ref, onMounted } from 'vue'
+import CustomTabBar from '../../custom-tab-bar/index.vue'
 import { getStationAccounts, getSplitRecords } from '../../api/finance.js'
 import { getDashboardData } from '../../api/stat.js'
 import { formatTime, showToast, formatAmount, storage, fmtDictName, getDictColor } from '../../utils/index.js'
@@ -175,8 +202,7 @@ const loadFinanceData = async () => {
       // 其他财务数据可能需要调用专门的API获取
     }
   } catch (error) {
-    console.error('加载财务数据失败:', error)
-    showToast('加载财务数据失败')
+        showToast('加载财务数据失败')
   } finally {
     loading.value = false
   }
@@ -186,15 +212,14 @@ const loadStationAccounts = async () => {
   loading.value = true
   try {
     const res = await getStationAccounts({ page: 1, pageSize: 10 })
-    console.log('站点账户API返回数据:', res)
+    
     if (res && res.code === 200) {
       const data = res.data
       // 适配不同的数据结构
       stationAccounts.value = data.records || data.list || data
     }
   } catch (error) {
-    console.error('获取站点账户失败:', error)
-    showToast('获取站点账户失败')
+        showToast('获取站点账户失败')
   } finally {
     loading.value = false
   }
@@ -204,15 +229,14 @@ const loadSplitRecords = async () => {
   loading.value = true
   try {
     const res = await getSplitRecords({ page: 1, pageSize: 10 })
-    console.log('分账记录API返回数据:', res)
+    
     if (res && res.code === 200) {
       const data = res.data
       // 适配不同的数据结构
       splitRecords.value = data.records || data.list || data
     }
   } catch (error) {
-    console.error('获取分账记录失败:', error)
-    showToast('获取分账记录失败')
+        showToast('获取分账记录失败')
   } finally {
     loading.value = false
   }
@@ -228,6 +252,10 @@ const navigateToWithdraw = (account) => {
   })
 }
 
+const navigateTo = (url) => {
+  uni.navigateTo({ url })
+}
+
 const getChangeClass = (change) => {
   return change >= 0 ? 'change-up' : 'change-down'
 }
@@ -257,7 +285,7 @@ const getSplitStatusStyle = (status) => {
 <style scoped>
 .finance-container {
   padding: 30rpx;
-  background-color: #F8F9FA;
+  background-color: #F5F7FA;
   min-height: 100vh;
   box-sizing: border-box;
   padding-bottom: 120rpx;
@@ -314,19 +342,46 @@ const getSplitStatusStyle = (status) => {
 }
 
 .change-down {
-  color: #F5222D;
+  color: #F44336;
 }
 
-.change-icon {
-  font-size: 20rpx;
-}
 
-.stat-desc {
+.stat-footnote {
   font-size: 22rpx;
-  color: #CCCCCC;
+  color: #B0B0B0;
   display: block;
 }
 
+/* 快捷功能入口 */
+.finance-menu {
+  background-color: #FFFFFF;
+  border-radius: 24rpx;
+  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
+  margin-bottom: 30rpx;
+  overflow: hidden;
+}
+.menu-item {
+  display: flex;
+  align-items: center;
+  padding: 28rpx 30rpx;
+  border-bottom: 1rpx solid #F0F0F0;
+}
+.menu-item:last-child {
+  border-bottom: none;
+}
+.menu-item:active {
+  background-color: #F5F7FA;
+}
+.menu-icon {
+  margin-right: 20rpx;
+}
+.menu-title {
+  flex: 1;
+  font-size: 28rpx;
+  color: #1A1A1A;
+  font-weight: 500;
+}
+
 /* 切换标签 */
 .finance-tabs {
   background-color: #FFFFFF;
@@ -360,8 +415,8 @@ const getSplitStatusStyle = (status) => {
 .segment-item.active {
   color: #FFFFFF;
   font-weight: 600;
-  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
-  box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3);
+  background: #C6171E;
+  box-shadow: 0 4rpx 12rpx rgba(198, 23, 30, 0.3);
 }
 
 /* 内容区域 */
@@ -383,11 +438,6 @@ const getSplitStatusStyle = (status) => {
   text-align: center;
 }
 
-.empty-icon {
-  font-size: 120rpx;
-  margin-bottom: 32rpx;
-  opacity: 0.5;
-}
 
 .empty-state text {
   margin-top: 16rpx;
@@ -403,7 +453,7 @@ const getSplitStatusStyle = (status) => {
   right: 0;
   bottom: 0;
   background-color: rgba(0, 0, 0, 0.4);
-  backdrop-filter: blur(2px);
+  
   display: flex;
   align-items: center;
   justify-content: center;
@@ -419,9 +469,6 @@ const getSplitStatusStyle = (status) => {
   animation: spin 0.8s linear infinite;
 }
 
-@keyframes spin {
-  to { transform: rotate(360deg); }
-}
 
 /* ===== 站点账户列表 ===== */
 .account-list {
@@ -493,8 +540,8 @@ const getSplitStatusStyle = (status) => {
   grid-template-columns: repeat(3, 1fr);
   gap: 20rpx;
   padding: 20rpx;
-  background: linear-gradient(135deg, #F5F5F5 0%, #E8E8E8 100%);
-  border-radius: 16rpx;
+  background: rgba(0, 0, 0, 0.02);
+  border-radius: 12rpx;
 }
 
 .stat-column {
@@ -520,7 +567,7 @@ const getSplitStatusStyle = (status) => {
 
 .withdraw-btn {
   padding: 20rpx 32rpx;
-  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
+  background: #C6171E;
   color: #FFFFFF;
   border: none;
   border-radius: 16rpx;
@@ -588,16 +635,14 @@ const getSplitStatusStyle = (status) => {
 .amount-value {
   font-size: 40rpx;
   font-weight: 700;
-  color: #E70316;
+  color: #C6171E;
 }
 
 .split-content {
   display: flex;
   flex-direction: column;
-  gap: 16rpx;
-  padding: 20rpx;
-  background-color: #F8F9FA;
-  border-radius: 12rpx;
+  gap: 12rpx;
+  padding-top: 16rpx;
 }
 
 .split-detail {

+ 382 - 0
admin-mp/src/pages/finance/refund.vue

@@ -0,0 +1,382 @@
+<template>
+  <view class="refund-container">
+    <!-- 搜索栏 -->
+    <view class="search-bar">
+      <view class="search-input-wrapper">
+        <view class="search-icon">
+          <AppIcon name="search" :size="28" color="#999999" />
+        </view>
+        <input type="text" placeholder="搜索手机号" v-model="searchKeyword" @confirm="handleSearch" />
+      </view>
+      <button class="search-btn" @click="handleSearch">搜索</button>
+    </view>
+
+    <!-- 状态筛选 -->
+    <view class="filter-bar">
+      <view
+        v-for="(option, index) in statusOptions"
+        :key="index"
+        class="filter-item"
+        :class="{ active: activeStatus === option.value }"
+        @click="handleStatusChange(option.value)">
+        <text>{{ option.label }}</text>
+      </view>
+    </view>
+
+    <!-- 退款列表 -->
+    <view class="refund-list" v-if="list.length > 0">
+      <view
+        class="refund-item"
+        v-for="(item, index) in list"
+        :key="index">
+        <view class="item-header">
+          <view class="item-left">
+            <text class="item-phone">{{ item.mobilePhone || '-' }}</text>
+          </view>
+          <text class="status-tag" :style="getStatusStyle(item.status)">{{ fmtDictName('RefundLog.status', item.status) }}</text>
+        </view>
+        <view class="item-content">
+          <view class="info-row">
+            <text class="info-label">商户订单号</text>
+            <text class="info-value">{{ item.outTradeNo || '-' }}</text>
+          </view>
+          <view class="info-row">
+            <text class="info-label">退款单号</text>
+            <text class="info-value">{{ item.outRefundNo || '-' }}</text>
+          </view>
+          <view class="info-row">
+            <text class="info-label">原充值金额</text>
+            <text class="info-value amount">{{ formatAmount(item.total) }}</text>
+          </view>
+          <view class="info-row">
+            <text class="info-label">退款金额</text>
+            <text class="info-value amount-refund">{{ formatAmount(item.refund) }}</text>
+          </view>
+          <view class="info-row">
+            <text class="info-label">退款原因</text>
+            <text class="info-value">{{ item.reason || '-' }}</text>
+          </view>
+          <view class="info-row">
+            <text class="info-label">退款人</text>
+            <text class="info-value">{{ item.adminUsername || '-' }}</text>
+          </view>
+          <view class="info-row">
+            <text class="info-label">申请时间</text>
+            <text class="info-value">{{ formatTime(item.createTime) }}</text>
+          </view>
+          <view class="info-row" v-if="item.successTime">
+            <text class="info-label">退款成功时间</text>
+            <text class="info-value">{{ formatTime(item.successTime) }}</text>
+          </view>
+        </view>
+        <view class="item-footer" v-if="isRefundableStatus(item.status)">
+          <button class="refund-btn" @click="handleProcessRefund(item)">退款处理</button>
+        </view>
+      </view>
+    </view>
+
+    <!-- 加载更多 -->
+    <view class="load-more" v-if="list.length > 0">
+      <text class="load-more-text" v-if="loadMoreStatus === 'more'" @click="loadMore">上拉加载更多</text>
+      <text class="load-more-text" v-else-if="loadMoreStatus === 'loading'">正在加载...</text>
+      <text class="load-more-text" v-else>没有更多数据了</text>
+    </view>
+
+    <!-- 空状态 -->
+    <view class="empty-state" v-if="list.length === 0 && !loading">
+      <view class="empty-icon-wrapper">
+        <AppIcon name="inbox" :size="80" color="#999999" />
+      </view>
+      <text class="empty-text">暂无退款记录</text>
+    </view>
+
+    <!-- 加载状态 -->
+    <view class="loading-state" v-if="loading && list.length === 0">
+      <view class="loading-spinner"></view>
+      <text class="loading-text">加载中...</text>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, onMounted, computed } from 'vue'
+import { getRefundLogs, processRefund } from '../../api/finance.js'
+import { formatTime, showToast, formatAmount, fmtDictName, getDictColor } from '../../utils/index.js'
+import dictUtil from '../../utils/dict.js'
+
+const list = ref([])
+const loading = ref(true)
+const page = ref(1)
+const pageSize = ref(10)
+const hasMore = ref(true)
+const loadMoreStatus = ref('more')
+const searchKeyword = ref('')
+const activeStatus = ref('')
+
+const statusOptions = computed(() => dictUtil.getDictFilterOptions('RefundLog.status'))
+
+const getStatusStyle = (status) => {
+  const color = getDictColor('RefundLog.status', status)
+  if (color) return { color, backgroundColor: `${color}1A` }
+  return {}
+}
+
+const isRefundableStatus = (status) => {
+  return status == dictUtil.getDictValue('RefundLog.status', '待退款')
+}
+
+const loadData = async (isLoadMore = false) => {
+  if (!isLoadMore) {
+    page.value = 1
+    list.value = []
+    hasMore.value = true
+    loadMoreStatus.value = 'more'
+  } else {
+    loadMoreStatus.value = 'loading'
+  }
+
+  loading.value = true
+  try {
+    const params = {
+      pageNum: page.value,
+      pageSize: pageSize.value
+    }
+    if (searchKeyword.value) params.mobilePhone = searchKeyword.value
+    if (activeStatus.value) params.status = activeStatus.value
+
+    const res = await getRefundLogs(params)
+    if (res && res.code === 200) {
+      const data = res.data
+      const records = data.records || data.list || data
+      const total = data.total || 0
+
+      const totalPages = Math.ceil(total / pageSize.value)
+      hasMore.value = page.value < totalPages
+      loadMoreStatus.value = hasMore.value ? 'more' : 'noMore'
+
+      if (isLoadMore) {
+        list.value = [...list.value, ...records]
+        page.value++
+      } else {
+        list.value = records
+        page.value = 2
+      }
+    }
+  } catch (error) {
+    showToast('加载退款记录失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+const loadMore = () => {
+  if (hasMore.value && !loading.value) {
+    loadData(true)
+  }
+}
+
+const handleSearch = () => {
+  loadData()
+}
+
+const handleStatusChange = (status) => {
+  activeStatus.value = status
+  loadData()
+}
+
+const handleProcessRefund = (item) => {
+  uni.showModal({
+    title: '确认退款',
+    content: `确定对该笔退款(${item.outRefundNo || item.refundLogId})进行退款处理吗?`,
+    success: async (res) => {
+      if (res.confirm) {
+        try {
+          const result = await processRefund(item.refundLogId || item.id)
+          if (result && result.code === 200) {
+            showToast('退款处理成功', 'success')
+            loadData()
+          } else {
+            showToast(result?.msg || '退款处理失败')
+          }
+        } catch (error) {
+          showToast('退款处理失败')
+        }
+      }
+    }
+  })
+}
+
+onMounted(() => {
+  loadData()
+})
+</script>
+
+<style scoped>
+.refund-container {
+  min-height: 100vh;
+  background-color: #F5F7FA;
+  padding-bottom: 60rpx;
+}
+
+.search-bar {
+  display: flex;
+  align-items: center;
+  padding: 16rpx 20rpx;
+  background-color: #FFFFFF;
+}
+.search-input-wrapper {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  background-color: #F5F5F5;
+  border-radius: 16rpx;
+  padding: 0 16rpx;
+}
+.search-icon {
+  margin-right: 8rpx;
+}
+.search-input-wrapper input {
+  flex: 1;
+  height: 60rpx;
+  font-size: 28rpx;
+}
+.search-btn {
+  margin-left: 16rpx;
+  padding: 10rpx 28rpx;
+  background: #C6171E;
+  color: #FFFFFF;
+  border-radius: 16rpx;
+  font-size: 26rpx;
+  border: none;
+}
+
+.search-btn:active {
+  background: #A81212;
+  transform: scale(0.97);
+}
+
+.filter-bar {
+  display: flex;
+  padding: 16rpx 20rpx;
+  background-color: #FFFFFF;
+  gap: 16rpx;
+  flex-wrap: wrap;
+}
+.filter-item {
+  padding: 8rpx 24rpx;
+  background-color: #F5F5F5;
+  border-radius: 100rpx;
+  font-size: 24rpx;
+  color: #666666;
+}
+.filter-item.active {
+  background: #C6171E;
+  color: #FFFFFF;
+}
+
+.refund-list {
+  padding: 20rpx;
+}
+.refund-item {
+  background-color: #FFFFFF;
+  border-radius: 24rpx;
+  padding: 24rpx;
+  margin-bottom: 16rpx;
+  box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06);
+}
+.item-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding-bottom: 16rpx;
+  border-bottom: 1rpx solid #F0F0F0;
+  margin-bottom: 16rpx;
+}
+.item-phone {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #1A1A1A;
+}
+.status-tag {
+  font-size: 22rpx;
+  padding: 6rpx 24rpx;
+  border-radius: 100rpx;
+  font-weight: 500;
+}
+
+.info-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 10rpx 0;
+}
+.info-label {
+  font-size: 26rpx;
+  color: #999999;
+}
+.info-value {
+  font-size: 26rpx;
+  color: #1A1A1A;
+  max-width: 400rpx;
+  text-align: right;
+  word-break: break-all;
+}
+.info-value.amount {
+  color: #C6171E;
+  font-weight: 500;
+}
+.info-value.amount-refund {
+  color: #52C41A;
+  font-weight: 500;
+}
+
+.item-footer {
+  display: flex;
+  justify-content: flex-end;
+  padding-top: 16rpx;
+  border-top: 1rpx solid #F0F0F0;
+  margin-top: 16rpx;
+}
+.refund-btn {
+  padding: 12rpx 32rpx;
+  background: #C6171E;
+  color: #FFFFFF;
+  border-radius: 16rpx;
+  font-size: 26rpx;
+  border: none;
+}
+
+.load-more {
+  display: flex;
+  justify-content: center;
+  padding: 24rpx 0;
+}
+.load-more-text {
+  font-size: 24rpx;
+  color: #999999;
+}
+
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 80rpx 0;
+}
+.empty-text {
+  font-size: 28rpx;
+  color: #999999;
+  margin-top: 20rpx;
+}
+
+.loading-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 80rpx 0;
+}
+.loading-text {
+  font-size: 28rpx;
+  color: #999999;
+  margin-top: 16rpx;
+}
+</style>

+ 536 - 0
admin-mp/src/pages/finance/settlement.vue

@@ -0,0 +1,536 @@
+<template>
+  <view class="settlement-container">
+    <!-- 筛选栏 -->
+    <view class="filter-section">
+      <view class="filter-row">
+        <view class="filter-item">
+          <text class="filter-label">站点</text>
+          <picker mode="selector" :range="stationList" range-key="stationName" @change="handleStationChange">
+            <view class="picker-value">
+              <text :class="{ placeholder: !selectedStationName }">{{ selectedStationName || '全部站点' }}</text>
+              <AppIcon name="chevron-down" :size="20" color="#999999" class="picker-arrow" />
+            </view>
+          </picker>
+        </view>
+        <view class="filter-item">
+          <text class="filter-label">结算周期</text>
+          <input type="text" placeholder="如 2026-05" v-model="settlementPeriod" @confirm="handleSearch" />
+        </view>
+      </view>
+      <view class="filter-row">
+        <view class="filter-segments">
+          <view
+            v-for="(option, index) in statusOptions"
+            :key="index"
+            class="segment-item"
+            :class="{ active: activeStatus === option.value }"
+            @click="handleStatusChange(option.value)">
+            <text>{{ option.label }}</text>
+          </view>
+        </view>
+      </view>
+      <button class="search-btn" @click="handleSearch">查询</button>
+    </view>
+
+    <!-- 结算列表 -->
+    <view class="settlement-list" v-if="list.length > 0">
+      <view
+        class="settlement-item"
+        v-for="(item, index) in list"
+        :key="index"
+        @click="viewDetail(item)">
+        <view class="item-header">
+          <view class="item-left">
+            <text class="item-station">{{ item.stationName || '-' }}</text>
+            <text class="item-period">{{ item.settlementPeriod || '-' }}</text>
+          </view>
+          <text class="status-tag" :style="getStatusStyle(item.status)">{{ fmtDictName('Settlement.status', item.status) }}</text>
+        </view>
+        <view class="item-content">
+          <view class="info-grid">
+            <view class="info-item">
+              <text class="info-label">期初余额</text>
+              <text class="info-value">{{ formatAmount(item.openingPendingBalance) }}</text>
+            </view>
+            <view class="info-item">
+              <text class="info-label">结算金额</text>
+              <text class="info-value highlight">{{ formatAmount(item.settlementAmount) }}</text>
+            </view>
+            <view class="info-item">
+              <text class="info-label">总充值</text>
+              <text class="info-value">{{ formatAmount(item.totalRecharge) }}</text>
+            </view>
+            <view class="info-item">
+              <text class="info-label">总退款</text>
+              <text class="info-value">{{ formatAmount(item.totalRefund) }}</text>
+            </view>
+          </view>
+          <view class="item-time">
+            <text class="time-text">创建: {{ formatTime(item.createTime) }}</text>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 加载更多 -->
+    <view class="load-more" v-if="list.length > 0">
+      <text class="load-more-text" v-if="loadMoreStatus === 'more'" @click="loadMore">上拉加载更多</text>
+      <text class="load-more-text" v-else-if="loadMoreStatus === 'loading'">正在加载...</text>
+      <text class="load-more-text" v-else>没有更多数据了</text>
+    </view>
+
+    <!-- 空状态 -->
+    <view class="empty-state" v-if="list.length === 0 && !loading">
+      <view class="empty-icon-wrapper">
+        <AppIcon name="bar-chart" :size="80" color="#999999" />
+      </view>
+      <text class="empty-text">暂无结算记录</text>
+    </view>
+
+    <!-- 加载状态 -->
+    <view class="loading-state" v-if="loading && list.length === 0">
+      <view class="loading-spinner"></view>
+      <text class="loading-text">加载中...</text>
+    </view>
+
+    <!-- 详情弹窗 -->
+    <view class="detail-overlay" v-if="showDetail" @click="closeDetail">
+      <view class="detail-card" @click.stop>
+        <view class="detail-header">
+          <text class="detail-title">结算详情</text>
+          <view class="detail-close" @click="closeDetail">
+            <AppIcon name="x" :size="32" color="#999999" />
+          </view>
+        </view>
+        <view class="detail-content" v-if="detailItem">
+          <view class="detail-row">
+            <text class="d-label">站点名称</text>
+            <text class="d-value">{{ detailItem.stationName || '-' }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="d-label">结算周期</text>
+            <text class="d-value">{{ detailItem.settlementPeriod || '-' }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="d-label">期初余额</text>
+            <text class="d-value amount">{{ formatAmount(detailItem.openingPendingBalance) }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="d-label">总充值</text>
+            <text class="d-value">{{ formatAmount(detailItem.totalRecharge) }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="d-label">总退款</text>
+            <text class="d-value">{{ formatAmount(detailItem.totalRefund) }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="d-label">跨店收入</text>
+            <text class="d-value">{{ formatAmount(detailItem.totalCrossIncome) }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="d-label">跨店支出</text>
+            <text class="d-value">{{ formatAmount(detailItem.totalCrossExpend) }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="d-label">平台费基数</text>
+            <text class="d-value">{{ formatAmount(detailItem.platformFeeBase) }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="d-label">平台费</text>
+            <text class="d-value">{{ formatAmount(detailItem.platformFee) }}</text>
+          </view>
+          <view class="detail-row highlight-row">
+            <text class="d-label">结算金额</text>
+            <text class="d-value settlement-amount">{{ formatAmount(detailItem.settlementAmount) }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="d-label">期末余额</text>
+            <text class="d-value">{{ formatAmount(detailItem.closingPendingBalance) }}</text>
+          </view>
+          <view class="detail-row">
+            <text class="d-label">状态</text>
+            <text class="d-value" :style="getStatusStyle(detailItem.status)">{{ fmtDictName('Settlement.status', detailItem.status) }}</text>
+          </view>
+          <view class="detail-row" v-if="detailItem.remark">
+            <text class="d-label">备注</text>
+            <text class="d-value">{{ detailItem.remark }}</text>
+          </view>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, onMounted, computed } from 'vue'
+import { getSettlementRecords } from '../../api/finance.js'
+import { getStationList } from '../../api/station.js'
+import { formatTime, showToast, formatAmount, fmtDictName, getDictColor } from '../../utils/index.js'
+import dictUtil from '../../utils/dict.js'
+
+const list = ref([])
+const loading = ref(true)
+const page = ref(1)
+const pageSize = ref(10)
+const hasMore = ref(true)
+const loadMoreStatus = ref('more')
+const activeStatus = ref('')
+const settlementPeriod = ref('')
+const stationList = ref([])
+const selectedStationId = ref('')
+const selectedStationName = ref('')
+const showDetail = ref(false)
+const detailItem = ref(null)
+
+const statusOptions = computed(() => dictUtil.getDictFilterOptions('Settlement.status'))
+
+const getStatusStyle = (status) => {
+  const color = getDictColor('Settlement.status', status)
+  if (color) return { color, backgroundColor: `${color}1A` }
+  return {}
+}
+
+const loadStations = async () => {
+  try {
+    const res = await getStationList({ pageSize: 1024 })
+    if (res && res.code === 200) {
+      const data = res.data
+      stationList.value = data.records || data.list || data || []
+      stationList.value.unshift({ stationId: '', stationName: '全部站点' })
+    }
+  } catch (error) {
+    showToast('加载站点列表失败')
+  }
+}
+
+const handleStationChange = (e) => {
+  const index = e.detail.value
+  const selected = stationList.value[index]
+  if (selected) {
+    selectedStationId.value = selected.stationId || ''
+    selectedStationName.value = selected.stationName || ''
+  }
+}
+
+const loadData = async (isLoadMore = false) => {
+  if (!isLoadMore) {
+    page.value = 1
+    list.value = []
+    hasMore.value = true
+    loadMoreStatus.value = 'more'
+  } else {
+    loadMoreStatus.value = 'loading'
+  }
+
+  loading.value = true
+  try {
+    const params = {
+      pageNum: page.value,
+      pageSize: pageSize.value
+    }
+    if (selectedStationId.value) params.stationId = selectedStationId.value
+    if (settlementPeriod.value) params.settlementPeriod = settlementPeriod.value
+    if (activeStatus.value !== '') params.status = activeStatus.value
+
+    const res = await getSettlementRecords(params)
+    if (res && res.code === 200) {
+      const data = res.data
+      const records = data.records || data.list || data
+      const total = data.total || 0
+
+      const totalPages = Math.ceil(total / pageSize.value)
+      hasMore.value = page.value < totalPages
+      loadMoreStatus.value = hasMore.value ? 'more' : 'noMore'
+
+      if (isLoadMore) {
+        list.value = [...list.value, ...records]
+        page.value++
+      } else {
+        list.value = records
+        page.value = 2
+      }
+    }
+  } catch (error) {
+    showToast('加载结算记录失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+const loadMore = () => {
+  if (hasMore.value && !loading.value) {
+    loadData(true)
+  }
+}
+
+const handleSearch = () => {
+  loadData()
+}
+
+const handleStatusChange = (status) => {
+  activeStatus.value = status
+  loadData()
+}
+
+const viewDetail = (item) => {
+  detailItem.value = item
+  showDetail.value = true
+}
+
+const closeDetail = () => {
+  showDetail.value = false
+  detailItem.value = null
+}
+
+onMounted(() => {
+  loadStations()
+  loadData()
+})
+</script>
+
+<style scoped>
+.settlement-container {
+  min-height: 100vh;
+  background-color: #F5F7FA;
+  padding-bottom: 60rpx;
+}
+
+.filter-section {
+  background-color: #FFFFFF;
+  padding: 24rpx 20rpx 20rpx;
+  margin-bottom: 16rpx;
+}
+.filter-row {
+  display: flex;
+  gap: 16rpx;
+  margin-bottom: 12rpx;
+}
+.filter-item {
+  flex: 1;
+}
+.filter-label {
+  font-size: 24rpx;
+  color: #999999;
+  margin-bottom: 8rpx;
+  display: block;
+}
+.picker-value {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  background-color: #F5F5F5;
+  border-radius: 16rpx;
+  padding: 14rpx 20rpx;
+  font-size: 26rpx;
+}
+.picker-value .placeholder {
+  color: #B0B0B0;
+}
+.picker-arrow {
+  flex-shrink: 0;
+}
+.filter-item input {
+  background-color: #F5F5F5;
+  border-radius: 16rpx;
+  padding: 14rpx 20rpx;
+  font-size: 26rpx;
+  height: 56rpx;
+}
+.filter-segments {
+  display: flex;
+  gap: 12rpx;
+  flex-wrap: wrap;
+}
+.segment-item {
+  padding: 8rpx 24rpx;
+  background-color: #F5F5F5;
+  border-radius: 100rpx;
+  font-size: 24rpx;
+  color: #666666;
+}
+.segment-item.active {
+  background: #C6171E;
+  color: #FFFFFF;
+}
+.search-btn {
+  width: 100%;
+  padding: 16rpx 0;
+  background: #C6171E;
+  color: #FFFFFF;
+  border-radius: 16rpx;
+  font-size: 28rpx;
+  border: none;
+}
+
+.settlement-list {
+  padding: 0 20rpx;
+}
+.settlement-item {
+  background-color: #FFFFFF;
+  border-radius: 24rpx;
+  padding: 24rpx;
+  margin-bottom: 16rpx;
+  box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06);
+}
+.settlement-item:active {
+  transform: translateY(-4rpx);
+  box-shadow: 0 2px 6px rgba(0,0,0,0.1), 0 2px 4px rgba(0,0,0,0.08);
+}
+.item-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  padding-bottom: 16rpx;
+  border-bottom: 1rpx solid #F0F0F0;
+  margin-bottom: 16rpx;
+}
+.item-station {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #1A1A1A;
+  display: block;
+}
+.item-period {
+  font-size: 24rpx;
+  color: #999999;
+  margin-top: 4rpx;
+  display: block;
+}
+.status-tag {
+  font-size: 22rpx;
+  padding: 6rpx 24rpx;
+  border-radius: 100rpx;
+  font-weight: 500;
+}
+.info-grid {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 12rpx;
+}
+.info-item {
+  padding: 8rpx 0;
+}
+.info-label {
+  font-size: 24rpx;
+  color: #999999;
+  display: block;
+  margin-bottom: 4rpx;
+}
+.info-value {
+  font-size: 26rpx;
+  color: #1A1A1A;
+  font-weight: 500;
+}
+.info-value.highlight {
+  color: #C6171E;
+  font-size: 28rpx;
+}
+.item-time {
+  margin-top: 12rpx;
+  padding-top: 12rpx;
+  border-top: 1rpx solid #F0F0F0;
+}
+.time-text {
+  font-size: 24rpx;
+  color: #999999;
+}
+
+.load-more {
+  display: flex;
+  justify-content: center;
+  padding: 24rpx 0;
+}
+.load-more-text {
+  font-size: 24rpx;
+  color: #999999;
+}
+
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 80rpx 0;
+}
+.empty-text {
+  font-size: 28rpx;
+  color: #999999;
+  margin-top: 20rpx;
+}
+
+.loading-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 80rpx 0;
+}
+.loading-text {
+  font-size: 28rpx;
+  color: #999999;
+  margin-top: 16rpx;
+}
+
+/* 详情弹窗 */
+.detail-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: flex-end;
+  z-index: 1000;
+}
+.detail-card {
+  width: 100%;
+  max-height: 80vh;
+  background-color: #FFFFFF;
+  border-radius: 32rpx 32rpx 0 0;
+  padding: 30rpx;
+  overflow-y: auto;
+}
+.detail-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 30rpx;
+}
+.detail-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #1A1A1A;
+}
+.detail-close {
+  padding: 10rpx;
+}
+.detail-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16rpx 0;
+  border-bottom: 1rpx solid #F5F5F5;
+}
+.detail-row.highlight-row {
+  background-color: rgba(198, 23, 30, 0.04);
+  padding: 16rpx 20rpx;
+  border-radius: 16rpx;
+  margin: 8rpx 0;
+}
+.d-label {
+  font-size: 26rpx;
+  color: #999999;
+}
+.d-value {
+  font-size: 26rpx;
+  color: #1A1A1A;
+  font-weight: 500;
+}
+.d-value.amount {
+  color: #C6171E;
+}
+.d-value.settlement-amount {
+  color: #C6171E;
+  font-size: 28rpx;
+  font-weight: 700;
+}
+</style>

+ 345 - 0
admin-mp/src/pages/finance/split-record.vue

@@ -0,0 +1,345 @@
+<template>
+  <view class="split-container">
+    <!-- 筛选栏 -->
+    <view class="filter-section">
+      <view class="filter-row">
+        <view class="filter-item">
+          <text class="filter-label">站点</text>
+          <picker mode="selector" :range="stationList" range-key="stationName" @change="handleStationChange">
+            <view class="picker-value">
+              <text :class="{ placeholder: !selectedStationName }">{{ selectedStationName || '全部站点' }}</text>
+              <AppIcon name="chevron-down" :size="20" color="#999999" class="picker-arrow" />
+            </view>
+          </picker>
+        </view>
+        <view class="filter-item">
+          <text class="filter-label">交易类型</text>
+          <picker mode="selector" :range="typeOptions" range-key="label" @change="handleTypeChange">
+            <view class="picker-value">
+              <text :class="{ placeholder: !activeTypeLabel }">{{ activeTypeLabel || '全部类型' }}</text>
+              <AppIcon name="chevron-down" :size="20" color="#999999" class="picker-arrow" />
+            </view>
+          </picker>
+        </view>
+      </view>
+      <view class="filter-row">
+        <view class="filter-item full-width">
+          <text class="filter-label">交易流水号</text>
+          <input type="text" placeholder="输入流水号搜索" v-model="tradeNo" @confirm="handleSearch" />
+        </view>
+      </view>
+      <button class="search-btn" @click="handleSearch">查询</button>
+    </view>
+
+    <!-- 分账列表 -->
+    <view class="split-list" v-if="list.length > 0">
+      <view class="split-item" v-for="(item, index) in list" :key="index">
+        <view class="item-header">
+          <text class="item-type" :style="getTypeStyle(item.type)">{{ fmtDictName('SplitRecord.type', item.type) }}</text>
+          <text class="item-amount">{{ formatAmount(item.amount) }}</text>
+        </view>
+        <view class="item-content">
+          <view class="info-row">
+            <text class="info-label">入账站点</text>
+            <text class="info-value">{{ item.toStationName || item.toStationId || '-' }}</text>
+          </view>
+          <view class="info-row">
+            <text class="info-label">出账站点</text>
+            <text class="info-value">{{ item.fromStationName || item.fromStationId || '-' }}</text>
+          </view>
+          <view class="info-row">
+            <text class="info-label">流水号</text>
+            <text class="info-value trade-no">{{ item.tradeNo || '-' }}</text>
+          </view>
+          <view class="info-row">
+            <text class="info-label">创建时间</text>
+            <text class="info-value">{{ formatTime(item.createTime) }}</text>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 加载更多 -->
+    <view class="load-more" v-if="list.length > 0">
+      <text class="load-more-text" v-if="loadMoreStatus === 'more'" @click="loadMore">上拉加载更多</text>
+      <text class="load-more-text" v-else-if="loadMoreStatus === 'loading'">正在加载...</text>
+      <text class="load-more-text" v-else>没有更多数据了</text>
+    </view>
+
+    <!-- 空状态 -->
+    <view class="empty-state" v-if="list.length === 0 && !loading">
+      <view class="empty-icon-wrapper">
+        <AppIcon name="trending-up" :size="80" color="#999999" />
+      </view>
+      <text class="empty-text">暂无分账记录</text>
+    </view>
+
+    <!-- 加载状态 -->
+    <view class="loading-state" v-if="loading && list.length === 0">
+      <view class="loading-spinner"></view>
+      <text class="loading-text">加载中...</text>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { getSplitRecords } from '../../api/finance.js'
+import { getStationList } from '../../api/station.js'
+import { formatTime, formatAmount, fmtDictName, getDictColor } from '../../utils/index.js'
+
+const list = ref([])
+const loading = ref(true)
+const page = ref(1)
+const pageSize = ref(10)
+const hasMore = ref(true)
+const loadMoreStatus = ref('more')
+const tradeNo = ref('')
+const stationList = ref([])
+const selectedStationId = ref('')
+const selectedStationName = ref('')
+const activeType = ref('')
+const activeTypeLabel = ref('')
+
+const typeOptions = [
+  { label: '全部类型', value: '' }
+]
+
+const getTypeStyle = (type) => {
+  const color = getDictColor('SplitRecord.type', type)
+  if (color) return { color, backgroundColor: `${color}1A` }
+  return {}
+}
+
+const loadStations = async () => {
+  try {
+    const res = await getStationList({ pageSize: 1024 })
+    if (res && res.code === 200) {
+      const data = res.data
+      stationList.value = data.records || data.list || data || []
+      stationList.value.unshift({ stationId: '', stationName: '全部站点' })
+    }
+  } catch (error) {
+    showToast('加载站点列表失败')
+  }
+}
+
+const handleStationChange = (e) => {
+  const index = e.detail.value
+  const selected = stationList.value[index]
+  if (selected) {
+    selectedStationId.value = selected.stationId || ''
+    selectedStationName.value = selected.stationName || ''
+  }
+}
+
+const handleTypeChange = (e) => {
+  const index = e.detail.value
+  const selected = typeOptions[index]
+  if (selected) {
+    activeType.value = selected.value
+    activeTypeLabel.value = selected.label
+  }
+}
+
+const loadData = async (isLoadMore = false) => {
+  if (!isLoadMore) {
+    page.value = 1
+    list.value = []
+    hasMore.value = true
+    loadMoreStatus.value = 'more'
+  } else {
+    loadMoreStatus.value = 'loading'
+  }
+
+  loading.value = true
+  try {
+    const params = {
+      pageNum: page.value,
+      pageSize: pageSize.value
+    }
+    if (selectedStationId.value) params.stationId = selectedStationId.value
+    if (tradeNo.value) params.tradeNo = tradeNo.value
+    if (activeType.value) params.type = activeType.value
+
+    const res = await getSplitRecords(params)
+    if (res && res.code === 200) {
+      const data = res.data
+      const records = data.records || data.list || data
+      const total = data.total || 0
+
+      const totalPages = Math.ceil(total / pageSize.value)
+      hasMore.value = page.value < totalPages
+      loadMoreStatus.value = hasMore.value ? 'more' : 'noMore'
+
+      if (isLoadMore) {
+        list.value = [...list.value, ...records]
+        page.value++
+      } else {
+        list.value = records
+        page.value = 2
+      }
+
+      // 从数据中提取交易类型,补充到 typeOptions
+      if (!isLoadMore && records.length > 0) {
+        const types = new Set()
+        records.forEach(r => { if (r.type) types.add(r.type) })
+        if (typeOptions.length === 1) {
+          types.forEach(t => typeOptions.push({ label: fmtDictName('SplitRecord.type', t) || t, value: t }))
+        }
+      }
+    }
+  } catch (error) {
+    showToast('加载分账记录失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+const loadMore = () => {
+  if (hasMore.value && !loading.value) {
+    loadData(true)
+  }
+}
+
+const handleSearch = () => {
+  loadData()
+}
+
+onMounted(() => {
+  loadStations()
+  loadData()
+})
+</script>
+
+<style scoped>
+.split-container {
+  min-height: 100vh;
+  background-color: #F5F7FA;
+  padding-bottom: 60rpx;
+}
+
+.filter-section {
+  background-color: #FFFFFF;
+  padding: 24rpx 20rpx 20rpx;
+  margin-bottom: 16rpx;
+}
+.filter-row {
+  display: flex;
+  gap: 16rpx;
+  margin-bottom: 12rpx;
+}
+.filter-item {
+  flex: 1;
+}
+.filter-item.full-width {
+  flex: none;
+  width: 100%;
+}
+.filter-label {
+  font-size: 24rpx;
+  color: #999999;
+  margin-bottom: 8rpx;
+  display: block;
+}
+.picker-value {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  background-color: #F5F5F5;
+  border-radius: 16rpx;
+  padding: 14rpx 20rpx;
+  font-size: 26rpx;
+}
+.picker-value .placeholder { color: #B0B0B0; }
+.picker-arrow { flex-shrink: 0; }
+.filter-item input {
+  background-color: #F5F5F5;
+  border-radius: 16rpx;
+  padding: 14rpx 20rpx;
+  font-size: 26rpx;
+  height: 56rpx;
+}
+.search-btn {
+  width: 100%;
+  padding: 16rpx 0;
+  background: #C6171E;
+  color: #FFFFFF;
+  border-radius: 16rpx;
+  font-size: 28rpx;
+  border: none;
+}
+
+.split-list {
+  padding: 0 20rpx;
+}
+.split-item {
+  background-color: #FFFFFF;
+  border-radius: 24rpx;
+  padding: 24rpx;
+  margin-bottom: 16rpx;
+  box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06);
+}
+.item-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding-bottom: 16rpx;
+  border-bottom: 1rpx solid #F0F0F0;
+  margin-bottom: 16rpx;
+}
+.item-type {
+  font-size: 24rpx;
+  padding: 6rpx 24rpx;
+  border-radius: 100rpx;
+  font-weight: 500;
+}
+.item-amount {
+  font-size: 32rpx;
+  font-weight: 700;
+  color: #C6171E;
+}
+.info-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 10rpx 0;
+}
+.info-label {
+  font-size: 26rpx;
+  color: #999999;
+}
+.info-value {
+  font-size: 26rpx;
+  color: #1A1A1A;
+  max-width: 380rpx;
+  text-align: right;
+}
+.info-value.trade-no {
+  font-size: 22rpx;
+  word-break: break-all;
+}
+
+.load-more {
+  display: flex;
+  justify-content: center;
+  padding: 24rpx 0;
+}
+.load-more-text { font-size: 24rpx; color: #999999; }
+
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 80rpx 0;
+}
+.empty-text { font-size: 28rpx; color: #999999; margin-top: 20rpx; }
+
+.loading-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 80rpx 0;
+}
+.loading-text { font-size: 28rpx; color: #999999; margin-top: 16rpx; }
+</style>

+ 121 - 173
admin-mp/src/pages/finance/withdraw.vue

@@ -1,25 +1,20 @@
 <template>
   <view class="withdraw-container">
-    <!-- 顶部卡片 -->
-    <view class="top-card">
-      <text class="page-title">{{ stationName }} - 提现管理</text>
-    </view>
-    
-    <!-- 账户信息卡片 -->
+    <!-- 账户概览 -->
     <view class="section">
       <view class="section-title">账户信息</view>
       <view class="info-cards">
-        <view class="info-card blue">
+        <view class="info-card">
           <text class="info-label">可提现余额</text>
-          <text class="info-value">¥{{ formatAmount(availableBalance || 0) }}</text>
+          <text class="info-value accent-blue">¥{{ formatAmount(availableBalance || 0) }}</text>
         </view>
-        <view class="info-card pink">
+        <view class="info-card">
           <text class="info-label">待审核金额</text>
-          <text class="info-value">¥{{ formatAmount(pendingAmount || 0) }}</text>
+          <text class="info-value accent-warning">¥{{ formatAmount(pendingAmount || 0) }}</text>
         </view>
-        <view class="info-card green">
+        <view class="info-card">
           <text class="info-label">累计提现</text>
-          <text class="info-value">¥{{ formatAmount(totalWithdrawn || 0) }}</text>
+          <text class="info-value accent-success">¥{{ formatAmount(totalWithdrawn || 0) }}</text>
         </view>
       </view>
     </view>
@@ -81,7 +76,7 @@
             :class="{ 'active': activeFilter === index }"
             @click="activeFilter = index; handleFilterChange(index)"
           >
-            <text>{{ option }}</text>
+            <text>{{ option.label }}</text>
           </view>
         </view>
       </view>
@@ -117,7 +112,7 @@
               <text class="remark-value">{{ record.remark }}</text>
             </view>
           </view>
-          <view class="record-actions" v-if="record.status === 0">
+          <view class="record-actions" v-if="isPendingStatus(record.status)">
             <button class="approve-btn" @click="handleApprove(record.id)">
               审核通过
             </button>
@@ -129,7 +124,7 @@
       </view>
       
       <view class="empty-state" v-if="withdrawRecords.length === 0">
-        <text class="empty-icon">📭</text>
+        <view class="empty-icon-wrapper"><AppIcon name="inbox" :size="80" color="#B0B0B0" /></view>
         <text>暂无提现记录</text>
       </view>
     </view>
@@ -140,11 +135,13 @@
 import { ref, computed, onMounted } from 'vue'
 import { onLoad } from '@dcloudio/uni-app'
 import { getWithdrawnRecords, reviewWithdrawn, applyWithdrawn, getStationAccounts } from '../../api/finance.js'
-import { formatTime, showToast, formatAmount, storage, fmtDictName, getDictColor } from '../../utils/index.js'
+import { formatTime, showToast, formatAmount, fmtDictName, getDictColor } from '../../utils/index.js'
+import dictUtil from '../../utils/dict.js'
 
 const stationId = ref('')
 const stationName = ref('')
 
+
 // 真实数据
 const availableBalance = ref(0.00)
 const pendingAmount = ref(0.00)
@@ -159,7 +156,7 @@ const withdrawForm = ref({
 })
 
 const activeFilter = ref(0)
-const filterOptions = ['全部', '待审核', '已通过', '已拒绝']
+const filterOptions = computed(() => dictUtil.getDictFilterOptions('WithdrawnRecord.status'))
 const withdrawRecords = ref([])
 
 onLoad((options) => {
@@ -173,63 +170,46 @@ onLoad((options) => {
 const loadAccountData = async () => {
   loading.value = true
   try {
-    console.log('开始加载账户数据,stationId:', stationId.value)
-    
     // 获取站点账户详情
     const res = await getStationAccounts({ stationId: stationId.value, page: 1, pageSize: 1 })
-    console.log('获取站点账户API返回:', res)
-    
+
     if (res && res.code === 200 && res.data) {
-      // 适配不同的数据结构
       const records = res.data.records || res.data.list || (Array.isArray(res.data) ? res.data : [])
-      
+
       if (records.length > 0) {
         const account = records[0]
         availableBalance.value = account.balance || 0
-        console.log('设置可用余额:', availableBalance.value)
       } else {
-        console.warn('站点账户数据为空')
         availableBalance.value = 0
       }
     } else {
-      console.warn('获取站点账户失败:', res)
       availableBalance.value = 0
     }
-    
+
     // 获取提现统计数据
     const withdrawRes = await getWithdrawnRecords({ stationId: stationId.value })
-    console.log('获取提现记录API返回:', withdrawRes)
-    
+
     if (withdrawRes && withdrawRes.code === 200 && withdrawRes.data) {
-      // 适配不同的数据结构
       const records = withdrawRes.data.records || withdrawRes.data.list || (Array.isArray(withdrawRes.data) ? withdrawRes.data : [])
-      
+
       if (Array.isArray(records)) {
-        // 计算待审核金额
         pendingAmount.value = records
-          .filter(record => record.status === 0)
+          .filter(record => isPendingStatus(record.status))
           .reduce((sum, record) => sum + (record.amount || 0), 0)
-        
-        // 计算累计提现金额
+
         totalWithdrawn.value = records
-          .filter(record => record.status === 1)
+          .filter(record => isApprovedStatus(record.status))
           .reduce((sum, record) => sum + (record.amount || 0), 0)
-        
-        console.log('待审核金额:', pendingAmount.value, '累计提现:', totalWithdrawn.value)
       } else {
-        console.warn('提现记录数据格式异常')
         pendingAmount.value = 0
         totalWithdrawn.value = 0
       }
     } else {
-      console.warn('获取提现记录失败:', withdrawRes)
       pendingAmount.value = 0
       totalWithdrawn.value = 0
     }
   } catch (error) {
-    console.error('加载账户数据失败:', error)
     showToast('加载账户数据失败')
-    // 设置默认值,防止页面崩溃
     availableBalance.value = 0
     pendingAmount.value = 0
     totalWithdrawn.value = 0
@@ -257,11 +237,8 @@ const loadWithdrawRecords = async (isLoadMore = false) => {
       pageSize: 10
     }
     const res = await getWithdrawnRecords(params)
-    console.log('提现记录API返回数据:', res)
     if (res && res.code === 200) {
-      const data = res.data
-      // 适配不同的数据结构
-      const records = data.records || data.list || data
+      const records = res.data.records || res.data.list || res.data
       if (isLoadMore) {
         withdrawRecords.value = [...withdrawRecords.value, ...records]
       } else {
@@ -270,7 +247,6 @@ const loadWithdrawRecords = async (isLoadMore = false) => {
       page.value = params.page
     }
   } catch (error) {
-    console.error('获取提现记录失败:', error)
     showToast('获取提现记录失败')
   } finally {
     loading.value = false
@@ -296,7 +272,6 @@ const handleApplyWithdraw = async () => {
     loadAccountData()
     loadWithdrawRecords()
   } catch (error) {
-    console.error('提交提现申请失败:', error)
     showToast('提交提现申请失败')
   }
 }
@@ -312,7 +287,6 @@ const handleApprove = async (recordId) => {
     showToast('审核通过', 'success')
     loadWithdrawRecords()
   } catch (error) {
-    console.error('审核失败:', error)
     showToast('审核失败')
   }
 }
@@ -323,7 +297,6 @@ const handleReject = async (recordId) => {
     showToast('审核失败', 'success')
     loadWithdrawRecords()
   } catch (error) {
-    console.error('拒绝失败:', error)
     showToast('拒绝失败')
   }
 }
@@ -348,14 +321,17 @@ const getStatusStyle = (status) => {
 // 从字典获取提现状态值(用于过滤)
 const getStatusValue = (filterIndex) => {
   if (filterIndex === 0) return ''
-  const dicts = storage.get('dicts')
-  if (dicts && dicts['WithdrawnRecord.status']) {
-    const list = dicts['WithdrawnRecord.status']
-    if (list[filterIndex - 1]) {
-      return list[filterIndex - 1].value
-    }
-  }
-  return ''
+  const options = dictUtil.getDictOptions('WithdrawnRecord.status')
+  const idx = filterIndex - 1
+  return options[idx] ? options[idx].value : ''
+}
+
+const isPendingStatus = (status) => {
+  return status == dictUtil.getDictValue('WithdrawnRecord.status', '待审核')
+}
+
+const isApprovedStatus = (status) => {
+  return status == dictUtil.getDictValue('WithdrawnRecord.status', '已通过')
 }
 </script>
 
@@ -363,37 +339,34 @@ const getStatusValue = (filterIndex) => {
 /* 全局容器 */
 .withdraw-container {
   min-height: 100vh;
-  background: #F8F9FA;
-  padding-bottom: 120rpx;
-}
-
-/* 顶部卡片 */
-.top-card {
-  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
-  padding: 40rpx 30rpx;
-  border-radius: 0 0 40rpx 40rpx;
-  box-shadow: 0 4rpx 20rpx rgba(102, 126, 234, 0.3);
-}
-
-.page-title {
-  font-size: 36rpx;
-  font-weight: 600;
-  color: #FFFFFF;
-  display: block;
+  background: #F5F7FA;
+  padding-bottom: 60rpx;
 }
 
 /* 区域样式 */
 .section {
-  padding: 20rpx 30rpx;
+  padding: 32rpx 30rpx 20rpx;
 }
 
+
 .section-title {
-  font-size: 32rpx;
+  font-size: 30rpx;
   font-weight: 600;
   color: #1A1A1A;
   margin-bottom: 20rpx;
-  padding-left: 20rpx;
-  border-left: 6rpx solid #667EEA;
+  padding-left: 24rpx;
+  position: relative;
+}
+.section-title::before {
+  content: '';
+  position: absolute;
+  left: 0;
+  top: 50%;
+  transform: translateY(-50%);
+  width: 16rpx;
+  height: 16rpx;
+  background: #C6171E;
+  border-radius: 50%;
 }
 
 /* 信息卡片 */
@@ -405,49 +378,38 @@ const getStatusValue = (filterIndex) => {
 
 .info-card {
   background: #FFFFFF;
-  padding: 30rpx 24rpx;
+  padding: 24rpx 16rpx;
   border-radius: 24rpx;
   box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
-  transition: all 0.3s;
   text-align: center;
-  color: #FFFFFF;
-}
-
-.info-card:active {
-  transform: translateY(-4rpx);
-  box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
-}
-
-.info-card.blue {
-  background: linear-gradient(135deg, #4FACFE 0%, #00F2FE 100%);
-}
-
-.info-card.pink {
-  background: linear-gradient(135deg, #F093FB 0%, #F5576C 100%);
-}
-
-.info-card.green {
-  background: linear-gradient(135deg, #43E97B 0%, #38F9D7 100%);
 }
 
 .info-label {
-  font-size: 24rpx;
+  font-size: 22rpx;
   font-weight: 500;
-  margin-bottom: 12rpx;
+  color: #999999;
+  margin-bottom: 8rpx;
   display: block;
-  opacity: 0.9;
 }
 
 .info-value {
-  font-size: 40rpx;
+  font-size: 36rpx;
   font-weight: 700;
+  color: #1A1A1A;
   display: block;
 }
 
-/* 移除旧的颜色类 */
-/* .info-value.blue { color: #4FACFE; }
-.info-value.pink { color: #F093FB; }
-.info-value.green { color: #43E97B; } */
+.info-value.accent-blue {
+  color: #2196F3;
+}
+
+.info-value.accent-warning {
+  color: #FF9800;
+}
+
+.info-value.accent-success {
+  color: #52C41A;
+}
 
 /* 申请提现表单 */
 .apply-form {
@@ -460,8 +422,8 @@ const getStatusValue = (filterIndex) => {
 .form-item {
   display: flex;
   flex-direction: column;
-  gap: 10rpx;
-  margin-bottom: 20rpx;
+  gap: 8rpx;
+  margin-bottom: 16rpx;
 }
 
 .form-item:last-child {
@@ -477,23 +439,22 @@ const getStatusValue = (filterIndex) => {
 .form-input input,
 .form-input textarea {
   width: 100%;
-  padding: 24rpx 24rpx;
-  border: 2rpx solid #E0E0E0;
-  border-radius: 20rpx;
+  padding: 20rpx 28rpx;
+  border: none;
+  border-radius: 16rpx;
   font-size: 28rpx;
   color: #1A1A1A;
-  background-color: #FFFFFF;
+  background-color: #F5F5F5;
   box-sizing: border-box;
-  transition: all 0.3s ease;
+  transition: all 0.25s ease;
   line-height: 1.5;
   min-height: 80rpx;
 }
 
 .form-input input:focus,
 .form-input textarea:focus {
-  border-color: #667EEA;
   outline: none;
-  box-shadow: 0 0 0 8rpx rgba(102, 126, 234, 0.1);
+  box-shadow: 0 0 0 6rpx rgba(198, 23, 30, 0.2);
 }
 
 .balance-tip {
@@ -507,27 +468,25 @@ const getStatusValue = (filterIndex) => {
 .apply-btn {
   width: 100%;
   padding: 20rpx;
-  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
+  background: #C6171E;
   color: white;
   border: none;
-  border-radius: 20rpx;
+  border-radius: 44rpx;
   font-size: 28rpx;
   font-weight: 600;
-  cursor: pointer;
-  transition: all 0.3s ease;
-  box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.3);
+  transition: all 0.25s ease;
+  box-shadow: 0 4rpx 16rpx rgba(198, 23, 30, 0.3);
   margin-top: 12rpx;
 }
 
 .apply-btn:active:not(:disabled) {
   transform: scale(0.97);
-  box-shadow: 0 2rpx 10rpx rgba(102, 126, 234, 0.4);
+  box-shadow: 0 2rpx 10rpx rgba(198, 23, 30, 0.25);
 }
 
 .apply-btn:disabled {
-  background: linear-gradient(135deg, #C0C4CC 0%, #909399 100%);
-  cursor: not-allowed;
-  opacity: 0.6;
+  background: #CCCCCC;
+  opacity: 0.5;
   box-shadow: none;
 }
 
@@ -538,56 +497,49 @@ const getStatusValue = (filterIndex) => {
 
 .segmented-control {
   display: flex;
-  background: #FFFFFF;
-  border-radius: 24rpx;
+  background: #F5F5F5;
+  border-radius: 16rpx;
   padding: 6rpx;
-  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
 }
 
 .segment-item {
   flex: 1;
   text-align: center;
-  padding: 20rpx 0;
-  font-size: 28rpx;
-  color: #999999;
-  border-radius: 20rpx;
-  position: relative;
-  transition: all 0.3s ease;
-  cursor: pointer;
-  font-weight: 500;
+  padding: 18rpx 0;
+  font-size: 26rpx;
+  color: #666666;
+  border-radius: 12rpx;
+  transition: all 0.25s ease;
+  font-weight: 400;
 }
 
 .segment-item.active {
-  color: #667EEA;
-  background: linear-gradient(135deg, #E6F7FF 0%, #F0F5FF 100%);
-  font-weight: 600;
+  color: #FFFFFF;
+  background: #C6171E;
+  font-weight: 500;
+  box-shadow: 0 4rpx 12rpx rgba(198, 23, 30, 0.3);
 }
 
 /* 记录列表 */
 .records-list {
   display: flex;
   flex-direction: column;
-  gap: 20rpx;
+  gap: 16rpx;
 }
 
 .record-item {
-  padding: 28rpx;
+  padding: 24rpx;
   border-radius: 24rpx;
   background-color: #FFFFFF;
   box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
   transition: all 0.3s;
 }
 
-.record-item:active {
-  transform: translateY(-2rpx);
-  box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
-}
-
 .record-header {
   display: flex;
   justify-content: space-between;
   align-items: flex-start;
-  margin-bottom: 16rpx;
+  margin-bottom: 12rpx;
 }
 
 .record-info {
@@ -608,15 +560,15 @@ const getStatusValue = (filterIndex) => {
 }
 
 .record-status {
-  font-size: 24rpx;
-  font-weight: 600;
-  padding: 8rpx 20rpx;
-  border-radius: 20rpx;
+  font-size: 22rpx;
+  font-weight: 500;
+  padding: 6rpx 24rpx;
+  border-radius: 100rpx;
 }
 
 
 .record-content {
-  margin-bottom: 20rpx;
+  margin-bottom: 16rpx;
 }
 
 .record-amount,
@@ -624,8 +576,8 @@ const getStatusValue = (filterIndex) => {
 .record-remark {
   display: flex;
   align-items: center;
-  gap: 20rpx;
-  margin-bottom: 16rpx;
+  gap: 12rpx;
+  margin-bottom: 12rpx;
 }
 
 .record-amount:last-child,
@@ -651,28 +603,28 @@ const getStatusValue = (filterIndex) => {
 .account-value,
 .remark-value {
   font-size: 26rpx;
-  color: #333333;
+  color: #1A1A1A;
   flex: 1;
 }
 
 .record-footer {
-  margin-top: 20rpx;
-  padding-top: 20rpx;
-  border-top: 2rpx dashed #E0E0E0;
+  margin-top: 16rpx;
+  padding-top: 16rpx;
+  border-top: 1rpx solid #F0F0F0;
 }
 
 .record-actions {
   display: flex;
   gap: 16rpx;
-  margin-top: 20rpx;
-  padding-top: 20rpx;
-  border-top: 2rpx dashed #E0E0E0;
+  margin-top: 16rpx;
+  padding-top: 16rpx;
+  border-top: 1rpx solid #F0F0F0;
 }
 
 .approve-btn {
   flex: 1;
   padding: 18rpx;
-  background: linear-gradient(135deg, #43E97B 0%, #38F9D7 100%);
+  background: #52C41A;
   color: white;
   border: none;
   border-radius: 16rpx;
@@ -680,18 +632,18 @@ const getStatusValue = (filterIndex) => {
   cursor: pointer;
   transition: all 0.3s ease;
   font-weight: 500;
-  box-shadow: 0 4rpx 12rpx rgba(67, 233, 123, 0.3);
+  box-shadow: 0 4rpx 12rpx rgba(82, 196, 26, 0.25);
 }
 
 .approve-btn:active {
   transform: scale(0.98);
-  box-shadow: 0 2rpx 6rpx rgba(67, 233, 123, 0.4);
+  box-shadow: 0 2rpx 6rpx rgba(82, 196, 26, 0.3);
 }
 
 .reject-btn {
   flex: 1;
   padding: 18rpx;
-  background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
+  background: #C6171E;
   color: white;
   border: none;
   border-radius: 16rpx;
@@ -699,12 +651,12 @@ const getStatusValue = (filterIndex) => {
   cursor: pointer;
   transition: all 0.3s ease;
   font-weight: 500;
-  box-shadow: 0 4rpx 12rpx rgba(255, 107, 107, 0.3);
+  box-shadow: 0 4rpx 12rpx rgba(198, 23, 30, 0.25);
 }
 
 .reject-btn:active {
   transform: scale(0.98);
-  box-shadow: 0 2rpx 6rpx rgba(255, 107, 107, 0.4);
+  box-shadow: 0 2rpx 6rpx rgba(198, 23, 30, 0.35);
 }
 
 /* 空状态 */
@@ -713,18 +665,14 @@ const getStatusValue = (filterIndex) => {
   flex-direction: column;
   align-items: center;
   justify-content: center;
-  padding: 120rpx 0;
+  padding: 80rpx 0;
   color: #999999;
 }
 
-.empty-icon {
-  font-size: 128rpx;
-  margin-bottom: 30rpx;
-  opacity: 0.6;
-}
 
 .empty-state text {
   font-size: 28rpx;
   color: #666666;
+  margin-top: 20rpx;
 }
 </style>

+ 424 - 506
admin-mp/src/pages/index/index.vue

@@ -1,707 +1,625 @@
 <template>
   <view class="page-container">
-    <!-- 顶部简洁卡片 -->
-    <view class="top-card">
-      <view class="user-section">
-        <view class="user-avatar">
-          <text class="iconfont icon-user">👤</text>
-        </view>
-        <view class="user-info">
-          <text class="greeting">你好,{{ userInfo?.name || '管理员' }}</text>
-          <text class="station-name">{{ currentStation?.stationName || '暂无站点' }}</text>
+    <NavBar title="首页" :showBack="false">
+      <template #right>
+        <view class="nav-btn" @click="handleLogout">
+          <AppIcon name="log-out" :size="28" color="#FFFFFF" />
         </view>
+      </template>
+    </NavBar>
+
+    <!-- 加载骨架 -->
+    <view class="content" v-if="loading && !hasData">
+      <view class="skeleton-greeting"></view>
+      <view class="skeleton-hero">
+        <view class="skeleton-line hero-num"></view>
+        <view class="skeleton-line hero-sub"></view>
       </view>
-      
-      <view class="actions">
-        <picker 
-          mode="selector" 
-          :range="stationList" 
-          range-key="stationName"
-          :value="selectedStationIndex"
-          @change="onStationChange"
-        >
-          <view class="action-btn">
-            <text class="icon">🏢</text>
-          </view>
-        </picker>
-        
-        <view class="action-btn" @click="toggleUserMenu">
-          <text class="icon">⚙️</text>
-        </view>
+      <view class="skeleton-line station-sk"></view>
+      <view class="skeleton-grid">
+        <view class="skeleton-cell"></view>
+        <view class="skeleton-cell"></view>
+        <view class="skeleton-cell sm"></view>
+        <view class="skeleton-cell sm"></view>
+      </view>
+      <view class="skeleton-line section-title"></view>
+      <view class="skeleton-actions">
+        <view class="skeleton-action"></view>
+        <view class="skeleton-action"></view>
+        <view class="skeleton-action"></view>
       </view>
     </view>
-    
-    <!-- 用户菜单 -->
-    <view class="user-menu" v-if="showUserMenu" @click="handleLogout">
-      <text class="menu-text">退出登录</text>
-    </view>
-    
-    <!-- 今日数据概览 -->
-    <view class="section">
-      <view class="section-title">今日数据</view>
-      <view class="data-cards">
-        <view class="data-card purple">
-          <text class="data-value">{{ todayOrders }}</text>
-          <text class="data-label">订单数</text>
-          <view class="card-icon">📋</view>
-        </view>
-        
-        <view class="data-card pink">
-          <text class="data-value">¥{{ formatAmount(todayRevenue) }}</text>
-          <text class="data-label">营业额</text>
-          <view class="card-icon">💰</view>
-        </view>
-        
-        <view class="data-card blue">
-          <text class="data-value">{{ todayRegisteredMembers }}</text>
-          <text class="data-label">新会员</text>
-          <view class="card-icon">👥</view>
-        </view>
-        
-        <view class="data-card green">
-          <text class="data-value">{{ onlineDevices }}/{{ totalDevices }}</text>
-          <text class="data-label">在线设备</text>
-          <view class="card-icon">📱</view>
+
+    <!-- 错误态 -->
+    <view class="content" v-else-if="error && !hasData">
+      <view class="error-state">
+        <AppIcon name="inbox" :size="48" color="#BFBFBF" />
+        <text class="error-text">数据加载失败</text>
+        <text class="error-hint">下拉页面或点击下方按钮重试</text>
+        <view class="retry-btn" @click="refreshData">
+          <text class="retry-btn-text">重新加载</text>
         </view>
       </view>
     </view>
-    
-    <!-- 详细统计 -->
-    <view class="section">
-      <view class="section-title">数据统计</view>
-      <view class="stat-list">
-        <view class="stat-item">
-          <view class="stat-icon red">💳</view>
-          <view class="stat-info">
-            <text class="stat-label">今日消费金额</text>
-            <text class="stat-value">¥{{ formatAmount(todayConsumptionAmount) }}</text>
+
+    <!-- 正常内容 -->
+    <view class="content" v-else>
+      <!-- 问候语 -->
+      <text class="greeting">{{ timeGreeting }}</text>
+
+      <!-- Hero -->
+      <view class="hero-section">
+        <text class="hero-value">¥{{ formatAmount(todayRevenue) }}</text>
+        <text class="hero-label">今日收入</text>
+      </view>
+
+      <!-- 站点栏 -->
+      <picker
+        v-if="stationList.length > 0"
+        mode="selector"
+        :range="stationList"
+        range-key="stationName"
+        :value="selectedStationIndex"
+        @change="onStationChange"
+      >
+        <view class="station-card">
+          <view class="station-card-row">
+            <AppIcon name="building" :size="18" color="#C6171E" />
+            <text class="station-card-name">{{ currentStation?.stationName || '选择站点' }}</text>
+            <AppIcon name="chevron-down" :size="14" color="#999999" />
           </view>
+          <text class="station-card-date">{{ todayDate }}</text>
         </view>
-        
-        <view class="stat-item">
-          <view class="stat-icon blue">💵</view>
-          <view class="stat-info">
-            <text class="stat-label">平均订单金额</text>
-            <text class="stat-value">¥{{ formatAmount(avgOrderPrice) }}</text>
+      </picker>
+
+      <!-- 主指标:订单 + 消费 -->
+      <view class="metrics-primary">
+        <view class="metric-card-lg">
+          <text class="metric-lg-value">{{ todayOrders }}</text>
+          <text class="metric-lg-label">今日订单</text>
+        </view>
+        <view class="metric-card-lg">
+          <text class="metric-lg-value">¥{{ formatAmount(todayConsumption) }}</text>
+          <text class="metric-lg-label">消费总额</text>
+        </view>
+      </view>
+
+      <!-- 次指标:均价 + 时长 -->
+      <view class="metrics-secondary">
+        <view class="metric-card-sm">
+          <view class="metric-sm-icon">
+            <AppIcon name="trending-up" :size="18" color="#C6171E" />
           </view>
+          <text class="metric-sm-value">¥{{ formatAmount(avgOrderPrice) }}</text>
+          <text class="metric-sm-label">平均消费</text>
         </view>
-        
-        <view class="stat-item">
-          <view class="stat-icon purple">⏱️</view>
-          <view class="stat-info">
-            <text class="stat-label">平均洗车时长</text>
-            <text class="stat-value">{{ avgOrderDuration }} 分钟</text>
+        <view class="metric-card-sm">
+          <view class="metric-sm-icon">
+            <AppIcon name="clock" :size="18" color="#C6171E" />
           </view>
+          <text class="metric-sm-value">{{ avgDuration }} 分钟</text>
+          <text class="metric-sm-label">平均时长</text>
         </view>
       </view>
-    </view>
-    
-    <!-- 快捷操作 -->
-    <view class="section">
-      <view class="section-title">快捷操作</view>
-      <view class="action-grid">
-        <view class="action-item purple" @click="navigateTo('/pages/order/list')">
-          <view class="action-icon">📋</view>
+
+      <!-- 快捷入口标题 -->
+      <view class="section-header">
+        <view class="section-dot"></view>
+        <text class="section-title">快捷功能</text>
+      </view>
+
+      <!-- 快捷入口 -->
+      <view class="actions-row">
+        <view class="action-item" @click="navigateTo('/pages/order/list')">
+          <view class="action-icon-wrap">
+            <AppIcon name="clipboard" :size="26" color="#FFFFFF" />
+          </view>
           <text class="action-label">订单管理</text>
         </view>
-        
-        <view class="action-item pink" @click="navigateTo('/pages/device/list')">
-          <view class="action-icon">📺</view>
+        <view class="action-item" @click="navigateTo('/pages/device/list')">
+          <view class="action-icon-wrap">
+            <AppIcon name="monitor" :size="26" color="#FFFFFF" />
+          </view>
           <text class="action-label">设备监控</text>
         </view>
-        
-        <view class="action-item blue" @click="navigateTo('/pages/finance/index')">
-          <view class="action-icon">💰</view>
+        <view class="action-item" @click="navigateTo('/pages/finance/index')">
+          <view class="action-icon-wrap">
+            <AppIcon name="dollar" :size="26" color="#FFFFFF" />
+          </view>
           <text class="action-label">财务管理</text>
         </view>
-        
-        <view class="action-item green" @click="navigateToWithdraw">
-          <view class="action-icon">💸</view>
-          <text class="action-label">提现审核</text>
+      </view>
+
+      <!-- 底栏信息 -->
+      <view class="info-bar">
+        <view class="info-item">
+          <AppIcon name="users" :size="16" color="#999999" />
+          <text class="info-text">今日注册会员 <text class="info-num">{{ todayMembers }}</text> 人</text>
         </view>
-        
-        <view class="action-item orange" @click="navigateTo('/pages/setting/device-config')">
-          <view class="action-icon">⚙️</view>
-          <text class="action-label">设备配置</text>
+        <view class="info-item">
+          <AppIcon name="smartphone" :size="16" color="#999999" />
+          <text class="info-text">在线设备 <text class="info-num">{{ onlineDevices }}</text>/<text class="info-num">{{ totalDevices }}</text></text>
         </view>
       </view>
     </view>
-    
-    <!-- 加载指示器 -->
-    <view class="loading" v-if="loading">
-      <view class="spinner"></view>
-    </view>
+
+    <custom-tab-bar />
   </view>
 </template>
 
 <script setup>
-import { ref, onMounted } from 'vue'
+import { ref, computed, onMounted } from 'vue'
+import { onPullDownRefresh } from '@dcloudio/uni-app'
+import CustomTabBar from '../../custom-tab-bar/index.vue'
 import { logout } from '../../api/auth.js'
 import { getDashboardData, getDeviceStatus } from '../../api/stat.js'
 import { getStationList } from '../../api/station.js'
-import { getDataDictList } from '../../api/dict.js'
+import { loadDicts } from '../../utils/dict.js'
+import dictUtil from '../../utils/dict.js'
 import { storage, showToast, formatAmount } from '../../utils/index.js'
 
-const showUserMenu = ref(false)
 const userInfo = ref(storage.get('userInfo'))
 const loading = ref(false)
+const error = ref(false)
+const hasData = ref(false)
 
-// 站点相关数据
 const stationList = ref([])
 const currentStation = ref(null)
 const selectedStationIndex = ref(0)
 
-// 数据状态
+const todayRevenue = ref(0)
 const todayOrders = ref(0)
+const todayConsumption = ref(0)
+const avgOrderPrice = ref(0)
+const avgDuration = ref(0)
+const todayMembers = ref(0)
 const totalDevices = ref(0)
 const onlineDevices = ref(0)
-const todayRevenue = ref(0)
-const todayRegisteredMembers = ref(0)
-const todayConsumptionAmount = ref(0)
-const avgOrderPrice = ref(0)
-const avgOrderDuration = ref(0)
 
-// 切换用户菜单
-const toggleUserMenu = () => {
-  showUserMenu.value = !showUserMenu.value
-}
+const todayDate = computed(() => {
+  const d = new Date()
+  return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日`
+})
+
+const timeGreeting = computed(() => {
+  const h = new Date().getHours()
+  if (h < 6) return '夜深了'
+  if (h < 9) return '早上好'
+  if (h < 12) return '上午好'
+  if (h < 14) return '中午好'
+  if (h < 18) return '下午好'
+  return '晚上好'
+})
 
-// 页面加载时获取数据
 onMounted(async () => {
-  // 并行加载站点列表和系统字典数据
-  await Promise.all([
-    loadStationList(),
-    loadDictionaries()
-  ])
+  await Promise.all([loadStationList(), loadDictionaries()])
 })
 
-// 加载站点列表
 const loadStationList = async () => {
   try {
-    const res = await getStationList({
-      pageNum: 1,
-      pageSize: 1024
-    })
-    
+    const res = await getStationList({ pageNum: 1, pageSize: 1024 })
     if (res && res.code === 200 && res.data && res.data.list) {
       stationList.value = res.data.list
-      
-      // 默认选中第一个站点
       if (stationList.value.length > 0) {
         currentStation.value = stationList.value[0]
         selectedStationIndex.value = 0
-        
-        // 保存当前选中的站点ID到缓存
         storage.set('currentStationId', currentStation.value.stationId)
-        
-        // 加载该站点的数据
         await loadData(currentStation.value.stationId)
       }
     } else {
-      showToast('获取站点列表失败')
+      error.value = true
     }
-  } catch (error) {
-    console.error('加载站点列表失败:', error)
-    showToast('加载站点列表失败')
+  } catch (e) {
+    console.error('加载站点列表失败:', e)
+    error.value = true
   }
 }
 
-// 加载系统字典数据
 const loadDictionaries = async () => {
-  try {
-    const res = await getDataDictList()
-    if (res && res.code === 200) {
-      const list = res.data
-      // 按code分组
-      const dictGroup = {}  
-      list.forEach(item => {
-        if (!dictGroup[item.code]) {
-          dictGroup[item.code] = []
-        }
-        dictGroup[item.code].push(item)
-      })
-      // 保存到缓存
-      storage.set('dicts', dictGroup)
-      console.log('系统字典数据加载成功:', dictGroup)
-    }
-  } catch (error) {
-    console.error('加载系统字典数据失败:', error)
-  }
+  await loadDicts()
 }
 
-// 站点切换事件
 const onStationChange = async (e) => {
   const index = e.detail.value
   selectedStationIndex.value = index
   currentStation.value = stationList.value[index]
-  
-  // 保存当前选中的站点ID
   storage.set('currentStationId', currentStation.value.stationId)
-  
-  // 加载新站点的数据
+
   await loadData(currentStation.value.stationId)
-  
   showToast(`已切换至${currentStation.value.stationName}`, 'success')
 }
 
-// 加载首页数据(根据站点ID)
 const loadData = async (stationId) => {
-  if (!stationId) {
-    console.error('站点ID为空')
-    return
-  }
-  
-  loading.value = true
+  if (!stationId) return
+  error.value = false
+  if (!hasData.value) loading.value = true
+
   try {
-    // 并发请求两个接口
     const [dashboardRes, deviceRes] = await Promise.all([
-      getDashboardData(stationId).catch(err => {
-        console.error('获取仪表盘数据失败:', err)
-        return null
-      }),
-      getDeviceStatus(stationId).catch(err => {
-        console.error('获取设备状态失败:', err)
-        return null
-      })
+      getDashboardData(stationId).catch(err => { console.error('获取仪表盘数据失败:', err); return null }),
+      getDeviceStatus(stationId).catch(err => { console.error('获取设备状态失败:', err); return null })
     ])
-    
-    // 处理仪表盘数据
+
+    if (!dashboardRes && !deviceRes) { error.value = true; return }
+
     if (dashboardRes && dashboardRes.code === 200) {
       const data = dashboardRes.data
-      todayOrders.value = data.todayWashOrders || 0
       todayRevenue.value = data.todayIncome || 0
-      todayRegisteredMembers.value = data.todayRegisteredMembers || 0
-      todayConsumptionAmount.value = data.todayConsumptionAmount || 0
+      todayOrders.value = data.todayWashOrders || 0
+      todayConsumption.value = data.todayConsumptionAmount || 0
       avgOrderPrice.value = data.avgOrderPrice || 0
-      avgOrderDuration.value = data.avgOrderDuration || 0
+      avgDuration.value = data.avgOrderDuration || 0
+      todayMembers.value = data.todayRegisteredMembers || 0
     }
-    
-    // 处理设备状态数据
+
     if (deviceRes && deviceRes.code === 200) {
       const deviceData = deviceRes.data
       totalDevices.value = Object.values(deviceData).reduce((sum, count) => sum + count, 0)
-      // 从字典获取在线状态值,而不是硬编码 '1'
-      const dicts = storage.get('dicts')
-      let onlineCount = 0
-      if (dicts && dicts['WashDevice.status']) {
-        const onlineStatuses = dicts['WashDevice.status']
-          .filter(item => item.color === '#52C41A')
+      const onlineStatuses = dictUtil.getDictList('WashDevice.status')
+          .filter(item => item.name === '在线')
           .map(item => String(item.value))
-        onlineCount = Object.entries(deviceData)
+      const onlineCount = Object.entries(deviceData)
           .filter(([key]) => onlineStatuses.includes(key))
           .reduce((sum, [, count]) => sum + count, 0)
-      } else {
-        onlineCount = deviceData['1'] || 0
-      }
-      onlineDevices.value = onlineCount
     }
-  } catch (error) {
-    console.error('加载首页数据失败:', error)
+    hasData.value = true
+  } catch (e) {
+    console.error('加载首页数据失败:', e)
+    if (!hasData.value) error.value = true
   } finally {
     loading.value = false
   }
 }
 
-// 页面导航
+const refreshData = async () => {
+  if (currentStation.value) await loadData(currentStation.value.stationId)
+}
+
 const navigateTo = (url) => {
-  showUserMenu.value = false
-  
-  // 统一处理路径格式:确保以斜杠开头
+
   let fullUrl = url
-  if (!fullUrl.startsWith('/')) {
-    fullUrl = `/${fullUrl}`
-  }
-  
-  // 定义tabBar页面列表
-  const tabBarPages = [
-    '/pages/index/index',
-    '/pages/order/list',
-    '/pages/device/list',
-    '/pages/finance/index'
-  ]
-  
-  console.log('导航到:', fullUrl, '是否为TabBar页面:', tabBarPages.includes(fullUrl))
-  
+  if (!fullUrl.startsWith('/')) fullUrl = `/${fullUrl}`
+  const tabBarPages = ['/pages/index/index', '/pages/order/list', '/pages/device/list', '/pages/finance/index']
   if (tabBarPages.includes(fullUrl)) {
-    // 对于tabBar页面,使用switchTab
-    uni.switchTab({ 
-      url: fullUrl,
-      fail: (err) => {
-        console.error('switchTab失败:', err)
-        showToast('页面跳转失败')
-      }
-    })
+    uni.switchTab({ url: fullUrl, fail: (err) => { console.error('switchTab失败:', err); showToast('页面跳转失败') } })
   } else {
-    // 对于非tabBar页面,使用navigateTo
-    uni.navigateTo({ 
-      url: fullUrl,
-      fail: (err) => {
-        console.error('navigateTo失败:', err)
-        showToast('页面跳转失败')
-      }
-    })
+    uni.navigateTo({ url: fullUrl, fail: (err) => { console.error('navigateTo失败:', err); showToast('页面跳转失败') } })
   }
 }
 
-// 导航到提现管理页面
-const navigateToWithdraw = () => {
-  showUserMenu.value = false
-  
-  if (currentStation.value) {
-    const url = `/pages/finance/withdraw?stationId=${currentStation.value.stationId}&stationName=${currentStation.value.stationName}`
-    uni.navigateTo({ 
-      url: url,
-      fail: (err) => {
-        console.error('navigateTo失败:', err)
-        showToast('页面跳转失败')
-      }
-    })
-  } else {
-    showToast('请先选择站点')
-  }
-}
-
-// 退出登录
 const handleLogout = async () => {
-  showUserMenu.value = false
-  
   uni.showModal({
     title: '提示',
     content: '确定要退出登录吗?',
     success: async (res) => {
       if (res.confirm) {
-        try {
-          await logout()
-        } catch (error) {
-          console.error('登出接口调用失败:', error)
-        } finally {
-          storage.remove('token')
-          storage.remove('userInfo')
-          storage.remove('currentStationId')
-          
-          showToast('已退出登录', 'success')
-          
-          setTimeout(() => {
-            uni.reLaunch({
-              url: '/pages/login/login'
-            })
-          }, 500)
-        }
+        try { await logout() } catch (e) { console.error('登出接口调用失败:', e) }
+        storage.remove('token')
+        storage.remove('userInfo')
+        storage.remove('currentStationId')
+        showToast('已退出登录', 'success')
+        setTimeout(() => { uni.reLaunch({ url: '/pages/login/login' }) }, 500)
       }
     }
   })
 }
+
+onPullDownRefresh(async () => {
+  await refreshData()
+  uni.stopPullDownRefresh()
+})
 </script>
 
 <style scoped>
-/* 全局容器 */
 .page-container {
   min-height: 100vh;
-  background: #F8F9FA;
+  background: #F5F7FA;
   padding-bottom: 120rpx;
 }
 
-/* ===== 顶部卡片 ===== */
-.top-card {
-  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
-  padding: 40rpx 30rpx 30rpx;
+.nav-btn {
+  width: 56rpx;
+  height: 56rpx;
   display: flex;
-  justify-content: space-between;
   align-items: center;
-  border-radius: 0 0 40rpx 40rpx;
-  box-shadow: 0 4rpx 20rpx rgba(102, 126, 234, 0.3);
+  justify-content: center;
+  border-radius: 50%;
+  transition: background 0.25s;
 }
+.nav-btn:active { background: rgba(255, 255, 255, 0.2); }
 
-.user-section {
-  display: flex;
-  align-items: center;
-  gap: 20rpx;
-}
+/* ===== Content ===== */
+.content { padding: 0 28rpx; }
 
-.user-avatar {
-  width: 80rpx;
-  height: 80rpx;
-  background: rgba(255, 255, 255, 0.2);
-  border-radius: 50%;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 40rpx;
-  backdrop-filter: blur(10px);
-  border: 2rpx solid rgba(255, 255, 255, 0.3);
+/* ===== Greeting ===== */
+.greeting {
+  display: block;
+  text-align: center;
+  font-size: 30rpx;
+  font-weight: 500;
+  color: #666666;
+  padding: 32rpx 0 32rpx;
 }
 
-.user-info {
+/* ===== Hero ===== */
+.hero-section {
   display: flex;
   flex-direction: column;
-  gap: 8rpx;
+  align-items: center;
+  padding: 0 0 44rpx;
 }
-
-.greeting {
-  font-size: 32rpx;
-  font-weight: 600;
-  color: #FFFFFF;
+.hero-value {
+  font-size: 64rpx;
+  font-weight: 700;
+  color: #1A1A1A;
+  line-height: 1;
+  letter-spacing: -1px;
 }
-
-.station-name {
+.hero-label {
   font-size: 24rpx;
-  color: rgba(255, 255, 255, 0.85);
+  color: #999999;
+  margin-top: 18rpx;
+  letter-spacing: 1px;
 }
 
-.actions {
+/* ===== Station Card ===== */
+.station-card {
+  background: #FFFFFF;
+  border-radius: 20rpx;
+  padding: 24rpx;
   display: flex;
-  gap: 15rpx;
+  flex-direction: column;
+  align-items: center;
+  gap: 10rpx;
+  margin-bottom: 24rpx;
 }
-
-.action-btn {
-  width: 70rpx;
-  height: 70rpx;
-  background: rgba(255, 255, 255, 0.2);
-  border-radius: 50%;
+.station-card:active { background: #F5F7FA; }
+.station-card-row {
   display: flex;
   align-items: center;
   justify-content: center;
-  backdrop-filter: blur(10px);
-  border: 2rpx solid rgba(255, 255, 255, 0.3);
-  transition: all 0.3s;
-}
-
-.action-btn:active {
-  background: rgba(255, 255, 255, 0.35);
-  transform: scale(0.92);
-}
-
-.action-btn .icon {
-  font-size: 36rpx;
-}
-
-/* ===== 用户菜单 ===== */
-.user-menu {
-  position: fixed;
-  top: 140rpx;
-  right: 30rpx;
-  background: #FFFFFF;
-  padding: 24rpx 32rpx;
-  border-radius: 16rpx;
-  box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12);
-  z-index: 100;
-  animation: fadeIn 0.3s;
+  gap: 10rpx;
 }
-
-@keyframes fadeIn {
-  from { opacity: 0; transform: translateY(-10rpx); }
-  to { opacity: 1; transform: translateY(0); }
-}
-
-.menu-text {
+.station-card-name {
   font-size: 28rpx;
-  color: #667EEA;
-  font-weight: 500;
-}
-
-/* ===== 区域样式 ===== */
-.section {
-  padding: 30rpx;
-}
-
-.section-title {
-  font-size: 32rpx;
   font-weight: 600;
   color: #1A1A1A;
-  margin-bottom: 24rpx;
-  padding-left: 20rpx;
-  border-left: 6rpx solid #667EEA;
 }
-
-/* ===== 数据卡片 ===== */
-.data-cards {
-  display: grid;
-  grid-template-columns: repeat(2, 1fr);
-  gap: 20rpx;
+.station-card-date {
+  font-size: 22rpx;
+  color: #999999;
 }
 
-.data-card {
+/* ===== Primary Metrics ===== */
+.metrics-primary {
+  display: flex;
+  gap: 16rpx;
+  margin-bottom: 16rpx;
+}
+.metric-card-lg {
+  flex: 1;
   background: #FFFFFF;
-  padding: 40rpx 30rpx;
   border-radius: 24rpx;
-  position: relative;
-  overflow: hidden;
-  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
-  transition: all 0.3s;
+  padding: 32rpx 20rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 12rpx;
+  transition: box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1), transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
 }
-
-.data-card:active {
-  transform: translateY(-4rpx);
-  box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
+.metric-card-lg:active {
+  transform: translateY(-2rpx);
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.06);
 }
-
-.data-card.purple { background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%); }
-.data-card.pink { background: linear-gradient(135deg, #F093FB 0%, #F5576C 100%); }
-.data-card.blue { background: linear-gradient(135deg, #4FACFE 0%, #00F2FE 100%); }
-.data-card.green { background: linear-gradient(135deg, #43E97B 0%, #38F9D7 100%); }
-
-.data-value {
-  display: block;
-  font-size: 48rpx;
+.metric-lg-value {
+  font-size: 44rpx;
   font-weight: 700;
-  color: #FFFFFF;
-  margin-bottom: 8rpx;
+  color: #1A1A1A;
+  line-height: 1;
 }
-
-.data-label {
-  display: block;
+.metric-lg-label {
   font-size: 24rpx;
-  color: rgba(255, 255, 255, 0.9);
-  font-weight: 500;
+  color: #999999;
 }
 
-.card-icon {
-  position: absolute;
-  right: 20rpx;
-  bottom: 20rpx;
-  font-size: 56rpx;
-  opacity: 0.3;
+/* ===== Secondary Metrics ===== */
+.metrics-secondary {
+  display: flex;
+  gap: 16rpx;
+  margin-bottom: 40rpx;
 }
-
-/* ===== 统计列表 ===== */
-.stat-list {
+.metric-card-sm {
+  flex: 1;
   background: #FFFFFF;
-  border-radius: 24rpx;
-  overflow: hidden;
-  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
-}
-
-.stat-item {
+  border-radius: 20rpx;
+  padding: 28rpx 20rpx;
   display: flex;
+  flex-direction: column;
   align-items: center;
-  padding: 32rpx 30rpx;
-  border-bottom: 1rpx solid #F0F0F0;
-  transition: background 0.3s;
+  gap: 10rpx;
+  transition: box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1), transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
 }
-
-.stat-item:last-child {
-  border-bottom: none;
+.metric-card-sm:active {
+  transform: translateY(-2rpx);
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.06);
 }
-
-.stat-item:active {
-  background: #F8F9FA;
-}
-
-.stat-icon {
-  width: 80rpx;
-  height: 80rpx;
-  border-radius: 20rpx;
+.metric-sm-icon {
+  width: 56rpx;
+  height: 56rpx;
+  border-radius: 16rpx;
+  background: #F5F7FA;
   display: flex;
   align-items: center;
   justify-content: center;
-  font-size: 40rpx;
-  margin-right: 24rpx;
+}
+.metric-sm-value {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #1A1A1A;
+  line-height: 1.2;
+}
+.metric-sm-label {
+  font-size: 22rpx;
+  color: #999999;
 }
 
-.stat-icon.red { background: #FFE8E8; }
-.stat-icon.blue { background: #E6F7FF; }
-.stat-icon.purple { background: #F0F5FF; }
-
-.stat-info {
-  flex: 1;
+/* ===== Section Header ===== */
+.section-header {
   display: flex;
-  justify-content: space-between;
   align-items: center;
+  justify-content: center;
+  gap: 12rpx;
+  padding: 0 0 20rpx;
 }
-
-.stat-label {
-  font-size: 28rpx;
-  color: #666666;
+.section-dot {
+  width: 10rpx;
+  height: 10rpx;
+  border-radius: 50%;
+  background: #C6171E;
+  flex-shrink: 0;
 }
-
-.stat-value {
-  font-size: 32rpx;
+.section-title {
+  font-size: 28rpx;
   font-weight: 600;
   color: #1A1A1A;
 }
 
-/* ===== 快捷操作 ===== */
-.action-grid {
-  display: grid;
-  grid-template-columns: repeat(2, 1fr);
-  gap: 20rpx;
+/* ===== Quick Actions ===== */
+.actions-row {
+  display: flex;
+  gap: 16rpx;
+  margin-bottom: 28rpx;
 }
-
 .action-item {
+  flex: 1;
   background: #FFFFFF;
-  padding: 50rpx 30rpx;
   border-radius: 24rpx;
+  padding: 32rpx 12rpx 24rpx;
   display: flex;
   flex-direction: column;
   align-items: center;
-  gap: 20rpx;
-  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
-  position: relative;
-  overflow: hidden;
-  transition: all 0.3s;
-}
-
-.action-item:active {
-  transform: translateY(-4rpx);
-  box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
+  gap: 18rpx;
 }
-
-.action-item::before {
-  content: '';
-  position: absolute;
-  top: 0;
-  left: 0;
-  right: 0;
-  height: 6rpx;
-}
-
-.action-item.purple::before { background: linear-gradient(90deg, #667EEA, #764BA2); }
-.action-item.pink::before { background: linear-gradient(90deg, #F093FB, #F5576C); }
-.action-item.blue::before { background: linear-gradient(90deg, #4FACFE, #00F2FE); }
-.action-item.green::before { background: linear-gradient(90deg, #43E97B, #38F9D7); }
-.action-item.orange::before { background: linear-gradient(90deg, #FF9A9E, #FAD0C4); }
-
-.action-icon {
-  width: 96rpx;
-  height: 96rpx;
-  border-radius: 24rpx;
+.action-icon-wrap {
+  width: 88rpx;
+  height: 88rpx;
+  border-radius: 50%;
+  background: #C6171E;
   display: flex;
   align-items: center;
   justify-content: center;
-  font-size: 56rpx;
-  background: #F8F9FA;
 }
-
+.action-item:active .action-icon-wrap {
+  background: #A81212;
+  transform: scale(0.94);
+}
 .action-label {
-  font-size: 28rpx;
+  font-size: 26rpx;
   font-weight: 500;
   color: #1A1A1A;
 }
 
-/* ===== 加载指示器 ===== */
-.loading {
-  position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  background: rgba(0, 0, 0, 0.3);
+/* ===== Info Bar ===== */
+.info-bar {
   display: flex;
-  align-items: center;
   justify-content: center;
-  z-index: 999;
+  gap: 40rpx;
+  padding: 16rpx 0 40rpx;
 }
-
-.spinner {
-  width: 80rpx;
-  height: 80rpx;
-  border: 6rpx solid rgba(255, 255, 255, 0.3);
-  border-top-color: #FFFFFF;
-  border-radius: 50%;
-  animation: spin 0.8s linear infinite;
+.info-item {
+  display: flex;
+  align-items: center;
+  gap: 8rpx;
+}
+.info-text {
+  font-size: 24rpx;
+  color: #999999;
+}
+.info-num {
+  font-weight: 600;
+  color: #666666;
 }
 
-@keyframes spin {
-  to { transform: rotate(360deg); }
+/* ===== Skeleton ===== */
+.skeleton-greeting {
+  width: 180rpx;
+  height: 30rpx;
+  background: #E8E8E8;
+  border-radius: 8rpx;
+  margin: 32rpx auto;
 }
+.skeleton-hero {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 0 0 44rpx;
+}
+.skeleton-line {
+  background: #E8E8E8;
+  border-radius: 8rpx;
+}
+.skeleton-line.hero-num {
+  width: 360rpx;
+  height: 64rpx;
+  margin-bottom: 16rpx;
+}
+.skeleton-line.hero-sub {
+  width: 160rpx;
+  height: 26rpx;
+}
+.skeleton-line.station-sk {
+  width: 100%;
+  height: 90rpx;
+  border-radius: 20rpx;
+  margin-bottom: 24rpx;
+}
+.skeleton-line.section-title {
+  width: 160rpx;
+  height: 28rpx;
+  margin: 0 auto 20rpx;
+}
+.skeleton-grid {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 16rpx;
+  margin-bottom: 16rpx;
+}
+.skeleton-cell {
+  height: 120rpx;
+  background: #E8E8E8;
+  border-radius: 24rpx;
+}
+.skeleton-cell.sm {
+  height: 100rpx;
+  border-radius: 20rpx;
+}
+.skeleton-actions {
+  display: flex;
+  gap: 16rpx;
+  margin-bottom: 28rpx;
+}
+.skeleton-action {
+  flex: 1;
+  height: 170rpx;
+  background: #E8E8E8;
+  border-radius: 24rpx;
+}
+
+/* ===== Error ===== */
+.error-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 120rpx 0 0;
+}
+.error-text { font-size: 28rpx; color: #999999; margin-top: 24rpx; }
+.error-hint { font-size: 24rpx; color: #B0B0B0; margin-top: 8rpx; }
+.retry-btn {
+  margin-top: 40rpx;
+  background: #C6171E;
+  padding: 16rpx 48rpx;
+  border-radius: 44rpx;
+}
+.retry-btn:active { background: #A81212; transform: scale(0.97); }
+.retry-btn-text { font-size: 28rpx; color: #FFFFFF; font-weight: 500; }
 </style>

+ 15 - 35
admin-mp/src/pages/login/login.vue

@@ -8,7 +8,7 @@
       
       <view class="input-group">
         <view class="input-label">
-          <text class="iconfont">📱</text>
+          <AppIcon name="smartphone" size="20" color="#C6171E" />
           <text class="label">手机号</text>
         </view>
         <input 
@@ -22,7 +22,7 @@
       
       <view class="input-group">
         <view class="input-label">
-          <text class="iconfont">🔒</text>
+          <AppIcon name="lock" size="20" color="#C6171E" />
           <text class="label">密码</text>
         </view>
         <input 
@@ -44,7 +44,7 @@
 <script setup>
 import { ref, onMounted } from 'vue'
 import { login } from '../../api/auth.js'
-import { getDataDictList } from '../../api/dict.js'
+import { loadDicts } from '../../utils/dict.js'
 import { storage, showToast } from '../../utils/index.js'
 import JSEncrypt from 'jsencrypt'
 
@@ -64,27 +64,9 @@ const encryptData = (str) => {
   return encryptor.encrypt(str)
 }
 
-// 加载字典数据(与admin-web保持一致)
+// 加载字典数据
 const loadDictionaries = async () => {
-  try {
-    const res = await getDataDictList()
-    if (res && res.code === 200) {
-      const list = res.data
-      // 按code分组,与admin-web保持一致
-      const dictGroup = {}
-      list.forEach(item => {
-        if (!dictGroup[item.code]) {
-          dictGroup[item.code] = []
-        }
-        dictGroup[item.code].push(item)
-      })
-      // 保存到缓存
-      storage.set('dicts', dictGroup)
-      console.log('字典数据加载成功:', dictGroup)
-    }
-  } catch (error) {
-    console.error('加载字典数据失败:', error)
-  }
+  await loadDicts()
 }
 
 const handleLogin = async () => {
@@ -148,7 +130,7 @@ const handleLogin = async () => {
 <style scoped>
 /* 登录容器 */
 .login-container {
-  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
+  background: #C6171E;
   display: flex;
   align-items: center;
   justify-content: center;
@@ -176,7 +158,7 @@ const handleLogin = async () => {
 .logo-text {
   font-size: 56rpx;
   font-weight: 700;
-  color: #667EEA;
+  color: #C6171E;
   margin-bottom: 16rpx;
   display: block;
   letter-spacing: 4rpx;
@@ -200,10 +182,8 @@ const handleLogin = async () => {
   margin-bottom: 20rpx;
 }
 
-.input-label .iconfont {
-  font-size: 32rpx;
+.input-label .app-icon {
   margin-right: 12rpx;
-  color: #667EEA;
 }
 
 .label {
@@ -222,39 +202,39 @@ const handleLogin = async () => {
   font-size: 28rpx;
   color: #1A1A1A;
   box-sizing: border-box;
-  background-color: #F8F9FA;
+  background-color: #F5F7FA;
   transition: all 0.3s;
 }
 
 .input-field:focus {
-  border-color: #667EEA;
+  border-color: #C6171E;
   background-color: #FFFFFF;
   outline: none;
-  box-shadow: 0 0 0 4rpx rgba(102, 126, 234, 0.1);
+  box-shadow: 0 0 0 4rpx rgba(198, 23, 30, 0.1);
 }
 
 /* 登录按钮 */
 .login-btn {
   width: 100%;
   height: 90rpx;
-  background: linear-gradient(90deg, #667EEA 0%, #764BA2 100%);
+  background: #C6171E;
   color: #FFFFFF;
   border: none;
   border-radius: 16rpx;
   font-size: 32rpx;
   font-weight: 600;
   margin-top: 60rpx;
-  box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.4);
+  box-shadow: 0 8rpx 24rpx rgba(198, 23, 30, 0.4);
   transition: all 0.3s;
 }
 
 .login-btn:active {
   transform: translateY(2rpx);
-  box-shadow: 0 6rpx 20rpx rgba(102, 126, 234, 0.5);
+  box-shadow: 0 6rpx 20rpx rgba(198, 23, 30, 0.25);
 }
 
 .login-btn:disabled {
-  background: linear-gradient(90deg, #CCCCCC 0%, #BBBBBB 100%);
+  background: #CCCCCC;
   box-shadow: none;
   opacity: 0.6;
 }

+ 68 - 121
admin-mp/src/pages/order/detail.vue

@@ -1,10 +1,6 @@
 <template>
   <view class="order-detail-container">
-    <!-- 顶部紫色导航栏 -->
-    <view class="header-nav" @click="goBack">
-      <text class="back-icon">←</text>
-      <text class="nav-title">订单详情</text>
-    </view>
+    <NavBar title="订单详情" @back="goBack" />
     
     <!-- 订单号和状态卡片 -->
     <view class="status-card">
@@ -116,16 +112,19 @@
     </view>
     
     <!-- 加载状态 -->
-    <view class="loading-overlay" v-if="loading">
-      <text class="loading-spinner">🔄</text>
+    <view class="loading-state" v-if="loading">
+      <view class="loading-spinner"></view>
       <text class="loading-text">加载中...</text>
     </view>
-    
+
     <!-- 空状态 -->
-    <view class="empty-state" v-if="!loading && !orderDetail.id">
-      <text class="empty-icon">📭</text>
-      <text class="empty-text">订单不存在</text>
-      <button class="refresh-btn" @click="loadOrderDetail">刷新</button>
+    <view class="empty-state" v-if="!loading && !orderDetail.orderId">
+      <view class="empty-icon-wrapper">
+        <AppIcon name="inbox" size="48" color="#B0B0B0" />
+      </view>
+      <text class="empty-text">未找到该订单</text>
+      <text class="empty-hint">请检查订单号是否正确,或返回列表重新选择</text>
+      <button class="empty-back-btn" @click="goBack">返回订单列表</button>
     </view>
   </view>
 </template>
@@ -133,7 +132,8 @@
 <script setup>
 import { ref, onMounted, computed } from 'vue'
 import { getOrderDetail, handleRefund as refundApi } from '../../api/order.js'
-import { formatTime, showToast, formatAmount, storage, fmtDictName, getDictColor } from '../../utils/index.js'
+import { formatTime, showToast, formatAmount, fmtDictName, getDictColor } from '../../utils/index.js'
+import dictUtil from '../../utils/dict.js'
 
 const orderId = ref('')
 const orderDetail = ref({})
@@ -173,24 +173,21 @@ const getOrderStatusStyle = (status) => {
 
 // 计算是否可以处理退款
 const canHandleRefund = computed(() => {
-  return orderDetail.value.orderStatus === '4' && orderDetail.value.refundLogId
+  const refundValue = dictUtil.getDictValue('WashOrder.orderStatus', '退款中')
+  return orderDetail.value.orderStatus === refundValue && orderDetail.value.refundLogId
 })
 
 // 加载订单详情
 const loadOrderDetail = async () => {
   if (!orderId.value) {
-    console.error('订单ID为空')
     showToast('订单ID不存在')
     loading.value = false
     return
   }
   
-  console.log('正在加载订单详情,订单ID:', orderId.value)
   loading.value = true
   try {
     const res = await getOrderDetail(orderId.value)
-    console.log('订单详情响应:', res)
-    
     if (res && res.code === 200) {
       let data = res.data
       
@@ -210,21 +207,7 @@ const loadOrderDetail = async () => {
         data.detail = data.detail
           .filter(item => item.amount > 0 || item.seconds > 0)
           .map(item => {
-            // 从字典中获取服务项目名称
-            let serviceName = item.name
-            try {
-              const dicts = storage.get('dicts')
-              if (dicts && dicts['WashOrder.feeType']) {
-                const feeTypeDict = dicts['WashOrder.feeType']
-                const dictItem = feeTypeDict.find(d => d.value == item.name)
-                if (dictItem) {
-                  serviceName = dictItem.name
-                }
-              }
-            } catch (e) {
-              console.error('解析服务项目字典失败:', e)
-            }
-            
+            const serviceName = dictUtil.getDictLabel('WashOrder.feeType', item.name)
             return {
               name: serviceName,
               seconds: formatDuration(item.seconds),
@@ -293,11 +276,7 @@ onMounted(() => {
   const currentPage = pages[pages.length - 1]
   const receivedOrderId = currentPage.options.orderId || ''
   
-  console.log('订单详情页接收到的参数:', currentPage.options)
-  console.log('订单ID:', receivedOrderId)
-  
   if (!receivedOrderId) {
-    console.error('未接收到订单ID')
     showToast('订单ID不存在,无法加载详情')
     loading.value = false
     return
@@ -312,49 +291,16 @@ onMounted(() => {
 /* 容器 */
 .order-detail-container {
   min-height: 100vh;
-  background-color: #F8F9FA;
+  background-color: #F5F7FA;
   padding-bottom: 120rpx;
 }
 
-/* 顶部紫色导航栏 */
-.header-nav {
-  display: flex;
-  align-items: center;
-  padding-top: calc(24rpx + var(--status-bar-height));
-  padding-right: 30rpx;
-  padding-bottom: 24rpx;
-  padding-left: 30rpx;
-  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
-  box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.2);
-  position: relative;
-  z-index: 10;
-}
-
-/* 确保内容不被导航栏遮挡 */
-.status-card {
-  margin-top: 40rpx;
-}
-
-.back-icon {
-  font-size: 40rpx;
-  color: #FFFFFF;
-  font-weight: 600;
-  margin-right: 16rpx;
-}
-
-.nav-title {
-  font-size: 34rpx;
-  color: #FFFFFF;
-  font-weight: 600;
-}
-
 /* 订单号和状态卡片 */
 .status-card {
   margin: 20rpx 30rpx;
   padding: 24rpx 30rpx;
   background: #FFFFFF;
-  border-radius: 20rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
+  border-radius: 48rpx;
 }
 
 .order-number {
@@ -386,14 +332,14 @@ onMounted(() => {
 }
 
 .status-tag {
-  font-size: 22rpx;
-  font-weight: 600;
-  padding: 6rpx 20rpx;
-  border-radius: 30rpx;
+  font-size: 24rpx;
+  font-weight: 500;
+  padding: 6rpx 24rpx;
+  border-radius: 200rpx;
 }
 
 .create-time {
-  font-size: 22rpx;
+  font-size: 24rpx;
   color: #999999;
 }
 
@@ -403,14 +349,13 @@ onMounted(() => {
 .service-card {
   margin: 20rpx 30rpx;
   background: #FFFFFF;
-  border-radius: 20rpx;
+  border-radius: 48rpx;
   overflow: hidden;
-  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
 }
 
 .card-title {
   padding: 24rpx 30rpx 20rpx;
-  font-size: 28rpx;
+  font-size: 32rpx;
   font-weight: 600;
   color: #1A1A1A;
   border-bottom: 1rpx solid #F0F0F0;
@@ -426,7 +371,7 @@ onMounted(() => {
   align-items: center;
   justify-content: space-between;
   padding: 16rpx 0;
-  border-bottom: 1rpx solid #F8F9FA;
+  border-bottom: 1rpx solid #F5F7FA;
 }
 
 .info-item:last-child {
@@ -434,12 +379,12 @@ onMounted(() => {
 }
 
 .item-label {
-  font-size: 26rpx;
+  font-size: 28rpx;
   color: #666666;
 }
 
 .item-value {
-  font-size: 26rpx;
+  font-size: 28rpx;
   color: #1A1A1A;
   font-weight: 500;
   text-align: right;
@@ -455,7 +400,7 @@ onMounted(() => {
   align-items: center;
   justify-content: space-between;
   padding: 16rpx 0;
-  border-bottom: 1rpx solid #F8F9FA;
+  border-bottom: 1rpx solid #F5F7FA;
 }
 
 .amount-item:last-child {
@@ -463,7 +408,7 @@ onMounted(() => {
 }
 
 .amount-label {
-  font-size: 26rpx;
+  font-size: 28rpx;
   color: #666666;
 }
 
@@ -475,7 +420,7 @@ onMounted(() => {
 
 .amount-value.primary {
   font-size: 32rpx;
-  color: #667EEA;
+  color: #C6171E;
   font-weight: 700;
 }
 
@@ -500,13 +445,13 @@ onMounted(() => {
 }
 
 .table-header .col {
-  font-size: 22rpx;
+  font-size: 24rpx;
   color: #999999;
-  font-weight: 600;
+  font-weight: 500;
 }
 
 .table-row {
-  border-bottom: 1rpx solid #F8F9FA;
+  border-bottom: 1rpx solid #F5F7FA;
 }
 
 .table-row:last-child {
@@ -514,7 +459,7 @@ onMounted(() => {
 }
 
 .table-row .col {
-  font-size: 24rpx;
+  font-size: 28rpx;
   color: #1A1A1A;
 }
 
@@ -555,51 +500,40 @@ onMounted(() => {
 .refund-btn {
   width: 100%;
   padding: 22rpx 0;
-  background: linear-gradient(90deg, #F5222D 0%, #FF4D4F 100%);
+  background: #C6171E;
   color: #FFFFFF;
   border: none;
   border-radius: 16rpx;
   font-size: 30rpx;
   font-weight: 600;
-  box-shadow: 0 6rpx 20rpx rgba(245, 34, 45, 0.35);
+  box-shadow: 0 4rpx 16rpx rgba(198, 23, 30, 0.25);
 }
 
 .refund-btn:active {
-  transform: translateY(2rpx);
-  opacity: 0.9;
+  background: #A81212;
+  transform: scale(0.97);
 }
 
 /* 加载状态 */
-.loading-overlay {
-  position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  background-color: rgba(0, 0, 0, 0.4);
-  backdrop-filter: blur(2px);
+.loading-state {
   display: flex;
   flex-direction: column;
   align-items: center;
   justify-content: center;
-  z-index: 999;
+  padding: 200rpx 0;
+  color: #999999;
 }
 
 .loading-spinner {
   font-size: 64rpx;
   animation: spin 1s linear infinite;
   margin-bottom: 32rpx;
-  color: #fff;
+  color: #B0B0B0;
 }
 
 .loading-text {
   font-size: 28rpx;
-  color: #fff;
-}
-
-@keyframes spin {
-  from { transform: rotate(0deg); }
-  to { transform: rotate(360deg); }
+  color: #999999;
 }
 
 /* 空状态 */
@@ -608,35 +542,48 @@ onMounted(() => {
   flex-direction: column;
   align-items: center;
   justify-content: center;
-  padding: 120rpx 0;
+  padding: 160rpx 60rpx;
   color: #999999;
+  text-align: center;
 }
 
-.empty-icon {
-  font-size: 120rpx;
-  margin-bottom: 32rpx;
-  opacity: 0.5;
+.empty-icon-wrapper {
+  margin-bottom: 24rpx;
 }
 
 .empty-text {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #1A1A1A;
+  margin-top: 24rpx;
+  margin-bottom: 16rpx;
+}
+
+.empty-hint {
   font-size: 28rpx;
+  color: #999999;
   margin-bottom: 40rpx;
+  line-height: 1.5;
 }
 
-.refresh-btn {
+.empty-back-btn {
   padding: 20rpx 60rpx;
-  background: linear-gradient(90deg, #667EEA 0%, #764BA2 100%);
+  background: #C6171E;
   color: #FFFFFF;
   border: none;
-  border-radius: 16rpx;
+  border-radius: 32rpx;
   font-size: 28rpx;
   font-weight: 500;
 }
 
-.refresh-btn:active {
-  transform: scale(0.95);
-  opacity: 0.9;
+.empty-back-btn:active {
+  background: #A81212;
+  transform: scale(0.97);
 }
 
+@keyframes spin {
+  from { transform: rotate(0deg); }
+  to { transform: rotate(360deg); }
+}
 
 </style>

+ 67 - 72
admin-mp/src/pages/order/list.vue

@@ -3,7 +3,7 @@
     <!-- 搜索栏 -->
     <view class="search-bar">
       <view class="search-input-wrapper">
-        <text class="search-icon">🔍</text>
+        <AppIcon name="search" :size="28" color="#999999" class="search-icon" />
         <input 
           type="text" 
           placeholder="请输入用户手机号" 
@@ -82,7 +82,7 @@
         <view class="order-footer">
           <text class="update-time">更新于: {{ formatTime(order.updateTime) }}</text>
           <button 
-            v-if="order.orderStatus == '4'" 
+            v-if="isRefundStatus(order.orderStatus)"
             class="refund-btn"
             @click.stop="handleRefund(order.refundLogId)"
           >
@@ -106,24 +106,30 @@
       
       <!-- 空状态 -->
       <view class="empty-state" v-if="orderList.length === 0 && !loading">
-        <text class="empty-icon">📭</text>
+        <view class="empty-icon-wrapper">
+          <AppIcon name="inbox" size="48" color="#B0B0B0" />
+        </view>
         <text class="empty-text">暂无订单数据</text>
-        <button class="refresh-btn" @click="loadOrderList">刷新</button>
+        <button class="empty-refresh-btn" @click="loadOrderList">刷新</button>
       </view>
       
       <!-- 加载状态 -->
       <view class="loading-state" v-if="loading && orderList.length === 0">
-        <text class="loading-spinner">🔄</text>
+        <view class="loading-spinner"></view>
         <text class="loading-text">加载中...</text>
       </view>
     </view>
+
+    <custom-tab-bar />
   </view>
 </template>
 
 <script setup>
 import { ref, onMounted, reactive } from 'vue'
+import CustomTabBar from '../../custom-tab-bar/index.vue'
 import { getOrderList, handleRefund as refundApi } from '../../api/order.js'
 import { formatTime, showToast, formatAmount, fmtDictName, getDictColor } from '../../utils/index.js'
+import dictUtil from '../../utils/dict.js'
 
 const orderList = ref([])
 const loading = ref(true)
@@ -146,12 +152,6 @@ const state = reactive({
   }
 })
 
-const loadMoreText = {
-  contentdown: '上拉加载更多',
-  contentrefresh: '正在加载...',
-  contentnomore: '没有更多数据了'
-}
-
 onMounted(() => {
   // 加载订单列表
   loadOrderList()
@@ -247,19 +247,13 @@ const handleSearch = () => {
 
 
 const viewOrderDetail = (orderId) => {
-  console.log('点击查看订单详情,订单ID:', orderId)
-  
   if (!orderId) {
-    console.error('订单ID为空,无法跳转')
     showToast('订单ID不存在')
     return
   }
-  
-  const targetUrl = `/pages/order/detail?orderId=${orderId}`
-  console.log('跳转到:', targetUrl)
-  
+
   uni.navigateTo({
-    url: targetUrl,
+    url: `/pages/order/detail?orderId=${orderId}`,
     fail: (err) => {
       console.error('跳转失败:', err)
       showToast('跳转失败,请重试')
@@ -296,6 +290,11 @@ const getOrderStatusStyle = (status) => {
   return {}
 }
 
+// 判断是否为退款状态
+const isRefundStatus = (status) => {
+  return status === dictUtil.getDictValue('WashOrder.orderStatus', '退款中')
+}
+
 
 
 // 下拉刷新处理函数
@@ -309,7 +308,7 @@ const onPullDownRefresh = () => {
 /* 容器样式 */
 .order-list-container {
   padding: 30rpx;
-  background-color: #F8F9FA;
+  background-color: #F5F7FA;
   min-height: 100vh;
   width: 100%;
   box-sizing: border-box;
@@ -323,7 +322,7 @@ const onPullDownRefresh = () => {
   background-color: #FFFFFF;
   border-radius: 24rpx;
   padding: 20rpx;
-  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
+  box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06);
   align-items: center;
 }
 
@@ -332,14 +331,14 @@ const onPullDownRefresh = () => {
   display: flex;
   align-items: center;
   background-color: #F5F5F5;
-  border-radius: 16rpx;
+  border-radius: 8px;
   padding: 0 24rpx;
   margin-right: 20rpx;
 }
 
 .search-icon {
-  font-size: 28rpx;
   margin-right: 16rpx;
+  flex-shrink: 0;
 }
 
 .search-bar input {
@@ -352,20 +351,26 @@ const onPullDownRefresh = () => {
   background-color: transparent;
 }
 
+.search-input-wrapper:focus-within {
+  box-shadow: 0 0 0 6rpx rgba(198,23,30,0.2);
+}
+
 .search-btn {
-  padding: 20rpx 40rpx;
-  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
+  height: 70rpx;
+  line-height: 70rpx;
+  padding: 0 40rpx;
+  background: #C6171E;
   color: #FFFFFF;
   border: none;
   border-radius: 16rpx;
   font-size: 28rpx;
   font-weight: 500;
-  transition: all 0.3s;
+  transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
 }
 
 .search-btn:active {
-  transform: scale(0.95);
-  opacity: 0.9;
+  background: #A81212;
+  transform: scale(0.97);
 }
 
 /* 订单列表样式 */
@@ -378,15 +383,14 @@ const onPullDownRefresh = () => {
 /* 订单卡片样式 */
 .order-card {
   background-color: #FFFFFF;
-  border-radius: 24rpx;
+  border-radius: 48rpx;
   padding: 30rpx;
-  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
-  transition: all 0.3s;
+  transition: box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1), transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
 }
 
 .order-card:active {
-  transform: translateY(-4rpx);
-  box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
+  transform: translateY(-2px);
+  box-shadow: 0 4px 6px rgba(0,0,0,0.07), 0 2px 4px rgba(0,0,0,0.06);
 }
 
 /* 订单头部 */
@@ -412,7 +416,7 @@ const onPullDownRefresh = () => {
 }
 
 .order-id {
-  font-size: 26rpx;
+  font-size: 28rpx;
   color: #1A1A1A;
   font-weight: 600;
   word-break: break-all;
@@ -421,10 +425,10 @@ const onPullDownRefresh = () => {
 
 /* 订单状态 */
 .order-status {
-  font-size: 22rpx;
-  padding: 8rpx 24rpx;
-  border-radius: 30rpx;
-  font-weight: 600;
+  font-size: 24rpx;
+  padding: 6rpx 24rpx;
+  border-radius: 200rpx;
+  font-weight: 500;
   white-space: nowrap;
 }
 
@@ -472,7 +476,7 @@ const onPullDownRefresh = () => {
   justify-content: space-between;
   margin-bottom: 24rpx;
   padding: 20rpx;
-  background: linear-gradient(135deg, #FFF7E6 0%, #FFE7BA 100%);
+  background: #FAF8F8;
   border-radius: 16rpx;
 }
 
@@ -496,7 +500,7 @@ const onPullDownRefresh = () => {
 
 .amount-value.highlight {
   font-size: 36rpx;
-  color: #E70316;
+  color: #C6171E;
   font-weight: 700;
 }
 
@@ -509,24 +513,24 @@ const onPullDownRefresh = () => {
 }
 
 .update-time {
-  font-size: 22rpx;
-  color: #CCCCCC;
+  font-size: 24rpx;
+  color: #B0B0B0;
 }
 
 .refund-btn {
   padding: 12rpx 32rpx;
-  background: linear-gradient(135deg, #F5222D 0%, #FF4D4F 100%);
+  background: #C6171E;
   color: #FFFFFF;
   border: none;
   border-radius: 16rpx;
   font-size: 24rpx;
   font-weight: 500;
-  transition: all 0.3s;
+  transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
 }
 
 .refund-btn:active {
-  transform: scale(0.95);
-  opacity: 0.9;
+  background: #A81212;
+  transform: scale(0.97);
 }
 
 /* 加载更多容器 */
@@ -541,14 +545,13 @@ const onPullDownRefresh = () => {
   color: #666666;
   font-size: 28rpx;
   background-color: #FFFFFF;
-  border-radius: 24rpx;
-  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
-  transition: all 0.3s;
+  border-radius: 48rpx;
+  transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
 }
 
 .load-more:active {
-  transform: translateY(-4rpx);
-  box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
+  transform: translateY(-2px);
+  box-shadow: 0 4px 6px rgba(0,0,0,0.07), 0 2px 4px rgba(0,0,0,0.06);
 }
 
 /* 空状态 */
@@ -557,36 +560,34 @@ const onPullDownRefresh = () => {
   flex-direction: column;
   align-items: center;
   justify-content: center;
-  padding: 120rpx 0;
+  padding: 100rpx 0;
   color: #999999;
   text-align: center;
 }
 
-.empty-icon {
-  font-size: 120rpx;
-  margin-bottom: 32rpx;
-  opacity: 0.5;
+.empty-icon-wrapper {
+  margin-bottom: 24rpx;
 }
 
 .empty-text {
   font-size: 28rpx;
-  margin-bottom: 40rpx;
+  color: #999999;
+  margin-bottom: 32rpx;
 }
 
-.refresh-btn {
+.empty-refresh-btn {
   padding: 20rpx 60rpx;
-  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
+  background: #C6171E;
   color: #FFFFFF;
   border: none;
-  border-radius: 16rpx;
+  border-radius: 32rpx;
   font-size: 28rpx;
   font-weight: 500;
-  transition: all 0.3s;
 }
 
-.refresh-btn:active {
-  transform: scale(0.95);
-  opacity: 0.9;
+.empty-refresh-btn:active {
+  background: #A81212;
+  transform: scale(0.97);
 }
 
 /* 加载状态 */
@@ -599,18 +600,12 @@ const onPullDownRefresh = () => {
   color: #999999;
 }
 
-.loading-spinner {
-  font-size: 64rpx;
-  margin-bottom: 32rpx;
-  animation: spin 1s linear infinite;
+.loading-text {
+  font-size: 28rpx;
 }
 
 @keyframes spin {
   from { transform: rotate(0deg); }
   to { transform: rotate(360deg); }
 }
-
-.loading-text {
-  font-size: 28rpx;
-}
 </style>

+ 7 - 11
admin-mp/src/pages/setting/device-binding.vue

@@ -1,7 +1,7 @@
 <template>
   <view class="device-binding-container">
     <view class="header-nav">
-      <text class="back-icon" @click="goBack">←</text>
+      <AppIcon name="chevron-left" size="20" color="#FFFFFF" />
       <text class="nav-title">设备绑定管理</text>
     </view>
 
@@ -72,7 +72,7 @@
     </scroll-view>
 
     <view class="loading-overlay" v-if="loading">
-      <text class="loading-spinner">🔄</text>
+      <view class="loading-spinner"></view>
       <text class="loading-text">处理中...</text>
     </view>
   </view>
@@ -214,7 +214,7 @@ const goBack = () => {
 <style scoped>
 .device-binding-container {
   min-height: 100vh;
-  background-color: #F8F9FA;
+  background-color: #F5F7FA;
   display: flex;
   flex-direction: column;
 }
@@ -226,7 +226,7 @@ const goBack = () => {
   padding-right: 30rpx;
   padding-bottom: 24rpx;
   padding-left: 30rpx;
-  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
+  background: #C6171E;
   color: #fff;
 }
 
@@ -258,7 +258,7 @@ const goBack = () => {
 }
 
 .tab.active {
-  color: #667EEA;
+  color: #C6171E;
   font-weight: 600;
 }
 
@@ -269,7 +269,7 @@ const goBack = () => {
   left: 25%;
   width: 50%;
   height: 4rpx;
-  background-color: #667EEA;
+  background-color: #C6171E;
 }
 
 .content-scroll {
@@ -333,7 +333,7 @@ const goBack = () => {
 }
 
 .action-btn {
-  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
+  background: #C6171E;
   color: #fff;
   margin-top: 40rpx;
   border-radius: 12rpx;
@@ -369,8 +369,4 @@ const goBack = () => {
   color: #fff;
 }
 
-@keyframes spin {
-  from { transform: rotate(0deg); }
-  to { transform: rotate(360deg); }
-}
 </style>

+ 7 - 11
admin-mp/src/pages/setting/device-config-detail.vue

@@ -3,7 +3,7 @@
     <!-- 顶部导航栏 -->
     <view class="header-nav">
       <view class="nav-left">
-        <text class="back-icon" @click="goBack">←</text>
+        <AppIcon name="chevron-left" size="20" color="#FFFFFF" />
       </view>
       <text class="nav-title">{{ isEditMode ? '编辑配置' : '添加配置' }}</text>
       <view class="nav-right">
@@ -341,7 +341,7 @@
     
     <!-- 加载状态 -->
     <view class="loading-overlay" v-if="loading">
-      <text class="loading-spinner">🔄</text>
+      <view class="loading-spinner"></view>
       <text class="loading-text">保存中...</text>
     </view>
   </view>
@@ -568,7 +568,7 @@ onMounted(() => {
   display: flex;
   flex-direction: column;
   height: 100vh;
-  background-color: #F8F9FA;
+  background-color: #F5F7FA;
 }
 
 /* 顶部导航栏 */
@@ -580,8 +580,8 @@ onMounted(() => {
   padding-right: 30rpx;
   padding-bottom: 24rpx;
   padding-left: 30rpx;
-  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
-  box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.2);
+  background: #C6171E;
+  box-shadow: 0 4rpx 16rpx rgba(198, 23, 30, 0.2);
 }
 
 .nav-left {
@@ -638,13 +638,13 @@ onMounted(() => {
   width: 100%;
   height: 88rpx;
   line-height: 88rpx;
-  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
+  background: #C6171E;
   color: #FFFFFF;
   border-radius: 44rpx;
   font-size: 30rpx;
   font-weight: 600;
   border: none;
-  box-shadow: 0 8rpx 20rpx rgba(102, 126, 234, 0.3);
+  box-shadow: 0 8rpx 20rpx rgba(198, 23, 30, 0.3);
 }
 
 .submit-btn:active {
@@ -827,8 +827,4 @@ onMounted(() => {
   background-color: #F0F0F0;
 }
 
-@keyframes spin {
-  from { transform: rotate(0deg); }
-  to { transform: rotate(360deg); }
-}
 </style>

+ 15 - 24
admin-mp/src/pages/setting/device-config.vue

@@ -1,13 +1,6 @@
 <template>
   <view class="device-config-container">
-    <!-- 顶部导航栏 -->
-    <view class="header-nav">
-      <view class="nav-left">
-        <text class="back-icon" @click="goBack">←</text>
-      </view>
-      <text class="nav-title" style="text-align: center;">设备参数模板</text>
-      <view class="nav-right"></view>
-    </view>
+    <NavBar title="设备参数模板" @back="goBack" />
     
     <!-- 配置列表 -->
     <view class="config-list">
@@ -25,26 +18,28 @@
           <text class="config-desc">{{ config.remark || '无描述' }}</text>
         </view>
         <view class="config-action">
-          <text class="arrow">→</text>
+          <AppIcon name="chevron-right" size="18" color="#CCCCCC" />
         </view>
       </view>
     </view>
     
     <!-- 空状态 -->
     <view class="empty-state" v-if="configList.length === 0 && !loading">
-      <text class="empty-icon">⚙️</text>
+      <view class="empty-icon-wrapper">
+        <AppIcon name="settings" size="48" color="#BFBFBF" />
+      </view>
       <text class="empty-text">暂无设备配置</text>
       <text class="empty-desc">点击右上角添加配置</text>
     </view>
     
     <!-- 悬浮添加按钮 -->
     <view class="fab-add" @click="navigateToAddConfig">
-      <text class="plus-icon">+</text>
+      <AppIcon name="plus" size="28" color="#FFFFFF" />
     </view>
     
     <!-- 加载状态 -->
     <view class="loading-overlay" v-if="loading">
-      <text class="loading-spinner">🔄</text>
+      <view class="loading-spinner"></view>
       <text class="loading-text">加载中...</text>
     </view>
   </view>
@@ -115,7 +110,7 @@ const goBack = () => {
 <style scoped>
 .device-config-container {
   min-height: 100vh;
-  background-color: #F8F9FA;
+  background-color: #F5F7FA;
   padding-bottom: 180rpx; /* 关键:间距改在这里,确保滚动到底部不被按钮遮挡 */
   box-sizing: border-box;
 }
@@ -129,8 +124,8 @@ const goBack = () => {
   padding-right: 30rpx;
   padding-bottom: 24rpx;
   padding-left: 30rpx;
-  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
-  box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.2);
+  background: #C6171E;
+  box-shadow: 0 4rpx 16rpx rgba(198, 23, 30, 0.2);
 }
 
 .nav-left {
@@ -166,19 +161,19 @@ const goBack = () => {
   bottom: 60rpx;
   width: 110rpx;
   height: 110rpx;
-  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
+  background: #C6171E;
   border-radius: 50%;
   display: flex;
   align-items: center;
   justify-content: center;
-  box-shadow: 0 10rpx 30rpx rgba(102, 126, 234, 0.4);
+  box-shadow: 0 10rpx 30rpx rgba(198, 23, 30, 0.4);
   z-index: 99;
   transition: all 0.3s;
 }
 
 .fab-add:active {
   transform: translateX(-50%) scale(0.9);
-  box-shadow: 0 4rpx 15rpx rgba(102, 126, 234, 0.3);
+  box-shadow: 0 4rpx 15rpx rgba(198, 23, 30, 0.3);
 }
 
 .plus-icon {
@@ -218,8 +213,8 @@ const goBack = () => {
 .id-badge {
   display: inline-block;
   font-size: 22rpx;
-  color: #667EEA;
-  background-color: rgba(102, 126, 234, 0.1);
+  color: #C6171E;
+  background-color: rgba(198, 23, 30, 0.1);
   padding: 6rpx 16rpx;
   border-radius: 8rpx;
   font-weight: 600;
@@ -316,8 +311,4 @@ const goBack = () => {
   color: #fff;
 }
 
-@keyframes spin {
-  from { transform: rotate(0deg); }
-  to { transform: rotate(360deg); }
-}
 </style>

+ 76 - 6
admin-mp/src/pages/setting/index.vue

@@ -14,7 +14,7 @@
           <text class="menu-title">平台费率配置</text>
           <text class="menu-desc">设置平台相关费率及提现手续费</text>
         </view>
-        <text class="menu-arrow">→</text>
+        <AppIcon name="chevron-right" size="14" color="#CCCCCC" />
       </view>
       
       <view class="menu-item" @click="navigateToDeviceConfig">
@@ -22,7 +22,7 @@
           <text class="menu-title">设备参数模板</text>
           <text class="menu-desc">管理设备参数模板(价格、时间、模式等)</text>
         </view>
-        <text class="menu-arrow">→</text>
+        <AppIcon name="chevron-right" size="14" color="#CCCCCC" />
       </view>
 
       <view class="menu-item" @click="navigateToDeviceBinding">
@@ -30,7 +30,62 @@
           <text class="menu-title">设备绑定管理</text>
           <text class="menu-desc">设备与参数模板、费率模板的绑定</text>
         </view>
-        <text class="menu-arrow">→</text>
+        <AppIcon name="chevron-right" size="14" color="#CCCCCC" />
+      </view>
+    </view>
+
+    <!-- 系统管理菜单 -->
+    <view class="section-divider">
+      <text class="divider-text">系统管理</text>
+    </view>
+
+    <view class="setting-menu">
+      <view class="menu-item" @click="navigateTo('/pages/station/list')">
+        <view class="menu-info">
+          <text class="menu-title">站点清单</text>
+          <text class="menu-desc">查看管理所有洗车站点</text>
+        </view>
+        <AppIcon name="chevron-right" size="14" color="#CCCCCC" />
+      </view>
+
+      <view class="menu-item" @click="navigateTo('/pages/user/list')">
+        <view class="menu-info">
+          <text class="menu-title">用户列表</text>
+          <text class="menu-desc">查看App用户信息及余额</text>
+        </view>
+        <AppIcon name="chevron-right" size="14" color="#CCCCCC" />
+      </view>
+
+      <view class="menu-item" @click="navigateTo('/pages/system/feedback')">
+        <view class="menu-info">
+          <text class="menu-title">反馈上报</text>
+          <text class="menu-desc">查看用户提交的反馈信息</text>
+        </view>
+        <AppIcon name="chevron-right" size="14" color="#CCCCCC" />
+      </view>
+
+      <view class="menu-item" @click="navigateTo('/pages/system/notice')">
+        <view class="menu-info">
+          <text class="menu-title">系统公告</text>
+          <text class="menu-desc">管理系统公告通知</text>
+        </view>
+        <AppIcon name="chevron-right" size="14" color="#CCCCCC" />
+      </view>
+
+      <view class="menu-item" @click="navigateTo('/pages/system/log')">
+        <view class="menu-info">
+          <text class="menu-title">操作日志</text>
+          <text class="menu-desc">查看后台操作审计日志</text>
+        </view>
+        <AppIcon name="chevron-right" size="14" color="#CCCCCC" />
+      </view>
+
+      <view class="menu-item" @click="navigateTo('/pages/system/dict')">
+        <view class="menu-info">
+          <text class="menu-title">数据字典</text>
+          <text class="menu-desc">查看系统数据字典配置</text>
+        </view>
+        <AppIcon name="chevron-right" size="14" color="#CCCCCC" />
       </view>
     </view>
   </view>
@@ -59,12 +114,17 @@ const navigateToDeviceBinding = () => {
     url: '/pages/setting/device-binding'
   })
 }
+
+// 通用导航
+const navigateTo = (url) => {
+  uni.navigateTo({ url })
+}
 </script>
 
 <style scoped>
 .setting-container {
   min-height: 100vh;
-  background-color: #F8F9FA;
+  background-color: #F5F7FA;
 }
 
 /* 顶部导航栏 */
@@ -76,8 +136,8 @@ const navigateToDeviceBinding = () => {
   padding-right: 30rpx;
   padding-bottom: 24rpx;
   padding-left: 30rpx;
-  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
-  box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.2);
+  background: #C6171E;
+  box-shadow: 0 4rpx 16rpx rgba(198, 23, 30, 0.2);
 }
 
 .nav-left {
@@ -139,4 +199,14 @@ const navigateToDeviceBinding = () => {
   font-size: 30rpx;
   color: #CCCCCC;
 }
+
+/* 分组分隔 */
+.section-divider {
+  padding: 30rpx 20rpx 20rpx;
+}
+.divider-text {
+  font-size: 26rpx;
+  color: #999999;
+  font-weight: 500;
+}
 </style>

+ 12 - 69
admin-mp/src/pages/setting/rate-config-detail.vue

@@ -1,15 +1,10 @@
 <template>
   <view class="rate-config-detail-container">
-    <!-- 顶部导航栏 -->
-    <view class="header-nav">
-      <view class="nav-left">
-        <text class="back-icon" @click="goBack">←</text>
-      </view>
-      <text class="nav-title">{{ isEditMode ? '编辑模板' : '添加模板' }}</text>
-      <view class="nav-right">
-        <text class="save-btn" @click="saveConfig">保存</text>
-      </view>
-    </view>
+    <NavBar title="费率模板详情" @back="goBack">
+      <template #right>
+        <text class="nav-save" @click="saveConfig">保存</text>
+      </template>
+    </NavBar>
     
     <!-- 表单内容 -->
     <scroll-view class="form-content" scroll-y>
@@ -64,7 +59,7 @@
 
     <!-- 加载状态 -->
     <view class="loading-overlay" v-if="loading">
-      <text class="loading-spinner">🔄</text>
+      <view class="loading-spinner"></view>
       <text class="loading-text">处理中...</text>
     </view>
   </view>
@@ -167,54 +162,14 @@ const goBack = () => {
   display: flex;
   flex-direction: column;
   height: 100vh;
-  background-color: #F8F9FA;
-}
-
-.header-nav {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  padding-top: calc(24rpx + var(--status-bar-height));
-  padding-right: 30rpx;
-  padding-bottom: 24rpx;
-  padding-left: 30rpx;
-  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
-  box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.2);
-}
-
-.nav-left {
-  width: 180rpx;
-  display: flex;
-  align-items: center;
-}
-
-.back-icon {
-  font-size: 40rpx;
-  color: #FFFFFF;
-  font-weight: 600;
+  background-color: #F5F7FA;
 }
 
-.nav-title {
-  font-size: 34rpx;
-  color: #FFFFFF;
-  font-weight: 600;
-  flex: 1;
-  text-align: center;
-  white-space: nowrap;
-}
-
-.nav-right {
-  width: 180rpx;
-  display: flex;
-  align-items: center;
-  justify-content: flex-end;
-}
-
-.save-btn {
+/* 右侧保存按钮 */
+.nav-save {
   font-size: 30rpx;
   color: #FFFFFF;
   font-weight: 600;
-  text-align: right;
 }
 
 .form-content {
@@ -236,13 +191,13 @@ const goBack = () => {
   width: 100%;
   height: 88rpx;
   line-height: 88rpx;
-  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
+  background: #C6171E;
   color: #FFFFFF;
   border-radius: 44rpx;
   font-size: 30rpx;
   font-weight: 600;
   border: none;
-  box-shadow: 0 8rpx 20rpx rgba(102, 126, 234, 0.3);
+  box-shadow: 0 8rpx 20rpx rgba(198, 23, 30, 0.3);
 }
 
 .submit-btn:active {
@@ -255,7 +210,7 @@ const goBack = () => {
   border-radius: 16rpx;
   padding: 24rpx;
   margin-bottom: 20rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
+  box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06);
 }
 
 .form-item {
@@ -298,20 +253,8 @@ const goBack = () => {
   z-index: 999;
 }
 
-.loading-spinner {
-  font-size: 64rpx;
-  animation: spin 1s linear infinite;
-  margin-bottom: 32rpx;
-  color: #fff;
-}
-
 .loading-text {
   font-size: 28rpx;
   color: #fff;
 }
-
-@keyframes spin {
-  from { transform: rotate(0deg); }
-  to { transform: rotate(360deg); }
-}
 </style>

+ 14 - 88
admin-mp/src/pages/setting/rate-config.vue

@@ -1,13 +1,6 @@
 <template>
   <view class="rate-config-container">
-    <!-- 顶部导航栏 -->
-    <view class="header-nav">
-      <view class="nav-left">
-        <text class="back-icon" @click="goBack">←</text>
-      </view>
-      <text class="nav-title">价格模板管理</text>
-      <view class="nav-right"></view>
-    </view>
+    <NavBar title="平台费率配置" @back="goBack" />
     
     <!-- 模板列表 -->
     <view class="config-list">
@@ -23,26 +16,28 @@
         </view>
         <view class="config-status">
           <text class="status-text active">ID: {{ item.id }}</text>
-          <text class="arrow">→</text>
+          <AppIcon name="chevron-right" size="18" color="#CCCCCC" />
         </view>
       </view>
     </view>
     
     <!-- 空状态 -->
     <view class="empty-state" v-if="rateList.length === 0 && !loading">
-      <text class="empty-icon">💰</text>
+      <view class="empty-icon-wrapper">
+        <AppIcon name="dollar" size="48" color="#BFBFBF" />
+      </view>
       <text class="empty-text">暂无价格模板</text>
       <text class="empty-desc">点击右上角添加模板</text>
     </view>
     
     <!-- 悬浮添加按钮 -->
     <view class="fab-add" @click="navigateToAddConfig">
-      <text class="plus-icon">+</text>
+      <AppIcon name="plus" size="28" color="#FFFFFF" />
     </view>
     
     <!-- 加载状态 -->
     <view class="loading-overlay" v-if="loading">
-      <text class="loading-spinner">🔄</text>
+      <view class="loading-spinner"></view>
       <text class="loading-text">加载中...</text>
     </view>
   </view>
@@ -106,49 +101,11 @@ onMounted(() => {
 <style scoped>
 .rate-config-container {
   min-height: 100vh;
-  background-color: #F8F9FA;
+  background-color: #F5F7FA;
   padding-bottom: 180rpx;
   box-sizing: border-box;
 }
 
-/* 顶部导航栏 */
-.header-nav {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  padding-top: calc(24rpx + var(--status-bar-height));
-  padding-right: 30rpx;
-  padding-bottom: 24rpx;
-  padding-left: 30rpx;
-  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
-  box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.2);
-}
-
-.nav-left {
-  width: 180rpx;
-  display: flex;
-  align-items: center;
-}
-
-.back-icon {
-  font-size: 40rpx;
-  color: #FFFFFF;
-  font-weight: 600;
-}
-
-.nav-title {
-  font-size: 34rpx;
-  color: #FFFFFF;
-  font-weight: 600;
-  flex: 1;
-  text-align: center;
-  white-space: nowrap;
-}
-
-.nav-right {
-  width: 180rpx;
-}
-
 /* 悬浮添加按钮 */
 .fab-add {
   position: fixed;
@@ -157,26 +114,19 @@ onMounted(() => {
   bottom: 60rpx;
   width: 110rpx;
   height: 110rpx;
-  background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);
+  background: #C6171E;
   border-radius: 50%;
   display: flex;
   align-items: center;
   justify-content: center;
-  box-shadow: 0 10rpx 30rpx rgba(102, 126, 234, 0.4);
+  box-shadow: 0 10rpx 30rpx rgba(198, 23, 30, 0.4);
   z-index: 99;
   transition: all 0.3s;
 }
 
 .fab-add:active {
   transform: translateX(-50%) scale(0.9);
-  box-shadow: 0 4rpx 15rpx rgba(102, 126, 234, 0.3);
-}
-
-.plus-icon {
-  font-size: 70rpx;
-  color: #FFFFFF;
-  font-weight: 300;
-  margin-top: -6rpx;
+  box-shadow: 0 4rpx 15rpx rgba(198, 23, 30, 0.3);
 }
 
 /* 列表 */
@@ -184,8 +134,7 @@ onMounted(() => {
   margin: 20rpx;
   background-color: #FFFFFF;
   border-radius: 16rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
-  /* 移除内部 padding */
+  box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06);
 }
 
 .config-item {
@@ -225,8 +174,8 @@ onMounted(() => {
 
 .status-text {
   font-size: 22rpx;
-  color: #667EEA;
-  background-color: rgba(102, 126, 234, 0.1);
+  color: #C6171E;
+  background-color: rgba(198, 23, 30, 0.1);
   padding: 4rpx 12rpx;
   border-radius: 12rpx;
   margin-right: 12rpx;
@@ -237,11 +186,6 @@ onMounted(() => {
   background-color: rgba(82, 196, 26, 0.1);
 }
 
-.arrow {
-  font-size: 30rpx;
-  color: #CCCCCC;
-}
-
 /* 空状态 */
 .empty-state {
   display: flex;
@@ -252,12 +196,6 @@ onMounted(() => {
   color: #999999;
 }
 
-.empty-icon {
-  font-size: 120rpx;
-  margin-bottom: 32rpx;
-  opacity: 0.5;
-}
-
 .empty-text {
   font-size: 28rpx;
   margin-bottom: 16rpx;
@@ -283,20 +221,8 @@ onMounted(() => {
   z-index: 999;
 }
 
-.loading-spinner {
-  font-size: 64rpx;
-  animation: spin 1s linear infinite;
-  margin-bottom: 32rpx;
-  color: #fff;
-}
-
 .loading-text {
   font-size: 28rpx;
   color: #fff;
 }
-
-@keyframes spin {
-  from { transform: rotate(0deg); }
-  to { transform: rotate(360deg); }
-}
 </style>

+ 287 - 0
admin-mp/src/pages/station/detail.vue

@@ -0,0 +1,287 @@
+<template>
+  <view class="detail-container">
+    <!-- 顶部导航栏 -->
+    <view class="header-nav">
+      <view class="nav-left" @click="goBack">
+        <AppIcon name="chevron-left" size="20" color="#FFFFFF" />
+      </view>
+      <text class="nav-title">{{ stationName || '站点详情' }}</text>
+      <view class="nav-right"></view>
+    </view>
+
+    <!-- 加载状态 -->
+    <view class="loading-overlay" v-if="loading">
+      <view class="loading-spinner"></view>
+      <text class="loading-text">加载中...</text>
+    </view>
+
+    <!-- 站点详情 -->
+    <view class="detail-content" v-if="!loading && detail">
+      <!-- 站点图片 -->
+      <view class="image-section" v-if="detail.pictures && detail.pictures.length > 0">
+        <image
+          v-for="(pic, index) in detail.pictures"
+          :key="index"
+          :src="pic"
+          mode="aspectFill"
+          class="station-image"
+          @click="previewImage(pic)" />
+      </view>
+
+      <!-- 基本信息卡片 -->
+      <view class="info-card">
+        <text class="card-title">基本信息</text>
+        <view class="info-list">
+          <view class="info-item">
+            <text class="item-label">站点ID</text>
+            <text class="item-value">{{ detail.stationId || '-' }}</text>
+          </view>
+          <view class="info-item">
+            <text class="item-label">站点名称</text>
+            <text class="item-value">{{ detail.stationName || '-' }}</text>
+          </view>
+          <view class="info-item">
+            <text class="item-label">站点状态</text>
+            <text class="item-value status" :style="getStatusStyle(detail.stationStatus)">{{ fmtDictName('WashStation.status', detail.stationStatus) }}</text>
+          </view>
+          <view class="info-item">
+            <text class="item-label">站点类型</text>
+            <text class="item-value">{{ fmtDictName('WashStation.type', detail.stationType) }}</text>
+          </view>
+          <view class="info-item">
+            <text class="item-label">地址</text>
+            <text class="item-value">{{ detail.address || '-' }}</text>
+          </view>
+          <view class="info-item">
+            <text class="item-label">服务电话</text>
+            <text class="item-value">{{ detail.serviceTel || '-' }}</text>
+          </view>
+          <view class="info-item">
+            <text class="item-label">站点电话</text>
+            <text class="item-value">{{ detail.stationTel || '-' }}</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 运营信息卡片 -->
+      <view class="info-card">
+        <text class="card-title">运营信息</text>
+        <view class="info-list">
+          <view class="info-item">
+            <text class="item-label">营业时间</text>
+            <text class="item-value">{{ detail.businessHours || '-' }}</text>
+          </view>
+          <view class="info-item">
+            <text class="item-label">工位数量</text>
+            <text class="item-value">{{ detail.parkingNum || '-' }}</text>
+          </view>
+          <view class="info-item">
+            <text class="item-label">停车费用</text>
+            <text class="item-value">{{ detail.parkingFee || '-' }}</text>
+          </view>
+          <view class="info-item">
+            <text class="item-label">备注</text>
+            <text class="item-value">{{ detail.remark || '-' }}</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 位置信息 -->
+      <view class="info-card" v-if="detail.stationLat || detail.stationLng">
+        <text class="card-title">位置信息</text>
+        <view class="info-list">
+          <view class="info-item">
+            <text class="item-label">经度</text>
+            <text class="item-value">{{ detail.stationLng || '-' }}</text>
+          </view>
+          <view class="info-item">
+            <text class="item-label">纬度</text>
+            <text class="item-value">{{ detail.stationLat || '-' }}</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 时间信息 -->
+      <view class="info-card">
+        <text class="card-title">时间信息</text>
+        <view class="info-list">
+          <view class="info-item">
+            <text class="item-label">创建时间</text>
+            <text class="item-value">{{ formatTime(detail.createTime) }}</text>
+          </view>
+          <view class="info-item">
+            <text class="item-label">更新时间</text>
+            <text class="item-value">{{ formatTime(detail.updateTime) }}</text>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 空状态 -->
+    <view class="empty-state" v-if="!loading && !detail">
+      <view class="empty-icon-wrapper"><AppIcon name="home" size="48" color="#BFBFBF" /></view>
+      <text class="empty-text">站点信息不存在</text>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import { getStationDetail } from '../../api/station.js'
+import { formatTime, fmtDictName, getDictColor } from '../../utils/index.js'
+
+const loading = ref(true)
+const detail = ref(null)
+const stationName = ref('')
+
+onLoad((options) => {
+  const id = options.id || ''
+  stationName.value = decodeURIComponent(options.name || '')
+  if (id) {
+    loadDetail(id)
+  }
+})
+
+const goBack = () => {
+  uni.navigateBack()
+}
+
+const getStatusStyle = (status) => {
+  const color = getDictColor('WashStation.status', status)
+  if (color) return { color, backgroundColor: `${color}1A` }
+  return {}
+}
+
+const loadDetail = async (id) => {
+  loading.value = true
+  try {
+    const res = await getStationDetail(id)
+    if (res && res.code === 200) {
+      detail.value = res.data
+      if (!stationName.value && res.data.stationName) {
+        stationName.value = res.data.stationName
+      }
+    }
+  } catch (error) {
+    console.error('加载站点详情失败:', error)
+  } finally {
+    loading.value = false
+  }
+}
+
+const previewImage = (url) => {
+  uni.previewImage({
+    urls: detail.value.pictures,
+    current: url
+  })
+}
+</script>
+
+<style scoped>
+.detail-container {
+  min-height: 100vh;
+  background-color: #F5F7FA;
+  padding-bottom: 100rpx;
+}
+
+.header-nav {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding-top: calc(24rpx + var(--status-bar-height));
+  padding-right: 30rpx;
+  padding-bottom: 24rpx;
+  padding-left: 30rpx;
+  background: #C6171E;
+}
+.nav-left, .nav-right { width: 120rpx; }
+.back-btn { font-size: 36rpx; color: #FFFFFF; }
+.nav-title { font-size: 34rpx; color: #FFFFFF; font-weight: 600; }
+
+.loading-overlay {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 200rpx 0;
+}
+.loading-spinner {
+  width: 60rpx;
+  height: 60rpx;
+  border: 4rpx solid #E0E0E0;
+  border-top-color: #C6171E;
+  border-radius: 50%;
+  animation: spin 0.8s linear infinite;
+}
+.loading-text { font-size: 28rpx; color: #999999; margin-top: 20rpx; }
+
+.image-section {
+  display: flex;
+  padding: 20rpx;
+  gap: 12rpx;
+  overflow-x: auto;
+}
+.station-image {
+  width: 280rpx;
+  height: 200rpx;
+  border-radius: 16rpx;
+  flex-shrink: 0;
+}
+
+.info-card {
+  background-color: #FFFFFF;
+  border-radius: 20rpx;
+  padding: 24rpx;
+  margin: 0 20rpx 20rpx;
+  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
+}
+.card-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #1A1A1A;
+  margin-bottom: 20rpx;
+  display: block;
+  padding-bottom: 12rpx;
+  border-bottom: 1rpx solid #F0F0F0;
+}
+.info-list {
+  display: flex;
+  flex-direction: column;
+}
+.info-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 12rpx 0;
+  border-bottom: 1rpx solid #F8F8F8;
+}
+.info-item:last-child {
+  border-bottom: none;
+}
+.item-label {
+  font-size: 26rpx;
+  color: #999999;
+}
+.item-value {
+  font-size: 26rpx;
+  color: #1A1A1A;
+  max-width: 400rpx;
+  text-align: right;
+}
+.item-value.status {
+  font-size: 22rpx;
+  padding: 6rpx 16rpx;
+  border-radius: 20rpx;
+  font-weight: 500;
+}
+
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 200rpx 0;
+}
+.empty-icon { font-size: 120rpx; margin-bottom: 20rpx; }
+.empty-text { font-size: 28rpx; color: #999999; }
+</style>

+ 369 - 0
admin-mp/src/pages/station/list.vue

@@ -0,0 +1,369 @@
+<template>
+  <view class="station-container">
+    <!-- 顶部导航栏 -->
+    <view class="header-nav">
+      <view class="nav-left" @click="goBack">
+        <AppIcon name="chevron-left" size="20" color="#FFFFFF" />
+      </view>
+      <text class="nav-title">站点清单</text>
+      <view class="nav-right"></view>
+    </view>
+
+    <!-- 搜索栏 -->
+    <view class="search-bar">
+      <view class="search-input-wrapper">
+        <AppIcon name="search" size="16" color="#999999" class="search-icon" />
+        <input type="text" placeholder="搜索站点名称或地址" v-model="searchKeyword" @confirm="handleSearch" />
+      </view>
+      <button class="search-btn" @click="handleSearch">搜索</button>
+    </view>
+
+    <!-- 状态筛选 -->
+    <view class="filter-bar">
+      <view
+        v-for="(option, index) in statusFilterOptions"
+        :key="index"
+        class="filter-item"
+        :class="{ active: activeStatus === option.value }"
+        @click="handleStatusChange(option.value)">
+        <text>{{ option.label }}</text>
+      </view>
+    </view>
+
+    <!-- 站点列表 -->
+    <view class="station-list" v-if="list.length > 0">
+      <view
+        class="station-item"
+        v-for="(item, index) in list"
+        :key="index"
+        @click="viewDetail(item)">
+        <view class="item-header">
+          <view class="item-left">
+            <text class="item-name">{{ item.stationName || '-' }}</text>
+            <text class="item-id">ID: {{ item.stationId || '-' }}</text>
+          </view>
+          <text class="status-tag" :style="getStatusStyle(item.stationStatus)">{{ fmtDictName('WashStation.status', item.stationStatus) }}</text>
+        </view>
+        <view class="item-content">
+          <view class="info-row">
+            <text class="info-label">站点类型</text>
+            <text class="info-value">{{ fmtDictName('WashStation.type', item.stationType) }}</text>
+          </view>
+          <view class="info-row">
+            <text class="info-label">地址</text>
+            <text class="info-value addr">{{ item.address || '-' }}</text>
+          </view>
+          <view class="info-row">
+            <text class="info-label">服务电话</text>
+            <text class="info-value">{{ item.serviceTel || '-' }}</text>
+          </view>
+          <view class="info-row">
+            <text class="info-label">工位数量</text>
+            <text class="info-value">{{ item.parkingNum || '-' }}</text>
+          </view>
+          <view class="info-row">
+            <text class="info-label">营业时间</text>
+            <text class="info-value">{{ item.businessHours || '-' }}</text>
+          </view>
+        </view>
+        <view class="item-footer">
+          <text class="footer-time">{{ formatTime(item.createTime) }}</text>
+          <view class="footer-arrow"><text>查看详情</text><AppIcon name="chevron-right" size="12" color="#999999" /></view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 加载更多 -->
+    <view class="load-more" v-if="list.length > 0">
+      <text class="load-more-text" v-if="loadMoreStatus === 'more'" @click="loadMore">上拉加载更多</text>
+      <text class="load-more-text" v-else-if="loadMoreStatus === 'loading'">正在加载...</text>
+      <text class="load-more-text" v-else>没有更多数据了</text>
+    </view>
+
+    <!-- 空状态 -->
+    <view class="empty-state" v-if="list.length === 0 && !loading">
+      <view class="empty-icon-wrapper"><AppIcon name="home" size="48" color="#BFBFBF" /></view>
+      <text class="empty-text">暂无站点数据</text>
+    </view>
+
+    <!-- 加载状态 -->
+    <view class="loading-state" v-if="loading && list.length === 0">
+      <view class="loading-spinner"></view>
+      <text class="loading-text">加载中...</text>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, onMounted, computed } from 'vue'
+import { getStationList } from '../../api/station.js'
+import { formatTime, fmtDictName, getDictColor } from '../../utils/index.js'
+import dictUtil from '../../utils/dict.js'
+
+const list = ref([])
+const loading = ref(true)
+const page = ref(1)
+const pageSize = ref(10)
+const hasMore = ref(true)
+const loadMoreStatus = ref('more')
+const searchKeyword = ref('')
+const activeStatus = ref('')
+
+const statusFilterOptions = computed(() => dictUtil.getDictFilterOptions('WashStation.status'))
+
+const goBack = () => {
+  uni.navigateBack()
+}
+
+const getStatusStyle = (status) => {
+  const color = getDictColor('WashStation.status', status)
+  if (color) return { color, backgroundColor: `${color}1A` }
+  return {}
+}
+
+const loadData = async (isLoadMore = false) => {
+  if (!isLoadMore) {
+    page.value = 1
+    list.value = []
+    hasMore.value = true
+    loadMoreStatus.value = 'more'
+  } else {
+    loadMoreStatus.value = 'loading'
+  }
+
+  loading.value = true
+  try {
+    const params = {
+      pageNum: page.value,
+      pageSize: pageSize.value
+    }
+    if (searchKeyword.value) params.stationName = searchKeyword.value
+    if (activeStatus.value !== '') params.stationStatus = activeStatus.value
+
+    const res = await getStationList(params)
+    if (res && res.code === 200) {
+      const data = res.data
+      const records = data.records || data.list || data
+      const total = data.total || 0
+
+      const totalPages = Math.ceil(total / pageSize.value)
+      hasMore.value = page.value < totalPages
+      loadMoreStatus.value = hasMore.value ? 'more' : 'noMore'
+
+      if (isLoadMore) {
+        list.value = [...list.value, ...records]
+        page.value++
+      } else {
+        list.value = records
+        page.value = 2
+      }
+    }
+  } catch (error) {
+    console.error('加载站点列表失败:', error)
+  } finally {
+    loading.value = false
+  }
+}
+
+const loadMore = () => {
+  if (hasMore.value && !loading.value) {
+    loadData(true)
+  }
+}
+
+const handleSearch = () => {
+  loadData()
+}
+
+const handleStatusChange = (status) => {
+  activeStatus.value = status
+  loadData()
+}
+
+const viewDetail = (item) => {
+  uni.navigateTo({
+    url: `/pages/station/detail?id=${item.stationId || item.id}&name=${encodeURIComponent(item.stationName || '')}`
+  })
+}
+
+onMounted(() => {
+  loadData()
+})
+</script>
+
+<style scoped>
+.station-container {
+  min-height: 100vh;
+  background-color: #F5F7FA;
+  padding-bottom: 100rpx;
+}
+
+.header-nav {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding-top: calc(24rpx + var(--status-bar-height));
+  padding-right: 30rpx;
+  padding-bottom: 24rpx;
+  padding-left: 30rpx;
+  background: #C6171E;
+}
+.nav-left, .nav-right { width: 120rpx; }
+.back-btn { font-size: 36rpx; color: #FFFFFF; }
+.nav-title { font-size: 34rpx; color: #FFFFFF; font-weight: 600; }
+
+.search-bar {
+  display: flex;
+  align-items: center;
+  padding: 20rpx;
+  background-color: #FFFFFF;
+}
+.search-input-wrapper {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  background-color: #F5F5F5;
+  border-radius: 16rpx;
+  padding: 0 20rpx;
+}
+.search-icon { font-size: 28rpx; margin-right: 10rpx; }
+.search-input-wrapper input { flex: 1; height: 64rpx; font-size: 28rpx; }
+.search-btn {
+  margin-left: 20rpx;
+  height: 64rpx;
+  line-height: 64rpx;
+  padding: 0 28rpx;
+  background: #C6171E;
+  color: #FFFFFF;
+  border-radius: 16rpx;
+  font-size: 26rpx;
+  border: none;
+}
+
+.search-btn:active {
+  background: #A81212;
+  transform: scale(0.97);
+}
+
+.filter-bar {
+  display: flex;
+  padding: 16rpx 20rpx;
+  background-color: #FFFFFF;
+  gap: 16rpx;
+}
+.filter-item {
+  padding: 10rpx 24rpx;
+  background-color: #F5F5F5;
+  border-radius: 20rpx;
+  font-size: 24rpx;
+  color: #666666;
+}
+.filter-item.active {
+  background: #C6171E;
+  color: #FFFFFF;
+}
+
+.station-list {
+  padding: 20rpx;
+}
+.station-item {
+  background-color: #FFFFFF;
+  border-radius: 20rpx;
+  padding: 24rpx;
+  margin-bottom: 20rpx;
+  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
+}
+.station-item:active {
+  transform: translateY(-4rpx);
+  box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
+}
+.item-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  padding-bottom: 16rpx;
+  border-bottom: 1rpx solid #F0F0F0;
+  margin-bottom: 16rpx;
+}
+.item-name {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #1A1A1A;
+  display: block;
+}
+.item-id {
+  font-size: 22rpx;
+  color: #999999;
+  margin-top: 4rpx;
+  display: block;
+}
+.status-tag {
+  font-size: 22rpx;
+  padding: 6rpx 16rpx;
+  border-radius: 20rpx;
+  font-weight: 500;
+}
+.info-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 8rpx 0;
+}
+.info-label {
+  font-size: 26rpx;
+  color: #999999;
+}
+.info-value {
+  font-size: 26rpx;
+  color: #1A1A1A;
+  max-width: 360rpx;
+  text-align: right;
+}
+.info-value.addr {
+  font-size: 24rpx;
+}
+.item-footer {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding-top: 16rpx;
+  border-top: 1rpx solid #F0F0F0;
+  margin-top: 16rpx;
+}
+.footer-time {
+  font-size: 22rpx;
+  color: #999999;
+}
+.footer-arrow {
+  font-size: 24rpx;
+  color: #C6171E;
+}
+
+.load-more {
+  display: flex;
+  justify-content: center;
+  padding: 30rpx 0;
+}
+.load-more-text { font-size: 26rpx; color: #999999; }
+
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 120rpx 0;
+}
+.empty-icon { font-size: 120rpx; margin-bottom: 20rpx; }
+.empty-text { font-size: 28rpx; color: #999999; }
+
+.loading-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 120rpx 0;
+}
+.loading-spinner {
+  font-size: 60rpx;
+  animation: spin 1s linear infinite;
+}
+.loading-text { font-size: 28rpx; color: #999999; margin-top: 20rpx; }
+</style>

+ 602 - 0
admin-mp/src/pages/system/dict.vue

@@ -0,0 +1,602 @@
+<template>
+  <view class="dict-container">
+    <NavBar title="数据字典" @back="goBack" />
+
+    <!-- 字典编码列表视图 -->
+    <view class="dict-list-view" v-if="!showDetail">
+      <!-- 搜索栏 -->
+      <view class="search-bar">
+        <input
+          v-model="searchCode"
+          class="search-input"
+          placeholder="搜索字典编码"
+          @confirm="handleSearch"
+        />
+        <view class="search-actions">
+          <view class="btn-search" @click="handleSearch">搜索</view>
+          <view class="btn-reset" @click="handleReset">重置</view>
+          <view class="btn-create" @click="handleCreate">创建</view>
+        </view>
+      </view>
+
+      <!-- 字典编码列表 -->
+      <view class="code-list" v-if="codeList.length > 0">
+        <view
+          class="code-item"
+          v-for="(item, index) in codeList"
+          :key="index"
+          @click="handleSelectDict(item)"
+        >
+          <view class="item-main">
+            <text class="item-code">{{ item.code }}</text>
+            <text class="item-remark" v-if="item.remark">{{ item.remark }}</text>
+          </view>
+          <view class="item-meta">
+            <text class="item-count">{{ item.count }} 项</text>
+            <AppIcon name="chevron-right" size="14" color="#CCCCCC" />
+          </view>
+        </view>
+      </view>
+
+      <!-- 加载中 -->
+      <view class="loading-state" v-if="loading">
+        <view class="loading-spinner"></view>
+        <text class="loading-text">加载中...</text>
+      </view>
+
+      <!-- 空状态 -->
+      <view class="empty-state" v-if="codeList.length === 0 && !loading">
+        <view class="empty-icon-wrapper">
+          <AppIcon name="book-open" size="48" color="#CCCCCC" />
+        </view>
+        <text class="empty-text">暂无字典数据</text>
+        <view class="btn-create empty-btn" @click="handleCreate">创建字典</view>
+      </view>
+    </view>
+
+    <!-- 字典编辑视图 -->
+    <view class="dict-edit-view" v-if="showDetail">
+      <view class="edit-header">
+        <view class="back-row" @click="handleBackToList">
+          <AppIcon name="chevron-left" size="18" color="#C6171E" />
+          <text class="edit-title">{{ isNew ? '创建字典' : editForm.code }}</text>
+        </view>
+      </view>
+
+      <view class="edit-form">
+        <!-- 字典编码(新建时可编辑,已有不可改) -->
+        <view class="form-row">
+          <text class="form-label">字典编码</text>
+          <input
+            v-model="editForm.code"
+            class="form-input"
+            placeholder="请输入字典编码"
+            :disabled="!isNew"
+          />
+        </view>
+
+        <!-- 备注 -->
+        <view class="form-row">
+          <text class="form-label">备注</text>
+          <input
+            v-model="editForm.remark"
+            class="form-input"
+            placeholder="请输入备注"
+          />
+        </view>
+
+        <!-- 字典项列表 -->
+        <view class="items-section">
+          <view class="items-header">
+            <text class="items-title">字典项</text>
+          </view>
+
+          <view
+            class="dict-item-row"
+            v-for="(item, idx) in editForm.list"
+            :key="idx"
+          >
+            <view class="item-fields">
+              <view class="field-group">
+                <text class="field-label">名称</text>
+                <input
+                  v-model="item.name"
+                  class="field-input"
+                  placeholder="显示名称"
+                  @input="markDirty"
+                />
+              </view>
+              <view class="field-group">
+                <text class="field-label">码值</text>
+                <input
+                  v-model="item.value"
+                  class="field-input"
+                  placeholder="数据码值"
+                  :disabled="!!item.id"
+                  @input="markDirty"
+                />
+              </view>
+              <view class="field-group field-sm">
+                <text class="field-label">权重</text>
+                <input
+                  v-model.number="item.weight"
+                  class="field-input"
+                  type="number"
+                  placeholder="排序"
+                  @input="markDirty"
+                />
+              </view>
+            </view>
+            <view class="item-delete" @click="handleDeleteItem(idx)">
+              <AppIcon name="trash" size="18" color="#FF4D4F" />
+            </view>
+          </view>
+
+          <view class="add-item-btn" @click="handleAddItem">
+            <AppIcon name="plus" size="16" color="#C6171E" />
+            <text>新增字典项</text>
+          </view>
+        </view>
+
+        <!-- 保存按钮 -->
+        <view class="save-section" v-if="isDirty">
+          <view class="btn-save" @click="handleSave">保存</view>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { getDataDictList, saveOrUpdateDict } from '../../api/dict.js'
+import { showToast } from '../../utils/index.js'
+
+const searchCode = ref('')
+const codeList = ref([])
+const allDictData = ref([])
+const loading = ref(true)
+
+const showDetail = ref(false)
+const isNew = ref(false)
+const isDirty = ref(false)
+const editForm = ref({
+  code: '',
+  remark: '',
+  list: []
+})
+
+const goBack = () => {
+  if (showDetail.value) {
+    handleBackToList()
+  } else {
+    uni.navigateBack()
+  }
+}
+
+const loadData = async (code = '') => {
+  loading.value = true
+  try {
+    const params = {}
+    if (code) params.code = code
+    const res = await getDataDictList(params)
+    if (res && res.code === 200) {
+      const data = res.data
+      allDictData.value = data || []
+      buildCodeList(allDictData.value)
+    }
+  } catch (e) {
+    console.error('加载字典列表失败:', e)
+    showToast('加载字典数据失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+const buildCodeList = (data) => {
+  const seen = new Set()
+  const result = []
+  data.forEach(item => {
+    if (!seen.has(item.code)) {
+      seen.add(item.code)
+      const items = data.filter(k => k.code === item.code)
+      result.push({
+        code: item.code,
+        remark: item.remark || '',
+        count: items.length
+      })
+    }
+  })
+  codeList.value = result
+}
+
+const handleSearch = () => {
+  loadData(searchCode.value.trim())
+}
+
+const handleReset = () => {
+  searchCode.value = ''
+  loadData()
+}
+
+const handleCreate = () => {
+  isNew.value = true
+  isDirty.value = true
+  editForm.value = {
+    code: '',
+    remark: '',
+    list: [{ name: '', value: '', weight: 0 }]
+  }
+  showDetail.value = true
+}
+
+const handleSelectDict = (item) => {
+  isNew.value = false
+  isDirty.value = false
+  const items = allDictData.value
+    .filter(k => k.code === item.code)
+    .sort((a, b) => (a.weight || 0) - (b.weight || 0))
+  editForm.value = {
+    code: item.code,
+    remark: item.remark || '',
+    list: items.map(k => ({
+      id: k.id,
+      name: k.name || '',
+      value: k.value || '',
+      weight: k.weight || 0
+    }))
+  }
+  showDetail.value = true
+}
+
+const handleBackToList = () => {
+  showDetail.value = false
+  loadData(searchCode.value.trim())
+}
+
+const handleAddItem = () => {
+  isDirty.value = true
+  editForm.value.list.push({
+    name: '',
+    value: '',
+    weight: 0
+  })
+}
+
+const handleDeleteItem = (idx) => {
+  isDirty.value = true
+  editForm.value.list.splice(idx, 1)
+}
+
+const markDirty = () => {
+  isDirty.value = true
+}
+
+const handleSave = async () => {
+  // 校验
+  if (!editForm.value.code) {
+    showToast('字典编码不能为空')
+    return
+  }
+  const hasEmptyName = editForm.value.list.some(k => !k.name)
+  if (hasEmptyName) {
+    showToast('字典项名称不能为空')
+    return
+  }
+  const hasEmptyValue = editForm.value.list.some(k => !k.value && k.value !== 0)
+  if (hasEmptyValue) {
+    showToast('字典项码值不能为空')
+    return
+  }
+
+  const params = editForm.value.list.map(k => ({
+    id: k.id,
+    name: k.name,
+    value: k.value,
+    weight: k.weight,
+    code: editForm.value.code,
+    remark: editForm.value.remark
+  }))
+
+  try {
+    const res = await saveOrUpdateDict(params)
+    if (res && res.code === 200) {
+      showToast('保存成功', 'success')
+      isDirty.value = false
+      handleBackToList()
+    }
+  } catch (e) {
+    console.error('保存字典失败:', e)
+    showToast(e.msg || '保存失败')
+  }
+}
+
+onMounted(() => loadData())
+</script>
+
+<style scoped>
+.dict-container {
+  min-height: 100vh;
+  background-color: #F5F7FA;
+  padding-bottom: 100rpx;
+}
+
+/* ===== 搜索栏 ===== */
+.search-bar {
+  background-color: #FFFFFF;
+  padding: 20rpx;
+  margin: 20rpx;
+  border-radius: 20rpx;
+  box-shadow: 0 1px 3px rgba(0,0,0,0.06);
+}
+.search-input {
+  width: 100%;
+  height: 76rpx;
+  padding: 0 24rpx;
+  border: 2rpx solid #E8E8E8;
+  border-radius: 12rpx;
+  font-size: 28rpx;
+  color: #1A1A1A;
+  background-color: #F5F7FA;
+  box-sizing: border-box;
+  margin-bottom: 16rpx;
+}
+.search-input:focus {
+  border-color: #C6171E;
+  background-color: #FFFFFF;
+}
+.search-actions {
+  display: flex;
+  gap: 16rpx;
+}
+.btn-search,
+.btn-reset,
+.btn-create {
+  flex: 1;
+  height: 64rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 12rpx;
+  font-size: 26rpx;
+  font-weight: 500;
+  transition: all 0.2s;
+}
+.btn-search {
+  background-color: #C6171E;
+  color: #FFFFFF;
+}
+.btn-reset {
+  background-color: #F5F5F5;
+  color: #666666;
+  border: 2rpx solid #E8E8E8;
+}
+.btn-create {
+  background-color: #C6171E;
+  color: #FFFFFF;
+}
+.btn-search:active,
+.btn-create:active {
+  transform: translateY(1px);
+  opacity: 0.9;
+}
+.btn-reset:active {
+  background-color: #E8E8E8;
+}
+
+/* ===== 编码列表 ===== */
+.code-list {
+  padding: 0 20rpx;
+}
+.code-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  background-color: #FFFFFF;
+  border-radius: 20rpx;
+  padding: 28rpx;
+  margin-bottom: 16rpx;
+  box-shadow: 0 1px 3px rgba(0,0,0,0.06);
+}
+.code-item:active {
+  transform: translateY(-2rpx);
+}
+.item-main {
+  flex: 1;
+}
+.item-code {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #1A1A1A;
+  display: block;
+  margin-bottom: 4rpx;
+}
+.item-remark {
+  font-size: 24rpx;
+  color: #999999;
+}
+.item-meta {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+}
+.item-count {
+  font-size: 24rpx;
+  color: #C6171E;
+  background-color: rgba(198, 23, 30, 0.08);
+  padding: 6rpx 16rpx;
+  border-radius: 20rpx;
+}
+
+/* ===== 编辑视图 ===== */
+.edit-header {
+  background-color: #FFFFFF;
+  padding: 20rpx 30rpx;
+  margin-bottom: 20rpx;
+}
+.back-row {
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+}
+.edit-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #1A1A1A;
+}
+
+.edit-form {
+  padding: 0 20rpx;
+}
+.form-row {
+  background-color: #FFFFFF;
+  border-radius: 16rpx;
+  padding: 20rpx 24rpx;
+  margin-bottom: 16rpx;
+}
+.form-label {
+  font-size: 26rpx;
+  color: #999999;
+  margin-bottom: 12rpx;
+  display: block;
+}
+.form-input {
+  width: 100%;
+  height: 72rpx;
+  padding: 0 20rpx;
+  border: 2rpx solid #E8E8E8;
+  border-radius: 12rpx;
+  font-size: 28rpx;
+  color: #1A1A1A;
+  background-color: #F5F7FA;
+  box-sizing: border-box;
+}
+.form-input:focus {
+  border-color: #C6171E;
+  background-color: #FFFFFF;
+}
+.form-input:disabled {
+  background-color: #F0F0F0;
+  color: #999999;
+}
+
+/* ===== 字典项 ===== */
+.items-section {
+  background-color: #FFFFFF;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  margin-bottom: 24rpx;
+}
+.items-header {
+  margin-bottom: 20rpx;
+}
+.items-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #1A1A1A;
+}
+
+.dict-item-row {
+  display: flex;
+  align-items: flex-start;
+  padding: 16rpx 0;
+  border-bottom: 2rpx solid #F5F5F5;
+  gap: 12rpx;
+}
+.dict-item-row:last-child {
+  border-bottom: none;
+}
+.item-fields {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  gap: 12rpx;
+}
+.field-group {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+}
+.field-label {
+  width: 64rpx;
+  font-size: 24rpx;
+  color: #999999;
+  flex-shrink: 0;
+}
+.field-input {
+  flex: 1;
+  height: 60rpx;
+  padding: 0 16rpx;
+  border: 2rpx solid #E8E8E8;
+  border-radius: 10rpx;
+  font-size: 26rpx;
+  color: #1A1A1A;
+  background-color: #F5F7FA;
+  box-sizing: border-box;
+}
+.field-input:focus {
+  border-color: #C6171E;
+  background-color: #FFFFFF;
+}
+.field-input:disabled {
+  background-color: #F0F0F0;
+  color: #999999;
+}
+.field-sm .field-label {
+  width: 64rpx;
+}
+.item-delete {
+  padding: 30rpx 8rpx 0;
+  flex-shrink: 0;
+}
+
+/* ===== 新增按钮 & 保存 ===== */
+.add-item-btn {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8rpx;
+  height: 72rpx;
+  margin-top: 16rpx;
+  border: 2rpx dashed #C6171E;
+  border-radius: 12rpx;
+  color: #C6171E;
+  font-size: 26rpx;
+}
+.add-item-btn:active {
+  background-color: rgba(198, 23, 30, 0.05);
+}
+
+.save-section {
+  margin: 24rpx 0 40rpx;
+}
+.btn-save {
+  width: 100%;
+  height: 88rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background-color: #C6171E;
+  color: #FFFFFF;
+  border-radius: 16rpx;
+  font-size: 32rpx;
+  font-weight: 600;
+  box-shadow: 0 6rpx 20rpx rgba(198, 23, 30, 0.35);
+}
+.btn-save:active {
+  transform: translateY(2rpx);
+  box-shadow: 0 2rpx 10rpx rgba(198, 23, 30, 0.25);
+}
+
+/* ===== 空状态 ===== */
+.empty-btn {
+  width: auto;
+  padding: 16rpx 48rpx;
+  margin-top: 24rpx;
+  background-color: #C6171E;
+  color: #FFFFFF;
+  border-radius: 12rpx;
+  font-size: 28rpx;
+  font-weight: 500;
+}
+</style>

+ 223 - 0
admin-mp/src/pages/system/feedback.vue

@@ -0,0 +1,223 @@
+<template>
+  <view class="feedback-container">
+    <NavBar title="反馈上报" @back="goBack" />
+
+    <!-- 状态筛选 -->
+    <view class="filter-bar">
+      <view
+        v-for="(option, index) in statusOptions"
+        :key="index"
+        class="filter-item"
+        :class="{ active: activeStatus === option.value }"
+        @click="handleStatusChange(option.value)">
+        <text>{{ option.label }}</text>
+      </view>
+    </view>
+
+    <!-- 反馈列表 -->
+    <view class="feedback-list" v-if="list.length > 0">
+      <view class="feedback-item" v-for="(item, index) in list" :key="index" @click="viewDetail(item)">
+        <view class="item-header">
+          <view class="item-left">
+            <text class="item-user">{{ item.userName || item.nickName || '匿名用户' }}</text>
+            <text class="item-time">{{ formatTime(item.createTime) }}</text>
+          </view>
+          <text class="status-tag" :style="getStatusStyle(item.status)">{{ fmtDictName('Feedback.status', item.status) }}</text>
+        </view>
+        <view class="item-content">
+          <text class="content-text">{{ item.content || item.feedback || '-' }}</text>
+          <view class="item-images" v-if="item.images && item.images.length > 0">
+            <image
+              v-for="(img, i) in item.images"
+              :key="i"
+              :src="img"
+              mode="aspectFill"
+              class="feedback-img"
+              @click.stop="previewImage(img, item.images)" />
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 加载更多 -->
+    <view class="load-more" v-if="list.length > 0">
+      <text class="load-more-text" v-if="loadMoreStatus === 'more'" @click="loadMore">上拉加载更多</text>
+      <text class="load-more-text" v-else-if="loadMoreStatus === 'loading'">正在加载...</text>
+      <text class="load-more-text" v-else>没有更多数据了</text>
+    </view>
+
+    <!-- 空状态 -->
+    <view class="empty-state" v-if="list.length === 0 && !loading">
+      <view class="empty-icon-wrapper">
+        <AppIcon name="message-circle" size="48" color="#CCCCCC" />
+      </view>
+      <text class="empty-text">暂无反馈记录</text>
+    </view>
+
+    <!-- 加载中 -->
+    <view class="loading-state" v-if="loading && list.length === 0">
+      <view class="loading-spinner"></view>
+      <text class="loading-text">加载中...</text>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, onMounted, computed } from 'vue'
+import { getFeedbackList } from '../../api/system.js'
+import { formatTime, fmtDictName, getDictColor } from '../../utils/index.js'
+import dictUtil from '../../utils/dict.js'
+
+const list = ref([])
+const loading = ref(true)
+const page = ref(1)
+const pageSize = ref(10)
+const hasMore = ref(true)
+const loadMoreStatus = ref('more')
+const activeStatus = ref('')
+
+const statusOptions = computed(() => dictUtil.getDictFilterOptions('Feedback.status'))
+
+const goBack = () => uni.navigateBack()
+
+const getStatusStyle = (status) => {
+  const color = getDictColor('Feedback.status', status)
+  if (color) return { color, backgroundColor: `${color}1A` }
+  return {}
+}
+
+const loadData = async (isLoadMore = false) => {
+  if (!isLoadMore) {
+    page.value = 1
+    list.value = []
+    hasMore.value = true
+    loadMoreStatus.value = 'more'
+  } else {
+    loadMoreStatus.value = 'loading'
+  }
+  loading.value = true
+  try {
+    const params = { pageNum: page.value, pageSize: pageSize.value }
+    if (activeStatus.value !== '') params.status = activeStatus.value
+
+    const res = await getFeedbackList(params)
+    if (res && res.code === 200) {
+      const data = res.data
+      const records = data.records || data.list || data
+      const total = data.total || 0
+      const totalPages = Math.ceil(total / pageSize.value)
+      hasMore.value = page.value < totalPages
+      loadMoreStatus.value = hasMore.value ? 'more' : 'noMore'
+      if (isLoadMore) {
+        list.value = [...list.value, ...records]
+        page.value++
+      } else {
+        list.value = records
+        page.value = 2
+      }
+    }
+  } catch (e) {
+    console.error('加载反馈列表失败:', e)
+  } finally {
+    loading.value = false
+  }
+}
+
+const loadMore = () => { if (hasMore.value && !loading.value) loadData(true) }
+const handleStatusChange = (status) => { activeStatus.value = status; loadData() }
+
+const viewDetail = (item) => {
+  // 简单显示反馈详情
+  uni.showModal({
+    title: '反馈详情',
+    content: item.content || item.feedback || '暂无内容',
+    showCancel: false
+  })
+}
+
+const previewImage = (url, urls) => {
+  uni.previewImage({ urls, current: url })
+}
+
+onMounted(() => loadData())
+</script>
+
+<style scoped>
+.feedback-container {
+  min-height: 100vh;
+  background-color: #F5F7FA;
+  padding-bottom: 100rpx;
+}
+.filter-bar {
+  display: flex;
+  padding: 16rpx 20rpx;
+  background-color: #FFFFFF;
+  gap: 16rpx;
+}
+.filter-item {
+  padding: 10rpx 24rpx;
+  background-color: #F5F5F5;
+  border-radius: 20rpx;
+  font-size: 24rpx;
+  color: #666666;
+}
+.filter-item.active {
+  background: #C6171E;
+  color: #FFFFFF;
+}
+
+.feedback-list {
+  padding: 20rpx;
+}
+.feedback-item {
+  background-color: #FFFFFF;
+  border-radius: 20rpx;
+  padding: 24rpx;
+  margin-bottom: 20rpx;
+  box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06);
+}
+.item-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  margin-bottom: 16rpx;
+}
+.item-user {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #1A1A1A;
+  display: block;
+}
+.item-time {
+  font-size: 22rpx;
+  color: #999999;
+  margin-top: 4rpx;
+  display: block;
+}
+.status-tag {
+  font-size: 22rpx;
+  padding: 6rpx 16rpx;
+  border-radius: 20rpx;
+  font-weight: 500;
+}
+.content-text {
+  font-size: 26rpx;
+  color: #333333;
+  line-height: 1.6;
+}
+.item-images {
+  display: flex;
+  gap: 12rpx;
+  margin-top: 16rpx;
+  overflow-x: auto;
+}
+.feedback-img {
+  width: 160rpx;
+  height: 160rpx;
+  border-radius: 12rpx;
+  flex-shrink: 0;
+}
+
+.load-more { display: flex; justify-content: center; padding: 30rpx 0; }
+.load-more-text { font-size: 26rpx; color: #999999; }
+</style>

+ 197 - 0
admin-mp/src/pages/system/log.vue

@@ -0,0 +1,197 @@
+<template>
+  <view class="log-container">
+    <NavBar title="操作日志" @back="goBack" />
+
+    <!-- 搜索栏 -->
+    <view class="search-bar">
+      <view class="search-input-wrapper">
+        <AppIcon name="search" size="14" color="#999999" />
+        <input type="text" placeholder="搜索操作人" v-model="searchKeyword" @confirm="handleSearch" />
+      </view>
+      <button class="search-btn" @click="handleSearch">搜索</button>
+    </view>
+
+    <!-- 日志列表 -->
+    <view class="log-list" v-if="list.length > 0">
+      <view class="log-item" v-for="(item, index) in list" :key="index">
+        <view class="item-header">
+          <text class="item-operator">{{ item.operator || item.username || item.adminUserName || '-' }}</text>
+          <text class="item-time">{{ formatTime(item.createTime || item.optTime) }}</text>
+        </view>
+        <view class="item-content">
+          <text class="content-text">{{ item.operation || item.optContent || item.content || '-' }}</text>
+        </view>
+        <view class="item-meta" v-if="item.ip || item.module">
+          <text class="meta-item" v-if="item.module">{{ item.module }}</text>
+          <text class="meta-item" v-if="item.ip">IP: {{ item.ip }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 加载更多 -->
+    <view class="load-more" v-if="list.length > 0">
+      <text class="load-more-text" v-if="loadMoreStatus === 'more'" @click="loadMore">上拉加载更多</text>
+      <text class="load-more-text" v-else-if="loadMoreStatus === 'loading'">正在加载...</text>
+      <text class="load-more-text" v-else>没有更多数据了</text>
+    </view>
+
+    <!-- 空状态 -->
+    <view class="empty-state" v-if="list.length === 0 && !loading">
+      <view class="empty-icon-wrapper">
+        <AppIcon name="clock" size="48" color="#CCCCCC" />
+      </view>
+      <text class="empty-text">暂无操作日志</text>
+    </view>
+
+    <!-- 加载中 -->
+    <view class="loading-state" v-if="loading && list.length === 0">
+      <view class="loading-spinner"></view>
+      <text class="loading-text">加载中...</text>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { getOptLogList } from '../../api/system.js'
+import { formatTime, showToast } from '../../utils/index.js'
+
+const list = ref([])
+const loading = ref(true)
+const page = ref(1)
+const pageSize = ref(20)
+const hasMore = ref(true)
+const loadMoreStatus = ref('more')
+const searchKeyword = ref('')
+
+const goBack = () => uni.navigateBack()
+
+const loadData = async (isLoadMore = false) => {
+  if (!isLoadMore) {
+    page.value = 1
+    list.value = []
+    hasMore.value = true
+    loadMoreStatus.value = 'more'
+  } else {
+    loadMoreStatus.value = 'loading'
+  }
+  loading.value = true
+  try {
+    const params = { pageNum: page.value, pageSize: pageSize.value }
+    if (searchKeyword.value) params.operator = searchKeyword.value
+
+    const res = await getOptLogList(params)
+    if (res && res.code === 200) {
+      const data = res.data
+      const records = data.records || data.list || data
+      const total = data.total || 0
+      const totalPages = Math.ceil(total / pageSize.value)
+      hasMore.value = page.value < totalPages
+      loadMoreStatus.value = hasMore.value ? 'more' : 'noMore'
+      if (isLoadMore) {
+        list.value = [...list.value, ...records]
+        page.value++
+      } else {
+        list.value = records
+        page.value = 2
+      }
+    }
+  } catch (e) {
+    console.error('加载操作日志失败:', e)
+    showToast('加载操作日志失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+const loadMore = () => { if (hasMore.value && !loading.value) loadData(true) }
+const handleSearch = () => loadData()
+
+onMounted(() => loadData())
+</script>
+
+<style scoped>
+.log-container {
+  min-height: 100vh;
+  background-color: #F5F7FA;
+  padding-bottom: 100rpx;
+}
+.search-bar {
+  display: flex;
+  align-items: center;
+  padding: 20rpx;
+  background-color: #FFFFFF;
+  margin-bottom: 20rpx;
+}
+.search-input-wrapper {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  background-color: #F5F5F5;
+  border-radius: 16rpx;
+  padding: 0 20rpx;
+}
+.search-input-wrapper input { flex: 1; height: 64rpx; font-size: 28rpx; }
+.search-btn {
+  margin-left: 20rpx;
+  height: 64rpx;
+  line-height: 64rpx;
+  padding: 0 28rpx;
+  background: #C6171E;
+  color: #FFFFFF;
+  border-radius: 16rpx;
+  font-size: 26rpx;
+  border: none;
+}
+
+.search-btn:active {
+  background: #A81212;
+  transform: scale(0.97);
+}
+
+.log-list {
+  padding: 0 20rpx;
+}
+.log-item {
+  background-color: #FFFFFF;
+  border-radius: 16rpx;
+  padding: 20rpx 24rpx;
+  margin-bottom: 16rpx;
+  box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06);
+}
+.item-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12rpx;
+}
+.item-operator {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #C6171E;
+}
+.item-time {
+  font-size: 22rpx;
+  color: #999999;
+}
+.content-text {
+  font-size: 26rpx;
+  color: #333333;
+  line-height: 1.6;
+}
+.item-meta {
+  display: flex;
+  gap: 16rpx;
+  margin-top: 12rpx;
+  padding-top: 12rpx;
+  border-top: 1rpx solid #F5F5F5;
+}
+.meta-item {
+  font-size: 22rpx;
+  color: #CCCCCC;
+}
+
+.load-more { display: flex; justify-content: center; padding: 30rpx 0; }
+.load-more-text { font-size: 26rpx; color: #999999; }
+
+</style>

+ 198 - 0
admin-mp/src/pages/system/notice.vue

@@ -0,0 +1,198 @@
+<template>
+  <view class="notice-container">
+    <NavBar title="系统公告" @back="goBack" />
+
+    <!-- 公告列表 -->
+    <view class="notice-list" v-if="list.length > 0">
+      <view class="notice-item" v-for="(item, index) in list" :key="index" @click="viewDetail(item)">
+        <view class="item-header">
+          <text class="item-title">{{ item.title || '-' }}</text>
+          <text class="status-tag" :style="getStatusStyle(item.status)">{{ fmtDictName('notice_status', item.status) }}</text>
+        </view>
+        <view class="item-content">
+          <text class="content-preview">{{ truncateText(item.content || '', 100) }}</text>
+        </view>
+        <view class="item-footer">
+          <text class="footer-author">{{ item.adminUserName || '系统' }}</text>
+          <text class="footer-time">{{ formatTime(item.createTime) }}</text>
+        </view>
+        <view class="item-period" v-if="item.startTime || item.endTime">
+          <text class="period-text">{{ formatTime(item.startTime) }} ~ {{ formatTime(item.endTime) }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 加载更多 -->
+    <view class="load-more" v-if="list.length > 0">
+      <text class="load-more-text" v-if="loadMoreStatus === 'more'" @click="loadMore">上拉加载更多</text>
+      <text class="load-more-text" v-else-if="loadMoreStatus === 'loading'">正在加载...</text>
+      <text class="load-more-text" v-else>没有更多数据了</text>
+    </view>
+
+    <!-- 空状态 -->
+    <view class="empty-state" v-if="list.length === 0 && !loading">
+      <view class="empty-icon-wrapper">
+        <AppIcon name="megaphone" size="48" color="#BFBFBF" />
+      </view>
+      <text class="empty-text">暂无公告</text>
+    </view>
+
+    <!-- 加载中 -->
+    <view class="loading-state" v-if="loading && list.length === 0">
+      <view class="loading-spinner"></view>
+      <text class="loading-text">加载中...</text>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { getNoticeList } from '../../api/system.js'
+import { formatTime, fmtDictName, getDictColor } from '../../utils/index.js'
+
+const list = ref([])
+const loading = ref(true)
+const page = ref(1)
+const pageSize = ref(10)
+const hasMore = ref(true)
+const loadMoreStatus = ref('more')
+
+const goBack = () => uni.navigateBack()
+
+const getStatusStyle = (status) => {
+  const color = getDictColor('notice_status', status)
+  if (color) return { color, backgroundColor: `${color}1A` }
+  return {}
+}
+
+const truncateText = (text, maxLen) => {
+  if (!text) return ''
+  return text.length > maxLen ? text.substring(0, maxLen) + '...' : text
+}
+
+const loadData = async (isLoadMore = false) => {
+  if (!isLoadMore) {
+    page.value = 1
+    list.value = []
+    hasMore.value = true
+    loadMoreStatus.value = 'more'
+  } else {
+    loadMoreStatus.value = 'loading'
+  }
+  loading.value = true
+  try {
+    const params = { pageNum: page.value, pageSize: pageSize.value }
+    const res = await getNoticeList(params)
+    if (res && res.code === 200) {
+      const data = res.data
+      const records = data.records || data.list || data
+      const total = data.total || 0
+      const totalPages = Math.ceil(total / pageSize.value)
+      hasMore.value = page.value < totalPages
+      loadMoreStatus.value = hasMore.value ? 'more' : 'noMore'
+      if (isLoadMore) {
+        list.value = [...list.value, ...records]
+        page.value++
+      } else {
+        list.value = records
+        page.value = 2
+      }
+    }
+  } catch (e) {
+    console.error('加载公告列表失败:', e)
+  } finally {
+    loading.value = false
+  }
+}
+
+const loadMore = () => { if (hasMore.value && !loading.value) loadData(true) }
+
+const viewDetail = (item) => {
+  uni.showModal({
+    title: item.title || '公告详情',
+    content: item.content || '暂无内容',
+    showCancel: false,
+    confirmText: '关闭'
+  })
+}
+
+onMounted(() => loadData())
+</script>
+
+<style scoped>
+.notice-container {
+  min-height: 100vh;
+  background-color: #F5F7FA;
+  padding-bottom: 100rpx;
+}
+.notice-list {
+  padding: 20rpx;
+}
+.notice-item {
+  background-color: #FFFFFF;
+  border-radius: 20rpx;
+  padding: 24rpx;
+  margin-bottom: 20rpx;
+  box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06);
+}
+.notice-item:active {
+  transform: translateY(-4rpx);
+}
+.item-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  margin-bottom: 16rpx;
+}
+.item-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #1A1A1A;
+  flex: 1;
+  margin-right: 16rpx;
+}
+.status-tag {
+  font-size: 22rpx;
+  padding: 6rpx 16rpx;
+  border-radius: 20rpx;
+  font-weight: 500;
+  flex-shrink: 0;
+}
+.content-preview {
+  font-size: 26rpx;
+  color: #666666;
+  line-height: 1.6;
+}
+.item-footer {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 16rpx;
+  padding-top: 16rpx;
+  border-top: 1rpx solid #F0F0F0;
+}
+.footer-author {
+  font-size: 24rpx;
+  color: #C6171E;
+}
+.footer-time {
+  font-size: 24rpx;
+  color: #999999;
+}
+.item-period {
+  margin-top: 8rpx;
+}
+.period-text {
+  font-size: 22rpx;
+  color: #CCCCCC;
+}
+
+.load-more { display: flex; justify-content: center; padding: 30rpx 0; }
+.load-more-text { font-size: 26rpx; color: #999999; }
+
+.empty-state { display: flex; flex-direction: column; align-items: center; padding: 100rpx 0; }
+.empty-text { font-size: 28rpx; color: #999999; }
+
+.loading-state { display: flex; flex-direction: column; align-items: center; padding: 120rpx 0; }
+.loading-text { font-size: 28rpx; color: #999999; margin-top: 20rpx; }
+</style>

+ 537 - 0
admin-mp/src/pages/user/list.vue

@@ -0,0 +1,537 @@
+<template>
+  <view class="user-container">
+    <!-- 顶部导航栏 -->
+    <view class="header-nav">
+      <view class="nav-left" @click="goBack">
+        <AppIcon name="chevron-left" size="20" color="#FFFFFF" />
+      </view>
+      <text class="nav-title">用户列表</text>
+      <view class="nav-right"></view>
+    </view>
+
+    <!-- 搜索栏 -->
+    <view class="search-bar">
+      <view class="search-input-wrapper">
+        <AppIcon name="search" size="16" color="#999999" class="search-icon" />
+        <input type="text" placeholder="搜索手机号" v-model="searchKeyword" @confirm="handleSearch" />
+      </view>
+      <button class="search-btn" @click="handleSearch">搜索</button>
+    </view>
+
+    <!-- 用户列表 -->
+    <view class="user-list" v-if="list.length > 0">
+      <view class="user-item" v-for="(item, index) in list" :key="index">
+        <view class="item-header">
+          <view class="item-left">
+            <text class="item-phone">{{ item.mobilePhone || '-' }}</text>
+            <text class="item-id">UID: {{ item.userId || '-' }}</text>
+          </view>
+          <text class="status-tag" :style="getStatusStyle(item.status)">{{ fmtDictName('User.status', item.status) }}</text>
+        </view>
+        <view class="item-content">
+          <!-- 余额概览 -->
+          <view class="balance-grid">
+            <view class="balance-item">
+              <text class="balance-label">总余额</text>
+              <text class="balance-value primary">{{ formatAmount(item.balance) }}</text>
+            </view>
+            <view class="balance-item">
+              <text class="balance-label">充值余额</text>
+              <text class="balance-value">{{ formatAmount(item.rechargeBalance) }}</text>
+            </view>
+            <view class="balance-item">
+              <text class="balance-label">赠金余额</text>
+              <text class="balance-value">{{ formatAmount(item.grantsBalance) }}</text>
+            </view>
+            <view class="balance-item">
+              <text class="balance-label">冻结余额</text>
+              <text class="balance-value warn">{{ formatAmount(item.frozenAmount) }}</text>
+            </view>
+          </view>
+          <view class="divider"></view>
+          <!-- 消费概览 -->
+          <view class="info-grid">
+            <view class="info-item">
+              <text class="info-label">归属站点</text>
+              <text class="info-value">{{ item.stationName || '-' }}</text>
+            </view>
+            <view class="info-item">
+              <text class="info-label">注册时间</text>
+              <text class="info-value">{{ formatTime(item.registerTime) }}</text>
+            </view>
+          </view>
+          <view class="stats-row">
+            <view class="stat-item">
+              <text class="stat-num">{{ item.rechargeTimes || 0 }}</text>
+              <text class="stat-label">充值次数</text>
+            </view>
+            <view class="stat-item">
+              <text class="stat-num amount">{{ formatAmount(item.rechargeAmount) }}</text>
+              <text class="stat-label">充值金额</text>
+            </view>
+            <view class="stat-item">
+              <text class="stat-num">{{ item.washTimes || 0 }}</text>
+              <text class="stat-label">洗车次数</text>
+            </view>
+            <view class="stat-item">
+              <text class="stat-num amount">{{ formatAmount(item.amount) }}</text>
+              <text class="stat-label">消费总额</text>
+            </view>
+          </view>
+        </view>
+        <view class="item-footer">
+          <button class="refund-btn" @click.stop="handleApplyRefund(item)">申请退款</button>
+        </view>
+      </view>
+    </view>
+
+    <!-- 加载更多 -->
+    <view class="load-more" v-if="list.length > 0">
+      <text class="load-more-text" v-if="loadMoreStatus === 'more'" @click="loadMore">上拉加载更多</text>
+      <text class="load-more-text" v-else-if="loadMoreStatus === 'loading'">正在加载...</text>
+      <text class="load-more-text" v-else>没有更多数据了</text>
+    </view>
+
+    <!-- 空状态 -->
+    <view class="empty-state" v-if="list.length === 0 && !loading">
+      <view class="empty-icon-wrapper"><AppIcon name="users" size="48" color="#BFBFBF" /></view>
+      <text class="empty-text">暂无用户数据</text>
+    </view>
+
+    <!-- 加载状态 -->
+    <view class="loading-state" v-if="loading && list.length === 0">
+      <view class="loading-spinner"></view>
+      <text class="loading-text">加载中...</text>
+    </view>
+
+    <!-- 退款弹窗 -->
+    <view class="modal-overlay" v-if="showRefundModal" @click="closeRefundModal">
+      <view class="modal-card" @click.stop>
+        <text class="modal-title">申请退款</text>
+        <view class="modal-content">
+          <view class="modal-row">
+            <text class="modal-label">用户</text>
+            <text class="modal-value">{{ refundUser?.mobilePhone || '-' }}</text>
+          </view>
+          <view class="modal-row">
+            <text class="modal-label">退款原因</text>
+            <input class="modal-input" type="text" v-model="refundReason" placeholder="请输入退款原因" />
+          </view>
+        </view>
+        <view class="modal-footer">
+          <button class="modal-btn cancel" @click="closeRefundModal">取消</button>
+          <button class="modal-btn confirm" @click="confirmRefund" :disabled="!refundReason.trim()">确认</button>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { getAppUserList } from '../../api/user.js'
+import { applyRefund } from '../../api/finance.js'
+import { formatTime, showToast, formatAmount, fmtDictName, getDictColor } from '../../utils/index.js'
+
+const list = ref([])
+const loading = ref(true)
+const page = ref(1)
+const pageSize = ref(10)
+const hasMore = ref(true)
+const loadMoreStatus = ref('more')
+const searchKeyword = ref('')
+const showRefundModal = ref(false)
+const refundUser = ref(null)
+const refundReason = ref('')
+
+const goBack = () => {
+  uni.navigateBack()
+}
+
+const getStatusStyle = (status) => {
+  const color = getDictColor('User.status', status)
+  if (color) return { color, backgroundColor: `${color}1A` }
+  return {}
+}
+
+const loadData = async (isLoadMore = false) => {
+  if (!isLoadMore) {
+    page.value = 1
+    list.value = []
+    hasMore.value = true
+    loadMoreStatus.value = 'more'
+  } else {
+    loadMoreStatus.value = 'loading'
+  }
+
+  loading.value = true
+  try {
+    const params = {
+      pageNum: page.value,
+      pageSize: pageSize.value
+    }
+    if (searchKeyword.value) params.mobilePhone = searchKeyword.value
+
+    const res = await getAppUserList(params)
+    if (res && res.code === 200) {
+      const data = res.data
+      const records = data.records || data.list || data
+      const total = data.total || 0
+
+      const totalPages = Math.ceil(total / pageSize.value)
+      hasMore.value = page.value < totalPages
+      loadMoreStatus.value = hasMore.value ? 'more' : 'noMore'
+
+      if (isLoadMore) {
+        list.value = [...list.value, ...records]
+        page.value++
+      } else {
+        list.value = records
+        page.value = 2
+      }
+    }
+  } catch (error) {
+    console.error('加载用户列表失败:', error)
+  } finally {
+    loading.value = false
+  }
+}
+
+const loadMore = () => {
+  if (hasMore.value && !loading.value) {
+    loadData(true)
+  }
+}
+
+const handleSearch = () => {
+  loadData()
+}
+
+const handleApplyRefund = (item) => {
+  refundUser.value = item
+  refundReason.value = ''
+  showRefundModal.value = true
+}
+
+const closeRefundModal = () => {
+  showRefundModal.value = false
+  refundUser.value = null
+  refundReason.value = ''
+}
+
+const confirmRefund = async () => {
+  if (!refundReason.value.trim()) return
+
+  try {
+    const res = await applyRefund({
+      userId: refundUser.value.userId,
+      reason: refundReason.value.trim()
+    })
+    if (res && res.code === 200) {
+      showToast('退款申请已提交', 'success')
+      closeRefundModal()
+    } else {
+      showToast(res?.msg || '提交失败')
+    }
+  } catch (error) {
+    console.error('申请退款失败:', error)
+  }
+}
+
+onMounted(() => {
+  loadData()
+})
+</script>
+
+<style scoped>
+.user-container {
+  min-height: 100vh;
+  background-color: #F5F7FA;
+  padding-bottom: 100rpx;
+}
+
+.header-nav {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding-top: calc(24rpx + var(--status-bar-height));
+  padding-right: 30rpx;
+  padding-bottom: 24rpx;
+  padding-left: 30rpx;
+  background: #C6171E;
+}
+.nav-left, .nav-right { width: 120rpx; }
+.back-btn { font-size: 36rpx; color: #FFFFFF; }
+.nav-title { font-size: 34rpx; color: #FFFFFF; font-weight: 600; }
+
+.search-bar {
+  display: flex;
+  align-items: center;
+  padding: 20rpx;
+  background-color: #FFFFFF;
+}
+.search-input-wrapper {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  background-color: #F5F5F5;
+  border-radius: 16rpx;
+  padding: 0 20rpx;
+}
+.search-icon { font-size: 28rpx; margin-right: 10rpx; }
+.search-input-wrapper input { flex: 1; height: 64rpx; font-size: 28rpx; }
+.search-btn {
+  margin-left: 20rpx;
+  height: 64rpx;
+  line-height: 64rpx;
+  padding: 0 28rpx;
+  background: #C6171E;
+  color: #FFFFFF;
+  border-radius: 16rpx;
+  font-size: 26rpx;
+  border: none;
+}
+
+.search-btn:active {
+  background: #A81212;
+  transform: scale(0.97);
+}
+
+.user-list {
+  padding: 20rpx;
+}
+.user-item {
+  background-color: #FFFFFF;
+  border-radius: 20rpx;
+  padding: 24rpx;
+  margin-bottom: 20rpx;
+  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
+}
+.item-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  padding-bottom: 16rpx;
+  border-bottom: 1rpx solid #F0F0F0;
+  margin-bottom: 16rpx;
+}
+.item-phone {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #1A1A1A;
+  display: block;
+}
+.item-id {
+  font-size: 22rpx;
+  color: #999999;
+  margin-top: 4rpx;
+  display: block;
+}
+.status-tag {
+  font-size: 22rpx;
+  padding: 6rpx 16rpx;
+  border-radius: 20rpx;
+  font-weight: 500;
+}
+
+.balance-grid {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 16rpx;
+}
+.balance-item {
+  padding: 12rpx;
+  background-color: #F5F7FA;
+  border-radius: 12rpx;
+}
+.balance-label {
+  font-size: 22rpx;
+  color: #999999;
+  display: block;
+  margin-bottom: 4rpx;
+}
+.balance-value {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #1A1A1A;
+}
+.balance-value.primary {
+  color: #C6171E;
+}
+.balance-value.warn {
+  color: #C6171E;
+}
+
+.divider {
+  height: 1rpx;
+  background-color: #F0F0F0;
+  margin: 16rpx 0;
+}
+
+.info-grid {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 12rpx;
+}
+.info-item {
+  padding: 4rpx 0;
+}
+.info-label {
+  font-size: 24rpx;
+  color: #999999;
+  display: block;
+  margin-bottom: 4rpx;
+}
+.info-value {
+  font-size: 26rpx;
+  color: #1A1A1A;
+}
+
+.stats-row {
+  display: flex;
+  justify-content: space-between;
+  padding: 16rpx 0 8rpx;
+  border-top: 1rpx solid #F0F0F0;
+  margin-top: 12rpx;
+}
+.stat-item {
+  text-align: center;
+  flex: 1;
+}
+.stat-num {
+  font-size: 26rpx;
+  font-weight: 600;
+  color: #1A1A1A;
+  display: block;
+}
+.stat-num.amount {
+  font-size: 22rpx;
+  color: #C6171E;
+}
+.stat-label {
+  font-size: 20rpx;
+  color: #999999;
+  display: block;
+  margin-top: 2rpx;
+}
+
+.item-footer {
+  display: flex;
+  justify-content: flex-end;
+  padding-top: 16rpx;
+  border-top: 1rpx solid #F0F0F0;
+  margin-top: 16rpx;
+}
+.refund-btn {
+  padding: 10rpx 28rpx;
+  background: #C6171E;
+  color: #FFFFFF;
+  border-radius: 16rpx;
+  font-size: 24rpx;
+  border: none;
+}
+
+.load-more {
+  display: flex;
+  justify-content: center;
+  padding: 30rpx 0;
+}
+.load-more-text { font-size: 26rpx; color: #999999; }
+
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 120rpx 0;
+}
+.empty-icon { font-size: 120rpx; margin-bottom: 20rpx; }
+.empty-text { font-size: 28rpx; color: #999999; }
+
+.loading-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 120rpx 0;
+}
+.loading-spinner {
+  font-size: 60rpx;
+  animation: spin 1s linear infinite;
+}
+.loading-text { font-size: 28rpx; color: #999999; margin-top: 20rpx; }
+
+/* 退款弹窗 */
+.modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+.modal-card {
+  width: 600rpx;
+  background-color: #FFFFFF;
+  border-radius: 24rpx;
+  padding: 36rpx;
+}
+.modal-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #1A1A1A;
+  display: block;
+  text-align: center;
+  margin-bottom: 30rpx;
+}
+.modal-content {
+  margin-bottom: 30rpx;
+}
+.modal-row {
+  margin-bottom: 20rpx;
+}
+.modal-label {
+  font-size: 26rpx;
+  color: #999999;
+  display: block;
+  margin-bottom: 10rpx;
+}
+.modal-value {
+  font-size: 28rpx;
+  color: #1A1A1A;
+  font-weight: 500;
+}
+.modal-input {
+  width: 100%;
+  height: 72rpx;
+  background-color: #F5F5F5;
+  border-radius: 16rpx;
+  padding: 0 20rpx;
+  font-size: 28rpx;
+  box-sizing: border-box;
+}
+.modal-footer {
+  display: flex;
+  gap: 20rpx;
+}
+.modal-btn {
+  flex: 1;
+  padding: 20rpx 0;
+  border-radius: 16rpx;
+  font-size: 28rpx;
+  border: none;
+}
+.modal-btn.cancel {
+  background-color: #F5F5F5;
+  color: #666666;
+}
+.modal-btn.confirm {
+  background: #C6171E;
+  color: #FFFFFF;
+}
+.modal-btn.confirm[disabled] {
+  opacity: 0.5;
+}
+</style>

+ 69 - 76
admin-mp/src/uni.scss

@@ -1,90 +1,83 @@
-/* 洗车小程序设计规范 - 全局变量定义 */
+/* 洗车运营端设计规范 - Soft UI Evolution */
 
 /* 1. 品牌主色调 */
-$uni-color-primary: #C6171E; // 主红色 - 用于主要按钮、导航栏和强调元素
+$uni-color-primary: #C6171E;
+$uni-color-primary-light: #E84545;
+$uni-color-primary-dark: #A81212;
 
 /* 2. 辅助色 */
-$uni-color-success: #4CAF50; // 成功绿 - 用于成功状态和积极反馈
-$uni-color-warning: #FF9800; // 警告橙 - 用于警告状态和提醒
-$uni-color-error: #F44336;   // 错误红 - 用于错误状态和警示
-$uni-color-info: #2196F3;    // 信息蓝 - 用于信息提示和次要操作
+$uni-color-success: #4CAF50;
+$uni-color-warning: #FF9800;
+$uni-color-error: #F44336;
+$uni-color-info: #2196F3;
 
 /* 3. 中性色系统 */
-// 文字颜色
-$uni-text-color: #1A1A1A;     // 主文字 - 用于主要内容和标题
-$uni-text-color-grey: #666666; // 次要文字 - 用于辅助信息
-$uni-text-color-light: #999999; // 辅助文字 - 用于说明文字和占位符
-
-// 背景颜色
-$uni-bg-color: #FFFFFF;       // 主背景 - 用于卡片和内容区域
-$uni-page-bg-color: #F5F7FA;  // 页面背景 - 用于整体页面背景
-$uni-card-bg-color: #FFFFFF;  // 卡片背景 - 用于信息卡片
-
-// 边框颜色
-$uni-border-color: #E0E0E0;   // 边框颜色 - 用于分隔线和边框
-
-/* 4. 字体规范 */
-// 字体族
-$uni-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
-
-// 字体大小 (12px-32px的字体大小层级)
-$uni-font-size-xs: 12px;  // 超小字体 - 用于最小的说明文字
-$uni-font-size-sm: 14px;  // 小字体 - 用于次要信息和标签
-$uni-font-size-base: 16px;  // 基础字体 - 用于正文内容
-$uni-font-size-lg: 18px;  // 大字体 - 用于标题
-$uni-font-size-xl: 20px;  // 超大字体 - 用于主要标题
-$uni-font-size-xxl: 24px;   // 特大字体 - 用于重要标题
-$uni-font-size-xxxl: 32px;  // 最大字体 - 用于突出显示的数字和标题
-
-// 字重系统
-$uni-font-weight-regular: 400;  // 常规字重
-$uni-font-weight-medium: 500;    // 中等字重
-$uni-font-weight-semibold: 600;  // 半粗字重
-$uni-font-weight-bold: 700;      // 粗体字重
-
-// 行高系统 (1.2-2.0的行高层级)
-$uni-line-height-xs: 1.2;   // 超小行高 - 用于紧凑排版
-$uni-line-height-sm: 1.4;   // 小行高 - 用于常规内容
-$uni-line-height-base: 1.5;  // 基础行高 - 用于正文
-$uni-line-height-lg: 1.8;   // 大行高 - 用于长文本
-$uni-line-height-xl: 2.0;   // 超大行高 - 用于特殊排版
-
-/* 5. 空间系统 */
-// 垂直间距 (4px、8px、12px三级)
-$uni-spacing-vertical-xs: 4px;
-$uni-spacing-vertical-sm: 8px;
-$uni-spacing-vertical-md: 12px;
-
-// 水平间距 (5px、10px、15px三级)
-$uni-spacing-horizontal-xs: 5px;
-$uni-spacing-horizontal-sm: 10px;
-$uni-spacing-horizontal-md: 15px;
-
-// 卡片间距 (统一使用20-24rpx的卡片间距)
-$uni-card-spacing: 20rpx;
-
-// 通用间距变量
+$uni-text-color: #1A1A1A;
+$uni-text-color-grey: #666666;
+$uni-text-color-light: #999999;
+$uni-text-color-placeholder: #B0B0B0;
+
+$uni-bg-color: #FFFFFF;
+$uni-page-bg-color: #F5F7FA;
+$uni-card-bg-color: #FFFFFF;
+
+$uni-border-color: #E0E0E0;
+$uni-border-color-light: #F0F0F0;
+
+/* 4. 字体规范 — Inter + 系统回退 */
+$uni-font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+
+$uni-font-size-xs: 12px;
+$uni-font-size-sm: 14px;
+$uni-font-size-base: 16px;
+$uni-font-size-lg: 18px;
+$uni-font-size-xl: 20px;
+$uni-font-size-xxl: 24px;
+$uni-font-size-xxxl: 32px;
+
+$uni-font-weight-regular: 400;
+$uni-font-weight-medium: 500;
+$uni-font-weight-semibold: 600;
+$uni-font-weight-bold: 700;
+
+$uni-line-height-xs: 1.2;
+$uni-line-height-sm: 1.4;
+$uni-line-height-base: 1.5;
+$uni-line-height-lg: 1.8;
+
+/* 5. 间距系统 */
 $uni-spacing-xs: 8rpx;
 $uni-spacing-sm: 16rpx;
 $uni-spacing-base: 24rpx;
 $uni-spacing-lg: 32rpx;
 $uni-spacing-xl: 40rpx;
+$uni-card-spacing: 20rpx;
 
-/* 6. 圆角 */
-$uni-border-radius-sm: 4px;  // 小圆角
-$uni-border-radius-base: 8px;  // 基础圆角
-$uni-border-radius-lg: 12px;   // 大圆角
-$uni-border-radius-circle: 50%; // 圆形
-
-/* 7. 阴影 */
-$uni-box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);      // 基础阴影
-$uni-box-shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.15); // 大阴影
+/* 6. 圆角 — 标准化为 4 级 */
+$uni-border-radius-sm: 8px;
+$uni-border-radius-md: 12px;
+$uni-border-radius-lg: 16px;
+$uni-border-radius-xl: 24px;
+$uni-border-radius-circle: 50%;
+$uni-border-radius-pill: 100px;
+
+/* 7. 阴影 — Soft UI Evolution 分层软阴影 */
+$uni-box-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06), 0 1px 3px rgba(0, 0, 0, 0.04);
+$uni-box-shadow-base: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.06);
+$uni-box-shadow-lg: 0 4px 6px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.06);
+$uni-box-shadow-xl: 0 10px 15px rgba(0, 0, 0, 0.08), 0 4px 6px rgba(0, 0, 0, 0.05);
+$uni-box-shadow-nav: 0 2px 8px rgba(198, 23, 30, 0.15);
 
 /* 8. 动画过渡 */
-$uni-transition-duration: 0.3s;  // 基础过渡时间
-$uni-transition-timing-function: ease; // 基础过渡函数
-
-/* 9. 状态颜色 */
-$uni-color-idle: #4CAF50;    // 空闲状态 - 绿色
-$uni-color-busy: #FF9800;    // 忙碌状态 - 橙色
-$uni-color-error-state: #F44336; // 故障状态 - 红色
+$uni-transition-duration-fast: 0.15s;
+$uni-transition-duration-base: 0.25s;
+$uni-transition-duration-slow: 0.35s;
+$uni-transition-timing: cubic-bezier(0.4, 0, 0.2, 1);
+
+/* 9. 聚焦环 */
+$uni-focus-ring: 0 0 0 3px rgba(198, 23, 30, 0.2);
+
+/* 10. 状态颜色 */
+$uni-color-idle: #4CAF50;
+$uni-color-busy: #FF9800;
+$uni-color-error-state: #F44336;

+ 164 - 0
admin-mp/src/utils/dict.js

@@ -0,0 +1,164 @@
+import { getDataDictList } from '../api/dict.js'
+import { storage } from './index.js'
+
+const DICT_KEY = 'dicts'
+
+/**
+ * 字典工具模块 — 对标 PC 端 admin-web-new/src/utils/dict.ts
+ */
+const dictUtil = {
+  /**
+   * 加载全部字典数据,按 code 分组后存入 storage
+   * @returns {Promise<Object>} 分组后的字典数据
+   */
+  loadDicts: async () => {
+    try {
+      const res = await getDataDictList()
+      if (res && res.code === 200) {
+        const list = res.data
+        const dictGroup = dictUtil.groupByKey(list, 'code')
+        storage.set(DICT_KEY, dictGroup)
+        return dictGroup
+      }
+      return {}
+    } catch (e) {
+      console.error('加载字典数据失败:', e)
+      return {}
+    }
+  },
+
+  /**
+   * 获取缓存的全部字典数据
+   * @returns {Object} { code: [items] }
+   */
+  getDicts: () => {
+    return storage.get(DICT_KEY) || {}
+  },
+
+  /**
+   * 获取指定编码的字典项列表
+   * @param {string} code - 字典编码
+   * @returns {Array} 字典项数组
+   */
+  getDictList: (code) => {
+    const dicts = dictUtil.getDicts()
+    return dicts[code] || []
+  },
+
+  /**
+   * 根据字典编码和值获取显示名称
+   * @param {string} code - 字典编码
+   * @param {*} value - 字典值
+   * @returns {string} 显示名称
+   */
+  getDictLabel: (code, value) => {
+    if (value === null || value === undefined || value === '') {
+      return value
+    }
+    const dicts = dictUtil.getDicts()
+    const list = dicts[code]
+    if (!list || list.length === 0) {
+      return String(value)
+    }
+    const item = list.find(k => k.value == value)
+    return item ? item.name : String(value)
+  },
+
+  /**
+   * 根据字典编码和名称反查值
+   * @param {string} code - 字典编码
+   * @param {string} name - 显示名称
+   * @returns {*} 字典值
+   */
+  getDictValue: (code, name) => {
+    const dicts = dictUtil.getDicts()
+    const list = dicts[code]
+    if (!list || list.length === 0) {
+      return null
+    }
+    const item = list.find(k => k.name === name)
+    return item ? item.value : null
+  },
+
+  /**
+   * 根据字典编码和值获取颜色
+   * @param {string} code - 字典编码
+   * @param {*} value - 字典值
+   * @returns {string} 颜色值
+   */
+  getDictColor: (code, value) => {
+    if (value === null || value === undefined) {
+      return ''
+    }
+    const list = dictUtil.getDictList(code)
+    const item = list.find(k => k.value == value)
+    return item?.color || ''
+  },
+
+  /**
+   * 获取选项列表(用于下拉框等)
+   * @param {string} code - 字典编码
+   * @returns {Array<{label: string, value: any}>}
+   */
+  getDictOptions: (code) => {
+    const list = dictUtil.getDictList(code)
+    return list.map(item => ({
+      label: item.name,
+      value: item.value
+    }))
+  },
+
+  /**
+   * 获取带"全部"选项的筛选器选项列表
+   * @param {string} code - 字典编码
+   * @returns {Array<{label: string, value: any}>}
+   */
+  getDictFilterOptions: (code) => {
+    const opts = dictUtil.getDictOptions(code)
+    return [{ label: '全部', value: '' }, ...opts]
+  },
+
+  /**
+   * 格式化字典值(返回显示名称)
+   */
+  formatDict: (code, value) => {
+    return dictUtil.getDictLabel(code, value)
+  },
+
+  /**
+   * 清除缓存的字典数据
+   */
+  clearDicts: () => {
+    storage.remove(DICT_KEY)
+  },
+
+  /**
+   * 按 key 分组
+   * @param {Array} elements - 原始数组
+   * @param {string} key - 分组字段
+   * @returns {Object} 分组后的对象
+   */
+  groupByKey: (elements, key) => {
+    const map = {}
+    if (!elements || elements.length === 0) {
+      return map
+    }
+    for (let i = 0; i < elements.length; i++) {
+      if (!elements[i][key] && elements[i][key] !== 0) {
+        continue
+      }
+      const k = String(elements[i][key])
+      const tmp = map[k] || []
+      tmp.push(elements[i])
+      map[k] = tmp
+    }
+    return map
+  }
+}
+
+export default dictUtil
+
+// 便捷导出(兼容旧的调用方式)
+export const fmtDictName = (code, value) => dictUtil.getDictLabel(code, value)
+export const getDictColor = (code, value) => dictUtil.getDictColor(code, value)
+export const loadDicts = dictUtil.loadDicts

+ 3 - 53
admin-mp/src/utils/index.js

@@ -123,59 +123,9 @@ export const formatAmount = (amount, isCent = true) => {
   return yuan.toFixed(2)
 }
 
-/**
- * 从字典获取名称(标签)
- * @param {string} code - 字典编码
- * @param {any} value - 字典值
- * @returns {string} 字典名称,找不到时返回原值
- */
-export const fmtDictName = (code, value) => {
-  if (value === null || value === undefined || value === '') {
-    return value
-  }
-
-  const dictStorage = storage.get('dicts')
-
-  if (!dictStorage) {
-    return value
-  }
-
-  const elements = dictStorage[code]
-  if (elements) {
-    const ele = elements.find(k => k.value == value)
-    if (ele) {
-      return ele.name
-    }
-  }
-  return String(value)
-}
-
-/**
- * 从字典获取颜色值
- * @param {string} code - 字典编码
- * @param {any} value - 字典值
- * @returns {string} 颜色值,找不到时返回空字符串
- */
-export const getDictColor = (code, value) => {
-  if (value === null || value === undefined) {
-    return ''
-  }
-
-  const dictStorage = storage.get('dicts')
-
-  if (!dictStorage) {
-    return ''
-  }
-
-  const elements = dictStorage[code]
-  if (elements) {
-    const ele = elements.find(k => k.value == value)
-    if (ele) {
-      return ele.color || ''
-    }
-  }
-  return ''
-}
+// 字典工具函数 — 委托到 utils/dict.js
+import { fmtDictName, getDictColor } from './dict.js'
+export { fmtDictName, getDictColor }
 
 /**
  * 获取请求头

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio