other.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666
  1. <template>
  2. <el-dialog
  3. v-model="dialogVisible"
  4. title="其他情况"
  5. width="704px"
  6. :before-close="handleClose"
  7. align-center
  8. >
  9. <div class="dialog-content">
  10. <div class="left">
  11. <div class="description-text">
  12. 如存在不满足其它规则的情况下,则进入该情况,请选择该情况下要执行的动作
  13. </div>
  14. <div class="section-title">将执行以下动作</div>
  15. <el-form
  16. ref="formRef"
  17. :model="formData"
  18. label-width="auto"
  19. class="action-form"
  20. >
  21. <el-form-item
  22. v-for="(item, index) in formData.actions"
  23. :key="item.keyid"
  24. :prop="`actions.${index}`"
  25. :error="item.actionJson.duplicateError"
  26. >
  27. <el-select
  28. v-model="item.actionCode"
  29. placeholder="请选择动作"
  30. style="width: 276px; margin-right: 9px"
  31. @change="(val) => handleActionChange(val, index)"
  32. @blur="() => checkDuplicate(index)"
  33. >
  34. <el-option
  35. v-for="dictItem in getAvailableActions(index)"
  36. :key="dictItem.value"
  37. :value="dictItem.value"
  38. :label="dictItem.label"
  39. />
  40. </el-select>
  41. <img
  42. src="@/assets/images/delete.png"
  43. alt="删除"
  44. style="width: 18px; height: 18px; cursor: pointer"
  45. @click="deleteItem(index)"
  46. v-if="formData.actions.length > 1"
  47. />
  48. </el-form-item>
  49. </el-form>
  50. <el-button type="text" class="add-action-btn" @click="addItem">
  51. <el-icon><Plus /></el-icon>添加动作
  52. </el-button>
  53. </div>
  54. <div class="right">
  55. <div class="right-section">
  56. <div class="right-title">执行动作</div>
  57. <div class="action-label" v-if="lastActionLabel">
  58. {{ lastActionLabel }}
  59. </div>
  60. </div>
  61. <!-- 指定协议 -->
  62. <div
  63. v-if="
  64. formData.actions.length > 0 &&
  65. lastActionLabel === '报价失败指定协议'
  66. "
  67. style="margin-top: 20px"
  68. >
  69. <div
  70. style="
  71. font-weight: 600;
  72. font-size: 16px;
  73. margin-bottom: 4px;
  74. color: #333333;
  75. "
  76. >
  77. 选择报价协议
  78. </div>
  79. <div style="font-size: 14px; color: #666666; margin-bottom: 12px">
  80. 不满足所有规则的车辆报价,将使用该协议报价
  81. </div>
  82. <el-select
  83. v-model="
  84. formData.actions[formData.actions.length - 1].actionJson
  85. .agreementId
  86. "
  87. placeholder=""
  88. style="width: 304px"
  89. :rules="[
  90. {
  91. required: lastActionLabel === '报价失败指定协议',
  92. message: '请选择报价协议',
  93. trigger: 'blur',
  94. },
  95. ]"
  96. >
  97. <el-option
  98. v-for="item in agreementList"
  99. :key="item.id"
  100. :label="item.agreementName"
  101. :value="item.id"
  102. />
  103. </el-select>
  104. </div>
  105. <!-- 终止报价 -->
  106. <div
  107. v-if="formData.actions.length > 0 && lastActionLabel === '终止报价'"
  108. style="margin-top: 20px"
  109. >
  110. <div
  111. style="
  112. font-weight: 600;
  113. font-size: 16px;
  114. margin-bottom: 4px;
  115. color: #333333;
  116. "
  117. >
  118. 提示文案
  119. </div>
  120. <div style="font-size: 14px; color: #666666; margin-bottom: 12px">
  121. 设置终止该车辆报价的提示文案
  122. </div>
  123. <el-input
  124. v-model="
  125. formData.actions[formData.actions.length - 1].actionJson.message
  126. "
  127. style="width: 100%"
  128. :rows="4"
  129. type="textarea"
  130. placeholder="请输入提示文案"
  131. maxlength="500"
  132. show-word-limit
  133. />
  134. </div>
  135. <!-- 补录订单 -->
  136. <div
  137. v-if="formData.actions.length > 0 && lastActionLabel === '补录订单'"
  138. >
  139. <div style="font-size: 14px; color: #666666; margin-top: 10px">
  140. 终止报价,将报价信息写入补录订单内,生成一笔暂存的补录订单。
  141. </div>
  142. </div>
  143. <!-- 发送通知消息 -->
  144. <div
  145. v-if="
  146. formData.actions.length > 0 && lastActionLabel === '发送通知消息'
  147. "
  148. style="margin-top: 20px"
  149. >
  150. <div
  151. style="
  152. font-weight: 600;
  153. font-size: 16px;
  154. margin-bottom: 4px;
  155. color: #333333;
  156. "
  157. >
  158. 接收人
  159. </div>
  160. <div style="font-size: 14px; color: #666666; margin-bottom: 12px">
  161. 发送通知消息给接收人
  162. </div>
  163. <el-radio-group
  164. v-model="
  165. formData.actions[formData.actions.length - 1].actionJson.type
  166. "
  167. @change="handleTypeChange"
  168. style="
  169. display: flex;
  170. flex-direction: column;
  171. align-items: start;
  172. gap: 12px;
  173. "
  174. >
  175. <el-radio :value="1">业务员的管理人</el-radio>
  176. <el-radio :value="2">
  177. 指定人员
  178. <el-select
  179. v-model="
  180. formData.actions[formData.actions.length - 1].actionJson
  181. .user_id
  182. "
  183. placeholder="请选择人员"
  184. style="width: 204px; margin-left: 20px"
  185. clearable
  186. :disabled="
  187. formData.actions[formData.actions.length - 1].actionJson
  188. .type !== 2
  189. "
  190. >
  191. <el-option
  192. v-for="item in followerList"
  193. :key="item.id"
  194. :label="item.name"
  195. :value="item.id"
  196. />
  197. </el-select>
  198. </el-radio>
  199. </el-radio-group>
  200. <div style="font-size: 14px; margin: 20px 0px 10px 0px">标题</div>
  201. <div>
  202. <el-input
  203. v-model="
  204. formData.actions[formData.actions.length - 1].actionJson.title
  205. "
  206. style="width: 100%"
  207. placeholder="请输入标题"
  208. maxlength="50"
  209. show-word-limit
  210. />
  211. </div>
  212. <div style="font-size: 14px; margin: 20px 0px 10px 0px">内容</div>
  213. <el-input
  214. v-model="
  215. formData.actions[formData.actions.length - 1].actionJson.content
  216. "
  217. style="width: 100%"
  218. :rows="4"
  219. type="textarea"
  220. placeholder="请输入通知内容"
  221. maxlength="500"
  222. show-word-limit
  223. />
  224. </div>
  225. </div>
  226. </div>
  227. <template #footer>
  228. <div class="dialog-footer">
  229. <el-button class="border-[#0083FF] color-[#0083FF]" plain @click="(dialogVisible = false), emit('close')"
  230. >取消</el-button
  231. >
  232. <el-button type="primary" @click="handleSave">保存</el-button>
  233. </div>
  234. </template>
  235. </el-dialog>
  236. </template>
  237. <script lang="ts" setup>
  238. import { ref, onMounted, ElRef, watch, computed, defineProps } from "vue";
  239. import { ElMessageBox, ElForm, ElMessage } from "element-plus";
  240. import { Plus } from "@element-plus/icons-vue";
  241. import api from "@/api";
  242. import { getDictItems } from "@/api/dict";
  243. // 对话框显示状态
  244. const dialogVisible = ref(true);
  245. //协议列表,调度id
  246. const props = defineProps<{
  247. companyId: any;
  248. }>();
  249. // 表单引用
  250. const formRef = ref<ElRef<ElForm>>();
  251. const agreementList = ref([]); //协议列表
  252. // 表单数据:actions数组存储所有动作,每个动作包含完整字段
  253. const formData = ref({
  254. actions: [
  255. {
  256. companyId: props.companyId,
  257. keyid: generateId(),
  258. actionCode: "",
  259. actionJson: {
  260. agreementId: "", // 动作ID(关联字典)
  261. message: "", // 终止报价的提示文案
  262. type: null, // 通知接收人类型(1=业务员管理人,2=指定人员)
  263. content: "", // 通知内容
  264. user_id: null, // 指定人员ID
  265. title: "",
  266. duplicateError: "", // 重复选择错误提示
  267. },
  268. },
  269. ],
  270. });
  271. // 字典数据(动作列表,如“终止报价”“发送通知消息”等)
  272. const dictionaries = ref([]);
  273. // 从 dictionaries 数据中获取“指定协议”“终止报价”对应的 id
  274. const MUTUALLY_EXCLUSIVE_ACTIONS = {
  275. agreement: "414", // 假设“指定协议”的字典ID
  276. terminate: "415", // 假设“终止报价”的字典ID
  277. };
  278. // 最后一项动作的标签(用于右侧配置区显示)
  279. const lastActionLabel = ref("");
  280. // 后线人员列表(用于“指定人员”选择)
  281. const followerList = ref([]);
  282. /**
  283. * 生成唯一ID(避免列表key重复)
  284. * @returns 唯一ID字符串
  285. */
  286. function generateId(): string {
  287. return Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
  288. }
  289. /**
  290. * 计算属性:根据当前已选动作,生成“可选动作列表”(排除其他行已选,保留当前行)
  291. */
  292. const getAvailableActions = computed(() => {
  293. return (currentIndex: number) => {
  294. // 收集其他行已选的动作值(dictItem.value)
  295. const selectedValues = formData.value.actions
  296. .filter((_, idx) => idx !== currentIndex)
  297. .map((item) => item.actionCode)
  298. .filter(Boolean);
  299. // 筛选字典:排除已被其他行选择的动作(用value字段判断)
  300. return dictionaries.value.filter((dictItem) => !selectedValues.includes(dictItem.value));
  301. };
  302. });
  303. /**
  304. * 更新最后一项动作的标签(右侧配置区基于此显示对应内容)
  305. */
  306. function updateLastAction() {
  307. if (formData.value.actions.length === 0) return;
  308. // 获取最后一个动作的选中值(即选择框绑定的dictItem.value)
  309. const lastAction = formData.value.actions[formData.value.actions.length - 1];
  310. // 关键修复:用字典项的value字段匹配actionCode
  311. const matchedAction = dictionaries.value.find(
  312. (dictItem: any) => dictItem.value === lastAction.actionCode
  313. );
  314. lastActionLabel.value = matchedAction ? matchedAction.label : "";
  315. // 重置无关字段(保持不变)
  316. if (lastActionLabel.value !== "发送通知消息") {
  317. lastAction.actionJson.type = null;
  318. lastAction.actionJson.user_id = null;
  319. lastAction.actionJson.content = "";
  320. lastAction.actionJson.title = "";
  321. }
  322. }
  323. /**
  324. * 校验单个动作是否重复(并标记错误)
  325. * @param index 当前动作行索引
  326. */
  327. const checkDuplicate = (index: number) => {
  328. const currentAction = formData.value.actions[index];
  329. if (!currentAction.actionCode) {
  330. currentAction.actionJson.duplicateError = "";
  331. return;
  332. }
  333. // 统计当前动作值(value)的出现次数
  334. const duplicateCount = formData.value.actions.filter(
  335. (item) => item.actionCode === currentAction.actionCode
  336. ).length;
  337. if (duplicateCount > 1) {
  338. // 用value字段查找动作标签
  339. const actionLabel = dictionaries.value.find(
  340. (dictItem) => dictItem.value === currentAction.actionCode
  341. )?.label;
  342. currentAction.actionJson.duplicateError = `“${actionLabel}”已选择,请更换其他动作`;
  343. } else {
  344. currentAction.actionJson.duplicateError = "";
  345. }
  346. };
  347. /**
  348. * 处理动作选择变化(立即校验重复)
  349. * @param val 当前选择的动作ID
  350. * @param index 当前动作行索引
  351. */
  352. const handleActionChange = (val: string, index: number) => {
  353. const { agreement, terminate } = MUTUALLY_EXCLUSIVE_ACTIONS;
  354. // 用选择框的value(val)判断是否为互斥动作
  355. const isMutualAction = [agreement, terminate].includes(val);
  356. if (isMutualAction) {
  357. const otherMutualAction = val === agreement ? terminate : agreement;
  358. // 检查其他行是否包含互斥动作的value
  359. const hasOtherMutual = formData.value.actions.some(
  360. (item, idx) => idx !== index && item.actionCode === otherMutualAction
  361. );
  362. if (hasOtherMutual) {
  363. ElMessage.error("指定报价协议 与 终止报价,只能选择其中1个");
  364. formData.value.actions[index].actionCode = "";
  365. formData.value.actions[index].actionJson.duplicateError = "";
  366. return;
  367. }
  368. }
  369. checkDuplicate(index);
  370. formData.value.actions.forEach((_, idx) => idx !== index && checkDuplicate(idx));
  371. updateLastAction();
  372. };
  373. /**
  374. * 批量校验所有动作是否重复(用于保存前最终校验)
  375. * @returns boolean 无重复返回true,有重复返回false
  376. */
  377. const checkAllDuplicate = () => {
  378. let hasDuplicate = false;
  379. // 1. 收集所有已选动作ID
  380. const selectedIds = formData.value.actions
  381. .map((item) => item.actionCode)
  382. .filter(Boolean);
  383. // 2. 检查是否有重复ID
  384. const uniqueIds = [...new Set(selectedIds)];
  385. if (selectedIds.length !== uniqueIds.length) {
  386. hasDuplicate = true;
  387. // 3. 标记所有重复行的错误提示
  388. formData.value.actions.forEach((item, index) => {
  389. if (
  390. item.actionCode &&
  391. selectedIds.filter((id) => id === item.actionCode).length > 1
  392. ) {
  393. checkDuplicate(index); // 复用单个校验逻辑,标记错误
  394. }
  395. });
  396. // 4. 弹出全局提示,引导用户修改
  397. ElMessage.warning("存在重复选择的动作,请先修改后再保存");
  398. }
  399. return !hasDuplicate;
  400. };
  401. /**
  402. * 添加新的动作行
  403. */
  404. const addItem = () => {
  405. formData.value.actions.push({
  406. keyid: generateId(),
  407. actionCode: "",
  408. companyId: props.companyId,
  409. actionJson: {
  410. agreementId: "",
  411. message: "",
  412. type: null,
  413. content: "",
  414. user_id: null,
  415. title: "",
  416. duplicateError: "",
  417. },
  418. });
  419. // 添加后更新最后一项动作的显示
  420. updateLastAction();
  421. };
  422. /**
  423. * 删除指定索引的动作行(至少保留1行)
  424. * @param index 要删除的行索引
  425. */
  426. const deleteItem = (index: number) => {
  427. if (formData.value.actions.length <= 1) {
  428. ElMessageBox.alert("至少需要保留一个动作", "提示", {
  429. confirmButtonText: "确定",
  430. });
  431. return;
  432. }
  433. // 1. 记录被删除行的动作ID(用于后续校验)
  434. const deletedActionId = formData.value.actions[index].actionCode;
  435. // 2. 删除指定行
  436. formData.value.actions.splice(index, 1);
  437. // 3. 重新校验剩余行:若删除的是重复ID,清除其他行的重复标记
  438. if (deletedActionId) {
  439. formData.value.actions.forEach((_, idx) => checkDuplicate(idx));
  440. }
  441. // 原有逻辑:更新右侧配置区
  442. updateLastAction();
  443. };
  444. /**
  445. * 处理通知接收人类型变化:选择1时清空指定人员
  446. * @param value 接收人类型(1=业务员管理人,2=指定人员)
  447. */
  448. const handleTypeChange = (value: number) => {
  449. const lastAction = formData.value.actions[formData.value.actions.length - 1];
  450. // 选择“业务员的管理人”(value=1)时,清空指定人员选择
  451. if (value === 1) {
  452. lastAction.actionJson.user_id = null;
  453. }
  454. };
  455. /**
  456. * 处理对话框关闭(未保存时提示)
  457. * @param done 关闭回调函数
  458. */
  459. const handleClose = (done: () => void) => {
  460. emit("close");
  461. };
  462. /**
  463. * 获取后线人员列表(接口请求)
  464. */
  465. const getUserList = async () => {
  466. try {
  467. const res = await api.dispatchApi.getUserList({ isMember: "1" });
  468. return res?.data || []; // 按实际接口结构调整数据路径
  469. } catch (error) {
  470. console.error("获取后线人员列表失败:", error);
  471. return [];
  472. }
  473. };
  474. // 获取报价协议列表
  475. const getByCompanyId = async () => {
  476. try {
  477. const res = await api.dispatchApi.getByCompanyIdt(props.companyId);
  478. agreementList.value = res.data;
  479. } catch (error) {
  480. console.error("获取报价协议列表失败:", error);
  481. }
  482. };
  483. /**
  484. * 处理保存操作(含完整验证逻辑)
  485. */
  486. const handleSave = async () => {
  487. // 校验报价协议是否选择
  488. const hasEmptyAgreement = formData.value.actions.some((item) => {
  489. const matchedAction = dictionaries.value.find(
  490. (dictItem) => dictItem.id === item.actionCode
  491. );
  492. return (
  493. matchedAction?.label === "报价失败指定协议" && !item.actionJson.agreementId
  494. );
  495. });
  496. if (hasEmptyAgreement) {
  497. ElMessage.error("请选择报价协议");
  498. return;
  499. }
  500. // 校验所有动作是否重复
  501. if (!checkAllDuplicate()) {
  502. return;
  503. }
  504. // 校验通过,执行保存逻辑
  505. try {
  506. const saveRes = await api.dispatchApi.addAgreementDispatchAction(
  507. formData.value.actions
  508. );
  509. if (saveRes.code === 200) {
  510. ElMessage.success("保存成功");
  511. emit("close");
  512. }
  513. } catch (error) {
  514. console.error("保存失败:", error);
  515. ElMessage.error("保存失败,请稍后重试");
  516. }
  517. };
  518. const emit = defineEmits(["close"]);
  519. /**
  520. * 组件挂载时加载初始化数据(字典+人员列表)
  521. */
  522. onMounted(async () => {
  523. const [dictData, userList] = await Promise.all([
  524. getDictItems({ dictCode: "dispatch_after_action" }), // 动作字典
  525. getUserList(), // 后线人员列表
  526. ]);
  527. dictionaries.value = dictData || [];
  528. followerList.value = userList;
  529. // formData.value.companyId = props.companyId;
  530. console.log( props.companyId);
  531. });
  532. /**
  533. * 监听动作列表变化,自动更新最后一项动作的显示
  534. */
  535. watch(
  536. () => formData.value.actions,
  537. () => updateLastAction(),
  538. { deep: true }
  539. );
  540. </script>
  541. <style lang="scss" scoped>
  542. .dialog-content {
  543. display: flex;
  544. width: 100%;
  545. gap: 24px;
  546. }
  547. .left {
  548. width: 50%;
  549. border-right: 1px solid #eeeeee;
  550. padding-right: 8px;
  551. }
  552. .right {
  553. width: 50%;
  554. padding-left: 8px;
  555. }
  556. .right-section {
  557. width: 100%;
  558. }
  559. .right-title {
  560. font-size: 16px;
  561. color: #333;
  562. font-weight: 600;
  563. margin-bottom: 10px;
  564. }
  565. .action-label {
  566. font-size: 14px;
  567. color: #666;
  568. width: 100%;
  569. border-radius: 4px;
  570. border: 1px solid #e5e5e5;
  571. height: 36px;
  572. line-height: 36px;
  573. padding: 0 12px;
  574. margin-top: 10px;
  575. background-color: #fff;
  576. }
  577. .description-text {
  578. font-size: 14px;
  579. color: #666666;
  580. line-height: 1.5;
  581. margin-bottom: 20px;
  582. }
  583. .section-title {
  584. font-size: 16px;
  585. margin-top: 8px;
  586. margin-bottom: 16px;
  587. color: #333;
  588. font-weight: 500;
  589. }
  590. .action-form {
  591. margin-bottom: 16px;
  592. }
  593. .add-action-btn {
  594. margin-top: 8px;
  595. color: #409eff;
  596. &:hover {
  597. color: #66b1ff;
  598. }
  599. }
  600. ::v-deep .el-radio {
  601. display: flex;
  602. align-items: center;
  603. }
  604. ::v-deep .el-form-item {
  605. margin-bottom: 12px;
  606. }
  607. .dialog-footer {
  608. display: flex;
  609. justify-content: flex-end;
  610. gap: 12px;
  611. }
  612. </style>