kyoyue 8 kuukautta sitten
vanhempi
commit
122ecd5283
49 muutettua tiedostoa jossa 8095 lisäystä ja 201 poistoa
  1. 39 37
      package.json
  2. 15 0
      src/api/model/baseModel.ts
  3. 3 1
      src/api/wechat/model/sopTaskModel.ts
  4. 145 1
      src/api/wechat/sopTask.ts
  5. BIN
      src/assets/img/add-close.png
  6. BIN
      src/assets/img/add-close1.png
  7. BIN
      src/assets/img/cancel.png
  8. BIN
      src/assets/img/check_box.png
  9. BIN
      src/assets/img/icon_file.png
  10. BIN
      src/assets/img/icon_people.png
  11. BIN
      src/assets/img/icon_role.png
  12. BIN
      src/assets/img/jiaojiao.png
  13. BIN
      src/assets/img/list_search.png
  14. BIN
      src/assets/img/loading.gif
  15. BIN
      src/assets/img/next_level.png
  16. BIN
      src/assets/img/next_level_active.png
  17. 1 1
      src/components/VxeTable/src/css/index.scss
  18. 8 0
      src/enums/pageEnum.ts
  19. 5 2
      src/main.ts
  20. 157 0
      src/utils/setting.ts
  21. 9 0
      src/views/wechat/sop_group/index.vue
  22. 89 0
      src/views/wechat/sop_task/add_sop/components/customIcons.vue
  23. 263 0
      src/views/wechat/sop_task/add_sop/components/flowChart/api/index.js
  24. 205 0
      src/views/wechat/sop_task/add_sop/components/flowChart/components/blankBox.vue
  25. 343 0
      src/views/wechat/sop_task/add_sop/components/flowChart/components/nodeWrap.vue
  26. 1762 0
      src/views/wechat/sop_task/add_sop/components/flowChart/css/workflow.css
  27. 47 0
      src/views/wechat/sop_task/add_sop/components/flowChart/utils/axios.js
  28. 42 0
      src/views/wechat/sop_task/add_sop/components/flowChart/utils/const.js
  29. 157 0
      src/views/wechat/sop_task/add_sop/components/flowChart/utils/index.js
  30. 102 0
      src/views/wechat/sop_task/add_sop/components/htmlTextarea.vue
  31. 651 0
      src/views/wechat/sop_task/add_sop/components/leftStage.vue
  32. 472 0
      src/views/wechat/sop_task/add_sop/components/msgContant.vue
  33. 604 0
      src/views/wechat/sop_task/add_sop/components/nodeConfigDrawer.vue
  34. 451 0
      src/views/wechat/sop_task/add_sop/components/nodeMsgContant.vue
  35. 103 0
      src/views/wechat/sop_task/add_sop/components/productContant.vue
  36. 442 0
      src/views/wechat/sop_task/add_sop/components/productNote.vue
  37. 511 0
      src/views/wechat/sop_task/add_sop/components/sopTaskName.vue
  38. 166 0
      src/views/wechat/sop_task/add_sop/components/timedSending.vue
  39. 30 0
      src/views/wechat/sop_task/add_sop/components/tinymce.ts
  40. 618 0
      src/views/wechat/sop_task/add_sop/components/userPhasesDrawer.vue
  41. 44 0
      src/views/wechat/sop_task/add_sop/index.vue
  42. BIN
      src/views/wechat/sop_task/add_sop/static/note.png
  43. BIN
      src/views/wechat/sop_task/add_sop/static/weChat.png
  44. 92 0
      src/views/wechat/sop_task/add_sop/stores/index.js
  45. 196 119
      src/views/wechat/sop_task/index.vue
  46. 76 0
      src/views/wechat/sop_task/perform_tasks/index.vue
  47. 150 0
      src/views/wechat/sop_task/send_tasks/index.vue
  48. 97 40
      src/views/wechat/sop_task/sopTask.data.ts
  49. BIN
      微信后台.zip

+ 39 - 37
package.json

@@ -71,65 +71,65 @@
     "@ant-design/icons-vue": "^7.0.1",
     "@axolo/tree-array": "^0.1.0",
     "@codemirror/lang-json": "^6.0.1",
-    "@codemirror/language": "^6.10.1",
-    "@codemirror/legacy-modes": "^6.3.3",
+    "@codemirror/language": "^6.10.2",
+    "@codemirror/legacy-modes": "^6.4.0",
     "@iconify/iconify": "^3.1.1",
-    "@logicflow/core": "^1.2.22",
-    "@logicflow/extension": "^1.2.22",
-    "@uiw/codemirror-theme-github": "^4.21.24",
+    "@logicflow/core": "^1.2.27",
+    "@logicflow/extension": "^1.2.27",
+    "@uiw/codemirror-theme-github": "^4.22.2",
     "@uponu/vuedraggable": "^4.1.3",
     "@vben/hooks": "workspace:*",
-    "@vue/shared": "^3.4.21",
-    "@vueuse/core": "^10.9.0",
-    "@vueuse/shared": "^10.9.0",
+    "@vue/shared": "^3.4.29",
+    "@vueuse/core": "^10.11.0",
+    "@vueuse/shared": "^10.11.0",
     "@zxcvbn-ts/core": "^3.0.4",
-    "ant-design-vue": "^4.1.2",
-    "axios": "^1.6.8",
+    "ant-design-vue": "^4.2.3",
+    "axios": "^1.7.2",
     "codemirror": "^6.0.1",
-    "cropperjs": "^1.6.1",
+    "cropperjs": "^1.6.2",
     "crypto-js": "^4.2.0",
-    "dayjs": "^1.11.10",
+    "dayjs": "^1.11.11",
     "echarts": "^5.5.0",
     "exceljs": "^4.4.0",
     "file2md5": "^1.3.0",
     "lodash-es": "^4.17.21",
     "mockjs": "^1.1.0",
     "nprogress": "^0.2.0",
-    "path-to-regexp": "^6.2.1",
+    "path-to-regexp": "^6.2.2",
     "pinia": "2.1.7",
     "pinia-plugin-persistedstate": "^3.2.1",
     "print-js": "^1.6.0",
-    "qs": "^6.12.0",
+    "qs": "^6.12.1",
     "resize-observer-polyfill": "^1.5.1",
     "showdown": "^2.1.0",
     "sortablejs": "^1.15.2",
     "tinymce": "^6.8.3",
-    "unocss": "^0.58.6",
-    "vditor": "^3.10.1",
-    "vue": "^3.4.21",
+    "unocss": "^0.58.9",
+    "vditor": "^3.10.4",
+    "vue": "^3.4.29",
     "vue-clipboard3": "^2.0.0",
     "vue-codemirror": "^6.1.1",
-    "vue-i18n": "^9.10.2",
-    "vue-json-pretty": "^2.3.0",
-    "vue-router": "^4.3.0",
-    "vue-types": "^5.1.1",
+    "vue-i18n": "^9.13.1",
+    "vue-json-pretty": "^2.4.0",
+    "vue-router": "^4.3.3",
+    "vue-types": "^5.1.2",
     "vuedraggable": "^4.1.0",
-    "vxe-table": "^4.5.21",
-    "vxe-table-plugin-export-xlsx": "^4.0.1",
-    "xe-utils": "^3.5.22",
+    "vxe-table": "^4.7.17",
+    "vxe-table-plugin-export-xlsx": "^4.0.2",
+    "xe-utils": "^3.5.27",
     "xlsx": "^0.18.5"
   },
   "devDependencies": {
     "@commitlint/cli": "^18.6.1",
     "@commitlint/config-conventional": "^18.6.3",
-    "@iconify/json": "^2.2.192",
+    "@iconify/json": "^2.2.220",
     "@purge-icons/generated": "^0.10.0",
     "@types/codemirror": "^5.60.15",
     "@types/crypto-js": "^4.2.2",
     "@types/lodash-es": "^4.17.12",
     "@types/mockjs": "^1.0.10",
     "@types/nprogress": "^0.2.3",
-    "@types/qs": "^6.9.12",
+    "@types/qs": "^6.9.15",
     "@types/showdown": "^2.0.6",
     "@types/sortablejs": "^1.15.8",
     "@vben/eslint-config": "workspace:*",
@@ -137,21 +137,23 @@
     "@vben/ts-config": "workspace:*",
     "@vben/types": "workspace:*",
     "@vben/vite-config": "workspace:*",
-    "@vue/compiler-sfc": "^3.4.21",
-    "@vue/test-utils": "^2.4.5",
+    "@vue/compiler-sfc": "^3.4.29",
+    "@vue/test-utils": "^2.4.6",
+    "babel-plugin-import": "^1.13.8",
     "cross-env": "^7.0.3",
-    "cz-git": "^1.9.0",
-    "czg": "^1.9.0",
+    "cz-git": "^1.9.3",
+    "czg": "^1.9.3",
     "husky": "^9.0.11",
     "lint-staged": "15.2.2",
-    "prettier": "^3.2.5",
-    "prettier-plugin-packagejson": "^2.4.12",
-    "rimraf": "^5.0.5",
-    "turbo": "^1.12.5",
-    "typescript": "^5.4.2",
+    "prettier": "^3.3.2",
+    "prettier-plugin-packagejson": "^2.5.0",
+    "rimraf": "^5.0.7",
+    "turbo": "^1.13.4",
+    "typescript": "^5.4.5",
     "unbuild": "^2.0.0",
-    "vite": "^5.1.6",
-    "vite-plugin-mock": "^3.0.1",
+    "vite": "^5.3.1",
+    "vite-plugin-babel-import": "^2.0.5",
+    "vite-plugin-mock": "^3.0.2",
     "vue-tsc": "^1.8.27"
   },
   "packageManager": "pnpm@8.10.0",

+ 15 - 0
src/api/model/baseModel.ts

@@ -23,6 +23,21 @@ export interface BaseIDReq {
   id?: number;
 }
 
+export interface BaseNodeReq{
+  stageId?: number;
+  id?: number;
+  taskId?: number;
+  parentId?: number;
+  data?: any;
+  name?: string;
+  conditionType?: number;
+  conditionOperator?: number;
+  conditionList?: any[];
+  actionMessage?: any[];
+  actionLabel?: any[];
+  task_id?: number;
+}
+
 export interface BaseIDsReq {
   ids: number[];
 }

+ 3 - 1
src/api/wechat/model/sopTaskModel.ts

@@ -9,8 +9,10 @@ export interface SopTaskInfo {
   updatedAt?: number;
   status?: number;
   name?: string;
-  botWxidList?: []*string;
+  planStartTime?: number;
+  planEndTime?: number;
   type?: number;
+  botWxidList?: any;
   planStartTime?: number;
   planEndTime?: number;
   creatorId?: number;

+ 145 - 1
src/api/wechat/sopTask.ts

@@ -1,6 +1,6 @@
 import { defHttp } from '@/utils/http/axios';
 import { ErrorMessageMode } from '/#/axios';
-import { BaseDataResp, BaseListReq, BaseResp, BaseIDsReq, BaseIDReq } from '@/api/model/baseModel';
+import { BaseDataResp, BaseListReq, BaseResp, BaseIDsReq, BaseIDReq, BaseNodeReq } from '@/api/model/baseModel';
 import { SopTaskInfo, SopTaskListResp } from './model/sopTaskModel';
 
 enum Api {
@@ -9,6 +9,17 @@ enum Api {
   GetSopTaskList = '/wechat-api/sop_task/list',
   DeleteSopTask = '/wechat-api/sop_task/delete',
   GetSopTaskById = '/wechat-api/sop_task',
+  GetSopStageById = '/wechat-api/sop_stage',
+  GetSopStageDetailById = '/wechat-api/sop_stage/detail',
+  AddSopTaskStage = '/wechat-api/sop_stage/create',
+  EditSopTaskStage = '/wechat-api/sop_stage/update',
+  GetSopStageList = '/wechat-api/sop_stage/list',
+  GetSopStageNodeList = '/wechat-api/sop_node/list',
+  AddSopStageNode = '/wechat-api/sop_node/create', 
+  EditSopStageNode = '/wechat-api/sop_node/update',
+  GetSopStageNodeDetail = '/wechat-api/sop_node/detail',
+  DelSopStageNode = '/wechat-api/sop_node/delete',
+  PublishSopTask = '/wechat-api/sop_task/publish',
 }
 
 /**
@@ -72,3 +83,136 @@ export const getSopTaskById = (params: BaseIDReq, mode: ErrorMessageMode = 'noti
     },
   );
 };
+
+/**
+ *  @description: 通过ID获取sop阶段
+ */
+export const getSopStageById = (params: BaseNodeReq, mode: ErrorMessageMode = 'notice') => {
+  return defHttp.post<BaseDataResp<BaseNodeReq>>(
+    { url: Api.GetSopStageById, params: params },
+    {
+      errorMessageMode: mode,
+    },
+  );
+};
+
+/**
+ *  @description: 通过ID获取sop阶段详情
+ */
+export const getSopStageDetailById = (params: BaseNodeReq, mode: ErrorMessageMode = 'notice') => {
+  return defHttp.post<BaseDataResp<BaseNodeReq>>(
+    { url: Api.GetSopStageDetailById, params: params },
+    {
+      errorMessageMode: mode,
+    },
+  );
+};
+
+/**
+ *  @description: 新建SOP阶段
+ */
+export const addSopTaskStage = (params: BaseNodeReq, mode: ErrorMessageMode = 'notice') => {
+  return defHttp.post<BaseDataResp<BaseNodeReq>>(
+    { url: Api.AddSopTaskStage, params: params },
+    {
+      errorMessageMode: mode,
+    },
+  );
+};
+
+/**
+ *  @description: 新建SOP阶段
+ */
+export const editSopTaskStage = (params: BaseNodeReq, mode: ErrorMessageMode = 'notice') => {
+  return defHttp.post<BaseDataResp<BaseNodeReq>>(
+    { url: Api.EditSopTaskStage, params: params },
+    {
+      errorMessageMode: mode,
+    },
+  );
+};
+
+/**
+ *  @description: 获取sop阶段列表
+ */
+export const getSopStageList = (params: BaseNodeReq, mode: ErrorMessageMode = 'notice') => {
+  return defHttp.post<BaseDataResp<BaseNodeReq>>(
+    { url: Api.GetSopStageList, params: params },
+    {
+      errorMessageMode: mode,
+    },
+  );
+};
+
+
+/**
+ *  @description: 获取sop节点列表
+ */
+export const getSopStageNodeList = (params: BaseNodeReq, mode: ErrorMessageMode = 'notice') => {
+  return defHttp.post<BaseDataResp<BaseNodeReq>>(
+    { url: Api.GetSopStageNodeList, params: params },
+    {
+      errorMessageMode: mode,
+    },
+  );
+};
+
+/**
+ *  @description: 新增节点
+ */
+export const addSopStageNode = (params: BaseNodeReq, mode: ErrorMessageMode = 'notice') => {
+  return defHttp.post<BaseDataResp<BaseNodeReq>>(
+    { url: Api.AddSopStageNode, params: params },
+    {
+      errorMessageMode: mode,
+    },
+  );
+};
+
+/**
+ *  @description: 更新节点
+ */
+export const editSopStageNode = (params: BaseNodeReq, mode: ErrorMessageMode = 'notice') => {
+  return defHttp.post<BaseDataResp<BaseNodeReq>>(
+    { url: Api.EditSopStageNode, params: params },
+    {
+      errorMessageMode: mode,
+    },
+  );
+};
+
+/**
+ *  @description: 获取节点详情
+ */
+export const getSopStageNodeDetail = (params: BaseNodeReq, mode: ErrorMessageMode = 'notice') => {
+  return defHttp.post<BaseDataResp<BaseNodeReq>>(
+    { url: Api.GetSopStageNodeDetail, params: params },
+    {
+      errorMessageMode: mode,
+    },
+  );
+};
+
+/**
+ *  @description: 删除节点
+ */
+export const delSopStageNode = (params: BaseNodeReq, mode: ErrorMessageMode = 'notice') => {
+  return defHttp.post<BaseDataResp<BaseNodeReq>>(
+    { url: Api.DelSopStageNode, params: params },
+    {
+      errorMessageMode: mode,
+    },
+  );
+};
+
+/**
+ *  @description: 发布sop任务
+ */
+export const publishSopTask = (params: BaseNodeReq, mode: ErrorMessageMode = 'notice') => {
+  return defHttp.post<BaseDataResp<BaseNodeReq>>(
+    { url: Api.PublishSopTask, params: params },
+    {
+      errorMessageMode: mode,
+    },
+  );
+};

BIN
src/assets/img/add-close.png


BIN
src/assets/img/add-close1.png


BIN
src/assets/img/cancel.png


BIN
src/assets/img/check_box.png


BIN
src/assets/img/icon_file.png


BIN
src/assets/img/icon_people.png


BIN
src/assets/img/icon_role.png


BIN
src/assets/img/jiaojiao.png


BIN
src/assets/img/list_search.png


BIN
src/assets/img/loading.gif


BIN
src/assets/img/next_level.png


BIN
src/assets/img/next_level_active.png


+ 1 - 1
src/components/VxeTable/src/css/index.scss

@@ -2,4 +2,4 @@
 @import './variable';
 @import './toolbar';
 @import './component';
-@import 'vxe-table/styles/index';
+// @import 'vxe-table/styles/index';

+ 8 - 0
src/enums/pageEnum.ts

@@ -11,6 +11,14 @@ export enum PageEnum {
   ERROR_LOG_PAGE = '/error-log/list',
   // oauth callback page
   OAUTH_CALLBACK = '/oauth/login/callback',
+  //sop列表路径
+  SOP_LIST = '/wechat/sop_task',
+  // 添加SOP的路径
+  ADD_SOP = '/wechat/sop_task/addSop',
+  // 执行任务路径
+  PERFORM_TASKS = '/wechat/sop_task/performTasks',
+  // 发送任务路径
+  SEND_TASKS = '/wechat/sop_task/sendTasks',
 }
 
 export const PageWrapperFixedHeightKey = 'PageWrapperFixedHeight';

+ 5 - 2
src/main.ts

@@ -6,6 +6,8 @@ import 'ant-design-vue/dist/reset.css';
 import 'virtual:svg-icons-register';
 
 import { createApp } from 'vue';
+import 'vxe-table/lib/style.css';
+
 
 import { registerGlobComp } from '@/components/registerGlobComp';
 import { setupGlobDirectives } from '@/directives';
@@ -54,10 +56,11 @@ async function bootstrap() {
   // Configure global error handling
   // 配置全局错误处理
   setupErrorHandle(app);
-
+  if (process.env.NODE_ENV === 'development') {
+  app.config.warnHandler = () => {};
+}
   // https://next.router.vuejs.org/api/#isready
   // await router.isReady();
-
   app.mount('#app');
 }
 

+ 157 - 0
src/utils/setting.ts

@@ -0,0 +1,157 @@
+function All() {}
+All.prototype = {
+    timer: "",
+    debounce(fn, delay = 500) {
+        var _this = this;
+        return function(arg) {
+            //获取函数的作用域和变量
+            let that = this;
+            let args = arg;
+            clearTimeout(_this.timer) // 清除定时器
+            _this.timer = setTimeout(function() {
+                fn.call(that, args)
+            }, delay)
+        }
+    },
+    setCookie(val) { //cookie设置[{key:value}]、获取key、清除['key1','key2']
+        for (var i = 0, len = val.length; i < len; i++) {
+            for (var key in val[i]) {
+                document.cookie = key + '=' + encodeURIComponent(val[i][key]) + "; path=/";
+            }
+        }
+    },
+    getCookie(name) {
+        var strCookie = document.cookie;
+        var arrCookie = strCookie.split("; ");
+        for (var i = 0, len = arrCookie.length; i < len; i++) {
+            var arr = arrCookie[i].split("=");
+            if (name == arr[0]) {
+                return decodeURIComponent(arr[1]);
+            }
+        }
+    },
+    clearCookie(name) {
+        var myDate = new Date();
+        myDate.setTime(-1000); //设置时间    
+        for (var i = 0, len = name.length; i < len; i++) {
+            document.cookie = "" + name[i] + "=''; path=/; expires=" + myDate.toGMTString();
+        }
+    },
+    arrToStr(arr) {
+        if (arr) {
+            return arr.map(item => { return item.name }).toString()
+        }
+    },
+    toggleClass(arr, elem, key = 'id') {
+        return arr.some(item => { return item[key] == elem[key] });
+    },
+    toChecked(arr, elem, key = 'id') {
+        var isIncludes = this.toggleClass(arr, elem, key);
+        !isIncludes ? arr.push(elem) : this.removeEle(arr, elem, key);
+    },
+    removeEle(arr, elem, key = 'id') {
+        var includesIndex;
+        arr.map((item, index) => {
+            if (item[key] == elem[key]) {
+                includesIndex = index
+            }
+        });
+        arr.splice(includesIndex, 1);
+    },
+    setApproverStr(nodeConfig) {
+        if (nodeConfig.settype == 1) {
+            if (nodeConfig.nodeUserList.length == 1) {
+                return nodeConfig.nodeUserList[0].name
+            } else if (nodeConfig.nodeUserList.length > 1) {
+                if (nodeConfig.examineMode == 1) {
+                    return this.arrToStr(nodeConfig.nodeUserList)
+                } else if (nodeConfig.examineMode == 2) {
+                    return nodeConfig.nodeUserList.length + "人会签"
+                }
+            }
+        } else if (nodeConfig.settype == 2) {
+            let level = nodeConfig.directorLevel == 1 ? '直接主管' : '第' + nodeConfig.directorLevel + '级主管'
+            if (nodeConfig.examineMode == 1) {
+                return level
+            } else if (nodeConfig.examineMode == 2) {
+                return level + "会签"
+            }
+        } else if (nodeConfig.settype == 4) {
+            if (nodeConfig.selectRange == 1) {
+                return "发起人自选"
+            } else {
+                if (nodeConfig.nodeUserList.length > 0) {
+                    if (nodeConfig.selectRange == 2) {
+                        return "发起人自选"
+                    } else {
+                        return '发起人从' + nodeConfig.nodeUserList[0].name + '中自选'
+                    }
+                } else {
+                    return "";
+                }
+            }
+        } else if (nodeConfig.settype == 5) {
+            return "发起人自己"
+        } else if (nodeConfig.settype == 7) {
+            return '从直接主管到通讯录中级别最高的第' + nodeConfig.examineEndDirectorLevel + '个层级主管'
+        }
+    },
+    dealStr(str, obj) {
+        let arr = [];
+        let list = str.split(",");
+        for (var elem in obj) {
+            list.map(item => {
+                if (item == elem) {
+                    arr.push(obj[elem].value)
+                }
+            })
+        }
+        return arr.join("或")
+    },  
+    conditionStr(nodeConfig, index) {
+        var { conditionList, nodeUserList } = nodeConfig.conditionNodes[index];
+        if (conditionList.length == 0) {
+            return (index == nodeConfig.conditionNodes.length - 1) && nodeConfig.conditionNodes[0].conditionList.length != 0 ? '其他条件进入此流程' : '请设置条件'
+        } else {
+            let str = ""
+            for (var i = 0; i < conditionList.length; i++) {
+                var { columnId, columnType, showType, showName, optType, zdy1, opt1, zdy2, opt2, fixedDownBoxValue } = conditionList[i];
+                if (columnId == 0) {
+                    if (nodeUserList.length != 0) {
+                        str += '发起人属于:'
+                        str += nodeUserList.map(item => { return item.name }).join("或") + " 并且 "
+                    }
+                }
+                if (columnType == "String" && showType == "3") {
+                    if (zdy1) {
+                        str += showName + '属于:' + this.dealStr(zdy1, JSON.parse(fixedDownBoxValue)) + " 并且 "
+                    }
+                }
+                if (columnType == "Double") {
+                    if (optType != 6 && zdy1) {
+                        var optTypeStr = ["", "<", ">", "≤", "=", "≥"][optType]
+                        str += `${showName} ${optTypeStr} ${zdy1} 并且 `
+                    } else if (optType == 6 && zdy1 && zdy2) {
+                        str += `${zdy1} ${opt1} ${showName} ${opt2} ${zdy2} 并且 `
+                    }
+                }
+            }
+            return str ? str.substring(0, str.length - 4) : '请设置条件'
+        }
+    },
+    copyerStr(nodeConfig) {
+        if (nodeConfig.nodeUserList.length != 0) {
+            return this.arrToStr(nodeConfig.nodeUserList)
+        } else {
+            if (nodeConfig.ccSelfSelectFlag == 1) {
+                return "发起人自选"
+            }
+        }
+    }, 
+    toggleStrClass(item, key) {
+        let a = item.zdy1 ? item.zdy1.split(",") : []
+        return a.some(item => { return item == key });
+    },
+}
+
+export default new All();

+ 9 - 0
src/views/wechat/sop_group/index.vue

@@ -0,0 +1,9 @@
+<template>
+  <div>客群SOP</div>
+</template>
+
+<script lang="ts">
+export default {
+
+}
+</script>

+ 89 - 0
src/views/wechat/sop_task/add_sop/components/customIcons.vue

@@ -0,0 +1,89 @@
+<template>
+  <!-- 上移下移删除icon -->
+  <div class="custom-icons">
+    <a-tooltip>
+      <template #title>上移一层</template>
+      <VerticalAlignTopOutlined :disabled="!canMoveUp" :style="{ fontSize: '18px',}"  @click="moveUp"/>
+    </a-tooltip>
+    <a-tooltip>
+      <template #title>下移一层</template>
+      <VerticalAlignBottomOutlined :style="{ fontSize: '18px', }" :disabled="!canMoveDown" @click="moveDown"/>
+    </a-tooltip>
+    <a-tooltip>
+      <template #title>删除</template>
+      <DeleteOutlined style="font-size: 18px;color: red;"  @click="showDeleteConfirm"/>
+    </a-tooltip>
+  </div>
+</template>
+<script lang="ts">
+import { defineComponent,inject,props } from 'vue';
+import { Tooltip,Modal } from 'ant-design-vue';
+import { DeleteOutlined, VerticalAlignBottomOutlined, VerticalAlignTopOutlined } from '@ant-design/icons-vue';
+export default defineComponent({
+  name: 'CustomIcons',
+  components: {
+    'a-tooltip': Tooltip,
+    Modal,
+    DeleteOutlined,
+    VerticalAlignBottomOutlined,
+    VerticalAlignTopOutlined,
+  },
+  props: {
+    index:Number,
+    length:Number,
+    canMoveUp:Boolean,
+    canMoveDown:Boolean,
+  },
+  emits: ['remove', 'moveUp', 'moveDown'],
+  setup({ index, length,canMoveUp,canMoveDown },emit) {
+    const removeItem = inject('removeItem');
+    const isFirst = index === 0;
+    const isLast = index === length - 1;
+    const moveItem = inject('moveItem');
+    const moveUp = () => {
+      // emit('moveUp');
+      moveItem(index, 'up')
+    };
+
+    const moveDown = () => {
+      // emit('moveDown');
+      moveItem(index, 'down')
+    };
+    const showDeleteConfirm = () => {
+      Modal.confirm({
+        title: '提示',
+        content: '确定要删除此内容吗?',
+        okText: '确定',
+        okType: 'danger',
+        cancelText: '取消',
+        onOk() {
+          // 确定按钮的回调:执行删除
+          removeItem(index);
+        },
+        onCancel() {
+          // 取消按钮的回调:什么也不做
+          console.log('Cancel');
+        },
+      });
+    };
+    return {
+      removeItem,
+      isFirst,
+      isLast,
+      moveItem,
+      showDeleteConfirm,
+      moveUp,
+      moveDown,
+    };
+  },
+})
+</script>
+<style scoped lang="less">
+.custom-icons {
+  width: 80px;
+  display: flex;
+  height: 50px;
+  line-height: 50px;
+  justify-content: space-between;
+}
+</style>

+ 263 - 0
src/views/wechat/sop_task/add_sop/components/flowChart/api/index.js

@@ -0,0 +1,263 @@
+/*
+ * @Date: 2022-08-25 14:06:59
+ * @LastEditors: StavinLi 495727881@qq.com
+ * @LastEditTime: 2023-03-29 15:52:57
+ * @FilePath: /Workflow-Vue3/src/api/index.js
+ */
+
+import http from '../utils/axios'
+let baseUrl = '/'
+let paramsData = {
+    "code": "200",
+    "msg": "success",
+  "data":null
+      //   [
+      //  {
+      //       "nodeName": "测试节点 1",
+      //       "childNode": null,
+      //       "conditionNodes": null,
+      //       "id": 1,
+      //       "createdAt": null,
+      //       "updatedAt": null,
+      //       "status": 1,
+      //       "stageId": 1,
+      //       "parentId": 0,
+      //       "name": "测试节点 1",
+      //       "conditionType": 1,
+      //       "conditionList": [
+      //           "我感兴趣"
+      //       ],
+      //       "actionMessage": [
+      //           {
+      //               "type": 1,
+      //               "content": "谢谢"
+      //           }
+      //       ],
+      //       "actionLabel": [
+      //           1,
+      //           2
+      //       ],
+      //       "stageInfo": null
+      //   },
+      //   {
+      //       "nodeName": "测试节点 2",
+      //       "childNode": {
+      //           "nodeName": null,
+      //           "childNode": null,
+      //           "conditionNodes": [
+      //               {
+      //                   "nodeName": "测试节点 2.1",
+      //                   "childNode": {
+      //                       "nodeName": null,
+      //                       "childNode": null,
+      //                       "conditionNodes": [
+      //                           {
+      //                               "nodeName": "测试节点 3.1",
+      //                               "childNode": null,
+      //                               "conditionNodes": null,
+      //                               "id": 6,
+      //                               "createdAt": null,
+      //                               "updatedAt": null,
+      //                               "status": 1,
+      //                               "stageId": 1,
+      //                               "parentId": 3,
+      //                               "name": "测试节点 3.1",
+      //                               "conditionType": 1,
+      //                               "conditionList": [
+      //                                   " 我不考虑了"
+      //                               ],
+      //                               "actionMessage": [
+      //                                   {
+      //                                       "type": 1,
+      //                                       "content": "那好吧"
+      //                                   }
+      //                               ],
+      //                               "actionLabel": [
+      //                                   1,
+      //                                   2
+      //                               ],
+      //                               "stageInfo": null
+      //                           },
+      //                           {
+      //                               "nodeName": "测试节点 3.1",
+      //                               "childNode": null,
+      //                               "conditionNodes": null,
+      //                               "id": 7,
+      //                               "createdAt": null,
+      //                               "updatedAt": null,
+      //                               "status": 1,
+      //                               "stageId": 1,
+      //                               "parentId": 3,
+      //                               "name": "测试节点 3.1",
+      //                               "conditionType": 1,
+      //                               "conditionList": [
+      //                                   " 我不考虑了"
+      //                               ],
+      //                               "actionMessage": [
+      //                                   {
+      //                                       "type": 1,
+      //                                       "content": "那好吧"
+      //                                   }
+      //                               ],
+      //                               "actionLabel": [
+      //                                   1,
+      //                                   2
+      //                               ],
+      //                               "stageInfo": null
+      //                           }
+      //                       ],
+      //                       "id": null,
+      //                       "createdAt": null,
+      //                       "updatedAt": null,
+      //                       "status": null,
+      //                       "stageId": null,
+      //                       "parentId": null,
+      //                       "name": null,
+      //                       "conditionType": null,
+      //                       "conditionList": null,
+      //                       "actionMessage": null,
+      //                       "actionLabel": null,
+      //                       "stageInfo": null
+      //                   },
+      //                   "conditionNodes": null,
+      //                   "id": 3,
+      //                   "createdAt": null,
+      //                   "updatedAt": null,
+      //                   "status": 1,
+      //                   "stageId": 1,
+      //                   "parentId": 2,
+      //                   "name": "测试节点 2.1",
+      //                   "conditionType": 1,
+      //                   "conditionList": [
+      //                       " 我再考虑考虑"
+      //                   ],
+      //                   "actionMessage": [
+      //                       {
+      //                           "type": 1,
+      //                           "content": "非常感谢"
+      //                       }
+      //                   ],
+      //                   "actionLabel": [
+      //                       1,
+      //                       2
+      //                   ],
+      //                   "stageInfo": null
+      //               },
+      //               {
+      //                   "nodeName": "测试节点 2.2",
+      //                   "childNode": null,
+      //                   "conditionNodes": null,
+      //                   "id": 5,
+      //                   "createdAt": null,
+      //                   "updatedAt": null,
+      //                   "status": 1,
+      //                   "stageId": 1,
+      //                   "parentId": 2,
+      //                   "name": "测试节点 2.2",
+      //                   "conditionType": 1,
+      //                   "conditionList": [
+      //                       " 我不考虑了"
+      //                   ],
+      //                   "actionMessage": [
+      //                       {
+      //                           "type": 1,
+      //                           "content": "那好吧"
+      //                       }
+      //                   ],
+      //                   "actionLabel": [
+      //                       1,
+      //                       2
+      //                   ],
+      //                   "stageInfo": null
+      //               }
+      //           ],
+      //           "id": null,
+      //           "createdAt": null,
+      //           "updatedAt": null,
+      //           "status": null,
+      //           "stageId": null,
+      //           "parentId": null,
+      //           "name": null,
+      //           "conditionType": null,
+      //           "conditionList": null,
+      //           "actionMessage": null,
+      //           "actionLabel": null,
+      //           "stageInfo": null
+      //       },
+      //       "conditionNodes": null,
+      //       "id": 2,
+      //       "createdAt": null,
+      //       "updatedAt": null,
+      //       "status": 1,
+      //       "stageId": 1,
+      //       "parentId": 0,
+      //       "name": "测试节点 2",
+      //       "conditionType": 1,
+      //       "conditionList": [
+      //           "我不感兴趣"
+      //       ],
+      //       "actionMessage": [
+      //           {
+      //               "type": 1,
+      //               "content": "要不再考虑考虑"
+      //           }
+      //       ],
+      //       "actionLabel": [
+      //           1,
+      //           2
+      //       ],
+      //       "stageInfo": null
+      //   }
+      //   ],
+}
+/**
+ * 获取角色
+ * @param {*} data 
+ * @returns 
+ */
+export function getRoles(data) {
+  return http.get(`${baseUrl}roles.json`, { params: data })
+}
+
+/**
+ * 获取部门
+ * @param {*} data 
+ * @returns 
+ */
+export function getDepartments(data) {
+  return http.get(`${baseUrl}departments.json`, { params: data })
+}
+
+/**
+ * 获取职员
+ * @param {*} data 
+ * @returns 
+ */
+export function getEmployees(data) {
+  return http.get(`${baseUrl}employees.json`, { params: data })
+}
+/**
+ * 获取条件字段
+ * @param {*} data 
+ * @returns 
+ */
+export function getConditions(data) {
+  return http.get(`${baseUrl}conditions.json`, { params: data })
+}
+
+/**
+ * 获取审批数据
+ * @param {*} data 
+ * @returns 
+ */
+export function getWorkFlowData(data) {
+  return paramsData
+}
+/**
+ * 设置审批数据
+ * @param {*} data 
+ * @returns 
+ */
+export function setWorkFlowData(data) {
+  return http.post(`${baseUrl}`, data)
+}

+ 205 - 0
src/views/wechat/sop_task/add_sop/components/flowChart/components/blankBox.vue

@@ -0,0 +1,205 @@
+<template>
+    <div class="add-node-btn-box">
+        <div class="add-node-btn">
+                <div class="add-node-popover-body">
+                    
+                </div>
+                    <!-- <button class="btn" type="button">
+                        <span class="iconfont"></span>
+                    </button> -->
+        </div>
+    </div>
+</template>
+<script setup>
+import { ref } from 'vue'
+let props = defineProps({
+    childNodeP: {
+        type: Object,
+        default: ()=> ({})
+    }
+})
+let emits = defineEmits(['update:childNodeP'])
+let visible = ref(false)
+const addType = (type) => {
+    console.log('first',type)
+    visible.value = false;
+    // if (type != 4) {
+    //     var data;
+    //     if (type == 1) {
+    //         // data = {
+    //         //     "nodeName": "审核人",
+    //         //     "error": true,
+    //         //     "type": 1,
+    //         //     "settype": 1,
+    //         //     "selectMode": 0,
+    //         //     "selectRange": 0,
+    //         //     "directorLevel": 1,
+    //         //     "examineMode": 1,
+    //         //     "noHanderAction": 1,
+    //         //     "examineEndDirectorLevel": 0,
+    //         //     "childNode": props.childNodeP,
+    //         //     "nodeUserList": []
+    //         // }
+    //     } else if (type == 2) {
+    //         // data = {
+    //         //     "nodeName": "抄送人",
+    //         //     "type": 2,
+    //         //     "ccSelfSelectFlag": 1,
+    //         //     "childNode": props.childNodeP,
+    //         //     "nodeUserList": []
+    //         // }
+    //     }
+    //     emits("update:childNodeP", data)
+    // } else {
+    //    if(type ==4) {
+        emits("update:childNodeP", {
+            "nodeName": "路由",
+            "type": 4,
+            "childNode": null,
+            "conditionNodes": [{
+                "nodeName": "条件1",
+                "error": true,
+                "type": 3,
+                "priorityLevel": 1,
+                "conditionList": [],
+                "nodeUserList": [],
+                "childNode": props.childNodeP,
+            },
+                // {
+                // "nodeName": "条件2",
+                // "type": 3,
+                // "priorityLevel": 2,
+                // "conditionList": [],
+                // "nodeUserList": [],
+                // "childNode": null
+                // }
+            ]
+        })
+        // }
+    // }
+}
+</script>
+<style scoped lang="less">
+.add-node-btn-box {
+    width: 240px;
+    display: -webkit-inline-box;
+    display: -ms-inline-flexbox;
+    display: inline-flex;
+    -ms-flex-negative: 0;
+    flex-shrink: 0;
+    -webkit-box-flex: 1;
+    -ms-flex-positive: 1;
+    position: relative;
+    &:before {
+        content: "";
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        z-index: -1;
+        margin: auto;
+        width: 2px;
+        height: 100%;
+        background-color: #cacaca
+    }
+    .add-node-btn {
+        user-select: none;
+        width: 240px;
+        padding: 20px 0 32px;
+        display: flex;
+        -webkit-box-pack: center;
+        justify-content: center;
+        flex-shrink: 0;
+        -webkit-box-flex: 1;
+        flex-grow: 1;
+        .btn {
+            outline: none;
+            box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .1);
+            width: 30px;
+            height: 30px;
+            background: #3296fa;
+            border-radius: 50%;
+            position: relative;
+            top: -35px;
+            border: none;
+            line-height: 30px;
+            -webkit-transition: all .3s cubic-bezier(.645, .045, .355, 1);
+            transition: all .3s cubic-bezier(.645, .045, .355, 1);
+            .iconfont {
+                color: #fff;
+                font-size: 16px
+            }
+            &:hover {
+                transform: scale(1.3);
+                box-shadow: 0 13px 27px 0 rgba(0, 0, 0, .1)
+            }
+            &:active {
+                transform: none;
+                background: #1e83e9;
+                box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .1)
+            }
+        }
+    }
+}
+</style>
+<style lang="less">
+.add-node-popover-body {
+    display: flex;
+    .add-node-popover-item {
+        margin-right: 10px;
+        cursor: pointer;
+        text-align: center;
+        flex: 1;
+        color: #191f25!important;
+        .item-wrapper {
+            user-select: none;
+            display: inline-block;
+            width: 80px;
+            height: 80px;
+            margin-bottom: 5px;
+            background: #fff;
+            border: 1px solid #e2e2e2;
+            border-radius: 50%;
+            transition: all .3s cubic-bezier(.645, .045, .355, 1);
+            .iconfont {
+                font-size: 35px;
+                line-height: 80px
+            }
+        }
+        &.approver{
+            .item-wrapper {
+                color: #ff943e
+            }
+        }
+        &.notifier{
+            .item-wrapper {
+                color: #3296fa
+            }
+        }
+        &.condition{
+            .item-wrapper {
+                color: #15bc83
+            }
+        }
+        &:hover{
+            .item-wrapper {
+                background: #3296fa;
+                box-shadow: 0 10px 20px 0 rgba(50, 150, 250, .4)
+            }
+            .iconfont {
+                color: #fff
+            }
+        }
+        &:active{
+            .item-wrapper {
+                box-shadow: none;
+                background: #eaeaea
+            }
+            .iconfont {
+                color: inherit
+            }
+        }
+    }
+}
+</style>

