Преглед изворни кода

sop阶段、节点新增转发人工

kyoyue пре 5 месеци
родитељ
комит
414c1e1f38

+ 3 - 3
internal/eslint-config/dist/index.cjs

@@ -1,7 +1,7 @@
-module.exports = require("/Users/songbowen/development/Project/gooki/scrm/wechat-ui/node_modules/.pnpm/jiti@1.21.6/node_modules/jiti/lib/index.js")(null, {
+module.exports = require("/Users/wanganyue/Desktop/wechat-ui/node_modules/.pnpm/jiti@1.21.6/node_modules/jiti/lib/index.js")(null, {
   "esmResolve": true,
   "interopDefault": true,
   "alias": {
-    "@vben/eslint-config": "/Users/songbowen/development/Project/gooki/scrm/wechat-ui/internal/eslint-config"
+    "@vben/eslint-config": "/Users/wanganyue/Desktop/wechat-ui/internal/eslint-config"
   }
-})("/Users/songbowen/development/Project/gooki/scrm/wechat-ui/internal/eslint-config/src/index.ts")
+})("/Users/wanganyue/Desktop/wechat-ui/internal/eslint-config/src/index.ts")

+ 2 - 2
internal/eslint-config/dist/index.d.ts

@@ -1,2 +1,2 @@
-export * from "/Users/songbowen/development/Project/gooki/scrm/wechat-ui/internal/eslint-config/src/index";
-export { default } from "/Users/songbowen/development/Project/gooki/scrm/wechat-ui/internal/eslint-config/src/index";
+export * from "/Users/wanganyue/Desktop/wechat-ui/internal/eslint-config/src/index";
+export { default } from "/Users/wanganyue/Desktop/wechat-ui/internal/eslint-config/src/index";

+ 4 - 4
internal/eslint-config/dist/index.mjs

@@ -1,12 +1,12 @@
-import jiti from "file:///Users/songbowen/development/Project/gooki/scrm/wechat-ui/node_modules/.pnpm/jiti@1.21.6/node_modules/jiti/lib/index.js";
+import jiti from "file:///Users/wanganyue/Desktop/wechat-ui/node_modules/.pnpm/jiti@1.21.6/node_modules/jiti/lib/index.js";
 
-/** @type {import("/Users/songbowen/development/Project/gooki/scrm/wechat-ui/internal/eslint-config/src/index")} */
+/** @type {import("/Users/wanganyue/Desktop/wechat-ui/internal/eslint-config/src/index")} */
 const _module = jiti(null, {
   "esmResolve": true,
   "interopDefault": true,
   "alias": {
-    "@vben/eslint-config": "/Users/songbowen/development/Project/gooki/scrm/wechat-ui/internal/eslint-config"
+    "@vben/eslint-config": "/Users/wanganyue/Desktop/wechat-ui/internal/eslint-config"
   }
-})("/Users/songbowen/development/Project/gooki/scrm/wechat-ui/internal/eslint-config/src/index.ts");
+})("/Users/wanganyue/Desktop/wechat-ui/internal/eslint-config/src/index.ts");
 
 export default _module;

+ 3 - 3
internal/eslint-config/dist/strict.cjs

@@ -1,7 +1,7 @@
-module.exports = require("/Users/songbowen/development/Project/gooki/scrm/wechat-ui/node_modules/.pnpm/jiti@1.21.6/node_modules/jiti/lib/index.js")(null, {
+module.exports = require("/Users/wanganyue/Desktop/wechat-ui/node_modules/.pnpm/jiti@1.21.6/node_modules/jiti/lib/index.js")(null, {
   "esmResolve": true,
   "interopDefault": true,
   "alias": {
-    "@vben/eslint-config": "/Users/songbowen/development/Project/gooki/scrm/wechat-ui/internal/eslint-config"
+    "@vben/eslint-config": "/Users/wanganyue/Desktop/wechat-ui/internal/eslint-config"
   }
-})("/Users/songbowen/development/Project/gooki/scrm/wechat-ui/internal/eslint-config/src/strict.ts")
+})("/Users/wanganyue/Desktop/wechat-ui/internal/eslint-config/src/strict.ts")

+ 2 - 2
internal/eslint-config/dist/strict.d.ts

@@ -1,2 +1,2 @@
-export * from "/Users/songbowen/development/Project/gooki/scrm/wechat-ui/internal/eslint-config/src/strict";
-export { default } from "/Users/songbowen/development/Project/gooki/scrm/wechat-ui/internal/eslint-config/src/strict";
+export * from "/Users/wanganyue/Desktop/wechat-ui/internal/eslint-config/src/strict";
+export { default } from "/Users/wanganyue/Desktop/wechat-ui/internal/eslint-config/src/strict";

+ 4 - 4
internal/eslint-config/dist/strict.mjs

@@ -1,12 +1,12 @@
-import jiti from "file:///Users/songbowen/development/Project/gooki/scrm/wechat-ui/node_modules/.pnpm/jiti@1.21.6/node_modules/jiti/lib/index.js";
+import jiti from "file:///Users/wanganyue/Desktop/wechat-ui/node_modules/.pnpm/jiti@1.21.6/node_modules/jiti/lib/index.js";
 
-/** @type {import("/Users/songbowen/development/Project/gooki/scrm/wechat-ui/internal/eslint-config/src/strict")} */
+/** @type {import("/Users/wanganyue/Desktop/wechat-ui/internal/eslint-config/src/strict")} */
 const _module = jiti(null, {
   "esmResolve": true,
   "interopDefault": true,
   "alias": {
-    "@vben/eslint-config": "/Users/songbowen/development/Project/gooki/scrm/wechat-ui/internal/eslint-config"
+    "@vben/eslint-config": "/Users/wanganyue/Desktop/wechat-ui/internal/eslint-config"
   }
-})("/Users/songbowen/development/Project/gooki/scrm/wechat-ui/internal/eslint-config/src/strict.ts");
+})("/Users/wanganyue/Desktop/wechat-ui/internal/eslint-config/src/strict.ts");
 
 export default _module;

+ 3 - 3
internal/stylelint-config/dist/index.cjs

@@ -1,7 +1,7 @@
-module.exports = require("/Users/songbowen/development/Project/gooki/scrm/wechat-ui/node_modules/.pnpm/jiti@1.21.6/node_modules/jiti/lib/index.js")(null, {
+module.exports = require("/Users/wanganyue/Desktop/wechat-ui/node_modules/.pnpm/jiti@1.21.6/node_modules/jiti/lib/index.js")(null, {
   "esmResolve": true,
   "interopDefault": true,
   "alias": {
-    "@vben/stylelint-config": "/Users/songbowen/development/Project/gooki/scrm/wechat-ui/internal/stylelint-config"
+    "@vben/stylelint-config": "/Users/wanganyue/Desktop/wechat-ui/internal/stylelint-config"
   }
-})("/Users/songbowen/development/Project/gooki/scrm/wechat-ui/internal/stylelint-config/src/index.ts")
+})("/Users/wanganyue/Desktop/wechat-ui/internal/stylelint-config/src/index.ts")

+ 2 - 2
internal/stylelint-config/dist/index.d.ts

@@ -1,2 +1,2 @@
-export * from "/Users/songbowen/development/Project/gooki/scrm/wechat-ui/internal/stylelint-config/src/index";
-export { default } from "/Users/songbowen/development/Project/gooki/scrm/wechat-ui/internal/stylelint-config/src/index";
+export * from "/Users/wanganyue/Desktop/wechat-ui/internal/stylelint-config/src/index";
+export { default } from "/Users/wanganyue/Desktop/wechat-ui/internal/stylelint-config/src/index";

+ 4 - 4
internal/stylelint-config/dist/index.mjs

@@ -1,12 +1,12 @@
-import jiti from "file:///Users/songbowen/development/Project/gooki/scrm/wechat-ui/node_modules/.pnpm/jiti@1.21.6/node_modules/jiti/lib/index.js";
+import jiti from "file:///Users/wanganyue/Desktop/wechat-ui/node_modules/.pnpm/jiti@1.21.6/node_modules/jiti/lib/index.js";
 
-/** @type {import("/Users/songbowen/development/Project/gooki/scrm/wechat-ui/internal/stylelint-config/src/index")} */
+/** @type {import("/Users/wanganyue/Desktop/wechat-ui/internal/stylelint-config/src/index")} */
 const _module = jiti(null, {
   "esmResolve": true,
   "interopDefault": true,
   "alias": {
-    "@vben/stylelint-config": "/Users/songbowen/development/Project/gooki/scrm/wechat-ui/internal/stylelint-config"
+    "@vben/stylelint-config": "/Users/wanganyue/Desktop/wechat-ui/internal/stylelint-config"
   }
-})("/Users/songbowen/development/Project/gooki/scrm/wechat-ui/internal/stylelint-config/src/index.ts");
+})("/Users/wanganyue/Desktop/wechat-ui/internal/stylelint-config/src/index.ts");
 
 export default _module;

+ 3 - 3
internal/vite-config/dist/index.cjs

@@ -1,7 +1,7 @@
-module.exports = require("/Users/songbowen/development/Project/gooki/scrm/wechat-ui/node_modules/.pnpm/jiti@1.21.6/node_modules/jiti/lib/index.js")(null, {
+module.exports = require("/Users/wanganyue/Desktop/wechat-ui/node_modules/.pnpm/jiti@1.21.6/node_modules/jiti/lib/index.js")(null, {
   "esmResolve": true,
   "interopDefault": true,
   "alias": {
-    "@vben/vite-config": "/Users/songbowen/development/Project/gooki/scrm/wechat-ui/internal/vite-config"
+    "@vben/vite-config": "/Users/wanganyue/Desktop/wechat-ui/internal/vite-config"
   }
-})("/Users/songbowen/development/Project/gooki/scrm/wechat-ui/internal/vite-config/src/index.ts")
+})("/Users/wanganyue/Desktop/wechat-ui/internal/vite-config/src/index.ts")

+ 1 - 1
internal/vite-config/dist/index.d.ts

@@ -1 +1 @@
-export * from "/Users/songbowen/development/Project/gooki/scrm/wechat-ui/internal/vite-config/src/index";
+export * from "/Users/wanganyue/Desktop/wechat-ui/internal/vite-config/src/index";

+ 4 - 4
internal/vite-config/dist/index.mjs

@@ -1,13 +1,13 @@
-import jiti from "file:///Users/songbowen/development/Project/gooki/scrm/wechat-ui/node_modules/.pnpm/jiti@1.21.6/node_modules/jiti/lib/index.js";
+import jiti from "file:///Users/wanganyue/Desktop/wechat-ui/node_modules/.pnpm/jiti@1.21.6/node_modules/jiti/lib/index.js";
 
-/** @type {import("/Users/songbowen/development/Project/gooki/scrm/wechat-ui/internal/vite-config/src/index")} */
+/** @type {import("/Users/wanganyue/Desktop/wechat-ui/internal/vite-config/src/index")} */
 const _module = jiti(null, {
   "esmResolve": true,
   "interopDefault": true,
   "alias": {
-    "@vben/vite-config": "/Users/songbowen/development/Project/gooki/scrm/wechat-ui/internal/vite-config"
+    "@vben/vite-config": "/Users/wanganyue/Desktop/wechat-ui/internal/vite-config"
   }
-})("/Users/songbowen/development/Project/gooki/scrm/wechat-ui/internal/vite-config/src/index.ts");
+})("/Users/wanganyue/Desktop/wechat-ui/internal/vite-config/src/index.ts");
 
 export const defineApplicationConfig = _module.defineApplicationConfig;
 export const definePackageConfig = _module.definePackageConfig;

+ 13 - 0
src/api/wechat/sopTask.ts

@@ -27,6 +27,7 @@ enum Api {
   GetSopTaskDetailById = '/wechat-api/sop_task/detail',
   GetSopTaskRecordList = '/wechat-api/sop_task/record_list',
   GetSopTaskRecordMsg = '/wechat-api/message_records/list',
+  CopySopTask = '/wechat-api/sop_task/copy',
 }
 
 /**
@@ -306,4 +307,16 @@ export const getSopTaskRecordMsg = (params: BaseNodeReq, mode: ErrorMessageMode
       errorMessageMode: mode,
     },
   );
+};
+
+/**
+ *  @description: 复制sop任务
+ */
+export const copySopTask = (params: BaseNodeReq, mode: ErrorMessageMode = 'notice') => {
+  return defHttp.post<BaseDataResp<BaseNodeReq>>(
+    { url: Api.CopySopTask, params: params },
+    {
+      errorMessageMode: mode,
+    },
+  );
 };

+ 30 - 1
src/views/wechat/sop_task/add_sop/components/flowChart/components/nodeWrap.vue

