yyb
7 小时以前 0fc574668fcd7d262d14f7a9bb9c76c6f466b9f6
OA菜单模块
已添加36个文件
9350 ■■■■■ 文件已修改
src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/AttendManage/leave-apply/index.vue 934 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue 916 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ContractManage/purchase-contract/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ContractManage/sale-contract/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/HrManage/post-manage/index.vue 292 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/HrManage/regular-apply/index.vue 676 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/HrManage/resign-apply/components/formDia.vue 347 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/HrManage/resign-apply/index.vue 245 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/HrManage/staff-archive/components/BasicInfoSection.vue 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/HrManage/staff-archive/components/EducationWorkSection.vue 263 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/HrManage/staff-archive/components/EmergencyAndAttachmentSection.vue 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/HrManage/staff-archive/components/JobInfoSection.vue 176 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/HrManage/staff-archive/components/NewOrEditFormDia.vue 304 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/HrManage/staff-archive/components/RenewContract.vue 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/HrManage/staff-archive/components/Show.vue 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/HrManage/staff-archive/index.vue 407 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/HrManage/staff-contract/components/formDia.vue 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/HrManage/staff-contract/filesDia.vue 197 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/HrManage/staff-contract/index.vue 314 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/HrManage/transfer-apply/index.vue 792 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/HrManage/work-handover/index.vue 810 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/SysAdmin/dept-manage/index.vue 291 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/SysAdmin/log-manage/index.vue 315 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/SysAdmin/user-manage/authRole.vue 123 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/SysAdmin/user-manage/index.vue 550 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/SysAdmin/user-manage/profile/index.vue 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/SysAdmin/user-manage/profile/resetPwd.vue 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/SysAdmin/user-manage/profile/userAvatar.vue 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/SysAdmin/user-manage/profile/userInfo.vue 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/SysMonitor/cache-monitor/index.vue 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/SysMonitor/data-monitor/index.vue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/SysMonitor/server-monitor/index.vue 191 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
<!--
  æ¨¡å—中文名:审批列表
  ç›®å½•标识:ApproveManage/approve-list(approve-list â†’ ä¸­æ–‡ï¼šå®¡æ‰¹åˆ—表)
  å¤ç”¨é¡µé¢ï¼š@/views/procurementManagement/procurementLedger/index.vue(采购台账;文件名 index.vue â†’ å…¥å£é¡µï¼‰
-->
<template>
  <ProcurementLedger />
</template>
<script setup>
import ProcurementLedger from '@/views/procurementManagement/procurementLedger/index.vue'
</script>
src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
<!--
  æ¨¡å—中文名:审批模板
  ç›®å½•标识:ApproveManage/approve-template(approve-template â†’ ä¸­æ–‡ï¼šå®¡æ‰¹æ¨¡æ¿ï¼‰
  å¤ç”¨é¡µé¢ï¼š@/views/procurementManagement/procurementLedger/index.vue(采购台账;文件名 index.vue â†’ å…¥å£é¡µï¼‰
-->
<template>
  <ProcurementLedger />
</template>
<script setup>
import ProcurementLedger from '@/views/procurementManagement/procurementLedger/index.vue'
</script>
src/views/officeProcessAutomation/AttendManage/leave-apply/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,934 @@
<!--OA模块:请假申请(字段为前端占位,后期与后端接口对齐)-->
<template>
  <div class="app-container">
    <div class="search_form mb20">
      <div>
        <span class="search_title">申请人:</span>
        <el-input
          v-model="searchForm.applicantKeyword"
          style="width: 220px"
          placeholder="姓名或编号"
          clearable
          :prefix-icon="Search"
          @keyup.enter="handleQuery"
        />
        <span class="search_title" style="margin-left: 12px">请假类型:</span>
        <el-select v-model="searchForm.leaveType" placeholder="全部" clearable style="width: 180px">
          <el-option v-for="opt in LEAVE_TYPE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" />
        </el-select>
        <el-button type="primary" style="margin-left: 10px" @click="handleQuery">搜索</el-button>
        <el-button @click="resetSearch">重置</el-button>
      </div>
      <div>
        <el-button type="primary" @click="openFormDialog('add')">新增请假申请</el-button>
      </div>
    </div>
    <div class="table_list">
      <PIMTable
        rowKey="id"
        :column="tableColumn"
        :tableData="tableData"
        :page="page"
        :isSelection="false"
        :tableLoading="tableLoading"
        @pagination="pagination"
        :total="page.total"
      />
    </div>
    <!-- æ–°å¢ž / ç¼–辑 -->
    <el-dialog
      v-model="formDialog.visible"
      :title="formDialog.title"
      width="960px"
      append-to-body
      destroy-on-close
      class="leave-apply-form-dialog"
      @closed="onFormClosed"
    >
      <el-form ref="formRef" :model="form" :rules="formRules" label-width="140px" class="leave-apply-form">
        <el-row :gutter="24">
          <el-col :span="24">
            <el-form-item label="申请人" prop="applicantId">
              <el-select
                v-model="form.applicantId"
                filterable
                remote
                clearable
                reserve-keyword
                placeholder="请选择或搜索申请人"
                style="width: 100%"
                :remote-method="remoteSearchApplicantForm"
                :loading="applicantFormSearchLoading"
                @change="onApplicantChange"
              >
                <el-option
                  v-for="u in applicantFormOptions"
                  :key="u.userId"
                  :label="userSelectLabel(u)"
                  :value="u.userId"
                />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="12">
            <el-form-item label="请假类型" prop="leaveType">
              <el-select v-model="form.leaveType" placeholder="请选择请假类型" clearable filterable style="width: 100%">
                <el-option v-for="opt in LEAVE_TYPE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="假期余额" prop="leaveBalanceDays">
              <el-input-number
                v-model="form.leaveBalanceDays"
                :min="0"
                :max="999"
                :precision="2"
                :step="0.5"
                controls-position="right"
                placeholder="天"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="12">
            <el-form-item label="请假开始时间" prop="leaveStartTime">
              <el-date-picker
                v-model="form.leaveStartTime"
                type="datetime"
                placeholder="请选择开始时间"
                format="YYYY-MM-DD HH:mm:ss"
                value-format="YYYY-MM-DD HH:mm:ss"
                style="width: 100%"
                @change="onLeaveRangeChange"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="请假结束时间" prop="leaveEndTime">
              <el-date-picker
                v-model="form.leaveEndTime"
                type="datetime"
                placeholder="请选择结束时间"
                format="YYYY-MM-DD HH:mm:ss"
                value-format="YYYY-MM-DD HH:mm:ss"
                style="width: 100%"
                @change="onLeaveRangeChange"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="12">
            <el-form-item label="请假时长">
              <el-input :model-value="leaveDurationDisplay" readonly placeholder="根据起止时间自动计算">
                <template #append>天</template>
              </el-input>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="审批方式" prop="approvalMode">
              <el-radio-group v-model="form.approvalMode">
                <el-radio value="parallel">与签</el-radio>
                <el-radio value="or_sign">或签</el-radio>
              </el-radio-group>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="24">
            <el-form-item label="审批人" prop="approverIds">
              <el-tree-select
                v-model="form.approverIds"
                :data="approverTreeData"
                multiple
                collapse-tags
                collapse-tags-tooltip
                :max-collapse-tags="2"
                :render-after-expand="false"
                placeholder="请选择审批人(可多选)"
                style="width: 100%"
                :props="{ value: 'id', label: 'label', children: 'children', disabled: 'disabled' }"
                check-strictly
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="24">
            <el-form-item label="请假事由" prop="leaveReason">
              <el-input
                v-model="form.leaveReason"
                type="textarea"
                :rows="4"
                placeholder="请填写请假事由"
                maxlength="2000"
                show-word-limit
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="24">
            <el-form-item label="附件">
              <div class="upload-block">
                <FileUpload v-model:file-list="form.attachmentList" :limit="10" button-text="点击选择文件" />
              </div>
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
          <el-button @click="formDialog.visible = false">取 æ¶ˆ</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- è¯¦æƒ… -->
    <el-dialog v-model="detailDialog.visible" title="请假申请详情" width="720px" append-to-body>
      <el-descriptions :column="1" border>
        <el-descriptions-item label="申请人编号">{{ detailRow.applicantNo || "—" }}</el-descriptions-item>
        <el-descriptions-item label="申请人">{{ detailRow.applicantName }}</el-descriptions-item>
        <el-descriptions-item label="请假类型">{{ leaveTypeLabel(detailRow.leaveType) }}</el-descriptions-item>
        <el-descriptions-item label="假期余额">{{ formatBalance(detailRow.leaveBalanceDays) }}</el-descriptions-item>
        <el-descriptions-item label="请假开始时间">{{ detailRow.leaveStartTime || "—" }}</el-descriptions-item>
        <el-descriptions-item label="请假结束时间">{{ detailRow.leaveEndTime || "—" }}</el-descriptions-item>
        <el-descriptions-item label="请假时长">{{ formatDuration(detailRow.leaveDurationDays) }}</el-descriptions-item>
        <el-descriptions-item label="请假事由">{{ detailRow.leaveReason }}</el-descriptions-item>
        <el-descriptions-item label="审批结果">{{ approvalResultLabel(detailRow.approvalResult) }}</el-descriptions-item>
        <el-descriptions-item label="审批方式">{{ approvalModeLabel(detailRow.approvalMode) }}</el-descriptions-item>
        <el-descriptions-item label="审批人">{{ detailRow.approverNames || "—" }}</el-descriptions-item>
        <el-descriptions-item label="创建时间">{{ detailRow.createTime || "—" }}</el-descriptions-item>
        <el-descriptions-item label="附件">
          <template v-if="detailRow.attachmentList?.length">
            <el-tag v-for="(f, i) in detailRow.attachmentList" :key="i" class="mr6 mb6" type="info">
              {{ f.name }}
            </el-tag>
          </template>
          <span v-else>无</span>
        </el-descriptions-item>
      </el-descriptions>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="detailDialog.visible = false">关 é—­</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- é™„件列表 -->
    <el-dialog v-model="filesDialog.visible" title="附件" width="520px" append-to-body>
      <el-table v-if="filesDialog.row?.attachmentList?.length" :data="filesDialog.row.attachmentList" border>
        <el-table-column type="index" label="序号" width="60" align="center" />
        <el-table-column prop="name" label="文件名" min-width="200" show-overflow-tooltip />
        <el-table-column label="操作" width="100" align="center">
          <template #default="{ row }">
            <el-button link type="primary" @click="mockDownload(row)">下载</el-button>
          </template>
        </el-table-column>
      </el-table>
      <el-empty v-else description="暂无附件" />
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="filesDialog.visible = false">关 é—­</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { Search } from "@element-plus/icons-vue";
import dayjs from "dayjs";
import FileUpload from "@/components/AttachmentUpload/file/index.vue";
import { deptTreeSelect, userListNoPageByTenantId } from "@/api/system/user.js";
import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref, watch } from "vue";
/** è¯·å‡ç±»åž‹ï¼ˆvalue ä¸ŽåŽç«¯å¯¹é½å ä½ï¼‰ */
const LEAVE_TYPE_OPTIONS = [
  { label: "年假", value: "annual" },
  { label: "病假", value: "sick" },
  { label: "事假", value: "personal" },
  { label: "婚假", value: "marriage" },
  { label: "产假", value: "maternity" },
  { label: "哺乳假", value: "nursing" },
  { label: "慰唁假", value: "condolence" },
  { label: "调休", value: "compensatory" },
];
function leaveTypeLabel(v) {
  const hit = LEAVE_TYPE_OPTIONS.find((x) => x.value === v);
  return hit?.label || "—";
}
/** ä¸ŽåŽç«¯çº¦å®šå­—段(占位) */
const createEmptyForm = () => ({
  id: undefined,
  applicantId: "",
  applicantNo: "",
  applicantName: "",
  leaveType: "",
  leaveBalanceDays: undefined,
  leaveStartTime: "",
  leaveEndTime: "",
  leaveReason: "",
  approvalMode: "parallel",
  approverIds: [],
  approverNames: "",
  attachmentList: [],
});
const { proxy } = getCurrentInstance();
function unwrapArray(payload) {
  if (Array.isArray(payload)) return payload;
  if (payload && Array.isArray(payload.data)) return payload.data;
  if (payload && Array.isArray(payload.rows)) return payload.rows;
  return [];
}
function filterDisabledDept(deptList) {
  if (!Array.isArray(deptList)) return [];
  return deptList.filter((dept) => {
    if (dept.disabled) return false;
    if (dept.children?.length) {
      dept.children = filterDisabledDept(dept.children);
    }
    return true;
  });
}
function getUserDeptId(u) {
  return u.deptId ?? u.sysDeptId ?? u.dept?.deptId ?? u.dept?.id ?? u.dept_id;
}
function getDeptNodeKey(node) {
  const k = node?.id ?? node?.value ?? node?.deptId;
  if (k == null || k === "") return null;
  return k;
}
function isActiveUser(u) {
  if (u.delFlag === "2" || u.delFlag === 2) return false;
  if (u.status == null) return true;
  return String(u.status) === "0";
}
function userToTreeLeaf(u) {
  return {
    id: String(u.userId ?? u.id),
    label: u.nickName || u.userName || `用户${u.userId ?? u.id}`,
  };
}
function buildUsersByDeptId(users) {
  const map = new Map();
  const unassigned = [];
  for (const u of users) {
    if (!isActiveUser(u)) continue;
    const did = getUserDeptId(u);
    if (did == null || did === "" || did === 0 || did === "0") {
      unassigned.push(u);
      continue;
    }
    const k = String(did);
    if (!map.has(k)) map.set(k, []);
    map.get(k).push(u);
  }
  return { map, unassigned };
}
function collectUserLabels(nodes, map) {
  (nodes || []).forEach((n) => {
    if (n.children?.length) {
      collectUserLabels(n.children, map);
    } else if (n.id != null && !String(n.id).startsWith("dept_")) {
      map[String(n.id)] = n.label;
    }
  });
}
function mergeDeptTreeWithUsers(nodes, usersByDept) {
  if (!Array.isArray(nodes)) return [];
  const out = [];
  for (const node of nodes) {
    const deptIdRaw = getDeptNodeKey(node);
    if (deptIdRaw == null) continue;
    const sub = mergeDeptTreeWithUsers(node.children || [], usersByDept);
    const usersHere = usersByDept.get(String(deptIdRaw)) || [];
    const userChildren = usersHere.map(userToTreeLeaf);
    const children = [...sub, ...userChildren];
    if (!children.length) continue;
    out.push({
      id: `dept_${deptIdRaw}`,
      label: node.label ?? node.deptName ?? "部门",
      disabled: true,
      children,
    });
  }
  return out;
}
function buildFlatApproverTree(users) {
  const list = users.filter(isActiveUser).map(userToTreeLeaf);
  if (!list.length) return [];
  return [
    {
      id: "dept_all_users",
      label: "系统用户",
      disabled: true,
      children: list,
    },
  ];
}
const approverTreeData = ref([]);
const approverLabelMap = ref({});
async function loadApproverTree() {
  try {
    const [deptRes, userRes] = await Promise.all([deptTreeSelect(), userListNoPageByTenantId()]);
    let rawTree = unwrapArray(deptRes);
    rawTree = rawTree.length ? JSON.parse(JSON.stringify(rawTree)) : [];
    let deptTree = filterDisabledDept(JSON.parse(JSON.stringify(rawTree)));
    if (!deptTree.length && rawTree.length) {
      deptTree = JSON.parse(JSON.stringify(rawTree));
    }
    const users = unwrapArray(userRes);
    const { map: usersByDept, unassigned } = buildUsersByDeptId(users);
    let merged = mergeDeptTreeWithUsers(deptTree, usersByDept);
    if (unassigned.length) {
      merged.push({
        id: "dept_unassigned",
        label: "未分配部门",
        disabled: true,
        children: unassigned.map(userToTreeLeaf),
      });
    }
    if (!merged.length && users.length) {
      merged = buildFlatApproverTree(users);
    }
    approverTreeData.value = merged;
    const map = {};
    collectUserLabels(merged, map);
    approverLabelMap.value = map;
  } catch {
    approverTreeData.value = [];
    approverLabelMap.value = {};
    proxy?.$modal?.msgWarning?.("审批人数据加载失败,请检查网络或稍后重试");
  }
}
function resolveApproverNames(ids) {
  if (!ids?.length) return "";
  const map = approverLabelMap.value;
  return ids.map((id) => map[String(id)] || id).join("、");
}
function approvalModeLabel(mode) {
  if (mode === "or_sign") return "或签";
  return "与签";
}
function approvalResultLabel(v) {
  if (v === "approved") return "已通过";
  if (v === "rejected") return "已驳回";
  if (v === "cancelled") return "已撤销";
  return "待审批";
}
/** æŒ‰èµ·æ­¢æ—¶é—´è®¡ç®—请假天数(含时分秒,结果保留两位小数) */
function computeLeaveDays(startStr, endStr) {
  if (!startStr || !endStr) return null;
  const t0 = dayjs(startStr);
  const t1 = dayjs(endStr);
  if (!t0.isValid() || !t1.isValid() || !t1.isAfter(t0)) return null;
  const days = t1.diff(t0, "millisecond") / (24 * 60 * 60 * 1000);
  return Math.round(days * 100) / 100;
}
function formatDuration(v) {
  if (v == null || v === "") return "—";
  return `${v} å¤©`;
}
function formatBalance(v) {
  if (v == null || v === "") return "—";
  return `${v} å¤©`;
}
/** ç³»ç»Ÿç”¨æˆ·ç¼“å­˜ */
const allUsersCache = ref([]);
async function loadUserPool() {
  try {
    const res = await userListNoPageByTenantId();
    allUsersCache.value = unwrapArray(res);
  } catch {
    allUsersCache.value = [];
  }
}
function userSelectLabel(u) {
  const nick = u.nickName || "";
  const name = u.userName || "";
  if (nick && name && nick !== name) return `${nick}(${name})`;
  return nick || name || `用户${u.userId ?? u.id ?? ""}`;
}
function userById(id) {
  if (id == null || id === "") return undefined;
  return allUsersCache.value.find((u) => String(u.userId ?? u.id) === String(id));
}
function applicantNoFromUser(u) {
  if (!u) return "";
  return (
    u.userName ??
    u.userCode ??
    u.jobNumber ??
    u.workNo ??
    (u.userId != null ? String(u.userId) : "")
  );
}
/** æœ¬åœ°æ¨¡æ‹Ÿï¼šæ ¹æ®ç”¨æˆ·ç”Ÿæˆç¨³å®šã€Œå‡æœŸä½™é¢ã€å ä½ */
function mockLeaveBalance(u) {
  if (!u) return undefined;
  const idStr = String(u.userId ?? u.id ?? "0");
  let s = 0;
  for (let i = 0; i < idStr.length; i++) s += idStr.charCodeAt(i);
  return Math.round(((s % 130) / 10 + 5) * 100) / 100;
}
function filterUsersByQuery(query) {
  const list = allUsersCache.value.filter((u) => isActiveUser(u));
  const q = (query || "").trim().toLowerCase();
  if (!q) return [...list];
  return list.filter((u) => {
    const nick = (u.nickName || "").toLowerCase();
    const uname = (u.userName || "").toLowerCase();
    const phone = (u.phonenumber || u.phone || "").toString();
    return nick.includes(q) || uname.includes(q) || phone.includes(q);
  });
}
const applicantFormSearchLoading = ref(false);
const applicantFormOptions = ref([]);
async function remoteSearchApplicantForm(query) {
  applicantFormSearchLoading.value = true;
  try {
    if (!allUsersCache.value.length) {
      await loadUserPool();
    }
    applicantFormOptions.value = filterUsersByQuery(query);
  } finally {
    applicantFormSearchLoading.value = false;
  }
}
function onApplicantChange(uid) {
  const u = userById(uid);
  if (u) {
    form.applicantName = u.nickName || u.userName || "";
    form.applicantNo = applicantNoFromUser(u);
    form.leaveBalanceDays = mockLeaveBalance(u);
  } else {
    form.applicantName = "";
    form.applicantNo = "";
    form.leaveBalanceDays = undefined;
  }
}
/** æœ¬åœ°æ¨¡æ‹Ÿåˆ—表数据 */
const allRows = ref([
  {
    id: "1",
    applicantId: "mock_1",
    applicantNo: "zhangsan",
    applicantName: "张三",
    leaveType: "annual",
    leaveBalanceDays: 12,
    leaveStartTime: "2026-05-10 09:00:00",
    leaveEndTime: "2026-05-12 18:00:00",
    leaveDurationDays: 2.38,
    leaveReason: "年休假返乡探亲。",
    approvalMode: "parallel",
    approverIds: [],
    approverNames: "",
    approvalResult: "pending",
    attachmentList: [{ name: "车票订单.pdf" }],
    createTime: "2026-05-09 10:20:00",
  },
  {
    id: "2",
    applicantId: "mock_2",
    applicantNo: "lisi",
    applicantName: "李四",
    leaveType: "sick",
    leaveBalanceDays: 0,
    leaveStartTime: "2026-05-14 08:30:00",
    leaveEndTime: "2026-05-14 12:00:00",
    leaveDurationDays: 0.15,
    leaveReason: "上午门诊复查。",
    approvalMode: "or_sign",
    approverIds: [],
    approverNames: "",
    approvalResult: "approved",
    attachmentList: [],
    createTime: "2026-05-13 16:00:00",
  },
]);
const searchForm = reactive({
  applicantKeyword: "",
  leaveType: "",
});
const tableLoading = ref(false);
const page = reactive({
  current: 1,
  size: 10,
  total: 0,
});
const filteredList = computed(() => {
  let list = [...allRows.value];
  const kw = (searchForm.applicantKeyword || "").trim().toLowerCase();
  if (kw) {
    list = list.filter((r) => {
      const name = (r.applicantName || "").toLowerCase();
      const no = (r.applicantNo || "").toLowerCase();
      return name.includes(kw) || no.includes(kw);
    });
  }
  if (searchForm.leaveType) {
    list = list.filter((r) => r.leaveType === searchForm.leaveType);
  }
  return list.sort((a, b) => (String(a.createTime) < String(b.createTime) ? 1 : -1));
});
watch(
  filteredList,
  (list) => {
    page.total = list.length;
    const maxPage = Math.max(1, Math.ceil(list.length / page.size) || 1);
    if (page.current > maxPage) {
      page.current = maxPage;
    }
  },
  { immediate: true }
);
const tableData = computed(() => {
  const list = filteredList.value;
  const start = (page.current - 1) * page.size;
  return list.slice(start, start + page.size);
});
const tableColumn = ref([
  { label: "申请人编号", prop: "applicantNo", width: 120 },
  { label: "申请人", prop: "applicantName", minWidth: 100 },
  {
    label: "请假类型",
    prop: "leaveType",
    width: 100,
    formatData: (v) => leaveTypeLabel(v),
  },
  {
    label: "请假时长",
    prop: "leaveDurationDays",
    width: 120,
    formatData: (v) => (v == null || v === "" ? "—" : `${v} å¤©`),
  },
  { label: "请假事由", prop: "leaveReason", minWidth: 180 },
  { label: "创建时间", prop: "createTime", width: 170 },
  {
    label: "审批结果",
    prop: "approvalResult",
    width: 110,
    dataType: "tag",
    formatData: (v) => approvalResultLabel(v),
    formatType: (v) => {
      if (v === "approved") return "success";
      if (v === "rejected") return "danger";
      if (v === "cancelled") return "info";
      return "warning";
    },
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: "right",
    width: 220,
    operation: [
      {
        name: "编辑",
        type: "text",
        clickFun: (row) => openFormDialog("edit", row),
      },
      {
        name: "查看详情",
        type: "text",
        clickFun: (row) => openDetail(row),
      },
      {
        name: "附件",
        type: "text",
        clickFun: (row) => openFiles(row),
      },
    ],
  },
]);
const formDialog = reactive({
  visible: false,
  title: "",
  mode: "add",
});
const formRef = ref();
const form = reactive(createEmptyForm());
const leaveDurationDisplay = computed(() => {
  const d = computeLeaveDays(form.leaveStartTime, form.leaveEndTime);
  return d == null ? "" : String(d);
});
function onLeaveRangeChange() {
  nextTick(() => {
    formRef.value?.validateField?.("leaveEndTime");
  });
}
const formRules = {
  applicantId: [{ required: true, message: "请选择申请人", trigger: "change" }],
  leaveType: [{ required: true, message: "请选择请假类型", trigger: "change" }],
  leaveBalanceDays: [
    {
      required: true,
      message: "请填写假期余额",
      trigger: "blur",
    },
  ],
  leaveStartTime: [{ required: true, message: "请选择请假开始时间", trigger: "change" }],
  leaveEndTime: [
    { required: true, message: "请选择请假结束时间", trigger: "change" },
    {
      validator: (_rule, val, callback) => {
        if (!form.leaveStartTime || !val) {
          callback();
          return;
        }
        const d = computeLeaveDays(form.leaveStartTime, val);
        if (d == null) {
          callback(new Error("结束时间须晚于开始时间"));
        } else {
          callback();
        }
      },
      trigger: "change",
    },
  ],
  leaveReason: [{ required: true, message: "请填写请假事由", trigger: "blur" }],
  approvalMode: [{ required: true, message: "请选择审批方式", trigger: "change" }],
  approverIds: [
    {
      type: "array",
      required: true,
      message: "请选择审批人",
      trigger: "change",
    },
  ],
};
const detailDialog = reactive({ visible: false });
const detailRow = ref({});
const filesDialog = reactive({ visible: false, row: null });
function handleQuery() {
  page.current = 1;
  tableLoading.value = true;
  setTimeout(() => {
    tableLoading.value = false;
  }, 150);
}
function resetSearch() {
  searchForm.applicantKeyword = "";
  searchForm.leaveType = "";
  handleQuery();
}
function pagination(obj) {
  page.current = obj.page;
  page.size = obj.limit;
}
function openDetail(row) {
  detailRow.value = { ...row };
  detailDialog.visible = true;
}
function openFiles(row) {
  filesDialog.row = row;
  filesDialog.visible = true;
}
function mockDownload(row) {
  const url = row.url || row.downloadURL || row.previewURL || row.previewUrl;
  if (url) {
    window.open(url, "_blank");
    return;
  }
  proxy?.$modal?.msgSuccess?.(`已模拟下载:${row.name}`);
}
async function openFormDialog(mode, row) {
  formDialog.mode = mode;
  formDialog.title = mode === "add" ? "新增请假申请" : "编辑请假申请";
  await loadApproverTree();
  if (!allUsersCache.value.length) {
    await loadUserPool();
  }
  Object.assign(form, createEmptyForm());
  if (mode === "edit" && row) {
    Object.assign(form, {
      id: row.id,
      applicantId: row.applicantId,
      applicantNo: row.applicantNo,
      applicantName: row.applicantName,
      leaveType: row.leaveType,
      leaveBalanceDays: row.leaveBalanceDays,
      leaveStartTime: row.leaveStartTime,
      leaveEndTime: row.leaveEndTime,
      leaveReason: row.leaveReason,
      approvalMode: row.approvalMode === "countersign" ? "or_sign" : row.approvalMode || "parallel",
      approverIds: (row.approverIds || []).map((id) => String(id)),
      approverNames: row.approverNames,
      attachmentList: JSON.parse(JSON.stringify(row.attachmentList || [])),
    });
    const u = userById(row.applicantId);
    if (u) {
      applicantFormOptions.value = [u];
    } else if (row.applicantId) {
      applicantFormOptions.value = [
        {
          userId: row.applicantId,
          nickName: row.applicantName,
          userName: row.applicantNo,
        },
      ];
    }
  } else {
    remoteSearchApplicantForm("");
  }
  formDialog.visible = true;
  nextTick(() => formRef.value?.clearValidate?.());
}
function onFormClosed() {
  formRef.value?.resetFields?.();
}
async function submitForm() {
  try {
    await formRef.value?.validate?.();
  } catch {
    return;
  }
  const days = computeLeaveDays(form.leaveStartTime, form.leaveEndTime);
  if (days == null) {
    proxy?.$modal?.msgWarning?.("请检查请假起止时间,结束时间须晚于开始时间");
    return;
  }
  form.approverNames = resolveApproverNames(form.approverIds);
  const payload = {
    applicantId: form.applicantId,
    applicantNo: form.applicantNo,
    applicantName: form.applicantName,
    leaveType: form.leaveType,
    leaveBalanceDays: form.leaveBalanceDays,
    leaveStartTime: form.leaveStartTime,
    leaveEndTime: form.leaveEndTime,
    leaveDurationDays: days,
    leaveReason: form.leaveReason,
    approvalMode: form.approvalMode,
    approverIds: [...form.approverIds],
    approverNames: form.approverNames,
    attachmentList: JSON.parse(JSON.stringify(form.attachmentList || [])),
  };
  if (formDialog.mode === "add") {
    const id = `local_${Date.now()}`;
    allRows.value.unshift({
      id,
      ...payload,
      approvalResult: "pending",
      createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
    });
    proxy?.$modal?.msgSuccess?.("新增成功(本地模拟)");
  } else {
    const idx = allRows.value.findIndex((r) => r.id === form.id);
    if (idx !== -1) {
      const prev = allRows.value[idx];
      allRows.value[idx] = {
        ...prev,
        id: form.id,
        ...payload,
        approvalResult: prev.approvalResult ?? "pending",
        createTime: prev.createTime ?? dayjs().format("YYYY-MM-DD HH:mm:ss"),
      };
    }
    proxy?.$modal?.msgSuccess?.("保存成功(本地模拟)");
  }
  formDialog.visible = false;
  handleQuery();
}
onMounted(() => {
  loadApproverTree();
});
</script>
<style scoped>
.mb20 {
  margin-bottom: 20px;
}
.search_form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.search_title {
  font-size: 14px;
  color: var(--el-text-color-regular);
}
.upload-block {
  width: 100%;
}
.mr6 {
  margin-right: 6px;
}
.mb6 {
  margin-bottom: 6px;
}
.leave-apply-form :deep(.el-row) {
  margin-bottom: 0;
}
.leave-apply-form :deep(.el-form-item) {
  margin-bottom: 18px;
}
.leave-apply-form-dialog :deep(.el-dialog__body) {
  padding-top: 12px;
}
</style>
src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,916 @@
<!--OA模块:加班申请(字段为前端占位,后期与后端接口对齐)-->
<template>
  <div class="app-container">
    <div class="search_form mb20">
      <div>
        <span class="search_title">申请人:</span>
        <el-input
          v-model="searchForm.applicantKeyword"
          style="width: 220px"
          placeholder="姓名或编号"
          clearable
          :prefix-icon="Search"
          @keyup.enter="handleQuery"
        />
        <span class="search_title" style="margin-left: 12px">加班类型:</span>
        <el-select v-model="searchForm.overtimeType" placeholder="全部" clearable style="width: 180px">
          <el-option v-for="opt in OVERTIME_TYPE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" />
        </el-select>
        <el-button type="primary" style="margin-left: 10px" @click="handleQuery">搜索</el-button>
        <el-button @click="resetSearch">重置</el-button>
      </div>
      <div class="search_actions">
        <el-button type="success" plain @click="handleImportClick">导入</el-button>
        <el-button type="warning" plain @click="handleExport">导出</el-button>
        <el-button type="primary" @click="openFormDialog('add')">新增加班申请</el-button>
      </div>
    </div>
    <input ref="importInputRef" type="file" accept="application/json,.json" class="sr-only-input" @change="onImportFile" />
    <div class="table_list">
      <PIMTable
        rowKey="id"
        :column="tableColumn"
        :tableData="tableData"
        :page="page"
        :isSelection="false"
        :tableLoading="tableLoading"
        @pagination="pagination"
        :total="page.total"
      />
    </div>
    <!-- æ–°å¢ž / ç¼–辑 -->
    <el-dialog
      v-model="formDialog.visible"
      :title="formDialog.title"
      width="960px"
      append-to-body
      destroy-on-close
      class="overtime-apply-form-dialog"
      @closed="onFormClosed"
    >
      <el-form ref="formRef" :model="form" :rules="formRules" label-width="140px" class="overtime-apply-form">
        <el-row :gutter="24">
          <el-col :span="24">
            <el-form-item label="申请人" prop="applicantId">
              <el-select
                v-model="form.applicantId"
                filterable
                remote
                clearable
                reserve-keyword
                placeholder="请选择或搜索申请人"
                style="width: 100%"
                :remote-method="remoteSearchApplicantForm"
                :loading="applicantFormSearchLoading"
                @change="onApplicantChange"
              >
                <el-option
                  v-for="u in applicantFormOptions"
                  :key="u.userId"
                  :label="userSelectLabel(u)"
                  :value="u.userId"
                />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="12">
            <el-form-item label="加班类型" prop="overtimeType">
              <el-select v-model="form.overtimeType" placeholder="请选择加班类型" clearable filterable style="width: 100%">
                <el-option v-for="opt in OVERTIME_TYPE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="加班日期" prop="overtimeDate">
              <el-date-picker
                v-model="form.overtimeDate"
                type="date"
                placeholder="请选择加班日期"
                format="YYYY-MM-DD"
                value-format="YYYY-MM-DD"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="12">
            <el-form-item label="加班开始日期" prop="overtimeStartTime">
              <el-date-picker
                v-model="form.overtimeStartTime"
                type="datetime"
                placeholder="请选择开始时间"
                format="YYYY-MM-DD HH:mm:ss"
                value-format="YYYY-MM-DD HH:mm:ss"
                style="width: 100%"
                @change="onOvertimeRangeChange"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="加班结束日期" prop="overtimeEndTime">
              <el-date-picker
                v-model="form.overtimeEndTime"
                type="datetime"
                placeholder="请选择结束时间"
                format="YYYY-MM-DD HH:mm:ss"
                value-format="YYYY-MM-DD HH:mm:ss"
                style="width: 100%"
                @change="onOvertimeRangeChange"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="12">
            <el-form-item label="加班时长">
              <el-input :model-value="overtimeHoursDisplay" readonly placeholder="根据起止时间自动计算">
                <template #append>小时</template>
              </el-input>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="24">
            <el-form-item label="预设审批流">
              <div class="approval-flow-preview">
                <div
                  v-for="(node, index) in PRESET_APPROVAL_FLOW_NODES"
                  :key="node.roleCode"
                  class="flow-node-wrap"
                >
                  <div class="flow-node">
                    <span class="flow-node-order">{{ index + 1 }}</span>
                    <span class="flow-node-name">{{ node.roleName }}</span>
                  </div>
                  <el-icon v-if="index < PRESET_APPROVAL_FLOW_NODES.length - 1" class="flow-arrow">
                    <ArrowRight />
                  </el-icon>
                </div>
              </div>
              <p class="flow-tip">按顺序逐级审批,各节点审批人由系统根据组织架构自动匹配</p>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="24">
            <el-form-item label="加班事由" prop="overtimeReason">
              <el-input
                v-model="form.overtimeReason"
                type="textarea"
                :rows="4"
                placeholder="请填写加班事由"
                maxlength="2000"
                show-word-limit
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="24">
            <el-form-item label="附件">
              <div class="upload-block">
                <FileUpload v-model:file-list="form.attachmentList" :limit="10" button-text="点击选择文件" />
              </div>
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
          <el-button @click="formDialog.visible = false">取 æ¶ˆ</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- è¯¦æƒ… -->
    <el-dialog v-model="detailDialog.visible" title="加班申请详情" width="720px" append-to-body>
      <el-descriptions :column="1" border>
        <el-descriptions-item label="申请人编号">{{ detailRow.applicantNo || "—" }}</el-descriptions-item>
        <el-descriptions-item label="申请人">{{ detailRow.applicantName }}</el-descriptions-item>
        <el-descriptions-item label="加班类型">{{ overtimeTypeLabel(detailRow.overtimeType) }}</el-descriptions-item>
        <el-descriptions-item label="加班日期">{{ detailRow.overtimeDate || "—" }}</el-descriptions-item>
        <el-descriptions-item label="加班开始日期">{{ detailRow.overtimeStartTime || "—" }}</el-descriptions-item>
        <el-descriptions-item label="加班结束日期">{{ detailRow.overtimeEndTime || "—" }}</el-descriptions-item>
        <el-descriptions-item label="加班时长">{{ formatHours(detailRow.overtimeHours) }}</el-descriptions-item>
        <el-descriptions-item label="加班事由">{{ detailRow.overtimeReason }}</el-descriptions-item>
        <el-descriptions-item label="预设审批流">
          <div class="approval-flow-preview approval-flow-detail">
            <div
              v-for="(node, index) in detailApprovalFlowNodes"
              :key="node.roleCode"
              class="flow-node-wrap"
            >
              <div class="flow-node flow-node--compact">
                <span class="flow-node-order">{{ index + 1 }}</span>
                <span class="flow-node-name">{{ node.roleName }}</span>
              </div>
              <el-icon v-if="index < detailApprovalFlowNodes.length - 1" class="flow-arrow">
                <ArrowRight />
              </el-icon>
            </div>
          </div>
        </el-descriptions-item>
        <el-descriptions-item label="审批结果">{{ approvalResultLabel(detailRow.approvalResult) }}</el-descriptions-item>
        <el-descriptions-item label="创建时间">{{ detailRow.createTime || "—" }}</el-descriptions-item>
        <el-descriptions-item label="附件">
          <template v-if="detailRow.attachmentList?.length">
            <el-tag v-for="(f, i) in detailRow.attachmentList" :key="i" class="mr6 mb6" type="info">
              {{ f.name }}
            </el-tag>
          </template>
          <span v-else>无</span>
        </el-descriptions-item>
      </el-descriptions>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="detailDialog.visible = false">关 é—­</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- é™„件列表 -->
    <el-dialog v-model="filesDialog.visible" title="附件" width="520px" append-to-body>
      <el-table v-if="filesDialog.row?.attachmentList?.length" :data="filesDialog.row.attachmentList" border>
        <el-table-column type="index" label="序号" width="60" align="center" />
        <el-table-column prop="name" label="文件名" min-width="200" show-overflow-tooltip />
        <el-table-column label="操作" width="100" align="center">
          <template #default="{ row }">
            <el-button link type="primary" @click="mockDownload(row)">下载</el-button>
          </template>
        </el-table-column>
      </el-table>
      <el-empty v-else description="暂无附件" />
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="filesDialog.visible = false">关 é—­</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ArrowRight, Search } from "@element-plus/icons-vue";
import dayjs from "dayjs";
import FileUpload from "@/components/AttachmentUpload/file/index.vue";
import { userListNoPageByTenantId } from "@/api/system/user.js";
import { computed, getCurrentInstance, nextTick, reactive, ref, watch } from "vue";
/** åŠ ç­ç±»åž‹ï¼ˆvalue ä¸ŽåŽç«¯å¯¹é½å ä½ï¼‰ */
const OVERTIME_TYPE_OPTIONS = [
  { label: "工作日加班", value: "weekday" },
  { label: "休息日加班", value: "weekend" },
  { label: "法定节假日加班", value: "holiday" },
];
/** é¢„设审批流节点(与流程引擎配置对齐占位) */
const PRESET_APPROVAL_FLOW_NODES = [
  { roleCode: "direct_leader", roleName: "直属上级", sortOrder: 1 },
  { roleCode: "dept_leader", roleName: "部门负责人", sortOrder: 2 },
];
function resolveApprovalFlowNodes(row) {
  const nodes = row?.approvalFlowNodes;
  if (Array.isArray(nodes) && nodes.length) {
    return [...nodes].sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
  }
  return PRESET_APPROVAL_FLOW_NODES;
}
function cloneApprovalFlowNodes() {
  return PRESET_APPROVAL_FLOW_NODES.map((n) => ({ ...n }));
}
function overtimeTypeLabel(v) {
  const hit = OVERTIME_TYPE_OPTIONS.find((x) => x.value === v);
  return hit?.label || "—";
}
const createEmptyForm = () => ({
  id: undefined,
  applicantId: "",
  applicantNo: "",
  applicantName: "",
  overtimeType: "",
  overtimeDate: "",
  overtimeStartTime: "",
  overtimeEndTime: "",
  overtimeReason: "",
  attachmentList: [],
});
const { proxy } = getCurrentInstance();
function unwrapArray(payload) {
  if (Array.isArray(payload)) return payload;
  if (payload && Array.isArray(payload.data)) return payload.data;
  if (payload && Array.isArray(payload.rows)) return payload.rows;
  return [];
}
function isActiveUser(u) {
  if (u.delFlag === "2" || u.delFlag === 2) return false;
  if (u.status == null) return true;
  return String(u.status) === "0";
}
function approvalResultLabel(v) {
  if (v === "approved") return "已通过";
  if (v === "rejected") return "已驳回";
  if (v === "cancelled") return "已撤销";
  return "待审批";
}
/** æŒ‰èµ·æ­¢æ—¶é—´è®¡ç®—加班时长(小时,保留两位小数) */
function computeOvertimeHours(startStr, endStr) {
  if (!startStr || !endStr) return null;
  const t0 = dayjs(startStr);
  const t1 = dayjs(endStr);
  if (!t0.isValid() || !t1.isValid() || !t1.isAfter(t0)) return null;
  const hours = t1.diff(t0, "millisecond") / (60 * 60 * 1000);
  return Math.round(hours * 100) / 100;
}
function formatHours(v) {
  if (v == null || v === "") return "—";
  return `${v} å°æ—¶`;
}
const allUsersCache = ref([]);
async function loadUserPool() {
  try {
    const res = await userListNoPageByTenantId();
    allUsersCache.value = unwrapArray(res);
  } catch {
    allUsersCache.value = [];
  }
}
function userSelectLabel(u) {
  const nick = u.nickName || "";
  const name = u.userName || "";
  if (nick && name && nick !== name) return `${nick}(${name})`;
  return nick || name || `用户${u.userId ?? u.id ?? ""}`;
}
function userById(id) {
  if (id == null || id === "") return undefined;
  return allUsersCache.value.find((u) => String(u.userId ?? u.id) === String(id));
}
function applicantNoFromUser(u) {
  if (!u) return "";
  return (
    u.userName ??
    u.userCode ??
    u.jobNumber ??
    u.workNo ??
    (u.userId != null ? String(u.userId) : "")
  );
}
function filterUsersByQuery(query) {
  const list = allUsersCache.value.filter((u) => isActiveUser(u));
  const q = (query || "").trim().toLowerCase();
  if (!q) return [...list];
  return list.filter((u) => {
    const nick = (u.nickName || "").toLowerCase();
    const uname = (u.userName || "").toLowerCase();
    const phone = (u.phonenumber || u.phone || "").toString();
    return nick.includes(q) || uname.includes(q) || phone.includes(q);
  });
}
const applicantFormSearchLoading = ref(false);
const applicantFormOptions = ref([]);
async function remoteSearchApplicantForm(query) {
  applicantFormSearchLoading.value = true;
  try {
    if (!allUsersCache.value.length) {
      await loadUserPool();
    }
    applicantFormOptions.value = filterUsersByQuery(query);
  } finally {
    applicantFormSearchLoading.value = false;
  }
}
function onApplicantChange(uid) {
  const u = userById(uid);
  if (u) {
    form.applicantName = u.nickName || u.userName || "";
    form.applicantNo = applicantNoFromUser(u);
  } else {
    form.applicantName = "";
    form.applicantNo = "";
  }
}
const allRows = ref([
  {
    id: "1",
    applicantId: "mock_1",
    applicantNo: "zhangsan",
    applicantName: "张三",
    overtimeType: "weekday",
    overtimeDate: "2026-05-10",
    overtimeStartTime: "2026-05-10 18:00:00",
    overtimeEndTime: "2026-05-10 21:30:00",
    overtimeHours: 3.5,
    overtimeReason: "项目上线保障。",
    approvalFlowNodes: cloneApprovalFlowNodes(),
    approvalResult: "pending",
    attachmentList: [{ name: "任务单.pdf" }],
    createTime: "2026-05-09 10:20:00",
  },
  {
    id: "2",
    applicantId: "mock_2",
    applicantNo: "lisi",
    applicantName: "李四",
    overtimeType: "weekend",
    overtimeDate: "2026-05-11",
    overtimeStartTime: "2026-05-11 09:00:00",
    overtimeEndTime: "2026-05-11 12:15:00",
    overtimeHours: 3.25,
    overtimeReason: "客户现场支持。",
    approvalFlowNodes: cloneApprovalFlowNodes(),
    approvalResult: "approved",
    attachmentList: [],
    createTime: "2026-05-10 16:00:00",
  },
]);
const searchForm = reactive({
  applicantKeyword: "",
  overtimeType: "",
});
const tableLoading = ref(false);
const page = reactive({
  current: 1,
  size: 10,
  total: 0,
});
const filteredList = computed(() => {
  let list = [...allRows.value];
  const kw = (searchForm.applicantKeyword || "").trim().toLowerCase();
  if (kw) {
    list = list.filter((r) => {
      const name = (r.applicantName || "").toLowerCase();
      const no = (r.applicantNo || "").toLowerCase();
      return name.includes(kw) || no.includes(kw);
    });
  }
  if (searchForm.overtimeType) {
    list = list.filter((r) => r.overtimeType === searchForm.overtimeType);
  }
  return list.sort((a, b) => (String(a.createTime) < String(b.createTime) ? 1 : -1));
});
watch(
  filteredList,
  (list) => {
    page.total = list.length;
    const maxPage = Math.max(1, Math.ceil(list.length / page.size) || 1);
    if (page.current > maxPage) {
      page.current = maxPage;
    }
  },
  { immediate: true }
);
const tableData = computed(() => {
  const list = filteredList.value;
  const start = (page.current - 1) * page.size;
  return list.slice(start, start + page.size);
});
const tableColumn = ref([
  { label: "申请人编号", prop: "applicantNo", width: 120 },
  { label: "申请人", prop: "applicantName", minWidth: 100 },
  { label: "加班日期", prop: "overtimeDate", width: 120 },
  { label: "加班开始日期", prop: "overtimeStartTime", width: 170 },
  { label: "加班结束日期", prop: "overtimeEndTime", width: 170 },
  {
    label: "加班时长",
    prop: "overtimeHours",
    width: 120,
    formatData: (v) => (v == null || v === "" ? "—" : `${v} å°æ—¶`),
  },
  {
    label: "审批结果",
    prop: "approvalResult",
    width: 110,
    dataType: "tag",
    formatData: (v) => approvalResultLabel(v),
    formatType: (v) => {
      if (v === "approved") return "success";
      if (v === "rejected") return "danger";
      if (v === "cancelled") return "info";
      return "warning";
    },
  },
  { label: "创建时间", prop: "createTime", width: 170 },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: "right",
    width: 220,
    operation: [
      {
        name: "编辑",
        type: "text",
        clickFun: (row) => openFormDialog("edit", row),
      },
      {
        name: "查看详情",
        type: "text",
        clickFun: (row) => openDetail(row),
      },
      {
        name: "附件",
        type: "text",
        clickFun: (row) => openFiles(row),
      },
    ],
  },
]);
const formDialog = reactive({
  visible: false,
  title: "",
  mode: "add",
});
const formRef = ref();
const form = reactive(createEmptyForm());
const overtimeHoursDisplay = computed(() => {
  const h = computeOvertimeHours(form.overtimeStartTime, form.overtimeEndTime);
  return h == null ? "" : String(h);
});
function onOvertimeRangeChange() {
  nextTick(() => {
    formRef.value?.validateField?.("overtimeEndTime");
  });
}
const formRules = {
  applicantId: [{ required: true, message: "请选择申请人", trigger: "change" }],
  overtimeType: [{ required: true, message: "请选择加班类型", trigger: "change" }],
  overtimeDate: [{ required: true, message: "请选择加班日期", trigger: "change" }],
  overtimeStartTime: [{ required: true, message: "请选择加班开始时间", trigger: "change" }],
  overtimeEndTime: [
    { required: true, message: "请选择加班结束时间", trigger: "change" },
    {
      validator: (_rule, val, callback) => {
        if (!form.overtimeStartTime || !val) {
          callback();
          return;
        }
        const h = computeOvertimeHours(form.overtimeStartTime, val);
        if (h == null) {
          callback(new Error("结束时间须晚于开始时间"));
        } else {
          callback();
        }
      },
      trigger: "change",
    },
  ],
  overtimeReason: [{ required: true, message: "请填写加班事由", trigger: "blur" }],
};
const detailDialog = reactive({ visible: false });
const detailRow = ref({});
const detailApprovalFlowNodes = computed(() => resolveApprovalFlowNodes(detailRow.value));
const filesDialog = reactive({ visible: false, row: null });
const importInputRef = ref(null);
function handleQuery() {
  page.current = 1;
  tableLoading.value = true;
  setTimeout(() => {
    tableLoading.value = false;
  }, 150);
}
function resetSearch() {
  searchForm.applicantKeyword = "";
  searchForm.overtimeType = "";
  handleQuery();
}
function pagination(obj) {
  page.current = obj.page;
  page.size = obj.limit;
}
function openDetail(row) {
  detailRow.value = { ...row };
  detailDialog.visible = true;
}
function openFiles(row) {
  filesDialog.row = row;
  filesDialog.visible = true;
}
function mockDownload(row) {
  const url = row.url || row.downloadURL || row.previewURL || row.previewUrl;
  if (url) {
    window.open(url, "_blank");
    return;
  }
  proxy?.$modal?.msgSuccess?.(`已模拟下载:${row.name}`);
}
function handleExport() {
  const data = filteredList.value;
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json;charset=utf-8" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = `加班申请导出_${dayjs().format("YYYYMMDDHHmmss")}.json`;
  a.click();
  URL.revokeObjectURL(url);
  proxy?.$modal?.msgSuccess?.(`已导出 ${data.length} æ¡ï¼ˆå½“前筛选结果,JSON)`);
}
function handleImportClick() {
  importInputRef.value?.click?.();
}
function normalizeImportedRow(raw, idx) {
  const id = raw.id != null && String(raw.id).length ? `imp_${String(raw.id)}_${idx}` : `imp_${Date.now()}_${idx}`;
  const hours =
    raw.overtimeHours != null && raw.overtimeHours !== ""
      ? Number(raw.overtimeHours)
      : computeOvertimeHours(raw.overtimeStartTime, raw.overtimeEndTime);
  return {
    id,
    applicantId: raw.applicantId != null ? String(raw.applicantId) : `imp_user_${idx}`,
    applicantNo: raw.applicantNo ?? "",
    applicantName: raw.applicantName ?? "未知",
    overtimeType: raw.overtimeType || "weekday",
    overtimeDate: raw.overtimeDate ?? "",
    overtimeStartTime: raw.overtimeStartTime ?? "",
    overtimeEndTime: raw.overtimeEndTime ?? "",
    overtimeHours: hours == null || Number.isNaN(hours) ? 0 : Math.round(hours * 100) / 100,
    overtimeReason: raw.overtimeReason ?? "",
    approvalFlowNodes: Array.isArray(raw.approvalFlowNodes) && raw.approvalFlowNodes.length
      ? raw.approvalFlowNodes.map((n) => ({ ...n }))
      : cloneApprovalFlowNodes(),
    approvalResult: raw.approvalResult && ["pending", "approved", "rejected", "cancelled"].includes(raw.approvalResult)
      ? raw.approvalResult
      : "pending",
    attachmentList: Array.isArray(raw.attachmentList) ? raw.attachmentList : [],
    createTime: raw.createTime || dayjs().format("YYYY-MM-DD HH:mm:ss"),
  };
}
function onImportFile(e) {
  const input = e.target;
  const file = input.files?.[0];
  input.value = "";
  if (!file) return;
  const reader = new FileReader();
  reader.onload = () => {
    try {
      const text = String(reader.result || "");
      const parsed = JSON.parse(text);
      const arr = Array.isArray(parsed) ? parsed : parsed?.rows || parsed?.data;
      if (!Array.isArray(arr) || !arr.length) {
        proxy?.$modal?.msgWarning?.("导入文件格式不正确,需为加班申请对象数组 JSON");
        return;
      }
      let n = 0;
      for (let i = 0; i < arr.length; i++) {
        allRows.value.unshift(normalizeImportedRow(arr[i], i));
        n++;
      }
      proxy?.$modal?.msgSuccess?.(`成功导入 ${n} æ¡ï¼ˆæœ¬åœ°åˆå¹¶ï¼‰`);
      handleQuery();
    } catch {
      proxy?.$modal?.msgError?.("解析失败,请使用导出文件或约定 JSON ç»“æž„");
    }
  };
  reader.readAsText(file, "utf-8");
}
async function openFormDialog(mode, row) {
  formDialog.mode = mode;
  formDialog.title = mode === "add" ? "新增加班申请" : "编辑加班申请";
  if (!allUsersCache.value.length) {
    await loadUserPool();
  }
  Object.assign(form, createEmptyForm());
  if (mode === "edit" && row) {
    Object.assign(form, {
      id: row.id,
      applicantId: row.applicantId,
      applicantNo: row.applicantNo,
      applicantName: row.applicantName,
      overtimeType: row.overtimeType,
      overtimeDate: row.overtimeDate,
      overtimeStartTime: row.overtimeStartTime,
      overtimeEndTime: row.overtimeEndTime,
      overtimeReason: row.overtimeReason,
      attachmentList: JSON.parse(JSON.stringify(row.attachmentList || [])),
    });
    const u = userById(row.applicantId);
    if (u) {
      applicantFormOptions.value = [u];
    } else if (row.applicantId) {
      applicantFormOptions.value = [
        {
          userId: row.applicantId,
          nickName: row.applicantName,
          userName: row.applicantNo,
        },
      ];
    }
  } else {
    remoteSearchApplicantForm("");
  }
  formDialog.visible = true;
  nextTick(() => formRef.value?.clearValidate?.());
}
function onFormClosed() {
  formRef.value?.resetFields?.();
}
async function submitForm() {
  try {
    await formRef.value?.validate?.();
  } catch {
    return;
  }
  const hours = computeOvertimeHours(form.overtimeStartTime, form.overtimeEndTime);
  if (hours == null) {
    proxy?.$modal?.msgWarning?.("请检查加班起止时间,结束时间须晚于开始时间");
    return;
  }
  const payload = {
    applicantId: form.applicantId,
    applicantNo: form.applicantNo,
    applicantName: form.applicantName,
    overtimeType: form.overtimeType,
    overtimeDate: form.overtimeDate,
    overtimeStartTime: form.overtimeStartTime,
    overtimeEndTime: form.overtimeEndTime,
    overtimeHours: hours,
    overtimeReason: form.overtimeReason,
    approvalFlowNodes: cloneApprovalFlowNodes(),
    attachmentList: JSON.parse(JSON.stringify(form.attachmentList || [])),
  };
  if (formDialog.mode === "add") {
    const id = `local_${Date.now()}`;
    allRows.value.unshift({
      id,
      ...payload,
      approvalResult: "pending",
      createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
    });
    proxy?.$modal?.msgSuccess?.("新增成功(本地模拟)");
  } else {
    const idx = allRows.value.findIndex((r) => r.id === form.id);
    if (idx !== -1) {
      const prev = allRows.value[idx];
      allRows.value[idx] = {
        ...prev,
        id: form.id,
        ...payload,
        approvalResult: prev.approvalResult ?? "pending",
        createTime: prev.createTime ?? dayjs().format("YYYY-MM-DD HH:mm:ss"),
      };
    }
    proxy?.$modal?.msgSuccess?.("保存成功(本地模拟)");
  }
  formDialog.visible = false;
  handleQuery();
}
</script>
<style scoped>
.mb20 {
  margin-bottom: 20px;
}
.search_form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.search_actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}
.search_title {
  font-size: 14px;
  color: var(--el-text-color-regular);
}
.sr-only-input {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
.upload-block {
  width: 100%;
}
.mr6 {
  margin-right: 6px;
}
.mb6 {
  margin-bottom: 6px;
}
.overtime-apply-form :deep(.el-row) {
  margin-bottom: 0;
}
.overtime-apply-form :deep(.el-form-item) {
  margin-bottom: 18px;
}
.overtime-apply-form-dialog :deep(.el-dialog__body) {
  padding-top: 12px;
}
.approval-flow-preview {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  width: 100%;
}
.approval-flow-detail {
  padding: 4px 0;
}
.flow-node-wrap {
  display: flex;
  align-items: center;
  gap: 8px;
}
.flow-node {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 140px;
  padding: 10px 16px;
  background: var(--el-fill-color-light);
  border: 1px solid var(--el-border-color-lighter);
  border-radius: 8px;
}
.flow-node--compact {
  min-width: 120px;
  padding: 8px 12px;
}
.flow-node-order {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  font-size: 12px;
  font-weight: 600;
  color: #fff;
  background: var(--el-color-primary);
  border-radius: 50%;
  flex-shrink: 0;
}
.flow-node-name {
  font-size: 14px;
  color: var(--el-text-color-primary);
}
.flow-arrow {
  font-size: 18px;
  color: var(--el-text-color-secondary);
}
.flow-tip {
  margin: 10px 0 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--el-text-color-secondary);
}
</style>
src/views/officeProcessAutomation/ContractManage/purchase-contract/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
<!--
  æ¨¡å—中文名:采购合同
  ç›®å½•标识:ContractManage/purchase-contract(purchase-contract â†’ ä¸­æ–‡ï¼šé‡‡è´­åˆåŒï¼‰
  å¤ç”¨é¡µé¢ï¼š@/views/procurementManagement/procurementLedger/index.vue(采购台账;文件名 index.vue â†’ å…¥å£é¡µï¼‰
-->
<template>
  <ProcurementLedger />
</template>
<script setup>
import ProcurementLedger from '@/views/procurementManagement/procurementLedger/index.vue'
</script>
src/views/officeProcessAutomation/ContractManage/sale-contract/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
<!--
  æ¨¡å—中文名:销售合同
  ç›®å½•标识:ContractManage/sale-contract(sale-contract â†’ ä¸­æ–‡ï¼šé”€å”®åˆåŒï¼‰
  å¤ç”¨é¡µé¢ï¼š@/views/procurementManagement/procurementLedger/index.vue(采购台账;文件名 index.vue â†’ å…¥å£é¡µï¼‰
-->
<template>
  <ProcurementLedger />
</template>
<script setup>
import ProcurementLedger from '@/views/procurementManagement/procurementLedger/index.vue'
</script>
src/views/officeProcessAutomation/HrManage/post-manage/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,292 @@
<!--OA模块:岗位管理-->
<template>
  <div class="app-container">
     <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
        <el-form-item label="岗位编码" prop="postCode">
           <el-input
              v-model="queryParams.postCode"
              placeholder="请输入岗位编码"
              clearable
              style="width: 200px"
              @keyup.enter="handleQuery"
           />
        </el-form-item>
        <el-form-item label="岗位名称" prop="postName">
           <el-input
              v-model="queryParams.postName"
              placeholder="请输入岗位名称"
              clearable
              style="width: 200px"
              @keyup.enter="handleQuery"
           />
        </el-form-item>
        <el-form-item label="状态" prop="status">
           <el-select v-model="queryParams.status" placeholder="岗位状态" clearable style="width: 200px">
              <el-option
                 v-for="dict in sys_normal_disable"
                 :key="dict.value"
                 :label="dict.label"
                 :value="dict.value"
              />
           </el-select>
        </el-form-item>
        <el-form-item>
           <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
           <el-button icon="Refresh" @click="resetQuery">重置</el-button>
        </el-form-item>
     </el-form>
     <el-row :gutter="10" class="mb8">
        <el-col :span="1.5">
           <el-button
              type="primary"
              plain
              icon="Plus"
              @click="handleAdd"
              v-hasPermi="['system:post:add']"
           >新增</el-button>
        </el-col>
        <el-col :span="1.5">
           <el-button
              type="success"
              plain
              icon="Edit"
              :disabled="single"
              @click="handleUpdate"
              v-hasPermi="['system:post:edit']"
           >修改</el-button>
        </el-col>
        <el-col :span="1.5">
           <el-button
              type="danger"
              plain
              icon="Delete"
              :disabled="multiple"
              @click="handleDelete"
              v-hasPermi="['system:post:remove']"
           >删除</el-button>
        </el-col>
        <el-col :span="1.5">
           <el-button
              type="warning"
              plain
              icon="Download"
              @click="handleExport"
              v-hasPermi="['system:post:export']"
           >导出</el-button>
        </el-col>
        <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
     <el-table v-loading="loading" :data="postList" @selection-change="handleSelectionChange">
        <el-table-column type="selection" width="55" align="center" />
        <el-table-column label="岗位编号" align="center" prop="postId" />
        <el-table-column label="岗位编码" align="center" prop="postCode" />
        <el-table-column label="岗位名称" align="center" prop="postName" />
        <el-table-column label="岗位排序" align="center" prop="postSort" />
        <el-table-column label="状态" align="center" prop="status">
           <template #default="scope">
              <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
           </template>
        </el-table-column>
        <el-table-column label="创建时间" align="center" prop="createTime" width="180">
           <template #default="scope">
              <span>{{ parseTime(scope.row.createTime) }}</span>
           </template>
        </el-table-column>
        <el-table-column label="操作" width="180" align="center" class-name="small-padding fixed-width">
           <template #default="scope">
              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:post:edit']">修改</el-button>
              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:post:remove']">删除</el-button>
           </template>
        </el-table-column>
     </el-table>
     <pagination
        v-show="total > 0"
        :total="total"
        v-model:page="queryParams.pageNum"
        v-model:limit="queryParams.pageSize"
        @pagination="getList"
     />
     <!-- æ·»åŠ æˆ–ä¿®æ”¹å²—ä½å¯¹è¯æ¡† -->
     <el-dialog :title="title" v-model="open" width="500px" append-to-body>
        <el-form ref="postRef" :model="form" :rules="rules" label-width="80px">
           <el-form-item label="岗位名称" prop="postName">
              <el-input v-model="form.postName" placeholder="请输入岗位名称" />
           </el-form-item>
           <el-form-item label="岗位编码" prop="postCode">
              <el-input v-model="form.postCode" placeholder="请输入编码名称" />
           </el-form-item>
           <el-form-item label="岗位顺序" prop="postSort">
              <el-input-number v-model="form.postSort" controls-position="right" :min="0" />
           </el-form-item>
           <el-form-item label="岗位状态" prop="status">
              <el-radio-group v-model="form.status">
                 <el-radio
                    v-for="dict in sys_normal_disable"
                    :key="dict.value"
                    :value="dict.value"
                 >{{ dict.label }}</el-radio>
              </el-radio-group>
           </el-form-item>
           <el-form-item label="备注" prop="remark">
              <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
           </el-form-item>
        </el-form>
        <template #footer>
           <div class="dialog-footer">
              <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
              <el-button @click="cancel">取 æ¶ˆ</el-button>
           </div>
        </template>
     </el-dialog>
  </div>
</template>
<script setup name="Post">
import { listPost, addPost, delPost, getPost, updatePost } from "@/api/system/post"
import {onMounted} from "vue";
const { proxy } = getCurrentInstance()
const { sys_normal_disable } = proxy.useDict("sys_normal_disable")
const postList = ref([])
const open = ref(false)
const loading = ref(true)
const showSearch = ref(true)
const ids = ref([])
const single = ref(true)
const multiple = ref(true)
const total = ref(0)
const title = ref("")
const data = reactive({
 form: {},
 queryParams: {
   pageNum: 1,
   pageSize: 10,
   postCode: undefined,
   postName: undefined,
   status: undefined
 },
 rules: {
   postName: [{ required: true, message: "岗位名称不能为空", trigger: "blur" }],
   postCode: [{ required: true, message: "岗位编码不能为空", trigger: "blur" }],
   postSort: [{ required: true, message: "岗位顺序不能为空", trigger: "blur" }],
 }
})
const { queryParams, form, rules } = toRefs(data)
/** æŸ¥è¯¢å²—位列表 */
function getList() {
 loading.value = true
 listPost(queryParams.value).then(response => {
   postList.value = response.rows
   total.value = response.total
   loading.value = false
 })
}
/** å–消按钮 */
function cancel() {
 open.value = false
 reset()
}
/** è¡¨å•重置 */
function reset() {
 form.value = {
   postId: undefined,
   postCode: undefined,
   postName: undefined,
   postSort: 0,
   status: "0",
   remark: undefined
 }
 proxy.resetForm("postRef")
}
/** æœç´¢æŒ‰é’®æ“ä½œ */
function handleQuery() {
 queryParams.value.pageNum = 1
 getList()
}
/** é‡ç½®æŒ‰é’®æ“ä½œ */
function resetQuery() {
 proxy.resetForm("queryRef")
 handleQuery()
}
/** å¤šé€‰æ¡†é€‰ä¸­æ•°æ® */
function handleSelectionChange(selection) {
 ids.value = selection.map(item => item.postId)
 single.value = selection.length != 1
 multiple.value = !selection.length
}
/** æ–°å¢žæŒ‰é’®æ“ä½œ */
function handleAdd() {
 reset()
 open.value = true
 title.value = "添加岗位"
}
/** ä¿®æ”¹æŒ‰é’®æ“ä½œ */
function handleUpdate(row) {
 reset()
 const postId = row.postId || ids.value
 getPost(postId).then(response => {
   form.value = response.data
   open.value = true
   title.value = "修改岗位"
 })
}
/** æäº¤æŒ‰é’® */
function submitForm() {
 proxy.$refs["postRef"].validate(valid => {
   if (valid) {
     if (form.value.postId != undefined) {
       updatePost(form.value).then(response => {
         proxy.$modal.msgSuccess("修改成功")
         open.value = false
         getList()
       })
     } else {
       addPost(form.value).then(response => {
         proxy.$modal.msgSuccess("新增成功")
         open.value = false
         getList()
       })
     }
   }
 })
}
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
function handleDelete(row) {
 const postIds = row.postId || ids.value
 proxy.$modal.confirm('是否确认删除岗位编号为"' + postIds + '"的数据项?').then(function() {
   return delPost(postIds)
 }).then(() => {
   getList()
   proxy.$modal.msgSuccess("删除成功")
 }).catch(() => {})
}
/** å¯¼å‡ºæŒ‰é’®æ“ä½œ */
function handleExport() {
 proxy.download("system/post/export", {
   ...queryParams.value
 }, `post_${new Date().getTime()}.xlsx`)
}
onMounted(() => {
 getList();
});
</script>
src/views/officeProcessAutomation/HrManage/regular-apply/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,676 @@
<!--OA模块:转正申请-->
<template>
  <div class="app-container">
    <div class="search_form mb20">
      <div>
        <span class="search_title">申请人:</span>
        <el-input
          v-model="searchForm.applicantName"
          style="width: 220px"
          placeholder="请输入申请人"
          clearable
          :prefix-icon="Search"
          @keyup.enter="handleQuery"
        />
        <span class="search_title" style="margin-left: 12px">申请日期:</span>
        <el-date-picker
          v-model="searchForm.applyDateRange"
          type="daterange"
          range-separator="至"
          start-placeholder="开始"
          end-placeholder="结束"
          format="YYYY-MM-DD"
          value-format="YYYY-MM-DD"
          style="width: 260px"
          clearable
        />
        <el-button type="primary" style="margin-left: 10px" @click="handleQuery">搜索</el-button>
        <el-button @click="resetSearch">重置</el-button>
      </div>
      <div>
        <el-button type="primary" @click="openFormDialog('add')">新增转正申请</el-button>
      </div>
    </div>
    <div class="table_list">
      <PIMTable
        rowKey="id"
        :column="tableColumn"
        :tableData="tableData"
        :page="page"
        :isSelection="false"
        :tableLoading="tableLoading"
        @pagination="pagination"
        :total="page.total"
      />
    </div>
    <!-- æ–°å¢ž / ç¼–辑 -->
    <el-dialog
      v-model="formDialog.visible"
      :title="formDialog.title"
      width="960px"
      append-to-body
      destroy-on-close
      class="regular-apply-form-dialog"
      @closed="onFormClosed"
    >
      <el-form ref="formRef" :model="form" :rules="formRules" label-width="140px" class="regular-apply-form">
        <el-row :gutter="24">
          <el-col :span="12">
            <el-form-item label="申请人" prop="applicantName">
              <el-input v-model="form.applicantName" placeholder="请输入申请人" maxlength="50" show-word-limit />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="申请日期" prop="applyDate">
              <el-date-picker
                v-model="form.applyDate"
                type="date"
                placeholder="请选择申请日期"
                format="YYYY-MM-DD"
                value-format="YYYY-MM-DD"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="12">
            <el-form-item label="转正日期" prop="regularizationDate">
              <el-date-picker
                v-model="form.regularizationDate"
                type="date"
                placeholder="请选择转正日期"
                format="YYYY-MM-DD"
                value-format="YYYY-MM-DD"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="审批方式" prop="approvalMode">
              <el-radio-group v-model="form.approvalMode">
                <el-radio value="parallel">与签</el-radio>
                <el-radio value="countersign">会签</el-radio>
              </el-radio-group>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="24">
            <el-form-item label="审批人" prop="approverIds">
              <el-tree-select
                v-model="form.approverIds"
                :data="approverTreeData"
                multiple
                collapse-tags
                collapse-tags-tooltip
                :max-collapse-tags="2"
                :render-after-expand="false"
                placeholder="请选择审批人(可多选)"
                style="width: 100%"
                :props="{ value: 'id', label: 'label', children: 'children', disabled: 'disabled' }"
                check-strictly
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="24">
            <el-form-item label="试用期工作总结" prop="probationSummary">
              <el-input
                v-model="form.probationSummary"
                type="textarea"
                :rows="4"
                placeholder="请填写试用期工作总结"
                maxlength="2000"
                show-word-limit
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="24">
            <el-form-item label="附件">
              <div class="upload-block">
                <FileUpload v-model:file-list="form.attachmentList" :limit="10" button-text="点击选择文件" />
              </div>
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
          <el-button @click="formDialog.visible = false">取 æ¶ˆ</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- è¯¦æƒ…(只读) -->
    <el-dialog v-model="detailDialog.visible" title="转正申请详情" width="640px" append-to-body>
      <el-descriptions :column="1" border>
        <el-descriptions-item label="申请人">{{ detailRow.applicantName }}</el-descriptions-item>
        <el-descriptions-item label="申请日期">{{ detailRow.applyDate }}</el-descriptions-item>
        <el-descriptions-item label="转正日期">{{ detailRow.regularizationDate }}</el-descriptions-item>
        <el-descriptions-item label="试用期工作总结">{{ detailRow.probationSummary }}</el-descriptions-item>
        <el-descriptions-item label="审批结果">{{ approvalResultLabel(detailRow.approvalResult) }}</el-descriptions-item>
        <el-descriptions-item label="审批方式">{{ approvalModeLabel(detailRow.approvalMode) }}</el-descriptions-item>
        <el-descriptions-item label="审批人">{{ detailRow.approverNames || "—" }}</el-descriptions-item>
        <el-descriptions-item label="附件">
          <template v-if="detailRow.attachmentList?.length">
            <el-tag
              v-for="(f, i) in detailRow.attachmentList"
              :key="i"
              class="mr6 mb6"
              type="info"
            >
              {{ f.name }}
            </el-tag>
          </template>
          <span v-else>无</span>
        </el-descriptions-item>
      </el-descriptions>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="detailDialog.visible = false">关 é—­</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- é™„件列表 -->
    <el-dialog v-model="filesDialog.visible" title="附件" width="520px" append-to-body>
      <el-table v-if="filesDialog.row?.attachmentList?.length" :data="filesDialog.row.attachmentList" border>
        <el-table-column type="index" label="序号" width="60" align="center" />
        <el-table-column prop="name" label="文件名" min-width="200" show-overflow-tooltip />
        <el-table-column label="操作" width="100" align="center">
          <template #default="{ row }">
            <el-button link type="primary" @click="mockDownload(row)">下载</el-button>
          </template>
        </el-table-column>
      </el-table>
      <el-empty v-else description="暂无附件" />
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="filesDialog.visible = false">关 é—­</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { Search } from "@element-plus/icons-vue";
import FileUpload from "@/components/AttachmentUpload/file/index.vue";
import { deptTreeSelect, userListNoPageByTenantId } from "@/api/system/user.js";
import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref, watch } from "vue";
/** ä¸ŽåŽç«¯çº¦å®šå­—段(占位) */
const createEmptyForm = () => ({
  id: undefined,
  applicantName: "",
  applyDate: "",
  regularizationDate: "",
  probationSummary: "",
  approvalMode: "parallel",
  approverIds: [],
  approverNames: "",
  attachmentList: [],
});
const { proxy } = getCurrentInstance();
/** å®¡æ‰¹äººæ ‘:部门树 + ç³»ç»Ÿç”¨æˆ·ï¼ˆä¸Ž staff-archive / user-manage åŒæºæŽ¥å£ï¼‰ */
const approverTreeData = ref([]);
const approverLabelMap = ref({});
/** æŽ¥å£è¿”回统一拆成数组(兼容 axios æ‹¦æˆªå™¨å·²è§£åŒ…为 { data } æˆ–直接数组等情况) */
function unwrapArray(payload) {
  if (Array.isArray(payload)) return payload;
  if (payload && Array.isArray(payload.data)) return payload.data;
  if (payload && Array.isArray(payload.rows)) return payload.rows;
  return [];
}
function filterDisabledDept(deptList) {
  if (!Array.isArray(deptList)) return [];
  return deptList.filter((dept) => {
    if (dept.disabled) return false;
    if (dept.children?.length) {
      dept.children = filterDisabledDept(dept.children);
    }
    return true;
  });
}
function getUserDeptId(u) {
  return (
    u.deptId ??
    u.sysDeptId ??
    u.dept?.deptId ??
    u.dept?.id ??
    u.dept_id
  );
}
/** éƒ¨é—¨æ ‘节点主键(若依一般为 id,部分场景为 value) */
function getDeptNodeKey(node) {
  const k = node?.id ?? node?.value ?? node?.deptId;
  if (k == null || k === "") return null;
  return k;
}
function isActiveUser(u) {
  if (u.delFlag === "2" || u.delFlag === 2) return false;
  if (u.status == null) return true;
  return String(u.status) === "0";
}
function userToTreeLeaf(u) {
  return {
    id: String(u.userId ?? u.id),
    label: u.nickName || u.userName || `用户${u.userId ?? u.id}`,
  };
}
/** æŒ‰éƒ¨é—¨ id åˆ†ç»„;无部门或 id ä¸º 0 çš„用户进入未分配列表 */
function buildUsersByDeptId(users) {
  const map = new Map();
  const unassigned = [];
  for (const u of users) {
    if (!isActiveUser(u)) continue;
    const did = getUserDeptId(u);
    if (did == null || did === "" || did === 0 || did === "0") {
      unassigned.push(u);
      continue;
    }
    const k = String(did);
    if (!map.has(k)) map.set(k, []);
    map.get(k).push(u);
  }
  return { map, unassigned };
}
function collectUserLabels(nodes, map) {
  (nodes || []).forEach((n) => {
    if (n.children?.length) {
      collectUserLabels(n.children, map);
    } else if (n.id != null && !String(n.id).startsWith("dept_")) {
      map[String(n.id)] = n.label;
    }
  });
}
/** éƒ¨é—¨èŠ‚ç‚¹ id åŠ å‰ç¼€ï¼Œé¿å…ä¸Ž userId æ•°å€¼å†²çªï¼›å¯é€‰èŠ‚ç‚¹ä¸ºçœŸå®ž userId å­—符串 */
function mergeDeptTreeWithUsers(nodes, usersByDept) {
  if (!Array.isArray(nodes)) return [];
  const out = [];
  for (const node of nodes) {
    const deptIdRaw = getDeptNodeKey(node);
    if (deptIdRaw == null) continue;
    const sub = mergeDeptTreeWithUsers(node.children || [], usersByDept);
    const usersHere = usersByDept.get(String(deptIdRaw)) || [];
    const userChildren = usersHere.map(userToTreeLeaf);
    const children = [...sub, ...userChildren];
    if (!children.length) continue;
    out.push({
      id: `dept_${deptIdRaw}`,
      label: node.label ?? node.deptName ?? "部门",
      disabled: true,
      children,
    });
  }
  return out;
}
function buildFlatApproverTree(users) {
  const list = users.filter(isActiveUser).map(userToTreeLeaf);
  if (!list.length) return [];
  return [
    {
      id: "dept_all_users",
      label: "系统用户",
      disabled: true,
      children: list,
    },
  ];
}
async function loadApproverTree() {
  try {
    const [deptRes, userRes] = await Promise.all([deptTreeSelect(), userListNoPageByTenantId()]);
    let rawTree = unwrapArray(deptRes);
    rawTree = rawTree.length ? JSON.parse(JSON.stringify(rawTree)) : [];
    let deptTree = filterDisabledDept(JSON.parse(JSON.stringify(rawTree)));
    if (!deptTree.length && rawTree.length) {
      deptTree = JSON.parse(JSON.stringify(rawTree));
    }
    const users = unwrapArray(userRes);
    const { map: usersByDept, unassigned } = buildUsersByDeptId(users);
    let merged = mergeDeptTreeWithUsers(deptTree, usersByDept);
    if (unassigned.length) {
      merged.push({
        id: "dept_unassigned",
        label: "未分配部门",
        disabled: true,
        children: unassigned.map(userToTreeLeaf),
      });
    }
    if (!merged.length && users.length) {
      merged = buildFlatApproverTree(users);
    }
    approverTreeData.value = merged;
    const map = {};
    collectUserLabels(merged, map);
    approverLabelMap.value = map;
  } catch {
    approverTreeData.value = [];
    approverLabelMap.value = {};
    proxy?.$modal?.msgWarning?.("审批人数据加载失败,请检查网络或稍后重试");
  }
}
function resolveApproverNames(ids) {
  if (!ids?.length) return "";
  const map = approverLabelMap.value;
  return ids.map((id) => map[String(id)] || id).join("、");
}
function approvalModeLabel(mode) {
  if (mode === "countersign") return "会签";
  return "与签";
}
function approvalResultLabel(v) {
  if (v === "approved") return "已通过";
  if (v === "rejected") return "已驳回";
  if (v === "cancelled") return "已撤销";
  return "待审批";
}
/** æœ¬åœ°æ¨¡æ‹Ÿæ•°æ®æº */
const allRows = ref([
  {
    id: "1",
    applicantName: "周明",
    applyDate: "2026-05-01",
    regularizationDate: "2026-06-01",
    probationSummary: "试用期内完成模块开发与联调,熟悉业务流程。",
    approvalMode: "parallel",
    approverIds: [],
    approverNames: "",
    approvalResult: "pending",
    attachmentList: [{ name: "工作总结.pdf" }, { name: "考核表.xlsx" }],
  },
  {
    id: "2",
    applicantName: "吴芳",
    applyDate: "2026-05-08",
    regularizationDate: "2026-06-10",
    probationSummary: "完成入职培训与岗位实践,达到岗位要求。",
    approvalMode: "countersign",
    approverIds: [],
    approverNames: "",
    approvalResult: "approved",
    attachmentList: [],
  },
]);
const searchForm = reactive({
  applicantName: "",
  applyDateRange: null,
});
const tableLoading = ref(false);
const page = reactive({
  current: 1,
  size: 10,
  total: 0,
});
const filteredList = computed(() => {
  let list = [...allRows.value];
  const name = (searchForm.applicantName || "").trim();
  if (name) {
    list = list.filter((r) => r.applicantName.includes(name));
  }
  const range = searchForm.applyDateRange;
  if (range && range.length === 2) {
    const [start, end] = range;
    list = list.filter((r) => r.applyDate >= start && r.applyDate <= end);
  }
  return list.sort((a, b) => (a.applyDate < b.applyDate ? 1 : -1));
});
watch(
  filteredList,
  (list) => {
    page.total = list.length;
    const maxPage = Math.max(1, Math.ceil(list.length / page.size) || 1);
    if (page.current > maxPage) {
      page.current = maxPage;
    }
  },
  { immediate: true }
);
const tableData = computed(() => {
  const list = filteredList.value;
  const start = (page.current - 1) * page.size;
  return list.slice(start, start + page.size);
});
const tableColumn = ref([
  { label: "申请人", prop: "applicantName", minWidth: 100 },
  { label: "申请日期", prop: "applyDate", width: 120 },
  { label: "转正日期", prop: "regularizationDate", width: 120 },
  { label: "试用期工作总结", prop: "probationSummary", minWidth: 200 },
  {
    label: "审批结果",
    prop: "approvalResult",
    width: 110,
    dataType: "tag",
    formatData: (v) => approvalResultLabel(v),
    formatType: (v) => {
      if (v === "approved") return "success";
      if (v === "rejected") return "danger";
      if (v === "cancelled") return "info";
      return "warning";
    },
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: "right",
    width: 200,
    operation: [
      {
        name: "编辑",
        type: "text",
        clickFun: (row) => openFormDialog("edit", row),
      },
      {
        name: "查看详情",
        type: "text",
        clickFun: (row) => openDetail(row),
      },
      {
        name: "附件",
        type: "text",
        clickFun: (row) => openFiles(row),
      },
    ],
  },
]);
const formDialog = reactive({
  visible: false,
  title: "",
  mode: "add",
});
const formRef = ref();
const form = reactive(createEmptyForm());
const formRules = {
  applicantName: [{ required: true, message: "请输入申请人", trigger: "blur" }],
  applyDate: [{ required: true, message: "请选择申请日期", trigger: "change" }],
  regularizationDate: [{ required: true, message: "请选择转正日期", trigger: "change" }],
  probationSummary: [{ required: true, message: "请填写试用期工作总结", trigger: "blur" }],
  approvalMode: [{ required: true, message: "请选择审批方式", trigger: "change" }],
  approverIds: [
    {
      type: "array",
      required: true,
      message: "请选择审批人",
      trigger: "change",
    },
  ],
};
const detailDialog = reactive({ visible: false });
const detailRow = ref({});
const filesDialog = reactive({ visible: false, row: null });
function handleQuery() {
  page.current = 1;
  tableLoading.value = true;
  setTimeout(() => {
    tableLoading.value = false;
  }, 150);
}
function resetSearch() {
  searchForm.applicantName = "";
  searchForm.applyDateRange = null;
  handleQuery();
}
function pagination(obj) {
  page.current = obj.page;
  page.size = obj.limit;
}
function openDetail(row) {
  detailRow.value = { ...row };
  detailDialog.visible = true;
}
function openFiles(row) {
  filesDialog.row = row;
  filesDialog.visible = true;
}
function mockDownload(row) {
  const url = row.url || row.downloadURL || row.previewURL || row.previewUrl;
  if (url) {
    window.open(url, "_blank");
    return;
  }
  proxy?.$modal?.msgSuccess?.(`已模拟下载:${row.name}`);
}
function openFormDialog(mode, row) {
  formDialog.mode = mode;
  formDialog.title = mode === "add" ? "新增转正申请" : "编辑转正申请";
  loadApproverTree();
  Object.assign(form, createEmptyForm());
  if (mode === "edit" && row) {
    Object.assign(form, {
      id: row.id,
      applicantName: row.applicantName,
      applyDate: row.applyDate,
      regularizationDate: row.regularizationDate,
      probationSummary: row.probationSummary,
      approvalMode: row.approvalMode,
      approverIds: (row.approverIds || []).map((id) => String(id)),
      approverNames: row.approverNames,
      attachmentList: JSON.parse(JSON.stringify(row.attachmentList || [])),
    });
  }
  formDialog.visible = true;
  nextTick(() => formRef.value?.clearValidate?.());
}
function onFormClosed() {
  formRef.value?.resetFields?.();
}
async function submitForm() {
  try {
    await formRef.value?.validate?.();
  } catch {
    return;
  }
  form.approverNames = resolveApproverNames(form.approverIds);
  const payload = {
    applicantName: form.applicantName,
    applyDate: form.applyDate,
    regularizationDate: form.regularizationDate,
    probationSummary: form.probationSummary,
    approvalMode: form.approvalMode,
    approverIds: [...form.approverIds],
    approverNames: form.approverNames,
    attachmentList: JSON.parse(JSON.stringify(form.attachmentList || [])),
  };
  if (formDialog.mode === "add") {
    const id = `local_${Date.now()}`;
    allRows.value.unshift({ id, ...payload, approvalResult: "pending" });
    proxy?.$modal?.msgSuccess?.("新增成功(本地模拟)");
  } else {
    const idx = allRows.value.findIndex((r) => r.id === form.id);
    if (idx !== -1) {
      const prev = allRows.value[idx];
      allRows.value[idx] = {
        ...prev,
        id: form.id,
        ...payload,
        approvalResult: prev.approvalResult ?? "pending",
      };
    }
    proxy?.$modal?.msgSuccess?.("保存成功(本地模拟)");
  }
  formDialog.visible = false;
  handleQuery();
}
onMounted(() => {
  loadApproverTree();
});
</script>
<style scoped>
.mb20 {
  margin-bottom: 20px;
}
.search_form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.search_title {
  font-size: 14px;
  color: var(--el-text-color-regular);
}
.upload-block {
  width: 100%;
}
.mr6 {
  margin-right: 6px;
}
.mb6 {
  margin-bottom: 6px;
}
.regular-apply-form :deep(.el-row) {
  margin-bottom: 0;
}
.regular-apply-form :deep(.el-form-item) {
  margin-bottom: 18px;
}
.regular-apply-form-dialog :deep(.el-dialog__body) {
  padding-top: 12px;
}
</style>
src/views/officeProcessAutomation/HrManage/resign-apply/components/formDia.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,347 @@
<template>
  <div>
    <el-dialog
        v-model="dialogFormVisible"
        :title="operationType === 'add' ? '新增离职' : '编辑离职'"
        width="70%"
        @close="closeDia"
    >
      <!-- å‘˜å·¥ä¿¡æ¯å±•示区域 -->
      <div class="info-section">
        <div class="info-title">员工信息</div>
        <el-form :model="form" label-width="200px" label-position="left" :rules="rules" ref="formRef" style="margin-top: 20px">
          <el-row :gutter="30">
            <el-col :span="12">
              <el-form-item label="姓名:" prop="staffOnJobId">
                <el-select v-model="form.staffOnJobId"
                           placeholder="请选择人员"
                           style="width: 100%"
                           :disabled="operationType === 'edit'"
                           @change="handleSelect">
                  <el-option
                      v-for="item in personList"
                      :key="item.id"
                      :label="item.staffName"
                      :value="item.id"
                  />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="员工编号:">
                {{ currentStaffRecord.staffNo || '-' }}
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="30">
            <el-col :span="12">
              <el-form-item label="性别:">
                {{ currentStaffRecord.sex || '-' }}
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="户籍住址:">
                {{ currentStaffRecord.nativePlace || '-' }}
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="30">
            <el-col :span="12">
              <el-form-item label="岗位:">
                {{ currentStaffRecord.postName || '-' }}
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="现住址:">
                {{ currentStaffRecord.adress || '-' }}
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="30">
            <el-col :span="12">
              <el-form-item label="第一学历:">
                {{ currentStaffRecord.firstStudy || '-' }}
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="专业:">
                {{ currentStaffRecord.profession || '-' }}
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="30">
            <el-col :span="12">
              <el-form-item label="年龄:">
                {{ currentStaffRecord.age || '-' }}
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="30">
            <el-col :span="12">
              <el-form-item label="联系电话:">
                {{ currentStaffRecord.phone || '-' }}
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="紧急联系人:">
                {{ currentStaffRecord.emergencyContact || '-' }}
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="30">
            <el-col :span="12">
              <el-form-item label="紧急联系人联系电话:">
                {{ currentStaffRecord.emergencyContactPhone || '-' }}
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="30">
            <el-col :span="12">
              <el-form-item label="离职日期:" prop="leaveDate">
                <el-date-picker
                    v-model="form.leaveDate"
                    type="date"
                    :disabled="operationType === 'edit'"
                    :disabled-date="disabledFutureDate"
                    placeholder="请选择离职日期"
                    value-format="YYYY-MM-DD"
                    format="YYYY-MM-DD"
                    style="width: 100%"
                />
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="离职原因:" prop="reason">
                <el-select v-model="form.reason" placeholder="请选择离职原因" style="width: 100%" @change="handleSelectDimissionReason">
                  <el-option
                      v-for="(item, index) in dimissionReasonOptions"
                      :key="index"
                      :label="item.label"
                      :value="item.value"
                  />
                </el-select>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="30">
            <el-col :span="12">
              <el-form-item label="备注:" prop="remark" v-if="form.reason === 'other'">
                <el-input
                    v-model="form.remark"
                    type="textarea"
                    :rows="3"
                    placeholder="备注"
                    maxlength="500"
                    show-word-limit
                />
              </el-form-item>
            </el-col>
          </el-row>
        </el-form>
<!--        <el-row :gutter="30">-->
<!--          <el-col :span="12">-->
<!--            <div class="info-item">-->
<!--              <span class="info-label">离职原因:</span>-->
<!--              <el-select v-model="form.reason" placeholder="请选择人员" style="width: 100%" @change="handleSelect">-->
<!--                <el-option-->
<!--                    v-for="(item, index) in dimissionReasonOptions"-->
<!--                    :key="index"-->
<!--                    :label="item.label"-->
<!--                    :value="item.value"-->
<!--                />-->
<!--              </el-select>-->
<!--            </div>-->
<!--          </el-col>-->
<!--          <el-col :span="12">-->
<!--            <div class="info-item">-->
<!--              <span class="info-label">员工编号:</span>-->
<!--              <span class="info-value">{{ form.staffNo || '-' }}</span>-->
<!--            </div>-->
<!--          </el-col>-->
<!--        </el-row>-->
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">确认</el-button>
          <el-button @click="closeDia">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import {ref, reactive, toRefs, getCurrentInstance} from "vue";
import {staffOnJobListPage} from "@/api/personnelManagement/staffOnJob.js";
import {createStaffLeave, updateStaffLeave} from "@/api/personnelManagement/staffLeave.js";
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
const dialogFormVisible = ref(false);
const operationType = ref('')
const getTodayDate = () => {
  const now = new Date();
  const year = now.getFullYear();
  const month = `${now.getMonth() + 1}`.padStart(2, '0');
  const day = `${now.getDate()}`.padStart(2, '0');
  return `${year}-${month}-${day}`;
};
const disabledFutureDate = (time) => {
  const todayEnd = new Date();
  todayEnd.setHours(23, 59, 59, 999);
  return time.getTime() > todayEnd.getTime();
};
const data = reactive({
  form: {
    staffOnJobId: undefined,
    leaveDate: "",
    reason: "",
    remark: "",
  },
  rules: {
    staffName: [{ required: true, message: "请选择人员" }],
    leaveDate: [{ required: true, message: "请选择离职日期", trigger: "change" }],
    reason: [{ required: true, message: "请选择离职原因"}],
  },
  dimissionReasonOptions: [
      {label: '薪资待遇', value: 'salary'},
      {label: '职业发展', value: 'career_development'},
      {label: '工作环境', value: 'work_environment'},
      {label: '个人原因', value: 'personal_reason'},
      {label: '其他', value: 'other'},
  ],
  currentStaffRecord: {},
});
const { form, rules, dimissionReasonOptions, currentStaffRecord } = toRefs(data);
// æ‰“开弹框
const openDialog = (type, row) => {
  operationType.value = type;
  dialogFormVisible.value = true;
  if (operationType.value === 'edit') {
    currentStaffRecord.value = row
    form.value.staffOnJobId = row.staffOnJobId
    form.value.leaveDate = row.leaveDate
    form.value.reason = row.reason
    form.value.remark = row.remark
    personList.value = [
      {
        staffName: row.staffName,
        id: row.staffOnJobId,
      }
    ]
  } else {
    form.value.leaveDate = getTodayDate()
    getList()
  }
}
const handleSelectDimissionReason = (val) => {
  if (val === 'other') {
    form.value.remark = ''
  }
}
// æäº¤äº§å“è¡¨å•
const submitForm = () => {
  form.value.staffState = 0
  if (form.value.reason !== 'other') {
    form.value.remark = ''
  }
  proxy.$refs["formRef"].validate(valid => {
    if (valid) {
      if (operationType.value === "add") {
        createStaffLeave(form.value).then(res => {
          proxy.$modal.msgSuccess("提交成功");
          closeDia();
        })
      } else {
        updateStaffLeave(currentStaffRecord.value.id, form.value).then(res => {
          proxy.$modal.msgSuccess("提交成功");
          closeDia();
        })
      }
    }
  })
}
// å…³é—­å¼¹æ¡†
const closeDia = () => {
  // è¡¨å•已注释,手动重置表单数据
  form.value = {
    staffOnJobId: undefined,
    leaveDate: "",
    reason: "",
    remark: "",
  };
  dialogFormVisible.value = false;
  emit('close')
};
const personList = ref([]);
/**
 * èŽ·å–å½“å‰åœ¨èŒäººå‘˜åˆ—è¡¨
 */
const getList = () => {
  staffOnJobListPage({
    current: -1,
    size: -1,
        staffState: 1
  }).then(res => {
    personList.value = res.data.records || []
  })
};
const handleSelect = (val) => {
  let obj = personList.value.find(item => item.id === val)
  currentStaffRecord.value = {}
  if (obj) {
    // ä¿ç•™ç¦»èŒæ—¥æœŸå’Œç¦»èŒåŽŸå› ï¼Œåªæ›´æ–°å‘˜å·¥ä¿¡æ¯
    currentStaffRecord.value = obj
  }
}
defineExpose({
  openDialog,
});
</script>
<style scoped>
.info-section {
  background: #f5f7fa;
  padding: 20px;
  border-radius: 8px;
  margin-bottom: 20px;
}
.info-title {
  font-size: 16px;
  font-weight: 600;
  color: #303133;
  margin-bottom: 20px;
  padding-bottom: 10px;
  border-bottom: 1px solid #e4e7ed;
}
.info-item {
  display: flex;
  align-items: center;
  margin-bottom: 16px;
  min-height: 32px;
}
.info-label {
  min-width: 140px;
  color: #606266;
  font-size: 14px;
  font-weight: 500;
}
.info-value {
  flex: 1;
  color: #303133;
  font-size: 14px;
}
</style>
src/views/officeProcessAutomation/HrManage/resign-apply/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,245 @@
<!--OA模块:离职申请-->
<template>
  <div class="app-container">
    <div class="search_form mb20">
      <div>
        <span class="search_title">姓名:</span>
        <el-input
            v-model="searchForm.staffName"
            style="width: 240px"
            placeholder="请输入姓名搜索"
            @change="handleQuery"
            clearable
            :prefix-icon="Search"
        />
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
        >搜索</el-button
        >
      </div>
      <div>
        <el-button type="primary" @click="openForm('add')">新增离职</el-button>
        <el-button @click="handleOut">导出</el-button>
        <!-- <el-button type="danger" plain @click="handleDelete">删除</el-button> -->
      </div>
    </div>
    <div class="table_list">
      <PIMTable
          rowKey="id"
          :column="tableColumn"
          :tableData="tableData"
          :page="page"
          :isSelection="true"
          @selection-change="handleSelectionChange"
          :tableLoading="tableLoading"
          @pagination="pagination"
          :total="page.total"
      ></PIMTable>
    </div>
    <form-dia ref="formDia" @close="handleQuery"></form-dia>
  </div>
</template>
<script setup>
import { Search } from "@element-plus/icons-vue";
import {onMounted, ref} from "vue";
import FormDia from "@/views/personnelManagement/dimission/components/formDia.vue";
import {findStaffLeaveListPage, batchDeleteStaffLeaves} from "@/api/personnelManagement/staffLeave.js";
import {ElMessageBox} from "element-plus";
const data = reactive({
  searchForm: {
    staffName: "",
  },
});
const { searchForm } = toRefs(data);
const tableColumn = ref([
  {
    label: "状态",
    prop: "staffState",
    dataType: "tag",
    formatData: (params) => {
      if (params == 0) {
        return "离职";
      } else if (params == 1) {
        return "在职";
      } else {
        return null;
      }
    },
    formatType: (params) => {
      if (params == 0) {
        return "danger";
      } else if (params == 1) {
        return "primary";
      } else {
        return null;
      }
    },
  },
  {
    label: "离职日期",
    prop: "leaveDate",
  },
  {
    label: "员工编号",
    prop: "staffNo",
  },
  {
    label: "姓名",
    prop: "staffName",
  },
  {
    label: "性别",
    prop: "sex",
  },
  {
    label: "户籍住址",
    prop: "nativePlace",
  },
  {
    label: "部门",
    prop: "deptName",
  },
  {
    label: "岗位",
    prop: "postName",
  },
  {
    label: "现住址",
    prop: "adress",
    width:200
  },
  {
    label: "第一学历",
    prop: "firstStudy",
  },
  {
    label: "专业",
    prop: "profession",
    width:100
  },
  {
    label: "年龄",
    prop: "age",
  },
  {
    label: "联系电话",
    prop: "phone",
    width:150
  },
  {
    label: "紧急联系人",
    prop: "emergencyContact",
    width: 120
  },
  {
    label: "紧急联系人电话",
    prop: "emergencyContactPhone",
    width:150
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: 'right',
    operation: [
      {
        name: "编辑",
        type: "text",
        clickFun: (row) => {
          openForm("edit", row);
        },
      },
    ],
  },
]);
const tableData = ref([]);
const selectedRows = ref([]);
const tableLoading = ref(false);
const page = reactive({
  current: 1,
  size: 100,
  total: 0,
});
const formDia = ref()
const { proxy } = getCurrentInstance()
// æŸ¥è¯¢åˆ—表
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
  page.current = 1;
  getList();
};
const pagination = (obj) => {
  page.current = obj.page;
  page.size = obj.limit;
  getList();
};
const getList = () => {
  tableLoading.value = true;
  findStaffLeaveListPage({...page, ...searchForm.value}).then(res => {
    tableLoading.value = false;
    tableData.value = res.data.records
    page.total = res.data.total;
  }).catch(err => {
    tableLoading.value = false;
  })
};
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
};
// æ‰“开弹框
const openForm = (type, row) => {
  nextTick(() => {
    formDia.value?.openDialog(type, row)
  })
};
// åˆ é™¤
const handleDelete = () => {
  let ids = [];
  if (selectedRows.value.length > 0) {
    ids = selectedRows.value.map((item) => item.id);
  } else {
    proxy.$modal.msgWarning("请选择数据");
    return;
  }
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
      .then(() => {
        batchDeleteStaffLeaves(ids).then((res) => {
          proxy.$modal.msgSuccess("删除成功");
          getList();
        });
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
};
// å¯¼å‡º
const handleOut = () => {
  ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
      .then(() => {
        proxy.download("/staff/staffLeave/export", {}, "人员离职.xlsx");
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
};
onMounted(() => {
  getList();
});
</script>
<style scoped></style>
src/views/officeProcessAutomation/HrManage/staff-archive/components/BasicInfoSection.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,181 @@
<template>
  <el-card class="form-card" shadow="never">
    <template #header>
      <span class="card-title">
        <span class="card-title-line">|</span>
        åŸºæœ¬ä¿¡æ¯
      </span>
    </template>
    <el-row :gutter="24">
      <el-col :span="5">
        <el-form-item label="员工编号" prop="staffNo">
          <el-input
            v-model="form.staffNo"
            placeholder="请输入"
            clearable
            maxlength="20"
            show-word-limit
            :disabled="operationType !== 'add'"
          />
        </el-form-item>
      </el-col>
      <el-col :span="5">
        <el-form-item label="姓名" prop="staffName">
          <el-input
            v-model="form.staffName"
            placeholder="请输入"
            clearable
            maxlength="50"
            show-word-limit
          />
        </el-form-item>
      </el-col>
      <el-col :span="5">
        <el-form-item label="别名" prop="alias">
          <el-input
            v-model="form.alias"
            placeholder="请输入"
            clearable
            maxlength="50"
            show-word-limit
          />
        </el-form-item>
      </el-col>
      <el-col :span="5">
        <el-form-item label="手机" prop="phone">
          <el-input
            v-model="form.phone"
            placeholder="请输入"
            clearable
            maxlength="11"
            show-word-limit
          />
        </el-form-item>
      </el-col>
      <el-col :span="4">
        <el-form-item label="性别" prop="sex">
          <el-select
            v-model="form.sex"
            placeholder="请选择"
            clearable
            style="width: 100%"
          >
            <el-option label="男" value="男" />
            <el-option label="女" value="女" />
          </el-select>
        </el-form-item>
      </el-col>
    </el-row>
    <el-row :gutter="24">
      <el-col :span="5">
        <el-form-item label="出生日期" prop="birthDate">
          <el-date-picker
            v-model="form.birthDate"
            type="date"
            value-format="YYYY-MM-DD"
            format="YYYY-MM-DD"
            placeholder="请选择"
            style="width: 100%"
            clearable
          />
        </el-form-item>
      </el-col>
      <el-col :span="5">
        <el-form-item label="年龄" prop="age">
          <el-input-number
            v-model="form.age"
            :min="0"
            :max="150"
            :precision="0"
            :step="1"
            style="width: 100%"
          />
        </el-form-item>
      </el-col>
      <el-col :span="5">
        <el-form-item label="籍贯" prop="nativePlace">
          <el-input
            v-model="form.nativePlace"
            placeholder="请输入"
            clearable
            maxlength="50"
            show-word-limit
          />
        </el-form-item>
      </el-col>
      <el-col :span="5">
        <el-form-item label="民族" prop="nation">
          <el-input
            v-model="form.nation"
            placeholder="请输入"
            clearable
            maxlength="20"
            show-word-limit
          />
        </el-form-item>
      </el-col>
      <el-col :span="4">
        <el-form-item label="婚姻状况" prop="maritalStatus">
          <el-select
            v-model="form.maritalStatus"
            placeholder="请选择"
            clearable
            style="width: 100%"
          >
            <el-option label="未婚" value="未婚" />
            <el-option label="已婚" value="已婚" />
            <el-option label="离异" value="离异" />
            <el-option label="丧偶" value="丧偶" />
          </el-select>
        </el-form-item>
      </el-col>
    </el-row>
    <el-row :gutter="24">
      <el-col :span="10">
        <el-form-item label="角色" prop="roleId">
          <el-select
            v-model="form.roleId"
            placeholder="请选择"
            clearable
            style="width: 100%"
          >
            <el-option
              v-for="item in roleOptions"
              :key="item.roleId"
              :label="item.roleName"
              :value="item.roleId"
              :disabled="item.status == 1"
            />
          </el-select>
        </el-form-item>
      </el-col>
    </el-row>
  </el-card>
</template>
<script setup>
import { toRefs } from "vue";
const props = defineProps({
  form: { type: Object, required: true },
  operationType: { type: String, default: "add" },
  roleOptions: { type: Array, default: () => [] },
});
const { form, operationType, roleOptions } = toRefs(props);
</script>
<style scoped>
.form-card {
  margin-bottom: 16px;
}
.card-title-line {
  color: #f56c6c;
  margin-right: 4px;
}
</style>
src/views/officeProcessAutomation/HrManage/staff-archive/components/EducationWorkSection.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,263 @@
<template>
  <div>
    <!-- æ•™è‚²ç»åކ -->
    <el-card class="form-card" shadow="never">
      <template #header>
        <span class="card-title">
          <span class="card-title-line">|</span>
          æ•™è‚²ç»åކ
        </span>
      </template>
      <el-table :data="form.staffEducationList" border>
        <el-table-column label="学历" prop="education" width="120">
          <template #default="{ row }">
            <el-select
              v-model="row.education"
              placeholder="请选择"
              clearable
              style="width: 100%"
            >
              <el-option label="中专及以下" value="secondary" />
              <el-option label="大专" value="junior_college" />
              <el-option label="本科" value="bachelor" />
              <el-option label="硕士" value="master" />
              <el-option label="博士及以上" value="doctor" />
            </el-select>
          </template>
        </el-table-column>
        <el-table-column label="毕业院校" prop="schoolName" min-width="160">
          <template #default="{ row }">
            <el-input
              v-model="row.schoolName"
              placeholder="请输入"
              clearable
              maxlength="30"
              show-word-limit
            />
          </template>
        </el-table-column>
        <el-table-column label="入学时间" prop="enrollTime" width="150">
          <template #default="{ row }">
            <el-date-picker
              v-model="row.enrollTime"
              type="date"
              value-format="YYYY-MM-DD"
              format="YYYY-MM-DD"
              placeholder="请选择"
              style="width: 100%"
              clearable
            />
          </template>
        </el-table-column>
        <el-table-column label="毕业时间" prop="graduateTime" width="150">
          <template #default="{ row }">
            <el-date-picker
              v-model="row.graduateTime"
              type="date"
              value-format="YYYY-MM-DD"
              format="YYYY-MM-DD"
              placeholder="请选择"
              style="width: 100%"
              clearable
            />
          </template>
        </el-table-column>
        <el-table-column label="专业" prop="major" min-width="140">
          <template #default="{ row }">
            <el-input
              v-model="row.major"
              placeholder="请输入"
              clearable
              maxlength="20"
              show-word-limit
            />
          </template>
        </el-table-column>
        <el-table-column label="学位" prop="degree" width="140">
          <template #default="{ row }">
            <el-input
              v-model="row.degree"
              placeholder="请输入"
              clearable
              maxlength="20"
              show-word-limit
            />
          </template>
        </el-table-column>
        <el-table-column label="操作" width="80" align="center">
          <template #default="scope">
            <el-button
              v-if="form.staffEducationList.length > 1"
              type="primary"
              link
              @click="removeEducationRow(scope.$index)"
            >
              åˆ é™¤
            </el-button>
          </template>
        </el-table-column>
      </el-table>
      <div class="table-add-row" @click="addEducationRow">新建一行</div>
    </el-card>
    <!-- å·¥ä½œç»åކ -->
    <el-card class="form-card" shadow="never">
      <template #header>
        <span class="card-title">
          <span class="card-title-line">|</span>
          å·¥ä½œç»åކ
        </span>
      </template>
      <el-table :data="form.staffWorkExperienceList" border>
        <el-table-column label="前公司" prop="formerCompany" min-width="180">
          <template #default="{ row }">
            <el-input
              v-model="row.formerCompany"
              placeholder="请输入"
              clearable
              maxlength="30"
              show-word-limit
            />
          </template>
        </el-table-column>
        <el-table-column label="前公司部门" prop="formerDept" min-width="140">
          <template #default="{ row }">
            <el-input
              v-model="row.formerDept"
              placeholder="请输入"
              clearable
              maxlength="20"
              show-word-limit
            />
          </template>
        </el-table-column>
        <el-table-column label="前公司职位" prop="formerPosition" min-width="140">
          <template #default="{ row }">
            <el-input
              v-model="row.formerPosition"
              placeholder="请输入"
              clearable
              maxlength="20"
              show-word-limit
            />
          </template>
        </el-table-column>
        <el-table-column label="开始日期" prop="startDate" width="150">
          <template #default="{ row }">
            <el-date-picker
              v-model="row.startDate"
              type="date"
              value-format="YYYY-MM-DD"
              format="YYYY-MM-DD"
              placeholder="请选择"
              style="width: 100%"
              clearable
            />
          </template>
        </el-table-column>
        <el-table-column label="结束日期" prop="endDate" width="150">
          <template #default="{ row }">
            <el-date-picker
              v-model="row.endDate"
              type="date"
              value-format="YYYY-MM-DD"
              format="YYYY-MM-DD"
              placeholder="请选择"
              style="width: 100%"
              clearable
            />
          </template>
        </el-table-column>
        <el-table-column label="工作描述" prop="workDesc" min-width="220">
          <template #default="{ row }">
            <el-input
              v-model="row.workDesc"
              type="textarea"
              :rows="2"
              placeholder="请输入"
              clearable
              maxlength="500"
              show-word-limit
            />
          </template>
        </el-table-column>
        <el-table-column label="操作" width="80" align="center">
          <template #default="scope">
            <el-button
              v-if="form.staffWorkExperienceList.length > 1"
              type="primary"
              link
              @click="removeWorkRow(scope.$index)"
            >
              åˆ é™¤
            </el-button>
          </template>
        </el-table-column>
      </el-table>
      <div class="table-add-row" @click="addWorkRow">新建一行</div>
    </el-card>
  </div>
</template>
<script setup>
import { toRefs } from "vue";
const props = defineProps({
  form: { type: Object, required: true },
});
const emit = defineEmits(["update:form"]);
const { form } = toRefs(props);
const addEducationRow = () => {
  form.value.staffEducationList.push({
    education: "",
    schoolName: "",
    enrollTime: "",
    graduateTime: "",
    major: "",
    degree: "",
  });
};
const removeEducationRow = (index) => {
  if (form.value.staffEducationList.length <= 1) return;
  form.value.staffEducationList.splice(index, 1);
};
const addWorkRow = () => {
  form.value.staffWorkExperienceList.push({
    formerCompany: "",
    formerDept: "",
    formerPosition: "",
    startDate: "",
    endDate: "",
    workDesc: "",
  });
};
const removeWorkRow = (index) => {
  if (form.value.staffWorkExperienceList.length <= 1) return;
  form.value.staffWorkExperienceList.splice(index, 1);
};
</script>
<style scoped>
.form-card {
  margin-bottom: 16px;
}
.card-title-line {
  color: #f56c6c;
  margin-right: 4px;
}
.table-add-row {
  margin-top: 8px;
  color: #409eff;
  cursor: pointer;
  font-size: 14px;
}
</style>
src/views/officeProcessAutomation/HrManage/staff-archive/components/EmergencyAndAttachmentSection.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,115 @@
<template>
  <div>
    <!-- ç´§æ€¥è”系人 -->
    <el-card class="form-card" shadow="never">
      <template #header>
        <span class="card-title">
          <span class="card-title-line">|</span>
          ç´§æ€¥è”系人
        </span>
      </template>
      <el-table :data="form.staffEmergencyContactList" border>
        <el-table-column label="紧急联系人姓名" prop="contactName" min-width="160">
          <template #default="{ row }">
            <el-input
              v-model="row.contactName"
              placeholder="请输入"
              clearable
              maxlength="50"
              show-word-limit
            />
          </template>
        </el-table-column>
        <el-table-column label="紧急联系人关系" prop="contactRelation" min-width="140">
          <template #default="{ row }">
            <el-input
              v-model="row.contactRelation"
              placeholder="请输入"
              clearable
              maxlength="20"
              show-word-limit
            />
          </template>
        </el-table-column>
        <el-table-column label="紧急联系人手机" prop="contactPhone" width="160">
          <template #default="{ row }">
            <el-input
              v-model="row.contactPhone"
              placeholder="请输入"
              clearable
              maxlength="11"
              show-word-limit
            />
          </template>
        </el-table-column>
        <el-table-column label="紧急联系人住址" prop="contactAddress" min-width="220">
          <template #default="{ row }">
            <el-input
              v-model="row.contactAddress"
              placeholder="请输入"
              clearable
              maxlength="50"
              show-word-limit
            />
          </template>
        </el-table-column>
        <el-table-column label="操作" width="80" align="center">
          <template #default="scope">
            <el-button
              v-if="form.staffEmergencyContactList.length > 1"
              type="primary"
              link
              @click="removeEmergencyRow(scope.$index)"
            >
              åˆ é™¤
            </el-button>
          </template>
        </el-table-column>
      </el-table>
      <div class="table-add-row" @click="addEmergencyRow">新建一行</div>
    </el-card>
  </div>
</template>
<script setup>
import { toRefs } from "vue";
const props = defineProps({
  form: { type: Object, required: true }
});
const { form } = toRefs(props);
const addEmergencyRow = () => {
  form.value.staffEmergencyContactList.push({
    contactName: "",
    contactRelation: "",
    contactPhone: "",
    contactAddress: "",
  });
};
const removeEmergencyRow = (index) => {
  if (form.value.staffEmergencyContactList.length <= 1) return;
  form.value.staffEmergencyContactList.splice(index, 1);
};
</script>
<style scoped>
.form-card {
  margin-bottom: 16px;
}
.card-title-line {
  color: #f56c6c;
  margin-right: 4px;
}
.table-add-row {
  margin-top: 8px;
  color: #409eff;
  cursor: pointer;
  font-size: 14px;
}
</style>
src/views/officeProcessAutomation/HrManage/staff-archive/components/JobInfoSection.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,176 @@
<template>
  <el-card class="form-card" shadow="never">
    <template #header>
      <span class="card-title">
        <span class="card-title-line">|</span>
        åœ¨èŒä¿¡æ¯
      </span>
    </template>
    <!-- ç¬¬ä¸€è¡Œï¼šåˆåŒå¼€å§‹ / åˆåŒç»“束 / è¯•用期 / è½¬æ­£ -->
    <el-row :gutter="24">
      <el-col :span="6">
        <el-form-item label="入职日期" prop="contractStartTime">
          <el-date-picker
            v-model="form.contractStartTime"
            type="date"
            value-format="YYYY-MM-DD"
            format="YYYY-MM-DD"
            placeholder="请选择"
            style="width: 100%"
            clearable
            @change="calculateContractTerm"
          />
        </el-form-item>
      </el-col>
      <el-col :span="6">
        <el-form-item
          label="合同结束日期"
          prop="contractEndTime"
          required
          :rules="[
            {
              required: true,
              message: '请选择合同结束日期',
              trigger: 'change',
            },
          ]"
        >
          <el-date-picker
            v-model="form.contractEndTime"
            type="date"
            value-format="YYYY-MM-DD"
            format="YYYY-MM-DD"
            placeholder="请选择"
            style="width: 100%"
            clearable
            @change="calculateContractTerm"
          />
        </el-form-item>
      </el-col>
      <el-col :span="6">
        <el-form-item label="试用期(月)" prop="probationPeriod">
          <el-input-number
            v-model="form.proTerm"
            :min="0"
            :max="24"
            :precision="0"
            :step="1"
            style="width: 100%"
          />
        </el-form-item>
      </el-col>
      <el-col :span="6">
        <el-form-item label="转正日期" prop="positiveDate">
          <el-date-picker
            v-model="form.positiveDate"
            type="date"
            value-format="YYYY-MM-DD"
            format="YYYY-MM-DD"
            placeholder="请选择"
            style="width: 100%"
            clearable
          />
        </el-form-item>
      </el-col>
    </el-row>
    <!-- ç¬¬äºŒè¡Œï¼šéƒ¨é—¨ / å²—位 / åŸºæœ¬å·¥èµ„ -->
    <el-row :gutter="24">
      <el-col :span="8">
        <el-form-item label="部门" prop="sysDeptId">
          <el-tree-select
            v-model="form.sysDeptId"
            :data="deptOptions"
            check-strictly
            :render-after-expand="false"
            placeholder="请选择"
            style="width: 100%"
          />
        </el-form-item>
      </el-col>
      <el-col :span="8">
        <el-form-item label="岗位" prop="sysPostId">
          <el-select
            v-model="form.sysPostId"
            placeholder="请选择"
            clearable
            style="width: 100%"
          >
            <el-option
              v-for="item in postOptions"
              :key="item.postId"
              :label="item.postName"
              :value="item.postId"
              :disabled="item.status === '1'"
            />
          </el-select>
        </el-form-item>
      </el-col>
      <el-col :span="8">
        <el-form-item label="基本工资" prop="basicSalary">
          <el-input-number
            v-model="form.basicSalary"
            :min="0"
            :max="999999"
            :precision="2"
            :step="100"
            style="width: 100%"
          />
        </el-form-item>
      </el-col>
    </el-row>
  </el-card>
</template>
<script setup>
import { toRefs } from "vue";
const props = defineProps({
  form: { type: Object, required: true },
  postOptions: { type: Array, default: () => [] },
  deptOptions: { type: Array, default: () => [] },
});
const { form, postOptions, deptOptions } = toRefs(props);
// è®¡ç®—合同年限
const calculateContractTerm = () => {
  if (form.value.contractStartTime && form.value.contractEndTime) {
    const startDate = new Date(form.value.contractStartTime);
    const endDate = new Date(form.value.contractEndTime);
    if (endDate > startDate) {
      // è®¡ç®—年份差
      const yearDiff = endDate.getFullYear() - startDate.getFullYear();
      const monthDiff = endDate.getMonth() - startDate.getMonth();
      const dayDiff = endDate.getDate() - startDate.getDate();
      let years = yearDiff;
      // å¦‚果结束日期的月日小于开始日期的月日,则减去1å¹´
      if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) {
        years = yearDiff - 1;
      }
      form.value.contractTerm = Math.max(0, years);
    } else {
      form.value.contractTerm = 0;
    }
  } else {
    form.value.contractTerm = 0;
  }
};
</script>
<style scoped>
.form-card {
  margin-bottom: 16px;
}
.card-title-line {
  color: #f56c6c;
  margin-right: 4px;
}
</style>
src/views/officeProcessAutomation/HrManage/staff-archive/components/NewOrEditFormDia.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,304 @@
<template>
  <FormDialog
    v-model="dialogFormVisible"
    :operation-type="operationType"
    :title="dialogTitle"
    width="90%"
    @close="closeDia"
    @confirm="submitForm"
    @cancel="closeDia"
  >
    <div class="form-dia-body">
      <el-form
        ref="formRef"
        :model="form"
        :rules="rules"
        label-position="top"
      >
        <BasicInfoSection
          :form="form"
          :operation-type="operationType"
          :role-options="roleOptions"
        />
        <JobInfoSection
          :form="form"
          :post-options="postOptions"
          :dept-options="deptOptions"
        />
        <EducationWorkSection :form="form" />
        <EmergencyAndAttachmentSection :form="form" />
      </el-form>
    </div>
  </FormDialog>
</template>
<script setup>
import {
  ref,
  reactive,
  toRefs,
  onMounted,
  getCurrentInstance,
  nextTick,
} from "vue";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import { findPostOptions } from "@/api/system/post.js";
import { deptTreeSelect, getUser } from "@/api/system/user.js";
import {
  staffOnJobInfo,
  createStaffOnJob,
  updateStaffOnJob,
} from "@/api/personnelManagement/staffOnJob.js";
import BasicInfoSection from "./BasicInfoSection.vue";
import JobInfoSection from "./JobInfoSection.vue";
import EducationWorkSection from "./EducationWorkSection.vue";
import EmergencyAndAttachmentSection from "./EmergencyAndAttachmentSection.vue";
const { proxy } = getCurrentInstance();
const emit = defineEmits(["close"]);
const dialogFormVisible = ref(false);
const operationType = ref("add");
const id = ref(0);
const formRef = ref(null);
const dialogTitle = () =>
  operationType.value === "add" ? "新增入职" : "编辑人员";
const createEmptyEducation = () => ({
  education: "",
  schoolName: "",
  enrollTime: "",
  graduateTime: "",
  major: "",
  degree: "",
});
const createEmptyWork = () => ({
  formerCompany: "",
  formerDept: "",
  formerPosition: "",
  startDate: "",
  endDate: "",
  workDesc: "",
});
const createEmptyEmergency = () => ({
  contactName: "",
  contactRelation: "",
  contactPhone: "",
  contactAddress: "",
});
const createDefaultForm = () => ({
  id: undefined,
  // åŸºæœ¬ä¿¡æ¯
  staffNo: "",
  staffName: "",
  alias: "",
  phone: "",
  sex: "",
  birthDate: "",
  age: undefined,
  nativePlace: "",
  nation: "",
  maritalStatus: "",
  politicalStatus: "",
  firstWorkDate: "",
  workingYears: undefined,
  idCardNo: "",
  hukouType: "",
  email: "",
  currentAddress: "",
  // åœ¨èŒä¿¡æ¯
  contractStartTime: "",
  contractEndTime: "",
  proTerm: undefined,
  positiveDate: "",
  sysDeptId: undefined,
  sysPostId: undefined,
  basicSalary: undefined,
  // é“¶è¡Œå¡ä¿¡æ¯
  bankName: "",
  bankCardNo: "",
  // æ•™è‚²ç»åކ
  staffEducationList: [createEmptyEducation()],
  // å·¥ä½œç»åކ
  staffWorkExperienceList: [createEmptyWork()],
  // ç´§æ€¥è”系人
  staffEmergencyContactList: [createEmptyEmergency()],
  // è§’色(单选)
  roleId: undefined,
});
const state = reactive({
  form: createDefaultForm(),
  rules: {
    staffNo: [{ required: true, message: "请输入员工编号", trigger: "blur" }],
    staffName: [{ required: true, message: "请输入姓名", trigger: "blur" }],
    phone: [{ required: true, message: "请输入手机", trigger: "blur" }],
    sex: [{ required: true, message: "请选择性别", trigger: "change" }],
    birthDate: [
      { required: true, message: "请选择出生日期", trigger: "change" },
    ],
    contractStartTime: [
      { required: true, message: "请选择入职日期", trigger: "change" },
    ],
    contractEndTime: [
      { required: true, message: "请选择合同结束日期", trigger: "change" },
    ],
    sysDeptId: [
      { required: true, message: "请选择部门", trigger: "change" },
    ],
    roleId: [{ required: true, message: "请选择角色", trigger: "change" }],
  },
  postOptions: [],
  deptOptions: [],
});
const { form, rules, postOptions, deptOptions } = toRefs(state);
const roleOptions = ref([]);
const resetForm = () => {
  Object.assign(form.value, createDefaultForm());
  nextTick(() => {
    formRef.value?.clearValidate();
  });
};
const fetchPostOptions = () => {
  findPostOptions().then((res) => {
    postOptions.value = res.data || [];
  });
};
const fetchDeptOptions = () => {
  deptTreeSelect().then((response) => {
    deptOptions.value = filterDisabledDept(
      JSON.parse(JSON.stringify(response.data || []))
    );
  });
};
const fetchRoleOptions = () => {
  getUser().then((res) => {
    roleOptions.value = res.roles || [];
  });
};
function filterDisabledDept(deptList) {
  return deptList.filter((dept) => {
    if (dept.disabled) {
      return false;
    }
    if (dept.children && dept.children.length) {
      dept.children = filterDisabledDept(dept.children);
    }
    return true;
  });
}
const openDialog = (type, row) => {
  operationType.value = type;
  dialogFormVisible.value = true;
  fetchPostOptions();
  fetchDeptOptions();
  fetchRoleOptions();
  resetForm();
  if (type === "edit" && row?.id) {
    id.value = row.id;
    staffOnJobInfo(id.value, {}).then((res) => {
      const d = res.data || {};
      Object.assign(form.value, {
        ...form.value,
        ...d,
      });
      if (
        !Array.isArray(form.value.staffEducationList) ||
        !form.value.staffEducationList.length
      ) {
        form.value.staffEducationList = [createEmptyEducation()];
      }
      if (
        !Array.isArray(form.value.staffWorkExperienceList) ||
        !form.value.staffWorkExperienceList.length
      ) {
        form.value.staffWorkExperienceList = [createEmptyWork()];
      }
      if (
        !Array.isArray(form.value.staffEmergencyContactList) ||
        !form.value.staffEmergencyContactList.length
      ) {
        form.value.staffEmergencyContactList = [createEmptyEmergency()];
      }
      if (form.value.sysPostId === 0) {
        form.value.sysPostId = undefined;
      }
      if (form.value.sysDeptId === 0) {
        form.value.sysDeptId = undefined;
      }
    });
  }
};
onMounted(() => {
  fetchPostOptions();
  fetchDeptOptions();
});
const submitForm = () => {
  if (!form.value.sysPostId) {
    form.value.sysPostId = undefined;
  }
  if (!form.value.sysDeptId) {
    form.value.sysDeptId = undefined;
  }
  // å…¼å®¹åŽç«¯å¯èƒ½ä»ä½¿ç”¨ roleIds æ•°ç»„
  form.value.roleIds = form.value.roleId ? [form.value.roleId] : [];
  formRef.value?.validate((valid) => {
    if (valid) {
      if (operationType.value === "add") {
        createStaffOnJob(form.value).then(() => {
          proxy.$modal.msgSuccess("提交成功");
          closeDia();
        });
      } else {
        updateStaffOnJob(id.value, form.value).then(() => {
          proxy.$modal.msgSuccess("提交成功");
          closeDia();
        });
      }
    }
  });
};
const closeDia = () => {
  formRef.value?.resetFields();
  dialogFormVisible.value = false;
  emit("close");
};
defineExpose({
  openDialog,
});
</script>
<style scoped>
.form-dia-body {
  padding: 0;
}
.card-title-line {
  color: #f56c6c;
  margin-right: 4px;
}
.form-card {
  margin-bottom: 16px;
}
.dialog-footer {
  text-align: right;
}
</style>
src/views/officeProcessAutomation/HrManage/staff-archive/components/RenewContract.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,141 @@
<template>
  <el-dialog
      v-model="isShow"
      title="续签合同"
      width="800px"
      @close="closeModal"
  >
    <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef">
      <el-form-item label="合同开始日期:" prop="contractStartTime">
        <el-date-picker
            v-model="form.contractStartTime"
            type="date"
            placeholder="请选择日期"
            value-format="YYYY-MM-DD"
            format="YYYY-MM-DD"
            clearable
            style="width: 100%"
            @change="calculateContractTerm"
        />
      </el-form-item>
      <el-form-item label="合同结束日期:" prop="contractEndTime">
        <el-date-picker
            v-model="form.contractEndTime"
            type="date"
            placeholder="请选择日期"
            value-format="YYYY-MM-DD"
            format="YYYY-MM-DD"
            clearable
            style="width: 100%"
            @change="calculateContractTerm"
        />
      </el-form-item>
      <el-form-item label="合同年限:" prop="contractTerm">
        <el-input-number v-model="form.contractTerm" :precision="0" :step="1" style="width: 100%" :disabled="true"/>
      </el-form-item>
    </el-form>
    <template #footer>
      <div class="dialog-footer">
        <el-button type="primary" @click="submitForm">确认</el-button>
        <el-button @click="closeModal">取消</el-button>
      </div>
    </template>
  </el-dialog>
</template>
<script setup>
// ç»­ç­¾åˆåŒ
import { renewContract } from "@/api/personnelManagement/staffOnJob.js";
import {computed, getCurrentInstance,} from "vue";
const emit = defineEmits(['update:visible', 'completed']);
const data = reactive({
  form: {
    contractTerm: 0,
    contractStartTime: "",
    contractEndTime: "",
  },
  rules: {
    contractTerm: [{ required: true, message: "请输入", trigger: "blur" }],
    contractStartTime: [{ required: true, message: "请输入", trigger: "blur" }],
    contractEndTime: [{ required: true, message: "请输入", trigger: "blur" }],
  }
});
const { form, rules } = toRefs(data);
let { proxy } = getCurrentInstance()
const props = defineProps({
  id: {
    type: Number,
    default: 0,
  },
  visible: {
    type: Boolean,
    required: true,
  },
})
const isShow = computed({
  get() {
    return props.visible;
  },
  set(val) {
    emit('update:visible', val);
  },
});
// è®¡ç®—合同年限
const calculateContractTerm = () => {
  if (form.value.contractStartTime && form.value.contractEndTime) {
    const startDate = new Date(form.value.contractStartTime);
    const endDate = new Date(form.value.contractEndTime);
    if (endDate > startDate) {
      // è®¡ç®—年份差
      const yearDiff = endDate.getFullYear() - startDate.getFullYear();
      const monthDiff = endDate.getMonth() - startDate.getMonth();
      const dayDiff = endDate.getDate() - startDate.getDate();
      let years = yearDiff;
      // å¦‚果结束日期的月日小于开始日期的月日,则减去1å¹´
      if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) {
        years = yearDiff - 1;
      }
      form.value.contractTerm = Math.max(0, years);
    } else {
      form.value.contractTerm = 0;
    }
  } else {
    form.value.contractTerm = 0;
  }
};
const submitForm = () => {
  proxy.$refs["formRef"].validate(valid => {
    if (valid) {
      renewContract(props.id, form.value).then(res => {
        if (res.code === 200) {
          proxy.$modal.msgSuccess("续签合同成功");
          emit('completed');
          closeModal();
        }
      })
    }
  })
}
// å…³é—­å¼¹æ¡†
const closeModal = () => {
  // é‡ç½®è¡¨å•数据
  form.value = {
    contractTerm: 0,
    contractStartTime: "",
    contractEndTime: "",
  };
  isShow.value = false;
};
</script>
src/views/officeProcessAutomation/HrManage/staff-archive/components/Show.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,73 @@
<template>
  <div>
    <el-dialog
        v-model="dialogFormVisible"
        title="详情"
        width="70%"
        @close="closeDia"
    >
      <PIMTable
          rowKey="id"
          :column="tableColumn"
          :tableData="tableData"
          :tableLoading="tableLoading"
          height="600"
      ></PIMTable>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="closeDia">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import {ref} from "vue";
import {staffOnJobInfo} from "@/api/personnelManagement/staffOnJob.js";
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
const dialogFormVisible = ref(false);
const operationType = ref('')
const tableColumn = ref([
  // {
  //   label: "合同年限",
  //   prop: "contractTerm",
  // },
  {
    label: "合同开始日期",
    prop: "contractStartTime",
  },
  {
    label: "合同结束日期",
    prop: "contractEndTime",
  },
]);
const tableData = ref([]);
const tableLoading = ref(false);
// æ‰“开弹框
const openDialog = (type, row) => {
  operationType.value = type;
  dialogFormVisible.value = true;
  if (operationType.value === 'edit') {
    staffOnJobInfo({staffNo: row.staffNo}).then(res => {
      tableData.value = res.data
    })
  }
}
// å…³é—­å¼¹æ¡†
const closeDia = () => {
  dialogFormVisible.value = false;
  emit('close')
};
defineExpose({
  openDialog,
});
</script>
<style scoped>
</style>
src/views/officeProcessAutomation/HrManage/staff-archive/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,407 @@
<!--OA模块:员工档案-->
<template>
  <div class="app-container">
    <div class="search_form mb20">
      <div>
        <span class="search_title">姓名:</span>
        <el-input
            v-model="searchForm.staffName"
            style="width: 240px"
            placeholder="请输入姓名搜索"
            @change="handleQuery"
            clearable
            :prefix-icon="Search"
        />
        <span class="search_title search_title2">部门:</span>
          <el-tree-select
            v-model="searchForm.sysDeptId"
            :data="deptOptions"
            check-strictly
            :render-after-expand="false"
            style="width: 240px"
            placeholder="请选择"
          />
          <span class="search_title search_title2">入职日期:</span>
          <el-date-picker
            v-model="searchForm.contractStartTime"
            value-format="YYYY-MM-DD"
            format="YYYY-MM-DD"
            placeholder="请选择"
          />
        <!-- <span  style="margin-left: 10px" class="search_title">合同结束日期:</span> -->
        <!-- <el-date-picker  v-model="searchForm.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange"
                         placeholder="请选择" clearable @change="changeDaterange" /> -->
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
        >搜索</el-button
        >
      </div>
      <div>
        <el-button type="primary" @click="openFormNewOrEditFormDia('add')">新增入职</el-button>
        <el-button type="info" @click="handleImport">导入</el-button>
        <el-button @click="handleOut">导出</el-button>
        <!-- <el-button type="danger" plain @click="handleDelete">删除</el-button> -->
      </div>
    </div>
    <div class="table_list">
      <PIMTable
          rowKey="id"
          :column="tableColumn"
          :tableData="tableData"
          :page="page"
          :isSelection="true"
          @selection-change="handleSelectionChange"
          :tableLoading="tableLoading"
          @pagination="pagination"
          :total="page.total"
      ></PIMTable>
    </div>
    <show-form-dia ref="formDia" @close="handleQuery"></show-form-dia>
    <new-or-edit-form-dia ref="formDiaNewOrEditFormDia" @close="handleQuery"></new-or-edit-form-dia>
    <renew-contract
        v-if="isShowRenewContractModal"
        v-model:visible="isShowRenewContractModal"
        :id="id"
        @completed="handleQuery"
    />
    <!-- å¯¼å…¥å¯¹è¯æ¡† -->
    <el-dialog :title="upload.title" v-model="upload.open" width="400px" append-to-body>
      <el-upload
        ref="uploadRef"
        :limit="1"
        accept=".xlsx, .xls"
        :headers="upload.headers"
        :action="upload.url"
        :disabled="upload.isUploading"
        :on-progress="handleFileUploadProgress"
        :on-success="handleFileSuccess"
        :auto-upload="false"
        drag
      >
        <el-icon class="el-icon--upload"><upload-filled /></el-icon>
        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
        <template #tip>
          <div class="el-upload__tip text-center">
            <span>仅允许导入xls、xlsx格式文件。</span>
            <el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline; margin-left: 5px;" @click="importTemplate">下载模板</el-link>
          </div>
        </template>
      </el-upload>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitFileForm">ç¡® å®š</el-button>
          <el-button @click="upload.open = false">取 æ¶ˆ</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { Search, UploadFilled } from "@element-plus/icons-vue";
import {onMounted, ref} from "vue";
import {ElMessageBox} from "element-plus";
import { deptTreeSelect } from "@/api/system/user.js";
import {batchDeleteStaffOnJobs, staffOnJobListPage} from "@/api/personnelManagement/staffOnJob.js";
import { getToken } from "@/utils/auth";
import dayjs from "dayjs";
const NewOrEditFormDia = defineAsyncComponent(() => import("@/views/personnelManagement/employeeRecord/components/NewOrEditFormDia.vue"));
const ShowFormDia = defineAsyncComponent(() => import( "@/views/personnelManagement/employeeRecord/components/Show.vue"));
const RenewContract = defineAsyncComponent(() => import( "@/views/personnelManagement/employeeRecord/components/RenewContract.vue"));
const data = reactive({
  searchForm: {
    staffName: "",
    entryDate: undefined, // å½•入日期
    entryDateStart: undefined,
    entryDateEnd: undefined,
  },
  deptOptions: [],
});
const { searchForm, deptOptions } = toRefs(data);
const isShowRenewContractModal = ref(false);
const id = ref(0);
const tableColumn = ref([
  {
    label: "状态",
    prop: "staffState",
    dataType: "tag",
    formatData: (params) => {
      if (params == 0) {
        return "离职";
      } else if (params == 1) {
        return "在职";
      } else {
        return null;
      }
    },
    formatType: (params) => {
      if (params == 0) {
        return "danger";
      } else if (params == 1) {
        return "primary";
      } else {
        return null;
      }
    },
  },
  {
    label: "员工编号",
    prop: "staffNo",
  },
  {
    label: "姓名",
    prop: "staffName",
  },
  {
    label: "别名",
    prop: "alias",
  },
  {
    label: "手机",
    prop: "phone",
    width: 150,
  },
  {
    label: "性别",
    prop: "sex",
  },
  {
    label: "出生日期",
    prop: "birthDate",
    width: 120,
  },
  {
    label: "入职日期",
    prop: "contractStartTime",
    width: 120,
  },
  {
    label: "年龄",
    prop: "age",
  },
  {
    label: "籍贯",
    prop: "nativePlace",
  },
  {
    label: "民族",
    prop: "nation",
    width: 100,
  },
  {
    label: "婚姻状况",
    prop: "maritalStatus",
    width: 100,
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: 'right',
    width: 180,
    operation: [
      {
        name: "编辑",
        type: "text",
        clickFun: (row) => {
          openFormNewOrEditFormDia("edit", row);
        },
      },
      {
        name: "续签合同",
        type: "text",
        showHide: row => row.staffState === 1,
        clickFun: (row) => {
          isShowRenewContractModal.value = true;
          id.value = row.id;
        },
      },
      // {
      //   name: "详情",
      //   type: "text",
      //   clickFun: (row) => {
      //     openForm("edit", row);
      //   },
      // },
    ],
  },
]);
const tableData = ref([]);
const selectedRows = ref([]);
const tableLoading = ref(false);
const page = reactive({
  current: 1,
  size: 100,
  total: 0
});
const formDia = ref()
const formDiaNewOrEditFormDia = ref()
const { proxy } = getCurrentInstance()
// å¯¼å…¥ç›¸å…³
const uploadRef = ref(null)
const upload = reactive({
  // æ˜¯å¦æ˜¾ç¤ºå¼¹å‡ºå±‚
  open: false,
  // å¼¹å‡ºå±‚标题
  title: "",
  // æ˜¯å¦ç¦ç”¨ä¸Šä¼ 
  isUploading: false,
  // è®¾ç½®ä¸Šä¼ çš„请求头部
  headers: { Authorization: "Bearer " + getToken() },
  // ä¸Šä¼ çš„地址
  url: import.meta.env.VITE_APP_BASE_API + "/staff/staffOnJob/import"
})
const fetchDeptOptions = () => {
    deptTreeSelect().then(response => {
      console.log(response.data)
      deptOptions.value = filterDisabledDept(
        JSON.parse(JSON.stringify(response.data))
      );
    });
  };
const filterDisabledDept = deptList => {
    return deptList.filter(dept => {
      if (dept.disabled) {
        return false;
      }
      if (dept.children && dept.children.length) {
        dept.children = filterDisabledDept(dept.children);
      }
      return true;
    });
  };
const changeDaterange = (value) => {
  searchForm.value.entryDateStart = undefined;
  searchForm.value.entryDateEnd = undefined;
  if (value) {
    searchForm.value.entryDateStart = dayjs(value[0]).format("YYYY-MM-DD");
    searchForm.value.entryDateEnd = dayjs(value[1]).format("YYYY-MM-DD");
  }
  getList();
};
// æŸ¥è¯¢åˆ—表
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
  page.current = 1;
  getList();
};
const pagination = (obj) => {
  page.current = obj.page;
  page.size = obj.limit;
  getList();
};
const getList = () => {
  fetchDeptOptions();
  tableLoading.value = true;
  const params = { ...searchForm.value, ...page };
  params.entryDate = undefined
  staffOnJobListPage({...params}).then(res => {
    tableLoading.value = false;
    tableData.value = res.data.records
    page.total = res.data.total;
  }).catch(err => {
    tableLoading.value = false;
  })
};
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
};
// æ‰“开弹框
const openForm = (type, row) => {
  nextTick(() => {
    formDia.value?.openDialog(type, row)
  })
};
const openFormNewOrEditFormDia = (type, row) => {
  nextTick(() => {
    formDiaNewOrEditFormDia.value?.openDialog(type, row)
  })
};
// åˆ é™¤
const handleDelete = () => {
  let ids = [];
  if (selectedRows.value.length > 0) {
    ids = selectedRows.value.map((item) => item.id);
  } else {
    proxy.$modal.msgWarning("请选择数据");
    return;
  }
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
      .then(() => {
        batchDeleteStaffOnJobs(ids).then((res) => {
          proxy.$modal.msgSuccess("删除成功");
          getList();
        });
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
};
// å¯¼å‡º
const handleOut = () => {
  ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
      .then(() => {
        proxy.download("/staff/staffOnJob/export", {staffState: 1}, "员工台账.xlsx");
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
};
// å¯¼å…¥æŒ‰é’®æ“ä½œ
const handleImport = () => {
  upload.title = "员工导入"
  upload.open = true
}
// ä¸‹è½½æ¨¡æ¿æ“ä½œ
const importTemplate = () => {
  proxy.download("/staff/staffOnJob/downloadTemplate", {}, `员工导入模板_${new Date().getTime()}.xlsx`)
}
// æ–‡ä»¶ä¸Šä¼ ä¸­å¤„理
const handleFileUploadProgress = (event, file, fileList) => {
  upload.isUploading = true
}
// æ–‡ä»¶ä¸Šä¼ æˆåŠŸå¤„ç†
const handleFileSuccess = (response, file, fileList) => {
  upload.open = false
  upload.isUploading = false
  proxy.$refs["uploadRef"].handleRemove(file)
  proxy.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "导入结果", { dangerouslyUseHTMLString: true })
  getList()
}
// æäº¤ä¸Šä¼ æ–‡ä»¶
const submitFileForm = () => {
  proxy.$refs["uploadRef"].submit()
}
onMounted(() => {
  getList();
});
</script>
<style scoped>
.search_title2 {
  margin-left: 10px;
}
</style>
src/views/officeProcessAutomation/HrManage/staff-contract/components/formDia.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,96 @@
<template>
  <div>
    <el-dialog
        v-model="dialogFormVisible"
        title="详情"
        width="70%"
        @close="closeDia"
    >
      <PIMTable
          rowKey="id"
          :column="tableColumn"
          :tableData="tableData"
          :tableLoading="tableLoading"
          height="600"
      ></PIMTable>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="closeDia">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <Files ref="filesDia"></Files>
  </div>
</template>
<script setup>
import {ref} from "vue";
import {findStaffContractListPage} from "@/api/personnelManagement/staffContract.js";
const Files = defineAsyncComponent(() => import( "@/views/personnelManagement/contractManagement/filesDia.vue"));
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
const filesDia = ref()
const dialogFormVisible = ref(false);
const operationType = ref('')
const tableColumn = ref([
  {
    label: "合同年限",
    prop: "contractTerm",
  },
  {
    label: "合同开始日期",
    prop: "contractStartTime",
  },
  {
    label: "合同结束日期",
    prop: "contractEndTime",
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: 'right',
    width: 120,
    operation: [
      {
        name: "上传附件",
        type: "text",
        clickFun: (row) => {
          filesDia.value.openDialog( row,'合同')
        },
      }
    ],
  },
]);
const tableData = ref([]);
const tableLoading = ref(false);
// æ‰“开弹框
const openDialog = (type, row) => {
  operationType.value = type;
  dialogFormVisible.value = true;
  if (operationType.value === 'edit') {
    findStaffContractListPage({staffOnJobId: row.id}).then(res => {
      tableData.value = res.data.records
    })
  }
}
const openUploadFile = (row) => {
  filesDia.value.open = true
  filesDia.value.row = row
}
// å…³é—­å¼¹æ¡†
const closeDia = () => {
  dialogFormVisible.value = false;
  emit('close')
};
defineExpose({
  openDialog,
});
</script>
<style scoped>
</style>
src/views/officeProcessAutomation/HrManage/staff-contract/filesDia.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,197 @@
<template>
  <div>
    <el-dialog
        v-model="dialogFormVisible"
        title="上传附件"
        width="50%"
        @close="closeDia"
    >
      <div style="margin-bottom: 10px;text-align: right">
        <el-upload
            v-model:file-list="fileList"
            class="upload-demo"
            :action="uploadUrl"
            :on-success="handleUploadSuccess"
            :on-error="handleUploadError"
            name="file"
            :show-file-list="false"
            :headers="headers"
            style="display: inline;margin-right: 10px"
        >
          <el-button type="primary">上传附件</el-button>
        </el-upload>
        <el-button type="danger" plain @click="handleDelete">删除</el-button>
      </div>
      <PIMTable
          rowKey="id"
          :column="tableColumn"
          :tableData="tableData"
          :tableLoading="tableLoading"
          :isSelection="true"
          :page="page"
          @selection-change="handleSelectionChange"
          height="500"
          @pagination="paginationSearch"
          :total="page.total"
      >
      </PIMTable>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="closeDia">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <filePreview ref="filePreviewRef" />
  </div>
</template>
<script setup>
import {ref} from "vue";
import {ElMessageBox} from "element-plus";
import {getToken} from "@/utils/auth.js";
import filePreview from '@/components/filePreview/index.vue'
import {
  fileAdd,
  fileDel,
  fileListPage
} from "@/api/financialManagement/revenueManagement.js";
import Pagination from "@/components/PIMTable/Pagination.vue";
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
const dialogFormVisible = ref(false);
const currentId = ref('')
const selectedRows = ref([]);
const filePreviewRef = ref()
const tableColumn = ref([
  {
    label: "文件名称",
    prop: "name",
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    operation: [
      {
        name: "下载",
        type: "text",
        clickFun: (row) => {
          downLoadFile(row);
        },
      },
      {
        name: "预览",
        type: "text",
        clickFun: (row) => {
          lookFile(row);
        },
      }
    ],
  },
]);
const page = reactive({
    current: 1,
    size: 100,
});
const total = ref(0);
const tableData = ref([]);
const fileList = ref([]);
const tableLoading = ref(false);
const accountType = ref('')
const headers = ref({
  Authorization: "Bearer " + getToken(),
});
const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/file/upload"); // ä¸Šä¼ çš„图片服务器地址
// æ‰“开弹框
const openDialog = (row,type) => {
  accountType.value = type;
  dialogFormVisible.value = true;
  currentId.value = row.id;
  getList()
}
const paginationSearch = (obj) => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
};
const getList = () => {
  fileListPage({accountId: currentId.value,accountType:accountType.value, ...page}).then(res => {
    tableData.value = res.data.records;
    page.total = res.data.total;
  })
}
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
};
// å…³é—­å¼¹æ¡†
const closeDia = () => {
  dialogFormVisible.value = false;
  emit('close')
};
// ä¸Šä¼ æˆåŠŸå¤„ç†
function handleUploadSuccess(res, file) {
  // å¦‚果上传成功
  if (res.code == 200) {
    const fileRow = {}
    fileRow.name = res.data.originalName
    fileRow.url = res.data.tempPath
    uploadFile(fileRow)
  } else {
    proxy.$modal.msgError("文件上传失败");
  }
}
function uploadFile(file) {
  file.accountId = currentId.value;
  file.accountType = accountType.value;
  fileAdd(file).then(res => {
    proxy.$modal.msgSuccess("文件上传成功");
    getList()
  })
}
// ä¸Šä¼ å¤±è´¥å¤„理
function handleUploadError() {
  proxy.$modal.msgError("文件上传失败");
}
// ä¸‹è½½é™„ä»¶
const downLoadFile = (row) => {
    proxy.$download.byUrl(row.url, row.originalFilename);
}
// åˆ é™¤
const handleDelete = () => {
  let ids = [];
  if (selectedRows.value.length > 0) {
    ids = selectedRows.value.map((item) => item.id);
  } else {
    proxy.$modal.msgWarning("请选择数据");
    return;
  }
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    fileDel(ids).then((res) => {
      proxy.$modal.msgSuccess("删除成功");
      getList();
    });
  }).catch(() => {
    proxy.$modal.msg("已取消");
  });
};
// é¢„览附件
const lookFile = (row) => {
  filePreviewRef.value.open(row.url)
}
defineExpose({
  openDialog,
});
</script>
<style scoped>
</style>
src/views/officeProcessAutomation/HrManage/staff-contract/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,314 @@
<!--OA模块:员工合同-->
<template>
  <div class="app-container">
    <div class="search_form mb20">
      <div>
        <span class="search_title">姓名:</span>
        <el-input v-model="searchForm.staffName" style="width: 240px" placeholder="请输入姓名搜索" @change="handleQuery"
          clearable :prefix-icon="Search" />
        <span style="margin-left: 10px" class="search_title">合同结束日期:</span>
        <el-date-picker v-model="searchForm.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange"
          placeholder="请选择" clearable @change="changeDaterange" />
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">搜索</el-button>
      </div>
      <div>
        <el-button @click="handleOut">导出</el-button>
      </div>
    </div>
    <div class="table_list">
      <PIMTable rowKey="id" :column="tableColumn" :tableData="tableData" :page="page" :isSelection="true"
        @selection-change="handleSelectionChange" :tableLoading="tableLoading" @pagination="pagination"
        :total="page.total"></PIMTable>
    </div>
    <form-dia ref="formDia" @close="handleQuery"></form-dia>
    <!-- åˆåŒå¯¼å…¥å¯¹è¯æ¡† -->
    <el-dialog
      :title="upload.title"
      v-model="upload.open"
      width="400px"
      append-to-body
    >
      <el-upload
        ref="uploadRef"
        :limit="1"
        accept=".xlsx, .xls"
        :headers="upload.headers"
        :action="upload.url + '?updateSupport=' + upload.updateSupport"
        :disabled="upload.isUploading"
        :on-progress="handleFileUploadProgress"
        :on-success="handleFileSuccess"
        :auto-upload="false"
        drag
      >
        <el-icon class="el-icon--upload"><upload-filled /></el-icon>
        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
        <template #tip>
          <div class="el-upload__tip text-center">
            <span>仅允许导入xls、xlsx格式文件。</span>
            <!-- <el-link
              type="primary"
              :underline="false"
              style="font-size: 12px; vertical-align: baseline"
              @click="importTemplate"
              >下载模板</el-link
            > -->
          </div>
        </template>
      </el-upload>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitFileForm">ç¡® å®š</el-button>
          <el-button @click="upload.open = false">取 æ¶ˆ</el-button>
        </div>
      </template>
    </el-dialog>
    <files-dia ref="filesDia"></files-dia>
  </div>