+ 343 - 0
src/views/wechat/sop_task/add_sop/components/flowChart/components/nodeWrap.vue

@@ -0,0 +1,343 @@
+<template>
+  <!-- v-if="nodeConfig.type == 4" -->
+  <div class="branch-wrap" v-if="nodeConfig.conditionNodes && nodeConfig.conditionNodes.length">
+    <div class="branch-box-wrap">
+      <div class="branch-box">
+        <div class="add-branch"></div>
+        <div class="col-box" v-for="(item, index) in nodeConfig.conditionNodes" :key="index">
+          <div class="condition-node">
+            <div class="condition-node-box">
+              <div class="auto-judge" :class="isTried && item.error ? 'error active' : ''">
+                <div class="title-wrapper">
+                  <span class="editable-title">{{ item.nodeName }}</span>
+                  <Dropdown :trigger="['click']" arrow placement="bottomLeft">
+                    <a class="priority-title" @click.prevent>
+                      <SettingOutlined />
+                    </a>
+                    <template #overlay>
+                      <Menu>
+                        <MenuItem @click="setNode(item, 'edit')" key="edit">编辑</MenuItem>
+                        <MenuItem @click="setNode(item, 'del')" key="del">删除</MenuItem>
+                        <MenuItem @click="setNode(item, 'copy')" key="copy">复制</MenuItem>
+                        <MenuItem @click="setNode(item, 'add')" key="add">添加节点</MenuItem>
+                      </Menu>
+                    </template>
+                  </Dropdown>
+                </div>
+                <div class="content-note" @click="setPerson(item)">
+                  <div>回复情况:{{ '客户回复' }}</div>
+                  <div>执行任务:{{ '发消息' }}</div>
+                </div>
+                <div class="error_tip" v-if="isTried && item.error">
+                  <i class="anticon anticon-exclamation-circle"></i>
+                </div>
+              </div>
+              <!-- <addNode v-model:childNodeP="item.childNode" /> -->
+              <div class="add-node-btn-box">
+                <div class="add-node-btn">
+                  <button class="btn" type="button" @click="addType(item)">
+                    <span class="iconfont"></span>
+                  </button>
+                </div>
+              </div>
+            </div>
+          </div>
+          <nodeWrap
+            v-if="item.childNode"
+            v-model:nodeConfig="item.childNode"
+            @update:nodeDrawerOpen="addType"
+            @update:nodeDrawerOEdit="setPerson"
+            @update:delNodeUpdate="setNode(item)"
+          />
+          <template v-if="index == 0">
+            <div class="top-left-cover-line"></div>
+            <div class="bottom-left-cover-line"></div>
+          </template>
+          <template v-if="index == nodeConfig.conditionNodes.length - 1">
+            <div class="top-right-cover-line"></div>
+            <div class="bottom-right-cover-line"></div>
+          </template>
+        </div>
+      </div>
+      <!-- 去掉一个添加按钮 -->
+      <BlankBox />
+    </div>
+  </div>
+  <!-- <nodeWrap v-if="nodeConfig.childNode" v-model:nodeConfig="nodeConfig.childNode" @update:nodeDrawerOpen="addType(item)"/> -->
+</template>
+<script setup lang="ts">
+  import {
+    onMounted,
+    ref,
+    watch,
+    getCurrentInstance,
+    computed,
+    defineExpose,
+    defineEmits,
+  } from 'vue';
+  import { ClockCircleOutlined, ThunderboltOutlined, SettingOutlined } from '@ant-design/icons-vue';
+  import { Dropdown, Menu, MenuItem, Modal } from 'ant-design-vue';
+  import addNode from './addNode.vue';
+  import nodeWrap from './nodeWrap.vue';
+  import BlankBox from './blankBox.vue';
+  import $func from '../utils/index';
+  import { useStore } from '../../../stores/index';
+  import { bgColors, placeholderList } from '../utils/const';
+  import { delSopStageNode, getSopStageNodeList } from '@/api/wechat/sopTask';
+  let _uid = getCurrentInstance().uid;
+  defineExpose({
+    Dropdown,
+    Menu,
+    MenuItem,
+  });
+  let emit = defineEmits<{
+    (event: 'update:nodeDrawerOpen', value: { open: boolean; nodeId: number; isAdd: boolean }): void;
+    (event: 'update:delNodeUpdate', value: { reseat: boolean; stageId: number }): void;
+    (event: 'update:nodeDrawerOEdit', value: { open: boolean; nodeId: number; isAdd: boolean }): void;
+  }>();
+  // let emits = defineEmits(['update:flowPermission', 'update:nodeConfig']);
+  let drawerVisible = ref(false);
+  let props = defineProps({
+    nodeConfig: {
+      type: Object,
+      default: () => ({}),
+    },
+  });
+  onMounted(() => {
+    // if (props.nodeConfig.type == 1) {
+    //   props.nodeConfig.error = !$func.setApproverStr(props.nodeConfig);
+    // } else if (props.nodeConfig.type == 2) {
+    //   props.nodeConfig.error = !$func.copyerStr(props.nodeConfig);
+    // } else if (props.nodeConfig.type == 4) {
+    //   resetConditionNodesErr();
+    // }
+    // if(props.nodeConfig.length>0){
+    //   resetConditionNodesErr();
+    // }
+  });
+
+  let store = useStore();
+  let {
+    setPromoter,
+    setApprover,
+    setCopyer,
+    setCondition,
+    setFlowPermission,
+    setApproverConfig,
+    setCopyerConfig,
+    setConditionsConfig,
+  } = store;
+  let isTried = computed(() => store.isTried);
+  let conditionsConfig1 = computed(() => store.conditionsConfig1);
+  let sop_stage_list = computed(() => store.sop_stage_list);
+  async function setNode(item, key) {
+    console.log(item,)
+    if (key === 'edit') {
+      emit('update:nodeDrawerOpen', { open: true, nodeId: item.id, isAdd: false });
+    } else if (key === 'del') {
+      let res = await delSopStageNode({ id: item.id });
+      if (res.code === 0) {
+        console.log('节点ID删除', res);
+        emit('update:delNodeUpdate', { reseat: true, stageId: item.stageId });
+      }
+    } else if (key === 'add') {
+      emit('update:nodeDrawerOpen', { open: true, nodeId: item.id, isAdd: false });
+    }
+  }
+  // async function delSopNode(item) {
+  //   console.log('-----------删除节点',item)
+  //    let res = await delSopStageNode({ id: item.id });
+  //     if (res.code === 0) {
+  //       console.log('节点ID删除', res);
+  //       emit('update:delNodeUpdate', { reseat: true, stageId: item.stageId });
+  //       //  let data  = await getSopStageNodeList({ stageId: item.stageId });
+  //       //  props.nodeConfig.value = { conditionNodes: data };
+  //     }
+  // }
+  async function addType(item) {
+    console.log('节点下的加号按钮', item.isAdd);
+    emit('update:nodeDrawerOpen', { open: true, nodeId: item.id || item.nodeId, isAdd: true });
+  }
+
+  const addTerm = () => {
+    let len = props.nodeConfig.conditionNodes.length + 1;
+    props.nodeConfig.conditionNodes.push({
+      nodeName: '条件' + len,
+      type: 3,
+      priorityLevel: len,
+      conditionList: [],
+      nodeUserList: [],
+      childNode: null,
+    });
+    resetConditionNodesErr();
+    emits('update:nodeConfig', props.nodeConfig);
+  };
+
+  const setPerson = (item) => {
+    console.log(2222, item);
+    const editStatus = false;
+    emit('update:nodeDrawerOEdit', { open: true, nodeId: item.id || item.nodeId, isAdd: editStatus });
+  };
+</script>
+<style>
+  @import '../css/workflow.css';
+  .error_tip {
+    position: absolute;
+    top: 0px;
+    right: 0px;
+    transform: translate(150%, 0px);
+    font-size: 24px;
+  }
+
+  .promoter_person .el-dialog__body {
+    padding: 10px 20px 14px 20px;
+  }
+
+  .selected_list {
+    margin-bottom: 20px;
+    line-height: 30px;
+  }
+
+  .selected_list span {
+    margin-right: 10px;
+    padding: 3px 6px 3px 9px;
+    line-height: 12px;
+    white-space: nowrap;
+    border-radius: 2px;
+    border: 1px solid rgba(220, 220, 220, 1);
+  }
+
+  .selected_list img {
+    margin-left: 5px;
+    width: 7px;
+    height: 7px;
+    cursor: pointer;
+  }
+</style>
+<style scoped lang="less">
+  .add-node-btn-box {
+    width: 240px;
+    display: -webkit-inline-box;
+    display: -ms-inline-flexbox;
+    display: inline-flex;
+    -ms-flex-negative: 0;
+    flex-shrink: 0;
+    -webkit-box-flex: 1;
+    -ms-flex-positive: 1;
+    position: relative;
+    &:before {
+      content: '';
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      z-index: -1;
+      margin: auto;
+      width: 2px;
+      height: 100%;
+      background-color: #cacaca;
+    }
+    .add-node-btn {
+      user-select: none;
+      width: 240px;
+      padding: 20px 0 32px;
+      display: flex;
+      -webkit-box-pack: center;
+      justify-content: center;
+      flex-shrink: 0;
+      -webkit-box-flex: 1;
+      flex-grow: 1;
+      .btn {
+        outline: none;
+        box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
+        width: 30px;
+        height: 30px;
+        background: #fff;
+        border-radius: 50%;
+        position: relative;
+        top: -35px;
+        z-index: 2;
+        border: none;
+        line-height: 30px;
+        -webkit-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+        transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+        .iconfont {
+          color: #999;
+          font-size: 16px;
+        }
+        &:hover {
+          transform: scale(1.3);
+          z-index: 3;
+          background: #1677ff;
+          box-shadow: 0 13px 27px 0 rgba(0, 0, 0, 0.1);
+        }
+        &:active {
+          transform: none;
+          background: #1e83e9;
+          box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
+        }
+      }
+    }
+  }
+</style>
+<style scoped lang="less">
+  .add-node-popover-body {
+    display: flex;
+    .add-node-popover-item {
+      margin-right: 10px;
+      cursor: pointer;
+      text-align: center;
+      flex: 1;
+      color: #191f25 !important;
+      .item-wrapper {
+        user-select: none;
+        display: inline-block;
+        width: 80px;
+        height: 80px;
+        margin-bottom: 5px;
+        background: #fff;
+        border: 1px solid #e2e2e2;
+        border-radius: 50%;
+        transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+        .iconfont {
+          font-size: 35px;
+          line-height: 80px;
+        }
+      }
+      &.approver {
+        .item-wrapper {
+          color: #ff943e;
+        }
+      }
+      &.notifier {
+        .item-wrapper {
+          color: #3296fa;
+        }
+      }
+      &.condition {
+        .item-wrapper {
+          color: #15bc83;
+        }
+      }
+      &:hover {
+        .item-wrapper {
+          background: #3296fa;
+          box-shadow: 0 10px 20px 0 rgba(50, 150, 250, 0.4);
+        }
+        .iconfont {
+          color: #fff;
+        }
+      }
+      &:active {
+        .item-wrapper {
+          box-shadow: none;
+          background: #eaeaea;
+        }
+        .iconfont {
+          color: inherit;
+        }
+      }
+    }
+  }
+</style>

+ 1762 - 0
src/views/wechat/sop_task/add_sop/components/flowChart/css/workflow.css

