| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!-- |
| | | ä¸å¡å®¡æ¹ç³è¯·å表ï¼è½¬æ£/è°å²/交æ¥/请å/å çï¼ |
| | | --> |
| | | <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> |