From 5b248a9716688d8132cfb02b4ba0abecd4060b06 Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期三, 20 五月 2026 11:49:08 +0800
Subject: [PATCH] 审批模板流程化

---
 src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue |  370 +++++++++++++++++++++++++++++++---------------------
 1 files changed, 221 insertions(+), 149 deletions(-)

diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue b/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
index 774b322..b87a964 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
@@ -63,119 +63,97 @@
             {{ approvalTypeLabel(row.approvalType) }}
           </span>
         </template>
-        <template #approvalMethod="{ row }">
-          <span class="approval-method-text">{{ approvalModeLabel(row.approvalMode) }}</span>
-        </template>
       </PIMTable>
     </div>
 
     <!-- 鎻愪氦瀹℃壒锛堟寜妯℃澘锛� -->
     <el-dialog
       v-model="submitDialog.visible"
-      :title="submitDialog.step === 1 ? '閫夋嫨瀹℃壒妯℃澘' : `鎻愪氦${activeTemplate?.label || '瀹℃壒'}`"
+      :title="submitDialogTitle"
       width="720px"
       append-to-body
       destroy-on-close
       class="approve-submit-dialog"
-      @closed="submitDialog.step = 1"
+      @closed="resetSubmitDialogState"
     >
-      <template v-if="submitDialog.step === 1">
-        <p class="template-hint">璇烽�夋嫨瑕佹彁浜ょ殑瀹℃壒绫诲瀷锛岀郴缁熷皢鎸夊搴旀ā鏉垮紩瀵煎~鎶ワ紙瀛楁鍚庢湡涓庡悗绔悓姝ワ級銆�</p>
-        <div class="template-grid">
+      <template v-if="submitDialog.step === 1 && !isSubmitEdit">
+        <p class="template-hint">璇峰厛閫夋嫨妯℃澘绫诲瀷锛屽啀閫夋嫨璇ョ被鍨嬩笅宸插惎鐢ㄧ殑瀹℃壒妯℃澘銆�</p>
+        <div v-loading="submitTemplatesLoading" class="template-grid">
           <div
-            v-for="(tpl, key) in SUBMIT_TEMPLATES"
-            :key="key"
+            v-for="opt in submitBusinessTypeOptions"
+            :key="`biz-type-${opt.value}`"
             class="template-card"
-            @click="onTemplatePick(key)"
+            :class="{ 'is-disabled': !countTemplatesByBusinessType(opt.value) }"
+            @click="onBusinessTypePick(opt.value)"
           >
-            <span class="template-card-type" :style="approvalTypeStyle(tpl.approvalType)">
-              {{ tpl.label }}
+            <span class="template-card-type">{{ opt.label }}</span>
+            <span class="template-card-desc">
+              {{ countTemplatesByBusinessType(opt.value) }} 涓彲鐢ㄦā鏉�
             </span>
-            <span class="template-card-desc">{{ tpl.summaryPlaceholder || "鐐瑰嚮濉啓骞舵彁浜�" }}</span>
           </div>
+          <el-empty
+            v-if="!submitTemplatesLoading && !submitBusinessTypeOptions.length"
+            description="鏆傛棤妯℃澘绫诲瀷"
+            :image-size="80"
+            class="template-empty"
+          />
         </div>
       </template>
 
+      <template v-else-if="submitDialog.step === 2 && !isSubmitEdit">
+        <p class="template-hint">
+          褰撳墠绫诲瀷锛歿{ selectedBusinessTypeLabel || "鈥�" }}锛岃閫夋嫨鍏蜂綋瀹℃壒妯℃澘銆�
+          <el-button type="primary" link class="ml8" @click="backToBusinessTypePick">鏇存崲绫诲瀷</el-button>
+        </p>
+        <ApprovalTemplatePicker
+          :cards="submitTemplateCards"
+          :loading="submitTemplatesLoading"
+          @pick="onTemplatePick"
+        />
+      </template>
+
       <template v-else>
+        <div v-loading="submitTemplatesLoading && !isSubmitEdit">
         <el-form ref="submitFormRef" :model="submitForm" :rules="submitFormRules" label-width="120px">
