From b44addf784a1638bac1102799ed415645a373a55 Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期四, 21 五月 2026 14:29:30 +0800
Subject: [PATCH] 转正申请/调岗申请/工作交接/请假申请/加班申请页面画页面,接口联调和web端保持一致 日报
---
src/pages/oa/_utils/approvalTemplateType.js | 153 +
src/pages/oa/AttendManage/overtime-apply/index.vue | 12
src/config/oaPaths.js | 2
src/pages.json | 14
src/pages/oa/ApproveManage/approve-list/_components/ApproveInstanceDetailBody.vue | 415 ++++++
src/pages/oa/ApproveManage/approve-list/index.vue | 295 +--
src/pages/oa/_utils/approvalModuleListSearch.js | 319 +++++
src/pages/oa/ApproveManage/approve-list/template-select.vue | 146 +
src/pages/oa/_components/ApprovalInstanceListPage.vue | 347 +++++
src/pages/oa/AttendManage/leave-apply/index.vue | 12
src/pages/oa/_styles/oa-approval-list.scss | 409 ++++++
src/pages/oa/_utils/approvalModuleRegistry.js | 137 ++
src/pages/oa/_utils/approveListUtils.js | 327 +++++
src/pages/oa/_utils/approvalModuleApplyExtras.js | 293 ++++
src/pages/oa/HrManage/work-handover/index.vue | 12
src/api/system/post.js | 10
src/pages/oa/HrManage/transfer-apply/index.vue | 12
src/config/oaWorkbench.js | 2
src/pages/oa/ApproveManage/approve-list/approve.vue | 164 ++
src/pages/oa/_components/ApprovalModuleSearchPopup.vue | 268 ++++
src/api/oa/approvalInstance.js | 21
src/pages/oa/ApproveManage/approve-list/apply.vue | 250 +++
src/pages/oa/ApproveManage/approve-list/detail.vue | 116 +
src/pages/oa/HrManage/regular-apply/index.vue | 12
24 files changed, 3,441 insertions(+), 307 deletions(-)
diff --git a/src/api/oa/approvalInstance.js b/src/api/oa/approvalInstance.js
index 604f437..fa60c77 100644
--- a/src/api/oa/approvalInstance.js
+++ b/src/api/oa/approvalInstance.js
@@ -29,3 +29,24 @@
data: { approvalInstanceDto },
});
}
+
+/** 瀹℃壒锛堥�氳繃/椹冲洖锛塒OST /approvalInstance/approve */
+export function approveApprovalInstance(approvalInstanceDto) {
+ return request({
+ url: "/approvalInstance/approve",
+ method: "post",
+ data: { approvalInstanceDto },
+ });
+}
+
+/** 鍒犻櫎瀹℃壒瀹炰緥 DELETE /approvalInstance/delete */
+export function deleteApprovalInstance(ids) {
+ const idList = (Array.isArray(ids) ? ids : [ids]).filter(
+ id => id != null && id !== ""
+ );
+ return request({
+ url: "/approvalInstance/delete",
+ method: "delete",
+ data: idList,
+ });
+}
diff --git a/src/api/system/post.js b/src/api/system/post.js
new file mode 100644
index 0000000..c3f70c2
--- /dev/null
+++ b/src/api/system/post.js
@@ -0,0 +1,10 @@
+import request from "@/utils/request";
+
+/** 宀椾綅涓嬫媺 GET /system/post/optionselect */
+export function findPostOptions(query) {
+ return request({
+ url: "/system/post/optionselect",
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/config/oaPaths.js b/src/config/oaPaths.js
index 73cca36..2124c3f 100644
--- a/src/config/oaPaths.js
+++ b/src/config/oaPaths.js
@@ -26,6 +26,8 @@
approveList: `/${P}/ApproveManage/approve-list/index`,
approveListTemplateSelect: `/${P}/ApproveManage/approve-list/template-select`,
approveListApply: `/${P}/ApproveManage/approve-list/apply`,
+ approveListDetail: `/${P}/ApproveManage/approve-list/detail`,
+ approveListApprove: `/${P}/ApproveManage/approve-list/approve`,
approveTemplate: `/${P}/ApproveManage/approve-template/index`,
approveTemplateEdit: `/${P}/ApproveManage/approve-template/edit`,
approveTemplateDetail: `/${P}/ApproveManage/approve-template/detail`,
diff --git a/src/config/oaWorkbench.js b/src/config/oaWorkbench.js
index c1263d2..e4f6d7a 100644
--- a/src/config/oaWorkbench.js
+++ b/src/config/oaWorkbench.js
@@ -12,7 +12,7 @@
// { label: "鍛樺伐鍚堝悓", icon: "/static/images/icon/hetongguanli.svg", path: OA_NAV.staffContract },
{ label: "杞鐢宠", icon: "/static/images/icon/hetongguanli.svg", path: OA_NAV.regularApply },
{ label: "璋冨矖鐢宠", icon: "/static/images/icon/renyuanxinzi.svg", path: OA_NAV.transferApply },
- { label: "绂昏亴鐢宠", icon: "/static/images/icon/qingjiaguanli.svg", path: OA_NAV.resignApply },
+ // { label: "绂昏亴鐢宠", icon: "/static/images/icon/qingjiaguanli.svg", path: OA_NAV.resignApply },
{ label: "宸ヤ綔浜ゆ帴", icon: "/static/images/icon/gongchuguanli.svg", path: OA_NAV.workHandover },
// { label: "宀椾綅绠$悊", icon: "/static/images/icon/gongxuguanli.svg", path: OA_NAV.postManage },
],
diff --git a/src/pages.json b/src/pages.json
index e39fb89..607d08d 100644
--- a/src/pages.json
+++ b/src/pages.json
@@ -1418,6 +1418,20 @@
}
},
{
+ "path": "pages/oa/ApproveManage/approve-list/detail",
+ "style": {
+ "navigationBarTitleText": "瀹℃壒璇︽儏",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/oa/ApproveManage/approve-list/approve",
+ "style": {
+ "navigationBarTitleText": "瀹℃壒澶勭悊",
+ "navigationStyle": "custom"
+ }
+ },
+ {
"path": "pages/oa/ApproveManage/approve-template/index",
"style": {
"navigationBarTitleText": "瀹℃壒妯℃澘",
diff --git a/src/pages/oa/ApproveManage/approve-list/_components/ApproveInstanceDetailBody.vue b/src/pages/oa/ApproveManage/approve-list/_components/ApproveInstanceDetailBody.vue
new file mode 100644
index 0000000..da62d09
--- /dev/null
+++ b/src/pages/oa/ApproveManage/approve-list/_components/ApproveInstanceDetailBody.vue
@@ -0,0 +1,415 @@
+<!--
+ 瀹℃壒瀹炰緥璇︽儏灞曠ず锛氬熀鏈俊鎭� + 濉姤 + 娴佺▼ + 瀹℃壒璁板綍
+-->
+<template>
+ <view class="detail-body">
+ <view class="section-card">
+ <view class="section-head">
+ <text class="section-title">鍩烘湰淇℃伅</text>
+ </view>
+ <view class="info-rows">
+ <view class="info-row">
+ <text class="info-label">涓氬姟鍗曞彿</text>
+ <text class="info-value">{{ row.instanceNo || row.id || "鈥�" }}</text>
+ </view>
+ <view class="info-row">
+ <text class="info-label">瀹℃壒鐘舵��</text>
+ <u-tag :type="statusTagType(row.status)"
+ :text="statusLabel(row.status)"
+ size="mini" />
+ </view>
+ <view class="info-row">
+ <text class="info-label">妯℃澘鍚嶇О</text>
+ <text class="info-value">{{ row.templateName || "鈥�" }}</text>
+ </view>
+ <view class="info-row">
+ <text class="info-label">涓氬姟鍚嶇О</text>
+ <text class="info-value">{{ row.businessName || "鈥�" }}</text>
+ </view>
+ <view class="info-row">
+ <text class="info-label">鐢宠浜�</text>
+ <text class="info-value">{{ row.applicantName || "鈥�" }}</text>
+ </view>
+ <view class="info-row">
+ <text class="info-label">鐢宠鏍囬</text>
+ <text class="info-value">{{ row.title || "鈥�" }}</text>
+ </view>
+ <view v-if="rejectReason"
+ class="info-row">
+ <text class="info-label">椹冲洖鍘熷洜</text>
+ <text class="info-value reject-text">{{ rejectReason }}</text>
+ </view>
+ <view class="info-row">
+ <text class="info-label">鐢宠鏃堕棿</text>
+ <text class="info-value">{{ formatDateTime(row.applyTime || row.createTime) }}</text>
+ </view>
+ <view v-if="row.finishTime"
+ class="info-row">
+ <text class="info-label">瀹屾垚鏃堕棿</text>
+ <text class="info-value">{{ formatDateTime(row.finishTime) }}</text>
+ </view>
+ </view>
+ </view>
+
+ <view class="section-card">
+ <view class="section-head">
+ <text class="section-title">濉姤鍐呭</text>
+ </view>
+ <view v-if="displayFields.length"
+ class="info-rows">
+ <view v-for="field in displayFields"
+ :key="field.key"
+ class="info-row">
+ <text class="info-label">{{ field.label }}</text>
+ <text class="info-value">{{ displayFieldValue(field) }}</text>
+ </view>
+ <view v-for="(extra, idx) in moduleExtraRows"
+ :key="`extra-${idx}`"
+ class="info-row">
+ <text class="info-label">{{ extra.label }}</text>
+ <text class="info-value">{{ extra.value }}</text>
+ </view>
+ </view>
+ <view v-else
+ class="empty-hint">鏆傛棤濉姤鍐呭</view>
+ </view>
+
+ <view class="section-card">
+ <view class="section-head">
+ <text class="section-title">瀹℃壒娴佺▼锛坽{ flowNodes.length }} 椤癸級</text>
+ </view>
+ <view v-if="flowNodes.length"
+ class="flow-wrap">
+ <view v-for="(node, nodeIndex) in flowNodes"
+ :key="nodeIndex"
+ class="flow-node-block">
+ <view class="flow-node-card">
+ <view class="node-header">
+ <view class="node-level-badge">{{ node.levelNo }}</view>
+ <text class="node-level-text">绗瑊{ levelLabel(node.levelNo) }}绾�</text>
+ <u-tag size="mini"
+ :type="node.approveType === 'OR' ? 'warning' : 'primary'"
+ :text="node.approveType === 'OR' ? '鎴栫' : '浼氱'"
+ plain />
+ </view>
+ <view class="approver-list">
+ <view v-for="(a, aIdx) in node.approvers"
+ :key="aIdx"
+ class="approver-row">
+ <text class="approver-name">{{ a.approverName }}</text>
+ <u-tag v-if="a.taskStatus"
+ size="mini"
+ :type="taskStatusTagType(a.taskStatus)"
+ :text="taskStatusText(a.taskStatus)"
+ plain />
+ </view>
+ </view>
+ </view>
+ <view v-if="nodeIndex < flowNodes.length - 1"
+ class="flow-connector-line" />
+ </view>
+ </view>
+ <view v-else
+ class="empty-hint">鏆傛棤娴佺▼鑺傜偣</view>
+ </view>
+
+ <view class="section-card">
+ <view class="section-head">
+ <text class="section-title">瀹℃壒璁板綍</text>
+ </view>
+ <view v-if="approvalRecords.length"
+ class="record-list">
+ <view v-for="(rec, index) in approvalRecords"
+ :key="rec.id ?? index"
+ class="record-item">
+ <view class="record-head">
+ <text class="record-operator">{{ rec.operatorName }}</text>
+ <u-tag size="mini"
+ :type="rec.result === 'approved' ? 'success' : rec.result === 'rejected' ? 'error' : 'info'"
+ :text="recordActionLabel(rec.result)"
+ plain />
+ </view>
+ <text class="record-time">{{ rec.time }}</text>
+ <text class="record-opinion">{{ rec.opinion || "鏃犳剰瑙�" }}</text>
+ </view>
+ </view>
+ <view v-else
+ class="empty-hint">鏆傛棤瀹℃壒璁板綍</view>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { computed } from "vue";
+ import { APPROVAL_MODULE_KEYS } from "../../../_utils/approvalModuleRegistry.js";
+ import {
+ computeLeaveDurationDisplay,
+ computeOvertimeHoursDisplay,
+ } from "../../../_utils/approvalModuleApplyExtras.js";
+ import {
+ businessStatusTagType,
+ businessStatusText,
+ displayFieldValue,
+ formatDateTime,
+ getRejectReasonFromRecords,
+ instanceStatusTagType,
+ instanceStatusText,
+ mapApprovalRecords,
+ mapTasksToFlowNodes,
+ recordActionLabel,
+ resolveInstanceDisplayFields,
+ taskStatusTagType,
+ taskStatusText,
+ } from "../../../_utils/approveListUtils.js";
+ import { parseApprovalFormConfig } from "../../../_utils/approvalFormField.js";
+
+ const props = defineProps({
+ row: { type: Object, default: () => ({}) },
+ moduleKey: { type: String, default: "" },
+ });
+
+ const LEVEL_TEXT = ["", "涓�", "浜�", "涓�", "鍥�", "浜�", "鍏�", "涓�", "鍏�", "涔�", "鍗�"];
+
+ const isBusinessModule = computed(() =>
+ [
+ APPROVAL_MODULE_KEYS.LEAVE,
+ APPROVAL_MODULE_KEYS.OVERTIME,
+ APPROVAL_MODULE_KEYS.TRANSFER,
+ APPROVAL_MODULE_KEYS.REGULAR,
+ APPROVAL_MODULE_KEYS.WORK_HANDOVER,
+ ].includes(props.moduleKey)
+ );
+
+ const statusLabel = status =>
+ isBusinessModule.value ? businessStatusText(status) : instanceStatusText(status);
+
+ const statusTagType = status =>
+ isBusinessModule.value ? businessStatusTagType(status) : instanceStatusTagType(status);
+
+ const displayFields = computed(() =>
+ resolveInstanceDisplayFields(props.row?.formConfig)
+ );
+
+ const moduleExtraRows = computed(() => {
+ const rows = [];
+ const cfg = parseApprovalFormConfig(props.row?.formConfig);
+ const payload = {};
+ (cfg.fields || []).forEach(f => {
+ if (f?.key) payload[f.key] = f.value ?? "";
+ });
+ if (props.moduleKey === APPROVAL_MODULE_KEYS.LEAVE) {
+ const balance = payload.leaveBalanceDays;
+ if (balance != null && balance !== "") {
+ rows.push({ label: "鍋囨湡浣欓", value: `${balance} 澶ー });
+ }
+ const days = computeLeaveDurationDisplay(cfg.fields, payload);
+ if (days) rows.push({ label: "璇峰亣鏃堕暱", value: `${days} 澶ー });
+ }
+ if (props.moduleKey === APPROVAL_MODULE_KEYS.OVERTIME) {
+ const hours = computeOvertimeHoursDisplay(cfg.fields, payload);
+ if (hours) rows.push({ label: "鍔犵彮鏃堕暱", value: `${hours} 灏忔椂` });
+ }
+ if (props.moduleKey === APPROVAL_MODULE_KEYS.TRANSFER) {
+ const post = payload.originalPostName || payload.originalPost;
+ if (post) rows.push({ label: "鍘熷矖浣�", value: post });
+ }
+ return rows;
+ });
+
+ const flowNodes = computed(() => mapTasksToFlowNodes(props.row?.tasks));
+
+ const approvalRecords = computed(() =>
+ mapApprovalRecords(props.row?.records)
+ );
+
+ const rejectReason = computed(() =>
+ getRejectReasonFromRecords(props.row?.records)
+ );
+
+ const levelLabel = n => LEVEL_TEXT[Number(n)] || String(n);
+</script>
+
+<style scoped lang="scss">
+ $primary: #2979ff;
+ $text: #1f2d3d;
+ $text-muted: #909399;
+
+ .detail-body {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ .section-card {
+ background: #fff;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 12px rgba(31, 45, 61, 0.05);
+ }
+
+ .section-head {
+ padding: 12px 16px;
+ border-bottom: 1px solid #f2f4f7;
+ }
+
+ .section-title {
+ font-size: 15px;
+ font-weight: 600;
+ color: $text;
+ padding-left: 10px;
+ border-left: 3px solid $primary;
+ line-height: 1.2;
+ }
+
+ .info-rows {
+ padding: 4px 16px 12px;
+ }
+
+ .info-row {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 10px 0;
+ border-bottom: 1px solid #f5f6f8;
+
+ &:last-child {
+ border-bottom: none;
+ }
+ }
+
+ .info-label {
+ flex-shrink: 0;
+ font-size: 14px;
+ color: $text-muted;
+ min-width: 72px;
+ }
+
+ .info-value {
+ flex: 1;
+ font-size: 14px;
+ color: $text;
+ text-align: right;
+ word-break: break-all;
+ }
+
+ .reject-text {
+ color: #f56c6c;
+ }
+
+ .flow-wrap {
+ padding: 10px 16px 14px;
+ }
+
+ .flow-node-block {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .flow-node-card {
+ background: #fafbfd;
+ border: 1px solid #e8eef5;
+ border-radius: 10px;
+ padding: 12px;
+ }
+
+ .node-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 10px;
+ }
+
+ .node-level-badge {
+ width: 26px;
+ height: 26px;
+ border-radius: 8px;
+ background: $primary;
+ color: #fff;
+ font-size: 13px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .node-level-text {
+ flex: 1;
+ font-size: 14px;
+ font-weight: 600;
+ color: $text;
+ }
+
+ .approver-list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .approver-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ }
+
+ .approver-name {
+ font-size: 13px;
+ color: #606266;
+ }
+
+ .flow-connector-line {
+ width: 2px;
+ height: 12px;
+ background: #d0dff0;
+ margin: 4px auto;
+ }
+
+ .record-list {
+ padding: 8px 16px 14px;
+ }
+
+ .record-item {
+ padding: 10px 0;
+ border-bottom: 1px solid #f0f2f5;
+
+ &:last-child {
+ border-bottom: none;
+ }
+ }
+
+ .record-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ }
+
+ .record-operator {
+ font-size: 14px;
+ font-weight: 600;
+ color: $text;
+ }
+
+ .record-time {
+ display: block;
+ margin-top: 4px;
+ font-size: 12px;
+ color: $text-muted;
+ }
+
+ .record-opinion {
+ display: block;
+ margin-top: 6px;
+ font-size: 13px;
+ color: #606266;
+ line-height: 1.5;
+ }
+
+ .empty-hint {
+ padding: 12px 16px 16px;
+ font-size: 13px;
+ color: $text-muted;
+ }
+</style>
diff --git a/src/pages/oa/ApproveManage/approve-list/apply.vue b/src/pages/oa/ApproveManage/approve-list/apply.vue
index 3e873a8..7d6e107 100644
--- a/src/pages/oa/ApproveManage/approve-list/apply.vue
+++ b/src/pages/oa/ApproveManage/approve-list/apply.vue
@@ -56,7 +56,7 @@
label-width="88"
input-align="right"
class="dynamic-form">
- <up-form-item v-for="field in formConfigData.fields"
+ <up-form-item v-for="field in displayTemplateFields"
:key="field.key"
:label="field.label"
:required="!!field.required"
@@ -120,6 +120,66 @@
</up-form>
<view v-else
class="empty-hint">璇ユā鏉挎殏鏃犲~鎶ラ」</view>
+
+ <!-- 璇峰亣锛氬亣鏈熶綑棰� + 鏃堕暱鑷姩璁$畻 -->
+ <view v-if="isLeaveModule"
+ class="module-extra-block">
+ <up-form :model="extraForm"
+ label-width="88"
+ input-align="right"
+ class="dynamic-form">
+ <up-form-item label="鍋囨湡浣欓"
+ required
+ class="form-item-inline">
+ <up-input v-model="extraForm.leaveBalanceDays"
+ type="digit"
+ placeholder="璇疯緭鍏ュぉ鏁�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="璇峰亣鏃堕暱"
+ class="form-item-inline">
+ <view class="readonly-with-unit">
+ <up-input :model-value="leaveDurationText"
+ readonly
+ placeholder="鏍规嵁璇峰亣鏃堕棿鑷姩璁$畻" />
+ <text class="unit-text">澶�</text>
+ </view>
+ </up-form-item>
+ </up-form>
+ </view>
+
+ <!-- 鍔犵彮锛氭椂闀胯嚜鍔ㄨ绠� -->
+ <view v-if="isOvertimeModule"
+ class="module-extra-block">
+ <up-form label-width="88"
+ input-align="right"
+ class="dynamic-form">
+ <up-form-item label="鍔犵彮鏃堕暱"
+ class="form-item-inline">
+ <view class="readonly-with-unit">
+ <up-input :model-value="overtimeHoursText"
+ readonly
+ placeholder="鏍规嵁鍔犵彮鏃堕棿鑷姩璁$畻" />
+ <text class="unit-text">灏忔椂</text>
+ </view>
+ </up-form-item>
+ </up-form>
+ </view>
+
+ <!-- 璋冨矖锛氬師宀椾綅鑷姩甯﹀嚭 -->
+ <view v-if="isTransferModule"
+ class="module-extra-block">
+ <up-form label-width="88"
+ input-align="right"
+ class="dynamic-form">
+ <up-form-item label="鍘熷矖浣�"
+ class="form-item-readonly">
+ <up-input :model-value="extraForm.originalPostName"
+ readonly
+ placeholder="閫夋嫨鐢宠浜哄悗鑷姩甯﹀嚭" />
+ </up-form-item>
+ </up-form>
+ </view>
</view>
<view class="section-card">
@@ -200,7 +260,7 @@
</template>
<script setup>
- import { computed, reactive, ref } from "vue";
+ import { computed, reactive, ref, watch } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import PageHeader from "@/components/PageHeader.vue";
import FooterButtons from "@/components/FooterButtons.vue";
@@ -212,7 +272,25 @@
import useUserStore from "@/store/modules/user";
import { parseTime } from "@/utils/ruoyi";
import { getDept } from "@/api/collaborativeApproval/approvalProcess.js";
+ import { findPostOptions } from "@/api/system/post.js";
import { userListNoPageByTenantId } from "@/api/system/user";
+ import { APPROVAL_MODULE_KEYS } from "../../_utils/approvalModuleRegistry.js";
+ import {
+ computeLeaveDurationDisplay,
+ computeOvertimeHoursDisplay,
+ displayTemplateFieldsByModule,
+ findApplicantTemplateField,
+ findLeaveTimeTemplateField,
+ findOvertimeTimeTemplateField,
+ inferModuleKeyFromRow,
+ loadModuleExtrasFromRow,
+ resolveOriginalPostName,
+ syncModuleExtrasToFormValues,
+ unwrapUserArray,
+ userById,
+ validateModuleExtras,
+ buildPostIdToNameMap,
+ } from "../../_utils/approvalModuleApplyExtras.js";
import {
formatDatetimerangeDisplay,
formatFieldDateValue,
@@ -233,10 +311,12 @@
parseFieldDateToTs,
} from "../../_utils/approvalFormField.js";
- const EDIT_STORAGE_KEY = "oa_approve_instance_edit_row";
+ import { EDIT_STORAGE_KEY } from "../../_utils/approveListUtils.js";
+
const LEVEL_TEXT = ["", "涓�", "浜�", "涓�", "鍥�", "浜�", "鍏�", "涓�", "鍏�", "涔�", "鍗�"];
const userStore = useUserStore();
+ const moduleKey = ref("");
const templateId = ref("");
const instanceId = ref("");
const instanceRow = ref(null);
@@ -245,6 +325,22 @@
const submitting = ref(false);
const formValues = reactive({});
const form = reactive({ title: "" });
+ const extraForm = reactive({
+ leaveBalanceDays: undefined,
+ originalPostName: "",
+ });
+ const postIdToName = ref({});
+ const transferUserPool = ref([]);
+
+ const isLeaveModule = computed(
+ () => moduleKey.value === APPROVAL_MODULE_KEYS.LEAVE
+ );
+ const isOvertimeModule = computed(
+ () => moduleKey.value === APPROVAL_MODULE_KEYS.OVERTIME
+ );
+ const isTransferModule = computed(
+ () => moduleKey.value === APPROVAL_MODULE_KEYS.TRANSFER
+ );
const showDatePicker = ref(false);
const datePickerTs = ref(Date.now());
@@ -288,6 +384,31 @@
);
}
return parseApprovalFormConfig(detail.value?.formConfig);
+ });
+
+ const displayTemplateFields = computed(() =>
+ displayTemplateFieldsByModule(moduleKey.value, formConfigData.value.fields)
+ );
+
+ const leaveDurationText = computed(() => {
+ if (!isLeaveModule.value) return "";
+ const fields = formConfigData.value.fields;
+ const timeField = findLeaveTimeTemplateField(fields);
+ if (timeField?.key) void formValues[timeField.key];
+ return computeLeaveDurationDisplay(fields, formValues);
+ });
+
+ const overtimeHoursText = computed(() => {
+ if (!isOvertimeModule.value) return "";
+ const fields = formConfigData.value.fields;
+ const timeField = findOvertimeTimeTemplateField(fields);
+ if (timeField?.key) void formValues[timeField.key];
+ return computeOvertimeHoursDisplay(fields, formValues);
+ });
+
+ const applicantPickerValue = computed(() => {
+ const f = findApplicantTemplateField(formConfigData.value.fields);
+ return f?.key ? formValues[f.key] : undefined;
});
const levelLabel = n => LEVEL_TEXT[Number(n)] || String(n);
@@ -414,7 +535,7 @@
uni.showToast({ title: "璇疯緭鍏ュ鎵规爣棰�", icon: "none" });
return false;
}
- for (const field of formConfigData.value.fields) {
+ for (const field of displayTemplateFields.value) {
if (!field.required) continue;
const val = formValues[field.key];
if (val === undefined || val === null || String(val).trim() === "") {
@@ -453,17 +574,35 @@
uni.showToast({ title: "妯℃澘鏈厤缃鎵规祦绋�", icon: "none" });
return false;
}
+ const moduleMsg = validateModuleExtras(
+ moduleKey.value,
+ formConfigData.value.fields,
+ formValues,
+ extraForm
+ );
+ if (moduleMsg) {
+ uni.showToast({ title: moduleMsg, icon: "none" });
+ return false;
+ }
return true;
};
- const buildFormConfigPayload = () =>
- JSON.stringify({
+ const buildFormConfigPayload = () => {
+ syncModuleExtrasToFormValues(
+ moduleKey.value,
+ formValues,
+ extraForm,
+ formConfigData.value.fields
+ );
+ const allFields = formConfigData.value.fields || [];
+ return JSON.stringify({
prompt: formConfigData.value.prompt,
- fields: formConfigData.value.fields.map(field => ({
+ fields: allFields.map(field => ({
...field,
value: formValues[field.key] ?? "",
})),
});
+ };
const buildSavePayload = () => ({
templateId: detail.value.id,
@@ -562,7 +701,8 @@
try {
await loadTemplateDetail();
if (!detail.value) return;
- initFormValues(formConfigData.value.fields);
+ initFormValues(displayTemplateFields.value);
+ resetModuleExtras();
if (!form.title && detail.value.templateName) {
form.title = `${detail.value.templateName}鐢宠`;
}
@@ -579,6 +719,9 @@
return;
}
instanceRow.value = row;
+ if (!moduleKey.value) {
+ moduleKey.value = inferModuleKeyFromRow(row);
+ }
templateId.value = row.templateId;
form.title = row.title || "";
@@ -587,11 +730,65 @@
try {
await loadTemplateDetail();
if (!detail.value) return;
- initFormValues(formConfigData.value.fields);
+ initFormValues(displayTemplateFields.value);
+ applyModuleExtrasFromRow();
+ if (isTransferModule.value) {
+ await ensureTransferLookupData();
+ syncOriginalPostFromApplicant(applicantPickerValue.value);
+ }
} finally {
loading.value = false;
}
};
+
+ function resetModuleExtras() {
+ extraForm.leaveBalanceDays = undefined;
+ extraForm.originalPostName = "";
+ }
+
+ function applyModuleExtrasFromRow() {
+ const loaded = loadModuleExtrasFromRow(
+ moduleKey.value,
+ instanceRow.value,
+ formValues
+ );
+ if (loaded.leaveBalanceDays != null) {
+ extraForm.leaveBalanceDays = loaded.leaveBalanceDays;
+ }
+ if (loaded.originalPostName) {
+ extraForm.originalPostName = loaded.originalPostName;
+ }
+ }
+
+ async function ensureTransferLookupData() {
+ if (!transferUserPool.value.length) {
+ try {
+ const res = await userListNoPageByTenantId();
+ transferUserPool.value = unwrapUserArray(res);
+ } catch {
+ transferUserPool.value = [];
+ }
+ }
+ if (!Object.keys(postIdToName.value).length) {
+ try {
+ const res = await findPostOptions();
+ const rows = res?.data ?? res?.rows ?? [];
+ postIdToName.value = buildPostIdToNameMap(Array.isArray(rows) ? rows : []);
+ } catch {
+ postIdToName.value = {};
+ }
+ }
+ }
+
+ function syncOriginalPostFromApplicant(uid) {
+ if (!isTransferModule.value) return;
+ if (!uid) {
+ extraForm.originalPostName = "";
+ return;
+ }
+ const user = userById(transferUserPool.value, uid);
+ extraForm.originalPostName = resolveOriginalPostName(user, postIdToName.value);
+ }
const goBack = () => {
uni.navigateBack();
@@ -614,8 +811,18 @@
});
};
+ watch(applicantPickerValue, async uid => {
+ if (!isTransferModule.value) return;
+ await ensureTransferLookupData();
+ syncOriginalPostFromApplicant(uid);
+ });
+
onLoad(options => {
+ moduleKey.value = options?.moduleKey || "";
loadPickerSourceData();
+ if (isTransferModule.value) {
+ ensureTransferLookupData();
+ }
if (options?.id) {
instanceId.value = options.id;
loadForEdit();
@@ -1015,4 +1222,29 @@
.empty-wrap {
padding: 48px 20px;
}
+
+ .module-extra-block {
+ margin-top: 8px;
+ padding-top: 8px;
+ border-top: 1px dashed #e8ecf0;
+ }
+
+ .readonly-with-unit {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ width: 100%;
+ justify-content: flex-end;
+ }
+
+ .readonly-with-unit :deep(.u-input) {
+ flex: 1;
+ min-width: 0;
+ }
+
+ .unit-text {
+ flex-shrink: 0;
+ font-size: 14px;
+ color: $text-muted;
+ }
</style>
diff --git a/src/pages/oa/ApproveManage/approve-list/approve.vue b/src/pages/oa/ApproveManage/approve-list/approve.vue
new file mode 100644
index 0000000..3ed6220
--- /dev/null
+++ b/src/pages/oa/ApproveManage/approve-list/approve.vue
@@ -0,0 +1,164 @@
+<!--
+ OA / 瀹℃壒绠$悊 / 瀹℃壒澶勭悊
+ 璺敱锛�/pages/oa/ApproveManage/approve-list/approve
+-->
+<template>
+ <view class="oa-detail-page">
+ <PageHeader title="瀹℃壒澶勭悊"
+ @back="goBack" />
+
+ <scroll-view v-if="row"
+ class="oa-detail-scroll"
+ scroll-y
+ :show-scrollbar="false">
+ <ApproveInstanceDetailBody :row="row"
+ :module-key="detailModuleKey" />
+
+ <view class="section-card opinion-card">
+ <view class="section-head">
+ <text class="section-title">瀹℃壒鎰忚</text>
+ </view>
+ <view class="opinion-wrap">
+ <up-textarea v-model="approveOpinion"
+ placeholder="閫氳繃鍙暀绌猴紱椹冲洖璇峰~鍐欏叿浣撳師鍥�"
+ maxlength="500"
+ count
+ height="100"
+ border="surround" />
+ </view>
+ </view>
+ </scroll-view>
+
+ <view v-else
+ class="oa-empty">
+ <up-empty mode="data"
+ text="鏈幏鍙栧埌瀹℃壒鏁版嵁" />
+ </view>
+
+ <view v-if="row"
+ class="oa-page-footer">
+ <text class="oa-footer-btn btn-default"
+ :class="{ 'is-disabled': submitting }"
+ @click="goBack">鍙栨秷</text>
+ <text class="oa-footer-btn btn-danger"
+ :class="{ 'is-disabled': submitting }"
+ @click="submitApprove('rejected')">椹冲洖</text>
+ <text class="oa-footer-btn btn-success"
+ :class="{ 'is-disabled': submitting }"
+ @click="submitApprove('approved')">閫氳繃</text>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { computed, ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import ApproveInstanceDetailBody from "./_components/ApproveInstanceDetailBody.vue";
+ import { approveApprovalInstance } from "@/api/oa/approvalInstance.js";
+ import {
+ buildApproveInstanceDto,
+ canApproveInstance,
+ loadInstanceRow,
+ } from "../../_utils/approveListUtils.js";
+ import { inferModuleKeyFromRow } from "../../_utils/approvalModuleApplyExtras.js";
+
+ const instanceId = ref("");
+ const row = ref(null);
+ const detailModuleKey = computed(() => inferModuleKeyFromRow(row.value));
+ const approveOpinion = ref("");
+ const submitting = ref(false);
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ const submitApprove = uiResult => {
+ if (!row.value?.id || submitting.value) return;
+
+ if (uiResult === "rejected" && !(approveOpinion.value || "").trim()) {
+ uni.showToast({ title: "椹冲洖鏃惰濉啓瀹℃壒鎰忚", icon: "none" });
+ return;
+ }
+
+ submitting.value = true;
+ const dto = buildApproveInstanceDto(
+ row.value.id,
+ uiResult,
+ approveOpinion.value
+ );
+
+ approveApprovalInstance(dto)
+ .then(() => {
+ uni.showToast({
+ title: uiResult === "approved" ? "宸查�氳繃" : "宸查┏鍥�",
+ icon: "success",
+ });
+ setTimeout(() => {
+ const pages = getCurrentPages();
+ const prevRoute = pages[pages.length - 2]?.route || "";
+ const delta = prevRoute.includes("approve-list/detail") ? 2 : 1;
+ uni.navigateBack({ delta });
+ }, 300);
+ })
+ .catch(() => {
+ uni.showToast({ title: "瀹℃壒鎿嶄綔澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ submitting.value = false;
+ });
+ };
+
+ onLoad(options => {
+ if (!options?.id) {
+ uni.showToast({ title: "缂哄皯瀹℃壒 ID", icon: "none" });
+ setTimeout(goBack, 500);
+ return;
+ }
+ instanceId.value = options.id;
+ const cached = loadInstanceRow(options.id);
+ if (!cached) {
+ uni.showToast({ title: "璇蜂粠鍒楄〃杩涘叆瀹℃壒", icon: "none" });
+ setTimeout(goBack, 500);
+ return;
+ }
+ if (!canApproveInstance(cached)) {
+ uni.showToast({ title: "褰撳墠瀹℃壒涓嶅彲澶勭悊", icon: "none" });
+ setTimeout(goBack, 500);
+ return;
+ }
+ row.value = cached;
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "../../_styles/oa-approval-list.scss";
+
+ $primary: #2979ff;
+
+ .opinion-card {
+ margin-top: 10px;
+ background: #fff;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 12px rgba(31, 45, 61, 0.05);
+ }
+
+ .section-head {
+ padding: 12px 16px;
+ border-bottom: 1px solid #f2f4f7;
+ }
+
+ .section-title {
+ font-size: 15px;
+ font-weight: 600;
+ color: #1f2d3d;
+ padding-left: 10px;
+ border-left: 3px solid $primary;
+ line-height: 1.2;
+ }
+
+ .opinion-wrap {
+ padding: 12px 16px 16px;
+ }
+</style>
diff --git a/src/pages/oa/ApproveManage/approve-list/detail.vue b/src/pages/oa/ApproveManage/approve-list/detail.vue
new file mode 100644
index 0000000..61ac9e4
--- /dev/null
+++ b/src/pages/oa/ApproveManage/approve-list/detail.vue
@@ -0,0 +1,116 @@
+<!--
+ OA / 瀹℃壒绠$悊 / 瀹℃壒璇︽儏
+ 璺敱锛�/pages/oa/ApproveManage/approve-list/detail
+-->
+<template>
+ <view class="oa-detail-page">
+ <PageHeader title="瀹℃壒璇︽儏"
+ @back="goBack" />
+
+ <scroll-view v-if="row"
+ class="oa-detail-scroll"
+ scroll-y
+ :show-scrollbar="false">
+ <ApproveInstanceDetailBody :row="row"
+ :module-key="detailModuleKey" />
+ </scroll-view>
+
+ <view v-else
+ class="oa-empty">
+ <up-empty mode="data"
+ text="鏈幏鍙栧埌瀹℃壒鏁版嵁" />
+ </view>
+
+ <view v-if="row"
+ class="oa-page-footer">
+ <text class="oa-footer-btn btn-default"
+ @click="goBack">杩斿洖</text>
+ <text v-if="showEdit"
+ class="oa-footer-btn btn-warn"
+ @click="goEdit">淇敼</text>
+ <text v-if="showApprove && !fromBusiness"
+ class="oa-footer-btn btn-primary"
+ @click="goApprove">鍘诲鎵�</text>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { computed, ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import ApproveInstanceDetailBody from "./_components/ApproveInstanceDetailBody.vue";
+ import { OA_NAV } from "@/config/oaPaths.js";
+ import useUserStore from "@/store/modules/user";
+ import {
+ canApproveInstance,
+ canEditBusinessInstanceRow,
+ canModifyInstance,
+ EDIT_STORAGE_KEY,
+ loadInstanceRow,
+ stashInstanceRow,
+ } from "../../_utils/approveListUtils.js";
+ import { inferModuleKeyFromRow } from "../../_utils/approvalModuleApplyExtras.js";
+
+ const userStore = useUserStore();
+ const instanceId = ref("");
+ const fromBusiness = ref(false);
+ const row = ref(null);
+
+ const detailModuleKey = computed(() => inferModuleKeyFromRow(row.value));
+
+ const showEdit = computed(() => {
+ if (fromBusiness.value) {
+ return canEditBusinessInstanceRow(row.value);
+ }
+ return canModifyInstance(row.value, userStore);
+ });
+ const showApprove = computed(() => canApproveInstance(row.value));
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ const goEdit = () => {
+ if (!showEdit.value || !row.value?.id) return;
+ if (fromBusiness.value && !canEditBusinessInstanceRow(row.value)) {
+ uni.showToast({ title: "杩涜涓垨宸插畬鎴愮殑瀹℃壒涓嶅彲淇敼", icon: "none" });
+ return;
+ }
+ uni.setStorageSync(EDIT_STORAGE_KEY, row.value);
+ const mk = detailModuleKey.value;
+ const q = mk ? `&moduleKey=${mk}` : "";
+ uni.navigateTo({
+ url: `${OA_NAV.approveListApply}?id=${row.value.id}${q}`,
+ });
+ };
+
+ const goApprove = () => {
+ if (!row.value?.id) return;
+ stashInstanceRow(row.value);
+ uni.navigateTo({
+ url: `${OA_NAV.approveListApprove}?id=${row.value.id}`,
+ });
+ };
+
+ onLoad(options => {
+ fromBusiness.value = options?.from === "business";
+ if (!options?.id) {
+ uni.showToast({ title: "缂哄皯瀹℃壒 ID", icon: "none" });
+ setTimeout(goBack, 500);
+ return;
+ }
+ instanceId.value = options.id;
+ const cached = loadInstanceRow(options.id);
+ if (!cached) {
+ uni.showToast({ title: "璇蜂粠鍒楄〃杩涘叆璇︽儏", icon: "none" });
+ setTimeout(goBack, 500);
+ return;
+ }
+ row.value = cached;
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "../../_styles/oa-approval-list.scss";
+</style>
diff --git a/src/pages/oa/ApproveManage/approve-list/index.vue b/src/pages/oa/ApproveManage/approve-list/index.vue
index fd5e142..7c1603e 100644
--- a/src/pages/oa/ApproveManage/approve-list/index.vue
+++ b/src/pages/oa/ApproveManage/approve-list/index.vue
@@ -1,109 +1,94 @@
<!--
OA / 瀹℃壒绠$悊 / 瀹℃壒鍒楄〃
- 璺敱锛�/pages/oa/ApproveManage/approve-list/index
-->
<template>
- <view class="approve-list-page sales-account">
+ <view class="oa-approval-page">
<PageHeader title="瀹℃壒鍒楄〃"
@back="goBack" />
- <view class="search-section">
- <view class="search-bar">
- <view class="search-input">
- <up-input v-model="queryParams.keyword"
- class="search-text"
- placeholder="瀹℃壒鏍囬 / 瀹℃壒缂栧彿"
- clearable
- @confirm="handleSearch" />
- </view>
- <view class="filter-button"
- @click="handleSearch">
- <up-icon name="search"
- size="24"
- color="#999" />
- </view>
+
+ <view class="oa-toolbar">
+ <view class="oa-filter-chip active-search">
+ <up-icon name="search"
+ size="18"
+ color="#666" />
+ <up-input v-model="queryParams.keyword"
+ class="chip-input"
+ placeholder="瀹℃壒鏍囬 / 瀹℃壒缂栧彿"
+ clearable
+ border="none"
+ @confirm="handleSearch" />
+ </view>
+ <view class="oa-icon-btn"
+ @click="handleSearch">
+ <up-icon name="search"
+ size="20"
+ color="#2979ff" />
</view>
</view>
- <scroll-view class="list-scroll"
+ <scroll-view class="oa-list-scroll"
scroll-y
:show-scrollbar="false"
+ :style="{ height: listScrollHeight + 'px' }"
@scrolltolower="loadMore">
<view v-if="list.length"
- class="ledger-list">
+ class="oa-card-list">
<view v-for="item in list"
:key="item.id"
- class="ledger-item">
- <view class="item-header">
- <view class="item-left">
- <view class="document-icon">
- <up-icon name="file-text"
- size="16"
- color="#ffffff" />
+ class="oa-card"
+ @click="openDetail(item)">
+ <view class="oa-card-head">
+ <view class="oa-card-title-wrap">
+ <text class="oa-card-title">{{ item.title || item.instanceNo || "-" }}</text>
+ <text v-if="item.templateName"
+ class="oa-card-sub">{{ item.templateName }}</text>
+ </view>
+ <text :class="['oa-status', businessStatusClass(item.status)]">
+ {{ statusText(item.status) }}
+ </text>
+ </view>
+
+ <view class="oa-card-body">
+ <view class="oa-info-grid">
+ <view class="oa-info-row">
+ <text class="oa-info-label">瀹℃壒缂栧彿</text>
+ <text class="oa-info-value">{{ item.instanceNo || "-" }}</text>
</view>
- <text class="item-id">{{ item.title || item.instanceNo || "-" }}</text>
- </view>
- <u-tag :type="statusTagType(item.status)"
- :text="statusText(item.status)" />
- </view>
- <up-divider />
- <view class="item-details">
- <view class="detail-row">
- <text class="detail-label">瀹℃壒缂栧彿</text>
- <text class="detail-value">{{ item.instanceNo || "-" }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">妯℃澘鍚嶇О</text>
- <text class="detail-value">{{ item.templateName || "-" }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">涓氬姟鍚嶇О</text>
- <text class="detail-value">{{ item.businessName || "-" }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">鐢宠浜�</text>
- <text class="detail-value">{{ item.applicantName || "-" }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">褰撳墠绾у埆</text>
- <text class="detail-value">{{ formatLevel(item.currentLevel) }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">褰撳墠瀹℃壒浜�</text>
- <text class="detail-value">{{ currentApproverName(item) }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">鐢宠鏃堕棿</text>
- <text class="detail-value">{{ formatDateTime(item.applyTime) }}</text>
- </view>
- <view v-if="item.finishTime"
- class="detail-row">
- <text class="detail-label">瀹屾垚鏃堕棿</text>
- <text class="detail-value">{{ formatDateTime(item.finishTime) }}</text>
+ <view v-if="item.businessName"
+ class="oa-info-row">
+ <text class="oa-info-label">涓氬姟鍚嶇О</text>
+ <text class="oa-info-value">{{ item.businessName }}</text>
+ </view>
+ <view class="oa-info-row">
+ <text class="oa-info-label">鐢宠浜�</text>
+ <text class="oa-info-value">{{ item.applicantName || "-" }}</text>
+ </view>
+ <view class="oa-info-row">
+ <text class="oa-info-label">褰撳墠瀹℃壒浜�</text>
+ <text class="oa-info-value">{{ currentApproverName(item) }}</text>
+ </view>
+ <view class="oa-info-row">
+ <text class="oa-info-label">鐢宠鏃堕棿</text>
+ <text class="oa-info-value">{{ formatDateTime(item.applyTime) }}</text>
+ </view>
</view>
</view>
+
<view v-if="canModify(item) || item.isApprove"
- class="action-buttons">
- <up-button v-if="canModify(item)"
- class="action-btn"
- size="small"
- type="warning"
- plain
- @click.stop="goModify(item)">
- 缂栬緫
- </up-button>
- <up-button v-if="item.isApprove"
- class="action-btn"
- size="small"
- type="primary"
- @click.stop="handleApprove(item)">
- 瀹℃壒
- </up-button>
+ class="oa-card-foot"
+ @click.stop>
+ <text v-if="canModify(item)"
+ class="oa-foot-btn btn-edit"
+ @click="goModify(item)">缂栬緫</text>
+ <text v-if="item.isApprove"
+ class="oa-foot-btn btn-approve"
+ @click="handleApprove(item)">瀹℃壒</text>
</view>
</view>
<up-loadmore :status="pageStatus" />
</view>
<view v-else
- class="empty-wrap">
+ class="oa-empty">
<up-empty mode="list"
text="鏆傛棤瀹℃壒鏁版嵁" />
</view>
@@ -119,71 +104,48 @@
</template>
<script setup>
- import { reactive, ref } from "vue";
+ import { onMounted, reactive, ref } from "vue";
import { onShow } from "@dcloudio/uni-app";
import PageHeader from "@/components/PageHeader.vue";
import { listApprovalInstancePage } from "@/api/oa/approvalInstance.js";
import { OA_NAV } from "@/config/oaPaths.js";
import useUserStore from "@/store/modules/user";
import { parseTime } from "@/utils/ruoyi";
+ import {
+ businessStatusClass,
+ businessStatusText,
+ canModifyInstance,
+ EDIT_STORAGE_KEY,
+ stashInstanceRow,
+ } from "../../_utils/approveListUtils.js";
- const EDIT_STORAGE_KEY = "oa_approve_instance_edit_row";
const userStore = useUserStore();
-
- const queryParams = reactive({
- keyword: "",
- });
-
+ const queryParams = reactive({ keyword: "" });
const list = ref([]);
const pageStatus = ref("loadmore");
+ const page = reactive({ current: 1, size: 10, total: 0 });
+ const listScrollHeight = ref(400);
- const page = reactive({
- current: 1,
- size: 10,
- total: 0,
- });
+ function calcListScrollHeight() {
+ const sys = uni.getSystemInfoSync();
+ const statusBar = sys.statusBarHeight || 0;
+ const navBar = 44;
+ const toolbar = 56;
+ const fabGap = 16;
+ listScrollHeight.value = Math.max(
+ 200,
+ sys.windowHeight - statusBar - navBar - toolbar - fabGap
+ );
+ }
- const STATUS_TEXT = {
- PENDING: "杩涜涓�",
- APPROVED: "宸查�氳繃",
- REJECTED: "宸查┏鍥�",
- };
-
- const STATUS_TAG = {
- PENDING: "warning",
- APPROVED: "success",
- REJECTED: "error",
- };
-
- const statusText = status => STATUS_TEXT[status] || status || "-";
-
- const statusTagType = status => STATUS_TAG[status] || "info";
-
- const formatLevel = level => {
- if (level == null || level === "") return "-";
- return `绗� ${level} 绾;
- };
+ const statusText = status => businessStatusText(status);
const formatDateTime = val => {
if (!val) return "-";
return parseTime(val, "{y}-{m}-{d} {h}:{i}:{s}") || String(val);
};
- /** 鏄惁鏈汉鍙戣捣鐨勫鎵癸紙鍏煎鍒楄〃鏈繑鍥� applicantId锛� */
- const isOwnApplication = item => {
- const uid = userStore.id;
- if (item?.applicantId != null && uid != null && uid !== "") {
- return String(item.applicantId) === String(uid);
- }
- const loginName = userStore.nickName || userStore.name;
- if (loginName && item?.applicantName) {
- return String(item.applicantName).trim() === String(loginName).trim();
- }
- return false;
- };
-
- /** 浠呫�岃繘琛屼腑銆嶄笖鏈汉鍙戣捣鏃跺彲缂栬緫锛堝凡閫氳繃/宸查┏鍥炰笉鏄剧ず缂栬緫锛� */
- const canModify = item => item?.status === "PENDING" && isOwnApplication(item);
+ const canModify = item => canModifyInstance(item, userStore);
const currentApproverName = item => {
const tasks = item?.tasks;
@@ -204,31 +166,22 @@
dto.instanceNo = keyword;
}
}
- return {
- page: {
- current: page.current,
- size: page.size,
- },
- approvalInstanceDto: dto,
- };
+ return { current: page.current, size: page.size, ...dto };
};
const getList = () => {
if (pageStatus.value === "loading" || pageStatus.value === "nomore") return;
-
pageStatus.value = "loading";
listApprovalInstancePage(buildListParams())
.then(res => {
const pageData = res?.data || {};
const records = pageData.records || [];
const total = pageData.total ?? 0;
-
if (page.current === 1) {
list.value = records;
} else {
list.value = [...list.value, ...records];
}
-
page.total = total;
if (list.value.length >= total || records.length < page.size) {
pageStatus.value = "nomore";
@@ -238,9 +191,7 @@
}
})
.catch(() => {
- if (page.current === 1) {
- list.value = [];
- }
+ if (page.current === 1) list.value = [];
pageStatus.value = "loadmore";
uni.showToast({ title: "鏌ヨ澶辫触", icon: "none" });
});
@@ -254,17 +205,16 @@
};
const loadMore = () => {
- if (pageStatus.value === "loadmore") {
- getList();
- }
+ if (pageStatus.value === "loadmore") getList();
};
- const goBack = () => {
- uni.navigateBack();
- };
+ const goBack = () => uni.navigateBack();
+ const goAdd = () => uni.navigateTo({ url: OA_NAV.approveListTemplateSelect });
- const goAdd = () => {
- uni.navigateTo({ url: OA_NAV.approveListTemplateSelect });
+ const openDetail = item => {
+ if (!item?.id) return;
+ stashInstanceRow(item);
+ uni.navigateTo({ url: `${OA_NAV.approveListDetail}?id=${item.id}` });
};
const goModify = item => {
@@ -274,50 +224,43 @@
}
if (!item?.id) return;
uni.setStorageSync(EDIT_STORAGE_KEY, item);
- uni.navigateTo({
- url: `${OA_NAV.approveListApply}?id=${item.id}`,
- });
+ stashInstanceRow(item);
+ uni.navigateTo({ url: `${OA_NAV.approveListApply}?id=${item.id}` });
};
const handleApprove = item => {
if (!item?.id) return;
- uni.showToast({ title: "瀹℃壒璇︽儏椤靛緟瀵规帴", icon: "none" });
+ if (!item.isApprove) {
+ uni.showToast({ title: "褰撳墠瀹℃壒鏃犻渶鎮ㄥ鐞�", icon: "none" });
+ return;
+ }
+ stashInstanceRow(item);
+ uni.navigateTo({ url: `${OA_NAV.approveListApprove}?id=${item.id}` });
};
+ onMounted(() => calcListScrollHeight());
+
onShow(() => {
+ calcListScrollHeight();
handleSearch();
});
</script>
<style scoped lang="scss">
@import "@/styles/sales-common.scss";
+ @import "../../_styles/oa-approval-list.scss";
- .approve-list-page {
- display: flex;
- flex-direction: column;
- min-height: 100vh;
+ .active-search {
+ padding-right: 4px;
}
- .list-scroll {
+ .chip-input {
flex: 1;
- height: 0;
- padding-bottom: calc(80px + env(safe-area-inset-bottom));
+ font-size: 14px;
}
- .empty-wrap {
- padding: 48px 20px;
- }
-
- .action-buttons {
- display: flex;
- justify-content: flex-end;
- gap: 10px;
- margin-top: 12px;
- padding-top: 12px;
- border-top: 1px solid #f0f0f0;
- }
-
- .action-btn {
- min-width: 72px;
+ :deep(.chip-input .u-input__content) {
+ background: transparent !important;
+ padding: 0 !important;
}
</style>
diff --git a/src/pages/oa/ApproveManage/approve-list/template-select.vue b/src/pages/oa/ApproveManage/approve-list/template-select.vue
index 628483b..073b556 100644
--- a/src/pages/oa/ApproveManage/approve-list/template-select.vue
+++ b/src/pages/oa/ApproveManage/approve-list/template-select.vue
@@ -5,10 +5,10 @@
-->
<template>
<view class="template-select-page sales-account">
- <PageHeader title="閫夋嫨瀹℃壒妯℃澘"
+ <PageHeader :title="pageHeaderTitle"
@back="goBack" />
- <view v-if="typeOptions.length"
+ <view v-if="typeOptions.length && !moduleKey"
class="step-section">
<view class="tabs-wrap">
<up-tabs :list="tabList"
@@ -16,6 +16,11 @@
line-color="#2979ff"
@click="onTabClick" />
</view>
+ </view>
+
+ <view v-if="useAllTemplatesFallback && allTemplates.length"
+ class="fallback-hint">
+ <text>褰撳墠绫诲瀷涓嬫棤鍖归厤妯℃澘锛屽凡鏄剧ず鍏ㄩ儴 {{ allTemplates.length }} 涓彲鐢ㄦā鏉�</text>
</view>
<view class="search-section">
@@ -41,11 +46,6 @@
class="loading-wrap">
<up-loading-icon mode="circle" />
<text class="loading-text">鍔犺浇涓�...</text>
- </view>
- <view v-else-if="!typeOptions.length"
- class="empty-wrap">
- <up-empty mode="list"
- text="鏈幏鍙栧埌瀹℃壒绫诲瀷" />
</view>
<view v-else-if="displayList.length"
class="ledger-list">
@@ -95,17 +95,25 @@
import { computed, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import PageHeader from "@/components/PageHeader.vue";
- import { listApprovalTemplateByType } from "@/api/oa/approvalTemplate.js";
import { OA_NAV } from "@/config/oaPaths.js";
import {
buildTypeLabelMap,
- CUSTOM_TEMPLATE_LIST_TYPE,
fetchApprovalTemplateTypes,
+ buildTypeOptionsFromTemplates,
+ FALLBACK_BUSINESS_TYPE_OPTIONS,
+ fetchEnabledApprovalTemplates,
filterTemplatesByBusinessType,
+ filterTemplatesByBusinessTypes,
getBusinessTypeLabel,
- getDefaultTypeTabIndex,
+ pickTabIndexWithTemplates,
} from "../../_utils/approvalTemplateType.js";
+ import {
+ getApprovalModuleConfig,
+ getModuleMatchingBusinessTypes,
+ resolveModuleBusinessType,
+ } from "../../_utils/approvalModuleRegistry.js";
+ const moduleKey = ref("");
const typeOptions = ref([]);
const typeLabelMap = ref({});
/** 鍏ㄩ儴鑷畾涔夊凡鍚敤妯℃澘锛坙ist/1 涓�娆℃媺鍙栵級 */
@@ -118,9 +126,45 @@
typeOptions.value.map(opt => ({ name: opt.name }))
);
+ const moduleConfig = computed(() =>
+ moduleKey.value ? getApprovalModuleConfig(moduleKey.value) : null
+ );
+
+ const pageHeaderTitle = computed(() => {
+ if (moduleConfig.value?.label) {
+ return `閫夋嫨${moduleConfig.value.label}妯℃澘`;
+ }
+ return "閫夋嫨瀹℃壒妯℃澘";
+ });
+
+ const moduleBusinessTypes = computed(() => {
+ if (!moduleKey.value) return [];
+ return getModuleMatchingBusinessTypes(moduleKey.value, typeOptions.value);
+ });
+
const currentTypeOption = computed(() => typeOptions.value[activeTab.value]);
+ /** 鏃� moduleKey 涓斿綋鍓� Tab 绛涗笉鍒版椂锛屽睍绀哄叏閮ㄦā鏉块伩鍏嶃�屾湁鏁版嵁鍗寸┖鐧姐�� */
+ const useAllTemplatesFallback = computed(() => {
+ if (moduleKey.value) return false;
+ if (!allTemplates.value.length) return false;
+ const businessType = currentTypeOption.value?.value;
+ if (businessType == null || businessType === "") return true;
+ return filterTemplatesByBusinessType(allTemplates.value, businessType).length === 0;
+ });
+
const currentSource = computed(() => {
+ if (moduleKey.value && moduleBusinessTypes.value.length) {
+ const filtered = filterTemplatesByBusinessTypes(
+ allTemplates.value,
+ moduleBusinessTypes.value
+ );
+ if (filtered.length) return filtered;
+ return allTemplates.value;
+ }
+ if (useAllTemplatesFallback.value) {
+ return allTemplates.value;
+ }
const businessType = currentTypeOption.value?.value;
return filterTemplatesByBusinessType(allTemplates.value, businessType);
});
@@ -134,8 +178,17 @@
});
const emptyText = computed(() => {
+ if (allTemplates.value.length === 0) {
+ return "鏆傛棤宸插惎鐢ㄧ殑瀹℃壒妯℃澘锛岃鍏堝湪銆屽鎵规ā鏉裤�嶄腑鍒涘缓骞跺惎鐢�";
+ }
+ if (moduleConfig.value?.label) {
+ return `鏆傛棤${moduleConfig.value.label}鍙敤妯℃澘`;
+ }
+ if (useAllTemplatesFallback.value) {
+ return "褰撳墠绫诲瀷涓嬫棤鍖归厤妯℃澘";
+ }
const typeName = currentTypeOption.value?.name || "璇ュ鎵圭被鍨�";
- return `鏆傛棤${typeName}涓嬬殑妯℃澘`;
+ return `鏆傛棤${typeName}涓嬬殑妯℃澘锛堝彲鍒囨崲涓婃柟绫诲瀷锛塦;
});
const businessTypeText = type =>
@@ -160,41 +213,46 @@
return count != null ? `${count} 涓猔 : "-";
};
- const normalizeList = data => {
- const list = Array.isArray(data)
- ? data
- : Array.isArray(data?.records)
- ? data.records
- : [];
- return list.filter(item => String(item?.enabled ?? "1") === "1");
- };
-
- const loadCustomTemplates = () =>
- listApprovalTemplateByType(CUSTOM_TEMPLATE_LIST_TYPE)
- .then(res => {
- allTemplates.value = normalizeList(res?.data);
- })
- .catch(() => {
- allTemplates.value = [];
- uni.showToast({ title: "鍔犺浇妯℃澘鍒楄〃澶辫触", icon: "none" });
- });
-
const initPage = async () => {
loading.value = true;
keyword.value = "";
allTemplates.value = [];
try {
- const [opts] = await Promise.all([
+ const [opts, templates] = await Promise.all([
fetchApprovalTemplateTypes(),
- loadCustomTemplates(),
+ fetchEnabledApprovalTemplates(),
]);
- typeOptions.value = opts;
- typeLabelMap.value = buildTypeLabelMap(opts);
- activeTab.value = getDefaultTypeTabIndex(opts);
+ let resolvedOpts = opts?.length ? opts : buildTypeOptionsFromTemplates(templates);
+ if (!resolvedOpts.length) {
+ resolvedOpts = [...FALLBACK_BUSINESS_TYPE_OPTIONS];
+ }
+ typeOptions.value = resolvedOpts;
+ typeLabelMap.value = buildTypeLabelMap(resolvedOpts);
+ allTemplates.value = templates;
+
+ if (!templates.length) {
+ uni.showToast({
+ title: "鏈幏鍙栧埌宸插惎鐢ㄦā鏉�",
+ icon: "none",
+ duration: 2500,
+ });
+ }
+
+ if (moduleKey.value) {
+ const resolved = resolveModuleBusinessType(moduleKey.value, opts);
+ const idx = opts.findIndex(
+ opt => String(opt.value) === String(resolved)
+ );
+ activeTab.value =
+ idx >= 0 ? idx : pickTabIndexWithTemplates(resolvedOpts, templates);
+ } else {
+ activeTab.value = pickTabIndexWithTemplates(resolvedOpts, templates);
+ }
} catch {
typeOptions.value = [];
typeLabelMap.value = {};
- uni.showToast({ title: "鑾峰彇瀹℃壒绫诲瀷澶辫触", icon: "none" });
+ allTemplates.value = [];
+ uni.showToast({ title: "鍔犺浇妯℃澘澶辫触", icon: "none" });
} finally {
loading.value = false;
}
@@ -215,12 +273,14 @@
uni.showToast({ title: "璇ユā鏉垮凡鍋滅敤", icon: "none" });
return;
}
+ const base = `${OA_NAV.approveListApply}?templateId=${item.id}`;
uni.navigateTo({
- url: `${OA_NAV.approveListApply}?templateId=${item.id}`,
+ url: moduleKey.value ? `${base}&moduleKey=${moduleKey.value}` : base,
});
};
- onLoad(() => {
+ onLoad(options => {
+ moduleKey.value = options?.moduleKey || "";
initPage();
});
</script>
@@ -234,6 +294,16 @@
min-height: 100vh;
}
+ .fallback-hint {
+ margin: 8px 12px 0;
+ padding: 8px 12px;
+ font-size: 12px;
+ color: #e6a23c;
+ background: #fdf6ec;
+ border-radius: 6px;
+ line-height: 1.5;
+ }
+
.step-section {
background: #fff;
border-bottom: 1px solid #f0f0f0;
diff --git a/src/pages/oa/AttendManage/leave-apply/index.vue b/src/pages/oa/AttendManage/leave-apply/index.vue
index 237b92b..feb5e4c 100644
--- a/src/pages/oa/AttendManage/leave-apply/index.vue
+++ b/src/pages/oa/AttendManage/leave-apply/index.vue
@@ -3,16 +3,10 @@
璺敱锛�/pages/oa/AttendManage/leave-apply/index
-->
<template>
- <OaListPage v-if="config"
- :page-key="pageKey"
- :page-config="config" />
+ <ApprovalInstanceListPage :module-key="APPROVAL_MODULE_KEYS.LEAVE" />
</template>
<script setup>
- /** OA - 鍋囧嫟绠$悊 - 璇峰亣鐢宠 */
- import OaListPage from "../../_components/OaListPage.vue";
- import { useOaPage } from "../../_utils/useOaPage.js";
-
- const pageKey = "AttendManage/leave-apply";
- const { config } = useOaPage(pageKey);
+ import ApprovalInstanceListPage from "../../_components/ApprovalInstanceListPage.vue";
+ import { APPROVAL_MODULE_KEYS } from "../../_utils/approvalModuleRegistry.js";
</script>
diff --git a/src/pages/oa/AttendManage/overtime-apply/index.vue b/src/pages/oa/AttendManage/overtime-apply/index.vue
index 04b071a..439ea26 100644
--- a/src/pages/oa/AttendManage/overtime-apply/index.vue
+++ b/src/pages/oa/AttendManage/overtime-apply/index.vue
@@ -3,16 +3,10 @@
璺敱锛�/pages/oa/AttendManage/overtime-apply/index
-->
<template>
- <OaListPage v-if="config"
- :page-key="pageKey"
- :page-config="config" />
+ <ApprovalInstanceListPage :module-key="APPROVAL_MODULE_KEYS.OVERTIME" />
</template>
<script setup>
- /** OA - 鍋囧嫟绠$悊 - 鍔犵彮鐢宠 */
- import OaListPage from "../../_components/OaListPage.vue";
- import { useOaPage } from "../../_utils/useOaPage.js";
-
- const pageKey = "AttendManage/overtime-apply";
- const { config } = useOaPage(pageKey);
+ import ApprovalInstanceListPage from "../../_components/ApprovalInstanceListPage.vue";
+ import { APPROVAL_MODULE_KEYS } from "../../_utils/approvalModuleRegistry.js";
</script>
diff --git a/src/pages/oa/HrManage/regular-apply/index.vue b/src/pages/oa/HrManage/regular-apply/index.vue
index ae962c6..e45364a 100644
--- a/src/pages/oa/HrManage/regular-apply/index.vue
+++ b/src/pages/oa/HrManage/regular-apply/index.vue
@@ -3,16 +3,10 @@
璺敱锛�/pages/oa/HrManage/regular-apply/index
-->
<template>
- <OaListPage v-if="config"
- :page-key="pageKey"
- :page-config="config" />
+ <ApprovalInstanceListPage :module-key="APPROVAL_MODULE_KEYS.REGULAR" />
</template>
<script setup>
- /** OA - 浜轰簨绠$悊 - 杞鐢宠 */
- import OaListPage from "../../_components/OaListPage.vue";
- import { useOaPage } from "../../_utils/useOaPage.js";
-
- const pageKey = "HrManage/regular-apply";
- const { config } = useOaPage(pageKey);
+ import ApprovalInstanceListPage from "../../_components/ApprovalInstanceListPage.vue";
+ import { APPROVAL_MODULE_KEYS } from "../../_utils/approvalModuleRegistry.js";
</script>
diff --git a/src/pages/oa/HrManage/transfer-apply/index.vue b/src/pages/oa/HrManage/transfer-apply/index.vue
index f3161bf..99ccacf 100644
--- a/src/pages/oa/HrManage/transfer-apply/index.vue
+++ b/src/pages/oa/HrManage/transfer-apply/index.vue
@@ -3,16 +3,10 @@
璺敱锛�/pages/oa/HrManage/transfer-apply/index
-->
<template>
- <OaListPage v-if="config"
- :page-key="pageKey"
- :page-config="config" />
+ <ApprovalInstanceListPage :module-key="APPROVAL_MODULE_KEYS.TRANSFER" />
</template>
<script setup>
- /** OA - 浜轰簨绠$悊 - 璋冨矖鐢宠 */
- import OaListPage from "../../_components/OaListPage.vue";
- import { useOaPage } from "../../_utils/useOaPage.js";
-
- const pageKey = "HrManage/transfer-apply";
- const { config } = useOaPage(pageKey);
+ import ApprovalInstanceListPage from "../../_components/ApprovalInstanceListPage.vue";
+ import { APPROVAL_MODULE_KEYS } from "../../_utils/approvalModuleRegistry.js";
</script>
diff --git a/src/pages/oa/HrManage/work-handover/index.vue b/src/pages/oa/HrManage/work-handover/index.vue
index c5d0e19..9fa24b6 100644
--- a/src/pages/oa/HrManage/work-handover/index.vue
+++ b/src/pages/oa/HrManage/work-handover/index.vue
@@ -3,16 +3,10 @@
璺敱锛�/pages/oa/HrManage/work-handover/index
-->
<template>
- <OaListPage v-if="config"
- :page-key="pageKey"
- :page-config="config" />
+ <ApprovalInstanceListPage :module-key="APPROVAL_MODULE_KEYS.WORK_HANDOVER" />
</template>
<script setup>
- /** OA - 浜轰簨绠$悊 - 宸ヤ綔浜ゆ帴 */
- import OaListPage from "../../_components/OaListPage.vue";
- import { useOaPage } from "../../_utils/useOaPage.js";
-
- const pageKey = "HrManage/work-handover";
- const { config } = useOaPage(pageKey);
+ import ApprovalInstanceListPage from "../../_components/ApprovalInstanceListPage.vue";
+ import { APPROVAL_MODULE_KEYS } from "../../_utils/approvalModuleRegistry.js";
</script>
diff --git a/src/pages/oa/_components/ApprovalInstanceListPage.vue b/src/pages/oa/_components/ApprovalInstanceListPage.vue
new file mode 100644
index 0000000..cdf0e39
--- /dev/null
+++ b/src/pages/oa/_components/ApprovalInstanceListPage.vue
@@ -0,0 +1,347 @@
+<!--
+ 涓氬姟瀹℃壒鐢宠鍒楄〃锛堣浆姝�/璋冨矖/浜ゆ帴/璇峰亣/鍔犵彮锛�
+-->
+<template>
+ <view class="oa-approval-page">
+ <PageHeader :title="pageTitle"
+ @back="goBack" />
+
+ <view class="oa-toolbar">
+ <view class="oa-filter-chip"
+ :class="{ active: hasActiveFilter }"
+ @click="showFilter = true">
+ <up-icon name="list"
+ size="18"
+ :color="hasActiveFilter ? '#2979ff' : '#666'" />
+ <text class="chip-label">绛涢��</text>
+ <text v-if="filterSummary"
+ class="chip-value">{{ filterSummary }}</text>
+ <text v-else
+ class="chip-placeholder">鍏ㄩ儴鏉′欢</text>
+ </view>
+ <view class="oa-icon-btn"
+ @click="handleSearch">
+ <up-icon name="search"
+ size="20"
+ color="#666" />
+ </view>
+ </view>
+
+ <ApprovalModuleSearchPopup v-model:show="showFilter"
+ :module-key="moduleKey"
+ v-model="searchForm"
+ @search="handleSearch"
+ @reset="handleReset" />
+
+ <scroll-view class="oa-list-scroll"
+ scroll-y
+ :show-scrollbar="false"
+ :style="{ height: listScrollHeight + 'px' }"
+ @scrolltolower="loadMore">
+ <view v-if="displayList.length"
+ class="oa-card-list">
+ <view v-for="item in displayList"
+ :key="item.id"
+ class="oa-card"
+ @click="openDetail(item)">
+ <view class="oa-card-head">
+ <view class="oa-card-title-wrap">
+ <text class="oa-card-title">{{ cardTitle(item) }}</text>
+ <text v-if="item.instanceNo"
+ class="oa-card-sub">{{ item.instanceNo }}</text>
+ </view>
+ <text :class="['oa-status', businessStatusClass(item.status)]">
+ {{ businessStatusText(item.status) }}
+ </text>
+ </view>
+
+ <view class="oa-card-body">
+ <view class="oa-info-grid">
+ <view v-for="(row, idx) in visibleDisplayRows(item)"
+ :key="'f-' + idx"
+ class="oa-info-row">
+ <text class="oa-info-label">{{ row.label }}</text>
+ <text class="oa-info-value">{{ row.value || "-" }}</text>
+ </view>
+ <view class="oa-info-row">
+ <text class="oa-info-label">鐢宠浜�</text>
+ <text class="oa-info-value">{{ item.applicantName || "-" }}</text>
+ </view>
+ <view class="oa-info-row">
+ <text class="oa-info-label">鐢宠鏃堕棿</text>
+ <text class="oa-info-value">{{ item.createTime || "-" }}</text>
+ </view>
+ </view>
+ </view>
+
+ <view v-if="canEditBusinessInstanceRow(item)"
+ class="oa-card-foot"
+ @click.stop>
+ <text class="oa-foot-btn btn-edit"
+ @click="goEdit(item)">淇敼</text>
+ <text class="oa-foot-btn btn-delete"
+ @click="confirmDelete(item)">鍒犻櫎</text>
+ </view>
+ </view>
+ <up-loadmore :status="pageStatus" />
+ </view>
+
+ <view v-else-if="!tableLoading"
+ class="oa-empty">
+ <up-empty mode="list"
+ :text="`鏆傛棤${pageTitle}鏁版嵁`" />
+ </view>
+ <view v-if="tableLoading && !list.length"
+ class="oa-loading">
+ <up-loading-icon mode="circle" />
+ </view>
+ </scroll-view>
+
+ <view class="fab-button"
+ @click="handleAdd">
+ <up-icon name="plus"
+ size="28"
+ color="#ffffff" />
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { computed, onMounted, reactive, ref } from "vue";
+ import { onShow } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import ApprovalModuleSearchPopup from "./ApprovalModuleSearchPopup.vue";
+ import {
+ deleteApprovalInstance,
+ listApprovalInstancePage,
+ } from "@/api/oa/approvalInstance.js";
+ import { OA_NAV } from "@/config/oaPaths.js";
+ import { fetchApprovalTemplateTypes } from "../_utils/approvalTemplateType.js";
+ import {
+ getApprovalModuleConfig,
+ resolveModuleBusinessType,
+ } from "../_utils/approvalModuleRegistry.js";
+ import {
+ buildModuleListDto,
+ createModuleSearchForm,
+ filterRowsByModuleSearch,
+ filterRowsByModuleBusinessType,
+ formatDateRangeLabel,
+ getModuleSearchMeta,
+ } from "../_utils/approvalModuleListSearch.js";
+ import {
+ buildInstanceListParams,
+ businessStatusClass,
+ businessStatusText,
+ canEditBusinessInstanceRow,
+ EDIT_STORAGE_KEY,
+ mapInstanceListRow,
+ stashInstanceRow,
+ unwrapInstancePage,
+ } from "../_utils/approveListUtils.js";
+
+ const props = defineProps({
+ moduleKey: { type: String, required: true },
+ });
+
+ const moduleConfig = computed(() => getApprovalModuleConfig(props.moduleKey));
+ const pageTitle = computed(() => moduleConfig.value?.label || "鐢宠");
+
+ const showFilter = ref(false);
+ const searchForm = reactive(createModuleSearchForm(props.moduleKey));
+ const list = ref([]);
+ const tableLoading = ref(false);
+ const pageStatus = ref("loadmore");
+ const businessType = ref("");
+ const typeOptions = ref([]);
+
+ const page = reactive({ current: 1, size: 10, total: 0 });
+ const listScrollHeight = ref(400);
+
+ function calcListScrollHeight() {
+ const sys = uni.getSystemInfoSync();
+ const statusBar = sys.statusBarHeight || 0;
+ const navBar = 44;
+ const toolbar = 56;
+ const fabGap = 16;
+ listScrollHeight.value = Math.max(
+ 200,
+ sys.windowHeight - statusBar - navBar - toolbar - fabGap
+ );
+ }
+
+ const displayList = computed(() => {
+ const byType = filterRowsByModuleBusinessType(
+ props.moduleKey,
+ list.value,
+ typeOptions.value
+ );
+ return filterRowsByModuleSearch(props.moduleKey, byType, searchForm);
+ });
+
+ const hasActiveFilter = computed(() => Boolean(filterSummary.value));
+
+ const filterSummary = computed(() => {
+ const parts = [];
+ const meta = getModuleSearchMeta(props.moduleKey);
+ for (const field of meta.fields || []) {
+ const val = searchForm[field.key];
+ if (field.type === "input" && (val || "").trim()) {
+ parts.push(`${field.label}:${String(val).trim()}`);
+ } else if (field.type === "daterange" && Array.isArray(val) && val[0]) {
+ parts.push(`${field.label}:${formatDateRangeLabel(val)}`);
+ } else if (field.type === "select" && val) {
+ const opt = (field.options || []).find(o => o.value === val);
+ parts.push(`${field.label}:${opt?.label || val}`);
+ } else if (field.type === "user" && val) {
+ parts.push(`${field.label}:宸查�塦);
+ }
+ }
+ return parts.join("锛�");
+ });
+
+ function cardTitle(item) {
+ return item.summary || item.title || pageTitle.value;
+ }
+
+ function visibleDisplayRows(item) {
+ const rows = item.displayRows || [];
+ return rows.slice(0, 2);
+ }
+
+ const buildListRequestParams = () => {
+ const extraDto = buildModuleListDto(props.moduleKey, searchForm);
+ return buildInstanceListParams({
+ page,
+ businessType: businessType.value,
+ extraDto,
+ searchForm,
+ });
+ };
+
+ const fetchList = async (reset = false) => {
+ if (reset) {
+ page.current = 1;
+ pageStatus.value = "loadmore";
+ list.value = [];
+ }
+ if (pageStatus.value === "loading" || pageStatus.value === "nomore") return;
+ if (!businessType.value && businessType.value !== 0) return;
+
+ pageStatus.value = "loading";
+ tableLoading.value = true;
+
+ try {
+ const res = await listApprovalInstancePage(buildListRequestParams());
+ const { records, total } = unwrapInstancePage(res);
+ const listFields = moduleConfig.value?.listFields || [];
+ const mapped = records.map(row => mapInstanceListRow(row, listFields));
+
+ if (page.current === 1) {
+ list.value = mapped;
+ } else {
+ list.value = [...list.value, ...mapped];
+ }
+ page.total = total;
+
+ if (list.value.length >= total || records.length < page.size) {
+ pageStatus.value = "nomore";
+ } else {
+ pageStatus.value = "loadmore";
+ page.current += 1;
+ }
+ } catch {
+ if (page.current === 1) list.value = [];
+ pageStatus.value = "loadmore";
+ uni.showToast({ title: `${pageTitle.value}鍔犺浇澶辫触`, icon: "none" });
+ } finally {
+ tableLoading.value = false;
+ }
+ };
+
+ const initBusinessType = async () => {
+ try {
+ typeOptions.value = await fetchApprovalTemplateTypes();
+ const resolved = resolveModuleBusinessType(props.moduleKey, typeOptions.value);
+ businessType.value =
+ resolved != null && resolved !== ""
+ ? resolved
+ : moduleConfig.value?.approvalType ?? "";
+ } catch {
+ businessType.value = moduleConfig.value?.approvalType ?? "";
+ }
+ };
+
+ const handleSearch = () => fetchList(true);
+ const handleReset = () => {
+ Object.assign(searchForm, createModuleSearchForm(props.moduleKey));
+ fetchList(true);
+ };
+ const loadMore = () => {
+ if (pageStatus.value === "loadmore") fetchList(false);
+ };
+ const goBack = () => uni.navigateBack();
+
+ const openDetail = item => {
+ if (!item?.id) return;
+ stashInstanceRow(item);
+ uni.navigateTo({
+ url: `${OA_NAV.approveListDetail}?id=${item.id}&from=business`,
+ });
+ };
+
+ const goEdit = item => {
+ if (!canEditBusinessInstanceRow(item)) {
+ uni.showToast({ title: "杩涜涓垨宸插畬鎴愮殑瀹℃壒涓嶅彲淇敼", icon: "none" });
+ return;
+ }
+ if (!item?.id) return;
+ uni.setStorageSync(EDIT_STORAGE_KEY, item);
+ stashInstanceRow(item);
+ uni.navigateTo({
+ url: `${OA_NAV.approveListApply}?id=${item.id}&moduleKey=${props.moduleKey}`,
+ });
+ };
+
+ const confirmDelete = item => {
+ if (!item?.id) return;
+ const title = item.title || item.templateName || item.instanceNo || "璇ュ鎵�";
+ uni.showModal({
+ title: "鍒犻櫎纭",
+ content: `纭畾瑕佸垹闄ゃ��${title}銆嶅悧锛熷垹闄ゅ悗涓嶅彲鎭㈠銆俙,
+ confirmText: "纭畾鍒犻櫎",
+ confirmColor: "#f56c6c",
+ success: async res => {
+ if (!res.confirm) return;
+ try {
+ await deleteApprovalInstance([item.id]);
+ uni.showToast({ title: "鍒犻櫎鎴愬姛", icon: "success" });
+ fetchList(true);
+ } catch {
+ uni.showToast({ title: "鍒犻櫎澶辫触", icon: "none" });
+ }
+ },
+ });
+ };
+
+ const handleAdd = () => {
+ uni.navigateTo({
+ url: `${OA_NAV.approveListTemplateSelect}?moduleKey=${props.moduleKey}`,
+ });
+ };
+
+ onMounted(() => {
+ calcListScrollHeight();
+ });
+
+ onShow(async () => {
+ calcListScrollHeight();
+ await initBusinessType();
+ fetchList(true);
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/sales-common.scss";
+ @import "../_styles/oa-approval-list.scss";
+</style>
diff --git a/src/pages/oa/_components/ApprovalModuleSearchPopup.vue b/src/pages/oa/_components/ApprovalModuleSearchPopup.vue
new file mode 100644
index 0000000..522bd02
--- /dev/null
+++ b/src/pages/oa/_components/ApprovalModuleSearchPopup.vue
@@ -0,0 +1,268 @@
+<!--
+ 涓氬姟瀹℃壒鍒楄〃绛涢�夊脊绐�
+-->
+<template>
+ <up-popup :show="modelShow"
+ mode="bottom"
+ round
+ @close="closePopup">
+ <view class="oa-filter-popup">
+ <view class="oa-filter-head">绛涢�夋潯浠�</view>
+ <scroll-view scroll-y
+ class="oa-filter-scroll">
+ <view v-for="field in fields"
+ :key="field.key"
+ class="oa-filter-field">
+ <text class="oa-field-label">{{ field.label }}</text>
+
+ <up-input v-if="field.type === 'input'"
+ v-model="localForm[field.key]"
+ :placeholder="field.placeholder || `璇疯緭鍏�${field.label}`"
+ clearable
+ border="surround" />
+
+ <view v-else-if="field.type === 'daterange'"
+ class="oa-picker-row"
+ @click="openDateRange(field.key)">
+ <text :class="{ placeholder: !dateLabel(field.key) }">
+ {{ dateLabel(field.key) || `璇烽�夋嫨${field.label}` }}
+ </text>
+ <up-icon name="calendar"
+ size="18"
+ color="#999" />
+ </view>
+
+ <view v-else-if="field.type === 'user'"
+ class="oa-picker-row"
+ @click="openUserPicker">
+ <text :class="{ placeholder: !userLabel }">
+ {{ userLabel || field.placeholder || '璇烽�夋嫨鐢宠浜�' }}
+ </text>
+ <up-icon name="arrow-down"
+ size="14"
+ color="#999" />
+ </view>
+
+ <view v-else-if="field.type === 'select'"
+ class="oa-picker-row"
+ @click="openSelect(field)">
+ <text :class="{ placeholder: !selectLabel(field) }">
+ {{ selectLabel(field) || `璇烽�夋嫨${field.label}` }}
+ </text>
+ <up-icon name="arrow-down"
+ size="14"
+ color="#999" />
+ </view>
+ </view>
+ </scroll-view>
+
+ <view class="oa-filter-actions">
+ <up-button text="閲嶇疆"
+ shape="circle"
+ @click="handleReset" />
+ <up-button type="primary"
+ text="纭畾"
+ shape="circle"
+ @click="handleConfirm" />
+ </view>
+ </view>
+
+ <up-calendar :show="calendarShow"
+ mode="range"
+ :maxDate="maxDate"
+ minDate="2020-01-01"
+ :monthNum="24"
+ @confirm="onDateConfirm"
+ @close="calendarShow = false" />
+
+ <up-picker :show="pickerShow"
+ :columns="pickerColumns"
+ keyName="label"
+ @confirm="onPickerConfirm"
+ @cancel="pickerShow = false"
+ @close="pickerShow = false" />
+
+ <up-popup :show="userPickerShow"
+ mode="bottom"
+ round
+ @close="userPickerShow = false">
+ <view class="oa-user-popup">
+ <view class="oa-user-popup-title">閫夋嫨鐢宠浜�</view>
+ <up-input v-model="userKeyword"
+ placeholder="鎼滅储濮撳悕鎴栫紪鍙�"
+ clearable
+ border="surround" />
+ <scroll-view scroll-y
+ class="oa-user-list">
+ <view v-for="u in filteredUsers"
+ :key="u.userId ?? u.id"
+ class="oa-user-item"
+ @click="pickUser(u)">
+ <text>{{ userSelectLabel(u) }}</text>
+ </view>
+ <view v-if="!filteredUsers.length"
+ class="oa-user-empty">鏆傛棤鍖归厤鐢ㄦ埛</view>
+ </scroll-view>
+ </view>
+ </up-popup>
+ </up-popup>
+</template>
+
+<script setup>
+ import { computed, reactive, ref, watch } from "vue";
+ import dayjs from "dayjs";
+ import { userListNoPageByTenantId } from "@/api/system/user";
+ import {
+ formatDateRangeLabel,
+ getModuleSearchMeta,
+ resetModuleSearchForm,
+ userSelectLabel,
+ } from "../_utils/approvalModuleListSearch.js";
+
+ const props = defineProps({
+ show: { type: Boolean, default: false },
+ moduleKey: { type: String, required: true },
+ modelValue: { type: Object, default: () => ({}) },
+ });
+
+ const emit = defineEmits(["update:show", "update:modelValue", "search", "reset"]);
+
+ const modelShow = computed({
+ get: () => props.show,
+ set: v => emit("update:show", v),
+ });
+
+ function closePopup() {
+ emit("update:show", false);
+ }
+
+ const localForm = reactive({});
+ const calendarShow = ref(false);
+ const activeDateKey = ref("applyDateRange");
+ const maxDate = dayjs().format("YYYY-MM-DD");
+ const pickerShow = ref(false);
+ const pickerColumns = ref([[]]);
+ const activeSelectField = ref(null);
+ const userPickerShow = ref(false);
+ const userKeyword = ref("");
+ const allUsers = ref([]);
+
+ const fields = computed(() => getModuleSearchMeta(props.moduleKey).fields || []);
+
+ const userLabel = computed(() => {
+ const id = localForm.applicantId;
+ if (!id) return "";
+ const u = allUsers.value.find(x => String(x.userId ?? x.id) === String(id));
+ return u ? userSelectLabel(u) : String(id);
+ });
+
+ const filteredUsers = computed(() => {
+ const q = userKeyword.value.trim().toLowerCase();
+ const list = allUsers.value.filter(u => {
+ if (u.delFlag === "2" || u.delFlag === 2) return false;
+ if (u.status != null && String(u.status) !== "0") return false;
+ return true;
+ });
+ if (!q) return list.slice(0, 50);
+ return list
+ .filter(u => {
+ const nick = (u.nickName || "").toLowerCase();
+ const name = (u.userName || "").toLowerCase();
+ const id = String(u.userId ?? u.id ?? "");
+ return nick.includes(q) || name.includes(q) || id.includes(q);
+ })
+ .slice(0, 50);
+ });
+
+ function unwrapUsers(res) {
+ if (Array.isArray(res)) return res;
+ if (Array.isArray(res?.data)) return res.data;
+ if (Array.isArray(res?.rows)) return res.rows;
+ return [];
+ }
+
+ async function loadUsers() {
+ if (allUsers.value.length) return;
+ try {
+ const res = await userListNoPageByTenantId();
+ allUsers.value = unwrapUsers(res);
+ } catch {
+ allUsers.value = [];
+ }
+ }
+
+ watch(
+ () => props.show,
+ v => {
+ if (!v) return;
+ Object.assign(localForm, props.modelValue || {});
+ }
+ );
+
+ function dateLabel(key) {
+ return formatDateRangeLabel(localForm[key]);
+ }
+
+ function openDateRange(key) {
+ activeDateKey.value = key;
+ calendarShow.value = true;
+ }
+
+ function onDateConfirm(e) {
+ if (!e?.length) {
+ calendarShow.value = false;
+ return;
+ }
+ localForm[activeDateKey.value] = [e[0], e[e.length - 1]];
+ calendarShow.value = false;
+ }
+
+ function selectLabel(field) {
+ const val = localForm[field.key];
+ if (!val) return "";
+ const opt = (field.options || []).find(o => o.value === val);
+ return opt?.label || String(val);
+ }
+
+ function openSelect(field) {
+ activeSelectField.value = field;
+ pickerColumns.value = [field.options || []];
+ pickerShow.value = true;
+ }
+
+ function onPickerConfirm(e) {
+ const item = e?.value?.[0];
+ if (activeSelectField.value?.key && item) {
+ localForm[activeSelectField.value.key] = item.value;
+ }
+ pickerShow.value = false;
+ }
+
+ async function openUserPicker() {
+ await loadUsers();
+ userKeyword.value = "";
+ userPickerShow.value = true;
+ }
+
+ function pickUser(u) {
+ localForm.applicantId = u.userId ?? u.id ?? "";
+ userPickerShow.value = false;
+ }
+
+ function handleReset() {
+ resetModuleSearchForm(props.moduleKey, localForm);
+ emit("update:modelValue", { ...localForm });
+ emit("reset", { ...localForm });
+ emit("update:show", false);
+ }
+
+ function handleConfirm() {
+ emit("update:modelValue", { ...localForm });
+ emit("search", { ...localForm });
+ emit("update:show", false);
+ }
+</script>
+
+<style scoped lang="scss">
+ @import "../_styles/oa-approval-list.scss";
+</style>
diff --git a/src/pages/oa/_styles/oa-approval-list.scss b/src/pages/oa/_styles/oa-approval-list.scss
new file mode 100644
index 0000000..b130fbc
--- /dev/null
+++ b/src/pages/oa/_styles/oa-approval-list.scss
@@ -0,0 +1,409 @@
+// OA 瀹℃壒鍒楄〃锛堜笟鍔$敵璇� + 瀹℃壒鍒楄〃锛夌粺涓�鏍峰紡
+
+.oa-approval-page {
+ min-height: 100vh;
+ background: #f2f4f7;
+ display: flex;
+ flex-direction: column;
+ box-sizing: border-box;
+}
+
+.oa-list-scroll {
+ width: 100%;
+ box-sizing: border-box;
+}
+
+.oa-toolbar {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 16px;
+ background: #fff;
+ border-bottom: 1px solid #eef0f3;
+}
+
+.oa-filter-chip {
+ flex: 1;
+ min-height: 40px;
+ padding: 0 14px;
+ background: #f5f7fa;
+ border-radius: 20px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ border: 1px solid transparent;
+
+ &.active {
+ background: #ecf3ff;
+ border-color: #b3d4ff;
+ }
+
+ .chip-label {
+ font-size: 14px;
+ color: #333;
+ font-weight: 500;
+ flex-shrink: 0;
+ }
+
+ .chip-value {
+ flex: 1;
+ font-size: 13px;
+ color: #666;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .chip-placeholder {
+ flex: 1;
+ font-size: 13px;
+ color: #aaa;
+ }
+}
+
+.oa-icon-btn {
+ width: 40px;
+ height: 40px;
+ border-radius: 20px;
+ background: #f5f7fa;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+
+ &:active {
+ background: #e8ebf0;
+ }
+}
+
+.oa-card-list {
+ padding: 12px 16px 4px;
+}
+
+.oa-card {
+ background: #fff;
+ border-radius: 12px;
+ margin-bottom: 12px;
+ overflow: hidden;
+ box-shadow: 0 1px 4px rgba(15, 23, 42, 0.06);
+
+ &:active {
+ opacity: 0.92;
+ }
+}
+
+.oa-card-head {
+ padding: 14px 14px 10px;
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 10px;
+}
+
+.oa-card-title-wrap {
+ flex: 1;
+ min-width: 0;
+}
+
+.oa-card-title {
+ display: block;
+ font-size: 15px;
+ font-weight: 600;
+ color: #1a1a1a;
+ line-height: 1.4;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.oa-card-sub {
+ display: block;
+ margin-top: 4px;
+ font-size: 12px;
+ color: #8c8c8c;
+}
+
+.oa-status {
+ flex-shrink: 0;
+ font-size: 11px;
+ line-height: 1;
+ padding: 5px 8px;
+ border-radius: 4px;
+ font-weight: 500;
+
+ &.status-pending {
+ color: #d46b08;
+ background: #fff7e6;
+ }
+
+ &.status-approved {
+ color: #389e0d;
+ background: #f6ffed;
+ }
+
+ &.status-rejected {
+ color: #cf1322;
+ background: #fff1f0;
+ }
+
+ &.status-draft {
+ color: #595959;
+ background: #f5f5f5;
+ }
+
+ &.status-cancelled {
+ color: #8c8c8c;
+ background: #fafafa;
+ }
+}
+
+.oa-card-body {
+ padding: 0 14px 12px;
+}
+
+.oa-info-grid {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.oa-info-row {
+ display: flex;
+ align-items: baseline;
+ font-size: 13px;
+ line-height: 1.45;
+}
+
+.oa-info-label {
+ width: 72px;
+ flex-shrink: 0;
+ color: #8c8c8c;
+}
+
+.oa-info-value {
+ flex: 1;
+ color: #333;
+ word-break: break-all;
+}
+
+.oa-card-foot {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 10px;
+ padding: 10px 14px;
+ background: #fafbfc;
+ border-top: 1px solid #f0f2f5;
+}
+
+.oa-foot-btn {
+ min-width: 64px;
+ height: 32px;
+ line-height: 32px;
+ padding: 0 14px;
+ font-size: 13px;
+ border-radius: 16px;
+ text-align: center;
+
+ &.btn-edit {
+ color: #2979ff;
+ background: #ecf3ff;
+ }
+
+ &.btn-delete {
+ color: #ff4d4f;
+ background: #fff1f0;
+ }
+
+ &.btn-approve {
+ color: #fff;
+ background: #2979ff;
+ }
+}
+
+.oa-empty,
+.oa-loading {
+ padding: 48px 20px;
+}
+
+.oa-loading {
+ display: flex;
+ justify-content: center;
+}
+
+// 绛涢�夊脊绐�
+.oa-filter-popup {
+ padding: 16px 16px calc(16px + env(safe-area-inset-bottom));
+ max-height: 72vh;
+ display: flex;
+ flex-direction: column;
+ background: #fff;
+}
+
+.oa-filter-head {
+ text-align: center;
+ font-size: 16px;
+ font-weight: 600;
+ color: #1a1a1a;
+ padding-bottom: 12px;
+ border-bottom: 1px solid #f0f2f5;
+ margin-bottom: 12px;
+}
+
+.oa-filter-scroll {
+ max-height: 50vh;
+}
+
+.oa-filter-field {
+ margin-bottom: 16px;
+}
+
+.oa-field-label {
+ display: block;
+ font-size: 13px;
+ color: #666;
+ margin-bottom: 8px;
+}
+
+.oa-picker-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ min-height: 44px;
+ padding: 0 14px;
+ background: #f5f7fa;
+ border-radius: 8px;
+ font-size: 14px;
+ color: #333;
+
+ .placeholder {
+ color: #bbb;
+ }
+}
+
+.oa-filter-actions {
+ display: flex;
+ gap: 12px;
+ margin-top: 16px;
+ padding-top: 12px;
+}
+
+.oa-filter-actions :deep(.u-button) {
+ flex: 1;
+ border-radius: 22px !important;
+}
+
+.oa-user-popup {
+ padding: 16px;
+ max-height: 60vh;
+ background: #fff;
+}
+
+.oa-user-popup-title {
+ text-align: center;
+ font-weight: 600;
+ font-size: 16px;
+ margin-bottom: 12px;
+}
+
+.oa-user-list {
+ max-height: 40vh;
+ margin-top: 10px;
+}
+
+.oa-user-item {
+ padding: 14px 4px;
+ font-size: 14px;
+ color: #333;
+ border-bottom: 1px solid #f0f2f5;
+}
+
+.oa-user-empty {
+ text-align: center;
+ color: #999;
+ padding: 24px;
+ font-size: 13px;
+}
+
+// 璇︽儏 / 瀹℃壒澶勭悊绛夐〉搴曢儴鎿嶄綔鏍�
+.oa-page-footer {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 100;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 16px calc(10px + env(safe-area-inset-bottom));
+ background: #fff;
+ border-top: 1px solid #eef0f3;
+ box-shadow: 0 -4px 16px rgba(15, 23, 42, 0.08);
+}
+
+.oa-footer-btn {
+ flex: 1;
+ min-width: 0;
+ height: 44px;
+ line-height: 44px;
+ text-align: center;
+ font-size: 15px;
+ font-weight: 500;
+ border-radius: 22px;
+ border: none;
+
+ &:active {
+ opacity: 0.85;
+ }
+
+ &.btn-default {
+ color: #666;
+ background: #f2f4f7;
+ }
+
+ &.btn-primary {
+ color: #fff;
+ background: linear-gradient(140deg, #00baff 0%, #006cfb 100%);
+ box-shadow: 0 4px 10px rgba(0, 108, 251, 0.25);
+ }
+
+ &.btn-warn {
+ color: #fff;
+ background: linear-gradient(140deg, #ffb347 0%, #ff9800 100%);
+ box-shadow: 0 4px 10px rgba(255, 152, 0, 0.25);
+ }
+
+ &.btn-success {
+ color: #fff;
+ background: linear-gradient(140deg, #52c41a 0%, #389e0d 100%);
+ box-shadow: 0 4px 10px rgba(56, 158, 13, 0.25);
+ }
+
+ &.btn-danger {
+ color: #fff;
+ background: linear-gradient(140deg, #ff7875 0%, #f5222d 100%);
+ box-shadow: 0 4px 10px rgba(245, 34, 45, 0.2);
+ }
+
+ &.is-disabled {
+ opacity: 0.5;
+ pointer-events: none;
+ }
+}
+
+.oa-detail-page {
+ min-height: 100vh;
+ background: #f2f4f7;
+ display: flex;
+ flex-direction: column;
+}
+
+.oa-detail-scroll {
+ flex: 1;
+ height: 0;
+ padding: 10px 12px;
+ padding-bottom: calc(76px + env(safe-area-inset-bottom));
+ box-sizing: border-box;
+}
diff --git a/src/pages/oa/_utils/approvalModuleApplyExtras.js b/src/pages/oa/_utils/approvalModuleApplyExtras.js
new file mode 100644
index 0000000..820d584
--- /dev/null
+++ b/src/pages/oa/_utils/approvalModuleApplyExtras.js
@@ -0,0 +1,293 @@
+import dayjs from "dayjs";
+import {
+ APPROVAL_MODULE_KEYS,
+ APPROVAL_MODULE_REGISTRY,
+ getModuleMatchingBusinessTypes,
+} from "./approvalModuleRegistry.js";
+import { matchBusinessTypeValue } from "./approvalTemplateType.js";
+import { parseDatetimerangeValue } from "./approvalFormField.js";
+
+/** 浜哄憳涓嬫媺瀛楁璇嗗埆锛堜笌 Web SELECT_OPTION_SOURCE.USER 绛変环锛� */
+export function isUserSelectField(field) {
+ const src = String(field?.optionSource ?? "").toLowerCase();
+ return (
+ src === "user" ||
+ src === "personnel" ||
+ src === "userlist" ||
+ (field?.type === "select" && String(field?.label || "").includes("鐢宠浜�"))
+ );
+}
+
+export function findApplicantTemplateField(fields = []) {
+ return (
+ fields.find(f => String(f?.label || "").includes("鐢宠浜�")) ||
+ fields.find(f => isUserSelectField(f)) ||
+ null
+ );
+}
+
+/* ---------- 璇峰亣 ---------- */
+
+export function isLeaveBalanceField(field) {
+ const label = String(field?.label || "");
+ return label.includes("鍋囨湡浣欓") || field?.key === "leaveBalanceDays";
+}
+
+export function isLeaveDurationField(field) {
+ const label = String(field?.label || "");
+ return label.includes("璇峰亣鏃堕暱") || field?.key === "leaveDurationDays";
+}
+
+export function displayLeaveTemplateFields(fields = []) {
+ return (fields || []).filter(
+ f => !isLeaveBalanceField(f) && !isLeaveDurationField(f)
+ );
+}
+
+export function findLeaveTimeTemplateField(fields = []) {
+ return (
+ fields.find(
+ f => f?.type === "datetimerange" && String(f?.label || "").includes("璇峰亣鏃堕棿")
+ ) ||
+ fields.find(f => f?.type === "datetimerange" && f?.key === "dateRange") ||
+ fields.find(f => f?.type === "datetimerange") ||
+ null
+ );
+}
+
+export function resolveTimeRangeFromPayload(payload, timeField) {
+ if (!timeField?.key) return { start: "", end: "" };
+ const val = payload?.[timeField.key];
+ if (Array.isArray(val) && val.length >= 2) {
+ return { start: val[0] || "", end: val[1] || "" };
+ }
+ return parseDatetimerangeValue(val);
+}
+
+export 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;
+}
+
+export function computeLeaveDurationDisplay(fields, formPayload) {
+ const timeField = findLeaveTimeTemplateField(fields);
+ const { start, end } = resolveTimeRangeFromPayload(formPayload, timeField);
+ const d = computeLeaveDays(start, end);
+ return d == null ? "" : String(d);
+}
+
+export function validateLeaveBeforeSubmit(fields, formPayload) {
+ const timeField = findLeaveTimeTemplateField(fields);
+ const { start, end } = resolveTimeRangeFromPayload(formPayload, timeField);
+ if (computeLeaveDays(start, end) == null) {
+ return "璇锋鏌ユā鏉夸腑鐨勮鍋囨椂闂达紝缁撴潫鏃堕棿椤绘櫄浜庡紑濮嬫椂闂�";
+ }
+ return "";
+}
+
+/* ---------- 鍔犵彮 ---------- */
+
+export function isOvertimeDurationField(field) {
+ const label = String(field?.label || "");
+ return label.includes("鍔犵彮鏃堕暱") || field?.key === "overtimeHours";
+}
+
+export function displayOvertimeTemplateFields(fields = []) {
+ return (fields || []).filter(f => !isOvertimeDurationField(f));
+}
+
+export function findOvertimeTimeTemplateField(fields = []) {
+ return (
+ fields.find(
+ f => f?.type === "datetimerange" && String(f?.label || "").includes("鍔犵彮鏃堕棿")
+ ) ||
+ fields.find(f => f?.type === "datetimerange") ||
+ null
+ );
+}
+
+export 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;
+ return Math.round((t1.diff(t0, "millisecond") / 3600000) * 100) / 100;
+}
+
+export function computeOvertimeHoursDisplay(fields, formPayload) {
+ const field = findOvertimeTimeTemplateField(fields);
+ const { start, end } = resolveTimeRangeFromPayload(formPayload, field);
+ const h = computeOvertimeHours(start, end);
+ return h == null ? "" : String(h);
+}
+
+export function validateOvertimeBeforeSubmit(fields, formPayload) {
+ const field = findOvertimeTimeTemplateField(fields);
+ const { start, end } = resolveTimeRangeFromPayload(formPayload, field);
+ if (computeOvertimeHours(start, end) == null) {
+ return "璇锋鏌ユā鏉夸腑鐨勫姞鐝椂闂达紝缁撴潫鏃堕棿椤绘櫄浜庡紑濮嬫椂闂�";
+ }
+ return "";
+}
+
+/* ---------- 璋冨矖 ---------- */
+
+export function isOriginalPostField(field) {
+ const label = String(field?.label || "");
+ return (
+ label.includes("鍘熷矖浣�") ||
+ field?.key === "originalPost" ||
+ field?.key === "originalPostName" ||
+ field?.key === "originalPostId"
+ );
+}
+
+export function displayTransferTemplateFields(fields = []) {
+ return (fields || []).filter(f => !isOriginalPostField(f));
+}
+
+export function unwrapUserArray(payload) {
+ if (Array.isArray(payload)) return payload;
+ if (payload?.data && Array.isArray(payload.data)) return payload.data;
+ if (payload?.rows && Array.isArray(payload.rows)) return payload.rows;
+ return [];
+}
+
+export function isActiveUser(u) {
+ if (u?.delFlag === "2" || u?.delFlag === 2) return false;
+ if (u?.status == null) return true;
+ return String(u.status) === "0";
+}
+
+export 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;
+}
+
+export function buildPostIdToNameMap(postRows = []) {
+ const m = {};
+ for (const p of postRows) {
+ const id = p.postId ?? p.value ?? p.id;
+ if (id != null && id !== "") {
+ m[String(id)] = p.postName ?? p.label ?? p.name ?? "";
+ }
+ }
+ return m;
+}
+
+export function resolveOriginalPostName(user, postIdToName = {}) {
+ if (!user) return "";
+ const nameStr = (user.postName ?? user.postname ?? "").toString().trim();
+ if (nameStr) return nameStr;
+ if (Array.isArray(user.posts) && user.posts.length) {
+ return (user.posts[0].postName ?? "").toString() || "鏈懡鍚嶅矖浣�";
+ }
+ const pid = firstPostId(user);
+ if (pid != null && pid !== "") {
+ const n = postIdToName[String(pid)] || "";
+ return n || "褰撳墠宀椾綅锛堟湭鍦ㄥ矖浣嶅瓧鍏镐腑锛�";
+ }
+ return "鏈垎閰嶅矖浣�";
+}
+
+export function userById(users, id) {
+ if (id == null || id === "") return undefined;
+ return (users || []).find(u => String(u.userId ?? u.id) === String(id));
+}
+
+/** 鎸� moduleKey 杩囨护妯℃澘濉姤椤� */
+export function displayTemplateFieldsByModule(moduleKey, fields = []) {
+ if (moduleKey === APPROVAL_MODULE_KEYS.LEAVE) {
+ return displayLeaveTemplateFields(fields);
+ }
+ if (moduleKey === APPROVAL_MODULE_KEYS.OVERTIME) {
+ return displayOvertimeTemplateFields(fields);
+ }
+ if (moduleKey === APPROVAL_MODULE_KEYS.TRANSFER) {
+ return displayTransferTemplateFields(fields);
+ }
+ return fields || [];
+}
+
+/** 淇濆瓨鍓嶅皢涓氬姟鎵╁睍瀛楁鍐欏叆 formValues */
+export function syncModuleExtrasToFormValues(moduleKey, formValues, extras, fields) {
+ if (!moduleKey || !formValues) return;
+ if (moduleKey === APPROVAL_MODULE_KEYS.LEAVE) {
+ if (extras.leaveBalanceDays != null && extras.leaveBalanceDays !== "") {
+ formValues.leaveBalanceDays = extras.leaveBalanceDays;
+ }
+ const days = computeLeaveDurationDisplay(fields, formValues);
+ if (days) formValues.leaveDurationDays = days;
+ }
+ if (moduleKey === APPROVAL_MODULE_KEYS.OVERTIME) {
+ const hours = computeOvertimeHoursDisplay(fields, formValues);
+ if (hours) formValues.overtimeHours = hours;
+ }
+ if (moduleKey === APPROVAL_MODULE_KEYS.TRANSFER && extras.originalPostName) {
+ formValues.originalPostName = extras.originalPostName;
+ formValues.originalPost = extras.originalPostName;
+ }
+}
+
+/** 涓氬姟鎵╁睍鏍¢獙 */
+export function validateModuleExtras(moduleKey, fields, formPayload, extras) {
+ if (moduleKey === APPROVAL_MODULE_KEYS.LEAVE) {
+ if (
+ extras.leaveBalanceDays == null ||
+ extras.leaveBalanceDays === "" ||
+ Number.isNaN(Number(extras.leaveBalanceDays))
+ ) {
+ return "璇峰~鍐欏亣鏈熶綑棰�";
+ }
+ const msg = validateLeaveBeforeSubmit(fields, formPayload);
+ if (msg) return msg;
+ }
+ if (moduleKey === APPROVAL_MODULE_KEYS.OVERTIME) {
+ const msg = validateOvertimeBeforeSubmit(fields, formPayload);
+ if (msg) return msg;
+ }
+ return "";
+}
+
+/** 浠庡疄渚� businessType 鎺ㄦ柇 moduleKey锛堢紪杈戝叆鍙f湭甯� moduleKey 鏃讹級 */
+export function inferModuleKeyFromRow(row, typeOptions = []) {
+ const bt = row?.businessType;
+ if (bt == null || bt === "") return "";
+ for (const key of Object.values(APPROVAL_MODULE_KEYS)) {
+ const types = getModuleMatchingBusinessTypes(key, typeOptions);
+ if (types.some(t => matchBusinessTypeValue(t, bt))) return key;
+ const cfg = APPROVAL_MODULE_REGISTRY[key];
+ if (cfg && matchBusinessTypeValue(cfg.approvalType, bt)) return key;
+ }
+ return "";
+}
+
+/** 缂栬緫鍥炴樉锛氫粠瀹炰緥琛屾仮澶嶆墿灞曞瓧娈� */
+export function loadModuleExtrasFromRow(moduleKey, row, formPayload) {
+ const extras = {
+ leaveBalanceDays: undefined,
+ originalPostName: "",
+ };
+ if (!moduleKey || !row) return extras;
+
+ const payload = formPayload || {};
+ if (moduleKey === APPROVAL_MODULE_KEYS.LEAVE) {
+ const v = payload.leaveBalanceDays ?? row.leaveBalanceDays;
+ extras.leaveBalanceDays =
+ v != null && v !== "" ? Number(v) : undefined;
+ }
+ if (moduleKey === APPROVAL_MODULE_KEYS.TRANSFER) {
+ extras.originalPostName =
+ payload.originalPostName ||
+ payload.originalPost ||
+ row.originalPostName ||
+ "";
+ }
+ return extras;
+}
diff --git a/src/pages/oa/_utils/approvalModuleListSearch.js b/src/pages/oa/_utils/approvalModuleListSearch.js
new file mode 100644
index 0000000..45a7d43
--- /dev/null
+++ b/src/pages/oa/_utils/approvalModuleListSearch.js
@@ -0,0 +1,319 @@
+import {
+ APPROVAL_MODULE_KEYS,
+ APPROVAL_MODULE_REGISTRY,
+ getApprovalModuleConfig,
+ getModuleMatchingBusinessTypes,
+} from "./approvalModuleRegistry.js";
+import { parseApprovalFormConfig } from "./approvalFormField.js";
+import { matchBusinessTypeValue } from "./approvalTemplateType.js";
+
+/** 涓� Web leave-apply LEAVE_TYPE_OPTIONS 涓�鑷� */
+export 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" },
+];
+
+/** 涓� Web overtime-apply OVERTIME_TYPE_OPTIONS 涓�鑷� */
+export const OVERTIME_TYPE_OPTIONS = [
+ { label: "宸ヤ綔鏃ュ姞鐝�", value: "weekday" },
+ { label: "浼戞伅鏃ュ姞鐝�", value: "weekend" },
+ { label: "娉曞畾鑺傚亣鏃ュ姞鐝�", value: "holiday" },
+];
+
+export const HANDOVER_STATUS_OPTIONS = [
+ { label: "杩涜涓�", value: "in_progress" },
+ { label: "宸插畬鎴�", value: "completed" },
+ { label: "宸查��鍥�", value: "returned" },
+];
+
+export const HANDOVER_TYPE_OPTIONS = [
+ { label: "绂昏亴浜ゆ帴", value: "resignation" },
+ { label: "璋冨矖浜ゆ帴", value: "transfer" },
+];
+
+function buildFormPayloadFromFields(fields = []) {
+ const payload = {};
+ for (const f of fields) {
+ if (!f?.key) continue;
+ const val = f.value ?? f.defaultValue;
+ if (val !== undefined && val !== null && val !== "") {
+ payload[f.key] = val;
+ }
+ }
+ return payload;
+}
+
+/** 瑙f瀽瀹炰緥 formConfig / formPayload锛堜笌 Web resolveInstanceFormFields 瀵归綈锛� */
+export function resolveInstanceFormPayload(row) {
+ const cfg = parseApprovalFormConfig(row?.formConfig);
+ const fields = (row?.formFieldDefs?.length ? row.formFieldDefs : cfg.fields) || [];
+ const formPayload = {
+ ...(fields.length ? buildFormPayloadFromFields(fields) : {}),
+ ...cfg.formPayload,
+ ...(row?.formPayload || {}),
+ };
+ return { fields, formPayload };
+}
+
+export function getRowPayloadValue(row, keys) {
+ const keyList = Array.isArray(keys) ? keys : [keys];
+ const { formPayload } = resolveInstanceFormPayload(row);
+ for (const k of keyList) {
+ if (row?.[k] != null && row[k] !== "") return row[k];
+ if (formPayload[k] != null && formPayload[k] !== "") return formPayload[k];
+ }
+ return "";
+}
+
+function pickDateRange(searchForm) {
+ const range =
+ searchForm?.createTimeRange ??
+ searchForm?.applyDateRange ??
+ searchForm?.transferDateRange;
+ if (!Array.isArray(range) || !range[0]) return {};
+ const out = { createTimeStart: range[0] };
+ if (range[1]) out.createTimeEnd = range[1];
+ return out;
+}
+
+/** 鍚勬ā鍧楅粯璁ゆ煡璇㈣〃鍗曪紙涓� Web searchForm 瀛楁涓�鑷达級 */
+export function createModuleSearchForm(moduleKey) {
+ switch (moduleKey) {
+ case APPROVAL_MODULE_KEYS.REGULAR:
+ return { applicantName: "", applyDateRange: null };
+ case APPROVAL_MODULE_KEYS.TRANSFER:
+ return { applicantId: "", transferDateRange: null };
+ case APPROVAL_MODULE_KEYS.WORK_HANDOVER:
+ return { applicantId: "", handoverStatus: "", handoverType: "" };
+ case APPROVAL_MODULE_KEYS.LEAVE:
+ return { applicantKeyword: "", leaveType: "" };
+ case APPROVAL_MODULE_KEYS.OVERTIME:
+ return { applicantKeyword: "", overtimeType: "" };
+ default:
+ return {};
+ }
+}
+
+/** 鏈嶅姟绔� listPage DTO 鐗囨锛堜笌 Web buildExtraListParams + buildApprovalInstanceListParams 涓�鑷达級 */
+export function buildModuleListDto(moduleKey, searchForm = {}) {
+ const sf = searchForm || {};
+ const dto = { ...pickDateRange(sf) };
+
+ switch (moduleKey) {
+ case APPROVAL_MODULE_KEYS.REGULAR: {
+ const name = (sf.applicantName || "").trim();
+ if (name) dto.applicantName = name;
+ break;
+ }
+ case APPROVAL_MODULE_KEYS.TRANSFER:
+ break;
+ case APPROVAL_MODULE_KEYS.WORK_HANDOVER:
+ break;
+ case APPROVAL_MODULE_KEYS.LEAVE:
+ case APPROVAL_MODULE_KEYS.OVERTIME:
+ break;
+ default:
+ break;
+ }
+ return dto;
+}
+
+function matchApplicantKeyword(row, keyword) {
+ const kw = (keyword || "").trim().toLowerCase();
+ if (!kw) return true;
+ const parts = [
+ row?.applicantName,
+ row?.applicantNo,
+ row?.applicantId,
+ getRowPayloadValue(row, ["applicant", "applicantName", "applicantId"]),
+ ]
+ .filter(v => v != null && v !== "")
+ .map(v => String(v).toLowerCase());
+ return parts.some(p => p.includes(kw));
+}
+
+function matchSelectValue(row, keys, expected) {
+ if (!expected) return true;
+ const raw = getRowPayloadValue(row, keys);
+ return String(raw) === String(expected);
+}
+
+function matchApplicantId(row, applicantId) {
+ if (!applicantId) return true;
+ const id = String(applicantId);
+ if (row?.applicantId != null && String(row.applicantId) === id) return true;
+ const payloadApplicant = getRowPayloadValue(row, [
+ "applicant",
+ "applicantId",
+ "applicantUserId",
+ ]);
+ return String(payloadApplicant) === id;
+}
+
+/** 鎸夋ā鍧� businessType / 鏍囬褰掑睘杩囨护锛堟湇鍔$鏈敓鏁堟椂鐨勫厹搴曪級 */
+export function filterRowsByModuleBusinessType(moduleKey, rows, typeOptions = []) {
+ const cfg = getApprovalModuleConfig(moduleKey);
+ if (!cfg) return rows;
+
+ const types = getModuleMatchingBusinessTypes(moduleKey, typeOptions);
+ const myLabels = [cfg.label, ...(cfg.typeLabels || [])].filter(Boolean);
+
+ return (rows || []).filter(row => {
+ if (types.length && row?.businessType != null && row.businessType !== "") {
+ if (types.some(t => matchBusinessTypeValue(row.businessType, t))) {
+ return true;
+ }
+ }
+
+ const title = String(row?.title || row?.templateName || "").trim();
+ if (title) {
+ if (myLabels.some(l => title === l || title.includes(l))) return true;
+ for (const [key, other] of Object.entries(APPROVAL_MODULE_REGISTRY)) {
+ if (key === moduleKey) continue;
+ const otherLabels = [other.label, ...(other.typeLabels || [])].filter(Boolean);
+ if (otherLabels.some(l => title === l || (l.length > 2 && title.includes(l)))) {
+ return false;
+ }
+ }
+ }
+
+ return types.length === 0;
+ });
+}
+
+/** 鍓嶇绛涢�夛紙Web 鏈笅鍙戞帴鍙g殑瀛楁涓� Web 琛屼负涓�鑷达級 */
+export function filterRowsByModuleSearch(moduleKey, rows, searchForm = {}) {
+ const sf = searchForm || {};
+ const list = Array.isArray(rows) ? rows : [];
+
+ switch (moduleKey) {
+ case APPROVAL_MODULE_KEYS.TRANSFER:
+ return list.filter(
+ row =>
+ matchApplicantId(row, sf.applicantId) &&
+ matchApplicantKeyword(row, sf.applicantKeyword)
+ );
+ case APPROVAL_MODULE_KEYS.WORK_HANDOVER:
+ return list.filter(
+ row =>
+ matchApplicantId(row, sf.applicantId) &&
+ matchSelectValue(row, ["handoverStatus", "浜ゆ帴鐘舵��"], sf.handoverStatus) &&
+ matchSelectValue(row, ["handoverType", "浜ゆ帴绫诲瀷"], sf.handoverType)
+ );
+ case APPROVAL_MODULE_KEYS.LEAVE:
+ return list.filter(
+ row =>
+ matchApplicantKeyword(row, sf.applicantKeyword) &&
+ matchSelectValue(row, ["leaveType", "璇峰亣绫诲瀷"], sf.leaveType)
+ );
+ case APPROVAL_MODULE_KEYS.OVERTIME:
+ return list.filter(
+ row =>
+ matchApplicantKeyword(row, sf.applicantKeyword) &&
+ matchSelectValue(row, ["overtimeType", "鍔犵彮绫诲瀷"], sf.overtimeType)
+ );
+ default:
+ return list;
+ }
+}
+
+/** 妯″潡绛涢�� UI 閰嶇疆 */
+export function getModuleSearchMeta(moduleKey) {
+ switch (moduleKey) {
+ case APPROVAL_MODULE_KEYS.REGULAR:
+ return {
+ fields: [
+ { key: "applicantName", type: "input", label: "鐢宠浜�", placeholder: "璇疯緭鍏ョ敵璇蜂汉" },
+ { key: "applyDateRange", type: "daterange", label: "鐢宠鏃ユ湡" },
+ ],
+ };
+ case APPROVAL_MODULE_KEYS.TRANSFER:
+ return {
+ fields: [
+ { key: "applicantId", type: "user", label: "鐢宠浜�", placeholder: "璇烽�夋嫨鐢宠浜�" },
+ { key: "transferDateRange", type: "daterange", label: "杞矖鏃堕棿" },
+ ],
+ };
+ case APPROVAL_MODULE_KEYS.WORK_HANDOVER:
+ return {
+ fields: [
+ { key: "applicantId", type: "user", label: "鐢宠浜�", placeholder: "璇烽�夋嫨鐢宠浜�" },
+ {
+ key: "handoverStatus",
+ type: "select",
+ label: "浜ゆ帴鐘舵��",
+ options: HANDOVER_STATUS_OPTIONS,
+ },
+ {
+ key: "handoverType",
+ type: "select",
+ label: "浜ゆ帴绫诲瀷",
+ options: HANDOVER_TYPE_OPTIONS,
+ },
+ ],
+ };
+ case APPROVAL_MODULE_KEYS.LEAVE:
+ return {
+ fields: [
+ {
+ key: "applicantKeyword",
+ type: "input",
+ label: "鐢宠浜�",
+ placeholder: "濮撳悕鎴栫紪鍙�",
+ },
+ {
+ key: "leaveType",
+ type: "select",
+ label: "璇峰亣绫诲瀷",
+ options: LEAVE_TYPE_OPTIONS,
+ },
+ ],
+ };
+ case APPROVAL_MODULE_KEYS.OVERTIME:
+ return {
+ fields: [
+ {
+ key: "applicantKeyword",
+ type: "input",
+ label: "鐢宠浜�",
+ placeholder: "濮撳悕鎴栫紪鍙�",
+ },
+ {
+ key: "overtimeType",
+ type: "select",
+ label: "鍔犵彮绫诲瀷",
+ options: OVERTIME_TYPE_OPTIONS,
+ },
+ ],
+ };
+ default:
+ return { fields: [] };
+ }
+}
+
+export function resetModuleSearchForm(moduleKey, target) {
+ const defaults = createModuleSearchForm(moduleKey);
+ Object.keys(target).forEach(k => {
+ if (!(k in defaults)) delete target[k];
+ });
+ Object.assign(target, defaults);
+}
+
+export function formatDateRangeLabel(range) {
+ if (!Array.isArray(range) || !range[0]) return "";
+ if (range[1]) return `${range[0]} 鑷� ${range[1]}`;
+ return range[0];
+}
+
+export 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 ?? ""}`;
+}
diff --git a/src/pages/oa/_utils/approvalModuleRegistry.js b/src/pages/oa/_utils/approvalModuleRegistry.js
new file mode 100644
index 0000000..63d0c59
--- /dev/null
+++ b/src/pages/oa/_utils/approvalModuleRegistry.js
@@ -0,0 +1,137 @@
+
+/** 涓� Web approvalModuleRegistry 涓�鑷� */
+export const APPROVAL_MODULE_KEYS = {
+ REGULAR: "regular",
+ TRANSFER: "transfer",
+ WORK_HANDOVER: "work_handover",
+ LEAVE: "leave",
+ OVERTIME: "overtime",
+};
+
+export const APPROVAL_MODULE_REGISTRY = {
+ [APPROVAL_MODULE_KEYS.REGULAR]: {
+ label: "杞鐢宠",
+ approvalType: "regular",
+ typeLabels: ["杞", "杞鐢宠"],
+ listFields: [
+ { label: "鍏ヨ亴鏃ユ湡", prop: "entryDate" },
+ { label: "杞鏃ユ湡", prop: "regularDate" },
+ ],
+ },
+ [APPROVAL_MODULE_KEYS.TRANSFER]: {
+ label: "璋冨矖鐢宠",
+ approvalType: "transfer",
+ typeLabels: ["璋冨矖", "璋冨姩", "璋冨矖鐢宠", "璋冨姩鐢宠"],
+ listFields: [
+ { label: "鍘熷矖浣�", prop: "fromPost" },
+ { label: "鐩爣宀椾綅", prop: "toPost" },
+ ],
+ },
+ [APPROVAL_MODULE_KEYS.WORK_HANDOVER]: {
+ label: "宸ヤ綔浜ゆ帴",
+ approvalType: "work_handover",
+ typeLabels: ["宸ヤ綔浜ゆ帴", "浜ゆ帴", "宸ヤ綔浜ゆ帴瀹℃壒"],
+ listFields: [
+ { label: "浜ゆ帴浜�", prop: "handoverTo" },
+ { label: "浜ゆ帴浜嬮」", prop: "handoverItems" },
+ ],
+ },
+ [APPROVAL_MODULE_KEYS.LEAVE]: {
+ label: "璇峰亣鐢宠",
+ approvalType: "leave",
+ typeLabels: ["璇峰亣", "璇峰亣鐢宠", "璇峰亣瀹℃壒"],
+ listFields: [
+ { label: "璇峰亣绫诲瀷", prop: "leaveType" },
+ { label: "寮�濮嬫椂闂�", prop: "startTime" },
+ { label: "缁撴潫鏃堕棿", prop: "endTime" },
+ ],
+ },
+ [APPROVAL_MODULE_KEYS.OVERTIME]: {
+ label: "鍔犵彮鐢宠",
+ approvalType: "overtime",
+ typeLabels: ["鍔犵彮", "鍔犵彮鐢宠", "鍔犵彮瀹℃壒"],
+ listFields: [
+ { label: "鍔犵彮鏃ユ湡", prop: "overtimeDate" },
+ { label: "鏃堕暱(灏忔椂)", prop: "hours" },
+ ],
+ },
+};
+
+export function getApprovalModuleConfig(moduleKey) {
+ if (!moduleKey) return null;
+ return APPROVAL_MODULE_REGISTRY[moduleKey] || null;
+}
+
+export function getModuleListBusinessType(moduleKey) {
+ const cfg = getApprovalModuleConfig(moduleKey);
+ if (!cfg) return "";
+ if (cfg.businessType != null && cfg.businessType !== "") return cfg.businessType;
+ return cfg.approvalType || "";
+}
+
+function matchBiz(a, b) {
+ if (a == null || a === "" || b == null || b === "") return false;
+ return a === b || a === Number(b) || Number(a) === b || String(a) === String(b);
+}
+
+export function resolveModuleBusinessType(moduleKey, typeOptions = []) {
+ const cfg = getApprovalModuleConfig(moduleKey);
+ if (!cfg) return null;
+ if (cfg.businessType != null && cfg.businessType !== "") return cfg.businessType;
+
+ const labels = [cfg.label, ...(cfg.typeLabels || [])].filter(Boolean);
+ const hitByLabel = (typeOptions || []).find(opt => {
+ const optLabel = String(opt?.name || opt?.label || "").trim();
+ if (!optLabel) return false;
+ return labels.some(
+ l => optLabel === l || optLabel.includes(l) || l.includes(optLabel)
+ );
+ });
+ if (hitByLabel?.value != null && hitByLabel.value !== "") return hitByLabel.value;
+
+ if (cfg.approvalType) {
+ const hitByValue = (typeOptions || []).find(
+ opt =>
+ matchBiz(opt?.value, cfg.approvalType) || matchBiz(opt?.code, cfg.approvalType)
+ );
+ if (hitByValue?.value != null && hitByValue.value !== "") return hitByValue.value;
+ }
+
+ return cfg.approvalType || null;
+}
+
+export function getModuleMatchingBusinessTypes(moduleKey, typeOptions = []) {
+ const cfg = getApprovalModuleConfig(moduleKey);
+ if (!cfg) return [];
+
+ const values = new Set();
+ const primary = resolveModuleBusinessType(moduleKey, typeOptions);
+ if (primary != null && primary !== "") values.add(primary);
+ if (cfg.approvalType) values.add(cfg.approvalType);
+
+ const labels = [cfg.label, ...(cfg.typeLabels || [])].filter(Boolean);
+ for (const opt of typeOptions || []) {
+ const optLabel = String(opt?.name || opt?.label || "").trim();
+ if (!optLabel) continue;
+ const matched = labels.some(
+ l => optLabel === l || optLabel.includes(l) || l.includes(optLabel)
+ );
+ if (matched && opt.value != null && opt.value !== "") {
+ values.add(opt.value);
+ }
+ }
+ return [...values];
+}
+
+/** 鍒楄〃椤� moduleKey 涓庤矾鐢� pageKey 瀵圭収 */
+export const PAGE_KEY_TO_MODULE = {
+ "HrManage/regular-apply": APPROVAL_MODULE_KEYS.REGULAR,
+ "HrManage/transfer-apply": APPROVAL_MODULE_KEYS.TRANSFER,
+ "HrManage/work-handover": APPROVAL_MODULE_KEYS.WORK_HANDOVER,
+ "AttendManage/leave-apply": APPROVAL_MODULE_KEYS.LEAVE,
+ "AttendManage/overtime-apply": APPROVAL_MODULE_KEYS.OVERTIME,
+};
+
+export function getModuleKeyFromPageKey(pageKey) {
+ return PAGE_KEY_TO_MODULE[pageKey] || "";
+}
diff --git a/src/pages/oa/_utils/approvalTemplateType.js b/src/pages/oa/_utils/approvalTemplateType.js
index 4113ee8..e554068 100644
--- a/src/pages/oa/_utils/approvalTemplateType.js
+++ b/src/pages/oa/_utils/approvalTemplateType.js
@@ -1,4 +1,5 @@
import { getTypeEnums } from "@/api/basic/enum.js";
+import { listApprovalTemplateByType } from "@/api/oa/approvalTemplate.js";
/**
* GET /approvalTemplate/list/{type} 璺緞鍙傛暟涓� templateType
@@ -19,58 +20,120 @@
{ name: "璇峰亣绠$悊", value: 2 },
];
+/** 瑙f瀽 TypeEnums 鎺ュ彛杩斿洖锛堝吋瀹� { TypeEnums: [] } 宓屽锛� */
+export function unwrapEnumList(data) {
+ if (Array.isArray(data)) return data;
+ if (!data || typeof data !== "object") return [];
+ if (Array.isArray(data.TypeEnums)) return data.TypeEnums;
+ if (Array.isArray(data.typeEnums)) return data.typeEnums;
+ const nested = Object.values(data).find(v => Array.isArray(v));
+ return nested || [];
+}
+
+/** 瑙f瀽妯℃澘鍒楄〃鎺ュ彛杩斿洖 */
+export function unwrapTemplateList(payload) {
+ const data = payload?.data ?? payload;
+ if (Array.isArray(data)) return data;
+ if (Array.isArray(data?.records)) return data.records;
+ if (Array.isArray(data?.list)) return data.list;
+ if (Array.isArray(data?.rows)) return data.rows;
+ return [];
+}
+
+/** enabled锛�1 / true 涓哄惎鐢紙涓� Web mapEnabledFromApi 涓�鑷达級 */
+export function mapEnabledFromApi(enabled) {
+ if (enabled === undefined || enabled === null) return true;
+ if (enabled === true || enabled === 1) return true;
+ const s = String(enabled).toLowerCase();
+ return s === "1" || s === "true" || s === "yes";
+}
+
+export function filterEnabledTemplates(list) {
+ return (list || []).filter(row => mapEnabledFromApi(row?.enabled));
+}
+
/** 灏� /basic/enum/TypeEnums 鍝嶅簲瑙勮寖涓� { name, value }[] */
export function normalizeEnumOptions(data) {
- if (!data) return [];
-
- if (Array.isArray(data)) {
- return data
- .map(item => {
- const name =
- item?.name ??
- item?.label ??
- item?.text ??
- item?.dictLabel ??
- item?.description;
- const rawValue =
- item?.value ?? item?.code ?? item?.dictValue ?? item?.key ?? item?.id;
- if (name == null || rawValue === undefined || rawValue === null) {
- return null;
- }
- const num = Number(rawValue);
- return {
- name: String(name),
- value: Number.isNaN(num) ? rawValue : num,
- };
- })
- .filter(Boolean);
- }
-
- if (typeof data === "object") {
- return Object.entries(data).map(([value, name]) => {
- const num = Number(value);
+ return unwrapEnumList(data)
+ .map(item => {
+ const name =
+ item?.name ??
+ item?.label ??
+ item?.text ??
+ item?.dictLabel ??
+ item?.description;
+ const rawValue =
+ item?.value ?? item?.code ?? item?.dictValue ?? item?.key ?? item?.id;
+ if (name == null || rawValue === undefined || rawValue === null) {
+ return null;
+ }
+ const num = Number(rawValue);
return {
name: String(name),
- value: Number.isNaN(num) ? value : num,
+ value: Number.isNaN(num) ? rawValue : num,
};
- });
- }
-
- return [];
+ })
+ .filter(Boolean);
}
/** 鎷夊彇涓氬姟绫诲瀷鏋氫妇锛圱ypeEnums 鈫� businessType锛� */
export async function fetchApprovalTemplateTypes() {
- const res = await getTypeEnums();
- const options = normalizeEnumOptions(res?.data);
- return options.length ? options : [...FALLBACK_BUSINESS_TYPE_OPTIONS];
+ try {
+ const res = await getTypeEnums();
+ const options = normalizeEnumOptions(res?.data ?? res);
+ return options.length ? options : [...FALLBACK_BUSINESS_TYPE_OPTIONS];
+ } catch {
+ return [...FALLBACK_BUSINESS_TYPE_OPTIONS];
+ }
+}
+
+/** 閫夊彇绗竴涓�屾湁妯℃澘銆嶇殑 Tab 涓嬫爣 */
+export function pickTabIndexWithTemplates(typeOptions, templates) {
+ if (!typeOptions?.length) return 0;
+ for (let i = 0; i < typeOptions.length; i++) {
+ const bt = typeOptions[i]?.value;
+ if (filterTemplatesByBusinessType(templates, bt).length > 0) return i;
+ }
+ return 0;
+}
+
+/** 鎷夊彇宸插惎鐢ㄦā鏉匡紙鑷畾涔� + 绯荤粺鍐呯疆锛屼笌 Web 瀵煎叆閫昏緫涓�鑷达級 */
+export async function fetchEnabledApprovalTemplates() {
+ const [customRes, builtinRes] = await Promise.all([
+ listApprovalTemplateByType(CUSTOM_TEMPLATE_LIST_TYPE),
+ listApprovalTemplateByType(SYSTEM_TEMPLATE_TYPE),
+ ]);
+ const merged = [
+ ...unwrapTemplateList(customRes),
+ ...unwrapTemplateList(builtinRes),
+ ];
+ const byId = new Map();
+ merged.forEach(item => {
+ if (item?.id != null) byId.set(String(item.id), item);
+ });
+ return filterEnabledTemplates([...byId.values()]);
+}
+
+/** businessType 瀹芥澗鍖归厤锛堜笌 Web matchBusinessTypeValue 涓�鑷达級 */
+export function matchBusinessTypeValue(a, b) {
+ if (a == null || a === "" || b == null || b === "") return false;
+ return a === b || a === Number(b) || Number(a) === b || String(a) === String(b);
}
/** 鎸� businessType 绛涢�夋ā鏉� */
export function filterTemplatesByBusinessType(templates, businessType) {
if (businessType == null || businessType === "") return [];
- return (templates || []).filter(
- item => String(item.businessType) === String(businessType)
+ return (templates || []).filter(item =>
+ matchBusinessTypeValue(item.businessType, businessType)
+ );
+}
+
+/** 鎸夊涓� businessType 绛涢�夛紙涓氬姟妯″潡锛� */
+export function filterTemplatesByBusinessTypes(templates, businessTypes = []) {
+ const types = (businessTypes || []).filter(t => t != null && t !== "");
+ if (!types.length) return [];
+ return (templates || []).filter(item =>
+ types.some(t => matchBusinessTypeValue(item.businessType, t))
);
}
@@ -84,6 +147,20 @@
return idx >= 0 ? idx : 0;
}
+/** TypeEnums 涓虹┖鏃讹紝浠庢ā鏉垮垪琛ㄥ弽鎺� Tab */
+export function buildTypeOptionsFromTemplates(templates) {
+ const map = new Map();
+ (templates || []).forEach(item => {
+ const v = item?.businessType;
+ if (v == null || v === "") return;
+ const key = String(v);
+ if (!map.has(key)) {
+ map.set(key, { name: `瀹℃壒绫诲瀷 ${key}`, value: v });
+ }
+ });
+ return [...map.values()];
+}
+
export function buildTypeLabelMap(options) {
const map = {};
(options || []).forEach(opt => {
diff --git a/src/pages/oa/_utils/approveListUtils.js b/src/pages/oa/_utils/approveListUtils.js
new file mode 100644
index 0000000..965b3dd
--- /dev/null
+++ b/src/pages/oa/_utils/approveListUtils.js
@@ -0,0 +1,327 @@
+import { parseTime } from "@/utils/ruoyi";
+import {
+ formatFieldDisplayValue,
+ getFieldOptionLabel,
+ isSelectField,
+ mergeFormConfigForEdit,
+} from "./approvalFormField.js";
+
+export const DETAIL_STORAGE_KEY = "oa_approve_instance_detail_row";
+
+export const INSTANCE_STATUS_TEXT = {
+ PENDING: "杩涜涓�",
+ APPROVED: "宸查�氳繃",
+ REJECTED: "宸查┏鍥�",
+ DRAFT: "鑽夌",
+};
+
+export const INSTANCE_STATUS_TAG = {
+ PENDING: "warning",
+ APPROVED: "success",
+ REJECTED: "error",
+ DRAFT: "info",
+};
+
+export const TASK_STATUS_TEXT = {
+ PENDING: "寰呭鐞�",
+ APPROVED: "宸查�氳繃",
+ REJECTED: "宸查┏鍥�",
+};
+
+export const TASK_STATUS_TAG = {
+ PENDING: "warning",
+ APPROVED: "success",
+ REJECTED: "error",
+};
+
+export function instanceStatusText(status) {
+ return INSTANCE_STATUS_TEXT[status] || status || "-";
+}
+
+export function instanceStatusTagType(status) {
+ return INSTANCE_STATUS_TAG[status] || "info";
+}
+
+export function taskStatusText(status) {
+ const key = String(status || "").toUpperCase();
+ return TASK_STATUS_TEXT[key] || status || "寰呭鐞�";
+}
+
+export function taskStatusTagType(status) {
+ const key = String(status || "").toUpperCase();
+ return TASK_STATUS_TAG[key] || "info";
+}
+
+export function formatDateTime(val) {
+ if (!val) return "-";
+ return parseTime(val, "{y}-{m}-{d} {h}:{i}:{s}") || String(val);
+}
+
+/** 瑙f瀽瀹炰緥 formConfig 涓哄彧璇诲睍绀哄瓧娈� */
+export function resolveInstanceDisplayFields(formConfig) {
+ const merged = mergeFormConfigForEdit("", formConfig);
+ return (merged.fields || []).filter(f => f?.key);
+}
+
+export function displayFieldValue(field) {
+ const val = field.value ?? field.defaultValue;
+ if (val === undefined || val === null || val === "") return "-";
+ if (isSelectField(field)) {
+ return getFieldOptionLabel(field, val) || String(val);
+ }
+ const shown = formatFieldDisplayValue(field, val);
+ return shown || String(val);
+}
+
+/** 瀹℃壒璁板綍 result锛歛pproved | rejected | pending */
+export function mapRecordResult(action) {
+ const s = String(action || "").toUpperCase();
+ if (s === "APPROVED" || s === "APPROVE" || s === "PASS") return "approved";
+ if (s === "REJECTED" || s === "REJECT" || s === "REFUSE") return "rejected";
+ return "pending";
+}
+
+export function recordActionLabel(result) {
+ if (result === "approved") return "閫氳繃";
+ if (result === "rejected") return "椹冲洖";
+ return "寰呭鐞�";
+}
+
+export function mapApprovalRecords(records) {
+ const list = Array.isArray(records) ? records : [];
+ return list.map((r, index) => ({
+ id: r.id ?? index,
+ operatorName: r.approverName || r.operatorName || r.createUserName || "鈥�",
+ result: mapRecordResult(r.approveAction ?? r.action ?? r.status),
+ opinion: r.approveComment || r.comment || r.opinion || "",
+ time: formatDateTime(r.approveTime || r.createTime || r.time),
+ }));
+}
+
+export function getRejectReasonFromRecords(records) {
+ const mapped = mapApprovalRecords(records);
+ const hit = mapped.find(r => r.result === "rejected");
+ return hit?.opinion || "";
+}
+
+/** 鍒楄〃 tasks 鈫� 娴佺▼鑺傜偣锛堜笌 apply 椤佃妭鐐圭粨鏋勬帴杩戯級 */
+export function mapTasksToFlowNodes(tasks) {
+ const list = Array.isArray(tasks) ? tasks : [];
+ if (!list.length) return [];
+
+ const byLevel = new Map();
+ list.forEach(t => {
+ const level = Number(t.levelNo ?? t.taskLevel ?? t.nodeOrder ?? 1);
+ if (!byLevel.has(level)) {
+ byLevel.set(level, {
+ levelNo: level,
+ approveType: t.approveType || "AND",
+ approvers: [],
+ });
+ }
+ const node = byLevel.get(level);
+ node.approvers.push({
+ approverName: t.approverName || "鈥�",
+ taskStatus: t.taskStatus ?? t.status,
+ approveComment: t.approveComment,
+ approveTime: t.approveTime,
+ });
+ if (t.approveType) node.approveType = t.approveType;
+ });
+
+ return [...byLevel.entries()]
+ .sort(([a], [b]) => a - b)
+ .map(([, node]) => node);
+}
+
+/** 缁勮瀹℃壒鎻愪氦 DTO锛堜笌 Web buildApproveInstanceDto 涓�鑷达級 */
+export function buildApproveInstanceDto(id, uiResult, comment) {
+ const opinion = (comment || "").trim();
+ return {
+ id,
+ approveAction: uiResult === "rejected" ? "REJECTED" : "APPROVED",
+ approveComment: opinion || (uiResult === "approved" ? "鍚屾剰" : ""),
+ };
+}
+
+/** 鏄惁鏈汉鍙戣捣鐨勫鎵� */
+export function isOwnApplication(item, userStore) {
+ const uid = userStore?.id;
+ if (item?.applicantId != null && uid != null && uid !== "") {
+ return String(item.applicantId) === String(uid);
+ }
+ const loginName = userStore?.nickName || userStore?.name;
+ if (loginName && item?.applicantName) {
+ return String(item.applicantName).trim() === String(loginName).trim();
+ }
+ return false;
+}
+
+/** 浠呰繘琛屼腑涓旀湰浜哄彂璧锋椂鍙紪杈� */
+export function canModifyInstance(item, userStore) {
+ return item?.status === "PENDING" && isOwnApplication(item, userStore);
+}
+
+/** 寰呭綋鍓嶇敤鎴峰鎵� */
+export function canApproveInstance(item) {
+ return Boolean(item?.isApprove) && item?.status === "PENDING";
+}
+
+export function stashInstanceRow(item) {
+ if (item) {
+ uni.setStorageSync(DETAIL_STORAGE_KEY, item);
+ }
+}
+
+export function loadInstanceRow(id) {
+ const row = uni.getStorageSync(DETAIL_STORAGE_KEY);
+ if (!row || String(row.id) !== String(id)) return null;
+ return row;
+}
+
+export const EDIT_STORAGE_KEY = "oa_approve_instance_edit_row";
+
+/** 涓氬姟鐢宠椤电姸鎬侊細杩涜涓�/宸插畬鎴愪笉鍙慨鏀癸紙涓� Web canEditBusinessInstanceRow 涓�鑷达級 */
+export function normalizeApprovalStatusKey(v) {
+ if (v == null || v === "") return "pending";
+ const upper = String(v).trim().toUpperCase();
+ if (upper === "DRAFT") return "draft";
+ if (upper === "APPROVED" || upper === "PASS") return "approved";
+ if (upper === "REJECTED" || upper === "REJECT" || upper === "REFUSE") {
+ return "rejected";
+ }
+ if (upper === "CANCELLED" || upper === "CANCEL") return "cancelled";
+ if (upper === "PENDING" || upper === "IN_PROGRESS") return "pending";
+ const lower = String(v).trim().toLowerCase();
+ if (["draft", "pending", "approved", "rejected", "cancelled"].includes(lower)) {
+ return lower;
+ }
+ return "pending";
+}
+
+export function canEditBusinessInstanceRow(row) {
+ const key = normalizeApprovalStatusKey(row?.status ?? row?.approvalStatus);
+ return key !== "pending" && key !== "approved";
+}
+
+export function businessStatusText(status) {
+ const key = normalizeApprovalStatusKey(status);
+ if (key === "draft") return "鑽夌";
+ if (key === "pending") return "杩涜涓�";
+ if (key === "approved") return "宸插畬鎴�";
+ if (key === "rejected") return "宸查┏鍥�";
+ if (key === "cancelled") return "宸叉挙閿�";
+ return instanceStatusText(status);
+}
+
+export function businessStatusTagType(status) {
+ const key = normalizeApprovalStatusKey(status);
+ if (key === "approved") return "success";
+ if (key === "rejected") return "error";
+ if (key === "draft" || key === "cancelled") return "info";
+ return "warning";
+}
+
+/** OA 鍒楄〃鑷畾涔夌姸鎬佽鏍� class */
+export function businessStatusClass(status) {
+ return `status-${normalizeApprovalStatusKey(status)}`;
+}
+
+/**
+ * 涓� Web buildApprovalInstanceListParams 涓�鑷达細鎵佸钩 query锛坈urrent/size/businessType/...锛�
+ * 瀹℃壒鍒楄〃涓嶄紶 businessType 鍗虫煡鍏ㄩ儴
+ */
+export function buildInstanceListParams({
+ page,
+ businessType,
+ extraDto = {},
+ searchForm,
+}) {
+ const extra = { ...(extraDto && typeof extraDto === "object" ? extraDto : {}) };
+ if (extra.createTime != null && extra.createTimeStart == null) {
+ extra.createTimeStart = extra.createTime;
+ }
+ delete extra.createTime;
+
+ const params = {
+ current: page.current,
+ size: page.size,
+ ...extra,
+ };
+
+ const bizType = businessType ?? searchForm?.businessType;
+ if (bizType != null && bizType !== "") {
+ params.businessType = bizType;
+ }
+ if (searchForm?.status) {
+ params.status = searchForm.status;
+ }
+
+ const range =
+ searchForm?.createTimeRange ??
+ searchForm?.applyDateRange ??
+ searchForm?.transferDateRange;
+ if (Array.isArray(range) && range[0] && params.createTimeStart == null) {
+ params.createTimeStart = range[0];
+ }
+ if (Array.isArray(range) && range[1] && params.createTimeEnd == null) {
+ params.createTimeEnd = range[1];
+ }
+
+ return params;
+}
+
+export function unwrapInstancePage(res) {
+ const data = res?.data ?? res;
+ return {
+ records: Array.isArray(data?.records) ? data.records : [],
+ total: Number(data?.total ?? 0),
+ };
+}
+
+/** 浠� formConfig 鎻愬彇鍒楄〃灞曠ず瀛楁锛坙abel + value锛� */
+export function buildFormDisplayRows(formConfig, listFields = []) {
+ const fields = resolveInstanceDisplayFields(formConfig);
+ const rows = [];
+ const propKeys = (listFields || []).map(f => f.prop).filter(Boolean);
+
+ if (propKeys.length) {
+ propKeys.forEach(prop => {
+ const hit = fields.find(f => f.key === prop);
+ if (hit) {
+ rows.push({ label: hit.label, value: displayFieldValue(hit) });
+ }
+ });
+ } else {
+ fields.slice(0, 3).forEach(f => {
+ rows.push({ label: f.label, value: displayFieldValue(f) });
+ });
+ }
+ return rows;
+}
+
+/** 鍒楄〃琛屽寮猴紙淇濈暀鍘熷瀛楁渚涜鎯�/缂栬緫锛� */
+export function mapInstanceListRow(row, listFields = []) {
+ if (!row) return {};
+ const displayRows = buildFormDisplayRows(row.formConfig, listFields);
+ const extra = {};
+ const formFields = resolveInstanceDisplayFields(row.formConfig);
+ (listFields || []).forEach(def => {
+ if (!def?.prop) return;
+ const hit = formFields.find(f => f.key === def.prop);
+ extra[def.prop] = hit ? displayFieldValue(hit) : "-";
+ });
+ const formPayload = {};
+ formFields.forEach(f => {
+ if (f?.key) formPayload[f.key] = f.value ?? f.defaultValue ?? "";
+ });
+ return {
+ ...row,
+ approvalStatus: normalizeApprovalStatusKey(row.status),
+ summary: row.title || row.templateName || "",
+ createTime: formatDateTime(row.applyTime || row.createTime),
+ displayRows,
+ formPayload,
+ ...extra,
+ };
+}
--
Gitblit v1.9.3