@@ -19,6 +19,7 @@
                         <MenuItem @click="setNode(item, 'del')" key="del">删除</MenuItem>
                         <!-- <MenuItem @click="setNode(item, 'copy')" key="copy">复制</MenuItem> -->
                         <MenuItem @click="setNode(item, 'add')" key="add">添加节点</MenuItem>
+                        <!-- <MenuItem @click="setNode(item, 'debug')" key="debug">调试</MenuItem> -->
                       </Menu>
                     </template>
                   </Dropdown>
@@ -62,6 +63,32 @@
       <BlankBox />
     </div>
   </div>
+  <!-- <Modal
+      width="500px"
+      height="300px"
+      v-model:open="modalVisible"
+      title="待调试节点:打招呼"
+      @ok="handleOk"
+      @cancel="handleCancel"
+      class="custom-modal"
+    >
+      <Form :model="form" layout="inline" style="gap: 20px;height:auto;">
+        <FormItem name="id" label="用户回复:" style="margin-left: 30px">
+          <Select
+            v-model:value="form.id"
+            :options="modeList"
+            allowClear
+            size="middle"
+            placeholder="请选择"
+            :style="{ width: '330px', margin: '0 5px' }"
+          ></Select>
+          <Button type="primary">调试</Button>
+        </FormItem>
+        <FormItem name="name" label="结果触发:" style="margin-left: 30px" >
+          <Tag>Tag 1</Tag>
+        </FormItem>
+      </Form>
+    </Modal> -->
   <!-- <nodeWrap v-if="nodeConfig.childNode" v-model:nodeConfig="nodeConfig.childNode" @update:nodeDrawerOpen="addType(item)"/> -->
 </template>
 <script setup lang="ts">
@@ -73,7 +100,7 @@
     defineEmits,
   } from 'vue';
   import { SettingOutlined } from '@ant-design/icons-vue';
-  import { Dropdown, Menu, MenuItem, Button} from 'ant-design-vue';
+  import { Dropdown, Menu, MenuItem, Button,Modal,Tag} from 'ant-design-vue';
   import nodeWrap from './nodeWrap.vue';
   import BlankBox from './blankBox.vue';
   import { useStore } from '../../../stores/index';
@@ -82,6 +109,8 @@
     Dropdown,
     Menu,
     MenuItem,
+    Modal,
+    Tag,
   });
   let emit = defineEmits<{
     (event: 'update:nodeDrawerOpen', value: { open: boolean; nodeId: number; isAdd: boolean }): void;

+ 13 - 5
src/views/wechat/sop_task/add_sop/components/leftStage.vue

@@ -338,7 +338,7 @@
     let data = await getSopStageDetailById({ id: payload.id });
     stageName.value = data.data.name;
     stageType.value = data.data.conditionType === 1 ? '客户标签' : '客户基本信息';
-    if (data.data && data.data.actionLabel.length > 0) {
+    if (data.data && data.data.actionLabelAdd.length > 0) {
       stageTask.value.push({ name: '打标签' });
     }
 
@@ -349,6 +349,14 @@
         stageTask.value.push({ name: '发消息' });
       }
     }
+     
+    if (data.data && data.data.actionForward.action.length > 0) {
+      const taskContant = data.data.actionForward.action.every((item) => item.content.trim() != '');
+      console.log(taskContant,stageTask.value,'stageTask.value')
+      if (taskContant) {
+        stageTask.value.push({ name: '转发人工' });
+      }
+    }
   }
   // const handleSopStageListUpdate = (newValue) => {
   //   console.log(newValue, 'newValue');
@@ -441,8 +449,8 @@
             stageType.value = result.data.conditionType === 1 ? '客户标签' : '客户基本信息';
             if (
               result.data &&
-              Array.isArray(result.data.actionLabel) &&
-              result.data.actionLabel.length > 0
+              Array.isArray(result.data.actionLabelAdd) &&
+              result.data.actionLabelAdd.length > 0
             ) {
               stageTask.value.push({ name: '打标签' });
             }
@@ -468,8 +476,8 @@
           stageType.value = result.data.conditionType === 1 ? '客户标签' : '客户基本信息';
           if (
             result.data &&
-            Array.isArray(result.data.actionLabel) &&
-            result.data.actionLabel.length > 0
+            Array.isArray(result.data.actionLabelAdd) &&
+            result.data.actionLabelAdd.length > 0
           ) {
             stageTask.value.push({ name: '打标签' });
           }

+ 45 - 4
src/views/wechat/sop_task/add_sop/components/msgContant.vue

@@ -13,6 +13,7 @@
             :maxlength="1000"
             @focus="handleFocus"
             @blur="handleBlur"
+            @input="updateCursorPosition($event, index)"
           />
           <!-- <HtmlTextarea v-model="taskForm.messageContent" :rows="4" :maxlength="1000" @focus="handleFocus"
                     @blur="handleBlur" /> -->
@@ -50,13 +51,19 @@
           <!-- @remove="emit('remove')" @moveUp="emit('moveUp')" @moveDown="emit('moveDown')":length="length" -->
           <CustomIcons :index="index" :iconDisable="msgDisabled" />
           <span v-show="area.content == ''" class="warning-style">请输入文本内容</span>
+          <div style="margin-bottom:5px">
+            <span style="margin-right: 10px;">变量:</span>
+            <a-button style="margin-right: 10px;" :disabled="msgDisabled" @click="insertVariable('${客户名称}', index)">客户名称</a-button>
+            <a-button style="margin-right: 10px;" :disabled="msgDisabled" @click="insertVariable('${客户称谓}', index)">客户称谓</a-button>
+            <a-button @click="insertVariable('${客户手机号}', index)" :disabled="msgDisabled">客户手机号</a-button>
+          </div>
         </div>
       </a-form-item>
       <a-form-item v-else-if="area.type === 2" label="" name="content" class="message-content">
         <!-- class="upload-contant" -->
         <div :class="msgDisabled ? 'disabled-upload upload-contant' : 'upload-contant'">
         <BasicUpload 
-          :maxSize="100"
+          :maxSize="10"
           :maxNumber="1"
           @change="handleChange"
           :api="uploadApi"
@@ -64,7 +71,7 @@
           :showPreviewNumber=false
           emptyHidePreview
         />
-        <div class="upload-file-style" v-if="area.content">{{String(area.content).split('/').pop()}}</div>
+        <!-- <div class="upload-file-style" v-if="area.content">{{String(area.content).split('/').pop()}}</div> -->
           <CustomIcons :index="index" :length="length" :iconDisable="msgDisabled" />
         </div>
       </a-form-item>
@@ -105,7 +112,7 @@
   </div>
 </template>
 <script lang="ts">
-  import { defineComponent, ref, reactive, provide, props, watch } from 'vue';
+  import { defineComponent, ref, reactive, provide, props, watch ,nextTick} from 'vue';
   import { Form, Button, Dropdown, Menu, message, Input, SubMenu, Upload } from 'ant-design-vue';
   import {
     CloudUploadOutlined,
@@ -156,6 +163,13 @@
       // const internalValue = ref([...props.value]);
       const internalValue = ref(addCanMoveProperties(props.value));
       const msgDisabled = ref(props.msgDisabled);
+       // 用于存储每个 textarea 的光标位置
+      const cursorPositions = ref<Record<number, number>>({});
+      // 更新光标位置
+      const updateCursorPosition = (event: Event, index: number) => {
+        const textarea = event.target as HTMLTextAreaElement;
+        cursorPositions.value[index] = textarea.selectionStart;
+      };
       const taskForm = reactive({
         fileContent: '',
       });
@@ -164,7 +178,7 @@
           return
         }
           // createMessage.info(`已上传文件${JSON.stringify(list)}`);
-          console.log(list);
+          // console.log(list);
         };
       watch(
         internalValue,
@@ -174,6 +188,30 @@
         { deep: true },
       );
 
+      // 插入变量函数
+      const insertVariable = (variable: string, index: number) => {
+        const area = internalValue.value[index];
+        if (!area) {
+          console.error(`Invalid index ${index}: No content found.`);
+          return;
+        }
+        const cursorPosition = cursorPositions.value[index] || 0;
+        const content = area.content || '';
+
+        // 在光标位置插入变量
+        internalValue.value[index].content = 
+          content.slice(0, cursorPosition) + variable + content.slice(cursorPosition);
+
+        // 更新光标位置为插入变量之后
+        nextTick(() => {
+          const newPosition = cursorPosition + variable.length;
+          const textarea = document.querySelectorAll('.custom-textarea')[index] as HTMLTextAreaElement;
+          if (textarea) {
+            textarea.setSelectionRange(newPosition, newPosition);
+            // textarea.focus();
+          }
+        });
+      };
       function addCanMoveProperties(items) {
         return items.map((item, index, array) => ({
           ...item,
@@ -314,6 +352,9 @@
         internalValue,
         updateValue,
         addCanMoveProperties,
+        cursorPositions,
+        updateCursorPosition,
+        insertVariable,
       };
     },
   });

+ 488 - 0
src/views/wechat/sop_task/add_sop/components/msgContantForwards.vue

