Эх сурвалжийг харах

1.完成后台管理履约者部分功能开发

steelwei 1 сар өмнө
parent
commit
ca6935d9d6

+ 2 - 24
package-lock.json

@@ -120,7 +120,6 @@
       "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@babel/code-frame": "^7.29.0",
         "@babel/generator": "^7.29.0",
@@ -517,7 +516,6 @@
       "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
       "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@babel/types": "^7.29.0"
       },
@@ -2392,7 +2390,6 @@
       "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
       "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@types/lodash": "*"
       }
@@ -2403,7 +2400,6 @@
       "integrity": "sha512-PD03/U8g1F9T9MI+1OBisaIARhSzeidsUjQaf51fOxrfjeiKN9bLVO06lHuHYjxdnqLWJijJHfqXPSJri2EM2A==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "undici-types": "~6.21.0"
       }
@@ -2466,7 +2462,6 @@
       "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@typescript-eslint/scope-manager": "8.54.0",
         "@typescript-eslint/types": "8.54.0",
@@ -3430,7 +3425,6 @@
       "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.22.tgz",
       "integrity": "sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@babel/parser": "^7.28.4",
         "@vue/compiler-core": "3.5.22",
@@ -3701,7 +3695,6 @@
       "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.9.0.tgz",
       "integrity": "sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@types/web-bluetooth": "^0.0.21",
         "@vueuse/metadata": "13.9.0",
@@ -3754,7 +3747,6 @@
       "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "bin": {
         "acorn": "bin/acorn"
       },
@@ -4018,7 +4010,6 @@
         }
       ],
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "baseline-browser-mapping": "^2.9.0",
         "caniuse-lite": "^1.0.30001759",
@@ -4992,7 +4983,6 @@
       "integrity": "sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
         "globals": "^13.24.0",
@@ -5657,7 +5647,6 @@
       "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
       "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
       "license": "BSD-3-Clause",
-      "peer": true,
       "engines": {
         "node": ">=12.0.0"
       }
@@ -5840,7 +5829,6 @@
       "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "bin": {
         "jiti": "lib/jiti-cli.mjs"
       }
@@ -6045,15 +6033,13 @@
       "version": "4.17.23",
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
       "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
-      "license": "MIT",
-      "peer": true
+      "license": "MIT"
     },
     "node_modules/lodash-es": {
       "version": "4.17.23",
       "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz",
       "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==",
-      "license": "MIT",
-      "peer": true
+      "license": "MIT"
     },
     "node_modules/lodash-unified": {
       "version": "1.0.3",
@@ -6576,7 +6562,6 @@
         }
       ],
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "nanoid": "^3.3.11",
         "picocolors": "^1.1.1",
@@ -6623,7 +6608,6 @@
       "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "bin": {
         "prettier": "bin/prettier.cjs"
       },
@@ -6876,7 +6860,6 @@
       "integrity": "sha512-elOcIZRTM76dvxNAjqYrucTSI0teAF/L2Lv0s6f6b7FOwcwIuA357bIE871580AjHJuSvLIRUosgV+lIWx6Rgg==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "chokidar": "^4.0.0",
         "immutable": "^5.0.2",
@@ -7188,7 +7171,6 @@
       "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "engines": {
         "node": ">=12"
       },
@@ -7300,7 +7282,6 @@
       "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
       "devOptional": true,
       "license": "Apache-2.0",
