| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244 |
- <template>
- <view class="bill-list">
- <ErpSearchBar
- v-model="keyword"
- :placeholder="`搜索${module?.title || ''}`"
- @search="onSearch"
- />
- <!-- 单据状态筛选 -->
- <view v-if="module?.shape === 'bill'" class="status-filter">
- <view
- v-for="s in statusOptions"
- :key="String(s.value)"
- class="status-filter__item"
- :class="{ 'status-filter__item--active': statusFilter === s.value }"
- @click="onStatusChange(s.value)"
- >
- <text>{{ s.label }}</text>
- </view>
- </view>
- <!-- 列表 -->
- <ErpEmpty
- v-if="!state.loading && !state.records.length"
- :show-retry="state.error"
- @retry="refresh"
- />
- <ErpListItem
- v-for="(row, idx) in state.records"
- :key="(row as any).id ?? idx"
- :title="rowTitle(row)"
- :rows="rowRows(row)"
- :status-text="statusText(row)"
- :status-type="statusType(row)"
- @click="openDetail(row)"
- />
- <ErpLoadMore
- v-if="state.records.length"
- :status="moreStatus"
- @retry="loadMore"
- />
- </view>
- </template>
- <script setup lang="ts">
- /**
- * 通用列表模板(props 驱动):搜索 + 状态筛选 + 卡片列表 + 触底加载
- * 页面壳通过 ref 调用 refresh()/loadMore() 挂接 onPullDownRefresh/onReachBottom
- */
- import { ref, reactive, computed, watch } from 'vue'
- import { getModule, BILL_STATUS_TEXT, BILL_STATUS_TYPE, type ModuleConfig } from '@/modules/registry'
- import { usePagedList } from '@/composables/usePagedList'
- import { formatMoney, formatDateTime } from '@/utils/format'
- import ErpSearchBar from '@/components/common/ErpSearchBar.vue'
- import ErpEmpty from '@/components/common/ErpEmpty.vue'
- import ErpListItem from '@/components/common/ErpListItem.vue'
- import ErpLoadMore from '@/components/common/ErpLoadMore.vue'
- import { moduleDetailPage } from '@/constants/routes'
- const props = defineProps<{ moduleKey: string; initialStatus?: string }>()
- const module = ref<ModuleConfig | null>(null)
- const keyword = ref('')
- const statusFilter = ref<number | null>(null)
- let paged: ReturnType<typeof usePagedList> | null = null
- // 模板渲染用响应式状态(initPaged 内与 usePagedList 同步)
- const state = reactive({ records: [] as any[], loading: false, finished: false, error: false })
- const statusOptions = [
- { label: '全部', value: null },
- { label: '未审核', value: 0 },
- { label: '已审核', value: 1 },
- { label: '已完成', value: 2 }
- ] as { label: string; value: number | null }[]
- const moreStatus = computed(() => {
- if (state.error) return 'error'
- if (state.loading) return 'loading'
- if (state.finished) return 'nomore'
- return 'more'
- })
- watch(
- () => props.moduleKey,
- (key) => {
- if (!key) return
- const m = getModule(key)
- if (!m) {
- uni.showToast({ title: '未知模块', icon: 'none' })
- return
- }
- module.value = m
- uni.setNavigationBarTitle({ title: m.title })
- // 深链状态筛选(如工作台待办直达 ?status=0 → 未审核)
- const s = props.initialStatus
- statusFilter.value = s === undefined || s === '' ? null : Number(s)
- initPaged(m)
- },
- { immediate: true }
- )
- function initPaged(m: ModuleConfig) {
- paged = usePagedList({
- apiPrefix: m.apiPrefix,
- searchKey: m.searchKey,
- fixedQuery: statusFilter.value === null ? {} : { status: statusFilter.value }
- })
- // 同步响应式状态到模板
- watch(
- [paged.records, paged.loading, paged.finished, paged.error],
- ([records, loading, finished, error]) => {
- state.records = [...records]
- state.loading = loading
- state.finished = finished
- state.error = error
- },
- { immediate: true }
- )
- refresh()
- }
- function refresh() {
- return paged?.refresh()
- }
- function loadMore() {
- return paged?.loadMore()
- }
- function onSearch() {
- paged?.onSearchInput(keyword.value)
- }
- // 输入即搜(300ms 防抖在 usePagedList 内)
- watch(keyword, (v) => {
- paged?.onSearchInput(v)
- })
- function onStatusChange(value: number | null) {
- statusFilter.value = value
- if (!paged) return
- paged.setFixedQuery(value === null ? { status: undefined } : { status: value })
- }
- function rowTitle(row: any): string {
- const m = module.value!
- if (m.shape === 'bill') return row.billNo || `${m.title} #${row.id}`
- if (m.shape === 'entity') return row.name || row.attributeName || row.nativeName || `${row.id}`
- return row.materialName || row.billNo || row.name || String(row.id ?? m.title)
- }
- function rowRows(row: any): { label: string; value?: string | number | null; strong?: boolean }[] {
- const m = module.value!
- if (m.shape === 'bill') {
- return [
- { label: '往来单位', value: row.organName || row.memberName || '-' },
- { label: '单据日期', value: row.operTime ? formatDateTime(row.operTime) : row.createTime ? formatDateTime(row.createTime) : '-' },
- {
- label: '金额',
- value:
- row.discountLastMoney != null
- ? formatMoney(row.discountLastMoney)
- : row.totalPrice != null
- ? formatMoney(row.totalPrice)
- : row.changeAmount != null
- ? formatMoney(row.changeAmount)
- : '-',
- strong: true
- }
- ]
- }
- if (m.shape === 'entity') {
- const rows: { label: string; value?: string | number | null }[] = []
- if (row.contacts || row.principal) rows.push({ label: '联系人', value: row.contacts || row.principal })
- if (row.phoneNum || row.telephone) rows.push({ label: '电话', value: row.phoneNum || row.telephone })
- if (row.depotType) rows.push({ label: '类型', value: row.depotType })
- if (row.address) rows.push({ label: '地址', value: row.address })
- if (row.remark) rows.push({ label: '备注', value: row.remark })
- if (!rows.length && row.enabled !== undefined) rows.push({ label: '状态', value: row.enabled === 1 ? '启用' : '停用' })
- return rows
- }
- // 报表:宽松展示前 3 个标量字段
- const rows: { label: string; value?: string | number | null; strong?: boolean }[] = []
- for (const [key, value] of Object.entries(row)) {
- if (rows.length >= 3) break
- if (['id', 'createTime', 'updateTime', 'pageNo', 'pageSize'].includes(key)) continue
- if (value === null || value === undefined || typeof value === 'object') continue
- const strong = /amount|money|price/i.test(key)
- rows.push({
- label: key,
- value: typeof value === 'number' && strong ? formatMoney(value) : String(value),
- strong
- })
- }
- return rows
- }
- function statusText(row: any): string {
- if (module.value?.shape !== 'bill' || row.status === undefined || row.status === null) return ''
- return BILL_STATUS_TEXT[row.status] ?? ''
- }
- function statusType(row: any): 'warning' | 'success' | 'primary' | undefined {
- if (module.value?.shape !== 'bill' || row.status === undefined || row.status === null) return undefined
- return BILL_STATUS_TYPE[row.status]
- }
- function openDetail(row: any) {
- const m = module.value!
- if (m.listOnly) return
- uni.navigateTo({ url: `${moduleDetailPage(m.pkg)}?module=${m.key}&id=${row.id}` })
- }
- defineExpose({ refresh, loadMore })
- </script>
- <style lang="scss" scoped>
- .bill-list {
- min-height: 100vh;
- background: #f2f5fa;
- padding-bottom: 40rpx;
- }
- .status-filter {
- display: flex;
- padding: 12rpx 24rpx;
- background: #fff;
- gap: 16rpx;
- &__item {
- padding: 8rpx 24rpx;
- border-radius: 24rpx;
- font-size: 24rpx;
- color: #606266;
- background: #f4f5f7;
- &--active {
- color: #fff;
- background: #2979ff;
- }
- }
- }
- </style>
|