@@ -0,0 +1,488 @@
+<!-- 消息内容 -->
+<template>
+  <div>
+    <div v-for="(area, index) in internalValue" :key="index">
+      <!-- 转发人工文本内容  -->
+      <!-- <a-form-item
+       v-if="area.type === 1"
+        label="转发至"
+        name="area.wxid"
+        :rules="[{ required: true, message: '请填写接收人微信id' }]"
+        style="margin:10px;"
+      >
+        <a-input
+          :disabled="btnDisabled"
+          type="text"
+          v-model:value="area.wxid"
+          placeholder="请输入接收人微信id"
+        />
+      </a-form-item> -->
+      <a-form-item label="" v-if="area.type === 1" name="content" class="message-content">
+        <div class="custom-placeholder-wrapper">
+          <a-textarea
+            :disabled="msgDisabled"
+            class="custom-textarea"
+            v-model:value="area.content"
+            :rows="8"
+            :maxlength="1000"
+            @focus="handleFocus(index)"
+            @blur="handleBlur"
+            @input="updateCursorPosition($event, index)"
+          />
+          <!-- <HtmlTextarea v-model="taskForm.messageContent" :rows="4" :maxlength="1000" @focus="handleFocus"
+                    @blur="handleBlur" /> -->
+          <div v-if="!area.content && !isFocused" class="custom-placeholder">
+            <span>示例:</span>
+            <span class="highlight">xx客户</span>
+            <span class="highlight">性别</span>
+            <span>您好!</span>
+          </div>
+          <div class="textarea-bottom">
+            <!-- <a-dropdown :trigger="['click']" @click.stop arrow placement="bottomRight">
+          <a class="ant-dropdown-link" @click.prevent>
+            <PlusCircleOutlined />
+            引用客户信息
+          </a>
+          <template #overlay>
+            <a-menu @click="handleMenuClick">
+              <div class="ant-dropdown-menu-item">
+                <UserOutlined />
+                基本信息
+              </div>
+              <a-menu-item key="wechatNickname">微信昵称</a-menu-item>
+              <a-menu-item key="name">姓名</a-menu-item>
+              <a-menu-item key="title">称呼</a-menu-item>
+              <a-menu-item key="surname">姓氏</a-menu-item>
+              <a-sub-menu key="gender" title="性别">
+                <a-menu-item key="Mr/Ms">先生/女士</a-menu-item>
+                <a-menu-item key="youngMan/youngLady">小哥哥/小姐姐</a-menu-item>
+              </a-sub-menu>
+            </a-menu>
+          </template>
+        </a-dropdown> -->
+            <div class="character-count">{{ area.content.length }}/ 1000</div>
+          </div>
+          <CustomIcons :index="index" :iconDisable="msgDisabled" />
+          <span v-show="area.content == ''" class="warning-style">请输入文本内容</span>
+          <div style="margin-bottom: 5px">
+            <span style="margin-right: 10px">变量:</span>
+            <a-button style="margin-right: 10px" :disabled="msgDisabled" @click="insertVariable('${客户名称}', index)">客户名称</a-button>
+            <a-button style="margin-right: 10px" :disabled="msgDisabled" @click="insertVariable('${客户称谓}', index)">客户称谓</a-button>
+            <a-button style="margin-right: 10px" :disabled="msgDisabled" @click="insertVariable('${客户性别}', index)">客户性别</a-button>
+            <a-button style="margin-right: 10px" :disabled="msgDisabled" @click="insertVariable('${客户微信号}', index)">客户微信号</a-button>
+            <a-button style="margin-right: 10px" :disabled="msgDisabled" @click="insertVariable('${客户手机号}', index)">客户手机号</a-button>
+          </div>
+        </div>
+      </a-form-item>
+      <a-form-item v-else-if="area.type === 2" label="" name="content" class="message-content">
+        <!-- class="upload-contant" -->
+        <div :class="msgDisabled ? 'disabled-upload upload-contant' : 'upload-contant'">
+          <BasicUpload
+            :maxSize="10"
+            :maxNumber="1"
+            @change="handleChange"
+            :api="uploadApi"
+            v-model:value="area.content"
+            :showPreviewNumber="false"
+            emptyHidePreview
+          />
+          <!-- <div class="upload-file-style" v-if="area.content">{{String(area.content).split('/').pop()}}</div> -->
+          <CustomIcons :index="index" :length="length" :iconDisable="msgDisabled" />
+        </div>
+      </a-form-item>
+      <!-- 添加类型按钮 -->
+      <div v-if="index === internalValue.length - 1" class="message-content-btn">
+        <a-dropdown :trigger="['click']" @click.stop arrow placement="bottomRight">
+          <a-button :disabled="msgDisabled" value="large" type="primary" class="create-btn">
+            添加
+          </a-button>
+          <template #overlay>
+            <a-menu @click="handleCreateClick">
+              <a-menu-item key="1">文本</a-menu-item>
+              <a-menu-item key="2">文件</a-menu-item>
+              <!-- <a-menu-item key="productNote">产品笔记</a-menu-item>
+                <a-menu-item key="miniProgram">小程序</a-menu-item> -->
+            </a-menu>
+          </template>
+        </a-dropdown>
+      </div>
+    </div>
+    <div v-if="internalValue.length === 0" class="message-content-btn1">
+      <a-dropdown :trigger="['click']" @click.stop arrow placement="bottomRight">
+        <a-button :disabled="msgDisabled" value="large" type="primary" class="create-btn">
+          添加
+        </a-button>
+        <template #overlay>
+          <a-menu @click="handleCreateClick">
+            <a-menu-item key="1">文本</a-menu-item>
+            <a-menu-item key="2">文件</a-menu-item>
+            <!-- <a-menu-item key="productNote">产品笔记</a-menu-item>
+                <a-menu-item key="miniProgram">小程序</a-menu-item> -->
+          </a-menu>
+        </template>
+      </a-dropdown>
+    </div>
+    <!-- 产品笔记 -->
+    <!-- <ProductContant /> -->
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent, ref, reactive, provide, props, watch ,nextTick} from 'vue';
+  import { Form, Button, Dropdown, Menu, message, Input, SubMenu, Upload } from 'ant-design-vue';
+  import { CloudUploadOutlined, PlusCircleOutlined, UserOutlined } from '@ant-design/icons-vue';
+  import CustomIcons from './customIcons.vue';
+  import ProductNote from './productNote.vue';
+  import { BasicUpload } from '@/components/Upload';
+  import { deleteFile, downloadFile, getFileList, uploadApi } from '@/api/fms/file';
+  import ProductContant from './productContant.vue';
+  import type { UploadChangeParam, UploadProps } from 'ant-design-vue';
+  export default defineComponent({
+    name: 'MsgContant',
+    components: {
+      'a-form': Form,
+      'a-form-item': Form.Item,
+      'a-button': Button,
+      'a-dropdown': Dropdown,
+      'a-menu': Menu,
+      'a-menu-item': Menu.Item,
+      'a-input': Input,
+      'a-input-textarea': Input.TextArea,
+      'a-sub-menu': SubMenu,
+      'a-upload': Upload,
+      'a-upload-dragger': Upload.Dragger,
+      BasicUpload,
+      CloudUploadOutlined,
+      PlusCircleOutlined,
+      UserOutlined,
+      CustomIcons,
+      ProductNote,
+      ProductContant,
+    },
+
+    props: {
+      value: {
+        type: Array,
+        required: true,
+      },
+      msgDisabled: {
+        type: Boolean,
+        default: false,
+      },
+    },
+    emits: ['update:value'],
+    setup(props, { emit }) {
+      // const internalValue = ref([...props.value]);
+      const internalValue = ref(addCanMoveProperties(props.value));
+      const msgDisabled = ref(props.msgDisabled);
+      // 用于存储每个 textarea 的光标位置
+      const cursorPositions = ref<Record<number, number>>({});
+      // 更新光标位置
+      const updateCursorPosition = (event: Event, index: number) => {
+        const textarea = event.target as HTMLTextAreaElement;
+        cursorPositions.value[index] = textarea.selectionStart;
+      };
+    
+
+      const taskForm = reactive({
+        fileContent: '',
+      });
+      function handleChange(list: string[]) {
+        if (msgDisabled.value) {
+          return;
+        }
+        // createMessage.info(`已上传文件${JSON.stringify(list)}`);
+        // console.log(list);
+      }
+      watch(
+        internalValue,
+        (newValue) => {
+          emit('update:value', newValue);
+        },
+        { deep: true },
+      );
+
+      // 插入变量函数
+      const insertVariable = (variable: string, index: number) => {
+        const area = internalValue.value[index];
+        if (!area) {
+          console.error(`Invalid index ${index}: No content found.`);
+          return;
+        }
+        // const scrollPosition = window.scrollY;
+        const cursorPosition = cursorPositions.value[index] || 0;
+        const content = area.content || '';
+
+        // 在光标位置插入变量
+        internalValue.value[index].content = 
+          content.slice(0, cursorPosition) + variable + content.slice(cursorPosition);
+
+        // 更新光标位置为插入变量之后
+        nextTick(() => {
+          const newPosition = cursorPosition + variable.length;
+          const textarea = document.querySelectorAll('.custom-textarea')[index] as HTMLTextAreaElement;
+          if (textarea) {
+            textarea.setSelectionRange(newPosition, newPosition);
+            // textarea.focus();
+            // 恢复滚动位置
+            // window.scrollTo(0, scrollPosition);
+          }
+        });
+      };
+
+      function addCanMoveProperties(items) {
+        return items.map((item, index, array) => ({
+          ...item,
+          canMoveUp: index > 0,
+          canMoveDown: index < array.length - 1,
+        }));
+      }
+      function updateCanMoveProperties() {
+        internalValue.value.forEach((item, index, array) => {
+          item.canMoveUp = index > 0;
+          item.canMoveDown = index < array.length - 1;
+        });
+      }
+      const updateValue = (index: number, event: Event) => {
+        const input = event.target as HTMLInputElement;
+        internalValue.value[index] = input.value;
+        emit('update:value', internalValue.value);
+      };
+      const isFocused = ref(false);
+
+      const fileList = ref([]);
+
+      const handleRemove = () => {
+        fileList.value = [];
+        taskForm.fileContent = '';
+      };
+
+      const handleFocus = () => {
+        isFocused.value = true;
+      };
+
+      const handleBlur = () => {
+        isFocused.value = false;
+      };
+      const handleMenuClick = ({ key }: { key: string }) => {
+        const labelMap: { [key: string]: string } = {
+          wechatNickname: '微信昵称',
+          name: '姓名',
+          title: '称呼',
+          surname: '姓氏',
+          'Mr/Ms': '先生/女士',
+          'youngMan/youngLady': '小哥哥/小姐姐',
+        };
+        const label = labelMap[key] || key;
+        taskForm.content += `${label}`;
+      };
+
+      const addTextArea = () => {
+        const id = Date.now();
+        internalValue.value.push({
+          id,
+          type: 1,
+          content: '',
+          canMoveUp: internalValue.value.length > 0,
+          canMoveDown: false,
+        });
+        updateCanMoveProperties();
+      };
+
+      // const addFileArea = () => {
+      //   const id = Date.now();
+      //   msgContantArry.value.push({ id, type: 'file' });
+      // };
+      const addFileArea = () => {
+        const id = Date.now();
+        internalValue.value.push({
+          id,
+          type: 2,
+          content: '',
+          canMoveUp: internalValue.value.length > 0,
+          canMoveDown: false,
+        });
+
+        // 更新前一个项的 canMoveDown 为 true
+        updateCanMoveProperties();
+      };
+      // 根据传入的类型添加对应的区域
+      const handleCreateClick = (key: string) => {
+        if (key.key === '1') {
+          addTextArea();
+        } else {
+          addFileArea();
+        }
+      };
+
+      const removeItem = (index: number) => {
+        internalValue.value.splice(index, 1); // 根据索引删除项
+        updateCanMoveProperties();
+      };
+
+      provide('removeItem', removeItem);
+
+      const moveItem = (index, direction) => {
+        if (direction === 'up' && index > 0) {
+          [internalValue.value[index], internalValue.value[index - 1]] = [
+            internalValue.value[index - 1],
+            internalValue.value[index],
+          ];
+        } else if (direction === 'down' && index < internalValue.value.length - 1) {
+          [internalValue.value[index], internalValue.value[index + 1]] = [
+            internalValue.value[index + 1],
+            internalValue.value[index],
+          ];
+        }
+        updateCanMoveProperties();
+      };
+
+      const moveItemUp = (index) => {
+        moveItem(index, 'up');
+      };
+
+      const moveItemDown = (index) => {
+        moveItem(index, 'down');
+      };
+
+      provide('moveItem', moveItem);
+
+      return {
+        uploadApi,
+        taskForm,
+        handleCreateClick,
+        addTextArea,
+        addFileArea,
+        removeItem,
+        moveItem,
+        moveItemUp,
+        moveItemDown,
+        updateCanMoveProperties,
+        isFocused,
+        handleFocus,
+        handleBlur,
+        handleMenuClick,
+        fileList,
+        // beforeUpload,
+        handleChange,
+        handleRemove,
+        // customRequest,
+        internalValue,
+        updateValue,
+        addCanMoveProperties,
+        cursorPositions,
+        updateCursorPosition,
+        insertVariable,
+      };
+    },
+  });
+</script>
+<style scoped lang="less">
+  .disabled-upload {
+    cursor: not-allowed;
+    pointer-events: none;
+    opacity: 0.5;
+  }
+  .ant-form-item {
+    margin-bottom: 0;
+  }
+  .message-content-btn {
+    width: 800px;
+    background-color: #f9f9f9;
+    padding: 10px 30px 10px 40px;
+    display: flex;
+    justify-content: left;
+  }
+  .message-content-btn1 {
+    width: 800px;
+    background-color: #f9f9f9;
+    padding: 10px 130px 10px 50px;
+    display: flex;
+    justify-content: left;
+  }
+  .create-btn {
+    width: 120px;
+    border-radius: 3px;
+  }
+  .message-content {
+    width: 800px;
+    background-color: #f9f9f9;
+    padding: 10px 30px 10px 30px;
+    display: flex;
+    justify-content: center;
+  }
+  .custom-textarea {
+    width: 100%;
+    border-radius: 4px 4px 0 0;
+    background-color: #f9f8f8;
+  }
+  .warning-style {
+    color: #ff4949;
+    font-size: 12px;
+    margin-left: 2px;
+    position: absolute;
+    bottom: -13px;
+  }
+  .custom-placeholder-wrapper {
+    position: relative;
+    display: flex;
+    flex-direction: column;
+    width: 720px;
+  }
+  .custom-placeholder {
+    position: absolute;
+    top: 10px;
+    left: 10px;
+    color: #a9a9a9;
+    pointer-events: none;
+    user-select: none;
+    font-size: 14px;
+  }
+  .custom-placeholder .highlight {
+    color: #1c53d9;
+    background-color: #e8edfb;
+    padding: 3px 5px;
+    margin: 0 5px;
+    font-size: 12px;
+  }
+  .textarea-bottom {
+    display: flex;
+    background-color: #f5f8fd;
+    justify-content: space-between;
+    padding: 3px 16px;
+    border: 1px solid #c0ccda;
+    border-top: 0 none;
+    border-radius: 0 0 4px 4px;
+  }
+  .ant-dropdown-link {
+    color: rgb(119, 140, 162);
+  }
+  .character-count {
+    text-align: right;
+    color: #a9a9a9;
+    font-size: 12px;
+    margin-top: 5px;
+  }
+  .upload-contant {
+    width: 750px;
+    display: flex;
+    // justify-content: space-around;
+  }
+  ::deep .ant-upload-wrapper .ant-upload-drag {
+    background-color: #fff;
+    padding: 20px 0;
+  }
+  .upload {
+    width: 600px;
+    display: flex;
+  }
+  .upload:hover {
+    border-color: #1890ff;
+  }
+  .upload-file-style {
+    min-width: 400px;
+    padding: 5px;
+    display: inline-block;
+    background: #f6f6fa;
+    cursor: pointer;
+  }
+</style>

