Просмотр исходного кода

Merge branch 'dev' of http://39.101.143.165:8090/dong_k/tenant-Platform into dev

caiyuqin 10 месяцев назад
Родитель
Сommit
52be089820

+ 3 - 0
src/api/index.ts

@@ -38,6 +38,8 @@ import insurancePolicyApi from '@/api/modules/insurancePolicy.ts'
 import manageApi from '@/api/modules/consoleManage.ts'
 
 import saleApi from '@/api/modules/sale.ts'
+import dispatchApi from '@/api/modules/dispatch.ts'
+
 
 const axiosInstance:ApiInstance = axios.create({
   baseURL: (import.meta.env.DEV && import.meta.env.VITE_OPEN_PROXY === 'true') ? '/proxy/' : import.meta.env.VITE_APP_API_BASEURL,
@@ -143,6 +145,7 @@ const api = {
   settlementApi: settlementApi(axiosInstance),
   wechatApi:wechatApi(axiosInstance),
   saleApi: saleApi(axiosInstance),
+  dispatchApi: dispatchApi(axiosInstance),
 
 }
 export default api

+ 33 - 0
src/api/modules/dispatch.ts

@@ -0,0 +1,33 @@
+import { ApiInstance } from '../type'
+import * as Types from '../types/agreementTypes';
+const dispatchApi = (axiosInstance: ApiInstance) => ({
+
+  /**
+   * @param 协议管理
+   */
+  //保司列表
+  getCompanyList: () => axiosInstance.get(`scheme/getCompanyList?systemCode=${localStorage.getItem('login_system')}`),
+  //协议列表
+  dispatchList: (data: any) => axiosInstance.get('pltAgreement/attr/list', data),
+  //详情信息查询
+  getByCompanyIdt: (data: any) => axiosInstance.get(`agreement/getByCompanyId?companyId=${data}`),
+  //保司账号
+  getLoginUrl:(data: any) => axiosInstance.post(`manager/attribution/getLoginUrl`,data),
+  //保存
+  addAgreementDispatch:(data: any) => axiosInstance.post(`ptlAgreementDispatch/addAgreementDispatch`,data),
+  //获取后线人员    
+  getUserList: (data: any) => axiosInstance.post(`userInfo/getUser`,data),
+  //添加调度动作  
+  addAgreementDispatchAction: (data: any) => axiosInstance.post(`ptlAgreementDispatch/addAgreementDispatchAction`,data),
+  //获取调度规则列表 getAgreementDispatch
+  getAgreementDispatch: (data: any) => axiosInstance.post(`ptlAgreementDispatch/getAgreementDispatch`,data),
+  //删除调度规则
+  deleteAgreementDispatch: (data: any) => axiosInstance.get(`ptlAgreementDispatch/deleteAgreementDispatch?dispatchId=${data}`),
+  //修改调度规则
+  updateAgreementDispatch: (data: any) => axiosInstance.post(`ptlAgreementDispatch/updateAgreementDispatch`,data),
+  //修改规则名称  
+  updateDispatchName:(data:any) => axiosInstance.get(`ptlAgreementDispatch/updateDispatchName?dispatchId=${data.dispatchId}&name=${data.name}`),
+  //获取保司账号
+  getAgreementAttributionByCompanyId:(data:any) => axiosInstance.get(`/manager/attributionTemplate/getAgreementAttributionByCompanyId?companyId=${data}`,),
+})
+export default dispatchApi

BIN
src/assets/images/add.png


BIN
src/assets/images/delete.png


BIN
src/assets/images/edit.png


+ 20 - 2
src/router/modules/additionalOrder.ts

@@ -51,7 +51,6 @@ const routes: RouteRecordRaw = {
       //   },
       // ]
     },
-
     {
       path: 'order',
       name: 'MyOrder',
@@ -73,7 +72,6 @@ const routes: RouteRecordRaw = {
           },
       ]
     },
-
     {
       path: 'temporarily',
       name: 'Temporarily',
@@ -82,6 +80,26 @@ const routes: RouteRecordRaw = {
         title: '暂存订单管理',
         // auth:['role.add','role.view','role.delete','role.edit']
       },
+    },
+    {
+      path: 'adjustRecord',
+      name: 'AdditionalAdjustRecord',
+      component: () => import('@/views/additionalOrder/adjust/adjustRecord.vue'),
+      meta: {
+        title: '批改记录',
+      },
+      children: [
+        {
+          path: 'detail',
+          name: 'AdditionalAdjustRecordDetail',
+          component: () => import('@/views/orderBatchAdjust/recordDetail.vue'),
+          meta: {
+            title: '记录详情',
+            menu: false,
+            activeMenu: '/additionalOrder/adjustRecord',
+          },
+        }
+      ]
     }
   ],
 }

+ 6 - 6
src/router/modules/insurancePolicy.ts