@@ -0,0 +1,1762 @@
+
+
+@font-face {
+    font-family: Chinese Quote;
+    src: local("PingFang SC"), local("SimSun");
+    unicode-range: u+2018, u+2019, u+201c, u+201d
+}
+
+
+
+input::-ms-clear,
+input::-ms-reveal {
+    display: none
+}
+
+*,
+:after,
+:before {
+    box-sizing: border-box
+}
+
+
+
+@-ms-viewport {
+    width: device-width
+}
+
+article,
+aside,
+dialog,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+main,
+nav,
+section {
+    display: block
+}
+
+
+
+[tabindex="-1"]:focus {
+    outline: none!important
+}
+
+hr {
+    box-sizing: content-box;
+    height: 0;
+    overflow: visible
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+    margin-top: 0;
+    margin-bottom: .5em;
+    color: rgba(0, 0, 0, .85);
+    font-weight: 500
+}
+
+p {
+    margin-top: 0;
+    margin-bottom: 1em
+}
+
+abbr[data-original-title],
+abbr[title] {
+    text-decoration: underline;
+    text-decoration: underline dotted;
+    cursor: help;
+    border-bottom: 0
+}
+
+address {
+    margin-bottom: 1em;
+    font-style: normal;
+    line-height: inherit
+}
+
+input[type=number],
+input[type=password],
+input[type=text],
+textarea {
+    -webkit-appearance: none
+}
+
+dl,
+ol,
+ul {
+    margin-top: 0;
+    margin-bottom: 1em
+}
+
+ol ol,
+ol ul,
+ul ol,
+ul ul {
+    margin-bottom: 0
+}
+
+dt {
+    font-weight: 500
+}
+
+dd {
+    margin-bottom: .5em;
+    margin-left: 0
+}
+
+blockquote {
+    margin: 0 0 1em
+}
+
+dfn {
+    font-style: italic
+}
+
+b,
+strong {
+    font-weight: bolder
+}
+
+small {
+    font-size: 80%
+}
+
+sub,
+sup {
+    position: relative;
+    font-size: 75%;
+    line-height: 0;
+    vertical-align: baseline
+}
+
+sub {
+    bottom: -.25em
+}
+
+sup {
+    top: -.5em
+}
+
+a {
+    color: #1890ff;
+    background-color: transparent;
+    text-decoration: none;
+    outline: none;
+    cursor: pointer;
+    transition: color .3s;
+    -webkit-text-decoration-skip: objects
+}
+
+a:focus {
+    text-decoration: underline;
+    text-decoration-skip: auto
+}
+
+a:hover {
+    color: #40a9ff
+}
+
+a:active {
+    color: #096dd9
+}
+
+a:active,
+a:hover {
+    outline: 0;
+    text-decoration: none
+}
+
+code,
+kbd,
+pre,
+samp {
+    font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, Courier, monospace;
+    font-size: 1em
+}
+
+pre {
+    margin-top: 0;
+    margin-bottom: 1em;
+    overflow: auto
+}
+
+figure {
+    margin: 0 0 1em
+}
+
+img {
+    vertical-align: middle;
+    border-style: none
+}
+
+svg:not(:root) {
+    overflow: hidden
+}
+
+[role=button],
+a,
+area,
+button,
+input:not([type=range]),
+label,
+select,
+summary,
+textarea {
+    touch-action: manipulation
+}
+
+table {
+    border-collapse: collapse
+}
+
+caption {
+    padding-top: .75em;
+    padding-bottom: .3em;
+    color: rgba(0, 0, 0, .45);
+    text-align: left;
+    caption-side: bottom
+}
+
+th {
+    text-align: inherit
+}
+
+button,
+input,
+optgroup,
+select,
+textarea {
+    margin: 0;
+    font-family: inherit;
+    font-size: inherit;
+    line-height: inherit;
+    color: inherit
+}
+
+button,
+input {
+    overflow: visible
+}
+
+button,
+select {
+    text-transform: none
+}
+
+[type=reset],
+[type=submit],
+button,
+html [type=button] {
+    -webkit-appearance: button
+}
+
+[type=button]::-moz-focus-inner,
+[type=reset]::-moz-focus-inner,
+[type=submit]::-moz-focus-inner,
+button::-moz-focus-inner {
+    padding: 0;
+    border-style: none
+}
+
+input[type=checkbox],
+input[type=radio] {
+    box-sizing: border-box;
+    padding: 0
+}
+
+input[type=date],
+input[type=datetime-local],
+input[type=month],
+input[type=time] {
+    -webkit-appearance: listbox
+}
+
+textarea {
+    overflow: auto;
+    resize: vertical
+}
+
+fieldset {
+    min-width: 0;
+    padding: 0;
+    margin: 0;
+    border: 0
+}
+
+legend {
+    display: block;
+    width: 100%;
+    max-width: 100%;
+    padding: 0;
+    margin-bottom: .5em;
+    font-size: 1.5em;
+    line-height: inherit;
+    color: inherit;
+    white-space: normal
+}
+
+progress {
+    vertical-align: baseline
+}
+
+[type=number]::-webkit-inner-spin-button,
+[type=number]::-webkit-outer-spin-button {
+    height: auto
+}
+
+[type=search] {
+    outline-offset: -2px;
+    -webkit-appearance: none
+}
+
+[type=search]::-webkit-search-cancel-button,
+[type=search]::-webkit-search-decoration {
+    -webkit-appearance: none
+}
+
+::-webkit-file-upload-button {
+    font: inherit;
+    -webkit-appearance: button
+}
+
+output {
+    display: inline-block
+}
+
+summary {
+    display: list-item
+}
+
+template {
+    display: none
+}
+
+[hidden] {
+    display: none!important
+}
+
+mark {
+    padding: .2em;
+    background-color: #feffe6
+}
+
+::selection {
+    background: #1890ff;
+    color: #fff
+}
+
+.clearfix {
+    zoom: 1
+}
+
+.clearfix:after,
+.clearfix:before {
+    content: "";
+    display: table
+}
+
+.clearfix:after {
+    clear: both
+}
+
+@font-face {
+    font-family: anticons;
+    font-display: fallback;
+    src: url("https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i.eot");
+    src: url("https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i.woff") format("woff"), url("https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i.ttf") format("truetype"), url("https://at.alicdn.com/t/font_148784_v4ggb6wrjmkotj4i.svg#iconfont") format("svg")
+}
+
+.anticons {
+    display: inline-block;
+    font-style: normal;
+    vertical-align: baseline;
+    text-align: center;
+    text-transform: none;
+    line-height: 1;
+    text-rendering: optimizeLegibility;
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale
+}
+
+.anticons:before {
+    display: block;
+    font-family: anticons!important
+}
+.anticons-close:before {
+  content: "\E633"
+}
+.anticons-right:before {
+    content: "\E61F"
+}
+.anticons-exclamation-circle{
+    color: rgb(242, 86, 67)
+}
+.anticons-exclamation-circle:before {
+    content: "\E62C"
+}
+
+.anticons-left:before {
+    content: "\E620"
+}
+
+.anticons-close-circle:before {
+    content: "\E62E"
+}
+  
+.ant-btns {
+    line-height: 1.5;
+    display: inline-block;
+    font-weight: 400;
+    text-align: center;
+    touch-action: manipulation;
+    cursor: pointer;
+    background-image: none;
+    border: 1px solid transparent;
+    white-space: nowrap;
+    padding: 0 15px;
+    font-size: 14px;
+    border-radius: 4px;
+    height: 32px;
+    user-select: none;
+    transition: all .3s cubic-bezier(.645, .045, .355, 1);
+    position: relative;
+    color: rgba(0, 0, 0, .65);
+    background-color: #fff;
+    border-color: #d9d9d9
+}
+
+.ant-btns>.anticons {
+    line-height: 1
+}
+
+.ant-btns,
+.ant-btns:active,
+.ant-btns:focus {
+    outline: 0
+}
+
+.ant-btns>a:only-child {
+    color: currentColor
+}
+
+.ant-btns>a:only-child:after {
+    content: "";
+    position: absolute;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    right: 0;
+    background: transparent
+}
+
+.ant-btns:focus,
+.ant-btns:hover {
+    color: #40a9ff;
+    background-color: #fff;
+    border-color: #40a9ff
+}
+
+.ant-btns:focus>a:only-child,
+.ant-btns:hover>a:only-child {
+    color: currentColor
+}
+
+.ant-btns:focus>a:only-child:after,
+.ant-btns:hover>a:only-child:after {
+    content: "";
+    position: absolute;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    right: 0;
+    background: transparent
+}
+
+.ant-btns.active,
+.ant-btns:active {
+    color: #096dd9;
+    background-color: #fff;
+    border-color: #096dd9
+}
+
+.ant-btns.active>a:only-child,
+.ant-btns:active>a:only-child {
+    color: currentColor
+}
+
+.ant-btns.active>a:only-child:after,
+.ant-btns:active>a:only-child:after {
+    content: "";
+    position: absolute;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    right: 0;
+    background: transparent
+}
+
+.ant-btns.active,
+.ant-btns:active,
+.ant-btns:focus,
+.ant-btns:hover {
+    background: #fff;
+    text-decoration: none
+}
+
+.ant-btns>i,
+.ant-btns>span {
+    pointer-events: none
+}
+
+.ant-btns:before {
+    position: absolute;
+    top: -1px;
+    left: -1px;
+    bottom: -1px;
+    right: -1px;
+    background: #fff;
+    opacity: .35;
+    content: "";
+    border-radius: inherit;
+    z-index: 1;
+    transition: opacity .2s;
+    pointer-events: none;
+    display: none
+}
+
+.ant-btns .anticons {
+    transition: margin-left .3s cubic-bezier(.645, .045, .355, 1)
+}
+
+.ant-btns:active>span,
+.ant-btns:focus>span {
+    position: relative
+}
+
+.ant-btns>.anticons+span,
+.ant-btns>span+.anticons {
+    margin-left: 8px
+}
+
+.fd-nav-container {
+    display: inline-block;
+    position: relative
+}
+
+.fd-nav-container .ghost-bar {
+    position: absolute;
+    width: 150px;
+    height: 100%;
+    left: 0;
+    background: #1583f2;
+    -webkit-transition: all .3s cubic-bezier(.645, .045, .355, 1);
+    transition: all .3s cubic-bezier(.645, .045, .355, 1)
+}
+
+.fd-nav-container .ghost-bar:after {
+    content: "";
+    position: absolute;
+    bottom: 0;
+    left: 50%;
+    margin-left: -5px;
+    width: 0;
+    height: 0;
+    border-style: solid;
+    border-width: 0 5px 6px;
+    border-color: transparent transparent #f6f6f6
+}
+
+.fd-nav-item {
+    position: relative;
+    cursor: pointer;
+    display: inline-block;
+    line-height: 60px;
+    width: 150px;
+    text-align: center;
+    white-space: nowrap
+}
+
+.fd-nav-item .order-num {
+    display: inline-block;
+    width: 20px;
+    height: 20px;
+    line-height: 20px;
+    border: 1px solid #fff;
+    border-radius: 50%;
+    margin-right: 6px;
+    -webkit-transition: all .3s cubic-bezier(.645, .045, .355, 1);
+    transition: all .3s cubic-bezier(.645, .045, .355, 1)
+}
+
+.fd-nav-item.active .order-num {
+    color: #1583f2;
+    background: #fff
+}
+
+.ant-input {
+    font-family: Chinese Quote, -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif;
+    font-variant: tabular-nums;
+    box-sizing: border-box;
+    margin: 0;
+    padding: 0;
+    list-style: none;
+    position: relative;
+    display: inline-block;
+    padding: 4px 11px;
+    width: 100%;
+    height: 32px;
+    font-size: 14px;
+    line-height: 1.5;
+    color: rgba(0, 0, 0, .65);
+    background-color: #fff;
+    background-image: none;
+    border: 1px solid #d9d9d9;
+    border-radius: 4px;
+    transition: all .3s
+}
+
+.ant-input::-moz-placeholder {
+    color: #bfbfbf;
+    opacity: 1
+}
+
+.ant-input:-ms-input-placeholder {
+    color: #bfbfbf
+}
+
+.ant-input::-webkit-input-placeholder {
+    color: #bfbfbf
+}
+
+.ant-input:focus,
+.ant-input:hover {
+    border-color: #40a9ff;
+    border-right-width: 1px!important
+}
+
+.ant-input:focus {
+    outline: 0;
+    box-shadow: 0 0 0 2px rgba(24, 144, 255, .2)
+}
+
+textarea.ant-input {
+    max-width: 100%;
+    height: auto;
+    vertical-align: bottom;
+    transition: all .3s, height 0s;
+    min-height: 32px
+}
+
+a,
+abbr,
+acronym,
+address,
+applet,
+article,
+aside,
+audio,
+b,
+big,
+blockquote,
+body,
+canvas,
+caption,
+center,
+cite,
+code,
+dd,
+del,
+details,
+dfn,
+div,
+dl,
+dt,
+em,
+fieldset,
+figcaption,
+figure,
+footer,
+form,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+header,
+hgroup,
+html,
+i,
+iframe,
+img,
+ins,
+kbd,
+label,
+legend,
+li,
+mark,
+menu,
+nav,
+object,
+ol,
+p,
+pre,
+q,
+s,
+samp,
+section,
+small,
+span,
+strike,
+strong,
+sub,
+summary,
+sup,
+table,
+tbody,
+td,
+tfoot,
+th,
+thead,
+time,
+tr,
+tt,
+u,
+ul,
+var,
+video {
+    margin: 0;
+    padding: 0;
+    border: 0;
+    outline: 0;
+    font-size: 100%;
+    font: inherit;
+    vertical-align: baseline
+}
+
+*,
+:after,
+:before {
+    -webkit-box-sizing: border-box;
+    -moz-box-sizing: border-box;
+    box-sizing: border-box
+}
+
+html {
+    font-family: sans-serif;
+    -ms-text-size-adjust: 100%;
+    -webkit-text-size-adjust: 100%
+}
+
+body,
+html {
+    font-size: 14px
+}
+
+body {
+    font-family: Microsoft Yahei, Lucida Grande, Lucida Sans Unicode, Helvetica, Arial, Verdana, sans-serif;
+    line-height: 1.6;
+    background-color: #fff;
+    position: static!important;
+    -webkit-tap-highlight-color: rgba(0, 0, 0, 0)
+}
+
+ol,
+ul {
+    list-style-type: none
+}
+
+b,
+strong {
+    font-weight: 700
+}
+
+img {
+    border: 0
+}
+
+button,
+input,
+select,
+textarea {
+    font-family: inherit;
+    font-size: 100%;
+    margin: 0
+}
+
+textarea {
+    overflow: auto;
+    vertical-align: top;
+    -webkit-appearance: none
+}
+
+button,
+input {
+    line-height: normal
+}
+
+button,
+select {
+    text-transform: none
+}
+
+button,
+html input[type=button],
+input[type=reset],
+input[type=submit] {
+    -webkit-appearance: button;
+    cursor: pointer
+}
+
+input[type=search] {
+    -webkit-appearance: textfield;
+    -moz-box-sizing: content-box;
+    -webkit-box-sizing: content-box;
+    box-sizing: content-box
+}
+
+input[type=search]::-webkit-search-cancel-button,
+input[type=search]::-webkit-search-decoration {
+    -webkit-appearance: none
+}
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+    border: 0;
+    padding: 0
+}
+
+table {
+    width: 100%;
+    border-spacing: 0;
+    border-collapse: collapse
+}
+
+table,
+td,
+th {
+    border: 0
+}
+
+td,
+th {
+    padding: 0;
+    vertical-align: top
+}
+
+th {
+    font-weight: 700;
+    text-align: left
+}
+
+thead th {
+    white-space: nowrap
+}
+
+a {
+    text-decoration: none;
+    cursor: pointer;
+    color: #3296fa
+}
+
+a:active,
+a:hover {
+    outline: 0;
+    color: #3296fa
+}
+
+small {
+    font-size: 80%
+}
+
+body,
+html {
+    font-size: 12px!important;
+    color: #191f25!important;
+    background: #f6f6f6!important
+}
+
+.wrap {
+    display: -webkit-box;
+    display: -ms-flexbox;
+    display: flex;
+    -webkit-box-orient: vertical;
+    -webkit-box-direction: normal;
+    -ms-flex-direction: column;
+    flex-direction: column;
+    height: 100%
+}
+
+@font-face {
+    font-family: IconFont;
+    src: url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.eot");
+    src: url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.woff") format("woff"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.ttf") format("truetype"), url("//at.alicdn.com/t/font_135284_ph2thxxbzgf.svg#IconFont") format("svg")
+}
+
+.iconfont {
+    font-family: IconFont!important;
+    font-size: 16px;
+    font-style: normal;
+    -webkit-font-smoothing: antialiased;
+    -webkit-text-stroke-width: .2px;
+    -moz-osx-font-smoothing: grayscale
+}
+
+.fd-nav {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    z-index: 997;
+    width: 100%;
+    height: 60px;
+    font-size: 14px;
+    color: #fff;
+    background: #3296fa;
+    display: flex;
+    align-items: center
+}
+
+.fd-nav>* {
+    flex: 1;
+    width: 100%
+}
+
+.fd-nav .fd-nav-left {
+    display: -webkit-box;
+    display: flex;
+    align-items: center
+}
+
+.fd-nav .fd-nav-center {
+    flex: none;
+    width: 600px;
+    text-align: center
+}
+
+.fd-nav .fd-nav-right {
+    display: flex;
+    align-items: center;
+    justify-content: flex-end;
+    text-align: right
+}
+
+.fd-nav .fd-nav-back {
+    display: inline-block;
+    width: 60px;
+    height: 60px;
+    font-size: 22px;
+    border-right: 1px solid #1583f2;
+    text-align: center;
+    cursor: pointer
+}
+
+.fd-nav .fd-nav-back:hover {
+    background: #5af
+}
+
+.fd-nav .fd-nav-back:active {
+    background: #1583f2
+}
+
+.fd-nav .fd-nav-back .anticons {
+    line-height: 60px
+}
+
+.fd-nav .fd-nav-title {
+    width: 0;
+    flex: 1;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    padding: 0 15px
+}
+
+.fd-nav a {
+    color: #fff;
+    margin-left: 12px
+}
+
+.fd-nav .button-publish {
+    min-width: 80px;
+    margin-left: 4px;
+    margin-right: 15px;
+    color: #3296fa;
+    border-color: #fff
+}
+
+.fd-nav .button-publish.ant-btns:focus,
+.fd-nav .button-publish.ant-btns:hover {
+    color: #3296fa;
+    border-color: #fff;
+    box-shadow: 0 10px 20px 0 rgba(0, 0, 0, .3)
+}
+
+.fd-nav .button-publish.ant-btns:active {
+    color: #3296fa;
+    background: #d6eaff;
+    box-shadow: none
+}
+
+.fd-nav .button-preview {
+    min-width: 80px;
+    margin-left: 16px;
+    margin-right: 4px;
+    color: #fff;
+    border-color: #fff;
+    background: transparent
+}
+
+.fd-nav .button-preview.ant-btns:focus,
+.fd-nav .button-preview.ant-btns:hover {
+    color: #fff;
+    border-color: #fff;
+    background: #59acfc
+}
+
+.fd-nav .button-preview.ant-btns:active {
+    color: #fff;
+    border-color: #fff;
+    background: #2186ef
+}
+
+.fd-nav-content {
+    position: fixed;
+    top: 160px;
+    width: 70%;
+    /* left: 0; */
+    right: 0;
+    bottom: 0;
+    z-index: 1;
+    overflow-x: hidden;
+    overflow-y: auto;
+    padding-bottom: 30px
+}
+
+.error-modal-desc {
+    font-size: 13px;
+    color: rgba(25, 31, 37, .56);
+    line-height: 22px;
+    margin-bottom: 14px
+}
+
+.error-modal-list {
+    height: 200px;
+    overflow-y: auto;
+    margin-right: -25px;
+    padding-right: 25px
+}
+
+.error-modal-item {
+    padding: 10px 20px;
+    line-height: 21px;
+    background: #f6f6f6;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 8px;
+    border-radius: 4px
+}
+
+.error-modal-item-label {
+    flex: none;
+    font-size: 15px;
+    color: rgba(25, 31, 37, .56);
+    padding-right: 10px
+}
+
+.error-modal-item-content {
+    text-align: right;
+    flex: 1;
+    font-size: 13px;
+    color: #191f25
+}
+
+#body.blur {
+    -webkit-filter: blur(3px);
+    filter: blur(3px)
+}
+
+.zoom {
+    display: flex;
+    position: fixed;
+    -webkit-box-align: center;
+    -ms-flex-align: center;
+    align-items: center;
+    -webkit-box-pack: justify;
+    -ms-flex-pack: justify;
+    justify-content: space-between;
+    height: 40px;
+    width: 125px;
+    right: 40px;
+    margin-top: 30px;
+    z-index: 10
+}
+
+.zoom .zoom-in,
+.zoom .zoom-out {
+    width: 30px;
+    height: 30px;
+    background: #fff;
+    color: #c1c1cd;
+    cursor: pointer;
+    background-size: 100%;
+    background-repeat: no-repeat
+}
+
+.zoom .zoom-out {
+    background-image: url(https://gw.alicdn.com/tfs/TB1s0qhBHGYBuNjy0FoXXciBFXa-90-90.png)
+}
+
+.zoom .zoom-out.disabled {
+    opacity: .5
+}
+
+.zoom .zoom-in {
+    background-image: url(https://gw.alicdn.com/tfs/TB1UIgJBTtYBeNjy1XdXXXXyVXa-90-90.png)
+}
+
+.zoom .zoom-in.disabled {
+    opacity: .5
+}
+
+.auto-judge:hover .editable-title,
+.node-wrap-box:hover .editable-title {
+    border-bottom: 1px dashed #fff
+}
+
+.auto-judge:hover .editable-title.editing,
+.node-wrap-box:hover .editable-title.editing {
+    text-decoration: none;
+    border: 1px solid #d9d9d9
+}
+
+.auto-judge:hover .editable-title {
+    /* border-color: #15bc83 */
+}
+
+.editable-title {
+    line-height: 15px;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    border-bottom: 1px dashed transparent
+}
+
+.editable-title:before {
+    content: "";
+    position: absolute;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    right: 40px
+}
+
+.editable-title:hover {
+    border-bottom: 1px dashed #fff
+}
+
+.editable-title-input {
+    flex: none;
+    height: 18px;
+    padding-left: 4px;
+    text-indent: 0;
+    font-size: 12px;
+    line-height: 18px;
+    z-index: 1
+}
+
+.editable-title-input:hover {
+    text-decoration: none
+}
+
+.ant-btns {
+    position: relative
+}
+
+.node-wrap-box {
+    display: -webkit-inline-box;
+    display: -ms-inline-flexbox;
+    display: inline-flex;
+    -webkit-box-orient: vertical;
+    -webkit-box-direction: normal;
+    -ms-flex-direction: column;
+    flex-direction: column;
+    position: relative;
+    width: 430px;
+    min-height: 135px;
+    -ms-flex-negative: 0;
+    flex-shrink: 0;
+    background: #fff;
+    border-radius: 4px;
+    cursor: pointer
+}
+
+.node-wrap-box:after {
+    pointer-events: none;
+    content: "";
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    z-index: 2;
+    border-radius: 4px;
+    border: 1px solid transparent;
+    transition: all .1s cubic-bezier(.645, .045, .355, 1);
+    box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1)
+}
+
+.node-wrap-box.active:after,
+.node-wrap-box:active:after,
+.node-wrap-box:hover:after {
+    border: 1px solid #3296fa;
+    box-shadow: 0 0 6px 0 rgba(50, 150, 250, .3)
+}
+
+.node-wrap-box.active .close,
+.node-wrap-box:active .close,
+.node-wrap-box:hover .close {
+    display: block
+}
+
+.node-wrap-box.error:after {
+    border: 1px solid #f25643;
+    box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1)
+}
+
+.node-wrap-box .title {
+    position: relative;
+    display: flex;
+    align-items: center;
+    padding-left: 16px;
+    padding-right: 30px;
+    width: 100%;
+    height: 30px;
+    line-height: 30px;
+    font-size: 14px;
+    color: #fff;
+    text-align: left;
+    /* background: #576a95; */
+    border-radius: 4px 4px 0 0
+}
+
+.node-wrap-box .title .iconfont {
+    font-size: 12px;
+    margin-right: 5px
+}
+
+.node-wrap-box .placeholder {
+    color: #bfbfbf
+}
+
+.node-wrap-box .close {
+    display: none;
+    position: absolute;
+    right: 10px;
+    top: 50%;
+    transform: translateY(-50%);
+    width: 20px;
+    height: 20px;
+    font-size: 14px;
+    color: #fff;
+    border-radius: 50%;
+    text-align: center;
+    line-height: 20px
+}
+
+.node-wrap-box .content {
+    position: relative;
+    font-size: 14px;
+    padding: 16px;
+    padding-right: 30px
+}
+
+.node-wrap-box .content .text {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    display: -webkit-box;
+    -webkit-line-clamp: 3;
+    -webkit-box-orient: vertical
+}
+
+.node-wrap-box .content .arrow {
+    position: absolute;
+    right: 10px;
+    top: 50%;
+    transform: translateY(-50%);
+    width: 20px;
+    height: 14px;
+    font-size: 14px;
+    color: #979797
+}
+
+.start-node.node-wrap-box .content .text {
+    display: block;
+    white-space: nowrap
+}
+
+.node-wrap-box:before {
+    content: "";
+    position: absolute;
+    top: -12px;
+    left: 50%;
+    -webkit-transform: translateX(-50%);
+    transform: translateX(-50%);
+    width: 0;
+    height: 4px;
+    border-style: solid;
+    border-width: 8px 6px 4px;
+    border-color: #cacaca transparent transparent;
+    background: #f5f5f7
+}
+
+.node-wrap-box.start-node:before {
+    content: none
+}
+
+.top-left-cover-line {
+    left: -1px
+}
+
+.top-left-cover-line,
+.top-right-cover-line {
+    position: absolute;
+    height: 8px;
+    width: 50%;
+    background-color: #f5f5f7;
+    top: -4px
+}
+
+.top-right-cover-line {
+    right: -1px
+}
+
+.bottom-left-cover-line {
+    left: -1px
+}
+
+.bottom-left-cover-line,
+.bottom-right-cover-line {
+    position: absolute;
+    height: 8px;
+    width: 50%;
+    background-color: #f5f5f7;
+    bottom: -4px
+}
+
+.bottom-right-cover-line {
+    right: -1px
+}
+
+.dingflow-design {
+    width: 100%;
+    background-color: #f5f5f7;
+    overflow: auto;
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    top: 0
+}
+
+.dingflow-design .box-scale {
+    transform: scale(1);
+    display: inline-block;
+    position: relative;
+    width: 100%;
+    padding: 54.5px 0;
+    -webkit-box-align: start;
+    -ms-flex-align: start;
+    align-items: flex-start;
+    -webkit-box-pack: center;
+    -ms-flex-pack: center;
+    justify-content: center;
+    -ms-flex-wrap: wrap;
+    flex-wrap: wrap;
+    min-width: -webkit-min-content;
+    min-width: -moz-min-content;
+    min-width: min-content;
+    background-color: #f5f5f7;
+    transform-origin: 50% 0px 0px;
+}
+
+.dingflow-design .node-wrap {
+    flex-direction: column;
+    -webkit-box-pack: start;
+    -ms-flex-pack: start;
+    justify-content: flex-start;
+    -webkit-box-align: center;
+    -ms-flex-align: center;
+    align-items: center;
+    -ms-flex-wrap: wrap;
+    flex-wrap: wrap;
+    -webkit-box-flex: 1;
+    -ms-flex-positive: 1;
+    padding: 0 50px;
+    position: relative
+}
+
+.dingflow-design .branch-wrap,
+.dingflow-design .node-wrap {
+    display: inline-flex;
+    width: 100%
+}
+
+.dingflow-design .branch-box-wrap {
+    display: flex;
+    -webkit-box-orient: vertical;
+    -webkit-box-direction: normal;
+    -ms-flex-direction: column;
+    flex-direction: column;
+    -ms-flex-wrap: wrap;
+    flex-wrap: wrap;
+    -webkit-box-align: center;
+    -ms-flex-align: center;
+    align-items: center;
+    min-height: 270px;
+    width: 100%;
+    -ms-flex-negative: 0;
+    flex-shrink: 0
+}
+
+.dingflow-design .branch-box {
+    display: flex;
+    overflow: visible;
+    min-height: 180px;
+    height: auto;
+    border-bottom: 2px solid #ccc;
+    border-top: 2px solid #ccc;
+    position: relative;
+    margin-top: 15px
+}
+
+.dingflow-design .branch-box .col-box {
+    background: #f5f5f7
+}
+
+.dingflow-design .branch-box .col-box:before {
+    content: "";
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: 0;
+    margin: auto;
+    width: 2px;
+    height: 100%;
+    background-color: #cacaca
+}
+
+.add-branch {
+    border: none;
+    outline: none;
+    user-select: none;
+    justify-content: center;
+    font-size: 12px;
+    height: 19px;
+    width: 2px;
+    background: #c9c9c9;
+    /* box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .1); */
+    position: absolute;
+    top: -19px;
+    left: 50%;
+    transform: translateX(-50%);
+    transform-origin: center center;
+    z-index: 1;
+    display: inline-flex;
+    align-items: center;
+    -webkit-transition: all .3s cubic-bezier(.645, .045, .355, 1);
+    transition: all .3s cubic-bezier(.645, .045, .355, 1)
+}
+
+/* .dingflow-design .add-branch:hover {
+    transform: translateX(-50%) scale(1.1);
+    box-shadow: 0 8px 16px 0 rgba(0, 0, 0, .1)
+}
+
+.dingflow-design .add-branch:active {
+    transform: translateX(-50%);
+    box-shadow: none
+} */
+
+.dingflow-design .col-box {
+    display: inline-flex;
+    -webkit-box-orient: vertical;
+    -webkit-box-direction: normal;
+    flex-direction: column;
+    -webkit-box-align: center;
+    align-items: center;
+    position: relative
+}
+
+.dingflow-design .condition-node {
+    min-height: 220px
+}
+
+.dingflow-design .condition-node,
+.dingflow-design .condition-node-box {
+    display: inline-flex;
+    -webkit-box-orient: vertical;
+    -webkit-box-direction: normal;
+    flex-direction: column;
+    -webkit-box-flex: 1
+}
+
+.dingflow-design .condition-node-box {
+    padding-top: 30px;
+    padding-right: 50px;
+    padding-left: 50px;
+    -webkit-box-pack: center;
+    justify-content: center;
+    -webkit-box-align: center;
+    align-items: center;
+    flex-grow: 1;
+    position: relative
+}
+
+.dingflow-design .condition-node-box:before {
+    content: "";
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    margin: auto;
+    width: 2px;
+    height: 100%;
+    background-color: #cacaca
+}
+
+.dingflow-design .auto-judge {
+    position: relative;
+    width: 251px;
+    min-height: 114px;
+    background: #fff;
+    border-radius: 4px;
+    padding: 8px 12px;
+    cursor: pointer
+}
+
+.dingflow-design .auto-judge:after {
+    pointer-events: none;
+    content: "";
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    z-index: 2;
+    border-radius: 4px;
+    border: 1px solid transparent;
+    transition: all .1s cubic-bezier(.645, .045, .355, 1);
+    box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1)
+}
+
+.dingflow-design .auto-judge.active:after,
+.dingflow-design .auto-judge:active:after,
+.dingflow-design .auto-judge:hover:after {
+    border: 1px solid #3296fa;
+    box-shadow: 0 0 6px 0 rgba(50, 150, 250, .3)
+}
+
+.dingflow-design .auto-judge.active .close,
+.dingflow-design .auto-judge:active .close,
+.dingflow-design .auto-judge:hover .close {
+    display: block
+}
+
+.dingflow-design .auto-judge.error:after {
+    border: 1px solid #f25643;
+    box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1)
+}
+
+.dingflow-design .auto-judge .title-wrapper {
+    position: relative;
+    font-size: 14px;
+    font-weight: 500;
+    color: #3d3d3d;
+    text-align: left;
+    line-height: 16px
+}
+
+.dingflow-design .auto-judge .title-wrapper .editable-title {
+    display: inline-block;
+    max-width: 120px;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis
+}
+
+.dingflow-design .auto-judge .title-wrapper .priority-title {
+    display: inline-block;
+    float: right;
+    margin-right: 10px;
+    color: rgba(25, 31, 37, .56)
+}
+
+.dingflow-design .auto-judge .placeholder {
+    color: #bfbfbf
+}
+
+.dingflow-design .auto-judge .close {
+    display: none;
+    position: absolute;
+    right: -10px;
+    top: -10px;
+    width: 20px;
+    height: 20px;
+    font-size: 14px;
+    color: rgba(0, 0, 0, .25);
+    border-radius: 50%;
+    text-align: center;
+    line-height: 20px;
+    z-index: 2
+}
+
+.dingflow-design .auto-judge .content {
+    font-size: 14px;
+    color: #191f25;
+    text-align: left;
+    margin-top: 6px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    display: -webkit-box;
+    -webkit-line-clamp: 3;
+    -webkit-box-orient: vertical
+}
+.dingflow-design .auto-judge .content-note{
+    font-size: 12px;
+    color: #666;
+    text-align: left;
+    margin-top: 6px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    display: -webkit-box;
+    -webkit-line-clamp: 3;
+    -webkit-box-orient: vertical;
+    border-radius: 4px;
+    opacity: 1;
+    background: #f9f9f9;
+    padding: 14px 10px;
+}
+
+.dingflow-design .auto-judge .sort-left,
+.dingflow-design .auto-judge .sort-right {
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    display: none;
+    z-index: 1
+}
+
+.dingflow-design .auto-judge .sort-left {
+    left: 0;
+    border-right: 1px solid #f6f6f6
+}
+
+.dingflow-design .auto-judge .sort-right {
+    right: 0;
+    border-left: 1px solid #f6f6f6
+}
+
+.dingflow-design .auto-judge:hover .sort-left,
+.dingflow-design .auto-judge:hover .sort-right {
+    display: flex;
+    align-items: center
+}
+
+.dingflow-design .auto-judge .sort-left:hover,
+.dingflow-design .auto-judge .sort-right:hover {
+    background: #efefef
+}
+
+.dingflow-design .end-node {
+    border-radius: 50%;
+    font-size: 14px;
+    color: rgba(25, 31, 37, .4);
+    text-align: left
+}
+
+.dingflow-design .end-node .end-node-circle {
+    width: 10px;
+    height: 10px;
+    margin: auto;
+    border-radius: 50%;
+    background: #dbdcdc
+}
+
+.dingflow-design .end-node .end-node-text {
+    margin-top: 5px;
+    text-align: center
+}
+
+.approval-setting {
+    border-radius: 2px;
+    margin: 20px 0;
+    position: relative;
+    background: #fff
+}
+
+.ant-btns {
+    position: relative
+}
+
+.phase-content {
+    gap: 10px;
+    align-items: stretch;
+    padding: 10px;
+    height: 113px;
+    display: flex;
+    justify-content: center;
+  }
+
+  .left-content {
+    max-width: 50%;
+    text-align: left;
+    position: relative;
+    font-size: 14px;
+    padding: 13px;
+    border-radius: 4px;
+    background: #f9f9f9;
+    color: #666;
+    flex: 1;
+  }
+
+  .title-txt {
+    display: block;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    display: -webkit-box;
+    -webkit-line-clamp: 3;
+    -webkit-box-orient: vertical;
+    text-align: left;
+  }
+  .txt-contant {
+    white-space: nowrap;
+    font-size: 14px;
+    display: flex;
+    align-items: center;
+  }
+  .mag-bom {
+    margin-bottom: 10px;
+  }
+  .sop-middle {
+    max-width: 100%;
+    display: inline-block;
+    vertical-align: middle;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    overflow: hidden;
+    font-size: 14px;
+  }

