baijunde 10 månader sedan
förälder
incheckning
319cfd8e59

+ 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


+ 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: '协议调度',
       },

+ 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>

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

@@ -0,0 +1,1040 @@
+<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: {},
+  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
+
+// 表单校验规则
+// 先定义独立校验函数(放在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.conditionGroups[groupIndex].actions.push({
+    key: Date.now(),
+    min: "",
+    attrCode: "",
+    operator: "",
+    max: null,
+    errorMsg: "",
+    minArray: [],
+  });
+};
+
+// 删除组内条件行
+const handleRemoveRow = (groupIndex: number, rowIndex: number) => {
+  formData.conditionGroups[groupIndex].actions.splice(rowIndex, 1);
+};
+
+// 添加新的或者条件组
+const handleAddGroup = () => {
+  formData.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 () => {
+  // if (!formRef.value) return;
+
+ 
+    // await formRef.value.validate();
+    const actionJson = formData.value.actionJson;
+
+    // 构建提交的actions数据
+    // 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 = dispatchId.value
+      ? api.dispatchApi.updateAgreementDispatch
+      : 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 () => {
+  await Promise.all([getList(), getByCompanyId(), getAccountList()]);
+  dictionaries.value = await getDictItems({ dictCode: "dispatch_action" });
+
+  dispatchId.value = props.editData.dispatchId;
+  if (props.editData && Object.keys(props.editData).length > 0) {
+    const rawData = props.editData;
+
+    const conditionGroups: ConditionGroup[] = [];
+    const rawGroups = rawData.groupedAttrLinks
+      ? rawData.groupedAttrLinks
+      : rawData.attrLinks
+      ? [rawData.attrLinks]
+      : [];
+
+    rawGroups.forEach((rawGroup: any[], groupIndex: number) => {
+      const group: ConditionGroup = {
+        groupKey: rawData.dispatchId || Date.now(),
+        actions: [], // 组内"并且"条件行
+      };
+
+      rawGroup.forEach((rawAttr: any) => {
+        // 每个原始条件项对应一个 action 行
+        group.actions.push({
+          key: rawAttr.id || Date.now(),
+          attrCode: rawAttr.attrCode || "",
+          operator: rawAttr.operator || "",
+          min: rawAttr.min ? parseInt(rawAttr.min) : "",
+          max: rawAttr.max ? parseInt(rawAttr.max) : "",
+          // 强制确保minArray为数组(避免undefined导致校验失败)
+          minArray: rawAttr.minArray ? [...rawAttr.minArray] : [],
+          errorMsg: "",
+        });
+      });
+
+      // 确保每个组至少有一行条件(防止空组)
+      if (group.actions.length === 0) {
+        group.actions.push({
+          key: Date.now(),
+          attrCode: "",
+          operator: "",
+          min: "",
+          max: "",
+          errorMsg: "",
+          minArray: [],
+        });
+      }
+
+      conditionGroups.push(group);
+    });
+
+    // 3. 组装最终的formData
+    formData.value = {
+      agreement: "",
+      actionJson: [
+        // 第一个对象:报价协议(对应初始 actionJson[0])
+        {
+          actionCode: "specify_agreement",
+          agreementId: "", // 从编辑数据中取协议ID
+        },
+        // 第二个对象:对接人信息(对应初始 actionJson[1])
+        {
+          actionCode: "",
+          accountId: "",
+          name: "",
+          phone: "",
+        },
+      ],
+      conditionGroups:
+        conditionGroups.length > 0
+          ? conditionGroups
+          : [
+              {
+                groupKey: Date.now(),
+                actions: [
+                  {
+                    key: Date.now(),
+                    attrCode: "",
+                    operator: "",
+                    min: "",
+                    max: "",
+                    errorMsg: "",
+                    minArray: [],
+                  },
+                ],
+              },
+            ],
+    };
+  } else {
+    // 如果没有编辑数据,使用默认初始值
+    formData.value = {
+      agreement: "",
+      actionJson: [
+        // 第一个对象:报价协议(对应初始 actionJson[0])
+        {
+          actionCode: "specify_agreement",
+          agreementId: "", // 从编辑数据中取协议ID
+        },
+        // 第二个对象:对接人信息(对应初始 actionJson[1])
+        {
+          actionCode: "", // 显式添加 actionCode
+          accountId: "",
+          name: "",
+          phone: "",
+        },
+      ],
+      conditionGroups: [
+        {
+          groupKey: Date.now(),
+          actions: [
+            {
+              key: Date.now(),
+              attrCode: "",
+              operator: "",
+              min: "",
+              max: "",
+              errorMsg: "",
+              minArray: [],
+            },
+          ],
+        },
+      ],
+    };
+  }
+  console.log(formData.value);
+});
+</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>

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

@@ -0,0 +1,615 @@
+<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="item.editMode = true"
+                  />
+                  <img
+                    src="@/assets/images/add.png"
+                    style="margin: 0px 12px"
+                    @click="editDispatch(item)"
+                  />
+                  <el-popconfirm
+                    class="box-item"
+                    title="是否确定删除该规则?"
+                    placement="bottom"
+                    width="196px"
+                    confirm-button-text="确定删除"
+                    @confirm="deleteRule(item.dispatchId)"
+                  >
+                    <template #reference>
+                      <img src="@/assets/images/delete.png"
+                    /></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">
+                      <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>
+                    </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">
+                    <div class="name">
+                      {{ item.actions[0].actionName }}
+                    </div>
+                    {{
+                      item.actions[0].actionJson.agreementName ||
+                      "未设置协议名称"
+                    }}
+                  </div>
+                  <div class="action" v-if="item.actions.length > 1">
+                    <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"
+    />
+    <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 handleClose = () => {
+  drawer.value = false;
+  editData.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;
+  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;
+      }
+      .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;
+        .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>