</template>
<script setup>
import { Search } from "@element-plus/icons-vue";
import { onMounted, ref } from "vue";
import FormDia from "@/views/personnelManagement/contractManagement/components/formDia.vue";
import { ElMessageBox } from "element-plus";
import { staffOnJobListPage } from "@/api/personnelManagement/staffOnJob.js";
import dayjs from "dayjs";
import { getToken } from "@/utils/auth.js";
import FilesDia from "./filesDia.vue";
const data = reactive({
  searchForm: {
    staffName: "",
    entryDate: null, // å½•入日期
    entryDateStart: undefined,
    entryDateEnd: undefined,
  },
});
const { searchForm } = toRefs(data);
const tableColumn = ref([
  {
    label: "状态",
    prop: "staffState",
    dataType: "tag",
    formatData: (params) => {
      if (params == 0) {
        return "离职";
      } else if (params == 1) {
        return "在职";
      } else {
        return null;
      }
    },
    formatType: (params) => {
      if (params == 0) {
        return "danger";
      } else if (params == 1) {
        return "primary";
      } else {
        return null;
      }
    },
  },
  {
    label: "员工编号",
    prop: "staffNo",
  },
  {
    label: "姓名",
    prop: "staffName",
  },
  {
    label: "性别",
    prop: "sex",
  },
  {
    label: "户籍住址",
    prop: "nativePlace",
  },
  {
    label: "岗位",
    prop: "postJob",
  },
  {
    label: "现住址",
    prop: "adress",
    width: 200
  },
  {
    label: "第一学历",
    prop: "firstStudy",
  },
  {
    label: "专业",
    prop: "profession",
    width: 100
  },
  {
    label: "年龄",
    prop: "age",
  },
  {
    label: "联系电话",
    prop: "phone",
    width: 150
  },
  {
    label: "紧急联系人",
    prop: "emergencyContact",
    width: 120
  },
  {
    label: "紧急联系人电话",
    prop: "emergencyContactPhone",
    width: 150
  },
  // {
  //   label: "合同年限",
  //   prop: "contractTerm",
  // },
  // {
  //   label: "合同开始日期",
  //   prop: "contractStartTime",
  //   width: 120
  // },
  {
    label: "合同结束日期",
    prop: "contractExpireTime",
    width: 120
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: 'right',
    width: 120,
    operation: [
      {
        name: "详情",
        type: "text",
        clickFun: (row) => {
          openForm("edit", row);
        },
      }
    ],
  },
]);
const filesDia = ref()
const tableData = ref([]);
const selectedRows = ref([]);
const tableLoading = ref(false);
const page = reactive({
  current: 1,
  size: 100,
  total: 0,
});
const formDia = ref()
const { proxy } = getCurrentInstance()
const changeDaterange = (value) => {
  searchForm.value.entryDateStart = undefined;
  searchForm.value.entryDateEnd = undefined;
  if (value) {
    searchForm.value.entryDateStart = dayjs(value[0]).format("YYYY-MM-DD");
    searchForm.value.entryDateEnd = dayjs(value[1]).format("YYYY-MM-DD");
  }
  getList();
};
// æ‰“开附件弹框
const openFilesFormDia = (row) => {
  console.log(row)
  nextTick(() => {
    filesDia.value?.openDialog( row,'合同')
  })
};
// æŸ¥è¯¢åˆ—表
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
  page.current = 1;
  getList();
};
const pagination = (obj) => {
  page.current = obj.page;
  page.size = obj.limit;
  getList();
};
const getList = () => {
  tableLoading.value = true;
  const params = { ...searchForm.value, ...page };
  params.entryDate = undefined
  params.staffState = 1
  staffOnJobListPage(params).then(res => {
    tableLoading.value = false;
    tableData.value = res.data.records
    page.total = res.data.total;
  }).catch(err => {
    tableLoading.value = false;
  })
};
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
};
// æ‰“开弹框
const openForm = (type, row) => {
  nextTick(() => {
    formDia.value?.openDialog(type, row)
  })
};
// å¯¼å‡º
const handleOut = () => {
  ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      proxy.download("/staff/staffOnJob/export", {staffState: 1}, "合同管理.xlsx");
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
    });
};
const upload = reactive({
  // æ˜¯å¦æ˜¾ç¤ºå¼¹å‡ºå±‚(合同导入)
  open: false,
  // å¼¹å‡ºå±‚标题(合同导入)
  title: "",
  // æ˜¯å¦ç¦ç”¨ä¸Šä¼ 
  isUploading: false,
  // æ˜¯å¦æ›´æ–°å·²ç»å­˜åœ¨çš„用户数据
  updateSupport: 1,
  // è®¾ç½®ä¸Šä¼ çš„请求头部
  headers: { Authorization: "Bearer " + getToken() },
  // ä¸Šä¼ çš„地址
  url: import.meta.env.VITE_APP_BASE_API + "/staff/staffOnJob/import",
});
/** å¯¼å…¥æŒ‰é’®æ“ä½œ */
function handleImport() {
  upload.title = "合同导入";
  upload.open = true;
}
/** æäº¤ä¸Šä¼ æ–‡ä»¶ */
function submitFileForm() {
  console.log(upload.url + '?updateSupport=' + upload.updateSupport)
  proxy.$refs["uploadRef"].submit();
}
/**文件上传中处理 */
const handleFileUploadProgress = (event, file, fileList) => {
  upload.isUploading = true;
};
/** æ–‡ä»¶ä¸Šä¼ æˆåŠŸå¤„ç† */
const handleFileSuccess = (response, file, fileList) => {
  upload.open = false;
  upload.isUploading = false;
  proxy.$refs["uploadRef"].handleRemove(file);
  getList();
};
onMounted(() => {
  getList();
});
</script>
<style scoped></style>
src/views/officeProcessAutomation/HrManage/transfer-apply/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,792 @@
<!--OA模块:调岗申请-->
<template>
  <div class="app-container">
    <div class="search_form mb20">
      <div>
        <span class="search_title">申请人:</span>
        <el-select
          v-model="searchForm.applicantId"
          filterable
          remote
          clearable
          reserve-keyword
          placeholder="请选择或搜索申请人"
          style="width: 220px"
          :remote-method="remoteSearchApplicant"
          :loading="applicantSearchLoading"
        >
          <el-option
            v-for="u in applicantSearchOptions"
            :key="u.userId"
            :label="userSelectLabel(u)"
            :value="u.userId"
          />
        </el-select>
        <span class="search_title" style="margin-left: 12px">转岗时间:</span>
        <el-date-picker
          v-model="searchForm.transferDateRange"
          type="daterange"
          range-separator="至"
          start-placeholder="开始"
          end-placeholder="结束"
          format="YYYY-MM-DD"
          value-format="YYYY-MM-DD"
          style="width: 260px"
          clearable
        />
        <el-button type="primary" style="margin-left: 10px" @click="handleQuery">搜索</el-button>
        <el-button @click="resetSearch">重置</el-button>
      </div>
      <div>
        <el-button type="primary" @click="openFormDialog('add')">新增调岗申请</el-button>
      </div>
    </div>
    <div class="table_list">
      <PIMTable
        rowKey="id"
        :column="tableColumn"
        :tableData="tableData"
        :page="page"
        :isSelection="false"
        :tableLoading="tableLoading"
        @pagination="pagination"
        :total="page.total"
      />
    </div>
    <!-- æ–°å¢ž / ç¼–辑 -->
    <el-dialog
      v-model="formDialog.visible"
      :title="formDialog.title"
      width="720px"
      append-to-body
      destroy-on-close
      class="transfer-apply-form-dialog"
      @closed="onFormClosed"
    >
      <el-form ref="formRef" :model="form" :rules="formRules" label-width="120px" class="transfer-apply-form">
        <el-row :gutter="24">
          <el-col :span="24">
            <el-form-item label="申请人" prop="applicantId">
              <el-select
                v-model="form.applicantId"
                filterable
                remote
                clearable
                reserve-keyword
                placeholder="请选择或搜索申请人"
                style="width: 100%"
                :remote-method="remoteSearchApplicantForm"
                :loading="applicantFormSearchLoading"
                @change="onApplicantChange"
              >
                <el-option
                  v-for="u in applicantFormOptions"
                  :key="u.userId"
                  :label="userSelectLabel(u)"
                  :value="u.userId"
                />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="12">
            <el-form-item label="转岗日期" prop="transferDate">
              <el-date-picker
                v-model="form.transferDate"
                type="date"
                placeholder="请选择转岗日期"
                format="YYYY-MM-DD"
                value-format="YYYY-MM-DD"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="原岗位" prop="originalPostName">
              <el-input v-model="form.originalPostName" placeholder="选择申请人后自动带出" disabled />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="24">
            <el-form-item label="转入岗位" prop="targetPostId">
              <el-select v-model="form.targetPostId" placeholder="请选择转入岗位" clearable filterable style="width: 100%">
                <el-option
                  v-for="p in targetPostOptions"
                  :key="p.postId"
                  :label="p.postName"
                  :value="p.postId"
                  :disabled="p.status === '1' || p.status === 1"
                />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="12">
            <el-form-item label="审批方式" prop="approvalMode">
              <el-radio-group v-model="form.approvalMode">
                <el-radio value="parallel">与签</el-radio>
                <el-radio value="countersign">会签</el-radio>
              </el-radio-group>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="24">
            <el-form-item label="审批人" prop="approverIds">
              <el-tree-select
                v-model="form.approverIds"
                :data="approverTreeData"
                multiple
                collapse-tags
                collapse-tags-tooltip
                :max-collapse-tags="2"
                :render-after-expand="false"
                placeholder="请选择审批人(可多选)"
                style="width: 100%"
                :props="{ value: 'id', label: 'label', children: 'children', disabled: 'disabled' }"
                check-strictly
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
          <el-button @click="formDialog.visible = false">取 æ¶ˆ</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- è¯¦æƒ… -->
    <el-dialog v-model="detailDialog.visible" title="调岗申请详情" width="560px" append-to-body>
      <el-descriptions :column="1" border>
        <el-descriptions-item label="申请人">{{ detailRow.applicantName }}</el-descriptions-item>
        <el-descriptions-item label="转岗日期">{{ detailRow.transferDate }}</el-descriptions-item>
        <el-descriptions-item label="原岗位">{{ detailRow.originalPostName }}</el-descriptions-item>
        <el-descriptions-item label="转入岗位">{{ detailRow.targetPostName }}</el-descriptions-item>
        <el-descriptions-item label="审批结果">{{ approvalResultLabel(detailRow.approvalResult) }}</el-descriptions-item>
        <el-descriptions-item label="审批方式">{{ approvalModeLabel(detailRow.approvalMode) }}</el-descriptions-item>
        <el-descriptions-item label="审批人">{{ detailRow.approverNames || "—" }}</el-descriptions-item>
      </el-descriptions>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="detailDialog.visible = false">关 é—­</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { findPostOptions } from "@/api/system/post.js";
