zhangjie 3 жил өмнө
parent
commit
7bb4997179

+ 250 - 2
src/assets/styles/pages.scss

@@ -421,6 +421,254 @@
   }
 }
 // modify-exam-config
-// .modify-exam-config {
 
-// }
+// modify-flow-detail
+.modify-flow-detail {
+  .flow-box {
+    display: flex;
+    justify-content: space-between;
+    align-items: stretch;
+  }
+  .flow-property {
+    width: 300px;
+    min-height: 400px;
+    flex-grow: 0;
+    flex-shrink: 0;
+    border: 1px solid $--color-text-gray-5;
+    border-radius: 10px;
+    padding: 15px;
+    background-color: #fff;
+
+    &-title {
+      font-size: 16px;
+      line-height: 1;
+      padding-bottom: 10px;
+      margin-bottom: 10px;
+      border-bottom: 1px solid $--color-text-gray-5;
+    }
+  }
+  .property-part {
+    margin-bottom: 15px;
+    padding-bottom: 15px;
+    border-bottom: 1px solid $--color-text-gray-5;
+
+    &-title {
+      font-size: 14px;
+      line-height: 1;
+      margin-bottom: 10px;
+    }
+  }
+  .property-desc {
+    margin-bottom: 10px;
+  }
+  .flow-radio-v {
+    .el-radio {
+      display: block;
+      margin-bottom: 8px;
+    }
+  }
+  .flow-users {
+    margin-top: 10px;
+  }
+  .user-list {
+    margin-top: 10px;
+
+    .el-tag {
+      margin: 3px;
+    }
+  }
+  .user-clear {
+    padding: 5px 10px;
+    width: 68px;
+    margin: 3px;
+  }
+
+  .flow-content {
+    margin-right: 20px;
+    flex-grow: 1;
+    border: 1px solid $--color-text-gray-5;
+    border-radius: 10px;
+    padding: 10px;
+    position: relative;
+    background-color: #fff;
+  }
+  .flow-main {
+    width: 200px;
+    margin: 30px auto 0;
+  }
+  .flow-node {
+    position: relative;
+    box-shadow: 0 0 0 1px #ccc;
+    border-radius: 10px;
+    margin-bottom: 60px;
+    cursor: pointer;
+
+    &:hover {
+      opacity: 0.9;
+    }
+    &.is-active {
+      opacity: 1;
+      box-shadow: 0 0 0 2px $--color-blue;
+    }
+
+    &-title {
+      padding: 8px 10px;
+      border-bottom: 1px solid #ccc;
+      background-color: $--color-blue;
+      color: #fff;
+      border-top-left-radius: 10px;
+      border-top-right-radius: 10px;
+    }
+    &-content {
+      padding: 10px;
+      min-height: 40px;
+    }
+  }
+  .node-start {
+    cursor: default;
+    .flow-node-content {
+      background-color: mix(#fff, $--color-success, 20%);
+      text-align: center;
+      font-size: 18px;
+      border-radius: 10px;
+      color: #fff;
+    }
+  }
+  .node-end {
+    cursor: default;
+
+    .flow-node-content {
+      background-color: mix(#fff, $--color-danger, 20%);
+      text-align: center;
+      font-size: 18px;
+      border-radius: 10px;
+      color: #fff;
+    }
+  }
+  .flow-link {
+    position: absolute;
+    width: 30px;
+    left: 50%;
+    margin-left: -15px;
+    height: 52px;
+    bottom: -56px;
+
+    &::before {
+      content: "";
+      display: block;
+      position: absolute;
+      border-left: 2px solid $--color-text-gray-2;
+      top: 0;
+      bottom: 2px;
+      left: 50%;
+      margin-left: -1px;
+      z-index: 8;
+    }
+    &::after {
+      content: "";
+      display: block;
+      position: absolute;
+      width: 0;
+      height: 0;
+      border-width: 8px;
+      border-style: solid;
+      border-color: $--color-text-gray-2 transparent transparent transparent;
+      left: 50%;
+      margin-left: -8px;
+      bottom: -8px;
+      z-index: 9;
+    }
+
+    .node-add {
+      position: absolute;
+      width: 24px;
+      height: 24px;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      border-radius: 50%;
+      z-index: 99;
+      background-color: $--color-primary;
+      color: #fff;
+      font-size: 16px;
+      line-height: 25px;
+      text-align: center;
+      cursor: pointer;
+
+      &:hover {
+        background-color: mix(#000, $--color-primary, 10%);
+      }
+    }
+  }
+}
+.select-user-dialog {
+  .user-search {
+    margin-bottom: 5px;
+  }
+  .user-types {
+    font-size: 0;
+  }
+  .user-type {
+    display: inline-block;
+    vertical-align: top;
+    font-size: 14px;
+    height: 28px;
+    width: 50%;
+    padding: 0 10px;
+    line-height: 26px;
+    border: 1px solid #e0e0e0;
+    text-align: center;
+    cursor: pointer;
+    &:hover {
+      border-color: $--color-primary;
+      color: $--color-primary;
+    }
+
+    &.is-active {
+      background-color: $--color-primary;
+      border-color: $--color-primary;
+      color: #fff;
+    }
+  }
+  .user-tree {
+    padding: 5px;
+    border: 1px solid #e0e0e0;
+    height: 300px;
+    overflow: auto;
+  }
+  .user-part-title {
+    height: 28px;
+    line-height: 26px;
+    border-radius: 5px;
+    background-color: #f0f0f0;
+    border: 1px solid #e0e0e0;
+    text-align: center;
+    margin-bottom: 5px;
+  }
+  .user-list {
+    border: 1px solid #e0e0e0;
+    padding: 5px;
+    height: 333px;
+    overflow: auto;
+  }
+  .user-item {
+    margin: 3px 0;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    background-color: #f0f0f0;
+    border-radius: 3px;
+  }
+  .user-cont {
+    line-height: 20px;
+    padding: 4px 6px;
+  }
+  .user-delete {
+    padding: 0;
+    color: $--color-danger;
+
+    &:hover {
+      color: mix(#000, $--color-danger, 10%);
+    }
+  }
+}

+ 6 - 0
src/constants/enumerate.js

@@ -98,6 +98,12 @@ export const EXAM_TYPE_MODE = {
   THREE: "模式3:电子交卷环节不需要提交考务数据"
 };
 
+export const FLOW_TYPE = {
+  ONE: "电子交卷审核",
+  TWO: "题库试题提交审核",
+  THREE: "题库试卷审核"
+};
+
 // 命题 -------------->
 // 待办任务警告时间
 export const TASK_WARNING_TIME = 3 * 24 * 60 * 60 * 1000;

+ 76 - 0
src/modules/base/api.js

@@ -36,6 +36,9 @@ export const userRoleListPage = () => {
 export const syncUserToEcs = () => {
   return $postParam("/api/admin/sys/user/push", {});
 };
+export const roleUserTree = () => {
+  return $postParam("/api/admin/sys/user/org-tree", {});
+};
 
 // role-manage
 export const roleListPage = datas => {
@@ -152,6 +155,79 @@ export const flowDelete = id => {
 export const flowRegister = (datas, headers) => {
   return $post("/api/admin/flow/register", datas, { headers });
 };
+export const updateFlow = datas => {
+  return $post("/api/admin/flow/update", datas);
+};
+export const updateFlowDetail = datas => {
+  return $post("/api/admin/flow/update-detail", datas);
+};
+export const flowNodeList = flowId => {
+  // return $post("/api/admin/flow/nodes", { flowId });
+  return Promise.resolve([
+    {
+      id: 1,
+      type: "START",
+      content: "开始流程",
+      w: 200,
+      h: 45,
+      x: 562,
+      y: 40,
+      property: null
+    },
+    {
+      id: 2,
+      type: "PROCESS",
+      content: "",
+      w: 200,
+      h: 95,
+      x: 562,
+      y: 145,
+      property: {
+        approveUserType: "USER",
+        approveUsers: [{ id: 5, name: "李四 2-1" }],
+        approveRoles: [],
+        copyForUsers: [{ id: 6, name: "李四 2-2" }],
+        multipleUserApproveType: "ORDER",
+        rejectType: "PREV",
+        rejectResubmitType: "NORMAL"
+      }
+    },
+    {
+      id: 3,
+      type: "PROCESS",
+      content: "",
+      w: 200,
+      h: 95,
+      x: 562,
+      y: 301,
+      property: {
+        approveUserType: "USER",
+        approveUsers: [
+          { id: 9, name: "张三 1-1-1" },
+          { id: 10, name: "张三 1-1-2" }
+        ],
+        approveRoles: [],
+        copyForUsers: [
+          { id: 7, name: "李四 3-1" },
+          { id: 8, name: "李四 3-2" }
+        ],
+        multipleUserApproveType: "ORDER",
+        rejectType: "PREV",
+        rejectResubmitType: "NORMAL"
+      }
+    },
+    {
+      id: 4,
+      type: "END",
+      content: "结束流程",
+      w: 200,
+      h: 45,
+      x: 562,
+      y: 455,
+      property: null
+    }
+  ]);
+};
 
 // approve-record
 export const approveRecordListPage = datas => {

+ 6 - 4
src/modules/base/components/ModifyExam.vue

@@ -112,9 +112,11 @@ export default {
         name: [
           {
             required: true,
-            // pattern: /^[0-9a-zA-Z\u4E00-\u9FA5]{1,20}$/,
-            // message: "课程名称只能输入汉字、数字和字母,长度不能超过20",
-            message: "考试名称不能超过100个字",
+            message: "请输入考试名称",
+            trigger: "change"
+          },
+          {
+            message: "考试名称不能超过100个字符",
             max: 100,
             trigger: "change"
           }
@@ -175,7 +177,7 @@ export default {
 
       this.isSubmit = false;
       this.$message.success(this.title + "成功!");
-      this.$emit("modified");
+      this.$emit("modified", { isEdit: this.isEdit, exam: data });
       this.cancel();
     }
   }

+ 151 - 0
src/modules/base/components/ModifyFlow.vue

@@ -0,0 +1,151 @@
+<template>
+  <el-dialog
+    class="modify-flow"
+    :visible.sync="modalIsShow"
+    :title="title"
+    top="10vh"
+    width="500px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    @open="visibleChange"
+  >
+    <el-form
+      ref="modalFormComp"
+      :model="modalForm"
+      :rules="rules"
+      label-position="top"
+    >
+      <el-form-item prop="name" label="流程名称:">
+        <el-input
+          style="width:100%"
+          v-model.trim="modalForm.name"
+          placeholder="请输入名称"
+        ></el-input>
+      </el-form-item>
+      <el-form-item prop="type" label="流程类型:">
+        <el-select
+          v-model="modalForm.type"
+          style="width:100%"
+          placeholder="请选择流程类型"
+          clearable
+          :disabled="isEdit"
+        >
+          <el-option
+            v-for="(val, key) in FLOW_TYPE"
+            :key="key"
+            :value="key"
+            :label="val"
+          ></el-option>
+        </el-select>
+      </el-form-item>
+    </el-form>
+    <div slot="footer">
+      <el-button type="primary" :disabled="isSubmit" @click="submit"
+        >确定</el-button
+      >
+      <el-button @click="cancel">返回</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { FLOW_TYPE } from "../../../constants/enumerate";
+import { updateFlow } from "../api";
+
+const initModalForm = {
+  id: null,
+  name: "",
+  type: ""
+};
+
+export default {
+  name: "modify-flow",
+  props: {
+    instance: {
+      type: Object,
+      default() {
+        return {};
+      }
+    }
+  },
+  computed: {
+    isEdit() {
+      return !!this.instance.id;
+    },
+    title() {
+      return this.isEdit ? "重命名流程" : "新增流程";
+    }
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      isSubmit: false,
+      FLOW_TYPE,
+      modalForm: {},
+      rules: {
+        name: [
+          {
+            required: true,
+            message: "请输入流程名称",
+            trigger: "change"
+          },
+          {
+            message: "流程名称不能超过100个字符",
+            max: 100,
+            trigger: "change"
+          }
+        ],
+        type: [
+          {
+            required: true,
+            message: "请选择流程类型",
+            trigger: "change"
+          }
+        ]
+      }
+    };
+  },
+  methods: {
+    initData(val) {
+      if (val.id) {
+        this.modalForm = this.$objAssign(initModalForm, val);
+      } else {
+        this.modalForm = { ...initModalForm };
+      }
+    },
+    visibleChange() {
+      this.initData(this.instance);
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    async submit() {
+      const valid = await this.$refs.modalFormComp.validate().catch(() => {});
+      if (!valid) return;
+
+      if (!this.isEdit) {
+        this.$emit("modified", { isEdit: this.isEdit, data: this.modalForm });
+        this.cancel();
+        return;
+      }
+
+      if (this.isSubmit) return;
+      this.isSubmit = true;
+      const data = await updateFlow(this.modalForm).catch(() => {
+        this.isSubmit = false;
+      });
+
+      if (!data) return;
+
+      this.isSubmit = false;
+      this.$message.success(this.title + "成功!");
+      this.$emit("modified", { isEdit: this.isEdit, data });
+      this.cancel();
+    }
+  }
+};
+</script>

+ 538 - 0
src/modules/base/components/ModifyFlowDetail.vue

@@ -0,0 +1,538 @@
+<template>
+  <div>
+    <el-dialog
+      class="modify-flow-detail page-dialog"
+      :visible.sync="modalIsShow"
+      title="流程图编辑"
+      :close-on-click-modal="false"
+      :close-on-press-escape="false"
+      :show-close="false"
+      append-to-body
+      fullscreen
+      destroy-on-close
+      @open="visibleChange"
+    >
+      <div class="box-justify" slot="title">
+        <h4 class="el-dialog__title">流程图编辑:{{ instance.name }}</h4>
+        <div>
+          <el-button @click="cancel">取消</el-button>
+          <el-button type="primary" :disabled="isSubmit" @click="submit"
+            >发布</el-button
+          >
+        </div>
+      </div>
+
+      <div class="flow-box">
+        <div class="flow-content">
+          <div class="flow-main">
+            <!-- nodes -->
+            <div
+              v-for="node in nodes"
+              :key="node.id"
+              :id="`node-${node.id}`"
+              :class="[
+                'flow-node',
+                `node-${node.type.toLowerCase()}`,
+                { 'is-active': curNode.id === node.id }
+              ]"
+              @click="toSelectNode(node)"
+            >
+              <div v-if="node.type === 'PROCESS'" class="flow-node-title">
+                <i
+                  v-if="node.type === 'PROCESS'"
+                  :class="nodeIcons['PROCESS']"
+                ></i
+                >审批人
+              </div>
+              <div v-if="node.type === 'PROCESS'" class="flow-node-content">
+                <div v-if="node.property.approveUserType === 'USER'">
+                  <p>
+                    {{
+                      node.property.approveUsers
+                        .map(item => item.name)
+                        .join(",")
+                    }}
+                  </p>
+                  <p v-if="node.property.copyForUsers.length">
+                    抄送:{{
+                      node.property.copyForUsers
+                        .map(item => item.name)
+                        .join(",")
+                    }}
+                  </p>
+                </div>
+                <div v-else>
+                  <p>
+                    {{
+                      node.property.approveRoles
+                        .map(item => item.name)
+                        .join(",")
+                    }}
+                  </p>
+                </div>
+              </div>
+              <div v-else class="flow-node-content">
+                <span><i :class="nodeIcons[node.type]"></i></span>
+                <span>{{ node.content }}</span>
+              </div>
+              <div v-if="node.type !== 'END'" class="flow-link">
+                <div class="node-add" @click.stop="toAddNode(node.id)">
+                  <i class="el-icon-plus"></i>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="flow-property">
+          <div v-if="curNode.id" class="flow-property-main" :key="curNode.id">
+            <h3 class="flow-property-title">节点属性</h3>
+            <div class="property-part">
+              <h4 class="property-part-title">设置审批人</h4>
+              <div class="flow-radio">
+                <el-radio-group
+                  v-model="curNode.property.approveUserType"
+                  size="small"
+                  @change="curNodeChange"
+                >
+                  <el-radio
+                    v-for="(val, key) in APPROVE_USER_TYPE"
+                    :key="key"
+                    :label="key"
+                    >{{ val }}</el-radio
+                  >
+                </el-radio-group>
+              </div>
+              <div
+                v-if="curNode.property.approveUserType === 'USER'"
+                class="flow-users"
+              >
+                <el-button
+                  size="small"
+                  type="primary"
+                  @click="toAddUser('approveUsers')"
+                  >添加成员</el-button
+                >
+                <span class="tips-info">(最多添加5个)</span>
+                <div class="user-list">
+                  <el-tag
+                    v-for="user in curNode.property.approveUsers"
+                    :key="user.id"
+                    size="small"
+                    closable
+                    :disable-transitions="false"
+                    @close="deleteApproveUser(user)"
+                  >
+                    {{ user.name }}
+                  </el-tag>
+                  <el-button
+                    class="user-clear"
+                    type="danger"
+                    size="mini"
+                    plain
+                    @click="clearApproveUsers"
+                    >清空</el-button
+                  >
+                </div>
+              </div>
+              <div v-else class="flow-users">
+                <el-button size="small" type="primary" @click="toAddApproveRole"
+                  >添加角色</el-button
+                >
+                <div class="user-list">
+                  <el-tag
+                    v-for="role in curNode.property.approveRoles"
+                    :key="role.id"
+                    size="small"
+                    closable
+                    :disable-transitions="false"
+                    @close="deleteApproveRole(role)"
+                  >
+                    {{ role.name }}
+                  </el-tag>
+                  <el-button
+                    class="user-clear"
+                    type="danger"
+                    size="mini"
+                    plain
+                    @click="clearApproveRole"
+                    >清空</el-button
+                  >
+                </div>
+              </div>
+            </div>
+            <div
+              v-if="curNode.property.approveUserType === 'USER'"
+              class="property-part"
+            >
+              <h4 class="property-part-title">设置抄送人</h4>
+              <div class="flow-users">
+                <el-button
+                  size="small"
+                  type="primary"
+                  @click="toAddUser('copyForUsers')"
+                  >添加成员</el-button
+                >
+                <span class="tips-info">(最多添加5个)</span>
+                <div class="user-list">
+                  <el-tag
+                    v-for="user in curNode.property.copyForUsers"
+                    :key="user.id"
+                    closable
+                    size="small"
+                    :disable-transitions="false"
+                    @close="deleteCopyForUser(user)"
+                  >
+                    {{ user.name }}
+                  </el-tag>
+                  <el-button
+                    class="user-clear"
+                    type="danger"
+                    size="mini"
+                    plain
+                    @click="clearCopyForUsers"
+                    >清空</el-button
+                  >
+                </div>
+              </div>
+            </div>
+            <div class="property-part">
+              <h4 class="property-part-title">高级设置</h4>
+              <p class="property-desc">多人审批时采用的审批方式</p>
+              <div class="flow-radio-v">
+                <el-radio-group
+                  v-model="curNode.property.multipleUserApproveType"
+                  size="small"
+                  @change="curNodeChange"
+                >
+                  <el-radio
+                    v-for="(val, key) in MULTIPLE_USER_APPROVE_TYPE"
+                    :key="key"
+                    :label="key"
+                    >{{ val }}</el-radio
+                  >
+                </el-radio-group>
+              </div>
+            </div>
+            <div class="property-part">
+              <h4 class="property-part-title">驳回设置</h4>
+              <p class="property-desc">允许驳回节点</p>
+              <div class="flow-radio-v">
+                <el-radio-group
+                  v-model="curNode.property.rejectType"
+                  size="small"
+                  @change="curNodeChange"
+                >
+                  <el-radio
+                    v-for="(val, key) in REJECT_TYPE"
+                    :key="key"
+                    :label="key"
+                    >{{ val }}</el-radio
+                  >
+                </el-radio-group>
+              </div>
+              <p class="property-desc">驳回后提交方式</p>
+              <div class="flow-radio-v">
+                <el-radio-group
+                  v-model="curNode.property.rejectResubmitType"
+                  size="small"
+                  @change="curNodeChange"
+                >
+                  <el-radio
+                    v-for="(val, key) in REJECT_RESUBMIT_TYPE"
+                    :key="key"
+                    :label="key"
+                    >{{ val }}</el-radio
+                  >
+                </el-radio-group>
+              </div>
+            </div>
+            <el-button size="mini" type="danger" plain @click="toDeleteNode"
+              >删除节点</el-button
+            >
+          </div>
+        </div>
+      </div>
+    </el-dialog>
+
+    <!-- SelectUserDialog -->
+    <select-user-dialog
+      ref="SelectUserDialog"
+      :users="curAddUsers"
+      @modified="userModified"
+    ></select-user-dialog>
+    <!-- SelectRoleDialog -->
+    <select-role-dialog
+      ref="SelectRoleDialog"
+      :data="curAddRoles"
+      @modified="roleModified"
+    ></select-role-dialog>
+  </div>
+</template>
+
+<script>
+import { deepCopy } from "../../../plugins/utils";
+import { flowNodeList, updateFlowDetail } from "../api";
+import SelectUserDialog from "./SelectUserDialog";
+import SelectRoleDialog from "./SelectRoleDialog";
+
+const DEFAULT_NODE = {
+  id: "",
+  type: "PROCESS",
+  content: "",
+  w: 150,
+  h: 40,
+  x: 0,
+  y: 0,
+  property: {
+    approveUserType: "USER",
+    approveUsers: [],
+    approveRoles: [],
+    copyForUsers: [],
+    multipleUserApproveType: "ORDER",
+    rejectType: "PREV",
+    rejectResubmitType: "NORMAL"
+  }
+};
+
+export default {
+  name: "modify-flow-detail",
+  components: { SelectUserDialog, SelectRoleDialog },
+  props: {
+    instance: {
+      type: Object,
+      default() {
+        return {};
+      }
+    }
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      isSubmit: false,
+      initNodes: [
+        {
+          id: "1",
+          type: "START",
+          content: "开始流程",
+          w: 150,
+          h: 40,
+          x: 0,
+          y: 0,
+          property: null
+        },
+        {
+          id: "2",
+          type: "END",
+          content: "结束流程",
+          w: 150,
+          h: 40,
+          x: 0,
+          y: 0,
+          property: null
+        }
+      ],
+      nodes: [],
+      curNode: {},
+      curAddUsers: [],
+      curAddRoles: [],
+      curAddUserType: "",
+      nodeIcons: {
+        START: "el-icon-video-play",
+        END: "el-icon-switch-button",
+        PROCESS: "el-icon-circle-plus-outline"
+      },
+      APPROVE_USER_TYPE: {
+        USER: "成员",
+        ROLE: "角色"
+      },
+      MULTIPLE_USER_APPROVE_TYPE: {
+        ORDER: "依次审批",
+        ALL: "会签(所有人必须审批)",
+        SOME: "或签(一名审批人同意或拒绝即可)"
+      },
+      REJECT_TYPE: {
+        PREV: "上一节点",
+        START: "发起人节点",
+        PREV_ALL: "该节点前全部节点"
+      },
+      REJECT_RESUBMIT_TYPE: {
+        NORMAL: "按正常流程提交",
+        PREV_STEP: "提交到驳回节点"
+      }
+    };
+  },
+  methods: {
+    async initData(val) {
+      if (val.id) {
+        const data = await flowNodeList(val.id);
+        this.nodes = data;
+        this.toSelectNode(this.nodes[1]);
+      } else {
+        this.nodes = deepCopy(this.initNodes);
+        this.toSelectNode(this.nodes[1]);
+      }
+    },
+    visibleChange() {
+      this.initData(this.instance);
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    getDefaultNode() {
+      const nodeId = this.nodes.length
+        ? Math.max.apply(
+            null,
+            this.nodes.map(item => item.id)
+          ) + 1
+        : 1;
+      return { ...deepCopy(DEFAULT_NODE), id: nodeId };
+    },
+    toAddNode(curNodeId) {
+      const newNode = this.getDefaultNode();
+      const pos = this.nodes.findIndex(node => node.id === curNodeId);
+      this.nodes.splice(pos + 1, 0, newNode);
+      this.toSelectNode(newNode);
+    },
+    toDeleteNode() {
+      const pos = this.nodes.findIndex(node => node.id === this.curNode.id);
+      this.nodes.splice(pos, 1);
+      if (this.nodes[pos]) {
+        this.toSelectNode(this.nodes[pos]);
+      } else {
+        let prevPos = pos - 1;
+        prevPos = Math.max(prevPos, 0);
+        this.toSelectNode(this.nodes[prevPos]);
+      }
+    },
+    toSelectNode(node) {
+      if (node.type === "START" || node.type === "END") return;
+      this.curNode = node ? deepCopy(node) : {};
+    },
+    curNodeChange() {
+      const pos = this.nodes.findIndex(node => node.id === this.curNode.id);
+      this.nodes.splice(pos, 1, deepCopy(this.curNode));
+    },
+    toAddUser(type) {
+      this.curAddUserType = type;
+      this.curAddUsers = this.curNode.property[this.curAddUserType];
+      this.$refs.SelectUserDialog.open();
+    },
+    userModified(users) {
+      this.curNode.property[this.curAddUserType] = users;
+      this.curNodeChange();
+    },
+    deleteApproveUser(user) {
+      console.log(user);
+      const pos = this.curNode.property.approveUsers.findIndex(
+        item => item.id === user.id
+      );
+      this.curNode.property.approveUsers.splice(pos, 1);
+      this.curNodeChange();
+    },
+    clearApproveUsers() {
+      this.curNode.property.approveUsers = [];
+      this.curNodeChange();
+    },
+    deleteCopyForUser(user) {
+      const pos = this.curNode.property.copyForUsers.findIndex(
+        item => item.id === user.id
+      );
+      this.curNode.property.copyForUsers.splice(pos, 1);
+      this.curNodeChange();
+    },
+    clearCopyForUsers() {
+      this.curNode.property.copyForUsers = [];
+      this.curNodeChange();
+    },
+    toAddApproveRole() {
+      this.curAddRoles = this.curNode.property.approveRoles;
+      this.$refs.SelectRoleDialog.open();
+    },
+    roleModified(roles) {
+      this.curNode.property.approveRoles = roles;
+      this.curNodeChange();
+    },
+    deleteApproveRole(role) {
+      const pos = this.curNode.property.approveRoles.findIndex(
+        item => item.id === role.id
+      );
+      this.curNode.property.approveRoles.splice(pos, 1);
+      this.curNodeChange();
+    },
+    clearApproveRole() {
+      this.curNode.property.approveRoles = [];
+      this.curNodeChange();
+    },
+    checkData() {
+      if (!this.nodes.some(node => node.type === "PROCESS")) {
+        this.$message.error("请设置过程节点");
+        return;
+      }
+
+      const nodeUserValid = !this.nodes
+        .filter(node => node.type === "PROCESS")
+        .some(node => {
+          if (node.property.approveUserType === "USER") {
+            return !(
+              node.property.approveUsers.length &&
+              node.property.copyForUsers.length
+            );
+          } else {
+            return !node.property.approveRoles.length;
+          }
+        });
+      if (!nodeUserValid) {
+        this.$message.error("请完成节点设置");
+        return;
+      }
+
+      return true;
+    },
+    async submit() {
+      if (this.isSubmit) return;
+      if (!this.checkData()) return;
+
+      this.nodes.forEach(node => {
+        const dom = document.getElementById(`node-${node.id}`);
+        node.w = dom.clientWidth;
+        node.h = dom.clientHeight;
+        node.x = dom.offsetLeft;
+        node.y = dom.offsetTop;
+      });
+
+      const nodes = this.nodes.map((node, index) => {
+        let nnode = deepCopy(node);
+        nnode.id = index + 1;
+        if (node.property) {
+          if (node.property.approveUserType === "USER") {
+            nnode.approveRoles = [];
+          } else {
+            nnode.approveUsers = [];
+            nnode.copyForUsers = [];
+          }
+        }
+
+        return nnode;
+      });
+
+      this.isSubmit = true;
+
+      const res = await updateFlowDetail({
+        id: this.instance.id,
+        name: this.instance.name,
+        type: this.instance.type,
+        nodes: nodes
+      }).catch(() => {});
+      this.isSubmit = false;
+      if (!res) return;
+
+      this.$message.success("编辑成功!");
+      this.$emit("modified");
+      this.cancel();
+    }
+  }
+};
+</script>

+ 152 - 0
src/modules/base/components/SelectRoleDialog.vue

@@ -0,0 +1,152 @@
+<template>
+  <el-dialog
+    class="select-user-dialog"
+    :visible.sync="modalIsShow"
+    title="选择角色"
+    top="10px"
+    width="600px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    @opened="visibleChange"
+  >
+    <el-row type="flex" :gutter="10">
+      <el-col :span="12">
+        <div class="user-part-title">角色</div>
+        <div class="user-search">
+          <el-input
+            v-model="filterLabel"
+            placeholder="请输入角色名称"
+            clearable
+            size="mini"
+            prefix-icon="el-icon-search"
+            @input="labelChange"
+          ></el-input>
+        </div>
+        <div class="user-tree">
+          <el-tree
+            ref="RoleTree"
+            :data="roleTree"
+            show-checkbox
+            check-on-click-node
+            node-key="id"
+            :default-checked-keys="selectedRoleIds"
+            :props="defaultProps"
+            @check="roleChange"
+          >
+          </el-tree>
+        </div>
+      </el-col>
+      <el-col :span="12">
+        <div class="user-part-title">已选范围</div>
+        <div class="user-list">
+          <div v-for="role in selectedRoles" :key="role.id" class="user-item">
+            <p class="user-cont">
+              <span>{{ role.name }}</span
+              ><span>{{ role.phoneNumber }}</span>
+            </p>
+            <el-button
+              class="user-delete"
+              type="text"
+              icon="el-icon-remove"
+              @click="toDeleteRole(role)"
+            ></el-button>
+          </div>
+        </div>
+      </el-col>
+    </el-row>
+
+    <div slot="footer">
+      <el-button type="primary" @click="submit">确认</el-button>
+      <el-button @click="cancel">取消</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { userRoleListPage } from "../api";
+
+export default {
+  name: "select-role-dialog",
+  props: {
+    data: {
+      type: Array,
+      default() {
+        return [];
+      }
+    },
+    multiple: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      filterLabel: "",
+      roles: [],
+      roleTree: [],
+      selectedRoles: [],
+      selectedRoleIds: [],
+      defaultProps: {
+        children: "children",
+        label: "name"
+      }
+    };
+  },
+  mounted() {
+    this.getRoles();
+  },
+  methods: {
+    async getRoles() {
+      const data = await userRoleListPage();
+      this.roles = data || [];
+      this.roles = this.roles.filter(item => item.type !== "ADMIN");
+
+      this.labelChange();
+    },
+    labelChange() {
+      const escapeRegexpString = (value = "") =>
+        String(value).replace(/[|\\{}()[\]^$+*?.]/g, "\\$&");
+      const reg = new RegExp(escapeRegexpString(this.filterLabel), "i");
+
+      this.roleTree = this.roles.filter(item => reg.test(item.name));
+    },
+    visibleChange() {
+      this.filterLabel = "";
+      this.labelChange();
+      this.selectedRoles = this.data;
+      this.selectedRoleIds = this.data.map(item => item.id);
+      this.$refs.RoleTree.setCheckedKeys(this.selectedRoleIds);
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    roleChange(data) {
+      if (!this.multiple) {
+        this.$refs.RoleTree.setCheckedKeys([data.id]);
+      }
+      this.selectedRoles = this.$refs.RoleTree.getCheckedNodes(true);
+      this.selectedRoleIds = this.selectedRoles.map(item => item.id);
+    },
+    toDeleteRole(role) {
+      const pos = this.selectedRoles.findIndex(item => item.id === role.id);
+      this.selectedRoles.splice(pos, 1);
+      this.selectedRoleIds = this.selectedRoles.map(item => item.id);
+      this.$refs.RoleTree.setCheckedKeys(this.selectedRoleIds);
+    },
+    submit() {
+      if (!this.selectedRoles.length) {
+        this.$message.error("请选择角色");
+        return;
+      }
+
+      this.$emit("modified", this.selectedRoles);
+      this.cancel();
+    }
+  }
+};
+</script>

+ 223 - 0
src/modules/base/components/SelectUserDialog.vue

@@ -0,0 +1,223 @@
+<template>
+  <el-dialog
+    class="select-user-dialog"
+    :visible.sync="modalIsShow"
+    title="添加成员"
+    top="10px"
+    width="600px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    @opened="visibleChange"
+  >
+    <el-row type="flex" :gutter="10">
+      <el-col :span="12">
+        <div class="user-part-title">组织架构</div>
+        <div class="user-search">
+          <el-input
+            v-model="filterLabel"
+            placeholder="请输入角色名称"
+            clearable
+            size="mini"
+            prefix-icon="el-icon-search"
+            @input="labelChange"
+          ></el-input>
+        </div>
+        <div class="user-tree">
+          <el-tree
+            ref="UserTree"
+            :data="userTree"
+            show-checkbox
+            check-on-click-node
+            node-key="id"
+            :default-checked-keys="selectedUserIds"
+            :props="defaultProps"
+            @check-change="userChange"
+          >
+          </el-tree>
+        </div>
+      </el-col>
+      <el-col :span="12">
+        <div class="user-part-title">已选范围</div>
+        <div class="user-list">
+          <div v-for="user in selectedUsers" :key="user.id" class="user-item">
+            <p class="user-cont">
+              <span>{{ user.name }}</span
+              ><span>{{ user.phoneNumber }}</span>
+            </p>
+            <el-button
+              class="user-delete"
+              type="text"
+              icon="el-icon-remove"
+              @click="toDeleteUser(user)"
+            ></el-button>
+          </div>
+        </div>
+      </el-col>
+    </el-row>
+
+    <div slot="footer">
+      <el-button type="primary" @click="submit">确认</el-button>
+      <el-button @click="cancel">取消</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+// import { roleUserTree } from "../api";
+
+export default {
+  name: "select-user-dialog",
+  props: {
+    users: {
+      type: Array,
+      default() {
+        return [];
+      }
+    },
+    userLimitCount: {
+      type: Number,
+      default: 5
+    }
+  },
+  data() {
+    return {
+      modalIsShow: false,
+      filterLabel: "",
+      orgUsers: [],
+      userTree: [],
+      userList: [],
+      selectedUsers: [],
+      selectedUserIds: [],
+      defaultProps: {
+        children: "children",
+        label: "name"
+      }
+    };
+  },
+  mounted() {
+    this.getOrgUser();
+  },
+  methods: {
+    async getOrgUser() {
+      // const data = await roleUserTree();
+      // this.orgUsers = data;
+      this.orgUsers = [
+        {
+          id: 1,
+          name: "一级 1",
+          children: [
+            {
+              id: 4,
+              name: "二级 1-1",
+              children: [
+                {
+                  id: 9,
+                  name: "张三 1-1-1"
+                },
+                {
+                  id: 10,
+                  name: "张三 1-1-2"
+                }
+              ]
+            }
+          ]
+        },
+        {
+          id: 2,
+          name: "一级 2",
+          children: [
+            {
+              id: 5,
+              name: "李四 2-1"
+            },
+            {
+              id: 6,
+              name: "李四 2-2"
+            }
+          ]
+        },
+        {
+          id: 3,
+          name: "一级 3",
+          children: [
+            {
+              id: 7,
+              name: "李四 3-1"
+            },
+            {
+              id: 8,
+              name: "李四 3-2"
+            }
+          ]
+        }
+      ];
+      this.userTree = this.orgUsers;
+      this.getUserList();
+    },
+    getUserList() {
+      let userList = [];
+      const fetchUser = users => {
+        users.forEach(item => {
+          if (item["children"] && item["children"].length) {
+            fetchUser(item.children);
+          } else {
+            userList.push({ id: item.id, name: item.name });
+          }
+        });
+      };
+      fetchUser(this.orgUsers);
+
+      this.userList = userList;
+    },
+    labelChange() {
+      if (!this.filterLabel) {
+        this.userTree = this.orgUsers;
+      } else {
+        const escapeRegexpString = (value = "") =>
+          String(value).replace(/[|\\{}()[\]^$+*?.]/g, "\\$&");
+        const reg = new RegExp(escapeRegexpString(this.filterLabel), "i");
+
+        this.userTree = this.userList.filter(item => reg.test(item.name));
+      }
+    },
+    visibleChange() {
+      this.filterLabel = "";
+      this.labelChange();
+      this.selectedUsers = this.users;
+      this.selectedUserIds = this.users.map(item => item.id);
+      this.$refs.UserTree.setCheckedKeys(this.selectedUserIds);
+    },
+    cancel() {
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    userChange() {
+      this.selectedUsers = this.$refs.UserTree.getCheckedNodes(true);
+      this.selectedUserIds = this.selectedUsers.map(item => item.id);
+    },
+    toDeleteUser(user) {
+      const pos = this.selectedUsers.findIndex(item => item.id === user.id);
+      this.selectedUsers.splice(pos, 1);
+      this.selectedUserIds = this.selectedUsers.map(item => item.id);
+      this.$refs.UserTree.setCheckedKeys(this.selectedUserIds);
+    },
+    submit() {
+      if (!this.selectedUsers.length) {
+        this.$message.error("请选择用户");
+        return;
+      }
+
+      if (this.selectedUsers.length > this.userLimitCount) {
+        this.$message.error(`选择用户数不能超过${this.userLimitCount}`);
+        return;
+      }
+
+      this.$emit("modified", this.selectedUsers);
+      this.cancel();
+    }
+  }
+};
+</script>

+ 21 - 2
src/modules/base/views/ExamManage.vue

@@ -80,7 +80,10 @@
               >编辑</el-button
             >
             <el-button
-              v-if="checkPrivilege('link', 'edit')"
+              v-if="
+                checkPrivilege('link', 'edit') &&
+                  scope.row.examType === 'OFFLINE'
+              "
               class="btn-primary"
               type="text"
               @click="toEditConfig(scope.row)"
@@ -119,7 +122,7 @@
     <modify-exam
       ref="ModifyExam"
       :instance="curExam"
-      @modified="getList"
+      @modified="examModified"
     ></modify-exam>
     <!-- modify-exam-config -->
     <modify-exam-config
@@ -158,6 +161,16 @@ export default {
           examType: "OFFLINE",
           examTypeMode: "ONE",
           enable: true
+        },
+        {
+          id: "12",
+          name: "齐13考试",
+          semesterName: "上学期",
+          createTime: 19245121323,
+          semesterId: "12",
+          examType: "ONLINE",
+          examTypeMode: "ONE",
+          enable: true
         }
       ],
       curExam: {}
@@ -194,6 +207,12 @@ export default {
       this.curExam = row;
       this.$refs.ModifyExam.open();
     },
+    examModified({ isEdit, exam }) {
+      if (!isEdit && exam.examType === "OFFLINE") {
+        this.toEditConfig(exam);
+      }
+      this.getList();
+    },
     toEditConfig(row) {
       this.curExam = row;
       this.$refs.ModifyExamConfig.open();

+ 48 - 3
src/modules/base/views/FlowManage.vue

@@ -40,8 +40,22 @@
           :index="indexMethod"
         ></el-table-column>
         <el-table-column prop="name" label="流程名称"></el-table-column>
-        <el-table-column class-name="action-column" label="操作" width="120px">
+        <el-table-column class-name="action-column" label="操作" width="160">
           <template slot-scope="scope">
+            <el-button
+              v-if="!checkPrivilege('link', 'edit')"
+              class="btn-primary"
+              type="text"
+              @click="toEditDetail(scope.row)"
+              >编辑</el-button
+            >
+            <el-button
+              v-if="!checkPrivilege('link', 'edit')"
+              class="btn-primary"
+              type="text"
+              @click="toEdit(scope.row)"
+              >重命名</el-button
+            >
             <el-button
               v-if="checkPrivilege('link', 'delete')"
               class="btn-danger"
@@ -64,6 +78,17 @@
         </el-pagination>
       </div>
     </div>
+    <!-- ModifyFlow -->
+    <modify-flow
+      ref="ModifyFlow"
+      :instance="curFlow"
+      @modified="flowModified"
+    ></modify-flow>
+    <!-- ModifyFlowDetail -->
+    <modify-flow-detail
+      ref="ModifyFlowDetail"
+      :instance="curFlow"
+    ></modify-flow-detail>
     <!-- RegistFlowDialog -->
     <regist-flow-dialog
       ref="RegistFlowDialog"
@@ -75,11 +100,15 @@
 <script>
 import { flowListPage, flowPublish, flowDelete } from "../api";
 import RegistFlowDialog from "../components/RegistFlowDialog";
+import ModifyFlow from "../components/ModifyFlow";
+import ModifyFlowDetail from "../components/ModifyFlowDetail";
 
 export default {
   name: "flow-manage",
   components: {
-    RegistFlowDialog
+    RegistFlowDialog,
+    ModifyFlow,
+    ModifyFlowDetail
   },
   data() {
     return {
@@ -115,7 +144,23 @@ export default {
     },
     toAdd() {
       this.curFlow = {};
-      this.$refs.RegistFlowDialog.open();
+      this.$refs.ModifyFlow.open();
+    },
+    toEdit(row) {
+      this.curFlow = { ...row };
+      this.$refs.ModifyFlow.open();
+    },
+    toEditDetail(row) {
+      this.curFlow = { ...row };
+      this.$refs.ModifyFlowDetail.open();
+    },
+    flowModified({ isEdit, data }) {
+      if (!isEdit) {
+        this.curFlow = data;
+        this.$refs.ModifyFlowDetail.open();
+        return;
+      }
+      this.getList();
     },
     toPublish(row) {
       if (row.publish) return;