+ 157 - 6
src/views/wechat/sop_task/add_sop/components/nodeConfigDrawer.vue

@@ -52,9 +52,28 @@
                   :rules="[{ required: true, message: '请输入' }]"
                   :disabled="btnDisabled"
                 />
-                分钟,执行该动作
+                <Select
+                  v-model:value="form.noReplyUnit"
+                  :options="[
+                    { value: 'm', label: '分钟' },
+                    { value: 'h', label: '小时' },
+                    { value: 'D', label: '日' },
+                    { value: 'W', label: '周' },
+                  ]"
+                  :style="{ width: '80px', margin: '0 5px' }"
+                  placeholder="分钟"
+                  :disabled="btnDisabled"
+                ></Select>执行该动作
               </div>
             </FormItem>
+            <!-- <FormItem
+              v-show="form.conditionType === '2'"
+              class="no-colon"
+              label=""
+              name="noReplyCondition"
+            >
+              <Select></Select>
+            </FormItem> -->
             <FormItem
             v-show="form.conditionType === '1'" 
               label="回复内容"
@@ -141,6 +160,16 @@
                 </template>
                 打标签
               </Button>
+              <Button
+                :disabled="btnDisabled"
+                :class="isForwardAction ? 'action-btn' : 'btn-style'"
+                @click="setTaskType('forward')"
+              >
+                <template #icon>
+                  <ShareAltOutlined />
+                </template>
+                转发人工
+              </Button>
             </FormItem>
             <!-- 发消息 -->
             <Collapse
@@ -212,6 +241,73 @@
                     ></Select>
                     <span v-show="taskForm.tagValue.length === 0" class="warning-style">请选择标签</span>
                   </FormItem>
+                  <FormItem
+                    label="用户进入当前节点时,可以移出标签"
+                    name="tagging"
+                    v-if="msgAction"
+                    :labelCol="{ span: 8 }"
+                  >
+                    <!-- :rules="[{ required: true, message: '请选择一个标签', trigger: 'change' },]" -->
+                    <Select
+                      :disabled="btnDisabled"
+                      v-model:value="taskForm.delTagValue"
+                      :options="actionLabel"
+                      allowClear
+                      mode="multiple"
+                      size="middle"
+                      placeholder="请选择"
+                      :style="{ width: '240px', margin: '0 5px' }"
+                      :max-tag-count="1"
+                    ></Select>
+                    <span v-show="taskForm.delTagValue.length === 0" class="warning-style">
+                      请选择标签
+                    </span>
+                  </FormItem>
+                </div>
+              </Collapse.Panel>
+            </Collapse>
+            <!-- 转发人工 -->
+            <Collapse
+              v-show="isForwardAction"
+              v-model:activeKey="activeKeyForward"
+              collapsible="header"
+              class="custom-collapse-forwards"
+            >
+              <Collapse.Panel key="1" class="custom-collapse-panel">
+                <template #header>
+                  <div @click.stop="toggleTimeoutAction" style="display: flex; align-items: center">
+                    <ShareAltOutlined style="color: #eabb63" />
+                    &nbsp;&nbsp;
+                    <span>转发人工</span>
+                  </div>
+                </template>
+                <div @click.stop>
+                  <!-- 根据发送方式显示 MsgContant 或在折叠面板中展示 -->
+                  <div>
+                    <FormItem
+                    label="转发至"
+                      name="wxid"
+                      :rules="[{ required: true, message: '请填写接收人微信id' }]"
+                    >
+                    <Input
+                      :disabled="btnDisabled"
+                      type="text"
+                      v-model:value="taskForm.wxid"
+                      placeholder="请输入接收人微信id,多个微信id请用逗号分隔"
+                    />
+                    </FormItem>
+                    <FormItem
+                      label=""
+                      name="forwardMessage"
+                      :rules="[{ required: true, message: '请输入消息内容' }]"
+                    >
+                      <NodeMsgContantForwards
+                        v-model:value="taskForm.forwardMessage"
+                        :msgDisabled="btnDisabled"
+                      />
+                    </FormItem>
+                    <!-- <component :is="MsgContant" /> -->
+                  </div>
                 </div>
               </Collapse.Panel>
             </Collapse>
@@ -253,6 +349,7 @@
     PlusCircleOutlined,
     MessageOutlined,
     TagOutlined,
+    ShareAltOutlined
   } from '@ant-design/icons-vue';
   import {
     addSopStageNode,
@@ -266,6 +363,7 @@
   import type { FormInstance } from 'ant-design-vue';
   // import HtmlTextarea from './htmlTextarea.vue';
   import NodeMsgContant from './nodeMsgContant.vue';
+  import NodeMsgContantForwards from './nodeMsgContantForwards.vue';
   import TimedSending from './timedSending.vue';
   
   //   name: 'NodeConfigDrawer',
@@ -309,6 +407,7 @@
     noReplyCondition: undefined,
     content: 'concreteContent',
     conditionList: [{ expression: '' }],
+    noReplyUnit:'',
   });
   let initialForm = reactive({
     nodeName: '',
@@ -316,26 +415,35 @@
     noReplyCondition: undefined,
     content: 'concreteContent',
     conditionList: [{ expression: '' }],
+    noReplyUnit:'',
   });
   let taskForm = reactive({
     taskType: 'sendMessage',
     sendType: 'immediate',
     actionMessage: [{ type: 1, content: '' }],
     tagValue: [],
+    delTagValue: [],
+    forwardMessage: [{ type: 1, content: '' }],
+    wxid: '',
   });
   let initialTaskForm = reactive({
     taskType: 'sendMessage',
     sendType: 'immediate',
     actionMessage: [{ type: 1, content: '' }],
     tagValue: [],
+    delTagValue: [],
+    forwardMessage: [{ type: 1, content: '' }],
+    wxid: '',
   });
   let activeKey = ref<number | string>('1');
   let activeKeyTiem = ref<number | string>('time1');
   let activeKeyTag = ref<number | string>('1');
+  const activeKeyForward = ref<number | string>('1');
   // const msgAction = ref<boolean>(false);
   let msgAction = ref(taskForm.taskType === 'sendMessage');
   let isMsgAction = ref<boolean>(true);
   let isTagAction = ref<boolean>(false);
