Переглянути джерело

运营平台前端重构工程

skyline 2 місяців тому
батько
коміт
0d261d3275

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

@@ -15,7 +15,7 @@
   "HideTabs": false,
   "HideFooter": false,
   "Stretch": false,
-  "SidebarStatus": true,
+  "SidebarStatus": false,
   "EpThemeColor": "#409EFF",
   "ShowLogo": true,
   "ShowModel": "smart",

+ 11 - 1
admin-web-new/src/App.vue

@@ -7,7 +7,7 @@
 </template>
 
 <script lang="ts">
-import { defineComponent } from "vue";
+import { defineComponent, onMounted } from "vue";
 import { checkVersion } from "version-rocket";
 import { ElConfigProvider } from "element-plus";
 import { ReDialog } from "@/components/ReDialog";
@@ -16,6 +16,8 @@ import en from "element-plus/es/locale/lang/en";
 import zhCn from "element-plus/es/locale/lang/zh-cn";
 import plusEn from "plus-pro-components/es/locale/lang/en";
 import plusZhCn from "plus-pro-components/es/locale/lang/zh-cn";
+import dictUtil from "@/utils/dict";
+import { getToken } from "@/utils/auth";
 
 export default defineComponent({
   name: "app",
@@ -53,6 +55,14 @@ export default defineComponent({
         }
       );
     }
+  },
+  setup() {
+    onMounted(() => {
+      const token = getToken();
+      if (token?.accessToken) {
+        dictUtil.loadDicts();
+      }
+    });
   }
 });
 </script>

+ 2 - 7
admin-web-new/src/components/ExtForm/ExtDLabel.vue

@@ -4,6 +4,7 @@
 
 <script setup lang="ts">
 import { onMounted, reactive, watch } from "vue";
+import dictUtil from "@/utils/dict";
 
 defineOptions({ name: "ExtDLabel" });
 
@@ -56,11 +57,6 @@ const setupColorStyle = (hex: string = "#000000") => {
   };
 };
 