-          <el-form-item label="瀹℃壒绫诲瀷">
+          <el-form-item v-if="isSubmitEdit" label="瀹℃壒绫诲瀷">
             <span class="approve-type-cell" :style="approvalTypeStyle(activeTemplate.approvalType)">
               {{ activeTemplate.label }}
             </span>
-            <el-button type="primary" link class="ml12" @click="backToTemplatePick">鏇存崲妯℃澘</el-button>
           </el-form-item>
-          <el-form-item label="瀹℃壒鏂瑰紡" prop="approvalMode">
-            <el-radio-group v-model="submitForm.approvalMode">
-              <el-radio value="parallel">涓庣</el-radio>
-              <el-radio value="or_sign">鎴栫</el-radio>
-            </el-radio-group>
-          </el-form-item>
-          <template v-for="field in activeTemplate.fields" :key="field.key">
-            <el-form-item :label="field.label" :prop="`formPayload.${field.key}`">
-              <el-input
-                v-if="field.type === 'text'"
-                v-model="submitForm.formPayload[field.key]"
-                :placeholder="`璇疯緭鍏�${field.label}`"
-                maxlength="200"
-              />
-              <el-input
-                v-else-if="field.type === 'textarea'"
-                v-model="submitForm.formPayload[field.key]"
-                type="textarea"
-                :rows="field.rows || 3"
-                :placeholder="`璇峰~鍐�${field.label}`"
-                maxlength="2000"
-                show-word-limit
-              />
-              <el-input-number
-                v-else-if="field.type === 'number'"
-                v-model="submitForm.formPayload[field.key]"
-                :min="field.min ?? 0"
-                :precision="field.precision ?? 0"
-                controls-position="right"
-                style="width: 100%"
-              />
-              <el-date-picker
-                v-else-if="field.type === 'date'"
-                v-model="submitForm.formPayload[field.key]"
-                type="date"
-                :placeholder="`璇烽�夋嫨${field.label}`"
-                format="YYYY-MM-DD"
-                value-format="YYYY-MM-DD"
-                style="width: 100%"
-              />
-              <el-date-picker
-                v-else-if="field.type === 'datetimerange'"
-                v-model="submitForm.formPayload[field.key]"
-                type="datetimerange"
-                range-separator="鑷�"
-                start-placeholder="寮�濮嬫椂闂�"
-                end-placeholder="缁撴潫鏃堕棿"
-                format="YYYY-MM-DD HH:mm:ss"
-                value-format="YYYY-MM-DD HH:mm:ss"
-                style="width: 100%"
-              />
-              <el-select
-                v-else-if="field.type === 'select'"
-                v-model="submitForm.formPayload[field.key]"
-                :placeholder="`璇烽�夋嫨${field.label}`"
-                style="width: 100%"
-                clearable
-              >
-                <el-option v-for="o in field.options" :key="o.value" :label="o.label" :value="o.value" />
-              </el-select>
-            </el-form-item>
-          </template>
-          <el-form-item label="瀹℃壒娴佺▼">
-            <ApprovalFlowEditor v-model="submitForm.approvalFlowNodes" :user-options="flowUserOptions" />
-            <p class="flow-tip">鑷冲皯淇濈暀涓�涓鎵硅妭鐐癸紱鎻愪氦鍚庤繘鍏ャ�屽鏍镐腑銆嶇姸鎬併��</p>
-          </el-form-item>
+          <ApprovalTemplateFormSection
+            :active-template="activeTemplate"
+            :fields="submitFormFields"
+            :form-payload="submitForm.formPayload"
+            v-model:flow-nodes="submitForm.flowNodes"
+            v-model:attachments="submitForm.storageBlobDTOs"
+            :template-attachments="submitForm.templateAttachments"
+            :user-options="flowUserOptions"
+            :show-template-name="!isSubmitEdit"
+            :allow-change-template="!isSubmitEdit"
+            @change-template="backToTemplatePick"
+          />
         </el-form>
+        </div>
       </template>
 
       <template #footer>