+  const isForwardAction = ref<boolean>(false);
   // const isDataLoaded = ref(false); // 控制表单的渲染
   onMounted(async () => {
       // loading.value = true;
@@ -416,6 +524,7 @@
       noReplyCondition: data.data.noReplyCondition,
       conditionOperator: String(data.data.conditionOperator),
       conditionList: conArray,
+      noReplyUnit: data.data.noReplyUnit,
     });
     if(!data.data.noReplyCondition && conArray[0].expression == ''){
       Object.assign(form,{
@@ -429,16 +538,24 @@
     if (data.data.actionLabel.length > 0) {
       taskTypes.push('tag');
     }
+    if (data.data.actionForward.action.length > 0) {
+      taskTypes.push('forward');
+    }
     // 填充 taskForm 数据
     Object.assign(taskForm, {
       taskType: taskTypes,
       sendType: 'immediate',
       actionMessage: data.data.actionMessage || [{ type: 1, content: '' }],
-      tagValue: data.data.actionLabel || [],
+      // tagValue: data.data.actionLabel || [],
+      tagValue: data.data.actionLabelAdd || [],
+      delTagValue:data.data.actionLabelDel || [],
+      forwardMessage: data.data.actionForward.action || [{ type: 1, content: '' }],
+      wxid: data.data.actionForward.wxid || '',  
     });
     // 根据 taskType 设置 isMsgAction 和 isTagAction
     isMsgAction.value = taskTypes.includes('sendMessage');
     isTagAction.value = taskTypes.includes('tag');
+    isForwardAction.value = taskTypes.includes('forward');
   }
   function processArray(array: any[]) {
     array.forEach((item) => {
@@ -451,15 +568,31 @@
     });
     return array;
   }
+  function processForwardsArray(array: any[]) {
+    array.forEach((item) => {
+      if (item.type === 2 && Array.isArray(item.content)) {
+        const contentString = item.content[0]; // 假设content只有一个元素
+        const filename = contentString.split('/').pop(); // 获取最后一个/后的值
+        item.content = contentString; // 将content数组转成字符串
+        item.meta = { filename }; // 添加meta对象
+      }
+    });
+    return array;
+  }
   async function submitForm() {
     if (!btnDisabled.value) {
       loading.value = true;
       // 提交表单逻辑
       let noReply = null
       let listCon = [""]
-      let { nodeName, conditionType, conditionList ,noReplyCondition} = form;
-      let { taskType, sendType, actionMessage, tagValue } = taskForm;
+      let { nodeName, conditionType, conditionList ,noReplyCondition,noReplyUnit} = form;
+      let { taskType, sendType, actionMessage, tagValue,delTagValue,forwardMessage, wxid  } = taskForm;
+      let actionForward = {};
+      actionForward.wxid = wxid;
+      actionForward.action = forwardMessage;
+
       processArray(actionMessage)
+      processForwardsArray(forwardMessage)
       if(form.conditionType === '1'){
         if(form.content !== 'anyContant'){
            listCon = conditionList.map((item) =>  item.expression);
@@ -472,13 +605,17 @@
         conditionType: Number(conditionType),
         conditionList: listCon,
         noReplyCondition: noReply,
+        noReplyUnit,
         taskType,
         sendType,
         actionMessage,
-        actionLabel: tagValue,
+        // actionLabel: tagValue,
+        actionLabelAdd: tagValue,
+        actionLabelDel:delTagValue,
+        actionForward,
       };
       const allData = actionMessage.every(item => item.content.trim()!== '');
-      if(isMsgAction.value && !allData || isTagAction.value && tagValue.length === 0){
+      if(isMsgAction.value && !allData || isTagAction.value && tagValue.length === 0 && delTagValue == 0){
         return;
       }
       console.log('|||||||||||提交表单', !isAddNode.value, requestData);
@@ -573,6 +710,13 @@
         taskForm.taskType = '';
         taskForm.actionMessage= [{type: 1, content: ""}];
       }
+    }else if(type === 'forward'){
+      msgAction.value = type === 'forward';
+      isForwardAction.value = !isForwardAction.value;
+      if (!isForwardAction.value) {
+        taskForm.taskType = '';
+        taskForm.forwardMessage = [{ type: 1, content: '' }];
+      }
     }
   };
 
@@ -628,6 +772,13 @@
     color: #333;
     border-radius: 0px;
     border: none;
+    margin: 30px 0 30px 0;
+  }
+  .custom-collapse-forwards {
+    background-color: rgb(253, 248, 238);
+    color: #333;
+    border-radius: 0px;
+    border: none;
     margin: 30px 0 90px 0;
   }
   .custom-collapse-panel {

+ 43 - 3
src/views/wechat/sop_task/add_sop/components/nodeMsgContant.vue

@@ -13,6 +13,7 @@
             :maxlength="1000"
             @focus="handleFocus"
             @blur="handleBlur"
+            @input="updateCursorPosition($event, index)"
           />
           <!-- <HtmlTextarea v-model="taskForm.messageContent" :rows="4" :maxlength="1000" @focus="handleFocus"
                     @blur="handleBlur" /> -->
@@ -50,13 +51,19 @@
           <!-- @remove="emit('remove')" @moveUp="emit('moveUp')" @moveDown="emit('moveDown')":length="length" -->
           <CustomIcons :index="index" :iconDisable="msgDisabled"/>
           <span v-show="area.content == ''" class="warning-style">请输入文本内容</span>
+          <div style="margin-bottom:5px">
+            <span style="margin-right: 10px;">变量:</span>
+            <a-button style="margin-right: 10px;" :disabled="msgDisabled" @click="insertVariable('${客户名称}', index)">客户名称</a-button>
+            <a-button style="margin-right: 10px;" :disabled="msgDisabled" @click="insertVariable('${客户称谓}', index)">客户称谓</a-button>
+            <a-button @click="insertVariable('${客户手机号}', index)" :disabled="msgDisabled">客户手机号</a-button>
+          </div>
         </div>
       </a-form-item>
       <a-form-item v-else-if="area.type === 2" label="" name="fileContent" class="message-content">
         <div class="upload-contant">
            <BasicUpload 
            :disabled="msgDisabled"
-          :maxSize="100"
+          :maxSize="10"
           :maxNumber="1"
           @change="handleChange"
           :api="uploadApi"
@@ -64,7 +71,7 @@
           :showPreviewNumber=false
           emptyHidePreview
         />
-        <div class="upload-file-style" v-if="area.content">{{String(area.content).split('/').pop()}}</div>
+        <!-- <div class="upload-file-style" v-if="area.content">{{String(area.content).split('/').pop()}}</div> -->
           <CustomIcons :index="index" :length="length" :iconDisable="msgDisabled"/>
         </div>
       </a-form-item>
@@ -101,7 +108,7 @@
   </div>
 </template>
 <script lang="ts">
-  import { defineComponent, ref, reactive, provide, props, watch } from 'vue';
+  import { defineComponent, ref, reactive, provide, props, watch ,nextTick} from 'vue';
   import { Form, Button, Dropdown, Menu, message, Input, SubMenu, Upload } from 'ant-design-vue';
   import { CloudUploadOutlined, PlusCircleOutlined, UserOutlined } from '@ant-design/icons-vue';
   import CustomIcons from './customIcons.vue';
@@ -149,6 +156,13 @@
       // const internalValue = ref([...props.value]);
       const internalValue = ref(addCanMoveProperties(props.value));
       const msgDisabled = ref(props.msgDisabled);
+       // 用于存储每个 textarea 的光标位置
+      const cursorPositions = ref<Record<number, number>>({});
+      // 更新光标位置
+      const updateCursorPosition = (event: Event, index: number) => {
+        const textarea = event.target as HTMLTextAreaElement;
+        cursorPositions.value[index] = textarea.selectionStart;
+      };
       const taskForm = reactive({
         fileContent: '',
       });
@@ -161,6 +175,30 @@
         { deep: true },
       );
 
+      // 插入变量函数
+      const insertVariable = (variable: string, index: number) => {
+        const area = internalValue.value[index];
+        if (!area) {
+          console.error(`Invalid index ${index}: No content found.`);
+          return;
+        }
+        const cursorPosition = cursorPositions.value[index] || 0;
+        const content = area.content || '';
+
+        // 在光标位置插入变量
+        internalValue.value[index].content = 
+          content.slice(0, cursorPosition) + variable + content.slice(cursorPosition);
+
+        // 更新光标位置为插入变量之后
+        nextTick(() => {
+          const newPosition = cursorPosition + variable.length;
+          const textarea = document.querySelectorAll('.custom-textarea')[index] as HTMLTextAreaElement;
+          if (textarea) {
+            textarea.setSelectionRange(newPosition, newPosition);
+            // textarea.focus();
+          }
+        });
+      };
       function addCanMoveProperties(items) {
         return items.map((item, index, array) => ({
           ...item,
@@ -328,6 +366,8 @@
         updateValue,
         addCanMoveProperties,
         uploadApi,
+        updateCursorPosition,
+        insertVariable,
       };
     },
   });

+ 477 - 0
src/views/wechat/sop_task/add_sop/components/nodeMsgContantForwards.vue

@@ -0,0 +1,477 @@
+<!-- 消息内容 -->
+<template>
+  <div>
+    <div v-for="(area, index) in internalValue" :key="index">
+      <!-- 文本内容  -->
+      <a-form-item label="" v-if="area.type === 1" name="content" class="message-content">
+        <div class="custom-placeholder-wrapper">
+          <a-textarea
+          :disabled="msgDisabled"
+            class="custom-textarea"
+            v-model:value="area.content"
+            :rows="8"
+            :maxlength="1000"
+            @focus="handleFocus"
+            @blur="handleBlur"
+            @input="updateCursorPosition($event, index)"
+          />
+          <!-- <HtmlTextarea v-model="taskForm.messageContent" :rows="4" :maxlength="1000" @focus="handleFocus"
+                    @blur="handleBlur" /> -->
+          <!-- <div v-if="!area.content && !isFocused" class="custom-placeholder">
+            <span>示例:</span>
+            <span class="highlight">xx客户</span>
+            <span class="highlight">性别</span>
+            <span>您好!</span>
+          </div> -->
+          <div class="textarea-bottom">
+            <!-- <a-dropdown :trigger="['click']" @click.stop arrow placement="bottomRight">
+          <a class="ant-dropdown-link" @click.prevent>
+            <PlusCircleOutlined />
+            引用客户信息
+          </a>
+          <template #overlay>
+            <a-menu @click="handleMenuClick">
+              <div class="ant-dropdown-menu-item">
+                <UserOutlined />
+                基本信息
+              </div>
+              <a-menu-item key="wechatNickname">微信昵称</a-menu-item>
+              <a-menu-item key="name">姓名</a-menu-item>
+              <a-menu-item key="title">称呼</a-menu-item>
+              <a-menu-item key="surname">姓氏</a-menu-item>
+              <a-sub-menu key="gender" title="性别">
+                <a-menu-item key="Mr/Ms">先生/女士</a-menu-item>
+                <a-menu-item key="youngMan/youngLady">小哥哥/小姐姐</a-menu-item>
+              </a-sub-menu>
+            </a-menu>
+          </template>
+        </a-dropdown> -->
+            <div class="character-count">{{ area.content.length }}/ 1000</div>
+          </div>
+          <!-- @remove="emit('remove')" @moveUp="emit('moveUp')" @moveDown="emit('moveDown')":length="length" -->
+          <CustomIcons :index="index" :iconDisable="msgDisabled"/>
+          <span v-show="area.content == ''" class="warning-style">请输入文本内容</span>
+          <div style="margin-bottom: 5px">
+            <span style="margin-right: 10px">变量:</span>
+            <a-button style="margin-right: 10px" :disabled="msgDisabled" @click="insertVariable('${客户名称}', index)">客户名称</a-button>
+            <a-button style="margin-right: 10px" :disabled="msgDisabled" @click="insertVariable('${客户称谓}', index)">客户称谓</a-button>
+            <a-button style="margin-right: 10px" :disabled="msgDisabled" @click="insertVariable('${客户性别}', index)">客户性别</a-button>
+            <a-button style="margin-right: 10px" :disabled="msgDisabled" @click="insertVariable('${客户微信号}', index)">客户微信号</a-button>
+            <a-button style="margin-right: 10px" :disabled="msgDisabled" @click="insertVariable('${客户手机号}', index)">客户手机号</a-button>
+          </div>
+        </div>
+      </a-form-item>
+      <a-form-item v-else-if="area.type === 2" label="" name="fileContent" class="message-content">
+        <div class="upload-contant">
+           <BasicUpload 
+           :disabled="msgDisabled"
+          :maxSize="10"
+          :maxNumber="1"
+          @change="handleChange"
+          :api="uploadApi"
+          v-model:value="area.content"
+          :showPreviewNumber=false
+          emptyHidePreview
+        />
+        <!-- <div class="upload-file-style" v-if="area.content">{{String(area.content).split('/').pop()}}</div> -->
+          <CustomIcons :index="index" :length="length" :iconDisable="msgDisabled"/>
+        </div>
+      </a-form-item>
+      <!-- 添加类型按钮 -->
+      <div v-if="index == internalValue.length - 1" class="message-content-btn">
+        <a-dropdown :trigger="['click']" @click.stop arrow placement="bottomRight">
+          <a-button :disabled="msgDisabled" value="large" type="primary" class="create-btn">添加</a-button>
+          <template #overlay>
+            <a-menu @click="handleCreateClick">
+              <a-menu-item key="1">文本</a-menu-item>
+              <a-menu-item key="2">文件</a-menu-item>
+              <!-- <a-menu-item key="productNote">产品笔记</a-menu-item>
+                <a-menu-item key="miniProgram">小程序</a-menu-item> -->
+            </a-menu>
+          </template>
+        </a-dropdown>
+      </div>
+    </div>
+    <div v-if="internalValue.length == 0" class="message-content-btn">
+        <a-dropdown :trigger="['click']" @click.stop arrow placement="bottomRight">
+          <a-button :disabled="msgDisabled" value="large" type="primary" class="create-btn">添加</a-button>
+          <template #overlay>
+            <a-menu @click="handleCreateClick">
+              <a-menu-item key="1">文本</a-menu-item>
+              <a-menu-item key="2">文件</a-menu-item>
+              <!-- <a-menu-item key="productNote">产品笔记</a-menu-item>
+                <a-menu-item key="miniProgram">小程序</a-menu-item> -->
+            </a-menu>
+          </template>
+        </a-dropdown>
+      </div>
+    <!-- 产品笔记 -->
+    <!-- <ProductContant /> -->
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent, ref, reactive, provide, props, watch ,nextTick} from 'vue';
+  import { Form, Button, Dropdown, Menu, message, Input, SubMenu, Upload } from 'ant-design-vue';
+  import { CloudUploadOutlined, PlusCircleOutlined, UserOutlined } from '@ant-design/icons-vue';
+  import CustomIcons from './customIcons.vue';
+  import { BasicUpload } from '@/components/Upload';
+  import { deleteFile, downloadFile, getFileList, uploadApi } from '@/api/fms/file';
+  import ProductNote from './productNote.vue';
+  import ProductContant from './productContant.vue';
+  export default defineComponent({
+    name: 'MsgContant',
+    components: {
+      'a-form': Form,
+      'a-form-item': Form.Item,
+      'a-button': Button,
+      'a-dropdown': Dropdown,
+      'a-menu': Menu,
+      'a-menu-item': Menu.Item,
+      'a-input': Input,
+      'a-input-textarea': Input.TextArea,
+      'a-sub-menu': SubMenu,
+      'a-upload': Upload,
+      'a-upload-dragger': Upload.Dragger,
+      CloudUploadOutlined,
+      PlusCircleOutlined,
+      UserOutlined,
+      CustomIcons,
+      ProductNote,
+      ProductContant,
+      BasicUpload,
+    },
+    // props: {
+    //   actionMessage: Array,
+    // },
+    props: {
+      value: {
+        type: Array,
+        required: true,
+      },
+      msgDisabled:{
+        type: Boolean,
+        default: false,
+      }
+    },
+    emits: ['update:value'],
+    setup(props, { emit }) {
+      // const internalValue = ref([...props.value]);
+      const internalValue = ref(addCanMoveProperties(props.value));
+      const msgDisabled = ref(props.msgDisabled);
+
+      // 用于存储每个 textarea 的光标位置
+      const cursorPositions = ref<Record<number, number>>({});
+      // 更新光标位置
+      const updateCursorPosition = (event: Event, index: number) => {
+        const textarea = event.target as HTMLTextAreaElement;
+        cursorPositions.value[index] = textarea.selectionStart;
+      };
+      const taskForm = reactive({
+        fileContent: '',
+      });
+      
+      watch(
+        internalValue,
+        (newValue) => {
+          emit('update:value', newValue);
+        },
+        { deep: true },
+      );
+
+      function addCanMoveProperties(items) {
+        return items.map((item, index, array) => ({
+          ...item,
+          canMoveUp: index > 0,
+          canMoveDown: index < array.length - 1,
+        }));
+      }
+      function updateCanMoveProperties() {
+        internalValue.value.forEach((item, index, array) => {
+          item.canMoveUp = index > 0;
+          item.canMoveDown = index < array.length - 1;
+        });
+      }
+      const updateValue = (index: number, event: Event) => {
+        const input = event.target as HTMLInputElement;
+        internalValue.value[index] = input.value;
+        emit('update:value', internalValue.value);
+      };
+      const isFocused = ref(false);
+
+      const fileList = ref([]);
+      function handleChange(list: string[]) {
+          // createMessage.info(`已上传文件${JSON.stringify(list)}`);
+          console.log(list);
+        };
+
+        // 插入变量函数
+      const insertVariable = (variable: string, index: number) => {
+        const area = internalValue.value[index];
+        if (!area) {
+          console.error(`Invalid index ${index}: No content found.`);
+          return;
+        }
+        // const scrollPosition = window.scrollY;
+        const cursorPosition = cursorPositions.value[index] || 0;
+        const content = area.content || '';
+
+        // 在光标位置插入变量
+        internalValue.value[index].content = 
+          content.slice(0, cursorPosition) + variable + content.slice(cursorPosition);
+
+        // 更新光标位置为插入变量之后
+        nextTick(() => {
+          const newPosition = cursorPosition + variable.length;
+          const textarea = document.querySelectorAll('.custom-textarea')[index] as HTMLTextAreaElement;
+          if (textarea) {
+            textarea.setSelectionRange(newPosition, newPosition);
+            // textarea.focus();
+            // 恢复滚动位置
+            // window.scrollTo(0, scrollPosition);
+          }
+        });
+      };
+      // const handleChange = ({ file, fileList: newFileList }) => {
+      //   fileList.value = newFileList.slice(-1); // 保证只保留最新上传的一个文件
+      //   if (file.status === 'done') {
+      //     taskForm.fileContent = file.response;
+      //   } else if (file.status === 'error') {
+      //     message.error(`${file.name} 文件上传失败.`);
+      //   }
+      // };
+
+      const handleRemove = () => {
+        fileList.value = [];
+        taskForm.fileContent = '';
+      };
+
+      const handleFocus = () => {
+        isFocused.value = true;
+      };
+
+      const handleBlur = () => {
+        isFocused.value = false;
+      };
+      const handleMenuClick = ({ key }: { key: string }) => {
+        const labelMap: { [key: string]: string } = {
+          wechatNickname: '微信昵称',
+          name: '姓名',
+          title: '称呼',
+          surname: '姓氏',
+          'Mr/Ms': '先生/女士',
+          'youngMan/youngLady': '小哥哥/小姐姐',
+        };
+        const label = labelMap[key] || key;
+        taskForm.content += `${label}`;
+      };
+
+      // const taskForm = reactive({
+      //   // taskType: 'sendMessage',
+      //   // sendType: 'immediate',
+      //   // scheduledTime: null,
+      //   // messageContent: '',
+      //   // fileContent:'',
+      //   actionMessage: [
+      //     {type:1,content:''}
+      //   ]//命中后发送的消息内容
+      // });
+      // const msgContantArry = internalValue;
+      // const msgContantArry = ref<Array<{ type: string; content?: string }>>([
+      //   { type: 'txt', canMoveUp: false, canMoveDown: true }, // 初始包含一个文本类型的对象
+      // ]);
+      const addTextArea = () => {
+        const id = Date.now();
+        internalValue.value.push({
+          id,
+          type: 1,
+          content: '',
+          canMoveUp: internalValue.value.length > 0,
+          canMoveDown: false,
+        });
+        updateCanMoveProperties();
+      };
+
+      // const addFileArea = () => {
+      //   const id = Date.now();
+      //   msgContantArry.value.push({ id, type: 'file' });
+      // };
+      const addFileArea = () => {
+        const id = Date.now();
+        internalValue.value.push({
+          id,
+          type: 2,
+          content: '',
+          canMoveUp: internalValue.value.length > 0,
+          canMoveDown: false,
+        });
+
+        // 更新前一个项的 canMoveDown 为 true
+        updateCanMoveProperties();
+      };
+      // 根据传入的类型添加对应的区域
+      const handleCreateClick = (key: string) => {
+        if (key.key === '1') {
+          addTextArea();
+        } else {
+          addFileArea();
+        }
+      };
+
+      const removeItem = (index: number) => {
+        internalValue.value.splice(index, 1); // 根据索引删除项
+        updateCanMoveProperties();
+      };
+
+      provide('removeItem', removeItem);
+
+      const moveItem = (index, direction) => {
+        if (direction === 'up' && index > 0) {
+          [internalValue.value[index], internalValue.value[index - 1]] = [
+            internalValue.value[index - 1],
+            internalValue.value[index],
+          ];
+        } else if (direction === 'down' && index < internalValue.value.length - 1) {
+          [internalValue.value[index], internalValue.value[index + 1]] = [
+            internalValue.value[index + 1],
+            internalValue.value[index],
+          ];
+        }
+        updateCanMoveProperties();
+      };
+
+      // ...其余代码
+
+      const moveItemUp = (index) => {
+        moveItem(index, 'up');
+      };
+
+      const moveItemDown = (index) => {
+        moveItem(index, 'down');
+      };
+
+      provide('moveItem', moveItem);
+
+      return {
+        taskForm,
+        handleCreateClick,
+        addTextArea,
+        // addFileArea,
+        removeItem,
+        moveItem,
+        moveItemUp,
+        moveItemDown,
+        updateCanMoveProperties,
+        isFocused,
+        handleFocus,
+        handleBlur,
+        handleMenuClick,
+        fileList,
+        handleChange,
+        handleRemove,
+        internalValue,
+        updateValue,
+        addCanMoveProperties,
+        uploadApi,
+        updateCursorPosition,
+        insertVariable,
+      };
+    },
+  });
+</script>
+<style scoped lang="less">
+  .ant-form-item {
+    margin-bottom: 0;
+  }
+  .message-content-btn {
+    width: 800px;
+    background-color: #f9f9f9;
+    padding: 10px 30px 10px 40px;
+    display: flex;
+    justify-content: left;
+  }
+  .create-btn {
+    width: 120px;
+    border-radius: 3px;
+  }
+  .message-content {
+    width: 800px;
+    background-color: #f9f9f9;
+    padding: 10px 30px 10px 30px;
+    display: flex;
+    justify-content: center;
+  }
+  .custom-textarea {
+    width: 100%;
+    border-radius: 4px 4px 0 0;
+    background-color: #f9f8f8;
+  }
+  .custom-placeholder-wrapper {
+    position: relative;
+    display: flex;
+    flex-direction: column;
+    width: 720px;
+  }
+  .warning-style{
+    color: #FF4949;
+    font-size: 12px;
+    margin-left: 2px;
+    position: absolute;
+    bottom: -13px;
+  }
+  .custom-placeholder {
+    position: absolute;
+    top: 10px;
+    left: 10px;
+    color: #a9a9a9;
+    pointer-events: none;
+    user-select: none;
+    font-size: 14px;
+  }
+  .custom-placeholder .highlight {
+    color: #1c53d9;
+    background-color: #e8edfb;
+    padding: 3px 5px;
+    margin: 0 5px;
+    font-size: 12px;
+  }
+  .textarea-bottom {
+    display: flex;
+    background-color: #f5f8fd;
+    justify-content: space-between;
+    padding: 3px 16px;
+    border: 1px solid #c0ccda;
+    border-top: 0 none;
+    border-radius: 0 0 4px 4px;
+  }
+  .ant-dropdown-link {
+    color: rgb(119, 140, 162);
+  }
+  .character-count {
+    text-align: right;
+    color: #a9a9a9;
+    font-size: 12px;
+    margin-top: 5px;
+  }
+  .upload-contant {
+    width: 750px;
+    display: flex;
+  }
+  ::deep .ant-upload-wrapper .ant-upload-drag {
+    background-color: #fff;
+    padding: 20px 0;
+  }
+  .upload {
+    width: 600px;
+    display: flex;
+  }
+  .upload:hover {
+    border-color: #1890ff;
+  }
+  .upload-file-style{
+    min-width: 400px;
+    padding: 5px;
+    display: inline-block;
+    background: #f6f6fa;
+    cursor: pointer;
+  }
+</style>

+ 10 - 5
src/views/wechat/sop_task/add_sop/components/sopTaskName.vue

@@ -12,12 +12,12 @@
               @blur="handleInputBlur"
             />
           </FormItem>
-          <FormItem label="有效期">
+          <!-- <FormItem label="有效期">
             <Radio.Group v-model:value="form.validity" @change="handleValidityChange">
-              <Radio value="permanent" :disabled="publishBtn">不限</Radio>
+              <Radio value="permanent" :disabled="publishBtn">不限</Radio> -->
               <!-- <Radio value="temporary">时间段</Radio> -->
-            </Radio.Group>
-          </FormItem>
+            <!-- </Radio.Group>
+          </FormItem> -->
           <template v-if="form.validity === 'temporary'">
             <FormItem>
               <DatePicker.RangePicker
@@ -29,10 +29,11 @@
           </template>
           <FormItem>
             <!-- <Button @click="handleAdminConfig">配置社交账号</Button> -->
-            <Button :disabled="!nameTimeValidation" @click="handleSaveDraft">暂存草稿</Button>
+            <!-- <Button :disabled="!nameTimeValidation" @click="handleSaveDraft">暂存草稿</Button> -->
             <Button type="primary" html-type="submit" :disabled="publishBtn" @click="handlePublish">
               发布
             </Button>
+            <!-- <Button style="margin-left: 10px" :disabled="!nameTimeValidation" @click="handleTest">测试</Button>  -->
           </FormItem>
         </Form>
 
@@ -442,6 +443,10 @@
   //     });
   //   }
   // };
+  function handleTest() {
+
+    console.log('测试按钮');
+  }
 </script>
 
 <style scoped lang="less">

+ 135 - 38
src/views/wechat/sop_task/add_sop/components/userPhasesDrawer.vue

@@ -179,6 +179,16 @@
                 </template>
                 打标签
               </Button>
+              <Button
+                :disabled="btnDisabled"
+                :class="isForwardAction ? 'action-btn' : 'btn-style'"
+                @click="setTaskType('forward')"
+              >
+                <template #icon>
+                  <ShareAltOutlined />
+                </template>
+                转发人工
+              </Button>
             </FormItem>
             <!-- 发消息 -->
             <Collapse
@@ -258,6 +268,73 @@
                       请选择标签
                     </span>
                   </FormItem>
+                  <FormItem
+                    label="用户进入当前阶段时,可以移出标签"
+                    name="tagging"
+                    v-if="msgAction"
+                    :labelCol="{ span: 8 }"
+                  >
+                    <!-- :rules="[{ required: true, message: '请选择一个标签', trigger: 'change' },]" -->
+                    <Select
+                      :disabled="btnDisabled"
+                      v-model:value="taskForm.delTagValue"
+                      :options="actionLabel"
+                      allowClear
+                      mode="multiple"
+                      size="middle"
+                      placeholder="请选择"
+                      :style="{ width: '240px', margin: '0 5px' }"
+                      :max-tag-count="1"
+                    ></Select>
+                    <span v-show="taskForm.delTagValue.length === 0" class="warning-style">
+                      请选择标签
+                    </span>
+                  </FormItem>
+                </div>
+              </Collapse.Panel>
+            </Collapse>
+            <!-- 转发人工 -->
+            <Collapse
+              v-show="isForwardAction"
+              v-model:activeKey="activeKeyForward"
+              collapsible="header"
+              class="custom-collapse-forwards"
+            >
+              <Collapse.Panel key="1" class="custom-collapse-panel">
+                <template #header>
+                  <div @click.stop="toggleTimeoutAction" style="display: flex; align-items: center">
+                    <ShareAltOutlined style="color: #eabb63" />
+                    &nbsp;&nbsp;
+                    <span>转发人工</span>
+                  </div>
+                </template>
+                <div @click.stop>
+                  <!-- 根据发送方式显示 MsgContant 或在折叠面板中展示 -->
+                  <div>
+                    <FormItem
+                    label="转发至"
+                      name="wxid"
+                      :rules="[{ required: true, message: '请填写接收人微信id' }]"
+                    >
+                    <Input
+                      :disabled="btnDisabled"
+                      type="text"
+                      v-model:value="taskForm.wxid"
+                      placeholder="请输入接收人微信id,多个微信id请用逗号分隔"
+                    />
+                    </FormItem>
+                    <FormItem
+                      label=""
+                      name="forwardMessage"
+                      :rules="[{ required: true, message: '请输入消息内容' }]"
+                    >
+                      <MsgContantForwards
+                        v-model:value="taskForm.forwardMessage"
+                        :msgDisabled="btnDisabled"
+                      />
+                    </FormItem>
+                    <!-- <component :is="MsgContant" /> -->
+                  </div>
                 </div>
               </Collapse.Panel>
             </Collapse>
@@ -299,10 +376,12 @@
     CloseCircleOutlined,
     MessageOutlined,
     TagOutlined,
+    ShareAltOutlined,
   } from '@ant-design/icons-vue';
   import type { FormInstance } from 'ant-design-vue';
   // import HtmlTextarea from './htmlTextarea.vue';
   import MsgContant from './msgContant.vue';
