From 8bba0a2d08c7abc07604a0654661efc884e5d751 Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期六, 16 五月 2026 15:23:17 +0800
Subject: [PATCH] 审批列表和审批模板页面

---
 src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue |  438 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 431 insertions(+), 7 deletions(-)

diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue b/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
index d4ff149..774b322 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
@@ -1,12 +1,436 @@
-<!--
-  妯″潡涓枃鍚嶏細瀹℃壒鍒楄〃
-  鐩綍鏍囪瘑锛欰pproveManage/approve-list锛坅pprove-list 鈫� 涓枃锛氬鎵瑰垪琛級
-  澶嶇敤椤甸潰锛欯/views/procurementManagement/procurementLedger/index.vue锛堥噰璐彴璐︼紱鏂囦欢鍚� index.vue 鈫� 鍏ュ彛椤碉級
--->
+<!--OA妯″潡锛氬鎵瑰垪琛�-->
 <template>
-  <ProcurementLedger />
+  <div class="app-container">
+    <div class="search_form mb20">
+      <div class="search_fields">
+        <span class="search_title">瀹℃壒绫诲瀷锛�</span>
+        <el-select
+          v-model="searchForm.approvalType"
+          placeholder="璇烽�夋嫨瀹℃壒绫诲瀷"
+          clearable
+          filterable
+          style="width: 200px"
+        >
+          <el-option
+            v-for="opt in APPROVAL_TYPE_OPTIONS"
+            :key="opt.value"
+            :label="opt.label"
+            :value="opt.value"
+          />
+        </el-select>
+        <span class="search_title" style="margin-left: 12px">鐢宠浜哄悕绉帮細</span>
+        <el-input
+          v-model="searchForm.applicantKeyword"
+          style="width: 200px"
+          placeholder="璇疯緭鍏ョ敵璇蜂汉鍚嶇О"
+          clearable
+          :prefix-icon="Search"
+          @keyup.enter="handleQuery"
+        />
+        <span class="search_title" style="margin-left: 12px">鍒涘缓鏃堕棿锛�</span>
+        <el-date-picker
+          v-model="searchForm.createTimeRange"
+          type="daterange"
+          range-separator="-"
+          start-placeholder="寮�濮嬫棩鏈�"
+          end-placeholder="缁撴潫鏃ユ湡"
+          format="YYYY-MM-DD"
+          value-format="YYYY-MM-DD"
+          style="width: 260px"
+          clearable
+        />
+        <el-button type="primary" :icon="Search" style="margin-left: 10px" @click="handleQuery">鎼滅储</el-button>
+        <el-button :icon="RefreshRight" @click="resetSearch">閲嶇疆</el-button>
+      </div>
+      <div class="search_actions">
+        <el-button type="primary" :icon="Plus" @click="openSubmitDialog">鎻愪氦瀹℃壒</el-button>
+      </div>
+    </div>
+
+    <div class="table_list">
+      <PIMTable
+        rowKey="id"
+        :column="tableColumn"
+        :tableData="tableData"
+        :page="page"
+        :isSelection="false"
+        :tableLoading="tableLoading"
+        :total="page.total"
+        @pagination="pagination"
+      >
+        <template #approveType="{ row }">
+          <span class="approve-type-cell" :style="approvalTypeStyle(row.approvalType)">
+            {{ 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 || '瀹℃壒'}`"
+      width="720px"
+      append-to-body
+      destroy-on-close
+      class="approve-submit-dialog"
+      @closed="submitDialog.step = 1"
+    >
+      <template v-if="submitDialog.step === 1">
+        <p class="template-hint">璇烽�夋嫨瑕佹彁浜ょ殑瀹℃壒绫诲瀷锛岀郴缁熷皢鎸夊搴旀ā鏉垮紩瀵煎~鎶ワ紙瀛楁鍚庢湡涓庡悗绔悓姝ワ級銆�</p>
+        <div class="template-grid">
+          <div
+            v-for="(tpl, key) in SUBMIT_TEMPLATES"
+            :key="key"
+            class="template-card"
+            @click="onTemplatePick(key)"
+          >
+            <span class="template-card-type" :style="approvalTypeStyle(tpl.approvalType)">
+              {{ tpl.label }}
+            </span>
+            <span class="template-card-desc">{{ tpl.summaryPlaceholder || "鐐瑰嚮濉啓骞舵彁浜�" }}</span>
+          </div>
+        </div>
+      </template>
+
+      <template v-else>
+        <el-form ref="submitFormRef" :model="submitForm" :rules="submitFormRules" label-width="120px">
+          <el-form-item 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>
+        </el-form>
+      </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>
+      </template>
+    </el-dialog>
+
+    <!-- 璇︽儏 -->
+    <el-dialog
+      v-model="detailDialog.visible"
+      title="瀹℃壒璇︽儏"
+      width="920px"
+      append-to-body
+      destroy-on-close
+    >
+      <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" />
+      <template #footer>
+        <el-button
+          v-if="detailRow.approvalStatus === 'pending'"
+          type="primary"
+          @click="openApproveFromDetail"
+        >
+          鍘诲鎵�
+        </el-button>
+        <el-button @click="detailDialog.visible = false">鍏� 闂�</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 瀹℃壒鎿嶄綔 -->
+    <el-dialog
+      v-model="approveDialog.visible"
+      title="瀹℃壒澶勭悊"
+      width="960px"
+      append-to-body
+      destroy-on-close
+      @closed="approveOpinion = ''"
+    >
+      <ApproveDetailPanel :row="approveDialog.row" />
+      <el-divider content-position="left">娴佺▼杩涘害</el-divider>
+      <ApprovalFlowProgress
+        :nodes="approveDialog.row?.approvalFlowNodes"
+        :current-index="approveDialog.row?.currentNodeIndex ?? 0"
+      />
+      <el-form label-width="100px" class="mt16">
+        <el-form-item label="瀹℃壒鎰忚" required>
+          <el-input
+            v-model="approveOpinion"
+            type="textarea"
+            :rows="3"
+            maxlength="500"
+            show-word-limit
+            placeholder="閫氳繃鍙暀绌猴紱椹冲洖璇峰~鍐欏叿浣撳師鍥�"
+          />
+        </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>
+      </template>
+    </el-dialog>
+  </div>
 </template>
 
 <script setup>
-import ProcurementLedger from '@/views/procurementManagement/procurementLedger/index.vue'
+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 { approvalTypeStyle } from "./approveListConstants.js";
+import ApproveDetailPanel from "./components/ApproveDetailPanel.vue";
+import { useApproveList } from "./useApproveList.js";
+
+const al = useApproveList();
+const {
+  Search,
+  APPROVAL_TYPE_OPTIONS,
+  SUBMIT_TEMPLATES,
+  approvalTypeLabel,
+  approvalModeLabel,
+  approvalActionLabel,
+  searchForm,
+  tableLoading,
+  page,
+  tableData,
+  tableColumn,
+  detailDialog,
+  detailRow,
+  approveDialog,
+  approveOpinion,
+  submitDialog,
+  submitForm,
+  submitFormRef,
+  activeTemplate,
+  submitFormRules,
+  handleQuery,
+  resetSearch,
+  pagination,
+  openSubmitDialog,
+  onTemplatePick,
+  backToTemplatePick,
+  submitNewApproval,
+  submitApprove,
+  openDetail,
+  openApprove,
+} = al;
+
+const flowUserOptions = ref([]);
+
+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 [];
+}
+
+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);
+  if (ret?.needOpinion) {
+    ElMessage.warning("椹冲洖鏃惰濉啓瀹℃壒鎰忚");
+    return;
+  }
+  if (ret?.ok) {
+    ElMessage.success(result === "approved" ? "宸查�氳繃" : "宸查┏鍥�");
+  }
+}
+
+function openApproveFromDetail() {
+  const row = detailRow.value;
+  detailDialog.visible = false;
+  openApprove(row);
+}
+
+onMounted(() => {
+  loadUsers();
+  handleQuery();
+});
 </script>
+
+<style scoped>
+.mb20 {
+  margin-bottom: 20px;
+}
+.ml12 {
+  margin-left: 12px;
+}
+.mt16 {
+  margin-top: 16px;
+}
+.search_form {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+}
+.search_fields {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 4px;
+}
+.search_actions {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+.approve-type-cell {
+  display: inline-block;
+  padding: 2px 10px;
+  border-radius: 4px;
+  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);
+  margin: 0 0 16px;
+}
+.template-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+  gap: 12px;
+}
+.template-card {
+  padding: 14px 16px;
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: var(--radius-md, 8px);
+  cursor: pointer;
+  transition: border-color 0.2s, box-shadow 0.2s;
+  background: var(--el-fill-color-blank);
+}
+.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-type {
+  display: inline-block;
+  padding: 2px 8px;
+  border-radius: 4px;
+  font-size: 13px;
+  font-weight: 600;
+  margin-bottom: 8px;
+}
+.template-card-desc {
+  display: block;
+  font-size: 12px;
+  color: var(--el-text-color-secondary);
+  line-height: 1.5;
+}
+.flow-tip {
+  font-size: 12px;
+  color: var(--el-text-color-secondary);
+  margin-top: 8px;
+}
+.approve-submit-dialog :deep(.el-dialog__body) {
+  padding-top: 12px;
+}
+</style>

--
Gitblit v1.9.3