+ 47 - 0
src/views/wechat/sop_task/add_sop/components/flowChart/utils/axios.js

@@ -0,0 +1,47 @@
+/*
+ * @Date: 2022-08-25 14:43:53
+ * @LastEditors: StavinLi 495727881@qq.com
+ * @LastEditTime: 2022-09-21 14:37:02
+ * @FilePath: /Workflow-Vue3/src/plugins/axios.js
+ */
+"use strict";
+
+import axios from "axios";
+
+// Full config:  https://github.com/axios/axios#request-config
+// axios.defaults.baseURL = process.env.baseURL || process.env.apiUrl || '';
+// axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
+// axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
+
+let config = {
+  // baseURL: process.env.baseURL || process.env.apiUrl || ""
+  // timeout: 60 * 1000, // Timeout
+  // withCredentials: true, // Check cross-site Access-Control
+};
+
+const _axios = axios.create(config);
+
+_axios.interceptors.request.use(
+  function (config) {
+    // Do something before request is sent
+    return config;
+  },
+  function (error) {
+    // Do something with request error
+    return Promise.reject(error);
+  }
+);
+
+// Add a response interceptor
+_axios.interceptors.response.use(
+  function (response) {
+    // Do something with response data
+    return response.data;
+  },
+  function (error) {
+    // Do something with response error
+    return Promise.reject(error);
+  }
+);
+
+export default _axios;

+ 42 - 0
src/views/wechat/sop_task/add_sop/components/flowChart/utils/const.js

@@ -0,0 +1,42 @@
+/*
+ * @Date: 2023-03-29 15:25:37
+ * @LastEditors: StavinLi 495727881@qq.com
+ * @LastEditTime: 2023-03-29 15:52:38
+ * @FilePath: /Workflow-Vue3/src/utils/const.js
+ */
+
+export let bgColors = ['87, 106, 149', '255, 148, 62', '50, 150, 250']
+export let placeholderList = ["发起人", "审核人", "抄送人"];
+
+export let setTypes = [
+  {value: 1, label: '指定成员'},
+  {value: 2, label: '主管'},
+  {value: 4, label: '发起人自选'},
+  {value: 5, label: '发起人自己'},
+  {value: 7, label: '连续多级主管'},
+]
+
+export let selectModes = [
+  {value: 1, label: '选一个人'},
+  {value: 2, label: '选多个人'},
+]
+
+export let selectRanges = [
+  {value: 1, label: '全公司'},
+  {value: 2, label: '指定成员'},
+  {value: 3, label: '指定角色'},
+]
+
+export let optTypes = [
+  {value: '1', label: '小于'},
+  {value: '2', label: '大于'},
+  {value: '3', label: '小于等于'},
+  {value: '4', label: '等于'},
+  {value: '5', label: '大于等于'},
+  {value: '6', label: '介于两个数之间'},
+]
+
+export let opt1s = [
+  {value: '<', label: '<'},
+  {value: '≤', label: '≤'},
+]

+ 157 - 0
src/views/wechat/sop_task/add_sop/components/flowChart/utils/index.js

@@ -0,0 +1,157 @@
+function All() {}
+All.prototype = {
+    timer: "",
+    debounce(fn, delay = 500) {
+        var _this = this;
+        return function(arg) {
+            //获取函数的作用域和变量
+            let that = this;
+            let args = arg;
+            clearTimeout(_this.timer) // 清除定时器
+            _this.timer = setTimeout(function() {
+                fn.call(that, args)
+            }, delay)
+        }
+    },
+    setCookie(val) { //cookie设置[{key:value}]、获取key、清除['key1','key2']
+        for (var i = 0, len = val.length; i < len; i++) {
+            for (var key in val[i]) {
+                document.cookie = key + '=' + encodeURIComponent(val[i][key]) + "; path=/";
+            }
+        }
+    },
+    getCookie(name) {
+        var strCookie = document.cookie;
+        var arrCookie = strCookie.split("; ");
+        for (var i = 0, len = arrCookie.length; i < len; i++) {
+            var arr = arrCookie[i].split("=");
+            if (name == arr[0]) {
+                return decodeURIComponent(arr[1]);
+            }
+        }
+    },
+    clearCookie(name) {
+        var myDate = new Date();
+        myDate.setTime(-1000); //设置时间    
+        for (var i = 0, len = name.length; i < len; i++) {
+            document.cookie = "" + name[i] + "=''; path=/; expires=" + myDate.toGMTString();
+        }
+    },
+    arrToStr(arr) {
+        if (arr) {
+            return arr.map(item => { return item.name }).toString()
+        }
+    },
+    toggleClass(arr, elem, key = 'id') {
+        return arr.some(item => { return item[key] == elem[key] });
+    },
+    toChecked(arr, elem, key = 'id') {
+        var isIncludes = this.toggleClass(arr, elem, key);
+        !isIncludes ? arr.push(elem) : this.removeEle(arr, elem, key);
+    },
+    removeEle(arr, elem, key = 'id') {
+        var includesIndex;
+        arr.map((item, index) => {
+            if (item[key] == elem[key]) {
+                includesIndex = index
+            }
+        });
+        arr.splice(includesIndex, 1);
+    },
+    setApproverStr(nodeConfig) {
+        if (nodeConfig.settype == 1) {
+            if (nodeConfig.nodeUserList.length == 1) {
+                return nodeConfig.nodeUserList[0].name
+            } else if (nodeConfig.nodeUserList.length > 1) {
+                if (nodeConfig.examineMode == 1) {
+                    return this.arrToStr(nodeConfig.nodeUserList)
+                } else if (nodeConfig.examineMode == 2) {
+                    return nodeConfig.nodeUserList.length + "人会签"
+                }
+            }
+        } else if (nodeConfig.settype == 2) {
+            let level = nodeConfig.directorLevel == 1 ? '直接主管' : '第' + nodeConfig.directorLevel + '级主管'
+            if (nodeConfig.examineMode == 1) {
+                return level
+            } else if (nodeConfig.examineMode == 2) {
+                return level + "会签"
+            }
+        } else if (nodeConfig.settype == 4) {
+            if (nodeConfig.selectRange == 1) {
+                return "发起人自选"
+            } else {
+                if (nodeConfig.nodeUserList.length > 0) {
+                    if (nodeConfig.selectRange == 2) {
+                        return "发起人自选"
+                    } else {
+                        return '发起人从' + nodeConfig.nodeUserList[0].name + '中自选'
+                    }
+                } else {
+                    return "";
+                }
+            }
+        } else if (nodeConfig.settype == 5) {
+            return "发起人自己"
+        } else if (nodeConfig.settype == 7) {
+            return '从直接主管到通讯录中级别最高的第' + nodeConfig.examineEndDirectorLevel + '个层级主管'
+        }
+    },
+    dealStr(str, obj) {
+        let arr = [];
+        let list = str.split(",");
+        for (var elem in obj) {
+            list.map(item => {
+                if (item == elem) {
+                    arr.push(obj[elem].value)
+                }
+            })
+        }
+        return arr.join("或")
+    },  
+    conditionStr(nodeConfig, index) {
+        var { conditionList, nodeUserList } = nodeConfig.conditionNodes[index];
+        if (conditionList.length == 0) {
+            return (index == nodeConfig.conditionNodes.length - 1) && nodeConfig.conditionNodes[0].conditionList.length != 0 ? '其他条件进入此流程' : '请设置条件'
+        } else {
+            let str = ""
+            for (var i = 0; i < conditionList.length; i++) {
+                var { columnId, columnType, showType, showName, optType, zdy1, opt1, zdy2, opt2, fixedDownBoxValue } = conditionList[i];
+                if (columnId == 0) {
+                    if (nodeUserList.length != 0) {
+                        str += '发起人属于:'
+                        str += nodeUserList.map(item => { return item.name }).join("或") + " 并且 "
+                    }
+                }
+                if (columnType == "String" && showType == "3") {
+                    if (zdy1) {
+                        str += showName + '属于:' + this.dealStr(zdy1, JSON.parse(fixedDownBoxValue)) + " 并且 "
+                    }
+                }
+                if (columnType == "Double") {
+                    if (optType != 6 && zdy1) {
+                        var optTypeStr = ["", "<", ">", "≤", "=", "≥"][optType]
+                        str += `${showName} ${optTypeStr} ${zdy1} 并且 `
+                    } else if (optType == 6 && zdy1 && zdy2) {
+                        str += `${zdy1} ${opt1} ${showName} ${opt2} ${zdy2} 并且 `
+                    }
+                }
+            }
+            return str ? str.substring(0, str.length - 4) : '请设置条件'
+        }
+    },
+    copyerStr(nodeConfig) {
+        if (nodeConfig.nodeUserList.length != 0) {
+            return this.arrToStr(nodeConfig.nodeUserList)
+        } else {
+            if (nodeConfig.ccSelfSelectFlag == 1) {
+                return "发起人自选"
+            }
+        }
+    }, 
+    toggleStrClass(item, key) {
+        let a = item.zdy1 ? item.zdy1.split(",") : []
+        return a.some(item => { return item == key });
+    },
+}
+
+export default new All();

+ 102 - 0
src/views/wechat/sop_task/add_sop/components/htmlTextarea.vue

@@ -0,0 +1,102 @@
+<template>
+  <div class="html-textarea">
+    <div  v-html="formattedContent" class="display-area" @click="handleClick"></div>
+    <textarea v-if="false" v-model="taskForm.messageContent" @blur="handleBlur" @focus="handleFocus"
+      class="edit-area" :rows="rows" :maxlength="maxlength"></textarea>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref, watch, PropType, computed } from 'vue';
+
+export default defineComponent({
+  name: 'HtmlTextarea',
+  props: {
+    modelValue: {
+      type: String as PropType<string>,
+      required: true,
+    },
+    rows: {
+      type: Number as PropType<number>,
+      default: 4,
+    },
+    maxlength: {
+      type: Number as PropType<number>,
+      default: 1000,
+    },
+  },
+  emits: ['update:modelValue'],
+  setup(props, { emit }) {
+    const isFocused = ref(false);
+    const taskForm = ref({
+      messageContent: props.modelValue,
+    });
+
+    watch(
+      () => props.modelValue,
+      (newValue) => {
+        taskForm.value.messageContent = newValue;
+      }
+    );
+
+    watch(
+      () => taskForm.value.messageContent,
+      (newValue) => {
+        emit('update:modelValue', newValue);
+      }
+    );
+
+    const handleFocus = () => {
+      isFocused.value = true;
+    };
+
+    const handleBlur = () => {
+      isFocused.value = false;
+    };
+
+    const handleClick = () => {
+      isFocused.value = true;
+    };
+
+    const formattedContent = computed(() => {
+      return taskForm.value.messageContent.replace(/{{(.*?)}}/g, '<span class="highlight">$1</span>');
+    });
+
+    return {
+      isFocused,
+      taskForm,
+      handleFocus,
+      handleBlur,
+      handleClick,
+      formattedContent,
+    };
+  },
+});
+</script>
+
+<style scoped lang="less">
+.html-textarea {
+  position: relative;
+}
+
+.edit-area {
+  width: 100%;
+  padding: 8px;
+}
+
+.display-area {
+  padding: 8px;
+  white-space: pre-wrap;
+  cursor: text;
+  border: 1px solid #d9d9d9;
+  border-radius: 2px;
+  min-height: 34px;
+}
+
+.highlight {
+  background-color: yellow;
+  color: red;
+  padding: 0 2px;
+  border-radius: 2px;
+}
+</style>

+ 651 - 0
src/views/wechat/sop_task/add_sop/components/leftStage.vue

@@ -0,0 +1,651 @@
+<template>
+  <div class="container">
+    <a-button type="primary" class="create-button" @click="handleCreate">新建用户阶段</a-button>
+    <template v-for="(item, index) in stageList" :key="item.id">
+      <div class="user-stage-card" :style="getBorderStyle(item.id)"    @click="handleEdit(item.id, item)">
+        <div class="header">
+          <span class="title">{{ item.name }}</span>
+          <!-- @click="handleCreate" -->
+          <!-- <SettingOutlined  class="settings-icon"  /> -->
+          <Dropdown :trigger="['click']" arrow placement="bottomLeft">
+            <a class="priority-title" @click.prevent>
+              <SettingOutlined />
+            </a>
+            <template #overlay>
+              <Menu @click="handleSetting">
+                <MenuItem key="copy">复制</MenuItem>
+                <MenuItem key="del">删除</MenuItem>
+                <MenuItem key="edit">编辑</MenuItem>
+                <!-- <MenuItem key="add">添加节点</MenuItem> -->
+              </Menu>
+            </template>
+          </Dropdown>
+        </div>
+        <div class="content">
+          <div class="condition" >
+            <FilterOutlined class="icon" />
+            <span class="tit">
+              触发条件:
+              <span class="txt">{{ item.conditionType === 1 ? '客户标签' : '基本信息' }}</span>
+            </span>
+          </div>
+          <div class="relation" >
+            <BulbOutlined class="icon" />
+            <span class="tit">
+              条件关系:
+              <span class="txt">
+                {{ item.conditionOperator === 1 ? '满足所有条件' : '满足一个条件即可' }}
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </template>
+  </div>
+  <!-- 用户阶段设置弹窗 -->
+  <Spin class="spin-style" :spinning="loading" tip="Loading...">
+    <UserPhasesDrawer
+      v-if="!loading"
+      :visible="drawerVisible"
+      @update:open="drawerVisible = $event"
+      @update:getStageList="getStageList"
+      :stageId="stageItemId"
+    />
+  </Spin>
+  <!-- 流程图部分 -->
+  <div class="flow-chart-container">
+    <div class="flow-chart">
+      <div class="flow-chart-contant">
+        <div class="fd-nav-content">
+          <section class="dingflow-design">
+            <div class="zoom">
+              <div class="zoom-out" :class="nowVal == 50 && 'disabled'" @click="zoomSize(1)"></div>
+              <span>{{ nowVal }}%</span>
+              <div class="zoom-in" :class="nowVal == 300 && 'disabled'" @click="zoomSize(2)"></div>
+            </div>
+            <div class="box-scale" :style="`transform: scale(${nowVal / 100});`">
+              <div class="node-wrap">
+                <div class="node-wrap-box" :class="'start-node ' + ' '">
+                  <div
+                    class="title"
+                    :style="{ background: store.sopNameAndTimeValid ? '#1677ff' : '#b8c1d6' }"
+                  >
+                    <span>客户阶段设置</span>
+                  </div>
+                  <div class="phase-content" @click="setPerson">
+                    <div class="left-content">
+                      <div class="title-txt">
+                        <div class="txt-contant mag-bom">
+                          <ClockCircleOutlined style="color: #467cea; width: 20px; height: 20px" />
+                          <span v-if="stageName == ''" class="sop-middle">
+                            创建用户阶段名称、触发
+                          </span>
+                          <span v-else class="sop-middle">阶段名称:{{ stageName }}</span>
+                        </div>
+                        <div class="txt-contant">执行任务:{{ stageType }}</div>
+                      </div>
+                    </div>
+                    <div class="left-content">
+                      <div class="title-txt">
+                        <div class="txt-contant mag-bom">
+                          <ThunderboltOutlined style="color: #467cea; width: 20px; height: 20px" />
+                          <span class="sop-middle">{{ '配置阶段任务' }}</span>
+                        </div>
+                        <div v-if="stageTask.length == 0" class="txt-contant">
+                          设置阶段的执行任务
+                        </div>
+                        <div v-else class="txt-contant">
+                          <span
+                            v-for="(item, index) in stageTask"
+                            style="margin-right: 5px"
+                            :key="index"
+                          >
+                            {{ item.name }}
+                          </span>
+                        </div>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+                <div class="add-node-btn-box">
+                  <div class="add-node-btn">
+                    <button class="btn" type="button" @click="addType(0)">
+                      <span class="iconfont"></span>
+                    </button>
+                  </div>
+                </div>
+              </div>
+              <!-- v-model:flowPermission="flowPermission"  -->
+              <nodeWrap v-model:nodeConfig="nodeConfig" @update:nodeDrawerOpen="handleNodeOpen" @update:nodeDrawerOEdit="handleNodeEdit" @update:delNodeUpdate="handleDelNodeUpdate"/>
+
+              <div class="end-node">
+                <div class="end-node-circle"></div>
+                <div class="end-node-text">流程结束</div>
+              </div>
+            </div>
+          </section>
+        </div>
+      </div>
+    </div>
+  </div>
+  <!-- 节点配置弹窗 -->
+  <Spin class="spin-style" :spinning="loading" tip="Loading...">
+    <NodeConfigDrawer
+      v-if="!loading"
+      :visible="drawerVisibleNode"
+      @update:open="drawerVisibleNode = $event"
+      @update:getNodeList="getNodeList"
+      :stageId="stageItemId"
+      :isAddNode="isAddNode"
+      :nodeId="nodeId"
+      
+    />
+  </Spin>
+</template>
+
+<script setup lang="ts">
+  import {
+    ref,
+    onMounted,
+    watch,
+    defineProps,
+    getCurrentInstance,
+    defineEmits,
+    reactive,
+    computed,
+  } from 'vue';
+  // import { Button } from 'ant-design-vue';
+  // import FlowChart from './flowChart.vue';
+  import nodeWrap from '../components/flowChart/components/nodeWrap.vue';
+  import UserPhasesDrawer from './userPhasesDrawer.vue';
+  import NodeConfigDrawer from './nodeConfigDrawer.vue';
+  import { getLabelSelectList } from '@/api/wechat/label';
+  import BlankBox from '../components/flowChart/components/blankBox.vue';
+  import $func from '../components/flowChart/utils/index';
+  let _uid = getCurrentInstance().uid;
+  import {
+    deleteSopTask,
+    getSopStageList,
+    getSopStageDetailById,
+    getSopStageNodeList,
+    getSopStageNodeDetail,
+  } from '@/api/wechat/sopTask';
+  import { getWorkFlowData, setWorkFlowData } from './flowChart/api/index';
+  import { useStore } from '../stores/index';
+  import { Modal, Dropdown, Menu, MenuItem, Spin } from 'ant-design-vue';
+  import {
+    SettingOutlined,
+    FilterOutlined,
+    BulbOutlined,
+    ClockCircleOutlined,
+    ThunderboltOutlined,
+  } from '@ant-design/icons-vue';
+import { active } from 'sortablejs';
+  let store = useStore();
+  let props = defineProps({
+    childNodeP: {
+      type: Object,
+      default: () => ({}),
+    },
+  });
+  let isAddNode = ref(true);
+  //  let emits = defineEmits(['update:childNodeP']);
+  // let { setSopLabelList ,setSopStadeId} = useStore();
+  //  let { setTableId, setIsTried } = useStore();
+  const drawerVisible = ref(false);
+  const drawerVisibleNode = ref(false);
+  let nowVal = ref(100);
+  let nodeId = ref(undefined);
+  // import addNode from '../components/flowChart/components/addNode.vue';
+  // let {sopNameAndTimeVilidate,sopLabelList,sopTaskDtat} = store
+  const loading = ref(false);
+  const stageList = ref([{ name: '默认', conditionType: 1, conditionOperator: 2 }]);
+  let stageItemId = ref(undefined);
+  let nodeConfig = ref([]);
+  let stageItem = reactive({});
+  let stageName = ref('');
+  let stageType = ref('客户标签');
+  let stageTask = ref<Array<{ name: string }>>([]);
+let titActive = ref(null);
+  onMounted(async () => {
+    if (store.sopTaskDtat) {
+      const dataSource = store.sopTaskDtat;
+      console.log(dataSource.name, 'sopTaskDtat======');
+    }
+    // let { data } = await getSopStageNodeList({ stageId: payload.stageId });
+    // nodeConfig.value = { conditionNodes: data };
+    console.log('nodeConfig是多少', nodeConfig);
+  });
+  function getBorderStyle(id) {
+      return titActive.value === id ? 'border: 1px solid #307ef2;' : 'border: 1px solid #f3f3f3;';
+    }
+  const zoomSize = (type) => {
+    if (type == 1) {
+      if (nowVal.value == 50) {
+        return;
+      }
+      nowVal.value -= 10;
+    } else {
+      if (nowVal.value == 300) {
+        return;
+      }
+      nowVal.value += 10;
+    }
+  };
+  async function handleDelNodeUpdate(payload: {reseat:boolean,stageId:number}){
+    loading.value = true;
+    if(payload.reseat ) {
+      let res = await getSopStageNodeList({ stageId: payload.stageId });
+    nodeConfig.value = { conditionNodes: res.data };
+    loading.value = false;
+    }
+  }
+  const handleUpdateChildNodeP = (newChildNodeP) => {
+    console.log('Updated childNodeP:', newChildNodeP);
+    nodeConfig.value.childNode = newChildNodeP;
+    // nodeConfig.value.push(newChildNodeP);
+    console.log('nodeConfignodeConfignodeConfig', nodeConfig.value);
+  };
+
+  async function addType(val) {
+    console.log('阶段下的加号按钮', val);
+    ModalTips('node');
+    stageItemId.value = stageItemId.value;
+    nodeId.value = val;
+  }
+  async function setPerson(priorityLevel) {
+    ModalTips('stage');
+    stageItemId.value = stageItemId.value;
+  }
+
+  async function getNodeDetail(id) {
+    // let res = await getSopStageNodeDetail({ id });
+    let res = await getSopStageNodeList({ stageId: id });
+    nodeConfig.value = { conditionNodes: res.data };
+  }
+  async function getNodeList(payload: { addNode: boolean; nodeId: number; stageId: number }) {
+    if (payload.addNode) {
+      loading.value = true;
+      let res = await getSopStageNodeList({ stageId: payload.stageId });
+      if (res.code == 0 && res.data) {
+        nodeConfig.value = { conditionNodes: res.data };
+        loading.value = false;
+      }
+    } else {
+      loading.value = true;
+      let res = await getSopStageNodeList({ stageId: payload.stageId });
+      if (res.code == 0 && res.data) {
+        nodeConfig.value = { conditionNodes: res.data };
+        loading.value = false;
+      }
+    }
+  }
+  async function handleNodeEdit (payload: {
+    open: boolean;
+    nodeId: number;
+    isAdd: boolean;
+  }) {
+    if (payload.open) {
+      if (payload.isAdd) {
+        isAddNode.value = true;
+      }else{
+        isAddNode.value = false;
+      }
+      ModalTips('node');
+      nodeId.value = payload.nodeId;
+    }
+  }
+  async function handleNodeOpen(payload: {
+    open: boolean;
+    nodeId: number;
+    isAdd: boolean;
+  }) {
+    if (payload.open) {
+      if (payload.isAdd) {
+        isAddNode.value = true;
+      }else{
+        isAddNode.value = false;
+      }
+      ModalTips('node');
+      nodeId.value = payload.nodeId;
+    }
+  }
+  async function getStageList(payload: { value: boolean; id: number }) {
+    stageTask.value = [];
+    stageItemId.value = payload.id;
+    let res = await getSopStageList({ taskId: store.sopTaskId });
+    if (res && res.code === 0) {
+      // store.setSopStageList(res.data.data || [])
+      stageList.value = res.data.data;
+    }
+    let data = await getSopStageDetailById({ id: payload.id });
+    console.log('[[[[]]]]', data);
+    stageName.value = data.data.name;
+    stageType.value = data.data.conditionType === 1 ? '客户标签' : '客户基本信息';
+    if (data.data && data.data.actionLabel.length > 0) {
+      stageTask.value.push({ name: '打标签' });
+    }
+
+    if (data.data && data.data.actionMessage.length > 0) {
+      stageTask.value.push({ name: '发消息' });
+    }
+  }
+  // const handleSopStageListUpdate = (newValue) => {
+  //   console.log(newValue, 'newValue');
+  //   stageList.value = newValue;
+  // };
+
+  async function ModalTips(val) {
+    if (store.sopNameAndTimeVilidate) {
+      loading.value = true;
+      if (val == 'stage') {
+        drawerVisible.value = true;
+      } else {
+        drawerVisibleNode.value = true;
+      }
+      try {
+        let res = await getLabelSelectList({ page: 1, pageSize: 1000, type: 1 });
+        if (res.code === 0) {
+          store.setSopLabelList(res.data);
+          console.log('first', store.sopLabelList);
+        }
+      } finally {
+        loading.value = false;
+      }
+    } else {
+      Modal.confirm({
+        title: '提示',
+        content: '请先填写SOP名称或选择完时间段,才能继续完成以下操作!',
+        okText: '确定',
+        okType: 'danger',
+        cancelText: '取消',
+        onOk() {},
+        onCancel() {
+          // 取消按钮的回调:什么也不做
+          console.log('Cancel');
+        },
+      });
+    }
+  }
+  async function handleCreate() {
+    ModalTips('stage');
+    // setSopStadeId(undefined);
+    stageItemId.value = undefined;
+  }
+  function handleSetting() {}
+  async function handleEdit(id, val) {
+    // ModalTips();
+    titActive.value = id
+    stageTask.value = [];
+    stageItemId.value = id;
+    stageItem = val;
+    stageName.value = val.name;
+    stageType.value = val.conditionType === 1 ? '客户标签' : '客户基本信息';
+    if (val && val.actionLabel.length > 0) {
+      stageTask.value.push({ name: '打标签' });
+    }
+
+    if (val.actionMessage && val.actionMessage.length > 0) {
+      stageTask.value.push({ name: '发消息' });
+    }
+    //获取节点列表
+    let res = await getSopStageNodeList({ stageId: id });
+    nodeConfig.value = { conditionNodes: res.data };
+    console.log('节点数组', res.data);
+  }
+</script>
+
+<style scoped lang="less">
+  @import '../components/flowChart/css/workflow.css';
+  .flow-chart {
+    width: 100%;
+    height: 100%;
+    position: relative;
+  }
+  .flow-chart-contant {
+    width: 80%;
+    height: 100%;
+    position: absolute;
+    top: 0;
+    right: 0;
+  }
+  .container {
+    width: 230px;
+    height: calc(100vh - 160px);
+    margin: 10px 0 0 0px;
+    background: #fff;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+  }
+
+  .create-button {
+    width: 190px;
+    border-radius: 3px;
+    margin: 15px;
+  }
+
+  .user-stage-card {
+    
+    border-radius: 4px;
+    padding: 28px 10px 15px 10px;
+    margin-bottom: 10px;
+    cursor: pointer;
+  }
+  .tit {
+    font-size: 13px;
+  }
+  .txt {
+    font-size: 12px;
+  }
+  .header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 10px;
+  }
+
+  .title {
+    font-size: 16px;
+    font-weight: bold;
+  }
+
+  .settings-icon {
+    font-size: 20px;
+    width: 15px;
+    height: 15px;
+  }
+
+  .content {
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+  }
+
+  .condition,
+  .relation {
+    display: flex;
+    align-items: center;
+  }
+
+  .icon {
+    margin-right: 5px;
+  }
+  .spin-style {
+    width: 100%;
+    height: 100%;
+    z-index: 1000;
+    position: fixed;
+    top: 0;
+    left: 0;
+    background: rgba(255, 255, 255, 0.5);
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+  }
+  .flow-chart-container {
+    width: calc(80% - 10px);
+    height: calc(100% - 10px);
+    margin: 10px 10px 0 0;
+  }
+  .error_tip {
+    position: absolute;
+    top: 0px;
+    right: 0px;
+    transform: translate(150%, 0px);
+    font-size: 24px;
+  }
+
+  .promoter_person .el-dialog__body {
+    padding: 10px 20px 14px 20px;
+  }
+
+  .selected_list {
+    margin-bottom: 20px;
+    line-height: 30px;
+  }
+
+  .selected_list span {
+    margin-right: 10px;
+    padding: 3px 6px 3px 9px;
+    line-height: 12px;
+    white-space: nowrap;
+    border-radius: 2px;
+    border: 1px solid rgba(220, 220, 220, 1);
+  }
+
+  .selected_list img {
+    margin-left: 5px;
+    width: 7px;
+    height: 7px;
+    cursor: pointer;
+  }
+</style>
+<style scoped lang="less">
+  .add-node-btn-box {
+    width: 240px;
+    display: -webkit-inline-box;
+    display: -ms-inline-flexbox;
+    display: inline-flex;
+    -ms-flex-negative: 0;
+    flex-shrink: 0;
+    -webkit-box-flex: 1;
+    -ms-flex-positive: 1;
+    position: relative;
+    &:before {
+      content: '';
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      z-index: -1;
+      margin: auto;
+      width: 2px;
+      height: 100%;
+      background-color: #cacaca;
+    }
+    .add-node-btn {
+      user-select: none;
+      width: 240px;
+      padding: 20px 0 32px;
+      display: flex;
+      -webkit-box-pack: center;
+      justify-content: center;
+      flex-shrink: 0;
+      -webkit-box-flex: 1;
+      flex-grow: 1;
+      .btn {
+        outline: none;
+        box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
+        width: 30px;
+        height: 30px;
+        background: #fff;
+        border-radius: 50%;
+        position: relative;
+        top: -35px;
+        z-index: 2;
+        border: none;
+        line-height: 30px;
+        -webkit-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+        transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+        .iconfont {
+          color: #999;
+          font-size: 16px;
+        }
+        &:hover {
+          transform: scale(1.3);
+          z-index: 3;
+          background: #1677ff;
+          box-shadow: 0 13px 27px 0 rgba(0, 0, 0, 0.1);
+        }
+        &:active {
+          transform: none;
+          background: #1e83e9;
+          box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
+        }
+      }
+    }
+  }
+</style>
+<style scoped lang="less">
+  .add-node-popover-body {
+    display: flex;
+    .add-node-popover-item {
+      margin-right: 10px;
+      cursor: pointer;
+      text-align: center;
+      flex: 1;
+      color: #191f25 !important;
+      .item-wrapper {
+        user-select: none;
+        display: inline-block;
+        width: 80px;
+        height: 80px;
+        margin-bottom: 5px;
+        background: #fff;
+        border: 1px solid #e2e2e2;
+        border-radius: 50%;
+        transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+        .iconfont {
+          font-size: 35px;
+          line-height: 80px;
+        }
+      }
+      &.approver {
+        .item-wrapper {
+          color: #ff943e;
+        }
+      }
+      &.notifier {
+        .item-wrapper {
+          color: #3296fa;
+        }
+      }
+      &.condition {
+        .item-wrapper {
+          color: #15bc83;
+        }
+      }
+      &:hover {
+        .item-wrapper {
+          background: #3296fa;
+          box-shadow: 0 10px 20px 0 rgba(50, 150, 250, 0.4);
+        }
+        .iconfont {
+          color: #fff;
+        }
+      }
+      &:active {
+        .item-wrapper {
+          box-shadow: none;
+          background: #eaeaea;
+        }
+        .iconfont {
+          color: inherit;
+        }
+      }
+    }
+  }
+</style>