+  import MsgContantForwards from './msgContantForwards.vue';
   import TimedSending from './timedSending.vue';
 
   import {
@@ -346,7 +425,6 @@
   const taskFormRef = ref<FormInstance | null>(null);
   const dayValue = ref<number>(1);
   const strValue = ref<string>('09:00:00');
-  // const tagValue = ref<number>();
   const labelIsShow = ref<boolean>(false);
   // 定义动态组件插槽或具名插槽
   // const MsgContantSlot = MsgContant;
@@ -379,20 +457,28 @@
     sendType: 'immediate',
     actionMessage: [{ type: 1, content: '' }],
     tagValue: [],
+    delTagValue: [],
+    forwardMessage: [{ type: 1, content: '' }],
+    wxid: '',
   });
   const initialTaskForm = reactive({
     taskType: 'sendMessage',
     sendType: 'immediate',
     actionMessage: [{ type: 1, content: '' }],
     tagValue: [],
+    delTagValue: [],
+    forwardMessage: [{ type: 1, content: '' }],
+    wxid: '',
   });
   const activeKey = ref<number | string>('1');
   const activeKeyTiem = ref<number | string>('time1');
   const activeKeyTag = ref<number | string>('1');
+  const activeKeyForward = ref<number | string>('1');
   // const msgAction = ref<boolean>(false);
   const msgAction = ref(taskForm.taskType === 'sendMessage');
   const isMsgAction = ref<boolean>(true);
   const isTagAction = ref<boolean>(false);