-      "peer": true,
       "bin": {
         "tsc": "bin/tsc",
         "tsserver": "bin/tsserver"
@@ -7909,7 +7890,6 @@
       "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "esbuild": "^0.25.0",
         "fdir": "^6.4.4",
@@ -8219,7 +8199,6 @@
       "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
       "dev": true,
       "license": "MIT",
-      "peer": true,
       "engines": {
         "node": ">=12"
       },
@@ -8332,7 +8311,6 @@
       "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
       "integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "@vue/compiler-dom": "3.5.22",
         "@vue/compiler-sfc": "3.5.22",

+ 55 - 0
src/api/fulfiller/audit/index.ts

@@ -0,0 +1,55 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { FlfAuditVO, FlfAuditQuery } from './types';
+
+/**
+ * 查询审核记录列表
+ */
+export const listAudit = (query?: FlfAuditQuery): AxiosPromise<FlfAuditVO[]> => {
+  return request({
+    url: '/fulfiller/audit/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询审核记录详细
+ */
+export const getAudit = (id: string | number): AxiosPromise<FlfAuditVO> => {
+  return request({
+    url: '/fulfiller/audit/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 查询待审核数量
+ */
+export const getPendingCount = (): AxiosPromise<number> => {
+  return request({
+    url: '/fulfiller/audit/pendingCount',
+    method: 'get'
+  });
+};
+
+/**
+ * 审核通过
+ */
+export const passAudit = (id: string | number) => {
+  return request({
+    url: '/fulfiller/audit/pass/' + id,
+    method: 'put'
+  });
+};
+
+/**
+ * 审核驳回
+ */
+export const rejectAudit = (id: string | number, rejectReason: string) => {
+  return request({
+    url: '/fulfiller/audit/reject/' + id,
+    method: 'put',
+    params: { rejectReason }
+  });
+};

+ 42 - 0
src/api/fulfiller/audit/types.ts

@@ -0,0 +1,42 @@
+/**
+ * 审核记录 VO
+ */
+export interface FlfAuditVO {
+  id: string | number;
+  fulfillerId: string | number;
+  type: string;
+  name: string;
+  phone: string;
+  gender: string;
+  birthday: string;
+  workType: string;
+  city: string;
+  location: string;
+  realName: string;
+  idCard: string;
+  idValidDate: string;
+  idCardFront: string | number;
+  idCardBack: string | number;
+  idCardFrontUrl: string;
+  idCardBackUrl: string;
+  qualifications: string;
+  qualificationUrls: string[];
+  serviceTypes: string;
+  serviceTypeList: string[];
+  stationId: string | number;
+  stationName: string;
+  status: number;
+  auditBy: string | number;
+  auditTime: string;
+  rejectReason: string;
+  createTime: string;
+}
+
+/**
+ * 审核查询参数
+ */
+export interface FlfAuditQuery extends PageQuery {
+  keyword?: string;
+  type?: string;
+  status?: number;
+}

+ 138 - 0
src/api/fulfiller/pool/index.ts

@@ -0,0 +1,138 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import {
+  FlfFulfillerVO, FlfFulfillerForm, FlfFulfillerQuery,
+  FlfRewardForm, FlfAdjustPointsForm, FlfAdjustBalanceForm,
+  FlfPointsLogVO, FlfBalanceLogVO, FlfRewardLogVO
+} from './types';
+
+/**
+ * 查询履约者列表
+ */
+export const listFulfiller = (query?: FlfFulfillerQuery): AxiosPromise<FlfFulfillerVO[]> => {
+  return request({
+    url: '/fulfiller/fulfiller/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询履约者详细
+ */
+export const getFulfiller = (id: string | number): AxiosPromise<FlfFulfillerVO> => {
+  return request({
+    url: '/fulfiller/fulfiller/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增履约者
+ */
+export const addFulfiller = (data: FlfFulfillerForm) => {
+  return request({
+    url: '/fulfiller/fulfiller',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改履约者
+ */
+export const updateFulfiller = (data: FlfFulfillerForm) => {
+  return request({
+    url: '/fulfiller/fulfiller',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 切换状态
+ */
+export const changeStatus = (id: string | number, status: string) => {
+  return request({
+    url: '/fulfiller/fulfiller/changeStatus',
+    method: 'put',
+    params: { id, status }
+  });
+};
+
+/**
+ * 重置密码
+ */
+export const resetPwd = (id: string | number, password: string) => {
+  return request({
+    url: '/fulfiller/fulfiller/resetPwd',
+    method: 'put',
+    params: { id, password }
+  });
+};
+
+/**
+ * 奖惩操作
+ */
+export const reward = (data: FlfRewardForm) => {
+  return request({
+    url: '/fulfiller/fulfiller/reward',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 调整积分
+ */
+export const adjustPoints = (data: FlfAdjustPointsForm) => {
+  return request({
+    url: '/fulfiller/fulfiller/adjustPoints',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 调整余额
+ */
+export const adjustBalance = (data: FlfAdjustBalanceForm) => {
+  return request({
+    url: '/fulfiller/fulfiller/adjustBalance',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 查询积分日志
+ */
+export const listPointsLog = (fulfillerId: string | number, query?: PageQuery): AxiosPromise<FlfPointsLogVO[]> => {
+  return request({
+    url: '/fulfiller/log/points',
+    method: 'get',
+    params: { fulfillerId, ...query }
+  });
+};
+
+/**
+ * 查询余额日志
+ */
+export const listBalanceLog = (fulfillerId: string | number, query?: PageQuery): AxiosPromise<FlfBalanceLogVO[]> => {
+  return request({
+    url: '/fulfiller/log/balance',
+    method: 'get',
+    params: { fulfillerId, ...query }
+  });
+};
+
+/**
+ * 查询奖惩日志
+ */
+export const listRewardLog = (fulfillerId: string | number, query?: PageQuery): AxiosPromise<FlfRewardLogVO[]> => {
+  return request({
+    url: '/fulfiller/log/reward',
+    method: 'get',
+    params: { fulfillerId, ...query }
+  });
+};

+ 165 - 0
src/api/fulfiller/pool/types.ts

@@ -0,0 +1,165 @@
+/**
+ * 履约者信息 VO
+ */
+export interface FlfFulfillerVO {
+  id: string | number;
+  userId: string | number;
+  name: string;
+  realName: string;
+  phone: string;
+  gender: string;
+  birthday: string;
+  age: number;
+  avatar: string | number;
+  avatarUrl: string;
+  idCard: string;
+  idCardFront: string | number;
+  idCardFrontUrl: string;
+  idCardBack: string | number;
+  idCardBackUrl: string;
+  idCardExpiry: string;
+  serviceTypes: string;
+  cityCode: string;
+  cityName: string;
+  stationId: string | number;
+  stationName: string;
+  workType: string;
+  levelId: string | number;
+  levelName: string;
+  points: number;
+  balance: number;
+  status: string;
+  authId: boolean;
+  authQual: boolean;
+  qualImages: string;
+  qualImageUrls: string[];
+  orderCount: number;
+  rejectCount: number;
+  rating: number;
+  createTime: string;
+  tags: FlfTagVO[];
+}
+
+/**
+ * 履约者标签 VO(简化)
+ */
+export interface FlfTagVO {
+  id: string | number;
+  name: string;
+  colorType: string;
+  description: string;
+  status: number;
+}
+
+/**
+ * 履约者表单
+ */
+export interface FlfFulfillerForm extends BaseEntity {
+  id?: string | number;
+  name?: string;
+  realName?: string;
+  phone?: string;
+  password?: string;
+  gender?: string;
+  birthday?: string;
+  idCard?: string;
+  idCardExpiry?: string;
+  serviceTypes?: string;
+  cityCode?: string;
+  cityName?: string;
+  stationId?: string | number;
+  workType?: string;
+  levelId?: string | number;
+  status?: string;
+  authId?: boolean;
+  authQual?: boolean;
+  tagIds?: (string | number)[];
+}
+
+/**
+ * 履约者查询参数
+ */
+export interface FlfFulfillerQuery extends PageQuery {
+  keyword?: string;
+  status?: string;
+  cityCode?: string;
+  stationId?: string | number;
+  workType?: string;
+}
+
+/**
+ * 奖惩操作
+ */
+export interface FlfRewardForm {
+  fulfillerId: string | number;
+  type: string;
+  target: string;
+  amount: number;
+  reason: string;
+}
+
+/**
+ * 积分调整
+ */
+export interface FlfAdjustPointsForm {
+  fulfillerId: string | number;
+  type: string;
+  amount: number;
+  reason: string;
+}
+
+/**
+ * 余额调整
+ */
+export interface FlfAdjustBalanceForm {
+  fulfillerId: string | number;
+  type: string;
+  subType: string;
+  amount: number;
+  reason: string;
+}
+
+/**
+ * 积分日志
+ */
+export interface FlfPointsLogVO {
+  id: string | number;
+  fulfillerId: string | number;
+  type: string;
+  bizType: string;
+  amount: number;
+  pointsAfter: number;
+  reason: string;
+  operatorId: string | number;
+  createTime: string;
+}
+
+/**
+ * 余额日志
+ */
+export interface FlfBalanceLogVO {
+  id: string | number;
+  fulfillerId: string | number;
+  type: string;
+  subType: string;
+  amount: number;
+  balanceAfter: number;
+  reason: string;
+  operatorId: string | number;
+  createTime: string;
+}
+
+/**
+ * 奖惩日志
+ */
+export interface FlfRewardLogVO {
+  id: string | number;
+  fulfillerId: string | number;
+  type: string;
+  target: string;
+  amount: number;
+  reason: string;
+  operatorId: string | number;
+  operatorName: string;
+  createTime: string;
+}

+ 67 - 0
src/api/fulfiller/tag/index.ts

@@ -0,0 +1,67 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { FlfTagVO, FlfTagForm, FlfTagQuery } from './types';
+
+/**
+ * 查询标签列表
+ */
+export const listTag = (query?: FlfTagQuery): AxiosPromise<FlfTagVO[]> => {
+  return request({
+    url: '/fulfiller/tag/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询全部标签(不分页)
+ */
+export const listAllTag = (query?: FlfTagQuery): AxiosPromise<FlfTagVO[]> => {
+  return request({
+    url: '/fulfiller/tag/listAll',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询标签详细
+ */
+export const getTag = (id: string | number): AxiosPromise<FlfTagVO> => {
+  return request({
+    url: '/fulfiller/tag/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增标签
+ */
+export const addTag = (data: FlfTagForm) => {
+  return request({
+    url: '/fulfiller/tag',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改标签
+ */
+export const updateTag = (data: FlfTagForm) => {
+  return request({
+    url: '/fulfiller/tag',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除标签
+ */
+export const delTag = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/fulfiller/tag/' + id,
+    method: 'delete'
+  });
+};

+ 36 - 0
src/api/fulfiller/tag/types.ts

@@ -0,0 +1,36 @@
+/**
+ * 履约者标签 VO
+ */
+export interface FlfTagVO {
+  id: string | number;
+  name: string;
+  colorType: string;
+  category: string;
+  description: string;
+  type: number;
+  status: number;
+  createTime: string;
+}
+
+/**
+ * 履约者标签表单
+ */
+export interface FlfTagForm extends BaseEntity {
+  id?: string | number;
+  name?: string;
+  colorType?: string;
+  category?: string;
+  description?: string;
+  status?: number;
+}
+
+/**
+ * 履约者标签查询参数
+ */
+export interface FlfTagQuery {
+  pageNum?: number;
+  pageSize?: number;
+  name?: string;
+  category?: string;
+  status?: number;
+}

+ 180 - 39
src/views/archieves/customer/index.vue

@@ -21,7 +21,7 @@
         <el-table-column label="用户基本信息" width="250">
           <template #default="scope">
             <div style="display: flex; align-items: center;">
-              <el-avatar :size="40" :src="scope.row.avatar" style="margin-right: 10px;" />
+              <el-avatar :size="40" :src="scope.row.avatarUrl" style="margin-right: 10px;" />
               <div>
                 <div style="font-weight: bold;">{{ scope.row.name }}
                   <dict-tag :options="sys_user_sex" :value="scope.row.gender" />
@@ -33,7 +33,7 @@
         </el-table-column>
         <el-table-column label="住址" show-overflow-tooltip min-width="150">
           <template #default="scope">
-            {{ [scope.row.regionCode ? scope.row.regionCode.replace(/\//g, ' ') : '', scope.row.address].filter(Boolean).join(' ') || '-' }}
+            {{ [scope.row.regionCode ? scope.row.regionCode.split('/').map(c => codeToText[c] || '').filter(Boolean).join(' ') : '', scope.row.address].filter(Boolean).join(' ') || '-' }}
           </template>
         </el-table-column>
         <el-table-column label="用户标签" width="200">
@@ -108,7 +108,7 @@
     <!-- User Detail Drawer -->
     <el-drawer v-model="drawerVisible" title="用户档案详情" size="60%" destroy-on-close>
       <div class="profile-header">
-        <el-avatar :size="80" :src="currentUser.avatar" />
+        <el-avatar :size="80" :src="currentUser.avatarUrl" />
         <div class="profile-basic">
           <div class="name-row">
             <span class="name">{{ currentUser.name }}</span>
@@ -222,8 +222,8 @@
       <el-form :model="form" label-width="90px" class="user-form">
         <el-row :gutter="20">
           <el-col :span="24" style="text-align: center; margin-bottom: 25px;">
-            <el-upload action="#" :show-file-list="false" :auto-upload="false">
-              <el-avatar :size="80" :src="form.avatar || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'" class="upload-avatar" />
+            <el-upload action="#" :show-file-list="false" :auto-upload="false" :on-change="handleUserUploadFile">
+              <el-avatar :size="80" :src="userAvatarDisplayUrl || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'" class="upload-avatar" />
               <div style="margin-top: 8px; font-size: 12px; color: #409EFF;">点击修改头像</div>
             </el-upload>
           </el-col>
@@ -231,22 +231,22 @@
           <el-col :span="24"><div class="form-section-header">基本资料</div></el-col>
           <el-col :span="12">
             <el-form-item label="录入来源">
-              <el-select v-model="form.source" style="width: 100%" filterable allow-create default-first-option placeholder="请选择或输入">
-                <el-option label="平台录入" value="平台录入" />
-                <el-option label="萌它宠物连锁录入" value="萌它宠物连锁录入" />
-              </el-select>
+              <PageSelect v-model="form.source"
+                :options="brandList.map(item => ({ value: item.name, label: item.name }))"
+                :total="brandTotal" :pageSize="10" placeholder="请选择所属品牌"
+                @page-change="handleBrandPageChange"
+                @visible-change="handleBrandVisibleChange" />
             </el-form-item>
           </el-col>
           <el-col :span="12">
             <el-form-item label="所属区域">
-              <el-select v-model="form.areaId" style="width: 100%" filterable placeholder="请选择区域" clearable @change="form.stationId = undefined">
-                <el-option v-for="area in areaList" :key="area.id" :label="area.name" :value="area.id" />
-              </el-select>
+              <el-cascader v-model="formAreaValue" :options="areaTreeOptions" placeholder="请选择区域"
+                style="width: 100%" clearable @change="handleFormAreaChange" />
             </el-form-item>
           </el-col>
           <el-col :span="12">
             <el-form-item label="所属站点">
-              <el-select v-model="form.stationId" style="width: 100%" filterable placeholder="请选择站点" clearable>
+              <el-select v-model="form.stationId" style="width: 100%" filterable placeholder="请选择站点" clearable :disabled="!form.areaId">
                 <el-option v-for="station in formStationList" :key="station.id" :label="station.name" :value="station.id" />
               </el-select>
             </el-form-item>
@@ -269,10 +269,11 @@
           <el-col :span="24">
             <el-form-item label="所在地区">
               <el-cascader
-                v-model="form.region"
-                :options="pcaOptions"
+                v-model="regionCascaderValue"
+                :options="regionData"
                 placeholder="请选择省/市/区"
                 style="width: 100%"
+                clearable
               />
             </el-form-item>
           </el-col>
@@ -350,7 +351,7 @@
                   :auto-upload="false"
                   :on-change="handlePetUploadFile"
                 >
-                  <el-avatar v-if="petForm.avatar" :src="petForm.avatar" :size="80" />
+                  <el-avatar v-if="petAvatarDisplayUrl" :src="petAvatarDisplayUrl" :size="80" />
                   <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
                 </el-upload>
               </el-col>
@@ -451,7 +452,7 @@
             </el-form-item>
             <el-form-item label="疫苗凭证">
               <el-upload class="avatar-uploader" action="#" :show-file-list="false" :auto-upload="false" :on-change="handlePetUploadVaccineCert">
-                <img v-if="petForm.vaccineCert" :src="petForm.vaccineCert" class="avatar" style="width: 100px; height: 100px; object-fit: cover;" />
+                <img v-if="petVaccineCertDisplayUrl" :src="petVaccineCertDisplayUrl" class="avatar" style="width: 100px; height: 100px; object-fit: cover;" />
                 <el-icon v-else class="avatar-uploader-icon" style="width: 100px; height: 100px; line-height: 100px;"><Plus /></el-icon>
               </el-upload>
             </el-form-item>
@@ -476,12 +477,16 @@
 
 <script setup>
 import { ref, reactive, computed, onMounted, getCurrentInstance, toRefs } from 'vue'
+import { globalHeaders } from '@/utils/request'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { listCustomer, getCustomer, addCustomer, updateCustomer, delCustomer, changeCustomerStatus } from '@/api/archieves/customer'
 import { listAllTag } from '@/api/archieves/tag'
 import { listPetByUser, addPet, updatePet, delPet } from '@/api/archieves/pet'
 import { listAllChangeLog } from '@/api/archieves/changeLog'
 import { listOnStore } from '@/api/system/areaStation'
+import { listOnStore as listBrandOnStore } from '@/api/system/tenant'
+import { regionData, codeToText } from 'element-china-area-data'
+import PageSelect from '@/components/PageSelect/index.vue'
 
 const { proxy } = getCurrentInstance()
 const { sys_user_sex, sys_customer_status, sys_house_type, sys_entry_method, sys_pet_gender, sys_pet_size, sys_pet_type, sys_pet_breed } = toRefs(
@@ -553,6 +558,14 @@ const tableData = ref([])
 const allUserTags = ref([])
 const allPetTags = ref([])
 const changeLogs = ref([])
+const userAvatarDisplayUrl = ref('')
+const petAvatarDisplayUrl = ref('')
+const petVaccineCertDisplayUrl = ref('')
+
+const brandList = ref([])
+const brandTotal = ref(0)
+const formAreaValue = ref([])
+const regionCascaderValue = ref([])
 
 
 const mockOrders = ref([
@@ -565,7 +578,7 @@ const form = reactive({
   name: '',
   phone: '',
   avatar: undefined,
-  gender: 0,
+  gender: undefined,
   birthday: '',
   idCard: '',
   areaId: undefined,
@@ -592,7 +605,7 @@ const petForm = reactive({
   avatar: undefined,
   name: '',
   type: 0,
-  gender: 0,
+  gender: undefined,
   breed: '',
   birthday: '',
   age: 1,
@@ -618,20 +631,48 @@ const petForm = reactive({
 
 const remarkForm = reactive({ content: '' })
 
-const pcaOptions = computed(() => {
-  const cities = allNodes.value.filter(n => n.type === 0)
-  return cities.map(city => ({
-    value: city.name,
-    label: city.name,
-    children: allNodes.value
-      .filter(n => n.type === 1 && n.parentId === city.id)
-      .map(district => ({
-        value: district.name,
-        label: district.name
-      }))
-  }))
+const areaTreeOptions = computed(() => {
+  const buildTree = (data, parentId) => {
+    return data
+      .filter(item => String(item.parentId) === String(parentId))
+      .map(item => {
+        const children = buildTree(data, item.id)
+        const node = { value: item.id, label: item.name }
+        if (children.length > 0) node.children = children
+        return node
+      })
+  }
+  const areaData = allNodes.value.filter(n => n.type === 0 || n.type === 1)
+  return buildTree(areaData, 0)
 })
 
+const handleFormAreaChange = (value) => {
+  form.stationId = undefined
+  if (value && value.length > 0) {
+    form.areaId = value[value.length - 1]
+  } else {
+    form.areaId = undefined
+  }
+}
+
+const getBrandList = async (pageNum = 1) => {
+  const res = await listBrandOnStore({ pageNum, pageSize: 10 })
+  if (res.code === 200) {
+    brandList.value = res.rows || []
+    brandTotal.value = res.total || 0
+  }
+}
+
+const handleBrandPageChange = (page) => {
+  getBrandList(Number(page))
+}
+
+const handleBrandVisibleChange = (visible) => {
+  if (visible) {
+    getBrandList(1)
+  }
+}
+
 const getList = () => {
   loading.value = true
   queryParams.keyword = searchForm.keyword
@@ -667,11 +708,14 @@ const handleAdd = () => {
   isEdit.value = false
   selectedTagIds.value = []
   Object.assign(form, {
-    id: undefined, name: '', phone: '', avatar: undefined, gender: 0, birthday: '', idCard: '',
+    id: undefined, name: '', phone: '', avatar: undefined, gender: undefined, birthday: '', idCard: '',
     areaId: undefined, stationId: undefined, regionCode: '', region: [], address: '',
     houseType: '', entryMethod: '', entryPassword: '', keyLocation: '', source: '',
     emergencyContact: '', emergencyPhone: '', memberLevel: 0, status: 0, remark: '', tagIds: []
   })
+  userAvatarDisplayUrl.value = ''
+  formAreaValue.value = []
+  regionCascaderValue.value = []
   dialogVisible.value = true
 }
 
@@ -688,6 +732,28 @@ const handleEdit = (row) => {
       emergencyContact: data.emergencyContact, emergencyPhone: data.emergencyPhone,
       memberLevel: data.memberLevel, status: data.status, remark: data.remark, tagIds: []
     })
+    userAvatarDisplayUrl.value = data.avatarUrl || ''
+    // Restore area cascader value path
+    if (data.areaId) {
+      const findPath = (nodes, targetId, path = []) => {
+        for (const node of nodes) {
+          const currentPath = [...path, node.id]
+          if (node.id === targetId) return currentPath
+          const children = allNodes.value.filter(n => (n.type === 0 || n.type === 1) && String(n.parentId) === String(node.id))
+          if (children.length > 0) {
+            const result = findPath(children, targetId, currentPath)
+            if (result) return result
+          }
+        }
+        return null
+      }
+      const roots = allNodes.value.filter(n => (n.type === 0 || n.type === 1) && String(n.parentId) === '0')
+      formAreaValue.value = findPath(roots, data.areaId) || []
+    } else {
+      formAreaValue.value = []
+    }
+    // Restore region cascader value
+    regionCascaderValue.value = data.regionCode ? data.regionCode.split('/') : []
     selectedTagIds.value = data.tags ? data.tags.map(t => t.id) : []
     dialogVisible.value = true
   })
@@ -737,8 +803,8 @@ const saveUser = () => {
   if (!form.phone) return ElMessage.warning('请输入电话')
   submitLoading.value = true
   form.tagIds = selectedTagIds.value
-  if (form.region && form.region.length > 0) {
-    form.regionCode = form.region.join('/')
+  if (regionCascaderValue.value && regionCascaderValue.value.length > 0) {
+    form.regionCode = regionCascaderValue.value.join('/')
   } else {
     form.regionCode = ''
   }
@@ -786,22 +852,94 @@ const handleCommand = (command, row) => {
   }
 }
 
-const handlePetUploadFile = (file) => {
-  petForm.avatar = URL.createObjectURL(file.raw)
+const baseUrl = import.meta.env.VITE_APP_BASE_API
+const uploadUrl = baseUrl + '/resource/oss/upload'
+
+const handleUserUploadFile = async (file) => {
+  const formData = new FormData()
+  formData.append('file', file.raw)
+  try {
+    const headers = globalHeaders()
+    const res = await fetch(uploadUrl, {
+      method: 'POST',
+      headers: {
+        'Authorization': headers.Authorization,
+        'clientid': headers.clientid
+      },
+      body: formData
+    })
+    const result = await res.json()
+    if (result.code === 200) {
+      form.avatar = result.data.ossId
+      userAvatarDisplayUrl.value = result.data.url
+    } else {
+      ElMessage.error(result.msg || '头像上传失败')
+    }
+  } catch (e) {
+    ElMessage.error('头像上传失败')
+  }
+}
+
+const handlePetUploadFile = async (file) => {
+  const formData = new FormData()
+  formData.append('file', file.raw)
+  try {
+    const headers = globalHeaders()
+    const res = await fetch(uploadUrl, {
+      method: 'POST',
+      headers: {
+        'Authorization': headers.Authorization,
+        'clientid': headers.clientid
+      },
+      body: formData
+    })
+    const result = await res.json()
+    if (result.code === 200) {
+      petForm.avatar = result.data.ossId
+      petAvatarDisplayUrl.value = result.data.url
+    } else {
+      ElMessage.error(result.msg || '头像上传失败')
+    }
+  } catch (e) {
+    ElMessage.error('头像上传失败')
+  }
 }
-const handlePetUploadVaccineCert = (file) => {
-  petForm.vaccineCert = URL.createObjectURL(file.raw)
+const handlePetUploadVaccineCert = async (file) => {
+  const formData = new FormData()
+  formData.append('file', file.raw)
+  try {
+    const headers = globalHeaders()
+    const res = await fetch(uploadUrl, {
+      method: 'POST',
+      headers: {
+        'Authorization': headers.Authorization,
+        'clientid': headers.clientid
+      },
+      body: formData
+    })
+    const result = await res.json()
+    if (result.code === 200) {
+      petForm.vaccineCert = result.data.ossId
+      petVaccineCertDisplayUrl.value = result.data.url
+    } else {
+      ElMessage.error(result.msg || '疫苗凭证上传失败')
+    }
+  } catch (e) {
+    ElMessage.error('疫苗凭证上传失败')
+  }
 }
 
 const openAddPet = () => {
   petDialogActiveTab.value = 'basic'
   Object.assign(petForm, {
-    id: undefined, userId: currentUser.value.id, avatar: undefined, name: '', type: 0, gender: 0,
+    id: undefined, userId: currentUser.value.id, avatar: undefined, name: '', type: 0, gender: undefined,
     breed: '', birthday: '', age: 1, weight: 5, size: 'small', isSterilized: 0,
     arrivalTime: '', houseType: '', entryMethod: '', entryPassword: '', keyLocation: '',
     personality: '', cutePersonality: '', healthStatus: '健康', aggression: 0,
     vaccineStatus: '无', vaccineCert: undefined, medicalHistory: '', allergies: '', remark: '', tagIds: []
   })
+  petAvatarDisplayUrl.value = ''
+  petVaccineCertDisplayUrl.value = ''
   petDialogVisible.value = true
 }
 
@@ -823,6 +961,8 @@ const handlePetEdit = (row) => {
     medicalHistory: row.medicalHistory, allergies: row.allergies, remark: row.remark,
     tagIds: row.tags ? row.tags.map(t => t.id) : []
   })
+  petAvatarDisplayUrl.value = row.avatarUrl || ''
+  petVaccineCertDisplayUrl.value = row.vaccineCertUrl || ''
   petDialogVisible.value = true
 }
 
@@ -860,6 +1000,7 @@ onMounted(() => {
   getList()
   loadTags()
   loadAreaStation()
+  getBrandList()
 })
 </script>
 

+ 62 - 10
src/views/archieves/pet/index.vue

@@ -15,7 +15,7 @@
         <el-table-column label="宠物信息" width="220">
           <template #default="scope">
             <div style="display: flex; align-items: center">
-              <el-avatar :size="50" :src="scope.row.avatar" style="margin-right: 10px" />
+              <el-avatar :size="50" :src="scope.row.avatarUrl" style="margin-right: 10px" />
               <div>
                 <div style="font-weight: bold">{{ scope.row.name }}</div>
                 <div style="font-size: 12px; color: #999">{{ scope.row.breed }} | {{ scope.row.age }}岁</div>
@@ -80,7 +80,7 @@
             <el-row>
               <el-col :span="24" style="display: flex; justify-content: center; margin-bottom: 20px">
                 <el-upload class="avatar-uploader" action="#" :show-file-list="false" :auto-upload="false" :on-change="handleUploadFile">
-                  <el-avatar v-if="form.avatar" :src="form.avatar" :size="80" />
+                  <el-avatar v-if="avatarDisplayUrl" :src="avatarDisplayUrl" :size="80" />
                   <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
                 </el-upload>
               </el-col>
@@ -184,7 +184,7 @@
             </el-form-item>
             <el-form-item label="疫苗凭证">
               <el-upload class="avatar-uploader" action="#" :show-file-list="false" :auto-upload="false" :on-change="handleUploadVaccineCert">
-                <img v-if="form.vaccineCert" :src="form.vaccineCert" class="avatar" style="width: 100px; height: 100px; object-fit: cover" />
+                <img v-if="vaccineCertDisplayUrl" :src="vaccineCertDisplayUrl" class="avatar" style="width: 100px; height: 100px; object-fit: cover" />
                 <el-icon v-else class="avatar-uploader-icon" style="width: 100px; height: 100px; line-height: 100px"><Plus /></el-icon>
               </el-upload>
             </el-form-item>
@@ -208,7 +208,7 @@
     <!-- Pet Profile Drawer -->
     <el-drawer v-model="drawerVisible" title="宠物档案详情" size="60%" destroy-on-close>
       <div class="profile-header">
-        <el-avatar :size="80" :src="currentPet.avatar" />
+        <el-avatar :size="80" :src="currentPet.avatarUrl" />
         <div class="profile-basic">
           <div class="name-row">
             <span class="name">{{ currentPet.name }}</span>
@@ -318,6 +318,7 @@
 
 <script setup>
 import { ref, reactive, onMounted, getCurrentInstance, toRefs } from 'vue';
+import { globalHeaders } from '@/utils/request';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import { listPet, getPet, addPet, updatePet, delPet } from '@/api/archieves/pet';
 import { listAllTag } from '@/api/archieves/tag';
@@ -360,6 +361,8 @@ const mockOrders = ref([
 ]);
 
 const remarkForm = reactive({ content: '' });
+const avatarDisplayUrl = ref('');
+const vaccineCertDisplayUrl = ref('');
 
 
 const form = reactive({
@@ -368,7 +371,7 @@ const form = reactive({
   avatar: undefined,
   name: '',
   type: 0,
-  gender: 0,
+  gender: undefined,
   breed: '',
   birthday: '',
   age: 1,
@@ -424,12 +427,14 @@ const handleAdd = () => {
   isEdit.value = false;
   activeTab.value = 'basic';
   Object.assign(form, {
-    id: undefined, userId: undefined, avatar: undefined, name: '', type: 0, gender: 0,
+    id: undefined, userId: undefined, avatar: undefined, name: '', type: 0, gender: undefined,
     breed: '', birthday: '', age: 1, weight: 5, size: 'small', isSterilized: 0,
     arrivalTime: '', houseType: '', entryMethod: '', entryPassword: '', keyLocation: '',
     personality: '', cutePersonality: '', healthStatus: '健康', aggression: 0,
     vaccineStatus: '无', vaccineCert: undefined, medicalHistory: '', allergies: '', remark: '', tagIds: []
   });
+  avatarDisplayUrl.value = '';
+  vaccineCertDisplayUrl.value = '';
   dialogVisible.value = true;
 };
 
@@ -450,6 +455,8 @@ const handleEdit = (row) => {
       medicalHistory: data.medicalHistory, allergies: data.allergies, remark: data.remark,
       tagIds: data.tags ? data.tags.map(t => t.id) : []
     });
+    avatarDisplayUrl.value = data.avatarUrl || '';
+    vaccineCertDisplayUrl.value = data.vaccineCertUrl || '';
     dialogVisible.value = true;
   });
 };
@@ -490,12 +497,57 @@ const handleDelete = (row) => {
   });
 };
 
-const handleUploadFile = (file) => {
-  form.avatar = URL.createObjectURL(file.raw);
+const baseUrl = import.meta.env.VITE_APP_BASE_API;
+const uploadUrl = baseUrl + '/resource/oss/upload';
+
+const handleUploadFile = async (file) => {
+  const formData = new FormData();
+  formData.append('file', file.raw);
+  try {
+    const headers = globalHeaders();
+    const res = await fetch(uploadUrl, {
+      method: 'POST',
+      headers: {
+        'Authorization': headers.Authorization,
+        'clientid': headers.clientid
+      },
+      body: formData
+    });
+    const result = await res.json();
+    if (result.code === 200) {
+      form.avatar = result.data.ossId;
+      avatarDisplayUrl.value = result.data.url;
+    } else {
+      ElMessage.error(result.msg || '头像上传失败');
+    }
+  } catch (e) {
+    ElMessage.error('头像上传失败');
+  }
 };
 
-const handleUploadVaccineCert = (file) => {
-  form.vaccineCert = URL.createObjectURL(file.raw);
+const handleUploadVaccineCert = async (file) => {
+  const formData = new FormData();
+  formData.append('file', file.raw);
+  try {
+    const headers = globalHeaders();
+    const res = await fetch(uploadUrl, {
+      method: 'POST',
+      headers: {
+        'Authorization': headers.Authorization,
+        'clientid': headers.clientid
+      },
+      body: formData
+    });
+    const result = await res.json();
+    if (result.code === 200) {
+      form.vaccineCert = result.data.ossId;
+      vaccineCertDisplayUrl.value = result.data.url;
+    } else {
+      ElMessage.error(result.msg || '疫苗凭证上传失败');
+    }
+  } catch (e) {
+    ElMessage.error('疫苗凭证上传失败');
+  }
 };
 
 const saveData = () => {

+ 137 - 153
src/views/fulfiller/audit/index.vue

@@ -5,10 +5,10 @@
         <div class="card-header">
           <div class="left-panel">
             <span class="title">入驻审核</span>
-            <el-tag type="warning" effect="plain" style="margin-left: 10px">待审核 3 人</el-tag>
+            <el-tag type="warning" effect="plain" style="margin-left: 10px">待审核 {{ pendingCount }} 人</el-tag>
           </div>
           <div class="right-panel">
-            <el-input v-model="searchKey" placeholder="搜索姓名/手机号" class="search-input" prefix-icon="Search" clearable />
+            <el-input v-model="searchKey" placeholder="搜索姓名/手机号" class="search-input" prefix-icon="Search" clearable @keyup.enter="handleSearch" @clear="handleSearch" />
           </div>
         </div>
         <!-- Tab切换 -->
@@ -20,7 +20,7 @@
         </el-tabs>
       </template>
 
-      <el-table :data="filteredTableData" style="width: 100%" :header-cell-style="{ background: '#f5f7fa' }">
+      <el-table v-loading="loading" :data="tableData" style="width: 100%" :header-cell-style="{ background: '#f5f7fa' }">
         <el-table-column label="申请人信息" width="220">
           <template #default="scope">
             <div class="user-info">
@@ -28,7 +28,7 @@
               <div class="text-col">
                 <div class="name-row">
                   <span class="name">{{ scope.row.name }}</span>
-                  <el-icon v-if="scope.row.gender === 'male'" color="#409eff"><Male /></el-icon>
+                  <el-icon v-if="scope.row.gender === '0'" color="#409eff"><Male /></el-icon>
                   <el-icon v-else color="#f56c6c"><Female /></el-icon>
                 </div>
                 <div class="sub-text">{{ scope.row.phone }}</div>
@@ -42,19 +42,19 @@
           <template #default="scope">
             <div class="work-info">
               <div class="info-item">
-                <span class="label">类型: </span>
-                <el-tag size="small" :type="scope.row.workType === 'full_time' ? 'warning' : 'info'" effect="light">
-                  {{ scope.row.workType === 'full_time' ? '全职专送' : '兼职众包' }}
+                <span class="label">服务: </span>
+                <el-tag v-for="st in (scope.row.serviceTypeList || [])" :key="st" size="small" type="warning" effect="light" style="margin-right:4px">
+                  {{ st }}
                 </el-tag>
               </div>
               <div class="info-item">
                 <span class="label">城市: </span>
                 <span>{{ scope.row.city }}</span>
               </div>
-              <div class="info-item">
-                <el-tooltip :content="scope.row.location" placement="top">
+              <div class="info-item" v-if="scope.row.stationName">
+                <el-tooltip :content="scope.row.stationName" placement="top">
                   <span class="location-text"
-                    ><el-icon><Location /></el-icon> {{ scope.row.location }}</span
+                    ><el-icon><Location /></el-icon> {{ scope.row.stationName }}</span
                   >
                 </el-tooltip>
               </div>
@@ -72,14 +72,14 @@
                 <el-icon><Medal /></el-icon> 专业资质
               </div>
             </div>
-            <div class="sub-text" style="margintop: 8px">申请时间: {{ scope.row.applyTime }}</div>
+            <div class="sub-text" style="margin-top: 8px">申请时间: {{ scope.row.createTime }}</div>
           </template>
         </el-table-column>
 
         <el-table-column prop="status" label="状态" width="120">
           <template #default="scope">
             <div class="status-cell">
-              <div class="status-dot" :class="scope.row.status"></div>
+              <div class="status-dot" :class="getStatusClass(scope.row.status)"></div>
               <span class="status-text">{{ getStatusText(scope.row.status) }}</span>
             </div>
           </template>
@@ -88,7 +88,7 @@
         <el-table-column label="操作" width="150" fixed="right">
           <template #default="scope">
             <div class="op-cell">
-              <el-button v-if="scope.row.status === 'pending'" link type="primary" size="small" @click="handleDetail(scope.row)">审核</el-button>
+              <el-button v-if="scope.row.status === 0" link type="primary" size="small" @click="handleDetail(scope.row)">审核</el-button>
               <el-button link type="primary" size="small" @click="handleDetail(scope.row)">详情</el-button>
             </div>
           </template>
@@ -97,8 +97,8 @@
 
       <div class="pagination-container">
         <el-pagination
-          v-model:current-page="currentPage"
-          v-model:page-size="pageSize"
+          v-model:current-page="queryParams.pageNum"
+          v-model:page-size="queryParams.pageSize"
           :page-sizes="[10, 20, 50, 100]"
           layout="total, sizes, prev, pager, next, jumper"
           :total="total"
@@ -112,17 +112,17 @@
     <el-dialog v-model="dialogVisible" title="入驻审核详情" width="700px" top="5vh">
       <div class="audit-content" v-if="currentItem">
         <!-- 审核结果栏 -->
-        <div v-if="currentItem.status !== 'pending'" class="result-box" :class="currentItem.status">
+        <div v-if="currentItem.status !== 0" class="result-box" :class="getStatusClass(currentItem.status)">
           <div class="result-header">
-            <el-icon v-if="currentItem.status === 'approved'"><SuccessFilled /></el-icon>
+            <el-icon v-if="currentItem.status === 1"><SuccessFilled /></el-icon>
             <el-icon v-else><CircleCloseFilled /></el-icon>
-            <span>{{ currentItem.status === 'approved' ? '审核通过' : '审核驳回' }}</span>
+            <span>{{ currentItem.status === 1 ? '审核通过' : '审核驳回' }}</span>
           </div>
           <div class="result-info">
-            <span>审核人:{{ currentItem.auditInfo?.auditor || 'admin' }}</span>
-            <span>审核时间:{{ currentItem.auditInfo?.time || '2026-02-04 12:00:00' }}</span>
+            <span>审核人:{{ currentItem.auditBy || '-' }}</span>
+            <span>审核时间:{{ currentItem.auditTime || '-' }}</span>
           </div>
-          <div v-if="currentItem.status === 'rejected'" class="reject-reason">驳回原因:{{ currentItem.auditInfo?.reason }}</div>
+          <div v-if="currentItem.status === 2" class="reject-reason">驳回原因:{{ currentItem.rejectReason }}</div>
         </div>
 
         <!-- 基础信息 -->
@@ -130,15 +130,15 @@
         <el-descriptions :column="3" border>
           <el-descriptions-item label="姓名">{{ currentItem.name }}</el-descriptions-item>
           <el-descriptions-item label="手机号">{{ currentItem.phone }}</el-descriptions-item>
-          <el-descriptions-item label="性别">{{ currentItem.gender === 'male' ? '男' : '女' }}</el-descriptions-item>
+          <el-descriptions-item label="性别">{{ currentItem.gender === '0' ? '男' : '女' }}</el-descriptions-item>
           <el-descriptions-item label="出生日期">{{ currentItem.birthday }}</el-descriptions-item>
-          <el-descriptions-item label="工作类型">
-            <el-tag size="small" :type="currentItem.workType === 'full_time' ? 'warning' : 'info'">
-              {{ currentItem.workType === 'full_time' ? '全职专送' : '兼职众包' }}
+          <el-descriptions-item label="服务类型">
+            <el-tag v-for="st in (currentItem.serviceTypeList || [])" :key="st" size="small" type="warning" style="margin-right:4px">
+              {{ st }}
             </el-tag>
           </el-descriptions-item>
           <el-descriptions-item label="意向城市">{{ currentItem.city }}</el-descriptions-item>
-          <el-descriptions-item label="意向地点" :span="3">{{ currentItem.location }}</el-descriptions-item>
+          <el-descriptions-item label="意向站点" :span="3">{{ currentItem.stationName || '-' }}</el-descriptions-item>
         </el-descriptions>
 
         <!-- 实名认证信息 -->
@@ -146,15 +146,15 @@
         <el-descriptions :column="2" border>
           <el-descriptions-item label="真实姓名">{{ currentItem.realName || currentItem.name }}</el-descriptions-item>
           <el-descriptions-item label="证件类型">居民身份证</el-descriptions-item>
-          <el-descriptions-item label="身份证号">{{ currentItem.idNo }}</el-descriptions-item>
+          <el-descriptions-item label="身份证号">{{ currentItem.idCard }}</el-descriptions-item>
           <el-descriptions-item label="有效期至">{{ currentItem.idValidDate }}</el-descriptions-item>
         </el-descriptions>
         <div class="img-row" style="margin-top: 15px">
           <div class="img-box">
             <el-image
               style="width: 200px; height: 120px; border-radius: 4px; border: 1px solid #eee"
-              :src="currentItem.idCardFront"
-              :preview-src-list="[currentItem.idCardFront, currentItem.idCardBack]"
+              :src="currentItem.idCardFrontUrl"
+              :preview-src-list="[currentItem.idCardFrontUrl, currentItem.idCardBackUrl].filter(Boolean)"
               fit="cover"
             >
               <template #error>
@@ -168,8 +168,8 @@
           <div class="img-box">
             <el-image
               style="width: 200px; height: 120px; border-radius: 4px; border: 1px solid #eee"
-              :src="currentItem.idCardBack"
-              :preview-src-list="[currentItem.idCardFront, currentItem.idCardBack]"
+              :src="currentItem.idCardBackUrl"
+              :preview-src-list="[currentItem.idCardFrontUrl, currentItem.idCardBackUrl].filter(Boolean)"
               fit="cover"
             >
               <template #error>
@@ -184,12 +184,12 @@
 
         <!-- 资质信息 -->
         <div class="section-title" style="margin-top: 20px">专业资质证明</div>
-        <div class="img-row" v-if="currentItem.qualifications && currentItem.qualifications.length">
-          <div class="img-box" v-for="(img, index) in currentItem.qualifications" :key="index">
+        <div class="img-row" v-if="currentItem.qualificationUrls && currentItem.qualificationUrls.length">
+          <div class="img-box" v-for="(img, index) in currentItem.qualificationUrls" :key="index">
             <el-image
               style="width: 120px; height: 120px; border-radius: 4px; border: 1px solid #eee"
               :src="img"
-              :preview-src-list="currentItem.qualifications"
+              :preview-src-list="currentItem.qualificationUrls"
               fit="cover"
             />
             <span class="label">资质材料 {{ index + 1 }}</span>
@@ -198,7 +198,7 @@
         <div v-else style="color: #999; font-size: 13px; padding-left: 10px">未上传其他资质材料</div>
       </div>
       <template #footer>
-        <span class="dialog-footer" v-if="currentItem?.status === 'pending'">
+        <span class="dialog-footer" v-if="currentItem?.status === 0">
           <el-button type="danger" plain @click="rejectDialogVisible = true">驳回申请</el-button>
           <el-button type="primary" @click="handlePass">通过审核</el-button>
         </span>
@@ -221,91 +221,59 @@
   </div>
 </template>
 
-<script setup>
-import { ref, reactive, computed } from 'vue';
-import { ElMessage } from 'element-plus';
+<script setup lang="ts">
+import { ref, reactive, onMounted } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { listAudit, getAudit, passAudit, rejectAudit, getPendingCount } from '@/api/fulfiller/audit';
+import type { FlfAuditVO, FlfAuditQuery } from '@/api/fulfiller/audit/types';
 
+const loading = ref(false);
 const searchKey = ref('');
 const activeTab = ref('all');
-const currentPage = ref(1);
-const pageSize = ref(10);
-const total = ref(3);
+const total = ref(0);
+const tableData = ref<FlfAuditVO[]>([]);
+const pendingCount = ref(0);
 
-const handleSizeChange = (val) => {
-  console.log(`每页 ${val} 条`);
-};
-const handleCurrentChange = (val) => {
-  console.log(`当前页: ${val}`);
-};
+const queryParams = reactive<FlfAuditQuery>({
+  pageNum: 1,
+  pageSize: 10
+});
 
 const dialogVisible = ref(false);
 const rejectDialogVisible = ref(false);
 const rejectReason = ref('');
-const currentItem = ref(null);
-
-const tableData = ref([
-  {
-    id: 1,
-    name: '张三哥',
-    gender: 'male',
-    birthday: '1998-05-12',
-    phone: '13612345678',
-    workType: 'full_time',
-    city: '广东省 深圳市 龙华区',
-    location: '大润发(嘉熙业广场店)',
-    applyTime: '2026-02-04 10:00',
-    status: 'pending',
-    realName: '张三',
-    idNo: '44030619980512****',
-    idValidDate: '2036-05-12',
-    idCardFront: 'https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg',
-    idCardBack: 'https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg',
-    qualifications: ['https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg']
-  },
-  {
-    id: 2,
-    name: '李小妹',
-    gender: 'female',
-    birthday: '2000-11-20',
-    phone: '13933334444',
-    workType: 'part_time',
-    city: '北京市 朝阳区',
-    location: '三里屯SOHO',
-    applyTime: '2026-02-04 11:30',
-    status: 'pending',
-    realName: '李小妹',
-    idNo: '11010520001120****',
-    idValidDate: '2040-11-20',
-    idCardFront: 'https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg',
-    idCardBack: 'https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg',
-    qualifications: []
-  },
-  {
-    id: 3,
-    name: '周大华',
-    gender: 'male',
-    birthday: '1985-03-15',
-    phone: '13755556666',
-    workType: 'full_time',
-    city: '上海市 浦东新区',
-    location: '世纪广场',
-    applyTime: '2026-02-03 16:20',
-    status: 'rejected',
-    realName: '周大华',
-    idNo: '31011519850315****',
-    idValidDate: '2025-03-15',
-    idCardFront: '',
-    idCardBack: '',
-    qualifications: [],
-    auditInfo: {
-      auditor: 'admin',
-      time: '2026-02-03 17:00',
-      reason: '身份证照片模糊,无法识别'
+const currentItem = ref<FlfAuditVO | null>(null);
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true;
+  try {
+    const params: FlfAuditQuery = { ...queryParams };
+    if (searchKey.value) {
+      params.keyword = searchKey.value;
+    }
+    if (activeTab.value !== 'all') {
+      const statusMap: Record<string, number> = { pending: 0, approved: 1, rejected: 2 };
+      params.status = statusMap[activeTab.value];
     }
+    const res = await listAudit(params);
+    tableData.value = res.rows;
+    total.value = res.total;
+  } finally {
+    loading.value = false;
   }
-]);
+};
 
-const getAge = (birthday) => {
+const handleSizeChange = (val: number) => {
+  queryParams.pageSize = val;
+  getList();
+};
+const handleCurrentChange = (val: number) => {
+  queryParams.pageNum = val;
+  getList();
+};
+
+const getAge = (birthday: string) => {
   if (!birthday) return 0;
   const birthDate = new Date(birthday);
   const today = new Date();
@@ -317,67 +285,83 @@ const getAge = (birthday) => {
   return age;
 };
 
-const getStatusText = (status) => {
-  const map = { pending: '待审核', approved: '已通过', rejected: '已驳回' };
-  return map[status] || '未知';
+const getStatusText = (status: number | string) => {
+  const map: Record<string, string> = { '0': '待审核', '1': '已通过', '2': '已驳回', pending: '待审核', approved: '已通过', rejected: '已驳回' };
+  return map[String(status)] || '未知';
 };
 
-const handleTabClick = (tab) => {
-  // Filter logic is handled by computed property
+const getStatusClass = (status: number | string) => {
+  const map: Record<string, string> = { '0': 'pending', '1': 'approved', '2': 'rejected' };
+  return map[String(status)] || '';
 };
 
-const filteredTableData = computed(() => {
-  let data = tableData.value;
-  // Status Filter
-  if (activeTab.value !== 'all') {
-    data = data.filter((item) => item.status === activeTab.value);
-  }
-  // Search Filter
-  if (searchKey.value) {
-    const key = searchKey.value.toLowerCase();
-    data = data.filter((item) => item.name.toLowerCase().includes(key) || item.phone.includes(key));
+const handleTabClick = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
+
+const handleSearch = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
+
+const loadPendingCount = async () => {
+  try {
+    const res = await getPendingCount();
+    pendingCount.value = res.data;
+  } catch {
+    // ignore
   }
-  return data;
-});
+};
 
-const handleDetail = (row) => {
-  currentItem.value = JSON.parse(JSON.stringify(row));
-  dialogVisible.value = true;
+const handleDetail = async (row: FlfAuditVO) => {
+  try {
+    const res = await getAudit(row.id);
+    currentItem.value = res.data;
+    dialogVisible.value = true;
+  } catch {
+    currentItem.value = { ...row };
+    dialogVisible.value = true;
+  }
 };
 
-const handlePass = () => {
-  ElMessage.success('已通过该申请');
-  const idx = tableData.value.findIndex((item) => item.id === currentItem.value.id);
-  if (idx !== -1) {
-    tableData.value[idx].status = 'approved';
-    tableData.value[idx].auditInfo = {
-      auditor: 'admin',
-      time: new Date().toLocaleString(),
-      result: 'pass'
-    };
+const handlePass = async () => {
+  if (!currentItem.value) return;
+  await ElMessageBox.confirm('确认通过该申请吗?', '提示', { type: 'warning' });
+  try {
+    await passAudit(currentItem.value.id);
+    ElMessage.success('已通过该申请');
+    dialogVisible.value = false;
+    getList();
+    loadPendingCount();
+  } catch (e) {
+    // error handled by interceptor
   }
-  dialogVisible.value = false;
 };
 
-const confirmReject = () => {
+const confirmReject = async () => {
   if (!rejectReason.value) {
     ElMessage.warning('请输入驳回原因');
     return;
   }
-  ElMessage.warning('已驳回该申请');
-  const idx = tableData.value.findIndex((item) => item.id === currentItem.value.id);
-  if (idx !== -1) {
-    tableData.value[idx].status = 'rejected';
-    tableData.value[idx].auditInfo = {
-      auditor: 'admin',
-      time: new Date().toLocaleString(),
-      reason: rejectReason.value
-    };
+  if (!currentItem.value) return;
+  try {
+    await rejectAudit(currentItem.value.id, rejectReason.value);
+    ElMessage.warning('已驳回该申请');
+    rejectDialogVisible.value = false;
+    dialogVisible.value = false;
+    rejectReason.value = '';
+    getList();
+    loadPendingCount();
+  } catch (e) {
+    // error handled by interceptor
   }
-  rejectDialogVisible.value = false;
-  dialogVisible.value = false;
-  rejectReason.value = '';
 };
+
+onMounted(() => {
+  getList();
+  loadPendingCount();
+});
 </script>
 
 <style scoped>

+ 420 - 380
src/views/fulfiller/pool/index.vue

@@ -5,7 +5,7 @@
         <div class="card-header">
           <div class="left-panel">
             <span class="title">履约者池</span>
-            <el-tag type="info" effect="plain" style="margin-left: 10px;">共 1205 人</el-tag>
+            <el-tag type="info" effect="plain" style="margin-left: 10px;">共 {{ total }} 人</el-tag>
           </div>
           <div class="right-panel">
             <el-button type="primary" icon="Plus" style="margin-right: 15px" @click="handleCreate">新增履约者</el-button>
@@ -15,17 +15,20 @@
               class="search-input"
               prefix-icon="Search"
               clearable
+              @keyup.enter="handleSearch"
+              @clear="handleSearch"
             />
-            <el-select v-model="filterCity" placeholder="所属城市" style="width: 120px; margin-left: 10px;">
-              <el-option label="所有城市" value="" />
-              <el-option label="北京市" value="beijing" />
-              <el-option label="上海市" value="shanghai" />
-              <el-option label="深圳市" value="shenzhen" />
-            </el-select>
-            <el-select v-model="filterStation" placeholder="所属站点" style="width: 150px; margin-left: 10px;">
-              <el-option label="所有站点" value="" />
-              <el-option label="北京朝阳一站" value="bj-cy-01" />
-              <el-option label="上海浦东一站" value="sh-pd-01" />
+            <el-cascader
+              v-model="filterCascaderValue"
+              :options="cityCascaderOptions"
+              :props="{ checkStrictly: true }"
+              placeholder="所属城市/区域"
+              clearable
+              style="width: 200px; margin-left: 10px;"
+              @change="handleFilterCascaderChange"
+            />
+            <el-select v-model="queryParams.stationId" placeholder="所属站点" style="width: 150px; margin-left: 10px;" clearable @change="getList">
+              <el-option v-for="station in stationOptions" :key="station.id" :label="station.name" :value="station.id" />
             </el-select>
           </div>
         </div>
@@ -39,16 +42,16 @@
         </el-tabs>
       </template>
 
-      <el-table :data="filteredTableData" style="width: 100%" :header-cell-style="{ background: '#f5f7fa' }">
+      <el-table v-loading="loading" :data="tableData" style="width: 100%" :header-cell-style="{ background: '#f5f7fa' }">
         <el-table-column label="基本信息" width="280">
           <template #default="scope">
             <div class="user-info">
-              <el-avatar :size="45" :src="scope.row.avatar">{{ scope.row.name.charAt(0) }}</el-avatar>
+              <el-avatar :size="45" :src="scope.row.avatarUrl">{{ scope.row.name.charAt(0) }}</el-avatar>
               <div class="text-col">
                 <div class="name-row">
                   <span class="name">{{ scope.row.name }}</span>
                   <span class="gender-tag">
-                        <el-icon v-if="scope.row.gender === 'male'" color="#409eff"><Male /></el-icon>
+                        <el-icon v-if="scope.row.gender === '0'" color="#409eff"><Male /></el-icon>
                         <el-icon v-else color="#f56c6c"><Female /></el-icon>
                     </span>
                 </div>
@@ -58,8 +61,8 @@
                     {{ scope.row.workType === 'full_time' ? '全职专送' : '兼职众包' }}
                   </el-tag>
                   <!-- 等级展示 -->
-                  <el-tag size="small" :type="getLevelType(scope.row.level)" effect="plain" class="level-tag">
-                    {{ getLevelText(scope.row.level) }}
+                  <el-tag size="small" :type="getLevelType(scope.row.levelName)" effect="plain" class="level-tag">
+                    {{ getLevelText(scope.row.levelName) }}
                   </el-tag>
                 </div>
                 <div class="sub-text">{{ scope.row.age }}岁 | {{ scope.row.phone }}</div>
@@ -78,15 +81,15 @@
                 <el-icon><Medal /></el-icon> 资质证
               </div>
             </div>
-            <div class="sub-text" style="margin-top:5px;">ID: {{ scope.row.idNo }}</div>
+            <div class="sub-text" style="margin-top:5px;">ID: {{ scope.row.idCard }}</div>
           </template>
         </el-table-column>
 
         <el-table-column label="服务区域" width="180">
           <template #default="scope">
             <div class="text-col">
-              <span style="font-size: 13px; color: #333;">{{ scope.row.city }}</span>
-              <span style="font-size: 12px; color: #999;">{{ scope.row.station }}</span>
+              <span style="font-size: 13px; color: #333;">{{ scope.row.cityName }}</span>
+              <span style="font-size: 12px; color: #999;">{{ scope.row.stationName }}</span>
             </div>
           </template>
         </el-table-column>
@@ -95,8 +98,8 @@
           <template #default="scope">
             <el-tag
               v-for="tag in scope.row.tags"
-              :key="tag.name"
-              :type="tag.type"
+              :key="tag.id"
+              :type="tag.colorType"
               size="small"
               class="skill-tag"
               effect="plain"
@@ -154,8 +157,8 @@
 
       <div class="pagination-container">
         <el-pagination
-          v-model:current-page="currentPage"
-          v-model:page-size="pageSize"
+          v-model:current-page="queryParams.pageNum"
+          v-model:page-size="queryParams.pageSize"
           :page-sizes="[10, 20, 50, 100]"
           layout="total, sizes, prev, pager, next, jumper"
           :total="total"
@@ -176,22 +179,22 @@
       <div class="drawer-content" v-if="currentItem">
         <!-- 头部概览 -->
         <div class="user-header-card">
-          <el-avatar :size="70" :src="currentItem.avatar" class="header-avatar">{{ currentItem.name?.charAt(0) }}</el-avatar>
+          <el-avatar :size="70" :src="currentItem.avatarUrl" class="header-avatar">{{ currentItem.name?.charAt(0) }}</el-avatar>
           <div class="header-info">
             <div class="top-row">
               <span class="user-name">{{ currentItem.name }}</span>
-              <el-tag size="small" :type="currentItem.gender === 'male' ? '' : 'danger'" effect="plain" round style="margin-left: 8px;">
-                {{ currentItem.gender === 'male' ? '男' : '女' }} {{ currentItem.age }}岁
+              <el-tag size="small" :type="currentItem.gender === '0' ? '' : 'danger'" effect="plain" round style="margin-left: 8px;">
+                {{ currentItem.gender === '0' ? '男' : '女' }} {{ currentItem.age }}岁
               </el-tag>
               <span class="status-badge" :class="currentItem.status">{{ getStatusText(currentItem.status) }}</span>
             </div>
             <div class="sub-row">
               <span class="info-item"><el-icon><Iphone /></el-icon> {{ currentItem.phone }}</span>
               <span class="divider">|</span>
-              <span class="info-item"><el-icon><Location /></el-icon> {{ currentItem.city }}</span>
+              <span class="info-item"><el-icon><Location /></el-icon> {{ currentItem.cityName }}</span>
             </div>
             <div class="tags-row">
-              <el-tag size="small" :type="getLevelType(currentItem.level)" effect="dark">{{ getLevelText(currentItem.level) }}</el-tag>
+              <el-tag size="small" :type="getLevelType(currentItem.levelName)" effect="dark">{{ getLevelText(currentItem.levelName) }}</el-tag>
               <el-tag size="small" type="warning" effect="plain" v-if="currentItem.workType === 'full_time'" style="margin-left:5px">全职专送</el-tag>
             </div>
           </div>
@@ -226,11 +229,11 @@
               <div class="section-block">
                 <div class="section-title">基础信息</div>
                 <el-descriptions :column="2" border>
-                  <el-descriptions-item label="身份证号">{{ currentItem.idNo }}</el-descriptions-item>
+                  <el-descriptions-item label="身份证号">{{ currentItem.idCard }}</el-descriptions-item>
                   <el-descriptions-item label="真实姓名">{{ currentItem.realName || currentItem.name }}</el-descriptions-item>
-                  <el-descriptions-item label="归属站点">{{ currentItem.station }}</el-descriptions-item>
-                  <el-descriptions-item label="证件有效期">{{ currentItem.idExpiry || '2030-01-01' }}</el-descriptions-item>
-                  <el-descriptions-item label="入驻时间">2024-05-12</el-descriptions-item>
+                  <el-descriptions-item label="归属站点">{{ currentItem.stationName }}</el-descriptions-item>
+                  <el-descriptions-item label="证件有效期">{{ currentItem.idCardExpiry || '-' }}</el-descriptions-item>
+                  <el-descriptions-item label="入驻时间">{{ currentItem.createTime }}</el-descriptions-item>
                   <el-descriptions-item label="工作性质">{{ currentItem.workType === 'full_time' ? '全职' : '兼职' }}</el-descriptions-item>
                 </el-descriptions>
               </div>
@@ -238,14 +241,14 @@
               <div class="section-block">
                 <div class="section-title">实名认证</div>
                 <div class="cert-row">
-                  <div class="cert-item" @click="handleViewImage(currentItem.idCardFront)">
-                    <el-image :src="currentItem.idCardFront || ''" fit="cover" class="cert-img">
+                  <div class="cert-item" @click="handleViewImage(currentItem.idCardFrontUrl)">
+                    <el-image :src="currentItem.idCardFrontUrl || ''" fit="cover" class="cert-img">
                       <template #error><div class="img-slot"><el-icon><Picture /></el-icon></div></template>
                     </el-image>
                     <div class="cert-name">身份证人像面</div>
                   </div>
-                  <div class="cert-item" @click="handleViewImage(currentItem.idCardBack)">
-                    <el-image :src="currentItem.idCardBack || ''" fit="cover" class="cert-img">
+                  <div class="cert-item" @click="handleViewImage(currentItem.idCardBackUrl)">
+                    <el-image :src="currentItem.idCardBackUrl || ''" fit="cover" class="cert-img">
                       <template #error><div class="img-slot"><el-icon><Picture /></el-icon></div></template>
                     </el-image>
                     <div class="cert-name">身份证国徽面</div>
@@ -255,8 +258,8 @@
 
               <div class="section-block">
                 <div class="section-title">资质认证</div>
-                <div class="cert-row" v-if="currentItem.qualImages && currentItem.qualImages.length">
-                  <div class="cert-item" v-for="(img, index) in currentItem.qualImages" :key="index" @click="handleViewImage(img)">
+                <div class="cert-row" v-if="currentItem.qualImageUrls && currentItem.qualImageUrls.length">
+                  <div class="cert-item" v-for="(img, index) in currentItem.qualImageUrls" :key="index" @click="handleViewImage(img)">
                     <el-image :src="img" fit="cover" class="cert-img">
                       <template #error><div class="img-slot"><el-icon><Picture /></el-icon></div></template>
                     </el-image>
@@ -267,7 +270,7 @@
               <div class="section-block">
                 <div class="section-title">技能标签</div>
                 <div class="tag-list">
-                  <el-tag v-for="tag in currentItem.tags" :key="tag.name" :type="tag.type" size="large" style="margin-right: 12px; margin-bottom: 8px;">{{ tag.name }}</el-tag>
+                  <el-tag v-for="tag in currentItem.tags" :key="tag.id" :type="tag.colorType" size="large" style="margin-right: 12px; margin-bottom: 8px;">{{ tag.name }}</el-tag>
                 </div>
               </div>
             </div>
@@ -275,7 +278,7 @@
 
           <el-tab-pane label="服务订单" name="orders">
             <div class="tab-content-wrapper">
-              <el-table :data="mockOrders" stripe style="width: 100%" :header-cell-style="{background:'#f5f7fa', color:'#606266'}">
+              <el-table :data="[]" stripe style="width: 100%" :header-cell-style="{background:'#f5f7fa', color:'#606266'}">
                 <el-table-column prop="orderNo" label="订单号" width="160" show-overflow-tooltip />
                 <el-table-column prop="serviceName" label="服务项目" show-overflow-tooltip />
                 <el-table-column prop="serviceFee" label="收入" width="100">
@@ -297,11 +300,11 @@
 
           <el-tab-pane label="积分记录" name="pointLogs">
             <div class="tab-content-wrapper">
-              <el-table :data="mockPointLogs" stripe style="width: 100%" :header-cell-style="{background:'#f5f7fa', color:'#606266'}">
-                <el-table-column prop="time" label="变动时间" width="180" />
+              <el-table v-loading="logLoading" :data="pointsLogData" stripe style="width: 100%" :header-cell-style="{background:'#f5f7fa', color:'#606266'}">
+                <el-table-column prop="createTime" label="变动时间" width="180" />
                 <el-table-column prop="bizType" label="业务类型" width="120">
                   <template #default="{ row }">
-                    <el-tag :type="getBizTypeTag(row.bizType)" size="small" effect="plain">{{ row.bizTypeName }}</el-tag>
+                    <el-tag :type="getBizTypeTag(row.bizType)" size="small" effect="plain">{{ getBizTypeName(row.bizType) }}</el-tag>
                   </template>
                 </el-table-column>
                 <el-table-column prop="amount" label="变动数值" width="120">
@@ -312,18 +315,18 @@
                   </template>
                 </el-table-column>
                 <el-table-column prop="reason" label="变动原因" show-overflow-tooltip />
-                <el-table-column prop="operator" label="操作人" width="120" />
+                <el-table-column prop="operatorId" label="操作人" width="120" />
               </el-table>
             </div>
           </el-tab-pane>
 
           <el-tab-pane label="余额变动" name="balanceLogs">
             <div class="tab-content-wrapper">
-              <el-table :data="mockBalanceLogs" stripe style="width: 100%" :header-cell-style="{background:'#f5f7fa', color:'#606266'}">
-                <el-table-column prop="time" label="变动时间" width="180" show-overflow-tooltip />
+              <el-table v-loading="logLoading" :data="balanceLogData" stripe style="width: 100%" :header-cell-style="{background:'#f5f7fa', color:'#606266'}">
+                <el-table-column prop="createTime" label="变动时间" width="180" show-overflow-tooltip />
                 <el-table-column prop="subType" label="资金类型" width="120">
                   <template #default="{ row }">
-                    <el-tag :type="row.amount > 0 ? 'success' : 'danger'" size="small" effect="plain">{{ row.subTypeName }}</el-tag>
+                    <el-tag :type="getBizTypeTag(row.subType)" size="small" effect="plain">{{ getSubTypeName(row.subType) }}</el-tag>
                   </template>
                 </el-table-column>
                 <el-table-column prop="amount" label="变动金额" width="120">
@@ -339,15 +342,15 @@
                   </template>
                 </el-table-column>
                 <el-table-column prop="reason" label="备注说明" show-overflow-tooltip />
-                <el-table-column prop="operator" label="操作人" width="100" />
+                <el-table-column prop="operatorId" label="操作人" width="100" />
               </el-table>
             </div>
           </el-tab-pane>
 
           <el-tab-pane label="奖惩记录" name="rewards">
             <div class="tab-content-wrapper">
-              <el-table :data="mockRewards" stripe style="width: 100%" :header-cell-style="{background:'#f5f7fa', color:'#606266'}">
-                <el-table-column prop="time" label="操作时间" width="180" />
+              <el-table v-loading="logLoading" :data="rewardLogData" stripe style="width: 100%" :header-cell-style="{background:'#f5f7fa', color:'#606266'}">
+                <el-table-column prop="createTime" label="操作时间" width="180" />
                 <el-table-column prop="type" label="奖惩类型" width="100">
                   <template #default="{ row }">
                     <el-tag :type="row.type==='reward' ? 'success' : 'danger'" size="small">{{ row.type === 'reward' ? '奖励' : '惩罚' }}</el-tag>
@@ -358,15 +361,15 @@
                     <el-tag type="info" size="small" effect="plain">{{ row.target === 'points' ? '积分' : '余额' }}</el-tag>
                   </template>
                 </el-table-column>
-                <el-table-column prop="val" label="涉及数值" width="120">
+                <el-table-column prop="amount" label="涉及数值" width="120">
                   <template #default="{ row }">
                                      <span :style="{ color: row.type==='reward' ? '#67c23a' : '#f56c6c', fontWeight: 'bold' }">
-                                         {{ row.type === 'reward' ? '+' : '-' }}{{ row.val }} {{ row.target === 'points' ? '分' : '元' }}
+                                         {{ row.type === 'reward' ? '+' : '-' }}{{ row.amount }} {{ row.target === 'points' ? '分' : '元' }}
                                      </span>
                   </template>
                 </el-table-column>
                 <el-table-column prop="reason" label="奖惩原因" show-overflow-tooltip />
-                <el-table-column prop="operator" label="操作人" width="100" />
+                <el-table-column prop="operatorName" label="操作人" width="100" />
               </el-table>
             </div>
           </el-tab-pane>
@@ -396,14 +399,14 @@
           <el-col :span="12">
             <el-form-item label="性别">
               <el-radio-group v-model="editDialog.form.gender">
-                <el-radio label="male">男</el-radio>
-                <el-radio label="female">女</el-radio>
+                <el-radio label="0">男</el-radio>
+                <el-radio label="1">女</el-radio>
               </el-radio-group>
             </el-form-item>
           </el-col>
           <el-col :span="12">
             <el-form-item label="身份证号">
-              <el-input v-model="editDialog.form.idNo" />
+              <el-input v-model="editDialog.form.idCard" />
             </el-form-item>
           </el-col>
         </el-row>
@@ -411,19 +414,13 @@
         <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="服务城市">
-              <el-select v-model="editDialog.form.city" style="width: 100%">
-                <el-option label="北京市" value="北京市" />
-                <el-option label="上海市" value="上海市" />
-                <el-option label="深圳市" value="深圳市" />
-              </el-select>
+              <el-cascader v-model="editDialog.cascaderValue" :options="cityCascaderOptions" :props="{ checkStrictly: true }" placeholder="请选择城市/区域" clearable style="width: 100%" @change="handleEditCascaderChange" />
             </el-form-item>
           </el-col>
           <el-col :span="12">
             <el-form-item label="归属站点">
-              <el-select v-model="editDialog.form.station" style="width: 100%">
-                <el-option label="北京朝阳一站" value="北京朝阳一站" />
-                <el-option label="上海浦东一站" value="上海浦东一站" />
-                <el-option label="北京海淀二站" value="北京海淀二站" />
+              <el-select v-model="editDialog.form.stationId" placeholder="请选择站点" style="width: 100%">
+                <el-option v-for="station in editDialog.stationOptions" :key="station.id" :label="station.name" :value="station.id" />
               </el-select>
             </el-form-item>
           </el-col>
@@ -432,11 +429,7 @@
         <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="等级">
-              <el-select v-model="editDialog.form.level" style="width: 100%">
-                <el-option label="金牌" value="gold" />
-                <el-option label="银牌" value="silver" />
-                <el-option label="铜牌" value="bronze" />
-              </el-select>
+              <el-input v-model="editDialog.form.levelId" placeholder="等级ID" />
             </el-form-item>
           </el-col>
           <el-col :span="12">
@@ -456,13 +449,8 @@
         </el-form-item>
 
         <el-form-item label="技能标签">
-          <el-checkbox-group v-model="editDialog.form.tags">
-            <el-checkbox label="行为矫正" value="行为矫正" />
-            <el-checkbox label="驾驶" value="驾驶" />
-            <el-checkbox label="摄影" value="摄影" />
-            <el-checkbox label="洗护护理" value="洗护护理" />
-            <el-checkbox label="精细美容" value="精细美容" />
-            <el-checkbox label="基础喂遛" value="基础喂遛" />
+          <el-checkbox-group v-model="editDialog.form.tagIds">
+            <el-checkbox v-for="t in allTags" :key="t.id" :label="t.id" :value="t.id">{{ t.name }}</el-checkbox>
           </el-checkbox-group>
         </el-form-item>
       </el-form>
@@ -521,21 +509,16 @@
         </el-form-item>
         <el-form-item label="性别">
           <el-radio-group v-model="createDialog.form.gender">
-            <el-radio label="male">男</el-radio>
-            <el-radio label="female">女</el-radio>
+            <el-radio label="0">男</el-radio>
+            <el-radio label="1">女</el-radio>
           </el-radio-group>
         </el-form-item>
         <el-form-item label="服务城市">
-          <el-select v-model="createDialog.form.city" style="width: 100%">
-            <el-option label="北京市" value="北京市" />
-            <el-option label="上海市" value="上海市" />
-            <el-option label="深圳市" value="深圳市" />
-          </el-select>
+          <el-cascader v-model="createDialog.cascaderValue" :options="cityCascaderOptions" :props="{ checkStrictly: true }" placeholder="请选择城市/区域" clearable style="width: 100%" @change="handleCreateCascaderChange" />
         </el-form-item>
         <el-form-item label="归属站点">
-          <el-select v-model="createDialog.form.station" style="width: 100%">
-            <el-option label="北京朝阳一站" value="北京朝阳一站" />
-            <el-option label="上海浦东一站" value="上海浦东一站" />
+          <el-select v-model="createDialog.form.stationId" placeholder="请选择站点" style="width: 100%">
+            <el-option v-for="station in createDialog.stationOptions" :key="station.id" :label="station.name" :value="station.id" />
           </el-select>
         </el-form-item>
       </el-form>
@@ -610,397 +593,454 @@
   </div>
 </template>
 
-<script setup>
-import { ref, reactive, computed } from 'vue'
+<script setup lang="ts">
+import { ref, reactive, onMounted } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
-
+import {
+  listFulfiller, getFulfiller, addFulfiller, updateFulfiller,
+  changeStatus, resetPwd, reward, adjustPoints, adjustBalance,
+  listPointsLog, listBalanceLog, listRewardLog
+} from '@/api/fulfiller/pool'
+import type {
+  FlfFulfillerVO, FlfFulfillerForm, FlfFulfillerQuery,
+  FlfRewardForm, FlfAdjustPointsForm, FlfAdjustBalanceForm,
+  FlfPointsLogVO, FlfBalanceLogVO, FlfRewardLogVO
+} from '@/api/fulfiller/pool/types'
+import { listAllTag } from '@/api/fulfiller/tag'
+import type { FlfTagVO } from '@/api/fulfiller/tag/types'
+import { listOnStore } from '@/api/system/areaStation'
+import type { SysAreaStationOnStoreVo } from '@/api/system/areaStation/types'
+
+const loading = ref(false)
 const searchKey = ref('')
-const filterCity = ref('')
-const filterStation = ref('')
 const activeTab = ref('all')
+const total = ref(0)
+const tableData = ref<FlfFulfillerVO[]>([])
+const allTags = ref<FlfTagVO[]>([])
+const areaStationList = ref<SysAreaStationOnStoreVo[]>([])
+const cityCascaderOptions = ref<any[]>([])
+const stationOptions = ref<SysAreaStationOnStoreVo[]>([])
+const filterCascaderValue = ref<any[]>([])
+
+const queryParams = reactive<FlfFulfillerQuery>({
+  pageNum: 1,
+  pageSize: 10
+})
 
-const currentPage = ref(1)
-const pageSize = ref(10)
-const total = ref(1205)
-
-const handleSizeChange = (val) => { console.log(`每页 ${val} 条`) }
-const handleCurrentChange = (val) => { console.log(`当前页: ${val}`) }
-
-// Drawer State instead of Dialog
+// Drawer State
 const detailVisible = ref(false)
 const activeDetailTab = ref('info')
-const currentItem = ref(null)
-
-const tableData = ref([
-  {
-    id: 101,
-    name: '王大力',
-    gender: 'male',
-    age: 28,
-    workType: 'full_time',
-    phone: '13566668888',
-    level: 'gold',
-    authId: true,
-    authQual: true,
-    idNo: '1101************12',
-    city: '北京市',
-    station: '北京朝阳一站',
-    idExpiry: '2028-12-31',
-    realName: '王大力',
-    tags: [
-      { name: '行为矫正', type: 'warning' },
-      { name: '驾驶', type: 'info' },
-      { name: '摄影', type: 'success' }
-    ],
-    points: 2450,
-    balance: 1280.50,
-    orderCount: 1240,
-    rejectCount: 2,
-    status: 'busy',
-    avatar: '',
-    idCardFront: 'https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg',
-    idCardBack: 'https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg',
-    qualImages: [
-      'https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg',
-      'https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg'
-    ]
-  },
-  {
-    id: 102,
-    name: '张小美',
-    gender: 'female',
-    age: 24,
-    workType: 'part_time',
-    phone: '13612345678',
-    level: 'silver',
-    authId: true,
-    authQual: false,
-    idNo: '3101************34',
-    city: '上海市',
-    station: '上海浦东一站',
-    tags: [
-      { name: '洗护护理', type: 'primary' },
-      { name: '精细美容', type: 'danger' }
-    ],
-    points: 890,
-    balance: 320.00,
-    orderCount: 450,
-    rejectCount: 0,
-    status: 'resting',
-    avatar: '',
-    idCardFront: 'https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg'
-  },
-  {
-    id: 103,
-    name: '李建国',
-    gender: 'male',
-    age: 35,
-    workType: 'full_time',
-    phone: '13987654321',
-    level: 'bronze',
-    authId: true,
-    authQual: true,
-    idNo: '1101************99',
-    city: '北京市',
-    station: '北京海淀二站',
-    tags: [
-      { name: '基础喂遛', type: '' }
-    ],
-    points: 120,
-    balance: 50.00,
-    orderCount: 56,
-    rejectCount: 5,
-    status: 'disabled',
-    avatar: ''
+const currentItem = ref<FlfFulfillerVO | null>(null)
+
+// Log data for detail tabs
+const pointsLogData = ref<FlfPointsLogVO[]>([])
+const balanceLogData = ref<FlfBalanceLogVO[]>([])
+const rewardLogData = ref<FlfRewardLogVO[]>([])
+const logLoading = ref(false)
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const params: FlfFulfillerQuery = {
+      pageNum: queryParams.pageNum,
+      pageSize: queryParams.pageSize,
+      status: activeTab.value === 'all' ? undefined : activeTab.value,
+      keyword: searchKey.value || undefined,
+      cityCode: queryParams.cityCode || undefined,
+      stationId: queryParams.stationId || undefined
+    }
+    const res = await listFulfiller(params)
+    tableData.value = res.rows
+    total.value = res.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 加载全部标签(选择器用) */
+const loadAllTags = async () => {
+  try {
+    const res = await listAllTag({ category: 'fulfiller' })
+    allTags.value = res.data || []
+  } catch { /* ignore */ }
+}
+
+/** 加载区域站点数据并构建级联树 */
+const loadAreaStations = async () => {
+  try {
+    const res = await listOnStore()
+    const list = res.data || []
+    areaStationList.value = list
+    // 构建城市→区域的级联树(不含站点type=2)
+    const cities = list.filter(item => item.type === 0)
+    cityCascaderOptions.value = cities.map(city => {
+      const districts = list.filter(d => d.parentId == city.id && d.type === 1)
+      return {
+        value: city.id,
+        label: city.name,
+        children: districts.length > 0 ? districts.map(d => ({ value: d.id, label: d.name })) : undefined
+      }
+    })
+  } catch { /* ignore */ }
+}
+
+/** 根据级联选择的最后一级ID加载站点列表 */
+const loadStationsByAreaId = (areaId: string | number) => {
+  stationOptions.value = areaStationList.value.filter(item => item.parentId == areaId && item.type === 2)
+}
+
+/** 根据级联值获取cityCode和cityName */
+const getCityInfoFromCascader = (cascaderValue: any[]) => {
+  if (!cascaderValue || cascaderValue.length === 0) return { cityCode: '', cityName: '' }
+  const lastId = cascaderValue[cascaderValue.length - 1]
+  const names: string[] = []
+  for (const id of cascaderValue) {
+    const item = areaStationList.value.find(i => i.id == id)
+    if (item) names.push(item.name || '')
   }
-])
-
-// Mock Data for Detail Tabs
-const mockOrders = ref([
-  { orderNo: 'ORD20240204001', serviceName: '上门洗澡-中型犬', amount: 88, serviceFee: 66, time: '2024-02-04 10:00', status: 'completed' },
-  { orderNo: 'ORD20240204002', serviceName: '家庭寄养-3天', amount: 240, serviceFee: 190, time: '2024-02-03 14:30', status: 'completed' },
-  { orderNo: 'ORD20240203009', serviceName: '遛狗服务-1小时', amount: 45, serviceFee: 0, time: '2024-02-03 09:00', status: 'cancelled' },
-])
-
-const mockRewards = ref([
-  { time: '2024-02-01 10:00:00', type: 'reward', target: 'points', val: 20, reason: '月度全勤奖励', operator: '系统' },
-  { time: '2024-01-25 15:30:12', type: 'punish', target: 'balance', val: 50.00, reason: '用户投诉服务态度差', operator: 'admin' },
-])
-
-const mockBalanceLogs = ref([
-  { time: '2024-02-04 11:30:00', subType: 'settle', subTypeName: '服务结算', amount: 66.00, balanceAfter: 1280.50, reason: '订单ORD20240204001结算', operator: '系统' },
-  { time: '2024-02-04 10:00:00', subType: 'withdraw', subTypeName: '提现', amount: -200.00, balanceAfter: 1214.50, reason: '用户提现申请', operator: '系统' },
-  { time: '2024-02-03 15:00:00', subType: 'salary', subTypeName: '工资发放', amount: 3500.00, balanceAfter: 4714.50, reason: '2024年1月工资发放', operator: '财务' },
-  { time: '2024-01-25 15:30:12', subType: 'punish', subTypeName: '惩罚', amount: -50.00, balanceAfter: 1224.50, reason: '用户投诉服务态度差', operator: 'admin' },
-  { time: '2024-01-20 10:00:00', subType: 'reward', subTypeName: '奖励', amount: 100.00, balanceAfter: 1274.50, reason: '季度优秀员工奖励', operator: 'admin' },
-])
-
-const mockPointLogs = ref([
-  { time: '2024-02-04 10:00:00', bizType: 'order', bizTypeName: '订单完成', amount: 50, reason: '完成订单 ORD20240204001', operator: '系统' },
-  { time: '2024-02-03 12:00:00', bizType: 'reward', bizTypeName: '奖励', amount: 20, reason: '获得用户5星好评', operator: '系统' },
-  { time: '2024-02-01 10:00:00', bizType: 'other', bizTypeName: '其他', amount: 100, reason: '系统补偿', operator: '系统' },
-  { time: '2024-01-20 15:30:00', bizType: 'punish', bizTypeName: '惩罚', amount: -10, reason: '接单后无故取消', operator: 'admin' }
-])
-
-const getBizTypeTag = (type) => {
-  // Points: order, reward, punish, other
-  // Balance: settle, withdraw, salary, reward, punish, other
-  const map = {
-    order: '',
-    reward: 'success',
-    salary: 'success',
-    settle: 'success',
-    punish: 'danger',
-    withdraw: 'warning',
-    other: 'info'
+  return { cityCode: String(lastId), cityName: names.join(' ') }
+}
+
+/** 搜索框回车/清除触发查询 */
+const handleSearch = () => {
+  queryParams.pageNum = 1
+  getList()
+}
+
+const handleSizeChange = (val: number) => { queryParams.pageSize = val; getList() }
+const handleCurrentChange = (val: number) => { queryParams.pageNum = val; getList() }
+
+/** 积分业务类型标签颜色 */
+const getBizTypeTag = (type: string) => {
+  const map: Record<string, string> = {
+    order: '', reward: 'success', admin_reward: 'success',
+    punish: 'danger', admin_punish: 'danger',
+    adjust: 'info', admin_adjust: 'info'
   }
   return map[type] || 'info'
 }
 
+/** 积分业务类型中文名称 */
+const getBizTypeName = (type: string) => {
+  const map: Record<string, string> = {
+    order: '订单',
+    reward: '奖励',
+    punish: '惩罚',
+    adjust: '手动调整',
+    admin_adjust: '后台调整',
+    admin_reward: '后台奖励',
+    admin_punish: '后台惩罚'
+  }
+  return map[type] || type
+}
+
+/** 余额资金类型中文名称 */
+const getSubTypeName = (type: string) => {
+  const map: Record<string, string> = {
+    reward: '奖励',
+    punish: '惩罚',
+    salary: '工资发放',
+    withdraw: '提现',
+    settle: '结算',
+    other: '其他',
+    admin_adjust: '后台调整',
+    admin_reward: '后台奖励',
+    admin_punish: '后台惩罚'
+  }
+  return map[type] || type
+}
+
 const rewardDialog = reactive({
   visible: false,
   userName: '',
-  form: {
-    type: 'punish',
-    target: 'points',
-    amount: 10,
-    reason: ''
-  }
+  fulfillerId: 0 as number | string,
+  form: { type: 'reward', target: 'points', amount: 10, reason: '' }
 })
 
 const editDialog = reactive({
   visible: false,
-  form: {
-    id: null,
-    name: '',
-    phone: '',
-    gender: '',
-    idNo: '',
-    city: '',
-    station: '',
-    level: '',
-    status: '',
-    authId: false,
-    authQual: false,
-    tags: []
-  }
+  form: {} as FlfFulfillerForm,
+  cascaderValue: [] as any[],
+  stationOptions: [] as SysAreaStationOnStoreVo[]
 })
 
 const createDialog = reactive({
   visible: false,
   form: {
-    name: '',
-    phone: '',
-    password: '',
-    city: '北京市',
-    station: '',
-    gender: 'male'
-  }
+    name: '', phone: '', password: '', cityCode: '', cityName: '', stationId: undefined as any, gender: '0', workType: 'full_time'
+  },
+  cascaderValue: [] as any[],
+  stationOptions: [] as SysAreaStationOnStoreVo[]
 })
 
 const pointsDialog = reactive({
   visible: false,
-  currentRow: null,
+  currentRow: null as FlfFulfillerVO | null,
   form: { type: 'add', amount: 0, reason: '' }
 })
 
 const balanceDialog = reactive({
   visible: false,
-  currentRow: null,
+  currentRow: null as FlfFulfillerVO | null,
   form: { type: 'add', subType: 'reward', amount: 0, reason: '' }
 })
 
-const getStatusText = (status) => {
-  const map = { busy: '接单中', resting: '休息', disabled: '禁用', frozen: '冻结' }
+const getStatusText = (status: string) => {
+  const map: Record<string, string> = { busy: '接单中', resting: '休息', disabled: '禁用', frozen: '冻结' }
   return map[status] || '未知'
 }
 
-const getLevelText = (level) => {
-  const map = { gold: 'Lv.3 金牌', silver: 'Lv.2 银牌', bronze: 'Lv.1 铜牌' }
-  return map[level] || 'Lv.0 普通'
+const getLevelText = (levelName: string) => {
+  return levelName || '普通'
 }
 
-const getLevelType = (level) => {
-  const map = { gold: 'warning', silver: 'info', bronze: 'danger' }
-  return map[level] || 'info'
+const getLevelType = (levelName: string) => {
+  if (!levelName) return 'info'
+  if (levelName.includes('金')) return 'warning'
+  if (levelName.includes('银')) return 'info'
+  if (levelName.includes('铜')) return 'danger'
+  return 'info'
 }
 
-const handleTabClick = (tab) => {
-  console.log('Tab switch:', tab.props.name)
+const handleTabClick = (tab: any) => {
+  activeTab.value = tab.paneName
+  queryParams.pageNum = 1
+  getList()
 }
 
-const filteredTableData = computed(() => {
-  let data = tableData.value
-  // Status Filter
-  if (activeTab.value !== 'all') {
-    data = data.filter(item => item.status === activeTab.value)
+/** 加载日志数据 */
+const loadLogs = async (fulfillerId: string | number) => {
+  logLoading.value = true
+  try {
+    const [pRes, bRes, rRes] = await Promise.all([
+      listPointsLog(fulfillerId, { pageNum: 1, pageSize: 20 }),
+      listBalanceLog(fulfillerId, { pageNum: 1, pageSize: 20 }),
+      listRewardLog(fulfillerId, { pageNum: 1, pageSize: 20 })
+    ])
+    pointsLogData.value = pRes.rows || []
+    balanceLogData.value = bRes.rows || []
+    rewardLogData.value = rRes.rows || []
+  } catch { /* ignore */ } finally {
+    logLoading.value = false
   }
-  // Search Filter
-  if (searchKey.value) {
-    const key = searchKey.value.toLowerCase()
-    data = data.filter(item =>
-      item.name.toLowerCase().includes(key) ||
-      item.phone.includes(key) ||
-      (item.idNo && item.idNo.includes(key))
-    )
-  }
-  // City & Station Filter
-  if (filterCity.value) {
-    // Simple mapping for demo, usually would match exact city code/name
-    if(filterCity.value === 'beijing') data = data.filter(item => item.city.includes('北京'))
-    if(filterCity.value === 'shanghai') data = data.filter(item => item.city.includes('上海'))
-    if(filterCity.value === 'shenzhen') data = data.filter(item => item.city.includes('深圳'))
-  }
-  if (filterStation.value) {
-    if(filterStation.value === 'bj-cy-01') data = data.filter(item => item.station.includes('北京朝阳一站'))
-    if(filterStation.value === 'sh-pd-01') data = data.filter(item => item.station.includes('上海浦东一站'))
-  }
-  return data
-})
+}
 
-const handleDetail = (row) => {
-  currentItem.value = row
+const handleDetail = async (row: FlfFulfillerVO) => {
+  try {
+    const res = await getFulfiller(row.id)
+    currentItem.value = res.data
+  } catch {
+    currentItem.value = { ...row }
+  }
   activeDetailTab.value = 'info'
   detailVisible.value = true
+  loadLogs(row.id)
 }
 
-const handleEdit = (row) => {
+const handleEdit = (row: FlfFulfillerVO) => {
   editDialog.form = {
     id: row.id,
     name: row.name,
     phone: row.phone,
-    password: '', // Reset password field
     gender: row.gender,
-    idNo: row.idNo,
-    city: row.city,
-    station: row.station,
-    level: row.level,
+    idCard: row.idCard,
+    cityCode: row.cityCode,
+    cityName: row.cityName,
+    stationId: row.stationId,
+    levelId: row.levelId,
     status: row.status,
     authId: row.authId,
     authQual: row.authQual,
-    tags: row.tags ? row.tags.map(t => t.name) : []
+    tagIds: row.tags ? row.tags.map(t => t.id) : []
+  }
+  // 根据cityCode构建级联选择器的值
+  editDialog.cascaderValue = []
+  editDialog.stationOptions = []
+  if (row.cityCode) {
+    const item = areaStationList.value.find(i => String(i.id) === row.cityCode)
+    if (item) {
+      if (item.type === 1 && item.parentId) {
+        // 区域级:cascaderValue = [城市ID, 区域ID]
+        editDialog.cascaderValue = [item.parentId, item.id]
+      } else {
+        // 城市级:cascaderValue = [城市ID]
+        editDialog.cascaderValue = [item.id]
+      }
+      loadStationsByAreaId(item.id)
+      editDialog.stationOptions = stationOptions.value
+    }
   }
   editDialog.visible = true
 }
 
 const handleCreate = () => {
-  createDialog.form = { name: '', phone: '', password: '', city: '北京市', station: '', gender: 'male' }
+  createDialog.form = { name: '', phone: '', password: '', cityCode: '', cityName: '', stationId: undefined, gender: '0', workType: 'full_time' }
+  createDialog.cascaderValue = []
+  createDialog.stationOptions = []
   createDialog.visible = true
 }
 
-const submitCreate = () => {
-  if(!createDialog.form.name || !createDialog.form.phone || !createDialog.form.password) {
+const submitCreate = async () => {
+  if (!createDialog.form.name || !createDialog.form.phone || !createDialog.form.password) {
     ElMessage.warning('请填写完整信息')
     return
   }
-  // Simulate creation
-  tableData.value.unshift({
-    id: Date.now(),
-    ...createDialog.form,
-    level: 'bronze',
-    status: 'online',
-    points: 0,
-    balance: 0,
-    orderCount: 0,
-    rejectCount: 0,
-    status: 'online',
-    avatar: '',
-    authId: false,
-    authQual: false,
-    tags: []
-  })
-  createDialog.visible = false
-  ElMessage.success('创建成功')
+  try {
+    await addFulfiller(createDialog.form as FlfFulfillerForm)
+    createDialog.visible = false
+    ElMessage.success('创建成功')
+    getList()
+  } catch { /* handled by interceptor */ }
 }
 
-const saveEdit = () => {
-  const idx = tableData.value.findIndex(item => item.id === editDialog.form.id)
-  if(idx !== -1) {
-    // update basic info
-    const target = tableData.value[idx]
-    target.name = editDialog.form.name
-    target.phone = editDialog.form.phone
-    target.gender = editDialog.form.gender
-    target.idNo = editDialog.form.idNo
-    target.city = editDialog.form.city
-    target.station = editDialog.form.station
-    target.level = editDialog.form.level
-    target.status = editDialog.form.status
-    target.authId = editDialog.form.authId
-    target.authQual = editDialog.form.authQual
-
-    // update tags
-    // simplistic approach: find predefined colors or default 'info'
-    const colorMap = { '行为矫正': 'warning', '驾驶': 'info', '摄影': 'success', '洗护护理': 'primary', '精细美容': 'danger' }
-    target.tags = editDialog.form.tags.map(t => ({ name: t, type: colorMap[t] || 'info' }))
-
+const saveEdit = async () => {
+  try {
+    await updateFulfiller(editDialog.form)
     ElMessage.success('更新成功')
-  }
-  editDialog.visible = false
+    editDialog.visible = false
+    getList()
+  } catch { /* handled by interceptor */ }
 }
 
-const handleReward = (row) => {
+const handleReward = (row: FlfFulfillerVO) => {
   rewardDialog.userName = row.name
+  rewardDialog.fulfillerId = row.id
   rewardDialog.form = { type: 'reward', target: 'points', amount: 10, reason: '' }
   rewardDialog.visible = true
 }
-const submitReward = () => {
-  ElMessage.success('操作成功')
-  rewardDialog.visible = false
+
+const submitReward = async () => {
+  try {
+    await reward({
+      fulfillerId: rewardDialog.fulfillerId,
+      type: rewardDialog.form.type,
+      target: rewardDialog.form.target,
+      amount: rewardDialog.form.amount,
+      reason: rewardDialog.form.reason
+    })
+    ElMessage.success('操作成功')
+    rewardDialog.visible = false
+    getList()
+  } catch { /* handled by interceptor */ }
 }
 
-const handleCommand = (cmd, row) => {
-  if(cmd === 'adjustPoints') {
+const handleCommand = async (cmd: string, row: FlfFulfillerVO) => {
+  if (cmd === 'adjustPoints') {
     pointsDialog.currentRow = row
     pointsDialog.form = { type: 'add', amount: 0, reason: '' }
     pointsDialog.visible = true
-  } else if(cmd === 'adjustBalance') {
+  } else if (cmd === 'adjustBalance') {
     balanceDialog.currentRow = row
     balanceDialog.form = { type: 'add', subType: 'reward', amount: 0, reason: '' }
     balanceDialog.visible = true
-  } else if(cmd === 'disable') {
-    ElMessageBox.confirm(`确定禁用履约者【${row.name}】吗?禁用后将无法接单。`, '提示', { type: 'warning' })
-      .then(() => {
-        row.status = 'disabled'
-        ElMessage.success('账号已禁用')
-      })
-  } else if(cmd === 'enable') {
-    ElMessageBox.confirm(`确定启用履约者【${row.name}】吗?`, '提示', { type: 'success' })
-      .then(() => {
-        row.status = 'resting' // Default to resting when enabled
-        ElMessage.success('账号已启用')
-      })
-  } else if(cmd === 'resetPwd') {
-    ElMessageBox.confirm('确定重置密码为默认密码 [123456] 吗?', '提示', { type: 'info' })
-      .then(() => { ElMessage.success('密码重置成功') })
+  } else if (cmd === 'disable') {
+    await ElMessageBox.confirm(`确定禁用履约者【${row.name}】吗?禁用后将无法接单。`, '提示', { type: 'warning' })
+    try {
+      await changeStatus(row.id, 'disabled')
+      ElMessage.success('账号已禁用')
+      getList()
+    } catch { /* handled by interceptor */ }
+  } else if (cmd === 'enable') {
+    await ElMessageBox.confirm(`确定启用履约者【${row.name}】吗?`, '提示', { type: 'success' })
+    try {
+      await changeStatus(row.id, 'resting')
+      ElMessage.success('账号已启用')
+      getList()
+    } catch { /* handled by interceptor */ }
+  } else if (cmd === 'resetPwd') {
+    await ElMessageBox.confirm('确定重置密码为默认密码 [123456] 吗?', '提示', { type: 'info' })
+    try {
+      await resetPwd(row.id, '123456')
+      ElMessage.success('密码重置成功')
+    } catch { /* handled by interceptor */ }
   }
 }
 
-const submitPointsAdjust = () => {
-  if(pointsDialog.currentRow) {
-    if(pointsDialog.form.type === 'add') pointsDialog.currentRow.points += pointsDialog.form.amount
-    else pointsDialog.currentRow.points -= pointsDialog.form.amount
+const submitPointsAdjust = async () => {
+  if (!pointsDialog.currentRow) return
+  try {
+    await adjustPoints({
+      fulfillerId: pointsDialog.currentRow.id,
+      type: pointsDialog.form.type,
+      amount: pointsDialog.form.amount,
+      reason: pointsDialog.form.reason
+    })
     ElMessage.success('积分调整成功')
-  }
-  pointsDialog.visible = false
+    pointsDialog.visible = false
+    getList()
+  } catch { /* handled by interceptor */ }
 }
 
-const submitBalanceAdjust = () => {
-  if(balanceDialog.currentRow) {
-    let amt = balanceDialog.form.amount
-    if(balanceDialog.form.type === 'reduce') amt = -amt
-    balanceDialog.currentRow.balance += amt
-    // keep 2 decimals
-    balanceDialog.currentRow.balance = Math.round(balanceDialog.currentRow.balance * 100) / 100
+const submitBalanceAdjust = async () => {
+  if (!balanceDialog.currentRow) return
+  try {
+    await adjustBalance({
+      fulfillerId: balanceDialog.currentRow.id,
+      type: balanceDialog.form.type,
+      subType: balanceDialog.form.subType,
+      amount: balanceDialog.form.amount,
+      reason: balanceDialog.form.reason
+    })
     ElMessage.success('余额调整成功')
-  }
-  balanceDialog.visible = false
+    balanceDialog.visible = false
+    getList()
+  } catch { /* handled by interceptor */ }
 }
 
-const handleViewImage = (url) => {
+const handleViewImage = (url: string) => {
   // Already handled by el-image preview
 }
 
+/** 顶部筛选:级联选择变化时 */
+const handleFilterCascaderChange = (val: any[]) => {
+  if (val && val.length > 0) {
+    const lastId = val[val.length - 1]
+    queryParams.cityCode = String(lastId)
+    loadStationsByAreaId(lastId)
+  } else {
+    queryParams.cityCode = undefined
+    stationOptions.value = []
+  }
+  queryParams.stationId = undefined
+  getList()
+}
+
+/** 编辑对话框:级联选择变化时 */
+const handleEditCascaderChange = (val: any[]) => {
+  const { cityCode, cityName } = getCityInfoFromCascader(val)
+  editDialog.form.cityCode = cityCode
+  editDialog.form.cityName = cityName
+  if (val && val.length > 0) {
+    const lastId = val[val.length - 1]
+    loadStationsByAreaId(lastId)
+    editDialog.stationOptions = stationOptions.value
+  } else {
+    editDialog.stationOptions = []
+  }
+  editDialog.form.stationId = undefined
+}
+
+/** 新增对话框:级联选择变化时 */
+const handleCreateCascaderChange = (val: any[]) => {
+  const { cityCode, cityName } = getCityInfoFromCascader(val)
+  createDialog.form.cityCode = cityCode
+  createDialog.form.cityName = cityName
+  if (val && val.length > 0) {
+    const lastId = val[val.length - 1]
+    loadStationsByAreaId(lastId)
+    createDialog.stationOptions = stationOptions.value
+  } else {
+    createDialog.stationOptions = []
+  }
+  createDialog.form.stationId = undefined
+}
+
+onMounted(() => {
+  getList()
+  loadAllTags()
+  loadAreaStations()
+})
 </script>
 
 <style scoped>

+ 150 - 47
src/views/fulfiller/tag/index.vue

@@ -8,29 +8,32 @@
         </div>
       </template>
 
-      <el-table :data="tableData" border style="width: 100%">
+      <el-table v-loading="loading" :data="tableData" border style="width: 100%">
         <el-table-column prop="name" label="标签名称" width="200">
           <template #default="scope">
             <el-tag :type="scope.row.colorType">{{ scope.row.name }}</el-tag>
           </template>
         </el-table-column>
-        <el-table-column prop="desc" label="说明" />
+        <el-table-column prop="description" label="说明" />
         <el-table-column prop="status" label="状态" width="100">
           <template #default="scope">
-            <el-switch v-model="scope.row.status" />
+            <el-tag :type="scope.row.status === 0 ? 'success' : 'info'" size="small">
+              {{ scope.row.status === 0 ? '启用' : '停用' }}
+            </el-tag>
           </template>
         </el-table-column>
+        <el-table-column prop="createTime" label="创建时间" width="180" />
         <el-table-column label="操作" width="150">
           <template #default="scope">
             <el-button link type="primary" @click="handleEdit(scope.row)">编辑</el-button>
-            <el-button link type="danger">删除</el-button>
+            <el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
           </template>
         </el-table-column>
       </el-table>
       <div class="pagination-container">
         <el-pagination
-          v-model:current-page="currentPage"
-          v-model:page-size="pageSize"
+          v-model:current-page="queryParams.pageNum"
+          v-model:page-size="queryParams.pageSize"
           :page-sizes="[10, 20, 50, 100]"
           layout="total, sizes, prev, pager, next, jumper"
           :total="total"
@@ -41,80 +44,159 @@
     </el-card>
 
     <el-dialog v-model="dialogVisible" :title="isEdit ? '编辑标签' : '新增标签'" width="400px">
-      <el-form :model="form" label-width="80px">
-        <el-form-item label="标签名称" required>
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="标签名称" prop="name">
           <el-input v-model="form.name" placeholder="如:非常有耐心" />
         </el-form-item>
-        <el-form-item label="标签颜色">
-          <el-select v-model="form.colorType" style="width: 100%">
-            <el-option label="默认蓝" value="" />
-            <el-option label="成功绿" value="success" />
-            <el-option label="警告橙" value="warning" />
-            <el-option label="危险红" value="danger" />
-            <el-option label="信息灰" value="info" />
-          </el-select>
+        <el-form-item label="标签颜色" prop="colorType">
+          <div class="color-picker-group">
+            <span
+              v-for="item in colorOptions"
+              :key="item.value"
+              class="color-dot"
+              :class="{ active: form.colorType === item.value }"
+              :style="{ backgroundColor: item.color }"
+              :title="item.label"
+              @click="form.colorType = item.value"
+            ></span>
+          </div>
         </el-form-item>
-        <el-form-item label="描述说明">
-          <el-input v-model="form.desc" type="textarea" />
+        <el-form-item label="状态">
+          <el-radio-group v-model="form.status">
+            <el-radio :value="0">正常</el-radio>
+            <el-radio :value="1">停用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="描述说明" prop="description">
+          <el-input v-model="form.description" type="textarea" placeholder="请输入描述说明" />
         </el-form-item>
       </el-form>
       <template #footer>
         <span class="dialog-footer">
           <el-button @click="dialogVisible = false">取消</el-button>
-          <el-button type="primary" @click="saveTag">保存</el-button>
+          <el-button type="primary" :loading="submitLoading" @click="saveTag">保存</el-button>
         </span>
       </template>
     </el-dialog>
   </div>
 </template>
 
-<script setup>
-import { ref, reactive } from 'vue';
-import { ElMessage } from 'element-plus';
+<script setup lang="ts">
+import { ref, reactive, onMounted } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import type { FormInstance, FormRules } from 'element-plus';
+import { listTag, addTag, updateTag, delTag } from '@/api/fulfiller/tag';
+import type { FlfTagVO, FlfTagForm, FlfTagQuery } from '@/api/fulfiller/tag/types';
 
+const loading = ref(false);
+const submitLoading = ref(false);
 const dialogVisible = ref(false);
 const isEdit = ref(false);
+const formRef = ref<FormInstance>();
+const total = ref(0);
+const tableData = ref<FlfTagVO[]>([]);
 
-const currentPage = ref(1);
-const pageSize = ref(10);
-const total = ref(100);
-
-const handleSizeChange = (val) => {
-  console.log(`每页 ${val} 条`);
-};
-const handleCurrentChange = (val) => {
-  console.log(`当前页: ${val}`);
-};
-
-const tableData = ref([
-  { id: 1, name: '接单王', colorType: 'danger', desc: '累计接单数量TOP10', status: true },
-  { id: 2, name: '零差评', colorType: 'success', desc: '近半年无差评记录', status: true },
-  { id: 3, name: '车技娴熟', colorType: 'primary', desc: '驾驶技术评价高', status: true },
-  { id: 4, name: '五星金牌', colorType: 'warning', desc: '系统评定最高等级', status: true }
-]);
+const queryParams = reactive<FlfTagQuery>({
+  pageNum: 1,
+  pageSize: 10,
+  category: 'fulfiller'
+});
 
-const form = reactive({
+const form = reactive<FlfTagForm>({
+  id: undefined,
   name: '',
   colorType: '',
-  desc: ''
+  category: 'fulfiller',
+  description: '',
+  status: 0
 });
 
+const colorOptions = [
+  { value: 'info', label: '信息灰', color: '#909399' },
+  { value: '', label: '默认蓝', color: '#409eff' },
+  { value: 'success', label: '成功绿', color: '#67c23a' },
+  { value: 'warning', label: '警告橙', color: '#e6a23c' },
+  { value: 'danger', label: '危险红', color: '#f56c6c' }
+];
+
+const rules = reactive<FormRules>({
+  name: [{ required: true, message: '标签名称不能为空', trigger: 'blur' }]
+});
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true;
+  try {
+    const res = await listTag(queryParams);
+    tableData.value = res.rows;
+    total.value = res.total;
+  } finally {
+    loading.value = false;
+  }
+};
+
+const handleSizeChange = (val: number) => {
+  queryParams.pageSize = val;
+  getList();
+};
+const handleCurrentChange = (val: number) => {
+  queryParams.pageNum = val;
+  getList();
+};
+
+const resetForm = () => {
+  Object.assign(form, { id: undefined, name: '', colorType: '', category: 'fulfiller', description: '', status: 0 });
+  formRef.value?.resetFields();
+};
+
 const handleAdd = () => {
   isEdit.value = false;
-  Object.assign(form, { name: '', colorType: '', desc: '' });
+  resetForm();
   dialogVisible.value = true;
 };
 
-const handleEdit = (row) => {
+const handleEdit = (row: FlfTagVO) => {
   isEdit.value = true;
-  Object.assign(form, row);
+  resetForm();
+  Object.assign(form, {
+    id: row.id,
+    name: row.name,
+    colorType: row.colorType,
+    category: row.category,
+    description: row.description,
+    status: row.status
+  });
   dialogVisible.value = true;
 };
 
-const saveTag = () => {
-  ElMessage.success('保存成功');
-  dialogVisible.value = false;
+const handleDelete = (row: FlfTagVO) => {
+  ElMessageBox.confirm('确认删除标签「' + row.name + '」吗?', '提示', { type: 'warning' }).then(async () => {
+    await delTag(row.id);
+    ElMessage.success('删除成功');
+    getList();
+  }).catch(() => {});
 };
+
+const saveTag = async () => {
+  await formRef.value?.validate();
+  submitLoading.value = true;
+  try {
+    if (isEdit.value) {
+      await updateTag(form);
+    } else {
+      await addTag(form);
+    }
+    ElMessage.success(isEdit.value ? '修改成功' : '新增成功');
+    dialogVisible.value = false;
+    getList();
+  } finally {
+    submitLoading.value = false;
+  }
+};
+
+onMounted(() => {
+  getList();
+});
 </script>
 
 <style scoped>
@@ -134,4 +216,25 @@ const saveTag = () => {
   justify-content: flex-end;
   margin-top: 20px;
 }
+.color-picker-group {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+.color-dot {
+  display: inline-block;
+  width: 28px;
+  height: 28px;
+  border-radius: 50%;
+  cursor: pointer;
+  border: 3px solid transparent;
+  transition: border-color 0.2s, box-shadow 0.2s;
+}
+.color-dot:hover {
+  box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
+}
+.color-dot.active {
+  border-color: #409eff;
+  box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.3);
+}
 </style>