+ 472 - 0
src/views/wechat/sop_task/add_sop/components/msgContant.vue

@@ -0,0 +1,472 @@
+<!-- 消息内容 -->
+<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
+            class="custom-textarea"
+            v-model:value="area.content"
+            :rows="8"
+            :maxlength="1000"
+            @focus="handleFocus"
+            @blur="handleBlur"
+          />
+          <!-- <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" />
+        </div>
+      </a-form-item>
+      <a-form-item v-else-if="area.type === 2" label="" name="fileContent" class="message-content">
+        <div class="upload-contant">
+          <a-upload-dragger
+            class="upload"
+            :custom-request="customRequest"
+            :before-upload="beforeUpload"
+            :file-list="fileList"
+            :on-change="handleChange"
+            :on-remove="handleRemove"
+            :multiple="false"
+            :accept="acceptTypes"
+            v-model:file-list="fileList"
+            list-type="picture-card"
+          >
+            <template #default>
+              <div v-if="fileList.length === 0">
+                <CloudUploadOutlined style="font-size: 25px" />
+                <div>
+                  将文件拖到此处,或
+                  <span style="color: #1890ff">点击上传</span>
+                </div>
+                <div>支持上传文件为图片、视频、pdf、word、excel</div>
+              </div>
+            </template>
+          </a-upload-dragger>
+          <!-- @remove="emit('remove')" @moveUp="emit('moveUp')" @moveDown="emit('moveDown')" -->
+          <CustomIcons :index="index" :length="length" />
+        </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 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 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, computed } 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 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,
+    },
+    // props: {
+    //   actionMessage: Array,
+    // },
+    props: {
+      value: {
+        type: Array,
+        required: true,
+      },
+    },
+    emits: ['update:value'],
+    setup(props, { emit }) {
+      // const internalValue = ref([...props.value]);
+      const internalValue = ref(addCanMoveProperties(props.value));
+      const taskForm = reactive({
+        fileContent: '',
+      });
+
+     
+      watch(
+        () => props.value,
+        (newValue) => {
+          internalValue.value = addCanMoveProperties(newValue);
+        },
+      );
+
+      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 uploadUrl = ref('/upload'); // 上传地址
+      const acceptTypes = ref(
+        'image/*,video/*,application/pdf,application/msword,application/vnd.ms-excel',
+      );
+      const fileList = ref([]);
+
+      const beforeUpload = (file) => {
+        const isAllowedType = /image|video|pdf|msword|vnd.ms-excel/.test(file.type);
+        if (!isAllowedType) {
+          message.error('只能上传图片、视频、PDF、Word、Excel文件');
+        }
+        return isAllowedType;
+      };
+
+      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 customRequest = async ({ file, onSuccess, onError }) => {
+        const formData = new FormData();
+        formData.append('file', file);
+
+        try {
+          const response = await fetch(uploadUrl.value, {
+            method: 'POST',
+            body: formData,
+          });
+
+          if (response.ok) {
+            const result = await response.json();
+            onSuccess(result);
+          } else {
+            onError(new Error('上传失败'));
+          }
+        } catch (error) {
+          onError(error);
+        }
+      };
+
+      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:''}
+      //   ]//命中后发送的消息内容
+      // });
+      console.log(internalValue, '====');
+      // 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,
+          file: '',
+          canMoveUp: internalValue.value.length > 0,
+          canMoveDown: false,
+        });
+
+        // 更新前一个项的 canMoveDown 为 true
+        updateCanMoveProperties();
+      };
+      // 根据传入的类型添加对应的区域
+      const handleCreateClick = (key: string) => {
+        if (key.key === '1') {
+          console.log('first');
+          addTextArea();
+        } else {
+          addFileArea();
+        }
+      };
+
+      const removeItem = (index: number) => {
+        console.log('first', index);
+        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,
+        uploadUrl,
+        acceptTypes,
+        fileList,
+        beforeUpload,
+        handleChange,
+        handleRemove,
+        customRequest,
+        internalValue,
+        updateValue,
+        addCanMoveProperties,
+      };
+    },
+  });
+</script>
+<style scoped lang="less">
+  .ant-form-item {
+    margin-bottom: 0;
+  }
+  .message-content-btn {
+    width: 800px;
+    background-color: #f9f9f9;
+    padding: 10px 30px 10px 50px;
+    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;
+  }
+  .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;
+  }
+</style>

+ 604 - 0
src/views/wechat/sop_task/add_sop/components/nodeConfigDrawer.vue

@@ -0,0 +1,604 @@
+<template>
+  <!-- 任务节点配置 -->
+  <Spin class="loading-style" :spinning="loading" tip="Loading...">
+    <div v-if="!loading">
+      <Drawer v-model:open="visible" title="任务节点配置" width="900" @close="handleClose">
+        <Steps :current="currentStep" size="default" class="step-style">
+          <Step class="custom-step" title="节点触发条件"></Step>
+          <Step class="custom-step" title="配置节点内容"></Step>
+        </Steps>
+        <div v-if="currentStep === 0" class="step-content">
+          <Form :model="form" :label-col="{ span: 3 }" :wrapper-col="{ span: 19 }" ref="formRef">
+            <FormItem
+              label="节点名称"
+              name="nodeName"
+              :rules="[{ required: true, message: '请输入节点名称' }]"
+            >
+              <Input
+                v-model:value="form.nodeName"
+                placeholder="请输入节点名称"
+                :maxlength="20"
+                show-count
+              />
+            </FormItem>
+            <FormItem label="用户满足条件"></FormItem>
+            <div class="description">
+              <InfoCircleOutlined />
+              主要用于执行任务的触发的条件,满足条件才可进入群发,转人工的动作
+            </div>
+            <FormItem
+              label="回复情况"
+              name="conditionType"
+              :rules="[{ required: true, message: '请选择回复情况' }]"
+            >
+              <Radio.Group v-model:value="form.conditionType">
+                <Radio value="2">客户不回复</Radio>
+                <Radio value="1">客户回复</Radio>
+              </Radio.Group>
+            </FormItem>
+            <FormItem
+              v-show="form.conditionType === '2'"
+              class="no-colon"
+              label="超时时间"
+              name="timeout"
+            >
+              <div style="display: flex; align-items: center">
+                超过
+                <Input
+                  v-model:value="form.timeout"
+                  :style="{ width: '80px', margin: '0 5px' }"
+                  :rules="[{ required: true, message: '请输入' }]"
+                />
+                小时,执行该动作
+              </div>
+            </FormItem>
+            <FormItem
+              v-show="form.conditionType === '1'"
+              label="回复内容"
+              name="content"
+              :rules="[{ required: true, message: '请选择回复内容' }]"
+            >
+              <Radio.Group v-model:value="form.content">
+                <Radio value="concreteContent">具体内容</Radio>
+                <Radio value="anyContant">任意内容</Radio>
+              </Radio.Group>
+            </FormItem>
+            <!-- 具体内容 -->
+            <FormItem
+              v-show="form.content === 'concreteContent'"
+              label="具体内容"
+              name="conditionList"
+            >
+              <div class="concrete-content-container">
+                <div
+                  v-for="(item, index) in form.conditionList"
+                  :key="index"
+                  class="concrete-content-item"
+                >
+                  <span>当用户表达</span>
+                  <Input
+                    v-model:value="item.expression"
+                    placeholder="示例:请给我下商品的链接"
+                    :style="{ width: '230px', margin: '0 5px' }"
+                  />
+                  <span>的意思时,执行该动作。</span>
+                  <CloseCircleOutlined
+                    style="color: rgb(211, 210, 210)"
+                    @click="removeContent(index)"
+                  />
+                  <!-- <a-button type="text" @click="removeContent(index)">x</a-button> -->
+                </div>
+                <div @click="addContent">
+                  <PlusCircleOutlined style="color: rgb(211, 210, 210)" />
+                  添加
+                </div>
+              </div>
+              <!-- <a-button type="dashed" @click="addContent">+ 添加</a-button> -->
+            </FormItem>
+          </Form>
+        </div>
+        <div v-if="currentStep === 1" class="step-content">
+          <Form
+            :model="taskForm"
+            :label-col="{ span: 3 }"
+            :wrapper-col="{ span: 19 }"
+            ref="taskFormRef"
+          >
+            <FormItem label="执行任务" name="taskType">
+              <Button
+                :class="isMsgAction ? 'action-btn' : 'btn-style'"
+                @click="setTaskType('sendMessage')"
+              >
+                <template #icon>
+                  <CommentOutlined />
+                </template>
+                发消息
+              </Button>
+              <Button :class="isTagAction ? 'action-btn' : 'btn-style'" @click="setTaskType('tag')">
+                <template #icon>
+                  <TagsOutlined />
+                </template>
+                打标签
+              </Button>
+            </FormItem>
+            <!-- 发消息 -->
+            <Collapse
+              v-show="isMsgAction"
+              v-model:activeKey="activeKey"
+              collapsible="header"
+              class="custom-collapse"
+            >
+              <Collapse.Panel key="1" header="发消息" class="custom-collapse-panel">
+                <div @click.stop>
+                  <FormItem label="发送方式" name="sendType">
+                    <Radio.Group v-model:value="taskForm.sendType">
+                      <Radio value="immediate">立即发送</Radio>
+                      <!-- <Radio value="scheduled">定时发送</Radio> -->
+                    </Radio.Group>
+                  </FormItem>
+                  <!-- 根据发送方式显示 MsgContant 或在折叠面板中展示 -->
+                  <div v-if="taskForm.sendType === 'immediate'">
+                    <FormItem label="" name="actionMessage">
+                      <NodeMsgContant v-model:value="taskForm.actionMessage" />
+                    </FormItem>
+                  </div>
+                  <TimedSending v-else />
+                </div>
+              </Collapse.Panel>
+            </Collapse>
+            <!-- 打标签 -->
+            <Collapse
+              v-show="isTagAction"
+              v-model:activeKey="activeKeyTag"
+              collapsible="header"
+              class="custom-collapse-tag"
+            >
+              <Collapse.Panel key="1" header="打标签" class="tag-collapse-panel">
+                <div @click.stop>
+                  <FormItem
+                    label="用户进入当前阶段时,可以打上标签"
+                    name="tagging"
+                    v-if="msgAction"
+                    :labelCol="{ span: 8 }"
+                  >
+                    <!-- :rules="[{ required: true, message: '请选择一个标签', trigger: 'change' },]" -->
+                    <Select
+                      v-model:value="taskForm.tagValue"
+                      :options="actionLabel"
+                      allowClear
+                      mode="multiple"
+                      size="middle"
+                      placeholder="请选择"
+                      :style="{ width: '240px', margin: '0 5px' }"
+                      :max-tag-count="1"
+                    ></Select>
+                  </FormItem>
+                </div>
+              </Collapse.Panel>
+            </Collapse>
+          </Form>
+        </div>
+        <div class="steps-action">
+          <Button v-if="currentStep > 0" @click="prevStep">上一步</Button>
+          <Button type="primary" v-if="currentStep < 1" @click="nextStep">下一步</Button>
+          <Button type="primary" v-if="currentStep === 1" @click="submitForm">完成</Button>
+          <Button @click="handleClose">取消</Button>
+        </div>
+      </Drawer>
+    </div>
+  </Spin>
+</template>
+
+<script setup lang="ts">
+  import { defineProps, unref, onMounted, defineEmits, ref, reactive, toRefs, watch } from 'vue';
+  import {
+    Drawer,
+    Spin,
+    Form,
+    FormItem,
+    Input,
+    Radio,
+    Button,
+    Steps,
+    Step,
+    Collapse,
+    Select,
+  } from 'ant-design-vue';
+  import {
+    InfoCircleOutlined,
+    CommentOutlined,
+    TagsOutlined,
+    CloseCircleOutlined,
+    PlusCircleOutlined,
+  } from '@ant-design/icons-vue';
+  import {
+    addSopStageNode,
+    getSopStageNodeDetail,
+    getSopStageNodeList,
+    getSopStageList,
+    editSopStageNode,
+  } from '@/api/wechat/sopTask';
+  import { useStore } from '../stores/index';
+  let store = useStore();
+  import type { FormInstance } from 'ant-design-vue';
+  // import HtmlTextarea from './htmlTextarea.vue';
+  import NodeMsgContant from './nodeMsgContant.vue';
+  import TimedSending from './timedSending.vue';
+
+  //   name: 'NodeConfigDrawer',
+  const loading = ref(false);
+  const props = defineProps<{
+    visible: boolean;
+    stageId: {
+      type: Number; // 支持 number 和 ref 类型
+      required: true;
+    };
+    nodeId: {
+      type: Number; // 支持 number 和 ref 类型
+      required: false;
+    };
+    isAddNode: {
+      type: Boolean; // 支持 boolean 和 ref 类型
+      required: false;
+    };
+  }>();
+  const emit = defineEmits<{
+    (event: 'update:open', value: boolean): void;
+    (event: 'update:getNodeList', value: { addNode: boolean; nodeId: number; stageId: number }): void;
+  }>();
+
+  // setup(props, { emit }) {
+  const { visible, stageId, nodeId,isAddNode } = toRefs(props);
+  const currentStep = ref(0);
+  const formRef = ref<FormInstance | null>(null);
+  const taskFormRef = ref<FormInstance | null>(null);
+  const dayValue = ref<number>(1);
+  const strValue = ref<string>('09:00:00');
+  // 定义动态组件插槽或具名插槽
+  // const MsgContantSlot = MsgContant;
+  let actionLabel = store.sopLabelList;
+  const form = reactive({
+    nodeName: '', //节点名称
+    conditionType: '1', //触发条件:1为客户回复,2为客户不回复
+    timeout: '',
+    content: 'concreteContent',
+    conditionList: [{ expression: '' }],
+  });
+  const initialForm = reactive({
+    nodeName: '',
+    conditionType: '1',
+    timeout: '',
+    content: 'concreteContent',
+    conditionList: [{ expression: '' }],
+  });
+  const taskForm = reactive({
+    taskType: 'sendMessage',
+    sendType: 'immediate',
+    actionMessage: [{ type: 1, content: '' }],
+    tagValue: [],
+  });
+  const initialTaskForm = reactive({
+    taskType: 'sendMessage',
+    sendType: 'immediate',
+    actionMessage: [{ type: 1, content: '' }],
+    tagValue: [],
+  });
+  const activeKey = ref<number | string>('1');
+  const activeKeyTiem = ref<number | string>('time1');
+  const activeKeyTag = 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);
+
+  onMounted(async () => {
+    console.log('节点中阶段IDstageid', isAddNode.value);
+    if (unref(stageId)) {
+      let res = await getSopStageNodeList({ stageId: unref(stageId) });
+      console.log('节点数组', res.data);
+    }
+    if (isAddNode.value) {
+      console.log('新建----');
+      resetForms();
+    } else {
+      getNodeDetail(unref(nodeId));
+      console.log('编辑----');
+    }
+  });
+  const addContent = () => {
+    form.conditionList.push({ expression: '' });
+  };
+
+  const removeContent = (index: number) => {
+    form.conditionList.splice(index, 1);
+  };
+  function resetForms() {
+    Object.assign(form, initialForm);
+    Object.assign(taskForm, initialTaskForm);
+    currentStep.value = 0;
+  }
+  const nextStep = () => {
+    if (formRef.value) {
+      formRef.value
+        .validateFields(['nodeName'])
+        .then(() => {
+          if (currentStep.value < 1) {
+            currentStep.value += 1;
+          }
+        })
+        .catch((error) => {
+          console.log('Validation Failed:', error);
+        });
+    }
+  };
+
+  const prevStep = () => {
+    if (currentStep.value > 0) {
+      currentStep.value -= 1;
+    }
+  };
+  // const submitForm = () => {
+  //   console.log('提交表单', form, taskForm);
+  //   handleClose();
+  // };
+
+  async function getNodeDetail(id) {
+    // 模拟 API 调用
+    let data = await getSopStageNodeDetail({ id });
+    const conArray = []
+    data.data.conditionList.forEach((item) => {
+      conArray.push({ expression: item });
+    });
+    console.log(conArray);
+    // 填充 form 数据
+    Object.assign(form, {
+      nodeName: data.data.name,
+      conditionType: String(data.data.conditionType),
+      conditionOperator: String(data.data.conditionOperator),
+      conditionList: conArray,
+    });
+    const taskTypes = [];
+    if (data.data.actionMessage.length > 0) {
+      taskTypes.push('sendMessage');
+    }
+    if (data.data.actionLabel.length > 0) {
+      taskTypes.push('tag');
+    }
+    // 填充 taskForm 数据
+    Object.assign(taskForm, {
+      taskType: taskTypes,
+      sendType: 'immediate',
+      actionMessage: data.data.actionMessage || [{ type: 1, content: '' }],
+      tagValue: data.data.actionLabel || [],
+    });
+    // 根据 taskType 设置 isMsgAction 和 isTagAction
+    isMsgAction.value = taskTypes.includes('sendMessage');
+    isTagAction.value = taskTypes.includes('tag');
+  }
+  async function submitForm() {
+    // 提交表单逻辑
+    const { nodeName, conditionType, conditionList } = form;
+    const { taskType, sendType, actionMessage, tagValue } = taskForm;
+    const listCon = conditionList.map((item) => {
+      return item.expression;
+    });
+    const requestData = {
+      name: nodeName,
+      conditionType: ~~conditionType,
+      conditionList: listCon,
+      taskType,
+      sendType,
+      actionMessage,
+      actionLabel: tagValue,
+    };
+    console.log('|||||||||||提交表单', requestData);
+    if (!isAddNode) {
+      // 编辑
+      loading.value = true;
+      console.log('编辑节点');
+      const id = unref(nodeId);
+      let response = await editSopStageNode({ id, ...requestData });
+      if (response && response.code == 0) {
+        console.log(response.code);
+        loading.value = false;
+        // getStageList()
+        emit('update:getNodeList', { addNode: false, nodeId: id, stageId: unref(stageId) });
+        console.log('编辑成功');
+        handleClose();
+      } else {
+        console.error(response.code, '编辑失败');
+      }
+    } else {
+      // 新增
+      console.log('新增节点@@@@',unref(nodeId));
+      loading.value = true;
+      let response = await addSopStageNode({
+        stageId: unref(stageId),
+        parentId: unref(nodeId),
+        ...requestData,
+      });
+      if (response && response.code === 0) {
+        loading.value = false;
+        emit('update:getNodeList', { addNode: true, nodeId: response.data, stageId: unref(stageId) }); //id为当前新增节点的id
+        handleClose();
+      } else {
+        loading.value = false;
+        console.error('新增失败');
+      }
+    }
+  }
+
+  // 打开或关闭抽屉
+  const handleClose = () => {
+    if (visible.value) {
+      resetForms();
+    }
+    emit('update:open', false);
+  };
+
+  // const handleClose = () => {
+  // // 重置表单
+  // Object.assign(form, initialForm);
+  // Object.assign(taskForm, initialTaskForm);
+  // currentStep.value = 0; // 将步骤重置回初始步骤
+  // emit('update:open', false); // 通知父组件关闭抽屉
+  // };
+
+  // const resetForms = () => {
+  //   Object.assign(form, initialForm);
+  //   Object.assign(taskForm, initialTaskForm);
+  //   msgAction.value = 'sendMessage';
+  //   isMsgAction.value = true;
+  //   isTagAction.value = false;
+  //   dayValue.value = 1;
+  //   strValue.value = '09:00:00';
+  //   tagValue.value = '';
+  // };
+
+  // 在打开抽屉时保存初始状态
+  watch(visible, (newVal) => {
+    if (newVal) {
+      Object.assign(initialForm, { ...form });
+      Object.assign(initialTaskForm, { ...taskForm });
+    }
+  });
+
+  // 设置任务类型
+  const setTaskType = (type: string) => {
+    taskForm.taskType = type;
+    if (type === 'tag') {
+      isTagAction.value = !isTagAction.value;
+      // isTagAction.value = type === 'tag';
+      msgAction.value = type === 'tag';
+    } else if (type === 'sendMessage') {
+      msgAction.value = type === 'sendMessage';
+      // isMsgAction.value = type === 'sendMessage';
+      isMsgAction.value = !isMsgAction.value;
+    }
+    // taskForm.taskType = type;
+    // msgAction.value = type === 'sendMessage';
+    // isMsgAction.value = type === 'sendMessage';
+    // isTagAction.value = type === 'tag';
+  };
+  // const setTaskType = (taskType: string) => {
+  // console.log('first',taskType)
+  // 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'] : [];
+  // }
+  // };
+
+  const toggleTimeoutAction = () => {
+    // 切换折叠面板的动作
+    if (taskForm.taskType === 'scheduled') {
+      msgAction.value = !msgAction.value;
+      if (msgAction.value) {
+        activeKeyTiem.value = ['time1'];
+      } else {
+        activeKeyTiem.value = [];
+      }
+    }
+  };
+  // 切换超时动作
+  // const toggleTimeoutAction = () => {
+  //   activeKeyTiem.value = activeKeyTiem.value ? '' : 'time1';
+  // };
+</script>
+<style scoped lang="less">
+  .concrete-content-container {
+    border: 0.5px solid #c0ccda;
+    border-radius: 2px;
+    padding: 16px 16px 16px 16px;
+  }
+  .concrete-content-item {
+    display: flex;
+    align-items: center;
+    margin-bottom: 8px;
+  }
+  .concrete-content-item a-input {
+    flex: 1;
+    margin-right: 8px;
+  }
+  .custom-collapse-tag {
+    background-color: #ecf5fc;
+    color: #333;
+    border-radius: 0px;
+    border: none;
+    margin: 30px 0 90px 0;
+  }
+  .custom-collapse-panel {
+    border: none !important;
+    border-radius: 0px;
+  }
+  .step-content {
+    margin-top: 20px;
+  }
+  .steps-action {
+    margin-top: 20px;
+    display: flex;
+    justify-content: flex-end;
+    gap: 10px;
+  }
+  .step-style {
+    margin: 0 auto;
+    width: 60%;
+  }
+  .custom-step .ant-steps-item-title {
+    font-size: 14px;
+  }
+  .description {
+    width: calc(100% - 90px);
+    margin: 0px 0 30px 20px;
+    font-size: 14px;
+    padding: 10px 15px;
+    color: #666;
+    background-color: #f9f9f9;
+  }
+  .description .anticon {
+    margin-right: 8px;
+  }
+  .btn-style {
+    margin: 0 15px;
+    border-color: #cececd;
+    color: #606266;
+  }
+  .action-btn {
+    margin: 0 15px;
+    color: #4096ff;
+    border-color: #4096ff;
+  }
+  .btn-style:hover {
+    margin: 0 15px;
+    border-color: #cececd;
+    color: #606266;
+  }
+  a-textarea {
+    width: 100%;
+  }
+  .highlighted-text {
+    color: #1c53d9;
+    margin: 0 5px;
+    background-color: #e8edfb;
+    padding: 3px 5px;
+  }
+  .custom-collapse {
+    background-color: #f3faef;
+    color: #333;
+    border-radius: 0px;
+    border: none;
+    margin-top: 20px;
+  }
+  .tag-collapse-panel {
+    border: none;
+  }
+</style>

+ 451 - 0
src/views/wechat/sop_task/add_sop/components/nodeMsgContant.vue