import { deptTreeSelect, userListNoPageByTenantId } from "@/api/system/user.js";
import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref, watch } from "vue";
const { proxy } = getCurrentInstance();
/** ä¸ŽåŽç«¯çº¦å®šå­—段(本地占位,后期接口对齐) */
const createEmptyForm = () => ({
  id: undefined,
  applicantId: "",
  applicantName: "",
  transferDate: "",
  originalPostId: "",
  originalPostName: "",
  targetPostId: "",
  targetPostName: "",
  approvalMode: "parallel",
  approverIds: [],
  approverNames: "",
});
/** ç³»ç»Ÿç”¨æˆ·ç¼“存(/system/user/userListNoPageByTenantId,与转正申请等一致) */
const allUsersCache = ref([]);
/** å²—位字典 postId -> postName(/system/post/optionselect,与员工档案入职表单一致) */
const postIdToName = ref({});
const targetPostOptions = ref([]);
function rebuildPostIdMap() {
  const m = {};
  for (const p of targetPostOptions.value || []) {
    const id = p.postId ?? p.value ?? p.id;
    if (id != null && id !== "") m[String(id)] = p.postName ?? p.label ?? "";
  }
  postIdToName.value = m;
}
function targetPostNameById(postId) {
  if (postId == null || postId === "") return "";
  const k = String(postId);
  return (
    postIdToName.value[k] ||
    targetPostOptions.value.find((x) => String(x.postId ?? x.id ?? x.value) === k)?.postName ||
    ""
  );
}
function userSelectLabel(u) {
  const nick = u.nickName || "";
  const name = u.userName || "";
  if (nick && name && nick !== name) return `${nick}(${name})`;
  return nick || name || `用户${u.userId ?? u.id ?? ""}`;
}
function firstPostId(user) {
  if (!user) return undefined;
  if (Array.isArray(user.postIds) && user.postIds.length) return user.postIds[0];
  if (user.postId != null && user.postId !== "") return user.postId;
  return undefined;
}
/** ä»Žç”¨æˆ·å¯¹è±¡è§£æžã€ŒåŽŸå²—ä½ã€ï¼ˆå…¼å®¹ postName / postIds / posts ç­‰å¸¸è§è¿”回) */
function resolveOriginalPost(user) {
  if (!user) return { originalPostId: "", originalPostName: "" };
  const nameStr = (user.postName ?? user.postname ?? "").toString().trim();
  if (nameStr) {
    const pid = firstPostId(user);
    return { originalPostId: pid != null && pid !== "" ? String(pid) : "", originalPostName: nameStr };
  }
  if (Array.isArray(user.posts) && user.posts.length) {
    const p0 = user.posts[0];
    return {
      originalPostId: p0.postId != null ? String(p0.postId) : "",
      originalPostName: (p0.postName ?? "").toString() || "未命名岗位",
    };
  }
  const pid = firstPostId(user);
  if (pid != null && pid !== "") {
    const n = postIdToName.value[String(pid)] || "";
    return {
      originalPostId: String(pid),
      originalPostName: n || "当前岗位(未在岗位字典中)",
    };
  }
  return { originalPostId: "", originalPostName: "未分配岗位" };
}
function userById(id) {
  if (id == null || id === "") return undefined;
  return allUsersCache.value.find((u) => String(u.userId ?? u.id) === String(id));
}
function filterUsersByQuery(query) {
  const list = allUsersCache.value.filter((u) => isActiveUser(u));
  const q = (query || "").trim().toLowerCase();
  if (!q) return [...list];
  return list.filter((u) => {
    const nick = (u.nickName || "").toLowerCase();
    const uname = (u.userName || "").toLowerCase();
    const phone = (u.phonenumber || u.phone || "").toString();
    return nick.includes(q) || uname.includes(q) || phone.includes(q);
  });
}
async function loadUserPool() {
  try {
    const res = await userListNoPageByTenantId();
    allUsersCache.value = unwrapArray(res);
  } catch {
    allUsersCache.value = [];
  }
}
async function loadPostOptions() {
  try {
    const res = await findPostOptions();
    const rows = res.data ?? res.rows ?? [];
    targetPostOptions.value = Array.isArray(rows) ? rows : [];
  } catch {
    targetPostOptions.value = [];
  }
  rebuildPostIdMap();
}
/** æŸ¥è¯¢åŒºï¼šä¸‹æ‹‰è¿œç¨‹æ¨¡ç³Šï¼ˆæ•°æ®æ¥è‡ª userListNoPageByTenantId,前端过滤) */
const applicantSearchLoading = ref(false);
const applicantSearchOptions = ref([]);
async function remoteSearchApplicant(query) {
  applicantSearchLoading.value = true;
  try {
    if (!allUsersCache.value.length) {
      await loadUserPool();
    }
    applicantSearchOptions.value = filterUsersByQuery(query);
  } finally {
    applicantSearchLoading.value = false;
  }
}
/** è¡¨å•内申请人下拉 */
const applicantFormSearchLoading = ref(false);
const applicantFormOptions = ref([]);
async function remoteSearchApplicantForm(query) {
  applicantFormSearchLoading.value = true;
  try {
    if (!allUsersCache.value.length) {
      await loadUserPool();
    }
    applicantFormOptions.value = filterUsersByQuery(query);
  } finally {
    applicantFormSearchLoading.value = false;
  }
}
function onApplicantChange(uid) {
  const u = userById(uid);
  if (u) {
    form.applicantName = u.nickName || u.userName || "";
    const { originalPostId, originalPostName } = resolveOriginalPost(u);
    form.originalPostId = originalPostId;
    form.originalPostName = originalPostName;
  } else {
    form.applicantName = "";
    form.originalPostId = "";
    form.originalPostName = "";
  }
}
/** å®¡æ‰¹äººæ ‘ */
const approverTreeData = ref([]);
const approverLabelMap = ref({});
function unwrapArray(payload) {
  if (Array.isArray(payload)) return payload;
  if (payload && Array.isArray(payload.data)) return payload.data;
  if (payload && Array.isArray(payload.rows)) return payload.rows;
  return [];
}
function filterDisabledDept(deptList) {
  if (!Array.isArray(deptList)) return [];
  return deptList.filter((dept) => {
    if (dept.disabled) return false;
    if (dept.children?.length) {
      dept.children = filterDisabledDept(dept.children);
    }
    return true;
  });
}
function getUserDeptId(u) {
  return u.deptId ?? u.sysDeptId ?? u.dept?.deptId ?? u.dept?.id ?? u.dept_id;
}
function getDeptNodeKey(node) {
  const k = node?.id ?? node?.value ?? node?.deptId;
  if (k == null || k === "") return null;
  return k;
}
function isActiveUser(u) {
  if (u.delFlag === "2" || u.delFlag === 2) return false;
  if (u.status == null) return true;
  return String(u.status) === "0";
}
function userToTreeLeaf(u) {
  return {
    id: String(u.userId ?? u.id),
    label: u.nickName || u.userName || `用户${u.userId ?? u.id}`,
  };
}
function buildUsersByDeptId(users) {
  const map = new Map();
  const unassigned = [];
  for (const u of users) {
    if (!isActiveUser(u)) continue;
    const did = getUserDeptId(u);
    if (did == null || did === "" || did === 0 || did === "0") {
      unassigned.push(u);
      continue;
    }
    const k = String(did);
    if (!map.has(k)) map.set(k, []);
    map.get(k).push(u);
  }
  return { map, unassigned };
}
function collectUserLabels(nodes, map) {
  (nodes || []).forEach((n) => {
    if (n.children?.length) {
      collectUserLabels(n.children, map);
    } else if (n.id != null && !String(n.id).startsWith("dept_")) {
      map[String(n.id)] = n.label;
    }
  });
}
function mergeDeptTreeWithUsers(nodes, usersByDept) {
  if (!Array.isArray(nodes)) return [];
  const out = [];
  for (const node of nodes) {
    const deptIdRaw = getDeptNodeKey(node);
    if (deptIdRaw == null) continue;
    const sub = mergeDeptTreeWithUsers(node.children || [], usersByDept);
    const usersHere = usersByDept.get(String(deptIdRaw)) || [];
    const userChildren = usersHere.map(userToTreeLeaf);
    const children = [...sub, ...userChildren];
    if (!children.length) continue;
    out.push({
      id: `dept_${deptIdRaw}`,
      label: node.label ?? node.deptName ?? "部门",
      disabled: true,
      children,
    });
  }
  return out;
}
function buildFlatApproverTree(users) {
  const list = users.filter(isActiveUser).map(userToTreeLeaf);
  if (!list.length) return [];
  return [
    {
      id: "dept_all_users",
      label: "系统用户",
      disabled: true,
      children: list,
    },
  ];
}
async function loadApproverTree() {
  try {
    const needFetchUsers = !allUsersCache.value.length;
    const [deptRes, userRes] = await Promise.all([
      deptTreeSelect(),
      needFetchUsers ? userListNoPageByTenantId() : Promise.resolve(null),
    ]);
    let rawTree = unwrapArray(deptRes);
    rawTree = rawTree.length ? JSON.parse(JSON.stringify(rawTree)) : [];
    let deptTree = filterDisabledDept(JSON.parse(JSON.stringify(rawTree)));
    if (!deptTree.length && rawTree.length) {
      deptTree = JSON.parse(JSON.stringify(rawTree));
    }
    let users = needFetchUsers ? unwrapArray(userRes) : [...allUsersCache.value];
    if (needFetchUsers && users.length) {
      allUsersCache.value = users;
    }
    const { map: usersByDept, unassigned } = buildUsersByDeptId(users);
    let merged = mergeDeptTreeWithUsers(deptTree, usersByDept);
    if (unassigned.length) {
      merged.push({
        id: "dept_unassigned",
        label: "未分配部门",
        disabled: true,
        children: unassigned.map(userToTreeLeaf),
      });
    }
    if (!merged.length && users.length) {
      merged = buildFlatApproverTree(users);
    }
    approverTreeData.value = merged;
    const map = {};
    collectUserLabels(merged, map);
    approverLabelMap.value = map;
  } catch {
    approverTreeData.value = [];
    approverLabelMap.value = {};
    proxy?.$modal?.msgWarning?.("审批人数据加载失败,请检查网络或稍后重试");
  }
}
function resolveApproverNames(ids) {
  if (!ids?.length) return "";
  const map = approverLabelMap.value;
  return ids.map((id) => map[String(id)] || id).join("、");
}
function approvalModeLabel(mode) {
  if (mode === "countersign") return "会签";
  return "与签";
}
function approvalResultLabel(v) {
  if (v === "approved") return "已通过";
  if (v === "rejected") return "已驳回";
  if (v === "cancelled") return "已撤销";
  return "待审批";
}
/** æœ¬åœ°æ¨¡æ‹Ÿåˆ—表数据 */
const allRows = ref([
  {
    id: "1",
    applicantId: "1001",
    applicantName: "周明",
    transferDate: "2026-05-20",
    originalPostId: "post_dev",
    originalPostName: "软件开发工程师",
    targetPostId: "post_senior_dev",
    targetPostName: "高级软件开发工程师",
    approvalResult: "pending",
    approvalMode: "parallel",
    approverIds: [],
    approverNames: "",
  },
  {
    id: "2",
    applicantId: "1002",
    applicantName: "吴芳",
    transferDate: "2026-05-10",
    originalPostId: "post_pm",
    originalPostName: "产品经理",
    targetPostId: "post_senior_pm",
    targetPostName: "高级产品经理",
    approvalResult: "approved",
    approvalMode: "countersign",
    approverIds: [],
    approverNames: "张三、李四",
  },
]);
const searchForm = reactive({
  applicantId: "",
  transferDateRange: null,
});
const tableLoading = ref(false);
const page = reactive({
  current: 1,
  size: 10,
  total: 0,
});
const filteredList = computed(() => {
  let list = [...allRows.value];
  if (searchForm.applicantId) {
    list = list.filter((r) => String(r.applicantId) === String(searchForm.applicantId));
  }
  const range = searchForm.transferDateRange;
  if (range && range.length === 2) {
    const [start, end] = range;
    list = list.filter((r) => r.transferDate >= start && r.transferDate <= end);
  }
  return list.sort((a, b) => (a.transferDate < b.transferDate ? 1 : -1));
});
watch(
  filteredList,
  (list) => {
    page.total = list.length;
    const maxPage = Math.max(1, Math.ceil(list.length / page.size) || 1);
    if (page.current > maxPage) {
      page.current = maxPage;
    }
  },
  { immediate: true }
);
const tableData = computed(() => {
  const list = filteredList.value;
  const start = (page.current - 1) * page.size;
  return list.slice(start, start + page.size);
});
const tableColumn = ref([
  { label: "申请人", prop: "applicantName", minWidth: 100 },
  { label: "转岗日期", prop: "transferDate", width: 120 },
  { label: "原岗位", prop: "originalPostName", minWidth: 140 },
  { label: "转入岗位", prop: "targetPostName", minWidth: 160 },
  {
    label: "审批结果",
    prop: "approvalResult",
    width: 110,
    dataType: "tag",
    formatData: (v) => approvalResultLabel(v),
    formatType: (v) => {
      if (v === "approved") return "success";
      if (v === "rejected") return "danger";
      if (v === "cancelled") return "info";
      return "warning";
    },
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: "right",
    width: 180,
    operation: [
      { name: "编辑", type: "text", clickFun: (row) => openFormDialog("edit", row) },
      { name: "查看详情", type: "text", clickFun: (row) => openDetail(row) },
    ],
  },
]);
const formDialog = reactive({
  visible: false,
  title: "",
  mode: "add",
});
const formRef = ref();
const form = reactive(createEmptyForm());
const formRules = {
  applicantId: [{ required: true, message: "请选择申请人", trigger: "change" }],
  transferDate: [{ required: true, message: "请选择转岗日期", trigger: "change" }],
  originalPostName: [{ required: true, message: "原岗位不能为空", trigger: "change" }],
  targetPostId: [{ required: true, message: "请选择转入岗位", trigger: "change" }],
  approvalMode: [{ required: true, message: "请选择审批方式", trigger: "change" }],
  approverIds: [{ type: "array", required: true, message: "请选择审批人", trigger: "change" }],
};
const detailDialog = reactive({ visible: false });
const detailRow = ref({});
function handleQuery() {
  page.current = 1;
  tableLoading.value = true;
  setTimeout(() => {
    tableLoading.value = false;
  }, 150);
}
async function resetSearch() {
  searchForm.applicantId = "";
  searchForm.transferDateRange = null;
  handleQuery();
  await remoteSearchApplicant("");
}
function pagination(obj) {
  page.current = obj.page;
  page.size = obj.limit;
}
function openDetail(row) {
  detailRow.value = { ...row };
  detailDialog.visible = true;
}
function ensureApplicantInFormOptions(row) {
  if (!row?.applicantId) return;
  const id = String(row.applicantId);
  if (!applicantFormOptions.value.some((u) => String(u.userId ?? u.id) === id)) {
    applicantFormOptions.value = [
      {
        userId: row.applicantId,
        nickName: row.applicantName,
        userName: row.applicantUserName,
      },
      ...applicantFormOptions.value,
    ];
  }
}
async function openFormDialog(mode, row) {
  formDialog.mode = mode;
  formDialog.title = mode === "add" ? "新增调岗申请" : "编辑调岗申请";
  loadApproverTree();
  Object.assign(form, createEmptyForm());
  await remoteSearchApplicantForm("");
  if (mode === "edit" && row) {
    ensureApplicantInFormOptions(row);
    Object.assign(form, {
      id: row.id,
      applicantId: row.applicantId,
      applicantName: row.applicantName,
      transferDate: row.transferDate,
      originalPostId: row.originalPostId,
      originalPostName: row.originalPostName,
      targetPostId: row.targetPostId,
      targetPostName: row.targetPostName,
      approvalMode: row.approvalMode,
      approverIds: (row.approverIds || []).map((id) => String(id)),
      approverNames: row.approverNames,
    });
  }
  formDialog.visible = true;
  nextTick(() => formRef.value?.clearValidate?.());
}
function onFormClosed() {
  formRef.value?.resetFields?.();
}
async function submitForm() {
  try {
    await formRef.value?.validate?.();
  } catch {
    return;
  }
  form.approverNames = resolveApproverNames(form.approverIds);
  form.targetPostName = targetPostNameById(form.targetPostId);
  const payload = {
    applicantId: form.applicantId,
    applicantName: form.applicantName,
    transferDate: form.transferDate,
    originalPostId: form.originalPostId,
    originalPostName: form.originalPostName,
    targetPostId: form.targetPostId,
    targetPostName: form.targetPostName,
    approvalMode: form.approvalMode,
    approverIds: [...form.approverIds],
    approverNames: form.approverNames,
  };
  if (formDialog.mode === "add") {
    const id = `local_${Date.now()}`;
    allRows.value.unshift({
      id,
      ...payload,
      approvalResult: "pending",
    });
    proxy?.$modal?.msgSuccess?.("新增成功(本地模拟)");
  } else {
    const idx = allRows.value.findIndex((r) => r.id === form.id);
    const prev = idx !== -1 ? allRows.value[idx] : {};
    if (idx !== -1) {
      allRows.value[idx] = {
        ...prev,
        id: form.id,
        ...payload,
      };
    }
    proxy?.$modal?.msgSuccess?.("保存成功(本地模拟)");
  }
  formDialog.visible = false;
  handleQuery();
}
onMounted(async () => {
  await Promise.all([loadUserPool(), loadPostOptions()]);
  rebuildPostIdMap();
  loadApproverTree();
  await remoteSearchApplicant("");
});
</script>
<style scoped>
.mb20 {
  margin-bottom: 20px;
}
.search_form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.search_title {
  font-size: 14px;
  color: var(--el-text-color-regular);
}
.transfer-apply-form :deep(.el-row) {
  margin-bottom: 0;
}
.transfer-apply-form :deep(.el-form-item) {
  margin-bottom: 18px;
}
.transfer-apply-form-dialog :deep(.el-dialog__body) {
  padding-top: 12px;
}
</style>
src/views/officeProcessAutomation/HrManage/work-handover/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,810 @@
<!--OA模块:工作交接-->
<template>
  <div class="app-container">
    <div class="search_form mb20">
      <div>
        <span class="search_title">申请人:</span>
        <el-select
          v-model="searchForm.applicantId"
          filterable
          remote
          clearable
          reserve-keyword
          placeholder="请选择或搜索申请人"
          style="width: 220px"
          :remote-method="remoteSearchApplicant"
          :loading="applicantSearchLoading"
        >
          <el-option
            v-for="u in applicantSearchOptions"
            :key="u.userId"
            :label="userSelectLabel(u)"
            :value="u.userId"
          />
        </el-select>
        <span class="search_title" style="margin-left: 12px">交接状态:</span>
        <el-select v-model="searchForm.handoverStatus" placeholder="全部" clearable style="width: 140px">
          <el-option v-for="o in handoverStatusOptions" :key="o.value" :label="o.label" :value="o.value" />
        </el-select>
        <span class="search_title" style="margin-left: 12px">交接类型:</span>
        <el-select v-model="searchForm.handoverType" placeholder="全部" clearable style="width: 140px">
          <el-option v-for="o in handoverTypeOptions" :key="o.value" :label="o.label" :value="o.value" />
        </el-select>
        <el-button type="primary" style="margin-left: 10px" @click="handleQuery">搜索</el-button>
        <el-button @click="resetSearch">重置</el-button>
      </div>
      <div>
        <el-button type="primary" @click="openFormDialog('add')">新增工作交接</el-button>
      </div>
    </div>
    <div class="table_list">
      <PIMTable
        rowKey="id"
        :column="tableColumn"
        :tableData="tableData"
        :page="page"
        :isSelection="false"
        :tableLoading="tableLoading"
        @pagination="pagination"
        :total="page.total"
      />
    </div>
    <!-- æ–°å¢ž / ç¼–辑 -->
    <el-dialog
      v-model="formDialog.visible"
      :title="formDialog.title"
      width="720px"
      append-to-body
      destroy-on-close
      class="work-handover-form-dialog"
      @closed="onFormClosed"
    >
      <el-form ref="formRef" :model="form" :rules="formRules" label-width="120px" class="work-handover-form">
        <el-row :gutter="24">
          <el-col :span="24">
            <el-form-item label="申请人" prop="applicantId">
              <el-select
                v-model="form.applicantId"
                filterable
                remote
                clearable
                reserve-keyword
                placeholder="请选择或搜索申请人"
                style="width: 100%"
                :remote-method="remoteSearchApplicantForm"
                :loading="applicantFormSearchLoading"
                @change="onApplicantChange"
              >
                <el-option
                  v-for="u in applicantFormOptions"
                  :key="u.userId"
                  :label="userSelectLabel(u)"
                  :value="u.userId"
                />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="12">
            <el-form-item label="离职日期" prop="leaveDate">
              <el-date-picker
                v-model="form.leaveDate"
                type="date"
                placeholder="请选择离职日期"
                format="YYYY-MM-DD"
                value-format="YYYY-MM-DD"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="交接状态" prop="handoverStatus">
              <el-select v-model="form.handoverStatus" placeholder="请选择交接状态" style="width: 100%">
                <el-option v-for="o in handoverStatusOptions" :key="o.value" :label="o.label" :value="o.value" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="12">
            <el-form-item label="交接类型" prop="handoverType">
              <el-select v-model="form.handoverType" placeholder="请选择交接类型" style="width: 100%">
                <el-option v-for="o in handoverTypeOptions" :key="o.value" :label="o.label" :value="o.value" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="交接人" prop="handoverPersonId">
              <el-select
                v-model="form.handoverPersonId"
                filterable
                remote
                clearable
                reserve-keyword
                placeholder="请选择或搜索交接人"
                style="width: 100%"
                :remote-method="remoteSearchHandoverPerson"
                :loading="handoverPersonSearchLoading"
                @change="onHandoverPersonChange"
              >
                <el-option
                  v-for="u in handoverPersonOptions"
                  :key="u.userId"
                  :label="userSelectLabel(u)"
                  :value="u.userId"
                />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="12">
            <el-form-item label="审批方式" prop="approvalMode">
              <el-radio-group v-model="form.approvalMode">
                <el-radio value="parallel">与签</el-radio>
                <el-radio value="countersign">会签</el-radio>
              </el-radio-group>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="24">
          <el-col :span="24">
            <el-form-item label="审批人" prop="approverIds">
              <el-tree-select
                v-model="form.approverIds"
                :data="approverTreeData"
                multiple
                collapse-tags
                collapse-tags-tooltip
                :max-collapse-tags="2"
                :render-after-expand="false"
                placeholder="请选择审批人(可多选)"
                style="width: 100%"
                :props="{ value: 'id', label: 'label', children: 'children', disabled: 'disabled' }"
                check-strictly
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
          <el-button @click="formDialog.visible = false">取 æ¶ˆ</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- è¯¦æƒ… -->
    <el-dialog v-model="detailDialog.visible" title="工作交接详情" width="560px" append-to-body>
      <el-descriptions :column="1" border>
        <el-descriptions-item label="申请人">{{ detailRow.applicantName }}</el-descriptions-item>
        <el-descriptions-item label="离职日期">{{ detailRow.leaveDate || "—" }}</el-descriptions-item>
        <el-descriptions-item label="交接状态">{{ handoverStatusLabel(detailRow.handoverStatus) }}</el-descriptions-item>
        <el-descriptions-item label="交接类型">{{ handoverTypeLabel(detailRow.handoverType) }}</el-descriptions-item>
        <el-descriptions-item label="交接人">{{ detailRow.handoverPersonName || "—" }}</el-descriptions-item>
        <el-descriptions-item label="审批结果">{{ approvalResultLabel(detailRow.approvalResult) }}</el-descriptions-item>
        <el-descriptions-item label="审批方式">{{ approvalModeLabel(detailRow.approvalMode) }}</el-descriptions-item>
        <el-descriptions-item label="审批人">{{ detailRow.approverNames || "—" }}</el-descriptions-item>
      </el-descriptions>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="detailDialog.visible = false">关 é—­</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { deptTreeSelect, userListNoPageByTenantId } from "@/api/system/user.js";
