From a9d97b150701e634bdb751eab277696abd136cca Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期二, 16 六月 2026 14:39:47 +0800
Subject: [PATCH] 君歌app 1.依照web端功能修改

---
 src/pages/oa/_components/ApprovalInstanceListPage.vue |  353 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 353 insertions(+), 0 deletions(-)

diff --git a/src/pages/oa/_components/ApprovalInstanceListPage.vue b/src/pages/oa/_components/ApprovalInstanceListPage.vue
new file mode 100644
index 0000000..02b1f37
--- /dev/null
+++ b/src/pages/oa/_components/ApprovalInstanceListPage.vue
@@ -0,0 +1,353 @@
+<!--
+  涓氬姟瀹℃壒鐢宠鍒楄〃锛堣浆姝�/璋冨矖/浜ゆ帴/璇峰亣/鍔犵彮锛�
+-->
+<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,
+    getModuleListBusinessType,
+  } from "../_utils/approvalModuleRegistry.js";
+  import {
+    buildModuleListDto,
+    createModuleSearchForm,
+    filterRowsByModuleSearch,
+    filterRowsByModuleBusinessType,
+    formatDateRangeLabel,
+    getModuleSearchMeta,
+    hasActiveModuleSearch,
+  } 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 || [];
+      let mapped = records.map(row => mapInstanceListRow(row, listFields));
+      if (hasActiveModuleSearch(props.moduleKey, searchForm)) {
+        mapped = filterRowsByModuleSearch(props.moduleKey, mapped, searchForm);
+      }
+
+      if (page.current === 1) {
+        list.value = mapped;
+      } else {
+        list.value = [...list.value, ...mapped];
+      }
+      const dropped = records.length - mapped.length;
+      page.total = hasActiveModuleSearch(props.moduleKey, searchForm)
+        ? list.value.length
+        : dropped > 0
+          ? Math.max(0, Number(total) - dropped)
+          : Number(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 () => {
+    const fixed = getModuleListBusinessType(props.moduleKey);
+    businessType.value = fixed != null && fixed !== "" ? fixed : "";
+    try {
+      typeOptions.value = await fetchApprovalTemplateTypes();
+    } catch {
+      typeOptions.value = [];
+    }
+  };
+
+  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>

--
Gitblit v1.9.3