-const getDicts = () => {
-  const value = sessionStorage.getItem("dicts");
-  return value ? JSON.parse(value) : {};
-};
-
 const setupLabel = () => {
   if (props.dataRange && props.dataRange.length > 0) {
     const data = props.dataRange.find(k => k.value == props.modelValue);
@@ -69,8 +65,7 @@ const setupLabel = () => {
       state.style = setupColorStyle(data.color || "#000000");
     }
   } else {
-    const dicts = getDicts();
-    const dictList = dicts[props.type] || [];
+    const dictList = dictUtil.getDictList(props.type);
     const dict = dictList.find(
       (k: any) => k.value == props.modelValue || k.value === String(props.modelValue)
     );

+ 2 - 8
admin-web-new/src/components/ExtForm/ExtDSelect.vue

@@ -24,6 +24,7 @@
 
 <script setup lang="ts">
 import { onMounted, reactive, watch, nextTick } from "vue";
+import dictUtil from "@/utils/dict";
 
 defineOptions({ name: "ExtDSelect" });
 
@@ -64,12 +65,6 @@ const state = reactive({
   dataVal: null as any
 });
 
-// Session 工具
-const getDicts = () => {
-  const value = sessionStorage.getItem("dicts");
-  return value ? JSON.parse(value) : {};
-};
-
 watch(
   () => props.modelValue,
   val => {
@@ -93,8 +88,7 @@ const setupDicts = () => {
   if (props.dataRange && props.dataRange.length > 0) {
     state.dicts = props.dataRange;
   } else {
-    const dicts = getDicts();
-    state.dicts = dicts[props.type] || [];
+    state.dicts = dictUtil.getDictList(props.type);
   }
 };
 

+ 1 - 23
admin-web-new/src/layout/components/lay-tag/index.vue

@@ -664,29 +664,7 @@ onBeforeUnmount(() => {
       </ul>
     </transition>
     <!-- 右侧功能按钮 -->
-    <el-dropdown
-      trigger="click"
-      placement="bottom-end"
-      @command="handleCommand"
-    >
-      <span class="arrow-down">
-        <IconifyIconOffline :icon="ArrowDown" class="dark:text-white" />
-      </span>
-      <template #dropdown>
-        <el-dropdown-menu>
-          <el-dropdown-item
-            v-for="(item, key) in tagsViews"
-            :key="key"
-            :command="{ key, item }"
-            :divided="item.divided"
-            :disabled="item.disabled"
-          >
-            <IconifyIconOffline :icon="item.icon" />
-            {{ transformI18n(item.text) }}
-          </el-dropdown-item>
-        </el-dropdown-menu>
-      </template>
-    </el-dropdown>
+    <!-- 已移除折叠按钮 -->
   </div>
 </template>
 

+ 14 - 11
admin-web-new/src/router/modules/admin.ts

@@ -5,10 +5,6 @@ export default {
   name: "Root",
   component: Layout,
   redirect: "/admin/dashboard",
-  meta: {
-    icon: "ri:dashboard-line",
-    title: "menus.pureHome"
-  },
   children: [
     // 信息总览
     {
@@ -26,7 +22,8 @@ export default {
       path: "/admin/station",
       meta: {
         icon: "ri:map-pin-2-line",
-        title: "站点管理"
+        title: "站点管理",
+        rank: 1
       },
       children: [
         {
@@ -65,7 +62,8 @@ export default {
       component: () => import("@/views/admin/ordering/index.vue"),
       meta: {
         icon: "ri:file-list-3-line",
-        title: "订单管理"
+        title: "订单管理",
+        rank: 2
       }
     },
     // 用户管理
@@ -75,7 +73,8 @@ export default {
       component: () => import("@/views/admin/user/index.vue"),
       meta: {
         icon: "ri:user-line",
-        title: "用户管理"
+        title: "用户管理",
+        rank: 3
       }
     },
     // 财务管理(包含子菜单)
@@ -83,7 +82,8 @@ export default {
       path: "/admin/finance",
       meta: {
         icon: "ri:copper-coin-line",
-        title: "财务管理"
+        title: "财务管理",
+        rank: 4
       },
       children: [
         {
@@ -131,7 +131,8 @@ export default {
       component: () => import("@/views/admin/banner/index.vue"),
       meta: {
         icon: "ri:image-line",
-        title: "横幅广告"
+        title: "横幅广告",
+        rank: 5
       }
     },
     // 平台配置(包含子菜单)
@@ -139,7 +140,8 @@ export default {
       path: "/admin/platform",
       meta: {
         icon: "ri:settings-3-line",
-        title: "平台配置"
+        title: "平台配置",
+        rank: 6
       },
       children: [
         {
@@ -167,7 +169,8 @@ export default {
       path: "/admin/system",
       meta: {
         icon: "ri:settings-6-line",
-        title: "系统配置"
+        title: "系统配置",
+        rank: 7
       },
       children: [
         {

+ 99 - 0
admin-web-new/src/utils/dict.ts

@@ -0,0 +1,99 @@
+import { storageSession } from "@pureadmin/utils";
+import { getDictList } from "@/api/dict";
+
+export interface DictItem {
+  id?: number;
+  code: string;
+  name: string;
+  value: any;
+  weight?: number;
+  remark?: string;
+  list?: DictItem[];
+}
+
+export interface DictGroup {
+  [code: string]: DictItem[];
+}
+
+const DICT_KEY = "dicts";
+
+const dictUtil = {
+  loadDicts: async (): Promise<DictGroup> => {
+    try {
+      const list: any[] = await getDictList({ pageSize: 1024 });
+      const dictGroup = dictUtil.groupByKey(list, "code");
+      storageSession().setItem(DICT_KEY, dictGroup);
+      return dictGroup;
+    } catch (error) {
+      console.error("加载字典数据失败:", error);
+      return {};
+    }
+  },
+
+  getDicts: (): DictGroup => {
+    return storageSession().getItem(DICT_KEY) || {};
+  },
+
+  getDictList: (code: string): DictItem[] => {
+    const dicts = dictUtil.getDicts();
+    return dicts[code] || [];
+  },
+
+  getDictLabel: (code: string, value: any): string => {
+    if (value === null || value === undefined || value === "") {
+      return "--";
+    }
+    const dicts = dictUtil.getDicts();
+    const list = dicts[code];
+    if (!list || list.length === 0) {
+      return "--";
+    }
+    const item = list.find(k => k.value == value);
+    return item ? item.name : "--";
+  },
+
+  getDictValue: (code: string, name: string): any => {
+    const dicts = dictUtil.getDicts();
+    const list = dicts[code];
+    if (!list || list.length === 0) {
+      return null;
+    }
+    const item = list.find(k => k.name === name);
+    return item ? item.value : null;
+  },
+
+  groupByKey: (elements: any[], key: string): DictGroup => {
+    const map: DictGroup = {};
+    if (!elements || elements.length === 0) {
+      return map;
+    }
+    for (let i = 0; i < elements.length; i++) {
+      if (!elements[i][key] && elements[i][key] !== 0) {
+        continue;
+      }
+      const k = elements[i][key].toString();
+      const tmp = map[k] || [];
+      tmp.push(elements[i]);
+      map[k] = tmp;
+    }
+    return map;
+  },
+
+  clearDicts: () => {
+    storageSession().removeItem(DICT_KEY);
+  }
+};
+
+export const formatDict = (code: string, value: any): string => {
+  return dictUtil.getDictLabel(code, value);
+};
+
+export const getDictOptions = (code: string): { label: string; value: any }[] => {
+  const list = dictUtil.getDictList(code);
+  return list.map(item => ({
+    label: item.name,
+    value: item.value
+  }));
+};
+
+export default dictUtil;

+ 25 - 20
admin-web-new/src/views/admin/account/index.vue

@@ -2,6 +2,7 @@
 import { reactive, onMounted, ref, nextTick } from "vue";
 import { getAdminUserList } from "@/api/admin";
 import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+import { ExtDLabel, ExtDSelect } from "@/components/ExtForm";
 
 defineOptions({ name: "AdminAccount" });
 
@@ -30,25 +31,12 @@ const state = reactive({
     data: [] as Array<any>,
     loading: false,
     columns: [
-      { label: "用户ID", prop: "userId", width: 200 },
-      { label: "归属站点", prop: "stationName", width: 200 },
-      { label: "手机号", prop: "mobilePhone", width: 120 },
-      { label: "总余额", prop: "balance", width: 80 },
-      { label: "充值余额", prop: "rechargeBalance", width: 90 },
-      { label: "赠金余额", prop: "grantsBalance", width: 90 },
-      { label: "冻结余额", prop: "frozenAmount", width: 90 },
-      { label: "状态", prop: "status", width: 80 },
-      { label: "注册时间", prop: "registerTime", width: 160 },
-      { label: "充值次数", prop: "rechargeTimes", width: 90 },
-      { label: "充值金额", prop: "rechargeAmount", width: 90 },
-      { label: "退款次数", prop: "refundTimes", width: 90 },
-      { label: "退款金额", prop: "refundAmount", width: 90 },
-      { label: "洗车次数", prop: "washTimes", width: 90 },
-      { label: "消费总额", prop: "amount", width: 100 },
-      { label: "应付总额", prop: "amountReceivable", width: 100 },
-      { label: "实付总额", prop: "amountReceived", width: 100 },
-      { label: "优惠总额", prop: "discountAmount", width: 100 },
-      { label: "退款扣除优惠", prop: "refundDiscountAmount", width: 125 }
+      { label: "用户名", prop: "username", width: 120 },
+      { label: "昵称", prop: "nickname", width: 180 },
+      { label: "手机号", prop: "mobilePhone", width: 130 },
+      { label: "状态", prop: "status", width: 100 },
+      { label: "最后登录时间", prop: "lastLoginTime", width: 180 },
+      { label: "创建时间", prop: "createTime", width: 180 }
     ] as ColumnItem[]
   }
 });
@@ -119,6 +107,14 @@ const handleReset = () => {
         <el-form-item label="手机号">
           <el-input v-model="state.formQuery.mobilePhone" placeholder="请输入手机号" clearable @keyup.enter="handleSearch" />
         </el-form-item>
+        <el-form-item label="状态">
+          <ExtDSelect
+            v-model="state.formQuery.status"
+            type="AdminUser.status"
+            placeholder="请选择状态"
+            @on-change="handleSearch"
+          />
+        </el-form-item>
         <el-form-item>
           <el-button
             type="primary"
@@ -152,7 +148,16 @@ const handleReset = () => {
           :label="col.label"
           :width="col.width"
           show-overflow-tooltip
-        />
+        >
+          <template #default="{ row }">
+            <template v-if="col.prop === 'status'">
+              <ExtDLabel type="AdminUser.status" :model-value="row.status" />
+            </template>
+            <template v-else>
+              {{ row[col.prop] }}
+            </template>
+          </template>
+        </el-table-column>
         <el-table-column label="操作" width="150" fixed="right">
           <template #default>
             <el-button type="primary" link size="small">编辑</el-button>

+ 7 - 42
admin-web-new/src/views/admin/finance/refund.vue

@@ -4,6 +4,7 @@ import { getRefundList } from "@/api/finance";
 import { useRenderIcon } from "@/components/ReIcon/src/hooks";
 import { http } from "@/utils/http";
 import { ElMessage, ElMessageBox } from "element-plus";
+import { ExtDLabel, ExtDSelect } from "@/components/ExtForm";
 
 defineOptions({
   name: "AdminFinanceRefund"
@@ -39,14 +40,7 @@ const state = reactive({
       { label: "退款原因", prop: "reason", width: 180 },
       { label: "退款人", prop: "adminUsername", width: 180 }
     ]
-  },
-  statusOptions: [
-    { label: "全部", value: "" },
-    { label: "待处理", value: "NEW" },
-    { label: "成功", value: "SUCCESS" },
-    { label: "失败", value: "FAIL" },
-    { label: "退款关闭", value: "CLOSED" }
-  ]
+  }
 });
 
 onMounted(() => {
@@ -128,26 +122,6 @@ const formatMoney = (value: number) => {
   if (value === null || value === undefined) return "0.00";
   return (value / 100).toFixed(2);
 };
-
-const getStatusLabel = (status: string) => {
-  const map: Record<string, string> = {
-    NEW: "待处理",
-    SUCCESS: "成功",
-    FAIL: "失败",
-    CLOSED: "退款关闭"
-  };
-  return map[status] || status;
-};
-
-const getStatusType = (status: string) => {
-  const map: Record<string, string> = {
-    NEW: "warning",
-    SUCCESS: "success",
-    FAIL: "danger",
-    CLOSED: "info"
-  };
-  return map[status] || "info";
-};
 </script>
 
 <template>
@@ -168,19 +142,12 @@ const getStatusType = (status: string) => {
           />
         </el-form-item>
         <el-form-item label="退款状态">
-          <el-select
+          <ExtDSelect
             v-model="state.formQuery.status"
+            type="Refund.status"
             placeholder="请选择状态"
-            clearable
-            @change="handleSearch"
-          >
-            <el-option
-              v-for="item in state.statusOptions"
-              :key="item.value"
-              :label="item.label"
-              :value="item.value"
-            />
-          </el-select>
+            @on-change="handleSearch"
+          />
         </el-form-item>
         <el-form-item>
           <el-button
@@ -223,9 +190,7 @@ const getStatusType = (status: string) => {
               {{ formatMoney(row[col.prop]) }}
             </template>
             <template v-else-if="col.prop === 'status'">
-              <el-tag :type="getStatusType(row.status)" size="small">
-                {{ getStatusLabel(row.status) }}
-              </el-tag>
+              <ExtDLabel type="Refund.status" :model-value="row.status" />
             </template>
             <template v-else>
               {{ row[col.prop] }}

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

@@ -3,6 +3,7 @@ import { reactive, onMounted, ref, nextTick } from "vue";
 import { getSplitRecordList } from "@/api/finance";
 import { getStationList } from "@/api/station";
 import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+import { ExtDLabel, ExtDSelect } from "@/components/ExtForm";
 
 defineOptions({
   name: "AdminFinanceSplitRecord"
@@ -102,14 +103,6 @@ const formatMoney = (value: number) => {
   if (value === null || value === undefined) return "0.00";
   return (value / 100).toFixed(2);
 };
-
-const getTypeText = (type: string) => {
-  const map: Record<string, string> = {
-    "1": "分账",
-    "2": "分账回退"
-  };
-  return map[type] || type;
-};
 </script>
 
 <template>
@@ -146,15 +139,12 @@ const getTypeText = (type: string) => {
           />
         </el-form-item>
         <el-form-item label="分账类型">
-          <el-select
+          <ExtDSelect
             v-model="state.formQuery.type"
+            type="SplitRecord.type"
             placeholder="请选择分账类型"
-            clearable
-            @change="handleSearch"
-          >
-            <el-option label="分账" value="1" />
-            <el-option label="分账回退" value="2" />
-          </el-select>
+            @on-change="handleSearch"
+          />
         </el-form-item>
         <el-form-item>
           <el-button
@@ -211,7 +201,7 @@ const getTypeText = (type: string) => {
               <span class="money">¥{{ formatMoney(row.amount) }}</span>
             </template>
             <template v-else-if="col.prop === 'type'">
-              {{ getTypeText(row.type) }}
+              <ExtDLabel type="SplitRecord.type" :model-value="row.type" />
             </template>
             <template v-else>
               {{ row[col.prop] }}

+ 136 - 24
admin-web-new/src/views/admin/log/index.vue

@@ -2,24 +2,21 @@
 import { reactive, onMounted, ref, nextTick } from "vue";
 import { http } from "@/utils/http";
 import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+import { ElMessage, ElMessageBox } from "element-plus";
 
 defineOptions({ name: "AdminLog" });
 
 const queryRef = ref();
-
-interface ColumnItem {
-  label: string;
-  prop: string;
-  width?: number;
-}
+const detailVisible = ref(false);
+const currentLog = ref<any>(null);
 
 const state = reactive({
   formQuery: {
-    operation: "",
     username: "",
-    startTime: "",
-    endTime: ""
+    startDate: "",
+    endDate: ""
   },
+  dateRange: [] as string[],
   pageQuery: {
     pageNum: 1,
     pageSize: 10,
@@ -30,12 +27,14 @@ const state = reactive({
     data: [] as Array<any>,
     loading: false,
     columns: [
-      { label: "操作", prop: "operation", width: 200 },
-      { label: "操作人", prop: "username", width: 150 },
-      { label: "IP地址", prop: "ip", width: 150 },
-      { label: "操作时间", prop: "createTime", width: 180 },
-      { label: "详情", prop: "detail" }
-    ] as ColumnItem[]
+      { label: "ID", prop: "id", width: 80 },
+      { label: "操作用户", prop: "username", width: 120 },
+      { label: "操作名称", prop: "operation", width: 200 },
+      { label: "请求方法", prop: "method", width: 150 },
+      { label: "IP地址", prop: "ip", width: 130 },
+      { label: "耗时(ms)", prop: "executeTime", width: 100 },
+      { label: "操作时间", prop: "createTime", width: 170 }
+    ]
   }
 });
 
@@ -52,8 +51,16 @@ const loadData = (refresh: boolean = false) => {
   if (refresh) {
     state.pageQuery.pageNum = 1;
   }
+  
+  if (state.dateRange && state.dateRange.length === 2) {
+    state.formQuery.startDate = state.dateRange[0];
+    state.formQuery.endDate = state.dateRange[1];
+  } else {
+    state.formQuery.startDate = "";
+    state.formQuery.endDate = "";
+  }
+  
   state.tableData.loading = true;
-  // TODO: 后端日志接口待实现
   http.request<any>("get", "/system-log/list", { params: { ...state.formQuery, ...state.pageQuery } })
     .then((res: any) => {
       const { list, total } = res || {};
@@ -84,24 +91,61 @@ const handleSearch = () => {
 
 const handleReset = () => {
   state.formQuery = {
-    operation: "",
     username: "",
-    startTime: "",
-    endTime: ""
+    startDate: "",
+    endDate: ""
   };
+  state.dateRange = [];
   loadData(true);
 };
+
+const handleDetail = (row: any) => {
+  currentLog.value = row;
+  detailVisible.value = true;
+};
+
+const handleClearLog = async () => {
+  await ElMessageBox.confirm("确定要清空所有操作日志吗?此操作不可恢复!", "警告", { type: "warning" });
+  try {
+    await http.request("post", "/system-log/clear");
+    ElMessage.success("清空成功");
+    loadData(true);
+  } catch (error) {
+    ElMessage.error("清空失败");
+  }
+};
+
+const getExecuteTimeType = (time: number) => {
+  if (time > 1000) return "danger";
+  if (time > 500) return "warning";
+  return "success";
+};
 </script>
 
 <template>
   <div class="page-container">
     <el-card shadow="hover">
       <el-form ref="queryRef" :model="state.formQuery" inline class="search-form">
-        <el-form-item label="操作">
-          <el-input v-model="state.formQuery.operation" placeholder="请输入操作关键词" clearable @keyup.enter="handleSearch" />
+        <el-form-item label="操作用户">
+          <el-input
+            v-model="state.formQuery.username"
+            placeholder="请输入操作用户"
+            clearable
+            style="width: 150px"
+            @keyup.enter="handleSearch"
+          />
         </el-form-item>
-        <el-form-item label="操作人">
-          <el-input v-model="state.formQuery.username" placeholder="请输入操作人" clearable @keyup.enter="handleSearch" />
+        <el-form-item label="操作时间">
+          <el-date-picker
+            v-model="state.dateRange"
+            type="daterange"
+            range-separator="至"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            value-format="YYYY-MM-DD"
+            style="width: 240px"
+            @change="handleSearch"
+          />
         </el-form-item>
         <el-form-item>
           <el-button
@@ -117,8 +161,17 @@ const handleReset = () => {
           >
             重置
           </el-button>
+          <el-button
+            type="danger"
+            :icon="useRenderIcon('ri/delete-bin-line')"
+            @click="handleClearLog"
+            :disabled="state.tableData.data.length === 0"
+          >
+            清空日志
+          </el-button>
         </el-form-item>
       </el-form>
+
       <el-table
         v-loading="state.tableData.loading"
         :data="state.tableData.data"
@@ -136,8 +189,27 @@ const handleReset = () => {
           :label="col.label"
           :width="col.width"
           show-overflow-tooltip
-        />
+        >
+          <template #default="{ row }">
+            <template v-if="col.prop === 'executeTime'">
+              <el-tag :type="getExecuteTimeType(row.executeTime)" size="small">
+                {{ row.executeTime }}
+              </el-tag>
+            </template>
+            <template v-else>
+              {{ row[col.prop] }}
+            </template>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="80" fixed="right">
+          <template #default="{ row }">
+            <el-button type="primary" link size="small" @click="handleDetail(row)">
+              详情
+            </el-button>
+          </template>
+        </el-table-column>
       </el-table>
+
       <div class="pagination-container">
         <el-pagination
           v-model:current-page="state.pageQuery.pageNum"
@@ -150,6 +222,30 @@ const handleReset = () => {
         />
       </div>
     </el-card>
+
+    <!-- 详情对话框 -->
+    <el-dialog v-model="detailVisible" title="日志详情" width="600px">
+      <el-descriptions v-if="currentLog" :column="2" border>
+        <el-descriptions-item label="操作用户">{{ currentLog.username }}</el-descriptions-item>
+        <el-descriptions-item label="IP地址">{{ currentLog.ip }}</el-descriptions-item>
+        <el-descriptions-item label="操作名称" :span="2">{{ currentLog.operation }}</el-descriptions-item>
+        <el-descriptions-item label="请求方法" :span="2">{{ currentLog.method }}</el-descriptions-item>
+        <el-descriptions-item label="耗时">{{ currentLog.executeTime }}ms</el-descriptions-item>
+        <el-descriptions-item label="操作时间">{{ currentLog.createTime }}</el-descriptions-item>
+      </el-descriptions>
+      <div v-if="currentLog && currentLog.requestParam" class="detail-section">
+        <div class="section-title">请求参数</div>
+        <el-input
+          type="textarea"
+          :rows="6"
+          :model-value="currentLog.requestParam"
+          readonly
+        />
+      </div>
+      <template #footer>
+        <el-button @click="detailVisible = false">关闭</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
@@ -167,4 +263,20 @@ const handleReset = () => {
   justify-content: flex-end;
   margin-top: 15px;
 }
+
+.detail-section {
+  margin-top: 15px;
+
+  .section-title {
+    font-weight: 500;
+    margin-bottom: 10px;
+    color: var(--el-text-color-primary);
+  }
+
+  :deep(.el-textarea__inner) {
+    font-family: monospace;
+    font-size: 13px;
+    background: var(--el-fill-color-lighter);
+  }
+}
 </style>

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

@@ -2,6 +2,8 @@
 import { reactive, onMounted, ref, nextTick } from "vue";
 import { http } from "@/utils/http";
 import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+import { ExtDLabel, ExtDSelect } from "@/components/ExtForm";
+import { formatDict } from "@/utils/dict";
 import OrderDialog from "./dialog.vue";
 
 defineOptions({
@@ -111,62 +113,38 @@ const formatMoney = (value: number) => {
   return (value / 100).toFixed(2);
 };
 
-const getCloseTypeText = (type: string) => {
-  const map: Record<string, string> = {
-    "0": "手动关机",
-    "1": "自动关机",
-    "2": "远程关机"
-  };
-  return map[type] || type;
-};
-
-const getOrderStatusText = (status: string) => {
-  const map: Record<string, string> = {
-    "0": "进行中",
-    "1": "已完成",
-    "2": "已取消"
-  };
-  return map[status] || status;
-};
-
-const getOrderStatusType = (status: string) => {
-  const map: Record<string, string> = {
-    "0": "warning",
-    "1": "success",
-    "2": "info"
-  };
-  return map[status] || "info";
-};
-
-const getPayStatusText = (status: string) => {
-  const map: Record<string, string> = {
-    "0": "待支付",
-    "1": "已支付",
-    "2": "已退款"
-  };
-  return map[status] || status;
-};
-
-const getPayStatusType = (status: string) => {
-  const map: Record<string, string> = {
-    "0": "warning",
-    "1": "success",
-    "2": "info"
-  };
-  return map[status] || "info";
-};
-
-const getInvoiceStatusText = (status: string) => {
-  const map: Record<string, string> = {
-    "0": "未开票",
-    "1": "已开票"
-  };
-  return map[status] || status;
+const formatDuration = (ms: number) => {
+  if (!ms) return "0秒";
+  const seconds = Math.floor(ms / 1000);
+  const minutes = Math.floor(seconds / 60);
+  const hours = Math.floor(minutes / 60);
+  
+  const h = hours % 24;
+  const m = minutes % 60;
+  const s = seconds % 60;
+  
+  let str = "";
+  if (h > 0) str += `${h}时`;
+  if (m > 0) str += `${m}分`;
+  if (s > 0 || str === "") str += `${s}秒`;
+  return str;
 };
 
 const isMoneyField = (prop: string) => {
   return ["amount", "amountReceivable", "amountReceived"].includes(prop);
 };
+
+const processDetailData = (detail: any[]) => {
+  if (!detail || !Array.isArray(detail)) return [];
+  return detail
+    .filter((s: any) => s.amount > 0 || s.seconds > 0)
+    .map((s: any) => ({
+      name: formatDict("Order.feeType", s.name),
+      seconds: formatDuration(s.seconds * 1000),
+      price: formatMoney(s.price),
+      amount: formatMoney(s.amount)
+    }));
+};
 </script>
 
 <template>
@@ -195,28 +173,20 @@ const isMoneyField = (prop: string) => {
           />
         </el-form-item>
         <el-form-item label="订单状态">
-          <el-select
+          <ExtDSelect
             v-model="state.formQuery.orderStatus"
+            type="Order.status"
             placeholder="请选择订单状态"
-            clearable
-            @change="handleSearch"
-          >
-            <el-option label="进行中" value="0" />
-            <el-option label="已完成" value="1" />
-            <el-option label="已取消" value="2" />
-          </el-select>
+            @on-change="handleSearch"
+          />
         </el-form-item>
         <el-form-item label="支付状态">
-          <el-select
+          <ExtDSelect
             v-model="state.formQuery.payStatus"
+            type="Order.pay"
             placeholder="请选择支付状态"
-            clearable
-            @change="handleSearch"
-          >
-            <el-option label="待支付" value="0" />
-            <el-option label="已支付" value="1" />
-            <el-option label="已退款" value="2" />
-          </el-select>
+            @on-change="handleSearch"
+          />
         </el-form-item>
         <el-form-item>
           <el-button
@@ -246,6 +216,24 @@ const isMoneyField = (prop: string) => {
         <template #empty>
           <el-empty description="暂无数据" />
         </template>
+        <el-table-column type="expand" width="60">
+          <template #default="{ row }">
+            <div class="order-detail">
+              <el-table
+                v-if="row.detail && row.detail.length > 0"
+                :data="processDetailData(row.detail)"
+                border
+                stripe
+              >
+                <el-table-column width="120" label="服务项目" prop="name" />
+                <el-table-column width="120" label="服务时长" prop="seconds" />
+                <el-table-column width="120" label="服务单价" prop="price" />
+                <el-table-column width="120" label="小计金额" prop="amount" />
+              </el-table>
+              <div v-else class="no-detail">暂无消费详情</div>
+            </div>
+          </template>
+        </el-table-column>
         <el-table-column
           v-for="col in state.tableData.columns"
           :key="col.prop"
@@ -259,20 +247,16 @@ const isMoneyField = (prop: string) => {
               {{ formatMoney(row[col.prop]) }}
             </template>
             <template v-else-if="col.prop === 'orderStatus'">
-              <el-tag :type="getOrderStatusType(row.orderStatus)" size="small">
-                {{ getOrderStatusText(row.orderStatus) }}
-              </el-tag>
+              <ExtDLabel type="Order.status" :model-value="row.orderStatus" />
             </template>
             <template v-else-if="col.prop === 'payStatus'">
-              <el-tag :type="getPayStatusType(row.payStatus)" size="small">
-                {{ getPayStatusText(row.payStatus) }}
-              </el-tag>
+              <ExtDLabel type="Order.pay" :model-value="row.payStatus" />
             </template>
             <template v-else-if="col.prop === 'closeType'">
-              {{ getCloseTypeText(row.closeType) }}
+              <ExtDLabel type="Order.closeType" :model-value="row.closeType" />
             </template>
             <template v-else-if="col.prop === 'invoiceStatus'">
-              {{ getInvoiceStatusText(row.invoiceStatus) }}
+              <ExtDLabel type="Invoice.status" :model-value="row.invoiceStatus" />
             </template>
             <template v-else>
               {{ row[col.prop] }}
@@ -319,4 +303,15 @@ const isMoneyField = (prop: string) => {
   justify-content: flex-end;
   margin-top: 15px;
 }
+
+.order-detail {
+  margin-left: 20px;
+  padding: 10px;
+}
+
+.no-detail {
+  padding: 20px;
+  text-align: center;
+  color: #999;
+}
 </style>

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

@@ -4,6 +4,7 @@ import { getStationDeviceList, removeDevice, getStationList } from "@/api/statio
 import { useRenderIcon } from "@/components/ReIcon/src/hooks";
 import { useRoute, useRouter } from "vue-router";
 import { ElMessage, ElMessageBox } from "element-plus";
+import { ExtDLabel, ExtDSelect } from "@/components/ExtForm";
 import DeviceDialog from "./device-dialog.vue";
 
 defineOptions({
@@ -165,30 +166,6 @@ const handleDelete = (row: any) => {
   });
 };
 
-const getStateText = (status: string) => {
-  const map: Record<string, string> = {
-    "0": "离线",
-    "1": "在线"
-  };
-  return map[status] || status;
-};
-
-const getStateType = (status: string) => {
-  const map: Record<string, string> = {
-    "0": "info",
-    "1": "success"
-  };
-  return map[status] || "info";
-};
-
-const getFoamWaterText = (val: string) => {
-  const map: Record<string, string> = {
-    "0": "否",
-    "1": "是"
-  };
-  return map[val] || val;
-};
-
 const formatDuration = (ms: number) => {
   if (!ms) return "-";
   const seconds = Math.floor(ms / 1000);
@@ -258,37 +235,28 @@ const formatDuration = (ms: number) => {
           />
         </el-form-item>
         <el-form-item label="状态">
-          <el-select
+          <ExtDSelect
             v-model="state.formQuery.state"
+            type="Device.state"
             placeholder="请选择状态"
-            clearable
-            @change="handleSearch"
-          >
-            <el-option label="离线" value="0" />
-            <el-option label="在线" value="1" />
-          </el-select>
+            @on-change="handleSearch"
+          />
         </el-form-item>
         <el-form-item label="是否有泡沫">
-          <el-select
+          <ExtDSelect
             v-model="state.formQuery.hasFoam"
+            type="YesNo"
             placeholder="请选择"
-            clearable
-            @change="handleSearch"
-          >
-            <el-option label="否" value="0" />
-            <el-option label="是" value="1" />
-          </el-select>
+            @on-change="handleSearch"
+          />
         </el-form-item>
         <el-form-item label="是否有水">
-          <el-select
+          <ExtDSelect
             v-model="state.formQuery.hasWater"
+            type="YesNo"
             placeholder="请选择"
-            clearable
-            @change="handleSearch"
-          >
-            <el-option label="否" value="0" />
-            <el-option label="是" value="1" />
-          </el-select>
+            @on-change="handleSearch"
+          />
         </el-form-item>
         <el-form-item>
           <el-button
@@ -342,15 +310,13 @@ const formatDuration = (ms: number) => {
               </div>
             </template>
             <template v-else-if="col.prop === 'state'">
-              <el-tag :type="getStateType(row.state)" size="small">
-                {{ getStateText(row.state) }}
-              </el-tag>
+              <ExtDLabel type="Device.state" :model-value="row.state" />
             </template>
             <template v-else-if="col.prop === 'uptimeMs'">
               {{ formatDuration(row.uptimeMs) }}
             </template>
             <template v-else-if="col.prop === 'hasFoam' || col.prop === 'hasWater'">
-              {{ getFoamWaterText(row[col.prop]) }}
+              <ExtDLabel type="YesNo" :model-value="row[col.prop]" />
             </template>
             <template v-else>
               {{ row[col.prop] }}

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

@@ -3,6 +3,7 @@ import { reactive, onMounted, ref, nextTick } from "vue";
 import { getStationList, removeStation } from "@/api/station";
 import { useRenderIcon } from "@/components/ReIcon/src/hooks";
 import { ElMessage, ElMessageBox } from "element-plus";
+import { ExtDLabel, ExtDSelect } from "@/components/ExtForm";
 import StationDialog from "./dialog.vue";
 
 defineOptions({
@@ -138,38 +139,6 @@ const handleShowQrCode = (row: any) => {
 const handleGotoDevice = (row: any) => {
   window.location.href = `/#/admin/station/device?stationId=${row.stationId}`;
 };
-
-const getStationStatusText = (status: string) => {
-  const map: Record<string, string> = {
-    "0": "已关闭",
-    "1": "营业中"
-  };
-  return map[status] || status;
-};
-
-const getStationStatusType = (status: string) => {
-  const map: Record<string, string> = {
-    "0": "danger",
-    "1": "success"
-  };
-  return map[status] || "info";
-};
-
-const getStationTypeText = (type: string) => {
-  const map: Record<string, string> = {
-    "1": "公共",
-    "2": "私人"
-  };
-  return map[type] || type;
-};
-
-const getStationTypeType = (type: string) => {
-  const map: Record<string, string> = {
-    "1": "primary",
-    "2": "warning"
-  };
-  return map[type] || "info";
-};
 </script>
 
 <template>
@@ -190,26 +159,20 @@ const getStationTypeType = (type: string) => {
           />
         </el-form-item>
         <el-form-item label="站点状态">
-          <el-select
+          <ExtDSelect
             v-model="state.formQuery.stationStatus"
+            type="Station.status"
             placeholder="请选择站点状态"
-            clearable
-            @change="handleSearch"
-          >
-            <el-option label="营业中" value="1" />
-            <el-option label="已关闭" value="0" />
-          </el-select>
+            @on-change="handleSearch"
+          />
         </el-form-item>
         <el-form-item label="站点类型">
-          <el-select
+          <ExtDSelect
             v-model="state.formQuery.stationType"
+            type="Station.type"
             placeholder="请选择站点类型"
-            clearable
-            @change="handleSearch"
-          >
-            <el-option label="公共" value="1" />
-            <el-option label="私人" value="2" />
-          </el-select>
+            @on-change="handleSearch"
+          />
         </el-form-item>
         <el-form-item label="地址">
           <el-input
@@ -281,14 +244,10 @@ const getStationTypeType = (type: string) => {
               <span v-else>-</span>
             </template>
             <template v-else-if="col.prop === 'stationStatus'">
-              <el-tag :type="getStationStatusType(row.stationStatus)" size="small">
-                {{ getStationStatusText(row.stationStatus) }}
-              </el-tag>
+              <ExtDLabel type="Station.status" :model-value="row.stationStatus" />
             </template>
             <template v-else-if="col.prop === 'stationType'">
-              <el-tag :type="getStationTypeType(row.stationType)" size="small">
-                {{ getStationTypeText(row.stationType) }}
-              </el-tag>
+              <ExtDLabel type="Station.type" :model-value="row.stationType" />
             </template>
             <template v-else-if="col.prop === 'parkingQrCode'">
               <el-button

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

@@ -2,6 +2,7 @@
 import { reactive, onMounted, ref, nextTick } from "vue";
 import { getCustomUserList } from "@/api/custom";
 import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+import { ExtDLabel } from "@/components/ExtForm";
 
 defineOptions({
   name: "AdminUser"
@@ -105,22 +106,6 @@ const formatMoney = (value: number) => {
   return (value / 100).toFixed(2);
 };
 
-const getStatusText = (status: string) => {
-  const map: Record<string, string> = {
-    "0": "正常",
-    "1": "禁用"
-  };
-  return map[status] || status;
-};
-
-const getStatusType = (status: string) => {
-  const map: Record<string, string> = {
-    "0": "success",
-    "1": "danger"
-  };
-  return map[status] || "info";
-};
-
 const isMoneyField = (prop: string) => {
   return [
     "balance",
@@ -204,9 +189,7 @@ const isMoneyField = (prop: string) => {
               {{ formatMoney(row[col.prop]) }}
             </template>
             <template v-else-if="col.prop === 'status'">
-              <el-tag :type="getStatusType(row.status)" size="small">
-                {{ getStatusText(row.status) }}
-              </el-tag>
+              <ExtDLabel type="User.status" :model-value="row.status" />
             </template>
             <template v-else>
               {{ row[col.prop] }}