Browse Source

✨ feat: 优化健康二师

Pchen. 1 month ago
parent
commit
e59d799eff
57 changed files with 1153 additions and 5160 deletions
  1. 1 1
      index.html
  2. 4 1
      package.json
  3. 61 0
      pnpm-lock.yaml
  4. 0 120
      public/download.html
  5. 0 0
      public/logo.svg
  6. 0 103
      src/api/ai.js
  7. 0 77
      src/api/ic.js
  8. 0 22
      src/api/jwAccount.js
  9. 1 56
      src/api/lepao.js
  10. 0 65
      src/api/power.js
  11. 0 191
      src/api/qk.js
  12. 0 40
      src/api/qxs.js
  13. 1 1
      src/api/upload.js
  14. 1 1
      src/components/Footer/index.vue
  15. 2 2
      src/components/Header/index.vue
  16. 126 227
      src/components/LepaoAccountCard/accountDetailCard.vue
  17. 2 2
      src/components/Navbar/index.vue
  18. 57 9
      src/components/userCard/userCard.vue
  19. 3 1
      src/main.js
  20. 3 3
      src/pages/Login/Login.vue
  21. 2 2
      src/pages/Login/components/login.vue
  22. 2 2
      src/pages/Login/components/register.vue
  23. 2 2
      src/pages/Login/uniLogin/uniLogin.vue
  24. 7 7
      src/pages/Main/Main.vue
  25. 1 1
      src/pages/Main/components/center.vue
  26. 1 1
      src/pages/User/setting/components/social-bindings.vue
  27. 2 2
      src/pages/admin/goods/addGoods.vue
  28. 1 1
      src/pages/admin/goods/goodsList.vue
  29. 6 6
      src/pages/admin/goods/sendCountRequestList.vue
  30. 123 240
      src/pages/admin/lepaoAccount/accountList.vue
  31. 8 8
      src/pages/admin/lepaoCountLedger/index.vue
  32. 103 37
      src/pages/admin/lepaoRecords/lepaoRecords.vue
  33. 45 46
      src/pages/admin/lepaoRecords/recordDetail.vue
  34. 22 11
      src/pages/admin/mqQueue/index.vue
  35. 0 325
      src/pages/admin/powerTask/index.vue
  36. 0 175
      src/pages/admin/qkBatch/index.vue
  37. 0 493
      src/pages/admin/qkClient/index.vue
  38. 0 328
      src/pages/admin/qkReport/index.vue
  39. 0 937
      src/pages/admin/qkTask/index.vue
  40. 0 312
      src/pages/admin/qxsAccount/index.vue
  41. 6 6
      src/pages/admin/reqLog/index.vue
  42. 5 5
      src/pages/admin/workOrder/orderList.vue
  43. 1 1
      src/pages/doc/detail.vue
  44. 204 111
      src/pages/lepao/accountList/index.vue
  45. 28 8
      src/pages/lepao/countLedger/index.vue
  46. 101 37
      src/pages/lepao/lepaoRecords/index.vue
  47. 45 45
      src/pages/lepao/lepaoRecords/recordDetail.vue
  48. 6 6
      src/pages/path/pathList.vue
  49. 0 639
      src/pages/qk/taskList.vue
  50. 0 259
      src/pages/qxs/getBookList.vue
  51. 1 1
      src/pages/store/goodsList/index.vue
  52. 6 6
      src/pages/store/sendCountRecords/index.vue
  53. 5 157
      src/router/index.js
  54. 22 12
      src/style.css
  55. 78 1
      src/styles/store-theme.less
  56. 57 7
      src/utils/lepaoRecord.js
  57. 1 1
      src/utils/request.js

+ 1 - 1
index.html

@@ -4,7 +4,7 @@
     <meta charset="UTF-8" />
     <link rel="icon" type="image/svg+xml" href="/logo.svg" />
     <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
-    <title>哪吒乐跑 - 智能校园乐跑平台</title>
+    <title>沐晨乐跑 - 智能校园乐跑平台</title>
   </head>
   <body>
     <div id="app"></div>

+ 4 - 1
package.json

@@ -1,5 +1,5 @@
 {
-  "name": "哪吒乐跑",
+  "name": "沐晨乐跑",
   "private": true,
   "version": "1.0.0",
   "type": "module",
@@ -10,8 +10,10 @@
   },
   "dependencies": {
     "@amap/amap-jsapi-loader": "^1.0.1",
+    "@arco-themes/vue-hisense-pangea": "^1.0.5",
     "@arco-themes/vue-hst-oem": "^0.0.1",
     "@arco-themes/vue-mengtu": "^0.1.1",
+    "@arco-themes/vue-taolu-publishing-platform": "^0.0.2",
     "@chenfengyuan/vue-qrcode": "^2.0.0",
     "@vladmandic/face-api": "^1.7.15",
     "@wangeditor/editor": "^5.1.23",
@@ -19,6 +21,7 @@
     "axios": "^1.8.3",
     "crypto-js": "^4.2.0",
     "echarts": "^5.6.0",
+    "import": "^0.0.6",
     "jsencrypt": "^3.3.2",
     "lru-cache": "^11.1.0",
     "markdown-it": "^14.1.0",

+ 61 - 0
pnpm-lock.yaml

@@ -11,9 +11,18 @@ importers:
       '@amap/amap-jsapi-loader':
         specifier: ^1.0.1
         version: 1.0.1
+      '@arco-themes/vue-hisense-pangea':
+        specifier: ^1.0.5
+        version: 1.0.5(@arco-design/web-vue@2.57.0(vue@3.5.18))
+      '@arco-themes/vue-hst-oem':
+        specifier: ^0.0.1
+        version: 0.0.1(@arco-design/web-vue@2.57.0(vue@3.5.18))
       '@arco-themes/vue-mengtu':
         specifier: ^0.1.1
         version: 0.1.1(@arco-design/web-vue@2.57.0(vue@3.5.18))
+      '@arco-themes/vue-taolu-publishing-platform':
+        specifier: ^0.0.2
+        version: 0.0.2(@arco-design/web-vue@2.57.0(vue@3.5.18))
       '@chenfengyuan/vue-qrcode':
         specifier: ^2.0.0
         version: 2.0.0(qrcode@1.5.4)(vue@3.5.18)
@@ -35,6 +44,9 @@ importers:
       echarts:
         specifier: ^5.6.0
         version: 5.6.0
+      import:
+        specifier: ^0.0.6
+        version: 0.0.6
       jsencrypt:
         specifier: ^3.3.2
         version: 3.5.4
@@ -98,11 +110,26 @@ packages:
     peerDependencies:
       vue: ^3.1.0
 
+  '@arco-themes/vue-hisense-pangea@1.0.5':
+    resolution: {integrity: sha512-DyeLRb2zAdX4wqHrVztBQ1eoiRIak/P9sYrq5p4559TscH0ngxwL9U34u8QCpTGcZG+NCCVDU0pE43jmYEr7ng==}
+    peerDependencies:
+      '@arco-design/web-vue': ^2.56.3
+
+  '@arco-themes/vue-hst-oem@0.0.1':
+    resolution: {integrity: sha512-OlSgpy5XykTFvlmyR3fpCrjwnlzrMmiqHUCzEs24Z3W/mPK842GlkRBdvgBpatx831BSrdvtbsh8iVv/HhMxvg==}
+    peerDependencies:
+      '@arco-design/web-vue': ^2.38.0
+
   '@arco-themes/vue-mengtu@0.1.1':
     resolution: {integrity: sha512-7WqM8Vvzb3HszdRU6pdhSjwP6OxT2EchRHohAZD1W7uKdlbML5Y3PdEdpZojfCYKXjPf7zXiFNpgD6NjWg0WfQ==}
     peerDependencies:
       '@arco-design/web-vue': ^2.54.3
 
+  '@arco-themes/vue-taolu-publishing-platform@0.0.2':
+    resolution: {integrity: sha512-LPMzx/Lf8L20eyiD1w3mNacdz4cthd6K312D83No4QniHbxy+BWqhs919DK52RsIAVCI9IXRxP4q759YVorfjw==}
+    peerDependencies:
+      '@arco-design/web-vue': ^2.37.1
+
   '@babel/helper-string-parser@7.27.1':
     resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
     engines: {node: '>=6.9.0'}
@@ -800,6 +827,11 @@ packages:
   immer@9.0.21:
     resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
 
+  import@0.0.6:
+    resolution: {integrity: sha512-QPhTdjy9J4wUzmWSG7APkSgMFuPGPw+iJTYUblcfc2AfpqaatbwgCldK1HoLYx+v/+lWvab63GWZtNkcnj9JcQ==}
+    engines: {node: '>=0.10.0'}
+    hasBin: true
+
   is-arrayish@0.3.2:
     resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
 
@@ -921,6 +953,9 @@ packages:
   number-precision@1.6.0:
     resolution: {integrity: sha512-05OLPgbgmnixJw+VvEh18yNPUo3iyp4BEWJcrLu4X9W05KmMifN7Mu5exYvQXqxxeNWhvIF+j3Rij+HmddM/hQ==}
 
+  optimist@0.3.7:
+    resolution: {integrity: sha512-TCx0dXQzVtSCg2OgY/bO9hjM9cV4XYx09TVK+s3+FhkjT6LovsLe+pPMzpWf+6yXK/hUizs2gUoTw3jHM0VaTQ==}
+
   p-limit@2.3.0:
     resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
     engines: {node: '>=6'}
@@ -1179,6 +1214,10 @@ packages:
   wildcard@1.1.2:
     resolution: {integrity: sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==}
 
+  wordwrap@0.0.3:
+    resolution: {integrity: sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==}
+    engines: {node: '>=0.4.0'}
+
   wrap-ansi@6.2.0:
     resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
     engines: {node: '>=8'}
@@ -1217,10 +1256,22 @@ snapshots:
       scroll-into-view-if-needed: 2.2.31
       vue: 3.5.18
 
+  '@arco-themes/vue-hisense-pangea@1.0.5(@arco-design/web-vue@2.57.0(vue@3.5.18))':
+    dependencies:
+      '@arco-design/web-vue': 2.57.0(vue@3.5.18)
+
+  '@arco-themes/vue-hst-oem@0.0.1(@arco-design/web-vue@2.57.0(vue@3.5.18))':
+    dependencies:
+      '@arco-design/web-vue': 2.57.0(vue@3.5.18)
+
   '@arco-themes/vue-mengtu@0.1.1(@arco-design/web-vue@2.57.0(vue@3.5.18))':
     dependencies:
       '@arco-design/web-vue': 2.57.0(vue@3.5.18)
 
+  '@arco-themes/vue-taolu-publishing-platform@0.0.2(@arco-design/web-vue@2.57.0(vue@3.5.18))':
+    dependencies:
+      '@arco-design/web-vue': 2.57.0(vue@3.5.18)
+
   '@babel/helper-string-parser@7.27.1': {}
 
   '@babel/helper-validator-identifier@7.27.1': {}
@@ -1876,6 +1927,10 @@ snapshots:
 
   immer@9.0.21: {}
 
+  import@0.0.6:
+    dependencies:
+      optimist: 0.3.7
+
   is-arrayish@0.3.2: {}
 
   is-fullwidth-code-point@3.0.0: {}
@@ -1982,6 +2037,10 @@ snapshots:
 
   number-precision@1.6.0: {}
 
+  optimist@0.3.7:
+    dependencies:
+      wordwrap: 0.0.3
+
   p-limit@2.3.0:
     dependencies:
       p-try: 2.2.0
@@ -2188,6 +2247,8 @@ snapshots:
 
   wildcard@1.1.2: {}
 
+  wordwrap@0.0.3: {}
+
   wrap-ansi@6.2.0:
     dependencies:
       ansi-styles: 4.3.0

+ 0 - 120
public/download.html

@@ -1,120 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-
-<head>
-  <meta charset="UTF-8">
-  <meta http-equiv="refresh" content="0;url=/#/download">
-  <meta name="viewport" content="width=device-width, initial-scale=1.0">
-  <title>哪吒乐跑应用下载中心</title>
-  <link rel="icon" type="image/svg+xml" href="//xxoo365.top/logo.svg" />
-  <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
-</head>
-
-<body class="bg-gray-100 min-h-screen flex items-center justify-center">
-
-  <div class="max-w-3xl w-full bg-white shadow-xl rounded-2xl p-6 md:p-10">
-
-    <!-- 头部 -->
-    <div class="text-center mb-8">
-      <h1 class="text-3xl font-bold text-gray-800">哪吒乐跑应用下载中心</h1>
-      <!-- <p class="text-gray-500 mt-2">获取最新版本与历史版本</p> -->
-    </div>
-
-    <!-- 应用信息 -->
-    <div class="flex items-center gap-4 mb-8">
-      <img src="//xxoo365.top/logo.svg" alt="App Icon" class="w-20 h-20 rounded-xl">
-      <div>
-        <h2 class="text-xl font-semibold">哪吒乐跑智能校园乐跑平台</h2>
-        <p class="text-gray-500 text-sm">稳定 · 高效 · 安全</p>
-        <p class="text-gray-400 text-sm mt-1"></p>
-      </div>
-    </div>
-
-    <div class="space-y-6">
-      <div class="border rounded-xl p-5 hover:shadow-md transition">
-        <div class="flex items-center justify-between">
-          <div>
-            <h3 class="text-lg font-medium">乐跑登录器(Android手机版 - 推荐使用)</h3>
-            <p class="text-gray-500 text-sm">支持 Android 8.0 及以上,仅包含登录更新乐跑账号的功能</p>
-          </div>
-          <a href="//download.xxoo365.top/files/哪吒乐跑安卓登录器1.2.1.apk" class="bg-blue-600 text-white px-5 py-2 rounded-lg hover:bg-blue-700" target="_blank">
-            下载 APK
-          </a>
-        </div>
-
-        <div class="mt-4 text-sm text-gray-500">
-          当前版本:
-          <a href="#" class="text-blue-600 hover:underline">v1.2.1</a>
-        </div>
-      </div>
-
-      <div class="border rounded-xl p-5 hover:shadow-md transition">
-        <div class="flex items-center justify-between">
-          <div>
-            <h3 class="text-lg font-medium">乐跑登录器(Windows电脑版)</h3>
-            <p class="text-gray-500 text-sm">支持 Windows 10/11 64位系统,仅包含登录更新乐跑账号的功能,体积小巧</p>
-          </div>
-          <a href="//download.xxoo365.top/files/哪吒乐跑 登录器.exe" class="bg-green-600 text-white px-5 py-2 rounded-lg hover:bg-green-700" target="_blank">
-            下载 EXE
-          </a>
-        </div>
-
-        <div class="mt-4 text-sm text-gray-500">
-          当前版本:
-          <a href="#" class="text-blue-600 hover:underline">v1.0.0</a>
-        </div>
-      </div>
-
-      <div class="border rounded-xl p-5 hover:shadow-md transition">
-        <div class="flex items-center justify-between">
-          <div>
-            <h3 class="text-lg font-medium">哪吒乐跑乐跑客户端(Windows电脑版)</h3>
-            <p class="text-gray-500 text-sm">支持 Windows 10/11 64位系统,涵盖平台所有功能,体积小巧</p>
-          </div>
-          <a href="//download.xxoo365.top/files/哪吒乐跑智能校园乐跑平台-Windows-1.0.1-Setup.exe" class="bg-green-600 text-white px-5 py-2 rounded-lg hover:bg-green-700" target="_blank">
-            下载 EXE
-          </a>
-        </div>
-
-        <div class="mt-4 text-sm text-gray-500">
-          当前版本:
-          <a href="#" class="text-blue-600 hover:underline">v1.0.0</a>
-        </div>
-      </div>
-
-      <div class="border rounded-xl p-5 hover:shadow-md transition">
-        <div class="flex items-center justify-between">
-          <div>
-            <h3 class="text-lg font-medium">iOS - 操作说明</h3>
-            <p class="text-gray-500 text-sm">iOS系统无自动化乐跑登录软件,需要用户下载安装证书后手动设置登录。</p>
-          </div>
-          <a href="https:\/\/lepao-cloud.xxoo365.top/down.php\/fba1d571166b4c95592c7c4e624a9390.pdf" class="bg-black text-white px-5 py-2 rounded-lg hover:opacity-90" target="_blank">
-            操作说明下载
-          </a>
-        </div>
-
-       <div class="mt-4 text-sm text-gray-500">
-          当前版本:
-          <a href="#" class="text-blue-600 hover:underline">v1.0.0</a>
-        </div>
-      </div>
-    </div>
-
-    <!-- 更新日志 -->
-    <div class="mt-10">
-      <h3 class="text-lg font-semibold mb-3"></h3>
-      <ul class="text-sm text-gray-600 space-y-2 list-disc pl-5">
-        <li></li>
-      </ul>
-    </div>
-
-    <!-- 底部 -->
-    <div class="mt-10 text-center text-sm text-gray-400">
-      © 2026 哪吒乐跑智能校园乐跑平台
-    </div>
-
-  </div>
-
-</body>
-
-</html>

File diff suppressed because it is too large
+ 0 - 0
public/logo.svg


+ 0 - 103
src/api/ai.js

@@ -1,103 +0,0 @@
-import request from '@/utils/request'
-
-const api = {
-    ScanRepo: '/AI/ScanRepo',
-    GetScanTaskList: '/AI/GetScanTaskList',
-    GetScanTaskDetail: '/AI/GetScanTaskDetail',
-    GetCommitSummary: '/AI/GetCommitSummary',
-    SummaryCommit: '/AI/SummaryCommit',
-    SummaryFile: '/AI/SummaryFile',
-    GetFileSummary: '/AI/GetFileSummary',
-    GetAIChatMessages: '/AI/GetAIChatMessages',
-    GetAIChatMessage: '/AI/GetAIChatMessage',
-    DeleteAIChatMessages: '/AI/DeleteAIChatMessages',
-    AIChat: '/AI/Chat'
-}
-
-export function ScanRepo(parameter) {
-    return request({
-        url: api.ScanRepo,
-        method: 'get',
-        params: parameter
-    })
-}
-
-export function GetScanTaskList(parameter) {
-    return request({
-        url: api.GetScanTaskList,
-        method: 'get',
-        params: parameter
-    })
-}
-
-export function GetScanTaskDetail(parameter) {
-    return request({
-        url: api.GetScanTaskDetail,
-        method: 'get',
-        params: parameter
-    })
-}
-
-export function GetCommitSummary(parameter) {
-    return request({
-        url: api.GetCommitSummary,
-        method: 'get',
-        params: parameter
-    })
-}
-
-export function SummaryCommit(parameter) {
-    return request({
-        url: api.SummaryCommit,
-        method: 'get',
-        params: parameter
-    })
-}
-
-export function SummaryFile(parameter) {
-    return request({
-        url: api.SummaryFile,
-        method: 'get',
-        params: parameter
-    })
-}
-
-export function GetFileSummary(parameter) {
-    return request({
-        url: api.GetFileSummary,
-        method: 'get',
-        params: parameter
-    })
-}
-
-export function GetAIChatMessages(parameter) {
-    return request({
-        url: api.GetAIChatMessages,
-        method: 'get',
-        params: parameter
-    })
-}
-
-export function GetAIChatMessage(parameter) {
-    return request({
-        url: api.GetAIChatMessage,
-        method: 'get',
-        params: parameter
-    })
-}
-
-export function DeleteAIChatMessages(parameter) {
-    return request({
-        url: api.DeleteAIChatMessages,
-        method: 'get',
-        params: parameter
-    })
-}
-
-export function AIChat(parameter) {
-    return request({
-        url: api.AIChat,
-        method: 'post',
-        data: parameter
-    })
-}

+ 0 - 77
src/api/ic.js

@@ -1,77 +0,0 @@
-import request from '../utils/request'
-
-const api = {
-  getSeatMenu: '/IC/GetSeatMenu',
-  getSeatInfo: '/IC/GetSeatInfo',
-  seat: '/IC/Seat',
-  endAhead: '/IC/Seat/EndAhead',
-  star: '/IC/Star',
-  starGroup: '/IC/StarGroup',
-  rule: '/IC/Rule',
-  myOrder: '/IC/Seat/MyOrder',
-  orderHistory: '/IC/OrderHistory'
-}
-
-export function getSeatMenu(parameter) {
-  return request({ url: api.getSeatMenu, method: 'get', params: parameter })
-}
-
-export function getSeatInfo(parameter) {
-  return request({ url: api.getSeatInfo, method: 'get', params: parameter })
-}
-
-export function orderSeat(parameter) {
-  return request({ url: api.seat, method: 'post', data: parameter })
-}
-
-export function myOrder(parameter) {
-  return request({ url: api.myOrder, method: 'post', data: parameter })
-}
-
-export function deleteSeat(parameter) {
-  return request({ url: api.seat, method: 'delete', data: parameter })
-}
-
-export function endAhead(parameter) {
-  return request({ url: api.endAhead, method: 'post', data: parameter })
-}
-
-export function getStar() {
-  return request({ url: api.star, method: 'get' })
-}
-
-export function saveStar(parameter) {
-  return request({ url: api.star, method: 'post', data: parameter })
-}
-
-export function deleteStar(parameter) {
-  return request({ url: api.star, method: 'delete', data: parameter })
-}
-
-export function getStarGroup() {
-  return request({ url: api.starGroup, method: 'get' })
-}
-
-export function saveStarGroup(parameter) {
-  return request({ url: api.starGroup, method: 'post', data: parameter })
-}
-
-export function deleteStarGroup(parameter) {
-  return request({ url: api.starGroup, method: 'delete', data: parameter })
-}
-
-export function getRule() {
-  return request({ url: api.rule, method: 'get' })
-}
-
-export function saveRule(parameter) {
-  return request({ url: api.rule, method: 'post', data: parameter })
-}
-
-export function deleteRule(parameter) {
-  return request({ url: api.rule, method: 'delete', data: parameter })
-}
-
-export function getOrderHistory() {
-  return request({ url: api.orderHistory, method: 'get' })
-}

+ 0 - 22
src/api/jwAccount.js

@@ -1,22 +0,0 @@
-import request from '../utils/request'
-
-const api = {
-  account: '/JW/Account',
-  active: '/JW/ActiveAccount'
-}
-
-export function getJwAccount() {
-  return request({ url: api.account, method: 'get' })
-}
-
-export function saveJwAccount(parameter) {
-  return request({ url: api.account, method: 'post', data: parameter })
-}
-
-export function deleteJwAccount(parameter) {
-  return request({ url: api.account, method: 'delete', data: parameter })
-}
-
-export function activeJwAccount(parameter) {
-  return request({ url: api.active, method: 'post', data: parameter })
-}

+ 1 - 56
src/api/lepao.js

@@ -3,23 +3,16 @@ import request from '../utils/request'
 const api = {
   Account: '/Lepao/Account',
   UpdateSelfAccount: '/Lepao/Account/UpdateSelfAccount',
-  AdminUpdateAccountInfo: '/Admin/Lepao/Account/UpdateAccountInfo',
   AdminAccount: '/Admin/Lepao/Account',
   CountLedgerMyList: '/Lepao/Count/Ledger/MyList',
   CountLedgerAdminList: '/Admin/Lepao/Count/Ledger/List',
   Records: '/Lepao/Records',
-  OfficialTermRecords: '/Lepao/OfficialTermRecords',
   ChangeAutoRun: '/Lepao/ChangeAutoRun',
   SingleRun: '/Lepao/SingleRun',
   JkesRunOverview: '/Lepao/JkesRunOverview',
   GetRecordDetail: '/Lepao/GetRecordDetail',
   AdminRecords: '/Admin/Lepao/Records',
-  AdminOfficialTermRecords: '/Admin/Lepao/OfficialTermRecords',
-  AdminGetRecordDetail: '/Admin/Lepao/GetRecordDetail',
-  BeginFaceReco: '/Face/BeginFaceReco',
-  BindAuditList: '/Lepao/BindAudit/List',
-  AdminBindAuditByAccount: '/Admin/Lepao/BindAudit/ByAccount',
-  AdminBindAuditList: '/Admin/Lepao/BindAudit/List'
+  AdminGetRecordDetail: '/Admin/Lepao/GetRecordDetail'
 }
 
 export function addAccount (parameter) {
@@ -54,14 +47,6 @@ export function adminAccountList (parameter) {
   })
 }
 