@@ -0,0 +1,451 @@
+<!-- 消息内容 -->
+<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
+            class="custom-textarea"
+            v-model:value="area.content"
+            :rows="8"
+            :maxlength="1000"
+            @focus="handleFocus"
+            @blur="handleBlur"
+          />
+          <!-- <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" />
+        </div>
+      </a-form-item>
+      <a-form-item v-else-if="area.type === 2" label="" name="fileContent" class="message-content">
+        <div class="upload-contant">
+          <a-upload-dragger
+            class="upload"
+            :custom-request="customRequest"
+            :before-upload="beforeUpload"
+            :file-list="fileList"
+            :on-change="handleChange"
+            :on-remove="handleRemove"
+            :multiple="false"
+            :accept="acceptTypes"
+            v-model:file-list="fileList"
+            list-type="picture-card"
+          >
+            <template #default>
+              <div v-if="fileList.length === 0">
+                <CloudUploadOutlined style="font-size: 25px" />
+                <div>
+                  将文件拖到此处,或
+                  <span style="color: #1890ff">点击上传</span>
+                </div>
+                <div>支持上传文件为图片、视频、pdf、word、excel</div>
+              </div>
+            </template>
+          </a-upload-dragger>
+          <!-- @remove="emit('remove')" @moveUp="emit('moveUp')" @moveDown="emit('moveDown')" -->
+          <CustomIcons :index="index" :length="length" />
+        </div>
+      </a-form-item>
+      <!-- 添加类型按钮 -->
+      <div class="message-content-btn">
+        <a-dropdown :trigger="['click']" @click.stop arrow placement="bottomRight">
+          <a-button 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>
+    <!-- 产品笔记 -->
+    <!-- <ProductContant /> -->
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent, ref, reactive, provide, props, watch, computed } 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 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,
+    },
+    // props: {
+    //   actionMessage: Array,
+    // },
+    props: {
+      value: {
+        type: Array,
+        required: true,
+      },
+    },
+    emits: ['update:value'],
+    setup(props, { emit }) {
+      // const internalValue = ref([...props.value]);
+      const internalValue = ref(addCanMoveProperties(props.value));
+      const taskForm = reactive({
+        fileContent: '',
+      });
+
+     
+      watch(
+        () => props.value,
+        (newValue) => {
+          internalValue.value = addCanMoveProperties(newValue);
+        },
+      );
+
+      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 uploadUrl = ref('/upload'); // 上传地址
+      const acceptTypes = ref(
+        'image/*,video/*,application/pdf,application/msword,application/vnd.ms-excel',
+      );
+      const fileList = ref([]);
+
+      const beforeUpload = (file) => {
+        const isAllowedType = /image|video|pdf|msword|vnd.ms-excel/.test(file.type);
+        if (!isAllowedType) {
+          message.error('只能上传图片、视频、PDF、Word、Excel文件');
+        }
+        return isAllowedType;
+      };
+
+      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 customRequest = async ({ file, onSuccess, onError }) => {
+        const formData = new FormData();
+        formData.append('file', file);
+
+        try {
+          const response = await fetch(uploadUrl.value, {
+            method: 'POST',
+            body: formData,
+          });
+
+          if (response.ok) {
+            const result = await response.json();
+            onSuccess(result);
+          } else {
+            onError(new Error('上传失败'));
+          }
+        } catch (error) {
+          onError(error);
+        }
+      };
+
+      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:''}
+      //   ]//命中后发送的消息内容
+      // });
+      console.log(internalValue, '====');
+      // 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,
+          file: '',
+          canMoveUp: internalValue.value.length > 0,
+          canMoveDown: false,
+        });
+
+        // 更新前一个项的 canMoveDown 为 true
+        updateCanMoveProperties();
+      };
+      // 根据传入的类型添加对应的区域
+      const handleCreateClick = (key: string) => {
+        if (key.key === '1') {
+          console.log('first');
+          addTextArea();
+        } else {
+          addFileArea();
+        }
+      };
+
+      const removeItem = (index: number) => {
+        console.log('first', index);
+        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,
+        uploadUrl,
+        acceptTypes,
+        fileList,
+        beforeUpload,
+        handleChange,
+        handleRemove,
+        customRequest,
+        internalValue,
+        updateValue,
+        addCanMoveProperties,
+      };
+    },
+  });
+</script>
+<style scoped lang="less">
+  .ant-form-item {
+    margin-bottom: 0;
+  }
+  .message-content-btn {
+    width: 800px;
+    background-color: #f9f9f9;
+    padding: 10px 30px 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;
+  }
+  .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;
+  }
+</style>

+ 103 - 0
src/views/wechat/sop_task/add_sop/components/productContant.vue

@@ -0,0 +1,103 @@
+<!-- 产品笔记 -->
+<template>
+    <a-form-item label="" name="messageContent" class="message-content">
+      <div style="display: flex; justify-content: space-around;width: 750px;cursor: pointer; ">
+        <div class="product-note-contant" @click="handleOpenNote">
+          <div class="note-data" v-html="noteContent"></div>
+          <div class="image"></div>
+        </div>
+        <CustomIcons />
+      </div>
+      <ProductNote :visible="isNoteVisible" @close="handleNoteClose" @save-content="handleSaveContent" />
+    </a-form-item>
+</template>
+
+<script lang="ts">
+  import { defineComponent, ref, reactive, } from 'vue';
+  import { Form } from 'ant-design-vue';
+  import CustomIcons from './customIcons.vue';
+  import ProductNote from './productNote.vue';
+  export default defineComponent({
+    name: 'ProductContant',
+    components: {
+      'a-form': Form,
+      'a-form-item': Form.Item,
+      CustomIcons,
+      ProductNote,
+    },
+    setup() {
+      const isNoteVisible = ref(false);
+      const noteContent = ref(null);
+      const taskForm = reactive({
+        taskType: 'sendMessage',
+        sendType: 'immediate',
+        scheduledTime: null,
+        messageContent: '',
+        fileContent:'',
+      });
+
+      const handleSaveContent= (content) => {
+        // 假设有一个名为noteContent的数据属性用于保存接收到的内容
+        noteContent.value = content;
+      };
+
+      const handleOpenNote = () => {
+        // 打开产品笔记
+        // this.$emit('openNote');
+        isNoteVisible.value = true;
+      };
+
+      const handleNoteClose = () => {
+        // 关闭产品笔记
+        // this.$emit('closeNote');
+        isNoteVisible.value = false;
+      };
+      return {
+        taskForm,
+        handleOpenNote,
+        handleNoteClose,
+        isNoteVisible,
+        handleSaveContent,
+        noteContent
+      };
+    },
+  });
+</script>
+
+<style scoped lang="less">
+  .ant-form-item {
+    margin-bottom: 0;
+  }
+  .message-content {
+    background-color: #f9f9f9;
+    padding: 10px 30px 10px 30px;
+    display: flex;
+    justify-content: center;
+  }
+  .product-note-contant {
+    width: 600px;
+    white-space: break-spaces;
+    word-break: break-all;
+    background-color: #f6f6fa;
+    line-height: 1.2;
+    border-radius: 4px;
+    padding: 20px;
+    display: flex;
+    justify-content: space-between;
+  }
+  .note-data {
+    max-width: 80%;
+    color: #000;
+    max-height: 92px;
+    font-weight: 700 !important;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    overflow: hidden;
+  }
+  .image {
+    width: 92px;
+    height: 92px;
+    background-image: url(../static/note.png);
+    background-size: 100% 100%;
+  }
+</style>

+ 442 - 0
src/views/wechat/sop_task/add_sop/components/productNote.vue

@@ -0,0 +1,442 @@
+<template>
+  <a-modal v-model:open="visible" title="产品笔记" width="700px" :body-style="{ height: '600px' }" @ok="handleSave"
+    @cancel="handleCancel">
+    <div :class="prefixCls"
+      :style="{ width: '700px', height: '600px',display: 'flex', justifyContent: 'center', }">
+      <!-- <ImgUpload :fullscreen="fullscreen" @uploading="handleImageUploading" @done="handleDone" v-if="showImageUpload"
+      v-show="editorRef" :disabled="disabled" /> -->
+      <textarea :id="tinymceId" ref="elRef" :style="{ visibility: 'hidden', height: '600px', }"
+        v-if="!initOptions.inline"></textarea>
+      <slot v-else></slot>
+    </div>
+  </a-modal>
+
+</template>
+
+<script lang="ts">
+import type { Editor, RawEditorOptions } from 'tinymce';
+import { Modal } from 'ant-design-vue';
+import tinymce from 'tinymce/tinymce';
+import 'tinymce/themes/silver';
+import 'tinymce/icons/default/icons';
+import 'tinymce/plugins/lists';
+import 'tinymce/plugins/advlist';
+import 'tinymce/plugins/autolink';
+import 'tinymce/plugins/link';
+import 'tinymce/plugins/table'
+import 'tinymce/plugins/emoticons';
+import 'tinymce/plugins/image';
+
+import { defineComponent, computed, nextTick, ref, toRefs, unref,watch, watchEffect, onDeactivated, onBeforeUnmount,} from 'vue';
+// import ImgUpload from './ImgUpload.vue';
+import { plugins } from './tinymce';
+import { buildShortUUID } from '@/utils/uuid';
+import { onMountedOrActivated } from '@vben/hooks';
+import { useDesign } from '@/hooks/web/useDesign';
+// import { isNumber } from '@/utils/is';
+import { useLocale } from '@/locales/useLocale';
+import { useAppStore } from '@/store/modules/app';
+
+export default defineComponent({
+  name: 'Tinymce',
+  components: {
+    'a-modal': Modal,
+  },
+  inheritAttrs: false,
+  // props: { tinymceProps, visible: Boolean },
+  props: {
+    visible: Boolean,
+    tinymceProps: {
+      type: Object as PropType<{
+        height: number;
+        width: number;
+        toolbar: string;
+        plugins: string;
+        options: any;
+        modelValue: string;
+        // 在这里定义tinymceProps对象的其他属性
+      }>,
+      default: () => ({
+        // height: 600, // 提供默认值
+        width: 700, // 提供默认值
+        toolbar: 'undo redo | bold italic underline | forecolor | bullist numlist | hr | emoticons',
+        plugins: plugins,
+        options: {},
+        modelValue: '',
+        // 提供其他属性的默认值
+      })
+    }
+  },
+  emits: ['change', 'update:modelValue', 'inited', 'init-error', 'close', 'save-content'],
+  setup(props, { emit, attrs }) {
+    const originalContent = ref(props.tinymceProps.modelValue)//初始内容
+    const { visible } = toRefs(props); // 控制弹窗的显示隐藏
+    const editorRef = ref<Nullable<Editor>>(null);
+    // const fullscreen = ref(false);
+    const tinymceId = ref<string>(buildShortUUID('tiny-vue'));
+    // const elRef = ref<Nullable<HTMLElement>>(null);
+    const elRef = ref<HTMLElement | null>(null);
+
+    watchEffect(() => {
+      if (visible.value) {
+        // 当visible变为true时确保编辑器初始化
+        initEditor();
+      }
+    });
+
+    watch(() => props.tinymceProps.modelValue, (newVal) => {
+      originalContent.value = newVal;
+      if (editorRef.value) {
+        editorRef.value.setContent(newVal);
+      }
+    });
+
+    const handleSave = () => {
+      const currentContent = editorRef.value?.getContent();
+      console.log('内容保存:', editorContent.value, currentContent);
+      // 更新原始内容为当前内容
+      originalContent.value = currentContent || '';
+      emit('save-content', currentContent);
+      emit('close');
+    };
+
+    const handleCancel = () => {
+      Modal.confirm({
+        title: '提示',
+        content: '关闭后所有新建内容将会清空,确定关闭吗?',
+        okText: '确定',
+        cancelText: '取消',
+        onOk: () => {
+          if (editorRef.value) {
+            // 重置编辑器内容为原始内容
+            editorRef.value.setContent(originalContent.value || '');
+          }
+          emit('close');
+        },
+        onCancel() {
+          console.log('取消操作,继续编辑');
+        },
+      });
+      // if (editorRef.value) {
+      //   // 重置编辑器内容为原始内容
+      //   editorRef.value.setContent(originalContent.value);
+      // }
+      // emit('close');
+    };
+
+    const { prefixCls } = useDesign('tinymce-container');
+
+    const appStore = useAppStore();
+
+    const tinymceContent = computed(() => props.tinymceProps.modelValue);
+
+    // const containerWidth = computed(() => {
+    //   const width = props.width;
+    //   if (isNumber(width)) {
+    //     return `${width}px`;
+    //   }
+    //   return width;
+    // });
+
+    const skinName = computed(() => {
+      return appStore.getDarkMode === 'light' ? 'oxide' : 'oxide-dark';
+    });
+
+    const contentCssName = computed(() => {
+      return appStore.getDarkMode === 'light' ? 'default' : 'dark';
+    });
+
+    const langName = computed(() => {
+      const lang = useLocale().getLocale.value;
+      if (lang === 'zh_CN') {
+        return 'zh-Hans';
+      }
+      return lang;
+    });
+
+    const initOptions = computed((): RawEditorOptions => {
+      const { options, toolbar, plugins } = props.tinymceProps;
+      const publicPath = import.meta.env.VITE_PUBLIC_PATH || '/';
+      return {
+        selector: `#${unref(tinymceId)}`,
+        // height,
+        toolbar,
+        menubar: false,
+        plugins,
+        language_url: publicPath + 'resource/tinymce/langs/' + langName.value + '.js',
+        language: langName.value,
+        branding: false,
+        default_link_target: '_blank',
+        link_title: false,
+        object_resizing: false,
+        auto_focus: true,
+        skin: skinName.value,
+        promotion: false,
+        model_url: publicPath + 'resource/tinymce/models/dom/model.min.js',
+        skin_url: publicPath + 'resource/tinymce/skins/ui/' + skinName.value,
+        content_css:
+          publicPath +
+          'resource/tinymce/skins/content/' +
+          contentCssName.value +
+          '/content.min.css',
+        ...options,
+        elementpath: false, // 禁用路径条
+        // setup: (editor: Editor) => {
+        //   editorRef.value = editor;
+        //   editor.on('init', (e) => initSetup(e));
+        // },
+        setup: (editor: Editor) => {
+          editorRef.value = editor;
+          editor.on('init', (e) => initSetup(e));
+
+          // 监听多种可能变更内容的事件
+          editor.on('change keyup undo redo', () => {
+            emit('update:modelValue', editor.getContent());
+          });
+        },
+      };
+    });
+
+    // 监听编辑器内容变化事件
+    // editorRef.value?.on('change', () => {
+    //   const content = editorRef.value?.getContent({ format: attrs.outputFormat });
+    //   console.log('first', editorRef)
+    //   // emit('update:modelValue', content);
+    //   // emit('change', content);
+    // });
+    const editorContent = computed(() => editorRef.value?.getContent());
+    // console.log('first=============', editorRef)
+
+    const disabled = computed(() => {
+      const { options } = props.tinymceProps;
+      const getdDisabled = options && Reflect.get(options, 'readonly');
+      const editor = unref(editorRef);
+      if (editor) {
+        editor.mode.set(getdDisabled ? 'readonly' : 'design');
+      }
+      return getdDisabled ?? false;
+    });
+
+    // watch(
+    //   () => attrs.disabled,
+    //   () => {
+    //     const editor = unref(editorRef);
+    //     if (!editor) {
+    //       return;
+    //     }
+    //     editor.mode.set(attrs.disabled ? 'readonly' : 'design');
+    //   },
+    // );
+
+    onMountedOrActivated(() => {
+      if (!initOptions.value.inline) {
+        tinymceId.value = buildShortUUID('tiny-vue');
+      }
+      nextTick(() => {
+        setTimeout(() => {
+          initEditor();
+        }, 30);
+      });
+    });
+
+    // onBeforeUnmount(() => {
+    //   destory();
+    // });
+    onBeforeUnmount(() => {
+      if (editorRef.value) {
+        editorRef.value.remove();
+        editorRef.value = null;
+      }
+    });
+
+    onDeactivated(() => {
+      destory();
+    });
+
+    function destory() {
+      if (tinymce !== null) {
+        tinymce?.remove?.(unref(initOptions).selector!);
+      }
+    }
+
+    function initEditor() {
+      const el = unref(elRef);
+      if (el) {
+        el.style.visibility = '';
+      }
+      tinymce
+        .init(unref(initOptions))
+        .then((editor) => {
+          emit('inited', editor);
+        })
+        .catch((err) => {
+          emit('init-error', err);
+        });
+    }
+
+    function initSetup(e) {
+      const editor = unref(editorRef);
+      if (!editor) {
+        return;
+      }
+      const value = props.tinymceProps.modelValue || '';
+
+      editor.setContent(value);
+      bindModelHandlers(editor);
+      // bindHandlers(e, attrs, unref(editorRef));
+    }
+
+    function setValue(editor: Recordable, val?: string, prevVal?: string) {
+      if (
+        editor &&
+        typeof val === 'string' &&
+        val !== prevVal &&
+        val !== editor.getContent({ format: attrs.outputFormat })
+      ) {
+        editor.setContent(val);
+      }
+    }
+
+    function bindModelHandlers(editor: any) {
+      const modelEvents = attrs.modelEvents ? attrs.modelEvents : null;
+      const normalizedEvents = Array.isArray(modelEvents) ? modelEvents.join(' ') : modelEvents;
+
+      // watch(
+      //   () => props.modelValue as string,
+      //   (val, prevVal) => {
+      //     setValue(editor, val, prevVal);
+      //   },
+      // );
+
+      // watch(
+      //   () => props.value as string,
+      //   (val, prevVal) => {
+      //     setValue(editor, val, prevVal);
+      //   },
+      //   {
+      //     immediate: true,
+      //   },
+      // );
+
+      editor.on(normalizedEvents ? normalizedEvents : 'change keyup undo redo', () => {
+        const content = editor.getContent({ format: attrs.outputFormat });
+        emit('update:modelValue', content);
+        emit('change', content);
+      });
+
+      // editor.on('FullscreenStateChanged', (e) => {
+      //   fullscreen.value = e.state;
+      // });
+    }
+
+    function handleImageUploading(name: string) {
+      const editor = unref(editorRef);
+      if (!editor) {
+        return;
+      }
+      editor.execCommand('mceInsertContent', false, getUploadingImgName(name));
+      const content = editor?.getContent() ?? '';
+      setValue(editor, content);
+    }
+
+    function handleDone(name: string, url: string) {
+      const editor = unref(editorRef);
+      if (!editor) {
+        return;
+      }
+      const content = editor?.getContent() ?? '';
+      const val = content?.replace(getUploadingImgName(name), `<img src="${url}"/>`) ?? '';
+      setValue(editor, val);
+    }
+
+    function getUploadingImgName(name: string) {
+      return `[uploading:${name}]`;
+    }
+
+    return {
+      prefixCls,
+      // containerWidth,
+      initOptions,
+      tinymceContent,
+      elRef,
+      tinymceId,
+      handleImageUploading,
+      handleDone,
+      editorRef,
+      // fullscreen,
+      disabled,
+      visible,
+      handleSave,
+      handleCancel,
+      editorContent,
+      originalContent
+    };
+  },
+});
+
+// const tinymceProps = {
+//   options: {
+//     type: Object as PropType<Partial<RawEditorOptions>>,
+//     default: () => ({}),
+//   },
+//   value: {
+//     type: String,
+//   },
+
+//   // toolbar: {
+//   //   type: Array as PropType<string[]>,
+//   //   default: toolbar,
+//   // },
+//   toolbar: {
+//     type: Array as PropType<string[]>,
+//     default: () => [
+//       'undo redo | bold italic underline | forecolor | bullist numlist | hr | emoticons'
+//     ],
+//   },
+//   plugins: {
+//     type: Array as PropType<string[]>,
+//     default: plugins,
+//   },
+//   //  plugins: {
+//   //   type: Array as PropType<string[]>,
+//   //   default: () => [
+//   //     'advlist autolink lists link pagebreak',
+//   //   ],
+//   // },
+//   modelValue: {
+//     type: String,
+//   },
+//   height: {
+//     type: [Number, String] as PropType<string | number>,
+//     required: false,
+//     default: 600,
+//   },
+//   width: {
+//     type: [Number, String] as PropType<string | number>,
+//     required: false,
+//     default:700,
+//   },
+//   showImageUpload: {
+//     type: Boolean,
+//     default: false,
+//   },
+// };
+
+
+</script>
+
+<style lang="less" scoped></style>
+
+<style scoped lang="less">
+@prefix-cls: ~'@{namespace}-tinymce-container';
+
+.@{prefix-cls} {
+  position: relative;
+  line-height: normal;
+  height: 600px;
+  textarea {
+    visibility: hidden;
+    z-index: -1;
+  }
+}
+</style>

+ 511 - 0
src/views/wechat/sop_task/add_sop/components/sopTaskName.vue