+  const isForwardAction = ref<boolean>(false);
   // const isDataLoaded = ref(false); // 控制表单的渲染
   onMounted(() => {
     // loading.value = true;
@@ -465,16 +551,23 @@
     if (data.data.actionLabel.length > 0) {
       taskTypes.push('tag');
     }
+    if (data.data.actionForward.action.length > 0) {
+      taskTypes.push('forward');
+    }
     // 填充 taskForm 数据
     Object.assign(taskForm, {
       taskType: taskTypes,
       sendType: 'immediate',
       actionMessage: data.data.actionMessage || [{ type: 1, content: '' }],
-      tagValue: data.data.actionLabel || [],
+      tagValue: data.data.actionLabelAdd || [],
+      delTagValue:data.data.actionLabelDel || [],
+      forwardMessage: data.data.actionForward.action || [{ type: 1, content: '' }],
+      wxid: data.data.actionForward.wxid || '',      
     });
-    // 根据 taskType 设置 isMsgAction 和 isTagAction
+    // 根据 taskType 设置 isMsgAction 和 isTagAction、isForwardAction
     isMsgAction.value = taskTypes.includes('sendMessage');
     isTagAction.value = taskTypes.includes('tag');
+    isForwardAction.value = taskTypes.includes('forward');
   }
 
   function processArray(array: any[]) {
@@ -488,12 +581,28 @@
     });
     return array;
   }
