Huanyi 4 недель назад
Родитель
Сommit
cbe078d0b8

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

@@ -0,0 +1,48 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { FlfLevelRightsVO, FlfLevelRightsForm } from './types';
+
+/**
+ * 查询等级权益库列表(全部)
+ */
+export const listAllLevelRights = (): AxiosPromise<FlfLevelRightsVO[]> => {
+  return request({
+    url: '/fulfiller/levelRights/listAll',
+    method: 'get'
+  });
+};
+
+/**
+ * 新增等级权益
+ * @param data
+ */
+export const addLevelRights = (data: FlfLevelRightsForm): AxiosPromise<void> => {
+  return request({
+    url: '/fulfiller/levelRights/add',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改等级权益
+ * @param data
+ */
+export const updateLevelRights = (data: FlfLevelRightsForm): AxiosPromise<void> => {
+  return request({
+    url: '/fulfiller/levelRights/edit',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除等级权益
+ * @param id
+ */
+export const delLevelRights = (id: string | number): AxiosPromise<void> => {
+  return request({
+    url: '/fulfiller/levelRights/remove/' + id,
+    method: 'delete'
+  });
+};

+ 35 - 0
src/api/fulfiller/levelRights/types.ts

@@ -0,0 +1,35 @@
+/**
+ * 等级权益库对象
+ */
+export interface FlfLevelRightsVO {
+  /** 主键ID */
+  id: number | string;
+  /** 权益名称 */
+  name: string;
+  /** 权益图标URL */
+  iconUrl: string;
+  /** 图标ID */
+  icon: number | string;
+  /** 状态 true可用 false不可用 */
+  status: boolean;
+  /** 权益说明 */
+  statement: string;
+}
+
+/**
+ * 权益表单对象
+ */
+export interface FlfLevelRightsForm {
+  /** 主键ID */
+  id?: number | string;
+  /** 权益名称 */
+  name: string;
+  /** 权益图标ossId */
+  icon: number | string;
+  /** 状态 true可用 false不可用 */
+  status: boolean;
+  /** 权益说明 */
+  statement: string;
+  /** 其他参数 */
+  params?: any;
+}

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

@@ -64,6 +64,8 @@ export interface FlfFulfillerForm extends BaseEntity {
   birthday?: string;
   idCard?: string;
   idCardExpiry?: string;
+  idCardFront?: string | number;
+  idCardBack?: string | number;
   serviceTypes?: string;
   cityCode?: string;
   cityName?: string;

+ 25 - 0
src/api/fulfiller/violation/index.ts

@@ -0,0 +1,25 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { FlfViolationForm, FlfViolationQuery, FlfViolationVO } from './types';
+
+/**
+ * 新增违规记录
+ */
+export const addViolation = (data: FlfViolationForm) => {
+  return request({
+    url: '/fulfiller/violation/add',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 根据履约者查询违规记录(分页)
+ */
+export const listViolationByFulfiller = (query: FlfViolationQuery): AxiosPromise<FlfViolationVO[]> => {
+  return request({
+    url: '/fulfiller/violation/pageByFulfiller',
+    method: 'get',
+    params: query
+  });
+};

+ 51 - 0
src/api/fulfiller/violation/types.ts

@@ -0,0 +1,51 @@
+/**
+ * 违规记录对象
+ */
+export interface FlfViolationForm {
+  /** 主键ID */
+  id?: number | string;
+  /** 履约者ID */
+  fulfiller: number | string;
+  /** 违规次数 */
+  count: number;
+  /** 违规时间 */
+  violationTime: string;
+  /** 违规原因 */
+  reason: string;
+  /** 创建部门 */
+  createDept?: number | string;
+  /** 创建人 */
+  createBy?: number | string;
+  /** 创建时间 */
+  createTime?: string;
+  /** 更新人 */
+  updateBy?: number | string;
+  /** 更新时间 */
+  updateTime?: string;
+  /** 参数 */
+  params?: any;
+}
+
+/**
+ * 违规记录视图对象
+ */
+export interface FlfViolationVO {
+  /** 主键ID */
+  id: number | string;
+  /** 履约者ID */
+  fulfiller: number | string;
+  /** 违规次数 */
+  count: number;
+  /** 违规时间 */
+  violationTime: string;
+  /** 违规原因 */
+  reason: string;
+}
+
+/**
+ * 违规记录查询参数
+ */
+export interface FlfViolationQuery extends PageQuery {
+  /** 履约者ID */
+  fulfillerId: number | string;
+}

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

@@ -24,7 +24,7 @@
           <el-descriptions-item label="电话">{{ currentUser.phone }}</el-descriptions-item>
           <el-descriptions-item label="所属区域">{{ currentUser.areaName || '-' }}</el-descriptions-item>
           <el-descriptions-item label="所属站点">{{ currentUser.stationName || '-' }}</el-descriptions-item>
-          <el-descriptions-item label="录入来源">{{ currentUser.source || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="所属品牌">{{ currentUser.tenantName || '-' }}</el-descriptions-item>
           <el-descriptions-item label="录入时间">{{ currentUser.createTime || '-' }}</el-descriptions-item>
         </el-descriptions>
 

+ 87 - 59
src/views/fulfiller/level/index.vue

@@ -3,7 +3,7 @@
     <el-tabs v-model="activeTab" type="card" class="level-tabs">
       <el-tab-pane label="等级配置" name="levels">
         <el-card shadow="never" class="content-card">
-          <template #header>
+          <template v-slot:header>
             <div class="card-header">
               <span class="title">等级体系列表</span>
               <el-button type="primary" icon="Plus" @click="handleEditLevel(null)">新增等级</el-button>
@@ -32,7 +32,12 @@
             </el-table-column>
             <el-table-column label="包含权益" min-width="300">
               <template #default="{ row }">
-                <el-tag v-for="right in row.rights" :key="right.id" size="small" class="right-tag" type="info">{{ right.name }}</el-tag>
+                <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>
+                </template>
+                <span v-else style="color: #999; font-size: 12px">暂无权益</span>
               </template>
             </el-table-column>
             <el-table-column label="操作" width="180">
@@ -47,7 +52,7 @@
 
       <el-tab-pane label="权益库管理" name="rights">
         <el-card shadow="never" class="content-card">
-          <template #header>
+          <template v-slot:header>
             <div class="card-header">
               <span class="title">等级权益库</span>
               <el-button type="primary" icon="Plus" @click="handleEditRight(null)">新增权益</el-button>
@@ -59,7 +64,7 @@
                 <div class="level-name-col">
                   <el-image
                     style="width: 30px; height: 30px; border-radius: 4px"
-                    :src="row.icon || 'https://cube.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg'"
+                    :src="row.iconUrl || 'https://cube.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg'"
                     fit="cover"
                   />
                   <span>{{ row.name }}</span>
@@ -71,11 +76,11 @@
                 <el-switch v-model="row.status" size="small" @change="handleRightStatusChange(row)" />
               </template>
             </el-table-column>
-            <el-table-column prop="desc" label="权益说明" show-overflow-tooltip />
+            <el-table-column prop="statement" label="权益说明" show-overflow-tooltip />
             <el-table-column label="操作" width="150">
               <template #default="{ row }">
                 <el-button link type="primary" @click="handleEditRight(row)">编辑</el-button>
-                <el-button link type="danger">删除</el-button>
+                <el-button link type="danger" @click="handleDeleteRight(row)">删除</el-button>
               </template>
             </el-table-column>
           </el-table>
@@ -208,20 +213,17 @@
     <!-- 权益编辑弹窗 -->
     <el-dialog v-model="rightDialog.visible" :title="rightDialog.isEdit ? '编辑权益' : '新增权益'" width="500px">
       <el-form :model="rightDialog.form" label-width="100px">
-        <el-form-item label="权益名称" required>
-          <el-input v-model="rightDialog.form.name" />
+        <el-form-item label="权益名称" required prop="name">
+          <el-input v-model="rightDialog.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="handleRightIconChange">
-            <img v-if="rightDialog.form.icon" :src="rightDialog.form.icon" class="avatar" />
-            <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
-          </el-upload>
+        <el-form-item label="权益图标" required prop="icon">
+          <image-upload v-model="rightDialog.form.icon" :limit="1" />
         </el-form-item>
         <el-form-item label="权益状态">
           <el-switch v-model="rightDialog.form.status" active-text="启用" inactive-text="停用" />
         </el-form-item>
         <el-form-item label="权益描述">
-          <el-input v-model="rightDialog.form.desc" type="textarea" :rows="3" />
+          <el-input v-model="rightDialog.form.statement" type="textarea" :rows="3" />
         </el-form-item>
       </el-form>
       <template #footer>
@@ -232,36 +234,27 @@
   </div>
 </template>
 
-<script setup>
-import { ref, reactive, computed } from 'vue';
-import { ElMessage } from 'element-plus';
+<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 ImageUpload from '@/components/ImageUpload/index.vue';
 
 const activeTab = ref('levels');
 
 // Data: Rights
-const rightsList = ref([
-  {
-    id: 1,
-    name: '节假日双倍积分',
-    icon: 'https://cube.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg',
-    status: true,
-    desc: '在法定节假日接单,获得积分翻倍'
-  },
-  {
-    id: 2,
-    name: '专属人工客服',
-    icon: 'https://cube.elemecdn.com/9/c2/f0ee8a3c7c9638a54940382568c9dpng.png',
-    status: true,
-    desc: '拥有专属客服经理,问题优先处理'
-  },
-  {
-    id: 3,
-    name: '提现免手续费',
-    icon: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
-    status: true,
-    desc: '每月前3笔提现免除平台手续费'
+// Data: Rights
+const rightsList = ref([]);
+
+const getRightsList = async () => {
+  try {
+    const res = await listAllLevelRights();
+    rightsList.value = res.data || [];
+  } catch (error) {
+    console.error('Failed to fetch rights list:', error);
   }
-]);
+};
 
 // Data: Levels
 const levelList = ref([
@@ -273,7 +266,7 @@ const levelList = ref([
     minPoints: 0,
     maxPoints: 1000,
     holidayMultiplier: 1.0,
-    rights: [rightsList.value[1]],
+    rights: [],
     rules: { periodDays: 30, minOrders: 10, onTimeRate: 90, complaintRate: 5, maxViolations: 0, accumulatedPoints: 100 },
     deductions: { late: 5, violation: 20, complaint: 50 },
     downgradeType: 'points'
@@ -286,7 +279,7 @@ const levelList = ref([
     minPoints: 1001,
     maxPoints: 5000,
     holidayMultiplier: 1.2,
-    rights: [rightsList.value[1], rightsList.value[2]],
+    rights: [],
     rules: { periodDays: 30, minOrders: 30, onTimeRate: 95, complaintRate: 2, maxViolations: 0, accumulatedPoints: 1000 },
     deductions: { late: 10, violation: 50, complaint: 100 },
     downgradeType: 'points'
@@ -299,7 +292,7 @@ const levelList = ref([
     minPoints: 5001,
     maxPoints: -1,
     holidayMultiplier: 1.5,
-    rights: [rightsList.value[0], rightsList.value[1], rightsList.value[2]],
+    rights: [],
     rules: { periodDays: 30, minOrders: 100, onTimeRate: 98, complaintRate: 0, maxViolations: 0, accumulatedPoints: 5000 },
     deductions: { late: 20, violation: 100, complaint: 200 },
     downgradeType: 'points'
@@ -328,7 +321,13 @@ const levelDialog = reactive({
 const rightDialog = reactive({
   visible: false,
   isEdit: false,
-  form: { name: '', icon: '', status: true, desc: '' }
+  form: { 
+    id: undefined, 
+    name: '', 
+    icon: '', 
+    status: true, 
+    statement: '' 
+  } as FlfLevelRightsForm
 });
 
 // Methods
@@ -395,30 +394,59 @@ const handleEditRight = (row) => {
   if (row) {
     rightDialog.form = { ...row };
   } else {
-    rightDialog.form = { name: '', icon: '', status: true, desc: '' };
+    rightDialog.form = { name: '', icon: '', status: true, statement: '' };
   }
 };
 
-const handleRightIconChange = (file) => {
-  rightDialog.form.icon = URL.createObjectURL(file.raw);
-};
+const saveRight = async () => {
+  if (!rightDialog.form.name) return ElMessage.warning('请输入权益名称');
+  if (!rightDialog.form.icon) return ElMessage.warning('请选择权益图标');
 
-const saveRight = () => {
-  if (rightDialog.isEdit) {
-    const idx = rightsList.value.findIndex((r) => r.id === rightDialog.form.id);
-    if (idx !== -1) rightsList.value[idx] = { ...rightsList.value[idx], ...rightDialog.form };
-  } else {
-    // Default icon if empty for demo
-    if (!rightDialog.form.icon) rightDialog.form.icon = 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png';
-    rightsList.value.push({ ...rightDialog.form, id: Date.now() });
+  try {
+    if (rightDialog.isEdit) {
+      await updateLevelRights(rightDialog.form);
+      ElMessage.success('权益编辑成功');
+    } else {
+      await addLevelRights(rightDialog.form);
+      ElMessage.success('权益新增成功');
+    }
+    rightDialog.visible = false;
+    getRightsList();
+  } catch (error) {
+    console.error('Save right failed:', error);
   }
-  rightDialog.visible = false;
-  ElMessage.success('权益保存成功');
 };
 
-const handleRightStatusChange = (row) => {
-  ElMessage.success(`权益 "${row.name}" 已${row.status ? '启用' : '停用'}`);
+const handleDeleteRight = (row) => {
+  ElMessageBox.confirm(`确定要删除权益 "${row.name}" 吗?此操作不可撤销。`, '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(async () => {
+    try {
+      await delLevelRights(row.id);
+      ElMessage.success('删除成功');
+      getRightsList();
+    } catch (error) {
+      console.error('Delete right failed:', error);
+    }
+  }).catch(() => {});
 };
+
+const handleRightStatusChange = async (row) => {
+  try {
+    const data = { ...row };
+    await updateLevelRights(data);
+    ElMessage.success(`权益 "${row.name}" 已${row.status ? '启用' : '停用'}`);
+  } catch (error) {
+    row.status = !row.status; // 切换失败恢复状态
+    console.error('Update right status failed:', error);
+  }
+};
+
+onMounted(() => {
+  getRightsList();
+});
 </script>
 
 <style scoped>

+ 97 - 35
src/views/fulfiller/pool/index.vue

@@ -150,6 +150,7 @@
                     <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>
                 </template>
@@ -366,11 +367,6 @@
                     </el-tag>
                   </template>
                 </el-table-column>
-                <!-- <el-table-column prop="type" label="操作" width="80">
-                  <template #default="{ row }">
-                    <el-tag :type="['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? 'success' : 'danger'" size="small">{{ ['reward', fulfillerEnums.ActionType.ADD].includes(row.type) ? '增加' : '减少' }}</el-tag>
-                  </template>
-                </el-table-column> -->
                 <el-table-column prop="target" label="关联项目" width="100">
                   <template #default="{ row }">
                     <el-tag type="info" size="small" effect="plain">{{ row.target === 'points' ? '积分' : '余额' }}</el-tag>
@@ -388,6 +384,27 @@
               </el-table>
             </div>
           </el-tab-pane>
+
+          <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-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="reason" label="违规原因" show-overflow-tooltip />
+              </el-table>
+            </div>
+          </el-tab-pane>
         </el-tabs>
       </div>
     </el-drawer>
@@ -463,16 +480,6 @@
           <el-checkbox v-model="editDialog.form.authQual">专业资质认证</el-checkbox>
         </el-form-item>
 
-        <el-form-item label="技能标签">
-          <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-item label="服务类型">
-          <el-select v-model="editDialog.serviceTypesArray" multiple placeholder="请选择服务类型" style="width: 100%">
-            <el-option v-for="item in serviceOptions" :key="item.id" :label="item.name" :value="String(item.id)" />
-          </el-select>
-        </el-form-item>
       </el-form>
       <template #footer>
             <span class="dialog-footer">
@@ -541,11 +548,6 @@
             <el-option v-for="station in createDialog.stationOptions" :key="station.id" :label="station.name" :value="station.id" />
           </el-select>
         </el-form-item>
-        <el-form-item label="服务类型">
-          <el-select v-model="createDialog.serviceTypesArray" multiple placeholder="请选择服务类型" style="width: 100%">
-            <el-option v-for="item in serviceOptions" :key="item.id" :label="item.name" :value="String(item.id)" />
-          </el-select>
-        </el-form-item>
       </el-form>
       <template #footer>
         <el-button @click="createDialog.visible = false">取消</el-button>
@@ -553,6 +555,32 @@
       </template>
     </el-dialog>
 
+    <!-- 违规记录弹窗 -->
+    <el-dialog v-model="violationDialog.visible" title="新增违规记录" width="450px">
+      <el-form :model="violationDialog.form" label-width="80px">
+        <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-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="请输入违规原因说明" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="violationDialog.visible = false">取消</el-button>
+        <el-button type="primary" @click="submitViolation">确认提交</el-button>
+      </template>
+    </el-dialog>
+
     <!-- 积分调整弹窗 -->
     <el-dialog v-model="pointsDialog.visible" title="修改积分" width="400px">
       <el-form :model="pointsDialog.form" label-width="80px">
@@ -626,6 +654,8 @@ import {
   changeStatus, resetPwd, reward, adjustPoints, adjustBalance,
   listPointsLog, listBalanceLog, listRewardLog
 } from '@/api/fulfiller/pool'
+import { addViolation, listViolationByFulfiller } from '@/api/fulfiller/violation'
+import type { FlfViolationVO } from '@/api/fulfiller/violation/types'
 import { listSubOrderOnFulfiller } from '@/api/order/subOrder/index'
 import { listOnStore as listServiceOnStore } from '@/api/service/list/index'
 import type {
@@ -638,6 +668,7 @@ import type { FlfTagVO } from '@/api/fulfiller/tag/types'
 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'
 
 const loading = ref(false)
 const searchKey = ref('')
@@ -670,6 +701,7 @@ const qualImageUrlList = computed(() => {
 const pointsLogData = ref<FlfPointsLogVO[]>([])
 const balanceLogData = ref<FlfBalanceLogVO[]>([])
 const rewardLogData = ref<FlfRewardLogVO[]>([])
+const violationLogData = ref<FlfViolationVO[]>([])
 const serviceOrderData = ref<any[]>([])
 const serviceOptions = ref<any[]>([])
 const logLoading = ref(false)
@@ -806,8 +838,7 @@ const editDialog = reactive({
   visible: false,
   form: {} as FlfFulfillerForm,
   cascaderValue: [] as any[],
-  stationOptions: [] as SysAreaStationOnStoreVo[],
-  serviceTypesArray: [] as string[]
+  stationOptions: [] as SysAreaStationOnStoreVo[]
 })
 
 const createDialog = reactive({
@@ -816,8 +847,7 @@ const createDialog = reactive({
     name: '', phone: '', password: '', cityCode: '', cityName: '', stationId: undefined as any, gender: '0', workType: 'full_time'
   },
   cascaderValue: [] as any[],
-  stationOptions: [] as SysAreaStationOnStoreVo[],
-  serviceTypesArray: [] as string[]
+  stationOptions: [] as SysAreaStationOnStoreVo[]
 })
 
 const pointsDialog = reactive({
@@ -832,6 +862,16 @@ const balanceDialog = reactive({
   form: { type: fulfillerEnums.ActionType.ADD, subType: 'admin_reward', amount: 0, reason: '' }
 })
 
+const violationDialog = reactive({
+  visible: false,
+  fulfillerId: 0 as number | string,
+  form: {
+    count: 1,
+    violationTime: '',
+    reason: ''
+  }
+})
+
 const getStatusText = (status: string) => {
   const map: Record<string, string> = { busy: '接单中', resting: '休息', disabled: '禁用', frozen: '冻结' }
   return map[status] || '未知'
@@ -859,16 +899,18 @@ const handleTabClick = (tab: any) => {
 const loadLogs = async (fulfillerId: string | number) => {
   logLoading.value = true
   try {
-    const [pRes, bRes, rRes, sRes] = await Promise.all([
+    const [pRes, bRes, rRes, sRes, vRes] = await Promise.all([
       listPointsLog(fulfillerId, { pageNum: 1, pageSize: 20 }),
       listBalanceLog(fulfillerId, { pageNum: 1, pageSize: 20 }),
       listRewardLog(fulfillerId, { pageNum: 1, pageSize: 20 }),
-      listSubOrderOnFulfiller(fulfillerId)
+      listSubOrderOnFulfiller(fulfillerId),
+      listViolationByFulfiller({ fulfillerId, pageNum: 1, pageSize: 20 })
     ])
     pointsLogData.value = pRes.rows || []
     balanceLogData.value = bRes.rows || []
     rewardLogData.value = rRes.rows || []
     serviceOrderData.value = sRes.data || []
+    violationLogData.value = vRes.rows || []
   } catch { /* ignore */ } finally {
     logLoading.value = false
   }
@@ -900,10 +942,8 @@ const handleEdit = (row: FlfFulfillerVO) => {
     status: row.status,
     authId: row.authId,
     authQual: row.authQual,
-    tagIds: row.tags ? row.tags.map(t => t.id) : [],
-    serviceTypes: row.serviceTypes
+    tagIds: row.tags ? row.tags.map(t => t.id) : []
   }
-  editDialog.serviceTypesArray = row.serviceTypes ? row.serviceTypes.split(',') : []
   // 根据cityCode构建级联选择器的值
   editDialog.cascaderValue = []
   editDialog.stationOptions = []
@@ -925,10 +965,11 @@ const handleEdit = (row: FlfFulfillerVO) => {
 }
 
 const handleCreate = () => {
-  createDialog.form = { name: '', phone: '', password: '', cityCode: '', cityName: '', stationId: undefined, gender: '0', workType: 'full_time' }
+  createDialog.form = {
+    name: '', phone: '', password: '', cityCode: '', cityName: '', stationId: undefined, gender: '0', workType: 'full_time'
+  }
   createDialog.cascaderValue = []
   createDialog.stationOptions = []
-  createDialog.serviceTypesArray = []
   createDialog.visible = true
 }
 
@@ -938,8 +979,7 @@ const submitCreate = async () => {
     return
   }
   try {
-    const submitForm = { ...createDialog.form, serviceTypes: createDialog.serviceTypesArray.join(',') };
-    await addFulfiller(submitForm as FlfFulfillerForm)
+    await addFulfiller(createDialog.form as FlfFulfillerForm)
     createDialog.visible = false
     ElMessage.success('创建成功')
     getList()
@@ -948,8 +988,7 @@ const submitCreate = async () => {
 
 const saveEdit = async () => {
   try {
-    const submitForm = { ...editDialog.form, serviceTypes: editDialog.serviceTypesArray.join(',') };
-    await updateFulfiller(submitForm)
+    await updateFulfiller(editDialog.form)
     ElMessage.success('更新成功')
     editDialog.visible = false
     getList()
@@ -1012,9 +1051,32 @@ const handleCommand = async (cmd: string, row: FlfFulfillerVO) => {
       await resetPwd(row.id, '123456')
       ElMessage.success('密码重置成功')
     } 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: ''
+    }
+    violationDialog.visible = true
   }
 }
 
+const submitViolation = async () => {
+  if (!violationDialog.form.violationTime || !violationDialog.form.reason) {
+    ElMessage.warning('请填写完整信息')
+    return
+  }
+  try {
+    await addViolation({
+      fulfiller: violationDialog.fulfillerId,
+      ...violationDialog.form
+    } as any)
+    ElMessage.success('添加违规记录成功')
+    violationDialog.visible = false
+  } catch { /* handled by interceptor */ }
+}
+
 const submitPointsAdjust = async () => {
   if (!pointsDialog.currentRow) return
   try {

+ 22 - 4
src/views/system/areaStation/index.vue

@@ -50,7 +50,7 @@
               <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"
                 v-hasPermi="['system:areaStation:edit']" />
             </el-tooltip>
-            <el-tooltip content="新增" placement="top">
+            <el-tooltip v-if="scope.row.type !== 2" content="新增" placement="top">
               <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)"
                 v-hasPermi="['system:areaStation:add']" />
             </el-tooltip>
@@ -69,8 +69,8 @@
           <el-input v-model="form.name" placeholder="请输入区域名称" />
         </el-form-item>
         <el-form-item label="区域类型" prop="type">
-          <el-radio-group v-model="form.type">
-            <el-radio v-for="type in typeList" :key="type.value" :label="type.value">
+          <el-radio-group v-model="form.type" :disabled="!!form.id">
+            <el-radio v-for="type in filteredTypeList" :key="type.value" :label="type.value">
               <el-tag :type="type.style">
                 {{ type.label }}
               </el-tag>
@@ -142,6 +142,7 @@ const loading = ref(false);
 const queryFormRef = ref<ElFormInstance>();
 const areaStationFormRef = ref<ElFormInstance>();
 const areaStationTableRef = ref<ElTableInstance>();
+const filteredTypeList = ref<SysAreaStationTypeVo[]>([]);
 
 const dialog = reactive<DialogOption>({
   visible: false,
@@ -248,8 +249,23 @@ const handleAdd = (row?: AreaStationVO) => {
   getTreeselect();
   if (row != null && row.id) {
     form.value.parentId = row.id;
+    if (row.type === 0) {
+      // 城市底下只能新增区域
+      filteredTypeList.value = typeList.value.filter((item) => item.value === 1);
+      form.value.type = 1;
+    } else if (row.type === 1) {
+      // 区域底下可以新增区域或者站点
+      filteredTypeList.value = typeList.value.filter((item) => item.value === 1 || item.value === 2);
+      form.value.type = 1; // 默认选中区域
+    } else {
+      // 站点底下不能新增,理论上按钮已隐藏
+      filteredTypeList.value = [];
+    }
   } else {
+    // 头部新增只能新增城市
     form.value.parentId = 0;
+    filteredTypeList.value = typeList.value.filter((item) => item.value === 0);
+    form.value.type = 0;
   }
   dialog.visible = true;
   dialog.title = '添加区域站点';
@@ -273,10 +289,12 @@ const toggleExpandAll = (data: AreaStationVO[], status: boolean) => {
 const handleUpdate = async (row: AreaStationVO) => {
   reset();
   await getTreeselect();
+  // 修改时只能查看当前类型,不能切换
+  filteredTypeList.value = typeList.value.filter((item) => item.value === row.type);
   if (row != null) {
     form.value.parentId = row.parentId;
   }
-  const res = await getAreaStation(row.id);
+  const res = await getAreaStation(row.id as number);
   Object.assign(form.value, res.data);
   dialog.visible = true;
   dialog.title = '修改区域站点';