@@ -0,0 +1,511 @@
+<template>
+  <div class="container">
+    <!-- @submit="handleSubmit" -->
+    <Form class="contant" :model="form" layout="inline" >
+      <FormItem label="SOP名称" :rules="[{ required: true, message: '请输入任务名称' }]">
+        <Input v-model:value="form.sopName" placeholder="请输入任务名称" @blur="handleInputBlur"/>
+      </FormItem>
+      <FormItem label="有效期">
+        <Radio.Group v-model:value="form.validity" @change="handleValidityChange">
+          <Radio value="permanent">不限</Radio>
+          <Radio value="temporary">时间段</Radio>
+        </Radio.Group>
+      </FormItem>
+      <template v-if="form.validity === 'temporary'">
+        <FormItem>
+          <DatePicker.RangePicker v-model:value="form.dateRange" :disabled-date="disabledDate" @change="handleDateChange"/>
+        </FormItem>
+      </template>
+      <FormItem>
+        <Button @click="handleAdminConfig">配置社交账号</Button>
+        <Button disabled>暂存草稿</Button>
+        <Button type="primary" html-type="submit" @click="handlePublish">发布</Button>
+      </FormItem>
+    </Form>
+
+    <!-- 配置社交账号弹窗 -->
+    <Modal
+      width="1000px"
+      v-model:open="configModalVisible"
+      title="选择AI员工"
+      @ok="handleConfigOk"
+      @cancel="handleConfigCancel"
+      class="custom-modal"
+    >
+      <div>
+        <Alert
+          class="tips-style"
+          v-if="alertVisible"
+          message="若AI员工离线,则无法执行SOP任务;同一客户同时存在多个AI员工账号内,则优先跟进AI员工执行SOP任务;若未指定跟进AI员工,则与该客户有聊天的AI员工执行SOP任务。"
+          type="info"
+          show-icon
+          closable
+          @close="handleAlertClose"
+        />
+        
+        <div class="table-style">
+          <Table
+            :columns="columns"
+            :data-source="dataSource"
+            rowKey="id"
+            :loading="loading"
+            :pagination="pagination"
+            :row-selection="rowSelection"
+            @change="handleTableChange"
+
+          >
+            <template #bodyCell="{ column, record }">
+              <template v-if="column.key === 'actions'">
+                <Popconfirm
+                  title="确定要设定发送人吗?"
+                  @confirm="() => confirmSend(record.id)"
+                  @cancel="handleCancel"
+                >
+                  <Button type="link" v-if="!senderStates[record.id]">设定发送人</Button>
+                </Popconfirm>
+                <Popconfirm
+                  title="确定要取消设置吗?"
+                  @confirm="() => confirmCancel(record.id)"
+                  @cancel="handleCancel"
+                >
+                  <Button type="link" v-if="senderStates[record.id]">取消设置</Button>
+                </Popconfirm>
+              </template>
+            </template>
+            <!-- <template #bodyCell="{ column, record }">
+              <template v-if="column.key === 'actions'">
+                <a-popconfirm v-if="selectedRowKeys.value.includes(record.id)" title="确定要取消设置吗?"
+                  @confirm="() => confirmCancel(record.id)" @cancel="handleCancel">
+                  <Button type="link">取消设置</Button>
+                </a-popconfirm>
+                <a-popconfirm v-else title="确定要设定发送人吗?" @confirm="() => confirmSend(record.id)" @cancel="handleCancel">
+                  <Button type="link">设定发送人</Button>
+                </a-popconfirm>
+              </template>
+            </template> -->
+          </Table>
+        </div>
+      </div>
+    </Modal>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { reactive, ref, computed, watch, h, onMounted } from 'vue';
+  import { createSopTask, updateSopTask, getSopStageDetailById ,publishSopTask} from '@/api/wechat/sopTask';
+  import { WechatOutlined } from '@ant-design/icons-vue';
+  import { getWxList } from '@/api/wechat/wx';
+import { useRoute } from 'vue-router';
+  import debounce from 'lodash-es/debounce';
+  import { useGo } from "/@/hooks/web/usePage";
+import { PageEnum } from '@/enums/pageEnum';
+  import dayjs from 'dayjs';
+  import {
+    Form,
+    Input,
+    FormItem,
+    Radio,
+    Button,
+    DatePicker,
+    Modal,
+    Alert,
+    Table,
+    Popconfirm,
+  } from 'ant-design-vue';
+  import { useStore } from '../stores/index';
+
+  const route = useRoute();
+  const form = reactive({
+    sopName: '',
+    validity: 'temporary',
+    dateRange: [undefined, undefined] as [dayjs.Dayjs | undefined, dayjs.Dayjs | undefined],
+  });
+  const disabledDate = (current) => {
+    // 禁用今天以前的日期
+    return current && current < new Date().setHours(0, 0, 0, 0);
+  };
+  // const handleInput = (value) => {
+  //     console.log('Input event triggered:', value);
+  //     // 处理输入事件,比如发送请求
+  //   };
+  // const debouncedInput = debounce(handleInput, 300);
+  // onMounted(async () => {
+  //   const task_id = route.query.task_id;
+  //   if (task_id) {
+  //     try {
+  //       let res = await getSopStageDetailById({ id: ~~task_id });
+  //       console.log(res.data);
+  //       const data = res.data;
+  //       form.sopName = data.name || '';
+  //       // format('YYYY-MM-DD')
+  //       form.dateRange = [
+  //         data.createdAt ? dayjs(data.createdAt) : undefined,
+  //         data.updatedAt ? dayjs(data.updatedAt) : undefined,
+  //       ];
+  //       form.validity = data.createdAt && data.updatedAt ? 'temporary' : 'permanent';
+  //       setSopTaskId(data.taskId);
+  //     } catch (error) {
+  //       console.error('Error fetching SOP stage detail:', error);
+  //     }
+  //   }
+  // });
+  const dataSource = reactive<[]>([]);
+  const store = useStore();
+  let { setSopNameAndTime, setSopTaskId ,setSopTaskDtat} = store;
+  const configModalVisible = ref(false);
+  const alertVisible = ref(true);
+  // const selectedRowKeys = ref([]);
+  // 每行的发送人状态
+  // const senderStates = reactive({});
+  const selectedRowKeys = ref<number[]>([]); // 存储选中的行的 key
+  const senderStates = reactive<Record<number, boolean>>({}); // 每行的发送人状态
+  const loading = ref(false);
+  const taskId = ref(undefined); //任务ID,添加成功后接口会返回
+const go = useGo();
+  const pagination = reactive({
+    total: 0,
+    pageSize: 10,
+    current: 1,
+  });
+
+  const fetchDataSource = async () => {
+    try {
+      loading.value = true;
+      const res = await getWxList({ page: 1, pageSize: 50 });
+      if (res.code == 0) {
+        loading.value = false;
+        // 清空 dataSource 数组
+        dataSource.length = 0;
+        // 使用 push 方法将新数据添加到 dataSource 数组中
+        dataSource.push(...res.data.data);
+      }
+    } catch (error) {
+      console.error('Error fetching data source:', error);
+    } finally {
+      loading.value = false; // 请求结束后设置 loading 为 false
+    }
+  };
+
+  const handleTableChange = (newPagination) => {
+    pagination.current = newPagination.current;
+    pagination.pageSize = newPagination.pageSize;
+    fetchDataSource();
+  };
+
+  const columns = [
+    {
+      title: '序号',
+      dataIndex: 'index',
+      customRender: ({ index }) => {
+        return index + 1;
+      },
+    },
+    {
+      title: '社交头像',
+      dataIndex: 'headBig',
+      key: 'headBig',
+      customRender: ({ record }) => {
+        return h('img', { src: record.headBig, width: 30 });
+      },
+    },
+    {
+      title: '社交昵称',
+      dataIndex: 'nickname',
+      key: 'nickname',
+      customRender: ({ record }) => {
+        return h('span', [
+          h(WechatOutlined, {
+            style: { marginRight: '8px', fontSize: '20px', color: '#00BB29' },
+          }), // 使用微信图标
+          record.nickname,
+          h(
+            'span',
+            {
+              style:
+                'margin-left: 8px; color: #13ce66;background-color: #e7faf0;display:inline-block;border:1px solid #d0f5e0;line-height:22px;padding: 0px 8px;border-radius: 4px;font-size: 12px;',
+            },
+            '在线',
+          ),
+        ]);
+      },
+    },
+    {
+      title: '社交账号',
+      dataIndex: 'account',
+      key: 'account',
+    },
+    {
+      title: '操作',
+      key: 'actions',
+    },
+  ];
+
+  async function handlePublish() {
+    
+    if (store.sopNameAndTimeVilidate) {
+      configModalVisible.value = true;
+      // let res = await publishSopTask({id:~~taskId.value})
+      // console.log('发布任务',res)
+    } else {
+      Modal.confirm({
+        title: '提示',
+        content: '请先填写SOP名称或选择完时间段,才能继续完成以下操作!',
+        okText: '确定',
+        okType: 'danger',
+        cancelText: '取消',
+        onOk() {},
+        onCancel() {
+          // 取消按钮的回调:什么也不做
+          console.log('Cancel');
+        },
+      });
+    }
+  }
+  // const rowSelection = ref({
+  //   onChange: (selectedKeys) => {
+  //     // console.log('first', selectedKeys[0])
+  //     // if (selectedKeys.length > 0) {
+  //     //   senderStates[selectedKeys[0]] = true;
+  //     // } else {
+  //     //   senderStates[selectedKeys[0]] = false;
+  //     // }
+  //     selectedRowKeys.value = selectedKeys;
+  //     // 或者你可以在这里处理更多逻辑
+  //   },
+  // });
+  const rowSelection = ref({
+    selectedRowKeys: selectedRowKeys.value,
+    onChange: (selectedKeys: number[]) => {
+      selectedRowKeys.value = selectedKeys;
+    },
+  });
+
+const handleInputBlur = async () => {
+    // if (form.sopName) {
+      await updateSopTaskDetails();
+    // }
+  };
+
+  const handleValidityChange = async () => {
+    await updateSopTaskDetails();
+  };
+
+  const handleDateChange = async () => {
+    await updateSopTaskDetails();
+  };
+
+const updateSopTaskDetails = async () => {
+    // if ((form.sopName && form.validity === 'permanent' )|| (form.sopName && form.dateRange[0] )) {
+      if(form.sopName && (form.validity === 'permanent' || (form.dateRange &&form.dateRange[0] && form.dateRange[1]))){
+      const startDate = form.dateRange[0]?.isValid() ? form.dateRange[0].valueOf() : undefined;
+      const endDate = form.dateRange[1]?.isValid() ? form.dateRange[1].valueOf() : undefined;
+      setSopNameAndTime(true);
+      if (store.sopTaskId !== undefined) {
+        console.log('更新Task=====');
+        try {
+          await updateSopTask({
+            id: store.sopTaskId,
+            name: form.sopName,
+            planStartTime: startDate,
+            planEndTime: endDate,
+            type: 1,
+          });
+        } catch (error) {
+          console.error('Error updating task:', error);
+        }
+      } else {
+        try {
+          let res = await createSopTask({
+            name: form.sopName,
+            planStartTime: startDate,
+            planEndTime: endDate,
+            type: 1,
+          });
+          if (res.code == 0) {
+            setSopTaskId(res.data);
+          }
+        } catch (error) {
+          console.error('Error creating task:', error);
+        }
+      }
+    } else {
+      setSopNameAndTime(false);
+    }
+  };
+
+  onMounted(async () => {
+    const task_id = route.query.task_id;
+    taskId.value = task_id;
+    if (task_id) {
+      try {
+        let res = await getSopStageDetailById({ id: ~~task_id });
+        if (res.code == 0) { 
+           const data = res.data;
+        setSopTaskDtat(res.data)
+         Object.assign(form, {
+          sopName: data.name,
+          dateRange: [
+          data.createdAt ? dayjs(data.createdAt) : undefined,
+          data.updatedAt ? dayjs(data.updatedAt) : undefined,
+        ],
+          validity: data.createdAt && data.updatedAt ? 'temporary' : 'permanent',
+        });
+        setSopTaskId(data.id);
+        setSopNameAndTime(true);
+        }
+       
+      } catch (error) {
+        console.error('Error fetching SOP stage detail:', error);
+      }
+    }
+  });
+
+  async function handleAdminConfig() {
+    if (store.sopNameAndTimeVilidate) {
+      configModalVisible.value = true;
+      fetchDataSource();
+    } else {
+      Modal.confirm({
+        title: '提示',
+        content: '请先填写SOP名称或选择完时间段,才能继续完成以下操作!',
+        okText: '确定',
+        okType: 'danger',
+        cancelText: '取消',
+        onOk() {},
+        onCancel() {
+          // 取消按钮的回调:什么也不做
+          console.log('Cancel');
+        },
+      });
+    }
+  }
+
+  const handleAlertClose = () => {
+    alertVisible.value = false;
+  };
+
+  const confirmSend = (id) => {
+    senderStates[id] = true;
+    selectedRowKeys.value.push(id); // 设置发送人时勾选该行
+    console.log(`Confirmed sending to ID: ${id}`);
+  };
+
+  const confirmCancel = (id) => {
+    senderStates[id] = false;
+    selectedRowKeys.value = selectedRowKeys.value.filter((key) => key !== id); // 取消发送人时取消勾选该行
+    console.log(`Cancelled sending to ID: ${id}`);
+  };
+
+  const handleCancel = () => {
+    // 可以在这里处理取消逻辑
+  };
+
+  async function handleConfigOk() {
+    const selectedItems = dataSource.filter((item) => selectedRowKeys.value.includes(item.id));
+    const botWxidList = selectedItems.map((item) => item.wxid);
+    console.log('Selected items:', botWxidList);
+    const startDate = form.dateRange[0]?.isValid() ? form.dateRange[0].valueOf() : undefined;
+    const endDate = form.dateRange[1]?.isValid() ? form.dateRange[1].valueOf() : undefined;
+    await updateSopTask({
+      id: store.sopTaskId,
+      name: form.sopName,
+      planStartTime: startDate,
+      planEndTime: endDate,
+      type: 1,
+      botWxidList,
+    });
+    let res = await publishSopTask({id:store.sopTaskId})
+      console.log('发布任务',res)
+      if(res.code===0){
+        go(PageEnum.SOP_LIST);
+      }
+    configModalVisible.value = false;
+  }
+
+  const handleConfigCancel = () => {
+    configModalVisible.value = false;
+  };
+
+  const handleSubmit = () => {
+    if (store.sopNameAndTimeVilidate) {
+      configModalVisible.value = true;
+    } else {
+      Modal.confirm({
+        title: '提示',
+        content: '请先填写SOP名称或选择完时间段,才能继续完成以下操作!',
+        okText: '确定',
+        okType: 'danger',
+        cancelText: '取消',
+        onOk() {},
+        onCancel() {
+          // 取消按钮的回调:什么也不做
+          console.log('Cancel');
+        },
+      });
+    }
+  };
+</script>
+
+<style scoped lang="less">
+  .custom-footer {
+    display: flex;
+    justify-content: flex-end;
+    padding: 8px;
+  }
+
+  .confirm-button {
+    background-color: #1890ff;
+    color: white;
+    border: none;
+    padding: 6px 12px;
+    cursor: pointer;
+    border-radius: 4px;
+  }
+
+  .confirm-button:hover {
+    background-color: #40a9ff;
+  }
+  .container {
+    display: inline;
+  }
+
+  .contant {
+    margin: 0 10px;
+    padding: 20px 10px;
+    background: #fff;
+    width: calc(100% - 20px);
+  }
+
+  .sop-form {
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
+  }
+
+  .table-style {
+    margin: 10px 15px;
+  }
+
+  .btn-del {
+    margin: 0px 15px;
+  }
+
+  .modal-style {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  .sop-form .ant-form-item {
+    margin-bottom: 0;
+  }
+
+  .tips-style {
+    margin: 10px 15px;
+  }
+
+  .sop-form span {
+    line-height: 32px;
+    margin: 0 10px;
+  }
+</style>

+ 166 - 0
src/views/wechat/sop_task/add_sop/components/timedSending.vue

@@ -0,0 +1,166 @@
+<template>
+  <a-collapse
+    v-model:activeKey="activeKeyTiem"
+    collapsible="header"
+    :bordered="false"
+    class="time-collapse"
+  >
+    <a-collapse-panel key="time1" class="custom-collapse-panel">
+      <template #header>
+        <div @click.stop="toggleTimeoutAction" style="display: flex; align-items: center">
+          <MinusCircleOutlined />
+          &nbsp;&nbsp;
+          <span>进入后, 第</span>
+          &nbsp;&nbsp;
+          <a-input-number id="inputNumber" v-model:value="dayValue" :min="1" :max="10">
+            <template #upIcon>
+              <ArrowUpOutlined />
+            </template>
+            <template #downIcon>
+              <ArrowDownOutlined />
+            </template>
+          </a-input-number>
+          &nbsp;&nbsp;
+          <span>天,</span>
+          &nbsp;&nbsp;
+          <a-time-picker
+            v-model:value="strValue"
+            placeholder="选择开始时间(时分秒)"
+            value-format="HH:mm:ss"
+          />
+          &nbsp;&nbsp;
+          <span>发送</span>
+          &nbsp;&nbsp;
+          <a-tooltip>
+            <template #title>注: 第一天指当天</template>
+            <QuestionCircleOutlined />
+          </a-tooltip>
+        </div>
+      </template>
+      <a-divider style="border-color: #828eaf; width: 750px" dashed />
+      <component :is="MsgContant" />
+    </a-collapse-panel>
+  </a-collapse>
+</template>
+
+<script lang="ts">
+  import { defineComponent, ref, reactive, toRefs, watch } from 'vue';
+  import {
+    Input,
+    InputNumber,
+    Divider,
+    Collapse,
+    Tooltip,
+    DatePicker,
+  } from 'ant-design-vue';
+  import {
+    InfoCircleOutlined,
+    CommentOutlined,
+    TagsOutlined,
+    MinusCircleOutlined,
+    ArrowUpOutlined,
+    ArrowDownOutlined,
+    QuestionCircleOutlined,
+  } from '@ant-design/icons-vue';
+  import type { FormInstance } from 'ant-design-vue';
+  // import HtmlTextarea from './htmlTextarea.vue';
+  import MsgContant from './msgContant.vue';
+  interface SelectOption {
+    label: string;
+    value: string;
+  }
+
+  export default defineComponent({
+    name: 'TimedSending',
+    components: {
+      'a-input': Input,
+      'a-input-number': InputNumber,
+      'a-collapse': Collapse,
+      'a-collapse-panel': Collapse.Panel,
+      'a-tooltip': Tooltip,
+      'a-divider': Divider,
+      'a-time-picker': DatePicker.TimePicker,
+      ArrowUpOutlined,
+      ArrowDownOutlined,
+      MinusCircleOutlined,
+      QuestionCircleOutlined,
+      // HtmlTextarea,
+      MsgContant,
+    },
+
+    setup(props, { emit }) {
+      const dayValue = ref<number>(1);
+      const strValue = ref<string>('09:00:00');
+      // 定义动态组件插槽或具名插槽
+      // const MsgContantSlot = MsgContant;
+      // 定义选项数组
+      
+      // const form = reactive({
+      //   phaseName: '',
+      //   condition: 'tags',
+      //   conditionRelation: 'all',
+      // });
+      // const initialForm = reactive({
+      //   phaseName: '',
+      //   condition: 'tags',
+      //   conditionRelation: 'all',
+      // });
+      const taskForm = reactive({
+        taskType: 'sendMessage',
+        sendType: 'immediate',
+      });
+      // const initialTaskForm = reactive({
+      //   taskType: 'sendMessage',
+      //   sendType: 'immediate',
+      // });
+      const activeKey = ref<number | string>('1');
+      const activeKeyTiem = ref<number | string>('time1');
+      const msgAction = ref(taskForm.taskType === 'sendMessage');
+
+      
+      const toggleTimeoutAction = () => {
+        // 切换折叠面板的动作
+        if (taskForm.taskType === 'scheduled') {
+          msgAction.value = !msgAction.value;
+          if (msgAction.value) {
+            activeKeyTiem.value = ['time1'];
+          } else {
+            activeKeyTiem.value = [];
+          }
+        }
+      };
+      // 切换超时动作
+      // const toggleTimeoutAction = () => {
+      //   activeKeyTiem.value = activeKeyTiem.value ? '' : 'time1';
+      // };
+
+      return {
+        taskForm,
+        activeKey,
+        activeKeyTiem,
+        dayValue,
+        strValue,
+        msgAction,
+        toggleTimeoutAction,
+        MsgContant,
+        // MsgContantSlot,
+      };
+    },
+  });
+</script>
+<style scoped lang="less">
+  .custom-collapse {
+    background-color: #f3faef;
+    color: #333;
+    border-radius: 0px;
+    border: none;
+    margin-top: 20px;
+  }
+  .time-collapse {
+    width: 100%;
+    background-color: #f9f8f8;
+  }
+  .custom-collapse-panel {
+    border: none;
+  }
+</style>

+ 30 - 0
src/views/wechat/sop_task/add_sop/components/tinymce.ts

@@ -0,0 +1,30 @@
+export const plugins = [
+  'advlist',
+  'anchor',
+  'autolink',
+  'autosave',
+  'autoresize',
+  'code',
+  'codesample',
+  'directionality',
+  'fullscreen',
+  'insertdatetime',
+  'link',
+  'lists',
+  'media',
+  'nonbreaking',
+  'pagebreak',
+  'preview',
+  'save',
+  'searchreplace',
+  'visualblocks',
+  'visualchars',
+  'wordcount',
+  'image',
+  'emoticons'
+];
+
+export const toolbar = [
+  'fontsizeselect lineheight searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent  blockquote undo redo removeformat subscript superscript code codesample',
+  'hr bullist numlist link  preview anchor pagebreak insertdatetime media image forecolor backcolor fullscreen emoticons',
+];

+ 618 - 0
src/views/wechat/sop_task/add_sop/components/userPhasesDrawer.vue

@@ -0,0 +1,618 @@
+<template>
+  <Spin class="loading-style" :spinning="loading" tip="Loading...">
+    <div v-if="!loading">
+      <!--  v-model="visible"-->
+      <Drawer :open="visible" title="用户阶段设置" width="900" @close="handleClose">
+        <Steps :current="currentStep" size="default" class="step-style">
+          <Step class="custom-step" title="设置阶段信息"></Step>
+          <Step class="custom-step" title="配置阶段任务"></Step>
+        </Steps>
+        <div v-if="currentStep === 0" class="step-content">
+          <Form :model="form" :label-col="{ span: 3 }" :wrapper-col="{ span: 19 }" ref="formRef">
+            <FormItem
+              label="阶段名称"
+              name="phaseName"
+              :rules="[{ required: true, message: '请输入阶段名称' }]"
+            >
+              <Input
+                v-model:value="form.phaseName"
+                placeholder="请输入阶段名称"
+                :maxlength="20"
+                show-count
+              />
+            </FormItem>
+            <FormItem label="用户满足条件"></FormItem>
+            <div class="description">
+              <InfoCircleOutlined />
+              主要用于执行任务的触发的条件,满足条件才可进入群发,转人工的动作
+            </div>
+            <FormItem
+              label="选择条件"
+              name="conditionType"
+              :rules="[{ required: true, message: '请选择条件' }]"
+            >
+              <Radio.Group v-model:value="form.conditionType">
+                <Radio value="1">客户标签</Radio>
+              </Radio.Group>
+            </FormItem>
+            <FormItem
+              label="条件关系"
+              name="conditionOperator"
+              :rules="[{ required: true, message: '请选择条件关系' }]"
+            >
+              <Radio.Group v-model:value="form.conditionOperator">
+                <Radio value="1">满足所有条件</Radio>
+                <Radio value="2">满足一个条件即可</Radio>
+              </Radio.Group>
+            </FormItem>
+            <FormItem label="触发条件" name="conditionList">
+              <div @click="addContent" style="color: #307ef2; display: inline-block">
+                <PlusCircleOutlined style="color: #307ef2; display: inline-block" />
+                添加
+              </div>
+              <div v-if="form.conditionList.length > 0" class="concrete-content-container">
+                <div
+                  v-for="(item, index) in form.conditionList"
+                  :key="index"
+                  class="concrete-content-item"
+                >
+                  <span>标签</span>
+                  <Select
+                    v-model:value="item.equal"
+                    :options="equalList"
+                    :style="{ width: '80px', margin: '0 5px' }"
+                  ></Select>
+                  <Select
+                    v-model:value="item.labelIdList"
+                    :options="actionLabel"
+                    allowClear
+                    mode="multiple"
+                    size="middle"
+                    placeholder="请选择"
+                    :style="{ width: '240px', margin: '0 5px' }"
+                    :max-tag-count="1"
+                  ></Select>
+                  <CloseCircleOutlined style="color: #307ef2" @click="removeContent(index)" />
+                </div>
+                <!-- <div @click="addContent" style="color:#307ef2"><PlusCircleOutlined style="color:#307ef2"/> 添加</div> -->
+              </div>
+            </FormItem>
+          </Form>
+        </div>
+        <div v-if="currentStep === 1" class="step-content">
+          <Form
+            :model="taskForm"
+            :label-col="{ span: 3 }"
+            :wrapper-col="{ span: 19 }"
+            ref="taskFormRef"
+          >
+            <FormItem label="执行任务" name="taskType">
+              <Button
+                :class="isMsgAction ? 'action-btn' : 'btn-style'"
+                @click="setTaskType('sendMessage')"
+              >
+                <template #icon>
+                  <CommentOutlined />
+                </template>
+                发消息
+              </Button>
+              <Button :class="isTagAction ? 'action-btn' : 'btn-style'" @click="setTaskType('tag')">
+                <template #icon>
+                  <TagsOutlined />
+                </template>
+                打标签
+              </Button>
+            </FormItem>
+            <!-- 发消息 -->
+            <Collapse
+              v-show="isMsgAction"
+              v-model:activeKey="activeKey"
+              collapsible="header"
+              class="custom-collapse"
+            >
+              <Collapse.Panel key="1" header="发消息" class="custom-collapse-panel">
+                <div @click.stop>
+                  <FormItem label="发送方式" name="sendType">
+                    <Radio.Group v-model:value="taskForm.sendType">
+                      <Radio value="immediate">立即发送</Radio>
+                      <!-- <Radio value="scheduled">定时发送</Radio> -->
+                    </Radio.Group>
+                  </FormItem>
+                  <!-- 根据发送方式显示 MsgContant 或在折叠面板中展示 -->
+                  <div v-if="taskForm.sendType === 'immediate'">
+                    <FormItem label="" name="actionMessage">
+                      <MsgContant v-model:value="taskForm.actionMessage" />
+                    </FormItem>
+
+                    <!-- <component :is="MsgContant" /> -->
+                  </div>
+                  <TimedSending v-else />
+                </div>
+              </Collapse.Panel>
+            </Collapse>
+            <!-- 打标签 -->
+            <Collapse
+              v-show="isTagAction"
+              v-model:activeKey="activeKeyTag"
+              collapsible="header"
+              class="custom-collapse-tag"
+            >
+              <Collapse.Panel key="1" header="打标签" class="tag-collapse-panel">
+                <div @click.stop>
+                  <FormItem
+                    label="用户进入当前阶段时,可以打上标签"
+                    name="tagging"
+                    v-if="msgAction"
+                    :labelCol="{ span: 8 }"
+                  >
+                    <!-- :rules="[{ required: true, message: '请选择一个标签', trigger: 'change' },]" -->
+                    <Select
+                      v-model:value="taskForm.tagValue"
+                      :options="actionLabel"
+                      allowClear
+                      mode="multiple"
+                      size="middle"
+                      placeholder="请选择"
+                      :style="{ width: '240px', margin: '0 5px' }"
+                      :max-tag-count="1"
+                    ></Select>
+                  </FormItem>
+                </div>
+              </Collapse.Panel>
+            </Collapse>
+          </Form>
+        </div>
+        <div class="steps-action">
+          <Button v-if="currentStep > 0" @click="prevStep">上一步</Button>
+          <Button type="primary" v-if="currentStep < 1" @click="nextStep">下一步</Button>
+          <Button type="primary" v-if="currentStep === 1" @click="submitForm">完成</Button>
+          <Button @click="handleClose">取消</Button>
+        </div>
+      </Drawer>
+    </div>
+  </Spin>
+</template>
+
+<script setup lang="ts">
+  import { unref,defineProps, defineEmits,ref, toRefs, watch, reactive, onMounted,computed } from 'vue';
+  import {
+    Drawer,
+    Form,
+    FormItem,
+    Input,
+    Radio,
+    Button,
+    Steps,
+    Step,
+    Collapse,
+    Select,
+    Spin,
+  } from 'ant-design-vue';
+  import {
+    InfoCircleOutlined,
+    CommentOutlined,
+    TagsOutlined,
+    PlusCircleOutlined,
+    CloseCircleOutlined,
+  } from '@ant-design/icons-vue';
+  import type { FormInstance } from 'ant-design-vue';
+  // import HtmlTextarea from './htmlTextarea.vue';
+  import MsgContant from './msgContant.vue';
+import TimedSending from './timedSending.vue';
+ 
+  import {
+    addSopTaskStage,
+    getSopStageDetailById,
+    getSopStageList,
+    editSopTaskStage,
+  } from '@/api/wechat/sopTask';
+  import { useStore } from '../stores/index';
+  let store = useStore();
+  let { sopLabelList, sopStageId, sopTaskId, sopTaskDtat, setSopStageId, setSopStageList,setStageDrawer } = store;
+
+  // name: 'UserPhasesDrawer',
+  const props = defineProps<{
+    visible: boolean;
+    stageId: {
+      type: Number, // 支持 number 和 ref 类型
+      required: true
+    }
+  }>();
+  const emit = defineEmits<{
+  (event: 'update:open', value: boolean): void;
+  (event: 'update:getStageList', value:{value: boolean,id:number}): void;
+}>();
+  const { visible } = toRefs(props);
+  const { stageId } = toRefs(props);
+  const loading = ref(false);
+  const currentStep = ref(0);
+  const formRef = ref<FormInstance | null>(null);
+  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;
+  // 定义选项数组
+  const actionLabel = sopLabelList;
+  const equalList = [
+    { label: '是', value: 1 },
+    { label: '不是', value: 2 },
+  ];
+  const form = reactive({
+    phaseName: '',
+    conditionType: '1', //1:客户标签2:基本信息
+    conditionOperator: '1',
+    conditionList: [],
+  });
+  const initialForm = reactive({
+    phaseName: '',
+    conditionType: '1',
+    conditionOperator: '1',
+    conditionList: [],
+  });
+  const taskForm = reactive({
+    taskType: 'sendMessage',
+    sendType: 'immediate',
+    actionMessage: [{ type: 1, content: '' }],
+    tagValue: [],
+  });
+  const initialTaskForm = reactive({
+    taskType: 'sendMessage',
+    sendType: 'immediate',
+    actionMessage: [{ type: 1, content: '' }],
+    tagValue: [],
+  });
+  const activeKey = ref<number | string>('1');
+  const activeKeyTiem = ref<number | string>('time1');
+  const activeKeyTag = 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);
+
+  // onMounted(() => {
+  //   if (Object.keys(sopTaskDtat).length > 0 && sopStageId) {
+  //     console.log('编辑////', sopStageId);
+  //     const dataSource = sopTaskDtat;
+  //     form.phaseName = dataSource.name ? dataSource.name : '';
+  //     form.conditionType = dataSource.conditionType;
+  //     form.conditionOperator = dataSource.conditionOperator === 1 ? '1' : '2';
+  //     form.conditionList = dataSource.conditionList ? dataSource.conditionList : [];
+  //   } else {
+  //     console.log('新建----', sopStageId);
+  //     resetForms();
+  //   }
+  // });
+  onMounted(() => {
+    // Object.keys(sopTaskDtat).length > 0 &&
+    // if (sopStageId) {
+    //   console.log('编辑////', sopStageId);
+    //   getStageDetail(sopStageId);
+    // } else {
+    //   console.log('新建----', sopStageId);
+    //   resetForms();
+    // }
+    console.log(unref(stageId),'000000')
+    // unref(stageId);
+    if (unref(stageId) ) {
+      getStageDetail(unref(stageId));
+    
+    } else {
+      console.log('新建----');
+      resetForms();
+    }
+  });
+
+  function nextStep() {
+    if (formRef.value) {
+      formRef.value
+        .validateFields(['phaseName'])
+        .then(() => {
+          if (currentStep.value < 1) {
+            currentStep.value += 1;
+          }
+        })
+        .catch((error) => {
+          console.log('Validation Failed:', error);
+        });
+    }
+  }
+
+  function prevStep() {
+    if (currentStep.value > 0) {
+      currentStep.value -= 1;
+    }
+  }
+
+  function addContent() {
+    labelIsShow.value = true;
+    form.conditionList.push({ equal: 1, labelIdList: [] });
+  }
+
+  function removeContent(index: number) {
+    form.conditionList.splice(index, 1);
+  }
+  
+  function resetForms() {
+    Object.assign(form, initialForm);
+    Object.assign(taskForm, initialTaskForm);
+    currentStep.value = 0;
+  }
+
+  async function getStageDetail(id) {
+    // 模拟 API 调用
+    let data = await getSopStageDetailById({ id });
+    // 填充 form 数据
+    Object.assign(form, {
+      phaseName: data.data.name,
+      conditionType: String(data.data.conditionType),
+      conditionOperator: String(data.data.conditionOperator),
+      conditionList: data.data.conditionList || [],
+    });
+    const taskTypes = [];
+    if (data.data.actionMessage.length > 0) {
+      taskTypes.push('sendMessage');
+    }
+    if (data.data.actionLabel.length > 0) {
+      taskTypes.push('tag');
+    }
+    // 填充 taskForm 数据
+    Object.assign(taskForm, {
+      taskType: taskTypes,
+      sendType: 'immediate',
+      actionMessage: data.data.actionMessage || [{ type: 1, content: '' }],
+      tagValue: data.data.actionLabel || [],
+    });
+    // 根据 taskType 设置 isMsgAction 和 isTagAction
+    isMsgAction.value = taskTypes.includes('sendMessage');
+    isTagAction.value = taskTypes.includes('tag');
+  }
+  // async function getStageDetail(id) {
+  //   console.log('获取阶段细节', sopStageId);
+  //   let res = await getSopStageDetailById({ id });
+  //   const data = red.data;
+  //   form.phaseName = data.name;
+  //   form.conditionType = data.conditionType;
+  //   form.conditionOperator = data.conditionOperator === 1 ? '1' : '2';
+  //   form.conditionList = data.conditionList ? data.conditionList : [];
+  // }
+  // async function getStageList() {
+  //  let res = await getSopStageList({ taskId: store.sopTaskId });
+  //  if(res && res.code ===0){
+  //   setSopStageList(res.data.data || [])
+  //   emit('update:sopStageList', res.data.data || [])
+  //  }
+  // }
+
+  async function submitForm() {
+    // 提交表单逻辑
+    const { phaseName, conditionType, conditionOperator, conditionList } = form;
+    const { taskType, sendType, actionMessage, tagValue } = taskForm;
+
+    const requestData = {
+      name: phaseName,
+      conditionType: ~~conditionType,
+      conditionOperator: ~~conditionOperator,
+      conditionList,
+      taskType,
+      sendType,
+      actionMessage,
+      actionLabel: tagValue,
+    };
+
+    if (unref(stageId)) {
+      // 编辑
+      loading.value = true;
+      console.log('编辑阶段');
+      const id = unref(stageId)
+      let response = await editSopTaskStage({ id, ...requestData });
+      if (response && response.code == 0) {
+        console.log(response.code);
+        loading.value = false;
+        // getStageList()
+        emit('update:getStageList', {value:true,id});
+        console.log('编辑成功');
+        handleClose();
+      } else {
+        console.error(response.code, '编辑失败');
+      }
+    } else {
+      // 新增
+      console.log('新增阶段');
+      loading.value = true;
+      let response = await addSopTaskStage({ taskId: sopTaskId, ...requestData });
+      if (response && response.code === 0) {
+        console.log('新增成功');
+        loading.value = false;
+        // getStageList()
+        emit('update:getStageList',{ value:true,id:response.data});
+        // setSopStageId(response.data); // 更新 sopStageId
+        handleClose();
+      } else {
+        loading.value = false;
+        console.error('新增失败');
+      }
+    }
+  }
+
+  // 打开或关闭抽屉
+  function handleClose() {
+    if (visible.value) {
+      resetForms();
+    }
+    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) {
+      Object.assign(initialForm, { ...form });
+      Object.assign(initialTaskForm, { ...taskForm });
+    }
+  });
+
+  // 设置任务类型
+  function setTaskType(type: string) {
+    taskForm.taskType = type;
+    if (type === 'tag') {
+      isTagAction.value = !isTagAction.value;
+      // isTagAction.value = type === 'tag';
+      msgAction.value = type === 'tag';
+    } else if (type === 'sendMessage') {
+      msgAction.value = type === 'sendMessage';
+      // isMsgAction.value = type === 'sendMessage';
+      isMsgAction.value = !isMsgAction.value;
+    }
+    // taskForm.taskType = type;
+    // msgAction.value = type === 'sendMessage';
+    // isMsgAction.value = type === 'sendMessage';
+    // isTagAction.value = type === 'tag';
+  }
+  // const setTaskType = (taskType: string) => {
+  // console.log('first',taskType)
+  // 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() {
+    // 切换折叠面板的动作
+    if (taskForm.taskType === 'scheduled') {
+      msgAction.value = !msgAction.value;
+      if (msgAction.value) {
+        activeKeyTiem.value = ['time1'];
+      } else {
+        activeKeyTiem.value = [];
+      }
+    }
+  }
+  // 切换超时动作
+  // const toggleTimeoutAction = () => {
+  //   activeKeyTiem.value = activeKeyTiem.value ? '' : 'time1';
+  // };
+</script>
+<style scoped lang="less">
+  .concrete-content-container {
+    border: 0.5px solid #c0ccda;
+    border-radius: 2px;
+    padding: 16px 16px 16px 16px;
+  }
+  .concrete-content-item {
+    display: flex;
+    align-items: center;
+    margin-bottom: 8px;
+  }
+  .concrete-content-item a-input {
+    flex: 1;
+    margin-right: 8px;
+  }
+  .custom-collapse-tag {
+    background-color: #ecf5fc;
+    color: #333;
+    border-radius: 0px;
+    border: none;
+    margin: 30px 0 90px 0;
+  }
+  .custom-collapse-panel {
+    border: none !important;
+    border-radius: 0px;
+  }
+  .step-content {
+    margin-top: 20px;
+  }
+  .steps-action {
+    margin-top: 20px;
+    display: flex;
+    justify-content: flex-end;
+    gap: 10px;
+  }
+  .step-style {
+    margin: 0 auto;
+    width: 60%;
+  }
+  .custom-step .ant-steps-item-title {
+    font-size: 14px;
+  }
+  .description {
+    width: calc(100% - 90px);
+    margin: 0px 0 30px 20px;
+    font-size: 14px;
+    padding: 10px 15px;
+    color: #666;
+    background-color: #f9f9f9;
+  }
+  .description .anticon {
+    margin-right: 8px;
+  }
+  .btn-style {
+    margin: 0 15px;
+    border-color: #cececd;
+    color: #606266;
+  }
+  .action-btn {
+    margin: 0 15px;
+    color: #4096ff;
+    border-color: #4096ff;
+  }
+  .btn-style:hover {
+    margin: 0 15px;
+    border-color: #cececd;
+    color: #606266;
+  }
+  a-textarea {
+    width: 100%;
+  }
+  .highlighted-text {
+    color: #1c53d9;
+    margin: 0 5px;
+    background-color: #e8edfb;
+    padding: 3px 5px;
+  }
+  .custom-collapse {
+    background-color: #f3faef;
+    color: #333;
+    border-radius: 0px;
+    border: none;
+    margin-top: 20px;
+  }
+  .tag-collapse-panel {
+    border: none;
+  }
+  .loading-style {
+    width: 100%;
+    height: 100%;
+    z-index: 1000;
+    position: fixed !important;
+    top: 0;
+    left: 0;
+    background: rgba(255, 255, 255, 0.5);
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+  }
+</style>