+  function processForwardsArray(array: any[]) {
+    array.forEach((item) => {
+      if (item.type === 2 && Array.isArray(item.content)) {
+        const contentString = item.content[0]; // 假设content只有一个元素
+        const filename = contentString.split('/').pop(); // 获取最后一个/后的值
+        item.content = contentString; // 将content数组转成字符串
+        item.meta = { filename }; // 添加meta对象
+      }
+    });
+    return array;
+  }
   async function submitForm() {
     if (!btnDisabled.value) {
       // 提交表单逻辑 conditionType,
       const { phaseName,  conditionOperator, conditionList } = form;
-      const { taskType, sendType, actionMessage, tagValue } = taskForm;
+      const { taskType, sendType, actionMessage, tagValue, delTagValue,forwardMessage, wxid } = taskForm;
+      let actionForward = {};
+      actionForward.wxid = wxid;
+      actionForward.action = forwardMessage;
+
       processArray(actionMessage)
+      processForwardsArray(forwardMessage)
       const requestData = {
         name: phaseName,
         // conditionType: ~~conditionType,
@@ -503,12 +612,15 @@
         taskType,
         sendType,
         actionMessage,
-        actionLabel: tagValue,
+        actionLabelAdd: tagValue,
+        actionLabelDel:delTagValue,
+        actionForward,
       };
       const allData = actionMessage.every((item) => item.content.trim() !== '');
-      if ((isMsgAction.value && !allData) || (isTagAction.value && tagValue.length === 0)) {
+      if ((isMsgAction.value && !allData) || (isTagAction.value && tagValue.length === 0 && delTagValue == 0)) {
         return;
       }
+      console.log(forwardMessage, wxid,'forwardMessage, wxid')
       if (unref(stageId)) {
         // 编辑
         loading.value = true;
@@ -553,17 +665,6 @@
     emit('update:open', false);
   }
 
-  // function resetForms () {
-  //   Object.assign(form, initialForm);
-  //   Object.assign(taskForm, initialTaskForm);
-  //   msgAction.value = '';
-  //   isMsgAction.value = true;
-  //   isTagAction.value = false;
-  //   dayValue.value = 1;
-  //   strValue.value = '09:00:00';
-  //   // tagValue.value = [];
-  // };
-
   // 在打开抽屉时保存初始状态
   watch(visible, (newVal) => {
     if (newVal) {
@@ -581,6 +682,7 @@
       if (!isTagAction.value) {
         taskForm.taskType = '';
         taskForm.tagValue = [];
+        taskForm.delTagValue = [];
       }
     } else if (type === 'sendMessage') {
       msgAction.value = type === 'sendMessage';
@@ -589,28 +691,16 @@
         taskForm.taskType = '';
         taskForm.actionMessage = [{ type: 1, content: '' }];
       }
+    }else if(type === 'forward'){
+      msgAction.value = type === 'forward';
+      isForwardAction.value = !isForwardAction.value;
+      if (!isForwardAction.value) {
+        taskForm.taskType = '';
+        taskForm.forwardMessage = [{ type: 1, content: '' }];
+      }
     }
-    // taskForm.taskType = type;
-    // msgAction.value = type === 'sendMessage';
-    // isMsgAction.value = type === 'sendMessage';
-    // isTagAction.value = type === 'tag';
-  }
-  // const setTaskType = (taskType: string) => {
-  // if(taskType === 'tag'){
-  //   isTagAction.value = !isTagAction.value;
-  // }else if(taskType === 'sendMessage'){
-  //   isMsgAction.value = !isMsgAction.value;
-  // }
-  // if (taskForm.taskType === taskType) {
-  //   // Toggle the state if the taskType is already the same
-  //   msgAction.value = !msgAction.value;
-  //   activeKey.value = msgAction.value ? ['1'] : [];
-  // } else {
-  //   taskForm.taskType = taskType;
-  //   msgAction.value = taskType === 'sendMessage';
-  //   activeKey.value = msgAction.value ? ['1'] : [];
-  // }
-  // };
+    
+  }
 
   function toggleTimeoutAction() {
     // 切换折叠面板的动作
@@ -648,6 +738,13 @@
     color: #333;
     border-radius: 0px;
     border: none;
+    margin: 30px 0 30px 0;
+  }
+  .custom-collapse-forwards {
+    background-color: rgb(253, 248, 238);
+    color: #333;
+    border-radius: 0px;
+    border: none;
     margin: 30px 0 90px 0;
   }
   .custom-collapse-panel {

+ 440 - 211
src/views/wechat/sop_task/index.vue

@@ -2,12 +2,18 @@
   <div>
     <BasicTable @register="registerTable" :scroll="{ x: 'max-content', y: '500' }">
       <template #tableTitle>
-        <Button type="primary" danger preIcon="ant-design:delete-outlined" v-if="showDeleteButton"
-          @click="handleBatchDelete">
+        <Button
+          type="primary"
+          danger
+          preIcon="ant-design:delete-outlined"
+          v-if="showDeleteButton"
+          @click="handleBatchDelete"
+        >
           {{ t('common.delete') }}
         </Button>
       </template>
       <template #toolbar>
+        <!-- <a-button @click="handleExport">导入</a-button> -->
         <a-button type="primary" @click="handleCreate">
           {{ t('wechat.sopTask.addSopTask') }}
         </a-button>
@@ -18,234 +24,457 @@
         </template>
       </template>
     </BasicTable>
+    <!-- 树形控件展示 -->
+    <Modal v-model:visible="treeModalVisible" title="大纲" footer>
+      <Tree show-line :default-expanded-keys="['0-0-0']" :tree-data="treeData" @select="onSelect">
+        <template #icon><CarryOutOutlined /></template>
+        <template #title="{ dataRef }">
+          <template v-if="dataRef.key === '0-0-0-1'">
+            <div>multiple line title</div>
+            <!-- <div>multiple line title</div> -->
+          </template>
+          <template v-else>{{ dataRef.title }}</template>
+        </template>
+        <template #switcherIcon="{ dataRef, defaultIcon }">
+          <!-- <SmileTwoTone v-if="dataRef.key === '0-0-2'" /> -->
+          <component :is="defaultIcon" />
+        </template>
+      </Tree>
+    </Modal>
+    <!-- <Modal
+      width="500px"
+      height="300px"
+      v-model:open="exportModalVisible"
+      @ok="handleExportOk"
+      @cancel="handleExportCancel"
+      title="导入"
+    >
+      <Form
+        :model="exportModalForm"
+        layout="inline"
+        style="gap: 20px; height: auto; display: flex; flex-direction: column"
+      >
+        <FormItem name="name" label="任务名称" style="margin-left: 30px">
+          <Input v-model:value="exportModalForm.name" placeholder="请输入任务名称" />
+        </FormItem>
+        <FormItem name="" style="margin-left: 30px">
+          <BasicUpload
+            :maxSize="10"
+            :maxNumber="1"
+            @change="handleChange"
+            :api="uploadApi"
+            v-model:value="exportModalForm.url"
+            :showPreviewNumber="false"
+            emptyHidePreview
+            :accept="['.xls', '.xlsx']"
+          />
+        </FormItem>
+      </Form>
+    </Modal> -->
+    <Modal
+      width="500px"
+      height="300px"
+      v-model:open="modalVisible"
+      title="复制"
+      @ok="handleOk"
+      @cancel="handleCancel"
+      class="custom-modal"
+    >
+      <Form :model="form" layout="inline" style="gap: 20px; height: auto">
+        <FormItem
+          name="name"
+          label="任务名称"
+          style="margin-left: 30px"
+          :rules="[{ required: true, message: '请输入任务名称' }]"
+        >
+          <Input v-model:value="form.name" placeholder="请输入任务名称" />
+        </FormItem>
+        <FormItem v-if="permCode === '001'" name="id" label="租户" style="margin-left: 30px">
+          <Select
+            v-model:value="form.id"
+            :options="modeList"
+            allowClear
+            size="middle"
+            placeholder="请选择"
+            :style="{ width: '330px', margin: '0 5px' }"
+          ></Select>
+        </FormItem>
+      </Form>
+    </Modal>
   </div>
 </template>
 <script lang="ts">
-import { createVNode, defineComponent, ref } from 'vue';
-import { Modal, message } from 'ant-design-vue';
-import { ExclamationCircleOutlined } from '@ant-design/icons-vue/lib/icons';
-import { BasicTable, useTable, TableAction } from '@/components/Table';
-import { Button } from '@/components/Button';
-import { useI18n } from 'vue-i18n';
-import { columns, searchFormSchema } from './sopTask.data';
-import { getSopTaskList, deleteSopTask ,stopSopTask,startSopTask} from '@/api/wechat/sopTask';
-import { useGo } from "/@/hooks/web/usePage";
-import { PageEnum } from '@/enums/pageEnum';
-import { useRouter } from 'vue-router';
-export default defineComponent({
-  name: 'SopTaskManagement',
-  components: { BasicTable, TableAction, Button },
+  import { createVNode, defineComponent, ref, reactive, onMounted } from 'vue';
+  import { Modal, message, Form, FormItem, Select, Input, Tree } from 'ant-design-vue';
+  import {
+    ExclamationCircleOutlined,
+    CarryOutOutlined,
+    SmileTwoTone,
+  } from '@ant-design/icons-vue/lib/icons';
+  import { BasicTable, useTable, TableAction } from '@/components/Table';
+  import { Button } from '@/components/Button';
+  import { useI18n } from 'vue-i18n';
+  import { columns, searchFormSchema } from './sopTask.data';
+  import {
+    getSopTaskList,
+    deleteSopTask,
+    stopSopTask,
+    startSopTask,
+    copySopTask,
+  } from '@/api/wechat/sopTask';
+  import { getDepartmentList } from '@/api/sys/department';
+  import { useGo } from '/@/hooks/web/usePage';
+  import { PageEnum } from '@/enums/pageEnum';
+  import { useRouter } from 'vue-router';
+  import { getPermCode } from '@/api/sys/user';
+  import { BasicUpload } from '@/components/Upload';
+  import { deleteFile, downloadFile, getFileList, uploadApi } from '@/api/fms/file';
+  export default defineComponent({
+    name: 'SopTaskManagement',
+    components: {
+      BasicTable,
+      TableAction,
+      Button,
+      Modal,
+      Form,
+      FormItem,
+      Select,
+      Input,
+      BasicUpload,
+      Tree,
+    },
 
-  setup() {
-    const router = useRouter();
-    const { t } = useI18n();
-    const selectedIds = ref<number[] | string[]>();
-    const showDeleteButton = ref<boolean>(false);
-    
-    const go = useGo();
-    const [registerTable, { reload }] = useTable({
-      title: t('wechat.sopTask.sopTaskList'),
-      api: getSopTaskList,
-      columns,
-      formConfig: {
-        // labelWidth: 120,
-        schemas: searchFormSchema,
-      },
-      useSearchForm: true,
-      showTableSetting: true,
-      bordered: false,
-      showIndexColumn: false,
-      clickToRowSelect: false,
-      scroll:{ x: 1000, y: 500 },
-      actionColumn: {
-        width: 200,
-        title: t('common.action'),
-        dataIndex: 'action',
-        fixed: 'right',
-      },
-      rowKey: 'id',
-      rowSelection: {
-        type: 'checkbox',
-        onChange: (selectedRowKeys, _selectedRows) => {
-          selectedIds.value = selectedRowKeys as number[];
-          showDeleteButton.value = selectedRowKeys.length > 0;
+    setup() {
+      const router = useRouter();
+      const { t } = useI18n();
+      const selectedIds = ref<number[] | string[]>();
+      const showDeleteButton = ref<boolean>(false);
+      const form = reactive({
+        name: '',
+        id: undefined,
+      });
+      const initialForm = reactive({
+        name: '',
+        id: undefined,
+      });
+      const modeList = ref<any[]>([]);
+      const exportModalVisible = ref(false);
+      const treeModalVisible = ref(false);
+      const treeData = ref([]);
+      const exportModalForm = reactive({
+        name: '',
+        url: '',
+      });
+      const initialExportModalForm = reactive({
+        name: '',
+      });
+      const recordId = ref<number>();
+      const modalVisible = ref(false);
+      const go = useGo();
+      const permCode = ref('');
+      const [registerTable, { reload }] = useTable({
+        title: t('wechat.sopTask.sopTaskList'),
+        api: getSopTaskList,
+        columns,
+        formConfig: {
+          // labelWidth: 120,
+          schemas: searchFormSchema,
+        },
+        useSearchForm: true,
+        showTableSetting: true,
+        bordered: false,
+        showIndexColumn: false,
+        clickToRowSelect: false,
+        scroll: { x: 1000, y: 500 },
+        actionColumn: {
+          width: 300,
+          title: t('common.action'),
+          dataIndex: 'action',
+          fixed: 'right',
+        },
+        rowKey: 'id',
+        rowSelection: {
+          type: 'checkbox',
+          onChange: (selectedRowKeys, _selectedRows) => {
+            selectedIds.value = selectedRowKeys as number[];
+            showDeleteButton.value = selectedRowKeys.length > 0;
+          },
         },
-      },
-    });
-
-    //新建SOP页面跳转
-    function handleCreate() {
-      console.log('跳转SOP页面')
-      go(PageEnum.ADD_SOP);
-    }
-
-    function handleEdit(record: Recordable) {
-      console.log('编辑',record.id);
-      router.push({
-        path: PageEnum.ADD_SOP,
-        query: { task_id: record.id }
       });
-    }
-
-    async function handleDelete(record: Recordable) {
-      const result = await deleteSopTask({ ids: [record.id] });
-      if (result.code === 0) {
-        await reload();
-      }
-    }
-
-    //复制
-    function handleCopy(record: Recordable) {
-      message.success('复制成功!')
-      console.log('复制')
-    }
 
-    //执行任务
-    function handleTask(record: Recordable) {
-      console.log('执行任务')
-      router.push({
-        path: PageEnum.PERFORM_TASKS,
-        query: { task_id: record.id }
+      onMounted(async () => {
+        let res = await getPermCode();
+        permCode.value = res.data[0];
+        let res1 = await getDepartmentList({ page: 1, pageSize: 1000 });
+        const data = res1.data.data.map((item) => ({
+          label: item.name,
+          value: item.id,
+        }));
+        modeList.value = data;
       });
-    }
-
-    //启用
-    async function handleOpen(record: Recordable) {
-      console.log('启用')
-      // record.status = 2;
-      await startSopTask({ id: record.id });
-      message.success('启用成功!')
-      await reload();
-    }
+      //新建SOP页面跳转
+      function handleCreate() {
+        go(PageEnum.ADD_SOP);
+      }
 
-    //禁用
-    async function handleStop(record: Recordable) {
-      console.log('禁用',record.id)
-      // record.status = 3;
-      let result = await stopSopTask({id: record.id});
-      message.success('禁用成功!')
-       await reload();
-    }
+      //导入表格
+      function handleExport() {
+        exportModalVisible.value = true;
+      }
+      function handleEdit(record: Recordable) {
+        router.push({
+          path: PageEnum.ADD_SOP,
+          query: { task_id: record.id },
+        });
+      }
 
-    //查看 
-    function handleView(record: Recordable) {
-      console.log('查看')
-      router.push({
-        path: PageEnum.ADD_SOP,
-        query: { task_id: record.id }
-      });
-    }
+      async function handleDelete(record: Recordable) {
+        const result = await deleteSopTask({ ids: [record.id] });
+        if (result.code === 0) {
+          await reload();
+        }
+      }
 
-    async function handleBatchDelete() {
-      Modal.confirm({
-        title: t('common.deleteConfirm'),
-        icon: createVNode(ExclamationCircleOutlined),
-        async onOk() {
-          const result = await deleteSopTask({ ids: selectedIds.value as number[] });
-          if (result.code === 0) {
-            showDeleteButton.value = false;
-            await reload();
-          }
-        },
-        onCancel() {
-          console.log('Cancel');
-        },
-      });
-    }
+      //复制
+      async function handleCopy(record: Recordable) {
+        modalVisible.value = true;
+        recordId.value = record.id;
+      }
 
-    function getActions(record: Recordable) {
-      if (record.status === 1) {
-        return [
+      //大纲
+      async function handleTree(record: Recordable) {
+        treeModalVisible.value = true;
+        treeData.value = [
           {
-            label: '编辑',
-            onClick: handleEdit.bind(null, record),
-            ifShow: true,
+            title: 'parent 1',
+            key: '0-0',
+            children: [
+              {
+                title: 'parent 1-0',
+                key: '0-0-0',
+                children: [
+                  { title: 'leaf', key: '0-0-0-0' },
+                  {
+                    key: '0-0-0-1',
+                  },
+                  { title: 'leaf', key: '0-0-0-2' },
+                ],
+              },
+              {
+                title: 'parent 1-1',
+                key: '0-0-1',
+                children: [{ title: 'leaf', key: '0-0-1-0' }],
+              },
+              {
+                title: 'parent 1-2',
+                key: '0-0-2',
+                children: [
+                  { title: 'leaf 1', key: '0-0-2-0' },
+                  {
+                    title: 'leaf 2',
+                    key: '0-0-2-1',
+                  },
+                ],
+              },
+            ],
           },
           {
-            color: 'error',
-            label: '删除',
-            popConfirm: {
-              title: t('common.deleteConfirm'),
-              placement: 'left',
-              confirm: handleDelete.bind(null, record),
-            },
-            ifShow: true,
-          },
-          {
-            label: '复制',
-            onClick: handleCopy.bind(null, record),
-            ifShow: true,
-          }
-        ];
-      } else if (record.status === 2) {
-        return [
-          // {
-          //   icon: 'clarity:note-edit-line',
-          //   onClick: handleEdit.bind(null, record),
-          //   tooltip: '编辑',
-          //   ifShow: true,
-          // },
-          {
-            // color: 'error',
-            onClick: handleOpen.bind(null, record),
-            label: '启用',
-            ifShow: true,
-          },
-          {
-            onClick: handleTask.bind(null, record),
-            label: '执行任务',
-            ifShow: true,
+            title: 'parent 2',
+            key: '0-1',
+            children: [
+              {
+                title: 'parent 2-0',
+                key: '0-1-0',
+                children: [
+                  { title: 'leaf', key: '0-1-0-0' },
+                  { title: 'leaf', key: '0-1-0-1' },
+                ],
+              },
+            ],
           },
-          {
-            onClick: handleView.bind(null, record),
-            label: '查看',
-            ifShow: true,
-          }
         ];
-      } else if (record.status === 3) {
-        return [
-          // {
-          //   icon: 'clarity:note-edit-line',
-          //   onClick: handleEdit.bind(null, record),
-          //   tooltip: '编辑',
-          //   ifShow: true,
-          // },
-          {
-            color: 'error',
-            onClick: handleStop.bind(null, record),
-            label: '禁用',
-            ifShow: true,
+
+      }
+
+      function onSelect(keys, event) {
+        console.log('Selected: ', keys, event);
+      }
+      async function handleOk() {
+        let res = await copySopTask({ id: recordId.value, name: form.name, organizationId: form.id });
+        message.success('复制成功!');
+        modalVisible.value = false;
+        await reload();
+      }
+      //执行任务
+      function handleTask(record: Recordable) {
+        router.push({
+          path: PageEnum.PERFORM_TASKS,
+          query: { task_id: record.id },
+        });
+      }
+
+      //启用
+      async function handleOpen(record: Recordable) {
+        // record.status = 2;
+        await startSopTask({ id: record.id });
+        message.success('启用成功!');
+        await reload();
+      }
+
+      //禁用
+      async function handleStop(record: Recordable) {
+        // record.status = 3;
+        let result = await stopSopTask({ id: record.id });
+        message.success('禁用成功!');
+        await reload();
+      }
+
+      //查看
+      function handleView(record: Recordable) {
+        router.push({
+          path: PageEnum.ADD_SOP,
+          query: { task_id: record.id },
+        });
+      }
+
+      async function handleBatchDelete() {
+        Modal.confirm({
+          title: t('common.deleteConfirm'),
+          icon: createVNode(ExclamationCircleOutlined),
+          async onOk() {
+            const result = await deleteSopTask({ ids: selectedIds.value as number[] });
+            if (result.code === 0) {
+              showDeleteButton.value = false;
+              await reload();
+            }
           },
-          {
-            onClick: handleTask.bind(null, record),
-            label: '执行任务',
-            ifShow: true,
+          onCancel() {
+            console.log('Cancel');
           },
-          {
-            onClick: handleView.bind(null, record),
-            label: '查看',
-            ifShow: true,
-          }
-        ];
+        });
+      }
+
+      function getActions(record: Recordable) {
+        if (record.status === 1) {
+          return [
+            {
+              label: '编辑',
+              onClick: handleEdit.bind(null, record),
+            },
+            {
+              color: 'error',
+              label: '删除',
+              popConfirm: {
+                title: t('common.deleteConfirm'),
+                placement: 'left',
+                confirm: handleDelete.bind(null, record),
+              },
+            },
+            {
+              label: '复制',
+              onClick: handleCopy.bind(null, record),
+            },
+            // {
+            //   label: '大纲',
+            //   onClick: handleTree.bind(null, record),
+            // },
+          ];
+        } else if (record.status === 2) {
+          return [
+            // {
+            //   icon: 'clarity:note-edit-line',
+            //   onClick: handleEdit.bind(null, record),
+            //   tooltip: '编辑',
+            // },
+            {
+              onClick: handleOpen.bind(null, record),
+              label: '启用',
+              ifShow: true,
+            },
+            {
+              onClick: handleTask.bind(null, record),
+              label: '执行任务',
+              ifShow: true,
+            },
+            {
+              onClick: handleView.bind(null, record),
+              label: '查看',
+              ifShow: true,
+            },
+            {
+              label: '复制',
+              onClick: handleCopy.bind(null, record),
+              ifShow: true,
+            },
+            // {
+            //   label: '大纲',
+            //   onClick: handleTree.bind(null, record),
+            // },
+          ];
+        } else if (record.status === 3) {
+          return [
+            // {
+            //   icon: 'clarity:note-edit-line',
+            //   onClick: handleEdit.bind(null, record),
+            //   tooltip: '编辑',
+            // },
+            {
+              color: 'error',
+              onClick: handleStop.bind(null, record),
+              label: '禁用',
+            },
+            {
+              onClick: handleTask.bind(null, record),
+              label: '执行任务',
+            },
+            {
+              onClick: handleView.bind(null, record),
+              label: '查看',
+            },
+            {
+              label: '复制',
+              onClick: handleCopy.bind(null, record),
+            },
+            // {
+            //   label: '大纲',
+            //   onClick: handleTree.bind(null, record),
+            // },
+          ];
+        }
+        return [];
       }
-      return [];
-    }
 
-    return {
-      t,
-      registerTable,
-      handleCreate,
-      handleEdit,
-      handleDelete,
-      handleCopy,
-      handleOpen,
-      handleTask,
-      handleStop,
-      handleView,
-      getActions,
-      handleBatchDelete,
-      showDeleteButton,
-    };
-  },
-});
+      return {
+        t,
+        registerTable,
+        handleCreate,
+        handleEdit,
+        handleDelete,
+        handleCopy,
+        handleOpen,
+        handleTask,
+        handleStop,
+        handleView,
+        getActions,
+        handleBatchDelete,
+        showDeleteButton,
+        modalVisible,
+        form,
+        initialForm,
+        permCode,
+        handleExport,
+        exportModalVisible,
+        initialExportModalForm,
+        exportModalForm,
+        modeList,
+        recordId,
+        handleOk,
+        uploadApi,
+        handleTree,
+        treeModalVisible,
+        treeData,
+        onSelect,
+      };
+    },
+  });
 </script>