This commit is contained in:
zlwu committed 2026-09-02 08:20:52 +08:00
1 parent 506cc7991a
commit 1dfcc66d2c
269 files changed
+1413333 -55990

No files matched your search

@@ -2,7 +2,6 @@
<template>
<el-dialog
v-model="visible"
:title="isPreviewMode ? '预览送(提)货单' : '编辑送(提)货单'"
width="1500px"
align-center
append-to-body
@@ -12,6 +11,24 @@
:close-on-click-modal="false"
@closed="handleClosed"
>
<template #header>
<div class="pl-slip-print-dialog-header">
<span class="pl-slip-print-dialog-title">{{ isPreviewMode ? '预览送(提)货单' : '编辑送(提)货单' }}</span>
<el-radio-group
v-model="selectedCompanyKey"
class="pl-slip-company-radio"
@change="handleCompanyChange"
>
<el-radio
v-for="item in PL_SLIP_COMPANY_OPTIONS"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</el-radio>
</el-radio-group>
</div>
</template>
<TinyMceEditor
v-model="slipContent"
:height="editorHeight"
@@ -76,6 +93,11 @@ import {
type DeliverySlipRenderContext
} from '@/utils/pl-delivery-slip-template'
import { buildBatchPrintPlanView, normalizePlanIds } from '@/utils/pl-batch-print'
import {
applyPlSlipCompanyToHtml,
PL_SLIP_COMPANY_OPTIONS,
type PlSlipCompanyKey
} from '@/utils/pl-slip-company'
import { injectSlipSealImages, stripSlipSealImages } from '@/utils/pl-slip-red-seal'
import { getPlSlipPdfCanvasScale } from '@/utils/pl-slip-pdf-render'
@@ -85,6 +107,7 @@ const isPreviewMode = ref(false)
const isBlankMode = ref(false)
/** 打印弹窗内红章显隐(仅本次弹窗有效,打开时默认显示) */
const showRedSeal = ref(true)
const selectedCompanyKey = ref<PlSlipCompanyKey>('shanxia')
const saveLoading = ref(false)
const pdfGenerating = ref(false)
const currentPlanId = ref('')
@@ -95,6 +118,10 @@ const editorHeight = computed(() => {
return Math.min(860, Math.max(720, window.innerHeight - 200))
})
function handleCompanyChange(key: PlSlipCompanyKey) {
slipContent.value = applyPlSlipCompanyToHtml(slipContent.value, key, { swapSeal: true })
}
async function waitForImagesLoaded(container: HTMLElement) {
const imgs = Array.from(container.querySelectorAll('img'))
await Promise.all(
@@ -150,6 +177,7 @@ function openBlank() {
isBlankMode.value = true
isPreviewMode.value = false
showRedSeal.value = false
selectedCompanyKey.value = 'shanxia'
slipContent.value = renderBlankDeliverySlipHtml()
visible.value = true
}
@@ -164,6 +192,7 @@ async function open(planIdOrIds: string | string[], options?: { customerId?: str
isBlankMode.value = false
isPreviewMode.value = false
showRedSeal.value = true
selectedCompanyKey.value = 'shanxia'
try {
ElMessage.info({
message: ids.length > 1 ? '正在生成合打送(提)货单...' : '正在生成送(提)货单...',
@@ -212,6 +241,7 @@ function handleClosed() {
isBlankMode.value = false
isPreviewMode.value = false
showRedSeal.value = true
selectedCompanyKey.value = 'shanxia'
}
/** 切换红章显示/隐藏(同步影响编辑器、打印、PDF) */
@@ -223,7 +253,7 @@ function toggleRedSeal() {
}
slipContent.value = injectSlipSealImages(
slipContent.value,
buildDeliverySlipCompanySealHtml(),
buildDeliverySlipCompanySealHtml(selectedCompanyKey.value),
'delivery-slip-seal-block'
)
showRedSeal.value = true
@@ -246,7 +276,9 @@ async function handleSaveTemplate() {
try {
const plan = (await refreshPlan()) || currentPlan.value
if (!plan) throw new Error('计划数据无效')
const finalTemplate = restoreDeliverySlipPlaceholders(slipContent.value, plan)
// 保存前归一为山峡抬头与山峡红章,避免把云栋切换态写入模板
const normalized = applyPlSlipCompanyToHtml(slipContent.value, 'shanxia', { swapSeal: true })
const finalTemplate = restoreDeliverySlipPlaceholders(normalized, plan)
await planApi.saveDeliverySlipTemplate(currentPlanId.value, finalTemplate)
ElMessage.success('模板保存成功')
} catch (error) {
@@ -305,6 +337,7 @@ async function handleGeneratePdf() {
if (plan) {
const renderCtx = await buildSlipRenderContext(plan)
htmlContent = replaceDeliverySlipPlaceholders(htmlContent, plan, renderCtx)
htmlContent = applyPlSlipCompanyToHtml(htmlContent, selectedCompanyKey.value, { swapSeal: true })
}
}
@@ -377,6 +410,38 @@ defineExpose({ open, openBlank })
</script>
<style scoped lang="scss">
.pl-slip-print-dialog-header {
position: relative;
display: flex;
align-items: center;
width: 100%;
min-height: 24px;
padding-right: 28px;
box-sizing: border-box;
}
.pl-slip-print-dialog-title {
position: relative;
z-index: 1;
flex-shrink: 0;
font-size: 18px;
font-weight: 600;
line-height: 24px;
color: var(--el-text-color-primary);
}
.pl-slip-company-radio {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
display: inline-flex;
flex-wrap: wrap;
gap: 8px 20px;
justify-content: center;
max-width: calc(100% - 280px);
}
.delivery-slip-print-actions {
display: flex;
flex-wrap: wrap;
@@ -2,7 +2,6 @@
<template>
<el-dialog
v-model="visible"
:title="isPreviewMode ? '预览入库单' : '编辑入库单'"
width="1500px"
align-center
append-to-body
@@ -12,6 +11,24 @@
:close-on-click-modal="false"
@closed="handleClosed"
>
<template #header>
<div class="pl-slip-print-dialog-header">
<span class="pl-slip-print-dialog-title">{{ isPreviewMode ? '预览入库单' : '编辑入库单' }}</span>
<el-radio-group
v-model="selectedCompanyKey"
class="pl-slip-company-radio"
@change="handleCompanyChange"
>
<el-radio
v-for="item in PL_SLIP_COMPANY_OPTIONS"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</el-radio>
</el-radio-group>
</div>
</template>
<TinyMceEditor
v-model="slipContent"
:height="editorHeight"
@@ -74,6 +91,11 @@ import {
type InboundSlipRenderContext
} from '@/utils/pl-inbound-slip-template'
import { buildBatchPrintPlanView, normalizePlanIds } from '@/utils/pl-batch-print'
import {
applyPlSlipCompanyToHtml,
PL_SLIP_COMPANY_OPTIONS,
type PlSlipCompanyKey
} from '@/utils/pl-slip-company'
import { injectSlipSealImages, stripSlipSealImages } from '@/utils/pl-slip-red-seal'
import { getPlSlipPdfCanvasScale } from '@/utils/pl-slip-pdf-render'
@@ -83,6 +105,7 @@ const isPreviewMode = ref(false)
const isBlankMode = ref(false)
/** 打印弹窗内红章显隐(仅本次弹窗有效,打开时默认显示) */
const showRedSeal = ref(true)
const selectedCompanyKey = ref<PlSlipCompanyKey>('shanxia')
const saveLoading = ref(false)
const pdfGenerating = ref(false)
const currentPlanId = ref('')
@@ -93,6 +116,10 @@ const editorHeight = computed(() => {
return Math.min(860, Math.max(720, window.innerHeight - 200))
})
function handleCompanyChange(key: PlSlipCompanyKey) {
slipContent.value = applyPlSlipCompanyToHtml(slipContent.value, key)
}
async function waitForImagesLoaded(container: HTMLElement) {
const imgs = Array.from(container.querySelectorAll('img'))
await Promise.all(
@@ -148,6 +175,7 @@ function openBlank() {
isBlankMode.value = true
isPreviewMode.value = false
showRedSeal.value = true
selectedCompanyKey.value = 'shanxia'
slipContent.value = renderBlankInboundSlipHtml()
visible.value = true
}
@@ -162,6 +190,7 @@ async function open(planIdOrIds: string | string[]) {
isBlankMode.value = false
isPreviewMode.value = false
showRedSeal.value = true
selectedCompanyKey.value = 'shanxia'
try {
ElMessage.info({ message: ids.length > 1 ? '正在生成合打入库单...' : '正在生成入库单...', duration: 800 })
const plans = await Promise.all(
@@ -202,6 +231,7 @@ function handleClosed() {
isBlankMode.value = false
isPreviewMode.value = false
showRedSeal.value = true
selectedCompanyKey.value = 'shanxia'
}
/** 切换红章显示/隐藏(同步影响编辑器、打印、PDF) */
@@ -236,7 +266,9 @@ async function handleSaveTemplate() {
try {
const plan = (await refreshPlan()) || currentPlan.value
if (!plan) throw new Error('计划数据无效')
const finalTemplate = restoreInboundSlipPlaceholders(slipContent.value, plan)
// 保存前归一为山峡抬头,避免把云栋切换态写入模板
const normalized = applyPlSlipCompanyToHtml(slipContent.value, 'shanxia')
const finalTemplate = restoreInboundSlipPlaceholders(normalized, plan)
await planApi.saveInboundSlipTemplate(currentPlanId.value, finalTemplate)
ElMessage.success('模板保存成功')
} catch (error) {
@@ -295,6 +327,7 @@ async function handleGeneratePdf() {
if (plan) {
const renderCtx = await buildSlipRenderContext(plan)
htmlContent = replaceInboundSlipPlaceholders(htmlContent, plan, renderCtx)
htmlContent = applyPlSlipCompanyToHtml(htmlContent, selectedCompanyKey.value)
}
}
@@ -367,6 +400,38 @@ defineExpose({ open, openBlank })
</script>
<style scoped lang="scss">
.pl-slip-print-dialog-header {
position: relative;
display: flex;
align-items: center;
width: 100%;
min-height: 24px;
padding-right: 28px;
box-sizing: border-box;
}
.pl-slip-print-dialog-title {
position: relative;
z-index: 1;
flex-shrink: 0;
font-size: 18px;
font-weight: 600;
line-height: 24px;
color: var(--el-text-color-primary);
}
.pl-slip-company-radio {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
display: inline-flex;
flex-wrap: wrap;
gap: 8px 20px;
justify-content: center;
max-width: calc(100% - 280px);
}
.inbound-slip-print-actions {
display: flex;
flex-wrap: wrap;
@@ -2,7 +2,6 @@
<template>
<el-dialog
v-model="visible"
:title="isPreviewMode ? '预览分装单' : '编辑分装单'"
width="1500px"
align-center
append-to-body
@@ -12,6 +11,24 @@
:close-on-click-modal="false"
@closed="handleClosed"
>
<template #header>
<div class="pl-slip-print-dialog-header">
<span class="pl-slip-print-dialog-title">{{ isPreviewMode ? '预览分装单' : '编辑分装单' }}</span>
<el-radio-group
v-model="selectedCompanyKey"
class="pl-slip-company-radio"
@change="handleCompanyChange"
>
<el-radio
v-for="item in PL_SLIP_COMPANY_OPTIONS"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</el-radio>
</el-radio-group>
</div>
</template>
<TinyMceEditor
v-model="slipContent"
:height="editorHeight"
@@ -77,6 +94,11 @@ import {
type SubpackSlipRenderContext
} from '@/utils/pl-subpack-slip-template'
import { buildBatchPrintPlanView, normalizePlanIds } from '@/utils/pl-batch-print'
import {
applyPlSlipCompanyToHtml,
PL_SLIP_COMPANY_OPTIONS,
type PlSlipCompanyKey
} from '@/utils/pl-slip-company'
import { injectSlipSealImages, stripSlipSealImages } from '@/utils/pl-slip-red-seal'
import { getPlSlipPdfCanvasScale } from '@/utils/pl-slip-pdf-render'
@@ -86,6 +108,7 @@ const isPreviewMode = ref(false)
const isBlankMode = ref(false)
/** 打印弹窗内红章显隐(仅本次弹窗有效,打开时默认显示) */
const showRedSeal = ref(true)
const selectedCompanyKey = ref<PlSlipCompanyKey>('shanxia')
const saveLoading = ref(false)
const pdfGenerating = ref(false)
const currentPlanId = ref('')
@@ -97,6 +120,10 @@ const editorHeight = computed(() => {
return Math.min(860, Math.max(720, window.innerHeight - 200))
})
function handleCompanyChange(key: PlSlipCompanyKey) {
slipContent.value = applyPlSlipCompanyToHtml(slipContent.value, key)
}
async function waitForImagesLoaded(container: HTMLElement) {
const imgs = Array.from(container.querySelectorAll('img'))
await Promise.all(
@@ -142,6 +169,7 @@ function openBlank() {
isBlankMode.value = true
isPreviewMode.value = false
showRedSeal.value = false
selectedCompanyKey.value = 'shanxia'
slipContent.value = renderBlankSubpackSlipHtml()
visible.value = true
}
@@ -156,6 +184,7 @@ async function open(planIdOrIds: string | string[]) {
isBlankMode.value = false
isPreviewMode.value = false
showRedSeal.value = true
selectedCompanyKey.value = 'shanxia'
try {
ElMessage.info({ message: ids.length > 1 ? '正在生成合打分装单...' : '正在生成分装单...', duration: 800 })
const plans = await Promise.all(
@@ -202,6 +231,7 @@ function handleClosed() {
isBlankMode.value = false
isPreviewMode.value = false
showRedSeal.value = true
selectedCompanyKey.value = 'shanxia'
}
/** 切换红章显示/隐藏(同步影响编辑器、打印、PDF) */
@@ -238,7 +268,9 @@ async function handleSaveTemplate() {
try {
const plan = (await refreshPlan()) || currentPlan.value
if (!plan) throw new Error('计划数据无效')
const finalTemplate = restoreSubpackSlipPlaceholders(slipContent.value, plan)
// 保存前归一为山峡抬头,避免把云栋切换态写入模板
const normalized = applyPlSlipCompanyToHtml(slipContent.value, 'shanxia')
const finalTemplate = restoreSubpackSlipPlaceholders(normalized, plan)
await planApi.saveSubpackSlipTemplate(currentPlanId.value, finalTemplate)
ElMessage.success('模板保存成功')
} catch (error) {
@@ -297,6 +329,7 @@ async function handleGeneratePdf() {
if (plan) {
const renderCtx = await buildSlipRenderContext(plan)
htmlContent = replaceSubpackSlipPlaceholders(htmlContent, plan, renderCtx)
htmlContent = applyPlSlipCompanyToHtml(htmlContent, selectedCompanyKey.value)
}
}
@@ -369,6 +402,38 @@ defineExpose({ open, openBlank })
</script>
<style scoped lang="scss">
.pl-slip-print-dialog-header {
position: relative;
display: flex;
align-items: center;
width: 100%;
min-height: 24px;
padding-right: 28px;
box-sizing: border-box;
}
.pl-slip-print-dialog-title {
position: relative;
z-index: 1;
flex-shrink: 0;
font-size: 18px;
font-weight: 600;
line-height: 24px;
color: var(--el-text-color-primary);
}
.pl-slip-company-radio {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
display: inline-flex;
flex-wrap: wrap;
gap: 8px 20px;
justify-content: center;
max-width: calc(100% - 280px);
}
.subpack-slip-print-actions {
display: flex;
flex-wrap: wrap;
@@ -1,6 +1,7 @@
import type { Plan, PlanProduct } from '@/api/pl'
import type { SlipPlanProduct } from '@/utils/pl-batch-print'
import { formatRemarkForSlipHtml } from '@/utils/pl-batch-print'
import { getPlSlipSealSrc, type PlSlipCompanyKey } from '@/utils/pl-slip-company'
import { applyPdfStableTableCellBorders } from '@/utils/pl-slip-pdf-render'
/** 送(提)货单打印默认公司信息(可在富文本中修改) */
@@ -597,9 +598,9 @@ export const DELIVERY_SLIP_PRINT_CSS = `
export const DELIVERY_SLIP_STYLE_BLOCK = `<style type="text/css">${DELIVERY_SLIP_PRINT_CSS}</style>`
/** 生成公司印章 HTML(地址/电话单元格右上角) */
export function buildDeliverySlipCompanySealHtml(): string {
return `<img class="delivery-slip-seal-img" src="${DELIVERY_SLIP_SEAL_IMAGE_SRC}" alt="印章" style="${DELIVERY_SLIP_SEAL_IMAGE_STYLE}" />`
/** 生成公司印章 HTML(地址/电话单元格右上角;云栋用合同同款 yd.png) */
export function buildDeliverySlipCompanySealHtml(companyKey: PlSlipCompanyKey = 'shanxia'): string {
return `<img class="delivery-slip-seal-img" src="${getPlSlipSealSrc(companyKey)}" alt="印章" style="${DELIVERY_SLIP_SEAL_IMAGE_STYLE}" />`
}
const SEAL_BLOCK = `<div class="delivery-slip-seal-block" style="position:relative;padding:0;${FONT_BODY}${SEAL_BLOCK_LINE_STYLE}">
+89
View File
@@ -0,0 +1,89 @@
/** 运输单据打印:抬头公司(山峡 / 云栋)切换 */
import ydSealImg from '@/assets/images/yd.png'
export const PL_SLIP_COMPANY_SHANXIA = '安庆市山峡化工有限公司'
export const PL_SLIP_COMPANY_YUNDONG = '安徽云栋化工有限公司'
export type PlSlipCompanyKey = 'shanxia' | 'yundong'
export const PL_SLIP_COMPANY_OPTIONS: { label: string; value: PlSlipCompanyKey }[] = [
{ label: PL_SLIP_COMPANY_SHANXIA, value: 'shanxia' },
{ label: PL_SLIP_COMPANY_YUNDONG, value: 'yundong' }
]
/** 山峡红章(与现有单据 public 路径一致) */
export const PL_SLIP_SHANXIA_SEAL_SRC = '/sxshzyz.png'
/** 云栋红章(与合同管理 yd.png 一致) */
export const PL_SLIP_YUNDONG_SEAL_SRC = ydSealImg as string
const TITLE_HEADING_SELECTOR =
'.inbound-slip-title-heading, .delivery-slip-title-heading, .subpack-slip-title-heading'
export function getPlSlipCompanyName(key: PlSlipCompanyKey): string {
return key === 'yundong' ? PL_SLIP_COMPANY_YUNDONG : PL_SLIP_COMPANY_SHANXIA
}
export function getPlSlipSealSrc(key: PlSlipCompanyKey): string {
return key === 'yundong' ? PL_SLIP_YUNDONG_SEAL_SRC : PL_SLIP_SHANXIA_SEAL_SRC
}
function isSlipLogoImg(img: HTMLImageElement): boolean {
const alt = (img.getAttribute('alt') || '').trim()
if (alt === '印章') return false
if (alt === 'logo') return true
const src = img.getAttribute('src') || ''
if (src.includes('watermask')) return false
return src.includes('logo.gif') || /\/logo(\.|$)/.test(src) || src.includes('/logo')
}
function isCompanyTitleText(text: string): boolean {
const normalized = text.replace(/\u00a0/g, ' ').replace(/\s+/g, '').trim()
if (!normalized) return false
if (normalized.includes('入库') || normalized.includes('货单') || normalized.includes('分装')) {
return false
}
return (
normalized === PL_SLIP_COMPANY_SHANXIA.replace(/\s+/g, '') ||
normalized === PL_SLIP_COMPANY_YUNDONG.replace(/\s+/g, '') ||
normalized.includes('山峡化工') ||
normalized.includes('云栋化工')
)
}
/**
* 按抬头公司更新单据 HTML:标题公司名、山峡 logo 显隐;可选替换红章图。
* 仅本次弹窗预览有效,不改后端默认模板生成逻辑。
*/
export function applyPlSlipCompanyToHtml(
html: string,
companyKey: PlSlipCompanyKey,
options?: { swapSeal?: boolean }
): string {
if (!html || typeof document === 'undefined') return html
const wrap = document.createElement('div')
wrap.innerHTML = html
const companyName = getPlSlipCompanyName(companyKey)
const isYundong = companyKey === 'yundong'
wrap.querySelectorAll('img').forEach((img) => {
if (!isSlipLogoImg(img as HTMLImageElement)) return
;(img as HTMLImageElement).style.display = isYundong ? 'none' : ''
})
wrap.querySelectorAll(TITLE_HEADING_SELECTOR).forEach((el) => {
const text = (el.textContent || '').trim()
if (!isCompanyTitleText(text)) return
el.textContent = companyName
})
if (options?.swapSeal) {
const sealSrc = getPlSlipSealSrc(companyKey)
wrap.querySelectorAll('img[alt="印章"]').forEach((img) => {
img.setAttribute('src', sealSrc)
})
}
return wrap.innerHTML
}
@@ -186,10 +186,11 @@
</ListTable>
</div>
<div ref="expenseItemPagerRef" class="pagination list-pager-bar">
<div ref="expenseItemPagerRef" class="pagination list-pager-bar">
<div class="list-pager-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
</div>
</div>
</section>
+11 -11
View File
@@ -397,6 +397,8 @@ import ListExportDropdown from '@/components/business/list-export-dropdown/index
import { ElMessage } from 'element-plus'
import { Search, RefreshLeft } from '@element-plus/icons-vue'
import { advanceApi, paymentApi, reimburseApi } from '@/api/bx'
import { pickBxPageResult } from '@/utils/http/bx'
import { fetchAllPagedRecords } from '@/utils/fetch-paged-list'
import {
bizTypeTagType,
statusLabel,
@@ -728,19 +730,17 @@ const selectedRows = ref<any[]>([])
async function loadData() {
loading.value = true
try {
const listParams = { page: 1, pageSize: 9999, ...buildApiQuery() }
const apiQuery = buildApiQuery()
const [advRes, reimRes] = await Promise.allSettled([
advanceApi.list(listParams),
reimburseApi.list(listParams)
fetchAllPagedRecords(async (page, pageSize) =>
pickBxPageResult(await advanceApi.list({ page, pageSize, ...apiQuery }))
),
fetchAllPagedRecords(async (page, pageSize) =>
pickBxPageResult(await reimburseApi.list({ page, pageSize, ...apiQuery }))
)
])
const advRows =
advRes.status === 'fulfilled'
? ((advRes.value as any).data?.rows || []).map(mapAdvanceRow)
: []
const reimRows =
reimRes.status === 'fulfilled'
? ((reimRes.value as any).data?.rows || []).map(mapReimburseRow)
: []
const advRows = advRes.status === 'fulfilled' ? advRes.value.map(mapAdvanceRow) : []
const reimRows = reimRes.status === 'fulfilled' ? reimRes.value.map(mapReimburseRow) : []
mergedRows.value = [...advRows, ...reimRows]
} catch {
// error handled by interceptor
+10 -7
View File
@@ -385,6 +385,8 @@ import ListExportDropdown from '@/components/business/list-export-dropdown/index
import { ElMessage } from 'element-plus'
import { Search, RefreshLeft } from '@element-plus/icons-vue'
import { advanceApi, paymentApi, reimburseApi } from '@/api/bx'
import { pickBxPageResult } from '@/utils/http/bx'
import { fetchAllPagedRecords } from '@/utils/fetch-paged-list'
import {
bizTypeTagType,
statusLabel,
@@ -678,17 +680,18 @@ const selectedRows = ref<any[]>([])
tableLoading.value = true
try {
const apiQuery = buildApiQuery()
const listParams = { page: 1, pageSize: 9999, ...apiQuery }
const [advRes, reimRes] = await Promise.allSettled([
advanceApi.my(listParams),
reimburseApi.my(listParams)
fetchAllPagedRecords(async (page, pageSize) =>
pickBxPageResult(await advanceApi.my({ page, pageSize, ...apiQuery }))
),
fetchAllPagedRecords(async (page, pageSize) =>
pickBxPageResult(await reimburseApi.my({ page, pageSize, ...apiQuery }))
)
])
const advRows =
advRes.status === 'fulfilled' ? (advRes.value.data?.rows || []).map(mapAdvanceMineRow) : []
advRes.status === 'fulfilled' ? advRes.value.map(mapAdvanceMineRow) : []
const reimRows =
reimRes.status === 'fulfilled'
? (reimRes.value.data?.rows || []).map(mapReimburseMineRow)
: []
reimRes.status === 'fulfilled' ? reimRes.value.map(mapReimburseMineRow) : []
mergedRows.value = sortMergedRows([...advRows, ...reimRows])
} catch {
// error handled by interceptor
+11 -7
View File
@@ -463,6 +463,8 @@ import ListExportDropdown from '@/components/business/list-export-dropdown/index
import { ElMessage } from 'element-plus'
import { Check, Close, Search, RefreshLeft } from '@element-plus/icons-vue'
import { advanceApi, paymentApi, reimburseApi } from '@/api/bx'
import { pickBxPageResult } from '@/utils/http/bx'
import { fetchAllPagedRecords } from '@/utils/fetch-paged-list'
import {
bizTypeTagType,
formatMoneyYuan,
@@ -729,17 +731,19 @@ import ListExportDropdown from '@/components/business/list-export-dropdown/index
if (options.buttonLoading) searchLoading.value = true
tableLoading.value = true
try {
const listParams = { page: 1, pageSize: 9999, ...buildApiQuery() }
const apiQuery = buildApiQuery()
const [advRes, reimRes] = await Promise.allSettled([
advanceApi.pendingApproval(listParams),
reimburseApi.pendingApproval(listParams)
fetchAllPagedRecords(async (page, pageSize) =>
pickBxPageResult(await advanceApi.pendingApproval({ page, pageSize, ...apiQuery }))
),
fetchAllPagedRecords(async (page, pageSize) =>
pickBxPageResult(await reimburseApi.pendingApproval({ page, pageSize, ...apiQuery }))
)
])
const advRows =
advRes.status === 'fulfilled' ? (advRes.value.data?.rows || []).map(mapAdvanceTodoRow) : []
advRes.status === 'fulfilled' ? advRes.value.map(mapAdvanceTodoRow) : []
const reimRows =
reimRes.status === 'fulfilled'
? (reimRes.value.data?.rows || []).map(mapReimburseTodoRow)
: []
reimRes.status === 'fulfilled' ? reimRes.value.map(mapReimburseTodoRow) : []
mergedRows.value = [...advRows, ...reimRows]
} catch {
// error handled by interceptor
@@ -119,10 +119,11 @@
</ListTable>
</div>
<div ref="wfAdvancePagerRef" class="pagination list-pager-bar">
<div ref="wfAdvancePagerRef" class="pagination list-pager-bar">
<div class="list-pager-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
</div>
</div>
</section>
@@ -124,6 +124,7 @@
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
</div>
</div>
</section>
<el-dialog
@@ -1345,7 +1345,7 @@ const handleDetailCopy = () => {
const handleDetailEdit = async () => {
if (!detailContract.value?.id) return
if (!isContractEditableStatus(detailContract.value.status)) {
$modal.msgWarning('仅草稿、审批中或已审批(未执行)的合同才能修改')
$modal.msgWarning('已完成的合同不能修改')
return
}
const confirmMsg = getContractReeditConfirmMessage(detailContract.value.status)
@@ -2042,18 +2042,19 @@ const replacePlaceholders = (htmlContent: string, contractData: ContractVO | und
}
// 合计区(12 列):第 1 行左右对半运费/桶押金;第 2 行合计;第 3 行提示
// 合计区(12 列):运费/桶押金 → 不含税/税额 → 含税金额合计 → 提示
const freightRaw = Number(contractData.freight);
const freightAmount = Number.isFinite(freightRaw) && freightRaw > 0 ? freightRaw : 0;
const depositRaw = Number(contractData.barrelDeposit);
const barrelDepositAmount = Number.isFinite(depositRaw) && depositRaw > 0 ? depositRaw : 0;
const totalWithExtras = productTotal + freightAmount + barrelDepositAmount;
const totalWithExtrasChinese = numberToChinese(totalWithExtras);
const freightText = freightAmount > 0 ? `¥${formatNumber(freightAmount)}` : '-';
const barrelDepositText = barrelDepositAmount > 0 ? `¥${formatNumber(barrelDepositAmount)}` : '-';
const totalWithExtrasText = isFrameworkContract
? '随行就市'
: `${totalWithExtrasChinese}(¥${formatNumber(totalWithExtras)})`;
const formatRmbAmountText = (amount: number) =>
isFrameworkContract ? '随行就市' : `${numberToChinese(amount)}(¥${formatNumber(amount)})`;
const beforeTaxTotalText = formatRmbAmountText(productTotalBeforeTax);
const taxTotalText = formatRmbAmountText(totalTax);
const totalWithExtrasText = formatRmbAmountText(totalWithExtras);
const tipRowText = hasSelfPickupProduct(contractData.products)
? '含税、客户自提结算以实际到货吨位为准'
: '含税、含运费、装卸费(一票制)&nbsp;&nbsp;&nbsp;&nbsp;结算以实际到货吨位为准';
@@ -2065,7 +2066,13 @@ const replacePlaceholders = (htmlContent: string, contractData: ContractVO | und
<td colspan="4" style="padding: 5px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${barrelDepositText}</td>
</tr>
<tr style="background-color: #fff; font-weight: bold;">
<td colspan="2" style="padding: 5px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">合计(人民币)</td>
<td colspan="2" style="padding: 5px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">不含税金额合计(人民币)</td>
<td colspan="4" style="padding: 5px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${beforeTaxTotalText}</td>
<td colspan="2" style="padding: 5px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">税额合计(人民币)</td>
<td colspan="4" style="padding: 5px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${taxTotalText}</td>
</tr>
<tr style="background-color: #fff; font-weight: bold;">
<td colspan="2" style="padding: 5px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">含税金额合计(人民币)</td>
<td colspan="10" style="padding: 5px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${totalWithExtrasText}</td>
</tr>
<tr>
@@ -2074,7 +2081,16 @@ const replacePlaceholders = (htmlContent: string, contractData: ContractVO | und
</td>
</tr>
`;
result = result.replace(/\{\{productTotalRow\}\}/gi, productTotalRow);
// 优先替换占位符;否则替换已展开的「运费~提示」合计块(兼容旧模板,打印/PDF 共用)
if (/\{\{productTotalRow\}\}/i.test(result)) {
result = result.replace(/\{\{productTotalRow\}\}/gi, productTotalRow);
} else {
const summaryBlockPattern =
/(?:<tr[^>]*font-weight:\s*bold[^>]*>[\s\S]*?<\/tr>\s*){1,3}<tr>[\s\S]*?结算以实际到货吨位为准[\s\S]*?<\/tr>/i;
if (summaryBlockPattern.test(result)) {
result = result.replace(summaryBlockPattern, productTotalRow.trim());
}
}
// 特殊处理:印章图片
// 从合同编号中提取供方编码
@@ -2421,7 +2437,7 @@ const generateContractTemplate = (contractData: ContractVO | undefined): string
</thead>
<tbody>
{{productTable}}
<!-- 合计行+提示行(由 {{productTotalRow}} 一并输出,此处不再重复) -->
<!-- 运费/桶押金 + 不含税/税额 + 含税合计 + 提示(由 {{productTotalRow}} 一并输出) -->
{{productTotalRow}}
</tbody>
</table>
@@ -2562,9 +2578,9 @@ const handleSaveTemplate = async () => {
const productTableMatch = templateWithPlaceholders.match(/<tbody>[\s\S]*?<\/tbody>/);
const productTableHtml = productTableMatch ? productTableMatch[0] : '';
// 2. 提取运费/桶押金行 + 合计行(连续两行加粗)
// 2. 提取运费/桶押金 + 不含税/税额 + 含税合计(连续 2~3 行加粗,兼容旧版)
const totalRowMatch = templateWithPlaceholders.match(
/(?:<tr style="background-color: #fff; font-weight: bold;">[\s\S]*?<\/tr>\s*){2}/
/(?:<tr style="background-color: #fff; font-weight: bold;">[\s\S]*?<\/tr>\s*){2,3}/
);
const totalRowHtml = totalRowMatch ? totalRowMatch[0] : '';
@@ -2583,10 +2599,10 @@ const handleSaveTemplate = async () => {
);
}
// 替换运费/桶押金行 + 合计行(兼容旧版单行合计)
// 替换运费/桶押金行 + 合计相关行(兼容旧版 1~2 行、新版 3 行)
if (totalRowHtml) {
const totalRowPattern =
/(?:<tr style="background-color: #fff; font-weight: bold;">[\s\S]*?<\/tr>\s*){1,2}/;
/(?:<tr style="background-color: #fff; font-weight: bold;">[\s\S]*?<\/tr>\s*){1,3}/;
if (totalRowPattern.test(finalTemplate)) {
finalTemplate = finalTemplate.replace(totalRowPattern, totalRowHtml);
}
@@ -2956,13 +2972,12 @@ const handleGeneratePdfFromDialog = async () => {
// 判断是否为SX供方编码
const isSXSupplier = supplierCode === 'SX';
// 获取合同内容并替换占位符(使用最新的合同数据)
// 获取合同内容并用最新数据刷新合计区(打印弹窗与 PDF 同源)
let htmlContent = contractContent.value;
// 如果内容中包含占位符,需要替换
if (htmlContent.includes('{{') || htmlContent.includes('SETTLEMENT_METHOD_CONTENT')) {
// 重新获取最新合同数据来替换占位符
const res = await getContractDetail(detailContract.value.id);
const res = await getContractDetail(detailContract.value.id);
if (res?.data) {
htmlContent = replacePlaceholders(htmlContent, res.data);
contractContent.value = htmlContent;
}
// 1. 创建临时容器(border-box:794 含 padding)
@@ -20,24 +20,35 @@ export function isPurchaseLikeContractType(type?: string | null): boolean {
return PURCHASE_LIKE_CONTRACT_TYPES.includes((type || '').trim() as PurchaseLikeContractType)
}
/** 允许打开编辑的合同状态(保存后回草稿) */
export const CONTRACT_EDITABLE_STATUSES = ['draft', 'approving', 'approved'] as const
/** 禁止再编辑的合同状态:仅已完成 */
export const CONTRACT_NON_EDITABLE_STATUSES = ['finished'] as const
export type ContractNonEditableStatus = (typeof CONTRACT_NON_EDITABLE_STATUSES)[number]
/** 允许打开编辑的合同状态(保存后回草稿);除已完成外均可 */
export const CONTRACT_EDITABLE_STATUSES = [
'draft',
'approving',
'approved',
'executing',
'invalid'
] as const
export type ContractEditableStatus = (typeof CONTRACT_EDITABLE_STATUSES)[number]
export function isContractEditableStatus(status?: string | null): boolean {
return CONTRACT_EDITABLE_STATUSES.includes(
(status || '').trim() as ContractEditableStatus
)
const s = (status || '').trim()
if (!s) return true
return !CONTRACT_NON_EDITABLE_STATUSES.includes(s as ContractNonEditableStatus)
}
/** 审批中 / 已审批打开编辑前的确认文案;草稿返回 null(无需确认) */
/** 非草稿打开编辑前的确认文案;草稿返回 null(无需确认) */
export function getContractReeditConfirmMessage(status?: string | null): string | null {
const s = (status || '').trim()
if (s === 'approving') {
return '编辑保存后将作废当前审批流,合同变回草稿,需重新提交审批'
}
if (s === 'approved') {
if (s === 'approved' || s === 'executing' || s === 'invalid') {
return '编辑保存后合同将变回草稿,需重新提交审批'
}
return null
+45 -14
View File
@@ -1418,7 +1418,7 @@ const goBack = () => {
/** 编辑 */
const handleEdit = async () => {
if (!isContractEditableStatus(contract.value?.status)) {
$modal.msgWarning('仅草稿、审批中或已审批(未执行)的合同才能修改')
$modal.msgWarning('已完成的合同不能修改')
return
}
const confirmMsg = getContractReeditConfirmMessage(contract.value?.status)
@@ -1741,18 +1741,19 @@ const replacePlaceholders = (htmlContent: string, contractData: ContractVO | und
)
}
// 合计区(12 列):第 1 行左右对半运费/桶押金;第 2 行合计;第 3 行提示
// 合计区(12 列):运费/桶押金 → 不含税/税额 → 含税金额合计 → 提示
const freightRaw = Number(contractData.freight);
const freightAmount = Number.isFinite(freightRaw) && freightRaw > 0 ? freightRaw : 0;
const depositRaw = Number(contractData.barrelDeposit);
const barrelDepositAmount = Number.isFinite(depositRaw) && depositRaw > 0 ? depositRaw : 0;
const totalWithExtras = productTotal + freightAmount + barrelDepositAmount;
const totalWithExtrasChinese = numberToChinese(totalWithExtras);
const freightText = freightAmount > 0 ? `¥${formatNumber(freightAmount)}` : '-';
const barrelDepositText = barrelDepositAmount > 0 ? `¥${formatNumber(barrelDepositAmount)}` : '-';
const totalWithExtrasText = isFrameworkContract
? '随行就市'
: `${totalWithExtrasChinese}(¥${formatNumber(totalWithExtras)})`;
const formatRmbAmountText = (amount: number) =>
isFrameworkContract ? '随行就市' : `${numberToChinese(amount)}(¥${formatNumber(amount)})`;
const beforeTaxTotalText = formatRmbAmountText(productTotalBeforeTax);
const taxTotalText = formatRmbAmountText(totalTax);
const totalWithExtrasText = formatRmbAmountText(totalWithExtras);
const tipRowText = hasSelfPickupProduct(contractData.products)
? '含税、客户自提结算以实际到货吨位为准'
: '含税、含运费、装卸费(一票制)&nbsp;&nbsp;&nbsp;&nbsp;结算以实际到货吨位为准';
@@ -1764,7 +1765,13 @@ const replacePlaceholders = (htmlContent: string, contractData: ContractVO | und
<td colspan="4" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${barrelDepositText}</td>
</tr>
<tr style="background-color: #fff; font-weight: bold;">
<td colspan="2" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">合计(人民币)</td>
<td colspan="2" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">不含税金额合计(人民币)</td>
<td colspan="4" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${beforeTaxTotalText}</td>
<td colspan="2" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">税额合计(人民币)</td>
<td colspan="4" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${taxTotalText}</td>
</tr>
<tr style="background-color: #fff; font-weight: bold;">
<td colspan="2" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">含税金额合计(人民币)</td>
<td colspan="10" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${totalWithExtrasText}</td>
</tr>
<tr>
@@ -1773,7 +1780,16 @@ const replacePlaceholders = (htmlContent: string, contractData: ContractVO | und
</td>
</tr>
`;
result = result.replace(/\{\{productTotalRow\}\}/gi, productTotalRow);
// 优先替换占位符;否则替换已展开的「运费~提示」合计块(兼容旧模板,打印/PDF 共用)
if (/\{\{productTotalRow\}\}/i.test(result)) {
result = result.replace(/\{\{productTotalRow\}\}/gi, productTotalRow);
} else {
const summaryBlockPattern =
/(?:<tr[^>]*font-weight:\s*bold[^>]*>[\s\S]*?<\/tr>\s*){1,3}<tr>[\s\S]*?结算以实际到货吨位为准[\s\S]*?<\/tr>/i;
if (summaryBlockPattern.test(result)) {
result = result.replace(summaryBlockPattern, productTotalRow.trim());
}
}
return result;
};
@@ -1989,13 +2005,14 @@ const generateContractTemplate = (contractData: ContractVO | undefined): string
const depositRaw = Number(contractData.barrelDeposit);
const barrelDepositAmount = Number.isFinite(depositRaw) && depositRaw > 0 ? depositRaw : 0;
const totalWithExtras = productTotal + freightAmount + barrelDepositAmount;
const totalWithExtrasChinese = numberToChinese(totalWithExtras);
const freightText = freightAmount > 0 ? `¥${formatNumber(freightAmount)}` : '-';
const barrelDepositText = barrelDepositAmount > 0 ? `¥${formatNumber(barrelDepositAmount)}` : '-';
const isFrameworkContract = isFrameworkContractType(contractData.contractType);
const totalWithExtrasText = isFrameworkContract
? '随行就市'
: `${totalWithExtrasChinese}(¥${formatNumber(totalWithExtras)})`;
const formatRmbAmountText = (amount: number) =>
isFrameworkContract ? '随行就市' : `${numberToChinese(amount)}(¥${formatNumber(amount)})`;
const beforeTaxTotalText = formatRmbAmountText(productTotalBeforeTax);
const taxTotalText = formatRmbAmountText(totalTax);
const totalWithExtrasText = formatRmbAmountText(totalWithExtras);
// 构建产品明细表格行
const productRows =
@@ -2078,7 +2095,7 @@ const generateContractTemplate = (contractData: ContractVO | undefined): string
</thead>
<tbody>
${productRows}
<!-- 运费/桶押金一行(11 列对半),合计单独一行 -->
<!-- 运费/桶押金 → 不含税/税额 → 含税金额合计 → 提示 -->
<tr style="background-color: #fff; font-weight: bold;">
<td colspan="2" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">运费(元)</td>
<td colspan="4" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${freightText}</td>
@@ -2086,7 +2103,13 @@ const generateContractTemplate = (contractData: ContractVO | undefined): string
<td colspan="3" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${barrelDepositText}</td>
</tr>
<tr style="background-color: #fff; font-weight: bold;">
<td colspan="2" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">合计(人民币)</td>
<td colspan="2" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">不含税金额合计(人民币)</td>
<td colspan="4" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${beforeTaxTotalText}</td>
<td colspan="2" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">税额合计(人民币)</td>
<td colspan="3" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${taxTotalText}</td>
</tr>
<tr style="background-color: #fff; font-weight: bold;">
<td colspan="2" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">含税金额合计(人民币)</td>
<td colspan="9" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${totalWithExtrasText}</td>
</tr>
<!-- 提示行 -->
@@ -2816,6 +2839,14 @@ const handleGeneratePdfFile = async () => {
pdfGenerating.value = true;
$modal.loading('正在生成PDF,请稍候...');
// 用最新合同数据刷新合计区(与打印弹窗同源)
if (contract.value?.id) {
const res = await getContract(contract.value.id);
if (res?.data) {
contractContent.value = replacePlaceholders(contractContent.value, res.data);
}
}
// 创建一个临时div来渲染HTML内容
const tempDiv = document.createElement('div');
tempDiv.style.position = 'absolute';
@@ -1159,6 +1159,8 @@
onMounted(() => {
loadData()
// keepAlive:false 时不会触发 onActivated,须在挂载时处理计划跳转 query
openAddFromPlanQuery()
nextTick(() => recalcTableHeight())
})
@@ -1202,6 +1202,8 @@
onMounted(() => {
loadData()
// keepAlive:false 时不会触发 onActivated,须在挂载时处理出入库跳转 query
openAddFromInventoryQuery()
nextTick(() => recalcTableHeight())
})
+18 -18
View File
@@ -91,19 +91,19 @@
<div ref="sysDeptTableWrapRef" class="table-scroll bx-list-table-wrap">
<ListTable
ref="sysDeptTableRef"
:height="tableHeight" table-key="system-dept"
ref="tableRef"
:key="listTableKey"
class="list-table"
:data="tableData"
v-loading="loading"
stripe
row-key="id"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
:default-expand-all="isExpandAll"
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
>
table-key="system-dept"
:key="listTableKey"
class="list-table"
:height="tableHeight"
:data="tableData"
v-loading="loading"
stripe
row-key="id"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
:default-expand-all="isExpandAll"
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
>
<el-table-column type="selection" width="55" align="center" header-align="center" />
<el-table-column
type="index"
@@ -171,9 +171,10 @@
</div>
<div ref="sysDeptBelowRef">
<div class="pagination list-pager-bar">
<div class="list-pager-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<div class="list-pager-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
</div>
</div>
</div>
@@ -401,7 +402,6 @@ const dictStore = useDictStore()
const selectedRows = ref<Api.Dept.DeptItem[]>([])
const loading = ref(false)
const isExpandAll = ref(true)
const tableRef = ref()
const dialogVisible = ref(false)
const dialogTitle = ref('新增组织')
@@ -643,7 +643,7 @@ const dictStore = useDictStore()
/** 树表格的 default-expand-all 只在初次挂载生效,展开态需用表格 API 同步 */
function applyTreeExpand(expand: boolean) {
const table = tableRef.value
const table = sysDeptTableRef.value
if (!table?.toggleRowExpansion) return
const preorder = (items: Api.Dept.DeptItem[]) => {
for (const row of items) {
+17 -17
View File
@@ -90,18 +90,18 @@
<div ref="sysMenuTableWrapRef" class="table-scroll bx-list-table-wrap">
<ListTable
ref="sysMenuTableRef"
:height="tableHeight" table-key="system-menu"
ref="tableRef"
class="list-table"
:data="tableData"
v-loading="loading"
stripe
row-key="id"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
:default-expand-all="isExpandAll"
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
>
table-key="system-menu"
class="list-table"
:height="tableHeight"
:data="tableData"
v-loading="loading"
stripe
row-key="id"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
:default-expand-all="isExpandAll"
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
>
<el-table-column type="selection" width="55" align="center" header-align="center" />
<el-table-column
type="index"
@@ -205,9 +205,10 @@
</div>
<div ref="sysMenuBelowRef">
<div class="pagination list-pager-bar">
<div class="list-pager-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<div class="list-pager-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
</div>
</div>
</div>
@@ -385,7 +386,6 @@ import { useListTableViewportHeight } from '@/composables/useListTableViewportHe
const selectedRows = ref<Api.Menu.MenuItem[]>([])
const loading = ref(false)
const isExpandAll = ref(true)
const tableRef = ref()
function getMenuExportFilterParams() {
const quickKeyword = quickSearchKeyword.value?.trim()
@@ -494,7 +494,7 @@ import { useListTableViewportHeight } from '@/composables/useListTableViewportHe
/** 树表格的 default-expand-all 只在初次挂载生效,展开态需用表格 API 同步 */
function applyTreeExpand(expand: boolean) {
const table = tableRef.value
const table = sysMenuTableRef.value
if (!table?.toggleRowExpansion) return
const preorder = (items: Api.Menu.MenuItem[]) => {
for (const row of items) {