From 9d89dedf542a7b9f8e2549c44723771133f79ef2 Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期二, 19 五月 2026 18:04:10 +0800
Subject: [PATCH] 模板类型变更
---
src/api/oa/approvalTemplate.js | 11
src/pages/oa/_utils/approvalTemplateType.js | 94 +++
src/config/oaPaths.js | 2
src/pages.json | 14
src/pages/oa/ApproveManage/approve-list/index.vue | 297 +++++++++++
src/pages/oa/ApproveManage/approve-template/edit.vue | 112 +++-
src/pages/oa/ApproveManage/approve-list/template-select.vue | 308 ++++++++++++
src/api/oa/approvalInstance.js | 28 +
src/pages/oa/ApproveManage/approve-list/apply.vue | 548 +++++++++++++++++++++
src/api/basic/enum.js | 9
src/pages/oa/ApproveManage/approve-template/detail.vue | 23
src/pages/oa/ApproveManage/approve-template/index.vue | 26
12 files changed, 1,417 insertions(+), 55 deletions(-)
diff --git a/src/api/basic/enum.js b/src/api/basic/enum.js
new file mode 100644
index 0000000..982123f
--- /dev/null
+++ b/src/api/basic/enum.js
@@ -0,0 +1,9 @@
+import request from "@/utils/request";
+
+/** 瀹℃壒妯℃澘绫诲瀷鏋氫妇 GET /basic/enum/TypeEnums */
+export function getTypeEnums() {
+ return request({
+ url: "/basic/enum/TypeEnums",
+ method: "get",
+ });
+}
diff --git a/src/api/oa/approvalInstance.js b/src/api/oa/approvalInstance.js
new file mode 100644
index 0000000..08b1f0c
--- /dev/null
+++ b/src/api/oa/approvalInstance.js
@@ -0,0 +1,28 @@
+import request from "@/utils/request";
+
+/** 瀹℃壒瀹炰緥鍒嗛〉鏌ヨ GET /approvalInstance/listPage */
+export function listApprovalInstancePage(params) {
+ return request({
+ url: "/approvalInstance/listPage",
+ method: "get",
+ params,
+ });
+}
+
+/** 鏂板缓瀹℃壒瀹炰緥 POST /approvalInstance/save */
+export function saveApprovalInstance(approvalInstanceDto) {
+ return request({
+ url: "/approvalInstance/save",
+ method: "post",
+ data: { approvalInstanceDto },
+ });
+}
+
+/** 瀹℃牳涓慨鏀瑰鎵瑰疄渚� PUT /approvalInstance/update */
+export function updateApprovalInstance(approvalInstanceDto) {
+ return request({
+ url: "/approvalInstance/update",
+ method: "put",
+ data: { approvalInstanceDto },
+ });
+}
diff --git a/src/api/oa/approvalTemplate.js b/src/api/oa/approvalTemplate.js
index 5d34aea..ab13897 100644
--- a/src/api/oa/approvalTemplate.js
+++ b/src/api/oa/approvalTemplate.js
@@ -1,5 +1,16 @@
import request from "@/utils/request";
+/**
+ * 鎸� templateType 鏌ヨ宸插惎鐢ㄦā鏉垮垪琛紙闈� businessType锛�
+ * GET /approvalTemplate/list/{templateType} 渚嬶細list/1 = 鑷畾涔夊凡鍚敤
+ */
+export function listApprovalTemplateByType(templateType) {
+ return request({
+ url: `/approvalTemplate/list/${templateType}`,
+ method: "get",
+ });
+}
+
/** 瀹℃壒妯℃澘鍒嗛〉鏌ヨ */
export function listApprovalTemplatePage(params) {
return request({
diff --git a/src/config/oaPaths.js b/src/config/oaPaths.js
index 03246bf..73cca36 100644
--- a/src/config/oaPaths.js
+++ b/src/config/oaPaths.js
@@ -24,6 +24,8 @@
saleContract: `/${P}/ContractManage/sale-contract/index`,
/** 瀹℃壒绠$悊 */
approveList: `/${P}/ApproveManage/approve-list/index`,
+ approveListTemplateSelect: `/${P}/ApproveManage/approve-list/template-select`,
+ approveListApply: `/${P}/ApproveManage/approve-list/apply`,
approveTemplate: `/${P}/ApproveManage/approve-template/index`,
approveTemplateEdit: `/${P}/ApproveManage/approve-template/edit`,
approveTemplateDetail: `/${P}/ApproveManage/approve-template/detail`,
diff --git a/src/pages.json b/src/pages.json
index 7a4dc16..e39fb89 100644
--- a/src/pages.json
+++ b/src/pages.json
@@ -1404,6 +1404,20 @@
}
},
{
+ "path": "pages/oa/ApproveManage/approve-list/template-select",
+ "style": {
+ "navigationBarTitleText": "閫夋嫨瀹℃壒妯℃澘",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/oa/ApproveManage/approve-list/apply",
+ "style": {
+ "navigationBarTitleText": "鍙戣捣瀹℃壒",
+ "navigationStyle": "custom"
+ }
+ },
+ {
"path": "pages/oa/ApproveManage/approve-template/index",
"style": {
"navigationBarTitleText": "瀹℃壒妯℃澘",
diff --git a/src/pages/oa/ApproveManage/approve-list/apply.vue b/src/pages/oa/ApproveManage/approve-list/apply.vue
new file mode 100644
index 0000000..12f49c5
--- /dev/null
+++ b/src/pages/oa/ApproveManage/approve-list/apply.vue
@@ -0,0 +1,548 @@
+<!--
+ OA / 瀹℃壒绠$悊 / 鍙戣捣瀹℃壒
+ 璺敱锛�/pages/oa/ApproveManage/approve-list/apply
+-->
+<template>
+ <view class="approve-apply-page">
+ <PageHeader :title="pageTitle"
+ @back="goBack" />
+
+ <scroll-view class="form-scroll"
+ scroll-y
+ :show-scrollbar="false">
+ <view v-if="loading"
+ class="loading-wrap">
+ <up-loading-icon mode="circle" />
+ <text class="loading-text">鍔犺浇涓�...</text>
+ </view>
+ <template v-else-if="detail">
+ <view class="section">
+ <view class="section-title">鍩烘湰淇℃伅</view>
+ <view class="form-body">
+ <view class="form-row">
+ <text class="form-label required">瀹℃壒鏍囬</text>
+ <up-input v-model="form.title"
+ placeholder="璇疯緭鍏ュ鎵规爣棰�"
+ maxlength="100"
+ clearable />
+ </view>
+ <view class="form-row">
+ <text class="form-label">瀹℃壒妯℃澘</text>
+ <text class="form-readonly">{{ templateName }}</text>
+ </view>
+ <view class="form-row">
+ <text class="form-label">鐢宠浜�</text>
+ <text class="form-readonly">{{ displayApplicantName }}</text>
+ </view>
+ </view>
+ </view>
+
+ <view class="section">
+ <view class="section-title">濉姤鍐呭</view>
+ <view v-if="formConfigData.prompt"
+ class="form-prompt">
+ {{ formConfigData.prompt }}
+ </view>
+ <view v-if="formConfigData.fields.length"
+ class="form-body">
+ <view v-for="field in formConfigData.fields"
+ :key="field.key"
+ class="form-row form-row--field">
+ <text class="form-label"
+ :class="{ required: field.required }">{{ field.label }}</text>
+ <up-textarea v-if="field.type === 'textarea'"
+ v-model="formValues[field.key]"
+ :placeholder="`璇疯緭鍏�${field.label}`"
+ maxlength="500"
+ border="surround"
+ height="80" />
+ <view v-else-if="field.type === 'date'"
+ class="date-trigger"
+ @click="openDatePicker(field.key)">
+ <up-input :model-value="formValues[field.key]"
+ :placeholder="`璇烽�夋嫨${field.label}`"
+ readonly />
+ </view>
+ <up-input v-else
+ v-model="formValues[field.key]"
+ :type="field.type === 'number' ? 'digit' : 'text'"
+ :placeholder="`璇疯緭鍏�${field.label}`"
+ clearable />
+ </view>
+ </view>
+ <view v-else
+ class="empty-hint">璇ユā鏉挎殏鏃犲~鎶ラ」</view>
+ </view>
+
+ <view class="section">
+ <view class="section-title">瀹℃壒娴佺▼</view>
+ <view v-if="detail.nodes?.length"
+ class="flow-list">
+ <view v-for="(node, index) in detail.nodes"
+ :key="node.id || index"
+ class="flow-card">
+ <view class="flow-card-head">
+ <text class="flow-level">绗瑊{ levelLabel(node.levelNo || index + 1) }}绾�</text>
+ <text class="flow-type">{{ approveTypeText(node.approveType) }}</text>
+ </view>
+ <view class="approver-tags">
+ <text v-for="(approver, aIdx) in node.approvers || []"
+ :key="approver.id || aIdx"
+ class="approver-tag">
+ {{ approver.approverName || "-" }}
+ </text>
+ <text v-if="!(node.approvers || []).length"
+ class="empty-hint inline">鏆傛棤瀹℃壒浜�</text>
+ </view>
+ </view>
+ </view>
+ <view v-else
+ class="empty-hint">鏆傛棤瀹℃壒鑺傜偣</view>
+ </view>
+ </template>
+ <view v-else
+ class="empty-wrap">
+ <up-empty mode="data"
+ text="鏈幏鍙栧埌妯℃澘璇︽儏" />
+ </view>
+ </scroll-view>
+
+ <FooterButtons v-if="!loading && detail"
+ cancel-text="鍙栨秷"
+ :confirm-text="confirmText"
+ :loading="submitting"
+ @cancel="goBack"
+ @confirm="handleSubmit" />
+
+ <up-popup :show="showDatePicker"
+ mode="bottom"
+ @close="showDatePicker = false">
+ <up-datetime-picker :show="true"
+ v-model="datePickerTs"
+ mode="date"
+ @confirm="onDateConfirm"
+ @cancel="showDatePicker = false" />
+ </up-popup>
+ </view>
+</template>
+
+<script setup>
+ import { computed, reactive, ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import FooterButtons from "@/components/FooterButtons.vue";
+ import { getApprovalTemplateDetail } from "@/api/oa/approvalTemplate.js";
+ import {
+ saveApprovalInstance,
+ updateApprovalInstance,
+ } from "@/api/oa/approvalInstance.js";
+ import useUserStore from "@/store/modules/user";
+ import { formatDateToYMD, parseTime } from "@/utils/ruoyi";
+
+ const EDIT_STORAGE_KEY = "oa_approve_instance_edit_row";
+ const LEVEL_TEXT = ["", "涓�", "浜�", "涓�", "鍥�", "浜�", "鍏�", "涓�", "鍏�", "涔�", "鍗�"];
+
+ const userStore = useUserStore();
+ const templateId = ref("");
+ const instanceId = ref("");
+ const instanceRow = ref(null);
+ const detail = ref(null);
+ const loading = ref(false);
+ const submitting = ref(false);
+ const formValues = reactive({});
+ const form = reactive({ title: "" });
+
+ const showDatePicker = ref(false);
+ const datePickerTs = ref(Date.now());
+ const activeDateFieldKey = ref("");
+
+ const isEditMode = computed(() => !!instanceId.value);
+
+ const pageTitle = computed(() => (isEditMode.value ? "缂栬緫瀹℃壒" : "鍙戣捣瀹℃壒"));
+ const confirmText = computed(() => (isEditMode.value ? "淇濆瓨" : "鎻愪氦瀹℃壒"));
+
+ const applicantName = computed(
+ () => userStore.nickName || userStore.name || "-"
+ );
+
+ const displayApplicantName = computed(
+ () => instanceRow.value?.applicantName || applicantName.value
+ );
+
+ const templateName = computed(
+ () => detail.value?.templateName || instanceRow.value?.templateName || "-"
+ );
+
+ const parseFormConfig = raw => {
+ if (!raw) return { prompt: "", fields: [] };
+ try {
+ const obj = typeof raw === "string" ? JSON.parse(raw) : raw;
+ return {
+ prompt: obj?.prompt || "",
+ fields: Array.isArray(obj?.fields) ? obj.fields : [],
+ };
+ } catch {
+ return { prompt: "", fields: [] };
+ }
+ };
+
+ const formConfigData = computed(() => {
+ const raw = isEditMode.value
+ ? instanceRow.value?.formConfig
+ : detail.value?.formConfig;
+ return parseFormConfig(raw);
+ });
+
+ const levelLabel = n => LEVEL_TEXT[Number(n)] || String(n);
+ const approveTypeText = type => (type === "OR" ? "鎴栫" : "浼氱");
+
+ const initFormValues = fields => {
+ Object.keys(formValues).forEach(key => {
+ delete formValues[key];
+ });
+ fields.forEach(field => {
+ if (!field?.key) return;
+ formValues[field.key] = field.value ?? field.defaultValue ?? "";
+ });
+ };
+
+ const openDatePicker = fieldKey => {
+ activeDateFieldKey.value = fieldKey;
+ const current = formValues[fieldKey];
+ datePickerTs.value = current ? new Date(current).getTime() : Date.now();
+ showDatePicker.value = true;
+ };
+
+ const onDateConfirm = e => {
+ const ts = e?.value ?? datePickerTs.value;
+ if (activeDateFieldKey.value) {
+ formValues[activeDateFieldKey.value] = formatDateToYMD(ts);
+ }
+ showDatePicker.value = false;
+ };
+
+ const validateForm = () => {
+ if (!form.title?.trim()) {
+ uni.showToast({ title: "璇疯緭鍏ュ鎵规爣棰�", icon: "none" });
+ return false;
+ }
+ for (const field of formConfigData.value.fields) {
+ if (!field.required) continue;
+ const val = formValues[field.key];
+ if (val === undefined || val === null || String(val).trim() === "") {
+ uni.showToast({ title: `璇峰~鍐�${field.label}`, icon: "none" });
+ return false;
+ }
+ }
+ if (!detail.value?.nodes?.length) {
+ uni.showToast({ title: "妯℃澘鏈厤缃鎵规祦绋�", icon: "none" });
+ return false;
+ }
+ return true;
+ };
+
+ const buildFormConfigPayload = () =>
+ JSON.stringify({
+ prompt: formConfigData.value.prompt,
+ fields: formConfigData.value.fields.map(field => ({
+ ...field,
+ value: formValues[field.key] ?? "",
+ })),
+ });
+
+ const buildSavePayload = () => ({
+ templateId: detail.value.id,
+ templateName: detail.value.templateName,
+ businessType: detail.value.businessType,
+ title: form.title.trim(),
+ status: "PENDING",
+ currentLevel: 1,
+ applicantId: userStore.id,
+ applicantName: applicantName.value,
+ applyTime: parseTime(new Date()),
+ deptId: userStore.currentDeptId || undefined,
+ formConfig: buildFormConfigPayload(),
+ });
+
+ const buildUpdatePayload = () => {
+ const row = instanceRow.value || {};
+ return {
+ id: instanceId.value,
+ instanceNo: row.instanceNo,
+ templateId: row.templateId ?? detail.value?.id,
+ templateName: row.templateName ?? detail.value?.templateName,
+ businessId: row.businessId,
+ businessType: row.businessType,
+ title: form.title.trim(),
+ status: row.status || "PENDING",
+ currentLevel: row.currentLevel,
+ applicantId: row.applicantId,
+ applicantName: row.applicantName,
+ applyTime: row.applyTime,
+ deptId: row.deptId,
+ formConfig: buildFormConfigPayload(),
+ };
+ };
+
+ const handleSubmit = () => {
+ if (!validateForm() || submitting.value) return;
+
+ submitting.value = true;
+ const submitApi = isEditMode.value
+ ? updateApprovalInstance
+ : saveApprovalInstance;
+ const payload = isEditMode.value ? buildUpdatePayload() : buildSavePayload();
+
+ submitApi(payload)
+ .then(() => {
+ uni.showToast({
+ title: isEditMode.value ? "淇濆瓨鎴愬姛" : "鎻愪氦鎴愬姛",
+ icon: "success",
+ });
+ if (isEditMode.value) {
+ uni.removeStorageSync(EDIT_STORAGE_KEY);
+ }
+ setTimeout(() => {
+ uni.navigateBack({ delta: isEditMode.value ? 1 : 2 });
+ }, 300);
+ })
+ .catch(() => {
+ uni.showToast({
+ title: isEditMode.value ? "淇濆瓨澶辫触" : "鎻愪氦澶辫触",
+ icon: "none",
+ });
+ })
+ .finally(() => {
+ submitting.value = false;
+ });
+ };
+
+ const loadTemplateDetail = () => {
+ if (!templateId.value) return Promise.resolve();
+ return getApprovalTemplateDetail(templateId.value)
+ .then(res => {
+ detail.value = res?.data || null;
+ if (!detail.value) {
+ uni.showToast({ title: "鏈幏鍙栧埌妯℃澘璇︽儏", icon: "none" });
+ }
+ return detail.value;
+ })
+ .catch(() => {
+ uni.showToast({ title: "鑾峰彇妯℃澘璇︽儏澶辫触", icon: "none" });
+ return null;
+ });
+ };
+
+ const loadForCreate = async () => {
+ loading.value = true;
+ detail.value = null;
+ try {
+ await loadTemplateDetail();
+ if (!detail.value) return;
+ initFormValues(formConfigData.value.fields);
+ if (!form.title && detail.value.templateName) {
+ form.title = `${detail.value.templateName}鐢宠`;
+ }
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ const loadForEdit = async () => {
+ const row = uni.getStorageSync(EDIT_STORAGE_KEY);
+ if (!row || String(row.id) !== String(instanceId.value)) {
+ uni.showToast({ title: "鏈幏鍙栧埌瀹℃壒鏁版嵁", icon: "none" });
+ return;
+ }
+ uni.removeStorageSync(EDIT_STORAGE_KEY);
+ instanceRow.value = row;
+ templateId.value = row.templateId;
+ form.title = row.title || "";
+
+ loading.value = true;
+ detail.value = null;
+ try {
+ await loadTemplateDetail();
+ initFormValues(formConfigData.value.fields);
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ onLoad(options => {
+ if (options?.id) {
+ instanceId.value = options.id;
+ loadForEdit();
+ return;
+ }
+ if (options?.templateId) {
+ templateId.value = options.templateId;
+ loadForCreate();
+ return;
+ }
+ uni.showToast({ title: "缂哄皯椤甸潰鍙傛暟", icon: "none" });
+ });
+</script>
+
+<style scoped lang="scss">
+ .approve-apply-page {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ background: #f0f3f8;
+ }
+
+ .form-scroll {
+ flex: 1;
+ height: 0;
+ padding: 10px 12px calc(96px + env(safe-area-inset-bottom));
+ }
+
+ .loading-wrap {
+ padding: 48px 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 12px;
+ }
+
+ .loading-text {
+ font-size: 14px;
+ color: #909399;
+ }
+
+ .section {
+ background: #fff;
+ border-radius: 12px;
+ margin-bottom: 10px;
+ overflow: hidden;
+ box-shadow: 0 2px 12px rgba(31, 45, 61, 0.05);
+ }
+
+ .section-title {
+ padding: 12px 16px;
+ font-size: 15px;
+ font-weight: 600;
+ color: #1f2d3d;
+ border-bottom: 1px solid #f2f4f7;
+ border-left: 3px solid #2979ff;
+ padding-left: 13px;
+ }
+
+ .form-body {
+ padding: 8px 16px 16px;
+ }
+
+ .form-row {
+ padding: 10px 0;
+ border-bottom: 1px solid #f5f7fa;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &--field {
+ flex-direction: column;
+ align-items: stretch;
+ }
+ }
+
+ .form-label {
+ display: block;
+ margin-bottom: 8px;
+ font-size: 14px;
+ color: #606266;
+
+ &.required::before {
+ content: "*";
+ color: #f56c6c;
+ margin-right: 4px;
+ }
+ }
+
+ .form-readonly {
+ font-size: 14px;
+ color: #303133;
+ }
+
+ .form-prompt {
+ margin: 12px 16px 0;
+ padding: 10px 12px;
+ font-size: 13px;
+ color: #606266;
+ background: #f8fafc;
+ border-radius: 8px;
+ line-height: 1.5;
+ }
+
+ .date-trigger {
+ width: 100%;
+ }
+
+ .flow-list {
+ padding: 12px;
+ }
+
+ .flow-card {
+ padding: 12px;
+ margin-bottom: 8px;
+ background: #f8fafc;
+ border-radius: 8px;
+ border: 1px solid #eef2f6;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ .flow-card-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 8px;
+ }
+
+ .flow-level {
+ font-size: 14px;
+ font-weight: 600;
+ color: #303133;
+ }
+
+ .flow-type {
+ font-size: 13px;
+ color: #2979ff;
+ }
+
+ .approver-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+
+ .approver-tag {
+ padding: 4px 10px;
+ font-size: 13px;
+ color: #303133;
+ background: #fff;
+ border: 1px solid #dce8f8;
+ border-radius: 16px;
+ }
+
+ .empty-hint {
+ padding: 12px 16px 16px;
+ font-size: 13px;
+ color: #909399;
+
+ &.inline {
+ padding: 0;
+ }
+ }
+
+ .empty-wrap {
+ padding: 48px 20px;
+ }
+</style>
diff --git a/src/pages/oa/ApproveManage/approve-list/index.vue b/src/pages/oa/ApproveManage/approve-list/index.vue
index fd00bae..db97060 100644
--- a/src/pages/oa/ApproveManage/approve-list/index.vue
+++ b/src/pages/oa/ApproveManage/approve-list/index.vue
@@ -3,16 +3,297 @@
璺敱锛�/pages/oa/ApproveManage/approve-list/index
-->
<template>
- <OaListPage v-if="config"
- :page-key="pageKey"
- :page-config="config" />
+ <view class="approve-list-page sales-account">
+ <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>
+ </view>
+
+ <scroll-view class="list-scroll"
+ scroll-y
+ :show-scrollbar="false"
+ @scrolltolower="loadMore">
+ <view v-if="list.length"
+ class="ledger-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" />
+ </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">{{ item.applyTime || "-" }}</text>
+ </view>
+ <view v-if="item.finishTime"
+ class="detail-row">
+ <text class="detail-label">瀹屾垚鏃堕棿</text>
+ <text class="detail-value">{{ item.finishTime }}</text>
+ </view>
+ </view>
+ <view v-if="canEdit(item) || item.isApprove"
+ class="action-buttons">
+ <up-button v-if="canEdit(item)"
+ class="action-btn"
+ size="small"
+ @click.stop="goEdit(item)">
+ 缂栬緫
+ </up-button>
+ <up-button v-if="item.isApprove"
+ class="action-btn"
+ size="small"
+ type="primary"
+ @click.stop="handleApprove(item)">
+ 瀹℃壒
+ </up-button>
+ </view>
+ </view>
+ <up-loadmore :status="pageStatus" />
+ </view>
+ <view v-else
+ class="empty-wrap">
+ <up-empty mode="list"
+ text="鏆傛棤瀹℃壒鏁版嵁" />
+ </view>
+ </scroll-view>
+
+ <view class="fab-button"
+ @click="goAdd">
+ <up-icon name="plus"
+ size="28"
+ color="#ffffff" />
+ </view>
+ </view>
</template>
<script setup>
- /** OA - 瀹℃壒绠$悊 - 瀹℃壒鍒楄〃 */
- import OaListPage from "../../_components/OaListPage.vue";
- import { useOaPage } from "../../_utils/useOaPage.js";
+ import { 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";
- const pageKey = "ApproveManage/approve-list";
- const { config } = useOaPage(pageKey);
+ const EDIT_STORAGE_KEY = "oa_approve_instance_edit_row";
+ const userStore = useUserStore();
+
+ const queryParams = reactive({
+ keyword: "",
+ });
+
+ const list = ref([]);
+ const pageStatus = ref("loadmore");
+
+ const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+ });
+
+ 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 currentApproverName = item => {
+ const tasks = item?.tasks;
+ if (!Array.isArray(tasks) || !tasks.length) return "-";
+ const pending = tasks.find(t => t.taskStatus === "PENDING");
+ if (pending?.approverName) return pending.approverName;
+ const names = [...new Set(tasks.map(t => t.approverName).filter(Boolean))];
+ return names.length ? names.join("銆�") : "-";
+ };
+
+ const buildListParams = () => {
+ const keyword = queryParams.keyword?.trim();
+ const dto = {};
+ if (keyword) {
+ if (/[\u4e00-\u9fa5]/.test(keyword)) {
+ dto.title = keyword;
+ } else {
+ dto.instanceNo = keyword;
+ }
+ }
+ return {
+ page: {
+ current: page.current,
+ size: page.size,
+ },
+ approvalInstanceDto: 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";
+ } else {
+ pageStatus.value = "loadmore";
+ page.current += 1;
+ }
+ })
+ .catch(() => {
+ if (page.current === 1) {
+ list.value = [];
+ }
+ pageStatus.value = "loadmore";
+ uni.showToast({ title: "鏌ヨ澶辫触", icon: "none" });
+ });
+ };
+
+ const handleSearch = () => {
+ page.current = 1;
+ pageStatus.value = "loadmore";
+ list.value = [];
+ getList();
+ };
+
+ const loadMore = () => {
+ if (pageStatus.value === "loadmore") {
+ getList();
+ }
+ };
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ const goAdd = () => {
+ uni.navigateTo({ url: OA_NAV.approveListTemplateSelect });
+ };
+
+ const canEdit = item =>
+ item?.status === "PENDING" &&
+ String(item.applicantId) === String(userStore.id);
+
+ const goEdit = item => {
+ if (!item?.id) return;
+ uni.setStorageSync(EDIT_STORAGE_KEY, item);
+ uni.navigateTo({
+ url: `${OA_NAV.approveListApply}?id=${item.id}`,
+ });
+ };
+
+ const handleApprove = item => {
+ if (!item?.id) return;
+ uni.showToast({ title: "瀹℃壒璇︽儏椤靛緟瀵规帴", icon: "none" });
+ };
+
+ onShow(() => {
+ handleSearch();
+ });
</script>
+
+<style scoped lang="scss">
+ @import "@/styles/sales-common.scss";
+
+ .approve-list-page {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ }
+
+ .list-scroll {
+ flex: 1;
+ height: 0;
+ padding-bottom: calc(80px + env(safe-area-inset-bottom));
+ }
+
+ .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;
+ }
+</style>
diff --git a/src/pages/oa/ApproveManage/approve-list/template-select.vue b/src/pages/oa/ApproveManage/approve-list/template-select.vue
new file mode 100644
index 0000000..628483b
--- /dev/null
+++ b/src/pages/oa/ApproveManage/approve-list/template-select.vue
@@ -0,0 +1,308 @@
+<!--
+ OA / 瀹℃壒绠$悊 / 閫夋嫨瀹℃壒妯℃澘
+ 璺敱锛�/pages/oa/ApproveManage/approve-list/template-select
+ Tab锛歍ypeEnums 鈫� businessType锛涘垪琛細GET /approvalTemplate/list/1锛堣嚜瀹氫箟宸插惎鐢級鍚庢寜 businessType 绛涢��
+-->
+<template>
+ <view class="template-select-page sales-account">
+ <PageHeader title="閫夋嫨瀹℃壒妯℃澘"
+ @back="goBack" />
+
+ <view v-if="typeOptions.length"
+ class="step-section">
+ <view class="tabs-wrap">
+ <up-tabs :list="tabList"
+ :current="activeTab"
+ line-color="#2979ff"
+ @click="onTabClick" />
+ </view>
+ </view>
+
+ <view class="search-section">
+ <view class="search-bar">
+ <view class="search-input">
+ <up-input v-model="keyword"
+ class="search-text"
+ placeholder="璇疯緭鍏ユā鏉垮悕绉�"
+ clearable />
+ </view>
+ <view class="filter-button">
+ <up-icon name="search"
+ size="24"
+ color="#999" />
+ </view>
+ </view>
+ </view>
+
+ <scroll-view class="list-scroll"
+ scroll-y
+ :show-scrollbar="false">
+ <view v-if="loading"
+ 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">
+ <view v-for="item in displayList"
+ :key="item.id"
+ class="ledger-item ledger-item--clickable"
+ @click="selectTemplate(item)">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="file-text"
+ size="16"
+ color="#ffffff" />
+ </view>
+ <text class="item-id">{{ item.templateName || "-" }}</text>
+ </view>
+ <u-tag :type="enabledTagType(item.enabled)"
+ :text="enabledText(item.enabled)" />
+ </view>
+ <up-divider />
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">瀹℃壒绫诲瀷</text>
+ <text class="detail-value">{{ businessTypeText(item.businessType) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瀹℃壒鑺傜偣</text>
+ <text class="detail-value">{{ nodeCount(item) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">妯℃澘璇存槑</text>
+ <text class="detail-value">{{ item.description || "-" }}</text>
+ </view>
+ </view>
+ </view>
+ </view>
+ <view v-else
+ class="empty-wrap">
+ <up-empty mode="list"
+ :text="emptyText" />
+ </view>
+ </scroll-view>
+ </view>
+</template>
+
+<script setup>
+ 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,
+ filterTemplatesByBusinessType,
+ getBusinessTypeLabel,
+ getDefaultTypeTabIndex,
+ } from "../../_utils/approvalTemplateType.js";
+
+ const typeOptions = ref([]);
+ const typeLabelMap = ref({});
+ /** 鍏ㄩ儴鑷畾涔夊凡鍚敤妯℃澘锛坙ist/1 涓�娆℃媺鍙栵級 */
+ const allTemplates = ref([]);
+ const activeTab = ref(0);
+ const keyword = ref("");
+ const loading = ref(false);
+
+ const tabList = computed(() =>
+ typeOptions.value.map(opt => ({ name: opt.name }))
+ );
+
+ const currentTypeOption = computed(() => typeOptions.value[activeTab.value]);
+
+ const currentSource = computed(() => {
+ const businessType = currentTypeOption.value?.value;
+ return filterTemplatesByBusinessType(allTemplates.value, businessType);
+ });
+
+ const displayList = computed(() => {
+ const kw = keyword.value?.trim().toLowerCase();
+ if (!kw) return currentSource.value;
+ return currentSource.value.filter(item =>
+ (item.templateName || "").toLowerCase().includes(kw)
+ );
+ });
+
+ const emptyText = computed(() => {
+ const typeName = currentTypeOption.value?.name || "璇ュ鎵圭被鍨�";
+ return `鏆傛棤${typeName}涓嬬殑妯℃澘`;
+ });
+
+ const businessTypeText = type =>
+ getBusinessTypeLabel(type, typeLabelMap.value);
+
+ const enabledText = enabled => {
+ const val = String(enabled ?? "");
+ if (val === "1") return "鍚敤";
+ if (val === "0") return "鍋滅敤";
+ return "-";
+ };
+
+ const enabledTagType = enabled => {
+ const val = String(enabled ?? "");
+ if (val === "1") return "success";
+ if (val === "0") return "info";
+ return "info";
+ };
+
+ const nodeCount = item => {
+ const count = item?.nodes?.length;
+ 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([
+ fetchApprovalTemplateTypes(),
+ loadCustomTemplates(),
+ ]);
+ typeOptions.value = opts;
+ typeLabelMap.value = buildTypeLabelMap(opts);
+ activeTab.value = getDefaultTypeTabIndex(opts);
+ } catch {
+ typeOptions.value = [];
+ typeLabelMap.value = {};
+ uni.showToast({ title: "鑾峰彇瀹℃壒绫诲瀷澶辫触", icon: "none" });
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ const onTabClick = item => {
+ activeTab.value = item?.index ?? 0;
+ keyword.value = "";
+ };
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ const selectTemplate = item => {
+ if (!item?.id) return;
+ if (String(item.enabled) === "0") {
+ uni.showToast({ title: "璇ユā鏉垮凡鍋滅敤", icon: "none" });
+ return;
+ }
+ uni.navigateTo({
+ url: `${OA_NAV.approveListApply}?templateId=${item.id}`,
+ });
+ };
+
+ onLoad(() => {
+ initPage();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/sales-common.scss";
+
+ .template-select-page {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ }
+
+ .step-section {
+ background: #fff;
+ border-bottom: 1px solid #f0f0f0;
+ }
+
+ .step-label {
+ display: block;
+ padding: 10px 16px 0;
+ font-size: 13px;
+ font-weight: 600;
+ color: #303133;
+ }
+
+ .step-hint {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ padding: 10px 16px 4px;
+ gap: 8px;
+ }
+
+ .step-desc {
+ flex-shrink: 0;
+ font-size: 12px;
+ color: #909399;
+ }
+
+ .tabs-wrap {
+ padding: 0 12px 4px;
+ }
+
+ .list-scroll {
+ flex: 1;
+ height: 0;
+ padding-bottom: env(safe-area-inset-bottom);
+ }
+
+ .loading-wrap {
+ padding: 48px 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 12px;
+ }
+
+ .loading-text {
+ font-size: 14px;
+ color: #909399;
+ }
+
+ .empty-wrap {
+ padding: 48px 20px;
+ }
+
+ .ledger-item--clickable:active {
+ opacity: 0.92;
+ }
+
+ .card-footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 10px;
+ padding-top: 10px;
+ border-top: 1px dashed #e8ecf0;
+ }
+
+ .card-footer-tip {
+ font-size: 13px;
+ color: #2979ff;
+ }
+</style>
diff --git a/src/pages/oa/ApproveManage/approve-template/detail.vue b/src/pages/oa/ApproveManage/approve-template/detail.vue
index 804ddd9..75dba50 100644
--- a/src/pages/oa/ApproveManage/approve-template/detail.vue
+++ b/src/pages/oa/ApproveManage/approve-template/detail.vue
@@ -24,8 +24,8 @@
<text class="info-value">{{ detail.templateName || "-" }}</text>
</view>
<view class="info-item">
- <text class="info-label">妯℃澘绫诲瀷</text>
- <text class="info-value">{{ templateTypeText(detail.templateType) }}</text>
+ <text class="info-label">瀹℃壒绫诲瀷</text>
+ <text class="info-value">{{ businessTypeText(detail.businessType) }}</text>
</view>
<view class="info-item">
<text class="info-label">鍚敤鐘舵��</text>
@@ -125,6 +125,11 @@
import PageHeader from "@/components/PageHeader.vue";
import FooterButtons from "@/components/FooterButtons.vue";
import { getApprovalTemplateDetail } from "@/api/oa/approvalTemplate.js";
+ import {
+ buildTypeLabelMap,
+ fetchApprovalTemplateTypes,
+ getTemplateTypeLabel,
+ } from "../../_utils/approvalTemplateType.js";
const EDIT_STORAGE_KEY = "oa_approve_template_edit_row";
const LEVEL_TEXT = ["", "涓�", "浜�", "涓�", "鍥�", "浜�", "鍏�", "涓�", "鍏�", "涔�", "鍗�"];
@@ -139,6 +144,7 @@
const templateId = ref("");
const detail = ref(null);
const loading = ref(false);
+ const typeLabelMap = ref({});
const formConfigData = computed(() => {
const raw = detail.value?.formConfig;
@@ -156,12 +162,8 @@
const levelLabel = n => LEVEL_TEXT[Number(n)] || String(n);
- const templateTypeText = type => {
- const val = Number(type);
- if (val === 0) return "绯荤粺鍐呯疆";
- if (val === 1) return "鑷畾涔�";
- return "-";
- };
+ const businessTypeText = type =>
+ getTemplateTypeLabel(type, typeLabelMap.value);
const enabledText = enabled => {
const val = String(enabled ?? "");
@@ -213,6 +215,11 @@
};
onLoad(options => {
+ fetchApprovalTemplateTypes()
+ .then(opts => {
+ typeLabelMap.value = buildTypeLabelMap(opts);
+ })
+ .catch(() => {});
if (options?.id) {
templateId.value = options.id;
loadDetail();
diff --git a/src/pages/oa/ApproveManage/approve-template/edit.vue b/src/pages/oa/ApproveManage/approve-template/edit.vue
index 3b50270..aba2103 100644
--- a/src/pages/oa/ApproveManage/approve-template/edit.vue
+++ b/src/pages/oa/ApproveManage/approve-template/edit.vue
@@ -28,19 +28,18 @@
maxlength="50"
clearable />
</up-form-item>
- <up-form-item label="妯℃澘绫诲瀷"
- prop="templateType"
+ <up-form-item label="瀹℃壒绫诲瀷"
+ prop="businessType"
required
- class="form-item-type">
- <up-radio-group v-model="form.templateType"
- class="type-radio-group"
- placement="row"
- @change="onTemplateTypeChange">
- <up-radio v-for="opt in TEMPLATE_TYPE_OPTIONS"
- :key="opt.value"
- :name="opt.value"
- :label="opt.name" />
- </up-radio-group>
+ class="form-item-select"
+ @click="openBusinessTypeSheet">
+ <up-input :model-value="businessTypeText"
+ placeholder="璇烽�夋嫨瀹℃壒绫诲瀷"
+ readonly />
+ <template #right>
+ <up-icon name="arrow-right"
+ @click.stop="openBusinessTypeSheet" />
+ </template>
</up-form-item>
<up-form-item label="鍚敤鐘舵��"
class="form-item-switch">
@@ -311,6 +310,12 @@
</scroll-view>
</view>
</up-popup>
+
+ <up-action-sheet :show="showBusinessTypeSheet"
+ title="閫夋嫨瀹℃壒绫诲瀷"
+ :actions="businessTypeActions"
+ @select="onSelectBusinessType"
+ @close="showBusinessTypeSheet = false" />
</view>
</template>
@@ -325,6 +330,7 @@
} from "@/api/oa/approvalTemplate.js";
import { userListNoPageByTenantId } from "@/api/system/user";
import { formatDateToYMD } from "@/utils/ruoyi";
+ import { fetchApprovalTemplateTypes } from "../../_utils/approvalTemplateType.js";
const EDIT_STORAGE_KEY = "oa_approve_template_edit_row";
@@ -384,6 +390,7 @@
const form = reactive({
templateName: "",
+ businessType: null,
templateType: 1,
enabled: "1",
description: "",
@@ -413,11 +420,11 @@
const rules = {
templateName: [{ required: true, message: "璇疯緭鍏ユā鏉垮悕绉�", trigger: "blur" }],
- templateType: [
+ businessType: [
{
validator: (_rule, value, callback) => {
if (value === "" || value === null || value === undefined) {
- callback(new Error("璇烽�夋嫨妯℃澘绫诲瀷"));
+ callback(new Error("璇烽�夋嫨瀹℃壒绫诲瀷"));
return;
}
callback();
@@ -427,10 +434,22 @@
],
};
- const TEMPLATE_TYPE_OPTIONS = [
- { name: "绯荤粺鍐呯疆", value: 0 },
- { name: "鑷畾涔�", value: 1 },
- ];
+ const businessTypeOptions = ref([]);
+ const showBusinessTypeSheet = ref(false);
+
+ const businessTypeActions = computed(() =>
+ businessTypeOptions.value.map(opt => ({
+ name: opt.name,
+ value: opt.value,
+ }))
+ );
+
+ const businessTypeText = computed(() => {
+ const matched = businessTypeOptions.value.find(
+ opt => String(opt.value) === String(form.businessType)
+ );
+ return matched?.name || "";
+ });
const presetActions = FORM_PRESETS.map(item => ({
name: item.name,
@@ -487,10 +506,12 @@
if (!row) return;
templateId.value = row.id;
form.templateName = row.templateName || "";
- form.templateType =
- row.templateType === 0 || row.templateType === 1
- ? row.templateType
- : Number(row.templateType) || 1;
+ const parsedBusiness = Number(row.businessType);
+ form.businessType = Number.isNaN(parsedBusiness)
+ ? row.businessType
+ : parsedBusiness;
+ const parsedTemplateType = Number(row.templateType);
+ form.templateType = Number.isNaN(parsedTemplateType) ? 1 : parsedTemplateType;
form.enabled = String(row.enabled ?? "1");
form.description = row.description || "";
@@ -526,8 +547,18 @@
uni.navigateBack();
};
- const onTemplateTypeChange = () => {
- formRef.value?.validateField?.("templateType");
+ const openBusinessTypeSheet = () => {
+ if (!businessTypeOptions.value.length) {
+ uni.showToast({ title: "瀹℃壒绫诲瀷鍔犺浇涓�", icon: "none" });
+ return;
+ }
+ showBusinessTypeSheet.value = true;
+ };
+
+ const onSelectBusinessType = action => {
+ form.businessType = action.value;
+ showBusinessTypeSheet.value = false;
+ formRef.value?.validateField?.("businessType");
};
const onSelectPreset = action => {
@@ -710,6 +741,7 @@
templateName: form.templateName.trim(),
enabled: form.enabled,
description: form.description?.trim() || "",
+ businessType: form.businessType,
templateType: form.templateType,
formConfig: JSON.stringify({
prompt: formConfig.prompt?.trim() || "",
@@ -792,7 +824,25 @@
}
});
+ const loadTemplateTypes = () =>
+ fetchApprovalTemplateTypes()
+ .then(opts => {
+ businessTypeOptions.value = opts;
+ if (!templateId.value && opts.length) {
+ const matched = opts.some(
+ opt => String(opt.value) === String(form.businessType)
+ );
+ if (!matched) {
+ form.businessType = opts[0].value;
+ }
+ }
+ })
+ .catch(() => {
+ uni.showToast({ title: "鑾峰彇瀹℃壒绫诲瀷澶辫触", icon: "none" });
+ });
+
onMounted(() => {
+ loadTemplateTypes();
userListNoPageByTenantId()
.then(res => {
userList.value = res?.data || [];
@@ -926,18 +976,18 @@
font-size: 15px !important;
}
- :deep(.form-item-type .u-form-item__body) {
+ :deep(.form-item-select .u-form-item__body) {
align-items: center !important;
}
- .type-radio-group {
- display: flex;
- justify-content: flex-end;
- flex-wrap: nowrap;
+ :deep(.form-item-select .u-form-item__content) {
+ flex: 1 !important;
+ min-width: 0 !important;
+ justify-content: flex-end !important;
}
- :deep(.type-radio-group .u-radio) {
- margin-left: 20px;
+ :deep(.form-item-select .u-input__content__field-wrapper__field) {
+ text-align: right !important;
}
:deep(.form-item-switch .u-form-item__body) {
diff --git a/src/pages/oa/ApproveManage/approve-template/index.vue b/src/pages/oa/ApproveManage/approve-template/index.vue
index 96660bc..44b77c9 100644
--- a/src/pages/oa/ApproveManage/approve-template/index.vue
+++ b/src/pages/oa/ApproveManage/approve-template/index.vue
@@ -48,8 +48,8 @@
<up-divider />
<view class="item-details">
<view class="detail-row">
- <text class="detail-label">妯℃澘绫诲瀷</text>
- <text class="detail-value">{{ templateTypeText(item.templateType) }}</text>
+ <text class="detail-label">瀹℃壒绫诲瀷</text>
+ <text class="detail-value">{{ businessTypeText(item.businessType) }}</text>
</view>
<view class="detail-row">
<text class="detail-label">瀹℃壒鑺傜偣</text>
@@ -115,8 +115,14 @@
deleteApprovalTemplate,
listApprovalTemplatePage,
} from "@/api/oa/approvalTemplate.js";
+ import {
+ buildTypeLabelMap,
+ fetchApprovalTemplateTypes,
+ getTemplateTypeLabel,
+ } from "../../_utils/approvalTemplateType.js";
const EDIT_STORAGE_KEY = "oa_approve_template_edit_row";
+ const typeLabelMap = ref({});
const queryParams = reactive({
templateName: "",
@@ -155,12 +161,15 @@
return "info";
};
- const templateTypeText = type => {
- const val = Number(type);
- if (val === 0) return "绯荤粺鍐呯疆";
- if (val === 1) return "鑷畾涔�";
- return "-";
- };
+ const businessTypeText = type =>
+ getTemplateTypeLabel(type, typeLabelMap.value);
+
+ const loadTemplateTypes = () =>
+ fetchApprovalTemplateTypes()
+ .then(opts => {
+ typeLabelMap.value = buildTypeLabelMap(opts);
+ })
+ .catch(() => {});
const nodeCount = item => {
const count = item?.nodes?.length;
@@ -266,6 +275,7 @@
};
onShow(() => {
+ loadTemplateTypes();
handleSearch();
});
</script>
diff --git a/src/pages/oa/_utils/approvalTemplateType.js b/src/pages/oa/_utils/approvalTemplateType.js
new file mode 100644
index 0000000..fce013e
--- /dev/null
+++ b/src/pages/oa/_utils/approvalTemplateType.js
@@ -0,0 +1,94 @@
+import { getTypeEnums } from "@/api/basic/enum.js";
+
+/**
+ * GET /approvalTemplate/list/{type} 璺緞鍙傛暟涓� templateType
+ * 1 = 鑷畾涔変笖宸插惎鐢紙涓� businessType 鏃犲叧锛�
+ */
+export const CUSTOM_TEMPLATE_LIST_TYPE = 1;
+
+/** 涓氬姟绫诲瀷鏋氫妇鍏滃簳锛坅pproveType锛�1鍏嚭 2璇峰亣 鈥︼級 */
+export const FALLBACK_BUSINESS_TYPE_OPTIONS = [
+ { name: "鍏嚭绠$悊", value: 1 },
+ { name: "璇峰亣绠$悊", value: 2 },
+];
+
+/** 灏� /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 {
+ name: String(name),
+ value: Number.isNaN(num) ? value : num,
+ };
+ });
+ }
+
+ return [];
+}
+
+/** 鎷夊彇涓氬姟绫诲瀷鏋氫妇锛圱ypeEnums 鈫� businessType锛� */
+export async function fetchApprovalTemplateTypes() {
+ const res = await getTypeEnums();
+ const options = normalizeEnumOptions(res?.data);
+ return options.length ? options : [...FALLBACK_BUSINESS_TYPE_OPTIONS];
+}
+
+/** 鎸� businessType 绛涢�夋ā鏉� */
+export function filterTemplatesByBusinessType(templates, businessType) {
+ if (businessType == null || businessType === "") return [];
+ return (templates || []).filter(
+ item => String(item.businessType) === String(businessType)
+ );
+}
+
+/** 榛樿 Tab 涓嬫爣锛堝彲鎸変笟鍔$被鍨� value 鎸囧畾锛岄粯璁ょ涓�椤癸級 */
+export function getDefaultTypeTabIndex(options, defaultBusinessType) {
+ if (!options?.length) return 0;
+ if (defaultBusinessType == null) return 0;
+ const idx = options.findIndex(
+ opt => String(opt.value) === String(defaultBusinessType)
+ );
+ return idx >= 0 ? idx : 0;
+}
+
+export function buildTypeLabelMap(options) {
+ const map = {};
+ (options || []).forEach(opt => {
+ map[String(opt.value)] = opt.name;
+ });
+ return map;
+}
+
+/** 鏍规嵁 businessType 鏄剧ず涓氬姟绫诲瀷鍚嶇О */
+export function getTemplateTypeLabel(type, labelMap) {
+ if (type == null || type === "") return "-";
+ return labelMap?.[String(type)] ?? String(type);
+}
+
+export const getBusinessTypeLabel = getTemplateTypeLabel;
--
Gitblit v1.9.3