gaoluyang
2026-06-16 2f58128ac3b1c025a32f64016328992bf9bf5b48
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>