import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref, watch } from "vue";
const { proxy } = getCurrentInstance();
const handoverStatusOptions = [
  { value: "in_progress", label: "进行中" },
  { value: "completed", label: "已完成" },
  { value: "returned", label: "已退回" },
];
const handoverTypeOptions = [
  { value: "resignation", label: "离职交接" },
  { value: "transfer", label: "调岗交接" },
];
function handoverStatusLabel(v) {
  return handoverStatusOptions.find((o) => o.value === v)?.label || "—";
}
function handoverTypeLabel(v) {
  return handoverTypeOptions.find((o) => o.value === v)?.label || "—";
}
/** ä¸ŽåŽç«¯çº¦å®šå­—段(本地占位,后期接口对齐) */
const createEmptyForm = () => ({
  id: undefined,
  applicantId: "",
  applicantName: "",
  leaveDate: "",
  handoverStatus: "in_progress",
  handoverType: "resignation",
  handoverPersonId: "",
  handoverPersonName: "",
  approvalMode: "parallel",
  approverIds: [],
  approverNames: "",
});
const allUsersCache = ref([]);
function userSelectLabel(u) {
  const nick = u.nickName || "";
  const name = u.userName || "";
  if (nick && name && nick !== name) return `${nick}(${name})`;
  return nick || name || `用户${u.userId ?? u.id ?? ""}`;
}
function userById(id) {
  if (id == null || id === "") return undefined;
  return allUsersCache.value.find((u) => String(u.userId ?? u.id) === String(id));
}
function filterUsersByQuery(query) {
  const list = allUsersCache.value.filter((u) => isActiveUser(u));
  const q = (query || "").trim().toLowerCase();
  if (!q) return [...list];
  return list.filter((u) => {
    const nick = (u.nickName || "").toLowerCase();
    const uname = (u.userName || "").toLowerCase();
    const phone = (u.phonenumber || u.phone || "").toString();
    return nick.includes(q) || uname.includes(q) || phone.includes(q);
  });
}
async function loadUserPool() {
  try {
    const res = await userListNoPageByTenantId();
    allUsersCache.value = unwrapArray(res);
  } catch {
    allUsersCache.value = [];
  }
}
const applicantSearchLoading = ref(false);
const applicantSearchOptions = ref([]);
async function remoteSearchApplicant(query) {
  applicantSearchLoading.value = true;
  try {
    if (!allUsersCache.value.length) {
      await loadUserPool();
    }
    applicantSearchOptions.value = filterUsersByQuery(query);
  } finally {
    applicantSearchLoading.value = false;
  }
}
const applicantFormSearchLoading = ref(false);
const applicantFormOptions = ref([]);
async function remoteSearchApplicantForm(query) {
  applicantFormSearchLoading.value = true;
  try {
    if (!allUsersCache.value.length) {
      await loadUserPool();
    }
    applicantFormOptions.value = filterUsersByQuery(query);
  } finally {
    applicantFormSearchLoading.value = false;
  }
}
function onApplicantChange(uid) {
  const u = userById(uid);
  form.applicantName = u ? u.nickName || u.userName || "" : "";
}
const handoverPersonSearchLoading = ref(false);
const handoverPersonOptions = ref([]);
async function remoteSearchHandoverPerson(query) {
  handoverPersonSearchLoading.value = true;
  try {
    if (!allUsersCache.value.length) {
      await loadUserPool();
    }
    handoverPersonOptions.value = filterUsersByQuery(query);
  } finally {
    handoverPersonSearchLoading.value = false;
  }
}
function onHandoverPersonChange(uid) {
  const u = userById(uid);
  form.handoverPersonName = u ? u.nickName || u.userName || "" : "";
}
const approverTreeData = ref([]);
const approverLabelMap = ref({});
function unwrapArray(payload) {
  if (Array.isArray(payload)) return payload;
  if (payload && Array.isArray(payload.data)) return payload.data;
  if (payload && Array.isArray(payload.rows)) return payload.rows;
  return [];
}
function filterDisabledDept(deptList) {
  if (!Array.isArray(deptList)) return [];
  return deptList.filter((dept) => {
    if (dept.disabled) return false;
    if (dept.children?.length) {
      dept.children = filterDisabledDept(dept.children);
    }
    return true;
  });
}
function getUserDeptId(u) {
  return u.deptId ?? u.sysDeptId ?? u.dept?.deptId ?? u.dept?.id ?? u.dept_id;
}
function getDeptNodeKey(node) {
  const k = node?.id ?? node?.value ?? node?.deptId;
  if (k == null || k === "") return null;
  return k;
}
function isActiveUser(u) {
  if (u.delFlag === "2" || u.delFlag === 2) return false;
  if (u.status == null) return true;
  return String(u.status) === "0";
}
function userToTreeLeaf(u) {
  return {
    id: String(u.userId ?? u.id),
    label: u.nickName || u.userName || `用户${u.userId ?? u.id}`,
  };
}
function buildUsersByDeptId(users) {
  const map = new Map();
  const unassigned = [];
  for (const u of users) {
    if (!isActiveUser(u)) continue;
    const did = getUserDeptId(u);
    if (did == null || did === "" || did === 0 || did === "0") {
      unassigned.push(u);
      continue;
    }
    const k = String(did);
    if (!map.has(k)) map.set(k, []);
    map.get(k).push(u);
  }
  return { map, unassigned };
}
function collectUserLabels(nodes, map) {
  (nodes || []).forEach((n) => {
    if (n.children?.length) {
      collectUserLabels(n.children, map);
    } else if (n.id != null && !String(n.id).startsWith("dept_")) {
      map[String(n.id)] = n.label;
    }
  });
}
function mergeDeptTreeWithUsers(nodes, usersByDept) {
  if (!Array.isArray(nodes)) return [];
  const out = [];
  for (const node of nodes) {
    const deptIdRaw = getDeptNodeKey(node);
    if (deptIdRaw == null) continue;
    const sub = mergeDeptTreeWithUsers(node.children || [], usersByDept);
    const usersHere = usersByDept.get(String(deptIdRaw)) || [];
    const userChildren = usersHere.map(userToTreeLeaf);
    const children = [...sub, ...userChildren];
    if (!children.length) continue;
    out.push({
      id: `dept_${deptIdRaw}`,
      label: node.label ?? node.deptName ?? "部门",
      disabled: true,
      children,
    });
  }
  return out;
}
function buildFlatApproverTree(users) {
  const list = users.filter(isActiveUser).map(userToTreeLeaf);
  if (!list.length) return [];
  return [
    {
      id: "dept_all_users",
      label: "系统用户",
      disabled: true,
      children: list,
    },
  ];
}
async function loadApproverTree() {
  try {
    const needFetchUsers = !allUsersCache.value.length;
    const [deptRes, userRes] = await Promise.all([
      deptTreeSelect(),
      needFetchUsers ? userListNoPageByTenantId() : Promise.resolve(null),
    ]);
    let rawTree = unwrapArray(deptRes);
    rawTree = rawTree.length ? JSON.parse(JSON.stringify(rawTree)) : [];
    let deptTree = filterDisabledDept(JSON.parse(JSON.stringify(rawTree)));
    if (!deptTree.length && rawTree.length) {
      deptTree = JSON.parse(JSON.stringify(rawTree));
    }
    let users = needFetchUsers ? unwrapArray(userRes) : [...allUsersCache.value];
    if (needFetchUsers && users.length) {
      allUsersCache.value = users;
    }
    const { map: usersByDept, unassigned } = buildUsersByDeptId(users);
    let merged = mergeDeptTreeWithUsers(deptTree, usersByDept);
    if (unassigned.length) {
      merged.push({
        id: "dept_unassigned",
        label: "未分配部门",
        disabled: true,
        children: unassigned.map(userToTreeLeaf),
      });
    }
    if (!merged.length && users.length) {
      merged = buildFlatApproverTree(users);
    }
    approverTreeData.value = merged;
    const map = {};
    collectUserLabels(merged, map);
    approverLabelMap.value = map;
  } catch {
    approverTreeData.value = [];
    approverLabelMap.value = {};
    proxy?.$modal?.msgWarning?.("审批人数据加载失败,请检查网络或稍后重试");
  }
}
function resolveApproverNames(ids) {
  if (!ids?.length) return "";
  const map = approverLabelMap.value;
  return ids.map((id) => map[String(id)] || id).join("、");
}
function approvalModeLabel(mode) {
  if (mode === "countersign") return "会签";
  return "与签";
}
function approvalResultLabel(v) {
  if (v === "approved") return "已通过";
  if (v === "rejected") return "已驳回";
  if (v === "cancelled") return "已撤销";
  return "待审批";
}
function handoverStatusTagType(v) {
  if (v === "completed") return "success";
  if (v === "returned") return "danger";
  return "warning";
}
function handoverTypeTagType(v) {
  return v === "transfer" ? "info" : "";
}
/** æœ¬åœ°æ¨¡æ‹Ÿåˆ—表数据 */
const allRows = ref([
  {
    id: "1",
    applicantId: "1001",
    applicantName: "周明",
    leaveDate: "2026-05-28",
    handoverStatus: "in_progress",
    handoverType: "resignation",
    handoverPersonId: "1003",
    handoverPersonName: "王强",
    approvalResult: "pending",
    approvalMode: "parallel",
    approverIds: [],
    approverNames: "",
  },
  {
    id: "2",
    applicantId: "1002",
    applicantName: "吴芳",
    leaveDate: "2026-05-15",
    handoverStatus: "completed",
    handoverType: "transfer",
    handoverPersonId: "1004",
    handoverPersonName: "赵敏",
    approvalResult: "approved",
    approvalMode: "countersign",
    approverIds: [],
    approverNames: "张三、李四",
  },
  {
    id: "3",
    applicantId: "1005",
    applicantName: "陈浩",
    leaveDate: "2026-04-20",
    handoverStatus: "returned",
    handoverType: "resignation",
    handoverPersonId: "1006",
    handoverPersonName: "刘洋",
    approvalResult: "rejected",
    approvalMode: "parallel",
    approverIds: [],
    approverNames: "李四",
  },
]);
const searchForm = reactive({
  applicantId: "",
  handoverStatus: "",
  handoverType: "",
});
const tableLoading = ref(false);
const page = reactive({
  current: 1,
  size: 10,
  total: 0,
});
const filteredList = computed(() => {
  let list = [...allRows.value];
  if (searchForm.applicantId) {
    list = list.filter((r) => String(r.applicantId) === String(searchForm.applicantId));
  }
  if (searchForm.handoverStatus) {
    list = list.filter((r) => r.handoverStatus === searchForm.handoverStatus);
  }
  if (searchForm.handoverType) {
    list = list.filter((r) => r.handoverType === searchForm.handoverType);
  }
  return list.sort((a, b) => (a.leaveDate < b.leaveDate ? 1 : -1));
});
watch(
  filteredList,
  (list) => {
    page.total = list.length;
    const maxPage = Math.max(1, Math.ceil(list.length / page.size) || 1);
    if (page.current > maxPage) {
      page.current = maxPage;
    }
  },
  { immediate: true }
);
const tableData = computed(() => {
  const list = filteredList.value;
  const start = (page.current - 1) * page.size;
  return list.slice(start, start + page.size);
});
const tableColumn = ref([
  { label: "申请人", prop: "applicantName", minWidth: 100 },
  { label: "离职日期", prop: "leaveDate", width: 120 },
  {
    label: "交接状态",
    prop: "handoverStatus",
    width: 110,
    dataType: "tag",
    formatData: (v) => handoverStatusLabel(v),
    formatType: (v) => handoverStatusTagType(v),
  },
  {
    label: "交接类型",
    prop: "handoverType",
    width: 110,
    dataType: "tag",
    formatData: (v) => handoverTypeLabel(v),
    formatType: (v) => handoverTypeTagType(v),
  },
  { label: "交接人", prop: "handoverPersonName", minWidth: 100 },
  {
    label: "审批结果",
    prop: "approvalResult",
    width: 110,
    dataType: "tag",
    formatData: (v) => approvalResultLabel(v),
    formatType: (v) => {
      if (v === "approved") return "success";
      if (v === "rejected") return "danger";
      if (v === "cancelled") return "info";
      return "warning";
    },
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: "right",
    width: 200,
    operation: [
      { name: "编辑", type: "text", clickFun: (row) => openFormDialog("edit", row) },
      { name: "详情", type: "text", clickFun: (row) => openDetail(row) },
    ],
  },
]);
const formDialog = reactive({
  visible: false,
  title: "",
  mode: "add",
});
const formRef = ref();
const form = reactive(createEmptyForm());
const formRules = {
  applicantId: [{ required: true, message: "请选择申请人", trigger: "change" }],
  leaveDate: [{ required: true, message: "请选择离职日期", trigger: "change" }],
  handoverStatus: [{ required: true, message: "请选择交接状态", trigger: "change" }],
  handoverType: [{ required: true, message: "请选择交接类型", trigger: "change" }],
  handoverPersonId: [{ required: true, message: "请选择交接人", trigger: "change" }],
  approvalMode: [{ required: true, message: "请选择审批方式", trigger: "change" }],
  approverIds: [{ type: "array", required: true, message: "请选择审批人", trigger: "change" }],
};
const detailDialog = reactive({ visible: false });
const detailRow = ref({});
function handleQuery() {
  page.current = 1;
  tableLoading.value = true;
  setTimeout(() => {
    tableLoading.value = false;
  }, 150);
}
async function resetSearch() {
  searchForm.applicantId = "";
  searchForm.handoverStatus = "";
  searchForm.handoverType = "";
  handleQuery();
  await remoteSearchApplicant("");
}
function pagination(obj) {
  page.current = obj.page;
  page.size = obj.limit;
}
function openDetail(row) {
  detailRow.value = { ...row };
  detailDialog.visible = true;
}
function ensureUserInOptions(optionsRef, row, idKey, nameKey) {
  const id = row?.[idKey];
  if (id == null || id === "") return;
  const sid = String(id);
  if (!optionsRef.value.some((u) => String(u.userId ?? u.id) === sid)) {
    optionsRef.value = [
      {
        userId: id,
        nickName: row[nameKey],
        userName: row.applicantUserName,
      },
      ...optionsRef.value,
    ];
  }
}
async function openFormDialog(mode, row) {
  formDialog.mode = mode;
  formDialog.title = mode === "add" ? "新增工作交接" : "编辑工作交接";
  loadApproverTree();
  Object.assign(form, createEmptyForm());
  await Promise.all([remoteSearchApplicantForm(""), remoteSearchHandoverPerson("")]);
  if (mode === "edit" && row) {
    ensureUserInOptions(applicantFormOptions, row, "applicantId", "applicantName");
    ensureUserInOptions(handoverPersonOptions, row, "handoverPersonId", "handoverPersonName");
    Object.assign(form, {
      id: row.id,
      applicantId: row.applicantId,
      applicantName: row.applicantName,
      leaveDate: row.leaveDate,
      handoverStatus: row.handoverStatus,
      handoverType: row.handoverType,
      handoverPersonId: row.handoverPersonId,
      handoverPersonName: row.handoverPersonName,
      approvalMode: row.approvalMode,
      approverIds: (row.approverIds || []).map((id) => String(id)),
      approverNames: row.approverNames,
    });
  }
  formDialog.visible = true;
  nextTick(() => formRef.value?.clearValidate?.());
}
function onFormClosed() {
  formRef.value?.resetFields?.();
}
async function submitForm() {
  try {
    await formRef.value?.validate?.();
  } catch {
    return;
  }
  form.approverNames = resolveApproverNames(form.approverIds);
  const payload = {
    applicantId: form.applicantId,
    applicantName: form.applicantName,
    leaveDate: form.leaveDate,
    handoverStatus: form.handoverStatus,
    handoverType: form.handoverType,
    handoverPersonId: form.handoverPersonId,
    handoverPersonName: form.handoverPersonName,
    approvalMode: form.approvalMode,
    approverIds: [...form.approverIds],
    approverNames: form.approverNames,
  };
  if (formDialog.mode === "add") {
    const id = `local_${Date.now()}`;
    allRows.value.unshift({
      id,
      ...payload,
      approvalResult: "pending",
    });
    proxy?.$modal?.msgSuccess?.("新增成功(本地模拟)");
  } else {
    const idx = allRows.value.findIndex((r) => r.id === form.id);
    const prev = idx !== -1 ? allRows.value[idx] : {};
    if (idx !== -1) {
      allRows.value[idx] = {
        ...prev,
        id: form.id,
        ...payload,
      };
    }
    proxy?.$modal?.msgSuccess?.("保存成功(本地模拟)");
  }
  formDialog.visible = false;
  handleQuery();
}
onMounted(async () => {
  await loadUserPool();
  loadApproverTree();
  await remoteSearchApplicant("");
});
</script>
<style scoped>
.mb20 {
  margin-bottom: 20px;
}
.search_form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.search_title {
  font-size: 14px;
  color: var(--el-text-color-regular);
}
.work-handover-form :deep(.el-row) {
  margin-bottom: 0;
}
.work-handover-form :deep(.el-form-item) {
  margin-bottom: 18px;
}
.work-handover-form-dialog :deep(.el-dialog__body) {
  padding-top: 12px;
}
</style>
src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
<!--
  æ¨¡å—中文名:费用报销
  ç›®å½•标识:ReimburseManage/cost-reimburse(cost-reimburse â†’ ä¸­æ–‡ï¼šè´¹ç”¨æŠ¥é”€ï¼‰
  å¤ç”¨é¡µé¢ï¼š@/views/procurementManagement/procurementLedger/index.vue(采购台账;文件名 index.vue â†’ å…¥å£é¡µï¼‰
-->
<template>
  <ProcurementLedger />
</template>
<script setup>
import ProcurementLedger from '@/views/procurementManagement/procurementLedger/index.vue'
</script>
src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
<!--
  æ¨¡å—中文名:差旅报销
  ç›®å½•标识:ReimburseManage/travel-reimburse(travel-reimburse â†’ ä¸­æ–‡ï¼šå·®æ—…报销)
  å¤ç”¨é¡µé¢ï¼š@/views/procurementManagement/procurementLedger/index.vue(采购台账;文件名 index.vue â†’ å…¥å£é¡µï¼‰
-->
<template>
  <ProcurementLedger />
</template>
<script setup>
import ProcurementLedger from '@/views/procurementManagement/procurementLedger/index.vue'
</script>
src/views/officeProcessAutomation/SysAdmin/dept-manage/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,291 @@
<!--OA模块:部门管理-->
<template>
    <div class="app-container">
        <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
            <el-form-item label="部门名称" prop="deptName">
                <el-input
                    v-model="queryParams.deptName"
                    placeholder="请输入部门名称"
                    clearable
                    style="width: 200px"
                    @keyup.enter="handleQuery"
                />
            </el-form-item>
            <el-form-item label="状态" prop="status">
                <el-select v-model="queryParams.status" placeholder="部门状态" clearable style="width: 200px">
                    <el-option
                        v-for="dict in sys_normal_disable"
                        :key="dict.value"
                        :label="dict.label"
                        :value="dict.value"
                    />
                </el-select>
            </el-form-item>
            <el-form-item>
                <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
                <el-button icon="Refresh" @click="resetQuery">重置</el-button>
            </el-form-item>
        </el-form>
        <el-row :gutter="10" class="mb8">
            <el-col :span="1.5">
                <el-button
                    type="primary"
                    plain
                    icon="Plus"
                    @click="handleAdd"
                    v-hasPermi="['system:dept:add']"
                >新增</el-button>
            </el-col>
            <el-col :span="1.5">
                <el-button
                    type="info"
                    plain
                    icon="Sort"
                    @click="toggleExpandAll"
                >展开/折叠</el-button>
            </el-col>
            <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
        </el-row>
        <el-table
            v-if="refreshTable"
            v-loading="loading"
            :data="deptList"
            row-key="deptId"
            :default-expand-all="isExpandAll"
            :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
        >
            <el-table-column prop="deptName" label="部门名称" width="260"></el-table-column>
            <el-table-column prop="orderNum" label="排序" width="200"></el-table-column>
            <el-table-column prop="status" label="状态" width="100">
                <template #default="scope">
                    <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
                </template>
            </el-table-column>
            <el-table-column label="创建时间" align="center" prop="createTime" width="200">
                <template #default="scope">
                    <span>{{ parseTime(scope.row.createTime) }}</span>
                </template>
            </el-table-column>
            <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
                <template #default="scope">
                    <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:dept:edit']">修改</el-button>
                    <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['system:dept:add']">新增</el-button>
                    <el-button v-if="scope.row.parentId != 0" link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:dept:remove']">删除</el-button>
                </template>
            </el-table-column>
        </el-table>
        <!-- æ·»åŠ æˆ–ä¿®æ”¹éƒ¨é—¨å¯¹è¯æ¡† -->
        <el-dialog :title="title" v-model="open" width="600px" append-to-body>
            <el-form ref="deptRef" :model="form" :rules="rules" label-width="80px">
                <el-row>
                    <el-col :span="24" v-if="form.parentId !== 0">
                        <el-form-item label="上级部门" prop="parentId">
                            <el-tree-select
                                v-model="form.parentId"
                                :data="deptOptions"
                                :props="{ value: 'deptId', label: 'deptName', children: 'children' }"
                                value-key="deptId"
                                placeholder="选择上级部门"
                                check-strictly
                            />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="部门名称" prop="deptName">
                            <el-input v-model="form.deptName" placeholder="请输入部门名称" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="显示排序" prop="orderNum">
                            <el-input-number v-model="form.orderNum" controls-position="right" :min="0"/>
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="负责人" prop="leader">
                            <el-input v-model="form.leader" placeholder="请输入负责人" maxlength="20" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="联系电话" prop="phone">
                            <el-input v-model="form.phone" placeholder="请输入联系电话" maxlength="11" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="邮箱" prop="email">
                            <el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="部门状态">
                            <el-radio-group v-model="form.status">
                                <el-radio
                                    v-for="dict in sys_normal_disable"
                                    :key="dict.value"
                                    :value="dict.value"
                                >{{ dict.label }}</el-radio>
                            </el-radio-group>
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="部门编号" prop="deptNick">
                            <el-input v-model="form.deptNick" placeholder="请输入部门编号" maxlength="50" />
                        </el-form-item>
                    </el-col>
                </el-row>
            </el-form>
            <template #footer>
                <div class="dialog-footer">
                    <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
                    <el-button @click="cancel">取 æ¶ˆ</el-button>
                </div>
            </template>
        </el-dialog>
    </div>
</template>
<script setup name="Dept">
import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild } from "@/api/system/dept"
const { proxy } = getCurrentInstance()
const { sys_normal_disable } = proxy.useDict("sys_normal_disable")
const deptList = ref([])
const open = ref(false)
const loading = ref(true)
const showSearch = ref(true)
const title = ref("")
const deptOptions = ref([])
const isExpandAll = ref(true)
const refreshTable = ref(true)
const data = reactive({
    form: {},
    queryParams: {
        deptName: undefined,
        status: undefined
    },
    rules: {
        parentId: [{ required: true, message: "上级部门不能为空", trigger: "blur" }],
        deptName: [{ required: true, message: "部门名称不能为空", trigger: "blur" }],
        orderNum: [{ required: true, message: "显示排序不能为空", trigger: "blur" }],
        email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] }],
        phone: [{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" }],
        deptNick: [{ required: true, message: "部门编号不能为空", trigger: "blur" }],
    },
})
const { queryParams, form, rules } = toRefs(data)
/** æŸ¥è¯¢éƒ¨é—¨åˆ—表 */
function getList() {
    loading.value = true
    listDept(queryParams.value).then(response => {
        deptList.value = proxy.handleTree(response.data, "deptId")
        loading.value = false
    })
}
/** å–消按钮 */
function cancel() {
    open.value = false
    reset()
}
/** è¡¨å•重置 */
function reset() {
    form.value = {
        deptId: undefined,
        parentId: undefined,
        deptName: undefined,
        orderNum: 0,
        leader: undefined,
        phone: undefined,
        email: undefined,
        status: "0",
        deptNick: undefined,
    }
    proxy.resetForm("deptRef")
}
/** æœç´¢æŒ‰é’®æ“ä½œ */
function handleQuery() {
    getList()
}
/** é‡ç½®æŒ‰é’®æ“ä½œ */
function resetQuery() {
    proxy.resetForm("queryRef")
    handleQuery()
}
/** æ–°å¢žæŒ‰é’®æ“ä½œ */
function handleAdd(row) {
    reset()
    listDept().then(response => {
        deptOptions.value = proxy.handleTree(response.data, "deptId")
    })
    if (row != undefined) {
        form.value.parentId = row.deptId
    }
    open.value = true
    title.value = "添加部门"
}
/** å±•å¼€/折叠操作 */
function toggleExpandAll() {
    refreshTable.value = false
    isExpandAll.value = !isExpandAll.value
    nextTick(() => {
        refreshTable.value = true
    })
}
/** ä¿®æ”¹æŒ‰é’®æ“ä½œ */
function handleUpdate(row) {
    reset()
    listDeptExcludeChild(row.deptId).then(response => {
        deptOptions.value = proxy.handleTree(response.data, "deptId")
    })
    getDept(row.deptId).then(response => {
        form.value = response.data
        open.value = true
        title.value = "修改部门"
    })
}
/** æäº¤æŒ‰é’® */
function submitForm() {
    proxy.$refs["deptRef"].validate(valid => {
        if (valid) {
            if (form.value.deptId != undefined) {
                updateDept(form.value).then(response => {
                    proxy.$modal.msgSuccess("修改成功")
                    open.value = false
                    getList()
                })
            } else {
                addDept(form.value).then(response => {
                    proxy.$modal.msgSuccess("新增成功")
                    open.value = false
                    getList()
                })
            }
        }
    })
}
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
function handleDelete(row) {
    proxy.$modal.confirm('是否确认删除名称为"' + row.deptName + '"的数据项?').then(function() {
        return delDept(row.deptId)
    }).then(() => {
        getList()
        proxy.$modal.msgSuccess("删除成功")
    }).catch(() => {})
}
getList()
</script>
src/views/officeProcessAutomation/SysAdmin/log-manage/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,315 @@
<!--OA模块:日志管理-->
<template>
  <div class="app-container">
     <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
        <el-form-item label="操作地址" prop="operIp">
           <el-input
              v-model="queryParams.operIp"
              placeholder="请输入操作地址"
              clearable
              style="width: 240px;"
              @keyup.enter="handleQuery"
           />
        </el-form-item>
        <el-form-item label="系统模块" prop="title">
           <el-input
              v-model="queryParams.title"
              placeholder="请输入系统模块"
              clearable
              style="width: 240px;"
              @keyup.enter="handleQuery"
           />
        </el-form-item>
        <el-form-item label="操作人员" prop="operName">
           <el-input
              v-model="queryParams.operName"
              placeholder="请输入操作人员"
              clearable
              style="width: 240px;"
              @keyup.enter="handleQuery"
           />
        </el-form-item>
        <el-form-item label="类型" prop="businessType">
           <el-select
              v-model="queryParams.businessType"
              placeholder="操作类型"
              clearable
              style="width: 240px"
           >
              <el-option
                 v-for="dict in sys_oper_type"
                 :key="dict.value"
                 :label="dict.label"
                 :value="dict.value"
              />
           </el-select>
        </el-form-item>
        <el-form-item label="状态" prop="status">
           <el-select
              v-model="queryParams.status"
              placeholder="操作状态"
              clearable
              style="width: 240px"
           >
              <el-option
                 v-for="dict in sys_common_status"
                 :key="dict.value"
                 :label="dict.label"
                 :value="dict.value"
              />
           </el-select>
        </el-form-item>
        <el-form-item label="操作时间" style="width: 308px">
           <el-date-picker
              v-model="dateRange"
              value-format="YYYY-MM-DD HH:mm:ss"
              type="daterange"
              range-separator="-"
              start-placeholder="开始日期"
              end-placeholder="结束日期"
              :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
           ></el-date-picker>
        </el-form-item>
        <el-form-item>
           <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
           <el-button icon="Refresh" @click="resetQuery">重置</el-button>
        </el-form-item>
     </el-form>
     <el-row :gutter="10" class="mb8">
        <el-col :span="1.5">
           <el-button
              type="danger"
              plain
              icon="Delete"
              :disabled="multiple"
              @click="handleDelete"
              v-hasPermi="['monitor:operlog:remove']"
           >删除</el-button>
        </el-col>
        <el-col :span="1.5">
           <el-button
              type="danger"
              plain
              icon="Delete"
              @click="handleClean"
              v-hasPermi="['monitor:operlog:remove']"
           >清空</el-button>
        </el-col>
        <el-col :span="1.5">
           <el-button
              type="warning"
              plain
              icon="Download"
              @click="handleExport"
              v-hasPermi="['monitor:operlog:export']"
           >导出</el-button>
        </el-col>
        <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
     <el-table ref="operlogRef" v-loading="loading" :data="operlogList" @selection-change="handleSelectionChange" :default-sort="defaultSort" @sort-change="handleSortChange">
        <el-table-column type="selection" width="50" align="center" />
        <el-table-column label="日志编号" align="center" prop="operId" />
        <el-table-column label="系统模块" align="center" prop="title" :show-overflow-tooltip="true" />
        <el-table-column label="操作类型" align="center" prop="businessType">
           <template #default="scope">
              <dict-tag :options="sys_oper_type" :value="scope.row.businessType" />
           </template>
        </el-table-column>
        <el-table-column label="操作人员" align="center" width="110" prop="operName" :show-overflow-tooltip="true" sortable="custom" :sort-orders="['descending', 'ascending']" />
        <el-table-column label="操作地址" align="center" prop="operIp" width="130" :show-overflow-tooltip="true" />
        <el-table-column label="操作状态" align="center" prop="status">
           <template #default="scope">
              <dict-tag :options="sys_common_status" :value="scope.row.status" />
           </template>
        </el-table-column>
        <el-table-column label="操作日期" align="center" prop="operTime" width="180" sortable="custom" :sort-orders="['descending', 'ascending']">
           <template #default="scope">
              <span>{{ parseTime(scope.row.operTime) }}</span>
           </template>
        </el-table-column>
        <el-table-column label="消耗时间" align="center" prop="costTime" width="110" :show-overflow-tooltip="true" sortable="custom" :sort-orders="['descending', 'ascending']">
           <template #default="scope">
              <span>{{ scope.row.costTime }}毫秒</span>
           </template>
        </el-table-column>
        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
           <template #default="scope">
              <el-button link type="primary" icon="View" @click="handleView(scope.row, scope.index)" v-hasPermi="['monitor:operlog:query']">详细</el-button>
           </template>
        </el-table-column>
     </el-table>
     <pagination
        v-show="total > 0"
        :total="total"
        v-model:page="queryParams.pageNum"
        v-model:limit="queryParams.pageSize"
        @pagination="getList"
     />
     <!-- æ“ä½œæ—¥å¿—详细 -->
     <el-dialog title="操作日志详细" v-model="open" width="800px" append-to-body>
        <el-form :model="form" label-width="100px">
           <el-row>
              <el-col :span="12">
                 <el-form-item label="操作模块:">{{ form.title }} / {{ typeFormat(form) }}</el-form-item>
                 <el-form-item
                   label="登录信息:"
                 >{{ form.operName }} / {{ form.operIp }} / {{ form.operLocation }}</el-form-item>
              </el-col>
              <el-col :span="12">
                 <el-form-item label="请求地址:">{{ form.operUrl }}</el-form-item>
                 <el-form-item label="请求方式:">{{ form.requestMethod }}</el-form-item>
              </el-col>
              <el-col :span="24">
                 <el-form-item label="操作方法:">{{ form.method }}</el-form-item>
              </el-col>
              <el-col :span="24">
                 <el-form-item label="请求参数:">{{ form.operParam }}</el-form-item>
              </el-col>
              <el-col :span="24">
                 <el-form-item label="返回参数:">{{ form.jsonResult }}</el-form-item>
              </el-col>
              <el-col :span="8">
                 <el-form-item label="操作状态:">
                    <div v-if="form.status === 0">正常</div>
                    <div v-else-if="form.status === 1">失败</div>
                 </el-form-item>
              </el-col>
              <el-col :span="8">
                 <el-form-item label="消耗时间:">{{ form.costTime }}毫秒</el-form-item>
              </el-col>
              <el-col :span="8">
                 <el-form-item label="操作时间:">{{ parseTime(form.operTime) }}</el-form-item>
              </el-col>
              <el-col :span="24">
                 <el-form-item label="异常信息:" v-if="form.status === 1">{{ form.errorMsg }}</el-form-item>
              </el-col>
           </el-row>
        </el-form>
        <template #footer>
           <div class="dialog-footer">
              <el-button @click="open = false">关 é—­</el-button>
           </div>
        </template>
     </el-dialog>
  </div>
</template>
<script setup name="Operlog">
import { list, delOperlog, cleanOperlog } from "@/api/monitor/operlog"
import {onMounted} from "vue";
const { proxy } = getCurrentInstance()
const { sys_oper_type, sys_common_status } = proxy.useDict("sys_oper_type","sys_common_status")
const operlogList = ref([])
const open = ref(false)
const loading = ref(true)
const showSearch = ref(true)
const ids = ref([])
const single = ref(true)
const multiple = ref(true)
const total = ref(0)
const title = ref("")
const dateRange = ref([])
const defaultSort = ref({ prop: "operTime", order: "descending" })
const data = reactive({
 form: {},
 queryParams: {
   pageNum: 1,
   pageSize: 10,
   operIp: undefined,
   title: undefined,
   operName: undefined,
   businessType: undefined,
   status: undefined
 }
})
const { queryParams, form } = toRefs(data)
/** æŸ¥è¯¢ç™»å½•日志 */
function getList() {
 loading.value = true
 list(proxy.addDateRange(queryParams.value, dateRange.value)).then(response => {
   operlogList.value = response.rows
   total.value = response.total
   loading.value = false
 })
}
/** æ“ä½œæ—¥å¿—类型字典翻译 */
function typeFormat(row, column) {
 return proxy.selectDictLabel(sys_oper_type.value, row.businessType)
}
/** æœç´¢æŒ‰é’®æ“ä½œ */
function handleQuery() {
 queryParams.value.pageNum = 1
 getList()
}
/** é‡ç½®æŒ‰é’®æ“ä½œ */
function resetQuery() {
 dateRange.value = []
 proxy.resetForm("queryRef")
 queryParams.value.pageNum = 1
 proxy.$refs["operlogRef"].sort(defaultSort.value.prop, defaultSort.value.order)
}
/** å¤šé€‰æ¡†é€‰ä¸­æ•°æ® */
function handleSelectionChange(selection) {
 ids.value = selection.map(item => item.operId)
 multiple.value = !selection.length
}
/** æŽ’序触发事件 */
function handleSortChange(column, prop, order) {
 queryParams.value.orderByColumn = column.prop
 queryParams.value.isAsc = column.order
 getList()
}
/** è¯¦ç»†æŒ‰é’®æ“ä½œ */
function handleView(row) {
 open.value = true
 form.value = row
}
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
function handleDelete(row) {
 const operIds = row.operId || ids.value
 proxy.$modal.confirm('是否确认删除日志编号为"' + operIds + '"的数据项?').then(function () {
   return delOperlog(operIds)
 }).then(() => {
   getList()
   proxy.$modal.msgSuccess("删除成功")
 }).catch(() => {})
}
/** æ¸…空按钮操作 */
function handleClean() {
 proxy.$modal.confirm("是否确认清空所有操作日志数据项?").then(function () {
   return cleanOperlog()
 }).then(() => {
   getList()
   proxy.$modal.msgSuccess("清空成功")
 }).catch(() => {})
}
/** å¯¼å‡ºæŒ‰é’®æ“ä½œ */
function handleExport() {
 proxy.download("monitor/operlog/export",{
   ...queryParams.value,
 }, `config_${new Date().getTime()}.xlsx`)
}
onMounted(() => {
 getList();
});
</script>
src/views/officeProcessAutomation/SysAdmin/user-manage/authRole.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,123 @@
<template>
   <div class="app-container">
      <h4 class="form-header h4">基本信息</h4>
      <el-form :model="form" label-width="80px">
         <el-row>
            <el-col :span="8" :offset="2">
               <el-form-item label="用户昵称" prop="nickName">
                  <el-input v-model="form.nickName" disabled />
               </el-form-item>
            </el-col>
            <el-col :span="8" :offset="2">
               <el-form-item label="登录账号" prop="userName">
                  <el-input v-model="form.userName" disabled />
               </el-form-item>
            </el-col>
         </el-row>
      </el-form>
      <h4 class="form-header h4">角色信息</h4>
      <el-table v-loading="loading" :row-key="getRowKey" @row-click="clickRow" ref="roleRef" @selection-change="handleSelectionChange" :data="roles.slice((pageNum - 1) * pageSize, pageNum * pageSize)">
         <el-table-column label="序号" width="55" type="index" align="center">
            <template #default="scope">
               <span>{{ (pageNum - 1) * pageSize + scope.$index + 1 }}</span>
            </template>
         </el-table-column>
         <el-table-column type="selection" :reserve-selection="true" :selectable="checkSelectable" width="55"></el-table-column>
         <el-table-column label="角色编号" align="center" prop="roleId" />
         <el-table-column label="角色名称" align="center" prop="roleName" />
         <el-table-column label="权限字符" align="center" prop="roleKey" />
         <el-table-column label="创建时间" align="center" prop="createTime" width="180">
            <template #default="scope">
               <span>{{ parseTime(scope.row.createTime) }}</span>
            </template>
         </el-table-column>
      </el-table>
      <pagination v-show="total > 0" :total="total" v-model:page="pageNum" v-model:limit="pageSize" />
      <el-form label-width="100px">
         <div style="text-align: center;margin-left:-120px;margin-top:30px;">
            <el-button type="primary" @click="submitForm()">提交</el-button>
            <el-button @click="close()">返回</el-button>
         </div>
      </el-form>
   </div>
</template>
<script setup name="AuthRole">
import { getAuthRole, updateAuthRole } from "@/api/system/user"
const route = useRoute()
const { proxy } = getCurrentInstance()
const loading = ref(true)
const total = ref(0)
const pageNum = ref(1)
const pageSize = ref(10)
const roleIds = ref([])
const roles = ref([])
const form = ref({
  nickName: undefined,
  userName: undefined,
  userId: undefined
})
/** å•击选中行数据 */
function clickRow(row) {
  if (checkSelectable(row)) {
    proxy.$refs["roleRef"].toggleRowSelection(row)
  }
}
/** å¤šé€‰æ¡†é€‰ä¸­æ•°æ® */
function handleSelectionChange(selection) {
  roleIds.value = selection.map(item => item.roleId)
}
/** ä¿å­˜é€‰ä¸­çš„æ•°æ®ç¼–号 */
function getRowKey(row) {
  return row.roleId
}
// æ£€æŸ¥è§’色状态
function checkSelectable(row) {
  return row.status === "0" ? true : false
}
/** å…³é—­æŒ‰é’® */
function close() {
  const obj = { path: "/system/user" }
  proxy.$tab.closeOpenPage(obj)
}
/** æäº¤æŒ‰é’® */
function submitForm() {
  const userId = form.value.userId
  const rIds = roleIds.value.join(",")
  updateAuthRole({ userId: userId, roleIds: rIds }).then(response => {
    proxy.$modal.msgSuccess("授权成功")
    close()
  })
}
(() => {
  const userId = route.params && route.params.userId
  if (userId) {
    loading.value = true
    getAuthRole(userId).then(response => {
      form.value = response.user
      roles.value = response.roles
      total.value = roles.value.length
      nextTick(() => {
        roles.value.forEach(row => {
          if (row.flag) {
            proxy.$refs["roleRef"].toggleRowSelection(row)
          }
        })
      })
      loading.value = false
    })
  }
})()
</script>
src/views/officeProcessAutomation/SysAdmin/user-manage/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,550 @@
<!--OA模块:用户管理-->
<template>
    <div class="app-container">
        <el-row :gutter="20" style="height: calc(100vh - 8em)">
            <splitpanes :horizontal="appStore.device === 'mobile'" class="default-theme">
                <!--部门数据-->
                <pane size="16">
                    <el-col style="padding: 10px">
                        <div class="head-container">
                            <el-input v-model="deptNames" placeholder="请输入部门名称" clearable prefix-icon="Search" style="margin-bottom: 20px" />
                        </div>
                        <div class="head-container">
                            <el-tree :data="deptOptions" :props="{ label: 'label', children: 'children' }" :expand-on-click-node="false" :filter-node-method="filterNode" ref="deptTreeRef" node-key="id" highlight-current default-expand-all @node-click="handleNodeClick" />
                        </div>
                    </el-col>
                </pane>
                <!--用户数据-->
                <pane size="84">
                    <el-col style="padding: 10px; height: 100%; display: flex; flex-direction: column;">
                        <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
                            <el-form-item label="登录账号" prop="userName">
                                <el-input v-model="queryParams.userName" placeholder="请输入登录账号" clearable style="width: 240px" @keyup.enter="handleQuery" />
                            </el-form-item>
                            <el-form-item label="手机号码" prop="phonenumber">
                                <el-input v-model="queryParams.phonenumber" placeholder="请输入手机号码" clearable style="width: 240px" @keyup.enter="handleQuery" />
                            </el-form-item>
                            <el-form-item label="状态" prop="status">
                                <el-select v-model="queryParams.status" placeholder="用户状态" clearable style="width: 240px">
                                    <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
                                </el-select>
                            </el-form-item>
                            <el-form-item label="创建时间" style="width: 308px">
                                <el-date-picker v-model="dateRange" value-format="YYYY-MM-DD" type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"></el-date-picker>
                            </el-form-item>
                            <el-form-item>
                                <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
                                <el-button icon="Refresh" @click="resetQuery">重置</el-button>
                            </el-form-item>
                        </el-form>
                        <el-row :gutter="10" class="mb8">
                            <el-col :span="1.5">
                                <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:user:add']">新增</el-button>
                            </el-col>
                            <el-col :span="1.5">
                                <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate" v-hasPermi="['system:user:edit']">修改</el-button>
                            </el-col>
                            <el-col :span="1.5">
                                <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete" v-hasPermi="['system:user:remove']">删除</el-button>
                            </el-col>
                            <el-col :span="1.5">
                                <el-button type="info" plain icon="Upload" @click="handleImport" v-hasPermi="['system:user:import']">导入</el-button>
                            </el-col>
                            <el-col :span="1.5">
                                <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:user:export']">导出</el-button>
                            </el-col>
                            <right-toolbar v-model:showSearch="showSearch" @queryTable="getList" :columns="columns"></right-toolbar>
                        </el-row>
                        <div style="flex: 1; overflow: hidden;">
                            <el-table v-loading="loading" :data="userList" height="100%" @selection-change="handleSelectionChange">
                                <el-table-column type="selection" width="50" align="center" />
                                <el-table-column label="用户编号" align="center" key="userId" prop="userId" v-if="columns[0].visible" />
                                <el-table-column label="登录账号" align="center" key="userName" prop="userName" v-if="columns[1].visible" :show-overflow-tooltip="true" />
                                <el-table-column label="用户昵称" align="center" key="nickName" prop="nickName" v-if="columns[2].visible" :show-overflow-tooltip="true" />
                                <el-table-column label="部门" align="center" key="deptNames" prop="deptNames" v-if="columns[3].visible" :show-overflow-tooltip="true" />
                                <el-table-column label="手机号码" align="center" key="phonenumber" prop="phonenumber" v-if="columns[4].visible" width="120" />
                                <el-table-column label="状态" align="center" key="status" v-if="columns[5].visible">
                                    <template #default="scope">
                                        <el-switch
                                            v-model="scope.row.status"
                                            active-value="0"
                                            inactive-value="1"
                                            @change="handleStatusChange(scope.row)"
                                        ></el-switch>
                                    </template>
                                </el-table-column>
                                <el-table-column label="创建时间" align="center" prop="createTime" v-if="columns[6].visible" width="160">
                                    <template #default="scope">
                                        <span>{{ parseTime(scope.row.createTime) }}</span>
                                    </template>
                                </el-table-column>
                                <el-table-column label="操作" align="center" width="150" class-name="small-padding fixed-width">
                                    <template #default="scope">
                                        <el-tooltip content="修改" placement="top" v-if="scope.row.userId !== 1">
                                            <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:user:edit']"></el-button>
                                        </el-tooltip>
                                        <el-tooltip content="删除" placement="top" v-if="scope.row.userId !== 1">
                                            <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:user:remove']"></el-button>
                                        </el-tooltip>
                                        <el-tooltip content="重置密码" placement="top" v-if="scope.row.userId !== 1">
                                            <el-button link type="primary" icon="Key" @click="handleResetPwd(scope.row)" v-hasPermi="['system:user:resetPwd']"></el-button>
                                        </el-tooltip>
                                        <el-tooltip content="分配角色" placement="top" v-if="scope.row.userId !== 1">
                                            <el-button link type="primary" icon="CircleCheck" @click="handleAuthRole(scope.row)" v-hasPermi="['system:user:edit']"></el-button>
                                        </el-tooltip>
                                    </template>
                                </el-table-column>
                            </el-table>
                        </div>
                        <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
                    </el-col>
                </pane>
            </splitpanes>
        </el-row>
        <!-- æ·»åŠ æˆ–ä¿®æ”¹ç”¨æˆ·é…ç½®å¯¹è¯æ¡† -->
        <el-dialog :title="title" v-model="open" width="600px" append-to-body>
            <el-form :model="form" :rules="rules" ref="userRef" label-width="80px">
                <el-row>
                    <el-col :span="12">
                        <el-form-item v-if="form.userId == undefined" label="登录账号" prop="userName">
                            <el-input v-model="form.userName" placeholder="请输入用户名称" maxlength="30" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item v-if="form.userId == undefined" label="用户密码" prop="password">
                            <el-input v-model="form.password" placeholder="请输入用户密码" type="password" maxlength="20" show-password />
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row>
                    <el-col :span="12">
                        <el-form-item label="用户昵称" prop="nickName">
                            <el-input v-model="form.nickName" placeholder="请输入用户昵称" maxlength="30" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="归属部门" prop="deptId">
                            <el-tree-select v-model="form.deptId" :data="enabledDeptOptions" :props="{ value: 'id', label: 'label', children: 'children' }" value-key="id" placeholder="请选择归属部门" check-strictly />
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row>
                    <el-col :span="12">
                        <el-form-item label="岗位" prop="postIds">
                            <el-select v-model="form.postIds" multiple placeholder="请选择">
                                <el-option v-for="item in postOptions" :key="item.postId" :label="item.postName" :value="item.postId" :disabled="item.status == 1"></el-option>
                            </el-select>
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="角色" prop="roleIds">
                            <el-select v-model="form.roleIds" multiple placeholder="请选择">
                                <el-option v-for="item in roleOptions" :key="item.roleId" :label="item.roleName" :value="item.roleId" :disabled="item.status == 1"></el-option>
                            </el-select>
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row>
                    <el-col :span="12">
                        <el-form-item label="手机号码" prop="phonenumber">
                            <el-input v-model="form.phonenumber" placeholder="请输入手机号码" maxlength="11" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="邮箱" prop="email">
                            <el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row>
                    <el-col :span="12">
                        <el-form-item label="用户性别">
                            <el-select v-model="form.sex" placeholder="请选择">
                                <el-option v-for="dict in sys_user_sex" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
                            </el-select>
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="状态">
                            <el-radio-group v-model="form.status">
                                <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
                            </el-radio-group>
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row>
                    <el-col :span="24">
                        <el-form-item label="备注">
                            <el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
                        </el-form-item>
                    </el-col>
                </el-row>
            </el-form>
            <template #footer>
                <div class="dialog-footer">
                    <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
                    <el-button @click="cancel">取 æ¶ˆ</el-button>
                </div>
            </template>
        </el-dialog>
        <!-- ç”¨æˆ·å¯¼å…¥å¯¹è¯æ¡† -->
        <el-dialog :title="upload.title" v-model="upload.open" width="400px" append-to-body>
            <el-upload ref="uploadRef" :limit="1" accept=".xlsx, .xls" :headers="upload.headers" :action="upload.url + '?updateSupport=' + upload.updateSupport" :disabled="upload.isUploading" :on-progress="handleFileUploadProgress" :on-success="handleFileSuccess" :auto-upload="false" drag>
                <el-icon class="el-icon--upload"><upload-filled /></el-icon>
                <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
                <template #tip>
                    <div class="el-upload__tip text-center">
                        <div class="el-upload__tip">
                            <el-checkbox v-model="upload.updateSupport" />是否更新已经存在的用户数据
                        </div>
                        <span>仅允许导入xls、xlsx格式文件。</span>
                        <el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="importTemplate">下载模板</el-link>
                    </div>
                </template>
            </el-upload>
            <template #footer>
                <div class="dialog-footer">
                    <el-button type="primary" @click="submitFileForm">ç¡® å®š</el-button>
                    <el-button @click="upload.open = false">取 æ¶ˆ</el-button>
                </div>
            </template>
        </el-dialog>
    </div>
</template>
<script setup name="User">
import { getToken } from "@/utils/auth"
import useAppStore from '@/store/modules/app'
import { changeUserStatus, listUser, resetUserPwd, delUser, getUser, updateUser, addUser, deptTreeSelect } from "@/api/system/user"
import { Splitpanes, Pane } from "splitpanes"
import "splitpanes/dist/splitpanes.css"
const router = useRouter()
const appStore = useAppStore()
const { proxy } = getCurrentInstance()
const { sys_normal_disable, sys_user_sex } = proxy.useDict("sys_normal_disable", "sys_user_sex")
const userList = ref([])
const open = ref(false)
const loading = ref(true)
const showSearch = ref(true)
const ids = ref([])
const single = ref(true)
const multiple = ref(true)
const total = ref(0)
const title = ref("")
const dateRange = ref([])
const deptNames = ref("")
const deptOptions = ref(undefined)
const enabledDeptOptions = ref(undefined)
const initPassword = ref(undefined)
const postOptions = ref([])
const roleOptions = ref([])
/*** ç”¨æˆ·å¯¼å…¥å‚æ•° */
const upload = reactive({
    // æ˜¯å¦æ˜¾ç¤ºå¼¹å‡ºå±‚(用户导入)
    open: false,
    // å¼¹å‡ºå±‚标题(用户导入)
    title: "",
    // æ˜¯å¦ç¦ç”¨ä¸Šä¼ 
    isUploading: false,
    // æ˜¯å¦æ›´æ–°å·²ç»å­˜åœ¨çš„用户数据
    updateSupport: 0,
    // è®¾ç½®ä¸Šä¼ çš„请求头部
    headers: { Authorization: "Bearer " + getToken() },
    // ä¸Šä¼ çš„地址
    url: import.meta.env.VITE_APP_BASE_API + "/system/user/importData"
})
// åˆ—显隐信息
const columns = ref([
    { key: 0, label: `用户编号`, visible: true },
    { key: 1, label: `登录账号`, visible: true },
    { key: 2, label: `用户昵称`, visible: true },
    { key: 3, label: `部门`, visible: true },
    { key: 4, label: `手机号码`, visible: true },
    { key: 5, label: `状态`, visible: true },
    { key: 6, label: `创建时间`, visible: true }
])
const data = reactive({
    form: {},
    queryParams: {
        pageNum: 1,
        pageSize: 10,
        userName: undefined,
        phonenumber: undefined,
        status: undefined,
        deptId: undefined
    },
    rules: {
        userName: [{ required: true, message: "用户名称不能为空", trigger: "blur" }, { min: 2, max: 20, message: "用户名称长度必须介于 2 å’Œ 20 ä¹‹é—´", trigger: "blur" }],
        nickName: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }],
        password: [{ required: true, message: "用户密码不能为空", trigger: "blur" }, { min: 5, max: 20, message: "用户密码长度必须介于 5 å’Œ 20 ä¹‹é—´", trigger: "blur" }, { pattern: /^[^<>"'|\\]+$/, message: "不能包含非法字符:< > \" ' \\\ |", trigger: "blur" }],
        email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] }],
        phonenumber: [{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" }],
        deptId: [{ required: true, message: "归属部门不能为空", trigger: "change" }],
        postIds: [{ required: true, message: "岗位不能为空", trigger: "change" }],
        roleIds: [{ required: true, message: "角色不能为空", trigger: "change" }]
    }
})
const { queryParams, form, rules } = toRefs(data)
/** é€šè¿‡æ¡ä»¶è¿‡æ»¤èŠ‚ç‚¹  */
const filterNode = (value, data) => {
    if (!value) return true
    return data.label.indexOf(value) !== -1
}
/** æ ¹æ®åç§°ç­›é€‰éƒ¨é—¨æ ‘ */
watch(deptNames, val => {
    proxy.$refs["deptTreeRef"].filter(val)
})
/** æŸ¥è¯¢ç”¨æˆ·åˆ—表 */
function getList() {
    loading.value = true
    listUser(proxy.addDateRange(queryParams.value, dateRange.value)).then(res => {
        loading.value = false
        userList.value = res.rows
        total.value = res.total
    })
}
/** æŸ¥è¯¢éƒ¨é—¨ä¸‹æ‹‰æ ‘结构 */
function getDeptTree() {
    deptTreeSelect().then(response => {
        deptOptions.value = response.data
        enabledDeptOptions.value = filterDisabledDept(JSON.parse(JSON.stringify(response.data)))
    })
}
/** è¿‡æ»¤ç¦ç”¨çš„部门 */
function filterDisabledDept(deptList) {
    return deptList.filter(dept => {
        if (dept.disabled) {
            return false
        }
        if (dept.children && dept.children.length) {
            dept.children = filterDisabledDept(dept.children)
        }
        return true
    })
}
/** èŠ‚ç‚¹å•å‡»äº‹ä»¶ */
function handleNodeClick(data) {
    queryParams.value.deptId = data.id
    handleQuery()
}
/** æœç´¢æŒ‰é’®æ“ä½œ */
function handleQuery() {
    queryParams.value.pageNum = 1
    getList()
}
/** é‡ç½®æŒ‰é’®æ“ä½œ */
function resetQuery() {
    dateRange.value = []
    proxy.resetForm("queryRef")
    queryParams.value.deptId = undefined
    proxy.$refs.deptTreeRef.setCurrentKey(null)
    handleQuery()
}
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
function handleDelete(row) {
    const userIds = row.userId || ids.value
    proxy.$modal.confirm('是否确认删除用户编号为"' + userIds + '"的数据项?').then(function () {
        return delUser(userIds)
    }).then(() => {
        getList()
        proxy.$modal.msgSuccess("删除成功")
    }).catch(() => {})
}
/** å¯¼å‡ºæŒ‰é’®æ“ä½œ */
function handleExport() {
    proxy.download("system/user/export", {
        ...queryParams.value,
    },`user_${new Date().getTime()}.xlsx`)
}
/** ç”¨æˆ·çŠ¶æ€ä¿®æ”¹  */
function handleStatusChange(row) {
    let text = row.status === "0" ? "启用" : "停用"
    proxy.$modal.confirm('确认要"' + text + '""' + row.userName + '"用户吗?').then(function () {
        return changeUserStatus(row.userId, row.status)
    }).then(() => {
        proxy.$modal.msgSuccess(text + "成功")
    }).catch(function () {
        row.status = row.status === "0" ? "1" : "0"
    })
}
/** æ›´å¤šæ“ä½œ */
function handleCommand(command, row) {
    switch (command) {
        case "handleResetPwd":
            handleResetPwd(row)
            break
        case "handleAuthRole":
            handleAuthRole(row)
            break
        default:
            break
    }
}
/** è·³è½¬è§’色分配 */
function handleAuthRole(row) {
    const userId = row.userId
    router.push("/system/user-auth/role/" + userId)
}
/** é‡ç½®å¯†ç æŒ‰é’®æ“ä½œ */
function handleResetPwd(row) {
    proxy.$prompt('请输入"' + row.userName + '"的新密码', "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        closeOnClickModal: false,
        inputPattern: /^.{5,20}$/,
        inputErrorMessage: "用户密码长度必须介于 5 å’Œ 20 ä¹‹é—´",
        inputValidator: (value) => {
            if (/<|>|"|'|\||\\/.test(value)) {
                return "不能包含非法字符:< > \" ' \\\ |"
            }
        },
    }).then(({ value }) => {
        resetUserPwd(row.userId, value).then(response => {
            proxy.$modal.msgSuccess("修改成功,新密码是:" + value)
        })
    }).catch(() => {})
}
/** é€‰æ‹©æ¡æ•°  */
function handleSelectionChange(selection) {
    ids.value = selection.map(item => item.userId)
    single.value = selection.length != 1
    multiple.value = !selection.length
}
/** å¯¼å…¥æŒ‰é’®æ“ä½œ */
function handleImport() {
    upload.title = "用户导入"
    upload.open = true
}
/** ä¸‹è½½æ¨¡æ¿æ“ä½œ */
function importTemplate() {
    proxy.download("system/user/importTemplate", {
    }, `user_template_${new Date().getTime()}.xlsx`)
}
/**文件上传中处理 */
const handleFileUploadProgress = (event, file, fileList) => {
    upload.isUploading = true
}
/** æ–‡ä»¶ä¸Šä¼ æˆåŠŸå¤„ç† */
const handleFileSuccess = (response, file, fileList) => {
    upload.open = false
    upload.isUploading = false
    proxy.$refs["uploadRef"].handleRemove(file)
    proxy.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "导入结果", { dangerouslyUseHTMLString: true })
    getList()
}
/** æäº¤ä¸Šä¼ æ–‡ä»¶ */
function submitFileForm() {
    proxy.$refs["uploadRef"].submit()
}
/** é‡ç½®æ“ä½œè¡¨å• */
function reset() {
    form.value = {
        userId: undefined,
        deptId: undefined,
        userName: undefined,
        nickName: undefined,
        password: undefined,
        phonenumber: undefined,
        email: undefined,
        sex: undefined,
        status: "0",
        remark: undefined,
        postIds: [],
        roleIds: []
    }
    proxy.resetForm("userRef")
}
/** å–消按钮 */
function cancel() {
    open.value = false
    reset()
}
/** æ–°å¢žæŒ‰é’®æ“ä½œ */
function handleAdd() {
    reset()
    getUser().then(response => {
        postOptions.value = response.posts
        roleOptions.value = response.roles
        open.value = true
        title.value = "添加用户"
        form.value.password = initPassword.value
    })
}
/** ä¿®æ”¹æŒ‰é’®æ“ä½œ */
function handleUpdate(row) {
    reset()
    const userId = row.userId || ids.value
    getUser(userId).then(response => {
        form.value = response.data
        postOptions.value = response.posts
        roleOptions.value = response.roles
        form.value.postIds = response.postIds
        form.value.roleIds = response.roleIds
        open.value = true
        title.value = "修改用户"
        form.password = ""
    })
}
/** æäº¤æŒ‰é’® */
function submitForm() {
    proxy.$refs["userRef"].validate(valid => {
        if (valid) {
            // å½’属部门虽然是单选,但后端需要传数组字段 deptIds
            const payload = {
                ...form.value,
                deptIds: form.value.deptId ? [form.value.deptId] : []
            }
            if (form.value.userId != undefined) {
                updateUser(payload).then(response => {
                    proxy.$modal.msgSuccess("修改成功")
                    open.value = false
                    getList()
                })
            } else {
                addUser(payload).then(response => {
                    proxy.$modal.msgSuccess("新增成功")
                    open.value = false
                    getList()
                })
            }
        }
    })
}
getDeptTree()
getList()
</script>
src/views/officeProcessAutomation/SysAdmin/user-manage/profile/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,87 @@
<template>
   <div class="app-container">
      <el-row :gutter="20">
         <el-col :span="6" :xs="24">
            <el-card class="box-card">
               <template v-slot:header>
                 <div class="clearfix">
                   <span>个人信息</span>
                 </div>
               </template>
               <div>
                  <div class="text-center">
                     <userAvatar />
                  </div>
                  <ul class="list-group list-group-striped">
                     <li class="list-group-item">
                        <svg-icon icon-class="user" />用户名称
                        <div class="pull-right">{{ state.user.userName }}</div>
                     </li>
                     <li class="list-group-item">
                        <svg-icon icon-class="phone" />手机号码
                        <div class="pull-right">{{ state.user.phonenumber }}</div>
                     </li>
                     <li class="list-group-item">
                        <svg-icon icon-class="email" />用户邮箱
                        <div class="pull-right">{{ state.user.email }}</div>
                     </li>
                     <li class="list-group-item">
                        <svg-icon icon-class="tree" />所属部门
                        <div class="pull-right" v-if="state.user.dept">{{ state.user.dept.deptName }} / {{ state.postGroup }}</div>
                     </li>
                     <li class="list-group-item">
                        <svg-icon icon-class="peoples" />所属角色
                        <div class="pull-right">{{ state.roleGroup }}</div>
                     </li>
                     <li class="list-group-item">
                        <svg-icon icon-class="date" />创建日期
                        <div class="pull-right">{{ state.user.createTime }}</div>
                     </li>
                  </ul>
               </div>
            </el-card>
         </el-col>
         <el-col :span="18" :xs="24">
            <el-card>
               <template v-slot:header>
                 <div class="clearfix">
                   <span>基本资料</span>
                 </div>
               </template>
               <el-tabs v-model="activeTab">
                  <el-tab-pane label="基本资料" name="userinfo">
                     <userInfo :user="state.user" />
                  </el-tab-pane>
                  <el-tab-pane label="修改密码" name="resetPwd">
                     <resetPwd />
                  </el-tab-pane>
               </el-tabs>
            </el-card>
         </el-col>
      </el-row>
   </div>
