ruoyi-nbcio-plus基于vue3的flowable的支持自定义业务流程处理页面detail.vue的升级修改

更多ruoyi-nbcio功能请看演示系统

gitee源代码地址

前后端代码: https://gitee.com/nbacheng/ruoyi-nbcio

演示地址:RuoYi-Nbcio后台管理系统 http://122.227.135.243:9666/

更多nbcio-boot功能请看演示系统

gitee源代码地址

后端代码: https://gitee.com/nbacheng/nbcio-boot

前端代码:https://gitee.com/nbacheng/nbcio-vue.git

在线演示(包括H5) : http://122.227.135.243:9888

1、因为修改vue3后也要支持自定义表单额显示处理,所以修改如下:

javascript 复制代码
<template>
  <div class="app-container">
    <el-tabs tab-position="top" :model-value="processed === true ? 'approval' : 'form'">
      <el-tab-pane label="任务办理" name="approval" v-if="processed === true">
        <el-card class="box-card" shadow="hover" v-if="taskFormOpen">
          <template #header>
            <span>填写表单</span>
          </template>
          <div class="cu-content">
            <v-form-render :form-json="{}" :form-data="{}" ref="vfRenderRef" />
          </div>
        </el-card>
        <el-card class="box-card" shadow="hover">
          <template #header>
            <span>审批流程</span>
          </template>
          <el-row>
            <el-col :span="20" :offset="2">
              <el-form ref="taskFormRef" :model="taskForm" :rules="rules" label-width="120px">
                <el-form-item label="审批意见" prop="comment">
                  <el-input type="textarea" :rows="5" v-model="taskForm.comment" placeholder="请输入 审批意见" />
                </el-form-item>
                <el-form-item label="抄送人" prop="copyUserIds">
                  <el-tag :key="index" v-for="(item, index) in copyUser" closable :disable-transitions="false" @close="handleClose('copy', item)">
                    {{ item.nickName }}
                  </el-tag>
                  <el-button class="button-new-tag" type="primary" icon="el-icon-plus" circle @click="onSelectCopyUsers" />
                </el-form-item>
                <el-form-item label="指定审批人" prop="copyUserIds">
                  <el-tag :key="index" v-for="(item, index) in nextUser" closable :disable-transitions="false" @close="handleClose('next', item)">
                    {{ item.nickName }}
                  </el-tag>
                  <el-button class="button-new-tag" type="primary" icon="el-icon-plus" circle @click="onSelectNextUsers" />
                </el-form-item>
              </el-form>
            </el-col>
          </el-row>
          <el-row :gutter="10" type="flex" justify="center">
            <el-col :span="1.5">
              <el-button icon="CircleCheck" type="success" @click="handleComplete">通过</el-button>
            </el-col>
            <el-col :span="1.5">
              <el-button icon="ChatLineSquare" type="primary" @click="handleDelegate">委派</el-button>
            </el-col>
            <el-col :span="1.5">
              <el-button icon="Switch" type="success" @click="handleTransfer">转办</el-button>
            </el-col>
            <el-col :span="1.5">
              <el-button icon="RefreshLeft" type="warning" @click="handleReturn">退回</el-button>
            </el-col>
            <el-col :span="1.5">
              <el-button icon="CircleClose" type="danger" @click="handleReject">拒绝</el-button>
            </el-col>
          </el-row>
        </el-card>
      </el-tab-pane>

      <el-tab-pane label="表单信息" name="form">
        <div v-if="customForm.visible"> <!-- 自定义表单 -->
          <component ref="refCustomForm" :disabled="customForm.disabled" :is="customForm.formComponent" :model="customForm.model"
            :customFormData="customForm.customFormData" :isNew = "customForm.isNew"></component>
        </div>
        <div v-if="formVisible">
          <el-card class="box-card" shadow="never" v-for="(item, index) in processFormList" :key="index">
            <template #header>
              <span>{{ item.title }}</span>
            </template>
            <!--流程处理表单模块-->
            <div class="cu-content">
              <v-form-render :form-json="item.formModel" :form-data="item.formData" ref="vFormRenderRef" />
            </div>
          </el-card>
        </div>
        <div style="margin-left:10%;margin-bottom: 30px">
           <!--对上传文件进行显示处理,临时方案 add by nbacheng 2022-07-27 -->
        <!--   <el-upload action="#" :on-preview="handleFilePreview" :file-list="fileList" v-if="fileDisplay" /> -->
        </div>
      </el-tab-pane >

      <el-tab-pane label="流转记录" name="record">
        <el-card class="box-card" shadow="never">
          <el-col :span="20" :offset="2">
            <div class="block">
              <el-timeline>
                <el-timeline-item v-for="(item, index) in historyProcNodeList" :key="index" :type="tagType(item.endTime)">
                  <p style="font-weight: 700">{{ item.activityName }}</p>
                  <el-card v-if="item.activityType === 'startEvent'" class="box-card" shadow="hover">
                    {{ item.assigneeName }} 在 {{ item.createTime }} 发起流程
                  </el-card>
                  <el-card v-if="item.activityType === 'userTask'" class="box-card" shadow="hover">
                    <el-descriptions :column="5" :labelStyle="{'font-weight': 'bold'}">
                      <el-descriptions-item label="实际办理">{{ item.assigneeName || '-'}}</el-descriptions-item>
                      <el-descriptions-item label="候选办理">{{ item.candidate || '-'}}</el-descriptions-item>
                      <el-descriptions-item label="接收时间">{{ item.createTime || '-'}}</el-descriptions-item>
                      <el-descriptions-item label="办结时间">{{ item.endTime || '-' }}</el-descriptions-item>
                      <el-descriptions-item label="耗时">{{ item.duration || '-'}}</el-descriptions-item>
                    </el-descriptions>
                    <div v-if="item.commentList && item.commentList.length > 0">
                      <div v-for="(comment, index) in item.commentList" :key="index">
                        <el-divider content-position="left">
                          <el-tag :type="approveTypeTag(comment.type)">{{ commentType(comment.type) }}</el-tag>
                          <el-tag type="info" effect="plain">{{ comment.time }}</el-tag>
                        </el-divider>
                        <span>{{ comment.fullMessage }}</span>
                      </div>
                    </div>
                  </el-card>
                  <el-card v-if="item.activityType === 'endEvent'" class="box-card" shadow="hover">
                    {{ item.createTime }} 结束流程
                  </el-card>
                </el-timeline-item>
              </el-timeline>
            </div>
          </el-col>
        </el-card>
      </el-tab-pane>

      <el-tab-pane label="流程跟踪" name="track">
        <el-card class="box-card" shadow="never">
          <process-viewer
            :key="`designer-${loadIndex}`"
            :style="'height:' + height"
            :xml="processXml"
            :finishedInfo="finishedInfo"
            :allCommentList="historyProcNodeList"
          />
        </el-card>
      </el-tab-pane>
    </el-tabs>

    <!--退回流程-->
    <el-dialog :title="returnDialog.title" v-model="returnDialog.visible" width="40%" append-to-body>
      <el-radio-group v-model="returnTaskKey">
        <el-radio-button v-for="item in returnTaskList" :key="item.id" :label="item.id">
          {{ item.name }}
        </el-radio-button>
      </el-radio-group>
      <template #footer>
        <el-button @click="returnDialog.visible = false">取 消</el-button>
        <el-button type="primary" @click="submitReturn">确 定</el-button>
      </template>
    </el-dialog>

    <el-dialog :title="userSelectDialog.title" v-model="userSelectDialog.visible" width="60%" append-to-body>
      <el-row type="flex" :gutter="20">
        <!--部门数据-->
        <el-col :span="5">
          <el-card shadow="never" style="height: 100%">
            <template #header>
              <span>部门列表</span>
            </template>
            <div class="head-container">
              <el-input v-model="deptName" placeholder="请输入部门名称" prefix-icon="Search" clearable />
              <el-tree
                :data="deptOptions"
                :props="{ label: 'label', children: 'children' }"
                :expand-on-click-node="false"
                :filter-node-method="filterNode"
                ref="deptTreeRef"
                default-expand-all
                @node-click="handleNodeClick"
              />
            </div>
          </el-card>
        </el-col>
        <el-col :span="18">
          <el-table
            ref="userTable"
            :key="userSelectType"
            height="500"
            v-loading="userLoading"
            :data="userList"
            highlight-current-row
            @current-change="changeCurrentUser"
            @selection-change="handleSelectionChange"
          >
            <el-table-column v-if="userSelectType === 'copy' || userSelectType === 'next'" width="55" type="selection" />
            <el-table-column v-else width="30">
              <template #default="scope">
                <el-radio :label="scope.row.userId" v-model="currentUserId">{{''}}</el-radio>
              </template>
            </el-table-column>
            <el-table-column label="用户名称" align="center" prop="userName" />
            <el-table-column label="用户昵称" align="center" prop="nickName" />
            <el-table-column label="手机" align="center" prop="phonenumber" />
          </el-table>
          <pagination :total="userTotal" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
        </el-col>
      </el-row>
      <template #footer>
        <el-button @click="userSelectDialog.visible = false">取 消</el-button>
        <el-button type="primary" @click="submitUserData">确 定</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script setup name="Detail" lang="ts">
  import { detailProcess, processIscompleted } from "@/api/workflow/process";
  import { getProcessVariables } from '@/api/workflow/task';
  import { complete, delegate, transfer, rejectTask, returnList, returnTask } from "@/api/workflow/work/task";
  import { deptTreeSelect, selectUser } from "@/api/workflow/identity";
  import { TaskForm } from "@/api/workflow/work/types";
  import { UserVO, DeptVO } from "@/api/workflow/identity/types";

  import { ComponentInternalInstance } from "vue";
  import {useRoute} from "vue-router";
  import {
        useFlowable
    } from '@/views/workflow/hooks/useFlowable'

  const route = useRoute();
  const router = useRouter();
  const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  const { getFormComponent } = useFlowable()

  const userList = ref<UserVO[]>([]);
  const processed = ref(false);
  const taskFormOpen = ref(false)
  const userMultipleSelection = ref([]);
  const userSelectType = ref();
  const currentUserId = ref();
  const userLoading = ref(false);
  const userTotal = ref(0);
  const loadIndex = ref('');
  const height = ref(document.documentElement.clientHeight - 205 + 'px;');
  const processXml = ref('');
  const taskFormVisible = ref(false);
  const processFormList = ref([]);
  const taskFormData = ref([]);
  const historyProcNodeList = ref<any>();
  const formVisible = ref(false);
  const finishedInfo = ref({});
  const customForm = ref({ //自定义业务表单
    formId: '',
    title: '',
    disabled: false,
    visible: false,
    formComponent: null,
    model: {},
    /*流程数据*/
    customFormData: {},
    isNew: false,
    disableSubmit: true
  })

  const deptName = ref('');
  const deptOptions = ref<DeptVO[]>([]);

  const returnTaskList = ref();
  const returnTaskKey = ref();

  const copyUser = ref([]);
  const nextUser = ref([]);

  const taskFormRef = ref(ElForm);
  const vFormRenderRef = ref(null);
  const deptTreeRef = ref(null);
  const refCustomForm = ref(null);

  const activeName = ref(''); //获取当然tabname

  const returnDialog = reactive<DialogOption>({
    visible: false,
    title: '退回流程'
  });

  const userSelectDialog = reactive<DialogOption>({
    visible: false,
    title: ''
  });

  const taskForm = reactive<TaskForm>({
    comment: '',
    procInsId: '',
    taskId: '',
    userId: '',
    copyUserIds: '',
    nextUserIds: '',
    vars: '',
    targetKey: ''
  });

  const rules = ref({
    comment: [{ required: true, message: '请输入审批意见', trigger: 'blur' }]
  });

  const queryParams = ref({
    pageNum: 1,
    pageSize: 10
  });
  const tagType = (val: any) => {
    if (val) {
        return "success";
    } else {
        return "info";
    }
  }
  const commentType = (val: string) => {
    switch (val) {
      case '1': return '通过'
      case '2': return '退回'
      case '3': return '驳回'
      case '4': return '委派'
      case '5': return '转办'
      case '6': return '终止'
      case '7': return '撤回'
      case '8': return '拒绝'
      case '9': return '跳过'
      case '10': return '前加签'
      case '11': return '后加签'
      case '12': return '多实例加签'
      case '13': return '跳转'
      case '14': return '收回'
    }
  }
  const approveTypeTag = (val: string) => {
    switch (val) {
      case '1': return 'success'
      case '2': return 'warning'
      case '3': return 'danger'
      case '4': return 'primary'
      case '5': return 'success'
      case '6': return 'danger'
      case '7': return 'info'
    }
  }

  const initData = () => {
    taskForm.procInsId = route.params && route.params.procInsId as string;
    taskForm.taskId  = route.query && route.query.taskId as string;
    processed.value = route.query && (route.query.processed || false) === "true";

    // 流程任务重获取变量表单
    //getProcessDetails(taskForm.procInsId, taskForm.taskId);
    //判断流程是否结束
    processIscompleted({procInsId: taskForm.procInsId}).then(res => {
      console.log("processIscompleted res=",res);
      if(res.data) {
       processed.value = false;
      }
      // 获取流程变量
      processVariables(taskForm.taskId);
    });
  };
  /** 通过条件过滤节点  */
  const filterNode = (value: string, data: any) => {
    if (!value) return true
    return data.label.indexOf(value) !== -1
  }
  // /** 根据名称筛选部门树 */
  // watchEffect(
  //     () => {deptTreeRef.value.filter(deptName.value);},
  //     {
  //       flush: 'post' // watchEffect会在DOM挂载或者更新之前就会触发,此属性控制在DOM元素更新后运行
  //     }
  // );
  // 节点单击事件
  const handleNodeClick = (data: any) => {
    getList(data.id);
  }
  /** 查询部门下拉树结构 */
  const getTreeSelect = async () => {
    const res = await deptTreeSelect();
    deptOptions.value = res.data;
  };
  /** 查询用户列表 */
  const getList = async (deptId?: number) => {
    userLoading.value = true;
    const res = await selectUser({deptId: deptId});
    userLoading.value = false;
    userList.value = res.rows;
    userTotal.value = res.total;
  }

  /** 获取流程变量内容 */
  const processVariables = (taskId: string) => {
    console.log("processVariables taskId",taskId);
    if (taskId) {
      getProcessVariables(taskId).then(res => {
        console.log("getProcessVariables res=",res);
        if(res.code == 200) {
          if(res.data.hasOwnProperty('dataId') && res.data.dataId) {
            customForm.value.formId = res.data.dataId;
            // 流程任务重获取变量表单
            getProcessDetails(taskForm.procInsId, taskForm.taskId, res.data.dataId);
            loadIndex.value = taskForm.procInsId;
            if(processed.value) {
              activeName.value = "approval";
            }
            else {
              activeName.value = "form";
            }
          }
          else {
            // 流程任务重获取变量表单
            getProcessDetails(taskForm.procInsId, taskForm.taskId, "");
            loadIndex.value = taskForm.procInsId;
            if(processed.value) {
              activeName.value = "approval";
            }
            else {
              activeName.value = "form";
              // 回填数据,这里主要是处理文件列表显示,临时解决,以后应该在formdesigner里完成
              /*this.processFormList.forEach((item, i) => {
                if (item?.hasOwnProperty('list')) {
                  fillFormData(item.list, item)
                  // 更新表单
                  this.key = +new Date().getTime()
                }
              });*/
            }
          }
        }
      });
    }
  }

  const fillFormData = (list, formConf) => { // for formdesigner
    console.log("fillFormData list=",list);
    console.log("fillFormData formConf=",formConf);
    list.forEach((item, i) => {
      // 特殊处理el-upload,包括 回显图片
      if(formConf.formValues[item.id] != '') {
        const val = formConf.formValues[item.id];
        if (item.ele === 'el-upload') {
          console.log('fillFormData val=',val)
          if(item['list-type'] != 'text') {//图片
            this.fileList = []    //隐藏加的el-upload文件列表
            //item['file-list'] = JSON.parse(val)
            if(val != '') {
              item['file-list'] = JSON.parse(val)
            }
          }
          else {  //列表
            console.log("列表fillFormData val",val)
            this.fileList = JSON.parse(val)
            item['file-list'] = [] //隐藏加的表单设计器的文件列表
          }
          // 回显图片
          this.fileDisplay = true
        }
      }

      if (Array.isArray(item.columns)) {
        this.fillFormData(item.columns, formConf)
      }
    })
  }

  const getProcessDetails = async (procInsId: string, taskId: string, dataId: string) => {
    const params = {procInsId: procInsId, taskId: taskId, dataId: dataId}
    const res = await detailProcess(params);
    const data = res.data;
    console.log("getProcessDetails data",data);
    processXml.value = data.bpmnXml;
    processFormList.value = data.processFormList;
    console.log("processFormList",processFormList);
    if(processFormList.value.length == 1 &&processFormList.value[0].formValues.hasOwnProperty('routeName')) {
      customForm.value.disabled = true;
      customForm.value.visible = true;
      customForm.value.formComponent = getFormComponent(processFormList.value[0].formValues.routeName).component;
      customForm.value.model = processFormList.value[0].formValues.formData;
      customForm.value.customFormData = processFormList.value[0].formValues.formData;
      if(data.startUserNode) {
        customForm.value.isNew = true;
        customForm.value.disabled = false;
      }
     console.log("detailProcess customForm",customForm.value);
    }
    else {
      taskFormVisible.value = data.existTaskForm;
      if (taskFormVisible.value) {
        taskFormData.value = data.taskFormData;
      }
      formVisible.value = true;
      nextTick(() => {
        processFormList.value.forEach((item: any, index: any) => {
          if (item.disabled) {
            vFormRenderRef.value[index].disableForm();
          }
        })
      })
    }
    historyProcNodeList.value = data.historyProcNodeList;
    finishedInfo.value = data.flowViewer;

  }
  const onSelectCopyUsers = () => {
    userMultipleSelection.value = copyUser;
    onSelectUsers('添加抄送人', 'copy')
  }
  const onSelectNextUsers = () => {
    userMultipleSelection.value = nextUser;
    onSelectUsers('指定审批人', 'next')
  }
  const onSelectUsers = (title: string, type: string) => {
    userSelectType.value = type;
    userSelectDialog.title = title;
    userSelectDialog.visible = true;
    getTreeSelect();
    getList()
  }
  /** 通过任务 */
  const handleComplete = () => {
    // 校验表单
    taskFormRef.value.validate(async (valid: boolean) => {
      if (valid) {
        const res = await complete(taskForm)
        proxy?.$modal.msgSuccess(res.msg);
        goBack();
      }
    });
  }
  /** 委派任务 */
  const handleDelegate = () => {
    userSelectType.value = 'delegate';
    userSelectDialog.title = '委派任务'
    userSelectDialog.visible = true;
    getTreeSelect();
  }
  /** 转办任务 */
  const handleTransfer = () => {
    userSelectType.value = 'transfer';
    userSelectDialog.title = '转办任务';
    userSelectDialog.visible = true;
    getTreeSelect();
  }
  /** 退回任务 */
  const handleReturn = async () => {
    // 校验表单
    taskFormRef.value.validate(async (valid: boolean) => {
      if (valid) {
        const res = await returnList(taskForm);
        returnTaskList.value = res.data;
        returnDialog.visible = true;
      }
    });
  }
  /** 拒绝任务 */
  const handleReject = async () => {
    await proxy?.$modal.confirm('拒绝审批单流程会终止,是否继续?');
    await rejectTask(taskForm);
    proxy?.$modal.msgSuccess("操作成功");
    goBack();
  }

  /** 返回页面 */
  const goBack = () => {
    // 关闭当前标签页并返回上个页面
    proxy?.$tab.closePage(route);
    router.back()
  }
  // 关闭标签
  const handleClose = (type: any, tag: any) => {
    let userObj = userMultipleSelection.value.find(item => item.userId === tag.id);
    userMultipleSelection.value.splice(userMultipleSelection.value.indexOf(userObj), 1);
    if (type === 'copy') {
      copyUser.value = userMultipleSelection.value;
      // 设置抄送人ID
      if (copyUser.value && copyUser.value.length > 0) {
        const val = copyUser.value.map(item => item.id);
        taskForm.copyUserIds = val instanceof Array ? val.join(',') : val;
      } else {
        taskForm.copyUserIds = '';
      }
    } else if (type === 'next') {
      nextUser.value = userMultipleSelection.value;
      // 设置抄送人ID
      if (nextUser.value && nextUser.value.length > 0) {
        const val = nextUser.value.map(item => item.id);
        taskForm.nextUserIds = val instanceof Array ? val.join(',') : val;
      } else {
        taskForm.nextUserIds = '';
      }
    }
  }
  const changeCurrentUser = (val: any) => {
    // currentUserId = val.userId
  }
  const handleSelectionChange = () => {

  }
  const submitReturn = () => {
    // 校验表单
    taskFormRef.value.validate(async (valid: boolean) => {
      if (valid) {
        if (!returnTaskKey) {
          proxy?.$modal.msgError("请选择退回节点!");
        }
        taskForm.targetKey = returnTaskKey.value;
        const res = await returnTask(taskForm);
        proxy?.$modal.msgSuccess(res.msg);
        goBack()
      }
    });
    console.log("taskForm => ", taskForm.targetKey);
  }
  const submitUserData = () => {
    let type = userSelectType.value;
    if (type === 'copy' || type === 'next') {
      if (!userMultipleSelection || userMultipleSelection.value.length <= 0) {
        proxy?.$modal.msgError("请选择用户");
        return false;
      }
      let userIds = userMultipleSelection.value.map(k => k.userId);
      if (type === 'copy') {
        // 设置抄送人ID信息
        copyUser.value = userMultipleSelection.value;
        taskForm.copyUserIds = userIds instanceof Array ? userIds.join(',') : userIds;
      } else if (type === 'next') {
        // 设置下一级审批人ID信息
        nextUser.value = userMultipleSelection.value;
        taskForm.nextUserIds = userIds instanceof Array ? userIds.join(',') : userIds;
      }
      userSelectDialog.visible = false;
    } else {
      if (!taskForm.comment) {
        proxy?.$modal.msgError("请输入审批意见");
        return false;
      }
      if (!currentUserId.value) {
        proxy?.$modal.msgError("请选择用户");
        return false;
      }
      taskForm.userId = currentUserId.value;
      if (type === 'delegate') {
        delegate(taskForm).then(res => {
          proxy?.$modal.msgSuccess(res.msg);
          goBack();
        });
      }
      if (type === 'transfer') {
        transfer(taskForm).then(res => {
          proxy?.$modal.msgSuccess(res.msg);
          goBack();
        });
      }
    }
  }

  onMounted(() => {
    initData();
  });
