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