</template>
<script setup name="Profile">
import userAvatar from "./userAvatar"
import userInfo from "./userInfo"
import resetPwd from "./resetPwd"
import { getUserProfile } from "@/api/system/user"
const activeTab = ref("userinfo")
const state = reactive({
  user: {},
  roleGroup: {},
  postGroup: {}
})
function getUser() {
  getUserProfile().then(response => {
    state.user = response.data
    state.roleGroup = response.roleGroup
    state.postGroup = response.postGroup
  })
}
getUser()
</script>
src/views/officeProcessAutomation/SysAdmin/user-manage/profile/resetPwd.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
<template>
   <el-form ref="pwdRef" :model="user" :rules="rules" label-width="80px">
      <el-form-item label="旧密码" prop="oldPassword">
         <el-input v-model="user.oldPassword" placeholder="请输入旧密码" type="password" show-password />
      </el-form-item>
      <el-form-item label="新密码" prop="newPassword">
         <el-input v-model="user.newPassword" placeholder="请输入新密码" type="password" show-password />
      </el-form-item>
      <el-form-item label="确认密码" prop="confirmPassword">
         <el-input v-model="user.confirmPassword" placeholder="请确认新密码" type="password" show-password/>
      </el-form-item>
      <el-form-item>
      <el-button type="primary" @click="submit">保存</el-button>
      <el-button type="danger" @click="close">关闭</el-button>
      </el-form-item>
   </el-form>
