提交
This commit is contained in:
1 parent
42ad896456
commit
6b2147aa46
66 files changed
+617071
-28046
No files matched your search
@@ -182,6 +182,8 @@ export interface PersonnelQuery {
|
||||
q?: string
|
||||
status?: string
|
||||
personnelAffiliation?: string
|
||||
/** driver | escort;不传则不过滤类型 */
|
||||
personnelType?: string
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
@@ -312,7 +312,7 @@ async function getList() {
|
||||
|
||||
function syncTableSelection() {
|
||||
const table = tableRef.value
|
||||
if (!table) return
|
||||
if (!table?.setCurrentRow) return
|
||||
if (!selectedId.value) {
|
||||
table.setCurrentRow(undefined)
|
||||
return
|
||||
@@ -426,7 +426,7 @@ function handleOpen() {
|
||||
queryParams.keyword = ''
|
||||
queryParams.customerId = props.customerId || undefined
|
||||
nextTick(() => {
|
||||
tableRef.value?.setCurrentRow(undefined)
|
||||
tableRef.value?.setCurrentRow?.(undefined)
|
||||
})
|
||||
getList()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
<!--
|
||||
列表可配置列统计行:点选单元格向上弹出无/求和/平均/最大/最小;
|
||||
仅统计当前页 data;偏好按 storageKey 存 localStorage。
|
||||
父级须用同一容器包裹 ListTable/el-table 与本组件,并传入 table-wrap-el。
|
||||
-->
|
||||
<template>
|
||||
<div
|
||||
ref="statsScrollRef"
|
||||
class="list-column-stats-row"
|
||||
aria-label="列表列统计"
|
||||
@scroll="onStatsRowScroll"
|
||||
>
|
||||
<div class="list-column-stats-row__inner">
|
||||
<div
|
||||
v-for="col in columns"
|
||||
:key="col.key"
|
||||
class="list-column-stats-row__cell"
|
||||
:class="{ 'is-stat': col.interactive, 'is-spacer': !col.interactive }"
|
||||
>
|
||||
<el-popover
|
||||
v-if="col.interactive && col.prop"
|
||||
:visible="menuOpenProp === col.prop"
|
||||
placement="top-start"
|
||||
:width="132"
|
||||
:teleported="true"
|
||||
trigger="click"
|
||||
:hide-after="0"
|
||||
popper-class="list-column-stats-dropdown"
|
||||
@update:visible="(v: boolean) => setMenuOpen(col.prop!, v)"
|
||||
>
|
||||
<template #reference>
|
||||
<button
|
||||
type="button"
|
||||
class="list-column-stats-row__trigger"
|
||||
:class="{ 'is-empty': !cellText(col) }"
|
||||
>
|
||||
{{ cellText(col) || '\u00a0' }}
|
||||
</button>
|
||||
</template>
|
||||
<div class="list-column-stats-menu">
|
||||
<button
|
||||
v-for="opt in COLUMN_STAT_METHOD_OPTIONS"
|
||||
:key="opt.label"
|
||||
type="button"
|
||||
class="list-column-stats-menu__item"
|
||||
:class="{ 'is-active': (config[col.prop!] || '') === opt.value }"
|
||||
@click="pick(col.prop!, opt.value)"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</button>
|
||||
</div>
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import {
|
||||
COLUMN_STAT_METHOD_OPTIONS,
|
||||
applyStatsCellAlignStyles,
|
||||
formatColumnStatDisplay,
|
||||
getTableColumnTextAligns,
|
||||
loadColumnStatConfig,
|
||||
resolveStatsCellAlign,
|
||||
saveColumnStatConfig,
|
||||
setColumnStatMethod,
|
||||
type ColumnStatConfigMap,
|
||||
type ColumnStatMethod,
|
||||
type StatsRowColumn
|
||||
} from '@/utils/list-column-stats'
|
||||
|
||||
defineOptions({ name: 'ListColumnStatsRow' })
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
storageKey: string
|
||||
columns: StatsRowColumn[]
|
||||
data: Record<string, unknown>[] | any[]
|
||||
/** 外层包裹表 + 统计行的元素 */
|
||||
tableWrapEl?: HTMLElement | null
|
||||
/** 自定义取值;默认 row[prop] */
|
||||
getCellValue?: (row: any, prop: string) => unknown
|
||||
}>(),
|
||||
{
|
||||
tableWrapEl: null,
|
||||
getCellValue: undefined
|
||||
}
|
||||
)
|
||||
|
||||
const statsScrollRef = ref<HTMLElement | null>(null)
|
||||
const config = ref<ColumnStatConfigMap>({})
|
||||
const menuOpenProp = ref<string | null>(null)
|
||||
|
||||
let scrollSyncing = false
|
||||
let headerResizeObserver: ResizeObserver | null = null
|
||||
let tableScrollElBound: HTMLElement | null = null
|
||||
|
||||
watch(
|
||||
() => props.storageKey,
|
||||
(key) => {
|
||||
config.value = loadColumnStatConfig(key)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const columnsRef = computed(() => props.columns || [])
|
||||
|
||||
function resolveCellValue(row: any, prop: string): unknown {
|
||||
if (props.getCellValue) return props.getCellValue(row, prop)
|
||||
return row?.[prop]
|
||||
}
|
||||
|
||||
function cellValues(prop: string): unknown[] {
|
||||
return (props.data || []).map((row) => resolveCellValue(row, prop))
|
||||
}
|
||||
|
||||
function cellText(col: StatsRowColumn): string {
|
||||
if (!col.prop) return ''
|
||||
const method = config.value[col.prop]
|
||||
return formatColumnStatDisplay(method, cellValues(col.prop), { money: !!col.money })
|
||||
}
|
||||
|
||||
function setMenuOpen(prop: string, visible: boolean) {
|
||||
menuOpenProp.value = visible ? prop : menuOpenProp.value === prop ? null : menuOpenProp.value
|
||||
}
|
||||
|
||||
function pick(prop: string, method: ColumnStatMethod | '') {
|
||||
config.value = setColumnStatMethod(config.value, prop, method)
|
||||
saveColumnStatConfig(config.value, props.storageKey)
|
||||
menuOpenProp.value = null
|
||||
}
|
||||
|
||||
function getMainTableEl(): HTMLElement | null {
|
||||
const wrap = props.tableWrapEl
|
||||
if (!wrap) return null
|
||||
return (
|
||||
(wrap.querySelector(':scope > .el-table') as HTMLElement | null) ||
|
||||
(wrap.querySelector('.el-table') as HTMLElement | null)
|
||||
)
|
||||
}
|
||||
|
||||
function getTableScrollEl(): HTMLElement | null {
|
||||
const table = getMainTableEl()
|
||||
if (!table) return null
|
||||
return (
|
||||
(table.querySelector('.el-table__body-wrapper .el-scrollbar__wrap') as HTMLElement | null) ||
|
||||
(table.querySelector('.el-table__body-wrapper') as HTMLElement | null)
|
||||
)
|
||||
}
|
||||
|
||||
function getHeaderWidths(): number[] {
|
||||
const table = getMainTableEl()
|
||||
if (!table) return []
|
||||
const cols = table.querySelectorAll('.el-table__header colgroup col')
|
||||
const fromCols: number[] = []
|
||||
cols.forEach((col) => {
|
||||
const el = col as HTMLElement
|
||||
if (el.getAttribute('name') === 'gutter') return
|
||||
const w =
|
||||
el.getBoundingClientRect().width || Number.parseFloat(el.getAttribute('width') || '') || 0
|
||||
if (w > 0) fromCols.push(w)
|
||||
})
|
||||
if (fromCols.length >= columnsRef.value.length) return fromCols
|
||||
|
||||
const ths = table.querySelectorAll('.el-table__header-wrapper th.el-table__cell')
|
||||
const fromThs: number[] = []
|
||||
ths.forEach((th) => {
|
||||
const el = th as HTMLElement
|
||||
if (el.classList.contains('gutter')) return
|
||||
const w = el.getBoundingClientRect().width
|
||||
if (w > 0) fromThs.push(w)
|
||||
})
|
||||
return fromThs
|
||||
}
|
||||
|
||||
function syncWidths() {
|
||||
const widths = getHeaderWidths()
|
||||
const aligns = getTableColumnTextAligns(getMainTableEl())
|
||||
const inner = statsScrollRef.value?.querySelector(
|
||||
'.list-column-stats-row__inner'
|
||||
) as HTMLElement | null
|
||||
const cells = statsScrollRef.value?.querySelectorAll('.list-column-stats-row__cell')
|
||||
if (!inner || !cells?.length || !widths.length) return
|
||||
|
||||
const cols = columnsRef.value
|
||||
const count = Math.min(cells.length, widths.length, cols.length)
|
||||
let total = 0
|
||||
for (let i = 0; i < count; i++) {
|
||||
const w = Math.round(widths[i])
|
||||
total += w
|
||||
const el = cells[i] as HTMLElement
|
||||
el.style.flex = 'none'
|
||||
el.style.width = `${w}px`
|
||||
el.style.minWidth = `${w}px`
|
||||
el.style.maxWidth = `${w}px`
|
||||
applyStatsCellAlignStyles(
|
||||
el,
|
||||
resolveStatsCellAlign(cols[i], aligns[i]),
|
||||
'.list-column-stats-row__trigger'
|
||||
)
|
||||
}
|
||||
inner.style.width = `${total}px`
|
||||
inner.style.minWidth = `${total}px`
|
||||
|
||||
bindTableScrollListener()
|
||||
const tableScroll = getTableScrollEl()
|
||||
const statsScroll = statsScrollRef.value
|
||||
if (tableScroll && statsScroll) {
|
||||
statsScroll.scrollLeft = tableScroll.scrollLeft
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleSync() {
|
||||
nextTick(() => {
|
||||
syncWidths()
|
||||
ensureHeaderObserver()
|
||||
})
|
||||
}
|
||||
|
||||
function ensureHeaderObserver() {
|
||||
const table = getMainTableEl()
|
||||
const header = table?.querySelector('.el-table__header-wrapper') as HTMLElement | null
|
||||
if (!header) return
|
||||
if (headerResizeObserver) headerResizeObserver.disconnect()
|
||||
headerResizeObserver = new ResizeObserver(() => syncWidths())
|
||||
headerResizeObserver.observe(header)
|
||||
header.querySelectorAll('th.el-table__cell').forEach((th) => {
|
||||
if ((th as HTMLElement).classList.contains('gutter')) return
|
||||
headerResizeObserver?.observe(th)
|
||||
})
|
||||
}
|
||||
|
||||
function bindTableScrollListener() {
|
||||
const el = getTableScrollEl()
|
||||
if (!el || el === tableScrollElBound) return
|
||||
if (tableScrollElBound) {
|
||||
tableScrollElBound.removeEventListener('scroll', onTableScroll)
|
||||
}
|
||||
tableScrollElBound = el
|
||||
el.addEventListener('scroll', onTableScroll, { passive: true })
|
||||
}
|
||||
|
||||
function onTableScroll() {
|
||||
if (scrollSyncing) return
|
||||
const tableScroll = getTableScrollEl()
|
||||
const statsScroll = statsScrollRef.value
|
||||
if (!tableScroll || !statsScroll) return
|
||||
if (statsScroll.scrollLeft === tableScroll.scrollLeft) return
|
||||
scrollSyncing = true
|
||||
statsScroll.scrollLeft = tableScroll.scrollLeft
|
||||
requestAnimationFrame(() => {
|
||||
scrollSyncing = false
|
||||
})
|
||||
}
|
||||
|
||||
function onStatsRowScroll() {
|
||||
if (scrollSyncing) return
|
||||
const tableScroll = getTableScrollEl()
|
||||
const statsScroll = statsScrollRef.value
|
||||
if (!tableScroll || !statsScroll) return
|
||||
if (tableScroll.scrollLeft === statsScroll.scrollLeft) return
|
||||
scrollSyncing = true
|
||||
tableScroll.scrollLeft = statsScroll.scrollLeft
|
||||
requestAnimationFrame(() => {
|
||||
scrollSyncing = false
|
||||
})
|
||||
}
|
||||
|
||||
/** 供父级在 header-dragend 后调用 */
|
||||
function resync() {
|
||||
scheduleSync()
|
||||
}
|
||||
|
||||
defineExpose({ resync })
|
||||
|
||||
watch(
|
||||
() => [props.columns, props.data, props.tableWrapEl] as const,
|
||||
() => scheduleSync(),
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
scheduleSync()
|
||||
setTimeout(() => scheduleSync(), 150)
|
||||
setTimeout(() => scheduleSync(), 400)
|
||||
window.addEventListener('resize', scheduleSync)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', scheduleSync)
|
||||
headerResizeObserver?.disconnect()
|
||||
headerResizeObserver = null
|
||||
if (tableScrollElBound) {
|
||||
tableScrollElBound.removeEventListener('scroll', onTableScroll)
|
||||
tableScrollElBound = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.list-column-stats-row {
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
border-top: 2px solid var(--el-color-primary, #10b981);
|
||||
background: var(--mint-primary-bg, #ecfdf5);
|
||||
font-weight: 600;
|
||||
color: var(--mint-text, #1e293b);
|
||||
min-height: 44px;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.list-column-stats-row::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.list-column-stats-row__inner {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
min-height: 44px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.list-column-stats-row__cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 0 8px;
|
||||
box-sizing: border-box;
|
||||
border-right: 1px solid var(--mint-border, #e2e8f0);
|
||||
flex: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-column-stats-row__cell.is-align-left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.list-column-stats-row__cell.is-align-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.list-column-stats-row__cell.is-align-right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.list-column-stats-row__cell.is-spacer {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.list-column-stats-row__trigger {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 28px;
|
||||
margin: 0;
|
||||
padding: 4px 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
line-height: 1.4;
|
||||
font-size: 13px;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
outline: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.list-column-stats-row__trigger.is-empty {
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.list-column-stats-dropdown {
|
||||
z-index: 4000 !important;
|
||||
padding: 6px !important;
|
||||
}
|
||||
|
||||
.list-column-stats-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.list-column-stats-menu__item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 8px 10px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #1e293b;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.list-column-stats-menu__item:hover {
|
||||
background: #ecfdf5;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.list-column-stats-menu__item.is-active {
|
||||
background: #d1fae5;
|
||||
color: #059669;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -129,7 +129,9 @@
|
||||
)
|
||||
|
||||
/**
|
||||
* 代理底层 el-table,保证父组件 ref 上的 clearSelection / doLayout 等仍可用
|
||||
* 代理底层 el-table,保证父组件 ref 上的 clearSelection / setCurrentRow / doLayout 等仍可用。
|
||||
* 必须实现 has:Vue exposeProxy 用 `key in target` 决定是否转发,空对象 Proxy 若不声明 has
|
||||
* 则 `in` 恒为 false,父组件拿到的方法永远是 undefined。
|
||||
*/
|
||||
const exposed = new Proxy(
|
||||
{},
|
||||
@@ -148,6 +150,17 @@
|
||||
if (!table) return undefined
|
||||
const value = table[prop as keyof typeof table]
|
||||
return typeof value === 'function' ? value.bind(table) : value
|
||||
},
|
||||
has(_target, prop) {
|
||||
if (
|
||||
prop === 'elTableRef' ||
|
||||
prop === 'applyColumnWidths' ||
|
||||
prop === 'resetColumnWidths'
|
||||
) {
|
||||
return true
|
||||
}
|
||||
const table = tableRef.value
|
||||
return !!table && prop in table
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -73,16 +73,20 @@
|
||||
<el-form-item label="选择司机" prop="driverId">
|
||||
<el-select
|
||||
v-model="formData.driverId"
|
||||
placeholder="请先选择车辆(可选)"
|
||||
placeholder="请选择司机(可选)"
|
||||
filterable
|
||||
remote
|
||||
clearable
|
||||
:disabled="!formData.vehicleId || vehicleDriverList.length === 0"
|
||||
:remote-method="searchDrivers"
|
||||
:loading="driverSelectLoading"
|
||||
:disabled="!formData.vehicleId"
|
||||
@focus="onDriverSelectFocus"
|
||||
>
|
||||
<el-option
|
||||
v-for="d in vehicleDriverList"
|
||||
:key="d.personnelId"
|
||||
:label="formatDriverLabel(d)"
|
||||
:value="d.personnelId!"
|
||||
v-for="d in driverList"
|
||||
:key="d.id"
|
||||
:label="formatDriverOptionLabel(d)"
|
||||
:value="d.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@@ -345,6 +349,7 @@ import {
|
||||
transportApi,
|
||||
type Plan,
|
||||
type PlanProduct,
|
||||
type Personnel,
|
||||
type Transport,
|
||||
type TransportArrangeResult,
|
||||
type VehiclePersonnelRow
|
||||
@@ -360,6 +365,7 @@ import type { PlanFormProductRow } from '@/composables/usePlContractToPlan'
|
||||
import { usePlFreightCalc } from '@/composables/usePlFreightCalc'
|
||||
import {
|
||||
usePlRemoteCarrierOptions,
|
||||
usePlRemotePersonnelOptions,
|
||||
usePlRemoteVehicleOptions
|
||||
} from '@/composables/usePlRemoteOptions'
|
||||
import { loadVehicleViewBundle, type VehicleViewBundle } from '@/composables/usePlVehicleViewDetail'
|
||||
@@ -370,6 +376,7 @@ import { isTonProductUnit } from '@/utils/crm-product-display'
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { pickBxData } from '@/utils/http/bx'
|
||||
import { canPrintSlipCommand, formatPlVehiclePlateDisplay, type PlPrintSlipCommand } from '@/utils/pl-display'
|
||||
import { getPersonnelAffiliationLabel } from '@/utils/pl-personnel-cert'
|
||||
|
||||
const dictStore = useDictStore()
|
||||
|
||||
@@ -440,7 +447,18 @@ const {
|
||||
getSelectedId: () => formData.carrierId,
|
||||
listParams: { status: '1' }
|
||||
})
|
||||
const vehicleDriverList = ref<VehiclePersonnelRow[]>([])
|
||||
/** 司机下拉:全部启用中的驾驶员(山峡自有 + 三方),不限车辆绑定 */
|
||||
const {
|
||||
list: driverList,
|
||||
loading: driverSelectLoading,
|
||||
search: searchDrivers,
|
||||
onFocus: onDriverSelectFocus,
|
||||
reset: resetDriverOptions,
|
||||
ensureById: ensureSelectedDriverOption
|
||||
} = usePlRemotePersonnelOptions({
|
||||
getSelectedId: () => formData.driverId,
|
||||
listParams: { status: '1', personnelType: 'driver' }
|
||||
})
|
||||
const prefilledDriverId = ref('')
|
||||
const vehicleBundle = ref<VehicleViewBundle | null>(null)
|
||||
const vehicleViewVisible = ref(false)
|
||||
@@ -471,8 +489,7 @@ const formData = reactive({
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
vehicleId: [{ required: true, message: '请选择车辆', trigger: 'change' }],
|
||||
driverId: [{ required: true, message: '请选择司机', trigger: 'change' }]
|
||||
vehicleId: [{ required: true, message: '请选择车辆', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
@@ -560,22 +577,25 @@ watch(canGenerateDibangPlan, allowed => {
|
||||
}
|
||||
})
|
||||
|
||||
function formatDriverLabel(d: VehiclePersonnelRow) {
|
||||
function formatDriverOptionLabel(d: Personnel) {
|
||||
const phone = d.phoneNo ? ` (${d.phoneNo})` : ''
|
||||
return `${d.personnelName}${phone}`
|
||||
const affiliation = getPersonnelAffiliationLabel(d.personnelAffiliation)
|
||||
const suffix = affiliation ? ` · ${affiliation}` : ''
|
||||
return `${d.personnelName}${phone}${suffix}`
|
||||
}
|
||||
|
||||
/** 默认司机仍按车辆绑定人员规则预填;下拉选项已改为全量驾驶员 */
|
||||
function applyVehiclePersonnel(personnelList: VehiclePersonnelRow[], preferredDriverId?: string) {
|
||||
vehicleDriverList.value = personnelList.filter(p => p.role === 'driver' && p.personnelId)
|
||||
if (preferredDriverId && vehicleDriverList.value.some(d => d.personnelId === preferredDriverId)) {
|
||||
const vehicleDrivers = personnelList.filter(p => p.role === 'driver' && p.personnelId)
|
||||
if (preferredDriverId && vehicleDrivers.some(d => d.personnelId === preferredDriverId)) {
|
||||
formData.driverId = preferredDriverId
|
||||
return
|
||||
}
|
||||
const defaultDriver = personnelList.find(p => p.role === 'driver' && p.isDefault === 1 && p.personnelId)
|
||||
if (defaultDriver?.personnelId) {
|
||||
formData.driverId = defaultDriver.personnelId
|
||||
} else if (vehicleDriverList.value.length === 1 && vehicleDriverList.value[0].personnelId) {
|
||||
formData.driverId = vehicleDriverList.value[0].personnelId
|
||||
} else if (vehicleDrivers.length === 1 && vehicleDrivers[0].personnelId) {
|
||||
formData.driverId = vehicleDrivers[0].personnelId
|
||||
} else {
|
||||
formData.driverId = ''
|
||||
}
|
||||
@@ -652,9 +672,9 @@ function resetState() {
|
||||
resetArrangeActions()
|
||||
plan.value = {}
|
||||
products.value = []
|
||||
vehicleDriverList.value = []
|
||||
resetVehicleOptions()
|
||||
resetCarrierOptions()
|
||||
resetDriverOptions()
|
||||
prefilledDriverId.value = ''
|
||||
vehicleBundle.value = null
|
||||
vehicleViewVisible.value = false
|
||||
@@ -687,14 +707,13 @@ function resetState() {
|
||||
}
|
||||
|
||||
async function loadMasterData() {
|
||||
await Promise.all([searchVehicles(''), searchCarriers('')])
|
||||
await Promise.all([searchVehicles(''), searchCarriers(''), searchDrivers('')])
|
||||
}
|
||||
|
||||
async function handleVehicleChange(vehicleId: string, preferredDriverId?: string) {
|
||||
if (!vehicleId) {
|
||||
formData.driverId = ''
|
||||
formData.carrierId = ''
|
||||
vehicleDriverList.value = []
|
||||
vehicleBundle.value = null
|
||||
return
|
||||
}
|
||||
@@ -718,6 +737,9 @@ async function handleVehicleChange(vehicleId: string, preferredDriverId?: string
|
||||
await ensureSelectedCarrierOption(formData.carrierId)
|
||||
}
|
||||
applyVehiclePersonnel(vehicleBundle.value.personnelList || [], preferredDriverId)
|
||||
if (formData.driverId) {
|
||||
await ensureSelectedDriverOption(formData.driverId)
|
||||
}
|
||||
} finally {
|
||||
vehicleLoading.value = false
|
||||
}
|
||||
|
||||
@@ -200,5 +200,16 @@ export function usePlRemotePersonnelOptions(options?: {
|
||||
loaded.value = false
|
||||
}
|
||||
|
||||
return { list, loading, loaded, search, onFocus, reset, invalidate }
|
||||
async function ensureById(personnelId: string) {
|
||||
if (!personnelId || list.value.some(item => item.id === personnelId)) return
|
||||
try {
|
||||
const res = await personnelApi.get(personnelId)
|
||||
const detail = pickBxData<Personnel>(res)
|
||||
if (detail?.id) list.value = [detail, ...list.value]
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
return { list, loading, loaded, search, onFocus, reset, invalidate, ensureById }
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
export type ColumnStatMethod = 'sum' | 'avg' | 'max' | 'min'
|
||||
|
||||
export type StatsCellAlign = 'left' | 'center' | 'right'
|
||||
|
||||
export type StatsRowColumn = {
|
||||
key: string
|
||||
interactive: boolean
|
||||
prop?: string
|
||||
money?: boolean
|
||||
/** 显式对齐;缺省时由统计行从主表表头同步 */
|
||||
align?: StatsCellAlign
|
||||
}
|
||||
|
||||
export const COLUMN_STAT_METHOD_OPTIONS: { value: ColumnStatMethod | ''; label: string }[] = [
|
||||
{ value: '', label: '无' },
|
||||
{ value: 'sum', label: '求和' },
|
||||
{ value: 'avg', label: '平均' },
|
||||
{ value: 'max', label: '最大值' },
|
||||
{ value: 'min', label: '最小值' }
|
||||
]
|
||||
|
||||
export const COLUMN_STAT_METHOD_LABEL: Record<ColumnStatMethod, string> = {
|
||||
sum: '求和',
|
||||
avg: '平均',
|
||||
max: '最大值',
|
||||
min: '最小值'
|
||||
}
|
||||
|
||||
export function normalizeCssTextAlign(value: string | null | undefined): StatsCellAlign {
|
||||
const ta = String(value || 'left').toLowerCase()
|
||||
if (ta === 'right' || ta === 'end') return 'right'
|
||||
if (ta === 'center') return 'center'
|
||||
return 'left'
|
||||
}
|
||||
|
||||
/**
|
||||
* 优先列配置 align;金额列默认 right;
|
||||
* 其余同步主表列对齐(表体 `align`,无数据时回退表头)。
|
||||
* 注意:多数业务表 `header-align=center` 且 `align=right`,不可只读表头。
|
||||
*/
|
||||
export function resolveStatsCellAlign(
|
||||
col: Pick<StatsRowColumn, 'align' | 'money'>,
|
||||
columnAlign?: string | null
|
||||
): StatsCellAlign {
|
||||
if (col.align === 'left' || col.align === 'center' || col.align === 'right') return col.align
|
||||
if (col.money) return 'right'
|
||||
if (columnAlign) return normalizeCssTextAlign(columnAlign)
|
||||
return 'left'
|
||||
}
|
||||
|
||||
/** 从主表读取各列 text-align:优先首行表体(对应 `align`),无行时回退表头 */
|
||||
export function getTableColumnTextAligns(table: HTMLElement | null | undefined): string[] {
|
||||
if (!table) return []
|
||||
const collect = (cells: NodeListOf<Element>): string[] => {
|
||||
const aligns: string[] = []
|
||||
cells.forEach((node) => {
|
||||
const el = node as HTMLElement
|
||||
if (el.classList.contains('gutter')) return
|
||||
aligns.push(getComputedStyle(el).textAlign || 'left')
|
||||
})
|
||||
return aligns
|
||||
}
|
||||
const firstRow = table.querySelector('.el-table__body-wrapper tbody tr')
|
||||
if (firstRow) {
|
||||
const fromBody = collect(firstRow.querySelectorAll('td.el-table__cell'))
|
||||
if (fromBody.length) return fromBody
|
||||
}
|
||||
return collect(table.querySelectorAll('.el-table__header-wrapper th.el-table__cell'))
|
||||
}
|
||||
|
||||
export function applyStatsCellAlignStyles(
|
||||
cellEl: HTMLElement,
|
||||
align: StatsCellAlign,
|
||||
triggerSelector: string
|
||||
) {
|
||||
cellEl.style.justifyContent =
|
||||
align === 'right' ? 'flex-end' : align === 'center' ? 'center' : 'flex-start'
|
||||
cellEl.classList.remove('is-align-left', 'is-align-center', 'is-align-right')
|
||||
cellEl.classList.add(`is-align-${align}`)
|
||||
const trigger = cellEl.querySelector(triggerSelector) as HTMLElement | null
|
||||
if (trigger) trigger.style.textAlign = align
|
||||
}
|
||||
|
||||
export function parseStatNumber(raw: unknown): number | null {
|
||||
if (raw == null || raw === '') return null
|
||||
if (typeof raw === 'number') return Number.isFinite(raw) ? raw : null
|
||||
let s = String(raw).trim()
|
||||
if (!s) return null
|
||||
s = s.replace(/^¥/, '').replace(/%$/, '').replace(/,/g, '')
|
||||
const n = Number(s)
|
||||
return Number.isFinite(n) ? n : null
|
||||
}
|
||||
|
||||
function nonEmptyString(raw: unknown): string | null {
|
||||
if (raw == null) return null
|
||||
const s = String(raw).trim()
|
||||
return s ? s : null
|
||||
}
|
||||
|
||||
export function aggregateColumnValues(
|
||||
values: unknown[],
|
||||
method: ColumnStatMethod
|
||||
): number | string | null {
|
||||
const nums: number[] = []
|
||||
const strs: string[] = []
|
||||
for (const v of values) {
|
||||
const n = parseStatNumber(v)
|
||||
if (n != null) nums.push(n)
|
||||
else {
|
||||
const s = nonEmptyString(v)
|
||||
if (s != null) strs.push(s)
|
||||
}
|
||||
}
|
||||
|
||||
if (method === 'sum') {
|
||||
if (!nums.length) return null
|
||||
return nums.reduce((a, b) => a + b, 0)
|
||||
}
|
||||
if (method === 'avg') {
|
||||
if (!nums.length) return null
|
||||
return nums.reduce((a, b) => a + b, 0) / nums.length
|
||||
}
|
||||
if (method === 'max') {
|
||||
if (nums.length) return Math.max(...nums)
|
||||
if (strs.length) return strs.reduce((a, b) => (a >= b ? a : b))
|
||||
return null
|
||||
}
|
||||
if (nums.length) return Math.min(...nums)
|
||||
if (strs.length) return strs.reduce((a, b) => (a <= b ? a : b))
|
||||
return null
|
||||
}
|
||||
|
||||
export function formatStatNumberResult(
|
||||
value: number,
|
||||
opts: { money?: boolean } = {}
|
||||
): string {
|
||||
if (opts.money) {
|
||||
return value.toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
})
|
||||
}
|
||||
if (Number.isInteger(value)) return String(value)
|
||||
return value.toLocaleString('zh-CN', { maximumFractionDigits: 4 })
|
||||
}
|
||||
|
||||
export function formatColumnStatDisplay(
|
||||
method: ColumnStatMethod | '' | null | undefined,
|
||||
values: unknown[],
|
||||
opts: { money?: boolean } = {}
|
||||
): string {
|
||||
if (!method) return ''
|
||||
const label = COLUMN_STAT_METHOD_LABEL[method]
|
||||
const agg = aggregateColumnValues(values, method)
|
||||
if (agg == null) return `${label} -`
|
||||
if (typeof agg === 'number') {
|
||||
return `${label} ${formatStatNumberResult(agg, opts)}`
|
||||
}
|
||||
return `${label} ${agg}`
|
||||
}
|
||||
|
||||
export type ColumnStatConfigMap = Partial<Record<string, ColumnStatMethod>>
|
||||
|
||||
export function loadColumnStatConfig(storageKey: string): ColumnStatConfigMap {
|
||||
try {
|
||||
const raw = localStorage.getItem(storageKey)
|
||||
if (!raw) return {}
|
||||
const parsed = JSON.parse(raw) as Record<string, unknown>
|
||||
if (!parsed || typeof parsed !== 'object') return {}
|
||||
const out: ColumnStatConfigMap = {}
|
||||
for (const [k, v] of Object.entries(parsed)) {
|
||||
if (v === 'sum' || v === 'avg' || v === 'max' || v === 'min') out[k] = v
|
||||
}
|
||||
return out
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export function saveColumnStatConfig(
|
||||
config: ColumnStatConfigMap,
|
||||
storageKey: string
|
||||
): void {
|
||||
const cleaned: ColumnStatConfigMap = {}
|
||||
for (const [k, v] of Object.entries(config)) {
|
||||
if (v === 'sum' || v === 'avg' || v === 'max' || v === 'min') cleaned[k] = v
|
||||
}
|
||||
localStorage.setItem(storageKey, JSON.stringify(cleaned))
|
||||
}
|
||||
|
||||
export function setColumnStatMethod(
|
||||
config: ColumnStatConfigMap,
|
||||
prop: string,
|
||||
method: ColumnStatMethod | ''
|
||||
): ColumnStatConfigMap {
|
||||
const next = { ...config }
|
||||
if (!method) delete next[prop]
|
||||
else next[prop] = method
|
||||
return next
|
||||
}
|
||||
|
||||
/** 构建与主表列顺序一致的统计行列(前导占位 + 固定列 + 可见可选列) */
|
||||
export function buildStatsRowColumns(options: {
|
||||
leadingSpacers?: number
|
||||
alwaysColumns?: { prop: string; money?: boolean }[]
|
||||
optionalDefs?: { prop: string }[]
|
||||
isColumnVisible?: (prop: string) => boolean
|
||||
moneyProps?: Set<string> | string[]
|
||||
}): StatsRowColumn[] {
|
||||
const {
|
||||
leadingSpacers = 0,
|
||||
alwaysColumns = [],
|
||||
optionalDefs = [],
|
||||
isColumnVisible = () => true,
|
||||
moneyProps
|
||||
} = options
|
||||
const moneySet =
|
||||
moneyProps instanceof Set
|
||||
? moneyProps
|
||||
: new Set(moneyProps || [])
|
||||
|
||||
const cols: StatsRowColumn[] = []
|
||||
for (let i = 0; i < leadingSpacers; i++) {
|
||||
cols.push({ key: `__spacer_${i}`, interactive: false })
|
||||
}
|
||||
for (const col of alwaysColumns) {
|
||||
const money = col.money ?? moneySet.has(col.prop)
|
||||
cols.push({
|
||||
key: col.prop,
|
||||
interactive: true,
|
||||
prop: col.prop,
|
||||
money,
|
||||
...(money ? { align: 'right' as const } : {})
|
||||
})
|
||||
}
|
||||
for (const def of optionalDefs) {
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
const money = moneySet.has(def.prop)
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop,
|
||||
money,
|
||||
...(money ? { align: 'right' as const } : {})
|
||||
})
|
||||
}
|
||||
return cols
|
||||
}
|
||||
@@ -107,16 +107,15 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<div class="table-scroll table-page-summary">
|
||||
<div ref="advanceApplyTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="bx-advance-apply"
|
||||
class="list-table"
|
||||
:data="rows"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
show-summary
|
||||
:summary-method="summaryMethod"
|
||||
@selection-change="onSelectionChange"
|
||||
@row-dblclick="openView"
|
||||
@header-dragend="onAdvanceApplyHeaderDragEnd"
|
||||
>
|
||||
<el-table-column type="selection" min-width="48" align="center" header-align="center" />
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
|
||||
@@ -233,6 +232,13 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="advanceApplyStatsRowRef"
|
||||
storage-key="bx-advance-apply-column-stats"
|
||||
:columns="advanceApplyStatsRowColumns"
|
||||
:data="rows"
|
||||
:table-wrap-el="advanceApplyTableWrapRef"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
@@ -412,6 +418,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
@@ -449,7 +456,7 @@ import BaseDetailTableSection from '@/components/bx/base/BaseDetailTableSection.
|
||||
import type { ViewField } from '@/components/business/system-view-dialog/types'
|
||||
import { bxViewKv, bxViewKvMoney } from '@/utils/bx/view-kv'
|
||||
import { buildApplyDateRangeQuery, type ApplyDateRange } from '@/utils/bx/apply-date-filter'
|
||||
import { buildPageSummaryMethod, sumField } from '@/utils/bx/table-summary'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
|
||||
type DialogKey = 'add' | 'view' | 'viewFlow'
|
||||
|
||||
@@ -487,6 +494,39 @@ const {
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('bx-advance-apply-list-columns', listColumnDefs)
|
||||
|
||||
const ADVANCE_APPLY_MONEY_PROPS = new Set(['amount', 'actualAmount', 'offsetAmount'])
|
||||
|
||||
const advanceApplyTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const advanceApplyStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
/** 列顺序与表头一致:selection、index、可选 applyTime、固定 applyCode、其余可选列 */
|
||||
const advanceApplyStatsRowColumns = computed((): StatsRowColumn[] => {
|
||||
const cols: StatsRowColumn[] = [
|
||||
{ key: '__selection', interactive: false },
|
||||
{ key: '__index', interactive: false }
|
||||
]
|
||||
if (isColumnVisible('applyTime')) {
|
||||
cols.push({ key: 'applyTime', interactive: true, prop: 'applyTime' })
|
||||
}
|
||||
cols.push({ key: 'applyCode', interactive: true, prop: 'applyCode' })
|
||||
for (const def of listColumnDefs) {
|
||||
if (def.prop === 'applyTime') continue
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop,
|
||||
money: ADVANCE_APPLY_MONEY_PROPS.has(def.prop),
|
||||
...(ADVANCE_APPLY_MONEY_PROPS.has(def.prop) ? { align: 'right' as const } : {})
|
||||
})
|
||||
}
|
||||
return cols
|
||||
})
|
||||
|
||||
function onAdvanceApplyHeaderDragEnd() {
|
||||
advanceApplyStatsRowRef.value?.resync?.()
|
||||
}
|
||||
|
||||
const filterForm = reactive({
|
||||
keyword: '',
|
||||
status: '',
|
||||
@@ -845,11 +885,10 @@ async function handleDelete(row: any) {
|
||||
}
|
||||
}
|
||||
|
||||
const summaryMethod = buildPageSummaryMethod({
|
||||
sumByProp: {
|
||||
amount: (data) => sumField(data, 'amount'),
|
||||
actualAmount: (data) => sumField(data, 'actualAmount', true),
|
||||
offsetAmount: (data) => sumField(data, 'offsetAmount')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -81,16 +81,15 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<div class="table-scroll table-page-summary">
|
||||
<div ref="pendingTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="bx-advance-approve"
|
||||
class="list-table"
|
||||
:data="pendingRows"
|
||||
v-loading="pendingLoading"
|
||||
stripe
|
||||
show-summary
|
||||
:summary-method="pendingSummaryMethod"
|
||||
@selection-change="onSelectionChange"
|
||||
@row-dblclick="openView"
|
||||
@header-dragend="() => pendingStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="selection" min-width="48" align="center" header-align="center" />
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
|
||||
@@ -163,6 +162,13 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="pendingStatsRowRef"
|
||||
storage-key="bx-advance-approve-pending-column-stats"
|
||||
:columns="pendingStatsRowColumns"
|
||||
:data="pendingRows"
|
||||
:table-wrap-el="pendingTableWrapRef"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
@@ -249,15 +255,14 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<div class="table-scroll table-page-summary">
|
||||
<div ref="approvedTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="bx-advance-approve-2"
|
||||
class="list-table"
|
||||
:data="approvedRows"
|
||||
v-loading="approvedLoading"
|
||||
stripe
|
||||
show-summary
|
||||
:summary-method="approvedSummaryMethod"
|
||||
@row-dblclick="openView"
|
||||
@header-dragend="() => approvedStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
@@ -343,6 +348,13 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="approvedStatsRowRef"
|
||||
storage-key="bx-advance-approve-approved-column-stats"
|
||||
:columns="approvedStatsRowColumns"
|
||||
:data="approvedRows"
|
||||
:table-wrap-el="approvedTableWrapRef"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
@@ -466,6 +478,7 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, computed, onMounted } from 'vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
@@ -491,7 +504,7 @@ import BxViewKvTable from '@/components/bx/base/BxViewKvTable.vue'
|
||||
import type { ViewField } from '@/components/business/system-view-dialog/types'
|
||||
import { bxViewKv, bxViewKvLong, bxViewKvMoney } from '@/utils/bx/view-kv'
|
||||
import { buildApplyDateRangeQuery, type ApplyDateRange } from '@/utils/bx/apply-date-filter'
|
||||
import { buildPageSummaryMethod, sumField } from '@/utils/bx/table-summary'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
|
||||
type DialogKey = 'view' | 'approve'
|
||||
|
||||
@@ -512,6 +525,43 @@ const {
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('bx-advance-approve-list-columns', listColumnDefs)
|
||||
|
||||
const pendingTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const approvedTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const pendingStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
const approvedStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
function buildAdvanceApproveStatsColumns(opts: {
|
||||
withSelection: boolean
|
||||
includeStatus: boolean
|
||||
}): StatsRowColumn[] {
|
||||
const cols: StatsRowColumn[] = []
|
||||
if (opts.withSelection) cols.push({ key: '__selection', interactive: false })
|
||||
cols.push({ key: '__index', interactive: false })
|
||||
if (isColumnVisible('applyTime')) {
|
||||
cols.push({ key: 'applyTime', interactive: true, prop: 'applyTime' })
|
||||
}
|
||||
cols.push({ key: 'applyCode', interactive: true, prop: 'applyCode' })
|
||||
for (const def of listColumnDefs) {
|
||||
if (def.prop === 'applyTime') continue
|
||||
if (def.prop === 'status' && !opts.includeStatus) continue
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop,
|
||||
money: def.prop === 'amount'
|
||||
})
|
||||
}
|
||||
return cols
|
||||
}
|
||||
|
||||
const pendingStatsRowColumns = computed(() =>
|
||||
buildAdvanceApproveStatsColumns({ withSelection: true, includeStatus: false })
|
||||
)
|
||||
const approvedStatsRowColumns = computed(() =>
|
||||
buildAdvanceApproveStatsColumns({ withSelection: false, includeStatus: true })
|
||||
)
|
||||
|
||||
const loadingMap = reactive<Record<string, boolean>>({})
|
||||
const dialogs = reactive<Record<DialogKey, boolean>>({
|
||||
view: false,
|
||||
@@ -769,14 +819,6 @@ async function openView(row: any) {
|
||||
}
|
||||
}
|
||||
|
||||
const amountSummary = buildPageSummaryMethod({
|
||||
sumByProp: {
|
||||
amount: (data) => sumField(data, 'amount')
|
||||
}
|
||||
})
|
||||
const pendingSummaryMethod = amountSummary
|
||||
const approvedSummaryMethod = amountSummary
|
||||
|
||||
async function handleConfirmApprove() {
|
||||
if (!approveRows.value.length) {
|
||||
ElMessage.warning('没有可审批的申请')
|
||||
@@ -818,6 +860,10 @@ async function handleConfirmApprove() {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.batch-action-alert {
|
||||
margin-bottom: 12px;
|
||||
|
||||
|
||||
@@ -80,16 +80,15 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<div class="table-scroll table-page-summary">
|
||||
<div ref="pendingIssueTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="bx-advance-issue"
|
||||
class="list-table"
|
||||
:data="rows"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
show-summary
|
||||
:summary-method="advanceIssueSummary"
|
||||
@selection-change="onSelectionChange"
|
||||
@row-dblclick="openView"
|
||||
@header-dragend="() => pendingIssueStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="selection" min-width="48" align="center" header-align="center" />
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
|
||||
@@ -182,6 +181,13 @@
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="pendingIssueStatsRowRef"
|
||||
storage-key="bx-advance-issue-pending-column-stats"
|
||||
:columns="pendingIssueStatsRowColumns"
|
||||
:data="rows"
|
||||
:table-wrap-el="pendingIssueTableWrapRef"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
@@ -271,15 +277,14 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<div class="table-scroll table-page-summary">
|
||||
<div ref="issuedTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="bx-advance-issue-2"
|
||||
class="list-table"
|
||||
:data="rows"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
show-summary
|
||||
:summary-method="advanceIssueSummary"
|
||||
@row-dblclick="openView"
|
||||
@header-dragend="() => issuedStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
@@ -378,6 +383,13 @@
|
||||
header-align="center"
|
||||
/>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="issuedStatsRowRef"
|
||||
storage-key="bx-advance-issue-issued-column-stats"
|
||||
:columns="issuedStatsRowColumns"
|
||||
:data="rows"
|
||||
:table-wrap-el="issuedTableWrapRef"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
@@ -516,6 +528,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
@@ -535,7 +548,7 @@ import BxAttachmentReadonly from '@/components/bx/base/BxAttachmentReadonly.vue'
|
||||
import type { ViewField } from '@/components/business/system-view-dialog/types'
|
||||
import { bxViewKv } from '@/utils/bx/view-kv'
|
||||
import { buildApplyDateRangeQuery, type ApplyDateRange } from '@/utils/bx/apply-date-filter'
|
||||
import { buildPageSummaryMethod, sumField } from '@/utils/bx/table-summary'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
|
||||
type DialogKey = 'issue' | 'view'
|
||||
|
||||
@@ -559,6 +572,46 @@ const {
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('bx-advance-issue-list-columns', listColumnDefs)
|
||||
|
||||
const ISSUE_MONEY_PROPS = new Set(['amount', 'actualAmount', 'offsetAmount'])
|
||||
|
||||
const pendingIssueTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const issuedTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const pendingIssueStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
const issuedStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
function buildIssueStatsColumns(opts: {
|
||||
withSelection: boolean
|
||||
mode: 'pending' | 'issued'
|
||||
}): StatsRowColumn[] {
|
||||
const cols: StatsRowColumn[] = []
|
||||
if (opts.withSelection) cols.push({ key: '__selection', interactive: false })
|
||||
cols.push({ key: '__index', interactive: false })
|
||||
if (isColumnVisible('applyTime')) {
|
||||
cols.push({ key: 'applyTime', interactive: true, prop: 'applyTime' })
|
||||
}
|
||||
cols.push({ key: 'applyCode', interactive: true, prop: 'applyCode' })
|
||||
for (const def of listColumnDefs) {
|
||||
if (def.prop === 'applyTime') continue
|
||||
if (opts.mode === 'pending' && (def.prop === 'issueTime' || def.prop === 'issueUserName')) continue
|
||||
if (opts.mode === 'issued' && def.prop === 'approveTime') continue
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop,
|
||||
money: ISSUE_MONEY_PROPS.has(def.prop)
|
||||
})
|
||||
}
|
||||
return cols
|
||||
}
|
||||
|
||||
const pendingIssueStatsRowColumns = computed(() =>
|
||||
buildIssueStatsColumns({ withSelection: true, mode: 'pending' })
|
||||
)
|
||||
const issuedStatsRowColumns = computed(() =>
|
||||
buildIssueStatsColumns({ withSelection: false, mode: 'issued' })
|
||||
)
|
||||
|
||||
const loadingMap = reactive<Record<string, boolean>>({})
|
||||
const dialogs = reactive<Record<DialogKey, boolean>>({
|
||||
issue: false,
|
||||
@@ -629,14 +682,6 @@ function issueStatusTagType(status: string): 'success' | 'warning' {
|
||||
return status === 'issued' ? 'success' : 'warning'
|
||||
}
|
||||
|
||||
const advanceIssueSummary = buildPageSummaryMethod({
|
||||
sumByProp: {
|
||||
amount: (data) => sumField(data, 'amount'),
|
||||
actualAmount: (data) => sumField(data, 'actualAmount'),
|
||||
offsetAmount: (data) => sumField(data, 'offsetAmount')
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadApplicantOptions()
|
||||
refresh()
|
||||
@@ -798,3 +843,9 @@ async function openView(row: any) {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -44,40 +44,50 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<ListTable table-key="bx-invoice-wallet-admin"
|
||||
v-loading="loading"
|
||||
class="list-table"
|
||||
:data="rows"
|
||||
stripe
|
||||
@row-dblclick="openDetail"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" />
|
||||
<el-table-column prop="ownerUserName" label="员工" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="openDetail(row)">{{ row.ownerUserName || '-' }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalAmount" label="总额(元)" min-width="120" align="right">
|
||||
<template #default="{ row }">{{ formatMoneyYuan(row.totalAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="usedAmount" label="已报销占用(元)" min-width="140" align="right">
|
||||
<template #default="{ row }">{{ formatMoneyYuan(row.usedAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="balance" label="余额(元)" min-width="120" align="right">
|
||||
<template #default="{ row }">{{ formatMoneyYuan(row.balance) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="invoiceCount" label="发票张数" min-width="100" align="center" />
|
||||
<el-table-column label="操作" fixed="right" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="openDetail(row)">
|
||||
<el-icon><View /></el-icon>管理发票
|
||||
</el-button>
|
||||
<el-button type="primary" link @click="openProxyUpload(row)">
|
||||
<el-icon><Upload /></el-icon>代传
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
<div ref="walletAdminTableWrapRef" class="bx-list-table-wrap">
|
||||
<ListTable table-key="bx-invoice-wallet-admin"
|
||||
v-loading="loading"
|
||||
class="list-table"
|
||||
:data="rows"
|
||||
stripe
|
||||
@row-dblclick="openDetail"
|
||||
@header-dragend="() => walletAdminStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" />
|
||||
<el-table-column prop="ownerUserName" label="员工" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="openDetail(row)">{{ row.ownerUserName || '-' }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalAmount" label="总额(元)" min-width="120" align="right">
|
||||
<template #default="{ row }">{{ formatMoneyYuan(row.totalAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="usedAmount" label="已报销占用(元)" min-width="140" align="right">
|
||||
<template #default="{ row }">{{ formatMoneyYuan(row.usedAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="balance" label="余额(元)" min-width="120" align="right">
|
||||
<template #default="{ row }">{{ formatMoneyYuan(row.balance) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="invoiceCount" label="发票张数" min-width="100" align="center" />
|
||||
<el-table-column label="操作" fixed="right" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="openDetail(row)">
|
||||
<el-icon><View /></el-icon>管理发票
|
||||
</el-button>
|
||||
<el-button type="primary" link @click="openProxyUpload(row)">
|
||||
<el-icon><Upload /></el-icon>代传
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="walletAdminStatsRowRef"
|
||||
storage-key="bx-invoice-wallet-admin-column-stats"
|
||||
:columns="walletAdminStatsRowColumns"
|
||||
:data="rows"
|
||||
:table-wrap-el="walletAdminTableWrapRef"
|
||||
/>
|
||||
</div>
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
@@ -264,7 +274,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import {
|
||||
Search,
|
||||
RefreshLeft,
|
||||
@@ -277,6 +287,8 @@
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import BxInvoiceFileSelect from '@/components/bx/base/BxInvoiceFileSelect.vue'
|
||||
import BxAttachmentReadonly from '@/components/bx/base/BxAttachmentReadonly.vue'
|
||||
import { useApplicantOptions } from '@/hooks/bx/useApplicantOptions'
|
||||
@@ -294,6 +306,20 @@
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
|
||||
const walletAdminTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const walletAdminStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
const walletAdminStatsRowColumns = computed((): StatsRowColumn[] => [
|
||||
{ key: '__index', interactive: false },
|
||||
{ key: 'ownerUserName', interactive: true, prop: 'ownerUserName' },
|
||||
{ key: 'totalAmount', interactive: true, prop: 'totalAmount', money: true },
|
||||
{ key: 'usedAmount', interactive: true, prop: 'usedAmount', money: true },
|
||||
{ key: 'balance', interactive: true, prop: 'balance', money: true },
|
||||
{ key: 'invoiceCount', interactive: true, prop: 'invoiceCount' },
|
||||
{ key: '__ops', interactive: false }
|
||||
])
|
||||
|
||||
const filterForm = reactive({ keyword: '' })
|
||||
const quickSearchKeyword = ref('')
|
||||
const detailVisible = ref(false)
|
||||
@@ -537,6 +563,10 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.detail-empty-hint {
|
||||
margin: 8px 0 0;
|
||||
font-size: 13px;
|
||||
|
||||
@@ -72,60 +72,86 @@
|
||||
<el-tabs v-model="activeTab" class="wallet-tabs">
|
||||
<el-tab-pane label="发票列表" name="invoices">
|
||||
<div class="list-body list-wrap">
|
||||
<ListTable table-key="bx-invoice-wallet-mine"
|
||||
v-loading="loading"
|
||||
class="list-table"
|
||||
:data="filteredInvoiceRows"
|
||||
stripe
|
||||
@row-dblclick="openInvoiceView"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="52" align="center" />
|
||||
<el-table-column prop="amount" label="金额(元)" min-width="88" align="right">
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="openInvoiceView(row)">
|
||||
{{ formatMoneyYuan(row.amount) }}
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件" min-width="420">
|
||||
<template #default="{ row }">
|
||||
<BxAttachmentReadonly
|
||||
v-if="invoiceThumbFiles(row).length"
|
||||
class="invoice-att-thumbs"
|
||||
:files="invoiceThumbFiles(row)"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="100" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.remark || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="uploadByName" label="上传人" min-width="80">
|
||||
<template #default="{ row }">{{ row.uploadByName || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="上传时间" min-width="140" />
|
||||
</ListTable>
|
||||
<div ref="invoiceTableWrapRef" class="bx-list-table-wrap">
|
||||
<ListTable table-key="bx-invoice-wallet-mine"
|
||||
v-loading="loading"
|
||||
class="list-table"
|
||||
:data="filteredInvoiceRows"
|
||||
stripe
|
||||
@row-dblclick="openInvoiceView"
|
||||
@header-dragend="() => invoiceStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="52" align="center" />
|
||||
<el-table-column prop="amount" label="金额(元)" min-width="88" align="right">
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="openInvoiceView(row)">
|
||||
{{ formatMoneyYuan(row.amount) }}
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件" min-width="420">
|
||||
<template #default="{ row }">
|
||||
<BxAttachmentReadonly
|
||||
v-if="invoiceThumbFiles(row).length"
|
||||
class="invoice-att-thumbs"
|
||||
:files="invoiceThumbFiles(row)"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="100" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.remark || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="uploadByName" label="上传人" min-width="80">
|
||||
<template #default="{ row }">{{ row.uploadByName || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="上传时间" min-width="140" />
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="invoiceStatsRowRef"
|
||||
storage-key="bx-invoice-wallet-mine-invoices-column-stats"
|
||||
:columns="invoiceStatsRowColumns"
|
||||
:data="filteredInvoiceRows"
|
||||
:table-wrap-el="invoiceTableWrapRef"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="额度流水" name="ledgers">
|
||||
<div class="list-body list-wrap">
|
||||
<ListTable table-key="bx-invoice-wallet-mine-2" v-loading="ledgerLoading" class="list-table" :data="filteredLedgerRows" stripe>
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" />
|
||||
<el-table-column prop="createTime" label="时间" min-width="170" />
|
||||
<el-table-column prop="entryType" label="类型" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="ledgerTagType(row.entryType)">
|
||||
{{ ledgerTypeLabel(row.entryType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="amount" label="金额(元)" min-width="120" align="right">
|
||||
<template #default="{ row }">{{ formatMoneyYuan(row.amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.remark || '-' }}</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
<div ref="ledgerTableWrapRef" class="bx-list-table-wrap">
|
||||
<ListTable
|
||||
table-key="bx-invoice-wallet-mine-2"
|
||||
v-loading="ledgerLoading"
|
||||
class="list-table"
|
||||
:data="filteredLedgerRows"
|
||||
stripe
|
||||
@header-dragend="() => ledgerStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" />
|
||||
<el-table-column prop="createTime" label="时间" min-width="170" />
|
||||
<el-table-column prop="entryType" label="类型" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="ledgerTagType(row.entryType)">
|
||||
{{ ledgerTypeLabel(row.entryType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="amount" label="金额(元)" min-width="120" align="right">
|
||||
<template #default="{ row }">{{ formatMoneyYuan(row.amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.remark || '-' }}</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="ledgerStatsRowRef"
|
||||
storage-key="bx-invoice-wallet-mine-ledgers-column-stats"
|
||||
:columns="ledgerStatsRowColumns"
|
||||
:data="filteredLedgerRows"
|
||||
:table-wrap-el="ledgerTableWrapRef"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
@@ -236,6 +262,8 @@
|
||||
import { ElMessage } from 'element-plus'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import BxInvoiceFileSelect from '@/components/bx/base/BxInvoiceFileSelect.vue'
|
||||
import BxAttachmentReadonly from '@/components/bx/base/BxAttachmentReadonly.vue'
|
||||
import BxViewKvTable from '@/components/bx/base/BxViewKvTable.vue'
|
||||
@@ -259,6 +287,28 @@
|
||||
const invoiceRows = ref<any[]>([])
|
||||
const ledgerRows = ref<any[]>([])
|
||||
const summary = ref<any>(null)
|
||||
|
||||
const invoiceTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const invoiceStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
const ledgerTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const ledgerStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
const invoiceStatsRowColumns = computed((): StatsRowColumn[] => [
|
||||
{ key: '__index', interactive: false },
|
||||
{ key: 'amount', interactive: true, prop: 'amount', money: true },
|
||||
{ key: '__attachments', interactive: true, prop: '__attachments' },
|
||||
{ key: 'remark', interactive: true, prop: 'remark' },
|
||||
{ key: 'uploadByName', interactive: true, prop: 'uploadByName' },
|
||||
{ key: 'createTime', interactive: true, prop: 'createTime' }
|
||||
])
|
||||
|
||||
const ledgerStatsRowColumns = computed((): StatsRowColumn[] => [
|
||||
{ key: '__index', interactive: false },
|
||||
{ key: 'createTime', interactive: true, prop: 'createTime' },
|
||||
{ key: 'entryType', interactive: true, prop: 'entryType' },
|
||||
{ key: 'amount', interactive: true, prop: 'amount', money: true },
|
||||
{ key: 'remark', interactive: true, prop: 'remark' }
|
||||
])
|
||||
const uploadVisible = ref(false)
|
||||
const viewVisible = ref(false)
|
||||
const viewRow = ref<any>(null)
|
||||
@@ -500,6 +550,10 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.panel-top-meta {
|
||||
margin: 6px 0 0;
|
||||
font-size: 13px;
|
||||
|
||||
@@ -81,16 +81,15 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<div class="table-scroll table-page-summary">
|
||||
<div ref="pendingPaymentTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="bx-payment"
|
||||
class="list-table"
|
||||
:data="rows"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
show-summary
|
||||
:summary-method="summaryMethod"
|
||||
@selection-change="onSelectionChange"
|
||||
@row-dblclick="openView"
|
||||
@header-dragend="() => pendingPaymentStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="selection" min-width="48" align="center" header-align="center" />
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
|
||||
@@ -174,6 +173,14 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="pendingPaymentStatsRowRef"
|
||||
storage-key="bx-payment-pending-column-stats"
|
||||
:columns="pendingPaymentStatsRowColumns"
|
||||
:data="rows"
|
||||
:table-wrap-el="pendingPaymentTableWrapRef"
|
||||
:get-cell-value="getPaymentStatCellValue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
@@ -270,15 +277,14 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<div class="table-scroll table-page-summary">
|
||||
<div ref="processedPaymentTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="bx-payment-2"
|
||||
class="list-table"
|
||||
:data="processedRows"
|
||||
v-loading="processedLoading"
|
||||
stripe
|
||||
show-summary
|
||||
:summary-method="processedSummaryMethod"
|
||||
@row-dblclick="openProcessedView"
|
||||
@header-dragend="() => processedPaymentStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
@@ -386,6 +392,14 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="processedPaymentStatsRowRef"
|
||||
storage-key="bx-payment-processed-column-stats"
|
||||
:columns="processedPaymentStatsRowColumns"
|
||||
:data="processedRows"
|
||||
:table-wrap-el="processedPaymentTableWrapRef"
|
||||
:get-cell-value="getPaymentStatCellValue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
@@ -677,6 +691,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
@@ -705,7 +720,7 @@ import BxAttachmentReadonly from '@/components/bx/base/BxAttachmentReadonly.vue'
|
||||
import type { ViewField } from '@/components/business/system-view-dialog/types'
|
||||
import { bxViewKv, bxViewKvMoney } from '@/utils/bx/view-kv'
|
||||
import { buildApplyDateRangeQuery, type ApplyDateRange } from '@/utils/bx/apply-date-filter'
|
||||
import { buildPageSummaryMethod, sumField } from '@/utils/bx/table-summary'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
|
||||
type DialogKey = 'viewDetail' | 'issue' | 'offset' | 'processedDetail'
|
||||
|
||||
@@ -728,6 +743,51 @@ const {
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('bx-payment-list-columns', listColumnDefs)
|
||||
|
||||
const PAYMENT_MONEY_PROPS = new Set(['amount', 'offsetAmount', 'payAmount'])
|
||||
|
||||
const pendingPaymentTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const processedPaymentTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const pendingPaymentStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
const processedPaymentStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
function buildPaymentStatsColumns(opts: {
|
||||
withSelection: boolean
|
||||
mode: 'pending' | 'processed'
|
||||
}): StatsRowColumn[] {
|
||||
const cols: StatsRowColumn[] = []
|
||||
if (opts.withSelection) cols.push({ key: '__selection', interactive: false })
|
||||
cols.push({ key: '__index', interactive: false })
|
||||
if (isColumnVisible('applyTime')) {
|
||||
cols.push({ key: 'applyTime', interactive: true, prop: 'applyTime' })
|
||||
}
|
||||
cols.push({ key: 'applyCode', interactive: true, prop: 'applyCode' })
|
||||
for (const def of listColumnDefs) {
|
||||
if (def.prop === 'applyTime') continue
|
||||
if (opts.mode === 'pending' && (def.prop === 'processStatus' || def.prop === 'payTime')) continue
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop,
|
||||
money: PAYMENT_MONEY_PROPS.has(def.prop)
|
||||
})
|
||||
}
|
||||
return cols
|
||||
}
|
||||
|
||||
const pendingPaymentStatsRowColumns = computed(() =>
|
||||
buildPaymentStatsColumns({ withSelection: true, mode: 'pending' })
|
||||
)
|
||||
const processedPaymentStatsRowColumns = computed(() =>
|
||||
buildPaymentStatsColumns({ withSelection: false, mode: 'processed' })
|
||||
)
|
||||
|
||||
function getPaymentStatCellValue(row: any, prop: string): unknown {
|
||||
if (prop === 'payAmount') return row?.actualAmount
|
||||
if (prop === 'processStatus') return processedStatusLabel(row?.status, row?.payStatus)
|
||||
return row?.[prop]
|
||||
}
|
||||
|
||||
const { applicantOptions, loadApplicantOptions } = useApplicantOptions()
|
||||
const loadingMap = reactive<Record<string, boolean>>({})
|
||||
const dialogs = reactive<Record<DialogKey, boolean>>({
|
||||
@@ -1209,16 +1269,6 @@ async function submitOffset() {
|
||||
}
|
||||
}
|
||||
|
||||
const paymentSummary = buildPageSummaryMethod({
|
||||
sumByProp: {
|
||||
amount: (data) => sumField(data, 'amount'),
|
||||
offsetAmount: (data) => sumField(data, 'offsetAmount'),
|
||||
actualAmount: (data) => sumField(data, 'actualAmount')
|
||||
}
|
||||
})
|
||||
const summaryMethod = paymentSummary
|
||||
const processedSummaryMethod = paymentSummary
|
||||
|
||||
async function openProcessedView(row: any) {
|
||||
processedViewData.value = row
|
||||
processedApprovals.value = []
|
||||
@@ -1297,3 +1347,9 @@ function processedStatusTagType(
|
||||
return 'info'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -97,16 +97,15 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<div class="table-scroll table-page-summary">
|
||||
<div ref="reimburseApplyTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="bx-reimburse-apply"
|
||||
class="list-table"
|
||||
:data="rows"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
show-summary
|
||||
:summary-method="summaryMethod"
|
||||
@selection-change="onSelectionChange"
|
||||
@row-dblclick="openView"
|
||||
@header-dragend="onReimburseApplyHeaderDragEnd"
|
||||
>
|
||||
<el-table-column type="selection" min-width="48" align="center" header-align="center" />
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
|
||||
@@ -206,6 +205,14 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="reimburseApplyStatsRowRef"
|
||||
storage-key="bx-reimburse-apply-column-stats"
|
||||
:columns="reimburseApplyStatsRowColumns"
|
||||
:data="rows"
|
||||
:table-wrap-el="reimburseApplyTableWrapRef"
|
||||
:get-cell-value="getReimburseApplyStatCellValue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
@@ -564,6 +571,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
@@ -609,7 +617,7 @@ import BxAttachmentReadonly from '@/components/bx/base/BxAttachmentReadonly.vue'
|
||||
import BxInvoiceSelectDialog from '@/components/bx/base/BxInvoiceSelectDialog.vue'
|
||||
import type { ViewField } from '@/components/business/system-view-dialog/types'
|
||||
import { bxViewKv, bxViewKvLong, bxViewKvMoney } from '@/utils/bx/view-kv'
|
||||
import { buildPageSummaryMethod, sumField } from '@/utils/bx/table-summary'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import {
|
||||
type BxAttachmentItem,
|
||||
loadBxAttachments,
|
||||
@@ -679,6 +687,43 @@ const {
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('bx-reimburse-apply-list-columns', listColumnDefs)
|
||||
|
||||
const reimburseApplyTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const reimburseApplyStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
const reimburseApplyStatsRowColumns = computed((): StatsRowColumn[] => {
|
||||
const cols: StatsRowColumn[] = [
|
||||
{ key: '__selection', interactive: false },
|
||||
{ key: '__index', interactive: false }
|
||||
]
|
||||
if (isColumnVisible('applyTime')) {
|
||||
cols.push({ key: 'applyTime', interactive: true, prop: 'applyTime' })
|
||||
}
|
||||
cols.push({ key: 'applyCode', interactive: true, prop: 'applyCode' })
|
||||
for (const def of listColumnDefs) {
|
||||
if (def.prop === 'applyTime') continue
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop,
|
||||
money: def.prop === 'amount'
|
||||
})
|
||||
}
|
||||
return cols
|
||||
})
|
||||
|
||||
function getReimburseApplyStatCellValue(row: any, prop: string): unknown {
|
||||
if (prop === 'expenseName') {
|
||||
const lines = getReimburseExpenseItemLines(row.details, row)
|
||||
return lines?.length ? lines.join('\n') : row.expenseName
|
||||
}
|
||||
return row?.[prop]
|
||||
}
|
||||
|
||||
function onReimburseApplyHeaderDragEnd() {
|
||||
reimburseApplyStatsRowRef.value?.resync?.()
|
||||
}
|
||||
|
||||
const filterForm = reactive({
|
||||
keyword: '',
|
||||
status: '',
|
||||
@@ -1329,12 +1374,6 @@ async function handleDelete(row: any) {
|
||||
}
|
||||
}
|
||||
|
||||
const summaryMethod = buildPageSummaryMethod({
|
||||
sumByProp: {
|
||||
amount: (data) => sumField(data, 'amount')
|
||||
}
|
||||
})
|
||||
|
||||
function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
|
||||
const sums: string[] = []
|
||||
columns.forEach((_: any, index: number) => {
|
||||
@@ -1489,4 +1528,8 @@ function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -108,16 +108,15 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<div class="table-scroll table-page-summary">
|
||||
<div ref="pendingTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="bx-reimburse-approve"
|
||||
class="list-table"
|
||||
:data="pendingRows"
|
||||
v-loading="pendingLoading"
|
||||
stripe
|
||||
show-summary
|
||||
:summary-method="pendingSummaryMethod"
|
||||
@selection-change="onSelectionChange"
|
||||
@row-dblclick="openView"
|
||||
@header-dragend="() => pendingStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="selection" min-width="48" align="center" header-align="center" />
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
|
||||
@@ -211,6 +210,14 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="pendingStatsRowRef"
|
||||
storage-key="bx-reimburse-approve-pending-column-stats"
|
||||
:columns="pendingStatsRowColumns"
|
||||
:data="pendingRows"
|
||||
:table-wrap-el="pendingTableWrapRef"
|
||||
:get-cell-value="getReimburseApproveStatCellValue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
@@ -324,15 +331,14 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<div class="table-scroll table-page-summary">
|
||||
<div ref="approvedTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="bx-reimburse-approve-2"
|
||||
class="list-table"
|
||||
:data="approvedRows"
|
||||
v-loading="approvedLoading"
|
||||
stripe
|
||||
show-summary
|
||||
:summary-method="approvedSummaryMethod"
|
||||
@row-dblclick="openView"
|
||||
@header-dragend="() => approvedStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
@@ -439,6 +445,14 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="approvedStatsRowRef"
|
||||
storage-key="bx-reimburse-approve-approved-column-stats"
|
||||
:columns="approvedStatsRowColumns"
|
||||
:data="approvedRows"
|
||||
:table-wrap-el="approvedTableWrapRef"
|
||||
:get-cell-value="getReimburseApproveStatCellValue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
@@ -597,6 +611,7 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, computed, onMounted } from 'vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
@@ -632,7 +647,7 @@ import BxAttachmentReadonly from '@/components/bx/base/BxAttachmentReadonly.vue'
|
||||
import type { ViewField } from '@/components/business/system-view-dialog/types'
|
||||
import { bxViewKv, bxViewKvLong } from '@/utils/bx/view-kv'
|
||||
import { buildApplyDateRangeQuery, type ApplyDateRange } from '@/utils/bx/apply-date-filter'
|
||||
import { buildPageSummaryMethod, sumField } from '@/utils/bx/table-summary'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
|
||||
type DialogKey = 'viewDetail' | 'approve'
|
||||
|
||||
@@ -654,6 +669,51 @@ const {
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('bx-reimburse-approve-list-columns', listColumnDefs)
|
||||
|
||||
const pendingTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const approvedTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const pendingStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
const approvedStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
function buildReimburseApproveStatsColumns(opts: {
|
||||
withSelection: boolean
|
||||
includeStatus: boolean
|
||||
}): StatsRowColumn[] {
|
||||
const cols: StatsRowColumn[] = []
|
||||
if (opts.withSelection) cols.push({ key: '__selection', interactive: false })
|
||||
cols.push({ key: '__index', interactive: false })
|
||||
if (isColumnVisible('applyTime')) {
|
||||
cols.push({ key: 'applyTime', interactive: true, prop: 'applyTime' })
|
||||
}
|
||||
cols.push({ key: 'applyCode', interactive: true, prop: 'applyCode' })
|
||||
for (const def of listColumnDefs) {
|
||||
if (def.prop === 'applyTime') continue
|
||||
if (def.prop === 'status' && !opts.includeStatus) continue
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop,
|
||||
money: def.prop === 'amount'
|
||||
})
|
||||
}
|
||||
return cols
|
||||
}
|
||||
|
||||
const pendingStatsRowColumns = computed(() =>
|
||||
buildReimburseApproveStatsColumns({ withSelection: true, includeStatus: false })
|
||||
)
|
||||
const approvedStatsRowColumns = computed(() =>
|
||||
buildReimburseApproveStatsColumns({ withSelection: false, includeStatus: true })
|
||||
)
|
||||
|
||||
function getReimburseApproveStatCellValue(row: any, prop: string): unknown {
|
||||
if (prop === 'expenseName') {
|
||||
const lines = getReimburseExpenseItemLines(row.details, row)
|
||||
return lines?.length ? lines.join('\n') : row.expenseName
|
||||
}
|
||||
return row?.[prop]
|
||||
}
|
||||
|
||||
const pendingQuickSearchKeyword = ref('')
|
||||
const approvedQuickSearchKeyword = ref('')
|
||||
|
||||
@@ -1021,14 +1081,6 @@ async function handleConfirmApprove() {
|
||||
}
|
||||
}
|
||||
|
||||
const amountSummary = buildPageSummaryMethod({
|
||||
sumByProp: {
|
||||
amount: (data) => sumField(data, 'amount')
|
||||
}
|
||||
})
|
||||
const pendingSummaryMethod = amountSummary
|
||||
const approvedSummaryMethod = amountSummary
|
||||
|
||||
function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
|
||||
const sums: string[] = []
|
||||
columns.forEach((_: any, index: number) => {
|
||||
@@ -1048,6 +1100,10 @@ function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.batch-action-alert {
|
||||
margin-bottom: 12px;
|
||||
|
||||
|
||||
@@ -84,17 +84,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap table-page-summary table-page-summary-biz-split">
|
||||
<ListTable table-key="bx-view-all"
|
||||
class="list-table"
|
||||
:data="pager.pagedRows.value"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
show-summary
|
||||
:summary-method="summaryMethod"
|
||||
@row-dblclick="openViewDetail"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<div class="list-body list-wrap">
|
||||
<div ref="allTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="bx-view-all"
|
||||
class="list-table"
|
||||
:data="pager.pagedRows.value"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
@row-dblclick="openViewDetail"
|
||||
@selection-change="handleSelectionChange"
|
||||
@header-dragend="() => allStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
@@ -193,7 +193,15 @@
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="allStatsRowRef"
|
||||
storage-key="bx-view-all-column-stats"
|
||||
:columns="allStatsRowColumns"
|
||||
:data="pager.pagedRows.value"
|
||||
:table-wrap-el="allTableWrapRef"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<span class="list-range">{{ pager.rangeText.value }}</span>
|
||||
@@ -280,6 +288,8 @@
|
||||
<script setup lang="ts">
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
@@ -307,7 +317,6 @@ import { invoicesToLinkedAttachmentFiles } from '@/utils/bx/invoice-link-files'
|
||||
import { matchesApplyDateRange, type ApplyDateRange } from '@/utils/bx/apply-date-filter'
|
||||
import { usePagedTable } from '@/hooks/bx/usePagedTable'
|
||||
import { useApplicantOptions } from '@/hooks/bx/useApplicantOptions'
|
||||
import { buildPageSummaryMethod, sumFieldByBizType } from '@/utils/bx/table-summary'
|
||||
|
||||
type DialogKey = 'viewDetail'
|
||||
|
||||
@@ -338,6 +347,31 @@ const {
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('bx-view-all-list-columns', listColumnDefs)
|
||||
|
||||
const allTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const allStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
const allStatsRowColumns = computed((): StatsRowColumn[] => {
|
||||
const cols: StatsRowColumn[] = [
|
||||
{ key: '__selection', interactive: false },
|
||||
{ key: '__index', interactive: false }
|
||||
]
|
||||
if (isColumnVisible('applyTime')) {
|
||||
cols.push({ key: 'applyTime', interactive: true, prop: 'applyTime' })
|
||||
}
|
||||
cols.push({ key: 'applyCode', interactive: true, prop: 'applyCode' })
|
||||
for (const def of listColumnDefs) {
|
||||
if (def.prop === 'applyTime') continue
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop,
|
||||
money: def.prop === 'amount'
|
||||
})
|
||||
}
|
||||
return cols
|
||||
})
|
||||
|
||||
const quickSearchKeyword = ref('')
|
||||
const appliedQuickSearch = ref('')
|
||||
|
||||
@@ -540,12 +574,6 @@ function handleQuickSearchClear() {
|
||||
pager.currentPage.value = 1
|
||||
}
|
||||
|
||||
const summaryMethod = buildPageSummaryMethod({
|
||||
sumByProp: {
|
||||
amount: (data) => sumFieldByBizType(data, 'amount')
|
||||
}
|
||||
})
|
||||
|
||||
function handleSelectionChange(rows: any[]) {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
@@ -615,3 +643,9 @@ onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -77,16 +77,15 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<div class="table-scroll table-page-summary table-page-summary-biz-split">
|
||||
<div ref="mineTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="bx-view-mine"
|
||||
class="list-table"
|
||||
:data="pagedRows"
|
||||
v-loading="tableLoading"
|
||||
stripe
|
||||
show-summary
|
||||
:summary-method="summaryMethod"
|
||||
@row-dblclick="openViewDetail"
|
||||
@selection-change="handleSelectionChange"
|
||||
@header-dragend="() => mineStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
|
||||
@@ -178,6 +177,13 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="mineStatsRowRef"
|
||||
storage-key="bx-view-mine-column-stats"
|
||||
:columns="mineStatsRowColumns"
|
||||
:data="pagedRows"
|
||||
:table-wrap-el="mineTableWrapRef"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
@@ -264,6 +270,8 @@
|
||||
<script setup lang="ts">
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { computed, onActivated, onMounted, reactive, ref } from 'vue'
|
||||
@@ -290,7 +298,6 @@ import { type BxAttachmentItem, loadBxAttachments } from '@/utils/bx/attachment'
|
||||
import { invoicesToLinkedAttachmentFiles } from '@/utils/bx/invoice-link-files'
|
||||
import { buildApplyDateRangeQuery, matchesApplyDateRange, type ApplyDateRange } from '@/utils/bx/apply-date-filter'
|
||||
import { usePagedTable } from '@/hooks/bx/usePagedTable'
|
||||
import { buildPageSummaryMethod, sumFieldByBizType } from '@/utils/bx/table-summary'
|
||||
|
||||
type DialogKey = 'viewDetail'
|
||||
|
||||
@@ -320,6 +327,31 @@ const {
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('bx-view-mine-list-columns', listColumnDefs)
|
||||
|
||||
const mineTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const mineStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
const mineStatsRowColumns = computed((): StatsRowColumn[] => {
|
||||
const cols: StatsRowColumn[] = [
|
||||
{ key: '__selection', interactive: false },
|
||||
{ key: '__index', interactive: false }
|
||||
]
|
||||
if (isColumnVisible('applyTime')) {
|
||||
cols.push({ key: 'applyTime', interactive: true, prop: 'applyTime' })
|
||||
}
|
||||
cols.push({ key: 'applyCode', interactive: true, prop: 'applyCode' })
|
||||
for (const def of listColumnDefs) {
|
||||
if (def.prop === 'applyTime') continue
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop,
|
||||
money: def.prop === 'amount'
|
||||
})
|
||||
}
|
||||
return cols
|
||||
})
|
||||
|
||||
const quickSearchKeyword = ref('')
|
||||
|
||||
const filterForm = reactive({
|
||||
@@ -431,12 +463,6 @@ function sortMergedRows(rows: any[]) {
|
||||
return [...rows].sort((a, b) => String(b.applyTime || '').localeCompare(String(a.applyTime || '')))
|
||||
}
|
||||
|
||||
const summaryMethod = buildPageSummaryMethod({
|
||||
sumByProp: {
|
||||
amount: (data) => sumFieldByBizType(data, 'amount')
|
||||
}
|
||||
})
|
||||
|
||||
async function loadData(options: { buttonLoading?: boolean } = {}) {
|
||||
if (options.buttonLoading) searchLoading.value = true
|
||||
tableLoading.value = true
|
||||
@@ -576,3 +602,9 @@ onActivated(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -83,16 +83,15 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<div class="table-scroll table-page-summary">
|
||||
<div ref="todoTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="bx-view-todo"
|
||||
class="list-table"
|
||||
:data="pagedRows"
|
||||
v-loading="tableLoading"
|
||||
stripe
|
||||
show-summary
|
||||
:summary-method="summaryMethod"
|
||||
@selection-change="onSelectionChange"
|
||||
@row-dblclick="openViewDetailFromRow"
|
||||
@header-dragend="() => todoStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="selection" min-width="48" align="center" header-align="center" />
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
|
||||
@@ -179,6 +178,13 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="todoStatsRowRef"
|
||||
storage-key="bx-view-todo-column-stats"
|
||||
:columns="todoStatsRowColumns"
|
||||
:data="pagedRows"
|
||||
:table-wrap-el="todoTableWrapRef"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
@@ -285,6 +291,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
@@ -315,7 +323,6 @@ import { usePagedTable } from '@/hooks/bx/usePagedTable'
|
||||
import { useApplicantOptions } from '@/hooks/bx/useApplicantOptions'
|
||||
import { refreshMenuBadges } from '@/utils/menu-badge-registry'
|
||||
import { useMenuStore } from '@/store/modules/menu'
|
||||
import { buildPageSummaryMethod, sumField } from '@/utils/bx/table-summary'
|
||||
|
||||
type DialogKey = 'viewDetail' | 'approve'
|
||||
|
||||
@@ -351,6 +358,31 @@ const {
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('bx-view-todo-list-columns', listColumnDefs)
|
||||
|
||||
const todoTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const todoStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
const todoStatsRowColumns = computed((): StatsRowColumn[] => {
|
||||
const cols: StatsRowColumn[] = [
|
||||
{ key: '__selection', interactive: false },
|
||||
{ key: '__index', interactive: false }
|
||||
]
|
||||
if (isColumnVisible('applyTime')) {
|
||||
cols.push({ key: 'applyTime', interactive: true, prop: 'applyTime' })
|
||||
}
|
||||
cols.push({ key: 'applyCode', interactive: true, prop: 'applyCode' })
|
||||
for (const def of listColumnDefs) {
|
||||
if (def.prop === 'applyTime') continue
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop,
|
||||
money: def.prop === 'amount'
|
||||
})
|
||||
}
|
||||
return cols
|
||||
})
|
||||
|
||||
const filterForm = reactive({
|
||||
code: '',
|
||||
bizType: '' as string,
|
||||
@@ -644,12 +676,6 @@ function handleQuickSearchClear() {
|
||||
loadData()
|
||||
}
|
||||
|
||||
const summaryMethod = buildPageSummaryMethod({
|
||||
sumByProp: {
|
||||
amount: (data) => sumField(data, 'amount')
|
||||
}
|
||||
})
|
||||
|
||||
function openViewDetailFromRow(row: any) {
|
||||
openViewDetail(row)
|
||||
}
|
||||
@@ -684,3 +710,9 @@ onActivated(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -147,6 +147,7 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap"> <!-- 操作按钮栏 + 数据表格 -->
|
||||
<div ref="contractTableWrapRef" class="contract-table-wrap">
|
||||
<ListTable table-key="crm-contract"
|
||||
v-loading="loading"
|
||||
class="list-table"
|
||||
@@ -156,6 +157,7 @@
|
||||
@selection-change="handleSelectionChange"
|
||||
@expand-change="handleExpandChange"
|
||||
@row-dblclick="handleView"
|
||||
@header-dragend="onContractHeaderDragEnd"
|
||||
>
|
||||
<el-table-column type="expand" width="55">
|
||||
<template #default="scope">
|
||||
@@ -456,6 +458,16 @@
|
||||
<el-table-column v-if="isColumnVisible('remark')" label="备注" align="center" prop="remark" min-width="200" show-overflow-tooltip />
|
||||
</ListTable>
|
||||
|
||||
<ListColumnStatsRow
|
||||
ref="contractStatsRowRef"
|
||||
storage-key="crm-contract-column-stats"
|
||||
:columns="contractStatsRowColumns"
|
||||
:data="contractList"
|
||||
:table-wrap-el="contractTableWrapRef"
|
||||
:get-cell-value="getContractStatCellValue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<div class="list-pager-selected">
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
@@ -551,7 +563,9 @@ import {
|
||||
} from '@/utils/crm-attachment'
|
||||
import ListToolbarDateShortcuts from '@/components/core/list-toolbar-date-shortcuts/index.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { buildStatsRowColumns } from '@/utils/list-column-stats'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -991,6 +1005,45 @@ const formatContractProductInfoLines = (row: ContractVO): string[] => {
|
||||
.filter((line) => line.length > 0)
|
||||
}
|
||||
|
||||
const CONTRACT_MONEY_STAT_PROPS = new Set([
|
||||
'totalContractAmount',
|
||||
'amount',
|
||||
'freight',
|
||||
'barrelDeposit',
|
||||
'unreceivedAmount',
|
||||
'invoiceAmount',
|
||||
'uninvoiceAmount'
|
||||
])
|
||||
|
||||
const contractTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const contractStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
const contractStatsRowColumns = computed(() =>
|
||||
buildStatsRowColumns({
|
||||
leadingSpacers: 2,
|
||||
alwaysColumns: [
|
||||
{ prop: 'contractNo' },
|
||||
{ prop: 'contractName' }
|
||||
],
|
||||
optionalDefs: listColumnDefs,
|
||||
isColumnVisible,
|
||||
moneyProps: CONTRACT_MONEY_STAT_PROPS
|
||||
})
|
||||
)
|
||||
|
||||
function getContractStatCellValue(row: ContractVO, prop: string): unknown {
|
||||
if (prop === 'totalContractAmount') return getContractTotalAmount(row)
|
||||
if (prop === 'productInfo') {
|
||||
const lines = formatContractProductInfoLines(row)
|
||||
return lines.length ? lines.join('\n') : null
|
||||
}
|
||||
return (row as Record<string, unknown>)[prop]
|
||||
}
|
||||
|
||||
function onContractHeaderDragEnd() {
|
||||
contractStatsRowRef.value?.resync?.()
|
||||
}
|
||||
|
||||
/** 多选框选中数据 */
|
||||
const handleSelectionChange = (selection: ContractVO[]) => {
|
||||
selectedRows.value = selection;
|
||||
@@ -1501,4 +1554,8 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.contract-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -383,42 +383,51 @@
|
||||
</ListTable>
|
||||
|
||||
<div
|
||||
ref="quoteStatsScrollRef"
|
||||
class="quote-list-stats-row"
|
||||
aria-label="列表列统计"
|
||||
@scroll="onStatsRowScroll"
|
||||
>
|
||||
<div class="quote-list-stats-row__fixed">
|
||||
<div class="quote-list-stats-row__cell is-fixed" style="width: 55px" />
|
||||
<div class="quote-list-stats-row__cell is-fixed" style="width: 55px" />
|
||||
</div>
|
||||
<div
|
||||
ref="quoteStatsScrollRef"
|
||||
class="quote-list-stats-row__scroll"
|
||||
@scroll="onStatsRowScroll"
|
||||
>
|
||||
<div class="quote-list-stats-row__inner">
|
||||
<div
|
||||
v-for="col in statsBusinessColumns"
|
||||
:key="col.prop"
|
||||
class="quote-list-stats-row__cell is-stat"
|
||||
v-for="col in statsRowColumns"
|
||||
:key="col.key"
|
||||
class="quote-list-stats-row__cell"
|
||||
:class="{ 'is-stat': col.interactive, 'is-spacer': !col.interactive }"
|
||||
>
|
||||
<el-dropdown trigger="click" @command="(cmd: string | number) => onStatCommand(col.prop, cmd)">
|
||||
<span
|
||||
class="quote-list-stats-row__trigger"
|
||||
:class="{ 'is-empty': !statsCellText(col.prop, col.money) }"
|
||||
>
|
||||
{{ statsCellText(col.prop, col.money) || '统计' }}
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="opt in COLUMN_STAT_METHOD_OPTIONS"
|
||||
:key="opt.label"
|
||||
:command="opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
<el-popover
|
||||
v-if="col.interactive && col.prop"
|
||||
:visible="statsMenuOpenProp === col.prop"
|
||||
placement="top-start"
|
||||
:width="132"
|
||||
:teleported="true"
|
||||
trigger="click"
|
||||
:hide-after="0"
|
||||
popper-class="quote-list-stats-dropdown"
|
||||
@update:visible="(v: boolean) => setStatsMenuOpen(col.prop!, v)"
|
||||
>
|
||||
<template #reference>
|
||||
<button
|
||||
type="button"
|
||||
class="quote-list-stats-row__trigger"
|
||||
:class="{ 'is-empty': !statsCellText(col.prop, !!col.money) }"
|
||||
>
|
||||
{{ statsCellText(col.prop, !!col.money) || '\u00a0' }}
|
||||
</button>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<div class="quote-list-stats-menu">
|
||||
<button
|
||||
v-for="opt in COLUMN_STAT_METHOD_OPTIONS"
|
||||
:key="opt.label"
|
||||
type="button"
|
||||
class="quote-list-stats-menu__item"
|
||||
:class="{ 'is-active': (columnStatConfig[col.prop!] || '') === opt.value }"
|
||||
@click="pickColumnStat(col.prop!, opt.value)"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</button>
|
||||
</div>
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -863,8 +872,11 @@ import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import {
|
||||
COLUMN_STAT_METHOD_OPTIONS,
|
||||
QUOTE_MONEY_STAT_PROPS,
|
||||
applyStatsCellAlignStyles,
|
||||
formatColumnStatDisplay,
|
||||
getTableColumnTextAligns,
|
||||
loadColumnStatConfig,
|
||||
resolveStatsCellAlign,
|
||||
saveColumnStatConfig,
|
||||
setColumnStatMethod,
|
||||
type ColumnStatConfigMap,
|
||||
@@ -1899,16 +1911,29 @@ const formatQuoteProductInfoLines = (row: QuoteVO): string[] => {
|
||||
|
||||
const columnStatConfig = ref<ColumnStatConfigMap>(loadColumnStatConfig())
|
||||
|
||||
const statsBusinessColumns = computed(() => {
|
||||
const cols: { prop: string; money: boolean }[] = [
|
||||
{ prop: 'quoteNo', money: false },
|
||||
{ prop: 'quoteTitle', money: false }
|
||||
type StatsRowColumn = {
|
||||
key: string
|
||||
interactive: boolean
|
||||
prop?: string
|
||||
money?: boolean
|
||||
}
|
||||
|
||||
/** 与主表列顺序一致:展开、勾选占位 + 业务列 */
|
||||
const statsRowColumns = computed((): StatsRowColumn[] => {
|
||||
const cols: StatsRowColumn[] = [
|
||||
{ key: '__expand', interactive: false },
|
||||
{ key: '__selection', interactive: false },
|
||||
{ key: 'quoteNo', interactive: true, prop: 'quoteNo', money: false },
|
||||
{ key: 'quoteTitle', interactive: true, prop: 'quoteTitle', money: false }
|
||||
]
|
||||
for (const def of listColumnDefs) {
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop,
|
||||
money: QUOTE_MONEY_STAT_PROPS.has(def.prop)
|
||||
money: QUOTE_MONEY_STAT_PROPS.has(def.prop),
|
||||
...(QUOTE_MONEY_STAT_PROPS.has(def.prop) ? { align: 'right' as const } : {})
|
||||
})
|
||||
}
|
||||
return cols
|
||||
@@ -1935,24 +1960,30 @@ function onSelectColumnStat(prop: string, method: ColumnStatMethod | '') {
|
||||
saveColumnStatConfig(columnStatConfig.value)
|
||||
}
|
||||
|
||||
function onStatCommand(prop: string, cmd: string | number) {
|
||||
onSelectColumnStat(
|
||||
prop,
|
||||
(cmd === '' || cmd === 'sum' || cmd === 'avg' || cmd === 'max' || cmd === 'min'
|
||||
? cmd
|
||||
: '') as ColumnStatMethod | ''
|
||||
)
|
||||
const statsMenuOpenProp = ref<string | null>(null)
|
||||
|
||||
function setStatsMenuOpen(prop: string, visible: boolean) {
|
||||
statsMenuOpenProp.value = visible ? prop : statsMenuOpenProp.value === prop ? null : statsMenuOpenProp.value
|
||||
}
|
||||
|
||||
function pickColumnStat(prop: string, method: ColumnStatMethod | '') {
|
||||
onSelectColumnStat(prop, method)
|
||||
statsMenuOpenProp.value = null
|
||||
}
|
||||
|
||||
const quoteTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const quoteStatsScrollRef = ref<HTMLElement | null>(null)
|
||||
let quoteStatsScrollSyncing = false
|
||||
let quoteHeaderResizeObserver: ResizeObserver | null = null
|
||||
let quoteTableScrollElBound: HTMLElement | null = null
|
||||
|
||||
function getQuoteMainTableEl(): Element | null {
|
||||
function getQuoteMainTableEl(): HTMLElement | null {
|
||||
const wrap = quoteTableWrapRef.value
|
||||
if (!wrap) return null
|
||||
return wrap.querySelector(':scope > .el-table') || wrap.querySelector('.el-table')
|
||||
return (
|
||||
(wrap.querySelector(':scope > .el-table') as HTMLElement | null) ||
|
||||
(wrap.querySelector('.el-table') as HTMLElement | null)
|
||||
)
|
||||
}
|
||||
|
||||
function getQuoteTableScrollEl(): HTMLElement | null {
|
||||
@@ -1964,22 +1995,67 @@ function getQuoteTableScrollEl(): HTMLElement | null {
|
||||
)
|
||||
}
|
||||
|
||||
function syncStatsColumnWidths() {
|
||||
function getQuoteHeaderWidthSources(): number[] {
|
||||
const table = getQuoteMainTableEl()
|
||||
if (!table) return
|
||||
const headerCells = table.querySelectorAll('.el-table__header-wrapper th')
|
||||
const cells = quoteTableWrapRef.value?.querySelectorAll(
|
||||
'.quote-list-stats-row__scroll .quote-list-stats-row__cell'
|
||||
)
|
||||
if (!cells) return
|
||||
const offset = 2
|
||||
cells.forEach((el, i) => {
|
||||
const th = headerCells[i + offset] as HTMLElement | undefined
|
||||
if (!th) return
|
||||
;(el as HTMLElement).style.flex = 'none'
|
||||
;(el as HTMLElement).style.width = `${th.offsetWidth}px`
|
||||
;(el as HTMLElement).style.minWidth = `${th.offsetWidth}px`
|
||||
if (!table) return []
|
||||
|
||||
// 优先用 header colgroup(与实际列宽一致,并跳过 gutter)
|
||||
const cols = table.querySelectorAll('.el-table__header colgroup col')
|
||||
const fromCols: number[] = []
|
||||
cols.forEach((col) => {
|
||||
const el = col as HTMLElement
|
||||
if (el.getAttribute('name') === 'gutter') return
|
||||
const w = el.getBoundingClientRect().width || Number.parseFloat(el.getAttribute('width') || '') || 0
|
||||
if (w > 0) fromCols.push(w)
|
||||
})
|
||||
if (fromCols.length >= statsRowColumns.value.length) return fromCols
|
||||
|
||||
const ths = table.querySelectorAll('.el-table__header-wrapper th.el-table__cell')
|
||||
const fromThs: number[] = []
|
||||
ths.forEach((th) => {
|
||||
const el = th as HTMLElement
|
||||
if (el.classList.contains('gutter')) return
|
||||
const w = el.getBoundingClientRect().width
|
||||
if (w > 0) fromThs.push(w)
|
||||
})
|
||||
return fromThs
|
||||
}
|
||||
|
||||
function syncStatsColumnWidths() {
|
||||
const widths = getQuoteHeaderWidthSources()
|
||||
const aligns = getTableColumnTextAligns(getQuoteMainTableEl())
|
||||
const inner = quoteStatsScrollRef.value?.querySelector(
|
||||
'.quote-list-stats-row__inner'
|
||||
) as HTMLElement | null
|
||||
const cells = quoteStatsScrollRef.value?.querySelectorAll('.quote-list-stats-row__cell')
|
||||
if (!inner || !cells?.length || !widths.length) return
|
||||
|
||||
const cols = statsRowColumns.value
|
||||
const count = Math.min(cells.length, widths.length, cols.length)
|
||||
let total = 0
|
||||
for (let i = 0; i < count; i++) {
|
||||
const w = Math.round(widths[i])
|
||||
total += w
|
||||
const el = cells[i] as HTMLElement
|
||||
el.style.flex = 'none'
|
||||
el.style.width = `${w}px`
|
||||
el.style.minWidth = `${w}px`
|
||||
el.style.maxWidth = `${w}px`
|
||||
applyStatsCellAlignStyles(
|
||||
el,
|
||||
resolveStatsCellAlign(cols[i], aligns[i]),
|
||||
'.quote-list-stats-row__trigger'
|
||||
)
|
||||
}
|
||||
inner.style.width = `${total}px`
|
||||
inner.style.minWidth = `${total}px`
|
||||
|
||||
bindQuoteTableScrollListener()
|
||||
const tableScroll = getQuoteTableScrollEl()
|
||||
const statsScroll = quoteStatsScrollRef.value
|
||||
if (tableScroll && statsScroll) {
|
||||
statsScroll.scrollLeft = tableScroll.scrollLeft
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleSyncStatsColumnWidths() {
|
||||
@@ -2000,11 +2076,22 @@ function ensureQuoteHeaderResizeObserver() {
|
||||
syncStatsColumnWidths()
|
||||
})
|
||||
quoteHeaderResizeObserver.observe(header)
|
||||
header.querySelectorAll('th').forEach((th) => {
|
||||
header.querySelectorAll('th.el-table__cell').forEach((th) => {
|
||||
if ((th as HTMLElement).classList.contains('gutter')) return
|
||||
quoteHeaderResizeObserver?.observe(th)
|
||||
})
|
||||
}
|
||||
|
||||
function bindQuoteTableScrollListener() {
|
||||
const el = getQuoteTableScrollEl()
|
||||
if (!el || el === quoteTableScrollElBound) return
|
||||
if (quoteTableScrollElBound) {
|
||||
quoteTableScrollElBound.removeEventListener('scroll', onQuoteTableScroll)
|
||||
}
|
||||
quoteTableScrollElBound = el
|
||||
el.addEventListener('scroll', onQuoteTableScroll, { passive: true })
|
||||
}
|
||||
|
||||
function onQuoteHeaderDragEnd() {
|
||||
scheduleSyncStatsColumnWidths()
|
||||
}
|
||||
@@ -2036,7 +2123,7 @@ function onStatsRowScroll() {
|
||||
}
|
||||
|
||||
watch(
|
||||
[statsBusinessColumns, quoteList, visibleColumns],
|
||||
[statsRowColumns, quoteList, visibleColumns],
|
||||
() => {
|
||||
scheduleSyncStatsColumnWidths()
|
||||
},
|
||||
@@ -2963,8 +3050,9 @@ watchDebounced(
|
||||
onMounted(() => {
|
||||
getList();
|
||||
scheduleSyncStatsColumnWidths();
|
||||
// ListTable 会在挂载后异步回写持久化列宽,补一次对齐
|
||||
// ListTable 会在挂载后异步回写持久化列宽,补几次对齐
|
||||
setTimeout(() => scheduleSyncStatsColumnWidths(), 150);
|
||||
setTimeout(() => scheduleSyncStatsColumnWidths(), 400);
|
||||
window.addEventListener('resize', scheduleSyncStatsColumnWidths);
|
||||
});
|
||||
|
||||
@@ -2972,6 +3060,10 @@ onUnmounted(() => {
|
||||
window.removeEventListener('resize', scheduleSyncStatsColumnWidths);
|
||||
quoteHeaderResizeObserver?.disconnect();
|
||||
quoteHeaderResizeObserver = null;
|
||||
if (quoteTableScrollElBound) {
|
||||
quoteTableScrollElBound.removeEventListener('scroll', onQuoteTableScroll);
|
||||
quoteTableScrollElBound = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -3285,50 +3377,76 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.quote-list-stats-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
border-top: 2px solid var(--el-color-primary, #10b981);
|
||||
background: var(--mint-primary-bg, #ecfdf5);
|
||||
font-weight: 600;
|
||||
color: var(--mint-text, #1e293b);
|
||||
min-height: 44px;
|
||||
overflow: hidden;
|
||||
/* 隐藏自身滚动条,横向位置跟主表同步 */
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.quote-list-stats-row__fixed {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
.quote-list-stats-row::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.quote-list-stats-row__scroll {
|
||||
.quote-list-stats-row__inner {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
align-items: stretch;
|
||||
min-height: 44px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.quote-list-stats-row__cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 10px;
|
||||
justify-content: flex-start;
|
||||
padding: 0 8px;
|
||||
box-sizing: border-box;
|
||||
border-right: 1px solid var(--mint-border, #e2e8f0);
|
||||
flex: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.quote-list-stats-row__cell.is-stat {
|
||||
flex: 1 1 120px;
|
||||
min-width: 120px;
|
||||
.quote-list-stats-row__cell.is-align-left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.quote-list-stats-row__cell.is-align-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.quote-list-stats-row__cell.is-align-right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.quote-list-stats-row__cell.is-spacer {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.quote-list-stats-row__trigger {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 28px;
|
||||
margin: 0;
|
||||
padding: 4px 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
line-height: 1.4;
|
||||
font-size: 13px;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
outline: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.quote-list-stats-row__trigger.is-empty {
|
||||
@@ -3336,3 +3454,43 @@ onUnmounted(() => {
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
/* teleported 到 body,需非 scoped */
|
||||
.quote-list-stats-dropdown {
|
||||
z-index: 4000 !important;
|
||||
padding: 6px !important;
|
||||
}
|
||||
|
||||
.quote-list-stats-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.quote-list-stats-menu__item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 8px 10px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #1e293b;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.quote-list-stats-menu__item:hover {
|
||||
background: #ecfdf5;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.quote-list-stats-menu__item.is-active {
|
||||
background: #d1fae5;
|
||||
color: #059669;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -56,3 +56,15 @@ describe('setColumnStatMethod', () => {
|
||||
assert.deepEqual(b, {})
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveStatsCellAlign', () => {
|
||||
it('prefers explicit align, then money right, then column align', async () => {
|
||||
const { resolveStatsCellAlign } = await import('./quoteListColumnStats')
|
||||
assert.equal(resolveStatsCellAlign({ align: 'right' }, 'left'), 'right')
|
||||
assert.equal(resolveStatsCellAlign({ money: true }, 'center'), 'right')
|
||||
assert.equal(resolveStatsCellAlign({}, 'right'), 'right')
|
||||
assert.equal(resolveStatsCellAlign({}, 'center'), 'center')
|
||||
assert.equal(resolveStatsCellAlign({ money: true }), 'right')
|
||||
assert.equal(resolveStatsCellAlign({}), 'left')
|
||||
})
|
||||
})
|
||||
@@ -1,23 +1,27 @@
|
||||
export type ColumnStatMethod = 'sum' | 'avg' | 'max' | 'min'
|
||||
/** @deprecated 请优先使用 `@/utils/list-column-stats`;本文件保留 quote 兼容导出 */
|
||||
export {
|
||||
aggregateColumnValues,
|
||||
applyStatsCellAlignStyles,
|
||||
buildStatsRowColumns,
|
||||
COLUMN_STAT_METHOD_LABEL,
|
||||
COLUMN_STAT_METHOD_OPTIONS,
|
||||
formatColumnStatDisplay,
|
||||
formatStatNumberResult,
|
||||
getTableColumnTextAligns,
|
||||
loadColumnStatConfig,
|
||||
normalizeCssTextAlign,
|
||||
parseStatNumber,
|
||||
resolveStatsCellAlign,
|
||||
saveColumnStatConfig,
|
||||
setColumnStatMethod,
|
||||
type ColumnStatConfigMap,
|
||||
type ColumnStatMethod,
|
||||
type StatsCellAlign,
|
||||
type StatsRowColumn
|
||||
} from '@/utils/list-column-stats'
|
||||
|
||||
export const COLUMN_STAT_STORAGE_KEY = 'crm-quote-column-stats'
|
||||
|
||||
export const COLUMN_STAT_METHOD_OPTIONS: { value: ColumnStatMethod | ''; label: string }[] = [
|
||||
{ value: '', label: '无' },
|
||||
{ value: 'sum', label: '求和' },
|
||||
{ value: 'avg', label: '平均' },
|
||||
{ value: 'max', label: '最大值' },
|
||||
{ value: 'min', label: '最小值' }
|
||||
]
|
||||
|
||||
export const COLUMN_STAT_METHOD_LABEL: Record<ColumnStatMethod, string> = {
|
||||
sum: '求和',
|
||||
avg: '平均',
|
||||
max: '最大值',
|
||||
min: '最小值'
|
||||
}
|
||||
|
||||
/** 列表中按金额格式展示的列 prop */
|
||||
export const QUOTE_MONEY_STAT_PROPS = new Set([
|
||||
'discountAmount',
|
||||
'finalAmount',
|
||||
@@ -25,128 +29,3 @@ export const QUOTE_MONEY_STAT_PROPS = new Set([
|
||||
'freight',
|
||||
'barrelDeposit'
|
||||
])
|
||||
|
||||
export function parseStatNumber(raw: unknown): number | null {
|
||||
if (raw == null || raw === '') return null
|
||||
if (typeof raw === 'number') return Number.isFinite(raw) ? raw : null
|
||||
let s = String(raw).trim()
|
||||
if (!s) return null
|
||||
s = s.replace(/^¥/, '').replace(/%$/, '').replace(/,/g, '')
|
||||
const n = Number(s)
|
||||
return Number.isFinite(n) ? n : null
|
||||
}
|
||||
|
||||
function nonEmptyString(raw: unknown): string | null {
|
||||
if (raw == null) return null
|
||||
const s = String(raw).trim()
|
||||
return s ? s : null
|
||||
}
|
||||
|
||||
export function aggregateColumnValues(
|
||||
values: unknown[],
|
||||
method: ColumnStatMethod
|
||||
): number | string | null {
|
||||
const nums: number[] = []
|
||||
const strs: string[] = []
|
||||
for (const v of values) {
|
||||
const n = parseStatNumber(v)
|
||||
if (n != null) nums.push(n)
|
||||
else {
|
||||
const s = nonEmptyString(v)
|
||||
if (s != null) strs.push(s)
|
||||
}
|
||||
}
|
||||
|
||||
if (method === 'sum') {
|
||||
if (!nums.length) return null
|
||||
return nums.reduce((a, b) => a + b, 0)
|
||||
}
|
||||
if (method === 'avg') {
|
||||
if (!nums.length) return null
|
||||
return nums.reduce((a, b) => a + b, 0) / nums.length
|
||||
}
|
||||
if (method === 'max') {
|
||||
if (nums.length) return Math.max(...nums)
|
||||
if (strs.length) return strs.reduce((a, b) => (a >= b ? a : b))
|
||||
return null
|
||||
}
|
||||
// min
|
||||
if (nums.length) return Math.min(...nums)
|
||||
if (strs.length) return strs.reduce((a, b) => (a <= b ? a : b))
|
||||
return null
|
||||
}
|
||||
|
||||
export function formatStatNumberResult(
|
||||
value: number,
|
||||
opts: { money?: boolean } = {}
|
||||
): string {
|
||||
if (opts.money) {
|
||||
return value.toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
})
|
||||
}
|
||||
if (Number.isInteger(value)) return String(value)
|
||||
return value.toLocaleString('zh-CN', { maximumFractionDigits: 4 })
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成合计格展示文案。method 为空则返回 ''。
|
||||
* 聚合结果为 null →「{标签} -」
|
||||
*/
|
||||
export function formatColumnStatDisplay(
|
||||
method: ColumnStatMethod | '' | null | undefined,
|
||||
values: unknown[],
|
||||
opts: { money?: boolean } = {}
|
||||
): string {
|
||||
if (!method) return ''
|
||||
const label = COLUMN_STAT_METHOD_LABEL[method]
|
||||
const agg = aggregateColumnValues(values, method)
|
||||
if (agg == null) return `${label} -`
|
||||
if (typeof agg === 'number') {
|
||||
return `${label} ${formatStatNumberResult(agg, opts)}`
|
||||
}
|
||||
return `${label} ${agg}`
|
||||
}
|
||||
|
||||
export type ColumnStatConfigMap = Partial<Record<string, ColumnStatMethod>>
|
||||
|
||||
export function loadColumnStatConfig(
|
||||
storageKey: string = COLUMN_STAT_STORAGE_KEY
|
||||
): ColumnStatConfigMap {
|
||||
try {
|
||||
const raw = localStorage.getItem(storageKey)
|
||||
if (!raw) return {}
|
||||
const parsed = JSON.parse(raw) as Record<string, unknown>
|
||||
if (!parsed || typeof parsed !== 'object') return {}
|
||||
const out: ColumnStatConfigMap = {}
|
||||
for (const [k, v] of Object.entries(parsed)) {
|
||||
if (v === 'sum' || v === 'avg' || v === 'max' || v === 'min') out[k] = v
|
||||
}
|
||||
return out
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export function saveColumnStatConfig(
|
||||
config: ColumnStatConfigMap,
|
||||
storageKey: string = COLUMN_STAT_STORAGE_KEY
|
||||
): void {
|
||||
const cleaned: ColumnStatConfigMap = {}
|
||||
for (const [k, v] of Object.entries(config)) {
|
||||
if (v === 'sum' || v === 'avg' || v === 'max' || v === 'min') cleaned[k] = v
|
||||
}
|
||||
localStorage.setItem(storageKey, JSON.stringify(cleaned))
|
||||
}
|
||||
|
||||
export function setColumnStatMethod(
|
||||
config: ColumnStatConfigMap,
|
||||
prop: string,
|
||||
method: ColumnStatMethod | ''
|
||||
): ColumnStatConfigMap {
|
||||
const next = { ...config }
|
||||
if (!method) delete next[prop]
|
||||
else next[prop] = method
|
||||
return next
|
||||
}
|
||||
@@ -130,18 +130,18 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<ListTable table-key="db-inventory"
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
show-summary
|
||||
:summary-method="getSummary"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<div ref="inventoryTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="db-inventory"
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
@header-dragend="() => inventoryStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" min-width="60" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
@@ -356,6 +356,15 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="inventoryStatsRowRef"
|
||||
storage-key="db-inventory-column-stats"
|
||||
:columns="inventoryStatsRowColumns"
|
||||
:data="tableData"
|
||||
:table-wrap-el="inventoryTableWrapRef"
|
||||
:get-cell-value="getInventoryStatCellValue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<div class="list-pager-selected">
|
||||
@@ -512,7 +521,6 @@
|
||||
* UI 模式 P02 · 复杂业务列表
|
||||
* 参考:/#/crm/quote · src/views/crm/quote/index.vue
|
||||
*/
|
||||
import type { TableColumnCtx } from 'element-plus'
|
||||
import {
|
||||
Check,
|
||||
Close,
|
||||
@@ -529,6 +537,8 @@
|
||||
import ListToolbarDateShortcuts from '@/components/core/list-toolbar-date-shortcuts/index.vue'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { syncViewRowFromFlatList } from '@/utils/view-dialog-refresh'
|
||||
@@ -582,6 +592,45 @@
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('db-inventory-list-columns', listColumnDefs)
|
||||
|
||||
const inventoryTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const inventoryStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
const inventoryStatsRowColumns = computed((): StatsRowColumn[] => {
|
||||
const cols: StatsRowColumn[] = [
|
||||
{ key: '__selection', interactive: false },
|
||||
{ key: '__index', interactive: false }
|
||||
]
|
||||
if (isColumnVisible('id')) {
|
||||
cols.push({ key: 'id', interactive: true, prop: 'id' })
|
||||
}
|
||||
if (isColumnVisible('planNo')) {
|
||||
cols.push({ key: 'planNo', interactive: true, prop: 'planNo' })
|
||||
}
|
||||
cols.push({ key: 'batchNo', interactive: true, prop: 'batchNo' })
|
||||
for (const def of listColumnDefs) {
|
||||
if (def.prop === 'id' || def.prop === 'planNo') continue
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop
|
||||
})
|
||||
}
|
||||
return cols
|
||||
})
|
||||
|
||||
const KG_TO_TON_PROPS = new Set(['netWeight', 'grossWeight', 'tareWeight', 'deduction'])
|
||||
|
||||
function getInventoryStatCellValue(row: InventoryRecord, prop: string): unknown {
|
||||
if (KG_TO_TON_PROPS.has(prop)) {
|
||||
return (Number((row as any)[prop]) || 0) / 1000
|
||||
}
|
||||
if (prop === 'createByName') {
|
||||
return row.createByName || row.createBy || ''
|
||||
}
|
||||
return (row as any)?.[prop]
|
||||
}
|
||||
|
||||
const exportLoading = ref(false)
|
||||
const saving = ref(false)
|
||||
const tableData = ref<InventoryRecord[]>([])
|
||||
@@ -682,47 +731,6 @@
|
||||
return sortProp.value === prop ? sortOrder.value : null
|
||||
}
|
||||
|
||||
const getSummary = (param: { columns: TableColumnCtx<InventoryRecord>[]; data: InventoryRecord[] }) => {
|
||||
const { columns } = param
|
||||
const sums: string[] = []
|
||||
|
||||
columns.forEach((column, index) => {
|
||||
if (column.type === 'selection') {
|
||||
sums[index] = ''
|
||||
return
|
||||
}
|
||||
if (column.type === 'index') {
|
||||
sums[index] = '合计'
|
||||
return
|
||||
}
|
||||
if (column.property === 'quantity') {
|
||||
const totalQuantity = tableData.value.reduce(
|
||||
(sum, row) => sum + (Number(row.quantity) || 0),
|
||||
0
|
||||
)
|
||||
sums[index] = totalQuantity.toString()
|
||||
return
|
||||
}
|
||||
if (
|
||||
column.property === 'netWeight' ||
|
||||
column.property === 'grossWeight' ||
|
||||
column.property === 'tareWeight' ||
|
||||
column.property === 'deduction'
|
||||
) {
|
||||
const prop = column.property as keyof InventoryRecord
|
||||
const totalWeight = tableData.value.reduce(
|
||||
(sum, row) => sum + (Number(row[prop]) || 0) / 1000,
|
||||
0
|
||||
)
|
||||
sums[index] = totalWeight.toFixed(2)
|
||||
return
|
||||
}
|
||||
sums[index] = ''
|
||||
})
|
||||
|
||||
return sums
|
||||
}
|
||||
|
||||
const compare = (
|
||||
a: InventoryRecord,
|
||||
b: InventoryRecord,
|
||||
@@ -1175,6 +1183,10 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.db-date-with-shortcuts {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -130,17 +130,17 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<ListTable table-key="db-plans"
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
show-summary
|
||||
:summary-method="getSummary"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<div ref="plansTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="db-plans"
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
@header-dragend="() => plansStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" min-width="60" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
@@ -289,7 +289,16 @@
|
||||
{{ row.createByName || row.createBy || '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="plansStatsRowRef"
|
||||
storage-key="db-plans-column-stats"
|
||||
:columns="plansStatsRowColumns"
|
||||
:data="tableData"
|
||||
:table-wrap-el="plansTableWrapRef"
|
||||
:get-cell-value="getPlansStatCellValue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<div class="list-pager-selected">
|
||||
@@ -465,7 +474,6 @@
|
||||
* UI 模式 P02 · 复杂业务列表
|
||||
* 参考:/#/crm/quote · src/views/crm/quote/index.vue
|
||||
*/
|
||||
import type { TableColumnCtx } from 'element-plus'
|
||||
import {
|
||||
Check,
|
||||
Close,
|
||||
@@ -481,6 +489,8 @@
|
||||
import ListToolbarDateShortcuts from '@/components/core/list-toolbar-date-shortcuts/index.vue'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import DbCrmProductSelect from '@/components/business/db/DbCrmProductSelect.vue'
|
||||
import DbPlanRelatedViewDialog from '@/components/business/db/DbPlanRelatedViewDialog.vue'
|
||||
@@ -524,6 +534,36 @@
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('db-plans-list-columns', listColumnDefs)
|
||||
|
||||
const plansTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const plansStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
const plansStatsRowColumns = computed((): StatsRowColumn[] => {
|
||||
const cols: StatsRowColumn[] = [
|
||||
{ key: '__selection', interactive: false },
|
||||
{ key: '__index', interactive: false },
|
||||
{ key: 'planNo', interactive: true, prop: 'planNo' }
|
||||
]
|
||||
for (const def of listColumnDefs) {
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop === 'autoGenerate' ? 'autoGenerateEnabled' : def.prop
|
||||
})
|
||||
}
|
||||
return cols
|
||||
})
|
||||
|
||||
function getPlansStatCellValue(row: Plan, prop: string): unknown {
|
||||
if (prop === 'executedQuantity') {
|
||||
return (row.executedQuantity || 0) / 1000
|
||||
}
|
||||
if (prop === 'createByName') {
|
||||
return row.createByName || row.createBy || ''
|
||||
}
|
||||
return (row as any)?.[prop]
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const exportLoading = ref(false)
|
||||
const saving = ref(false)
|
||||
@@ -641,41 +681,6 @@
|
||||
return 'info'
|
||||
}
|
||||
|
||||
const getSummary = (param: { columns: TableColumnCtx<Plan>[]; data: Plan[] }) => {
|
||||
const { columns } = param
|
||||
const sums: string[] = []
|
||||
|
||||
columns.forEach((column, index) => {
|
||||
if (column.type === 'selection') {
|
||||
sums[index] = ''
|
||||
return
|
||||
}
|
||||
if (column.type === 'index') {
|
||||
sums[index] = '合计'
|
||||
return
|
||||
}
|
||||
if (column.property === 'plannedQuantity') {
|
||||
const totalPlanned = tableData.value.reduce(
|
||||
(sum, row) => sum + (Number(row.plannedQuantity) || 0),
|
||||
0
|
||||
)
|
||||
sums[index] = totalPlanned.toString()
|
||||
return
|
||||
}
|
||||
if (column.label === '已执行量(吨)') {
|
||||
const totalExecuted = tableData.value.reduce(
|
||||
(sum, row) => sum + (row.executedQuantity || 0) / 1000,
|
||||
0
|
||||
)
|
||||
sums[index] = totalExecuted.toFixed(2)
|
||||
return
|
||||
}
|
||||
sums[index] = ''
|
||||
})
|
||||
|
||||
return sums
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -1064,3 +1069,9 @@
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -109,17 +109,17 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<ListTable table-key="db-weighing"
|
||||
class="list-table"
|
||||
:data="paginatedData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
show-summary
|
||||
:summary-method="getSummary"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<div ref="weighingTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="db-weighing"
|
||||
class="list-table"
|
||||
:data="paginatedData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
@header-dragend="() => weighingStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" min-width="60" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
@@ -306,6 +306,15 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="weighingStatsRowRef"
|
||||
storage-key="db-weighing-column-stats"
|
||||
:columns="weighingStatsRowColumns"
|
||||
:data="paginatedData"
|
||||
:table-wrap-el="weighingTableWrapRef"
|
||||
:get-cell-value="getWeighingStatCellValue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<div class="list-pager-selected">
|
||||
@@ -506,7 +515,6 @@
|
||||
* UI 模式 P02 · 复杂业务列表
|
||||
* 参考:/#/crm/quote · src/views/crm/quote/index.vue
|
||||
*/
|
||||
import type { TableColumnCtx } from 'element-plus'
|
||||
import {
|
||||
Check,
|
||||
Close,
|
||||
@@ -520,6 +528,8 @@
|
||||
import ListToolbarDateShortcuts from '@/components/core/list-toolbar-date-shortcuts/index.vue'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import DbCrmProductSelect from '@/components/business/db/DbCrmProductSelect.vue'
|
||||
import DbRelatedViewDialog from '@/components/business/db/DbRelatedViewDialog.vue'
|
||||
@@ -571,6 +581,33 @@
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('db-weighing-list-columns', listColumnDefs)
|
||||
|
||||
const weighingTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const weighingStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
const weighingStatsRowColumns = computed((): StatsRowColumn[] => {
|
||||
const cols: StatsRowColumn[] = [
|
||||
{ key: '__selection', interactive: false },
|
||||
{ key: '__index', interactive: false }
|
||||
]
|
||||
if (isColumnVisible('inventoryDate')) {
|
||||
cols.push({ key: 'inventoryDate', interactive: true, prop: 'inventoryDate' })
|
||||
}
|
||||
if (isColumnVisible('planNo')) {
|
||||
cols.push({ key: 'planNo', interactive: true, prop: 'planNo' })
|
||||
}
|
||||
cols.push({ key: 'batchNo', interactive: true, prop: 'batchNo' })
|
||||
for (const def of listColumnDefs) {
|
||||
if (def.prop === 'inventoryDate' || def.prop === 'planNo') continue
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop
|
||||
})
|
||||
}
|
||||
return cols
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const exportLoading = ref(false)
|
||||
const saving = ref(false)
|
||||
@@ -693,6 +730,20 @@
|
||||
(row.createdAt ? row.createdAt.split(' ')[0] : '') ||
|
||||
''
|
||||
|
||||
function getWeighingStatCellValue(row: WeighingRecord, prop: string): unknown {
|
||||
if (prop === 'inventoryDate') return getRecordDate(row)
|
||||
if (prop === 'currentQuantity' || prop === 'netWeight') {
|
||||
return (Number((row as any)[prop]) || 0) / 1000
|
||||
}
|
||||
if (prop === 'docStatus') {
|
||||
return row.isVoided === 1 ? '已作废' : '正常'
|
||||
}
|
||||
if (prop === 'createByName') {
|
||||
return row.createByName || row.createBy || ''
|
||||
}
|
||||
return (row as any)?.[prop]
|
||||
}
|
||||
|
||||
const buildListQueryParams = (): WeighingListParams => {
|
||||
const params: WeighingListParams = {}
|
||||
const keyword = searchKeyword.value.trim()
|
||||
@@ -709,41 +760,6 @@
|
||||
return params
|
||||
}
|
||||
|
||||
const getSummary = (param: { columns: TableColumnCtx<WeighingRecord>[] }) => {
|
||||
const { columns } = param
|
||||
const sums: string[] = []
|
||||
|
||||
columns.forEach((column, index) => {
|
||||
if (column.type === 'selection') {
|
||||
sums[index] = ''
|
||||
return
|
||||
}
|
||||
if (column.type === 'index') {
|
||||
sums[index] = '合计'
|
||||
return
|
||||
}
|
||||
if (column.label === '本次量(吨)') {
|
||||
const totalCurrent = paginatedData.value.reduce(
|
||||
(sum, row) => sum + (row.currentQuantity || 0) / 1000,
|
||||
0
|
||||
)
|
||||
sums[index] = totalCurrent.toFixed(2)
|
||||
return
|
||||
}
|
||||
if (column.label === '净重(吨)') {
|
||||
const totalNet = paginatedData.value.reduce(
|
||||
(sum, row) => sum + (row.netWeight || 0) / 1000,
|
||||
0
|
||||
)
|
||||
sums[index] = totalNet.toFixed(2)
|
||||
return
|
||||
}
|
||||
sums[index] = ''
|
||||
})
|
||||
|
||||
return sums
|
||||
}
|
||||
|
||||
const calculateNetWeight = () => {
|
||||
formData.netWeight = netWeight.value
|
||||
}
|
||||
@@ -1196,3 +1212,9 @@
|
||||
openAddFromInventoryQuery()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -69,15 +69,17 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<ListTable table-key="pl--shared-io-order-list-page"
|
||||
class="list-table"
|
||||
:data="rows"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="(val: PlIoOrder[]) => (selectedRows = val)"
|
||||
@row-dblclick="openEdit"
|
||||
>
|
||||
<div ref="ioOrderTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable :table-key="`pl-${kind}-io-order-list`"
|
||||
class="list-table"
|
||||
:data="rows"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="(val: PlIoOrder[]) => (selectedRows = val)"
|
||||
@row-dblclick="openEdit"
|
||||
@header-dragend="() => ioOrderStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
@@ -202,6 +204,15 @@
|
||||
<el-table-column prop="actualQtyTotal" label="实际数量合计" min-width="120" align="right" />
|
||||
<el-table-column prop="siteFeedbackTime" label="最近提交" min-width="160" show-overflow-tooltip />
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="ioOrderStatsRowRef"
|
||||
:storage-key="`pl-${kind}-column-stats`"
|
||||
:columns="ioOrderStatsRowColumns"
|
||||
:data="rows"
|
||||
:table-wrap-el="ioOrderTableWrapRef"
|
||||
:get-cell-value="getIoOrderStatCellValue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<div class="list-pager-selected">
|
||||
@@ -416,6 +427,8 @@
|
||||
} from '@/components/business/brand-qty-chips-field/index.vue'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListToolbarDateShortcuts from '@/components/core/list-toolbar-date-shortcuts/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import ViewKvTable from '@/components/business/system-view-dialog/ViewKvTable.vue'
|
||||
import type { ViewField } from '@/components/business/system-view-dialog/types'
|
||||
import {
|
||||
@@ -441,6 +454,61 @@
|
||||
|
||||
const api = props.kind === 'inbound' ? inboundOrderApi : outboundOrderApi
|
||||
|
||||
const ioOrderTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const ioOrderStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
const ioOrderStatsRowColumns = computed((): StatsRowColumn[] => [
|
||||
{ key: '__selection', interactive: false },
|
||||
{ key: '__index', interactive: false },
|
||||
{ key: 'orderNo', interactive: true, prop: 'orderNo' },
|
||||
{ key: 'status', interactive: true, prop: 'status' },
|
||||
{ key: 'planNo', interactive: true, prop: 'planNo' },
|
||||
{ key: 'partnerCompany', interactive: true, prop: 'partnerCompany' },
|
||||
{ key: 'vehicleNo', interactive: true, prop: 'vehicleNo' },
|
||||
{ key: 'pickupAddress', interactive: true, prop: 'pickupAddress' },
|
||||
{ key: '__destination', interactive: true, prop: '__destination' },
|
||||
{ key: 'productName', interactive: true, prop: 'productName' },
|
||||
{ key: 'brand', interactive: true, prop: 'brand' },
|
||||
{ key: 'brandQty', interactive: true, prop: 'brandQty' },
|
||||
{ key: 'productSpec', interactive: true, prop: 'productSpec' },
|
||||
{ key: 'productUnit', interactive: true, prop: 'productUnit' },
|
||||
{ key: 'packageSpec', interactive: true, prop: 'packageSpec' },
|
||||
{ key: 'deliveryMethod', interactive: true, prop: 'deliveryMethod' },
|
||||
{ key: 'actualQtyTotal', interactive: true, prop: 'actualQtyTotal' },
|
||||
{ key: 'siteFeedbackTime', interactive: true, prop: 'siteFeedbackTime' }
|
||||
])
|
||||
|
||||
function joinItemLines(row: PlIoOrder, mapper: (item: PlIoOrder['items'][number]) => string): string {
|
||||
return (row.items || []).map(mapper).filter((s) => s && s !== '—').join('\n')
|
||||
}
|
||||
|
||||
function getIoOrderStatCellValue(row: PlIoOrder, prop: string): unknown {
|
||||
if (prop === 'status') return statusLabel(row.status)
|
||||
if (prop === '__destination') return row.destinationAddress || row.destinationName || ''
|
||||
if (prop === 'productName') {
|
||||
return joinItemLines(row, (item) => item.productName || '')
|
||||
}
|
||||
if (prop === 'brand') {
|
||||
return joinItemLines(row, (item) => brandConstraintLabel(item))
|
||||
}
|
||||
if (prop === 'brandQty') {
|
||||
return joinItemLines(row, (item) => resolveIoBrandQtyDisplay(item))
|
||||
}
|
||||
if (prop === 'productSpec') {
|
||||
return joinItemLines(row, (item) => item.specification || '')
|
||||
}
|
||||
if (prop === 'productUnit') {
|
||||
return joinItemLines(row, (item) => item.unitLabel || item.unit || '')
|
||||
}
|
||||
if (prop === 'packageSpec') {
|
||||
return joinItemLines(row, (item) => item.packageSpec || '')
|
||||
}
|
||||
if (prop === 'deliveryMethod') {
|
||||
return joinItemLines(row, (item) => deliveryMethodLabel(item.deliveryMethod))
|
||||
}
|
||||
return (row as any)?.[prop]
|
||||
}
|
||||
|
||||
type IoOrderEditItem = PlIoOrder['items'][number] & {
|
||||
brandQtyRows?: BrandQtyRow[]
|
||||
}
|
||||
@@ -809,6 +877,10 @@
|
||||
</style>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.io-order-multi-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -53,15 +53,17 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<ListTable table-key="pl-freight-components-approval-done-panel"
|
||||
class="list-table"
|
||||
:data="settlementList"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@row-dblclick="handleView"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<div ref="doneTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="pl-freight-components-approval-done-panel"
|
||||
class="list-table"
|
||||
:data="settlementList"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@row-dblclick="handleView"
|
||||
@selection-change="handleSelectionChange"
|
||||
@header-dragend="() => doneStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
@@ -133,6 +135,15 @@
|
||||
<template #default="{ row }">{{ row.submitterName || '—' }}</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="doneStatsRowRef"
|
||||
storage-key="pl-freight-approval-done-column-stats"
|
||||
:columns="doneStatsRowColumns"
|
||||
:data="settlementList"
|
||||
:table-wrap-el="doneTableWrapRef"
|
||||
:get-cell-value="getDoneStatCellValue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<div class="list-pager-selected">
|
||||
@@ -158,6 +169,8 @@
|
||||
<script setup lang="ts">
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { RefreshLeft, Search } from '@element-plus/icons-vue'
|
||||
@@ -187,6 +200,36 @@ const {
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('pl-freight-approval-done-columns', listColumnDefs)
|
||||
|
||||
const doneTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const doneStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
const doneStatsRowColumns = computed((): StatsRowColumn[] => {
|
||||
const cols: StatsRowColumn[] = [
|
||||
{ key: '__selection', interactive: false },
|
||||
{ key: '__index', interactive: false },
|
||||
{ key: 'settlementNo', interactive: true, prop: 'settlementNo' }
|
||||
]
|
||||
for (const def of listColumnDefs) {
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop,
|
||||
money: def.prop === 'totalFreight'
|
||||
})
|
||||
}
|
||||
return cols
|
||||
})
|
||||
|
||||
function getDoneStatCellValue(row: FreightSettlement, prop: string): unknown {
|
||||
if (prop === 'settlementStatus') return getSettlementStatusLabel(row.settlementStatus)
|
||||
if (prop === 'approvalFinishedTime') {
|
||||
return row.approvalFinishedTime ? formatDateTime(row.approvalFinishedTime) : ''
|
||||
}
|
||||
if (prop === 'submitterName') return row.submitterName || ''
|
||||
return (row as any)?.[prop]
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const selectedRows = ref<FreightSettlement[]>([])
|
||||
const settlementList = ref<FreightSettlement[]>([])
|
||||
@@ -284,3 +327,9 @@ onMounted(() => {
|
||||
|
||||
defineExpose({ refresh })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -71,15 +71,17 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<ListTable table-key="pl-freight-components-approval-pending-panel"
|
||||
class="list-table"
|
||||
:data="settlementList"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<div ref="pendingTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="pl-freight-components-approval-pending-panel"
|
||||
class="list-table"
|
||||
:data="settlementList"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
@header-dragend="() => pendingStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
@@ -145,6 +147,15 @@
|
||||
<template #default="{ row }">{{ formatDateTime(row.createTime) }}</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="pendingStatsRowRef"
|
||||
storage-key="pl-freight-approval-pending-column-stats"
|
||||
:columns="pendingStatsRowColumns"
|
||||
:data="settlementList"
|
||||
:table-wrap-el="pendingTableWrapRef"
|
||||
:get-cell-value="getPendingStatCellValue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<div class="list-pager-selected">
|
||||
@@ -190,6 +201,8 @@
|
||||
<script setup lang="ts">
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { CircleCheck, CircleClose, RefreshLeft, Search } from '@element-plus/icons-vue'
|
||||
@@ -216,6 +229,36 @@ const {
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('pl-freight-approval-pending-columns', listColumnDefs)
|
||||
|
||||
const pendingTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const pendingStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
const pendingStatsRowColumns = computed((): StatsRowColumn[] => {
|
||||
const cols: StatsRowColumn[] = [
|
||||
{ key: '__selection', interactive: false },
|
||||
{ key: '__index', interactive: false },
|
||||
{ key: 'settlementNo', interactive: true, prop: 'settlementNo' }
|
||||
]
|
||||
for (const def of listColumnDefs) {
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop,
|
||||
money: def.prop === 'totalFreight'
|
||||
})
|
||||
}
|
||||
return cols
|
||||
})
|
||||
|
||||
function getPendingStatCellValue(row: FreightSettlement, prop: string): unknown {
|
||||
if (prop === 'currentApproverName') return row.currentApproverName || ''
|
||||
if (prop === 'submitterName') return row.submitterName || ''
|
||||
if (prop === 'createTime') {
|
||||
return row.createTime ? formatDateTime(row.createTime) : ''
|
||||
}
|
||||
return (row as any)?.[prop]
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const settlementList = ref<FreightSettlement[]>([])
|
||||
const selectedRows = ref<FreightSettlement[]>([])
|
||||
@@ -340,3 +383,9 @@ onMounted(() => {
|
||||
|
||||
defineExpose({ refresh })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -55,15 +55,17 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<ListTable table-key="pl-freight-components-disburse-panel"
|
||||
class="list-table"
|
||||
:data="settlementList"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<div ref="disburseTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="pl-freight-components-disburse-panel"
|
||||
class="list-table"
|
||||
:data="settlementList"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
@header-dragend="() => disburseStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
@@ -122,6 +124,15 @@
|
||||
<template #default="{ row }">{{ row.submitterName || '—' }}</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="disburseStatsRowRef"
|
||||
storage-key="pl-freight-disburse-column-stats"
|
||||
:columns="disburseStatsRowColumns"
|
||||
:data="settlementList"
|
||||
:table-wrap-el="disburseTableWrapRef"
|
||||
:get-cell-value="getDisburseStatCellValue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<div class="list-pager-selected">
|
||||
@@ -154,6 +165,8 @@
|
||||
<script setup lang="ts">
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { Money, RefreshLeft, Search } from '@element-plus/icons-vue'
|
||||
@@ -181,6 +194,35 @@ const {
|
||||
|
||||
const emit = defineEmits<{ disbursed: [] }>()
|
||||
|
||||
const disburseTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const disburseStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
const disburseStatsRowColumns = computed((): StatsRowColumn[] => {
|
||||
const cols: StatsRowColumn[] = [
|
||||
{ key: '__selection', interactive: false },
|
||||
{ key: '__index', interactive: false },
|
||||
{ key: 'settlementNo', interactive: true, prop: 'settlementNo' }
|
||||
]
|
||||
for (const def of listColumnDefs) {
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop,
|
||||
money: def.prop === 'totalFreight'
|
||||
})
|
||||
}
|
||||
return cols
|
||||
})
|
||||
|
||||
function getDisburseStatCellValue(row: FreightSettlement, prop: string): unknown {
|
||||
if (prop === 'approvalFinishedTime') {
|
||||
return row.approvalFinishedTime ? formatDateTime(row.approvalFinishedTime) : ''
|
||||
}
|
||||
if (prop === 'submitterName') return row.submitterName || ''
|
||||
return (row as any)?.[prop]
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const settlementList = ref<FreightSettlement[]>([])
|
||||
const selectedRows = ref<FreightSettlement[]>([])
|
||||
@@ -295,3 +337,9 @@ onMounted(() => {
|
||||
|
||||
defineExpose({ refresh })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -47,15 +47,17 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<ListTable table-key="pl-freight-components-settled-panel"
|
||||
class="list-table"
|
||||
:data="settlementList"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@row-dblclick="handleView"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<div ref="settledTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="pl-freight-components-settled-panel"
|
||||
class="list-table"
|
||||
:data="settlementList"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@row-dblclick="handleView"
|
||||
@selection-change="handleSelectionChange"
|
||||
@header-dragend="() => settledStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
@@ -115,6 +117,15 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="settledStatsRowRef"
|
||||
storage-key="pl-freight-settled-column-stats"
|
||||
:columns="settledStatsRowColumns"
|
||||
:data="settlementList"
|
||||
:table-wrap-el="settledTableWrapRef"
|
||||
:get-cell-value="getSettledStatCellValue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<div class="list-pager-selected">
|
||||
@@ -140,6 +151,8 @@
|
||||
<script setup lang="ts">
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { RefreshLeft, Search } from '@element-plus/icons-vue'
|
||||
@@ -167,6 +180,36 @@ const {
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('pl-freight-settled-columns', listColumnDefs)
|
||||
|
||||
const settledTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const settledStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
const settledStatsRowColumns = computed((): StatsRowColumn[] => {
|
||||
const cols: StatsRowColumn[] = [
|
||||
{ key: '__selection', interactive: false },
|
||||
{ key: '__index', interactive: false },
|
||||
{ key: 'settlementNo', interactive: true, prop: 'settlementNo' }
|
||||
]
|
||||
for (const def of listColumnDefs) {
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop,
|
||||
money: def.prop === 'totalFreight' || def.prop === 'disburseAmount'
|
||||
})
|
||||
}
|
||||
return cols
|
||||
})
|
||||
|
||||
function getSettledStatCellValue(row: FreightSettlement, prop: string): unknown {
|
||||
if (prop === 'disburseAmount') return row.disburseAmount || 0
|
||||
if (prop === 'disburseTime') {
|
||||
return row.disburseTime ? formatDateTime(row.disburseTime) : ''
|
||||
}
|
||||
if (prop === 'settlementStatus') return getSettlementStatusLabel(row.settlementStatus)
|
||||
return (row as any)?.[prop]
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const selectedRows = ref<FreightSettlement[]>([])
|
||||
const settlementList = ref<FreightSettlement[]>([])
|
||||
@@ -256,3 +299,9 @@ onMounted(() => {
|
||||
|
||||
defineExpose({ refresh })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -126,15 +126,17 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<ListTable table-key="pl-freight-components-settlement-bill-panel"
|
||||
class="list-table"
|
||||
:data="settlementList"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<div ref="billsTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="pl-freight-components-settlement-bill-panel"
|
||||
class="list-table"
|
||||
:data="settlementList"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
@header-dragend="() => billsStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
@@ -235,6 +237,15 @@
|
||||
<template #default="{ row }">{{ formatDateTime(row.createTime) }}</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="billsStatsRowRef"
|
||||
storage-key="pl-freight-settlement-bill-column-stats"
|
||||
:columns="billsStatsRowColumns"
|
||||
:data="settlementList"
|
||||
:table-wrap-el="billsTableWrapRef"
|
||||
:get-cell-value="getBillsStatCellValue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<div class="list-pager-selected">
|
||||
@@ -309,6 +320,8 @@
|
||||
<script setup lang="ts">
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import {
|
||||
@@ -352,6 +365,41 @@ const {
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('pl-freight-settlement-bill-columns', listColumnDefs)
|
||||
|
||||
const billsTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const billsStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
const billsStatsRowColumns = computed((): StatsRowColumn[] => {
|
||||
const cols: StatsRowColumn[] = [
|
||||
{ key: '__selection', interactive: false },
|
||||
{ key: '__index', interactive: false },
|
||||
{ key: 'settlementNo', interactive: true, prop: 'settlementNo' }
|
||||
]
|
||||
for (const def of listColumnDefs) {
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop,
|
||||
money: def.prop === 'totalFreight' || def.prop === 'disburseAmount'
|
||||
})
|
||||
}
|
||||
return cols
|
||||
})
|
||||
|
||||
function getBillsStatCellValue(row: FreightSettlement, prop: string): unknown {
|
||||
if (prop === 'settlementStatus') return getSettlementStatusLabel(row.settlementStatus)
|
||||
if (prop === 'disburseAmount') return row.disburseAmount || 0
|
||||
if (prop === 'disburseTime') {
|
||||
return row.disburseTime ? formatDateTime(row.disburseTime) : ''
|
||||
}
|
||||
if (prop === 'currentApproverName') return row.currentApproverName || ''
|
||||
if (prop === 'submitterName') return row.submitterName || ''
|
||||
if (prop === 'createTime') {
|
||||
return row.createTime ? formatDateTime(row.createTime) : ''
|
||||
}
|
||||
return (row as any)?.[prop]
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const cancelLoadingMap = reactive<Record<string, boolean>>({})
|
||||
const {
|
||||
@@ -540,3 +588,9 @@ onMounted(() => {
|
||||
|
||||
defineExpose({ refresh })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -84,15 +84,17 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<ListTable table-key="pl-freight-components-unsettled-plan-panel"
|
||||
class="list-table"
|
||||
:data="planList"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleViewPlan"
|
||||
>
|
||||
<div ref="unsettledTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="pl-freight-components-unsettled-plan-panel"
|
||||
class="list-table"
|
||||
:data="planList"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleViewPlan"
|
||||
@header-dragend="() => unsettledStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="selection" min-width="48" align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
@@ -178,6 +180,15 @@
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="unsettledStatsRowRef"
|
||||
storage-key="pl-freight-unsettled-plan-column-stats"
|
||||
:columns="unsettledStatsRowColumns"
|
||||
:data="planList"
|
||||
:table-wrap-el="unsettledTableWrapRef"
|
||||
:get-cell-value="getUnsettledStatCellValue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<div class="list-pager-selected">
|
||||
@@ -203,6 +214,8 @@
|
||||
<script setup lang="ts">
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { DocumentAdd, RefreshLeft, Search } from '@element-plus/icons-vue'
|
||||
@@ -236,6 +249,39 @@ const {
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('pl-freight-unsettled-plan-columns', listColumnDefs)
|
||||
|
||||
const unsettledTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const unsettledStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
const unsettledStatsRowColumns = computed((): StatsRowColumn[] => {
|
||||
const cols: StatsRowColumn[] = [
|
||||
{ key: '__selection', interactive: false },
|
||||
{ key: '__index', interactive: false },
|
||||
{ key: 'planNo', interactive: true, prop: 'planNo' }
|
||||
]
|
||||
for (const def of listColumnDefs) {
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop,
|
||||
money: def.prop === 'freightAmount'
|
||||
})
|
||||
}
|
||||
return cols
|
||||
})
|
||||
|
||||
function getUnsettledStatCellValue(row: UnsettledPlanItem, prop: string): unknown {
|
||||
if (prop === 'planType') return getPlanTypeLabel(row.planType)
|
||||
if (prop === 'partnerName') return row.customerName || row.supplierName || ''
|
||||
if (prop === 'licensePlate') {
|
||||
return formatPlVehiclePlateDisplay(row.licensePlate, undefined, vehicleTrailerLookup.value)
|
||||
}
|
||||
if (prop === 'archiveTime') {
|
||||
return row.archiveTime ? formatDateTime(row.archiveTime) : ''
|
||||
}
|
||||
return (row as any)?.[prop]
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const createLoading = ref(false)
|
||||
const {
|
||||
@@ -396,3 +442,9 @@ onMounted(() => {
|
||||
|
||||
defineExpose({ refresh })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -82,7 +82,7 @@
|
||||
/>
|
||||
<ListQuickSearchInput
|
||||
v-model="quickSearch"
|
||||
placeholder="计划编号/客户名称"
|
||||
placeholder="计划编号/供方/需方/产品名称"
|
||||
@search="handleQuickSearch"
|
||||
@clear="handleQuickSearchClear"
|
||||
/>
|
||||
@@ -109,15 +109,17 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<ListTable table-key="pl-plan-archive"
|
||||
class="list-table"
|
||||
:data="planList"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<div ref="archiveTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="pl-plan-archive"
|
||||
class="list-table"
|
||||
:data="planList"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
@header-dragend="() => archiveStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
@@ -295,6 +297,15 @@
|
||||
align="center"
|
||||
/>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="archiveStatsRowRef"
|
||||
storage-key="pl-plan-archive-column-stats"
|
||||
:columns="archiveStatsRowColumns"
|
||||
:data="planList"
|
||||
:table-wrap-el="archiveTableWrapRef"
|
||||
:get-cell-value="getArchiveStatCellValue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<div class="list-pager-selected">
|
||||
@@ -342,6 +353,8 @@
|
||||
<script setup lang="ts">
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import {
|
||||
FolderChecked,
|
||||
@@ -412,6 +425,60 @@ const {
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('pl-plan-archive-list-columns', listColumnDefs)
|
||||
|
||||
const archiveTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const archiveStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
const archiveStatsRowColumns = computed((): StatsRowColumn[] => {
|
||||
const cols: StatsRowColumn[] = [
|
||||
{ key: '__selection', interactive: false },
|
||||
{ key: '__index', interactive: false },
|
||||
{ key: 'planNo', interactive: true, prop: 'planNo' }
|
||||
]
|
||||
for (const def of listColumnDefs) {
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop
|
||||
})
|
||||
}
|
||||
return cols
|
||||
})
|
||||
|
||||
function getArchiveStatCellValue(row: Plan, prop: string): unknown {
|
||||
if (prop === 'archiveStatus') return getPlPlanArchiveStatusLabel(row.archiveStatus)
|
||||
if (prop === 'followStatus') return getPlPlanFollowStatusLabel(row.followStatus)
|
||||
if (prop === 'planType') return getPlanTypeLabel(row.planType)
|
||||
if (prop === 'productPlanQty') {
|
||||
return (row.products || []).reduce((sum, p) => sum + (Number(p.quantity) || 0), 0)
|
||||
}
|
||||
if (prop === 'productActualQty') {
|
||||
return (row.products || []).reduce((sum, p) => sum + (Number(p.actualQty) || 0), 0)
|
||||
}
|
||||
if (prop === 'productName') {
|
||||
return (row.products || []).map((p) => p.productName || '').filter(Boolean).join('\n')
|
||||
}
|
||||
if (prop === 'productSpec') {
|
||||
return (row.products || [])
|
||||
.map((p) => p.model || p.packageSpec || '')
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
}
|
||||
if (prop === 'productUnit') {
|
||||
return (row.products || []).map((p) => p.unit || '').filter(Boolean).join('\n')
|
||||
}
|
||||
if (prop === 'planAttachments') {
|
||||
return ''
|
||||
}
|
||||
if (prop === 'licensePlate') {
|
||||
return formatPlVehiclePlateDisplay(row.licensePlate, row.trailerNo)
|
||||
}
|
||||
if (prop === 'personnelSummary') {
|
||||
return normalizePlPersonnelSummaryDisplay(row.personnelSummary)
|
||||
}
|
||||
return (row as any)?.[prop]
|
||||
}
|
||||
|
||||
const selectedRows = ref<Plan[]>([])
|
||||
const viewingPlan = ref<Plan | null>(null)
|
||||
|
||||
@@ -515,6 +582,7 @@ function handleReset() {
|
||||
function handleQuickSearch() {
|
||||
searchForm.planNo = ''
|
||||
searchForm.customerName = ''
|
||||
searchForm.productName = ''
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
@@ -611,6 +679,10 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pl-plan-list-toolbar-row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
/>
|
||||
<ListQuickSearchInput
|
||||
v-model="quickSearch"
|
||||
placeholder="计划编号/客户名称"
|
||||
placeholder="计划编号/供方/需方"
|
||||
@search="handleQuickSearch"
|
||||
@clear="handleQuickSearchClear"
|
||||
/>
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
/>
|
||||
<ListQuickSearchInput
|
||||
v-model="quickSearch"
|
||||
placeholder="计划编号/客户名称/产品名称/车牌号"
|
||||
placeholder="计划编号/供方/需方/产品名称/车牌号"
|
||||
@search="handleQuickSearch"
|
||||
@clear="handleQuickSearchClear"
|
||||
/>
|
||||
@@ -173,15 +173,17 @@
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<div class="list-body list-wrap">
|
||||
<ListTable table-key="pl-plan"
|
||||
class="list-table"
|
||||
:data="planList"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<div ref="planTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="pl-plan"
|
||||
class="list-table"
|
||||
:data="planList"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
@header-dragend="() => planStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
@@ -390,6 +392,15 @@
|
||||
align="center"
|
||||
/>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="planStatsRowRef"
|
||||
storage-key="pl-plan-column-stats"
|
||||
:columns="planStatsRowColumns"
|
||||
:data="planList"
|
||||
:table-wrap-el="planTableWrapRef"
|
||||
:get-cell-value="getPlanStatCellValue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar pl-plan-list-pager-bar">
|
||||
<div class="list-pager-selected">
|
||||
@@ -528,6 +539,8 @@ import PlPlanArrangeDialog from '@/components/business/pl-plan-arrange-dialog/in
|
||||
import PlPlanFormDialog from '@/components/business/pl-plan-form-dialog/index.vue'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import PlPlanListCertCell from '@/components/business/pl-plan-list-cert-cell/index.vue'
|
||||
import PlPlanListProductCell from '@/components/business/pl-plan-list-product-cell/index.vue'
|
||||
@@ -595,6 +608,64 @@ const {
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('pl-plan-list-columns-v2', listColumnDefs)
|
||||
|
||||
const planTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const planStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
const planStatsRowColumns = computed((): StatsRowColumn[] => {
|
||||
const cols: StatsRowColumn[] = [
|
||||
{ key: '__selection', interactive: false },
|
||||
{ key: '__index', interactive: false },
|
||||
{ key: 'planNo', interactive: true, prop: 'planNo' }
|
||||
]
|
||||
for (const def of listColumnDefs) {
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop
|
||||
})
|
||||
}
|
||||
return cols
|
||||
})
|
||||
|
||||
function getPlanStatCellValue(row: Plan, prop: string): unknown {
|
||||
if (prop === 'followStatus') return getPlPlanFollowStatusLabel(row.followStatus)
|
||||
if (prop === 'planType') return getPlanTypeLabel(row.planType)
|
||||
if (prop === 'contractSummary') {
|
||||
return planContractLines(row)
|
||||
.map((l) => `${l.side}${l.no}`)
|
||||
.join(' ')
|
||||
}
|
||||
if (prop === 'productPlanQty') {
|
||||
return (row.products || []).reduce((sum, p) => sum + (Number(p.quantity) || 0), 0)
|
||||
}
|
||||
if (prop === 'productActualQty') {
|
||||
return (row.products || []).reduce((sum, p) => sum + (Number(p.actualQty) || 0), 0)
|
||||
}
|
||||
if (prop === 'productName') {
|
||||
return (row.products || []).map((p) => p.productName || '').filter(Boolean).join('\n')
|
||||
}
|
||||
if (prop === 'productSpec') {
|
||||
return (row.products || []).map((p) => p.model || p.spec || '').filter(Boolean).join('\n')
|
||||
}
|
||||
if (prop === 'productPackageSpec') {
|
||||
return (row.products || []).map((p) => p.packageSpec || '').filter(Boolean).join('\n')
|
||||
}
|
||||
if (prop === 'productUnit') {
|
||||
return (row.products || []).map((p) => p.unit || '').filter(Boolean).join('\n')
|
||||
}
|
||||
if (prop === 'planAttachments') {
|
||||
return ''
|
||||
}
|
||||
if (prop === 'licensePlate') {
|
||||
return formatPlVehiclePlateDisplay(row.licensePlate, row.trailerNo)
|
||||
}
|
||||
if (prop === 'personnelSummary') {
|
||||
return normalizePlPersonnelSummaryDisplay(row.personnelSummary)
|
||||
}
|
||||
return (row as any)?.[prop]
|
||||
}
|
||||
|
||||
const selectedRows = ref<Plan[]>([])
|
||||
const viewingPlan = ref<Plan | null>(null)
|
||||
|
||||
@@ -1010,6 +1081,10 @@ async function openArrangeFromQuery() {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pl-plan-list-toolbar-row {
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
|
||||
@@ -99,15 +99,17 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<ListTable table-key="pl-vehicle-maintenance-apply"
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<div ref="maintApplyTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="pl-vehicle-maintenance-apply"
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
@header-dragend="() => maintApplyStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
@@ -198,6 +200,15 @@
|
||||
align="center"
|
||||
/>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="maintApplyStatsRowRef"
|
||||
storage-key="pl-vehicle-maintenance-apply-column-stats"
|
||||
:columns="maintApplyStatsRowColumns"
|
||||
:data="tableData"
|
||||
:table-wrap-el="maintApplyTableWrapRef"
|
||||
:get-cell-value="getMaintApplyStatCellValue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<div class="list-pager-selected">
|
||||
@@ -359,6 +370,8 @@
|
||||
<script setup lang="ts">
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import {
|
||||
@@ -415,6 +428,36 @@ const {
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('pl-vehicle-maintenance-apply-list-columns', listColumnDefs)
|
||||
|
||||
const maintApplyTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const maintApplyStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
const maintApplyStatsRowColumns = computed((): StatsRowColumn[] => {
|
||||
const cols: StatsRowColumn[] = [
|
||||
{ key: '__selection', interactive: false },
|
||||
{ key: '__index', interactive: false },
|
||||
{ key: 'applyNo', interactive: true, prop: 'applyNo' }
|
||||
]
|
||||
for (const def of listColumnDefs) {
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop
|
||||
})
|
||||
}
|
||||
return cols
|
||||
})
|
||||
|
||||
function getMaintApplyStatCellValue(row: VehicleMaintenanceApply, prop: string): unknown {
|
||||
if (prop === 'licensePlate') {
|
||||
return formatPlVehiclePlateDisplay(row.licensePlate, undefined, vehicleTrailerLookup.value)
|
||||
}
|
||||
if (prop === 'maintenanceType') return getPlMaintenanceTypeLabel(row.maintenanceType)
|
||||
if (prop === 'expectedPickupDate') return row.expectedPickupDate || ''
|
||||
if (prop === 'status') return getPlMaintenanceApplyStatusLabel(row.status)
|
||||
return (row as any)?.[prop]
|
||||
}
|
||||
|
||||
const currentUserId = computed(() => {
|
||||
const info = userStore.info as Record<string, unknown> | undefined
|
||||
return String(info?.id || info?.userId || '')
|
||||
@@ -773,3 +816,9 @@ onMounted(async () => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -114,15 +114,17 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<ListTable table-key="pl-vehicle-maintenance-process"
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="applyId"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<div ref="maintProcessTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="pl-vehicle-maintenance-process"
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="applyId"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
@header-dragend="() => maintProcessStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
@@ -244,6 +246,15 @@
|
||||
<template #default="{ row }">{{ row.handlerName || '—' }}</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="maintProcessStatsRowRef"
|
||||
storage-key="pl-vehicle-maintenance-process-column-stats"
|
||||
:columns="maintProcessStatsRowColumns"
|
||||
:data="tableData"
|
||||
:table-wrap-el="maintProcessTableWrapRef"
|
||||
:get-cell-value="getMaintProcessStatCellValue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<div class="list-pager-selected">
|
||||
@@ -403,6 +414,8 @@
|
||||
<script setup lang="ts">
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { Check, Close, Delete, Edit, Plus, RefreshLeft, Search, Tools } from '@element-plus/icons-vue'
|
||||
@@ -467,6 +480,41 @@ const {
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('pl-vehicle-maintenance-process-list-columns', listColumnDefs)
|
||||
|
||||
const maintProcessTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const maintProcessStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
const maintProcessStatsRowColumns = computed((): StatsRowColumn[] => {
|
||||
const cols: StatsRowColumn[] = [
|
||||
{ key: '__selection', interactive: false },
|
||||
{ key: '__index', interactive: false },
|
||||
{ key: 'applyNo', interactive: true, prop: 'applyNo' }
|
||||
]
|
||||
for (const def of listColumnDefs) {
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop,
|
||||
money: def.prop === 'amount'
|
||||
})
|
||||
}
|
||||
return cols
|
||||
})
|
||||
|
||||
function getMaintProcessStatCellValue(row: VehicleMaintenanceProcessBoardItem, prop: string): unknown {
|
||||
if (prop === 'applyStatus') return getPlMaintenanceApplyStatusLabel(row.applyStatus)
|
||||
if (prop === 'licensePlate') {
|
||||
return formatPlVehiclePlateDisplay(row.licensePlate, undefined, vehicleTrailerLookup.value)
|
||||
}
|
||||
if (prop === 'maintenanceType') return getPlMaintenanceTypeLabel(row.maintenanceType)
|
||||
if (prop === 'expectedPickupDate') return row.expectedPickupDate || ''
|
||||
if (prop === 'processNo') return row.processNo || ''
|
||||
if (prop === 'processDate') return row.processDate || ''
|
||||
if (prop === 'amount') return row.amount != null ? Number(row.amount) : null
|
||||
if (prop === 'handlerName') return row.handlerName || ''
|
||||
return (row as any)?.[prop]
|
||||
}
|
||||
|
||||
const submitLoading = ref(false)
|
||||
const deleteLoadingMap = reactive<Record<string, boolean>>({})
|
||||
const selectedRows = ref<VehicleMaintenanceProcessBoardItem[]>([])
|
||||
@@ -861,6 +909,10 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.apply-summary-block {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 14px;
|
||||
|
||||
@@ -81,15 +81,17 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<ListTable table-key="pl-vehicle-urea-apply"
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<div ref="ureaApplyTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="pl-vehicle-urea-apply"
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
@header-dragend="() => ureaApplyStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
@@ -183,6 +185,15 @@
|
||||
align="center"
|
||||
/>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="ureaApplyStatsRowRef"
|
||||
storage-key="pl-vehicle-urea-apply-column-stats"
|
||||
:columns="ureaApplyStatsRowColumns"
|
||||
:data="tableData"
|
||||
:table-wrap-el="ureaApplyTableWrapRef"
|
||||
:get-cell-value="getUreaApplyStatCellValue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<div class="list-pager-selected">
|
||||
@@ -324,6 +335,8 @@
|
||||
<script setup lang="ts">
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import {
|
||||
@@ -390,6 +403,35 @@ const {
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('pl-vehicle-urea-apply-list-columns', listColumnDefs)
|
||||
|
||||
const ureaApplyTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const ureaApplyStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
const ureaApplyStatsRowColumns = computed((): StatsRowColumn[] => {
|
||||
const cols: StatsRowColumn[] = [
|
||||
{ key: '__selection', interactive: false },
|
||||
{ key: '__index', interactive: false },
|
||||
{ key: 'applyNo', interactive: true, prop: 'applyNo' }
|
||||
]
|
||||
for (const def of listColumnDefs) {
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop,
|
||||
money: def.prop === 'applyAmount'
|
||||
})
|
||||
}
|
||||
return cols
|
||||
})
|
||||
|
||||
function getUreaApplyStatCellValue(row: VehicleUreaApply, prop: string): unknown {
|
||||
if (prop === 'licensePlate') {
|
||||
return formatPlVehiclePlateDisplay(row.licensePlate, undefined, vehicleTrailerLookup.value)
|
||||
}
|
||||
if (prop === 'status') return getPlUreaApplyStatusLabel(row.status)
|
||||
return (row as any)?.[prop]
|
||||
}
|
||||
|
||||
const submitLoading = ref(false)
|
||||
const cancelLoadingMap = reactive<Record<string, boolean>>({})
|
||||
const tableData = ref<VehicleUreaApply[]>([])
|
||||
@@ -701,3 +743,9 @@ onMounted(async () => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -96,15 +96,17 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<ListTable table-key="pl-vehicle-urea-disburse"
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="applyId"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<div ref="ureaDisburseTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||||
<ListTable table-key="pl-vehicle-urea-disburse"
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="applyId"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
@header-dragend="() => ureaDisburseStatsRowRef?.resync?.()"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
@@ -229,6 +231,15 @@
|
||||
<template #default="{ row }">{{ row.handlerName || '—' }}</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
<ListColumnStatsRow
|
||||
ref="ureaDisburseStatsRowRef"
|
||||
storage-key="pl-vehicle-urea-disburse-column-stats"
|
||||
:columns="ureaDisburseStatsRowColumns"
|
||||
:data="tableData"
|
||||
:table-wrap-el="ureaDisburseTableWrapRef"
|
||||
:get-cell-value="getUreaDisburseStatCellValue"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<div class="list-pager-selected">
|
||||
@@ -374,6 +385,8 @@
|
||||
<script setup lang="ts">
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
|
||||
import type { StatsRowColumn } from '@/utils/list-column-stats'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { Check, Close, Delete, Edit, Money, Plus, RefreshLeft, Search } from '@element-plus/icons-vue'
|
||||
@@ -435,6 +448,39 @@ const {
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('pl-vehicle-urea-disburse-list-columns', listColumnDefs)
|
||||
|
||||
const ureaDisburseTableWrapRef = ref<HTMLElement | null>(null)
|
||||
const ureaDisburseStatsRowRef = ref<{ resync?: () => void } | null>(null)
|
||||
|
||||
const ureaDisburseStatsRowColumns = computed((): StatsRowColumn[] => {
|
||||
const cols: StatsRowColumn[] = [
|
||||
{ key: '__selection', interactive: false },
|
||||
{ key: '__index', interactive: false },
|
||||
{ key: 'applyNo', interactive: true, prop: 'applyNo' }
|
||||
]
|
||||
for (const def of listColumnDefs) {
|
||||
if (!isColumnVisible(def.prop)) continue
|
||||
cols.push({
|
||||
key: def.prop,
|
||||
interactive: true,
|
||||
prop: def.prop,
|
||||
money: def.prop === 'applyAmount' || def.prop === 'amount'
|
||||
})
|
||||
}
|
||||
return cols
|
||||
})
|
||||
|
||||
function getUreaDisburseStatCellValue(row: VehicleUreaDisburseBoardItem, prop: string): unknown {
|
||||
if (prop === 'applyStatus') return getPlUreaApplyStatusLabel(row.applyStatus)
|
||||
if (prop === 'licensePlate') {
|
||||
return formatPlVehiclePlateDisplay(row.licensePlate, undefined, vehicleTrailerLookup.value)
|
||||
}
|
||||
if (prop === 'disburseNo') return row.disburseNo || ''
|
||||
if (prop === 'disburseDate') return row.disburseDate || ''
|
||||
if (prop === 'amount') return row.amount != null ? Number(row.amount) : null
|
||||
if (prop === 'handlerName') return row.handlerName || ''
|
||||
return (row as any)?.[prop]
|
||||
}
|
||||
|
||||
const submitLoading = ref(false)
|
||||
const deleteLoadingMap = reactive<Record<string, boolean>>({})
|
||||
const selectedRows = ref<VehicleUreaDisburseBoardItem[]>([])
|
||||
@@ -793,6 +839,10 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bx-list-table-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.apply-summary-block {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 14px;
|
||||
|
||||
Reference in new issue
Block a user