-        <el-button v-if="submitDialog.step === 2" type="primary" @click="onSubmitNew">鎻� 浜�</el-button>
-        <el-button @click="submitDialog.visible = false">{{ submitDialog.step === 1 ? "鍙� 娑�" : "鍏� 闂�" }}</el-button>
+        <el-button
+          v-if="submitDialog.step === 3 || isSubmitEdit"
+          type="primary"
+          :loading="submitSaving"
+          @click="onSubmitInstance"
+        >
+          {{ isSubmitEdit ? "淇� 瀛�" : "鎻� 浜�" }}
+        </el-button>
+        <el-button
+          v-if="submitDialog.step === 2 && !isSubmitEdit"
+          @click="backToBusinessTypePick"
+        >
+          涓婁竴姝�
+        </el-button>
+        <el-button @click="submitDialog.visible = false">
+          {{ submitDialog.step === 1 && !isSubmitEdit ? "鍙� 娑�" : "鍏� 闂�" }}
+        </el-button>
       </template>
     </el-dialog>
 
@@ -186,28 +164,51 @@
       width="920px"
       append-to-body
       destroy-on-close
+      class="approve-detail-dialog"
     >
-      <ApproveDetailPanel :row="detailRow" />
-      <el-divider content-position="left">瀹℃壒娴佺▼</el-divider>
-      <ApprovalFlowProgress
-        :nodes="detailRow.approvalFlowNodes"
-        :current-index="detailRow.currentNodeIndex ?? 0"
-      />
-      <el-divider content-position="left">瀹℃壒璁板綍</el-divider>
-      <el-timeline v-if="detailRow.approvalRecords?.length">
-        <el-timeline-item
-          v-for="(rec, i) in detailRow.approvalRecords"
-          :key="i"
-          :type="rec.result === 'approved' ? 'success' : rec.result === 'rejected' ? 'danger' : 'primary'"
-          :timestamp="rec.time"
-        >
-          {{ rec.operatorName }} 鈥� {{ approvalActionLabel(rec.result) }}锛歿{ rec.opinion || "鏃犳剰瑙�" }}
-        </el-timeline-item>
-      </el-timeline>
-      <el-empty v-else description="鏆傛棤瀹℃壒璁板綍" :image-size="60" />
+      <div class="approve-detail-body">
+        <ApproveDetailPanel :row="detailRow" />
+        <div class="detail-block">
+          <div class="detail-block-title">
+            瀹℃壒娴佺▼锛坽{ detailRow.tasks?.length || detailRow.flowNodes?.length || 0 }} 椤癸級
+          </div>
+          <InstanceFlowDisplay :tasks="detailRow.tasks" :nodes="detailRow.flowNodes" />
+        </div>
+        <div class="detail-block">
+          <div class="detail-block-title">瀹℃壒璁板綍</div>
+          <el-timeline v-if="detailRow.approvalRecords?.length" class="approve-record-timeline">
+            <el-timeline-item
+              v-for="(rec, i) in detailRow.approvalRecords"
+              :key="rec.id ?? i"
+              :type="rec.result === 'approved' ? 'success' : rec.result === 'rejected' ? 'danger' : 'primary'"
+              :timestamp="formatRecordTime(rec.time)"
+              placement="top"
+            >
+              <div class="record-item">
+                <span class="record-operator">{{ rec.operatorName || "鈥�" }}</span>
+                <el-tag
+                  size="small"
+                  :type="rec.result === 'approved' ? 'success' : rec.result === 'rejected' ? 'danger' : 'info'"
+                  effect="plain"
+                >
+                  {{ approvalActionLabel(rec.result) }}
+                </el-tag>
+                <p class="record-opinion">{{ rec.opinion || "鏃犳剰瑙�" }}</p>
+              </div>
+            </el-timeline-item>
+          </el-timeline>
+          <el-empty v-else description="鏆傛棤瀹℃壒璁板綍" :image-size="48" />
+        </div>
+      </div>
       <template #footer>
         <el-button
           v-if="detailRow.approvalStatus === 'pending'"
+          @click="openEditFromDetail"
+        >
+          淇� 鏀�
+        </el-button>
+        <el-button
+          v-if="detailRow.approvalStatus === 'pending' && detailRow.isApprove"
           type="primary"
           @click="openApproveFromDetail"
         >