</template>
<script setup>
import { updateUserPwd } from "@/api/system/user"
const { proxy } = getCurrentInstance()
const user = reactive({
  oldPassword: undefined,
  newPassword: undefined,
  confirmPassword: undefined
})
const equalToPassword = (rule, value, callback) => {
  if (user.newPassword !== value) {
    callback(new Error("两次输入的密码不一致"))
  } else {
    callback()
  }
}
const rules = ref({
  oldPassword: [{ required: true, message: "旧密码不能为空", trigger: "blur" }],
  newPassword: [{ required: true, message: "新密码不能为空", trigger: "blur" }, { min: 6, max: 20, message: "长度在 6 åˆ° 20 ä¸ªå­—符", trigger: "blur" }, { pattern: /^[^<>"'|\\]+$/, message: "不能包含非法字符:< > \" ' \\\ |", trigger: "blur" }],
  confirmPassword: [{ required: true, message: "确认密码不能为空", trigger: "blur" }, { required: true, validator: equalToPassword, trigger: "blur" }]
})
/** æäº¤æŒ‰é’® */
function submit() {
  proxy.$refs.pwdRef.validate(valid => {
    if (valid) {
      updateUserPwd(user.oldPassword, user.newPassword).then(response => {
        proxy.$modal.msgSuccess("修改成功")
      })
    }
  })
}
/** å…³é—­æŒ‰é’® */
function close() {
  proxy.$tab.closePage()
}
</script>
src/views/officeProcessAutomation/SysAdmin/user-manage/profile/userAvatar.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,168 @@
<template>
  <div class="user-info-head" @click="editCropper()">
    <img :src="options.img" title="点击上传头像" class="img-circle img-lg" />
    <el-dialog :title="title" v-model="open" width="800px" append-to-body @opened="modalOpened" @close="closeDialog">
      <el-row>
        <el-col :xs="24" :md="12" :style="{ height: '350px' }">
          <vue-cropper ref="cropper" :img="options.img" :info="true" :autoCrop="options.autoCrop"
            :autoCropWidth="options.autoCropWidth" :autoCropHeight="options.autoCropHeight" :fixedBox="options.fixedBox"
            :outputType="options.outputType" @realTime="realTime" v-if="visible" />
        </el-col>
        <el-col :xs="24" :md="12" :style="{ height: '350px' }">
          <div class="avatar-upload-preview">
            <img :src="options.previews.url" :style="options.previews.img" />
          </div>
        </el-col>
      </el-row>
      <br />
      <el-row>
        <el-col :lg="2" :md="2">
          <el-upload action="#" :http-request="requestUpload" :show-file-list="false" :before-upload="beforeUpload">
            <el-button>
              é€‰æ‹©
              <el-icon class="el-icon--right">
                <Upload />
              </el-icon>
            </el-button>
          </el-upload>
        </el-col>
        <el-col :lg="{ span: 1, offset: 2 }" :md="2">
          <el-button icon="Plus" @click="changeScale(1)"></el-button>
        </el-col>
        <el-col :lg="{ span: 1, offset: 1 }" :md="2">
          <el-button icon="Minus" @click="changeScale(-1)"></el-button>
        </el-col>
        <el-col :lg="{ span: 1, offset: 1 }" :md="2">
          <el-button icon="RefreshLeft" @click="rotateLeft()"></el-button>
        </el-col>
        <el-col :lg="{ span: 1, offset: 1 }" :md="2">
          <el-button icon="RefreshRight" @click="rotateRight()"></el-button>
        </el-col>
        <el-col :lg="{ span: 2, offset: 6 }" :md="2">
          <el-button type="primary" @click="uploadImg()">提 äº¤</el-button>
        </el-col>
      </el-row>
    </el-dialog>
  </div>
</template>
<script setup>
import "vue-cropper/dist/index.css"
import { VueCropper } from "vue-cropper"
import { uploadAvatar } from "@/api/system/user"
import useUserStore from "@/store/modules/user"
const userStore = useUserStore()
const { proxy } = getCurrentInstance()
const open = ref(false)
const visible = ref(false)
const title = ref("修改头像")
//图片裁剪数据
const options = reactive({
  img: userStore.avatar,     // è£å‰ªå›¾ç‰‡çš„地址
  autoCrop: true,            // æ˜¯å¦é»˜è®¤ç”Ÿæˆæˆªå›¾æ¡†
  autoCropWidth: 200,        // é»˜è®¤ç”Ÿæˆæˆªå›¾æ¡†å®½åº¦
  autoCropHeight: 200,       // é»˜è®¤ç”Ÿæˆæˆªå›¾æ¡†é«˜åº¦
  fixedBox: true,            // å›ºå®šæˆªå›¾æ¡†å¤§å° ä¸å…è®¸æ”¹å˜
  outputType: "png",         // é»˜è®¤ç”Ÿæˆæˆªå›¾ä¸ºPNG格式
  filename: 'avatar',        // æ–‡ä»¶åç§°
  previews: {}               //预览数据
})
/** ç¼–辑头像 */
function editCropper() {
  open.value = true
}
/** æ‰“开弹出层结束时的回调 */
function modalOpened() {
  visible.value = true
}
/** è¦†ç›–默认上传行为 */
function requestUpload() { }
/** å‘左旋转 */
function rotateLeft() {
  proxy.$refs.cropper.rotateLeft()
}
/** å‘右旋转 */
function rotateRight() {
  proxy.$refs.cropper.rotateRight()
}
/** å›¾ç‰‡ç¼©æ”¾ */
function changeScale(num) {
  num = num || 1
  proxy.$refs.cropper.changeScale(num)
}
/** ä¸Šä¼ é¢„处理 */
function beforeUpload(file) {
  if (file.type.indexOf("image/") == -1) {
    proxy.$modal.msgError("文件格式错误,请上传图片类型,如:JPG,PNG后缀的文件。")
  } else {
    const reader = new FileReader()
    reader.readAsDataURL(file)
    reader.onload = () => {
      options.img = reader.result
      options.filename = file.name
    }
  }
}
/** ä¸Šä¼ å›¾ç‰‡ */
function uploadImg() {
  proxy.$refs.cropper.getCropBlob(data => {
    let formData = new FormData()
    formData.append("avatarfile", data, options.filename)
    uploadAvatar(formData).then(response => {
      open.value = false
      options.img = import.meta.env.VITE_APP_BASE_API + '/profile/' + response.imgUrl
      userStore.avatar = options.img
      proxy.$modal.msgSuccess("修改成功")
      visible.value = false
    })
  })
}
/** å®žæ—¶é¢„览 */
function realTime(data) {
  options.previews = data
}
/** å…³é—­çª—口 */
function closeDialog() {
  options.img = userStore.avatar
  options.visible = false
}
</script>
<style lang='scss' scoped>
.user-info-head {
  position: relative;
  display: inline-block;
  height: 120px;
}
.user-info-head:hover:after {
  content: "+";
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  color: #eee;
  background: rgba(0, 0, 0, 0.5);
  font-size: 24px;
  font-style: normal;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  cursor: pointer;
  line-height: 110px;
  border-radius: 50%;
}
</style>
src/views/officeProcessAutomation/SysAdmin/user-manage/profile/userInfo.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,67 @@
<template>
   <el-form ref="userRef" :model="form" :rules="rules" label-width="80px">
      <el-form-item label="用户昵称" prop="nickName">
         <el-input v-model="form.nickName" maxlength="30" />
      </el-form-item>
      <el-form-item label="手机号码" prop="phonenumber">
         <el-input v-model="form.phonenumber" maxlength="11" />
      </el-form-item>
      <el-form-item label="邮箱" prop="email">
         <el-input v-model="form.email" maxlength="50" />
      </el-form-item>
      <el-form-item label="性别">
         <el-radio-group v-model="form.sex">
            <el-radio value="0">男</el-radio>
            <el-radio value="1">女</el-radio>
         </el-radio-group>
      </el-form-item>
      <el-form-item>
      <el-button type="primary" @click="submit">保存</el-button>
      <el-button type="danger" @click="close">关闭</el-button>
      </el-form-item>
   </el-form>
</template>
<script setup>
import { updateUserProfile } from "@/api/system/user"
const props = defineProps({
  user: {
    type: Object
  }
})
const { proxy } = getCurrentInstance()
const form = ref({})
const rules = ref({
  nickName: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }],
  email: [{ required: true, message: "邮箱地址不能为空", trigger: "blur" }, { type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] }],
  phonenumber: [{ required: true, message: "手机号码不能为空", trigger: "blur" }, { pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" }],
})
/** æäº¤æŒ‰é’® */
function submit() {
  proxy.$refs.userRef.validate(valid => {
    if (valid) {
      updateUserProfile(form.value).then(response => {
        proxy.$modal.msgSuccess("修改成功")
        props.user.phonenumber = form.value.phonenumber
        props.user.email = form.value.email
      })
    }
  })
}
/** å…³é—­æŒ‰é’® */
function close() {
  proxy.$tab.closePage()
}
// å›žæ˜¾å½“前登录用户信息
watch(() => props.user, user => {
  if (user) {
    form.value = { nickName: user.nickName, phonenumber: user.phonenumber, email: user.email, sex: user.sex }
  }
},{ immediate: true })
</script>
src/views/officeProcessAutomation/SysMonitor/cache-monitor/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,134 @@
<!--OA模块:缓存监控-->
<template>
  <div class="app-container">
    <el-row :gutter="10">
      <el-col :span="24" class="card-box">
        <el-card>
          <template #header><Monitor style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">基本信息</span></template>
          <div class="el-table el-table--enable-row-hover el-table--medium">
            <table cellspacing="0" style="width: 100%">
              <tbody>
                <tr>
                  <td class="el-table__cell is-leaf"><div class="cell">Redis版本</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.redis_version }}</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell">运行模式</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.redis_mode == "standalone" ? "单机" : "集群" }}</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell">端口</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.tcp_port }}</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell">客户端数</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.connected_clients }}</div></td>
                </tr>
                <tr>
                  <td class="el-table__cell is-leaf"><div class="cell">运行时间(天)</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.uptime_in_days }}</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell">使用内存</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.used_memory_human }}</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell">使用CPU</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ parseFloat(cache.info.used_cpu_user_children).toFixed(2) }}</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell">内存配置</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.maxmemory_human }}</div></td>
                </tr>
                <tr>
                  <td class="el-table__cell is-leaf"><div class="cell">AOF是否开启</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.aof_enabled == "0" ? "否" : "是" }}</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell">RDB是否成功</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.rdb_last_bgsave_status }}</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell">Key数量</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.dbSize">{{ cache.dbSize }} </div></td>
                  <td class="el-table__cell is-leaf"><div class="cell">网络入口/出口</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.instantaneous_input_kbps }}kps/{{cache.info.instantaneous_output_kbps}}kps</div></td>
                </tr>
              </tbody>
            </table>
          </div>
        </el-card>
      </el-col>
      <el-col :span="12" class="card-box">
        <el-card>
          <template #header><PieChart style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">命令统计</span></template>
          <div class="el-table el-table--enable-row-hover el-table--medium">
            <div ref="commandstats" style="height: 420px" />
          </div>
        </el-card>
      </el-col>
      <el-col :span="12" class="card-box">
        <el-card>
          <template #header><Odometer style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">内存信息</span></template>
          <div class="el-table el-table--enable-row-hover el-table--medium">
            <div ref="usedmemory" style="height: 420px" />
          </div>
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>
<script setup name="Cache">
import { getCache } from '@/api/monitor/cache'
import * as echarts from 'echarts'
const cache = ref([])
const commandstats = ref(null)
const usedmemory = ref(null)
const { proxy } = getCurrentInstance()
function getList() {
  proxy.$modal.loading("正在加载缓存监控数据,请稍候!")
  getCache().then(response => {
    proxy.$modal.closeLoading()
    cache.value = response.data
    const commandstatsIntance = echarts.init(commandstats.value, "macarons")
    commandstatsIntance.setOption({
      tooltip: {
        trigger: "item",
        formatter: "{a} <br/>{b} : {c} ({d}%)"
      },
      series: [
        {
          name: "命令",
          type: "pie",
          roseType: "radius",
          radius: [15, 95],
          center: ["50%", "38%"],
          data: response.data.commandStats,
          animationEasing: "cubicInOut",
          animationDuration: 1000
        }
      ]
    })
    const usedmemoryInstance = echarts.init(usedmemory.value, "macarons")
    usedmemoryInstance.setOption({
      tooltip: {
        formatter: "{b} <br/>{a} : " + cache.value.info.used_memory_human
      },
      series: [
        {
          name: "峰值",
          type: "gauge",
          min: 0,
          max: 1000,
          detail: {
            formatter: cache.value.info.used_memory_human
          },
          data: [
            {
              value: parseFloat(cache.value.info.used_memory_human),
              name: "内存消耗"
            }
          ]
        }
      ]
    })
    window.addEventListener("resize", () => {
      commandstatsIntance.resize()
      usedmemoryInstance.resize()
    })
  })
}
getList()
</script>
src/views/officeProcessAutomation/SysMonitor/data-monitor/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
<!--OA模块:数据监控-->
<template>
  <div>
     <i-frame v-model:src="url"></i-frame>
  </div>
