Sfoglia il codice sorgente

修复部分bug;违规、投诉基本完成;等级管理基本完成

Huanyi 3 settimane fa
parent
commit
7536981f95

+ 39 - 0
src/api/fulfiller/complaint/index.ts

@@ -0,0 +1,39 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import type { ComplaintForm, ComplaintVO, ComplaintPageVO } from './types';
+
+/**
+ * 新增投诉
+ * @param data
+ */
+export const addComplaint = (data: ComplaintForm): AxiosPromise<void> => {
+  return request({
+    url: '/fulfiller/complaint/add',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 根据订单查询投诉记录
+ * @param orderId
+ */
+export const listComplaintByOrder = (orderId: string | number): AxiosPromise<ComplaintVO[]> => {
+  return request({
+    url: '/fulfiller/complaint/listByOrder',
+    method: 'get',
+    params: { orderId }
+  });
+};
+
+/**
+ * 根据履约者分页查询投诉记录
+ * @param params
+ */
+export const pageComplaintByFulfiller = (params: { fulfiller: string | number; pageNum: number; pageSize: number }): AxiosPromise<{ total: number; rows: ComplaintPageVO[] }> => {
+  return request({
+    url: '/fulfiller/complaint/pageByFulfiller',
+    method: 'get',
+    params
+  });
+};

+ 30 - 0
src/api/fulfiller/complaint/types.ts

@@ -0,0 +1,30 @@
+export interface ComplaintForm {
+  id?: string | number;
+  fulfiller: string | number;
+  orderId: string | number;
+  reason: string;
+  createDept?: string | number;
+  createBy?: string | number;
+  createTime?: string;
+  updateBy?: string | number;
+  updateTime?: string;
+  params?: any;
+}
+
+export interface ComplaintVO {
+  id: string | number;
+  fulfiller: string;
+  orderId: string | number;
+  reason: string;
+  createTime: string;
+}
+
+export interface ComplaintPageVO {
+  id: string | number;
+  fulfiller: string | number;
+  orderCode: string;
+  orderId: string | number;
+  reason: string;
+  createTime: string;
+  createBy: string | number;
+}

+ 61 - 0
src/api/fulfiller/levelConfig/index.ts

@@ -0,0 +1,61 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { FlfLevelConfigVO, FlfLevelConfigForm } from './types';
+
+/**
+ * 查询等级配置列表
+ */
+export const listAllLevelConfig = (): AxiosPromise<FlfLevelConfigVO[]> => {
+  return request({
+    url: '/fulfiller/levelConfig/listAll',
+    method: 'get'
+  });
+};
+
+/**
+ * 新增等级配置
+ * @param data
+ */
+export const addLevelConfig = (data: FlfLevelConfigForm): AxiosPromise<void> => {
+  return request({
+    url: '/fulfiller/levelConfig/add',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改等级配置
+ * @param data
+ */
+export const updateLevelConfig = (data: FlfLevelConfigForm): AxiosPromise<void> => {
+  return request({
+    url: '/fulfiller/levelConfig/edit',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 编辑等级配置
+ * @param data
+ */
+export const editLevelConfig = (data: FlfLevelConfigForm): AxiosPromise<void> => {
+  return request({
+    url: '/fulfiller/levelConfig/edit',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除等级配置
+ * @param id
+ */
+export const delLevelConfig = (id: string | number): AxiosPromise<void> => {
+  return request({
+    url: '/fulfiller/levelConfig/remove',
+    method: 'delete',
+    params: { id }
+  });
+};

+ 28 - 0
src/api/fulfiller/levelConfig/types.ts

@@ -0,0 +1,28 @@
+export interface FlfLevelConfigVO {
+  id: number | string;
+  lvNo: number;
+  name: string;
+  icon: number | string;
+  iconUrl?: string;
+  background: number | string;
+  backgroundUrl?: string;
+  holidayBonus: number;
+  upgradeCompleteOrderQuality: number;
+  upgradeOntimeRate: number;
+  upgradeComplaintRate: number;
+  upgradeViolationsCount: number;
+  upgradePoints: number;
+  degradePoints: number;
+  degradeOntimeRate: number;
+  degradeOntimeRateScore: number;
+  degradeCompleteOrderQuality: number;
+  degradeCompleteOrderQualityScore: number;
+  degradeViolationsScore: number;
+  degradeComplaintCount: number;
+  degradeComplaintCountScore: number;
+  rights: number[];
+}
+
+export interface FlfLevelConfigForm extends Partial<FlfLevelConfigVO> {
+  // 表单可能需要的额外字段
+}

+ 12 - 0
src/api/fulfiller/levelRights/index.ts

@@ -36,6 +36,18 @@ export const updateLevelRights = (data: FlfLevelRightsForm): AxiosPromise<void>
   });
 };
 
+/**
+ * 修改等级权益状态
+ * @param data
+ */
+export const changeLevelRightsStatus = (data: { id: number; status: boolean }): AxiosPromise<void> => {
+  return request({
+    url: '/fulfiller/levelRights/changeStatus',
+    method: 'put',
+    data: data
+  });
+};
+
 /**
  * 删除等级权益
  * @param id

+ 4 - 0
src/enums/fulfiller.json

@@ -23,6 +23,10 @@
     "order_finish": {
       "label": "订单完成",
       "tagType": "info"
+    },
+    "violation_punish": {
+      "label": "违规惩罚",
+      "tagType": "danger"
     }
   },
   "BalanceBizType": {

+ 205 - 204
src/views/fulfiller/level/index.vue

@@ -11,39 +11,40 @@
           </template>
 
           <el-table :data="levelList" style="width: 100%" stripe>
-            <el-table-column prop="level" label="等级" width="100">
-              <template #default="{ row }">Lv.{{ row.level }}</template>
+            <el-table-column prop="lvNo" label="等级" width="100">
+              <template #default="{ row }">Lv.{{ row.lvNo }}</template>
             </el-table-column>
             <el-table-column label="等级名称" width="150">
               <template #default="{ row }">
                 <div class="level-name-col">
-                  <el-avatar shape="square" :size="30" :src="row.icon || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'" />
+                  <el-avatar shape="square" :size="30"
+                    :src="row.iconUrl || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'" />
                   <span>{{ row.name }}</span>
                 </div>
               </template>
             </el-table-column>
-            <el-table-column label="积分区间" width="200">
-              <template #default="{ row }"> {{ row.minPoints }} - {{ row.maxPoints === -1 ? '无限' : row.maxPoints }} </template>
+            <el-table-column label="升级积分" width="150">
+              <template #default="{ row }"> {{ row.upgradePoints }} </template>
             </el-table-column>
             <el-table-column label="节假日加成" width="150">
               <template #default="{ row }">
-                <span style="color: #ff9900; font-weight: bold">{{ row.holidayMultiplier }}倍</span> 积分
+                <span style="color: #ff9900; font-weight: bold">{{ (row.holidayBonus || 0) / 100 }}倍</span>
               </template>
             </el-table-column>
             <el-table-column label="包含权益" min-width="300">
               <template #default="{ row }">
                 <template v-if="row.rights && row.rights.length">
-                  <el-tag v-for="right in row.rights" :key="right?.id" v-if="right" size="small" class="right-tag" type="info">
-                    {{ right?.name }}
+                  <el-tag v-for="rightId in row.rights" :key="rightId" size="small" class="right-tag" type="info">
+                    {{ getRightName(rightId) }}
                   </el-tag>
                 </template>
                 <span v-else style="color: #999; font-size: 12px">暂无权益</span>
               </template>
             </el-table-column>
             <el-table-column label="操作" width="180">
-              <template #default="{ scope }">
-                <el-button link type="primary" @click="handleEditLevel(scope.row)">配置</el-button>
-                <el-button link type="danger" @click="handleDeleteLevel(scope.row)">删除</el-button>
+              <template #default="{ row }">
+                <el-button link type="primary" @click="handleEditLevel(row)">配置</el-button>
+                <el-button link type="danger" @click="handleDeleteLevel(row)">删除</el-button>
               </template>
             </el-table-column>
           </el-table>
@@ -62,11 +63,9 @@
             <el-table-column prop="name" label="权益名称" width="200">
               <template #default="{ row }">
                 <div class="level-name-col">
-                  <el-image
-                    style="width: 30px; height: 30px; border-radius: 4px"
+                  <el-image style="width: 30px; height: 30px; border-radius: 4px"
                     :src="row.iconUrl || 'https://cube.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg'"
-                    fit="cover"
-                  />
+                    fit="cover" />
                   <span>{{ row.name }}</span>
                 </div>
               </template>
@@ -93,113 +92,79 @@
       <el-form :model="levelDialog.form" label-width="120px">
         <el-tabs type="border-card">
           <el-tab-pane label="基础设置">
-            <el-form-item label="等级数值(Lv)" required>
-              <el-input-number v-model="levelDialog.form.level" :min="1" />
+            <el-form-item label="等级数值(Lv)">
+              <el-input-number v-model="levelDialog.form.lvNo" :min="1" />
             </el-form-item>
-            <el-form-item label="等级名称" required>
-              <el-input v-model="levelDialog.form.name" placeholder="如:青铜骑士" />
+            <el-form-item label="等级名称">
+              <el-input v-model="levelDialog.form.name" placeholder="请输入等级名称" />
             </el-form-item>
             <el-form-item label="等级图标">
-              <el-upload
-                class="avatar-uploader"
-                action="#"
-                :show-file-list="false"
-                :auto-upload="false"
-                :on-change="(file) => handleLevelIconChange(file, 'icon')"
-              >
-                <img v-if="levelDialog.form.icon" :src="levelDialog.form.icon" class="avatar" />
-                <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
-              </el-upload>
-              <div class="tips">建议尺寸 100x100px</div>
+              <image-upload v-model="levelDialog.form.icon" :limit="1" />
             </el-form-item>
             <el-form-item label="背景图片">
-              <el-upload
-                class="bg-uploader"
-                action="#"
-                :show-file-list="false"
-                :auto-upload="false"
-                :on-change="(file) => handleLevelIconChange(file, 'bg')"
-              >
-                <img v-if="levelDialog.form.bg" :src="levelDialog.form.bg" class="bg-img" />
-                <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
-              </el-upload>
-              <div class="tips">建议尺寸 600x300px</div>
+              <image-upload v-model="levelDialog.form.background" :limit="1" />
             </el-form-item>
-
-            <el-divider content-position="left">积分规则</el-divider>
-
-            <el-row>
-              <el-col :span="12">
-                <el-form-item label="最低积分" required>
-                  <el-input-number v-model="levelDialog.form.minPoints" :min="0" />
-                </el-form-item>
-              </el-col>
-              <el-col :span="12">
-                <el-form-item label="最高积分">
-                  <el-input-number v-model="levelDialog.form.maxPoints" :min="-1" placeholder="-1为无限" />
-                  <div class="tips">设置-1代表无上限</div>
-                </el-form-item>
-              </el-col>
-            </el-row>
-            <el-form-item label="节假日倍数" required>
-              <el-input-number v-model="levelDialog.form.holidayMultiplier" :min="1" :precision="1" :step="0.1" />
-              <div class="tips">法定节假日接单获得的积分倍率</div>
+            <el-form-item label="节假日加成">
+              <el-input-number v-model="levelDialog.form.holidayBonus" :min="0" :precision="2" :step="0.1" />
             </el-form-item>
-
-            <el-divider content-position="left">权益配置</el-divider>
-
-            <el-form-item label="权益关联">
-              <el-checkbox-group v-model="levelDialog.form.rightsIds">
+            <el-form-item label="权益配置">
+              <el-checkbox-group v-model="levelDialog.form.rights">
                 <el-checkbox v-for="r in rightsList" :key="r.id" :label="r.id" :disabled="!r.status">
                   {{ r.name }}
-                  <span v-if="!r.status" style="color: #999; font-size: 12px">(已停用)</span>
                 </el-checkbox>
               </el-checkbox-group>
             </el-form-item>
           </el-tab-pane>
 
-          <el-tab-pane label="升级规则">
+          <el-tab-pane label="升级规则(自然月内)">
             <div class="rule-group">
-              <div class="rule-title">自动升级条件 (需全部满足)</div>
-              <el-form-item label="考核周期">
-                最近 <el-input-number v-model="levelDialog.form.rules.periodDays" :min="1" size="small" style="width: 100px" /> 天
-              </el-form-item>
-              <el-form-item label="完成单量">
-                ≥ <el-input-number v-model="levelDialog.form.rules.minOrders" :min="0" size="small" style="width: 100px" /> 单
-              </el-form-item>
-              <el-form-item label="上门准时率">
-                ≥ <el-input-number v-model="levelDialog.form.rules.onTimeRate" :min="0" :max="100" size="small" style="width: 100px" /> %
-              </el-form-item>
-              <el-form-item label="投诉率">
-                ≤ <el-input-number v-model="levelDialog.form.rules.complaintRate" :min="0" :max="100" size="small" style="width: 100px" /> %
-              </el-form-item>
-              <el-form-item label="违规次数">
-                ≤ <el-input-number v-model="levelDialog.form.rules.maxViolations" :min="0" size="small" style="width: 100px" /> 次
-              </el-form-item>
-              <el-form-item label="积分门槛">
-                累计获得积分 ≥ <el-input-number v-model="levelDialog.form.rules.accumulatedPoints" :min="0" size="small" style="width: 120px" />
-              </el-form-item>
+              <div style="margin-bottom: 20px">
+                到达 <el-input-number v-model="levelDialog.form.upgradePoints" :min="0" /> 积分
+              </div>
+              <div style="margin-bottom: 20px">
+                完成 <el-input-number v-model="levelDialog.form.upgradeCompleteOrderQuality" :min="0" /> 单量
+              </div>
+              <div style="margin-bottom: 20px">
+                达到 <el-input-number v-model="levelDialog.form.upgradeOntimeRate" :min="0" :max="100" :precision="2"
+                  :step="0.01" /> 准时率
+              </div>
+              <div style="margin-bottom: 20px">
+                达到 <el-input-number v-model="levelDialog.form.upgradeComplaintRate" :min="0" :max="100" :precision="2"
+                  :step="0.01" /> 投诉率
+              </div>
+              <div style="margin-bottom: 20px">
+                低于 <el-input-number v-model="levelDialog.form.upgradeViolationsCount" :min="0" /> 违规次数
+              </div>
             </div>
           </el-tab-pane>
 
-          <el-tab-pane label="惩罚/降级">
-            <el-form-item label="降级规则">
-              <el-radio-group v-model="levelDialog.form.downgradeType">
-                <el-radio label="points">仅积分不足时降级</el-radio>
-                <el-radio label="strict">不满足升级条件即降级</el-radio>
-              </el-radio-group>
-            </el-form-item>
-            <div class="rule-group" style="margin-top: 10px">
-              <div class="rule-title">扣分配置</div>
-              <el-form-item label="准时率过低">
-                每低于1% 扣 <el-input-number v-model="levelDialog.form.deductions.late" :min="0" size="small" /> 分
-              </el-form-item>
-              <el-form-item label="一般违规">
-                每次 扣 <el-input-number v-model="levelDialog.form.deductions.violation" :min="0" size="small" /> 分
-              </el-form-item>
-              <el-form-item label="严重投诉">
-                每次 扣 <el-input-number v-model="levelDialog.form.deductions.complaint" :min="0" size="small" /> 分
-              </el-form-item>
+          <el-tab-pane label="扣分规则(自然月内)">
+            <div class="rule-group">
+              <div style="margin-bottom: 20px">
+                上门准时率低于 <el-input-number v-model="levelDialog.form.degradeOntimeRate" :min="0" :max="100" :precision="2"
+                  :step="0.01" /> %,自动扣罚积分 <el-input-number v-model="levelDialog.form.degradeOntimeRateScore"
+                  :min="0" />
+              </div>
+              <div style="margin-bottom: 20px">
+                完成订单为 <el-input-number v-model="levelDialog.form.degradeCompleteOrderQuality" :min="0" />,自动扣罚积分
+                <el-input-number v-model="levelDialog.form.degradeCompleteOrderQualityScore" :min="0" />
+              </div>
+              <div style="margin-bottom: 20px">
+                违规一次,人工扣罚积分 <el-input-number v-model="levelDialog.form.degradeViolationsScore" :min="0" />
+              </div>
+              <div style="margin-bottom: 20px">
+                投诉率达到 <el-input-number v-model="levelDialog.form.degradeComplaintCount" :min="0" :max="100"
+                  :precision="2" :step="0.01" /> %,自动扣罚积分 <el-input-number
+                  v-model="levelDialog.form.degradeComplaintCountScore" :min="0" />
+              </div>
+            </div>
+          </el-tab-pane>
+
+          <el-tab-pane label="降级规则">
+            <div class="rule-group">
+              <div style="margin-bottom: 20px">
+                积分低于 <el-input-number v-model="levelDialog.form.degradePoints" :min="0" /> 时自动降级
+              </div>
             </div>
           </el-tab-pane>
         </el-tabs>
@@ -237,15 +202,18 @@
 <script setup lang="ts">
 import { ref, reactive, computed, onMounted } from 'vue';
 import { ElMessage, ElMessageBox } from 'element-plus';
-import { listAllLevelRights, addLevelRights, updateLevelRights, delLevelRights } from '@/api/fulfiller/levelRights';
-import { FlfLevelRightsForm } from '@/api/fulfiller/levelRights/types';
+import { listAllLevelRights, addLevelRights, updateLevelRights, delLevelRights, changeLevelRightsStatus } from '@/api/fulfiller/levelRights';
+import { listAllLevelConfig, addLevelConfig, updateLevelConfig, delLevelConfig } from '@/api/fulfiller/levelConfig';
+import { FlfLevelConfigVO, FlfLevelConfigForm } from '@/api/fulfiller/levelConfig/types';
+import { FlfLevelRightsForm, FlfLevelRightsVO } from '@/api/fulfiller/levelRights/types';
 import ImageUpload from '@/components/ImageUpload/index.vue';
 
 const activeTab = ref('levels');
 
 // Data: Rights
 // Data: Rights
-const rightsList = ref([]);
+// 权益数据列表
+const rightsList = ref<FlfLevelRightsVO[]>([]);
 
 const getRightsList = async () => {
   try {
@@ -256,136 +224,160 @@ const getRightsList = async () => {
   }
 };
 
-// Data: Levels
-const levelList = ref([
-  {
-    id: 1,
-    level: 1,
-    name: '青铜骑士',
-    icon: '',
-    minPoints: 0,
-    maxPoints: 1000,
-    holidayMultiplier: 1.0,
-    rights: [],
-    rules: { periodDays: 30, minOrders: 10, onTimeRate: 90, complaintRate: 5, maxViolations: 0, accumulatedPoints: 100 },
-    deductions: { late: 5, violation: 20, complaint: 50 },
-    downgradeType: 'points'
-  },
-  {
-    id: 2,
-    level: 2,
-    name: '白银卫士',
-    icon: '',
-    minPoints: 1001,
-    maxPoints: 5000,
-    holidayMultiplier: 1.2,
-    rights: [],
-    rules: { periodDays: 30, minOrders: 30, onTimeRate: 95, complaintRate: 2, maxViolations: 0, accumulatedPoints: 1000 },
-    deductions: { late: 10, violation: 50, complaint: 100 },
-    downgradeType: 'points'
-  },
-  {
-    id: 3,
-    level: 3,
-    name: '黄金领主',
-    icon: '',
-    minPoints: 5001,
-    maxPoints: -1,
-    holidayMultiplier: 1.5,
-    rights: [],
-    rules: { periodDays: 30, minOrders: 100, onTimeRate: 98, complaintRate: 0, maxViolations: 0, accumulatedPoints: 5000 },
-    deductions: { late: 20, violation: 100, complaint: 200 },
-    downgradeType: 'points'
+const getLevelList = async () => {
+  try {
+    const res = await listAllLevelConfig();
+    levelList.value = res.data || [];
+  } catch (error) {
+    console.error('Failed to fetch level list:', error);
   }
-]);
+};
+
+const getRightName = (id: number | string) => {
+  const right = rightsList.value.find((r) => r.id === id);
+  return right ? right.name : `未知权益(${id})`;
+};
+
+// Data: Levels
+const levelList = ref<FlfLevelConfigVO[]>([]);
 
 // Dialogs
 const levelDialog = reactive({
   visible: false,
   isEdit: false,
   form: {
-    level: 1,
+    id: undefined,
+    lvNo: 1,
     name: '',
     icon: '',
-    bg: '',
-    minPoints: 0,
-    maxPoints: -1,
-    holidayMultiplier: 1.0,
-    rightsIds: [],
-    rules: { periodDays: 30, minOrders: 0, onTimeRate: 95, complaintRate: 0, maxViolations: 0, accumulatedPoints: 0 },
-    deductions: { late: 10, violation: 50, complaint: 100 },
-    downgradeType: 'points'
-  }
+    background: '',
+    holidayBonus: 1,
+    rights: [],
+    // 升级规则
+    upgradePoints: 0,
+    upgradeCompleteOrderQuality: 0,
+    upgradeOntimeRate: 1,
+    upgradeComplaintRate: 1,
+    upgradeViolationsCount: 0,
+    // 降级规则
+    degradePoints: 0,
+    degradeOntimeRate: 1,
+    degradeOntimeRateScore: 0,
+    degradeCompleteOrderQuality: 0,
+    degradeCompleteOrderQualityScore: 0,
+    degradeViolationsScore: 0,
+    degradeComplaintCount: 1,
+    degradeComplaintCountScore: 0
+  } as FlfLevelConfigForm
 });
 
 const rightDialog = reactive({
   visible: false,
   isEdit: false,
-  form: { 
-    id: undefined, 
-    name: '', 
-    icon: '', 
-    status: true, 
-    statement: '' 
+  form: {
+    id: undefined,
+    name: '',
+    icon: '',
+    status: true,
+    statement: ''
   } as FlfLevelRightsForm
 });
 
 // Methods
-const handleEditLevel = (row) => {
+const handleEditLevel = async (row) => {
   levelDialog.isEdit = !!row;
+  // 打开弹窗前同步刷新一次权益列表,确保数据最新
+  await getRightsList();
   levelDialog.visible = true;
   if (row) {
-    // Deep copy to break ref
-    const rightsIds = row.rights.map((r) => r.id);
-    levelDialog.form = JSON.parse(JSON.stringify({ ...row, rightsIds }));
+    // 处理回显数据,特别是需要从 *100 的整数转换回小数的部分
+    const formData = JSON.parse(JSON.stringify(row));
+    // 转换比例值:后端是整数 (*100),前端展示是小数
+    formData.holidayBonus = (formData.holidayBonus || 100) / 100;
+    formData.upgradeOntimeRate = (formData.upgradeOntimeRate || 100) / 100;
+    formData.upgradeComplaintRate = (formData.upgradeComplaintRate || 100) / 100;
+    formData.degradeOntimeRate = (formData.degradeOntimeRate || 100) / 100;
+    formData.degradeComplaintCount = (formData.degradeComplaintCount || 100) / 100;
+
+    // 图片 ID 转换回字符串,以便 ImageUpload 回显
+    formData.icon = formData.icon ? String(formData.icon) : '';
+    formData.background = formData.background ? String(formData.background) : '';
+
+    levelDialog.form = formData;
   } else {
     // Reset
     levelDialog.form = {
-      level: levelList.value.length + 1,
+      id: undefined,
+      lvNo: levelList.value.length + 1,
       name: '',
       icon: '',
-      bg: '',
-      minPoints: 0,
-      maxPoints: -1,
-      holidayMultiplier: 1.0,
-      rightsIds: [],
-      rules: { periodDays: 30, minOrders: 0, onTimeRate: 95, complaintRate: 0, maxViolations: 0, accumulatedPoints: 0 },
-      deductions: { late: 10, violation: 50, complaint: 100 },
-      downgradeType: 'points'
+      background: '',
+      holidayBonus: 1,
+      rights: [],
+      upgradePoints: 0,
+      upgradeCompleteOrderQuality: 0,
+      upgradeOntimeRate: 1,
+      upgradeComplaintRate: 1,
+      upgradeViolationsCount: 0,
+      degradePoints: 0,
+      degradeOntimeRate: 1,
+      degradeOntimeRateScore: 0,
+      degradeCompleteOrderQuality: 0,
+      degradeCompleteOrderQualityScore: 0,
+      degradeViolationsScore: 0,
+      degradeComplaintCount: 1,
+      degradeComplaintCountScore: 0
     };
   }
 };
 
-const handleLevelIconChange = (file, type) => {
-  // 模拟上传,实际应调用后端接口,这里直接读取本地文件展示
-  const url = URL.createObjectURL(file.raw);
-  if (type === 'icon') {
-    levelDialog.form.icon = url;
-  } else {
-    levelDialog.form.bg = url;
-  }
-};
+const saveLevel = async () => {
+  // 转换数据为后端需要的格式:小数 * 100 并取整
+  const submitData = JSON.parse(JSON.stringify(levelDialog.form));
 
-const saveLevel = () => {
-  // Map rightsIds back to objects
-  const selectedRights = rightsList.value.filter((r) => levelDialog.form.rightsIds.includes(r.id));
-  const newLevel = { ...levelDialog.form, rights: selectedRights };
+  submitData.holidayBonus = Math.round((submitData.holidayBonus || 0) * 100);
+  submitData.upgradeOntimeRate = Math.round((submitData.upgradeOntimeRate || 0) * 100);
+  submitData.upgradeComplaintRate = Math.round((submitData.upgradeComplaintRate || 0) * 100);
+  submitData.degradeOntimeRate = Math.round((submitData.degradeOntimeRate || 0) * 100);
+  submitData.degradeComplaintCount = Math.round((submitData.degradeComplaintCount || 0) * 100);
 
-  if (levelDialog.isEdit) {
-    const idx = levelList.value.findIndex((l) => l.id === newLevel.id);
-    if (idx !== -1) levelList.value[idx] = newLevel;
-  } else {
-    newLevel.id = Date.now();
-    levelList.value.push(newLevel);
+  // 图片处理:ImageUpload 返回的是逗号分隔的 ID 字符串,后端需要数字
+  if (submitData.icon && typeof submitData.icon === 'string') {
+    submitData.icon = parseInt(submitData.icon.split(',')[0]);
+  }
+  if (submitData.background && typeof submitData.background === 'string') {
+    submitData.background = parseInt(submitData.background.split(',')[0]);
   }
 
-  levelList.value.sort((a, b) => a.level - b.level);
-  levelDialog.visible = false;
-  ElMessage.success('等级配置保存成功');
+  try {
+    if (levelDialog.isEdit) {
+      await updateLevelConfig(submitData);
+      ElMessage.success('等级配置修改成功');
+    } else {
+      await addLevelConfig(submitData);
+      ElMessage.success('等级配置新增成功');
+    }
+    levelDialog.visible = false;
+    getLevelList();
+  } catch (error) {
+    console.error('Save level failed:', error);
+  }
 };
 
-const handleDeleteLevel = () => {
-  ElMessage.warning('演示模式:暂不执行删除');
+const handleDeleteLevel = (row) => {
+  ElMessageBox.confirm(`确定要删除等级 "Lv.${row.lvNo}" 吗?`, '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(async () => {
+    try {
+      await delLevelConfig(row.id);
+      ElMessage.success('删除成功');
+      getLevelList();
+    } catch (error) {
+      console.error('Delete level failed:', error);
+    }
+  }).catch(() => { });
 };
 
 const handleEditRight = (row) => {
@@ -430,13 +422,12 @@ const handleDeleteRight = (row) => {
     } catch (error) {
       console.error('Delete right failed:', error);
     }
-  }).catch(() => {});
+  }).catch(() => { });
 };
 
 const handleRightStatusChange = async (row) => {
   try {
-    const data = { ...row };
-    await updateLevelRights(data);
+    await changeLevelRightsStatus({ id: row.id, status: row.status });
     ElMessage.success(`权益 "${row.name}" 已${row.status ? '启用' : '停用'}`);
   } catch (error) {
     row.status = !row.status; // 切换失败恢复状态
@@ -446,6 +437,7 @@ const handleRightStatusChange = async (row) => {
 
 onMounted(() => {
   getRightsList();
+  getLevelList();
 });
 </script>
 
@@ -453,11 +445,13 @@ onMounted(() => {
 .page-container {
   padding: 20px;
 }
+
 .card-header {
   display: flex;
   justify-content: space-between;
   align-items: center;
 }
+
 .title {
   font-weight: bold;
   font-size: 16px;
@@ -469,6 +463,7 @@ onMounted(() => {
   gap: 8px;
   font-weight: bold;
 }
+
 .right-tag {
   margin-right: 5px;
   margin-bottom: 5px;
@@ -487,24 +482,29 @@ onMounted(() => {
   justify-content: center;
   align-items: center;
 }
+
 .bg-uploader {
   width: 160px;
   height: 80px;
 }
+
 .avatar-uploader:hover,
 .bg-uploader:hover {
   border-color: #409eff;
 }
+
 .avatar-uploader-icon {
   font-size: 28px;
   color: #8c939d;
 }
+
 .avatar {
   width: 80px;
   height: 80px;
   display: block;
   object-fit: cover;
 }
+
 .bg-img {
   width: 160px;
   height: 80px;
@@ -525,6 +525,7 @@ onMounted(() => {
   border-radius: 4px;
   border: 1px solid #ebf5f2;
 }
+
 .rule-title {
   font-weight: bold;
   margin-bottom: 12px;

+ 544 - 205
src/views/fulfiller/pool/index.vue

@@ -8,27 +8,17 @@
             <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" v-hasPermi="['fulfiller:pool:add']">新增履约者</el-button>
-            <el-input
-              v-model="searchKey"
-              placeholder="搜索姓名/手机号/身份证"
-              class="search-input"
-              prefix-icon="Search"
-              clearable
-              @keyup.enter="handleSearch"
-              @clear="handleSearch"
-            />
-            <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-button type="primary" icon="Plus" style="margin-right: 15px" @click="handleCreate"
+              v-hasPermi="['fulfiller:pool:add']">新增履约者</el-button>
+            <el-input v-model="searchKey" placeholder="搜索姓名/手机号/身份证" class="search-input" prefix-icon="Search" clearable
+              @keyup.enter="handleSearch" @clear="handleSearch" />
+            <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>
@@ -42,7 +32,8 @@
         </el-tabs>
       </template>
 
-      <el-table v-loading="loading" :data="tableData" 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">
@@ -51,13 +42,18 @@
                 <div class="name-row">
                   <span class="name">{{ scope.row.name }}</span>
                   <span class="gender-tag">
-                        <el-icon v-if="scope.row.gender === '0'" color="#409eff"><Male /></el-icon>
-                        <el-icon v-else color="#f56c6c"><Female /></el-icon>
-                    </span>
+                    <el-icon v-if="scope.row.gender === '0'" color="#409eff">
+                      <Male />
+                    </el-icon>
+                    <el-icon v-else color="#f56c6c">
+                      <Female />
+                    </el-icon>
+                  </span>
                 </div>
                 <div class="tags-row" style="margin: 3px 0">
                   <!-- work type -->
-                  <el-tag size="small" :type="scope.row.workType === 'full_time' ? 'warning' : 'info'" effect="light" style="margin-right: 5px">
+                  <el-tag size="small" :type="scope.row.workType === 'full_time' ? 'warning' : 'info'" effect="light"
+                    style="margin-right: 5px">
                     {{ scope.row.workType === 'full_time' ? '全职专送' : '兼职众包' }}
                   </el-tag>
                   <!-- 等级展示 -->
@@ -75,10 +71,14 @@
           <template #default="scope">
             <div class="auth-row">
               <div class="auth-card" :class="{ active: scope.row.authId }">
-                <el-icon><Postcard /></el-icon> 身份证
+                <el-icon>
+                  <Postcard />
+                </el-icon> 身份证
               </div>
               <div class="auth-card" :class="{ active: scope.row.authQual }">
-                <el-icon><Medal /></el-icon> 资质证
+                <el-icon>
+                  <Medal />
+                </el-icon> 资质证
               </div>
             </div>
             <div style="margin-top:5px;">
@@ -100,14 +100,8 @@
 
         <el-table-column label="技能标签" min-width="180">
           <template #default="scope">
-            <el-tag
-              v-for="tag in scope.row.tags"
-              :key="tag.id"
-              :type="tag.colorType"
-              size="small"
-              class="skill-tag"
-              effect="plain"
-            >
+            <el-tag v-for="tag in scope.row.tags" :key="tag.id" :type="tag.colorType" size="small" class="skill-tag"
+              effect="plain">
               {{ tag.name }}
             </el-tag>
           </template>
@@ -139,17 +133,24 @@
         <el-table-column label="操作" width="240" fixed="right">
           <template #default="scope">
             <div class="op-cell">
-              <el-button link type="primary" size="small" @click="handleDetail(scope.row)" v-hasPermi="['fulfiller:pool:query']">详情</el-button>
-              <el-button link type="primary" size="small" @click="handleEdit(scope.row)" v-hasPermi="['fulfiller:pool:edit']">编辑</el-button>
-              <el-button link type="warning" size="small" @click="handleReward(scope.row)" v-hasPermi="['fulfiller:pool:edit']">奖惩</el-button>
+              <el-button link type="primary" size="small" @click="handleDetail(scope.row)"
+                v-hasPermi="['fulfiller:pool:query']">详情</el-button>
+              <el-button link type="primary" size="small" @click="handleEdit(scope.row)"
+                v-hasPermi="['fulfiller:pool:edit']">编辑</el-button>
+              <el-button link type="warning" size="small" @click="handleReward(scope.row)"
+                v-hasPermi="['fulfiller:pool:edit']">奖惩</el-button>
               <el-dropdown trigger="click" @command="(cmd) => handleCommand(cmd, scope.row)">
                 <el-button link type="primary">更多<el-icon class="el-icon--right"><arrow-down /></el-icon></el-button>
                 <template #dropdown>
                   <el-dropdown-menu>
-                    <el-dropdown-item command="adjustPoints" v-hasPermi="['fulfiller:pool:edit']">修改积分</el-dropdown-item>
-                    <el-dropdown-item command="adjustBalance" v-hasPermi="['fulfiller:pool:edit']">余额增减</el-dropdown-item>
-                    <el-dropdown-item v-if="scope.row.status !== 'disabled'" command="disable" divided style="color: #f56c6c" v-hasPermi="['fulfiller:pool:edit']">禁用账号</el-dropdown-item>
-                    <el-dropdown-item v-else command="enable" divided style="color: #67c23a" v-hasPermi="['fulfiller:pool:edit']">启用账号</el-dropdown-item>
+                    <el-dropdown-item command="adjustPoints"
+                      v-hasPermi="['fulfiller:pool:edit']">修改积分</el-dropdown-item>
+                    <el-dropdown-item command="adjustBalance"
+                      v-hasPermi="['fulfiller:pool:edit']">余额增减</el-dropdown-item>
+                    <el-dropdown-item v-if="scope.row.status !== 'disabled'" command="disable" divided
+                      style="color: #f56c6c" v-hasPermi="['fulfiller:pool:edit']">禁用账号</el-dropdown-item>
+                    <el-dropdown-item v-else command="enable" divided style="color: #67c23a"
+                      v-hasPermi="['fulfiller:pool:edit']">启用账号</el-dropdown-item>
                     <el-dropdown-item command="violation" v-hasPermi="['fulfiller:pool:edit']">违规记录</el-dropdown-item>
                     <el-dropdown-item command="resetPwd" v-hasPermi="['fulfiller:pool:edit']">重置密码</el-dropdown-item>
                   </el-dropdown-menu>
@@ -161,46 +162,42 @@
       </el-table>
 
       <div class="pagination-container">
-        <el-pagination
-          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"
-          @size-change="handleSizeChange"
-          @current-change="handleCurrentChange"
-        />
+        <el-pagination 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"
+          @size-change="handleSizeChange" @current-change="handleCurrentChange" />
       </div>
     </el-card>
 
     <!-- 详情侧边栏 Drawer -->
-    <el-drawer
-      v-model="detailVisible"
-      title="履约者档案详情"
-      size="750px"
-      direction="rtl"
-      custom-class="detail-drawer"
-    >
+    <el-drawer v-model="detailVisible" title="履约者档案详情" size="750px" direction="rtl" custom-class="detail-drawer">
       <div class="drawer-content" v-if="currentItem">
         <!-- 头部概览 -->
         <div class="user-header-card">
-          <el-avatar :size="70" :src="currentItem.avatarUrl" 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 === '0' ? '' : 'danger'" effect="plain" round style="margin-left: 8px;">
+              <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="info-item"><el-icon>
+                  <Iphone />
+                </el-icon> {{ currentItem.phone }}</span>
               <span class="divider">|</span>
-              <span class="info-item"><el-icon><Location /></el-icon> {{ currentItem.cityName }}</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.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>
+              <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>
         </div>
@@ -235,11 +232,13 @@
                 <div class="section-title">基础信息</div>
                 <el-descriptions :column="2" border>
                   <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.realName || currentItem.name
+                  }}</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-item label="工作性质">{{ currentItem.workType === 'full_time' ? '全职' : '兼职'
+                  }}</el-descriptions-item>
                 </el-descriptions>
               </div>
 
@@ -248,13 +247,21 @@
                 <div class="cert-row">
                   <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>
+                      <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.idCardBackUrl)">
                     <el-image :src="currentItem.idCardBackUrl || ''" fit="cover" class="cert-img">
-                      <template #error><div class="img-slot"><el-icon><Picture /></el-icon></div></template>
+                      <template #error>
+                        <div class="img-slot"><el-icon>
+                            <Picture />
+                          </el-icon></div>
+                      </template>
                     </el-image>
                     <div class="cert-name">身份证国徽面</div>
                   </div>
@@ -264,9 +271,14 @@
               <div class="section-block">
                 <div class="section-title">资质认证</div>
                 <div class="cert-row" v-if="qualImageUrlList.length">
-                  <div class="cert-item" v-for="(img, index) in qualImageUrlList" :key="index" @click="handleViewImage(img)">
+                  <div class="cert-item" v-for="(img, index) in qualImageUrlList" :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>
+                      <template #error>
+                        <div class="img-slot"><el-icon>
+                            <Picture />
+                          </el-icon></div>
+                      </template>
                     </el-image>
                   </div>
                 </div>
@@ -275,7 +287,8 @@
               <div class="section-block">
                 <div class="section-title">技能标签</div>
                 <div class="tag-list">
-                  <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>
+                  <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>
@@ -283,7 +296,8 @@
 
           <el-tab-pane label="服务订单" name="orders">
             <div class="tab-content-wrapper">
-              <el-table v-loading="logLoading" :data="serviceOrderData" stripe style="width: 100%" :header-cell-style="{background:'#f5f7fa', color:'#606266'}">
+              <el-table v-loading="logLoading" :data="serviceOrderData" stripe style="width: 100%"
+                :header-cell-style="{ background: '#f5f7fa', color: '#606266' }">
                 <el-table-column prop="code" label="订单号" width="160" show-overflow-tooltip />
                 <el-table-column label="服务项目" show-overflow-tooltip>
                   <template #default="{ row }">
@@ -292,7 +306,8 @@
                 </el-table-column>
                 <el-table-column prop="price" label="收入" width="100">
                   <template #default="{ row }">
-                    <span style="color: #67c23a; font-weight: bold; font-size: 15px;">+{{ (row.price / 100).toFixed(2) }}</span>
+                    <span style="color: #67c23a; font-weight: bold; font-size: 15px;">+{{ (row.price / 100).toFixed(2)
+                    }}</span>
                   </template>
                 </el-table-column>
                 <el-table-column prop="serviceTime" label="时间" width="160" show-overflow-tooltip />
@@ -309,18 +324,21 @@
 
           <el-tab-pane label="积分记录" name="pointLogs">
             <div class="tab-content-wrapper">
-              <el-table v-loading="logLoading" :data="pointsLogData" stripe style="width: 100%" :header-cell-style="{background:'#f5f7fa', color:'#606266'}">
+              <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="getPointsBizTypeTag(row.bizType)" size="small" effect="plain">{{ getPointsBizTypeName(row.bizType) }}</el-tag>
+                    <el-tag :type="getPointsBizTypeTag(row.bizType)" size="small" effect="plain">{{
+                      getPointsBizTypeName(row.bizType) }}</el-tag>
                   </template>
                 </el-table-column>
                 <el-table-column prop="amount" label="变动数值" width="120">
                   <template #default="{ row }">
-                                    <span :style="{ color: ['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? '#67c23a' : '#f56c6c', fontWeight: 'bold', fontSize: '15px' }">
-                                        {{ ['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? '+' : '-' }}{{ row.amount }}
-                                    </span>
+                    <span
+                      :style="{ color: ['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? '#67c23a' : '#f56c6c', fontWeight: 'bold', fontSize: '15px' }">
+                      {{ ['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? '+' : '-' }}{{ row.amount }}
+                    </span>
                   </template>
                 </el-table-column>
                 <el-table-column prop="reason" label="变动原因" show-overflow-tooltip />
@@ -331,18 +349,22 @@
 
           <el-tab-pane label="余额变动" name="balanceLogs">
             <div class="tab-content-wrapper">
-              <el-table v-loading="logLoading" :data="balanceLogData" stripe style="width: 100%" :header-cell-style="{background:'#f5f7fa', color:'#606266'}">
+              <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="getBalanceBizTypeTag(row.subType)" size="small" effect="plain">{{ getBalanceBizTypeName(row.bizType) }}</el-tag>
+                    <el-tag :type="getBalanceBizTypeTag(row.subType)" size="small" effect="plain">{{
+                      getBalanceBizTypeName(row.bizType) }}</el-tag>
                   </template>
                 </el-table-column>
                 <el-table-column prop="amount" label="变动金额" width="120">
                   <template #default="{ row }">
-                                    <span :style="{ color: ['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? '#67c23a' : '#f56c6c', fontWeight: 'bold', fontSize: '15px' }">
-                                        {{ ['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? '+' : '-' }}{{ (row.amount / 100).toFixed(2) }}
-                                    </span>
+                    <span
+                      :style="{ color: ['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? '#67c23a' : '#f56c6c', fontWeight: 'bold', fontSize: '15px' }">
+                      {{ ['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? '+' : '-' }}{{ (row.amount /
+                        100).toFixed(2) }}
+                    </span>
                   </template>
                 </el-table-column>
                 <el-table-column prop="balanceAfter" label="变动后余额" width="120">
@@ -358,11 +380,13 @@
 
           <el-tab-pane label="奖惩记录" name="rewards">
             <div class="tab-content-wrapper">
-              <el-table v-loading="logLoading" :data="rewardLogData" stripe style="width: 100%" :header-cell-style="{background:'#f5f7fa', color:'#606266'}">
+              <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="bizType" label="奖惩类型" width="100">
                   <template #default="{ row }">
-                    <el-tag :type="fulfillerEnums.RewardBizType[row.bizType]?.tagType || 'info'" size="small" effect="plain">
+                    <el-tag :type="fulfillerEnums.RewardBizType[row.bizType]?.tagType || 'info'" size="small"
+                      effect="plain">
                       {{ getRewardBizTypeName(row.bizType) }}
                     </el-tag>
                   </template>
@@ -374,9 +398,12 @@
                 </el-table-column>
                 <el-table-column prop="amount" label="涉及数值" width="120">
                   <template #default="{ row }">
-                                     <span :style="{ color: ['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? '#67c23a' : '#f56c6c', fontWeight: 'bold' }">
-                                         {{ ['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? '+' : '-' }}{{ row.target === 'balance' ? (row.amount / 100).toFixed(2) : row.amount }} {{ row.target === 'points' ? '分' : '元' }}
-                                     </span>
+                    <span
+                      :style="{ color: ['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? '#67c23a' : '#f56c6c', fontWeight: 'bold' }">
+                      {{ ['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? '+' : '-' }}{{ row.target ===
+                        'balance' ? (row.amount / 100).toFixed(2) : row.amount }} {{ row.target === 'points' ? '分' : '元'
+                      }}
+                    </span>
                   </template>
                 </el-table-column>
                 <el-table-column prop="reason" label="奖惩原因" show-overflow-tooltip />
@@ -387,24 +414,39 @@
 
           <el-tab-pane label="违规记录" name="violation">
             <div class="tab-content-wrapper">
-              <el-table v-loading="logLoading" :data="violationLogData" stripe style="width: 100%" :header-cell-style="{background:'#f5f7fa', color:'#606266'}">
+              <el-table v-loading="logLoading" :data="violationLogData" stripe style="width: 100%"
+                :header-cell-style="{ background: '#f5f7fa', color: '#606266' }">
                 <el-table-column prop="violationTime" label="违规时间" width="180">
-                    <template #default="{ row }">
-                        <div style="display: flex; align-items: center; gap: 5px;">
-                            <el-icon color="#909399"><Timer /></el-icon>
-                            <span>{{ row.violationTime }}</span>
-                        </div>
-                    </template>
-                </el-table-column>
-                <el-table-column prop="count" label="违规次数" width="120" align="center">
-                    <template #default="{ row }">
-                        <el-tag type="danger" effect="plain" round v-if="row.count > 0">{{ row.count }} 次</el-tag>
-                    </template>
                 </el-table-column>
+                <el-table-column prop="count" label="违规次数" width="100" />
                 <el-table-column prop="reason" label="违规原因" show-overflow-tooltip />
+                <el-table-column prop="operatorName" label="操作人" width="100" />
               </el-table>
             </div>
           </el-tab-pane>
+
+          <el-tab-pane label="投诉记录" name="complaints">
+            <div class="tab-content-wrapper">
+              <el-table v-loading="logLoading" :data="complaintLogData" stripe style="width: 100%"
+                :header-cell-style="{ background: '#f5f7fa', color: '#606266' }">
+                <el-table-column prop="createTime" label="投诉时间" width="180" />
+                <el-table-column prop="orderCode" label="订单号" width="160" show-overflow-tooltip />
+                <el-table-column prop="reason" label="投诉原因" show-overflow-tooltip />
+                <el-table-column prop="createBy" label="操作人" width="100" />
+              </el-table>
+              <div style="margin-top: 20px; display: flex; justify-content: flex-end;">
+                <el-pagination
+                  v-model:current-page="complaintPagination.pageNum"
+                  v-model:page-size="complaintPagination.pageSize"
+                  :page-sizes="[10, 20, 50]"
+                  layout="total, sizes, prev, pager, next"
+                  :total="complaintPagination.total"
+                  @size-change="loadComplaintLogs"
+                  @current-change="loadComplaintLogs"
+                />
+              </div>
+            </div>
+          </el-tab-pane>
         </el-tabs>
       </div>
     </el-drawer>
@@ -446,13 +488,16 @@
         <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="服务城市">
-              <el-cascader v-model="editDialog.cascaderValue" :options="cityCascaderOptions" :props="{ checkStrictly: true }" placeholder="请选择城市/区域" clearable style="width: 100%" @change="handleEditCascaderChange" />
+              <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.stationId" placeholder="请选择站点" style="width: 100%">
-                <el-option v-for="station in editDialog.stationOptions" :key="station.id" :label="station.name" :value="station.id" />
+                <el-option v-for="station in editDialog.stationOptions" :key="station.id" :label="station.name"
+                  :value="station.id" />
               </el-select>
             </el-form-item>
           </el-col>
@@ -461,7 +506,7 @@
         <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="等级">
-              <el-input v-model="editDialog.form.levelId" placeholder="等级ID" />
+              <el-input v-model="editDialog.form.level" placeholder="等级" />
             </el-form-item>
           </el-col>
           <el-col :span="12">
@@ -482,10 +527,10 @@
 
       </el-form>
       <template #footer>
-            <span class="dialog-footer">
-                <el-button @click="editDialog.visible = false">取消</el-button>
-                <el-button type="primary" @click="saveEdit">保存变更</el-button>
-            </span>
+        <span class="dialog-footer">
+          <el-button @click="editDialog.visible = false">取消</el-button>
+          <el-button type="primary" @click="saveEdit">保存变更</el-button>
+        </span>
       </template>
     </el-dialog>
 
@@ -515,10 +560,10 @@
         </el-form-item>
       </el-form>
       <template #footer>
-            <span class="dialog-footer">
-            <el-button @click="rewardDialog.visible = false">取消</el-button>
-            <el-button type="primary" @click="submitReward">确认执行</el-button>
-            </span>
+        <span class="dialog-footer">
+          <el-button @click="rewardDialog.visible = false">取消</el-button>
+          <el-button type="primary" @click="submitReward">确认执行</el-button>
+        </span>
       </template>
     </el-dialog>
 
@@ -541,11 +586,14 @@
           </el-radio-group>
         </el-form-item>
         <el-form-item label="服务城市">
-          <el-cascader v-model="createDialog.cascaderValue" :options="cityCascaderOptions" :props="{ checkStrictly: true }" placeholder="请选择城市/区域" clearable style="width: 100%" @change="handleCreateCascaderChange" />
+          <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.stationId" placeholder="请选择站点" style="width: 100%">
-            <el-option v-for="station in createDialog.stationOptions" :key="station.id" :label="station.name" :value="station.id" />
+            <el-option v-for="station in createDialog.stationOptions" :key="station.id" :label="station.name"
+              :value="station.id" />
           </el-select>
         </el-form-item>
       </el-form>
@@ -561,15 +609,12 @@
         <el-form-item label="违规次数" required>
           <el-input-number v-model="violationDialog.form.count" :min="1" style="width: 100%" />
         </el-form-item>
+        <el-form-item label="扣罚积分" required>
+          <el-input-number v-model="violationDialog.form.points" :min="0" style="width: 100%" placeholder="违规扣罚积分" />
+        </el-form-item>
         <el-form-item label="违规时间" required>
-          <el-date-picker
-            v-model="violationDialog.form.violationTime"
-            type="datetime"
-            placeholder="请选择违规时间"
-            format="YYYY-MM-DD HH:mm:ss"
-            value-format="YYYY-MM-DD HH:mm:ss"
-            style="width: 100%"
-          />
+          <el-date-picker v-model="violationDialog.form.violationTime" type="datetime" placeholder="请选择违规时间"
+            format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" style="width: 100%" />
         </el-form-item>
         <el-form-item label="违规原因" required>
           <el-input v-model="violationDialog.form.reason" type="textarea" :rows="3" placeholder="请输入违规原因说明" />
@@ -610,10 +655,12 @@
     <el-dialog v-model="balanceDialog.visible" title="余额增减" width="450px">
       <el-form :model="balanceDialog.form" label-width="80px">
         <el-form-item label="当前余额">
-          <span style="color: #f56c6c; font-weight: bold">¥{{ (balanceDialog.currentRow?.balance / 100).toFixed(2) }}</span>
+          <span style="color: #f56c6c; font-weight: bold">¥{{ (balanceDialog.currentRow?.balance / 100).toFixed(2)
+          }}</span>
         </el-form-item>
         <el-form-item label="扣减类型">
-          <el-radio-group v-model="balanceDialog.form.type" @change="balanceDialog.form.subType = balanceDialog.form.type === fulfillerEnums.ActionType.ADD ? 'admin_reward' : 'admin_punish'">
+          <el-radio-group v-model="balanceDialog.form.type"
+            @change="balanceDialog.form.subType = balanceDialog.form.type === fulfillerEnums.ActionType.ADD ? 'admin_reward' : 'admin_punish'">
             <el-radio :label="fulfillerEnums.ActionType.ADD">增加</el-radio>
             <el-radio :label="fulfillerEnums.ActionType.REDUCE">减少</el-radio>
           </el-radio-group>
@@ -656,6 +703,7 @@ import {
 } from '@/api/fulfiller/pool'
 import { addViolation, listViolationByFulfiller } from '@/api/fulfiller/violation'
 import type { FlfViolationVO } from '@/api/fulfiller/violation/types'
+import { pageComplaintByFulfiller } from '@/api/fulfiller/complaint'
 import { listSubOrderOnFulfiller } from '@/api/order/subOrder/index'
 import { listOnStore as listServiceOnStore } from '@/api/service/list/index'
 import type {
@@ -669,6 +717,8 @@ import { listOnStore } from '@/api/system/areaStation'
 import type { SysAreaStationOnStoreVo } from '@/api/system/areaStation/types'
 import fulfillerEnums from '@/enums/fulfiller.json'
 import ImageUpload from '@/components/ImageUpload/index.vue'
+import { listAllLevelRights, addLevelRights, updateLevelRights, delLevelRights, changeLevelRightsStatus } from '@/api/fulfiller/levelRights';
+import { listAllLevelConfig, addLevelConfig, updateLevelConfig, delLevelConfig } from '@/api/fulfiller/levelConfig';
 
 const loading = ref(false)
 const searchKey = ref('')
@@ -702,6 +752,8 @@ const pointsLogData = ref<FlfPointsLogVO[]>([])
 const balanceLogData = ref<FlfBalanceLogVO[]>([])
 const rewardLogData = ref<FlfRewardLogVO[]>([])
 const violationLogData = ref<FlfViolationVO[]>([])
+const complaintLogData = ref<any[]>([])
+const complaintPagination = reactive({ pageNum: 1, pageSize: 20, total: 0 })
 const serviceOrderData = ref<any[]>([])
 const serviceOptions = ref<any[]>([])
 const logLoading = ref(false)
@@ -806,25 +858,25 @@ const getRewardBizTypeName = (type: string) => {
 
 /** 加载服务项目列表用于名称映射 */
 const loadServiceOptions = async () => {
-    try {
-        const res = await listServiceOnStore()
-        serviceOptions.value = res.data || []
-    } catch { /* ignore */ }
+  try {
+    const res = await listServiceOnStore()
+    serviceOptions.value = res.data || []
+  } catch { /* ignore */ }
 }
 
 const getServiceName = (serviceId: number | string) => {
-    const item = serviceOptions.value.find(i => i.id === serviceId)
-    return item ? item.name : '未知服务'
+  const item = serviceOptions.value.find(i => i.id === serviceId)
+  return item ? item.name : '未知服务'
 }
 
 const getSubOrderStatusName = (status: number) => {
-    const map: Record<number, string> = { 0: '待派单', 1: '待接单', 2: '待服务', 3: '服务中', 4: '已完成', 5: '已取消' }
-    return map[status] || '未知'
+  const map: Record<number, string> = { 0: '待派单', 1: '待接单', 2: '待服务', 3: '服务中', 4: '已完成', 5: '已取消' }
+  return map[status] || '未知'
 }
 
 const getSubOrderStatusType = (status: number) => {
-    const map: Record<number, string> = { 0: 'info', 1: 'warning', 2: 'primary', 3: 'success', 4: 'success', 5: 'danger' }
-    return map[status] || 'info'
+  const map: Record<number, string> = { 0: 'info', 1: 'warning', 2: 'primary', 3: 'success', 4: 'success', 5: 'danger' }
+  return map[status] || 'info'
 }
 
 const rewardDialog = reactive({
@@ -868,7 +920,8 @@ const violationDialog = reactive({
   form: {
     count: 1,
     violationTime: '',
-    reason: ''
+    reason: '',
+    points: 0
   }
 })
 
@@ -914,6 +967,24 @@ const loadLogs = async (fulfillerId: string | number) => {
   } catch { /* ignore */ } finally {
     logLoading.value = false
   }
+  loadComplaintLogs(fulfillerId)
+}
+
+const loadComplaintLogs = async (fulfillerId?: string | number) => {
+  const id = fulfillerId || currentItem.value?.id
+  if (!id) return
+  logLoading.value = true
+  try {
+    const res = await pageComplaintByFulfiller({
+      fulfiller: id,
+      pageNum: complaintPagination.pageNum,
+      pageSize: complaintPagination.pageSize
+    })
+    complaintLogData.value = res.rows || []
+    complaintPagination.total = res.total || 0
+  } catch { /* ignore */ } finally {
+    logLoading.value = false
+  }
 }
 
 const handleDetail = async (row: FlfFulfillerVO) => {
@@ -938,7 +1009,7 @@ const handleEdit = (row: FlfFulfillerVO) => {
     cityCode: row.cityCode,
     cityName: row.cityName,
     stationId: row.stationId,
-    levelId: row.levelId,
+    level: row.level,
     status: row.status,
     authId: row.authId,
     authQual: row.authQual,
@@ -1053,12 +1124,27 @@ const handleCommand = async (cmd: string, row: FlfFulfillerVO) => {
     } catch { /* handled by interceptor */ }
   } else if (cmd === 'violation') {
     violationDialog.fulfillerId = row.id
-    violationDialog.form = {
-      count: 1,
-      violationTime: new Date().toISOString().replace('T', ' ').split('.')[0],
-      reason: ''
+    // 获取当前履约者等级对应的违规扣罚积分
+    const getDefaultPoints = async () => {
+      try {
+        const levelConfigRes = await listAllLevelConfig()
+        const currentLevelConfig = levelConfigRes.data.find((config: any) => config.lvNo === row.level)
+        const defaultPoints = currentLevelConfig?.degradeViolationsScore || 0
+        return defaultPoints
+      } catch {
+        return 0
+      }
     }
-    violationDialog.visible = true
+
+    getDefaultPoints().then(defaultPoints => {
+      violationDialog.form = {
+        count: 1,
+        violationTime: new Date().toISOString().replace('T', ' ').split('.')[0],
+        reason: '',
+        points: defaultPoints
+      }
+      violationDialog.visible = true
+    })
   }
 }
 
@@ -1167,47 +1253,168 @@ onMounted(() => {
 </script>
 
 <style scoped>
-.page-container { padding: 20px; }
-.table-card { border-radius: 8px; border: none; }
-.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
+.page-container {
+  padding: 20px;
+}
+
+.table-card {
+  border-radius: 8px;
+  border: none;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+}
+
+.status-tabs {
+  margin-top: -15px;
+}
+
+:deep(.el-tabs__header) {
+  margin-bottom: 0;
+}
 
-.status-tabs { margin-top: -15px; }
-:deep(.el-tabs__header) { margin-bottom: 0; }
-:deep(.el-tabs__nav-wrap::after) { height: 1px; background-color: #f0f2f5; }
+:deep(.el-tabs__nav-wrap::after) {
+  height: 1px;
+  background-color: #f0f2f5;
+}
+
+.title {
+  font-size: 18px;
+  font-weight: bold;
+  color: #303133;
+}
 
-.title { font-size: 18px; font-weight: bold; color: #303133; }
-.right-panel { display: flex; align-items: center; }
-.search-input { width: 240px; }
+.right-panel {
+  display: flex;
+  align-items: center;
+}
+
+.search-input {
+  width: 240px;
+}
 
 /* Table Content Styles */
-.user-info { display: flex; align-items: center; }
-.text-col { margin-left: 10px; display: flex; flex-direction: column; justify-content: center; }
-.name-row { font-weight: bold; font-size: 14px; color: #333; display: flex; align-items: center; }
-.gender-tag { margin-left: 5px; display: flex; align-items: center; }
-.sub-text { font-size: 12px; color: #999; margin-top: 2px; }
+.user-info {
+  display: flex;
+  align-items: center;
+}
+
+.text-col {
+  margin-left: 10px;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+}
+
+.name-row {
+  font-weight: bold;
+  font-size: 14px;
+  color: #333;
+  display: flex;
+  align-items: center;
+}
+
+.gender-tag {
+  margin-left: 5px;
+  display: flex;
+  align-items: center;
+}
+
+.sub-text {
+  font-size: 12px;
+  color: #999;
+  margin-top: 2px;
+}
+
+.auth-row {
+  display: flex;
+  gap: 8px;
+  flex-wrap: wrap;
+}
 
-.auth-row { display: flex; gap: 8px; flex-wrap: wrap; }
 .auth-card {
-  font-size: 12px; padding: 2px 6px; border-radius: 4px; background: #f4f4f5; color: #909399;
-  display: flex; align-items: center; gap: 4px;
+  font-size: 12px;
+  padding: 2px 6px;
+  border-radius: 4px;
+  background: #f4f4f5;
+  color: #909399;
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.auth-card.active {
+  background: #ecf5ff;
+  color: #409eff;
+}
+
+.auth-card.need-review {
+  background: #fef0f0;
+  color: #f56c6c;
+  border: 1px dashed #f56c6c;
+}
+
+.finance-item {
+  font-size: 13px;
+  color: #606266;
+  line-height: 1.6;
+}
+
+.num {
+  font-weight: bold;
+  font-family: DIN, sans-serif;
+  margin-left: 5px;
+  color: #303133;
+}
+
+.num.error {
+  color: #f56c6c;
+}
+
+.status-cell {
+  display: flex;
+  align-items: center;
+}
+
+.status-dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  margin-right: 6px;
 }
-.auth-card.active { background: #ecf5ff; color: #409eff; }
-.auth-card.need-review { background: #fef0f0; color: #f56c6c; border: 1px dashed #f56c6c; }
 
-.finance-item { font-size: 13px; color: #606266; line-height: 1.6; }
-.num { font-weight: bold; font-family: DIN, sans-serif; margin-left: 5px; color: #303133; }
-.num.error { color: #f56c6c; }
+.status-dot.resting {
+  background: #e6a23c;
+}
 
-.status-cell { display: flex; align-items: center; }
-.status-dot { width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
-.status-dot.resting { background: #e6a23c; }
-.status-dot.busy { background: #409eff; }
-.status-dot.disabled { background: #f56c6c; }
-.status-dot.frozen { background: #909399; }
+.status-dot.busy {
+  background: #409eff;
+}
 
-.op-cell { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; }
+.status-dot.disabled {
+  background: #f56c6c;
+}
 
-.pagination-container { display: flex; justify-content: flex-end; margin-top: 20px; }
+.status-dot.frozen {
+  background: #909399;
+}
+
+.op-cell {
+  display: flex;
+  align-items: center;
+  gap: 5px;
+  flex-wrap: wrap;
+}
+
+.pagination-container {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 20px;
+}
 
 /* Drawer Styles */
 .user-header-card {
@@ -1218,24 +1425,77 @@ onMounted(() => {
   border-radius: 8px;
   margin-bottom: 25px;
 }
-.header-info { margin-left: 20px; flex: 1; }
-.top-row { display: flex; align-items: center; margin-bottom: 8px; }
-.user-name { font-size: 20px; font-weight: bold; color: #303133; }
+
+.header-info {
+  margin-left: 20px;
+  flex: 1;
+}
+
+.top-row {
+  display: flex;
+  align-items: center;
+  margin-bottom: 8px;
+}
+
+.user-name {
+  font-size: 20px;
+  font-weight: bold;
+  color: #303133;
+}
+
 .status-badge {
   margin-left: auto;
-  font-size: 12px; padding: 4px 10px; border-radius: 12px;
-  background: #e1f3d8; color: #67c23a;
+  font-size: 12px;
+  padding: 4px 10px;
+  border-radius: 12px;
+  background: #e1f3d8;
+  color: #67c23a;
+}
+
+.status-badge.resting {
+  background: #faecd8;
+  color: #e6a23c;
 }
-.status-badge.resting { background: #faecd8; color: #e6a23c; }
-.status-badge.disabled { background: #fde2e2; color: #f56c6c; }
-.status-badge.busy { background: #d9ecff; color: #409eff; }
-.status-badge.frozen { background: #f0f9eb; color: #909399; }
 
-.sub-row { display: flex; align-items: center; font-size: 13px; color: #606266; margin-bottom: 8px; }
-.info-item { display: flex; align-items: center; gap: 4px; }
-.divider { margin: 0 10px; color: #dcdfe6; }
+.status-badge.disabled {
+  background: #fde2e2;
+  color: #f56c6c;
+}
 
-.tags-row { display: flex; align-items: center; gap: 5px; }
+.status-badge.busy {
+  background: #d9ecff;
+  color: #409eff;
+}
+
+.status-badge.frozen {
+  background: #f0f9eb;
+  color: #909399;
+}
+
+.sub-row {
+  display: flex;
+  align-items: center;
+  font-size: 13px;
+  color: #606266;
+  margin-bottom: 8px;
+}
+
+.info-item {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.divider {
+  margin: 0 10px;
+  color: #dcdfe6;
+}
+
+.tags-row {
+  display: flex;
+  align-items: center;
+  gap: 5px;
+}
 
 .data-metrics-row {
   display: flex;
@@ -1245,25 +1505,104 @@ onMounted(() => {
   background: #fff;
   border-bottom: 1px solid #f0f0f0;
 }
-.metric-item { text-align: center; flex: 1; }
-.val { font-size: 20px; font-weight: bold; color: #303133; font-family: DIN, sans-serif; margin-bottom: 4px; }
-.lbl { font-size: 12px; color: #909399; }
-.text-primary { color: #409eff; }
-.text-danger { color: #f56c6c; }
-.text-warning { color: #e6a23c; }
-.divider-v { width: 1px; background: #e0e0e0; height: 30px; align-self: center; }
-
-.detail-tabs { margin-top: 0; }
-.section-block { margin-bottom: 25px; }
-.section-title { font-size: 15px; font-weight: bold; margin-bottom: 15px; border-left: 4px solid #409eff; padding-left: 10px; }
-
-.cert-row { display: flex; gap: 15px; }
-.cert-item { text-align: center; cursor: pointer; }
-.cert-img { width: 120px; height: 80px; border-radius: 6px; border: 1px solid #dcdfe6; background: #f5f7fa; }
-.img-slot { display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; color: #909399; font-size: 24px; }
-.cert-name { font-size: 12px; color: #606266; margin-top: 5px; }
-
-.tag-list { display: flex; flex-wrap: wrap; }
-.tab-content-wrapper { padding: 10px 0; }
-:deep(.el-table .el-table__cell) { padding: 12px 0; }
+
+.metric-item {
+  text-align: center;
+  flex: 1;
+}
+
+.val {
+  font-size: 20px;
+  font-weight: bold;
+  color: #303133;
+  font-family: DIN, sans-serif;
+  margin-bottom: 4px;
+}
+
+.lbl {
+  font-size: 12px;
+  color: #909399;
+}
+
+.text-primary {
+  color: #409eff;
+}
+
+.text-danger {
+  color: #f56c6c;
+}
+
+.text-warning {
+  color: #e6a23c;
+}
+
+.divider-v {
+  width: 1px;
+  background: #e0e0e0;
+  height: 30px;
+  align-self: center;
+}
+
+.detail-tabs {
+  margin-top: 0;
+}
+
+.section-block {
+  margin-bottom: 25px;
+}
+
+.section-title {
+  font-size: 15px;
+  font-weight: bold;
+  margin-bottom: 15px;
+  border-left: 4px solid #409eff;
+  padding-left: 10px;
+}
+
+.cert-row {
+  display: flex;
+  gap: 15px;
+}
+
+.cert-item {
+  text-align: center;
+  cursor: pointer;
+}
+
+.cert-img {
+  width: 120px;
+  height: 80px;
+  border-radius: 6px;
+  border: 1px solid #dcdfe6;
+  background: #f5f7fa;
+}
+
+.img-slot {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 100%;
+  height: 100%;
+  color: #909399;
+  font-size: 24px;
+}
+
+.cert-name {
+  font-size: 12px;
+  color: #606266;
+  margin-top: 5px;
+}
+
+.tag-list {
+  display: flex;
+  flex-wrap: wrap;
+}
+
+.tab-content-wrapper {
+  padding: 10px 0;
+}
+
+:deep(.el-table .el-table__cell) {
+  padding: 12px 0;
+}
 </style>

+ 28 - 0
src/views/order/orderList/components/OrderDetailDrawer.vue

@@ -279,6 +279,24 @@
                             </el-timeline>
                         </div>
                     </el-tab-pane>
+
+                    <!-- Tab 5: Complaint Records -->
+                    <el-tab-pane label="投诉记录" name="complaints">
+                        <div class="tab-pane-content">
+                            <div v-if="complaintList.length === 0" class="empty-state">
+                                <el-result icon="success" title="暂无投诉" sub-title="该订单暂无投诉记录"></el-result>
+                            </div>
+                            <el-timeline v-else>
+                                <el-timeline-item v-for="(complaint, index) in complaintList" :key="index" 
+                                    :timestamp="complaint.createTime" placement="top" color="#f56c6c">
+                                    <div class="log-card">
+                                        <div class="l-tit">履约者:{{ complaint.fulfiller }}</div>
+                                        <div class="l-txt">{{ complaint.reason }}</div>
+                                    </div>
+                                </el-timeline-item>
+                            </el-timeline>
+                        </div>
+                    </el-tab-pane>
                 </el-tabs>
             </div>
         </div>
@@ -298,6 +316,7 @@ import { ElMessage } from 'element-plus'
 import { getPet } from '@/api/archieves/pet'
 import { getCustomer } from '@/api/archieves/customer'
 import { listSubOrderLog, exportSubOrderLogUrl } from '@/api/order/subOrderLog/index'
+import { listComplaintByOrder } from '@/api/fulfiller/complaint'
 
 const { proxy } = getCurrentInstance()
 
@@ -338,12 +357,14 @@ const loadSeq = ref(0)
 
 const orderLogs = ref([])
 const fulfillerLogs = ref([])
+const complaintList = ref([])
 
 const loadOrderLogs = async (order) => {
     const id = order?.id
     if (!id) {
         orderLogs.value = []
         fulfillerLogs.value = []
+        complaintList.value = []
         return
     }
     try {
@@ -356,6 +377,13 @@ const loadOrderLogs = async (order) => {
         orderLogs.value = []
         fulfillerLogs.value = []
     }
+    
+    try {
+        const complaintRes = await listComplaintByOrder(id)
+        complaintList.value = complaintRes?.data || []
+    } catch {
+        complaintList.value = []
+    }
 }
 
 const loadPetAndCustomer = async (order) => {

+ 53 - 12
src/views/order/orderList/index.vue

@@ -123,6 +123,8 @@
                 @click="openDispatchDialog(row)">重新派单</el-button>
               <el-button v-if="[0, 1].includes(row.status)" link type="danger" size="small"
                 @click="handleCancel(row)">取消</el-button>
+              <el-button v-if="row.fulfiller && ![1].includes(row.status)" link type="warning" size="small"
+                @click="openComplaintDialog(row)">投诉</el-button>
 
               <el-dropdown v-if="[3, 4].includes(row.status)" trigger="click"
                 @command="(cmd) => handleCommand(cmd, row)">
@@ -134,7 +136,8 @@
                 <template #dropdown>
                   <el-dropdown-menu>
                     <el-dropdown-item v-if="row.status === 3" command="complete">确认完成</el-dropdown-item>
-                    <el-dropdown-item v-if="row.status === 4 && getServiceMode(row.service) == 0" command="care_summary">护理小结</el-dropdown-item>
+                    <el-dropdown-item v-if="row.status === 4 && getServiceMode(row.service) == 0"
+                      command="care_summary">护理小结</el-dropdown-item>
                     <el-dropdown-item command="reward">奖惩</el-dropdown-item>
                     <el-dropdown-item command="remark">备注</el-dropdown-item>
                   </el-dropdown-menu>
@@ -164,6 +167,19 @@
     <RewardDialog v-model:visible="rewardDialogVisible" :order="currentOperateRow" @submit="handleRewardSubmit" />
 
     <RemarkDialog v-model:visible="remarkDialogVisible" :order="currentOperateRow" @submit="handleRemarkSubmit" />
+
+    <!-- 投诉弹窗 -->
+    <el-dialog v-model="complaintDialogVisible" title="投诉" width="400px">
+      <el-form :model="complaintForm" label-width="60px">
+        <el-form-item label="原因" required>
+          <el-input v-model="complaintForm.reason" type="textarea" :rows="4" placeholder="请输入投诉原因" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="complaintDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="submitComplaint">确认</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
@@ -190,6 +206,7 @@ import { getStore } from '@/api/system/store';
 import { reward } from '@/api/fulfiller/pool';
 import { getPet } from '@/api/archieves/pet';
 import { getCustomer } from '@/api/archieves/customer';
+import { addComplaint } from '@/api/fulfiller/complaint';
 
 const loading = ref(false);
 
@@ -378,6 +395,9 @@ const careSummaryOrder = ref(null);
 
 const rewardDialogVisible = ref(false);
 const remarkDialogVisible = ref(false);
+const complaintDialogVisible = ref(false);
+const complaintForm = reactive({ reason: '' });
+const currentComplaintOrder = ref(null);
 const currentOperateRow = ref(null);
 
 // 详情
@@ -645,7 +665,7 @@ const handleRewardSubmit = async (form) => {
   try {
     // 针对余额(balance)目标,将元转为分
     const submitAmount = form.item === 'balance' ? Math.round(form.value * 100) : form.value;
-    
+
     await reward({
       fulfillerId: currentOperateRow.value.fulfiller,
       type: form.type,
@@ -676,18 +696,39 @@ const handleRemarkSubmit = async (text) => {
     return;
   }
   try {
-    await remarkSubOrder({
-      orderId: currentOperateRow.value.id,
-      remark: text
-    });
-    if (currentOperateRow.value) {
-      currentOperateRow.value.remark = text;
-    }
-    ElMessage.success('备注已保存');
+    await remarkSubOrder({ orderId: currentOperateRow.value.id, remark: text });
+    ElMessage.success('备注添加成功');
+    remarkDialogVisible.value = false;
     handleSearch();
-  } catch {
-    // Error handled by interceptor
+  } catch { /* handled by interceptor */ }
+};
+
+// 投诉
+const openComplaintDialog = (row) => {
+  currentComplaintOrder.value = row;
+  complaintForm.reason = '';
+  complaintDialogVisible.value = true;
+};
+
+const submitComplaint = async () => {
+  if (!complaintForm.reason.trim()) {
+    ElMessage.warning('请输入投诉原因');
+    return;
+  }
+  if (!currentComplaintOrder.value?.id || !currentComplaintOrder.value?.fulfiller) {
+    ElMessage.warning('订单信息不完整');
+    return;
   }
+  try {
+    await addComplaint({
+      orderId: currentComplaintOrder.value.id,
+      fulfiller: currentComplaintOrder.value.fulfiller,
+      reason: complaintForm.reason
+    });
+    ElMessage.success('投诉提交成功');
+    complaintDialogVisible.value = false;
+    handleSearch();
+  } catch { /* handled by interceptor */ }
 };
 
 // 更多操作