@@ -227,11 +228,12 @@
       @closed="approveOpinion = ''"
     >
       <ApproveDetailPanel :row="approveDialog.row" />
-      <el-divider content-position="left">娴佺▼杩涘害</el-divider>
-      <ApprovalFlowProgress
-        :nodes="approveDialog.row?.approvalFlowNodes"
-        :current-index="approveDialog.row?.currentNodeIndex ?? 0"
-      />
+      <div class="detail-block mt16">
+        <div class="detail-block-title">
+          瀹℃壒娴佺▼锛坽{ approveDialog.row?.tasks?.length || approveDialog.row?.flowNodes?.length || 0 }} 椤癸級
+        </div>
+        <InstanceFlowDisplay :tasks="approveDialog.row?.tasks" :nodes="approveDialog.row?.flowNodes" />
+      </div>
       <el-form label-width="100px" class="mt16">
         <el-form-item label="瀹℃壒鎰忚" required>
           <el-input
@@ -245,9 +247,23 @@
         </el-form-item>
       </el-form>
       <template #footer>
-        <el-button type="success" @click="onApprove('approved')">閫� 杩�</el-button>
-        <el-button type="danger" @click="onApprove('rejected')">椹� 鍥�</el-button>
-        <el-button @click="approveDialog.visible = false">鍙� 娑�</el-button>
+        <el-button
+          type="success"
+          :loading="approveSubmitting"
+          @click="onApprove('approved')"
+        >
+          閫� 杩�
+        </el-button>
+        <el-button
+          type="danger"
+          :loading="approveSubmitting"
+          @click="onApprove('rejected')"
+        >
+          椹� 鍥�
+        </el-button>
+        <el-button :disabled="approveSubmitting" @click="approveDialog.visible = false">
+          鍙� 娑�
+        </el-button>
       </template>
     </el-dialog>
   </div>
@@ -257,20 +273,27 @@
 import { Plus, RefreshRight } from "@element-plus/icons-vue";
 import { ElMessage } from "element-plus";
 import { onMounted, ref } from "vue";
-import { userListNoPageByTenantId } from "@/api/system/user.js";
-import ApprovalFlowEditor from "@/views/officeProcessAutomation/AttendManage/overtime-apply/components/ApprovalFlowEditor.vue";
-import ApprovalFlowProgress from "@/views/officeProcessAutomation/ReimburseManage/travel-reimburse/components/ApprovalFlowProgress.vue";
+import ApprovalTemplateFormSection from "../approve-shared/components/ApprovalTemplateFormSection.vue";
+import ApprovalTemplatePicker from "../approve-shared/components/ApprovalTemplatePicker.vue";
+import { useFlowUserOptions } from "../approve-shared/useFlowUserOptions.js";
+import { formatDisplayTime } from "../approve-template/approveTemplateConstants.js";
 import { approvalTypeStyle } from "./approveListConstants.js";
 import ApproveDetailPanel from "./components/ApproveDetailPanel.vue";
+import InstanceFlowDisplay from "./components/InstanceFlowDisplay.vue";
 import { useApproveList } from "./useApproveList.js";
 
 const al = useApproveList();
 const {
   Search,
   APPROVAL_TYPE_OPTIONS,
-  SUBMIT_TEMPLATES,
+  submitBusinessTypeOptions,
+  submitTemplateCards,
+  selectedBusinessTypeLabel,
+  countTemplatesByBusinessType,
+  submitTemplatesLoading,
+  onBusinessTypePick,
+  backToBusinessTypePick,
   approvalTypeLabel,
-  approvalModeLabel,
   approvalActionLabel,
   searchForm,
   tableLoading,
@@ -281,54 +304,39 @@
   detailRow,
   approveDialog,
   approveOpinion,
+  approveSubmitting,
   submitDialog,
+  isSubmitEdit,
+  submitDialogTitle,
   submitForm,
   submitFormRef,
+  submitSaving,
   activeTemplate,
+  submitFormFields,
   submitFormRules,
   handleQuery,
   resetSearch,
   pagination,
+  resetSubmitDialogState,
   openSubmitDialog,
+  openEditDialog,
   onTemplatePick,
   backToTemplatePick,
-  submitNewApproval,
+  submitInstanceForm,
   submitApprove,
   openDetail,
   openApprove,
 } = al;
 