@@ -64,21 +64,21 @@ const routes: RouteRecordRaw = {
       ]
     },
     {
-      path: 'record',
-      name: 'InsurancePolicyRecord',
-      component: () => import('@/views/insurancePolicy/record.vue'),
+      path: 'adjustRecord',
+      name: 'InsurancePolicyAdjustRecord',
+      component: () => import('@/views/insurancePolicy/adjustRecord.vue'),
       meta: {
         title: '批改记录',
       },
       children: [
         {
           path: 'detail',
-          name: 'InsurancePolicyRecordDetail',
-          component: () => import('@/views/insurancePolicy/recordDetail.vue'),
+          name: 'InsurancePolicyAdjustRecordDetail',
+          component: () => import('@/views/orderBatchAdjust/recordDetail.vue'),
           meta: {
             title: '记录详情',
             menu: false,
-            activeMenu: '/insurancePolicy/record',
+            activeMenu: '/insurancePolicy/adjustRecord',
           },
         }
       ]

+ 1 - 1
src/router/modules/quoteConfig.ts

@@ -108,7 +108,7 @@ const routes: RouteRecordRaw = {
     {
       path: 'dispatchIndex',
       name: 'dispatchIndex',
-      component: () => import('@/views/quoteConfig/dispatch/dispatch.vue'),
+      component: () => import('@/views/quoteConfig/dispatch/index.vue'),
       meta: {
         title: '协议调度',
       },

+ 4 - 4
src/views/insurancePolicy/record.vue → src/views/additionalOrder/adjust/adjustRecord.vue

@@ -1,6 +1,5 @@
 <script setup lang="ts">
 import api from '@/api';
-import { ref, reactive } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 import { dictData } from '@/views/localDict/index';
 import { loadDicts, getDict } from '@/utils/composables/dictService';
@@ -27,7 +26,7 @@ const pageInfo = ref<PageInfo>({
 	sizes: [10, 20, 30, 40, 50],
 	total: 0,
 });
-const columns = [
+const columns: any = [
 	{
 		prop: "orderNo", label: "订单号", width: 200,
 	},
@@ -127,6 +126,7 @@ const initialForm = {
 	time: [],
 	endDate: '',
 	startDate: '',
+	entryStatus: 3
 };
 const formInline = ref<any>({ ...initialForm });
 
@@ -198,13 +198,13 @@ const handleExport = () => {
 // 详情
 const toDetail = (e: any) => {
     router.push({
-        path: '/insurancePolicy/record/detail',
+        path: '/additionalOrder/adjustRecord/detail',
         query: {
             id: e.operationData.id,
 			orderNo: e.operationData.orderNo,
         }
     });
-	// window.open(`/#/insurancePolicy/record/detail?id=${e.operationData.id}&orderNo=${e.operationData.orderNo}`, '_blank');
+	// window.open(`/#/additionalOrder/adjustRecord/detail?id=${e.operationData.id}&orderNo=${e.operationData.orderNo}`, '_blank');
 }
 
 defineOptions({

+ 4 - 3
src/views/additionalOrder/order/index.vue

@@ -350,8 +350,8 @@
 
 <script setup lang="ts">
 import api from '@/api';
-import DialogInsuranceEdit from '@/views/insurancePolicy/modules/DialogInsuranceEdit.vue';
-import DialogInsuranceCancel from '@/views/insurancePolicy/modules/DialogInsuranceCancel.vue';
+import DialogInsuranceEdit from '@/views/orderBatchAdjust/DialogInsuranceEdit.vue';
+import DialogInsuranceCancel from '@/views/orderBatchAdjust/DialogInsuranceCancel.vue';
 import { useRouter } from 'vue-router';
 import { dictData } from '@/views/localDict/index';
 import { ElMessage, ElMessageBox } from "element-plus";
@@ -535,11 +535,12 @@ const initCompany = () => {
 const onClick = (type: any, row: any) => {
   api.insurancePolicyApi.adjustAdd({
     type: type,
-    orderNo: row.orderNo,
+    orderNo: row.id,
   }).then((res: any) => {
     if(res.code == 200) {
       formData.value = {
         ...row,
+        orderNo: row.id,
         adjustId: res.data.id,
         tableList: type === 1 ? res.data.changeInfoVosList : res.data.infoVosList
       };

+ 246 - 0
src/views/insurancePolicy/adjustRecord.vue

@@ -0,0 +1,246 @@
+<script setup lang="ts">
+import api from '@/api';
+import { useRoute, useRouter } from 'vue-router';
+import { dictData } from '@/views/localDict/index';
+import { loadDicts, getDict } from '@/utils/composables/dictService';
+import { downloadByData } from '@/utils/composables/download';
+
+interface PageInfo {
+  pageNum: number;
+  pageSize: number;
+  sizes: number[];
+  total: number;
+}
+// router
+const route = useRoute();
+const router = useRouter();
+// state
+const typeData = dictData.ADJUST_TYPEDATA;
+const statusData = dictData.ADJUST_STATUSDATA;
+const companyData = ref([]);
+// table
+const tableData = ref([]);
+const pageInfo = ref<PageInfo>({
+	pageNum: 1,
+	pageSize: 10,
+	sizes: [10, 20, 30, 40, 50],
+	total: 0,
+});
+const columns: any = [
+	{
+		prop: "orderNo", label: "订单号", width: 200,
+	},
+	{ 
+		prop: "licenseNo", label: "车牌号", width: 100,
+	},
+	{ 
+		prop: "companyName", label: "保险公司", width: 200,
+	},
+	{
+		prop: "premium", label: "保费", width: 150,
+		formatter: (row: any) => {
+			return '¥' + row.premium
+		}
+	},
+	{
+		prop: "status", label: "批单状态", width: 150, component: "Tag",
+		formatter: (row: any) => {
+			// let name = getDict('is_enable').find(val => val.value == 1).label;
+			// 批单状态 0.订单批改中 1.待审核 2.审核通过 3.退回修改 4.拒绝批改
+			let name = statusData.find(val => val.value == row.status)?.label;
+			let type = 'primary';
+			if (row.status === 4) {
+				type = 'danger';
+			} else if (row.status === 3) {
+				type = 'warning';
+			}
+			return { type: type, text: name };
+		}
+	},
+	{ 
+		prop: "type", label: "批改类型", width: 150,
+		formatter: (row: any) => {
+			let name = typeData.find(val => val.value == row.type)?.label;
+			return name;
+		}
+	},
+	{ 
+		prop: "changePremium", label: "保费变化", width: 150,
+		formatter: (row: any) => {
+			if (!row.afterPremium) {
+				return '--';
+			}
+			let val = (((row.premium * 100) - (row.afterPremium * 100)) / 100);
+			return val > 0 ? `-¥${val}` : `¥${Math.abs(val)}`;
+		}
+	},
+	{ 
+		prop: "afterPremium", label: "批改后保费", width: 150,
+		formatter: (row: any) => {
+			return row.afterPremium ? `¥${row.afterPremium}` : '--';
+		}
+	},
+	{ 
+		prop: "createBy", label: "批改人", width: 150,
+	},
+	{ 
+		prop: "createTime", label: "批改时间", width: 200,
+	},
+];
+// search
+const fields = computed(() => {
+	return [
+		{
+			label: "订单号", model: "orderNo", type: "input", placeholder: "输入订单号查找",
+		},
+		{
+			label: "车牌号", model: "licenseNo", type: "input", placeholder: "输入车牌号查找",
+		},
+		{
+			label: "保险公司", model: "companyId", type: "select", filterable: true, options: companyData.value,
+		},
+		{
+			label: "业务员信息", model: "salesman", type: "input", placeholder: "输入业务员姓名、手机号查找",
+		},
+		{
+			label: "录入类型", model: "entryStatus", type: "select", closeable: false, options: dictData.ADJUST_ENTRYSTATUSDATA,
+		},
+		{
+			label: "批改状态", model: "status", type: "select", options: statusData,
+		},
+		{
+			label: "批改类型", model: "type", type: "select", options: typeData,
+		},
+		{
+			label: "批改时间", model: "time", type: "date", dateType: "datetimerange", startplaceholder: "开始时间", endplaceholder: "结束时间",
+			onChange: (value: any) => {
+				if (value) {
+					formInline.value.endDate = value[1];
+					formInline.value.startDate = value[0];
+				} else {
+					formInline.value.endDate = '';
+					formInline.value.startDate = '';
+				}
+			}
+		},
+	]
+});
+const initialForm = {
+	time: [],
+	endDate: '',
+	startDate: '',
+	entryStatus: 1
+};
+const formInline = ref<any>({ ...initialForm });
+
+// 生命周期
+onBeforeMount(async () => {
+	// 路由参数
+	if (route.query.orderNo) formInline.value.orderNo = route.query.orderNo;
+	// 获取多个字典
+	// await loadDicts(['valid_status', 'is_enable', 'audit_status', 'insurance_type']);
+	api.insuranceApi.leftList('').then((res: any) => {
+        if(res.code == 200) {
+            companyData.value = res.data.map((item: any) => ({
+				label: item.name,
+				value: item.id
+			}));
+        }
+    });
+	// table数据
+	getData();
+});
+
+// 分页数据
+const getData = () => {
+	const { time, ...params } = formInline.value;
+	api.insurancePolicyApi.adjustList({ 
+		pages: pageInfo.value.pageNum, 
+		size: pageInfo.value.pageSize, 
+		...params
+	}).then((res: any) => {
+		if (res.code == '200') {
+			tableData.value = res.data.records;
+			// 分页
+			pageInfo.value.total = res.data.total;
+			pageInfo.value.pageNum = res.data.current;
+			pageInfo.value.pageSize = res.data.size;
+		}
+	});
+};
+// 搜索事件
+const searchForm = () => {
+	getData();
+}
+const resetForm = () => {
+	formInline.value = { ...initialForm };
+	getData();
+}
+// 分页事件
+const getList = (item: PageInfo) => {
+	pageInfo.value.pageNum = item.pageNum;
+	pageInfo.value.pageSize = item.pageSize;
+	getData();
+};
+// 多选事件
+const selectionChange = (val: any) => {
+	console.log('sel', val)
+};
+// 导出事件
+const exportLoading = ref(false);
+const handleExport = () => {
+	exportLoading.value = true;
+	const { time, ...params } = formInline.value;
+    api.insurancePolicyApi.exportAdjustList(params).then((res: any) => {
+        const fileName = '批改明细' + new Date().getTime() + '.xlsx';
+		downloadByData(res, fileName, 'application/vnd.ms-excel');
+    }).finally(() => {
+		exportLoading.value = false;
+	});
+}
+// 详情
+const toDetail = (e: any) => {
+    router.push({
+        path: '/insurancePolicy/adjustRecord/detail',
+        query: {
+            id: e.operationData.id,
+			orderNo: e.operationData.orderNo,
+        }
+    });
+	// window.open(`/#/insurancePolicy/adjustRecord/detail?id=${e.operationData.id}&orderNo=${e.operationData.orderNo}`, '_blank');
+}
+
+defineOptions({
+	name: 'InsurancePolicyRecord',
+})
+</script>
+
+<template>
+	<PageMain class="vh100">
+		<SearchForm :isOffset="true"
+			:fields="fields" :formInline="formInline" 
+			:onSearch="searchForm" :onReset="resetForm" 
+		/>
+		<ComplexTable 
+			:columns="columns" :columnwidth="200" 
+			:showIndex="false" :showSelect="true"
+			:pageInfo="pageInfo" 
+			:tableData="tableData" 
+			@getList="getList" 
+			@selectionChange="selectionChange">
+			<template v-slot:table-title-btn>
+				<el-button class="radius-2px width-100" type="primary" :loading="exportLoading" @click="handleExport">导出</el-button>
+			</template>
+			<template v-slot:operation="scope">
+				<el-button type="primary" link 
+					@click="toDetail(scope)">
+					详情
+				</el-button>
+			</template>
+		</ComplexTable>
+	</PageMain>
+</template>
+
+<style lang="scss" scoped>
+/* 样式 */
+</style>

+ 6 - 6
src/views/insurancePolicy/order.vue

@@ -153,8 +153,8 @@
 
 <script setup lang="ts">
 import api from '@/api';
-import DialogInsuranceEdit from './modules/DialogInsuranceEdit.vue';
-import DialogInsuranceCancel from './modules/DialogInsuranceCancel.vue';
+import DialogInsuranceEdit from '@/views/orderBatchAdjust/DialogInsuranceEdit.vue';
+import DialogInsuranceCancel from '@/views/orderBatchAdjust/DialogInsuranceCancel.vue';
 import { dictData } from '@/views/localDict/index';
 import { ElMessage } from "element-plus";
 import { useRouter } from 'vue-router';
@@ -226,7 +226,7 @@ const handleCancel = () => {
   initData()
 }
 // 详情
-const toDetail = (orderNo) => {
+const toDetail = (orderNo: any) => {
     router.push({
         path: '/insurancePolicy/order/insurancePolicyOrderDetail',
         query: {
@@ -236,7 +236,7 @@ const toDetail = (orderNo) => {
 }
 
 // 编辑
-const toEdit = (row) => {
+const toEdit = (row: any) => {
     router.push({
         name: 'InsurancePolicy',
         query: {
@@ -248,7 +248,7 @@ const toEdit = (row) => {
 // 支付码
 let payCodeShow = ref(false)
 let payCodeUrl = ref('')
-const payCode = (scope) => {
+const payCode = (scope: any) => {
     payCodeShow.value = true
     api.insurancePolicyApi.getQrCode(scope.row.orderNo).then((res: any) => {
         if(res.code == 200) {
@@ -263,7 +263,7 @@ const payCode = (scope) => {
     })
 }
 
-const uploadStatus = (row) => {
+const uploadStatus = (row: any) => {
   api.insurancePolicyApi.getOrderStatus({
       order:{ordersNo: row.orderNo}
   }).then((res: any) => {

+ 2 - 2
src/views/insurancePolicy/orderDetail.vue

@@ -1033,8 +1033,8 @@ import { useRoute, useRouter } from 'vue-router'
 import { ElMessage } from "element-plus";
 
 import QrcodeVue from 'qrcode.vue';
-import DialogInsuranceEdit from './modules/DialogInsuranceEdit.vue';
-import DialogInsuranceCancel from './modules/DialogInsuranceCancel.vue';
+import DialogInsuranceEdit from '@/views/orderBatchAdjust/DialogInsuranceEdit.vue';
+import DialogInsuranceCancel from '@/views/orderBatchAdjust/DialogInsuranceCancel.vue';
 
 const myRoute = useRoute()
 const myRouter = useRouter()

+ 14 - 0
src/views/localDict/index.ts

@@ -31,4 +31,18 @@ export const dictData = {
             value: 4,
         },
     ],
+    ADJUST_ENTRYSTATUSDATA: [
+        {
+            label: '车险录入',
+            value: 1,
+        },
+        {
+            label: '待客录单',
+            value: 2,
+        },
+        // {
+        //     label: '外部订单',
+        //     value: 3,
+        // },
+    ],
 };

+ 2 - 1
src/views/operationManagement/modules/allocation.vue

@@ -53,7 +53,8 @@ let userList = ref([])
 let userListSelect = ref([])
 const initUser = (id) => {
   api.operationApi.getUser({
-      deptId: id
+      deptId: id,
+      typeAttr: 2   // 新增参数 指定为成员后线
   }).then(res => {
     if(res.code == 200) {
         userList.value = res.data

+ 0 - 0
src/views/insurancePolicy/modules/DialogInsuranceCancel.vue → src/views/orderBatchAdjust/DialogInsuranceCancel.vue


+ 1 - 1
src/views/insurancePolicy/modules/DialogInsuranceEdit.vue → src/views/orderBatchAdjust/DialogInsuranceEdit.vue

@@ -284,7 +284,7 @@ watch(() => props.data.tableList, (val) => {
 			// 批增数据被返回时会带着数据,所以清空
 			type: '',
 			// 投保数据被返回时是null,所以修改
-			amount: !item.amount ? '投保' : item.amount,
+			amount: !item.amount || item.amount == 1 ? '投保' : item.amount,
 			// state: 0 退保 
 			state: item.kindType === 0 || (item.kindCode === 'A' || item.kindCode === 'SY_WBDWGZ') ? 0 : 1
 		}

+ 2 - 2
src/views/insurancePolicy/recordDetail.vue → src/views/orderBatchAdjust/recordDetail.vue

@@ -1,7 +1,7 @@
 <script setup lang="ts">
 import api from '@/api';
-import DialogInsuranceEdit from './modules/DialogInsuranceEdit.vue';
-import DialogInsuranceCancel from './modules/DialogInsuranceCancel.vue';
+import DialogInsuranceEdit from './DialogInsuranceEdit.vue';
+import DialogInsuranceCancel from './DialogInsuranceCancel.vue';
 import { dictData } from '@/views/localDict/index';
 import { useRoute, useRouter } from 'vue-router';
 import { ElMessage } from 'element-plus';

+ 666 - 0
src/views/quoteConfig/dispatch/components/other.vue

@@ -0,0 +1,666 @@
+<template>
+  <el-dialog
+    v-model="dialogVisible"
+    title="其他情况"
+    width="704px"
+    :before-close="handleClose"
+    align-center
+  >
+    <div class="dialog-content">
+      <div class="left">
+        <div class="description-text">
+          如存在不满足其它规则的情况下,则进入该情况,请选择该情况下要执行的动作
+        </div>
+
+        <div class="section-title">将执行以下动作</div>
+
+        <el-form
+          ref="formRef"
+          :model="formData"
+          label-width="auto"
+          class="action-form"
+        >
+          <el-form-item
+            v-for="(item, index) in formData.actions"
+            :key="item.keyid"
+            :prop="`actions.${index}`"
+            :error="item.actionJson.duplicateError"
+          >
+            <el-select
+              v-model="item.actionCode"
+              placeholder="请选择动作"
+              style="width: 276px; margin-right: 9px"
+              @change="(val) => handleActionChange(val, index)"
+              @blur="() => checkDuplicate(index)"
+            >
+              <el-option
+                v-for="dictItem in getAvailableActions(index)"
+                :key="dictItem.value"
+                :value="dictItem.value"
+                :label="dictItem.label"
+              />
+            </el-select>
+            <img
+              src="@/assets/images/delete.png"
+              alt="删除"
+              style="width: 18px; height: 18px; cursor: pointer"
+              @click="deleteItem(index)"
+              v-if="formData.actions.length > 1"
+            />
+          </el-form-item>
+        </el-form>
+
+        <el-button type="text" class="add-action-btn" @click="addItem">
+          <el-icon><Plus /></el-icon>添加动作
+        </el-button>
+      </div>
+
+      <div class="right">
+        <div class="right-section">
+          <div class="right-title">执行动作</div>
+          <div class="action-label" v-if="lastActionLabel">
+            {{ lastActionLabel }}
+          </div>
+        </div>
+        <!-- 指定协议 -->
+        <div
+          v-if="
+            formData.actions.length > 0 &&
+            lastActionLabel === '报价失败指定协议'
+          "
+          style="margin-top: 20px"
+        >
+          <div
+            style="
+              font-weight: 600;
+              font-size: 16px;
+              margin-bottom: 4px;
+              color: #333333;
+            "
+          >
+            选择报价协议
+          </div>
+          <div style="font-size: 14px; color: #666666; margin-bottom: 12px">
+            不满足所有规则的车辆报价,将使用该协议报价
+          </div>
+          <el-select
+            v-model="
+              formData.actions[formData.actions.length - 1].actionJson
+                .agreementId
+            "
+            placeholder=""
+            style="width: 304px"
+            :rules="[
+              {
+                required: lastActionLabel === '报价失败指定协议',
+                message: '请选择报价协议',
+                trigger: 'blur',
+              },
+            ]"
+          >
+            <el-option
+              v-for="item in agreementList"
+              :key="item.id"
+              :label="item.agreementName"
+              :value="item.id"
+            />
+          </el-select>
+        </div>
+        <!-- 终止报价 -->
+        <div
+          v-if="formData.actions.length > 0 && lastActionLabel === '终止报价'"
+          style="margin-top: 20px"
+        >
+          <div
+            style="
+              font-weight: 600;
+              font-size: 16px;
+              margin-bottom: 4px;
+              color: #333333;
+            "
+          >
+            提示文案
+          </div>
+          <div style="font-size: 14px; color: #666666; margin-bottom: 12px">
+            设置终止该车辆报价的提示文案
+          </div>
+          <el-input
+            v-model="
+              formData.actions[formData.actions.length - 1].actionJson.message
+            "
+            style="width: 100%"
+            :rows="4"
+            type="textarea"
+            placeholder="请输入提示文案"
+            maxlength="500"
+            show-word-limit
+          />
+        </div>
+        <!-- 补录订单 -->
+        <div
+          v-if="formData.actions.length > 0 && lastActionLabel === '补录订单'"
+        >
+          <div style="font-size: 14px; color: #666666; margin-top: 10px">
+            终止报价,将报价信息写入补录订单内,生成一笔暂存的补录订单。
+          </div>
+        </div>
+        <!-- 发送通知消息 -->
+        <div
+          v-if="
+            formData.actions.length > 0 && lastActionLabel === '发送通知消息'
+          "
+          style="margin-top: 20px"
+        >
+          <div
+            style="
+              font-weight: 600;
+              font-size: 16px;
+              margin-bottom: 4px;
+              color: #333333;
+            "
+          >
+            接收人
+          </div>
+          <div style="font-size: 14px; color: #666666; margin-bottom: 12px">
+            发送通知消息给接收人
+          </div>
+          <el-radio-group
+            v-model="
+              formData.actions[formData.actions.length - 1].actionJson.type
+            "
+            @change="handleTypeChange"
+            style="
+              display: flex;
+              flex-direction: column;
+              align-items: start;
+              gap: 12px;
+            "
+          >
+            <el-radio :value="1">业务员的管理人</el-radio>
+            <el-radio :value="2">
+              指定人员
+              <el-select
+                v-model="
+                  formData.actions[formData.actions.length - 1].actionJson
+                    .user_id
+                "
+                placeholder="请选择人员"
+                style="width: 204px; margin-left: 20px"
+                clearable
+                :disabled="
+                  formData.actions[formData.actions.length - 1].actionJson
+                    .type !== 2
+                "
+              >
+                <el-option
+                  v-for="item in followerList"
+                  :key="item.id"
+                  :label="item.name"
+                  :value="item.id"
+                />
+              </el-select>
+            </el-radio>
+          </el-radio-group>
+          <div style="font-size: 14px; margin: 20px 0px 10px 0px">标题</div>
+          <div>
+            <el-input
+              v-model="
+                formData.actions[formData.actions.length - 1].actionJson.title
+              "
+              style="width: 100%"
+              placeholder="请输入标题"
+              maxlength="50"
+              show-word-limit
+            />
+          </div>
+          <div style="font-size: 14px; margin: 20px 0px 10px 0px">内容</div>
+          <el-input
+            v-model="
+              formData.actions[formData.actions.length - 1].actionJson.content
+            "
+            style="width: 100%"
+            :rows="4"
+            type="textarea"
+            placeholder="请输入通知内容"
+            maxlength="500"
+            show-word-limit
+          />
+        </div>
+      </div>
+    </div>
+
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="(dialogVisible = false), emit('close')"
+          >取消</el-button
+        >
+        <el-button type="primary" @click="handleSave">保存</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, ElRef, watch, computed, defineProps } from "vue";
+import { ElMessageBox, ElForm, ElMessage } from "element-plus";
+import { Plus } from "@element-plus/icons-vue";
+import api from "@/api";
+import { getDictItems } from "@/api/dict";
+
+// 对话框显示状态
+const dialogVisible = ref(true);
+//协议列表,调度id
+const props = defineProps<{
+  companyId: any;
+}>();
+// 表单引用
+const formRef = ref<ElRef<ElForm>>();
+const agreementList = ref([]); //协议列表
+
+// 表单数据:actions数组存储所有动作,每个动作包含完整字段
+const formData = ref({
+  actions: [
+    {
+      companyId: props.companyId,
+      keyid: generateId(),
+      actionCode: "",
+      actionJson: {
+        agreementId: "", // 动作ID(关联字典)
+        message: "", // 终止报价的提示文案
+        type: null, // 通知接收人类型(1=业务员管理人,2=指定人员)
+        content: "", // 通知内容
+        user_id: null, // 指定人员ID
+        title: "",
+        duplicateError: "", // 重复选择错误提示
+      },
+    },
+  ],
+});
+
+// 字典数据(动作列表,如“终止报价”“发送通知消息”等)
+const dictionaries = ref([]);
+// 从 dictionaries 数据中获取“指定协议”“终止报价”对应的 id
+const MUTUALLY_EXCLUSIVE_ACTIONS = {
+  agreement: "414", // 假设“指定协议”的字典ID
+  terminate: "415", // 假设“终止报价”的字典ID
+};
+
+// 最后一项动作的标签(用于右侧配置区显示)
+const lastActionLabel = ref("");
+
+// 后线人员列表(用于“指定人员”选择)
+const followerList = ref([]);
+
+/**
+ * 生成唯一ID(避免列表key重复)
+ * @returns 唯一ID字符串
+ */
+function generateId(): string {
+  return Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
+}
+
+/**
+ * 计算属性:根据当前已选动作,生成“可选动作列表”(排除其他行已选,保留当前行)
+ */
+const getAvailableActions = computed(() => {
+  return (currentIndex: number) => {
+    // 收集其他行已选的动作值(dictItem.value)
+    const selectedValues = formData.value.actions
+      .filter((_, idx) => idx !== currentIndex)
+      .map((item) => item.actionCode)
+      .filter(Boolean);
+
+    // 筛选字典:排除已被其他行选择的动作(用value字段判断)
+    return dictionaries.value.filter((dictItem) => !selectedValues.includes(dictItem.value));
+  };
+});
+
+/**
+ * 更新最后一项动作的标签(右侧配置区基于此显示对应内容)
+ */
+function updateLastAction() {
+  if (formData.value.actions.length === 0) return;
+
+  // 获取最后一个动作的选中值(即选择框绑定的dictItem.value)
+  const lastAction = formData.value.actions[formData.value.actions.length - 1];
+  
+  // 关键修复:用字典项的value字段匹配actionCode
+  const matchedAction = dictionaries.value.find(
+    (dictItem: any) => dictItem.value === lastAction.actionCode
+  );
+  
+  lastActionLabel.value = matchedAction ? matchedAction.label : "";
+
+  // 重置无关字段(保持不变)
+  if (lastActionLabel.value !== "发送通知消息") {
+    lastAction.actionJson.type = null;
+    lastAction.actionJson.user_id = null;
+    lastAction.actionJson.content = "";
+    lastAction.actionJson.title = "";
+  }
+}
+
+/**
+ * 校验单个动作是否重复(并标记错误)
+ * @param index 当前动作行索引
+ */
+const checkDuplicate = (index: number) => {
+  const currentAction = formData.value.actions[index];
+  if (!currentAction.actionCode) {
+    currentAction.actionJson.duplicateError = "";
+    return;
+  }
+
+  // 统计当前动作值(value)的出现次数
+  const duplicateCount = formData.value.actions.filter(
+    (item) => item.actionCode === currentAction.actionCode
+  ).length;
+
+  if (duplicateCount > 1) {
+    // 用value字段查找动作标签
+    const actionLabel = dictionaries.value.find(
+      (dictItem) => dictItem.value === currentAction.actionCode
+    )?.label;
+    currentAction.actionJson.duplicateError = `“${actionLabel}”已选择,请更换其他动作`;
+  } else {
+    currentAction.actionJson.duplicateError = "";
+  }
+};
+
+/**
+ * 处理动作选择变化(立即校验重复)
+ * @param val 当前选择的动作ID
+ * @param index 当前动作行索引
+ */
+const handleActionChange = (val: string, index: number) => {
+  const { agreement, terminate } = MUTUALLY_EXCLUSIVE_ACTIONS;
+  // 用选择框的value(val)判断是否为互斥动作
+  const isMutualAction = [agreement, terminate].includes(val);
+  
+  if (isMutualAction) {
+    const otherMutualAction = val === agreement ? terminate : agreement;
+    // 检查其他行是否包含互斥动作的value
+    const hasOtherMutual = formData.value.actions.some(
+      (item, idx) => idx !== index && item.actionCode === otherMutualAction
+    );
+
+    if (hasOtherMutual) {
+      ElMessage.error("指定报价协议 与 终止报价,只能选择其中1个");
+      formData.value.actions[index].actionCode = "";
+      formData.value.actions[index].actionJson.duplicateError = "";
+      return;
+    }
+  }
+
+  checkDuplicate(index);
+  formData.value.actions.forEach((_, idx) => idx !== index && checkDuplicate(idx));
+  updateLastAction();
+};
+
+/**
+ * 批量校验所有动作是否重复(用于保存前最终校验)
+ * @returns boolean 无重复返回true,有重复返回false
+ */
+const checkAllDuplicate = () => {
+  let hasDuplicate = false;
+  // 1. 收集所有已选动作ID
+  const selectedIds = formData.value.actions
+    .map((item) => item.actionCode)
+    .filter(Boolean);
+  // 2. 检查是否有重复ID
+  const uniqueIds = [...new Set(selectedIds)];
+  if (selectedIds.length !== uniqueIds.length) {
+    hasDuplicate = true;
+    // 3. 标记所有重复行的错误提示
+    formData.value.actions.forEach((item, index) => {
+      if (
+        item.actionCode &&
+        selectedIds.filter((id) => id === item.actionCode).length > 1
+      ) {
+        checkDuplicate(index); // 复用单个校验逻辑,标记错误
+      }
+    });
+    // 4. 弹出全局提示,引导用户修改
+    ElMessage.warning("存在重复选择的动作,请先修改后再保存");
+  }
+  return !hasDuplicate;
+};
+
+/**
+ * 添加新的动作行
+ */
+const addItem = () => {
+  formData.value.actions.push({
+    keyid: generateId(),
+    actionCode: "",
+    companyId: props.companyId,
+    actionJson: {
+      agreementId: "",
+      message: "",
+      type: null,
+      content: "",
+      user_id: null,
+      title: "",
+      duplicateError: "",
+    },
+  });
+  // 添加后更新最后一项动作的显示
+  updateLastAction();
+};
+
+/**
+ * 删除指定索引的动作行(至少保留1行)
+ * @param index 要删除的行索引
+ */
+const deleteItem = (index: number) => {
+  if (formData.value.actions.length <= 1) {
+    ElMessageBox.alert("至少需要保留一个动作", "提示", {
+      confirmButtonText: "确定",
+    });
+    return;
+  }
+
+  // 1. 记录被删除行的动作ID(用于后续校验)
+  const deletedActionId = formData.value.actions[index].actionCode;
+  // 2. 删除指定行
+  formData.value.actions.splice(index, 1);
+  // 3. 重新校验剩余行:若删除的是重复ID,清除其他行的重复标记
+  if (deletedActionId) {
+    formData.value.actions.forEach((_, idx) => checkDuplicate(idx));
+  }
+  // 原有逻辑:更新右侧配置区
+  updateLastAction();
+};
+
+/**
+ * 处理通知接收人类型变化:选择1时清空指定人员
+ * @param value 接收人类型(1=业务员管理人,2=指定人员)
+ */
+const handleTypeChange = (value: number) => {
+  const lastAction = formData.value.actions[formData.value.actions.length - 1];
+  // 选择“业务员的管理人”(value=1)时,清空指定人员选择
+  if (value === 1) {
+    lastAction.actionJson.user_id = null;
+  }
+};
+
+/**
+ * 处理对话框关闭(未保存时提示)
+ * @param done 关闭回调函数
+ */
+const handleClose = (done: () => void) => {
+  emit("close");
+};
+
+/**
+ * 获取后线人员列表(接口请求)
+ */
+const getUserList = async () => {
+  try {
+    const res = await api.dispatchApi.getUserList({ isMember: "1" });
+    return res?.data || []; // 按实际接口结构调整数据路径
+  } catch (error) {
+    console.error("获取后线人员列表失败:", error);
+    return [];
+  }
+};
+
+// 获取报价协议列表
+const getByCompanyId = async () => {
+  try {
+    const res = await api.dispatchApi.getByCompanyIdt(props.companyId);
+    agreementList.value = res.data;
+  } catch (error) {
+    console.error("获取报价协议列表失败:", error);
+  }
+};
+
+/**
+ * 处理保存操作(含完整验证逻辑)
+ */
+const handleSave = async () => {
+
+  // 校验报价协议是否选择
+  const hasEmptyAgreement = formData.value.actions.some((item) => {
+    const matchedAction = dictionaries.value.find(
+      (dictItem) => dictItem.id === item.actionCode
+    );
+    return (
+      matchedAction?.label === "报价失败指定协议" && !item.actionJson.agreementId
+    );
+  });
+  if (hasEmptyAgreement) {
+    ElMessage.error("请选择报价协议");
+    return;
+  }
+
+  // 校验所有动作是否重复
+  if (!checkAllDuplicate()) {
+    return;
+  }
+
+  // 校验通过,执行保存逻辑
+  try {
+    const saveRes = await api.dispatchApi.addAgreementDispatchAction(
+      formData.value.actions
+    );
+    if (saveRes.code === 200) {
+      ElMessage.success("保存成功");
+      emit("close");
+    }
+  } catch (error) {
+    console.error("保存失败:", error);
+    ElMessage.error("保存失败,请稍后重试");
+  }
+};
+
+const emit = defineEmits(["close"]);
+
+/**
+ * 组件挂载时加载初始化数据(字典+人员列表)
+ */
+onMounted(async () => {
+  const [dictData, userList] = await Promise.all([
+    getDictItems({ dictCode: "dispatch_after_action" }), // 动作字典
+    getUserList(), // 后线人员列表
+  ]);
+  dictionaries.value = dictData || [];
+  followerList.value = userList;
+  // formData.value.companyId = props.companyId;
+  console.log( props.companyId);
+});
+
+/**
+ * 监听动作列表变化,自动更新最后一项动作的显示
+ */
+watch(
+  () => formData.value.actions,
+  () => updateLastAction(),
+  { deep: true }
+);
+</script>
+
+<style lang="scss" scoped>
+.dialog-content {
+  display: flex;
+  width: 100%;
+  gap: 24px;
+}
+
+.left {
+  width: 50%;
+  border-right: 1px solid #eeeeee;
+  padding-right: 8px;
+}
+
+.right {
+  width: 50%;
+  padding-left: 8px;
+}
+
+.right-section {
+  width: 100%;
+}
+
+.right-title {
+  font-size: 16px;
+  color: #333;
+  font-weight: 600;
+  margin-bottom: 10px;
+}
+
+.action-label {
+  font-size: 14px;
+  color: #666;
+  width: 100%;
+  border-radius: 4px;
+  border: 1px solid #e5e5e5;
+  height: 36px;
+  line-height: 36px;
+  padding: 0 12px;
+  margin-top: 10px;
+  background-color: #fff;
+}
+
+.description-text {
+  font-size: 14px;
+  color: #666666;
+  line-height: 1.5;
+  margin-bottom: 20px;
+}
+
+.section-title {
+  font-size: 16px;
+  margin-top: 8px;
+  margin-bottom: 16px;
+  color: #333;
+  font-weight: 500;
+}
+
+.action-form {
+  margin-bottom: 16px;
+}
+
+.add-action-btn {
+  margin-top: 8px;
+  color: #409eff;
+  &:hover {
+    color: #66b1ff;
+  }
+}
+
+::v-deep .el-radio {
+  display: flex;
+  align-items: center;
+}
+
+::v-deep .el-form-item {
+  margin-bottom: 12px;
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+}
+</style>

+ 1137 - 0
src/views/quoteConfig/dispatch/components/rule.vue

@@ -0,0 +1,1137 @@
+<template>
+  <div>
+    <el-drawer
+      v-model="drawer"
+      :title="props.drawerTitle"
+      size="918"
+      @close="handleClose"
+    >
+      <div class="item-title"><span></span> 当报价信息满足以下条件时</div>
+      <el-form
+        ref="formRef"
+        :model="formData"
+        label-width="auto"
+        class="demo-dynamic"
+        :rules="formRules"
+      >
+        <!-- 多组条件容器(组之间是"或者"关系) -->
+        <div class="condition-container">
+          <div
+            v-for="(group, groupIndex) in formData.conditionGroups"
+            :key="group.groupKey"
+            class="condition-group"
+          >
+            <!-- 组内条件行(行之间是"并且"关系) -->
+            <el-form-item
+              v-for="(domain, rowIndex) in group.actions"
+              :key="domain.key"
+              :prop="`conditionGroups.${groupIndex}.actions.${rowIndex}`"
+              :error="domain.errorMsg"
+            >
+              <div class="condition-row">
+                <!-- 1. 条件类型选择框 -->
+                <el-select
+                  v-model="domain.attrCode"
+                  placeholder="选择条件"
+                  style="width: 341px"
+                  @change="handleConditionChange(domain)"
+                  clearable
+                >
+                  <el-option
+                    v-for="item in dispatchList"
+                    :key="item.attrCode"
+                    :label="item.attrName"
+                    :value="item.attrCode"
+                  />
+                </el-select>
+
+                <!-- 2. 运算符选择框 -->
+                <el-select
+                  v-model="domain.operator"
+                  placeholder="选择运算符"
+                  style="width: 100px"
+                  :disabled="!domain.attrCode"
+                  clearable
+                  @change="handleOperatorChange(domain)"
+                >
+                  <el-option
+                    v-for="item in getOperations(domain.attrCode)"
+                    :key="item.value"
+                    :label="item.label"
+                    :value="item.value"
+                  />
+                </el-select>
+
+                <!-- 3. 值输入区域(动态切换) -->
+                <!-- 单选按钮组 -->
+                <el-radio-group
+                  v-model="domain.min"
+                  v-if="getConditionType(domain.attrCode) === 'radio'"
+                >
+                  <el-radio
+                    :value="item.id+''"
+                    size="large"
+                    v-for="item in getOptions(domain.attrCode)"
+                    :key="item.id+''"
+                    >{{ item.name }}</el-radio
+                  >
+                </el-radio-group>
+
+                <!-- 多选下拉 -->
+                <el-select
+                  v-model="domain.minArray"
+                  placeholder="选择值"
+                  multiple
+                  style="width: 341px"
+                  v-if="getConditionType(domain.attrCode) === 'checkbox'"
+                  :disabled="!domain.operator"
+                  clearable
+                >
+                  <el-option
+                    v-for="item in getOptions(domain.attrCode)"
+                    :key="item.id"
+                    :label="item.name"
+                    :value="item.id"
+                  />
+                </el-select>
+
+                <!-- 单选下拉 -->
+                <el-select
+                  v-model="domain.max"
+                  placeholder="选择值"
+                  style="width: 341px"
+                  v-if="getConditionType(domain.attrCode) === 'select'"
+                  :disabled="!domain.operator"
+                  clearable
+                >
+                  <el-option
+                    v-for="item in getOptions(domain.attrCode)"
+                    :key="item.code"
+                    :label="item.name"
+                    :value="item.code"
+                  />
+                </el-select>
+
+                <!-- 数值输入 -->
+                <el-input
+                  v-model="domain.min"
+                  placeholder="输入数值"
+                  type="number"
+                  style="width: 341px"
+                  v-if="
+                    getConditionType(domain.attrCode) === 'inputNumber' &&
+                    domain.operator !== '1'
+                  "
+                  :disabled="!domain.operator"
+                  clearable
+                />
+                <!-- 数值输入范围 -->
+                <div
+                  v-if="
+                    getConditionType(domain.attrCode) === 'inputNumber' &&
+                    domain.operator == '1'
+                  "
+                >
+                  <el-input-number
+                    v-model="domain.min"
+                    :disabled="!domain.operator"
+                    style="width: 160px"
+                  ></el-input-number>
+                  至
+                  <el-input-number
+                    v-model="domain.max"
+                    :disabled="!domain.operator"
+                    style="width: 170px"
+                  ></el-input-number>
+                </div>
+
+                <!-- 标签输入 -->
+                <el-input-tag
+                  v-model="domain.minArray"
+                  style="width: 341px"
+                  v-if="getConditionType(domain.attrCode) === 'inputTag'"
+                  :disabled="!domain.operator"
+                  placeholder="输入内容后按回车键隔开"
+                />
+
+                <!-- 日期选择 -->
+                <el-date-picker
+                  v-model="domain.max"
+                  type="date"
+                  placeholder="选择日期"
+                  style="width: 341px"
+                  v-if="
+                    getConditionType(domain.attrCode) === 'datetime' &&
+                    domain.operator !== '1'
+                  "
+                  :disabled="!domain.operator"
+                  clearable
+                />
+
+                <!-- 日期范围选择 -->
+                <div
+                  v-if="
+                    getConditionType(domain.attrCode) === 'datetime' &&
+                    domain.operator == '1'
+                  "
+                >
+                  <el-date-picker
+                    v-model="domain.min"
+                    :disabled="!domain.operator"
+                    type="datetime"
+                    format="YYYY-MM-DD HH:mm:ss"
+                    value-format="YYYY-MM-DD HH:mm:ss"
+                    time-format="HH:mm:ss"
+                    placeholder="选择日期时间"
+                    style="width: 170px"
+                  >
+                  </el-date-picker>
+                  至
+                  <el-date-picker
+                    v-model="domain.max"
+                    :disabled="!domain.operator"
+                    type="datetime"
+                    format="YYYY-MM-DD HH:mm:ss"
+                    value-format="YYYY-MM-DD HH:mm:ss"
+                    time-format="HH:mm:ss"
+                    placeholder="选择日期时间"
+                    style="width: 170px"
+                  >
+                  </el-date-picker>
+                </div>
+
+                <!-- 车牌级联选择 -->
+                <el-cascader
+                  :options="getOptions(domain.attrCode)"
+                  :props="cascaderProps"
+                  v-model="domain.max"
+                  placeholder="选择车牌"
+                  clearable
+                  v-if="getConditionType(domain.attrCode) === 'plate'"
+                  style="width: 341px"
+                  :disabled="!domain.operator"
+                />
+
+                <!-- 4. 删除行按钮 -->
+                <el-button
+                  type="text"
+                  :icon="Delete"
+                  size="small"
+                  style="color: #333333"
+                  @click="handleRemoveRow(groupIndex, rowIndex)"
+                  v-if="group.actions.length > 1"
+                />
+              </div>
+            </el-form-item>
+
+            <!-- 组内添加并且条件按钮 -->
+            <el-button
+              type="text"
+              @click="handleAddRow(groupIndex)"
+              class="add-condition-btn"
+            >
+              + 并且条件
+            </el-button>
+          </div>
+        </div>
+
+        <!-- 添加或者条件组按钮 -->
+        <el-form-item class="add-group-btn">
+          <el-button type="primary" @click="handleAddGroup">
+            + 或者条件
+          </el-button>
+        </el-form-item>
+      </el-form>
+
+      <!-- 执行动作区域 -->
+      <!-- 执行动作区域 -->
+      <div class="item-title"><span></span> 将执行以下动作</div>
+      <el-form
+        :model="formData"
+        :rules="formRules"
+        ref="formRef"
+        class="action-form"
+      >
+        <!-- 指定报价协议 -->
+        <el-form-item
+          prop="formdata.actionJson[0].agreementId"
+          class="form-item"
+        >
+          <span>指定报价协议:</span>
+          <el-select
+            v-model="formData.actionJson[0].agreementId"
+            placeholder="选择报价协议"
+            style="width: 240px"
+            clearable
+          >
+            <el-option
+              v-for="item in agreementList"
+              :key="item.id"
+              :label="item.agreementName"
+              :value="item.id"
+            />
+          </el-select>
+        </el-form-item>
+
+        <!-- 对接人变更方式选择 -->
+        <el-form-item>
+          <el-radio-group
+            v-model="formData.actionJson[1].linkType"
+            @change="handleTypeChange"
+          >
+            <!-- 手动输入对接人 -->
+            <el-radio
+              value="manual_input"
+              size="large"
+              style="margin: 20px 0; display: block"
+            >
+              手动输入对接人信息:
+              <el-form-item
+                prop="actionJson.name"
+                style="display: inline-block; margin: 0 20px"
+              >
+                <el-input
+                  v-model="formData.actionJson[1].name"
+                  style="width: 327px"
+                  placeholder="变更后对接人姓名"
+                  :disabled="
+                    formData.actionJson[1].linkType === 'select_account'
+                  "
+                  clearable
+                />
+              </el-form-item>
+              <el-form-item
+                prop="actionJson.phone"
+                style="display: inline-block"
+              >
+                <el-input
+                  v-model="formData.actionJson[1].phone"
+                  style="width: 327px"
+                  type="number"
+                  placeholder="变更后对接人手机号"
+                  maxlength="11"
+                  @input="handlePhoneInput"
+                  :disabled="
+                    formData.actionJson[1].linkType === 'select_account'
+                  "
+                  clearable
+                />
+              </el-form-item>
+            </el-radio>
+
+            <!-- 选择保司账号 -->
+            <el-radio
+              value="select_account"
+              size="large"
+              style="margin: 20px 0; display: block"
+            >
+              选择已有保司账号:
+              <el-form-item
+                prop="actionJson.accountId"
+                style="display: inline-block; margin-left: 20px"
+              >
+                <el-select
+                  v-model="formData.actionJson[1].accountId"
+                  placeholder="选择变更后的保司账号"
+                  style="width: 662px"
+                  :disabled="formData.actionJson[1].linkType === 'manual_input'"
+                  clearable
+                >
+                  <!--  -->
+                  <el-option
+                    v-for="item in accountList"
+                    :key="item.id"
+                    :label="item.username"
+                    :value="item.id"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-form>
+
+      <!-- 抽屉底部按钮 -->
+      <template #footer>
+        <div class="foot">
+          <el-button @click="handleClose"> 取消 </el-button>
+          <el-button type="primary" @click="submit"> 保存 </el-button>
+        </div>
+      </template>
+    </el-drawer>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { reactive, ref, type Ref, onMounted } from "vue";
+import type { FormInstance, FormRules } from "element-plus";
+import { Delete } from "@element-plus/icons-vue";
+import { ElMessage } from "element-plus";
+import api from "@/api";
+import { getDictItems } from "@/api/dict";
+
+// 抽屉状态
+const drawer = ref(true);
+
+const emit = defineEmits(["close"]);
+
+// 组件props
+const props = defineProps({
+  companyId: {
+    type: [String, Number],
+    required: true,
+    validator: (value: string | number) => value !== "" && value !== 0,
+  },
+  editData: {},
+  editType: "",
+  drawerTitle: {
+    type: String,
+    default: "规则配置",
+  },
+});
+
+// 表单实例引用
+const formRef: Ref<FormInstance | undefined> = ref(undefined);
+
+// 级联选择器配置
+const cascaderProps = { multiple: true, checkStrictly: true };
+
+// 数据类型定义
+interface ConditionRow {
+  key: number;
+  min: any;
+  operator: string;
+  attrCode: string;
+  max: any;
+  errorMsg: string;
+  minArray?: string[];
+}
+
+interface ConditionGroup {
+  groupKey: number;
+  actions: ConditionRow[];
+}
+
+// 表单核心数据
+// 表单核心数据 - 优化actionJson结构
+const formData = ref<{
+  conditionGroups: ConditionGroup[];
+  actionJson: [
+    {
+      actionCode: "specify_agreement"; // 固定:指定报价协议
+      agreementId: string; // 报价协议ID
+    },
+    {
+      actionCode: "change_bs_account"; // 固定:变更对接人
+      linkType: "manual_input" | "select_account"; // 对接人方式:手动输入/选择账号
+      name: string; // 手动输入-姓名
+      phone: string; // 手动输入-手机号
+      accountId: string; // 选择账号-账号ID
+    }
+  ];
+}>({
+  actionJson: [
+    {
+      actionCode: "specify_agreement",
+      agreementId: "", // 初始为空
+    },
+    {
+      actionCode: "change_bs_account",
+      linkType: "manual_input", // 默认选中“手动输入”
+      name: "",
+      phone: "",
+      accountId: "",
+    },
+  ],
+  conditionGroups: [
+    {
+      groupKey: Date.now(),
+      actions: [
+        {
+          attrCode: "",
+          key: Date.now(),
+          min: "",
+          operator: "",
+          max: "",
+          errorMsg: "",
+          minArray: [],
+        },
+      ],
+    },
+  ],
+});
+
+// 下拉选项数据源
+const dispatchList = ref([]);
+const agreementList = ref([]);
+const accountList = ref([]);
+const dictionaries = ref([]);
+
+const dispatchId = ref(""); //调度id
+const editType = ref("");
+
+// 表单校验规则
+// 先定义独立校验函数(放在formRules之前)
+const validateConditionGroups = (
+  rule: any,
+  value: ConditionGroup[],
+  callback: any
+) => {
+  // 1. 校验:至少1组条件
+  if (value.length === 0) {
+    return callback(new Error("至少配置1组“或者”条件"));
+  }
+
+  // 2. 遍历每组校验
+  for (const group of value) {
+    // 2.1 校验:每组至少1行“并且”条件
+    if (group.actions.length === 0) {
+      return callback(new Error("每组条件至少包含1条“并且”规则"));
+    }
+
+    // 2.2 遍历每行校验完整性
+    for (const row of group.actions) {
+      row.errorMsg = ""; // 清空历史错误,避免残留
+      const attrType = getConditionType(row.attrCode);
+      const isRangeOp = row.operator === "1"; // 是否为“介于”等范围运算符
+
+      // 校验1:条件类型未选择
+      if (!row.attrCode) {
+        row.errorMsg = "请选择条件类型(如“报价金额”“车辆类型”)";
+        return callback(new Error(row.errorMsg));
+      }
+
+      // 校验2:运算符未选择
+      if (!row.operator) {
+        row.errorMsg = "请选择运算符(如“等于”“大于”)";
+        return callback(new Error(row.errorMsg));
+      }
+
+      // 校验3:根据条件类型校验“值”
+      // 3.1 多选/标签类型(依赖minArray)
+      if (["checkbox", "inputTag"].includes(attrType)) {
+        if (!row.minArray || row.minArray.length === 0) {
+          row.errorMsg = `请选择至少一个条件值(当前为${
+            attrType === "checkbox" ? "多选" : "标签"
+          }类型)`;
+          return callback(new Error(row.errorMsg));
+        }
+      }
+      // 3.2 范围类型(需同时填min和max)
+      else if (isRangeOp) {
+        if (row.min === null || row.min === "" || row.min === undefined) {
+          row.errorMsg = "请填写范围起始值(如“1000”)";
+          return callback(new Error(row.errorMsg));
+        }
+        if (row.max === null || row.max === "" || row.max === undefined) {
+          row.errorMsg = "请填写范围结束值(如“2000”)";
+          return callback(new Error(row.errorMsg));
+        }
+      }
+      // 3.3 普通类型(单选/输入框,依赖min或max)
+      else {
+        const targetVal = ["radio", "select", "plate", "datetime"].includes(
+          attrType
+        )
+          ? row.max
+          : row.min;
+        if (targetVal === null || targetVal === "" || targetVal === undefined) {
+          row.errorMsg = `请${
+            attrType === "datetime" ? "选择" : "输入"
+          }条件值(当前为${
+            attrType === "select" ? "下拉选择" : "文本输入"
+          }类型)`;
+          return callback(new Error(row.errorMsg));
+        }
+      }
+    }
+  }
+
+  // 所有校验通过
+  callback();
+};
+
+const validateAgreementId = (rule: any, value: string, callback: any) => {
+  if (!value) {
+    return callback(new Error("请选择需要指定的报价协议"));
+  }
+  callback();
+};
+
+const validateLinkManName = (rule: any, value: string, callback: any) => {
+  const linkType = formData.value.actionJson[1].linkType;
+  if (linkType === "manual_input" && !value?.trim()) {
+    return callback(new Error("请输入对接人姓名(手动输入模式)"));
+  }
+  callback();
+};
+
+const validateLinkManPhone = (rule: any, value: string, callback: any) => {
+  const linkType = formData.value.actionJson[1].linkType;
+  if (linkType !== "manual_input") {
+    return callback();
+  }
+
+  if (!value) {
+    return callback(new Error("请输入对接人手机号(手动输入模式)"));
+  }
+  if (!/^1[3-9]\d{9}$/.test(value)) {
+    return callback(
+      new Error("手机号格式错误,请输入11位有效号码(如13800138000)")
+    );
+  }
+  callback();
+};
+
+const validateAccountId = (rule: any, value: string, callback: any) => {
+  const linkType = formData.value.actionJson[1].linkType;
+  if (linkType === "select_account" && !value) {
+    return callback(new Error("请选择变更后的保司账号(选择账号模式)"));
+  }
+  callback();
+};
+
+// 优化后的formRules
+const formRules = reactive<FormRules>({
+  // 1. 条件组校验(核心)
+  conditionGroups: [
+    {
+      required: true,
+      trigger: ["submit", "blur"], // 移除频繁的change触发,仅在提交和失焦时校验
+      // validator: validateConditionGroups,
+    },
+  ],
+
+  // 2. 执行动作:指定报价协议(必选)
+  "actionJson[0].agreementId": [
+    {
+      required: true,
+      trigger: ["submit", "blur"], // 下拉选择无需change触发,失焦+提交足够
+      validator: validateAgreementId,
+    },
+  ],
+
+  // 3. 执行动作:对接人姓名(仅手动输入时必选)
+  "actionJson[1].name": [
+    {
+      trigger: ["submit", "blur"],
+      validator: validateLinkManName,
+    },
+  ],
+
+  // 4. 执行动作:对接人手机号(仅手动输入时必选+格式校验)
+  "actionJson[1].phone": [
+    {
+      trigger: ["submit", "blur"], // 移除input触发,避免输入过程中频繁报错
+      validator: validateLinkManPhone,
+    },
+  ],
+
+  // 5. 执行动作:保司账号(仅选择账号时必选)
+  "actionJson[1].accountId": [
+    {
+      trigger: ["submit", "blur"],
+      validator: validateAccountId,
+    },
+  ],
+});
+
+// 工具函数:根据条件类型获取对应的运算符列表
+const getOperations = (conditionValue: string) => {
+  const target = dispatchList.value.find(
+    (item) => item.attrCode === conditionValue
+  );
+  return target?.rulesDict || [];
+};
+
+// 工具函数:根据条件类型获取对应的选项列表
+const getOptions = (conditionValue: string) => {
+  const target = dispatchList.value.find(
+    (item) => item.attrCode === conditionValue
+  );
+  return target?.dictLabal || [];
+};
+
+// 工具函数:根据条件类型获取输入组件类型
+const getConditionType = (conditionValue: string) => {
+  const target = dispatchList.value.find(
+    (item) => item.attrCode === conditionValue
+  );
+  return target?.attrType || "input";
+};
+
+// 条件切换时的处理
+const handleConditionChange = (domain: ConditionRow) => {
+  domain.max = null;
+  domain.operator = "";
+};
+
+// 运算符切换时重置值
+const handleOperatorChange = (domain: ConditionRow) => {
+  const attrType = getConditionType(domain.attrCode);
+  // 重置对应类型的值
+  if (["checkbox", "inputTag"].includes(attrType)) {
+    domain.minArray = [];
+  } else {
+    domain.min = null;
+    domain.max = null;
+  }
+};
+
+// 组内添加并且条件行
+const handleAddRow = (groupIndex: number) => {
+  formData.value.conditionGroups[groupIndex].actions.push({
+    key: Date.now(),
+    min: "",
+    attrCode: "",
+    operator: "",
+    max: null,
+    errorMsg: "",
+    minArray: [],
+  });
+};
+
+// 删除组内条件行
+const handleRemoveRow = (groupIndex: number, rowIndex: number) => {
+  formData.value.conditionGroups[groupIndex].actions.splice(rowIndex, 1);
+};
+
+// 添加新的或者条件组
+const handleAddGroup = () => {
+  formData.value.conditionGroups.push({
+    groupKey: Date.now(),
+    actions: [
+      {
+        key: Date.now(),
+        min: "",
+        attrCode: "",
+        operator: "",
+        max: null,
+        errorMsg: "",
+        minArray: [],
+      },
+    ],
+  });
+};
+
+// 手机号输入处理
+const handlePhoneInput = () => {
+  const phone = formData.value.actionJson[1].phone;
+  if (phone) {
+    // 截取前11位,去除非数字字符
+    const filtered = phone.replace(/\D/g, "").slice(0, 11);
+    formData.value.actionJson[1].phone = filtered;
+  }
+};
+
+// 获取条件类型列表
+const getList = async () => {
+  try {
+    const res = await api.dispatchApi.dispatchList({});
+    dispatchList.value = res.data;
+  } catch (error) {
+    console.error("获取条件类型列表失败:", error);
+  }
+};
+
+// 获取报价协议列表
+const getByCompanyId = async () => {
+  try {
+    const res = await api.dispatchApi.getByCompanyIdt(props.companyId);
+    agreementList.value = res.data;
+  } catch (error) {
+    console.error("获取报价协议列表失败:", error);
+  }
+};
+
+// 获取保司账号列表
+const getAccountList = async () => {
+  try {
+    const res = await api.dispatchApi.getAgreementAttributionByCompanyId(
+      props.companyId
+    );
+    accountList.value = res.data;
+  } catch (error) {
+    console.error("获取保司账号列表失败:", error);
+  }
+};
+
+//清空选项数据
+const handleTypeChange = () => {
+  const linkType = formData.value.actionJson[1].linkType;
+  const actionJson = formData.value.actionJson[1];
+
+  if (linkType === "manual_input") {
+    // 切换到手动输入:清空账号ID
+    actionJson.accountId = "";
+  } else if (linkType === "select_account") {
+    // 切换到选择账号:清空姓名和手机号
+    actionJson.name = "";
+    actionJson.phone = "";
+  }
+};
+
+// 提交表单
+const submit = async () => {
+  const { editType } = props;
+  // if (!formRef.value) return;
+
+  // await formRef.value.validate();
+  const actionJson = formData.value.actionJson;
+
+  // const actions =formData.value.actionJson
+  const actions = [
+    {
+      actionCode: actionJson[0].actionCode,
+      actionJson: {
+        agreementId: actionJson[0].agreementId,
+      },
+    },
+    {
+      actionCode:
+        actionJson[1].linkType == "manual_input"
+          ? "change_poc"
+          : "change_bs_account",
+      actionJson: {
+        ...(actionJson[1].linkType === "manual_input"
+          ? { name: actionJson[1].name, phone: actionJson[1].phone }
+          : { accountId: actionJson[1].accountId }),
+      },
+    },
+  ];
+
+  const submitData = {
+    companyId: props.companyId + "",
+    attrLinks: formData.value.conditionGroups.flatMap((group) => group.actions),
+    actions,
+    ...(dispatchId.value && { dispatchId: dispatchId.value }), // 编辑时携带ID
+  };
+
+  const apiFunc = editType==="edit"
+    ? api.dispatchApi.updateAgreementDispatch
+    : api.dispatchApi.addAgreementDispatch;
+  // const apiFunc = api.dispatchApi.addAgreementDispatch;
+  const res = await apiFunc(submitData);
+
+  if (res.code === 200) {
+    ElMessage.success("保存成功");
+    handleClose();
+  } else {
+    ElMessage.error("保存失败");
+  }
+  //    try {
+  // } catch (error) {
+  //   ElMessage.error("表单校验失败,请检查填写内容");
+  // }
+};
+
+// 关闭抽屉
+const handleClose = () => {
+  emit("close", false);
+};
+
+// 组件挂载时初始化数据
+// onMounted(async () => {
+//   await getList();
+//   await getByCompanyId();
+//   await getAccountList();
+//   dictionaries.value = await getDictItems({ dictCode: "dispatch_action" });
+//   console.log(dictionaries.value);
+//   formData.value = props.editData;
+// });
+onMounted(async () => {
+  // 1. 加载基础数据(条件类型、协议列表等)
+  await Promise.all([
+    getList(), // 条件类型列表
+    getByCompanyId(), // 报价协议列表
+    getAccountList(), // 保司账号列表
+  ]);
+  dictionaries.value = await getDictItems({ dictCode: "dispatch_action" });
+
+  // 2. 提取props参数
+  const { editType, editData } = props;
+  console.log("当前操作类型:", editType);
+  
+  dispatchId.value = editData?.dispatchId || "";
+
+  // 3. 初始化formData基础结构(固定保留actionJson默认值,仅动态处理conditionGroups)
+  const baseFormData = {
+    // actionJson保持初始默认结构(新增/编辑通用基础配置)
+    actionJson: [
+      {
+        actionCode: "specify_agreement",
+        agreementId: "", // 新增时为空,编辑时后续单独处理
+      },
+      {
+        actionCode: "change_bs_account",
+        linkType: "manual_input" as "manual_input" | "select_account",
+        name: "",
+        phone: "",
+        accountId: "",
+      },
+    ],
+    // 条件组:根据editType动态赋值
+    conditionGroups: [] as ConditionGroup[],
+  };
+ let isDefaultScene = false;
+  // 4. 新增场景:仅初始化conditionGroups为默认一组条件
+    if (!editType || editType === "add") { 
+    isDefaultScene = true;
+    console.log("[初始化] 进入场景:无editType或add"); // 新增日志
+
+    // 提取原始条件组(兼容editData可能的格式)
+    const rawGroups = editData?.groupedAttrLinks 
+      ? editData.groupedAttrLinks 
+      : editData?.attrLinks 
+        ? [editData.attrLinks] 
+        : [];
+    console.log("[初始化] 提取到的rawGroups:", rawGroups, "长度:", rawGroups.length); // 新增日志
+
+    // 转换rawGroups为conditionGroups
+    baseFormData.conditionGroups = rawGroups.map((rawGroup: any[], groupIdx: number) => {
+      const group: ConditionGroup = {
+        groupKey: editData?.dispatchId || `group_${groupIdx}_${Date.now()}`,
+        actions: [],
+      };
+
+      // 处理组内条件行
+      if (rawGroup.length > 0) {
+        group.actions = rawGroup.map((rawRow: any, rowIdx: number) => ({
+          key: rawRow.id || `row_${groupIdx}_${rowIdx}_${Date.now()}`,
+          attrCode: rawRow.attrCode || "",
+          operator: rawRow.operator || "",
+          min: rawRow.min !== undefined ? rawRow.min : null,
+          max: rawRow.max !== undefined ? rawRow.max : null,
+          minArray: rawRow.minArray ? [...rawRow.minArray] : [],
+          errorMsg: "",
+        }));
+      } else {
+        // 组内无数据时,兜底1行
+        group.actions.push({
+          key: Date.now(),
+          attrCode: "",
+          operator: "",
+          min: null,
+          max: null,
+          errorMsg: "",
+          minArray: [],
+        });
+      }
+      return group;
+    });
+
+    // 第一次兜底:如果转换后还是空,直接加1组
+    if (baseFormData.conditionGroups.length === 0) {
+      console.log("[初始化] rawGroups为空,第一次兜底添加1组条件"); // 新增日志
+      baseFormData.conditionGroups.push({
+        groupKey: Date.now(),
+        actions: [
+          {
+            key: Date.now(),
+            attrCode: "",
+            operator: "",
+            min: null,
+            max: null,
+            errorMsg: "",
+            minArray: [],
+          },
+        ],
+      });
+    }
+  }
+
+  // 5. 编辑场景:从editData解析conditionGroups(保持之前的编辑逻辑)
+  else if (
+    editType === "edit" &&
+    editData &&
+    Object.keys(editData).length > 0
+  ) {
+    const rawGroups = editData.groupedAttrLinks
+      ? editData.groupedAttrLinks
+      : editData.attrLinks
+      ? [editData.attrLinks]
+      : [];
+
+    baseFormData.conditionGroups = rawGroups.map(
+      (rawGroup: any[], groupIdx: number) => {
+        const group: ConditionGroup = {
+          groupKey: editData.dispatchId || `group_${groupIdx}_${Date.now()}`,
+          actions: [],
+        };
+
+        rawGroup.forEach((rawRow: any, rowIdx: number) => {
+          group.actions.push({
+            key: rawRow.id || `row_${groupIdx}_${rowIdx}_${Date.now()}`,
+            attrCode: rawRow.attrCode || "",
+            operator: rawRow.operator || "",
+            min: rawRow.min !== undefined ? rawRow.min : null,
+            max: rawRow.max !== undefined ? rawRow.max : null,
+            minArray: rawRow.minArray ? [...rawRow.minArray] : [],
+            errorMsg: "",
+          });
+        });
+
+        // 兜底:确保每组至少有一行条件
+        if (group.actions.length === 0) {
+          group.actions.push({
+            key: Date.now(),
+            attrCode: "",
+            operator: "",
+            min: null,
+            max: null,
+            errorMsg: "",
+            minArray: [],
+          });
+        }
+        return group;
+      }
+    );
+
+    // 编辑场景补充:回显actionJson(报价协议和对接人信息)
+    if (editData.actions && editData.actions.length > 0) {
+      // 回显报价协议
+      const agreementAction = editData.actions.find(
+        (action: any) => action.actionCode === "specify_agreement"
+      );
+      if (agreementAction?.actionJson?.agreementId) {
+        baseFormData.actionJson[0].agreementId =
+          agreementAction.actionJson.agreementId;
+      }
+
+      // 回显对接人信息
+      const linkAction = editData.actions.find(
+        (action: any) =>
+          action.actionCode === "change_bs_account" ||
+          action.actionCode === "change_poc"
+      );
+      if (linkAction?.actionJson) {
+        const linkJson = linkAction.actionJson;
+        if (linkJson.accountId) {
+          baseFormData.actionJson[1].linkType = "select_account";
+          baseFormData.actionJson[1].accountId = linkJson.accountId;
+        } else {
+          baseFormData.actionJson[1].linkType = "manual_input";
+          baseFormData.actionJson[1].name = linkJson.name || "";
+          baseFormData.actionJson[1].phone = linkJson.phone || "";
+        }
+      }
+    }
+
+    if (baseFormData.conditionGroups.length === 0) {
+      baseFormData.conditionGroups = [
+        {
+          groupKey: Date.now(),
+          actions: [
+            {
+              key: Date.now(),
+              attrCode: "",
+              operator: "",
+              min: null,
+              max: null,
+              errorMsg: "",
+              minArray: [],
+            },
+          ],
+        },
+      ];
+    }
+  }
+
+  // 6. 赋值给formData(确保响应式生效)
+  formData.value = baseFormData;
+  console.log(
+    `[${editType}] 场景初始化完成,conditionGroups长度:`,
+    formData.value.conditionGroups.length
+  );
+});
+</script>
+
+<style scoped lang="scss">
+.condition-container {
+  margin-bottom: 16px;
+}
+
+.condition-group {
+  width: 100%;
+  padding: 24px;
+  margin-bottom: 20px;
+  border: 1px solid #eee;
+  border-radius: 6px;
+  box-sizing: border-box;
+  background-color: #fff;
+  position: relative;
+}
+
+.group-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 16px;
+  padding-bottom: 8px;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.condition-row {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  flex-wrap: wrap;
+}
+
+.add-condition-btn {
+  margin-top: 8px;
+  text-align: left;
+}
+
+.add-group-btn {
+  text-align: left;
+}
+
+.item-title {
+  display: flex;
+  align-items: center;
+  margin-bottom: 20px;
+  span {
+    display: inline-block;
+    width: 4px;
+    height: 18px;
+    background: #0083ff;
+    border-radius: 10px 10px 10px 10px;
+    margin-right: 8px;
+  }
+}
+.form-item {
+  display: flex;
+  align-items: center;
+  span {
+    font-size: 14px;
+    margin-right: 10px;
+  }
+}
+.foot {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  gap: 12px;
+  padding-top: 10px;
+  border-top: 1px solid #eee;
+}
+
+:deep(.main-container) {
+  height: 100%;
+}
+:deep(.el-row) {
+  height: 100%;
+}
+:deep(.el-drawer__header) {
+  height: 48px;
+  font-size: 20px;
+  color: #333333;
+  border-bottom: 1px solid #eeeeee;
+  margin-bottom: 24px;
+  padding-bottom: 20px;
+}
+:deep(.el-drawer__body) {
+  padding: 0 24px;
+}
+</style>

+ 0 - 10
src/views/quoteConfig/dispatch/dispatch.vue

@@ -1,10 +0,0 @@
-<template>
-  <div>dispatch</div>
-</template>
-
-<script lang="ts" setup>
-</script>
-
-<style>
-
-</style>

+ 648 - 0
src/views/quoteConfig/dispatch/index.vue

@@ -0,0 +1,648 @@
+<template>
+  <div class="dispatch">
+    <PageMain class="vh100" style="overflow: hidden">
+      <ElRow class="vh99">
+        <ElCol :md="4">
+          <div
+            class="flex f-c"
+            style="
+              height: 100%;
+              border-right: 1px solid #eeeeee;
+              padding-right: 17px;
+            "
+          >
+            <ElFormItem>
+              <ElInput
+                v-model="searchValue"
+                placeholder="输入编码,名称查找"
+                type="text"
+                tabindex="1"
+                style="width: 100%"
+              >
+                <template #prefix>
+                  <SvgIcon name="i-ri:user-3-fill" />
+                </template>
+                <template #append>
+                  <el-button
+                    :icon="Search"
+                    class="ElInputSearch"
+                    @click="findpage"
+                  />
+                </template>
+              </ElInput>
+            </ElFormItem>
+            <div
+              style="width: 100%; height: calc(100vh - 350px); overflow-y: auto"
+              class="container"
+            >
+              <div
+                class="list-item"
+                :class="currentIndex == index ? 'active' : ''"
+                v-for="(item, index) in tenantList"
+                :key="item.id"
+                @click="roleitemClick(item, index)"
+              >
+                <span>{{ item.name }}</span>
+              </div>
+            </div>
+          </div>
+        </ElCol>
+        <ElCol :md="20">
+          <div class="right">
+            <div class="top">
+              <span>调度规则</span>
+              <el-button type="text" @click="drawer = true"
+                >+添加新方案</el-button
+              >
+            </div>
+            <div class="empty" v-if="ruleList.length == 0">
+              <img src="@/assets/images/empty.png" alt="" />暂无内容
+            </div>
+            <div class="center" v-else>
+              <!-- 规则列表:循环处理后的ruleList -->
+              <div
+                class="rule-item"
+                v-for="(item, ruleIndex) in ruleList"
+                :key="item.dispatchId"
+                
+              >
+                <div class="item-title">
+                  <div class="name" v-if="!item.editMode">
+                    {{ item.dispatchName }}
+                  </div>
+                  <div class="name" v-else>
+                    <el-input
+                      v-model="item.dispatchName"
+                      style="width: 240px"
+                      :placeholder="item.dispatchName"
+                      @blur="saveDispatchName(item)"
+                    />
+                  </div>
+                  <img
+                    src="@/assets/images/edit.png"
+                    @click.stop="item.editMode = !item.editMode"
+                  />
+                  <img
+                    src="@/assets/images/add.png"
+                    style="margin: 0px 12px"
+                    @click.stop="editDispatch(item)"
+                  />
+                  <el-popconfirm
+                    class="box-item"
+                    title="是否确定删除该规则?"
+                    placement="bottom"
+                    width="196px"
+                    confirm-button-text="确定删除"
+                    @confirm.stop.prevent="deleteRule( item.dispatchId)"
+                  >
+                    <template #reference>
+                      <img src="@/assets/images/delete.png"  @click.stop
+                    /></template>
+                  </el-popconfirm>
+                </div>
+                <p>当报价信息满足以下条件时</p>
+
+                <div class="condition">
+                  <div
+                    class="section-group"
+                    v-for="(sectionGroup, groupIndex) in item.groupedAttrLinks"
+                    :key="groupIndex"
+                  >
+                    <div class="condition-one" @click.stop="ruleitemEdit(item)">
+                      <div
+                        class="dispatch-item"
+                        v-for="(row, index) in sectionGroup"
+                        :key="row.id"
+                      >
+                        {{ row.attrCodeName || "未设置属性" }}
+                        <div class="opera">
+                          {{ row.operatorDesc }}
+                        </div>
+                        <!-- <div class="value">
+                          <template
+                            v-if="
+                              
+                              row.operator == 1
+                            "
+                          >
+                            {{
+                              Array.isArray(row.attrDesc)
+                                ? row.attrDesc.join(",")
+                                : row.attrDesc || "无"
+                            }}
+                          </template>
+                          <template v-else-if="
+                              row.operator == 2 ||
+                              row.operator == 3
+                              
+                            ">
+                            {{ row.min }}
+                          </template>
+                          <template v-else>
+                            {{ row.min }}~{{ row.max }}
+                          </template>
+                        </div> -->
+                        <div class="value">
+                          <template
+                            v-if="  row.min &&row.max"
+                          >
+                            {{ row.min }}~{{ row.max }}
+                          </template>
+                          <template
+                            v-else-if="row.attrDesc "
+                          >
+                           {{
+                              Array.isArray(row.attrDesc)
+                                ? row.attrDesc.join(",")
+                                : row.attrDesc || "无"
+                            }}
+                          </template>
+                          <template
+                            v-else
+                          >
+                            {{ row.min }}
+                          </template>
+                        </div>
+                      </div>
+                    </div>
+
+                    <div
+                      style="margin: 0px 16px 10px; font-size: 16px"
+                      v-if="groupIndex < item.groupedAttrLinks.length - 1"
+                    >
+                      或者
+                    </div>
+                  </div>
+
+                  <div style="margin: 0px 16px 10px">将执行以下动作</div>
+                  <div class="action" v-if="item.actions.length > 0"  @click.stop="ruleitemEdit(item)">
+                    <div class="name">
+                      {{ item.actions[0].actionName }}
+                    </div>
+                    {{
+                      item.actions[0].actionJson.agreementName ||
+                      "未设置协议名称"
+                    }}
+                  </div>
+                  <div class="action" v-if="item.actions.length > 1"  @click.stop="ruleitemEdit(item)">
+                    <div class="name">
+                      变更 {{ item.actions[1].actionName }}的
+                      {{
+                        item.actions[1].actionCode == "change_poc"
+                          ? "对接人"
+                          : "保司账号"
+                      }}
+                    </div>
+                    <template
+                      v-if="item.actions[1].actionCode == 'change_poc'"
+                    >
+                      {{ item.actions[1].actionJson.name || "未设置" }}-{{
+                        item.actions[1].actionJson.phone || "未设置"
+                      }}
+                    </template>
+                    <!-- <template v-if="'send_notify_msg'">
+
+                    </template> -->
+                    <template v-else>
+                      用户名:{{
+                        item.actions[1].actionJson.userName || "未设置"
+                      }}<br />
+                      简称:
+                      {{ item.actions[1].actionJson.companyName || "未设置" }}
+                    </template>
+                  </div>
+                </div>
+              </div>
+
+              <!-- 其他情况规则 -->
+              <div class="rule-item">
+                <div class="item-title">
+                  <div class="name">其他情况</div>
+                  <!-- <img src="@/assets/images/edit.png" /> -->
+                  <!-- <img src="@/assets/images/add.png" style="margin: 0px 12px" /> -->
+                  <!-- <el-popconfirm
+                    class="box-item"
+                    title="是否确定删除该规则?"
+                    placement="bottom"
+                    width="196px"
+                    confirm-button-text="确定删除"
+                    @confirm="deleteRule('other')"
+                  >
+                    <template #reference>
+                      <img src="@/assets/images/delete.png"
+                    /></template>
+                  </el-popconfirm> -->
+                </div>
+                <p>
+                  如存在不满足其它规则的情况下,则进入该情况,请选择该情况下要执行的动作
+                </p>
+                <div
+                  class="other"
+                  @click="(isother = true), (companyId = companyId)"
+                >
+                  点击选择该情况下要执行的动作
+                </div>
+                <div
+                  class="condition"
+                  v-for="item in afterActions"
+                  :key="item.id"
+                >
+                  <div class="action" v-if="item.actionCode == 'send_notify_msg'">
+                    <div class="name">
+                      {{ item.actionName }}
+                    </div>
+                    <div>标题:{{item.actionJson.title}}</div>
+                    <div>内容:{{item.actionJson.content}}</div>
+                  </div>
+                   <div class="action" v-else-if="item.actionCode == 'supplementary_order'">
+                    <div class="name">
+                      {{ item.actionName }}
+                    </div>
+                    将该车辆的报价单,在后台自动生成为:暂存的补录订单
+                  </div>
+                  <div class="action" v-else>
+                    <div class="name">
+                      {{ item.actionName }}
+                    </div>
+                    {{ item.actionJson.message || "未设置" }}
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </ElCol>
+      </ElRow>
+    </PageMain>
+    <rule
+      v-if="drawer"
+      @close="handleClose"
+      :editData="editData"
+      :companyId="currentId"
+      :editType="editType"
+    />
+    <other v-if="isother" :companyId="companyId" @close="closeOther"></other>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from "vue";
+import { ElMessage } from "element-plus";
+import { Search } from "@element-plus/icons-vue";
+import rule from "./components/rule.vue";
+import other from "./components/other.vue";
+import api from "@/api";
+import SvgIcon from "@/components/SvgIcon/index.vue"; // 确保SvgIcon组件已引入
+
+// 状态管理
+const isother = ref(false);
+const searchValue = ref<string>(""); // 初始化为空字符串,避免undefined
+const tenantList = ref([]);
+const ruleList = ref([]); //规则列表
+const afterActions = ref([]);
+const drawer = ref(false);
+const currentIndex = ref(0);
+const currentId = ref("");
+const companyId = ref(""); // 用于“其他情况”组件的companyId传递
+const editData = ref({});
+const editType = ref("");
+// 关闭弹窗
+const handleClose = () => {
+  drawer.value = false;
+  editData.value = {};
+  editType.value = "";
+  getRuleList(currentId.value);
+};
+//关闭其他弹窗
+const closeOther = () => {
+  isother.value = false;
+  getRuleList(currentId.value);
+};
+
+// 核心:处理attrLinks按section分组的工具函数
+const formatAttrLinksBySection = (attrLinks: any[]) => {
+  // 1. 按section分组:key为section值(null转为"null"),value为同section的attrLinks数组
+  const groupedObj = attrLinks.reduce((acc, link) => {
+    const sectionKey = link.section ?? "null"; // 统一null的key标识
+    if (!acc[sectionKey]) {
+      acc[sectionKey] = [];
+    }
+    acc[sectionKey].push(link);
+    return acc;
+  }, {} as Record<string, any[]>);
+
+  // 2. 转为数组(按section升序排序,确保展示顺序一致)
+  return Object.values(groupedObj).sort((a, b) => {
+    // 提取section值,null排在最后
+    const sectionA = a[0].section ? Number(a[0].section) : Infinity;
+    const sectionB = b[0].section ? Number(b[0].section) : Infinity;
+    return sectionA - sectionB;
+  });
+};
+
+// 获取租户列表并加载初始规则
+const getCompanyList = () => {
+  api.dispatchApi.getCompanyList().then((res) => {
+    tenantList.value = res.data || [];
+    // 安全获取第一个租户的id(避免数组为空)
+    if (tenantList.value.length > 0) {
+      currentId.value = tenantList.value[0].id;
+      companyId.value = tenantList.value[0].id;
+      getRuleList(currentId.value); // 调用统一的规则获取函数
+    }
+  });
+};
+
+// 点击租户切换规则(统一复用规则获取逻辑)
+const roleitemClick = (item: any, index: number) => {
+  console.log(item);
+
+  currentIndex.value = index;
+  currentId.value = item.id;
+  companyId.value = item.id;
+  getRuleList(item.id);
+};
+
+// 统一获取规则列表(包含attrLinks分组处理)
+const getRuleList = (companyId: string) => {
+  api.dispatchApi.getAgreementDispatch({ companyId }).then((res) => {
+    // 对每个规则的attrLinks进行分组处理,新增groupedAttrLinks属性
+    ruleList.value = (res.data.dispatchVos || []).map((ruleItem: any) => ({
+      ...ruleItem,
+      // 新增分组后的属性,替代原attrLinks的直接使用
+      groupedAttrLinks: formatAttrLinksBySection(ruleItem.attrLinks || []),
+      editMode: false,
+    }));
+    afterActions.value = res.data.afterActions || [];
+    console.log(ruleList.value);
+  });
+};
+
+// 删除规则
+const deleteRule = (id: string) => {
+  api.dispatchApi.deleteAgreementDispatch(id).then((res) => {
+    ElMessage.success("删除成功");
+    // 重新加载当前租户的规则列表(自动重新分组)
+    getRuleList(currentId.value);
+  });
+};
+
+// 搜索租户(需后端配合实现,此处预留方法)
+const findpage = () => {
+  if (!searchValue.value.trim()) {
+    getCompanyList(); // 无搜索值时加载全部
+    return;
+  }
+  // 调用搜索接口(示例,需按实际接口调整)
+  api.dispatchApi.searchCompany({ keyword: searchValue.value }).then((res) => {
+    tenantList.value = res.data || [];
+  });
+};
+
+const editDispatch = (item: any) => {
+  // console.log(item);
+  editData.value = item;
+  editType.value = 'add'
+  drawer.value = true;
+};
+
+const ruleitemEdit = (item:any) => {
+  editData.value = item;
+  editType.value = 'edit'
+  drawer.value = true;
+};
+
+// 保存规则名称(失焦时触发)
+const saveDispatchName = async (item: any) => {
+  item.editMode = false;
+
+  try {
+    const result = await api.dispatchApi.updateDispatchName({
+      dispatchId: item.dispatchId,
+      name: item.dispatchName,
+    });
+    if (result.code == 200) {
+      ElMessage.success(result.msg);
+      getCompanyList();
+    } else {
+      ElMessage.error(result.msg);
+      getCompanyList();
+    }
+  } catch (error) {
+    ElMessage.error("名称修改失败,请重试");
+    getCompanyList();
+  }
+};
+
+// 页面挂载时加载租户列表
+getCompanyList();
+</script>
+
+<style lang="scss" scoped>
+.dispatch {
+  width: 100%;
+}
+.ElInputSearch {
+  color: #fff !important;
+  background: linear-gradient(91deg, #4fcaff 0%, #2b62ff 100%) !important;
+  border-color: #4fcaffe8 !important;
+  border-top-left-radius: 0 !important;
+  border-bottom-left-radius: 0 !important;
+}
+.list-item {
+  cursor: pointer;
+  height: 36px;
+  font-size: 16px;
+  line-height: 36px;
+  padding-left: 8px;
+}
+.active {
+  background: #f4faff;
+  border-right: 1px solid #0083ff;
+}
+.left {
+  width: 20%;
+  border-right: 1px solid #123ea3;
+  padding-right: 17px;
+  border-radius: 0px;
+  overflow-y: auto;
+}
+.right {
+  height: 100%;
+  overflow-x: auto;
+  display: flex;
+  flex-direction: column;
+}
+.top {
+  width: 100%;
+  height: 42px;
+  border-bottom: 1px solid #eee;
+  padding-left: 17px;
+  display: flex;
+  align-items: start;
+  justify-content: space-between;
+  span {
+    font-size: 20px;
+    color: #333333;
+    font-weight: 600;
+  }
+}
+.empty {
+  width: 100%;
+  flex: 1;
+  height: calc(100vh - 350px);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  font-size: 14px;
+  img {
+    width: 180px;
+  }
+}
+.center {
+  width: 100%;
+  flex: 1;
+  padding: 24px 24px 0px 24px;
+  display: flex;
+  overflow-x: auto;
+  gap: 24px;
+  .rule-item {
+    min-width: 423px;
+    max-width: 423px;
+    height: 100%;
+    border-radius: 4px;
+    border: 1px solid #eeeeee;
+    overflow-y: auto;
+    .item-title {
+      height: 40px;
+      background: #f7f7f9;
+      border-radius: 4px 4px 0px 0px;
+      border-bottom: 1px solid #eeeeee;
+      padding: 4px 16px;
+      margin-bottom: 20px;
+      .name {
+        width: 283px;
+        margin-right: 24px;
+      }
+      img {
+        width: 20px;
+        height: 20px;
+        cursor: pointer;
+      }
+    }
+    p {
+      margin: 0;
+      padding-left: 16px;
+      font-size: 16px;
+    }
+
+    .condition {
+      width: 100%;
+      .section-group {
+      }
+      .condition-one {
+        width: 391px;
+        padding: 12px;
+        border-radius: 4px 4px 4px 4px;
+        border: 1px solid #eeeeee;
+        margin: 10px auto 20px;
+        cursor: pointer;
+      }
+      .dispatch-item {
+        display: flex;
+        align-items: center;
+        margin-bottom: 12px;
+
+        .opera {
+          // width: 62px;
+          height: 22px;
+          background: #f9f9f9;
+          border-radius: 2px 2px 2px 2px;
+          border: 1px solid #eeeeee;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          margin: 0 16px;
+          font-size: 14px;
+          color: #666666;
+          padding: 0 10px;
+        }
+        .value {
+          font-size: 14px;
+        }
+      }
+      .action {
+        width: 391px;
+        margin: 0 auto 10px;
+        min-height: 108px;
+        border-radius: 4px 4px 4px 4px;
+        border: 1px solid #eeeeee;
+        padding: 9px 12px;
+        font-size: 14px;
+        color: #333333;
+        line-height: 22px;
+        cursor: pointer;
+        .name {
+          width: 100%;
+          height: 40px;
+          font-size: 14px;
+          line-height: 40px;
+          border-bottom: 1px solid #eeeeee;
+          margin-bottom: 12px;
+          font-weight: 600;
+        }
+      }
+    }
+    .other {
+      width: 391px;
+      height: 46px;
+      border-radius: 4px 4px 4px 4px;
+      border: 1px solid #eeeeee;
+      font-size: 14px;
+      display: flex;
+      align-items: center;
+      padding-left: 12px;
+      margin: 10px auto;
+      cursor: pointer;
+    }
+  }
+}
+.item-title {
+  display: flex;
+  align-items: center;
+  margin-bottom: 20px;
+  span {
+    display: inline-block;
+    width: 4px;
+    height: 18px;
+    background: #0083ff;
+    border-radius: 10px 10px 10px 10px;
+    margin-right: 8px;
+  }
+}
+.form-item {
+  display: flex;
+  align-items: center;
+  span {
+    font-size: 14px;
+    margin-right: 10px;
+  }
+}
+:deep(.main-container) {
+  height: 100%;
+}
+:deep(.el-row) {
+  height: 100%;
+}
+:deep(.el-drawer__header) {
+  height: 48px;
+  font-size: 20px;
+  color: #333333;
+  border-bottom: 1px solid #eeeeee;
+  margin-bottom: 24px;
+  padding-bottom: 20px;
+}
+:deep(.el-drawer__body) {
+  padding: 0 24px;
+}
+</style>