+ 44 - 0
src/views/wechat/sop_task/add_sop/index.vue

@@ -0,0 +1,44 @@
+<template>
+  <div>
+    <!-- 头部选择SOP名称 -->
+    <SopTaskName ref="sopTaskName" />
+    <div class="sop-task-container">
+      <!-- 左侧部分 -->
+      <LeftStage  />
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+  import { defineComponent, ref } from 'vue';
+  import SopTaskName from './components/sopTaskName.vue';
+  import LeftStage from './components/leftStage.vue';
+  export default defineComponent({
+    name: 'AddSop',
+    components: { SopTaskName, LeftStage,},
+    setup() {
+      const showPrompt = ref(false);
+      const sopTaskName = ref(null);
+      
+     
+      return {
+        showPrompt,
+        sopTaskName,
+      };
+    },
+  });
+</script>
+<style scoped lang="less">
+  .sop-task-container {
+    width: 100%;
+    height: 80vh;
+    overflow: hidden;
+    display: flex;
+    justify-content: space-between;
+  }
+  .flow-chart-container {
+    width: calc(80% - 10px);
+    height: calc(100% - 10px);
+    margin: 10px 10px 0 0;
+  }
+</style>

BIN
src/views/wechat/sop_task/add_sop/static/note.png


BIN
src/views/wechat/sop_task/add_sop/static/weChat.png


+ 92 - 0
src/views/wechat/sop_task/add_sop/stores/index.js

@@ -0,0 +1,92 @@
+/*
+ * @Date: 2022-08-25 14:13:11
+ * @LastEditors: StavinLi 495727881@qq.com
+ * @LastEditTime: 2023-05-24 15:00:32
+ * @FilePath: /Workflow-Vue3/src/store/index.js
+ */
+import { defineStore } from 'pinia';
+
+export const useStore = defineStore('store', {
+  state: () => ({
+    sopNameAndTimeVilidate: false,
+    sopTaskDtat:{},//sop任务数据
+    sopLabelList: [],
+    sopTaskId: undefined,//sop任务ID
+    sopStageId: undefined,//sop阶段ID
+    sop_stage_list: [],//sop阶段列表数据
+    stageDrawer: false,//sop阶段列表drawer
+    stageNodeId: undefined,//节点ID
+    isAddNode: false,//是否新增节点
+    tableId: '',
+    isTried: false,
+    promoterDrawer: false,
+    flowPermission1: {},
+    approverDrawer: false,
+    approverConfig1: {},
+    copyerDrawer: false,
+    copyerConfig1: {},
+    conditionDrawer: false,
+    conditionsConfig1: {
+      conditionNodes: [],
+    },
+  }),
+  actions: {
+    setSopNameAndTime(payload) {
+      this.sopNameAndTimeVilidate = payload
+    }, 
+    setSopStageList(payload) {
+      this.sop_stage_list = payload
+    }, 
+    setStageDrawer(payload) {
+      this.stageDrawer = payload
+    },
+    setStageNodeId(payload) {
+      this.stageNodeId = payload
+    }, 
+    setIsAddNode(payload) {
+      this.isAddNode = payload
+    },
+    setSopTaskDtat(payload) {
+      this.sopTaskDtat = payload
+    },
+    setSopTaskId(payload) {
+      this.sopTaskId = payload
+    },
+    setSopStageId(payload) {
+      this.sopStageId = payload
+    },
+    setSopLabelList(payload) {
+      this.sopLabelList = payload
+    },
+    setTableId(payload) {
+      this.tableId = payload
+    },
+    setIsTried(payload) {
+      this.isTried = payload
+    },
+    setPromoter(payload) {
+      this.promoterDrawer = payload
+    },
+    setFlowPermission(payload) {
+      this.flowPermission1 = payload
+    },
+    setApprover(payload) {
+      this.approverDrawer = payload
+    },
+    setApproverConfig(payload) {
+      this.approverConfig1 = payload
+    },
+    setCopyer(payload) {
+      this.copyerDrawer = payload
+    },
+    setCopyerConfig(payload) {
+      this.copyerConfig1 = payload
+    },
+    setCondition(payload) {
+      this.conditionDrawer = payload
+    },
+    setConditionsConfig(payload) {
+      this.conditionsConfig1 = payload
+    },
+  }
+})

+ 196 - 119
src/views/wechat/sop_task/index.vue

@@ -1,14 +1,9 @@
 <template>
   <div>
-    <BasicTable @register="registerTable">
+    <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>
@@ -19,133 +14,215 @@
       </template>
       <template #bodyCell="{ column, record }">
         <template v-if="column.key === 'action'">
-          <TableAction
-            :actions="[
-              {
-                icon: 'clarity:note-edit-line',
-                onClick: handleEdit.bind(null, record),
-              },
-              {
-                icon: 'ant-design:delete-outlined',
-                color: 'error',
-                popConfirm: {
-                  title: t('common.deleteConfirm'),
-                  placement: 'left',
-                  confirm: handleDelete.bind(null, record),
-                },
-              },
-            ]"
-          />
+          <TableAction :actions="[
+      {
+        icon: 'clarity:note-edit-line',
+        onClick: handleEdit.bind(null, record),
+        tooltip: '编辑',
+        ifShow: isEdit,
+      },
+      {
+        icon: 'ant-design:poweroff-outlined',
+        onClick: handleOpen.bind(null, record),
+        tooltip: '启用',
+        ifShow: !isEdit,
+      },
+      {
+        icon: 'ant-design:delete-outlined',
+        color: 'error',
+        tooltip: '删除',
+        popConfirm: {
+          title: t('common.deleteConfirm'),
+          placement: 'left',
+          confirm: handleDelete.bind(null, record),
+        },
+        ifShow: !isDel,
+      },
+      {
+        icon: 'ant-design:stop-outlined',
+        color: 'error',
+        onClick: handleStop.bind(null, record),
+        tooltip: '禁用',
+        ifShow: isDel,
+      },
+      {
+        icon: 'ant-design:copy-outlined',
+        onClick: handleCopy.bind(null, record),
+        tooltip: '复制',
+      },
+      {
+        icon: 'ant-design:fund-outlined',
+        onClick: handleTask.bind(null, record),
+        tooltip: '执行任务',
+      },
+      {
+        icon: 'ant-design:eye-outlined',
+        onClick: handleView.bind(null, record),
+        tooltip: '查看',
+      },
+    ]" />
         </template>
       </template>
     </BasicTable>
-    <SopTaskDrawer @register="registerDrawer" @success="handleSuccess" />
+    <!-- <SopTaskDrawer @register="registerDrawer" @success="handleSuccess" /> -->
   </div>
 </template>
 <script lang="ts">
-  import { createVNode, defineComponent, ref } from 'vue';
-  import { Modal } 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 { 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 { useDrawer } from '@/components/Drawer';
-  import SopTaskDrawer from './SopTaskDrawer.vue';
-  import { useI18n } from 'vue-i18n';
+import { useDrawer } from '@/components/Drawer';
+// import SopTaskDrawer from './SopTaskDrawer.vue';
+import { useI18n } from 'vue-i18n';
+import { columns, searchFormSchema } from './sopTask.data';
+import { getSopTaskList, deleteSopTask } 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 { columns, searchFormSchema } from './sopTask.data';
-  import { getSopTaskList, deleteSopTask } from '@/api/wechat/sopTask';
+  setup() {
+    const router = useRouter();
+    const isEdit = ref(true);
+    const isDel = ref(true);
+    const { t } = useI18n();
+    const selectedIds = ref<number[] | string[]>();
+    const showDeleteButton = ref<boolean>(false);
+    
+    const go = useGo();
+    const [registerDrawer, { openDrawer }] = useDrawer();
+    const [registerTable, { reload }] = useTable({
+      title: t('wechat.sopTask.sopTaskList'),
+      api: getSopTaskList,
+      columns,
+      formConfig: {
+        labelWidth: 120,
+        schemas: searchFormSchema,
+      },
+      useSearchForm: true,
+      showTableSetting: true,
+      bordered: true,
+      showIndexColumn: false,
+      clickToRowSelect: false,
+      actionColumn: {
+        width: 10,
+        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;
+        },
+      },
+    });
 
-  export default defineComponent({
-    name: 'SopTaskManagement',
-    components: { BasicTable, SopTaskDrawer, TableAction, Button },
-    setup() {
-      const { t } = useI18n();
-      const selectedIds = ref<number[] | string[]>();
-      const showDeleteButton = ref<boolean>(false);
+    //旧版新建
+    // function handleCreate() {
+    //   openDrawer(true, {
+    //     isUpdate: false,
+    //   });
+    // }
+    //新建SOP页面跳转
+    function handleCreate() {
+      console.log('跳转SOP页面')
+      go(PageEnum.ADD_SOP);
+    }
 
-      const [registerDrawer, { openDrawer }] = useDrawer();
-      const [registerTable, { reload }] = useTable({
-        title: t('wechat.sopTask.sopTaskList'),
-        api: getSopTaskList,
-        columns,
-        formConfig: {
-          labelWidth: 120,
-          schemas: searchFormSchema,
-        },
-        useSearchForm: true,
-        showTableSetting: true,
-        bordered: true,
-        showIndexColumn: false,
-        clickToRowSelect: false,
-        actionColumn: {
-          width: 30,
-          title: t('common.action'),
-          dataIndex: 'action',
-          fixed: undefined,
-        },
-        rowKey: 'id',
-        rowSelection: {
-          type: 'checkbox',
-          onChange: (selectedRowKeys, _selectedRows) => {
-            selectedIds.value = selectedRowKeys as number[];
-            showDeleteButton.value = selectedRowKeys.length > 0;
-          },
-        },
+    function handleEdit(record: Recordable) {
+      console.log('编辑',record.id);
+      // go(PageEnum.ADD_SOP);
+      router.push({
+        path: PageEnum.ADD_SOP,
+        query: { task_id: record.id }
       });
+    }
 
-      function handleCreate() {
-        openDrawer(true, {
-          isUpdate: false,
-        });
+    async function handleDelete(record: Recordable) {
+      const result = await deleteSopTask({ ids: [record.id] });
+      if (result.code === 0) {
+        await reload();
       }
+    }
 
-      function handleEdit(record: Recordable) {
-        openDrawer(true, {
-          record,
-          isUpdate: true,
-        });
-      }
+    //复制
+    function handleCopy(record: Recordable) {
+      message.success('复制成功!')
+      console.log('复制')
+    }
 
-      async function handleDelete(record: Recordable) {
-        const result = await deleteSopTask({ ids: [record.id] });
-        if (result.code === 0) {
-          await reload();
-        }
-      }
+    //执行任务
+    function handleTask(record: Recordable) {
+      console.log('执行任务')
+      go(PageEnum.PERFORM_TASKS);
+    }
 
-      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');
-          },
-        });
-      }
+    //启用
+    function handleOpen(record: Recordable) {
+      console.log('启用')
+      isEdit.value = !isEdit.value;
+      isDel.value = !isDel.value;
+    }
 
-      async function handleSuccess() {
-        await reload();
-      }
+    //禁用
+    function handleStop(record: Recordable) {
+      isEdit.value = !isEdit.value;
+      isDel.value = !isDel.value;
+      console.log('禁用')
+    }
+
+    //查看 
+    function handleView(record: Recordable) {
+      console.log('查看')
+    }
+
+    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 handleSuccess() {
+      await reload();
+    }
 
-      return {
-        t,
-        registerTable,
-        registerDrawer,
-        handleCreate,
-        handleEdit,
-        handleDelete,
-        handleSuccess,
-        handleBatchDelete,
-        showDeleteButton,
-      };
-    },
-  });
+    return {
+      t,
+      registerTable,
+      registerDrawer,
+      handleCreate,
+      handleEdit,
+      handleDelete,
+      handleCopy,
+      handleOpen,
+      handleTask,
+      handleStop,
+      handleView,
+      handleSuccess,
+      handleBatchDelete,
+      showDeleteButton,
+      isEdit,
+      isDel,
+    };
+  },
+});
 </script>

+ 76 - 0
src/views/wechat/sop_task/perform_tasks/index.vue

@@ -0,0 +1,76 @@
+<template>
+  <div class="container">
+    <a-table :columns="columns" :dataSource="data" :pagination="pagination" class="table-style">
+      <!-- 自定义操作列 -->
+      <template #action="{ record }">
+        <a @click="viewRecord(record)">发送记录</a>
+      </template>
+    </a-table>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, reactive } from 'vue';
+import { Table, Pagination } from 'ant-design-vue';
+import { useGo } from "/@/hooks/web/usePage";
+import { PageEnum } from '@/enums/pageEnum';
+export default defineComponent({
+  name: 'PerformTasks',
+  components: {
+    'a-table': Table,
+    'a-pagination': Pagination,
+  },
+  setup() {
+    const data = reactive([
+      { key: '1', index: 1, task: '测试 3', date: '2024-06-12', successCount: 1, failCount: 0, successRate: '100%' },
+      { key: '2', index: 2, task: '测试 4', date: '2024-06-12', successCount: 1, failCount: 0, successRate: '100%' },
+    ]);
+    const go = useGo();
+    const columns = reactive([
+      { title: '序号', dataIndex: 'index', key: 'index' },
+      { title: 'SOP任务节点', dataIndex: 'task', key: 'task' },
+      { title: '日期', dataIndex: 'date', key: 'date' },
+      { title: '成功数', dataIndex: 'successCount', key: 'successCount' },
+      { title: '失败数', dataIndex: 'failCount', key: 'failCount' },
+      { title: '成功率', dataIndex: 'successRate', key: 'successRate' },
+      { title: '操作', key: 'action', slots: { customRender: 'action' }},
+    ]);
+
+    const pagination = reactive({
+      total: data.length,
+      pageSize: 10,
+      current: 1,
+      showTotal: (total) => `共 ${total} 条`,
+    });
+
+    const viewRecord = (record) => {
+      console.log('查看发送记录', record);
+      go(PageEnum.SEND_TASKS);
+      // 在这里添加你的逻辑,比如跳转到发送记录页面或显示模态框
+    };
+
+    return {
+      data,
+      columns,
+      pagination,
+      viewRecord,
+    };
+  },
+});
+</script>
+
+<style scoped>
+.container{
+  background-color: #fff;
+  margin: 20px;
+  border-radius: 4px;
+  height: calc(100vh - 40px);
+  width: calc(100% - 40px);
+}
+.ant-table-cell {
+  text-align: center;
+}
+.table-style{
+  padding: 16px;
+}
+</style>

+ 150 - 0
src/views/wechat/sop_task/send_tasks/index.vue

@@ -0,0 +1,150 @@
+<template>
+  <div class="container">
+    <a-form :model="filters" layout="inline" class="form-style">
+      <a-form-item label="客户">
+        <a-input v-model:value="filters.customer" placeholder="请输入" />
+      </a-form-item>
+      <a-form-item label="社交账号">
+        <a-select v-model:value="filters.socialAccount" placeholder="请选择" class="select-style">
+          <a-select-option value="account1">账号1</a-select-option>
+          <a-select-option value="account2">账号2</a-select-option>
+        </a-select>
+      </a-form-item>
+      <a-form-item label="类型">
+        <a-select v-model:value="filters.type" placeholder="请选择" class="select-style">
+          <a-select-option value="personal">个人微信</a-select-option>
+          <a-select-option value="enterprise">企业微信</a-select-option>
+        </a-select>
+      </a-form-item>
+      <a-form-item label="执行结果">
+        <a-select v-model:value="filters.result" placeholder="请选择" class="select-style">
+          <a-select-option value="notSend">未发送</a-select-option>
+          <a-select-option value="success">成功</a-select-option>
+          <a-select-option value="failure">失败</a-select-option>
+        </a-select>
+      </a-form-item>
+      <a-form-item>
+        <a-button type="primary" @click="onSearch">查询</a-button>
+      </a-form-item>
+      <a-form-item>
+        <a-button @click="onReset">重置</a-button>
+      </a-form-item>
+    </a-form>
+    
+    <a-table :columns="columns" :dataSource="data" :pagination="pagination" class="table-style">
+      <template #action="{ record }">
+        <a @click="viewRecord(record)">发送记录</a>
+      </template>
+    </a-table>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, reactive } from 'vue';
+import { Form, Input, Select, Button, Table, Pagination } from 'ant-design-vue';
+
+export default defineComponent({
+  name: 'SendTasks',
+  components: {
+    'a-form': Form,
+    'a-form-item': Form.Item,
+    'a-input': Input,
+    'a-select': Select,
+    'a-select-option': Select.Option,
+    'a-button': Button,
+    'a-table': Table,
+    'a-pagination': Pagination,
+  },
+  setup() {
+    const filters = reactive({
+      customer: '',
+      socialAccount: '',
+      type: '',
+      result: '',
+    });
+
+    const data = reactive([
+      {
+        key: '1',
+        index: 1,
+        socialAccount: '冠客小助手',
+        status: '在线',
+        customer: '宋伯文คิดถึง',
+        taskName: '测试 3',
+        result: '成功',
+        reason: '--',
+        lastExecutionTime: '2024-06-12 16:29:48',
+      },
+    ]);
+
+    const columns = reactive([
+      { title: '序号', dataIndex: 'index', key: 'index' },
+      { title: '社交账号', dataIndex: 'socialAccount', key: 'socialAccount' },
+      { title: '客户', dataIndex: 'customer', key: 'customer' },
+      { title: 'SOP任务节点名称', dataIndex: 'taskName', key: 'taskName' },
+      { title: '执行状态', dataIndex: 'result', key: 'result' },
+      { title: '异常原因', dataIndex: 'reason', key: 'reason' },
+      { title: '最新执行时间', dataIndex: 'lastExecutionTime', key: 'lastExecutionTime' },
+      // { title: '操作', key: 'action', slots: { customRender: 'action' } },
+    ]);
+
+    const pagination = reactive({
+      total: data.length,
+      pageSize: 10,
+      current: 1,
+      showTotal: (total) => `共 ${total} 条`,
+    });
+
+    const onSearch = () => {
+      console.log('查询条件', filters);
+      // 在这里添加查询逻辑
+    };
+
+    const onReset = () => {
+      filters.customer = '';
+      filters.socialAccount = '';
+      filters.type = '';
+      filters.result = '';
+    };
+
+    const viewRecord = (record) => {
+      console.log('查看发送记录', record);
+      // 在这里添加你的逻辑,比如跳转到发送记录页面或显示模态框
+    };
+
+    return {
+      filters,
+      data,
+      columns,
+      pagination,
+      onSearch,
+      onReset,
+      viewRecord,
+    };
+  },
+});
+</script>
+
+<style scoped>
+.container{
+  background-color: #fff;
+  margin: 20px;
+  border-radius: 4px;
+  height: calc(100vh - 40px);
+  width: calc(100% - 40px);
+}
+.ant-table-cell {
+  text-align: center;
+}
+.table-style{
+  padding: 16px;
+}
+.form-style{
+  /* width: ; */
+  height: 55px;
+  padding: 16px;
+}
+.select-style{
+  width: 180px;
+}
+</style>

+ 97 - 40
src/views/wechat/sop_task/sopTask.data.ts

@@ -6,12 +6,34 @@ import { Switch } from 'ant-design-vue';
 import { h } from 'vue';
 
 const { t } = useI18n();
-
+const statusOptions = [
+  { label: '草稿', value: 1 },
+  { label: '未开始', value: 2 },
+  { label: '已开始', value: 3 },
+  { label: '已结束', value: 4 },
+  { label: '禁用', value: 5 },
+];
+const typeOptions = [
+        { label: t(`好友`), value: 1 },
+        { label: t(`群组`), value: 2 },
+        { label: t(`企业微信联系人`), value: 3 },
+      ]
 export const columns: BasicColumn[] = [
   {
-    title: t('wechat.sopTask.name'),
+    title: '序号',
+    dataIndex: 'index',
+    width: 50,
+    fixed: 'left',
+    customRender: ({ index }) => {
+      return index + 1;
+    },
+  },
+  {
+    // title: t('wechat.sopTask.name'),
+    title: 'SOP名称',
     dataIndex: 'name',
-    width: 100,
+    fixed: 'left',
+    width: 60,
   },
   // {
   //   title: t('wechat.sopTask.botWxidList'),
@@ -21,17 +43,27 @@ export const columns: BasicColumn[] = [
   {
     title: t('wechat.sopTask.type'),
     dataIndex: 'type',
-    width: 100,
+    width: 80,
+    customRender: ({ record }) => {
+      const type = typeOptions.find(option => option.value === record.status);
+      return type ? type.label : '';
+    },
   },
   {
     title: t('wechat.sopTask.planStartTime'),
     dataIndex: 'planStartTime',
-    width: 100,
+    width: 85,
+    customRender: ({ record }) => {
+      return record.planStartTime ? formatToDateTime(record.planStartTime) : "不限制";
+    },
   },
   {
     title: t('wechat.sopTask.planEndTime'),
     dataIndex: 'planEndTime',
-    width: 100,
+    width: 85,
+    customRender: ({ record }) => {
+      return record.planEndTime ? formatToDateTime(record.planEndTime) : "不限制";
+    },
   },
   // {
   //   title: t('wechat.sopTask.creatorId'),
@@ -41,34 +73,22 @@ export const columns: BasicColumn[] = [
   {
     title: t('common.status'),
     dataIndex: 'status',
-    width: 50,
+    width: 70,
     customRender: ({ record }) => {
-      if (!Reflect.has(record, 'pendingStatus')) {
-        record.pendingStatus = false;
-      }
-      return h(Switch, {
-        checked: record.status === 1,
-        checkedChildren: t('common.on'),
-        unCheckedChildren: t('common.off'),
-        loading: record.pendingStatus,
-        onChange(checked, _) {
-          record.pendingStatus = true;
-          const newStatus = checked ? 1 : 2;
-          updateSopTask({ id: record.id, status: newStatus })
-            .then(() => {
-              record.status = newStatus;
-            })
-            .finally(() => {
-              record.pendingStatus = false;
-            });
-        },
-      });
+      const status = statusOptions.find(option => option.value === record.status);
+      return status ? status.label : '';
     },
   },
   {
+    // title: t('wechat.sopTask.name'),
+    title: '创建人',
+    dataIndex: 'creatorId',
+    width: 50,
+  },
+  {
     title: t('common.createTime'),
     dataIndex: 'createdAt',
-    width: 70,
+    width: 85,
     customRender: ({ record }) => {
       return formatToDateTime(record.createdAt);
     },
@@ -78,10 +98,54 @@ export const columns: BasicColumn[] = [
 export const searchFormSchema: FormSchema[] = [
   {
     field: 'name',
-    label: t('wechat.sopTask.name'),
+    // label: t('wechat.sopTask.name'),
+    label: 'SOP名称',
     component: 'Input',
     colProps: { span: 8 },
   },
+  {
+    field: 'planStartTime',
+    label: '开始时间',
+    component: 'DatePicker',
+    componentProps: {
+      // 这里可以添加日期选择器的属性
+      showTime: true, // 是否显示时间选择
+      format: 'YYYY-MM-DD HH:mm:ss', // 日期时间格式
+    },
+    colProps: { span: 8 },
+  },
+  {
+    field: 'planEndTime',
+    label: '结束时间',
+    component: 'DatePicker',
+    componentProps: {
+      // 这里可以添加日期选择器的属性
+      showTime: true, // 是否显示时间选择
+      format: 'YYYY-MM-DD HH:mm:ss', // 日期时间格式
+    },
+    colProps: { span: 8 },
+  },
+  {
+    field: 'creatorId',
+    // label: t('wechat.sopTask.name'),
+    label:'创建人',
+    component: 'Input',
+    // componentProps: {
+
+    // },
+    colProps: { span: 8 },
+  },
+  {
+    field: 'status',
+    // label: t('wechat.sopTask.name'),
+    //1 草稿 2 未开始 3 已开始 4 已结束 5 禁用
+    label:'状态',
+    component: 'Select',
+    componentProps: {
+      options: statusOptions,
+    },
+    colProps: { span: 8 },
+  },
 ];
 
 export const formSchema: FormSchema[] = [
@@ -109,13 +173,9 @@ export const formSchema: FormSchema[] = [
     label: t('wechat.sopTask.type'),
     component: 'RadioGroup',
     required: true,
+    // 1好友,2群组,3企业微信联系人
     componentProps: {
-      options: [
-        { label: t(`wechat.label_type[0]`), value: 1 },
-        { label: t(`wechat.label_type[1]`), value: 2 },
-        { label: t(`wechat.label_type[2]`), value: 3 },
-        { label: t(`wechat.label_type[3]`), value: 4 },
-      ],
+      options: typeOptions,
     },
   },
   {
@@ -140,13 +200,10 @@ export const formSchema: FormSchema[] = [
   {
     field: 'status',
     label: t('wechat.sopTask.status'),
-    component: 'RadioButtonGroup',
+    component: 'Select',
     defaultValue: 1,
     componentProps: {
-      options: [
-        { label: t('common.on'), value: 1 },
-        { label: t('common.off'), value: 2 },
-      ],
+       options: statusOptions,
     },
   },
 ];

BIN
微信后台.zip