-const flowUserOptions = ref([]);
+const { flowUserOptions, loadFlowUsers } = useFlowUserOptions();
 
-function unwrapArray(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 [];
+async function onSubmitInstance() {
+  const ok = await submitInstanceForm();
+  if (ok) ElMessage.success(isSubmitEdit.value ? "淇敼鎴愬姛" : "瀹℃壒宸叉彁浜�");
 }
 
-function isActiveUser(u) {
-  if (u.delFlag === "2" || u.delFlag === 2) return false;
-  if (u.status == null) return true;
-  return String(u.status) === "0";
-}
-
-async function loadUsers() {
-  try {
-    const res = await userListNoPageByTenantId();
-    flowUserOptions.value = unwrapArray(res).filter(isActiveUser);
-  } catch {
-    flowUserOptions.value = [];
-  }
-}
-
-async function onSubmitNew() {
-  const ok = await submitNewApproval();
-  if (ok) ElMessage.success("瀹℃壒宸叉彁浜�");
-}
-
-function onApprove(result) {
-  const ret = submitApprove(result);
+async function onApprove(result) {
+  const ret = await submitApprove(result);
   if (ret?.needOpinion) {
     ElMessage.warning("椹冲洖鏃惰濉啓瀹℃壒鎰忚");
     return;
@@ -338,14 +346,24 @@
   }
 }
 
+function formatRecordTime(time) {
+  return formatDisplayTime(time) || "鈥�";
+}
+
 function openApproveFromDetail() {
   const row = detailRow.value;
   detailDialog.visible = false;
   openApprove(row);
 }
 
+function openEditFromDetail() {
+  const row = detailRow.value;
+  detailDialog.visible = false;
+  openEditDialog(row);
+}
+
 onMounted(() => {
-  loadUsers();
+  loadFlowUsers();
   handleQuery();
 });
 </script>
@@ -385,10 +403,6 @@
   font-size: 13px;
   line-height: 1.5;
 }
-.approval-method-text {
-  color: var(--el-color-danger);
-  font-weight: 500;
-}
 .template-hint {
   font-size: 13px;
   color: var(--el-text-color-secondary);
@@ -398,6 +412,10 @@
   display: grid;
   grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
   gap: 12px;
+  min-height: 120px;
+}
+.template-empty {
+  grid-column: 1 / -1;
 }
 .template-card {
   padding: 14px 16px;
@@ -410,6 +428,17 @@
 .template-card:hover {
   border-color: var(--el-color-primary);
   box-shadow: var(--shadow-sm, 0 2px 8px rgba(0, 0, 0, 0.06));
+}
+.template-card.is-disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+.template-card.is-disabled:hover {
+  border-color: var(--el-border-color-lighter);
+  box-shadow: none;
+}
+.ml8 {
+  margin-left: 8px;
 }
 .template-card-type {
   display: inline-block;
@@ -433,4 +462,47 @@
 .approve-submit-dialog :deep(.el-dialog__body) {
   padding-top: 12px;
 }
+.approve-detail-dialog :deep(.el-dialog__body) {
+  padding-top: 16px;
+  max-height: 70vh;
+  overflow-y: auto;
+}
+.approve-detail-body .detail-block {
+  margin-top: 20px;
+}
+.approve-detail-body .detail-block-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+  margin: 0 0 12px;
+  padding-left: 10px;
+  border-left: 3px solid var(--el-color-primary);
+  line-height: 1.4;
+}
+.approve-record-timeline {
+  padding-left: 4px;
+}
+.record-item {
+  padding: 4px 0 2px;
+}
+.record-operator {
+  font-weight: 600;
+  margin-right: 8px;
+  color: var(--el-text-color-primary);
+}
+.record-opinion {
+  margin: 8px 0 0;
+  font-size: 13px;
+  color: var(--el-text-color-regular);
+  line-height: 1.5;
+}
+.detail-block-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+  margin: 0 0 12px;
+  padding-left: 10px;
+  border-left: 3px solid var(--el-color-primary);
+  line-height: 1.4;
+}
 </style>

--
Gitblit v1.9.3