转正申请/调岗申请/工作交接/请假申请/加班申请页面画页面,接口联调和web端保持一致
日报
| | |
| | | data: { approvalInstanceDto }, |
| | | }); |
| | | } |
| | | |
| | | /** 审æ¹ï¼éè¿/驳åï¼POST /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, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | /** å²ä½ä¸æ GET /system/post/optionselect */ |
| | | export function findPostOptions(query) { |
| | | return request({ |
| | | url: "/system/post/optionselect", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | |
| | | 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`, |
| | |
| | | // { 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 }, |
| | | ], |
| | |
| | | } |
| | | }, |
| | | { |
| | | "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": "å®¡æ¹æ¨¡æ¿", |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | 审æ¹å®ä¾è¯¦æ
å±ç¤ºï¼åºæ¬ä¿¡æ¯ + å¡«æ¥ + æµç¨ + 审æ¹è®°å½ |
| | | --> |
| | | <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> |
| | |
| | | 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" |
| | |
| | | </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"> |
| | |
| | | </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"; |
| | |
| | | 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, |
| | |
| | | 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); |
| | |
| | | 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()); |
| | |
| | | ); |
| | | } |
| | | 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); |
| | |
| | | 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() === "") { |
| | |
| | | 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, |
| | |
| | | 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}ç³è¯·`; |
| | | } |
| | |
| | | return; |
| | | } |
| | | instanceRow.value = row; |
| | | if (!moduleKey.value) { |
| | | moduleKey.value = inferModuleKeyFromRow(row); |
| | | } |
| | | templateId.value = row.templateId; |
| | | form.title = row.title || ""; |
| | | |
| | |
| | | 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(); |
| | |
| | | }); |
| | | }; |
| | | |
| | | 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(); |
| | |
| | | .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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | 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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | 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> |
| | |
| | | <!-- |
| | | 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> |
| | |
| | | </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; |
| | |
| | | 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"; |
| | |
| | | } |
| | | }) |
| | | .catch(() => { |
| | | if (page.current === 1) { |
| | | list.value = []; |
| | | } |
| | | if (page.current === 1) list.value = []; |
| | | pageStatus.value = "loadmore"; |
| | | uni.showToast({ title: "æ¥è¯¢å¤±è´¥", icon: "none" }); |
| | | }); |
| | |
| | | }; |
| | | |
| | | 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 => { |
| | |
| | | } |
| | | 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> |
| | |
| | | --> |
| | | <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" |
| | |
| | | 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"> |
| | |
| | | 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"> |
| | |
| | | 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({}); |
| | | /** å
¨é¨èªå®ä¹å·²å¯ç¨æ¨¡æ¿ï¼list/1 䏿¬¡æåï¼ */ |
| | |
| | | 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); |
| | | }); |
| | |
| | | }); |
| | | |
| | | 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 => |
| | |
| | | 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; |
| | | } |
| | |
| | | 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> |
| | |
| | | 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; |
| | |
| | | è·¯ç±ï¼/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> |
| | |
| | | è·¯ç±ï¼/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> |
| | |
| | | è·¯ç±ï¼/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> |
| | |
| | | è·¯ç±ï¼/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> |
| | |
| | | è·¯ç±ï¼/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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | ä¸å¡å®¡æ¹ç³è¯·å表ï¼è½¬æ£/è°å²/交æ¥/请å/å çï¼ |
| | | --> |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | ä¸å¡å®¡æ¹å表çéå¼¹çª |
| | | --> |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // 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; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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ï¼ç¼è¾å
¥å£æªå¸¦ 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; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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; |
| | | } |
| | | |
| | | /** è§£æå®ä¾ 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 æªä¸åæ¥å£çåæ®µä¸ 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 ?? ""}`; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | |
| | | /** ä¸ 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] || ""; |
| | | } |
| | |
| | | import { getTypeEnums } from "@/api/basic/enum.js"; |
| | | import { listApprovalTemplateByType } from "@/api/oa/approvalTemplate.js"; |
| | | |
| | | /** |
| | | * GET /approvalTemplate/list/{type} è·¯å¾åæ°ä¸º templateType |
| | |
| | | { name: "请å管ç", value: 2 }, |
| | | ]; |
| | | |
| | | /** è§£æ 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 || []; |
| | | } |
| | | |
| | | /** è§£ææ¨¡æ¿å表æ¥å£è¿å */ |
| | | 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); |
| | | } |
| | | |
| | | /** æåä¸å¡ç±»åæä¸¾ï¼TypeEnums â 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)) |
| | | ); |
| | | } |
| | | |
| | |
| | | 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 => { |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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); |
| | | } |
| | | |
| | | /** è§£æå®ä¾ 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ï¼approved | 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ï¼current/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 æåå表å±ç¤ºå段ï¼label + 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, |
| | | }; |
| | | } |