-export function adminUpdateAccountInfo (parameter) {
-  return request({
-    url: api.AdminUpdateAccountInfo,
-    method: 'post',
-    data: parameter
-  })
-}
-
 export function deleteAccount (parameter) {
   return request({
     url: api.Account,
@@ -86,22 +71,6 @@ export function adminLepaoRecords (parameter) {
   })
 }
 
-export function officialTermRecords (parameter) {
-  return request({
-    url: api.OfficialTermRecords,
-    method: 'get',
-    params: parameter
-  })
-}
-
-export function adminOfficialTermRecords (parameter) {
-  return request({
-    url: api.AdminOfficialTermRecords,
-    method: 'get',
-    params: parameter
-  })
-}
-
 export function changeAutoRun (parameter) {
   return request({
     url: api.ChangeAutoRun,
@@ -143,14 +112,6 @@ export function adminGetRecordDetail (parameter) {
   })
 }
 
-export function BeginFaceReco (parameter) {
-  return request({
-    url: api.BeginFaceReco,
-    method: 'post',
-    data: parameter
-  })
-}
-
 export function getMyCountLedger (parameter) {
   return request({
     url: api.CountLedgerMyList,
@@ -166,19 +127,3 @@ export function getAdminCountLedger (parameter) {
     params: parameter
   })
 }
-
-export function getAdminBindAuditByAccount (parameter) {
-  return request({
-    url: api.AdminBindAuditByAccount,
-    method: 'get',
-    params: parameter
-  })
-}
-
-export function getAdminBindAuditList (parameter) {
-  return request({
-    url: api.AdminBindAuditList,
-    method: 'get',
-    params: parameter
-  })
-}

+ 0 - 65
src/api/power.js

@@ -1,65 +0,0 @@
-import request from '../utils/request'
-
-const api = {
-  Account: '/Power/Account',
-  GetChangeRecord: '/Power/GetChangeRecord',
-  GetPowerData: '/Power/GetPowerData',
-  AdminTaskList: '/Admin/Power/Task/List',
-  AdminGetChangeRecord: '/Admin/Power/Task/GetChangeRecord'
-}
-
-export function getPowerData(parameter) {
-  return request({
-    url: api.GetPowerData,
-    method: 'post',
-    data: parameter
-  })
-}
-
-export function getChangeRecord(parameter) {
-  return request({
-    url: api.GetChangeRecord,
-    method: 'post',
-    data: parameter
-  })
-}
-
-export function addAccount(parameter) {
-  return request({
-    url: api.Account,
-    method: 'post',
-    data: parameter
-  })
-}
-
-export function deleteAccount(parameter) {
-  return request({
-    url: api.Account,
-    method: 'delete',
-    data: parameter
-  })
-}
-
-export function getAccount(parameter) {
-  return request({
-    url: api.Account,
-    method: 'get',
-    params: parameter
-  })
-}
-
-export function adminPowerTaskList(parameter) {
-  return request({
-    url: api.AdminTaskList,
-    method: 'get',
-    params: parameter
-  })
-}
-
-export function adminPowerGetChangeRecord(parameter) {
-  return request({
-    url: api.AdminGetChangeRecord,
-    method: 'get',
-    params: parameter
-  })
-}

+ 0 - 191
src/api/qk.js

@@ -1,191 +0,0 @@
-import request from '../utils/request'
-
-const api = {
-  Task: '/QK/Task',
-  TaskDetail: '/QK/Task/Detail',
-  AdminClientList: '/Admin/QK/Client/List',
-  AdminClientDelete: '/Admin/QK/Client/Delete',
-  AdminClientEnable: '/Admin/QK/Client/Enable',
-  AdminTaskList: '/Admin/QK/Task/List',
-  AdminTaskDetail: '/Admin/QK/Task/Detail',
-  AdminTaskUpdate: '/Admin/QK/Task/Update',
-  AdminTaskCancel: '/Admin/QK/Task/Cancel',
-  AdminTaskRetry: '/Admin/QK/Task/Retry',
-  AdminTaskStart: '/Admin/QK/Task/Start',
-  AdminTaskPause: '/Admin/QK/Task/Pause',
-  AdminReportList: '/Admin/QK/Report/List',
-  TaskStart: '/QK/Task/Start',
-  TaskPause: '/QK/Task/Pause',
-  Batch: '/QK/Batch',
-  AdminBatchList: '/Admin/QK/Batch/List',
-  AdminBatchSave: '/Admin/QK/Batch/Save',
-  AdminJwAccount: '/Admin/JW/Account'
-}
-
-export function getQkTask(parameter) {
-  return request({
-    url: api.Task,
-    method: 'get',
-    params: parameter
-  })
-}
-
-export function getQkTaskDetail(parameter) {
-  return request({
-    url: api.TaskDetail,
-    method: 'get',
-    params: parameter
-  })
-}
-
-export function saveQkTask(parameter) {
-  return request({
-    url: api.Task,
-    method: 'post',
-    data: parameter
-  })
-}
-
-export function deleteQkTask(parameter) {
-  return request({
-    url: api.Task,
-    method: 'delete',
-    data: parameter
-  })
-}
-
-export function startQkTask(parameter) {
-  return request({
-    url: api.TaskStart,
-    method: 'post',
-    data: parameter
-  })
-}
-
-export function pauseQkTask(parameter) {
-  return request({
-    url: api.TaskPause,
-    method: 'post',
-    data: parameter
-  })
-}
-
-export function getQkBatchList(parameter) {
-  return request({
-    url: api.Batch,
-    method: 'get',
-    params: parameter
-  })
-}
-
-export function adminQkBatchList(parameter) {
-  return request({
-    url: api.AdminBatchList,
-    method: 'get',
-    params: parameter
-  })
-}
-
-export function adminQkBatchSave(parameter) {
-  return request({
-    url: api.AdminBatchSave,
-    method: 'post',
-    data: parameter
-  })
-}
-
-export function adminQkClientList(parameter) {
-  return request({
-    url: api.AdminClientList,
-    method: 'get',
-    params: parameter
-  })
-}
-
-export function adminQkClientDelete(parameter) {
-  return request({
-    url: api.AdminClientDelete,
-    method: 'delete',
-    data: parameter
-  })
-}
-
-export function adminQkClientEnable(parameter) {
-  return request({
-    url: api.AdminClientEnable,
-    method: 'post',
-    data: parameter
-  })
-}
-
-export function adminQkTaskList(parameter) {
-  return request({
-    url: api.AdminTaskList,
-    method: 'get',
-    params: parameter
-  })
-}
-
-export function adminQkTaskDetail(parameter) {
-  return request({
-    url: api.AdminTaskDetail,
-    method: 'get',
-    params: parameter
-  })
-}
-
-export function adminQkTaskUpdate(parameter) {
-  return request({
-    url: api.AdminTaskUpdate,
-    method: 'post',
-    data: parameter
-  })
-}
-
-export function adminQkTaskCancel(parameter) {
-  return request({
-    url: api.AdminTaskCancel,
-    method: 'delete',
-    data: parameter
-  })
-}
-
-export function adminQkTaskRetry(parameter) {
-  return request({
-    url: api.AdminTaskRetry,
-    method: 'post',
-    data: parameter
-  })
-}
-
-export function adminQkTaskStart(parameter) {
-  return request({
-    url: api.AdminTaskStart,
-    method: 'post',
-    data: parameter
-  })
-}
-
-export function adminQkTaskPause(parameter) {
-  return request({
-    url: api.AdminTaskPause,
-    method: 'post',
-    data: parameter
-  })
-}
-
-export function adminQkReportList(parameter) {
-  return request({
-    url: api.AdminReportList,
-    method: 'get',
-    params: parameter
-  })
-}
-
-export function adminGetJwAccount(parameter) {
-  return request({
-    url: api.AdminJwAccount,
-    method: 'get',
-    params: parameter
-  })
-}

+ 0 - 40
src/api/qxs.js

@@ -1,40 +0,0 @@
-import request from '../utils/request'
-
-const api = {
-  getBookList: '/QXS/GetBookList',
-  getBookImg: '/QXS/GetBookImg',
-  AdminAccountList: '/Admin/QXS/Account/List',
-  AdminAccountDetail: '/Admin/QXS/Account/Detail'
-}
-
-export function GetBookList(parameter) {
-  return request({
-    url: api.getBookList,
-    method: 'post',
-    data: parameter
-  })
-}
-
-export function GetBookImg(parameter) {
-  return request({
-    url: api.getBookImg,
-    method: 'get',
-    params: parameter
-  })
-}
-
-export function adminQxsAccountList(parameter) {
-  return request({
-    url: api.AdminAccountList,
-    method: 'get',
-    params: parameter
-  })
-}
-
-export function adminQxsAccountDetail(parameter) {
-  return request({
-    url: api.AdminAccountDetail,
-    method: 'get',
-    params: parameter
-  })
-}

+ 1 - 1
src/api/upload.js

@@ -2,7 +2,7 @@ import request from '@/utils/request'
 
 const api = {
     uploadImg: '/UploadPicture',
-    uploadAvatar: '/uploadAvatar'
+    uploadAvatar: '/UploadAvatar'
 }
 
 export function uploadImg(parameter) {

+ 1 - 1
src/components/Footer/index.vue

@@ -1,6 +1,6 @@
 <template>
     <div class="footer">
-        <span>© {{ new Date().getFullYear() }} 哪吒乐跑 - 智能校园乐跑平台</span>
+        <span>© {{ new Date().getFullYear() }} 沐晨乐跑 - 智能校园乐跑平台</span>
     </div>
 </template>
 

+ 2 - 2
src/components/Header/index.vue

@@ -2,8 +2,8 @@
     <header class="site-header">
         <div class="header-inner">
             <div class="logo" @click="$router.push('/')">
-                <img alt="哪吒乐跑" src="/logo.svg" height="40">
-                <span class="title">哪吒乐跑</span>
+                <img alt="沐晨乐跑" src="/logo.svg" height="40">
+                <span class="title">沐晨乐跑</span>
             </div>
             <div class="nav-actions" :style="{ color: props.color }">
                 <div class="nav-links">

+ 126 - 227
src/components/LepaoAccountCard/accountDetailCard.vue

@@ -22,7 +22,6 @@
 
         <a-descriptions :column="isMobile ? 1 : 3" bordered size="small" class="account-desc">
           <a-descriptions-item v-if="admin" label="创建用户">{{ account.create_user || '-' }}</a-descriptions-item>
-          <a-descriptions-item v-if="admin" label="人脸状态">{{ faceStateLabel }}</a-descriptions-item>
           <a-descriptions-item v-if="admin" label="手机型号">{{ account.deviceModel || '-' }}</a-descriptions-item>
           <a-descriptions-item label="学院">{{ account.academy_name || '-' }}</a-descriptions-item>
           <a-descriptions-item label="跑区">{{ account.area || '随机分配' }}</a-descriptions-item>
@@ -33,7 +32,11 @@
           </a-descriptions-item>
           <a-descriptions-item label="自动星期">{{ autoDayLabel }}</a-descriptions-item>
           <a-descriptions-item label="自动时段">{{ autoTimeLabel }}</a-descriptions-item>
-          <a-descriptions-item label="学期目标">{{ termProgressLabel }}</a-descriptions-item>
+          <a-descriptions-item label="月度目标里程">{{ targetKmLabel }}</a-descriptions-item>
+          <a-descriptions-item label="本月里程">{{ formatKm(account.term_num) }}</a-descriptions-item>
+          <a-descriptions-item label="累计里程">{{ formatKm(account.total_num) }}</a-descriptions-item>
+          <a-descriptions-item v-if="account.auto_run" label="自动距离区间">{{ autoDistanceLabel }}</a-descriptions-item>
+          <a-descriptions-item v-if="account.auto_run" label="自动配速区间">{{ autoPaceLabel }}</a-descriptions-item>
           <a-descriptions-item label="添加时间">{{ formatTime(account.create_time) }}</a-descriptions-item>
           <a-descriptions-item label="上次更新">{{ account.update_time ? formatTime(account.update_time) : '待登录'
             }}</a-descriptions-item>
@@ -49,87 +52,71 @@
 
         <div class="section-title">
           <span>乐跑记录</span>
-          <span class="record-count">{{ activeRecordCountLabel }}</span>
+          <span class="record-count">{{ recordCountLabel }}</span>
         </div>
-        <a-tabs v-model:active-key="recordTab" @change="handleRecordTabChange">
-          <a-tab-pane key="platform" title="平台记录">
-            <a-table :data="records" :pagination="{ pageSize: 8, showTotal: true }" :scroll="{ x: 900, y: 280 }"
-              size="small" row-key="id" :bordered="false">
-              <template #columns>
-                <a-table-column v-if="admin" title="所属用户" :width="120">
-                  <template #cell="{ record }">{{ record.username || '-' }}</template>
-                </a-table-column>
-                <a-table-column title="状态" :width="200">
-                  <template #cell="{ record }">
-                    <div class="state">
-                      <div class="circle" :class="record.result?.record_failed_reason === '自动确认有效' ? 'one' : 'else'" />
-                      {{ record.result?.record_failed_reason }}
-                    </div>
-                  </template>
-                </a-table-column>
-                <a-table-column title="跑区" :width="180">
-                  <template #cell="{ record }">{{ record.result?.pass_tit }}</template>
-                </a-table-column>
-                <a-table-column title="距离" :width="90">
-                  <template #cell="{ record }">{{ record.result?.distance }} Km</template>
-                </a-table-column>
-                <a-table-column title="时长" :width="100">
-                  <template #cell="{ record }">{{ formatSecondsToMinSec(record.result?.time) }}</template>
-                </a-table-column>
-                <a-table-column title="配速" :width="80">
-                  <template #cell="{ record }">
-                    {{ calculatePace(record.result?.time, record.result?.distance) }}
-                  </template>
-                </a-table-column>
-                <a-table-column title="乐跑时间" :width="160">
-                  <template #cell="{ record }">{{ formatTime(record.time) }}</template>
-                </a-table-column>
-                <a-table-column title="" :width="90" :fixed="tableFixed('right')">
-                  <template #cell="{ record }">
-                    <a-button type="text" size="small" @click="openPlatformRecordDetail(record)">详情</a-button>
-                  </template>
-                </a-table-column>
+        <a-table :data="records" :pagination="{ pageSize: 8, showTotal: true }" :scroll="{ x: 980, y: 280 }"
+          size="small" row-key="public_id" :bordered="false">
+          <template #columns>
+            <a-table-column v-if="admin" title="所属用户" :width="120">
+              <template #cell="{ record }">{{ record.username || '-' }}</template>
+            </a-table-column>
+            <a-table-column title="状态" :width="200">
+              <template #cell="{ record }">
+                <div class="state">
+                  <div class="circle" :class="recordDisplay(record).statusLevel" />
+                  {{ recordDisplay(record).statusText }}
+                </div>
               </template>
-            </a-table>
-          </a-tab-pane>
-          <a-tab-pane key="official" title="官方记录" :disabled="!canViewOfficialRecords">
-            <a-alert v-if="!canViewOfficialRecords" type="warning" class="official-record-alert"
-              content="仅状态为正常的账号可查看官方记录,请使用登录器更新账号后再试。" />
-            <a-table :data="officialRecords" :loading="officialLoading" :pagination="officialPaginationConfig"
-              :scroll="{ x: 900, y: 280 }" size="small" row-key="id" :bordered="false"
-              @page-change="handleOfficialPageChange">
-              <template #columns>
-                <a-table-column title="状态" :width="70">
-                  <template #cell="{ record }">
-                    <a-tag :color="String(record.record_status) === '1' ? 'green' : 'orangered'">
-                      {{ officialRecordStatus(record) }}
-                    </a-tag>
-                  </template>
-                </a-table-column> 
-                <a-table-column title="跑区" :width="160">
-                  <template #cell="{ record }">{{ record.title || '-' }}</template>
-                </a-table-column>
-                <a-table-column title="类型" :width="100">
-                  <template #cell="{ record }">{{ record.tag || record.point_type_str || '-' }}</template>
-                </a-table-column>
-                <a-table-column title="配速" :width="80">
-                  <template #cell="{ record }">
-                    {{ calculatePace(Number(record.end_time) - Number(record.start_time), record.distance) }}
-                  </template>
-                </a-table-column>
-                <a-table-column title="距离" :width="90">
-                  <template #cell="{ record }">{{ record.distance || '-' }} Km</template>
-                </a-table-column>
-                <a-table-column title="时长" :width="100">
-                  <template #cell="{ record }">{{ formatOfficialDuration(record) }}</template>
-                </a-table-column>
-                <a-table-column title="乐跑时间" :width="160">
-                  <template #cell="{ record }">{{ formatUnixTime(record.start_time) }}</template>
-                </a-table-column>
+            </a-table-column>
+            <a-table-column title="乐跑跑区" :width="160">
+              <template #cell="{ record }">
+                <a-tag v-if="recordDisplay(record).routeRunZoneName" size="small">
+                  {{ recordDisplay(record).routeRunZoneName }}
+                </a-tag>
+                <span v-else>—</span>
               </template>
-            </a-table>
-          </a-tab-pane>
-        </a-tabs>
+            </a-table-column>
+            <a-table-column title="预计距离" :width="100">
+              <template #cell="{ record }">
+                <span v-if="recordDisplay(record).expectedKm != null">
+                  {{ formatKm(recordDisplay(record).expectedKm) }}
+                </span>
+                <span v-else>—</span>
+              </template>
+            </a-table-column>
+            <a-table-column title="实际距离" :width="100">
+              <template #cell="{ record }">
+                {{ formatKm(recordDisplay(record).distance) }}
+              </template>
+            </a-table-column>
+            <a-table-column title="扣费里程" :width="100">
+              <template #cell="{ record }">
+                <span v-if="recordDisplay(record).officialKm != null && Number.isFinite(Number(recordDisplay(record).officialKm))">
+                  {{ formatKm(recordDisplay(record).officialKm) }}
+                </span>
+                <span v-else>—</span>
+              </template>
+            </a-table-column>
+            <a-table-column title="跑步时长" :width="100">
+              <template #cell="{ record }">
+                {{ formatSecondsToMinSec(recordDisplay(record).timeSec) }}
+              </template>
+            </a-table-column>
+            <a-table-column title="平均配速" :width="90">
+              <template #cell="{ record }">
+                {{ formatPace(recordDisplay(record).timeSec, recordDisplay(record).distance) }}
+              </template>
+            </a-table-column>
+            <a-table-column title="乐跑时间" :width="160">
+              <template #cell="{ record }">{{ formatTime(record.time) }}</template>
+            </a-table-column>
+            <a-table-column title="" :width="90" :fixed="tableFixed('right')">
+              <template #cell="{ record }">
+                <a-button type="text" size="small" @click="openRecordDetail(record)">详情</a-button>
+              </template>
+            </a-table-column>
+          </template>
+        </a-table>
       </template>
     </a-spin>
   </a-modal>
@@ -138,15 +125,18 @@
 <script setup>
 import { ref, computed } from 'vue'
 import { useRouter } from 'vue-router'
-import {
-  lepaoRecords,
-  adminLepaoRecords,
-  officialTermRecords,
-  adminOfficialTermRecords
-} from '@/api/lepao'
+import { lepaoRecords, adminLepaoRecords } from '@/api/lepao'
 import { Notification, Message } from '@arco-design/web-vue'
 import useChartOption from '@/hooks/chart-option'
 import { useResponsiveTable } from '@/hooks/useResponsiveTable'
+import {
+  parseLepaoRecordRow,
+  formatKm,
+  formatPace,
+  formatSecondsToMinSec,
+  formatAutoDistanceRangeLabel,
+  formatAutoPaceRangeLabel
+} from '@/utils/lepaoRecord'
 
 const props = defineProps({
   admin: {
@@ -179,11 +169,6 @@ const visible = ref(false)
 const loading = ref(false)
 const account = ref(null)
 const records = ref([])
-const officialRecords = ref([])
-const officialLoading = ref(false)
-const officialSummary = ref({})
-const officialPagination = ref({ current: 1, pageSize: 10, total: 0 })
-const recordTab = ref('platform')
 const calendarYear = ref(new Date().getFullYear())
 const { isMobile, tableFixed } = useResponsiveTable()
 const router = useRouter()
@@ -219,7 +204,31 @@ const noticeLabel = computed(() => {
 const autoRunLabel = computed(() => {
   const item = account.value
   if (!item?.auto_run) return '关闭'
-  return item.target_count === 0 ? '开启 · 不限次' : `开启 · ${item.target_count} 次`
+  return item.target_count === 0 ? '开启 · 不限里程' : `开启 · 目标 ${formatKm(item.target_count)}`
+})
+
+const targetKmLabel = computed(() => {
+  const item = account.value
+  if (!item) return '-'
+  return item.target_count === 0 ? '不限' : formatKm(item.target_count)
+})
+
+const autoDistanceLabel = computed(() => {
+  const item = account.value
+  if (!item?.auto_run) return '—'
+  return formatAutoDistanceRangeLabel(
+    item.auto_run_distance_min_km ?? 2,
+    item.auto_run_distance_max_km ?? 2
+  )
+})
+
+const autoPaceLabel = computed(() => {
+  const item = account.value
+  if (!item?.auto_run) return '—'
+  return formatAutoPaceRangeLabel(
+    item.pace_min_sec_per_km ?? 180,
+    item.pace_max_sec_per_km ?? 600
+  )
 })
 
 const autoDayLabel = computed(() => {
@@ -246,40 +255,7 @@ const autoTimeLabel = computed(() => {
   return auto_time_options.find((t) => t.value === item.auto_time)?.label || '-'
 })
 
-const termProgressLabel = computed(() => {
-  const item = account.value
-  if (!item) return '-'
-  if (item.term_num != null && item.total_num != null && item.term_num !== item.total_num) {
-    return `${item.total_num} / ${item.term_num}`
-  }
-  return '已完成'
-})
-
-const faceStateLabel = computed(() => {
-  const faceState = account.value?.face_state
-  if (faceState === 0) return '未采集'
-  if (faceState === 1) return '已通过'
-  if (faceState != null) return '不通过'
-  return '-'
-})
-
-const canViewOfficialRecords = computed(() => account.value?.state === 1)
-
-const activeRecordCountLabel = computed(() => {
-  if (recordTab.value === 'official') {
-    const total = officialPagination.value.total || officialRecords.value.length
-    return `官方记录共 ${total} 条,有效次数 ${officialSummary.value?.total_score_num ?? 0} 次(${officialSummary.value?.term_name ?? '本学期'})`
-  }
-  return `平台记录共 ${records.value.length} 条`
-})
-
-const officialPaginationConfig = computed(() => ({
-  current: officialPagination.value.current,
-  pageSize: officialPagination.value.pageSize,
-  total: officialPagination.value.total || officialRecords.value.length,
-  showTotal: true,
-  showPageSize: false
-}))
+const recordCountLabel = computed(() => `共 ${records.value.length} 条`)
 
 const yearOptions = computed(() => {
   const years = new Set([calendarYear.value, new Date().getFullYear()])
@@ -291,6 +267,16 @@ const yearOptions = computed(() => {
     .map((year) => ({ label: `${year} 年`, value: year }))
 })
 
+const recordDisplay = (record) =>
+  parseLepaoRecordRow({
+    result: record?.result,
+    jkes_record: record?.jkes_record,
+    state: record?.state,
+    run_zone_name: record?.run_zone_name,
+    path_run_zone_name: record?.path_run_zone_name,
+    path_distance: record?.path_distance
+  })
+
 const formatDateKey = (timestamp) => {
   const d = new Date(timestamp)
   const y = d.getFullYear()
@@ -365,40 +351,6 @@ const formatTime = (time) => {
   })
 }
 
-const formatUnixTime = (time) => {
-  const timestamp = Number(time)
-  if (!Number.isFinite(timestamp) || timestamp <= 0) return '-'
-  return formatTime(timestamp * 1000)
-}
-
-function calculatePace(seconds, kilometers) {
-  if (!seconds || !kilometers) return '-'
-  const paceInSeconds = seconds / kilometers
-  const minutes = Math.floor(paceInSeconds / 60)
-  const remainingSeconds = Math.round(paceInSeconds % 60)
-  return `${minutes}'${remainingSeconds.toString().padStart(2, '0')}''`
-}
-
-function formatSecondsToMinSec(totalSeconds) {
-  if (!totalSeconds && totalSeconds !== 0) return '-'
-  const minutes = Math.floor(totalSeconds / 60)
-  const seconds = totalSeconds % 60
-  return `${minutes}分${seconds.toString().padStart(2, '0')}秒`
-}
-
-const formatOfficialDuration = (record) => {
-  const start = Number(record?.start_time)
-  const end = Number(record?.end_time)
-  if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return '-'
-  return formatSecondsToMinSec(end - start)
-}
-
-const officialRecordStatus = (record) => {
-  const status = String(record?.record_status ?? '')
-  if (status === '1') return '有效'
-  return '无效'
-}
-
 const fetchAllRecords = async (studentNum) => {
   const all = []
   let current = 1
@@ -424,63 +376,8 @@ const fetchAllRecords = async (studentNum) => {
   return all.sort((a, b) => b.time - a.time)
 }
 
-const fetchOfficialRecords = async (page = 1) => {
-  if (!account.value?.student_num) return
-  if (!canViewOfficialRecords.value) {
-    Message.warning('请使用登录器更新账号后查看官方乐跑记录')
-    return
-  }
-
-  officialLoading.value = true
-  try {
-    const fetchRecords = props.admin ? adminOfficialTermRecords : officialTermRecords
-    const res = await fetchRecords({
-      student_num: account.value.student_num,
-      page
-    })
-    if (!res || res.code !== 0) {
-      if (account.value && String(res?.msg || '').includes('登录')) {
-        account.value.state = 0
-      }
-      throw new Error(res?.msg || '获取乐跑记录失败')
-    }
-
-    const data = res.data || {}
-    officialSummary.value = data
-    officialRecords.value = data.list || []
-    officialPagination.value = {
-      ...officialPagination.value,
-      current: data.page || page,
-      total: Number(data.total_num || officialRecords.value.length)
-    }
-  } catch (error) {
-    Notification.error({
-      title: '获取官方乐跑记录失败',
-      content: error.message || '请稍后再试'
-    })
-  } finally {
-    officialLoading.value = false
-  }
-}
-
-const handleRecordTabChange = (key) => {
-  if (key !== 'official') return
-  if (!canViewOfficialRecords.value) {
-    recordTab.value = 'platform'
-    Message.warning('请使用登录器更新账号后查看官方记录')
-    return
-  }
-  if (officialRecords.value.length === 0) {
-    fetchOfficialRecords(1)
-  }
-}
-
-const handleOfficialPageChange = (page) => {
-  fetchOfficialRecords(page)
-}
-
-const openPlatformRecordDetail = (record) => {
-  const key = record.public_id || record.id
+const openRecordDetail = (record) => {
+  const key = record.public_id
   if (!key) {
     Message.warning('该记录缺少可用标识,无法打开详情')
     return
@@ -498,10 +395,6 @@ const openModal = async (record) => {
   visible.value = true
   loading.value = true
   records.value = []
-  officialRecords.value = []
-  officialSummary.value = {}
-  officialPagination.value = { current: 1, pageSize: 10, total: 0 }
-  recordTab.value = 'platform'
 
   try {
     const list = await fetchAllRecords(record.student_num)
@@ -576,10 +469,6 @@ defineExpose({ openModal })
   color: var(--color-text-3);
 }
 
-.official-record-alert {
-  margin-bottom: 12px;
-}
-
 .state {
   display: flex;
   align-items: center;
@@ -587,16 +476,26 @@ defineExpose({ openModal })
   .circle {
     border-radius: 50%;
     height: 8px;
+    min-height: 8px;
     width: 8px;
+    min-width: 8px;
     margin-right: 6px;
     flex-shrink: 0;
   }
 
-  .one {
+  .running {
+    background-color: rgb(var(--orange-6));
+  }
+
+  .success {
     background-color: rgb(var(--green-6));
   }
 
-  .else {
+  .sync {
+    background-color: rgb(var(--arcoblue-6));
+  }
+
+  .fail {
     background-color: rgb(var(--red-6));
   }
 }

+ 2 - 2
src/components/Navbar/index.vue

@@ -2,9 +2,9 @@
   <div class="navbar">
     <div class="left-side">
       <a-space style="cursor: pointer; -webkit-app-region: no-drag;" @click="$router.push('/')">
-        <img alt="哪吒乐跑" src="/logo.svg" height="35">
+        <img alt="沐晨乐跑" src="/logo.svg" height="35">
         <a-typography-title class="brand-title" :style="{ margin: 0, fontSize: '18px' }" :heading="5">
-          哪吒乐跑
+          沐晨乐跑
         </a-typography-title>
       </a-space>
     </div>

+ 57 - 9
src/components/userCard/userCard.vue

@@ -2,13 +2,16 @@
   <div class="user-card" :class="{ 'user-card--goods': props.type === 'goods' }">
     <div class="user-card__main">
       <div class="user-card__stat">
-        <span class="user-card__label">剩余乐跑次数</span>
+        <span class="user-card__label">剩余乐跑里程</span>
         <a-statistic
-          :value="userCount?.lepao_count ?? 0"
+          :value="lepaoCountValue"
+          :precision="2"
           :value-style="{ fontSize: '2rem', fontWeight: 700, color: 'var(--stat-color)' }"
           animation
           show-group-separator
-        />
+        >
+        <template #suffix>Km</template>
+        </a-statistic>
       </div>
       <div class="user-card__actions">
         <a-button
@@ -46,7 +49,7 @@
 
   <a-modal
     v-model:visible="visible"
-    title="赠送乐跑次数"
+    title="赠送乐跑里程"
     @cancel="handleCancel"
     @before-ok="handleBeforeOk"
     :ok-loading="ok_loading"
@@ -61,8 +64,8 @@
           allow-clear
         />
       </a-form-item>
-      <a-form-item field="count" label="赠送次数">
-        <a-input-number v-model="sendform.count" :min="1" :max="9999" mode="button" />
+      <a-form-item field="count" label="赠送里程(Km)">
+        <a-input-number v-model="sendform.count" :min="0.01" :max="9999" :precision="2" :step="0.1" mode="button" />
       </a-form-item>
     </a-form>
     <div v-if="hasPermission('action.goods.sendCount')" class="send-modal-extra">
@@ -74,7 +77,7 @@
 </template>
 
 <script setup>
-import { ref, reactive, onUnmounted, onMounted } from 'vue'
+import { ref, reactive, computed, onUnmounted, onMounted } from 'vue'
 import { useRouter } from 'vue-router'
 import { getCount, sendCount } from '@/api/goods'
 import { Notification, Message } from '@arco-design/web-vue'
@@ -85,6 +88,10 @@ const props = defineProps({
 })
 
 const userCount = ref({ lepao_count: 0 })
+const lepaoCountValue = computed(() => {
+  const n = Number(userCount.value?.lepao_count)
+  return Number.isFinite(n) ? n : 0
+})
 const router = useRouter()
 const visible = ref(false)
 const ok_loading = ref(false)
@@ -101,8 +108,8 @@ const handleBeforeOk = async (done) => {
       Message.error('请填写接收人用户名')
       return false
     }
-    if (!sendform.count || sendform.count < 1 || sendform.count > 9999) {
-      Notification.error({ title: '赠送失败', content: '赠送次数需在 1~9999 之间' })
+    if (!sendform.count || sendform.count < 0.01 || sendform.count > 9999) {
+      Notification.error({ title: '赠送失败', content: '赠送里程需在 0.01~9999 公里之间' })
       return false
     }
     const res = await sendCount({ username: sendform.username, count: sendform.count })
@@ -201,11 +208,52 @@ onUnmounted(stopPolling)
     display: flex;
     flex-wrap: wrap;
     gap: 10px;
+    align-items: center;
+  }
+
+  @media (max-width: @app-breakpoint-md) {
+    padding: 16px;
+
+    &__main {
+      flex-direction: column;
+      align-items: stretch;
+      gap: 16px;
+    }
+
+    &__stat {
+      text-align: center;
+    }
+
+    &__actions {
+      width: 100%;
+      display: grid;
+      grid-template-columns: repeat(2, minmax(0, 1fr));
+      gap: 10px;
+    }
+
+    &__actions .action-btn {
+      width: 100%;
+      margin: 0 !important;
+      justify-content: center;
+    }
+
+    &__actions .action-btn--primary {
+      grid-column: 1 / -1;
+    }
+  }
+
+  @media (max-width: @app-breakpoint-sm) {
+    &__actions {
+      grid-template-columns: 1fr;
+    }
   }
 }
 
 .action-btn {
   border-radius: 999px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
 
   &--primary {
     background: @store-primary !important;

+ 3 - 1
src/main.js

@@ -8,7 +8,9 @@ if (isElectron()) {
 }
 
 import ArcoVue from '@arco-design/web-vue'
-import '@arco-themes/vue-mengtu/index.less'
+// import '@arco-themes/vue-mengtu/index.less'
+import '@arco-themes/vue-hisense-pangea/index.less'
+
 import '@/styles/store-theme.less'
 
 import ArcoVueIcon from '@arco-design/web-vue/es/icon'

+ 3 - 3
src/pages/Login/Login.vue

@@ -2,8 +2,8 @@
   <div class="auth-page">
     <header class="auth-topbar">
       <router-link to="/" class="auth-topbar__brand">
-        <img src="/logo.svg" height="32" width="32" alt="哪吒乐跑" />
-        <span>哪吒乐跑</span>
+        <img src="/logo.svg" height="32" width="32" alt="沐晨乐跑" />
+        <span>沐晨乐跑</span>
       </router-link>
       <router-link v-if="!isElectron()" to="/" class="auth-topbar__link">返回首页</router-link>
     </header>
@@ -14,7 +14,7 @@
       <Container />
     </main>
 
-    <p class="auth-page__legal">© {{ year }} 哪吒乐跑 · 智能校园乐跑平台</p>
+    <p class="auth-page__legal">© {{ year }} 沐晨乐跑 · 智能校园乐跑平台</p>
   </div>
 </template>
 

+ 2 - 2
src/pages/Login/components/login.vue

@@ -1,8 +1,8 @@
 <template>
     <div class="auth-form-panel">
         <div class="logo">
-            <img alt="哪吒乐跑" src="/logo.svg" height="40">
-            <span class="title">哪吒乐跑 | 用户登录</span>
+            <img alt="沐晨乐跑" src="/logo.svg" height="40">
+            <span class="title">沐晨乐跑 | 用户登录</span>
         </div>
 
         <a-form size="large" :model="form" :rules="[]" class="form" @submit="handleSubmit">

+ 2 - 2
src/pages/Login/components/register.vue

@@ -1,8 +1,8 @@
 <template>
     <div class="auth-form-panel">
         <div class="logo">
-            <img alt="哪吒乐跑" src="/logo.svg" height="40">
-            <span class="title">哪吒乐跑 | 用户注册</span>
+            <img alt="沐晨乐跑" src="/logo.svg" height="40">
+            <span class="title">沐晨乐跑 | 用户注册</span>
         </div>
 
         <a-form size="large" ref="formRef" :model="form" :rules="[]" class="form" @submit="handleSubmit">

+ 2 - 2
src/pages/Login/uniLogin/uniLogin.vue

@@ -1,8 +1,8 @@
 <template>
     <div class="auth-form-panel uni-login-panel">
         <div class="logo">
-            <img alt="哪吒乐跑" src="/logo.svg" height="40">
-            <span class="title">哪吒乐跑 | 快捷登录</span>
+            <img alt="沐晨乐跑" src="/logo.svg" height="40">
+            <span class="title">沐晨乐跑 | 快捷登录</span>
         </div>
 
         <a-form size="large" :model="form" :rules="[]" class="form" @submit="handleSubmit">

+ 7 - 7
src/pages/Main/Main.vue

@@ -20,26 +20,26 @@
             </div>
         </div>
 
-        <a-button
+        <!-- <a-button
             shape="circle"
             class="floating-button"
             size="large"
             @click="scrollToSection('section2')"
         >
             <IconDown />
-        </a-button>
+        </a-button> -->
     </div>
 
-    <div id="section2" class="section features-wrap">
+    <!-- <div id="section2" class="section features-wrap">
         <div class="features-bg" aria-hidden="true">
             <div class="features-bg-gradient" />
             <div class="features-bg-orb" />
         </div>
         <Section2 />
-    </div>
-    <footer class="site-footer">
-        <span>© {{ new Date().getFullYear() }} 哪吒乐跑 - 智能校园乐跑平台</span>
-    </footer>
+    </div> -->
+    <!-- <footer class="site-footer">
+        <span>© {{ new Date().getFullYear() }} 沐晨乐跑 - 智能校园乐跑平台</span>
+    </footer> -->
 </template>
 
 <script setup>

+ 1 - 1
src/pages/Main/components/center.vue

@@ -2,7 +2,7 @@
     <div class="center">
         <div class="hero-copy">
             <p class="eyebrow">智能校园乐跑平台</p>
-            <h1>哪吒乐跑</h1>
+            <h1>沐晨乐跑</h1>
             <h2>轻松完成乐跑任务,成绩更省心</h2>
         </div>
         <div class="action-row">

+ 1 - 1
src/pages/User/setting/components/social-bindings.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="social-bindings">
     <div class="section-title">第三方账号</div>
-    <div class="description">绑定 QQ 和微信后,无论使用哪种方式登录,都会进入当前 哪吒乐跑 账户。</div>
+    <div class="description">绑定 QQ 和微信后,无论使用哪种方式登录,都会进入当前 沐晨乐跑 账户。</div>
 
     <div class="account-list">
       <div v-for="item in accounts" :key="item.type" class="account-card">

+ 2 - 2
src/pages/admin/goods/addGoods.vue

@@ -35,7 +35,7 @@
             </a-form-item>
           </a-col>
           <a-col :xs="24" :sm="8">
-            <a-form-item field="lepao_count" label="乐跑次数">
+            <a-form-item field="lepao_count" label="乐跑里程(Km)">
               <a-input-number v-model="form.lepao_count" :step="1" :precision="0" :min="0" style="width: 100%" />
             </a-form-item>
           </a-col>
@@ -155,7 +155,7 @@ const displayEmoji = computed(() => getGoodsEmoji(form.icon))
 const rules = {
   name: [{ required: true, message: '请输入商品名称' }],
   price: [{ required: true, message: '请填写商品价格' }],
-  lepao_count: [{ required: true, message: '请填写乐跑次数' }],
+  lepao_count: [{ required: true, message: '请填写乐跑里程' }],
   num: [{ required: true, message: '请填写商品库存' }],
   state: [{ required: true, message: '请选择商品状态' }]
 }

+ 1 - 1
src/pages/admin/goods/goodsList.vue

@@ -101,7 +101,7 @@ const columnDefs = [
     { title: '商品名称', dataIndex: 'name', ellipsis: true, tooltip: true },
   { title: '价格', slotName: 'price', width: 100 },
   { title: '库存', dataIndex: 'num', width: 80 },
-  { title: '乐跑次数', dataIndex: 'lepao_count', width: 100 },
+  { title: '乐跑里程(Km)', dataIndex: 'lepao_count', width: 110 },
   { title: '浏览量', dataIndex: 'views', width: 90 },
   { title: '状态', slotName: 'state', width: 90 },
   { title: '创建时间', slotName: 'create_time', width: 170 },

+ 6 - 6
src/pages/admin/goods/sendCountRequestList.vue

@@ -4,9 +4,9 @@
 
     <a-card title="赠送审核列表">
       <AppQueryFilter>
-      <a-row class="query-row app-query-form">
-        <a-col :flex="1">
-          <a-form :model="queryData" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }" label-align="left">
+      <a-row class="query-row">
+        <a-col :flex="'1000px'" class="query-main">
+          <a-form class="queryForm app-query-form" :model="queryData" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }" label-align="left">
             <a-row :gutter="16">
               <a-col :span="8">
                 <a-form-item field="status" label="审核状态">
@@ -16,9 +16,9 @@
             </a-row>
           </a-form>
         </a-col>
-        <a-divider style="height: 84px" direction="vertical" />
-        <a-col :flex="'86px'" style="text-align: right">
-          <a-space direction="vertical" :size="18">
+        <a-divider class="query-divider" style="height: 42px" direction="vertical" />
+        <a-col :flex="1" class="app-query-actions">
+          <a-space class="query-actions-space" direction="vertical" :size="18">
             <a-button type="primary" @click="search">
               <template #icon>
                 <icon-search />

+ 123 - 240
src/pages/admin/lepaoAccount/accountList.vue

@@ -44,16 +44,6 @@
                                     <a-input v-model="queryData.student_num" allow-clear />
                                 </a-form-item>
                             </a-col>
-                            <a-col :span="6">
-                                <a-form-item field="bind_code" label="绑定码">
-                                    <a-input v-model="queryData.bind_code" allow-clear />
-                                </a-form-item>
-                            </a-col>
-                            <a-col :span="6">
-                                <a-form-item field="bot_account" label="机器人账号">
-                                    <a-input v-model="queryData.bot_account" allow-clear />
-                                </a-form-item>
-                            </a-col>
                             <a-col :span="6">
                                 <a-form-item field="state" label="账号状态">
                                     <a-select v-model="queryData.state" :options="state" placeholder="请选择账号状态"
@@ -127,8 +117,7 @@
                     {{ record.sex === 1 ? '男' : (record.sex === 2 ? '女' : '') }}
                 </template>
                 <template #auto_run="{ record }">
-                    <a-tag color="green" v-if="record.auto_run">{{ record.target_count === 0 ? '开启-∞次' :
-                        `开启-${record.target_count}次` }}</a-tag>
+                    <a-tag color="green" v-if="record.auto_run">开启</a-tag>
                     <a-tag color="red" v-else>关闭</a-tag>
                 </template>
                 <template #notice_type="{ record }">
@@ -145,9 +134,15 @@
                     </span>
                     <span v-else>-</span>
                 </template>
-                <template #num="{ record }">
-                    {{ record.term_num != record.total_num ? `${record.total_num} / ${record.term_num}` :
-                        '已完成' }}
+                <template #target_km="{ record }">
+                    {{ record.target_count === 0 ? '不限' : formatKm(record.target_count) }}
+                </template>
+                <template #distance_range="{ record }">
+                    {{ record.auto_run ? formatAutoDistanceRangeLabel(record.auto_run_distance_min_km ?? 2, record.auto_run_distance_max_km ?? 2) : '—' }}
+                </template>
+                <template #pace_range="{ record }">
+                    <span v-if="record.auto_run">{{ formatAutoPaceRangeLabel(record.pace_min_sec_per_km ?? 180, record.pace_max_sec_per_km ?? 600) }}</span>
+                    <span v-else>—</span>
                 </template>
                 <template #state="{ record }">
                     <div v-if="record.state === 0" class="state">
@@ -160,17 +155,6 @@
                         <div class="circle else"></div>状态异常
                     </div>
                 </template>
-                <template #face_state="{ record }">
-                    <div v-if="record.face_state === 0" class="state">
-                        <div class="circle zero"></div>未采集
-                    </div>
-                    <div v-else-if="record.face_state === 1" class="state">
-                        <div class="circle one"></div>已通过
-                    </div>
-                    <div v-else class="state">
-                        <div class="circle else"></div>不通过
-                    </div>
-                </template>
                 <template #auto_time="{ record }">
                     {{ autoTimeLabel(record) }}
                 </template>
@@ -185,13 +169,9 @@
                         <a-button>操作 <icon-down /></a-button>
                         <template #content>
                             <a-doption v-if="hasPermission('action.lepao.admin.updateAccount')" @click="editAccount(record)"><icon-edit /> 编辑账号</a-doption>
-                            <a-doption @click="faceRecoRef.openModal(record)"><icon-video-camera /> 人脸采集</a-doption>
-                            <a-doption @click="bindBotRef.openModal(record)"><icon-robot-add /> 绑定智能机器人</a-doption>
-                            <a-doption @click="openBindAudit(record)"><icon-history /> 绑定解绑记录</a-doption>
                             <a-doption v-if="hasPermission('action.lepao.singleRun')" @click="SingleRun(record)"><icon-play-circle /> 开始单次乐跑</a-doption>
                             <a-doption v-if="hasPermission('action.lepao.changeAutoRun')" @click="ChangeAutoRun(record)"><icon-translate /> {{ record.auto_run ? '关闭' :
                                 '开启' }}自动乐跑</a-doption>
-                            <a-doption v-if="hasPermission('action.lepao.admin.updateAccount')" @click="UpdateAccountInfo(record)"><icon-refresh /> 更新账号信息</a-doption>
                             <a-doption v-if="hasPermission('action.lepao.deleteAccount')" @click="DeleteAccount(record)"><icon-minus-circle /> 解绑账号</a-doption>
                         </template>
                     </a-dropdown>
@@ -210,7 +190,6 @@
             <a-form-item field="notice_type" label="通知方式">
                 <a-radio-group v-model="form.notice_type">
                     <a-radio value="email"><icon-email /> 邮件</a-radio>
-                    <a-radio value="bot"><icon-robot /> 智能机器人</a-radio>
                     <a-radio value="none"><icon-notification-close /> 无通知</a-radio>
                 </a-radio-group>
             </a-form-item>
@@ -230,10 +209,11 @@
             <a-form-item field="auto_run" label="自动乐跑开关">
                 <a-switch v-model="form.auto_run" :checked-value="1" :unchecked-value="0" />
             </a-form-item>
-            <a-form-item field="target_count" label="乐跑目标次数" v-if="form.auto_run">
-                <a-input-number v-model="form.target_count" placeholder="请输入乐跑目标次数" mode="button" />
+            <a-form-item field="target_count" label="月度目标里程" v-if="form.auto_run">
+                <a-input-number v-model="form.target_count" placeholder="0–200,0 表示不限制" mode="button" :min="0" :max="200"
+                    :precision="2" :step="0.5" />
                 <template #extra>
-                    <div>当学期有效次数达到目标次数时自动乐跑将关闭,0为不限次</div>
+                    <div>月度里程(Km)达到目标后自动乐跑将关闭;填 0 表示不限制</div>
                 </template>
             </a-form-item>
             <a-form-item field="auto_day" label="自动乐跑星期" v-if="form.auto_run">
@@ -242,6 +222,28 @@
             <a-form-item field="auto_time" label="自动乐跑时段" v-if="form.auto_run">
                 <a-select v-model="form.auto_time" placeholder="请选择每天自动乐跑的时段" :options="auto_time" />
             </a-form-item>
+            <a-form-item field="auto_run_distance_range" label="自动距离区间" v-if="form.auto_run">
+                <a-space>
+                    <a-input-number v-model="form.auto_run_distance_min_km" mode="button" :min="1" :max="AUTO_SINGLE_RUN_MAX_KM"
+                        :precision="2" :step="0.1" placeholder="下限" />
+                    <span>~</span>
+                    <a-input-number v-model="form.auto_run_distance_max_km" mode="button" :min="1" :max="AUTO_SINGLE_RUN_MAX_KM"
+                        :precision="2" :step="0.1" placeholder="上限" />
+                </a-space>
+                <template #extra>
+                    <div>每次自动乐跑在区间内随机取值;范围 1~10 km</div>
+                </template>
+            </a-form-item>
+            <a-form-item field="pace_range" label="自动配速区间" v-if="form.auto_run">
+                <a-space>
+                    <a-input v-model="form.pace_min_str" allow-clear placeholder="如 6:00" style="width: 7.5rem" />
+                    <span>–</span>
+                    <a-input v-model="form.pace_max_str" allow-clear placeholder="如 10:00" style="width: 7.5rem" />
+                </a-space>
+                <template #extra>
+                    <div>每公里配速,格式为 分:秒;范围 3:00–10:00/km</div>
+                </template>
+            </a-form-item>
             <a-form-item field="notes" label="备注">
                 <a-textarea v-model="form.notes" no-trim placeholder="添加对账号的备注(非必填)" :max-length="{ length: 50 }" allow-clear
                     show-word-limit />
@@ -249,70 +251,39 @@
         </a-form>
     </a-modal>
 
-    <faceModal :faceInfo="faceInfo" ref="faceRecoRef" />
     <bindBot ref="bindBotRef" />
     <accountDetailCard ref="accountDetailRef" admin />
-
-    <a-modal v-model:visible="bindAuditVisible" title="绑定解绑记录" :footer="false" width="980px" draggable>
-        <a-table :bordered="false" :data="bindAuditData" :columns="bindAuditColumns" :loading="bindAuditLoading" :pagination="false"
-            :scroll="{ x: 1450, y: 420 }">
-            <template #lepao_user="{ record }">
-                <a-space>
-                    <a-avatar :size="26">
-                        <img :src="record.lepao_avatar || 'https://lepao-cloud.xxoo365.top/view.php/25aa126dc406974ff3579a99a2c6501a.png'" />
-                    </a-avatar>
-                    {{ record.lepao_name || '-' }}
-                </a-space>
-            </template>
-            <template #owner_user="{ record }">
-                <a-space>
-                    <a-avatar :size="26">
-                        <img :src="record.owner_avatar || 'https://lepao-cloud.xxoo365.top/view.php/25aa126dc406974ff3579a99a2c6501a.png'" />
-                    </a-avatar>
-                    {{ record.owner_username || '-' }}
-                </a-space>
-            </template>
-            <template #action="{ record }">
-                <a-tag>{{ actionLabel(record.action) }}</a-tag>
-            </template>
-            <template #source="{ record }">
-                {{ sourceLabel(record.source) }}
-            </template>
-            <template #operator_user="{ record }">
-                <a-space>
-                    <a-avatar :size="26">
-                        <img :src="record.operator_avatar || 'https://lepao-cloud.xxoo365.top/view.php/25aa126dc406974ff3579a99a2c6501a.png'" />
-                    </a-avatar>
-                    {{ record.operator_username || '-' }}
-                </a-space>
-            </template>
-            <template #detail_json="{ record }">
-                {{ formatDetail(record.detail_json) }}
-            </template>
-            <template #created_at="{ record }">
-                {{ stramptoTime(record.created_at) }}
-            </template>
-            <template #empty>
-                <a-empty description="暂无绑定解绑记录" />
-            </template>
-        </a-table>
-    </a-modal>
+    <SingleRunModal
+        v-model:visible="singleRunVisible"
+        :account="singleRunAccount"
+        @success="getAccounts"
+    />
 </template>
 
 <script setup>
 import { ref, reactive, onMounted, onUnmounted, h } from 'vue'
-import { addAccount, adminAccountList, deleteAccount, changeAutoRun, singleRun, adminUpdateAccountInfo, getAdminBindAuditByAccount, getAdminBindAuditList } from '@/api/lepao'
+import { addAccount, adminAccountList, deleteAccount, changeAutoRun } from '@/api/lepao'
 import { Modal, Notification, Message } from '@arco-design/web-vue'
-import faceModal from '@/components/FaceModal/faceModal.vue'
 import bindBot from '@/components/BindBot/bindBot.vue'
 import accountDetailCard from '@/components/LepaoAccountCard/accountDetailCard.vue'
+import SingleRunModal from '@/components/SingleRunModal/SingleRunModal.vue'
 import { getSemesterTimestamps } from '@/utils/util'
 import { hasPermission } from '@/utils/permission'
 import { useResponsiveTable } from '@/hooks/useResponsiveTable'
+import {
+    formatKm,
+    formatAutoPaceRangeLabel,
+    formatAutoDistanceRangeLabel,
+    formatSecPerKmAsMinSec,
+    parsePaceMinSecInput
+} from '@/utils/lepaoRecord'
+
+const AUTO_SINGLE_RUN_MAX_KM = 10
 
-const faceRecoRef = ref(null)
 const bindBotRef = ref(null)
 const accountDetailRef = ref(null)
+const singleRunVisible = ref(false)
+const singleRunAccount = ref(null)
 
 const ROW_DETAIL_CONFIRM_MS = 3000
 const pendingDetailAccountId = ref(null)
@@ -326,10 +297,6 @@ const clearPendingDetail = () => {
     }
 }
 
-const bindAuditVisible = ref(false)
-const bindAuditLoading = ref(false)
-const bindAuditData = ref([])
-
 const queryData = reactive({
     area: '',
     student_num: '',
@@ -337,7 +304,6 @@ const queryData = reactive({
     email: '',
     username: '',
     notes: '',
-    bind_code: '',
     bot_account: '',
     state: -1,
     auto_time: 0,
@@ -361,6 +327,10 @@ const form = reactive({
     auto_run: 1,
     auto_time: -1,
     target_count: 30,
+    auto_run_distance_min_km: 2.5,
+    auto_run_distance_max_km: 3,
+    pace_min_str: '6:00',
+    pace_max_str: '10:00',
     auto_day: [0, 1, 2, 3, 4, 5, 6],
     notes: ''
 })
@@ -381,7 +351,7 @@ const auto_day = [
 const state = [
     { label: '全部', value: -1 }, { label: '需登录', value: 0 }, { label: '正常', value: 1 }, { label: '状态异常', value: 2 }
 ]
-const area = ["兰花湖校区跑区", "主校区北跑区", "主校区南跑区", "重庆工商大学茶园校区"]
+const area = ["学府大道校区", "南山校区"]
 
 const columnDefs = [
     {
@@ -438,9 +408,17 @@ const columnDefs = [
         slotName: 'auto_time',
         width: 130
     }, {
-        title: '学期目标',
-        slotName: 'num',
-        width: 90
+        title: '月度目标',
+        slotName: 'target_km',
+        width: 100
+    }, {
+        title: '自动距离',
+        slotName: 'distance_range',
+        width: 120
+    }, {
+        title: '自动配速',
+        slotName: 'pace_range',
+        width: 130
     }, {
         title: '手机型号',
         dataIndex: 'deviceModel',
@@ -451,10 +429,6 @@ const columnDefs = [
         width: 120,
         ellipsis: true,
         tooltip: true
-    }, {
-        title: '人脸状态',
-        slotName: 'face_state',
-        width: 100
     }, {
         title: '添加时间',
         slotName: 'create_time',
@@ -477,17 +451,6 @@ const columnDefs = [
 const { useTableColumns } = useResponsiveTable()
 const columns = useTableColumns(columnDefs)
 
-const bindAuditColumns = [
-    { title: '学号', dataIndex: 'student_num', width: 140 },
-    { title: '乐跑账号', slotName: 'lepao_user', width: 180 },
-    { title: '所属用户', slotName: 'owner_user', width: 180 },
-    { title: '动作', slotName: 'action', width: 120 },
-    { title: '来源', slotName: 'source', width: 140 },
-    { title: '操作者', slotName: 'operator_user', width: 180 },
-    { title: '详情', slotName: 'detail_json', width: 220, ellipsis: true, tooltip: true },
-    { title: '时间', slotName: 'created_at', width: 180 }
-]
-
 const search = () => {
     pagination.current = 1
     getAccounts()
@@ -500,7 +463,6 @@ const reset = () => {
     queryData.email = ''
     queryData.username = ''
     queryData.notes = ''
-    queryData.bind_code = ''
     queryData.bot_account = ''
     queryData.state = -1
     queryData.auto_time = 0
@@ -563,7 +525,13 @@ const editAccount = (item) => {
         form.auto_time = item.auto_time
         form.auto_day = item.auto_day
         form.notice_type = item.notice_type || 'email'
-        form.target_count = item.target_count
+        form.target_count = Number(item.target_count)
+        form.auto_run_distance_min_km = Number(item.auto_run_distance_min_km ?? 2)
+        form.auto_run_distance_max_km = Number(item.auto_run_distance_max_km ?? item.auto_run_distance_min_km ?? 2)
+        const pMin = formatSecPerKmAsMinSec(item.pace_min_sec_per_km ?? 180)
+        const pMax = formatSecPerKmAsMinSec(item.pace_max_sec_per_km ?? 600)
+        form.pace_min_str = pMin === '—' ? '6:00' : pMin
+        form.pace_max_str = pMax === '—' ? '10:00' : pMax
         form.notes = item.notes
     } else {
         form.id = null
@@ -571,6 +539,10 @@ const editAccount = (item) => {
         form.auto_run = 1
         form.auto_time = 7
         form.target_count = 30
+        form.auto_run_distance_min_km = 2.5
+        form.auto_run_distance_max_km = 3
+        form.pace_min_str = '6:00'
+        form.pace_max_str = '10:00'
         form.auto_day = [0, 1, 2, 3, 4, 5, 6]
         form.email = ''
         form.notes = ''
@@ -599,11 +571,47 @@ const handleBeforeOk = async (done) => {
             return false
         }
 
-        let data = {
-            ...form
+        let pLo = null
+        let pHi = null
+        if (form.auto_run) {
+            pLo = parsePaceMinSecInput(form.pace_min_str)
+            pHi = parsePaceMinSecInput(form.pace_max_str)
+            if (pLo == null || pHi == null) {
+                Message.error('配速格式不正确,请使用 分:秒,如 5:30 或 10:00')
+                return false
+            }
+            if (pLo < 180 || pLo > 600 || pHi < 180 || pHi > 600) {
+                Message.error('配速须在 3:00–10:00/km 范围内')
+                return false
+            }
+            if (pLo > pHi) {
+                Message.error('配速下限不能慢于上限')
+                return false
+            }
+            const tk = Number(form.target_count)
+            if (!Number.isFinite(tk) || tk < 0 || tk > 200) {
+                Message.error('目标里程须在 0–200 公里之间')
+                return false
+            }
+            const dMin = Number(form.auto_run_distance_min_km)
+            const dMax = Number(form.auto_run_distance_max_km)
+            if (!Number.isFinite(dMin) || !Number.isFinite(dMax) || dMin < 1 || dMin > AUTO_SINGLE_RUN_MAX_KM || dMax < 1 || dMax > AUTO_SINGLE_RUN_MAX_KM) {
+                Message.error(`自动乐跑距离区间须在 1–${AUTO_SINGLE_RUN_MAX_KM} 公里之间`)
+                return false
+            }
+            if (dMin > dMax) {
+                Message.error('自动乐跑距离下限不能大于上限')
+                return false
+            }
         }
 
-        const res = await addAccount(data)
+        const payload = { ...form }
+        if (form.auto_run) {
+            payload.pace_min_sec_per_km = pLo
+            payload.pace_max_sec_per_km = pHi
+        }
+
+        const res = await addAccount(payload)
         if (!res || res.code !== 0) {
             Notification.error({
                 title: '保存乐跑账号失败!',
@@ -687,139 +695,14 @@ const ChangeAutoRun = async (record) => {
     }
 }
 
-const SingleRun = async (item) => {
+const SingleRun = (item) => {
     if (item.state !== 1)
         return Notification.warning({
             title: '当前乐跑账号需登录,请登录后再试',
             content: '如有疑问请联系客服'
         })
-    Modal.confirm({
-        title: '开始乐跑',
-        content: () => h('div', [
-            h('p', `您是否要为${item.name}(${item.student_num})乐跑?若乐跑成功将扣减乐跑次数`)
-        ]),
-        onOk: async () => {
-            const res = await singleRun({ student_num: item.student_num })
-            if (!res || res.code !== 0)
-                return Notification.error({
-                    title: '提交乐跑任务失败',
-                    content: res?.msg ?? '请稍后再试'
-                })
-            Message.success('提交乐跑任务成功!')
-        }
-    })
-}
-
-const UpdateAccountInfo = async (record) => {
-    try {
-        const res = await adminUpdateAccountInfo({
-            student_num: record.student_num
-        })
-        if (!res || res.code !== 0) {
-            return Notification.error({
-                title: '更新账号信息失败!',
-                content: res?.msg ?? '请稍后再试'
-            })
-        }
-        Message.success('已异步触发账号信息更新')
-        getAccounts()
-    } catch (error) {
-        Notification.error({
-            title: '更新账号信息失败!',
-            content: error.message || '请稍后再试'
-        })
-    }
-}
-
-const actionLabel = (action) => {
-    const map = {
-        platform_bind: '平台绑定',
-        platform_unbind: '平台解绑',
-        bot_bind: '机器人绑定',
-        bot_unbind: '机器人解绑'
-    }
-    return map[action] || action
-}
-
-const sourceLabel = (source) => {
-    const map = {
-        user_api: '用户接口',
-        admin_api: '管理员接口',
-        service_api: '客服接口',
-        mcp_qq: '机器人MCP',
-        mcp_work_order: '工单MCP'
-    }
-    return map[source] || source
-}
-
-const formatDetail = (detail) => {
-    if (!detail) return '-'
-    if (typeof detail === 'string') return detail
-    const keyMap = {
-        via: '触发方式',
-        sender: '发送者',
-        old_owner_uuid: '原所属用户UUID',
-        new_owner_uuid: '新所属用户UUID'
-    }
-    const valueMap = {
-        AddAccount: '用户新增账号',
-        DeleteAccount: '账号解绑接口',
-        'AddAccount:auto_unbind_rebind': '新增账号自动解绑并重绑'
-    }
-    return Object.keys(detail)
-        .map((key) => {
-            const label = keyMap[key] || key
-            const rawValue = detail[key]
-            const value = (typeof rawValue === 'string' && valueMap[rawValue]) ? valueMap[rawValue] : rawValue
-            return `${label}:${value}`
-        })
-        .join(';')
-}
-
-const openBindAudit = async (record) => {
-    bindAuditVisible.value = true
-    bindAuditLoading.value = true
-    bindAuditData.value = []
-    try {
-        const studentNum = String(record.student_num || '')
-        if (!studentNum) {
-            Message.warning('该账号缺少学号,无法查询审计记录')
-            return
-        }
-
-        const res = await getAdminBindAuditByAccount({
-            student_num: studentNum,
-            pagesize: 100,
-            current: 1
-        })
-        if (!res || res.code !== 0) {
-            Notification.error({
-                title: '获取绑定解绑记录失败',
-                content: res?.msg ?? '请稍后再试'
-            })
-            return
-        }
-        const primaryData = Array.isArray(res.data) ? res.data : []
-        if (primaryData.length > 0) {
-            bindAuditData.value = primaryData
-            return
-        }
-
-        const fallbackRes = await getAdminBindAuditList({
-            student_num: studentNum,
-            pagesize: 100,
-            current: 1
-        })
-        const fallbackData = (fallbackRes && fallbackRes.code === 0 && Array.isArray(fallbackRes.data)) ? fallbackRes.data : []
-        bindAuditData.value = fallbackData
-    } catch (error) {
-        Notification.error({
-            title: '获取绑定解绑记录失败',
-            content: error.message || '请稍后再试'
-        })
-    } finally {
-        bindAuditLoading.value = false
-    }
+    singleRunAccount.value = item
+    singleRunVisible.value = true
 }
 
 onMounted(() => {

+ 8 - 8
src/pages/admin/lepaoCountLedger/index.vue

@@ -2,11 +2,11 @@
   <div class="store-page">
     <Breadcrumb />
 
-    <a-card title="乐跑次数明细">
+    <a-card title="乐跑里程明细">
       <AppQueryFilter>
-      <a-row class="queryForm app-query-form">
-        <a-col :flex="1">
-          <a-form :model="queryData" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }" label-align="left">
+      <a-row class="query-row">
+        <a-col :flex="'1000px'" class="query-main">
+          <a-form class="queryForm app-query-form" :model="queryData" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }" label-align="left">
             <a-row :gutter="16">
               <a-col :span="6">
                 <a-form-item field="username" label="用户名">
@@ -49,9 +49,9 @@
             </a-row>
           </a-form>
         </a-col>
-        <a-divider style="height: 84px" direction="vertical" />
-        <a-col :flex="'86px'" style="text-align: right">
-          <a-space direction="vertical" :size="18">
+        <a-divider class="query-divider" style="height: 84px" direction="vertical" />
+        <a-col :flex="1" class="app-query-actions">
+          <a-space class="query-actions-space" direction="vertical" :size="18">
             <a-button type="primary" @click="search">
               <template #icon><icon-search /></template>
               搜索
@@ -212,7 +212,7 @@ const fetchData = async () => {
     })
     if (!res || res.code !== 0) {
       return Notification.error({
-        title: '获取次数明细失败',
+        title: '获取里程明细失败',
         content: res?.msg ?? '请稍后再试'
       })
     }

+ 103 - 37
src/pages/admin/lepaoRecords/lepaoRecords.vue

@@ -33,7 +33,7 @@
               <a-col :span="12">
                 <a-form-item field="queryTime" label="乐跑时间">
                   <a-range-picker v-model="queryData.queryTime" show-time format="YYYY-MM-DD HH:mm" value-format="x"
-                    @ok="getRecords()" />
+                    @ok="getRecordsAsync()" />
                 </a-form-item>
               </a-col>
             </a-row>
@@ -59,7 +59,7 @@
       </a-row>
       </AppQueryFilter>
 
-      <a-table :data="data" :bordered="false" hoverable class="table" :loading="loading" :scroll="{ x: 1650 }" :pagination="{
+      <a-table :data="data" :bordered="false" hoverable class="table" :loading="loading" :scroll="{ x: 1780 }" :pagination="{
         showPageSize: true,
         showJumper: true,
         showTotal: true,
@@ -115,37 +115,58 @@
               </a-tag>
             </template>
           </a-table-column>
-          <a-table-column title="状态" ellipsis tooltip :width="230">
+          <a-table-column title="状态" ellipsis tooltip :width="200">
             <template #cell="{ record }">
               <div class="state">
-                <div class="circle one" v-if="record.result.record_failed_reason === '自动确认有效'"></div>
-                <div class="circle else" v-else></div>
-                {{ record.result.record_failed_reason }}
+                <div class="circle" :class="recordDisplay(record).statusLevel"></div>
+                {{ recordDisplay(record).statusText }}
+                <div class="progress" v-if="isRunningLepaoRecord(record)">
+                  <a-progress size="mini" :percent="getLepaoSchedulePercent(record) ?? 0" />
+                </div>
               </div>
             </template>
           </a-table-column>
-          <a-table-column title="跑区" :filterable="{
-            filter: (value, record) => (record.result.pass_tit ?? '').includes(value),
+          <a-table-column title="乐跑跑区" :filterable="{
+            filter: (value, record) => (recordDisplay(record).routeRunZoneName ?? '').includes(value),
             slotName: 'name-filter',
             icon: () => h(IconSearch)
-          }" :width="220">
+          }" :width="160">
             <template #cell="{ record }">
-              {{ record.result.pass_tit }}
+              <a-tag v-if="recordDisplay(record).routeRunZoneName" size="small">
+                {{ recordDisplay(record).routeRunZoneName }}
+              </a-tag>
+              <span v-else>—</span>
+            </template>
+          </a-table-column>
+          <a-table-column title="预计距离" :width="100" ellipsis tooltip>
+            <template #cell="{ record }">
+              <span v-if="recordDisplay(record).expectedKm != null">
+                {{ formatKm(recordDisplay(record).expectedKm) }}
+              </span>
+              <span v-else>—</span>
+            </template>
+          </a-table-column>
+          <a-table-column title="实际距离" :width="100" ellipsis tooltip>
+            <template #cell="{ record }">
+              {{ formatKm(recordDisplay(record).distance) }}
             </template>
           </a-table-column>
-          <a-table-column title="乐跑距离" :width="120" ellipsis tooltip>
+          <a-table-column title="扣费里程" :width="100" ellipsis tooltip>
             <template #cell="{ record }">
-              {{ record.result.distance }} Km
+              <span v-if="recordDisplay(record).officialKm != null && Number.isFinite(Number(recordDisplay(record).officialKm))">
+                {{ formatKm(recordDisplay(record).officialKm) }}
+              </span>
+              <span v-else>—</span>
             </template>
           </a-table-column>
-          <a-table-column title="跑步时长" :width="120" ellipsis tooltip>
+          <a-table-column title="跑步时长" :width="100" ellipsis tooltip>
             <template #cell="{ record }">
-              {{ formatSecondsToMinSec(record.result.time) }}
+              {{ formatSecondsToMinSec(recordDisplay(record).timeSec) }}
             </template>
           </a-table-column>
-          <a-table-column title="平均配速" :width="120" ellipsis tooltip>
+          <a-table-column title="平均配速" :width="90" ellipsis tooltip>
             <template #cell="{ record }">
-              {{ calculatePace(record.result.time, record.result.distance) }}
+              {{ formatPace(recordDisplay(record).timeSec, recordDisplay(record).distance) }}
             </template>
           </a-table-column>
           <a-table-column title="乐跑时间" :width="175" ellipsis tooltip>
@@ -166,9 +187,17 @@
 </template>
 
 <script setup>
-import { ref, reactive, onMounted, h } from 'vue'
+import { ref, reactive, onMounted, onUnmounted, h } from 'vue'
 import { useRouter } from 'vue-router'
 import { adminLepaoRecords } from '@/api/lepao'
+import {
+  parseLepaoRecordRow,
+  formatSecondsToMinSec,
+  formatPace,
+  formatKm,
+  getLepaoSchedulePercent,
+  isRunningLepaoRecord
+} from '@/utils/lepaoRecord'
 import { Notification, Message } from '@arco-design/web-vue'
 import { IconSearch } from '@arco-design/web-vue/es/icon'
 import { getSemesterTimestamps } from '@/utils/util'
@@ -181,7 +210,7 @@ const loading = ref(false)
 const router = useRouter()
 
 const openDetail = (record) => {
-  const key = record.public_id || record.id
+  const key = record.public_id
   if (!key) {
     return Message.warning('该记录缺少可用标识,无法打开详情')
   }
@@ -204,7 +233,7 @@ const pagination = reactive({
 
 const search = () => {
   pagination.current = 1
-  getRecords()
+  getRecordsAsync()
 }
 
 const reset = () => {
@@ -215,12 +244,17 @@ const reset = () => {
   queryData.area = ''
   queryData.create_user = ''
   queryData.queryTime = getSemesterTimestamps()
-  getRecords()
+  getRecordsAsync()
+}
+
+const getRecordsAsync = async () => {
+  loading.value = true
+  await getRecords()
+  loading.value = false
 }
 
 const getRecords = async () => {
   try {
-    loading.value = true
     const reqData = {
       ...queryData,
       pagesize: pagination.pagesize,
@@ -232,6 +266,7 @@ const getRecords = async () => {
         title: '获取乐跑记录失败!',
         content: res?.msg ?? '请稍后再试'
       })
+
     data.value = res.data
     pagination.total = res.pagination.total
   } catch (error) {
@@ -239,46 +274,54 @@ const getRecords = async () => {
       title: '获取乐跑记录失败!',
       content: error.message || '请稍后再试'
     })
-  } finally {
-    loading.value = false
   }
 }
 
 // 分页 - 页码变化
 const handlePageChange = (page) => {
   pagination.current = page
-  getRecords()
+  getRecordsAsync()
 }
 
 // 分页 - 每页条数变化
 const handlePageSizeChange = (size) => {
   pagination.pagesize = size
   pagination.current = 1 // 页大小变化后回到第一页
-  getRecords()
+  getRecordsAsync()
 }
 
 const stramptoTime = (time) => {
-  return new Date(time).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' })
+  const t = Number(time)
+  if (!Number.isFinite(t)) return '—'
+  return new Date(t).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' })
 }
 
-function calculatePace(seconds, kilometers) {
-  const paceInSeconds = seconds / kilometers;
-  const minutes = Math.floor(paceInSeconds / 60);
-  const remainingSeconds = Math.round(paceInSeconds % 60);
+const recordDisplay = (record) => parseLepaoRecordRow(record)
 
-  return `${minutes}'${remainingSeconds.toString().padStart(2, '0')}''`;
-}
+let timer = null
 
-function formatSecondsToMinSec(totalSeconds) {
-  const minutes = Math.floor(totalSeconds / 60);
-  const seconds = totalSeconds % 60;
+const startPolling = () => {
+  if (!timer) {
+    timer = setInterval(async () => {
+      await getRecords()
+    }, 5000)
+  }
+}
 
-  return `${minutes}分${seconds.toString().padStart(2, '0')}秒`;
+const stopPolling = () => {
+  if (!timer) return
+  clearInterval(timer)
+  timer = null
 }
 
 onMounted(() => {
   queryData.queryTime = getSemesterTimestamps()
-  getRecords()
+  getRecordsAsync()
+  startPolling()
+})
+
+onUnmounted(() => {
+  stopPolling()
 })
 </script>
 
@@ -303,6 +346,13 @@ onMounted(() => {
     display: flex;
     align-items: center;
 
+    .progress {
+      margin-left: 8px;
+      min-width: 88px;
+      flex: 1;
+      max-width: 140px;
+    }
+
     .circle {
       border-radius: 50%;
       height: 8px;
@@ -316,6 +366,22 @@ onMounted(() => {
       background-color: rgb(var(--orange-6));
     }
 
+    .running {
+      background-color: rgb(var(--orange-6));
+    }
+
+    .success {
+      background-color: rgb(var(--green-6));
+    }
+
+    .sync {
+      background-color: rgb(var(--arcoblue-6));
+    }
+
+    .fail {
+      background-color: rgb(var(--red-6));
+    }
+
     .one {
       background-color: rgb(var(--green-6));
     }

+ 45 - 46
src/pages/admin/lepaoRecords/recordDetail.vue

@@ -10,8 +10,14 @@
                         :column="isMobile ? 1 : 2"
                         class="record-descriptions"
                     />
-                    <MapContainer v-if="showMap" :point_list="data.result.point_list" :log_list="data.point_data" :pathData="data.data" threeD
-                        style="margin-top: 10px;" />
+                    <MapContainer
+                        v-if="showMap"
+                        :point_list="pointListForMap"
+                        :log_list="logListForMap"
+                        :pathData="pathForMap"
+                        threeD
+                        style="margin-top: 10px;"
+                    />
                 </template>
             </a-skeleton>
         </a-card>
@@ -19,15 +25,19 @@
 </template>
 
 <script setup>
-import { ref, onMounted, onUnmounted } from 'vue'
+import { ref, computed, onMounted, onUnmounted } from 'vue'
 import { adminGetRecordDetail } from '@/api/lepao'
 import { Notification } from '@arco-design/web-vue'
 import { useRoute } from 'vue-router'
 import MapContainer from '@/components/Map/MapContainer.vue'
 import StoreRecordSkeleton from '@/components/skeleton/StoreRecordSkeleton.vue'
+import {
+    buildRecordDetailInfo,
+    parseLepaoRecordRow,
+    normalizePointData
+} from '@/utils/lepaoRecord'
 
 const route = useRoute()
-const uuidLike = /^[0-9a-fA-F-]{16,}$/
 
 const loading = ref(false)
 const showMap = ref(false)
@@ -36,41 +46,50 @@ const info = ref([])
 const isMobile = ref(false)
 let mobileResizeHandler = null
 
+const pathForMap = computed(() => {
+    const poly = data.value.path_polyline
+    if (Array.isArray(poly) && poly.length > 0) return poly
+    return []
+})
+
+const pointListForMap = computed(() => {
+    const row = parseLepaoRecordRow({
+        result: data.value.result,
+        jkes_record: data.value.jkes_record,
+        state: data.value.state,
+        run_zone_name: data.value.run_zone_name,
+        path_run_zone_name: data.value.path_run_zone_name,
+        path_distance: data.value.path_distance
+    })
+    return row.pointList || []
+})
+
+const logListForMap = computed(() => normalizePointData(data.value.point_data))
+
 const GetRecordDetail = async (routeId) => {
     try {
         loading.value = true
         showMap.value = false
         const raw = decodeURIComponent(String(routeId || ''))
-        const payload = uuidLike.test(raw)
-            ? { public_id: raw }
-            : { id: raw }
-        const res = await adminGetRecordDetail(payload)
+        if (!raw) {
+            return Notification.error({
+                title: '获取记录详情失败',
+                content: '记录标识无效'
+            })
+        }
+        const res = await adminGetRecordDetail({ public_id: raw })
         if (!res || res.code !== 0)
             return Notification.error({
-                title: '获取路径数据失败!',
+                title: '获取记录详情失败',
                 content: res?.msg ?? '请稍后再试'
             })
 
         data.value = res.data
         showMap.value = true
-
-        info.value = [
-            { label: '账号名称', value: res.data.name },
-            { label: '乐跑账号', value: res.data.lepao_account },
-            { label: '乐跑类型', value: res.data.run_mode === 'manual' ? '手动乐跑' : '自动乐跑' },
-            { label: '跑区名称', value: res.data.result.pass_tit },
-            { label: '路线ID', value: res.data.path_id },
-            { label: '记录时间', value: stramptoTime(res.data.time) },
-            { label: '开始时间', value: stramptoTime(res.data.result.start_time * 1000) },
-            { label: '乐跑状态', value: res.data.result.record_failed_reason },
-            { label: '打卡点数量', value: res.data.result.point_list.length },
-            { label: '跑步距离', value: res.data.result.distance + ' Km' },
-            { label: '跑步时长', value: formatSecondsToMinSec(res.data.result.time) },
-            { label: '平均配速', value: calculatePace(res.data.result.time, res.data.result.distance) }
-        ]
+        info.value = buildRecordDetailInfo(res.data, { isAdmin: true })
     } catch (error) {
         Notification.error({
-            title: '获取路径数据失败!',
+            title: '获取记录详情失败',
             content: error.message || '请稍后再试'
         })
     } finally {
@@ -90,26 +109,6 @@ onMounted(() => {
 onUnmounted(() => {
     if (mobileResizeHandler) window.removeEventListener('resize', mobileResizeHandler)
 })
-
-const stramptoTime = (time) => {
-    return new Date(time).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
-}
-
-function calculatePace(seconds, kilometers) {
-    const paceInSeconds = seconds / kilometers;
-    const minutes = Math.floor(paceInSeconds / 60);
-    const remainingSeconds = Math.round(paceInSeconds % 60);
-
-    return `${minutes}'${remainingSeconds.toString().padStart(2, '0')}''`;
-}
-
-function formatSecondsToMinSec(totalSeconds) {
-    const minutes = Math.floor(totalSeconds / 60);
-    const seconds = totalSeconds % 60;
-
-    return `${minutes}分${seconds.toString().padStart(2, '0')}秒`;
-}
-
 </script>
 
 <style scoped lang="less">
@@ -129,4 +128,4 @@ function formatSecondsToMinSec(totalSeconds) {
         text-overflow: ellipsis;
     }
 }
-</style>
+</style>

+ 22 - 11
src/pages/admin/mqQueue/index.vue

@@ -59,7 +59,7 @@
         <a-col :span="6">
           <a-form-item label="队列" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }">
             <a-select v-model="peekForm.queue" placeholder="选择队列">
-              <a-option v-for="q in ALLOWED_QUEUES" :key="q" :value="q">{{ q }}</a-option>
+              <a-option v-for="q in queueOptions" :key="q" :value="q">{{ q }}</a-option>
             </a-select>
           </a-form-item>
         </a-col>
@@ -136,16 +136,19 @@ import { ref, reactive, watch, onMounted, onUnmounted } from 'vue'
 import { adminGetQueueTasks } from '@/api/mq'
 import { Notification } from '@arco-design/web-vue'
 
-/** 与后端 ALLOWED_QUEUES 保持一致,用于下拉与展示 */
-const ALLOWED_QUEUES = [
-  'jkes_哪吒乐跑_task_queue',
-  '哪吒乐跑_task_result_queue',
-  '哪吒乐跑_task_dead_queue',
-  '哪吒乐跑_message_queue',
-  'order_payment_check',
-  'mq_health_check'
+/** 与后端 jkesMqNames.QUEUE_BASE_NAMES 保持一致(无 mqPrefix 时的基名) */
+const QUEUE_BASE_NAMES = [
+  'jkes_runforge_task_queue',
+  'jkes_runforge_task_result_queue',
+  'jkes_runforge_task_dead_queue',
+  'jkes_runforge_message_queue',
+  'jkes_order_payment_check',
+  'jkes_mq_health_check'
 ]
 
+/** 下拉与概览使用的队列名(首次用基名,概览拉取后同步后端实际名,含 mqPrefix) */
+const queueOptions = ref([...QUEUE_BASE_NAMES])
+
 const summaryLoading = ref(false)
 const summaryRows = ref([])
 const summaryFetchedAt = ref(null)
@@ -161,7 +164,7 @@ const peekMeta = reactive({
 })
 
 const peekForm = reactive({
-  queue: 'jkes_哪吒乐跑_task_queue',
+  queue: QUEUE_BASE_NAMES[0],
   limit: 30
 })
 
@@ -214,7 +217,15 @@ const loadSummary = async (silent = false) => {
       return
     }
     const queues = res.data?.queues || {}
-    summaryRows.value = ALLOWED_QUEUES.map((name) => {
+    const names = Object.keys(queues)
+    if (names.length) {
+      queueOptions.value = names
+      if (!names.includes(peekForm.queue)) {
+        peekForm.queue = names[0]
+      }
+    }
+    const displayNames = names.length ? names : QUEUE_BASE_NAMES
+    summaryRows.value = displayNames.map((name) => {
       const info = queues[name] || {}
       return {
         name,

+ 0 - 325
src/pages/admin/powerTask/index.vue

@@ -1,325 +0,0 @@
-<template>
-  <div class="store-page">
-    <Breadcrumb />
-    <a-card title="电费查询任务">
-      <AppQueryFilter>
-        <a-row class="query-row">
-          <a-col :flex="'1000px'" class="query-main">
-            <a-form class="queryForm app-query-form" :model="query" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }" label-align="left">
-              <a-row :gutter="16">
-                <a-col :span="12">
-                  <a-form-item field="create_user" label="用户UUID">
-                    <a-input v-model="query.create_user" placeholder="创建人 UUID" allow-clear />
-                  </a-form-item>
-                </a-col>
-                <a-col :span="12">
-                  <a-form-item field="username" label="用户名">
-                    <a-input v-model="query.username" placeholder="平台用户名" allow-clear />
-                  </a-form-item>
-                </a-col>
-                <a-col :span="12">
-                  <a-form-item field="email" label="通知邮箱">
-                    <a-input v-model="query.email" placeholder="任务通知邮箱" allow-clear />
-                  </a-form-item>
-                </a-col>
-                <a-col :span="12">
-                  <a-form-item field="area" label="校区">
-                    <a-input v-model="query.area" placeholder="校区" allow-clear />
-                  </a-form-item>
-                </a-col>
-                <a-col :span="12">
-                  <a-form-item field="building" label="楼栋">
-                    <a-input v-model="query.building" placeholder="楼栋" allow-clear />
-                  </a-form-item>
-                </a-col>
-                <a-col :span="12">
-                  <a-form-item field="room" label="宿舍号">
-                    <a-input v-model="query.room" placeholder="宿舍号" allow-clear />
-                  </a-form-item>
-                </a-col>
-                <a-col :span="12">
-                  <a-form-item field="queryTime" label="更新时间">
-                    <a-range-picker v-model="query.queryTime" show-time format="YYYY-MM-DD HH:mm" value-format="x" />
-                  </a-form-item>
-                </a-col>
-              </a-row>
-            </a-form>
-          </a-col>
-          <a-divider class="query-divider" style="height: 84px" direction="vertical" />
-          <a-col :flex="1" class="app-query-actions">
-            <a-space class="query-actions-space" direction="vertical" :size="18">
-              <a-button type="primary" @click="search">搜索</a-button>
-              <a-button @click="reset">重置</a-button>
-            </a-space>
-          </a-col>
-        </a-row>
-      </AppQueryFilter>
-
-      <a-table
-        :bordered="false"
-        class="table"
-        :data="data"
-        :loading="loading"
-        :scroll="{ x: 1670 }"
-        :pagination="{
-          showPageSize: true,
-          showJumper: true,
-          showTotal: true,
-          pageSize: pagination.pagesize,
-          current: pagination.current,
-          total: pagination.total
-        }"
-        @page-change="onPageChange"
-        @page-size-change="onPageSizeChange"
-      >
-        <template #columns>
-          <a-table-column title="ID" data-index="id" :width="80" />
-          <a-table-column title="创建用户" :width="180">
-            <template #cell="{ record }">
-              <a-space>
-                <a-avatar :size="26">
-                  <img :src="record.avatar || defaultAvatar" />
-                </a-avatar>
-                {{ record.username || record.create_user || '-' }}
-              </a-space>
-            </template>
-          </a-table-column>
-          <a-table-column title="宿舍" :width="220">
-            <template #cell="{ record }">
-              {{ record.area }} · {{ record.building }} · {{ record.room }}
-            </template>
-          </a-table-column>
-          <a-table-column title="余额" :width="100">
-            <template #cell="{ record }">
-              <a-tag :color="isLowBalance(record) ? 'red' : 'green'">¥{{ record.balance }}</a-tag>
-            </template>
-          </a-table-column>
-          <a-table-column title="阈值" :width="90">
-            <template #cell="{ record }">¥{{ record.lowest }}</template>
-          </a-table-column>
-          <a-table-column title="通知邮箱" data-index="email" :width="180" ellipsis tooltip />
-          <a-table-column title="扣费时间" data-index="koufei_date" :width="170" ellipsis tooltip />
-          <a-table-column title="已提醒" :width="90">
-            <template #cell="{ record }">
-              <a-tag :color="Number(record.is_notice) === 1 ? 'orange' : 'gray'">
-                {{ Number(record.is_notice) === 1 ? '是' : '否' }}
-              </a-tag>
-            </template>
-          </a-table-column>
-          <a-table-column title="备注" data-index="notes" :width="140" ellipsis tooltip />
-          <a-table-column title="添加时间" :width="170">
-            <template #cell="{ record }">{{ formatTime(record.create_time) }}</template>
-          </a-table-column>
-          <a-table-column title="更新时间" :width="170">
-            <template #cell="{ record }">{{ formatTime(record.update_time) }}</template>
-          </a-table-column>
-          <a-table-column title="操作" :width="120" :fixed="tableFixed('right')">
-            <template #cell="{ record }">
-              <a-button type="text" size="small" @click="openChangeRecord(record)">账单记录</a-button>
-            </template>
-          </a-table-column>
-        </template>
-      </a-table>
-    </a-card>
-
-    <a-modal
-      v-model:visible="recordVisible"
-      title="电费变更记录"
-      width="auto"
-      hide-cancel
-      esc-to-close
-      draggable
-    >
-      <a-table
-        :bordered="false"
-        :data="changeList"
-        :loading="recordLoading"
-        :scroll="{ x: 720 }"
-        :pagination="false"
-        stripe
-      >
-        <template #columns>
-          <a-table-column title="记录时间" :width="170">
-            <template #cell="{ record }">{{ formatTime(record.time) }}</template>
-          </a-table-column>
-          <a-table-column title="扣费时间" data-index="change_time" :width="170" ellipsis tooltip />
-          <a-table-column title="电费余额" :width="110">
-            <template #cell="{ record }">¥{{ record.balance }}</template>
-          </a-table-column>
-          <a-table-column title="原余额" :width="110">
-            <template #cell="{ record }">¥{{ record.old_balance }}</template>
-          </a-table-column>
-          <a-table-column title="变动金额" :width="110">
-            <template #cell="{ record }">
-              <a-tag :color="Number(record.balance) < Number(record.old_balance) ? 'red' : 'green'">
-                ¥{{ (Number(record.balance) - Number(record.old_balance)).toFixed(2) }}
-              </a-tag>
-            </template>
-          </a-table-column>
-        </template>
-      </a-table>
-    </a-modal>
-  </div>
-</template>
-
-<script setup>
-import { reactive, ref, onMounted } from 'vue'
-import { Notification } from '@arco-design/web-vue'
-import { adminPowerTaskList, adminPowerGetChangeRecord } from '@/api/power'
-import { useResponsiveTable } from '@/hooks/useResponsiveTable'
-
-const { tableFixed } = useResponsiveTable()
-const defaultAvatar = 'https://lepao-cloud.xxoo365.top/view.php/25aa126dc406974ff3579a99a2c6501a.png'
-const loading = ref(false)
-const data = ref([])
-const query = reactive({
-  create_user: '',
-  username: '',
-  email: '',
-  area: '',
-  building: '',
-  room: '',
-  queryTime: []
-})
-const pagination = reactive({
-  total: 0,
-  current: 1,
-  pagesize: 20
-})
-
-const recordVisible = ref(false)
-const recordLoading = ref(false)
-const changeList = ref([])
-
-const formatTime = (time) => {
-  if (!time) return '-'
-  return new Date(Number(time)).toLocaleString('zh-CN', {
-    year: 'numeric',
-    month: '2-digit',
-    day: '2-digit',
-    hour: '2-digit',
-    minute: '2-digit'
-  })
-}
-
-const isLowBalance = (record) => Number(record.balance) <= Number(record.lowest)
-
-const getList = async () => {
-  try {
-    loading.value = true
-    const res = await adminPowerTaskList({
-      ...query,
-      current: pagination.current,
-      pagesize: pagination.pagesize
-    })
-    if (!res || res.code !== 0) {
-      return Notification.error({
-        title: '获取电费任务失败',
-        content: res?.msg ?? '请稍后再试'
-      })
-    }
-    data.value = res.data || []
-    pagination.total = res.pagination?.total || 0
-  } catch (error) {
-    Notification.error({
-      title: '获取电费任务失败',
-      content: error.message || '请稍后再试'
-    })
-  } finally {
-    loading.value = false
-  }
-}
-
-const openChangeRecord = async (record) => {
-  try {
-    recordLoading.value = true
-    recordVisible.value = true
-    changeList.value = []
-    const res = await adminPowerGetChangeRecord({ id: record.id })
-    if (!res || res.code !== 0) {
-      return Notification.error({
-        title: '获取账单记录失败',
-        content: res?.msg ?? '请稍后再试'
-      })
-    }
-    changeList.value = res.data || []
-  } catch (error) {
-    Notification.error({
-      title: '获取账单记录失败',
-      content: error.message || '请稍后再试'
-    })
-  } finally {
-    recordLoading.value = false
-  }
-}
-
-const search = () => {
-  pagination.current = 1
-  getList()
-}
-
-const reset = () => {
-  query.create_user = ''
-  query.username = ''
-  query.email = ''
-  query.area = ''
-  query.building = ''
-  query.room = ''
-  query.queryTime = []
-  pagination.current = 1
-  getList()
-}
-
-const onPageChange = (page) => {
-  pagination.current = page
-  getList()
-}
-
-const onPageSizeChange = (size) => {
-  pagination.pagesize = size
-  pagination.current = 1
-  getList()
-}
-
-onMounted(() => {
-  getList()
-})
-</script>
-
-<style scoped>
-.query-row {
-  margin-bottom: 10px;
-}
-
-.query-main {
-  min-width: 0;
-}
-
-.query-actions-space {
-  width: 100%;
-}
-
-.table {
-  margin-top: 16px;
-}
-
-@media (max-width: 768px) {
-  .query-row {
-    display: block;
-  }
-
-  .query-divider {
-    display: none;
-  }
-
-  .query-actions-space {
-    margin-top: 4px;
-    flex-direction: row !important;
-    gap: 10px !important;
-  }
-
-  .query-actions-space :deep(.arco-btn) {
-    flex: 1;
-  }
-}
-</style>

+ 0 - 175
src/pages/admin/qkBatch/index.vue

@@ -1,175 +0,0 @@
-<template>
-  <div class="store-page">
-    <Breadcrumb />
-    <a-card title="抢课批次管理">
-      <div class="toolbar">
-        <a-button v-if="canManage" type="primary" @click="openModal()">
-          <template #icon><icon-plus /></template>
-          新建批次
-        </a-button>
-        <a-button @click="getList()">
-          <template #icon><icon-refresh /></template>
-          刷新
-        </a-button>
-      </div>
-
-      <a-alert type="info" class="info-alert">
-        停用批次会自动收回该批次下所有进行中的任务;修改批次 ID 后会自动将已分配任务重新放回待分配队列。
-      </a-alert>
-
-      <a-table
-        :data="data"
-        :bordered="false"
-        hoverable
-        class="table"
-        :loading="loading"
-        :pagination="false"
-        :scroll="{ x: 980 }"
-      >
-        <template #columns>
-          <a-table-column title="ID" data-index="id" :width="80" />
-          <a-table-column title="名称" data-index="name" :width="180" ellipsis tooltip />
-          <a-table-column title="批次 ID" data-index="jx0502zbid" :width="260" ellipsis tooltip />
-          <a-table-column title="状态" :width="100">
-            <template #cell="{ record }">
-              <a-tag :color="record.enabled ? 'green' : 'gray'">{{ record.enabled ? '启用' : '停用' }}</a-tag>
-            </template>
-          </a-table-column>
-          <a-table-column title="更新时间" :width="180">
-            <template #cell="{ record }">{{ formatTime(record.update_time) }}</template>
-          </a-table-column>
-          <a-table-column title="操作" :width="100" :fixed="tableFixed('right')">
-            <template #cell="{ record }">
-              <a-button v-if="canManage" type="text" size="small" @click="openModal(record)">编辑</a-button>
-            </template>
-          </a-table-column>
-        </template>
-        <template #empty>
-          <a-empty description="暂无抢课批次" />
-        </template>
-      </a-table>
-    </a-card>
-
-    <a-modal
-      v-model:visible="visible"
-      :title="form.id ? '编辑批次' : '新建批次'"
-      :width="560"
-      @ok="submitBatch"
-    >
-      <a-form :model="form" layout="vertical">
-        <a-form-item label="批次名称" required>
-          <a-input v-model="form.name" placeholder="例如:2025春公选课" allow-clear />
-        </a-form-item>
-        <a-form-item label="批次 ID(jx0502zbid)" required>
-          <a-input v-model="form.jx0502zbid" placeholder="教务系统选课批次 ID" allow-clear />
-        </a-form-item>
-        <a-form-item label="是否启用">
-          <a-switch v-model="form.enabled" />
-        </a-form-item>
-      </a-form>
-    </a-modal>
-  </div>
-</template>
-
-<script setup>
-import { computed, onMounted, reactive, ref } from 'vue'
-import { Notification } from '@arco-design/web-vue'
-import { IconPlus, IconRefresh } from '@arco-design/web-vue/es/icon'
-import { adminQkBatchList, adminQkBatchSave } from '@/api/qk'
-import { hasPermission } from '@/utils/permission'
-import { useResponsiveTable } from '@/hooks/useResponsiveTable'
-
-const { tableFixed } = useResponsiveTable()
-const canManage = computed(() => hasPermission('action.qk.admin.batchManage'))
-
-const data = ref([])
-const loading = ref(false)
-const visible = ref(false)
-const form = reactive({
-  id: '',
-  name: '',
-  jx0502zbid: '',
-  enabled: true
-})
-
-const formatTime = (time) => {
-  if (!time) return '-'
-  return new Date(Number(time)).toLocaleString('zh-CN')
-}
-
-const resetForm = () => {
-  Object.assign(form, {
-    id: '',
-    name: '',
-    jx0502zbid: '',
-    enabled: true
-  })
-}
-
-const openModal = (record) => {
-  resetForm()
-  if (record) {
-    Object.assign(form, {
-      id: record.id,
-      name: record.name,
-      jx0502zbid: record.jx0502zbid,
-      enabled: !!record.enabled
-    })
-  }
-  visible.value = true
-}
-
-const getList = async () => {
-  loading.value = true
-  try {
-    const res = await adminQkBatchList()
-    if (!res || res.code !== 0) {
-      Notification.error({ title: '获取批次失败', content: res?.msg || '请稍后再试' })
-      return
-    }
-    data.value = res.data || []
-  } catch (error) {
-    Notification.error({ title: '获取批次失败', content: error.message || '请稍后再试' })
-  } finally {
-    loading.value = false
-  }
-}
-
-const submitBatch = async () => {
-  const res = await adminQkBatchSave({
-    id: form.id || undefined,
-    name: form.name,
-    jx0502zbid: form.jx0502zbid,
-    enabled: form.enabled
-  })
-  if (!res || res.code !== 0) {
-    Notification.error({ title: '保存失败', content: res?.msg || '请稍后再试' })
-    return false
-  }
-  Notification.success({ title: '保存成功' })
-  visible.value = false
-  getList()
-}
-
-onMounted(() => {
-  getList()
-})
-</script>
-
-<style scoped lang="less">
-@import '@/styles/store-theme.less';
-
-.toolbar {
-  display: flex;
-  gap: 10px;
-  margin-bottom: 12px;
-}
-
-.info-alert {
-  margin-bottom: 16px;
-}
-
-.table {
-  margin-top: 4px;
-}
-</style>

+ 0 - 493
src/pages/admin/qkClient/index.vue

@@ -1,493 +0,0 @@
-<template>
-  <div class="store-page">
-    <Breadcrumb />
-    <a-card title="抢课客户端">
-      <a-row :gutter="[16, 16]" class="stats-row" align="stretch">
-        <a-col :xs="24" :md="10" :lg="8" class="stats-col">
-          <div class="stats-panel stats-highlight">
-            <a-progress
-              type="circle"
-              :percent="clientStats.occupancyRate / 100"
-              :stroke-width="8"
-              :width="isMobile ? 96 : 108"
-              :color="occupancyColor"
-            >
-              <template #text>
-                <div class="progress-text">
-                  <div class="progress-value">{{ clientStats.occupancyRate }}%</div>
-                  <div class="progress-label">槽位占用</div>
-                </div>
-              </template>
-            </a-progress>
-            <div class="stats-highlight-meta">
-              <div class="highlight-title">在线客户端容量</div>
-              <div class="highlight-desc">
-                已占用 <strong>{{ clientStats.usedSlots }}</strong> / 总容量 <strong>{{ clientStats.totalSlots }}</strong>
-              </div>
-              <div class="highlight-desc">
-                剩余可用槽位 <strong class="highlight-free">{{ clientStats.freeSlots }}</strong>
-              </div>
-            </div>
-          </div>
-        </a-col>
-        <a-col :xs="24" :md="14" :lg="16" class="stats-col">
-          <div class="stats-panel stats-grid-panel">
-            <a-row :gutter="[12, 12]" class="stats-grid">
-            <a-col :xs="12" :sm="8" :md="6">
-              <div class="stat-card">
-                <a-statistic title="任务总数" :value="taskStats.total" />
-              </div>
-            </a-col>
-            <a-col :xs="12" :sm="8" :md="6">
-              <div class="stat-card">
-                <a-statistic title="抢课中" :value="taskStats.active" value-style="color: rgb(var(--orange-6))" />
-              </div>
-            </a-col>
-            <a-col :xs="12" :sm="8" :md="6">
-              <div class="stat-card">
-                <a-statistic title="待分配" :value="taskStats.pending" value-style="color: rgb(var(--arcoblue-6))" />
-              </div>
-            </a-col>
-            <a-col :xs="12" :sm="8" :md="6">
-              <div class="stat-card">
-                <a-statistic title="客户端总数" :value="clientStats.total" />
-              </div>
-            </a-col>
-            <a-col :xs="12" :sm="8" :md="6">
-              <div class="stat-card">
-                <a-statistic title="在线客户端" :value="clientStats.online" value-style="color: rgb(var(--green-6))" />
-              </div>
-            </a-col>
-            <a-col :xs="12" :sm="8" :md="6">
-              <div class="stat-card">
-                <a-statistic title="已禁用" :value="clientStats.disabled" />
-              </div>
-            </a-col>
-            <a-col :xs="12" :sm="8" :md="6">
-              <div class="stat-card">
-                <a-statistic title="累计完成" :value="clientStats.totalCompleted" />
-              </div>
-            </a-col>
-            <a-col :xs="12" :sm="8" :md="6">
-              <div class="stat-card">
-                <a-statistic title="累计成功" :value="clientStats.totalSuccess" value-style="color: rgb(var(--green-6))" />
-              </div>
-            </a-col>
-            </a-row>
-          </div>
-        </a-col>
-      </a-row>
-
-      <a-table
-        row-key="client_id"
-        :data="data"
-        :bordered="false"
-        hoverable
-        class="table app-data-table"
-        :loading="loading"
-        :pagination="false"
-        :scroll="{ x: isMobile ? 960 : 1250 }"
-      >
-        <template #columns>
-          <a-table-column title="客户端 ID" data-index="client_id" :width="180" ellipsis tooltip />
-          <a-table-column title="备注" data-index="label" :width="140" ellipsis tooltip />
-          <a-table-column title="状态" :width="90">
-            <template #cell="{ record }">
-              <a-tag :color="Number(record.enabled) === 1 ? 'arcoblue' : 'red'">
-                {{ Number(record.enabled) === 1 ? '启用' : '已禁用' }}
-              </a-tag>
-            </template>
-          </a-table-column>
-          <a-table-column title="在线" :width="90">
-            <template #cell="{ record }">
-              <a-tag :color="Number(record.online) === 1 ? 'green' : 'gray'">
-                {{ Number(record.online) === 1 ? '在线' : '离线' }}
-              </a-tag>
-            </template>
-          </a-table-column>
-          <a-table-column title="槽位" :width="120">
-            <template #cell="{ record }">
-              <div class="slot-cell">
-                <div class="slot-cell__text">{{ record.current_slots }} / {{ record.max_slots }}</div>
-                <div class="slot-bar">
-                  <div
-                    class="slot-bar__inner"
-                    :style="{
-                      width: `${slotPercent(record)}%`,
-                      backgroundColor: slotBarColor(record)
-                    }"
-                  />
-                </div>
-              </div>
-            </template>
-          </a-table-column>
-          <a-table-column title="机器名" data-index="hostname" :width="140" ellipsis tooltip />
-          <a-table-column title="登录用户" data-index="os_username" :width="120" ellipsis tooltip />
-          <a-table-column title="CPU" :width="220">
-            <template #cell="{ record }">
-              <AppTableEllipsis
-                :row-key="`${record.client_id}-cpu`"
-                :content="`${record.cpu_model || '-'} (${record.cpu_threads || '-'}线程)`"
-              />
-            </template>
-          </a-table-column>
-          <a-table-column title="内存" :width="140">
-            <template #cell="{ record }">
-              {{ record.free_mem_mb || '-' }} / {{ record.total_mem_mb || '-' }} MB
-            </template>
-          </a-table-column>
-          <a-table-column title="系统" data-index="platform" :width="160" ellipsis tooltip />
-          <a-table-column title="完成/成功" :width="120">
-            <template #cell="{ record }">{{ record.total_completed }} / {{ record.total_success }}</template>
-          </a-table-column>
-          <a-table-column title="最后心跳" :width="180">
-            <template #cell="{ record }">{{ formatTime(record.last_heartbeat_at) }}</template>
-          </a-table-column>
-          <a-table-column title="操作" :width="120" :fixed="tableFixed('right')">
-            <template #cell="{ record }">
-              <a-popconfirm
-                v-if="Number(record.enabled) === 1"
-                content="确认禁用该客户端?"
-                @ok="deleteClient(record)"
-              >
-                <a-button type="text" status="danger" size="small">禁用</a-button>
-              </a-popconfirm>
-              <a-popconfirm
-                v-else
-                content="确认解禁该客户端?解禁后客户端可重新注册上线。"
-                @ok="enableClient(record)"
-              >
-                <a-button type="text" status="success" size="small">解禁</a-button>
-              </a-popconfirm>
-            </template>
-          </a-table-column>
-        </template>
-        <template #empty>
-          <a-empty description="暂无抢课客户端" />
-        </template>
-      </a-table>
-    </a-card>
-  </div>
-</template>
-
-<script setup>
-import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
-import { Notification } from '@arco-design/web-vue'
-import { adminQkClientDelete, adminQkClientEnable, adminQkClientList, adminQkTaskList } from '@/api/qk'
-import { useResponsiveTable } from '@/hooks/useResponsiveTable'
-
-const { tableFixed, isMobile } = useResponsiveTable()
-
-const data = ref([])
-const loading = ref(false)
-const taskStats = reactive({
-  total: 0,
-  active: 0,
-  pending: 0
-})
-
-const clientStats = computed(() => {
-  const list = data.value || []
-  const enabled = list.filter(item => Number(item.enabled) === 1)
-  const online = enabled.filter(item => Number(item.online) === 1)
-  const totalSlots = online.reduce((sum, item) => sum + Math.max(0, Number(item.max_slots) || 0), 0)
-  const usedSlots = online.reduce((sum, item) => sum + Math.max(0, Number(item.current_slots) || 0), 0)
-  const freeSlots = Math.max(0, totalSlots - usedSlots)
-  const occupancyRate = totalSlots > 0 ? Math.min(100, Math.round((usedSlots / totalSlots) * 100)) : 0
-  return {
-    total: list.length,
-    online: online.length,
-    disabled: list.length - enabled.length,
-    totalSlots,
-    usedSlots,
-    freeSlots,
-    occupancyRate,
-    totalCompleted: list.reduce((sum, item) => sum + (Number(item.total_completed) || 0), 0),
-    totalSuccess: list.reduce((sum, item) => sum + (Number(item.total_success) || 0), 0)
-  }
-})
-
-const occupancyColor = computed(() => {
-  const rate = clientStats.value.occupancyRate
-  if (rate >= 90) return '#F53F3F'
-  if (rate >= 70) return '#FF7D00'
-  return '#165DFF'
-})
-
-const slotPercent = (record) => {
-  const max = Math.max(0, Number(record.max_slots) || 0)
-  const current = Math.max(0, Number(record.current_slots) || 0)
-  if (!max) return 0
-  return Math.min(100, Math.round((current / max) * 100))
-}
-
-const slotBarColor = (record) => {
-  const percent = slotPercent(record)
-  if (percent >= 90) return '#F53F3F'
-  if (Number(record.online) !== 1 || Number(record.enabled) !== 1) return '#C9CDD4'
-  return '#165DFF'
-}
-
-const formatTime = (time) => {
-  if (!time) return '-'
-  return new Date(Number(time)).toLocaleString('zh-CN', {
-    year: 'numeric',
-    month: '2-digit',
-    day: '2-digit',
-    hour: '2-digit',
-    minute: '2-digit'
-  })
-}
-
-const fetchTaskStats = async () => {
-  try {
-    const [allRes, runningRes, assignedRes, pendingRes] = await Promise.all([
-      adminQkTaskList({ current: 1, pagesize: 1 }),
-      adminQkTaskList({ current: 1, pagesize: 1, status: 'running' }),
-      adminQkTaskList({ current: 1, pagesize: 1, status: 'assigned' }),
-      adminQkTaskList({ current: 1, pagesize: 1, status: 'pending' })
-    ])
-    if (allRes?.code === 0) taskStats.total = allRes.data?.total || 0
-    const running = runningRes?.code === 0 ? (runningRes.data?.total || 0) : 0
-    const assigned = assignedRes?.code === 0 ? (assignedRes.data?.total || 0) : 0
-    taskStats.active = running + assigned
-    if (pendingRes?.code === 0) taskStats.pending = pendingRes.data?.total || 0
-  } catch (_) {}
-}
-
-const getList = async (silent = false) => {
-  try {
-    if (!silent) loading.value = true
-    const res = await adminQkClientList()
-    if (!res || res.code !== 0) {
-      if (!silent) {
-        Notification.error({ title: '获取客户端失败', content: res?.msg || '请稍后再试' })
-      }
-      return
-    }
-    data.value = res.data || []
-  } catch (error) {
-    if (!silent) {
-      Notification.error({ title: '获取客户端失败', content: error.message || '请稍后再试' })
-    }
-  } finally {
-    if (!silent) loading.value = false
-  }
-}
-
-const deleteClient = async (record) => {
-  const res = await adminQkClientDelete({ client_id: record.client_id })
-  if (!res || res.code !== 0) {
-    return Notification.error({ title: '禁用客户端失败', content: res?.msg || '请稍后再试' })
-  }
-  Notification.success({ title: '已禁用' })
-  getList()
-  fetchTaskStats()
-}
-
-const enableClient = async (record) => {
-  const res = await adminQkClientEnable({ client_id: record.client_id })
-  if (!res || res.code !== 0) {
-    return Notification.error({ title: '解禁客户端失败', content: res?.msg || '请稍后再试' })
-  }
-  Notification.success({ title: '已解禁', content: '客户端下次上线时可正常注册' })
-  getList()
-  fetchTaskStats()
-}
-
-let pollTimer = null
-let statsTimer = null
-
-const startPolling = () => {
-  if (!pollTimer) {
-    pollTimer = setInterval(() => getList(true), 5000)
-  }
-  if (!statsTimer) {
-    statsTimer = setInterval(fetchTaskStats, 30000)
-  }
-}
-
-const stopPolling = () => {
-  if (pollTimer) {
-    clearInterval(pollTimer)
-    pollTimer = null
-  }
-  if (statsTimer) {
-    clearInterval(statsTimer)
-    statsTimer = null
-  }
-}
-
-onMounted(() => {
-  getList()
-  fetchTaskStats()
-  startPolling()
-})
-
-onUnmounted(() => {
-  stopPolling()
-})
-</script>
-
-<style scoped lang="less">
-@import '@/styles/store-theme.less';
-
-.stats-row {
-  margin-bottom: 24px;
-}
-
-.stats-col {
-  display: flex;
-}
-
-.stats-panel {
-  width: 100%;
-  min-height: 148px;
-  box-sizing: border-box;
-}
-
-.stats-highlight {
-  display: flex;
-  align-items: center;
-  gap: 16px;
-  height: 100%;
-  padding: 16px;
-  border: 1px solid var(--color-border-2);
-  border-radius: 12px;
-  background: linear-gradient(135deg, rgba(var(--primary-1), 0.35), rgba(var(--primary-1), 0.08));
-}
-
-.stats-grid-panel {
-  display: flex;
-  flex-direction: column;
-  height: 100%;
-  padding: 12px;
-  border: 1px solid var(--color-border-2);
-  border-radius: 12px;
-  background: var(--color-bg-1);
-}
-
-.stats-grid {
-  flex: 1;
-  align-content: stretch;
-}
-
-.stat-card {
-  height: 100%;
-  min-height: 72px;
-  padding: 12px;
-  border: 1px solid var(--color-border-2);
-  border-radius: 10px;
-  background: var(--color-bg-2);
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
-  box-sizing: border-box;
-}
-
-:deep(.stat-card .arco-statistic-title) {
-  margin-bottom: 4px;
-  font-size: 13px;
-}
-
-:deep(.stat-card .arco-statistic-value) {
-  font-size: 22px;
-  line-height: 1.2;
-}
-
-.slot-cell {
-  width: 96px;
-  max-width: 100%;
-}
-
-.slot-cell__text {
-  font-size: 13px;
-  line-height: 1.4;
-  white-space: nowrap;
-}
-
-.slot-bar {
-  height: 6px;
-  margin-top: 6px;
-  border-radius: 999px;
-  background: var(--color-fill-3);
-  overflow: hidden;
-}
-
-.slot-bar__inner {
-  height: 100%;
-  border-radius: 999px;
-  min-width: 0;
-  max-width: 100%;
-  transition: width 0.2s ease;
-}
-
-.progress-text {
-  text-align: center;
-}
-
-.progress-value {
-  font-size: 18px;
-  font-weight: 600;
-  line-height: 1.2;
-}
-
-.progress-label {
-  margin-top: 2px;
-  font-size: 12px;
-  color: var(--color-text-3);
-}
-
-.stats-highlight-meta {
-  flex: 1;
-  min-width: 0;
-}
-
-.highlight-title {
-  font-size: 15px;
-  font-weight: 600;
-  margin-bottom: 8px;
-}
-
-.highlight-desc {
-  color: var(--color-text-2);
-  font-size: 13px;
-  line-height: 1.6;
-
-  strong {
-    color: var(--color-text-1);
-  }
-}
-
-.highlight-free {
-  color: rgb(var(--green-6));
-}
-
-.info-alert {
-  margin-bottom: 24px;
-}
-
-.table {
-  margin-top: 4px;
-}
-
-@media (max-width: 768px) {
-  .stats-panel {
-    min-height: auto;
-  }
-
-  .stats-highlight {
-    flex-direction: column;
-    align-items: center;
-    text-align: center;
-  }
-
-  .stats-highlight-meta {
-    width: 100%;
-  }
-
-  .stat-card {
-    min-height: 64px;
-  }
-}
-</style>

+ 0 - 328
src/pages/admin/qkReport/index.vue

@@ -1,328 +0,0 @@
-<template>
-  <div class="store-page">
-    <Breadcrumb />
-    <a-card title="抢课回报记录">
-      <AppQueryFilter>
-        <a-row class="query-row">
-          <a-col :flex="'1000px'" class="query-main">
-            <a-form class="queryForm app-query-form" :model="query" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }" label-align="left">
-              <a-row :gutter="16">
-                <a-col :span="12">
-                  <a-form-item field="name" label="姓名">
-                    <a-input v-model="query.name" placeholder="任务备注姓名" allow-clear />
-                  </a-form-item>
-                </a-col>
-                <a-col :span="12">
-                  <a-form-item field="student_num" label="学号">
-                    <a-input v-model="query.student_num" placeholder="学号" allow-clear />
-                  </a-form-item>
-                </a-col>
-                <a-col :span="12">
-                  <a-form-item field="username" label="用户名">
-                    <a-input v-model="query.username" placeholder="平台用户名" allow-clear />
-                  </a-form-item>
-                </a-col>
-                <a-col :span="12">
-                  <a-form-item field="client_label" label="客户端名称">
-                    <a-input v-model="query.client_label" placeholder="客户端备注名" allow-clear />
-                  </a-form-item>
-                </a-col>
-                <a-col :span="12">
-                  <a-form-item field="task_id" label="任务 ID">
-                    <a-input v-model="query.task_id" placeholder="任务 ID" allow-clear />
-                  </a-form-item>
-                </a-col>
-                <a-col :span="12">
-                  <a-form-item field="client_id" label="客户端 ID">
-                    <a-input v-model="query.client_id" placeholder="client_id" allow-clear />
-                  </a-form-item>
-                </a-col>
-                <a-col :span="12">
-                  <a-form-item field="event" label="回报类型">
-                    <a-select v-model="query.event" placeholder="全部" allow-clear>
-                      <a-option value="request_result">单次请求</a-option>
-                      <a-option value="progress_snapshot">定时进度</a-option>
-                      <a-option value="grab_success">抢课成功</a-option>
-                      <a-option value="grab_fail">抢课失败</a-option>
-                    </a-select>
-                  </a-form-item>
-                </a-col>
-                <a-col :span="12">
-                  <a-form-item field="queryTime" label="回报时间">
-                    <a-range-picker v-model="query.queryTime" show-time format="YYYY-MM-DD HH:mm" value-format="x" />
-                  </a-form-item>
-                </a-col>
-              </a-row>
-            </a-form>
-          </a-col>
-          <a-divider class="query-divider" style="height: 120px" direction="vertical" />
-          <a-col :flex="1" class="app-query-actions">
-            <a-space class="query-actions-space" direction="vertical" :size="18">
-              <a-button type="primary" @click="search">
-                <template #icon><icon-search /></template>
-                搜索
-              </a-button>
-              <a-button @click="reset">
-                <template #icon><icon-refresh /></template>
-                重置
-              </a-button>
-            </a-space>
-          </a-col>
-        </a-row>
-      </AppQueryFilter>
-
-      <a-table
-        row-key="id"
-        :data="data"
-        :bordered="false"
-        hoverable
-        class="table"
-        :loading="loading"
-        :scroll="{ x: 1500 }"
-        :pagination="{
-          showPageSize: true,
-          showJumper: true,
-          showTotal: true,
-          pageSize: pagination.pagesize,
-          current: pagination.current,
-          total: pagination.total
-        }"
-        @page-change="onPageChange"
-        @page-size-change="onPageSizeChange"
-      >
-        <template #columns>
-          <a-table-column title="ID" data-index="id" :width="80" />
-          <a-table-column title="任务" :width="240">
-            <template #cell="{ record }">
-              <div>#{{ record.task_id }} {{ record.task_name || '' }}</div>
-              <div class="sub-text">学号:{{ record.student_num || '-' }}</div>
-              <div v-if="record.username" class="sub-text">用户:{{ record.username }}</div>
-            </template>
-          </a-table-column>
-          <a-table-column title="客户端" :width="220">
-            <template #cell="{ record }">
-              <div>{{ record.client_label || record.client_id || '-' }}</div>
-              <div class="sub-text">{{ record.client_id || '-' }}</div>
-            </template>
-          </a-table-column>
-          <a-table-column title="类型" data-index="event" :width="120">
-            <template #cell="{ record }">
-              <a-tag :color="eventColor(record.event)">{{ eventLabel(record.event) }}</a-tag>
-            </template>
-          </a-table-column>
-          <a-table-column title="课程" :width="180">
-            <template #cell="{ record }">
-              <AppTableEllipsis :row-key="`${record.id}-course`" :content="record.course_name || '-'" />
-            </template>
-          </a-table-column>
-          <a-table-column title="回报信息" :width="320">
-            <template #cell="{ record }">
-              <AppTableEllipsis
-                :row-key="`${record.id}-message`"
-                :content="record.display_message || record.message || '-'"
-              />
-            </template>
-          </a-table-column>
-          <a-table-column title="任务状态" data-index="task_status" :width="100">
-            <template #cell="{ record }">
-              {{ taskStatusLabel(record.task_status) }}
-            </template>
-          </a-table-column>
-          <a-table-column title="回报时间" :width="180">
-            <template #cell="{ record }">
-              {{ formatTime(record.create_time) }}
-            </template>
-          </a-table-column>
-          <a-table-column title="详情" :width="90" fixed="right">
-            <template #cell="{ record }">
-              <a-button type="text" size="small" @click="openDetail(record)">查看</a-button>
-            </template>
-          </a-table-column>
-        </template>
-        <template #empty>
-          <a-empty description="暂无回报记录" />
-        </template>
-      </a-table>
-    </a-card>
-
-    <a-modal v-model:visible="detailVisible" title="回报详情" :footer="false" width="720px">
-      <a-descriptions :column="1" bordered size="small">
-        <a-descriptions-item label="任务 ID">{{ detail.task_id }}</a-descriptions-item>
-        <a-descriptions-item label="客户端">{{ detail.client_id }}</a-descriptions-item>
-        <a-descriptions-item label="类型">{{ eventLabel(detail.event) }}</a-descriptions-item>
-        <a-descriptions-item label="课程">{{ detail.course_name || '-' }}</a-descriptions-item>
-        <a-descriptions-item label="信息">{{ detail.display_message || detail.message || '-' }}</a-descriptions-item>
-        <a-descriptions-item label="时间">{{ formatTime(detail.create_time) }}</a-descriptions-item>
-        <a-descriptions-item label="原始数据">
-          <pre class="payload-pre">{{ formatPayload(detail.payload_json) }}</pre>
-        </a-descriptions-item>
-      </a-descriptions>
-    </a-modal>
-  </div>
-</template>
-
-<script setup>
-import { onMounted, reactive, ref } from 'vue'
-import { Notification } from '@arco-design/web-vue'
-import { IconRefresh, IconSearch } from '@arco-design/web-vue/es/icon'
-import Breadcrumb from '@/components/Breadcrumb/index.vue'
-import { adminQkReportList } from '@/api/qk'
-
-const loading = ref(false)
-const data = ref([])
-const detailVisible = ref(false)
-const detail = ref({})
-const pagination = reactive({
-  current: 1,
-  pagesize: 20,
-  total: 0
-})
-const query = reactive({
-  name: '',
-  student_num: '',
-  username: '',
-  client_label: '',
-  task_id: '',
-  client_id: '',
-  event: '',
-  queryTime: []
-})
-
-const eventMap = {
-  request_result: '单次请求',
-  progress_snapshot: '定时进度',
-  grab_success: '抢课成功',
-  grab_fail: '抢课失败'
-}
-
-const taskStatusMap = {
-  paused: '未开始',
-  pending: '待分配',
-  assigned: '已分配',
-  running: '抢课中',
-  success: '成功',
-  failed: '失败',
-  cancelled: '已取消'
-}
-
-function eventLabel(event) {
-  return eventMap[event] || event || '-'
-}
-
-function eventColor(event) {
-  if (event === 'grab_success') return 'green'
-  if (event === 'grab_fail') return 'red'
-  if (event === 'progress_snapshot') return 'orange'
-  return 'blue'
-}
-
-function taskStatusLabel(status) {
-  return taskStatusMap[status] || status || '-'
-}
-
-function formatTime(value) {
-  if (!value) return '-'
-  return new Date(Number(value)).toLocaleString()
-}
-
-function formatPayload(payload) {
-  if (!payload) return '-'
-  try {
-    return JSON.stringify(payload, null, 2)
-  } catch (_) {
-    return String(payload)
-  }
-}
-
-function buildParams() {
-  const params = {
-    current: pagination.current,
-    pagesize: pagination.pagesize
-  }
-  if (query.name) params.name = query.name
-  if (query.student_num) params.student_num = query.student_num
-  if (query.username) params.username = query.username
-  if (query.client_label) params.client_label = query.client_label
-  if (query.task_id) params.task_id = query.task_id
-  if (query.client_id) params.client_id = query.client_id
-  if (query.event) params.event = query.event
-  if (Array.isArray(query.queryTime) && query.queryTime.length === 2) {
-    params.start_time = query.queryTime[0]
-    params.end_time = query.queryTime[1]
-  }
-  return params
-}
-
-function fetchList() {
-  loading.value = true
-  adminQkReportList(buildParams())
-    .then((res) => {
-      if (res?.code === 0) {
-        data.value = res.data?.list || []
-        pagination.total = res.data?.total || 0
-        pagination.current = res.data?.current || pagination.current
-        pagination.pagesize = res.data?.pagesize || pagination.pagesize
-      } else {
-        Notification.error({ title: '获取回报记录失败', content: res?.msg || '请稍后再试' })
-      }
-    })
-    .catch((error) => {
-      Notification.error({ title: '获取回报记录失败', content: error.message || '请稍后再试' })
-    })
-    .finally(() => {
-      loading.value = false
-    })
-}
-
-function search() {
-  pagination.current = 1
-  fetchList()
-}
-
-function reset() {
-  query.name = ''
-  query.student_num = ''
-  query.username = ''
-  query.client_label = ''
-  query.task_id = ''
-  query.client_id = ''
-  query.event = ''
-  query.queryTime = []
-  search()
-}
-
-function onPageChange(page) {
-  pagination.current = page
-  fetchList()
-}
-
-function onPageSizeChange(size) {
-  pagination.pagesize = size
-  pagination.current = 1
-  fetchList()
-}
-
-function openDetail(record) {
-  detail.value = record
-  detailVisible.value = true
-}
-
-onMounted(() => {
-  fetchList()
-})
-</script>
-
-<style scoped>
-.sub-text {
-  color: var(--color-text-3);
-  font-size: 12px;
-}
-
-.payload-pre {
-  margin: 0;
-  white-space: pre-wrap;
-  word-break: break-all;
-  max-height: 320px;
-  overflow: auto;
-}
-</style>

+ 0 - 937
src/pages/admin/qkTask/index.vue

@@ -1,937 +0,0 @@
-<template>
-  <div class="store-page">
-    <Breadcrumb />
-    <a-card title="抢课任务监控">
-      <div class="stats-section">
-      <a-row :gutter="[16, 16]" class="stats-row" align="stretch">
-        <a-col :xs="24" :md="10" :lg="8" class="stats-col">
-          <div class="stats-panel stats-highlight">
-            <a-progress
-              type="circle"
-              :percent="summary.activeRate / 100"
-              :stroke-width="8"
-              :width="isMobile ? 96 : 108"
-              :color="activeRateColor"
-            >
-              <template #text>
-                <div class="progress-text">
-                  <div class="progress-value">{{ summary.activeRate }}%</div>
-                  <div class="progress-label">活跃占比</div>
-                </div>
-              </template>
-            </a-progress>
-            <div class="stats-highlight-meta">
-              <div class="highlight-title">任务执行概况</div>
-              <div class="highlight-desc">
-                抢课中 <strong>{{ summary.grabbing }}</strong> · 待分配 <strong>{{ summary.pending }}</strong>
-              </div>
-              <div class="highlight-desc">
-                任务总数 <strong>{{ summary.total }}</strong> · 未开始 <strong>{{ summary.paused }}</strong>
-              </div>
-            </div>
-          </div>
-        </a-col>
-        <a-col :xs="24" :md="14" :lg="16" class="stats-col">
-          <div class="stats-panel stats-grid-panel">
-            <a-row :gutter="[12, 12]" class="stats-grid">
-            <a-col :xs="12" :sm="8" :md="6">
-              <div class="stat-card">
-                <a-statistic title="任务总数" :value="summary.total" />
-              </div>
-            </a-col>
-            <a-col :xs="12" :sm="8" :md="6">
-              <div class="stat-card">
-                <a-statistic title="抢课中" :value="summary.grabbing" value-style="color: rgb(var(--orange-6))" />
-              </div>
-            </a-col>
-            <a-col :xs="12" :sm="8" :md="6">
-              <div class="stat-card">
-                <a-statistic title="待分配" :value="summary.pending" value-style="color: rgb(var(--arcoblue-6))" />
-              </div>
-            </a-col>
-            <a-col :xs="12" :sm="8" :md="6">
-              <div class="stat-card">
-                <a-statistic title="未开始" :value="summary.paused" />
-              </div>
-            </a-col>
-            <a-col :xs="12" :sm="8" :md="6">
-              <div class="stat-card">
-                <a-statistic title="成功" :value="summary.success" value-style="color: rgb(var(--green-6))" />
-              </div>
-            </a-col>
-            <a-col :xs="12" :sm="8" :md="6">
-              <div class="stat-card">
-                <a-statistic title="失败" :value="summary.failed" value-style="color: rgb(var(--red-6))" />
-              </div>
-            </a-col>
-            <a-col :xs="12" :sm="8" :md="6">
-              <div class="stat-card">
-                <a-statistic title="已取消" :value="summary.cancelled" />
-              </div>
-            </a-col>
-            <a-col :xs="12" :sm="8" :md="6">
-              <div class="stat-card">
-                <a-statistic title="剩余槽位" :value="summary.freeSlots" value-style="color: rgb(var(--green-6))" />
-                <div class="stat-sub">占用 {{ summary.usedSlots }} / {{ summary.totalSlots }}</div>
-              </div>
-            </a-col>
-            </a-row>
-          </div>
-        </a-col>
-      </a-row>
-      </div>
-
-      <AppQueryFilter class="query-filter">
-        <a-row class="query-row">
-          <a-col :flex="'1000px'" class="query-main">
-            <a-form class="queryForm app-query-form" :model="query" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }" label-align="left">
-              <a-row :gutter="16">
-                <a-col :span="12">
-                  <a-form-item field="status" label="状态">
-                    <a-select v-model="query.status" placeholder="全部" allow-clear>
-                      <a-option value="paused">未开始</a-option>
-                      <a-option value="pending">待分配</a-option>
-                      <a-option value="assigned">已分配</a-option>
-                      <a-option value="running">抢课中</a-option>
-                      <a-option value="success">成功</a-option>
-                      <a-option value="failed">失败</a-option>
-                      <a-option value="cancelled">已取消</a-option>
-                    </a-select>
-                  </a-form-item>
-                </a-col>
-                <a-col :span="12">
-                  <a-form-item field="client_id" label="客户端">
-                    <a-input v-model="query.client_id" placeholder="client_id" allow-clear />
-                  </a-form-item>
-                </a-col>
-                <a-col :span="12">
-                  <a-form-item field="student_num" label="学号">
-                    <a-input v-model="query.student_num" placeholder="学号" allow-clear />
-                  </a-form-item>
-                </a-col>
-                <a-col :span="12">
-                  <a-form-item field="username" label="用户名">
-                    <a-input v-model="query.username" placeholder="平台用户名" allow-clear />
-                  </a-form-item>
-                </a-col>
-                <a-col :span="12">
-                  <a-form-item field="course_name" label="课程名称">
-                    <a-input v-model="query.course_name" placeholder="模糊匹配课程或分组" allow-clear />
-                  </a-form-item>
-                </a-col>
-              </a-row>
-            </a-form>
-          </a-col>
-          <a-divider class="query-divider" style="height: 120px" direction="vertical" />
-          <a-col :flex="1" class="app-query-actions">
-            <a-space class="query-actions-space" direction="vertical" :size="18">
-              <a-button type="primary" @click="search">
-                <template #icon><icon-search /></template>
-                搜索
-              </a-button>
-              <a-button @click="reset">
-                <template #icon><icon-refresh /></template>
-                重置
-              </a-button>
-            </a-space>
-          </a-col>
-        </a-row>
-      </AppQueryFilter>
-
-      <a-table
-        row-key="id"
-        :data="data"
-        :bordered="false"
-        hoverable
-        class="table"
-        :loading="loading"
-        :scroll="{ x: isMobile ? 1200 : 1880 }"
-        :pagination="{
-          showPageSize: true,
-          showJumper: true,
-          showTotal: true,
-          pageSize: pagination.pagesize,
-          current: pagination.current,
-          total: pagination.total
-        }"
-        @page-change="onPageChange"
-        @page-size-change="onPageSizeChange"
-      >
-        <template #columns>
-          <a-table-column title="ID" data-index="id" :width="80" />
-          <a-table-column title="创建用户" :width="180">
-            <template #cell="{ record }">
-              <a-space>
-                <a-avatar :size="26">
-                  <img :src="record.avatar || defaultAvatar" alt="" />
-                </a-avatar>
-                {{ record.username || record.create_user || '-' }}
-              </a-space>
-            </template>
-          </a-table-column>
-          <a-table-column title="学号" data-index="student_num" :width="130" />
-          <a-table-column title="客户端" data-index="assigned_client_id" :width="180" ellipsis tooltip />
-          <a-table-column title="状态" :width="100">
-            <template #cell="{ record }">
-              <a-tag :color="statusColor(record.status)">{{ statusText(record.status) }}</a-tag>
-            </template>
-          </a-table-column>
-          <a-table-column title="课程" :width="260">
-            <template #cell="{ record }">
-              <AppTableEllipsis :row-key="`${record.id}-course`" :content="formatCourseList(record)" />
-            </template>
-          </a-table-column>
-          <a-table-column title="错误" :width="220">
-            <template #cell="{ record }">
-              <AppTableEllipsis :row-key="`${record.id}-error`" :content="record.error_msg || '-'" />
-            </template>
-          </a-table-column>
-          <a-table-column title="备注" :width="140" ellipsis tooltip>
-            <template #cell="{ record }">{{ record.name || '-' }}</template>
-          </a-table-column>
-          <a-table-column title="创建时间" :width="180">
-            <template #cell="{ record }">{{ formatTime(record.create_time) }}</template>
-          </a-table-column>
-          <a-table-column title="更新时间" :width="180">
-            <template #cell="{ record }">{{ formatTime(record.update_time) }}</template>
-          </a-table-column>
-          <a-table-column v-if="canManage()" title="操作" :width="300" :fixed="tableFixed('right')">
-            <template #cell="{ record }">
-              <a-space wrap>
-                <a-button
-                  v-if="canStart(record)"
-                  :key="`${record.id}-start`"
-                  type="text"
-                  size="small"
-                  status="success"
-                  @click="startTask(record)"
-                >
-                  开启
-                </a-button>
-                <a-button
-                  v-if="canPause(record)"
-                  :key="`${record.id}-pause`"
-                  type="text"
-                  size="small"
-                  status="warning"
-                  @click="pauseTask(record)"
-                >
-                  暂停
-                </a-button>
-                <a-button
-                  v-if="canEdit(record)"
-                  :key="`${record.id}-edit`"
-                  type="text"
-                  size="small"
-                  @click="openModal(record)"
-                >
-                  编辑
-                </a-button>
-                <a-popconfirm
-                  v-if="canRetry(record)"
-                  :key="`${record.id}-retry`"
-                  content="确认重试该任务?将重新进入待分配队列。"
-                  @ok="retryTask(record)"
-                >
-                  <a-button type="text" size="small">重试</a-button>
-                </a-popconfirm>
-                <a-popconfirm
-                  v-if="canCancel(record)"
-                  :key="`${record.id}-cancel`"
-                  :content="cancelConfirmText(record)"
-                  @ok="cancelTask(record)"
-                >
-                  <a-button type="text" status="danger" size="small">取消</a-button>
-                </a-popconfirm>
-              </a-space>
-            </template>
-          </a-table-column>
-        </template>
-        <template #empty>
-          <a-empty description="暂无抢课任务" />
-        </template>
-      </a-table>
-    </a-card>
-
-    <a-modal
-      v-model:visible="visible"
-      title="编辑抢课任务"
-      :width="isMobile ? '100%' : '720px'"
-      draggable
-      esc-to-close
-      :ok-loading="saving"
-      @ok="submitTask"
-    >
-      <a-alert v-if="editingAssigned" type="warning" class="edit-alert">
-        该任务正在客户端执行,保存后将收回并变为未开始状态。
-      </a-alert>
-      <a-form :model="form" layout="vertical" class="task-form">
-        <a-form-item label="备注">
-          <a-textarea
-            v-model="form.name"
-            no-trim
-            placeholder="添加对任务的备注(非必填)"
-            :max-length="{ length: 50 }"
-            allow-clear
-            show-word-limit
-            :auto-size="{ minRows: 2, maxRows: 4 }"
-          />
-        </a-form-item>
-        <a-form-item label="选课批次" required>
-          <a-select v-model="form.batch_id" placeholder="请选择抢课批次" allow-clear allow-search>
-            <a-option v-for="item in batchOptions" :key="item.id" :value="item.id">
-              {{ item.name }}({{ item.jx0502zbid }}){{ item.enabled ? '' : ' [已停用]' }}
-            </a-option>
-          </a-select>
-        </a-form-item>
-        <a-form-item label="统一身份认证账号" required>
-          <a-select
-            v-model="form.jw_account_id"
-            placeholder="请选择该用户已验证的统一身份认证账号"
-            allow-search
-            allow-clear
-            :loading="jwAccountLoading"
-          >
-            <a-option v-for="item in jwAccounts" :key="item.id" :value="item.id">
-              {{ item.username }}({{ item.realname || '未知' }})
-            </a-option>
-          </a-select>
-        </a-form-item>
-        <a-form-item label="课程名称(每行一个,精确匹配)">
-          <a-textarea v-model="form.coursesText" :auto-size="{ minRows: 3 }" placeholder="每行填写一门课程名称" />
-          <template #extra>{{ targetHint }}</template>
-        </a-form-item>
-        <a-form-item label="课程分组(体育课请在此填写项目名称)">
-          <a-textarea v-model="form.courseGroupsText" :auto-size="{ minRows: 2 }" placeholder="每行填写一个课程分组" />
-        </a-form-item>
-        <a-row :gutter="16">
-          <a-col :xs="24" :sm="12">
-            <a-form-item label="抢课间隔(ms)">
-              <a-input-number v-model="form.interval_ms" :min="minInterval" :max="10000" style="width: 100%" />
-              <template #extra>当前最低 {{ minInterval }}ms</template>
-            </a-form-item>
-          </a-col>
-          <a-col :xs="24" :sm="12">
-            <a-form-item label="启用公选课接口">
-              <a-switch v-model="form.enable_ggxxk" />
-            </a-form-item>
-          </a-col>
-          <a-col :xs="24" :sm="12">
-            <a-form-item label="登录失效后自动重新登录">
-              <a-switch v-model="form.auto_relogin" />
-              <template #extra>开启后抢课过程中会话失效将自动重新登录并继续</template>
-            </a-form-item>
-          </a-col>
-        </a-row>
-      </a-form>
-    </a-modal>
-  </div>
-</template>
-
-<script setup>
-import { reactive, ref, onMounted, onUnmounted, computed } from 'vue'
-import { Notification } from '@arco-design/web-vue'
-import {
-  adminQkClientList,
-  adminQkTaskList,
-  adminQkTaskDetail,
-  adminQkTaskUpdate,
-  adminQkTaskCancel,
-  adminQkTaskRetry,
-  adminQkTaskStart,
-  adminQkTaskPause,
-  adminQkBatchList,
-  adminGetJwAccount
-} from '@/api/qk'
-import { useResponsiveTable } from '@/hooks/useResponsiveTable'
-import { hasPermission } from '@/utils/permission'
-import {
-  getMinQkInterval,
-  getQkTargetHint,
-  splitQkLines,
-  validateQkTaskTargets
-} from '@/utils/qkTaskRules'
-
-const { tableFixed, isMobile } = useResponsiveTable()
-const canManage = () => hasPermission('action.qk.admin.taskManage')
-const defaultAvatar = 'https://lepao-cloud.xxoo365.top/view.php/25aa126dc406974ff3579a99a2c6501a.png'
-
-const loading = ref(false)
-const saving = ref(false)
-const visible = ref(false)
-const editingAssigned = ref(false)
-const data = ref([])
-const batchOptions = ref([])
-const jwAccounts = ref([])
-const jwAccountLoading = ref(false)
-const query = reactive({
-  status: '',
-  client_id: '',
-  student_num: '',
-  username: '',
-  course_name: ''
-})
-const pagination = reactive({
-  total: 0,
-  current: 1,
-  pagesize: 20
-})
-const statusStats = reactive({
-  total: 0,
-  paused: 0,
-  pending: 0,
-  assigned: 0,
-  running: 0,
-  success: 0,
-  failed: 0,
-  cancelled: 0,
-  usedSlots: 0,
-  totalSlots: 0,
-  freeSlots: 0
-})
-
-const summary = computed(() => {
-  const grabbing = statusStats.running + statusStats.assigned
-  const active = grabbing + statusStats.pending
-  const total = statusStats.total || 0
-  const activeRate = total > 0 ? Math.min(100, Math.round((active / total) * 100)) : 0
-  return {
-    total,
-    paused: statusStats.paused,
-    pending: statusStats.pending,
-    grabbing,
-    success: statusStats.success,
-    failed: statusStats.failed,
-    cancelled: statusStats.cancelled,
-    activeRate,
-    usedSlots: statusStats.usedSlots,
-    totalSlots: statusStats.totalSlots,
-    freeSlots: statusStats.freeSlots
-  }
-})
-
-const activeRateColor = computed(() => {
-  const rate = summary.value.activeRate
-  if (rate >= 80) return '#F53F3F'
-  if (rate >= 50) return '#FF7D00'
-  return '#165DFF'
-})
-
-const form = reactive({
-  id: '',
-  name: '',
-  batch_id: '',
-  jw_account_id: undefined,
-  create_user: '',
-  coursesText: '',
-  courseGroupsText: '',
-  enable_ggxxk: true,
-  auto_relogin: false,
-  interval_ms: 500
-})
-
-const targetCount = computed(() => {
-  return splitQkLines(form.coursesText).length + splitQkLines(form.courseGroupsText).length
-})
-const minInterval = computed(() => getMinQkInterval(targetCount.value))
-const targetHint = computed(() => getQkTargetHint(form.coursesText, form.courseGroupsText))
-
-const splitLines = splitQkLines
-
-const formatCourseList = (record) => {
-  const list = [...(record.courses || []), ...(record.course_groups || [])]
-  return list.length ? list.join('、') : '-'
-}
-
-const formatTime = (time) => {
-  if (!time) return '-'
-  return new Date(Number(time)).toLocaleString('zh-CN', {
-    year: 'numeric',
-    month: '2-digit',
-    day: '2-digit',
-    hour: '2-digit',
-    minute: '2-digit'
-  })
-}
-
-const statusText = (status) => ({
-  paused: '未开始',
-  pending: '待分配',
-  assigned: '已分配',
-  running: '抢课中',
-  success: '成功',
-  failed: '失败',
-  cancelled: '已取消'
-}[status] || status)
-
-const statusColor = (status) => ({
-  paused: 'gray',
-  pending: 'blue',
-  assigned: 'arcoblue',
-  running: 'orange',
-  success: 'green',
-  failed: 'red',
-  cancelled: 'gray'
-}[status] || 'gray')
-
-const canEdit = (record) => record.status !== 'success'
-const canStart = (record) => ['paused', 'failed'].includes(record.status)
-const canPause = (record) => ['pending', 'assigned', 'running'].includes(record.status)
-const canRetry = (record) => ['failed', 'cancelled'].includes(record.status)
-const canCancel = (record) => ['paused', 'pending', 'assigned', 'running', 'failed'].includes(record.status)
-
-const cancelConfirmText = (record) => {
-  if (['assigned', 'running'].includes(record.status)) {
-    return '该任务正在执行,确认取消并收回?'
-  }
-  return '确认取消该抢课任务?'
-}
-
-const resetForm = () => {
-  Object.assign(form, {
-    id: '',
-    name: '',
-    batch_id: '',
-    jw_account_id: undefined,
-    create_user: '',
-    coursesText: '',
-    courseGroupsText: '',
-    enable_ggxxk: true,
-    auto_relogin: false,
-    interval_ms: 500
-  })
-  jwAccounts.value = []
-  editingAssigned.value = false
-}
-
-const loadJwAccounts = async (createUser) => {
-  if (!createUser) {
-    jwAccounts.value = []
-    return
-  }
-  jwAccountLoading.value = true
-  try {
-    const res = await adminGetJwAccount({ create_user: createUser })
-    if (res?.code === 0) {
-      jwAccounts.value = res.data || []
-    }
-  } finally {
-    jwAccountLoading.value = false
-  }
-}
-
-const openModal = async (record) => {
-  resetForm()
-  try {
-    const res = await adminQkTaskDetail({ id: record.id })
-    if (!res || res.code !== 0) {
-      return Notification.error({ title: '获取任务详情失败', content: res?.msg || '请稍后再试' })
-    }
-    const task = res.data || record
-    editingAssigned.value = ['assigned', 'running'].includes(task.status)
-    await loadJwAccounts(task.create_user)
-    Object.assign(form, {
-      id: task.id,
-      name: task.name,
-      batch_id: task.batch_id || '',
-      jw_account_id: task.jw_account_id || jwAccounts.value.find(item => item.username === task.student_num)?.id,
-      create_user: task.create_user,
-      coursesText: (task.courses || []).join('\n'),
-      courseGroupsText: (task.course_groups || []).join('\n'),
-      enable_ggxxk: task.enable_ggxxk,
-      auto_relogin: !!task.auto_relogin,
-      interval_ms: task.interval_ms
-    })
-    visible.value = true
-  } catch (error) {
-    Notification.error({ title: '获取任务详情失败', content: error.message || '请稍后再试' })
-  }
-}
-
-const getList = async (silent = false) => {
-  try {
-    if (!silent) loading.value = true
-    const res = await adminQkTaskList({
-      ...query,
-      current: pagination.current,
-      pagesize: pagination.pagesize
-    })
-    if (!res || res.code !== 0) {
-      if (!silent) {
-        Notification.error({ title: '获取抢课任务失败', content: res?.msg || '请稍后再试' })
-      }
-      return
-    }
-    data.value = res.data?.list || []
-    pagination.total = res.data?.total || 0
-  } catch (error) {
-    if (!silent) {
-      Notification.error({ title: '获取抢课任务失败', content: error.message || '请稍后再试' })
-    }
-  } finally {
-    if (!silent) loading.value = false
-  }
-}
-
-const fetchStats = async () => {
-  try {
-    const [
-      allRes,
-      pausedRes,
-      pendingRes,
-      assignedRes,
-      runningRes,
-      successRes,
-      failedRes,
-      cancelledRes,
-      clientRes
-    ] = await Promise.all([
-      adminQkTaskList({ current: 1, pagesize: 1 }),
-      adminQkTaskList({ current: 1, pagesize: 1, status: 'paused' }),
-      adminQkTaskList({ current: 1, pagesize: 1, status: 'pending' }),
-      adminQkTaskList({ current: 1, pagesize: 1, status: 'assigned' }),
-      adminQkTaskList({ current: 1, pagesize: 1, status: 'running' }),
-      adminQkTaskList({ current: 1, pagesize: 1, status: 'success' }),
-      adminQkTaskList({ current: 1, pagesize: 1, status: 'failed' }),
-      adminQkTaskList({ current: 1, pagesize: 1, status: 'cancelled' }),
-      adminQkClientList()
-    ])
-    if (allRes?.code === 0) statusStats.total = allRes.data?.total || 0
-    if (pausedRes?.code === 0) statusStats.paused = pausedRes.data?.total || 0
-    if (pendingRes?.code === 0) statusStats.pending = pendingRes.data?.total || 0
-    if (assignedRes?.code === 0) statusStats.assigned = assignedRes.data?.total || 0
-    if (runningRes?.code === 0) statusStats.running = runningRes.data?.total || 0
-    if (successRes?.code === 0) statusStats.success = successRes.data?.total || 0
-    if (failedRes?.code === 0) statusStats.failed = failedRes.data?.total || 0
-    if (cancelledRes?.code === 0) statusStats.cancelled = cancelledRes.data?.total || 0
-
-    const clients = clientRes?.code === 0 ? (clientRes.data || []) : []
-    const online = clients.filter(item => Number(item.enabled) === 1 && Number(item.online) === 1)
-    const totalSlots = online.reduce((sum, item) => sum + Math.max(0, Number(item.max_slots) || 0), 0)
-    const usedSlots = online.reduce((sum, item) => sum + Math.max(0, Number(item.current_slots) || 0), 0)
-    statusStats.totalSlots = totalSlots
-    statusStats.usedSlots = usedSlots
-    statusStats.freeSlots = Math.max(0, totalSlots - usedSlots)
-  } catch (_) {}
-}
-
-const submitTask = async () => {
-  const validated = validateQkTaskTargets(form.coursesText, form.courseGroupsText, form.interval_ms)
-  if (!validated.ok) {
-    return Notification.error({ title: '保存失败', content: validated.message })
-  }
-  if (!form.jw_account_id) {
-    return Notification.error({ title: '保存失败', content: '请选择统一身份认证账号' })
-  }
-  try {
-    saving.value = true
-    const res = await adminQkTaskUpdate({
-      id: form.id,
-      name: form.name,
-      batch_id: form.batch_id,
-      jw_account_id: form.jw_account_id,
-      courses: validated.courses,
-      course_groups: validated.courseGroups,
-      enable_ggxxk: form.enable_ggxxk,
-      auto_relogin: form.auto_relogin,
-      interval_ms: form.interval_ms
-    })
-    if (!res || res.code !== 0) {
-      return Notification.error({ title: '保存失败', content: res?.msg || '请稍后再试' })
-    }
-    Notification.success({ title: '保存成功' })
-    visible.value = false
-    getList()
-    fetchStats()
-  } catch (error) {
-    Notification.error({ title: '保存失败', content: error.message || '请稍后再试' })
-  } finally {
-    saving.value = false
-  }
-}
-
-const cancelTask = async (record) => {
-  const res = await adminQkTaskCancel({ id: record.id })
-  if (!res || res.code !== 0) {
-    return Notification.error({ title: '取消失败', content: res?.msg || '请稍后再试' })
-  }
-  Notification.success({ title: '已取消' })
-  getList()
-  fetchStats()
-}
-
-const startTask = async (record) => {
-  const res = await adminQkTaskStart({ id: record.id })
-  if (!res || res.code !== 0) {
-    return Notification.error({ title: '开启失败', content: res?.msg || '请稍后再试' })
-  }
-  Notification.success({ title: '已开启', content: '任务已进入待分配队列' })
-  getList()
-  fetchStats()
-}
-
-const pauseTask = async (record) => {
-  const res = await adminQkTaskPause({ id: record.id })
-  if (!res || res.code !== 0) {
-    return Notification.error({ title: '暂停失败', content: res?.msg || '请稍后再试' })
-  }
-  Notification.success({ title: '已暂停' })
-  getList()
-  fetchStats()
-}
-
-const retryTask = async (record) => {
-  const res = await adminQkTaskRetry({ id: record.id })
-  if (!res || res.code !== 0) {
-    return Notification.error({ title: '重试失败', content: res?.msg || '请稍后再试' })
-  }
-  Notification.success({ title: '已重新加入待分配队列' })
-  getList()
-  fetchStats()
-}
-
-const search = () => {
-  pagination.current = 1
-  getList()
-}
-
-const reset = () => {
-  query.status = ''
-  query.client_id = ''
-  query.student_num = ''
-  query.username = ''
-  query.course_name = ''
-  search()
-}
-
-const onPageChange = (current) => {
-  pagination.current = current
-  getList()
-}
-
-const onPageSizeChange = (pageSize) => {
-  pagination.pagesize = pageSize
-  pagination.current = 1
-  getList()
-}
-
-let pollTimer = null
-let statsTimer = null
-
-const startPolling = () => {
-  if (!pollTimer) {
-    pollTimer = setInterval(() => getList(true), 5000)
-  }
-  if (!statsTimer) {
-    statsTimer = setInterval(fetchStats, 30000)
-  }
-}
-
-const stopPolling = () => {
-  if (pollTimer) {
-    clearInterval(pollTimer)
-    pollTimer = null
-  }
-  if (statsTimer) {
-    clearInterval(statsTimer)
-    statsTimer = null
-  }
-}
-
-const loadBatches = async () => {
-  try {
-    const res = await adminQkBatchList()
-    if (res?.code === 0) {
-      batchOptions.value = res.data || []
-    }
-  } catch (_) {}
-}
-
-onMounted(() => {
-  loadBatches()
-  getList()
-  fetchStats()
-  startPolling()
-})
-
-onUnmounted(() => {
-  stopPolling()
-})
-</script>
-
-<style scoped lang="less">
-@import '@/styles/store-theme.less';
-
-.stats-section {
-  padding-bottom: 40px;
-}
-
-.stats-row {
-  margin-bottom: 0;
-}
-
-:deep(.query-filter.app-query-filter) {
-  margin-top: 0;
-  margin-bottom: 24px;
-}
-
-.stats-col {
-  display: flex;
-}
-
-.stats-panel {
-  width: 100%;
-  min-height: 148px;
-  box-sizing: border-box;
-}
-
-.stats-highlight {
-  display: flex;
-  align-items: center;
-  gap: 16px;
-  height: 100%;
-  padding: 16px;
-  border: 1px solid var(--color-border-2);
-  border-radius: 12px;
-  background: linear-gradient(135deg, rgba(var(--primary-1), 0.35), rgba(var(--primary-1), 0.08));
-}
-
-.stats-grid-panel {
-  display: flex;
-  flex-direction: column;
-  height: 100%;
-  padding: 12px;
-  border: 1px solid var(--color-border-2);
-  border-radius: 12px;
-  background: var(--color-bg-1);
-}
-
-.stats-grid {
-  flex: 1;
-  align-content: stretch;
-}
-
-.progress-text {
-  text-align: center;
-}
-
-.progress-value {
-  font-size: 18px;
-  font-weight: 600;
-  line-height: 1.2;
-}
-
-.progress-label {
-  margin-top: 2px;
-  font-size: 12px;
-  color: var(--color-text-3);
-}
-
-.stats-highlight-meta {
-  flex: 1;
-  min-width: 0;
-}
-
-.highlight-title {
-  font-size: 15px;
-  font-weight: 600;
-  margin-bottom: 8px;
-}
-
-.highlight-desc {
-  color: var(--color-text-2);
-  font-size: 13px;
-  line-height: 1.6;
-
-  strong {
-    color: var(--color-text-1);
-  }
-}
-
-.stat-card {
-  height: 100%;
-  min-height: 72px;
-  padding: 12px;
-  border: 1px solid var(--color-border-2);
-  border-radius: 10px;
-  background: var(--color-bg-2);
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
-  box-sizing: border-box;
-}
-
-:deep(.stat-card .arco-statistic-title) {
-  margin-bottom: 4px;
-  font-size: 13px;
-}
-
-:deep(.stat-card .arco-statistic-value) {
-  font-size: 22px;
-  line-height: 1.2;
-}
-
-.stat-sub {
-  margin-top: 4px;
-  font-size: 12px;
-  color: var(--color-text-3);
-}
-
-.query-row {
-  margin-bottom: 0;
-}
-
-.query-main {
-  min-width: 0;
-}
-
-.query-actions-space {
-  width: 100%;
-}
-
-.table {
-  margin-top: 4px;
-}
-
-.edit-alert {
-  margin-bottom: 16px;
-}
-
-@media (max-width: 768px) {
-  .stats-panel {
-    min-height: auto;
-  }
-
-  .stats-highlight {
-    flex-direction: column;
-    align-items: center;
-    text-align: center;
-  }
-
-  .stats-highlight-meta {
-    width: 100%;
-  }
-
-  .stat-card {
-    min-height: 64px;
-  }
-
-  .query-row {
-    display: block;
-  }
-
-  .query-divider {
-    display: none;
-  }
-
-  .query-actions-space {
-    margin-top: 4px;
-    flex-direction: row !important;
-    gap: 10px !important;
-  }
-
-  .query-actions-space :deep(.arco-btn) {
-    flex: 1;
-  }
-}
-</style>

+ 0 - 312
src/pages/admin/qxsAccount/index.vue

@@ -1,312 +0,0 @@
-<template>
-  <div class="store-page">
-    <Breadcrumb />
-    <a-card title="趣选书查询管理">
-      <AppQueryFilter>
-        <a-row class="query-row">
-          <a-col :flex="'1000px'" class="query-main">
-            <a-form class="queryForm app-query-form" :model="query" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }" label-align="left">
-              <a-row :gutter="16">
-                <a-col :span="12">
-                  <a-form-item field="username" label="趣选书账号">
-                    <a-input v-model="query.username" placeholder="登录账号" allow-clear />
-                  </a-form-item>
-                </a-col>
-                <a-col :span="12">
-                  <a-form-item field="realname" label="姓名">
-                    <a-input v-model="query.realname" placeholder="真实姓名" allow-clear />
-                  </a-form-item>
-                </a-col>
-                <a-col :span="12">
-                  <a-form-item field="email" label="邮箱">
-                    <a-input v-model="query.email" placeholder="邮箱" allow-clear />
-                  </a-form-item>
-                </a-col>
-                <a-col :span="12">
-                  <a-form-item field="mobile" label="手机号">
-                    <a-input v-model="query.mobile" placeholder="手机号" allow-clear />
-                  </a-form-item>
-                </a-col>
-                <a-col :span="12">
-                  <a-form-item field="className" label="班级">
-                    <a-input v-model="query.className" placeholder="班级名称" allow-clear />
-                  </a-form-item>
-                </a-col>
-                <a-col :span="12">
-                  <a-form-item field="termName" label="学期">
-                    <a-input v-model="query.termName" placeholder="学期名称" allow-clear />
-                  </a-form-item>
-                </a-col>
-                <a-col :span="12">
-                  <a-form-item field="queryTime" label="查询时间">
-                    <a-range-picker v-model="query.queryTime" show-time format="YYYY-MM-DD HH:mm" value-format="x" />
-                  </a-form-item>
-                </a-col>
-              </a-row>
-            </a-form>
-          </a-col>
-          <a-divider class="query-divider" style="height: 84px" direction="vertical" />
-          <a-col :flex="1" class="app-query-actions">
-            <a-space class="query-actions-space" direction="vertical" :size="18">
-              <a-button type="primary" @click="search">搜索</a-button>
-              <a-button @click="reset">重置</a-button>
-            </a-space>
-          </a-col>
-        </a-row>
-      </AppQueryFilter>
-
-      <a-table
-        :bordered="false"
-        class="table"
-        :data="data"
-        :loading="loading"
-        :scroll="{ x: 1320 }"
-        :pagination="{
-          showPageSize: true,
-          showJumper: true,
-          showTotal: true,
-          pageSize: pagination.pagesize,
-          current: pagination.current,
-          total: pagination.total
-        }"
-        @page-change="onPageChange"
-        @page-size-change="onPageSizeChange"
-      >
-        <template #columns>
-          <a-table-column title="ID" data-index="id" :width="80" />
-          <a-table-column title="趣选书账号" data-index="username" :width="160" ellipsis tooltip />
-          <a-table-column title="姓名" data-index="realname" :width="120" ellipsis tooltip />
-          <a-table-column title="班级" data-index="className" :width="160" ellipsis tooltip />
-          <a-table-column title="邮箱" data-index="email" :width="180" ellipsis tooltip />
-          <a-table-column title="手机号" data-index="mobile" :width="130" ellipsis tooltip />
-          <a-table-column title="学期" data-index="termName" :width="180" ellipsis tooltip />
-          <a-table-column title="书籍数量" data-index="book_count" :width="100" />
-          <a-table-column title="查询时间" :width="170">
-            <template #cell="{ record }">{{ formatTime(record.create_time) }}</template>
-          </a-table-column>
-          <a-table-column title="操作" :width="100" :fixed="tableFixed('right')">
-            <template #cell="{ record }">
-              <a-button type="text" size="small" @click="openDetail(record)">查看书单</a-button>
-            </template>
-          </a-table-column>
-        </template>
-      </a-table>
-    </a-card>
-
-    <a-modal
-      v-model:visible="detailVisible"
-      :title="detailTitle"
-      width="960px"
-      hide-cancel
-      esc-to-close
-      draggable
-    >
-      <a-descriptions v-if="detail" :column="2" bordered size="medium" style="margin-bottom: 16px">
-        <a-descriptions-item label="账号">{{ detail.username }}</a-descriptions-item>
-        <a-descriptions-item label="姓名">{{ detail.realname || '-' }}</a-descriptions-item>
-        <a-descriptions-item label="班级">{{ detail.className || '-' }}</a-descriptions-item>
-        <a-descriptions-item label="邮箱">{{ detail.email || '-' }}</a-descriptions-item>
-        <a-descriptions-item label="手机号">{{ detail.mobile || '-' }}</a-descriptions-item>
-        <a-descriptions-item label="学期" :span="2">{{ detail.termName || '-' }}</a-descriptions-item>
-        <a-descriptions-item label="查询时间" :span="2">{{ formatTime(detail.create_time) }}</a-descriptions-item>
-      </a-descriptions>
-
-      <a-table
-        :bordered="false"
-        :data="detail?.bookList || []"
-        :loading="detailLoading"
-        :pagination="false"
-        :scroll="{ x: 900 }"
-        stripe
-      >
-        <template #columns>
-          <a-table-column title="封面" :width="80">
-            <template #cell="{ record }">
-              <a-image v-if="record.imgUrl" width="50" height="50" fit="contain" :src="record.imgUrl" />
-              <a-avatar v-else shape="square" :size="50">
-                <icon-book />
-              </a-avatar>
-            </template>
-          </a-table-column>
-          <a-table-column title="书名" data-index="title" :width="220" ellipsis tooltip />
-          <a-table-column title="ISBN" data-index="isbn" :width="150" ellipsis tooltip />
-          <a-table-column title="课程" :width="180" ellipsis tooltip>
-            <template #cell="{ record }">
-              {{ record.crouseName || record.courseName || '-' }}
-              <span v-if="record.courseType">({{ record.courseType }})</span>
-            </template>
-          </a-table-column>
-          <a-table-column title="价格" :width="120">
-            <template #cell="{ record }">
-              ¥{{ record.textbookDiscountedPrice ?? record.totalDiscountedPrice ?? record.textbookPrice ?? '-' }}
-            </template>
-          </a-table-column>
-        </template>
-      </a-table>
-    </a-modal>
-  </div>
-</template>
-
-<script setup>
-import { reactive, ref, computed, onMounted } from 'vue'
-import { Notification } from '@arco-design/web-vue'
-import { adminQxsAccountList, adminQxsAccountDetail } from '@/api/qxs'
-import { useResponsiveTable } from '@/hooks/useResponsiveTable'
-
-const { tableFixed } = useResponsiveTable()
-const loading = ref(false)
-const data = ref([])
-const query = reactive({
-  username: '',
-  realname: '',
-  email: '',
-  mobile: '',
-  className: '',
-  termName: '',
-  queryTime: []
-})
-const pagination = reactive({
-  total: 0,
-  current: 1,
-  pagesize: 20
-})
-
-const detailVisible = ref(false)
-const detailLoading = ref(false)
-const detail = ref(null)
-
-const detailTitle = computed(() => {
-  if (!detail.value) return '书单详情'
-  return `书单详情 · ${detail.value.realname || detail.value.username || ''}`
-})
-
-const formatTime = (time) => {
-  if (!time) return '-'
-  return new Date(Number(time)).toLocaleString('zh-CN', {
-    year: 'numeric',
-    month: '2-digit',
-    day: '2-digit',
-    hour: '2-digit',
-    minute: '2-digit'
-  })
-}
-
-const getList = async () => {
-  try {
-    loading.value = true
-    const res = await adminQxsAccountList({
-      ...query,
-      current: pagination.current,
-      pagesize: pagination.pagesize
-    })
-    if (!res || res.code !== 0) {
-      return Notification.error({
-        title: '获取查询记录失败',
-        content: res?.msg ?? '请稍后再试'
-      })
-    }
-    data.value = res.data || []
-    pagination.total = res.pagination?.total || 0
-  } catch (error) {
-    Notification.error({
-      title: '获取查询记录失败',
-      content: error.message || '请稍后再试'
-    })
-  } finally {
-    loading.value = false
-  }
-}
-
-const openDetail = async (record) => {
-  try {
-    detailLoading.value = true
-    detailVisible.value = true
-    detail.value = null
-    const res = await adminQxsAccountDetail({ id: record.id })
-    if (!res || res.code !== 0) {
-      return Notification.error({
-        title: '获取书单详情失败',
-        content: res?.msg ?? '请稍后再试'
-      })
-    }
-    detail.value = res.data
-  } catch (error) {
-    Notification.error({
-      title: '获取书单详情失败',
-      content: error.message || '请稍后再试'
-    })
-  } finally {
-    detailLoading.value = false
-  }
-}
-
-const search = () => {
-  pagination.current = 1
-  getList()
-}
-
-const reset = () => {
-  query.username = ''
-  query.realname = ''
-  query.email = ''
-  query.mobile = ''
-  query.className = ''
-  query.termName = ''
-  query.queryTime = []
-  pagination.current = 1
-  getList()
-}
-
-const onPageChange = (page) => {
-  pagination.current = page
-  getList()
-}
-
-const onPageSizeChange = (size) => {
-  pagination.pagesize = size
-  pagination.current = 1
-  getList()
-}
-
-onMounted(() => {
-  getList()
-})
-</script>
-
-<style scoped>
-.query-row {
-  margin-bottom: 10px;
-}
-
-.query-main {
-  min-width: 0;
-}
-
-.query-actions-space {
-  width: 100%;
-}
-
-.table {
-  margin-top: 16px;
-}
-
-@media (max-width: 768px) {
-  .query-row {
-    display: block;
-  }
-
-  .query-divider {
-    display: none;
-  }
-
-  .query-actions-space {
-    margin-top: 4px;
-    flex-direction: row !important;
-    gap: 10px !important;
-  }
-
-  .query-actions-space :deep(.arco-btn) {
-    flex: 1;
-  }
-}
-</style>

+ 6 - 6
src/pages/admin/reqLog/index.vue

@@ -4,9 +4,9 @@
 
         <a-card title="请求日志">
             <AppQueryFilter>
-            <a-row class="query-row app-query-form">
-                <a-col :flex="1">
-                    <a-form :model="queryData" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }"
+            <a-row class="query-row">
+                <a-col :flex="'1000px'" class="query-main">
+                    <a-form class="queryForm app-query-form" :model="queryData" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }"
                         label-align="left">
                         <a-row :gutter="16">
                             <a-col :span="8">
@@ -39,9 +39,9 @@
                         </a-row>
                     </a-form>
                 </a-col>
-                <a-divider style="height: 84px" direction="vertical" />
-                <a-col :flex="'86px'" style="text-align: right">
-                    <a-space direction="vertical" :size="18">
+                <a-divider class="query-divider" style="height: 84px" direction="vertical" />
+                <a-col :flex="1" class="app-query-actions">
+                    <a-space class="query-actions-space" direction="vertical" :size="18">
                         <a-button type="primary" @click="search">
                             <template #icon>
                                 <icon-search />

+ 5 - 5
src/pages/admin/workOrder/orderList.vue

@@ -9,23 +9,23 @@
                     <a-form class="queryForm app-query-form" :model="queryData" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }"
                         label-align="left">
                         <a-row :gutter="16">
-                            <a-col :span="8">
+                            <a-col :span="6">
                                 <a-form-item field="id" label="工单ID">
                                     <a-input-number v-model="queryData.id" placeholder="请输入工单ID" :step="1"
-                                        :precision="0" />
+                                        :precision="0" style="width: 100%" />
                                 </a-form-item>
                             </a-col>
 
-                            <a-col :span="8">
+                            <a-col :span="6">
                                 <a-form-item field="state" label="状态">
                                     <a-select v-model="queryData.state" :options="state" placeholder="请选择工单状态"
-                                        :default-value="-1" />
+                                        :default-value="-1" style="width: 100%" />
                                 </a-form-item>
                             </a-col>
                         </a-row>
                     </a-form>
                 </a-col>
-                <a-divider class="query-divider" style="height: 84px" direction="vertical" />
+                <a-divider class="query-divider" style="height: 42px" direction="vertical" />
                 <a-col :flex="1" class="app-query-actions">
                     <a-space class="query-actions-space" direction="vertical" :size="18">
                         <a-button type="primary" @click="search">

+ 1 - 1
src/pages/doc/detail.vue

@@ -68,7 +68,7 @@ const loadArticle = async () => {
     }
     article.value = res.data
     if (article.value?.title) {
-      document.title = `${article.value.title} - 哪吒乐跑`
+      document.title = `${article.value.title} - 沐晨乐跑`
     }
   } catch (e) {
     Notification.error({ title: '加载文档失败', content: e.message || '请稍后再试' })

+ 204 - 111
src/pages/lepao/accountList/index.vue

@@ -137,15 +137,15 @@
       <a-alert v-if="notice" style="margin-bottom: 15px;">{{ notice }}</a-alert>
 
       <a-table :data="data" :bordered="false" hoverable class="table table-clickable" :loading="loading" expandable :scroll="{
-        x: 2400
-      }" @row-click="onRowClick" :pagination="{
+        x: 2200
+      }" :pagination="{
         showPageSize: true,
         showJumper: true,
         showTotal: true,
         pageSize: pagination.pagesize,
         current: pagination.current,
         total: pagination.total
-      }" @page-change="handlePageChange" @page-size-change="handlePageSizeChange">
+      }" @row-click="onRowClick" @page-change="handlePageChange" @page-size-change="handlePageSizeChange">
         <template #name-filter="{ filterValue, setFilterValue, handleFilterConfirm, handleFilterReset }">
           <div class="custom-filter">
             <a-space direction="vertical">
@@ -167,8 +167,8 @@
               </a-avatar>
             </template>
           </a-table-column>
-          <a-table-column title="学号" :width="117" data-index="student_num" ellipsis tooltip></a-table-column>
-          <a-table-column title="姓名" :width="130">
+          <a-table-column title="学号" :width="110" data-index="student_num" ellipsis tooltip></a-table-column>
+          <a-table-column title="姓名" :width="120">
             <template #cell="{ record }">
               {{ record.name ?? '请使用乐跑登录器更新账号信息' }}
             </template>
@@ -186,9 +186,8 @@
               {{ record.sex === 1 ? '男' : (record.sex === 2 ? '女' : '') }}
             </template>
           </a-table-column>
-          <a-table-column title="年级" :width="70" data-index="grade_id" tooltip></a-table-column>
-          <a-table-column title="学院" :width="220" data-index="academy_name" tooltip></a-table-column>
-          <a-table-column title="跑区" :width="130">
+          <a-table-column title="班级" :width="200" data-index="class_id" ellipsis tooltip></a-table-column>
+          <a-table-column title="跑区" :width="120">
             <template #cell="{ record }">
               {{ record.area || '随机分配' }}
             </template>
@@ -213,19 +212,6 @@
               </div>
             </template>
           </a-table-column>
-          <!-- <a-table-column title="人脸状态" :width="100" ellipsis tooltip>
-            <template #cell="{ record }">
-              <div v-if="record.face_state === 0" class="state">
-                <div class="circle zero"></div>未采集
-              </div>
-              <div v-else-if="record.face_state === 1" class="state">
-                <div class="circle one"></div>已通过
-              </div>
-              <div v-else class="state">
-                <div class="circle else"></div>不通过
-              </div>
-            </template>
-          </a-table-column> -->
           <a-table-column title="自动乐跑" :width="105" ellipsis tooltip :filterable="{
             filters: [
               { text: '开启', value: 1 },
@@ -234,8 +220,7 @@
             filter: (value, record) => record.auto_run == value
           }">
             <template #cell="{ record }">
-              <a-tag color="green" v-if="record.auto_run">{{ record.target_count === 0 ? '开启-∞次' :
-                `开启-${record.target_count}次` }}</a-tag>
+              <a-tag color="green" v-if="record.auto_run">开启</a-tag>
               <a-tag color="red" v-else>关闭</a-tag>
             </template>
           </a-table-column>
@@ -254,10 +239,30 @@
               {{ record.auto_run ? autoTimeLabel(record) : '-' }}
             </template>
           </a-table-column>
-          <a-table-column title="学期目标" :width="88" ellipsis tooltip>
+          <a-table-column title="目标里程" :width="100" ellipsis tooltip>
             <template #cell="{ record }">
-              {{ record.term_num != record.total_num ? `${record.total_num} / ${record.term_num}` :
-                '已完成' }}
+              {{ record.target_count === 0 ? '不限' : formatKm(record.target_count) }}
+            </template>
+          </a-table-column>
+          <a-table-column title="自动距离区间" :width="155" ellipsis tooltip>
+            <template #cell="{ record }">
+              {{ record.auto_run ? formatAutoDistanceRangeLabel(record.auto_run_distance_min_km ?? 2, record.auto_run_distance_max_km ?? 2) : '—' }}
+            </template>
+          </a-table-column>
+          <a-table-column title="自动配速区间" :width="150" ellipsis tooltip>
+            <template #cell="{ record }">
+              <span v-if="record.auto_run">{{ formatAutoPaceRangeLabel(record.pace_min_sec_per_km ?? 180, record.pace_max_sec_per_km ?? 600) }}</span>
+              <span v-else>—</span>
+            </template>
+          </a-table-column>
+          <a-table-column title="本月里程" :width="100" ellipsis tooltip>
+            <template #cell="{ record }">
+              {{ formatKm(record.term_num) }}
+            </template>
+          </a-table-column>
+          <a-table-column title="累计里程" :width="110" ellipsis tooltip>
+            <template #cell="{ record }">
+              {{ formatKm(record.total_num) }}
             </template>
           </a-table-column>
           <a-table-column title="添加时间" :width="150" ellipsis tooltip :sortable="{
@@ -284,8 +289,6 @@
                 <a-button>操作 <icon-down /></a-button>
                 <template #content>
                   <a-doption v-if="hasPermission('action.lepao.updateAccount')" @click="editAccount(record)"><icon-edit /> 编辑账号</a-doption>
-                  <!-- <a-doption @click="faceRecoRef.openModal(record)"><icon-video-camera /> 人脸采集</a-doption> -->
-                  <!-- <a-doption @click="bindBotRef.openModal(record)"><icon-robot-add /> 绑定智能机器人</a-doption> -->
                   <a-doption v-if="hasPermission('action.lepao.singleRun')" @click="SingleRun(record)"><icon-play-circle /> 开始单次乐跑</a-doption>
                   <a-doption v-if="hasPermission('action.lepao.changeAutoRun')" @click="ChangeAutoRun(record)"><icon-translate /> {{ record.auto_run ? '关闭' :
                     '开启' }}自动乐跑</a-doption>
@@ -307,7 +310,6 @@
       <a-form-item label="自动识别" v-if="!form.id">
         <a-textarea
           v-model="autoFillText"
-          no-trim
           placeholder="粘贴包含学号、邮箱、跑区等信息的文本,点击“识别并填入”自动填写"
           :auto-size="{ minRows: 2, maxRows: 4 }"
           allow-clear
@@ -315,7 +317,7 @@
         <template #extra>
           <a-space>
             <a-button type="primary" @click="handleAutoFill" size="small">识别并填入</a-button>
-            <span class="auto-fill-tip">示例:2023012345,abc@ctbu.edu.cn,主校区北跑区</span>
+            <span class="auto-fill-tip">示例:2301234567,abc@qq.com,南山校区</span>
           </a-space>
         </template>
       </a-form-item>
@@ -325,7 +327,6 @@
       <a-form-item field="notice_type" label="通知方式">
         <a-radio-group v-model="form.notice_type">
           <a-radio value="email"><icon-email /> 邮件</a-radio>
-          <!-- <a-radio value="bot"><icon-robot /> 智能机器人</a-radio> -->
           <a-radio value="none"><icon-notification-close /> 无通知</a-radio>
         </a-radio-group>
       </a-form-item>
@@ -338,7 +339,6 @@
           <a-option v-for="(item, index) in area" :key="index" :value="item">
             <span class="vipcontent">
               <span>{{ item }} </span>
-              <!-- <img src="@/assets/vip.svg" alt="vip" height="20"> -->
             </span>
           </a-option>
         </a-select>
@@ -346,10 +346,11 @@
       <a-form-item field="auto_run" label="自动乐跑开关">
         <a-switch v-model="form.auto_run" :checked-value="1" :unchecked-value="0" />
       </a-form-item>
-      <a-form-item field="target_count" label="乐跑目标次数" v-if="form.auto_run">
-        <a-input-number v-model="form.target_count" placeholder="请输入乐跑目标次数" mode="button" />
+      <a-form-item field="target_count" label="月度目标里程" v-if="form.auto_run">
+        <a-input-number v-model="form.target_count" placeholder="0–200,0 表示不限制" mode="button" :min="0" :max="200"
+          :precision="2" :step="0.5" />
         <template #extra>
-          <div>当学期有效次数达到目标次数时自动乐跑将关闭,0为不限次</div>
+          <div>月度里程(Km)达到目标后自动乐跑将关闭;填 0 表示不限制</div>
         </template>
       </a-form-item>
       <a-form-item field="auto_day" label="自动乐跑星期" v-if="form.auto_run">
@@ -358,8 +359,44 @@
       <a-form-item field="auto_time" label="自动乐跑时段" v-if="form.auto_run">
         <a-select v-model="form.auto_time" placeholder="请选择每天自动乐跑的时段" :options="auto_time" />
       </a-form-item>
+      <a-form-item field="auto_run_distance_range" label="自动距离区间" v-if="form.auto_run">
+        <a-space>
+          <a-input-number
+            v-model="form.auto_run_distance_min_km"
+            mode="button"
+            :min="1"
+            :max="AUTO_SINGLE_RUN_MAX_KM"
+            :precision="2"
+            :step="0.1"
+            placeholder="下限"
+          />
+          <span>~</span>
+          <a-input-number
+            v-model="form.auto_run_distance_max_km"
+            mode="button"
+            :min="1"
+            :max="AUTO_SINGLE_RUN_MAX_KM"
+            :precision="2"
+            :step="0.1"
+            placeholder="上限"
+          />
+        </a-space>
+        <template #extra>
+          <div>每次自动乐跑在区间内随机取值;上下限可相同(固定距离);范围 1~10 km</div>
+        </template>
+      </a-form-item>
+      <a-form-item field="pace_range" label="自动配速区间" v-if="form.auto_run">
+        <a-space>
+          <a-input v-model="form.pace_min_str" allow-clear placeholder="如 3:00" style="width: 7.5rem" />
+          <span>–</span>
+          <a-input v-model="form.pace_max_str" allow-clear placeholder="如 10:00" style="width: 7.5rem" />
+        </a-space>
+        <template #extra>
+          <div>每公里配速,格式为 分:秒(如 5:30);范围 3:00–10:00/km,每次自动乐跑在区间内随机</div>
+        </template>
+      </a-form-item>
       <a-form-item field="notes" label="备注">
-        <a-textarea v-model="form.notes" no-trim placeholder="添加对账号的备注(非必填)" :max-length="{ length: 50 }" allow-clear
+        <a-textarea v-model="form.notes" placeholder="添加对账号的备注(非必填)" :max-length="{ length: 50 }" allow-clear
           show-word-limit />
       </a-form-item>
     </a-form>
@@ -368,11 +405,16 @@
   <faceModal :faceInfo="faceInfo" ref="faceRecoRef" />
   <bindBot ref="bindBotRef" />
   <accountDetailCard ref="accountDetailRef" />
+  <SingleRunModal
+    v-model:visible="singleRunVisible"
+    :account="singleRunAccount"
+    @success="getAccountsAsync"
+  />
 </template>
 
 <script setup>
 import { ref, reactive, onUnmounted, onMounted, h } from 'vue'
-import { accountList, deleteAccount, addAccount, changeAutoRun, singleRun, updateSelfAccount } from '@/api/lepao'
+import { accountList, deleteAccount, addAccount, changeAutoRun, updateSelfAccount } from '@/api/lepao'
 import { Modal, Notification, Message } from '@arco-design/web-vue'
 import { IconSearch } from '@arco-design/web-vue/es/icon'
 import userCard from '@/components/userCard/userCard.vue'
@@ -380,16 +422,26 @@ import { isElectron } from '@/utils/electron'
 import faceModal from '@/components/FaceModal/faceModal.vue'
 import bindBot from '@/components/BindBot/bindBot.vue'
 import accountDetailCard from '@/components/LepaoAccountCard/accountDetailCard.vue'
+import SingleRunModal from '@/components/SingleRunModal/SingleRunModal.vue'
 import { useRoute } from 'vue-router'
 import { getNotice, getSemesterTimestamps } from '@/utils/util'
 import { hasPermission } from '@/utils/permission'
 import { useResponsiveTable } from '@/hooks/useResponsiveTable'
 
 const { tableFixed } = useResponsiveTable()
+import {
+  formatKm,
+  formatAutoPaceRangeLabel,
+  formatAutoDistanceRangeLabel,
+  formatSecPerKmAsMinSec,
+  parsePaceMinSecInput
+} from '@/utils/lepaoRecord'
 
-const notice = ref('')
+/** 与后端 jkes.autoSingleRunMaxKm 默认一致,保存时由服务端再校验 */
+const AUTO_SINGLE_RUN_MAX_KM = 10
 
 const faceInfo = ref({})
+const faceRecoRef = ref(null)
 const bindBotRef = ref(null)
 const accountDetailRef = ref(null)
 
@@ -405,6 +457,26 @@ const clearPendingDetail = () => {
   }
 }
 
+const onRowClick = (record, ev) => {
+  if (ev?.target?.closest?.('.arco-dropdown, .arco-btn')) return
+
+  const accountId = record?.id
+  if (accountId == null) return
+
+  if (pendingDetailAccountId.value === accountId) {
+    clearPendingDetail()
+    accountDetailRef.value?.openModal(record)
+    return
+  }
+
+  clearPendingDetail()
+  pendingDetailAccountId.value = accountId
+  Message.info('再点一次即可查看账号详情')
+  pendingDetailTimer = setTimeout(clearPendingDetail, ROW_DETAIL_CONFIRM_MS)
+}
+
+const notice = ref('')
+
 const queryDataForm = reactive({
   area: '',
   student_num: '',
@@ -432,7 +504,7 @@ const pagination = reactive({
   pagesize: 20
 })
 
-const area = ["兰花湖校区跑区", "主校区北跑区", "主校区南跑区", "重庆工商大学茶园校区"]
+const area = ["学府大道校区", "南山校区"]
 const state = [
   { label: '全部', value: -1 }, { label: '需登录', value: 0 }, { label: '正常', value: 1 }, { label: '状态异常', value: 2 }
 ]
@@ -530,6 +602,10 @@ const form = reactive({
   auto_run: 1,
   notice_type: 'email',
   target_count: 30,
+  auto_run_distance_min_km: 2.5,
+  auto_run_distance_max_km: 3,
+  pace_min_str: '6:00',
+  pace_max_str: '10:00',
   auto_day: [0, 1, 2, 3, 4, 5, 6],
   notes: ''
 })
@@ -544,10 +620,8 @@ const matchArea = (text) => {
   if (exact) return exact
 
   const areaAliasMap = [
-    { keywords: ['兰花'], value: '兰花湖校区跑区' },
-    { keywords: ['主校北', '主校区北', '北跑区', '北区', '北'], value: '主校区北跑区' },
-    { keywords: ['主校南', '主校区南', '南跑区', '南区', '南'], value: '主校区南跑区' },
-    { keywords: ['茶园', '茶园校区'], value: '重庆工商大学茶园校区' }
+    { keywords: ['南山'], value: '南山校区' },
+    { keywords: ['学府'], value: '学府大道校区' },
   ]
 
   const matchedAlias = areaAliasMap.find(item => item.keywords.some(keyword => normalized.includes(keyword)))
@@ -561,7 +635,7 @@ const handleAutoFill = () => {
     return
   }
 
-  const studentMatch = text.match(/\b20\d{8}\b/)
+  const studentMatch = text.match(/\b2\d{9}\b/)
   const emailMatch = text.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/)
   const areaMatch = matchArea(text)
 
@@ -589,24 +663,6 @@ const handleAutoFill = () => {
   Message.success(`识别成功,已填入 ${hitCount} 项信息`)
 }
 
-const onRowClick = (record, ev) => {
-  if (ev?.target?.closest?.('.arco-dropdown, .arco-btn')) return
-
-  const accountId = record?.id
-  if (accountId == null) return
-
-  if (pendingDetailAccountId.value === accountId) {
-    clearPendingDetail()
-    accountDetailRef.value?.openModal(record)
-    return
-  }
-
-  clearPendingDetail()
-  pendingDetailAccountId.value = accountId
-  Message.info('再点一次即可查看账号详情')
-  pendingDetailTimer = setTimeout(clearPendingDetail, ROW_DETAIL_CONFIRM_MS)
-}
-
 const editAccount = (item) => {
   if (item) {
     form.id = item.id
@@ -615,7 +671,15 @@ const editAccount = (item) => {
     form.area = item.area
     form.auto_time = item.auto_time
     form.auto_run = item.auto_run
-    form.target_count = item.target_count
+    form.target_count = Number(item.target_count)
+    form.auto_run_distance_min_km = Number(item.auto_run_distance_min_km ?? 2)
+    form.auto_run_distance_max_km = Number(item.auto_run_distance_max_km ?? item.auto_run_distance_min_km ?? 2)
+    {
+      const a = formatSecPerKmAsMinSec(item.pace_min_sec_per_km ?? 180)
+      const b = formatSecPerKmAsMinSec(item.pace_max_sec_per_km ?? 600)
+      form.pace_min_str = a === '—' ? '3:00' : a
+      form.pace_max_str = b === '—' ? '10:00' : b
+    }
     form.notice_type = item.notice_type || 'email'
     form.auto_day = item.auto_day
     form.notes = item.notes
@@ -627,6 +691,10 @@ const editAccount = (item) => {
     form.auto_run = 1
     form.auto_time = -1
     form.target_count = 30
+    form.auto_run_distance_min_km = 2.5
+    form.auto_run_distance_max_km = 3
+    form.pace_min_str = '6:00'
+    form.pace_max_str = '10:00'
     form.auto_day = [0, 1, 2, 3, 4, 5, 6]
     form.notice_type = 'email'
     form.area = ''
@@ -645,9 +713,9 @@ const handleBeforeOk = async (done) => {
       return false
     }
 
-    const studentNumRegex = /^\d{10}$/
-    if (!studentNumRegex.test(student_num)) {
-      Message.error('请检查学号格式是否正确')
+    const sid = String(student_num || '').trim()
+    if (!/^[A-Za-z0-9_-]{2,64}$/.test(sid)) {
+      Message.error('学号/账号编码格式不正确(2–64 位字母数字下划线横线)')
       return false
     }
 
@@ -657,8 +725,61 @@ const handleBeforeOk = async (done) => {
       return false
     }
 
-    let data = {
-      ...form
+    const pLo = parsePaceMinSecInput(form.pace_min_str)
+    const pHi = parsePaceMinSecInput(form.pace_max_str)
+    if (pLo == null || pHi == null) {
+      Message.error('配速格式不正确,请使用 分:秒,如 5:30 或 10:00')
+      return false
+    }
+    if (pLo < 180 || pLo > 600 || pHi < 180 || pHi > 600) {
+      Message.error('配速须在 3:00–10:00/km 范围内')
+      return false
+    }
+    if (pLo > pHi) {
+      Message.error('配速下限不能慢于上限(左侧应更快,分:秒更小)')
+      return false
+    }
+
+    if (form.auto_run) {
+      const tk = Number(form.target_count)
+      if (!Number.isFinite(tk) || tk < 0 || tk > 200) {
+        Message.error('目标里程须在 0–200 公里之间')
+        return false
+      }
+      const dMin = Number(form.auto_run_distance_min_km)
+      const dMax = Number(form.auto_run_distance_max_km)
+      if (
+        !Number.isFinite(dMin) ||
+        !Number.isFinite(dMax) ||
+        dMin < 1 ||
+        dMin > AUTO_SINGLE_RUN_MAX_KM ||
+        dMax < 1 ||
+        dMax > AUTO_SINGLE_RUN_MAX_KM
+      ) {
+        Message.error(`自动乐跑距离区间须在 1–${AUTO_SINGLE_RUN_MAX_KM} 公里之间`)
+        return false
+      }
+      if (dMin > dMax) {
+        Message.error('自动乐跑距离下限不能大于上限')
+        return false
+      }
+    }
+
+    const data = {
+      id: form.id,
+      student_num: form.student_num,
+      email: form.email,
+      area: form.area,
+      auto_time: form.auto_time,
+      auto_run: form.auto_run,
+      notice_type: form.notice_type,
+      target_count: form.target_count,
+      auto_run_distance_min_km: form.auto_run_distance_min_km,
+      auto_run_distance_max_km: form.auto_run_distance_max_km,
+      auto_day: form.auto_day,
+      notes: form.notes,
+      pace_min_sec_per_km: pLo,
+      pace_max_sec_per_km: pHi
     }
 
     const res = await addAccount(data)
@@ -728,27 +849,18 @@ const GetNotice = async () => {
   notice.value = res
 }
 
-const SingleRun = async (item) => {
-  if (item.state !== 1)
+const singleRunVisible = ref(false)
+const singleRunAccount = ref(null)
+
+const SingleRun = (item) => {
+  if (item.state !== 1) {
     return Notification.warning({
       title: '当前乐跑账号需登录,请登录后再试',
-      content: '如有疑问请联系哪吒乐跑客服'
+      content: '如有疑问请联系沐晨乐跑客服'
     })
-  Modal.confirm({
-    title: '开始乐跑',
-    content: () => h('div', [
-      h('p', `您是否要为 ${item.name}(${item.student_num}) 乐跑?若乐跑成功将扣减乐跑次数`)
-    ]),
-    onOk: async () => {
-      const res = await singleRun({ student_num: item.student_num })
-      if (!res || res.code !== 0)
-        return Notification.error({
-          title: '提交乐跑任务失败',
-          content: res?.msg ?? '请稍后再试'
-        })
-      Message.success('提交乐跑任务成功!')
-    }
-  })
+  }
+  singleRunAccount.value = item
+  singleRunVisible.value = true
 }
 
 const DeleteAccount = async (item) => {
@@ -782,7 +894,7 @@ const ChangeAutoRun = async (record) => {
         content: res?.msg ?? '请稍后再试'
       })
     } else {
-      Message.success(`自动乐跑状态已${record.auto_run === 1 ? '开启' : '关闭'}`)
+      Message.success(`自动乐跑状态已${record.auto_run === 1 ? '开启,请在编辑账号中配置自动乐跑参数' : '关闭'}`)
       getAccounts()
     }
   } catch (error) {
@@ -845,41 +957,21 @@ onMounted(async () => {
 
 // 组件销毁时停止轮询
 onUnmounted(() => {
-  clearPendingDetail()
   stopPolling()
+  clearPendingDetail()
 })
 </script>
 
 <style scoped lang="less">
 @import '@/styles/store-theme.less';
 
-.queryForm {
-  padding: 0;
-  width: 100%;
-  box-sizing: border-box;
-
-  @media (min-width: (@app-breakpoint-md + 1px)) {
-    margin-top: 20px;
-  }
-}
-
-.buttonGroup {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 10px;
-  margin-bottom: 12px;
-}
-
 .buttonGroup-mobile {
   display: none;
 }
 
-.query-main {
-  min-width: 0;
-}
-
-.query-actions-space {
-  width: 100%;
+.queryForm {
+  margin-top: 20px;
+  padding: 0 15px
 }
 
 .table-clickable {
@@ -948,6 +1040,7 @@ onUnmounted(() => {
   .buttonGroup-mobile {
     display: flex;
     align-items: center;
+    gap: 10px;
   }
 
   .query-row {

+ 28 - 8
src/pages/lepao/countLedger/index.vue

@@ -4,11 +4,11 @@
 
     <userCard />
 
-    <a-card title="乐跑次数明细" style="margin-top: 15px;">
+    <a-card title="乐跑里程明细" style="margin-top: 15px;">
       <AppQueryFilter>
-      <a-row class="queryForm app-query-form">
-        <a-col :flex="'1000px'">
-          <a-form :model="queryData" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }" label-align="left">
+      <a-row class="query-row">
+        <a-col :flex="'1000px'" class="query-main">
+          <a-form class="queryForm app-query-form" :model="queryData" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }" label-align="left">
             <a-row :gutter="16">
               <a-col :span="12">
                 <a-form-item field="biz_type" label="变动类型">
@@ -31,9 +31,9 @@
             </a-row>
           </a-form>
         </a-col>
-        <a-divider style="height: 84px" direction="vertical" />
-        <a-col :flex="'86px'" style="text-align: right">
-          <a-space direction="vertical" :size="18">
+        <a-divider class="query-divider" style="height: 84px" direction="vertical" />
+        <a-col :flex="1" class="app-query-actions">
+          <a-space class="query-actions-space" direction="vertical" :size="18">
             <a-button type="primary" @click="search">
               <template #icon><icon-search /></template>
               搜索
@@ -164,7 +164,7 @@ const fetchData = async () => {
     })
     if (!res || res.code !== 0) {
       return Notification.error({
-        title: '获取次数明细失败',
+        title: '获取里程明细失败',
         content: res?.msg ?? '请稍后再试'
       })
     }
@@ -227,6 +227,26 @@ onMounted(() => {
   }
 }
 
+@media (max-width: @app-breakpoint-md) {
+  .query-row {
+    display: block;
+  }
+
+  .query-divider {
+    display: none;
+  }
+
+  .query-actions-space {
+    margin-top: 4px;
+    flex-direction: row !important;
+    gap: 10px !important;
+  }
+
+  .query-actions-space :deep(.arco-btn) {
+    flex: 1;
+  }
+}
+
 .summary-muted {
   color: var(--color-text-3);
   font-size: 13px;

+ 101 - 37
src/pages/lepao/lepaoRecords/index.vue

@@ -28,7 +28,7 @@
               <a-col :span="12">
                 <a-form-item field="queryTime" label="乐跑时间">
                   <a-range-picker v-model="queryData.queryTime" show-time format="YYYY-MM-DD HH:mm" value-format="x"
-                    @ok="getRecords()" />
+                    @ok="getRecordsAsync()" />
                 </a-form-item>
               </a-col>
             </a-row>
@@ -56,7 +56,7 @@
 
       <a-alert v-if="notice" style="margin-bottom: 15px;">{{ notice }}</a-alert>
 
-      <a-table :data="data" :bordered="false" hoverable class="table" :loading="loading" :scroll="{ x: 1300 }" :pagination="{
+      <a-table :data="data" :bordered="false" hoverable class="table" :loading="loading" :scroll="{ x: 1580 }" :pagination="{
         showPageSize: true,
         showJumper: true,
         showTotal: true,
@@ -106,37 +106,58 @@
             </template>
 
           </a-table-column>
-          <a-table-column title="状态" ellipsis tooltip :width="230">
+          <a-table-column title="状态" ellipsis tooltip :width="200">
             <template #cell="{ record }">
               <div class="state">
-                <div class="circle one" v-if="record.result.record_failed_reason === '自动确认有效'"></div>
-                <div class="circle else" v-else></div>
-                {{ record.result.record_failed_reason }}
+                <div class="circle" :class="recordDisplay(record).statusLevel"></div>
+                {{ recordDisplay(record).statusText }}
+                <div class="progress" v-if="isRunningLepaoRecord(record)">
+                  <a-progress size="mini" :percent="getLepaoSchedulePercent(record) ?? 0" />
+                </div>
               </div>
             </template>
           </a-table-column>
-          <a-table-column title="跑区" :filterable="{
-            filter: (value, record) => (record.result.pass_tit ?? '').includes(value),
+          <a-table-column title="乐跑跑区" :filterable="{
+            filter: (value, record) => (recordDisplay(record).routeRunZoneName ?? '').includes(value),
             slotName: 'name-filter',
             icon: () => h(IconSearch)
-          }" :width="210">
+          }" :width="160">
             <template #cell="{ record }">
-              {{ record.result.pass_tit }}
+              <a-tag v-if="recordDisplay(record).routeRunZoneName" size="small">
+                {{ recordDisplay(record).routeRunZoneName }}
+              </a-tag>
+              <span v-else>—</span>
             </template>
           </a-table-column>
-          <a-table-column title="乐跑距离" :width="100" ellipsis tooltip>
+          <a-table-column title="预计距离" :width="100" ellipsis tooltip>
             <template #cell="{ record }">
-              {{ record.result.distance }} Km
+              <span v-if="recordDisplay(record).expectedKm != null">
+                {{ formatKm(recordDisplay(record).expectedKm) }}
+              </span>
+              <span v-else>—</span>
+            </template>
+          </a-table-column>
+          <a-table-column title="实际距离" :width="100" ellipsis tooltip>
+            <template #cell="{ record }">
+              {{ formatKm(recordDisplay(record).distance) }}
+            </template>
+          </a-table-column>
+          <a-table-column title="扣费里程" :width="100" ellipsis tooltip>
+            <template #cell="{ record }">
+              <span v-if="recordDisplay(record).officialKm != null && Number.isFinite(Number(recordDisplay(record).officialKm))">
+                {{ formatKm(recordDisplay(record).officialKm) }}
+              </span>
+              <span v-else>—</span>
             </template>
           </a-table-column>
           <a-table-column title="跑步时长" :width="100" ellipsis tooltip>
             <template #cell="{ record }">
-              {{ formatSecondsToMinSec(record.result.time) }}
+              {{ formatSecondsToMinSec(recordDisplay(record).timeSec) }}
             </template>
           </a-table-column>
           <a-table-column title="平均配速" :width="90" ellipsis tooltip>
             <template #cell="{ record }">
-              {{ calculatePace(record.result.time, record.result.distance) }}
+              {{ formatPace(recordDisplay(record).timeSec, recordDisplay(record).distance) }}
             </template>
           </a-table-column>
           <a-table-column title="乐跑时间" :width="155" ellipsis tooltip>
@@ -157,9 +178,17 @@
 </template>
 
 <script setup>
-import { ref, reactive, onMounted, h } from 'vue'
+import { ref, reactive, onMounted, onUnmounted, h } from 'vue'
 import { useRouter } from 'vue-router'
 import { lepaoRecords } from '@/api/lepao'
+import {
+  parseLepaoRecordRow,
+  formatSecondsToMinSec,
+  formatPace,
+  formatKm,
+  getLepaoSchedulePercent,
+  isRunningLepaoRecord
+} from '@/utils/lepaoRecord'
 import { Notification, Message } from '@arco-design/web-vue'
 import { IconSearch } from '@arco-design/web-vue/es/icon'
 import { useUserStore } from '@/store/modules/user'
@@ -204,7 +233,7 @@ const pagination = reactive({
 
 const search = () => {
   pagination.current = 1
-  getRecords()
+  getRecordsAsync()
 }
 
 const reset = () => {
@@ -214,12 +243,17 @@ const reset = () => {
   queryData.email = ''
   queryData.area = ''
   queryData.queryTime = getSemesterTimestamps()
-  getRecords()
+  getRecordsAsync()
+}
+
+const getRecordsAsync = async () => {
+  loading.value = true
+  await getRecords()
+  loading.value = false
 }
 
 const getRecords = async () => {
   try {
-    loading.value = true
     const reqData = {
       ...queryData,
       pagesize: pagination.pagesize,
@@ -231,6 +265,7 @@ const getRecords = async () => {
         title: '获取乐跑记录失败!',
         content: res?.msg ?? '请稍后再试'
       })
+
     data.value = res.data
     pagination.total = res.pagination.total
   } catch (error) {
@@ -238,13 +273,11 @@ const getRecords = async () => {
       title: '获取乐跑记录失败!',
       content: error.message || '请稍后再试'
     })
-  } finally {
-    loading.value = false
   }
 }
 
 const openDetail = (record) => {
-  const key = record.public_id || record.id
+  const key = record.public_id
   if (!key) {
     return Message.warning('该记录缺少可用标识,无法打开详情')
   }
@@ -254,41 +287,49 @@ const openDetail = (record) => {
 // 分页 - 页码变化
 const handlePageChange = (page) => {
   pagination.current = page
-  getRecords()
+  getRecordsAsync()
 }
 
 // 分页 - 每页条数变化
 const handlePageSizeChange = (size) => {
   pagination.pagesize = size
   pagination.current = 1 // 页大小变化后回到第一页
-  getRecords()
+  getRecordsAsync()
 }
 
 const stramptoTime = (time) => {
-  return new Date(time).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
+  const t = Number(time)
+  if (!Number.isFinite(t)) return '—'
+  return new Date(t).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
 }
 
-function calculatePace(seconds, kilometers) {
-  const paceInSeconds = seconds / kilometers;
-  const minutes = Math.floor(paceInSeconds / 60);
-  const remainingSeconds = Math.round(paceInSeconds % 60);
-
-  return `${minutes}'${remainingSeconds.toString().padStart(2, '0')}''`;
-}
+const recordDisplay = (record) => parseLepaoRecordRow(record)
 
-function formatSecondsToMinSec(totalSeconds) {
-  const minutes = Math.floor(totalSeconds / 60);
-  const seconds = totalSeconds % 60;
+let timer = null
 
-  return `${minutes}分${seconds.toString().padStart(2, '0')}秒`;
+const startPolling = () => {
+  if (!timer) {
+    timer = setInterval(async () => {
+      await getRecords()
+    }, 5000)
+  }
 }
 
-
+const stopPolling = () => {
+  if (!timer) return
+  clearInterval(timer)
+  timer = null
+}
 
 onMounted(() => {
   queryData.queryTime = getSemesterTimestamps()
-  getRecords()
+  getRecordsAsync()
   GetNotice()
+  startPolling()
+})
+
+onUnmounted(() => {
+  stopPolling()
 })
 </script>
 
@@ -313,6 +354,13 @@ onMounted(() => {
     display: flex;
     align-items: center;
 
+    .progress {
+      margin-left: 8px;
+      min-width: 88px;
+      flex: 1;
+      max-width: 140px;
+    }
+
     .circle {
       border-radius: 50%;
       height: 8px;
@@ -326,6 +374,22 @@ onMounted(() => {
       background-color: rgb(var(--orange-6));
     }
 
+    .running {
+      background-color: rgb(var(--orange-6));
+    }
+
+    .success {
+      background-color: rgb(var(--green-6));
+    }
+
+    .sync {
+      background-color: rgb(var(--arcoblue-6));
+    }
+
+    .fail {
+      background-color: rgb(var(--red-6));
+    }
+
     .one {
       background-color: rgb(var(--green-6));
     }

+ 45 - 45
src/pages/lepao/lepaoRecords/recordDetail.vue

@@ -10,8 +10,14 @@
                         :column="isMobile ? 1 : 2"
                         class="record-descriptions"
                     />
-                    <MapContainer v-if="showMap" :point_list="data.result.point_list" :log_list="data.point_data" :pathData="data.data" threeD
-                        style="margin-top: 10px;" />
+                    <MapContainer
+                        v-if="showMap"
+                        :point_list="pointListForMap"
+                        :log_list="logListForMap"
+                        :pathData="pathForMap"
+                        threeD
+                        style="margin-top: 10px;"
+                    />
                 </template>
             </a-skeleton>
         </a-card>
@@ -19,15 +25,19 @@
 </template>
 
 <script setup>
-import { ref, onMounted, onUnmounted } from 'vue'
+import { ref, computed, onMounted, onUnmounted } from 'vue'
 import { GetRecordDetail } from '@/api/lepao'
 import { Notification } from '@arco-design/web-vue'
 import { useRoute } from 'vue-router'
 import MapContainer from '@/components/Map/MapContainer.vue'
 import StoreRecordSkeleton from '@/components/skeleton/StoreRecordSkeleton.vue'
+import {
+    buildRecordDetailInfo,
+    parseLepaoRecordRow,
+    normalizePointData
+} from '@/utils/lepaoRecord'
 
 const route = useRoute()
-const uuidLike = /^[0-9a-fA-F-]{16,}$/
 
 const loading = ref(false)
 const showMap = ref(false)
@@ -36,40 +46,50 @@ const info = ref([])
 const isMobile = ref(false)
 let mobileResizeHandler = null
 
+const pathForMap = computed(() => {
+    const poly = data.value.path_polyline
+    if (Array.isArray(poly) && poly.length > 0) return poly
+    return []
+})
+
+const pointListForMap = computed(() => {
+    const row = parseLepaoRecordRow({
+        result: data.value.result,
+        jkes_record: data.value.jkes_record,
+        state: data.value.state,
+        run_zone_name: data.value.run_zone_name,
+        path_run_zone_name: data.value.path_run_zone_name,
+        path_distance: data.value.path_distance
+    })
+    return row.pointList || []
+})
+
+const logListForMap = computed(() => normalizePointData(data.value.point_data))
+
 const getRecordDetail = async (routeId) => {
     try {
         loading.value = true
         showMap.value = false
         const raw = decodeURIComponent(String(routeId || ''))
-        const payload = uuidLike.test(raw)
-            ? { public_id: raw }
-            : { id: raw }
-        const res = await GetRecordDetail(payload)
+        if (!raw) {
+            return Notification.error({
+                title: '获取记录详情失败',
+                content: '记录标识无效'
+            })
+        }
+        const res = await GetRecordDetail({ public_id: raw })
         if (!res || res.code !== 0)
             return Notification.error({
-                title: '获取路径数据失败!',
+                title: '获取记录详情失败',
                 content: res?.msg ?? '请稍后再试'
             })
 
         data.value = res.data
         showMap.value = true
-
-        info.value = [
-            { label: '账号名称', value: res.data.name },
-            { label: '乐跑账号', value: res.data.lepao_account },
-            { label: '乐跑类型', value: res.data.run_mode === 'manual' ? '手动乐跑' : '自动乐跑' },
-            { label: '跑区名称', value: res.data.result.pass_tit },
-            { label: '记录时间', value: stramptoTime(res.data.time) },
-            { label: '开始时间', value: stramptoTime(res.data.result.start_time * 1000) },
-            { label: '乐跑状态', value: res.data.result.record_failed_reason },
-            { label: '打卡点数量', value: res.data.result.point_list.length },
-            { label: '跑步距离', value: res.data.result.distance + ' Km' },
-            { label: '跑步时长', value: formatSecondsToMinSec(res.data.result.time) },
-            { label: '平均配速', value: calculatePace(res.data.result.time, res.data.result.distance) }
-        ]
+        info.value = buildRecordDetailInfo(res.data)
     } catch (error) {
         Notification.error({
-            title: '获取路径数据失败!',
+            title: '获取记录详情失败',
             content: error.message || '请稍后再试'
         })
     } finally {
@@ -89,26 +109,6 @@ onMounted(() => {
 onUnmounted(() => {
     if (mobileResizeHandler) window.removeEventListener('resize', mobileResizeHandler)
 })
-
-const stramptoTime = (time) => {
-    return new Date(time).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
-}
-
-function calculatePace(seconds, kilometers) {
-    const paceInSeconds = seconds / kilometers
-    const minutes = Math.floor(paceInSeconds / 60)
-    const remainingSeconds = Math.round(paceInSeconds % 60)
-
-    return `${minutes}'${remainingSeconds.toString().padStart(2, '0')}''`
-}
-
-function formatSecondsToMinSec(totalSeconds) {
-    const minutes = Math.floor(totalSeconds / 60);
-    const seconds = totalSeconds % 60;
-
-    return `${minutes}分${seconds.toString().padStart(2, '0')}秒`;
-}
-
 </script>
 
 <style scoped lang="less">
@@ -128,4 +128,4 @@ function formatSecondsToMinSec(totalSeconds) {
         text-overflow: ellipsis;
     }
 }
-</style>
+</style>

+ 6 - 6
src/pages/path/pathList.vue

@@ -4,9 +4,9 @@
 
         <a-card title="路径列表">
             <AppQueryFilter>
-            <a-row class="query-row app-query-form">
-                <a-col :flex="1">
-                    <a-form :model="queryData" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }"
+            <a-row class="query-row">
+                <a-col :flex="'1000px'" class="query-main">
+                    <a-form class="queryForm app-query-form" :model="queryData" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }"
                         label-align="left">
                         <a-row :gutter="16">
                             <a-col :span="8">
@@ -46,9 +46,9 @@
                         </a-row>
                     </a-form>
                 </a-col>
-                <a-divider style="height: 84px" direction="vertical" />
-                <a-col :flex="'86px'" style="text-align: right">
-                    <a-space direction="vertical" :size="18">
+                <a-divider class="query-divider" style="height: 84px" direction="vertical" />
+                <a-col :flex="1" class="app-query-actions">
+                    <a-space class="query-actions-space" direction="vertical" :size="18">
                         <a-button type="primary" @click="search">
                             <template #icon>
                                 <icon-search />

+ 0 - 639
src/pages/qk/taskList.vue

@@ -1,639 +0,0 @@
-<template>
-  <div class="store-page">
-    <Breadcrumb />
-
-    <a-card title="抢课任务">
-      <div class="buttonGroup buttonGroup-desktop">
-        <a-button v-if="canSubmit" type="primary" size="large" @click="openModal()">
-          <template #icon><icon-plus /></template>
-          提交任务
-        </a-button>
-        <a-button size="large" @click="refreshList()">
-          <template #icon><icon-refresh /></template>
-          刷新
-        </a-button>
-      </div>
-
-      <div class="buttonGroup buttonGroup-mobile">
-        <a-button v-if="canSubmit" type="primary" size="large" @click="openModal()">
-          <template #icon><icon-plus /></template>
-          提交任务
-        </a-button>
-        <a-button size="large" @click="refreshList()">
-          <template #icon><icon-refresh /></template>
-          刷新
-        </a-button>
-      </div>
-
-      <AppQueryFilter>
-      <a-row class="queryForm app-query-form query-row">
-        <a-col :flex="'1000px'" class="query-main">
-          <a-form :model="query" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }" label-align="left">
-            <a-row :gutter="16">
-              <a-col :span="6">
-                <a-form-item field="status" label="状态">
-                  <a-select v-model="query.status" placeholder="全部" allow-clear>
-                    <a-option value="paused">未开始</a-option>
-                    <a-option value="pending">待分配</a-option>
-                    <a-option value="assigned">已分配</a-option>
-                    <a-option value="running">抢课中</a-option>
-                    <a-option value="success">成功</a-option>
-                    <a-option value="failed">失败</a-option>
-                    <a-option value="cancelled">已取消</a-option>
-                  </a-select>
-                </a-form-item>
-              </a-col>
-              <a-col :span="6">
-                <a-form-item field="student_num" label="学号">
-                  <a-input v-model="query.student_num" placeholder="学号" allow-clear />
-                </a-form-item>
-              </a-col>
-              <a-col :span="6">
-                <a-form-item field="name" label="备注">
-                  <a-input v-model="query.name" placeholder="备注关键词" allow-clear />
-                </a-form-item>
-              </a-col>
-              <a-col :span="6">
-                <a-form-item field="batch_id" label="选课批次">
-                  <a-select v-model="query.batch_id" placeholder="全部" allow-clear allow-search>
-                    <a-option v-for="item in batchOptions" :key="item.id" :value="item.id">
-                      {{ item.name }}({{ item.jx0502zbid }})
-                    </a-option>
-                  </a-select>
-                </a-form-item>
-              </a-col>
-              <a-col :span="6">
-                <a-form-item field="course_name" label="课程名称">
-                  <a-input v-model="query.course_name" placeholder="模糊匹配课程或分组" allow-clear />
-                </a-form-item>
-              </a-col>
-            </a-row>
-          </a-form>
-        </a-col>
-        <a-divider class="query-divider" style="height: 84px" direction="vertical" />
-        <a-col :flex="1" class="app-query-actions">
-          <a-space class="query-actions-space" direction="vertical" :size="18">
-            <a-button type="primary" @click="search">
-              <template #icon><icon-search /></template>
-              搜索
-            </a-button>
-            <a-button @click="resetQuery">
-              <template #icon><icon-refresh /></template>
-              重置
-            </a-button>
-          </a-space>
-        </a-col>
-      </a-row>
-      </AppQueryFilter>
-
-      <a-table
-        row-key="id"
-        :data="data"
-        :bordered="false"
-        hoverable
-        class="table"
-        :loading="loading"
-        :scroll="{ x: 1200 }"
-        :pagination="false"
-      >
-        <template #columns>
-          <a-table-column title="学号" data-index="student_num" :width="130" />
-          <a-table-column title="选课批次" :width="220" ellipsis tooltip>
-            <template #cell="{ record }">
-              {{ record.batch_name || record.jx0502zbid || '-' }}
-            </template>
-          </a-table-column>
-          <a-table-column title="目标课程" :width="260">
-            <template #cell="{ record }">
-              <a-space wrap>
-                <a-tag v-for="item in record.courses || []" :key="item">{{ item }}</a-tag>
-                <a-tag v-for="item in record.course_groups || []" :key="item" color="purple">{{ item }}</a-tag>
-              </a-space>
-            </template>
-          </a-table-column>
-          <a-table-column title="状态" :width="110">
-            <template #cell="{ record }">
-              <a-tag :color="statusColor(record.status)">{{ statusText(record.status) }}</a-tag>
-            </template>
-          </a-table-column>
-          <a-table-column title="结果" :width="220">
-            <template #cell="{ record }">
-              <AppTableEllipsis :row-key="`${record.id}-result`" :content="resultText(record)" />
-            </template>
-          </a-table-column>
-          <a-table-column title="备注" :width="140" ellipsis tooltip>
-            <template #cell="{ record }">{{ record.name || '-' }}</template>
-          </a-table-column>
-          <a-table-column title="更新时间" :width="180">
-            <template #cell="{ record }">{{ formatTime(record.update_time) }}</template>
-          </a-table-column>
-          <a-table-column title="操作" :width="280" :fixed="tableFixed('right')">
-            <template #cell="{ record }">
-              <a-space wrap>
-                <a-button
-                  v-if="canControl && canStart(record)"
-                  type="text"
-                  size="small"
-                  status="success"
-                  @click="startTask(record)"
-                >
-                  开启
-                </a-button>
-                <a-button
-                  v-if="canControl && canPause(record)"
-                  type="text"
-                  size="small"
-                  status="warning"
-                  @click="pauseTask(record)"
-                >
-                  暂停
-                </a-button>
-                <a-button v-if="canSubmit" type="text" size="small" :disabled="!canEdit(record)" @click="openModal(record)">编辑</a-button>
-                <a-button v-if="canSubmit" type="text" size="small" @click="copyTask(record)">复制</a-button>
-                <a-popconfirm v-if="canSubmit" content="确认取消该抢课任务?" @ok="cancelTask(record)">
-                  <a-button type="text" status="danger" size="small" :disabled="!canCancel(record)">取消</a-button>
-                </a-popconfirm>
-              </a-space>
-            </template>
-          </a-table-column>
-        </template>
-        <template #empty>
-          <a-empty description="暂无抢课任务">
-            <a-button v-if="canSubmit" type="primary" @click="openModal()">提交第一个任务</a-button>
-          </a-empty>
-        </template>
-      </a-table>
-    </a-card>
-
-    <a-modal
-      v-model:visible="visible"
-      :title="modalTitle"
-      :width="isMobile ? '100%' : '720px'"
-      draggable
-      esc-to-close
-      :ok-loading="saving"
-      :on-before-ok="submitTask"
-    >
-      <a-spin :loading="detailLoading" style="width: 100%">
-      <a-form :model="form" layout="vertical" class="task-form">
-        <a-form-item label="备注">
-          <a-textarea
-            v-model="form.name"
-            no-trim
-            placeholder="添加对任务的备注(非必填)"
-            :max-length="{ length: 50 }"
-            allow-clear
-            show-word-limit
-            :auto-size="{ minRows: 2, maxRows: 4 }"
-          />
-        </a-form-item>
-        <a-form-item label="选课批次" required>
-          <a-select v-model="form.batch_id" placeholder="请选择抢课批次" allow-clear allow-search>
-            <a-option v-for="item in batchOptions" :key="item.id" :value="item.id">
-              {{ item.name }}({{ item.jx0502zbid }})
-            </a-option>
-          </a-select>
-        </a-form-item>
-        <a-form-item label="统一身份认证账号" required>
-          <a-select
-            v-model="form.jw_account_id"
-            placeholder="请选择已验证的统一身份认证账号"
-            allow-search
-            allow-clear
-          >
-            <a-option v-for="item in jwAccounts" :key="item.id" :value="item.id">
-              {{ item.username }}({{ item.realname || '未知' }})
-            </a-option>
-          </a-select>
-          <template #extra>
-            <span v-if="jwAccounts.length === 0">
-              暂无可用账号,请先到
-              <a-link @click="$router.push({ name: 'jwAccount.accountList' })">统一身份认证</a-link>
-              添加并验证
-            </span>
-          </template>
-        </a-form-item>
-        <a-form-item label="课程名称(每行一个,精确匹配)">
-          <a-textarea v-model="form.coursesText" :auto-size="{ minRows: 3 }" placeholder="每行填写一门课程名称" />
-          <template #extra>{{ targetHint }}</template>
-        </a-form-item>
-        <a-form-item label="课程分组(体育课请在此填写项目名称)">
-          <a-textarea v-model="form.courseGroupsText" :auto-size="{ minRows: 2 }" placeholder="每行填写一个课程分组" />
-        </a-form-item>
-        <a-row :gutter="16">
-          <a-col :xs="24" :sm="12">
-            <a-form-item label="抢课间隔(ms)">
-              <a-input-number v-model="form.interval_ms" :min="minInterval" :max="10000" style="width: 100%" />
-              <template #extra>当前最低 {{ minInterval }}ms</template>
-            </a-form-item>
-          </a-col>
-          <a-col :xs="24" :sm="12">
-            <a-form-item label="启用公选课接口">
-              <a-switch v-model="form.enable_ggxxk" />
-            </a-form-item>
-          </a-col>
-          <a-col :xs="24" :sm="12">
-            <a-form-item label="登录失效后自动重新登录">
-              <a-switch v-model="form.auto_relogin" />
-              <template #extra>开启后抢课过程中会话失效将自动重新登录并继续</template>
-            </a-form-item>
-          </a-col>
-        </a-row>
-      </a-form>
-      </a-spin>
-    </a-modal>
-  </div>
-</template>
-
-<script setup>
-import { reactive, ref, onMounted, onUnmounted, computed } from 'vue'
-import { Notification } from '@arco-design/web-vue'
-import { getQkTask, getQkTaskDetail, saveQkTask, deleteQkTask, startQkTask, pauseQkTask, getQkBatchList } from '@/api/qk'
-import { getJwAccount } from '@/api/jwAccount'
-import { useResponsiveTable } from '@/hooks/useResponsiveTable'
-import { hasAnyPermission, QK_ASSISTANT_CONTROL_CODES, QK_ASSISTANT_SUBMIT_CODES } from '@/utils/permission'
-import {
-  getMinQkInterval,
-  getQkTargetHint,
-  splitQkLines,
-  validateQkTaskForm
-} from '@/utils/qkTaskRules'
-
-const { tableFixed, isMobile } = useResponsiveTable()
-const canSubmit = computed(() => hasAnyPermission(QK_ASSISTANT_SUBMIT_CODES))
-const canControl = computed(() => hasAnyPermission(QK_ASSISTANT_CONTROL_CODES))
-
-const loading = ref(false)
-const saving = ref(false)
-const detailLoading = ref(false)
-const visible = ref(false)
-const isCopy = ref(false)
-const data = ref([])
-const batchOptions = ref([])
-const jwAccounts = ref([])
-const query = reactive({
-  status: '',
-  student_num: '',
-  name: '',
-  batch_id: '',
-  course_name: ''
-})
-const form = reactive({
-  id: '',
-  name: '',
-  batch_id: '',
-  jw_account_id: undefined,
-  coursesText: '',
-  courseGroupsText: '',
-  enable_ggxxk: true,
-  auto_relogin: false,
-  interval_ms: 500
-})
-
-const targetCount = computed(() => {
-  return splitQkLines(form.coursesText).length + splitQkLines(form.courseGroupsText).length
-})
-const minInterval = computed(() => getMinQkInterval(targetCount.value))
-const targetHint = computed(() => getQkTargetHint(form.coursesText, form.courseGroupsText))
-const modalTitle = computed(() => {
-  if (form.id) return '编辑抢课任务'
-  if (isCopy.value) return '复制抢课任务'
-  return '提交抢课任务'
-})
-
-const splitLines = splitQkLines
-
-const formatTime = (time) => {
-  if (!time) return '-'
-  return new Date(Number(time)).toLocaleString('zh-CN', {
-    year: 'numeric',
-    month: '2-digit',
-    day: '2-digit',
-    hour: '2-digit',
-    minute: '2-digit'
-  })
-}
-
-const statusText = (status) => ({
-  paused: '未开始',
-  pending: '待分配',
-  assigned: '已分配',
-  running: '抢课中',
-  success: '成功',
-  failed: '失败',
-  cancelled: '已取消'
-}[status] || status)
-
-const statusColor = (status) => ({
-  paused: 'gray',
-  pending: 'blue',
-  assigned: 'arcoblue',
-  running: 'orange',
-  success: 'green',
-  failed: 'red',
-  cancelled: 'gray'
-}[status] || 'gray')
-
-const canStart = (record) => ['paused', 'failed'].includes(record.status)
-const canPause = (record) => ['pending', 'assigned', 'running'].includes(record.status)
-const canEdit = (record) => ['paused', 'failed', 'cancelled'].includes(record.status)
-const canCancel = (record) => ['paused', 'pending', 'failed'].includes(record.status)
-const resultText = (record) => record.error_msg || record.result_json?.message || record.result_json?.course || '-'
-
-const resetForm = () => {
-  isCopy.value = false
-  Object.assign(form, {
-    id: '',
-    name: '',
-    batch_id: '',
-    jw_account_id: jwAccounts.value[0]?.id,
-    coursesText: '',
-    courseGroupsText: '',
-    enable_ggxxk: true,
-    auto_relogin: false,
-    interval_ms: 500
-  })
-}
-
-const fillFormFromTask = (task, { copy = false } = {}) => {
-  Object.assign(form, {
-    id: copy ? '' : task.id,
-    name: task.name,
-    batch_id: task.batch_id || '',
-    jw_account_id: task.jw_account_id || jwAccounts.value.find(item => item.username === task.student_num)?.id,
-    coursesText: (task.courses || []).join('\n'),
-    courseGroupsText: (task.course_groups || []).join('\n'),
-    enable_ggxxk: task.enable_ggxxk,
-    auto_relogin: !!task.auto_relogin,
-    interval_ms: task.interval_ms
-  })
-}
-
-const loadTaskDetail = async (id) => {
-  const res = await getQkTaskDetail({ id })
-  if (!res || res.code !== 0) {
-    throw new Error(res?.msg || '获取任务详情失败')
-  }
-  const task = res.data?.task
-  if (!task) {
-    throw new Error('任务不存在')
-  }
-  return task
-}
-
-const openModal = async (record) => {
-  resetForm()
-  visible.value = true
-  if (!record) return
-  try {
-    detailLoading.value = true
-    const task = await loadTaskDetail(record.id)
-    fillFormFromTask(task)
-  } catch (error) {
-    visible.value = false
-    Notification.error({ title: '获取任务详情失败', content: error.message || '请稍后再试' })
-  } finally {
-    detailLoading.value = false
-  }
-}
-
-const copyTask = async (record) => {
-  resetForm()
-  isCopy.value = true
-  visible.value = true
-  try {
-    detailLoading.value = true
-    const task = await loadTaskDetail(record.id)
-    fillFormFromTask(task, { copy: true })
-  } catch (error) {
-    visible.value = false
-    isCopy.value = false
-    Notification.error({ title: '复制失败', content: error.message || '请稍后再试' })
-  } finally {
-    detailLoading.value = false
-  }
-}
-
-const loadJwAccounts = async () => {
-  try {
-    const res = await getJwAccount()
-    if (res?.code === 0) {
-      jwAccounts.value = (res.data || []).filter(item => item.state === 1)
-      if (!form.jw_account_id && jwAccounts.value.length) {
-        form.jw_account_id = jwAccounts.value[0].id
-      }
-    }
-  } catch (_) {}
-}
-
-const loadBatches = async () => {
-  try {
-    const res = await getQkBatchList()
-    if (res?.code === 0) {
-      batchOptions.value = res.data || []
-    }
-  } catch (_) {}
-}
-
-const getList = async (silent = false) => {
-  try {
-    if (!silent) loading.value = true
-    const params = {}
-    if (query.status) params.status = query.status
-    if (query.student_num) params.student_num = query.student_num
-    if (query.name) params.name = query.name
-    if (query.batch_id) params.batch_id = query.batch_id
-    if (query.course_name) params.course_name = query.course_name
-    const res = await getQkTask(params)
-    if (!res || res.code !== 0) {
-      if (!silent) {
-        Notification.error({ title: '获取抢课任务失败', content: res?.msg || '请稍后再试' })
-      }
-      return
-    }
-    data.value = res.data || []
-  } catch (error) {
-    if (!silent) {
-      Notification.error({ title: '获取抢课任务失败', content: error.message || '请稍后再试' })
-    }
-  } finally {
-    if (!silent) loading.value = false
-  }
-}
-
-const search = () => getList()
-
-const resetQuery = () => {
-  query.status = ''
-  query.student_num = ''
-  query.name = ''
-  query.batch_id = ''
-  query.course_name = ''
-  getList()
-}
-
-const refreshList = () => getList()
-
-const submitTask = async () => {
-  const validated = validateQkTaskForm(form)
-  if (!validated.ok) {
-    Notification.error({ title: '保存抢课任务失败', content: validated.message })
-    return false
-  }
-  try {
-    saving.value = true
-    const payload = {
-      id: form.id || undefined,
-      name: validated.name,
-      batch_id: validated.batch_id,
-      jw_account_id: validated.jw_account_id,
-      courses: validated.courses,
-      course_groups: validated.courseGroups,
-      enable_ggxxk: form.enable_ggxxk,
-      auto_relogin: form.auto_relogin,
-      interval_ms: validated.intervalMs
-    }
-    const res = await saveQkTask(payload)
-    if (!res || res.code !== 0) {
-      Notification.error({ title: '保存抢课任务失败', content: res?.msg || '请稍后再试' })
-      return false
-    }
-    Notification.success({ title: '保存成功' })
-    await getList()
-    return true
-  } catch (error) {
-    Notification.error({ title: '保存抢课任务失败', content: error.message || '请稍后再试' })
-    return false
-  } finally {
-    saving.value = false
-  }
-}
-
-const cancelTask = async (record) => {
-  const res = await deleteQkTask({ id: record.id })
-  if (!res || res.code !== 0) {
-    return Notification.error({ title: '取消失败', content: res?.msg || '请稍后再试' })
-  }
-  Notification.success({ title: '已取消' })
-  getList()
-}
-
-const startTask = async (record) => {
-  const res = await startQkTask({ id: record.id })
-  if (!res || res.code !== 0) {
-    return Notification.error({ title: '开启失败', content: res?.msg || '请稍后再试' })
-  }
-  Notification.success({ title: '已开启', content: '任务已进入待分配队列' })
-  getList()
-}
-
-const pauseTask = async (record) => {
-  const res = await pauseQkTask({ id: record.id })
-  if (!res || res.code !== 0) {
-    return Notification.error({ title: '暂停失败', content: res?.msg || '请稍后再试' })
-  }
-  Notification.success({ title: '已暂停' })
-  getList()
-}
-
-let pollTimer = null
-
-const startPolling = () => {
-  if (!pollTimer) {
-    pollTimer = setInterval(() => getList(true), 5000)
-  }
-}
-
-const stopPolling = () => {
-  if (pollTimer) {
-    clearInterval(pollTimer)
-    pollTimer = null
-  }
-}
-
-onMounted(() => {
-  loadJwAccounts()
-  loadBatches()
-  getList()
-  startPolling()
-})
-
-onUnmounted(() => {
-  stopPolling()
-})
-</script>
-
-<style scoped lang="less">
-@import '@/styles/store-theme.less';
-
-.buttonGroup {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 10px;
-  margin-bottom: 12px;
-}
-
-.buttonGroup-mobile {
-  display: none;
-}
-
-.queryForm {
-  padding: 0;
-  width: 100%;
-  box-sizing: border-box;
-
-  @media (min-width: (@app-breakpoint-md + 1px)) {
-    margin-top: 20px;
-  }
-}
-
-.query-main {
-  min-width: 0;
-}
-
-.query-actions-space {
-  width: 100%;
-}
-
-.table {
-  margin-top: 4px;
-}
-
-.task-form {
-  max-height: 70vh;
-  overflow-y: auto;
-}
-
-@media (max-width: 768px) {
-  .buttonGroup-desktop {
-    display: none;
-  }
-
-  .buttonGroup-mobile {
-    display: flex;
-    align-items: center;
-  }
-
-  .buttonGroup-mobile :deep(.arco-btn) {
-    flex: 1;
-  }
-
-  .query-row {
-    display: block;
-  }
-
-  .query-divider {
-    display: none;
-  }
-
-  .query-actions-space {
-    margin-top: 4px;
-    flex-direction: row !important;
-    gap: 10px !important;
-  }
-
-  .query-actions-space :deep(.arco-btn) {
-    flex: 1;
-  }
-}
-</style>

+ 0 - 259
src/pages/qxs/getBookList.vue

@@ -1,259 +0,0 @@
-<template>
-    <div class="store-page">
-        <Breadcrumb />
-
-        <a-card title="趣选书 · 书单查询" class="card">
-            <div class="userLogin">
-                <a-form :model="form" size="large" class="login-form">
-                    <a-form-item field="username" label="用户名">
-                        <a-input v-model="form.username" :style="{ width: '320px' }" placeholder="请输入趣选书账户" allow-clear>
-                            <template #prefix>
-                                <icon-user />
-                            </template>
-                        </a-input>
-                        <template #extra>
-                            <div>默认账号:手机号/身份证号/ctbu+学号</div>
-                        </template>
-                    </a-form-item>
-                    <a-form-item field="password" label="密码">
-                        <a-input-password v-model="form.password" placeholder="请输入趣选书密码" :style="{ width: '320px' }"
-                            :defaultVisibility="false" allow-clear>
-                            <template #prefix>
-                                <icon-lock />
-                            </template>
-                        </a-input-password>
-                        <!-- <template #extra>
-                            <div>已为您填充默认密码:ctbu123456</div>
-                        </template> -->
-                    </a-form-item>
-                    <a-form-item>
-                        <a-space>
-                            <a-button @click="getBookList" :loading="loading">查询</a-button>
-                        </a-space>
-                    </a-form-item>
-                </a-form>
-
-                <a-divider direction="vertical" class="divider"/>
-
-                <div class="userInfo">
-                    <a-avatar :style="{ backgroundColor: '#FE82A5' }" :size="70">
-                        <IconUser />
-                    </a-avatar>
-
-                    <div class="username">{{ userInfo.userName ?? '待查询' }}</div>
-                    <div class="termName">{{ userInfo.univName ?? '' }} - {{ userInfo.termName ?? '' }}</div>
-                </div>
-            </div>
-
-            <a-alert v-if="notice" style="margin-top: 15px;">{{ notice }}</a-alert>
-
-            <a-watermark :content="['哪吒乐跑', 'xxoo365.top']" :alpha="0.5">
-                <a-table :bordered="false" :data="data" stripe hoverable :pagination="false" column-resizable class="table"
-                    :loading="loading" :columns="columns">
-                    <template #imgUrl="{ record }">
-                        <a-image width="50" height="50" fit="contain" v-if="record.imgUrl" :src="record.imgUrl" />
-                        <a-avatar shape="square" :size="50" :style="{ backgroundColor: 'rgba(0,0,0,0.25)' }"
-                            v-else-if="!bookInfo[record.isbn]?.img">
-                            <icon-book />
-                        </a-avatar>
-                        <a-image width="50" height="50" fit="contain" v-else :src="bookInfo[record.isbn]?.img" />
-                    </template>
-                    <template #textbookPrice="{ record }">
-                        <span class="oldPrice" v-if="record.textbookPrice"><del>¥{{ record.textbookPrice }}</del></span>
-                        ¥{{
-                            record.textbookDiscountedPrice ?? record.totalDiscountedPrice }}
-                    </template>
-                    <template #crouseName="{ record }">
-                        {{ record.crouseName }}({{ record.courseType }})
-                    </template>
-                    <template #optional="{ record }">
-                        <a-button @click="showDetail(record.isbn)">查看详情</a-button>
-                    </template>
-                </a-table>
-            </a-watermark>
-
-            <div class="total">
-                共找到 {{ data.length }} 本书籍
-            </div>
-        </a-card>
-    </div>
-</template>
-
-<script setup>
-import { ref, reactive, h } from 'vue'
-import { GetBookList, GetBookImg } from '@/api/qxs'
-import { Notification, Modal, Button } from '@arco-design/web-vue'
-import { useRoute } from 'vue-router'
-import { getNotice } from '@/utils/util'
-
-const notice = ref('')
-
-const GetNotice = async () => {
-  const { path } = useRoute()
-  const res = await getNotice(path)
-  notice.value = res
-}
-
-GetNotice()
-
-const form = reactive({
-    username: '',
-    password: '',
-})
-
-const loading = ref(false)
-const data = ref([])
-const bookInfo = ref({})
-const userInfo = ref({})
-
-const columns = [
-    {
-        title: '封面',
-        slotName: 'imgUrl',
-    }, {
-        title: '书名',
-        dataIndex: 'title',
-    }, {
-        title: '出版社',
-        dataIndex: 'pressName',
-    }, {
-        title: '作者',
-        dataIndex: 'author',
-    }, {
-        title: 'ISBN号',
-        dataIndex: 'isbn',
-    }, {
-        title: '定价',
-        slotName: 'textbookPrice',
-    }, {
-        title: '课程名称',
-        dataIndex: 'courseName',
-    }, {
-        title: '任课教师',
-        dataIndex: 'multiTeacherName',
-    }, {
-        title: '上课班级',
-        dataIndex: 'className',
-    }, {
-        title: '',
-        slotName: 'optional',
-    }
-]
-
-const showDetail = (isbn) => {
-    Modal.info({
-        title: '书籍详情',
-        content: () => h('div', { class: 'info-modal-content' }, [
-            h('div', { style: 'margin-bottom: 10px;' }, `出版日期:${bookInfo.value[isbn].pubdate != '' ? bookInfo.value[isbn].pubdate : '未知'}`),
-            h('div', { style: 'margin-bottom: 10px;' }, `书籍简介:${bookInfo.value[isbn].summary != '' ? bookInfo.value[isbn].summary : '暂无简介'}`)
-        ])
-    })
-}
-
-const getBookInfo = async (isbn) => {
-    try {
-        const res = await GetBookImg({ isbn })
-        if (!res || res.code !== 0)
-            return console.log(`获取书籍信息失败!${res.msg ?? ''}`)
-        bookInfo.value[isbn] = res.data
-    } catch (error) {
-        console.log(`获取书籍信息失败!${error.message}`)
-    }
-}
-
-const getBookList = async () => {
-    try {
-        loading.value = true
-        const { username, password } = form
-        if (!username || !password)
-            return Notification.error({
-                title: '请完善趣选书账号信息!'
-            })
-        const res = await GetBookList({ username, password })
-        if (!res || res.code !== 0)
-            return Notification.error({
-                title: '获取书单失败!',
-                content: res?.msg ?? '请稍后再试'
-            })
-
-        data.value = res.data.bookList || []
-        userInfo.value = res.data.userInfo
-
-        data.value.forEach((item) => {
-            getBookInfo(item.isbn)
-        })
-    } catch (error) {
-        Notification.error({
-            title: '获取书单失败!',
-            content: error.message || '请稍后再试'
-        })
-    } finally {
-        loading.value = false
-    }
-}
-
-</script>
-
-<style lang="less" scoped>
-.table {
-    margin-top: 15px;
-
-    .oldPrice {
-        color: #777;
-        font-size: 0.8em;
-    }
-}
-
-.card {
-    .userLogin {
-        display: flex;
-        flex-wrap: wrap;
-        gap: 24px;
-        width: 100%;
-        margin: 0 auto;
-
-        .login-form {
-            flex: 1 1 280px;
-            min-width: 0;
-            max-width: 100%;
-
-            :deep(.arco-input),
-            :deep(.arco-input-wrapper) {
-                width: 100% !important;
-                max-width: 360px;
-            }
-        }
-
-        .userInfo {
-            display: flex;
-            flex-direction: column;
-            justify-content: center;
-            align-items: center;
-            flex: 1 1 200px;
-            min-width: 0;
-
-            .username {
-                margin-top: 10px;
-                font-size: 1.6em;
-            }
-
-            .termName {
-                margin-top: 10px;
-                font-size: 1.1em;
-            }
-        }
-    }
-
-    .total {
-        text-align: center;
-        margin: 15px;
-        font-size: 0.8em;
-    }
-}
-
-@media (max-width: 768px) {
-    .divider {
-        display: none;
-    }
-  }
-</style>

+ 1 - 1
src/pages/store/goodsList/index.vue

@@ -9,7 +9,7 @@
     <header class="page-header">
       <div>
         <h1 class="store-section-title">云商城</h1>
-        <p class="store-section-desc">选购乐跑次数套餐,支付后自动到账</p>
+        <p class="store-section-desc">选购乐跑里程套餐,支付后自动到账</p>
       </div>
       <div class="store-search-wrap">
         <a-input-search

+ 6 - 6
src/pages/store/sendCountRecords/index.vue

@@ -4,9 +4,9 @@
 
     <a-card title="赠送记录">
       <AppQueryFilter>
-      <a-row class="queryForm app-query-form">
-        <a-col :flex="'1000px'">
-          <a-form :model="queryData" :label-col-props="{ span: 6 }"
+      <a-row class="query-row">
+        <a-col :flex="'1000px'" class="query-main">
+          <a-form class="queryForm app-query-form" :model="queryData" :label-col-props="{ span: 6 }"
             :wrapper-col-props="{ span: 18 }" label-align="left">
             <a-row :gutter="16">
               <a-col :span="12">
@@ -22,9 +22,9 @@
             </a-row>
           </a-form>
         </a-col>
-        <a-divider style="height: 84px" direction="vertical" />
-        <a-col :flex="'86px'" style="text-align: right">
-          <a-space direction="vertical" :size="18">
+        <a-divider class="query-divider" style="height: 42px" direction="vertical" />
+        <a-col :flex="1" class="app-query-actions">
+          <a-space class="query-actions-space" direction="vertical" :size="18">
             <a-button type="primary" @click="search">
               <template #icon>
                 <icon-search />

+ 5 - 157
src/router/index.js

@@ -1,7 +1,7 @@
 import * as VueRouter from 'vue-router'
 import { useUserStore } from '@/store'
 import { isElectron } from '../utils/electron'
-import { canAccessRoute, QK_ASSISTANT_ACCESS_CODES } from '@/utils/permission'
+import { canAccessRoute } from '@/utils/permission'
 const DEFAULT_LAYOUT = () => import('@/layout/default-layout.vue')
 const HTML_VIEW = () => import('@/layout/html-view.vue')
 // import { Message } from '@arco-design/web-vue'
@@ -157,7 +157,7 @@ const routes = [
                 name: 'lepao.countLedger',
                 component: () => import('../pages/lepao/countLedger/index.vue'),
                 meta: {
-                    title: '次数明细'
+                    title: '里程明细'
                 }
             },
             {
@@ -172,48 +172,6 @@ const routes = [
             }
         ]
     },
-    {
-        path: '/qk',
-        name: 'qk',
-        redirect: '/qk/taskList',
-        component: DEFAULT_LAYOUT,
-        meta: {
-            title: '抢课助手',
-            icon: 'icon-calendar-clock',
-            permissionCodes: QK_ASSISTANT_ACCESS_CODES
-        },
-        children: [
-            {
-                path: 'taskList',
-                name: 'qk.taskList',
-                component: () => import('../pages/qk/taskList.vue'),
-                meta: {
-                    title: '抢课任务',
-                    permissionCodes: QK_ASSISTANT_ACCESS_CODES
-                }
-            }
-        ]
-    },
-    {
-        path: "/qxs",
-        name: "qxs",
-        component: DEFAULT_LAYOUT,
-        meta: {
-            title: '趣选书',
-            icon: 'icon-book'
-        },
-        children: [
-            {
-                path: 'getBookList',
-                name: 'qxs.getBookList',
-                component: () => import('../pages/qxs/getBookList.vue'),
-                meta: {
-                    title: '书单查询',
-                    public: true
-                }
-            }
-        ]
-    },
     // {
     //     path: '/jwAccount',
     //     name: 'jwAccount',
@@ -383,21 +341,12 @@ const routes = [
                     permission: ['admin', 'service']
                 }
             },
-            {
-                path: 'lepaoBindAudit',
-                name: 'admin.lepaoBindAudit',
-                component: () => import('../pages/admin/lepaoBindAudit/index.vue'),
-                meta: {
-                    title: '绑定记录',
-                    permission: ['admin', 'service']
-                }
-            },
             {
                 path: 'lepaoCountLedger',
                 name: 'admin.lepaoCountLedger',
                 component: () => import('../pages/admin/lepaoCountLedger/index.vue'),
                 meta: {
-                    title: '乐跑次数明细',
+                    title: '乐跑里程明细',
                     permission: ['admin', 'service']
                 }
             },
@@ -468,110 +417,9 @@ const routes = [
                     title: '请求日志',
                     permission: ['admin']
                 }
-            },
-            {
-                path: 'powerTask',
-                name: 'admin.powerTask',
-                component: () => import('../pages/admin/powerTask/index.vue'),
-                meta: {
-                    title: '电费任务',
-                    permission: ['admin', 'service']
-                }
-            },
-            {
-                path: 'qxsAccount',
-                name: 'admin.qxsAccount',
-                component: () => import('../pages/admin/qxsAccount/index.vue'),
-                meta: {
-                    title: '趣选书管理',
-                    permission: ['admin', 'service']
-                }
             }
         ]
     },
-    {
-        path: '/qkManage',
-        name: 'qkManage',
-        redirect: '/qkManage/task',
-        component: DEFAULT_LAYOUT,
-        meta: {
-            title: '抢课管理',
-            icon: 'icon-schedule',
-            permissionCodes: [
-                'page.admin.qk.client',
-                'page.admin.qk.batch',
-                'page.admin.qk.task',
-                'page.admin.qk.report'
-            ]
-        },
-        children: [
-            {
-                path: 'batch',
-                name: 'qkManage.batch',
-                component: () => import('../pages/admin/qkBatch/index.vue'),
-                meta: {
-                    title: '批次管理',
-                    permission: ['admin', 'service'],
-                    permissionCode: 'page.admin.qk.batch'
-                }
-            },
-            {
-                path: 'client',
-                name: 'qkManage.client',
-                component: () => import('../pages/admin/qkClient/index.vue'),
-                meta: {
-                    title: '抢课客户端',
-                    permission: ['admin', 'service'],
-                    permissionCode: 'page.admin.qk.client'
-                }
-            },
-            {
-                path: 'task',
-                name: 'qkManage.task',
-                component: () => import('../pages/admin/qkTask/index.vue'),
-                meta: {
-                    title: '抢课任务',
-                    permission: ['admin', 'service'],
-                    permissionCode: 'page.admin.qk.task'
-                }
-            },
-            {
-                path: 'report',
-                name: 'qkManage.report',
-                component: () => import('../pages/admin/qkReport/index.vue'),
-                meta: {
-                    title: '回报记录',
-                    permission: ['admin', 'service'],
-                    permissionCode: 'page.admin.qk.report'
-                }
-            }
-        ]
-    },
-    {
-        path: '/lepao/qkAssistant',
-        redirect: '/qk/taskList',
-        meta: { hideInMenu: true }
-    },
-    {
-        path: '/admin/qkAssistant',
-        redirect: '/qk/taskList',
-        meta: { hideInMenu: true }
-    },
-    {
-        path: '/qkManage/assistant',
-        redirect: '/qk/taskList',
-        meta: { hideInMenu: true }
-    },
-    {
-        path: '/admin/qkClient',
-        redirect: '/qkManage/client',
-        meta: { hideInMenu: true }
-    },
-    {
-        path: '/admin/qkTask',
-        redirect: '/qkManage/task',
-        meta: { hideInMenu: true }
-    },
     {
         path: '/noticeManage',
         name: 'noticeManage',
@@ -872,8 +720,8 @@ router.beforeEach(async (to, from, next) => {
         if (to.meta.onlyElectron && !electronEnv) return next('/lepao/accountList')
     }
 
-    if (!to.meta.title) document.title = '哪吒乐跑 - 智能校园乐跑平台'
-    else document.title = to.meta.title + ' - 哪吒乐跑'
+    if (!to.meta.title) document.title = '沐晨乐跑 - 智能校园乐跑平台'
+    else document.title = to.meta.title + ' - 沐晨乐跑'
 
     next()
 })

+ 22 - 12
src/style.css

@@ -60,27 +60,37 @@ h1 {
 }
 
 @media (min-width: 769px) {
-  .queryForm.app-query-form,
-  .query-row {
+  /* flex 布局仅用于外层筛选行;勿作用于 form.queryForm,否则内层 grid 行无法撑满宽度 */
+  .query-row,
+  .queryForm.app-query-form.query-row {
     display: flex !important;
     flex-wrap: nowrap !important;
     align-items: flex-start;
   }
 
-  .queryForm.app-query-form > .arco-col:first-child,
-  .query-row .query-main {
+  .query-row .query-main,
+  .queryForm.app-query-form.query-row > .arco-col:first-child {
     flex: 1 1 auto !important;
     max-width: 100% !important;
     min-width: 0;
   }
 
-  .queryForm.app-query-form > .arco-col:last-child,
   .query-row > .arco-col[style*='text-align: right'],
-  .query-row .app-query-actions {
+  .query-row .app-query-actions,
+  .queryForm.app-query-form.query-row > .arco-col:last-child {
     flex: 0 0 86px !important;
     max-width: 86px !important;
     text-align: right;
   }
+
+  .queryForm.app-query-form.arco-form {
+    display: block;
+    width: 100%;
+  }
+
+  .queryForm.app-query-form.arco-form > .arco-row {
+    width: 100%;
+  }
 }
 
 @media (max-width: 768px) {
@@ -183,31 +193,31 @@ h1 {
     margin-top: 6px;
   }
 
-  .queryForm.app-query-form {
+  .queryForm.app-query-form.query-row {
     display: block;
   }
 
-  .queryForm.app-query-form > .arco-col {
+  .queryForm.app-query-form.query-row > .arco-col {
     flex: 0 0 100% !important;
     max-width: 100% !important;
   }
 
-  .queryForm.app-query-form > .arco-col:last-child {
+  .queryForm.app-query-form.query-row > .arco-col:last-child {
     margin-top: 6px;
     text-align: left !important;
   }
 
-  .queryForm.app-query-form > .arco-col:last-child .arco-space {
+  .queryForm.app-query-form.query-row > .arco-col:last-child .arco-space {
     width: 100%;
     flex-direction: row !important;
     gap: 10px !important;
   }
 
-  .queryForm.app-query-form > .arco-col:last-child .arco-space .arco-space-item {
+  .queryForm.app-query-form.query-row > .arco-col:last-child .arco-space .arco-space-item {
     flex: 1;
   }
 
-  .queryForm.app-query-form > .arco-col:last-child .arco-btn {
+  .queryForm.app-query-form.query-row > .arco-col:last-child .arco-btn {
     width: 100%;
   }
 

+ 78 - 1
src/styles/store-theme.less

@@ -204,10 +204,72 @@
 }
 
 // —— 筛选表单响应式 ——
-.app-query-form {
+.app-query-form,
+.queryForm {
   .arco-form-item {
     margin-bottom: 12px;
   }
+
+  .arco-form-item-label-col {
+    flex: 0 0 auto !important;
+    min-width: 4.5em;
+  }
+
+  .arco-form-item-label {
+    white-space: nowrap;
+  }
+
+  .arco-input-wrapper,
+  .arco-select-view-single,
+  .arco-select,
+  .arco-picker,
+  .arco-input-number {
+    width: 100%;
+  }
+}
+
+// form 上的 queryForm 保持块级,避免与外层 query-row 的 flex 冲突
+.queryForm.app-query-form.arco-form {
+  display: block;
+  width: 100%;
+
+  > .arco-row {
+    width: 100%;
+  }
+}
+
+// 侧栏布局下内容区较窄时,避免 query-main 固定 1000px 与操作列争抢宽度
+@media (max-width: @app-breakpoint-xl) {
+  .query-row {
+    flex-wrap: wrap !important;
+  }
+
+  .query-main {
+    flex: 1 1 100% !important;
+    max-width: 100% !important;
+    width: 100% !important;
+    min-width: 0;
+  }
+
+  .query-divider {
+    display: none !important;
+  }
+
+  .app-query-actions {
+    flex: 0 0 100% !important;
+    max-width: 100% !important;
+    margin-top: 4px;
+  }
+
+  .query-actions-space {
+    flex-direction: row !important;
+    gap: 10px !important;
+  }
+
+  .query-actions-space :deep(.arco-btn),
+  .query-actions-space .arco-btn {
+    flex: 1;
+  }
 }
 
 // —— 移动端筛选折叠(桌面端始终展示表单项)——
@@ -346,6 +408,21 @@
 }
 
 @media (max-width: @app-breakpoint-lg) {
+  .query-row {
+    flex-wrap: wrap !important;
+  }
+
+  .query-main {
+    flex: 0 0 100% !important;
+    max-width: 100% !important;
+    width: 100% !important;
+    min-width: 0;
+  }
+
+  .query-divider {
+    display: none !important;
+  }
+
   .app-query-form .arco-row > .arco-col,
   .queryForm .arco-row > .arco-col {
     flex: 0 0 100% !important;

+ 57 - 7
src/utils/lepaoRecord.js

@@ -45,10 +45,20 @@ function translateRunStatus(raw) {
   return s
 }
 
+function translateCampusStatus(raw) {
+  const s = normalizeText(raw).toUpperCase()
+  if (!s) return '—'
+  if (s === 'INCAMPUS' || s.includes('IN_CAMPUS')) return '校内有效'
+  if (s === 'OUTCAMPUS' || s.includes('OUT_CAMPUS') || s.includes('OUTSIDE')) return '校外无效'
+  return translateRunStatus(raw)
+}
+
 function isCampusOk(raw) {
   const s = normalizeText(raw)
   if (!s) return true
   const k = s.toLowerCase()
+  if (/out.?campus|outside|校外/i.test(k)) return false
+  if (/in.?campus|incampus|校内/i.test(k)) return true
   if (/(invalid|fail|failed|error|abnormal|exception|reject)/i.test(s)) return false
   if (/(valid|success|passed|pass|ok|normal)/i.test(s)) return true
   return true
@@ -66,6 +76,7 @@ export function parseLepaoRecordRow(record) {
     record?.path_distance != null ? normalizeJkesDistanceToKm(record.path_distance) : null
   const plannedKm = r?.planned_km != null ? Number(r.planned_km) : null
   const deductedKm = r?.deducted_km != null ? Number(r.deducted_km) : null
+  const actualKm = r?.actual_km != null ? Number(r.actual_km) : null
   const officialKm = r?.official_km != null ? Number(r.official_km) : null
   const refundedKm = r?.refunded_km != null ? Number(r.refunded_km) : null
   const expectedKm =
@@ -89,6 +100,7 @@ export function parseLepaoRecordRow(record) {
       pathDistanceKm,
       plannedKm,
       deductedKm,
+      actualKm,
       officialKm,
       refundedKm,
       expectedKm,
@@ -114,18 +126,20 @@ export function parseLepaoRecordRow(record) {
     const runStatus = jkes?.run_status ?? r.status?.value ?? r.status ?? ''
     const campus = jkes?.campus_status ?? r.dataStatus?.value ?? r.dataStatus ?? ''
     const runText = translateRunStatus(runStatus)
+    const campusText = translateCampusStatus(campus)
     const campusOk = isCampusOk(campus)
     const ok = runText === '自动确认有效' && campusOk
 
     return {
       isJkes: true,
       statusText: runText,
-      passTit: campus ? String(campus) : '—',
-      campusStatusText: campus ? String(campus) : '—',
+      passTit: campusText,
+      campusStatusText: campusText,
       routeRunZoneName,
       pathDistanceKm,
       plannedKm,
       deductedKm,
+      actualKm,
       officialKm,
       refundedKm,
       expectedKm,
@@ -148,6 +162,7 @@ export function parseLepaoRecordRow(record) {
     pathDistanceKm,
     plannedKm,
     deductedKm,
+    actualKm,
     officialKm,
     refundedKm,
     expectedKm,
@@ -188,16 +203,25 @@ export function buildRecordDetailInfo(data, options = {}) {
   const row = parseLepaoRecordRow({
     result: data?.result,
     jkes_record: data?.jkes_record,
+    state: data?.state,
     run_zone_name: data?.run_zone_name,
     path_run_zone_name: data?.path_run_zone_name,
     path_distance: data?.path_distance
   })
   const lines = [
     { label: '账号名称', value: data?.name ?? '—' },
-    { label: '乐跑账号', value: data?.lepao_account ?? '—' },
-    { label: '路线跑区', value: row.routeRunZoneName ?? '—' },
-    { label: '记录时间', value: data?.time != null ? new Date(data.time).toLocaleString('zh-CN') : '—' }
+    { label: '乐跑账号', value: data?.lepao_account ?? '—' }
   ]
+  if (data?.run_mode) {
+    lines.push({
+      label: '乐跑类型',
+      value: data.run_mode === 'manual' ? '手动乐跑' : '自动乐跑'
+    })
+  }
+  lines.push(
+    { label: '路线跑区', value: row.routeRunZoneName ?? '—' },
+    { label: '记录时间', value: data?.time != null ? new Date(Number(data.time)).toLocaleString('zh-CN') : '—' }
+  )
 
   if (row.isJkes) {
     const begin = data?.jkes_record?.begin_time ?? normalizeResult(data).beginTime
@@ -215,6 +239,9 @@ export function buildRecordDetailInfo(data, options = {}) {
       })
     }
     lines.push({ label: '跑步状态', value: row.statusText })
+    if (row.campusStatusText && row.campusStatusText !== '—') {
+      lines.push({ label: '校内状态', value: row.campusStatusText })
+    }
   } else {
     const r = normalizeResult(data)
     if (r.start_time) {
@@ -231,13 +258,16 @@ export function buildRecordDetailInfo(data, options = {}) {
     lines.push({ label: '预计距离', value: formatKm(row.expectedKm) })
   }
   if (row.deductedKm != null && Number.isFinite(row.deductedKm)) {
-    lines.push({ label: '扣费里程', value: formatKm(row.deductedKm) })
+    lines.push({ label: '预扣里程', value: formatKm(row.deductedKm) })
+  }
+  if (row.actualKm != null && Number.isFinite(row.actualKm)) {
+    lines.push({ label: '实际距离', value: formatKm(row.actualKm) })
   }
   if (row.refundedKm != null && Number.isFinite(row.refundedKm) && row.refundedKm > 0) {
     lines.push({ label: '退还里程', value: formatKm(row.refundedKm) })
   }
   if (row.officialKm != null && Number.isFinite(row.officialKm)) {
-    lines.push({ label: '实际扣费里程', value: formatKm(row.officialKm) })
+    lines.push({ label: '扣费里程', value: formatKm(row.officialKm) })
   }
   lines.push({ label: '跑步时长', value: formatSecondsToMinSec(row.timeSec) })
   lines.push({
@@ -313,3 +343,23 @@ export function formatAutoDistanceRangeLabel(minKm, maxKm) {
   const b = formatKm(maxKm ?? 2)
   return a === b ? a : `${a} ~ ${b}`
 }
+
+/** 进行中记录的 GPS 分段进度(0–1),供列表进度条使用 */
+export function getLepaoSchedulePercent(record) {
+  if (Number(record?.state) !== 0) return null
+  const schedule = record?.lepao_schedule ?? record?.schedule
+  if (!schedule) return 0
+  const current = Number(schedule.current)
+  const total = Number(schedule.total)
+  if (!Number.isFinite(total) || total <= 0) return 0
+  const done = Number.isFinite(current) ? Math.max(0, current) : 0
+  return Math.min(1, done / total)
+}
+
+export function isRunningLepaoRecord(record) {
+  return Number(record?.state) === 0
+}
+
+export function hasRunningLepaoRecord(records) {
+  return Array.isArray(records) && records.some((r) => Number(r?.state) === 0)
+}

+ 1 - 1
src/utils/request.js

@@ -79,7 +79,7 @@ request.interceptors.request.use(config => {
   }
 
   if (electron)
-    config.headers["Device-Type"] = '哪吒乐跑 Desktop Client'
+    config.headers["Device-Type"] = '沐晨乐跑 Desktop Client'
 
   return config
 }, errorHandler)

Some files were not shown because too many files changed in this diff