</script>

<style lang="scss" scoped>
  .clearfix:before,
  .clearfix:after {
    display: table;
    content: "";
  }
  .clearfix:after {
    clear: both
  }

  .box-card {
    width: 100%;
    margin-bottom: 20px;
  }

  .el-tag + .el-tag {
    margin-left: 10px;
  }

  .el-row {
    margin-bottom: 20px;
    &:last-child {
      margin-bottom: 0;
    }
  }
  .el-col {
    border-radius: 4px;
  }

  .button-new-tag {
    margin-left: 10px;
  }
</style>

2、其中hooks的useFlowable修改如下(原先minixs)

javascript 复制代码
export const useFlowable = () => {
  const allFormComponent = computed(() => {
    return [
        {
          text:'单表示例',
          routeName: '@/views/workflow/demo/wf',
          component: defineAsyncComponent( () => import('@/views/workflow/demo/wf')),
          businessTable:'wf_demo'
        },
        /*{
          text:'主子表示例',
          routeName:'@/views/workflow/demo/modules/CesOrderMainForm',
          component:() => defineAsyncComponent(import(`@/views/workflow/demo/modules/CesOrderMainForm`)),
          businessTable:'ces_order_main'
        }*/
    ]
  })
  const getFormComponent = (routeName) => {
    return allFormComponent.value.find((item) => item.routeName === routeName) || {}
  }
  return {
    allFormComponent,
    getFormComponent
  }
}

3、效果图如下:

相关推荐
我要洋人死4 分钟前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人15 分钟前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人16 分钟前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR21 分钟前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香23 分钟前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q24985969326 分钟前
前端预览word、excel、ppt
前端·word·excel
小华同学ai31 分钟前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书
Gavin_91540 分钟前
【JavaScript】模块化开发
前端·javascript·vue.js
懒大王爱吃狼2 小时前
Python教程:python枚举类定义和使用
开发语言·前端·javascript·python·python基础·python编程·python书籍
逐·風6 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#