From 0fc574668fcd7d262d14f7a9bb9c76c6f466b9f6 Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期五, 15 五月 2026 16:14:08 +0800
Subject: [PATCH] OA菜单模块
---
src/views/officeProcessAutomation/HrManage/staff-archive/components/JobInfoSection.vue | 176 +
src/views/officeProcessAutomation/HrManage/staff-archive/components/RenewContract.vue | 141
src/views/officeProcessAutomation/HrManage/resign-apply/components/formDia.vue | 347 ++
src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue | 916 +++++
src/views/officeProcessAutomation/HrManage/work-handover/index.vue | 810 +++++
src/views/officeProcessAutomation/HrManage/regular-apply/index.vue | 676 ++++
src/views/officeProcessAutomation/SysMonitor/cache-monitor/index.vue | 134
src/views/officeProcessAutomation/ContractManage/purchase-contract/index.vue | 12
src/views/officeProcessAutomation/HrManage/staff-contract/index.vue | 314 ++
src/views/officeProcessAutomation/HrManage/transfer-apply/index.vue | 792 +++++
src/views/officeProcessAutomation/HrManage/staff-archive/components/Show.vue | 73
src/views/officeProcessAutomation/SysAdmin/log-manage/index.vue | 315 ++
src/views/officeProcessAutomation/HrManage/staff-contract/components/formDia.vue | 96
src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue | 12
src/views/officeProcessAutomation/HrManage/staff-archive/components/EmergencyAndAttachmentSection.vue | 115
src/views/officeProcessAutomation/HrManage/staff-archive/components/NewOrEditFormDia.vue | 304 +
src/views/officeProcessAutomation/HrManage/staff-archive/components/EducationWorkSection.vue | 263 +
src/views/officeProcessAutomation/HrManage/staff-archive/index.vue | 407 ++
src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue | 12
src/views/officeProcessAutomation/HrManage/staff-archive/components/BasicInfoSection.vue | 181 +
src/views/officeProcessAutomation/HrManage/resign-apply/index.vue | 245 +
src/views/officeProcessAutomation/HrManage/staff-contract/filesDia.vue | 197 +
src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue | 12
src/views/officeProcessAutomation/SysAdmin/user-manage/profile/resetPwd.vue | 59
src/views/officeProcessAutomation/SysMonitor/server-monitor/index.vue | 191 +
src/views/officeProcessAutomation/SysAdmin/user-manage/profile/index.vue | 87
src/views/officeProcessAutomation/ContractManage/sale-contract/index.vue | 12
src/views/officeProcessAutomation/AttendManage/leave-apply/index.vue | 934 +++++
src/views/officeProcessAutomation/HrManage/post-manage/index.vue | 292 +
src/views/officeProcessAutomation/SysAdmin/user-manage/index.vue | 550 +++
src/views/officeProcessAutomation/SysAdmin/user-manage/profile/userAvatar.vue | 168 +
src/views/officeProcessAutomation/SysAdmin/user-manage/authRole.vue | 123
src/views/officeProcessAutomation/SysMonitor/data-monitor/index.vue | 14
src/views/officeProcessAutomation/SysAdmin/user-manage/profile/userInfo.vue | 67
src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue | 12
src/views/officeProcessAutomation/SysAdmin/dept-manage/index.vue | 291 +
36 files changed, 9,350 insertions(+), 0 deletions(-)
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue b/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
new file mode 100644
index 0000000..d4ff149
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
@@ -0,0 +1,12 @@
+<!--
+ 妯″潡涓枃鍚嶏細瀹℃壒鍒楄〃
+ 鐩綍鏍囪瘑锛欰pproveManage/approve-list锛坅pprove-list 鈫� 涓枃锛氬鎵瑰垪琛級
+ 澶嶇敤椤甸潰锛欯/views/procurementManagement/procurementLedger/index.vue锛堥噰璐彴璐︼紱鏂囦欢鍚� index.vue 鈫� 鍏ュ彛椤碉級
+-->
+<template>
+ <ProcurementLedger />
+</template>
+
+<script setup>
+import ProcurementLedger from '@/views/procurementManagement/procurementLedger/index.vue'
+</script>
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue b/src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue
new file mode 100644
index 0000000..f88c88f
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue
@@ -0,0 +1,12 @@
+<!--
+ 妯″潡涓枃鍚嶏細瀹℃壒妯℃澘
+ 鐩綍鏍囪瘑锛欰pproveManage/approve-template锛坅pprove-template 鈫� 涓枃锛氬鎵规ā鏉匡級
+ 澶嶇敤椤甸潰锛欯/views/procurementManagement/procurementLedger/index.vue锛堥噰璐彴璐︼紱鏂囦欢鍚� index.vue 鈫� 鍏ュ彛椤碉級
+-->
+<template>
+ <ProcurementLedger />
+</template>
+
+<script setup>
+import ProcurementLedger from '@/views/procurementManagement/procurementLedger/index.vue'
+</script>
diff --git a/src/views/officeProcessAutomation/AttendManage/leave-apply/index.vue b/src/views/officeProcessAutomation/AttendManage/leave-apply/index.vue
new file mode 100644
index 0000000..f52d57d
--- /dev/null
+++ b/src/views/officeProcessAutomation/AttendManage/leave-apply/index.vue
@@ -0,0 +1,934 @@
+<!--OA妯″潡锛氳鍋囩敵璇凤紙瀛楁涓哄墠绔崰浣嶏紝鍚庢湡涓庡悗绔帴鍙e榻愶級-->
+<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";
+
+/** 璇峰亣绫诲瀷锛坴alue 涓庡悗绔榻愬崰浣嶏級 */
+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>
diff --git a/src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue b/src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue
new file mode 100644
index 0000000..477bc06
--- /dev/null
+++ b/src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue
@@ -0,0 +1,916 @@
+<!--OA妯″潡锛氬姞鐝敵璇凤紙瀛楁涓哄墠绔崰浣嶏紝鍚庢湡涓庡悗绔帴鍙e榻愶級-->
+<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";
+
+/** 鍔犵彮绫诲瀷锛坴alue 涓庡悗绔榻愬崰浣嶏級 */
+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?.("瑙f瀽澶辫触锛岃浣跨敤瀵煎嚭鏂囦欢鎴栫害瀹� 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>
diff --git a/src/views/officeProcessAutomation/ContractManage/purchase-contract/index.vue b/src/views/officeProcessAutomation/ContractManage/purchase-contract/index.vue
new file mode 100644
index 0000000..d6d9ef4
--- /dev/null
+++ b/src/views/officeProcessAutomation/ContractManage/purchase-contract/index.vue
@@ -0,0 +1,12 @@
+<!--
+ 妯″潡涓枃鍚嶏細閲囪喘鍚堝悓
+ 鐩綍鏍囪瘑锛欳ontractManage/purchase-contract锛坧urchase-contract 鈫� 涓枃锛氶噰璐悎鍚岋級
+ 澶嶇敤椤甸潰锛欯/views/procurementManagement/procurementLedger/index.vue锛堥噰璐彴璐︼紱鏂囦欢鍚� index.vue 鈫� 鍏ュ彛椤碉級
+-->
+<template>
+ <ProcurementLedger />
+</template>
+
+<script setup>
+import ProcurementLedger from '@/views/procurementManagement/procurementLedger/index.vue'
+</script>
diff --git a/src/views/officeProcessAutomation/ContractManage/sale-contract/index.vue b/src/views/officeProcessAutomation/ContractManage/sale-contract/index.vue
new file mode 100644
index 0000000..6be106a
--- /dev/null
+++ b/src/views/officeProcessAutomation/ContractManage/sale-contract/index.vue
@@ -0,0 +1,12 @@
+<!--
+ 妯″潡涓枃鍚嶏細閿�鍞悎鍚�
+ 鐩綍鏍囪瘑锛欳ontractManage/sale-contract锛坰ale-contract 鈫� 涓枃锛氶攢鍞悎鍚岋級
+ 澶嶇敤椤甸潰锛欯/views/procurementManagement/procurementLedger/index.vue锛堥噰璐彴璐︼紱鏂囦欢鍚� index.vue 鈫� 鍏ュ彛椤碉級
+-->
+<template>
+ <ProcurementLedger />
+</template>
+
+<script setup>
+import ProcurementLedger from '@/views/procurementManagement/procurementLedger/index.vue'
+</script>
diff --git a/src/views/officeProcessAutomation/HrManage/post-manage/index.vue b/src/views/officeProcessAutomation/HrManage/post-manage/index.vue
new file mode 100644
index 0000000..a57137c
--- /dev/null
+++ b/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>
+
diff --git a/src/views/officeProcessAutomation/HrManage/regular-apply/index.vue b/src/views/officeProcessAutomation/HrManage/regular-apply/index.vue
new file mode 100644
index 0000000..b95b6e7
--- /dev/null
+++ b/src/views/officeProcessAutomation/HrManage/regular-apply/index.vue
@@ -0,0 +1,676 @@
+<!--OA妯″潡锛氳浆姝g敵璇�-->
+<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 鎷︽埅鍣ㄥ凡瑙e寘涓� { 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>
diff --git a/src/views/officeProcessAutomation/HrManage/resign-apply/components/formDia.vue b/src/views/officeProcessAutomation/HrManage/resign-apply/components/formDia.vue
new file mode 100644
index 0000000..86c59ce
--- /dev/null
+++ b/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>
diff --git a/src/views/officeProcessAutomation/HrManage/resign-apply/index.vue b/src/views/officeProcessAutomation/HrManage/resign-apply/index.vue
new file mode 100644
index 0000000..5ef0cf9
--- /dev/null
+++ b/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>
+
diff --git a/src/views/officeProcessAutomation/HrManage/staff-archive/components/BasicInfoSection.vue b/src/views/officeProcessAutomation/HrManage/staff-archive/components/BasicInfoSection.vue
new file mode 100644
index 0000000..0aa4f06
--- /dev/null
+++ b/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>
+
diff --git a/src/views/officeProcessAutomation/HrManage/staff-archive/components/EducationWorkSection.vue b/src/views/officeProcessAutomation/HrManage/staff-archive/components/EducationWorkSection.vue
new file mode 100644
index 0000000..c1470e7
--- /dev/null
+++ b/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>
+
diff --git a/src/views/officeProcessAutomation/HrManage/staff-archive/components/EmergencyAndAttachmentSection.vue b/src/views/officeProcessAutomation/HrManage/staff-archive/components/EmergencyAndAttachmentSection.vue
new file mode 100644
index 0000000..bd63608
--- /dev/null
+++ b/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>
+
diff --git a/src/views/officeProcessAutomation/HrManage/staff-archive/components/JobInfoSection.vue b/src/views/officeProcessAutomation/HrManage/staff-archive/components/JobInfoSection.vue
new file mode 100644
index 0000000..be33436
--- /dev/null
+++ b/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>
+
diff --git a/src/views/officeProcessAutomation/HrManage/staff-archive/components/NewOrEditFormDia.vue b/src/views/officeProcessAutomation/HrManage/staff-archive/components/NewOrEditFormDia.vue
new file mode 100644
index 0000000..2ad06fb
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/officeProcessAutomation/HrManage/staff-archive/components/RenewContract.vue b/src/views/officeProcessAutomation/HrManage/staff-archive/components/RenewContract.vue
new file mode 100644
index 0000000..9c2acfc
--- /dev/null
+++ b/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>
diff --git a/src/views/officeProcessAutomation/HrManage/staff-archive/components/Show.vue b/src/views/officeProcessAutomation/HrManage/staff-archive/components/Show.vue
new file mode 100644
index 0000000..9220d45
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/officeProcessAutomation/HrManage/staff-archive/index.vue b/src/views/officeProcessAutomation/HrManage/staff-archive/index.vue
new file mode 100644
index 0000000..c0d8b2b
--- /dev/null
+++ b/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>浠呭厑璁稿鍏ls銆亁lsx鏍煎紡鏂囦欢銆�</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>
+
diff --git a/src/views/officeProcessAutomation/HrManage/staff-contract/components/formDia.vue b/src/views/officeProcessAutomation/HrManage/staff-contract/components/formDia.vue
new file mode 100644
index 0000000..54b2ef9
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/officeProcessAutomation/HrManage/staff-contract/filesDia.vue b/src/views/officeProcessAutomation/HrManage/staff-contract/filesDia.vue
new file mode 100644
index 0000000..02f9cef
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/officeProcessAutomation/HrManage/staff-contract/index.vue b/src/views/officeProcessAutomation/HrManage/staff-contract/index.vue
new file mode 100644
index 0000000..1125445
--- /dev/null
+++ b/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>浠呭厑璁稿鍏ls銆亁lsx鏍煎紡鏂囦欢銆�</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>
+
diff --git a/src/views/officeProcessAutomation/HrManage/transfer-apply/index.vue b/src/views/officeProcessAutomation/HrManage/transfer-apply/index.vue
new file mode 100644
index 0000000..6b72316
--- /dev/null
+++ b/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();
+
+/** 涓庡悗绔害瀹氬瓧娈碉紙鏈湴鍗犱綅锛屽悗鏈熸帴鍙e榻愶級 */
+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锛屼笌鍛樺伐妗f鍏ヨ亴琛ㄥ崟涓�鑷达級 */
+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>
diff --git a/src/views/officeProcessAutomation/HrManage/work-handover/index.vue b/src/views/officeProcessAutomation/HrManage/work-handover/index.vue
new file mode 100644
index 0000000..2e05b85
--- /dev/null
+++ b/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 || "鈥�";
+}
+
+/** 涓庡悗绔害瀹氬瓧娈碉紙鏈湴鍗犱綅锛屽悗鏈熸帴鍙e榻愶級 */
+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>
diff --git a/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue b/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue
new file mode 100644
index 0000000..2ee0a60
--- /dev/null
+++ b/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue
@@ -0,0 +1,12 @@
+<!--
+ 妯″潡涓枃鍚嶏細璐圭敤鎶ラ攢
+ 鐩綍鏍囪瘑锛歊eimburseManage/cost-reimburse锛坈ost-reimburse 鈫� 涓枃锛氳垂鐢ㄦ姤閿�锛�
+ 澶嶇敤椤甸潰锛欯/views/procurementManagement/procurementLedger/index.vue锛堥噰璐彴璐︼紱鏂囦欢鍚� index.vue 鈫� 鍏ュ彛椤碉級
+-->
+<template>
+ <ProcurementLedger />
+</template>
+
+<script setup>
+import ProcurementLedger from '@/views/procurementManagement/procurementLedger/index.vue'
+</script>
diff --git a/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue
new file mode 100644
index 0000000..b78eb7b
--- /dev/null
+++ b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue
@@ -0,0 +1,12 @@
+<!--
+ 妯″潡涓枃鍚嶏細宸梾鎶ラ攢
+ 鐩綍鏍囪瘑锛歊eimburseManage/travel-reimburse锛坱ravel-reimburse 鈫� 涓枃锛氬樊鏃呮姤閿�锛�
+ 澶嶇敤椤甸潰锛欯/views/procurementManagement/procurementLedger/index.vue锛堥噰璐彴璐︼紱鏂囦欢鍚� index.vue 鈫� 鍏ュ彛椤碉級
+-->
+<template>
+ <ProcurementLedger />
+</template>
+
+<script setup>
+import ProcurementLedger from '@/views/procurementManagement/procurementLedger/index.vue'
+</script>
diff --git a/src/views/officeProcessAutomation/SysAdmin/dept-manage/index.vue b/src/views/officeProcessAutomation/SysAdmin/dept-manage/index.vue
new file mode 100644
index 0000000..9dd4e90
--- /dev/null
+++ b/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="璇疯緭鍏ヨ礋璐d汉" 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>
+
diff --git a/src/views/officeProcessAutomation/SysAdmin/log-manage/index.vue b/src/views/officeProcessAutomation/SysAdmin/log-manage/index.vue
new file mode 100644
index 0000000..2701c1a
--- /dev/null
+++ b/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">姝e父</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>
+
diff --git a/src/views/officeProcessAutomation/SysAdmin/user-manage/authRole.vue b/src/views/officeProcessAutomation/SysAdmin/user-manage/authRole.vue
new file mode 100644
index 0000000..a7546aa
--- /dev/null
+++ b/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>
diff --git a/src/views/officeProcessAutomation/SysAdmin/user-manage/index.vue b/src/views/officeProcessAutomation/SysAdmin/user-manage/index.vue
new file mode 100644
index 0000000..97a06b1
--- /dev/null
+++ b/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>浠呭厑璁稿鍏ls銆亁lsx鏍煎紡鏂囦欢銆�</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>
+
diff --git a/src/views/officeProcessAutomation/SysAdmin/user-manage/profile/index.vue b/src/views/officeProcessAutomation/SysAdmin/user-manage/profile/index.vue
new file mode 100644
index 0000000..719a028
--- /dev/null
+++ b/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>
diff --git a/src/views/officeProcessAutomation/SysAdmin/user-manage/profile/resetPwd.vue b/src/views/officeProcessAutomation/SysAdmin/user-manage/profile/resetPwd.vue
new file mode 100644
index 0000000..73c6b18
--- /dev/null
+++ b/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>
diff --git a/src/views/officeProcessAutomation/SysAdmin/user-manage/profile/userAvatar.vue b/src/views/officeProcessAutomation/SysAdmin/user-manage/profile/userAvatar.vue
new file mode 100644
index 0000000..2594543
--- /dev/null
+++ b/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", // 榛樿鐢熸垚鎴浘涓篜NG鏍煎紡
+ 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锛孭NG鍚庣紑鐨勬枃浠躲��")
+ } 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>
\ No newline at end of file
diff --git a/src/views/officeProcessAutomation/SysAdmin/user-manage/profile/userInfo.vue b/src/views/officeProcessAutomation/SysAdmin/user-manage/profile/userInfo.vue
new file mode 100644
index 0000000..5099ffa
--- /dev/null
+++ b/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>
diff --git a/src/views/officeProcessAutomation/SysMonitor/cache-monitor/index.vue b/src/views/officeProcessAutomation/SysMonitor/cache-monitor/index.vue
new file mode 100644
index 0000000..d5a90b0
--- /dev/null
+++ b/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("姝e湪鍔犺浇缂撳瓨鐩戞帶鏁版嵁锛岃绋嶅�欙紒")
+ 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>
+
diff --git a/src/views/officeProcessAutomation/SysMonitor/data-monitor/index.vue b/src/views/officeProcessAutomation/SysMonitor/data-monitor/index.vue
new file mode 100644
index 0000000..fe13414
--- /dev/null
+++ b/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>
diff --git a/src/views/officeProcessAutomation/SysMonitor/server-monitor/index.vue b/src/views/officeProcessAutomation/SysMonitor/server-monitor/index.vue
new file mode 100644
index 0000000..053d55e
--- /dev/null
+++ b/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">鏈嶅姟鍣↖P</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("姝e湪鍔犺浇鏈嶅姟鐩戞帶鏁版嵁锛岃绋嶅�欙紒")
+ getServer().then(response => {
+ server.value = response.data
+ proxy.$modal.closeLoading()
+ })
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
--
Gitblit v1.9.3