</template>
<script setup>
import iFrame from '@/components/iFrame'
import { ref } from 'vue'
const url = ref(import.meta.env.VITE_APP_BASE_API + '/druid/login.html')
</script>
src/views/officeProcessAutomation/SysMonitor/server-monitor/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,191 @@
<!--OA模块:服务器监控-->
<template>
  <div class="app-container">
    <el-row :gutter="10">
      <el-col :span="12" class="card-box">
        <el-card>
          <template #header><Cpu style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">CPU</span></template>
          <div class="el-table el-table--enable-row-hover el-table--medium">
            <table cellspacing="0" style="width: 100%;">
              <thead>
                <tr>
                  <th class="el-table__cell is-leaf"><div class="cell">属性</div></th>
                  <th class="el-table__cell is-leaf"><div class="cell">值</div></th>
                </tr>
              </thead>
              <tbody>
                <tr>
                  <td class="el-table__cell is-leaf"><div class="cell">核心数</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.cpu">{{ server.cpu.cpuNum }}</div></td>
                </tr>
                <tr>
                  <td class="el-table__cell is-leaf"><div class="cell">用户使用率</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.cpu">{{ server.cpu.used }}%</div></td>
                </tr>
                <tr>
                  <td class="el-table__cell is-leaf"><div class="cell">系统使用率</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.cpu">{{ server.cpu.sys }}%</div></td>
                </tr>
                <tr>
                  <td class="el-table__cell is-leaf"><div class="cell">当前空闲率</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.cpu">{{ server.cpu.free }}%</div></td>
                </tr>
              </tbody>
            </table>
          </div>
        </el-card>
      </el-col>
      <el-col :span="12" class="card-box">
        <el-card>
          <template #header><Tickets style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">内存</span></template>
          <div class="el-table el-table--enable-row-hover el-table--medium">
            <table cellspacing="0" style="width: 100%;">
              <thead>
                <tr>
                  <th class="el-table__cell is-leaf"><div class="cell">属性</div></th>
                  <th class="el-table__cell is-leaf"><div class="cell">内存</div></th>
                  <th class="el-table__cell is-leaf"><div class="cell">JVM</div></th>
                </tr>
              </thead>
              <tbody>
                <tr>
                  <td class="el-table__cell is-leaf"><div class="cell">总内存</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.mem">{{ server.mem.total }}G</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.total }}M</div></td>
                </tr>
                <tr>
                  <td class="el-table__cell is-leaf"><div class="cell">已用内存</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.mem">{{ server.mem.used}}G</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.used}}M</div></td>
                </tr>
                <tr>
                  <td class="el-table__cell is-leaf"><div class="cell">剩余内存</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.mem">{{ server.mem.free }}G</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.free }}M</div></td>
                </tr>
                <tr>
                  <td class="el-table__cell is-leaf"><div class="cell">使用率</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.mem" :class="{'text-danger': server.mem.usage > 80}">{{ server.mem.usage }}%</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm" :class="{'text-danger': server.jvm.usage > 80}">{{ server.jvm.usage }}%</div></td>
                </tr>
              </tbody>
            </table>
          </div>
        </el-card>
      </el-col>
      <el-col :span="24" class="card-box">
        <el-card>
          <template #header><Monitor style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">服务器信息</span></template>
          <div class="el-table el-table--enable-row-hover el-table--medium">
            <table cellspacing="0" style="width: 100%;">
              <tbody>
                <tr>
                  <td class="el-table__cell is-leaf"><div class="cell">服务器名称</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.sys">{{ server.sys.computerName }}</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell">操作系统</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.sys">{{ server.sys.osName }}</div></td>
                </tr>
                <tr>
                  <td class="el-table__cell is-leaf"><div class="cell">服务器IP</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.sys">{{ server.sys.computerIp }}</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell">系统架构</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.sys">{{ server.sys.osArch }}</div></td>
                </tr>
              </tbody>
            </table>
          </div>
        </el-card>
      </el-col>
      <el-col :span="24" class="card-box">
        <el-card>
          <template #header><CoffeeCup style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">Java虚拟机信息</span></template>
          <div class="el-table el-table--enable-row-hover el-table--medium">
            <table cellspacing="0" style="width: 100%;table-layout:fixed;">
              <tbody>
                <tr>
                  <td class="el-table__cell is-leaf"><div class="cell">Java名称</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.name }}</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell">Java版本</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.version }}</div></td>
                </tr>
                <tr>
                  <td class="el-table__cell is-leaf"><div class="cell">启动时间</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.startTime }}</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell">运行时长</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.runTime }}</div></td>
                </tr>
                <tr>
                  <td colspan="1" class="el-table__cell is-leaf"><div class="cell">安装路径</div></td>
                  <td colspan="3" class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.home }}</div></td>
                </tr>
                <tr>
                  <td colspan="1" class="el-table__cell is-leaf"><div class="cell">项目路径</div></td>
                  <td colspan="3" class="el-table__cell is-leaf"><div class="cell" v-if="server.sys">{{ server.sys.userDir }}</div></td>
                </tr>
                <tr>
                  <td colspan="1" class="el-table__cell is-leaf"><div class="cell">运行参数</div></td>
                  <td colspan="3" class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.inputArgs }}</div></td>
                </tr>
              </tbody>
            </table>
          </div>
        </el-card>
      </el-col>
      <el-col :span="24" class="card-box">
        <el-card>
          <template #header><MessageBox style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">磁盘状态</span></template>
          <div class="el-table el-table--enable-row-hover el-table--medium">
            <table cellspacing="0" style="width: 100%;">
              <thead>
                <tr>
                  <th class="el-table__cell el-table__cell is-leaf"><div class="cell">盘符路径</div></th>
                  <th class="el-table__cell is-leaf"><div class="cell">文件系统</div></th>
                  <th class="el-table__cell is-leaf"><div class="cell">盘符类型</div></th>
                  <th class="el-table__cell is-leaf"><div class="cell">总大小</div></th>
                  <th class="el-table__cell is-leaf"><div class="cell">可用大小</div></th>
                  <th class="el-table__cell is-leaf"><div class="cell">已用大小</div></th>
                  <th class="el-table__cell is-leaf"><div class="cell">已用百分比</div></th>
                </tr>
              </thead>
              <tbody v-if="server.sysFiles">
                <tr v-for="(sysFile, index) in server.sysFiles" :key="index">
                  <td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.dirName }}</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.sysTypeName }}</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.typeName }}</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.total }}</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.free }}</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.used }}</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" :class="{'text-danger': sysFile.usage > 80}">{{ sysFile.usage }}%</div></td>
                </tr>
              </tbody>
            </table>
          </div>
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>
<script setup>
import { getServer } from '@/api/monitor/server'
import {onMounted} from "vue";
const server = ref([])
const { proxy } = getCurrentInstance()
function getList() {
  proxy.$modal.loading("正在加载服务监控数据,请稍候!")
  getServer().then(response => {
    server.value = response.data
    proxy.$modal.closeLoading()
  })
}
onMounted(() => {
    getList();
});
</script>