feat(web-admin): add mobile print preview for transport plan slips.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
bc7a1c225a
commit
866a87f747
2 files changed
+266
-9
No files matched your search
@@ -0,0 +1,127 @@
|
||||
import { fetchProductDetail } from '@/api/crm/product'
|
||||
import { planApi, type Plan } from '@/api/pl'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { applyDeliveryCustomerToPlan } from '@/utils/pl-delivery-slip-print'
|
||||
import {
|
||||
generateDeliverySlipTemplate,
|
||||
renderBlankDeliverySlipHtml,
|
||||
replaceDeliverySlipPlaceholders
|
||||
} from '@/utils/pl-delivery-slip-template'
|
||||
import {
|
||||
generateInboundSlipTemplate,
|
||||
renderBlankInboundSlipHtml,
|
||||
replaceInboundSlipPlaceholders
|
||||
} from '@/utils/pl-inbound-slip-template'
|
||||
import {
|
||||
formatSubpackSlipDateIso,
|
||||
generateSubpackSlipTemplate,
|
||||
renderBlankSubpackSlipHtml,
|
||||
replaceSubpackSlipPlaceholders
|
||||
} from '@/utils/pl-subpack-slip-template'
|
||||
|
||||
export type MpPlanPrintKind =
|
||||
| 'inbound'
|
||||
| 'delivery'
|
||||
| 'subpack'
|
||||
| 'blank-inbound'
|
||||
| 'blank-delivery'
|
||||
| 'blank-subpack'
|
||||
|
||||
async function loadProductCodeContext(plan: Plan) {
|
||||
const dictStore = useDictStore()
|
||||
await dictStore.loadAllDict()
|
||||
const productIds = [
|
||||
...new Set(
|
||||
(plan.products || []).map((item) => String(item.productId || '').trim()).filter(Boolean)
|
||||
)
|
||||
]
|
||||
const productCodeById: Record<string, string> = {}
|
||||
await Promise.all(
|
||||
productIds.map(async (productId) => {
|
||||
try {
|
||||
const res = await fetchProductDetail({ id: productId })
|
||||
const data = (res as { data?: { productNo?: string }; productNo?: string }).data ?? res
|
||||
const productNo = String((data as { productNo?: string }).productNo || '').trim()
|
||||
if (productNo) productCodeById[productId] = productNo
|
||||
} catch {
|
||||
// 单个产品查询失败时保留计划明细中的冗余字段
|
||||
}
|
||||
})
|
||||
)
|
||||
return {
|
||||
productCodeById,
|
||||
getUnitLabel: (unitCode: string) => dictStore.getLabel('crm_product_unit', unitCode) || unitCode
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSavedTemplate(
|
||||
loader: (id: string) => Promise<unknown>,
|
||||
id: string,
|
||||
fallback: () => string
|
||||
) {
|
||||
try {
|
||||
const templateRes = await loader(id)
|
||||
const saved = (templateRes as { data?: string }).data ?? templateRes
|
||||
if (typeof saved === 'string' && saved.trim()) return saved
|
||||
} catch {
|
||||
// 无模板时用默认
|
||||
}
|
||||
return fallback()
|
||||
}
|
||||
|
||||
export async function buildMpPlanPrintHtml(
|
||||
kind: MpPlanPrintKind,
|
||||
plan: Plan,
|
||||
options?: { customerId?: string }
|
||||
): Promise<string> {
|
||||
if (kind === 'blank-inbound') return renderBlankInboundSlipHtml()
|
||||
if (kind === 'blank-delivery') return renderBlankDeliverySlipHtml()
|
||||
if (kind === 'blank-subpack') return renderBlankSubpackSlipHtml()
|
||||
|
||||
const id = String(plan.id || '').trim()
|
||||
if (!id) throw new Error('无法获取计划ID')
|
||||
|
||||
if (kind === 'inbound') {
|
||||
const htmlContent = await loadSavedTemplate(
|
||||
(planId) => planApi.getInboundSlipTemplate(planId),
|
||||
id,
|
||||
() => generateInboundSlipTemplate(plan)
|
||||
)
|
||||
const ctx = await loadProductCodeContext(plan)
|
||||
return replaceInboundSlipPlaceholders(htmlContent, plan, ctx)
|
||||
}
|
||||
|
||||
if (kind === 'delivery') {
|
||||
let view = plan
|
||||
const customerId = String(options?.customerId || '').trim()
|
||||
if (customerId) view = applyDeliveryCustomerToPlan(plan, customerId)
|
||||
const htmlContent = await loadSavedTemplate(
|
||||
(planId) => planApi.getDeliverySlipTemplate(planId),
|
||||
id,
|
||||
() => generateDeliverySlipTemplate(view)
|
||||
)
|
||||
const ctx = await loadProductCodeContext(view)
|
||||
return replaceDeliverySlipPlaceholders(htmlContent, view, ctx)
|
||||
}
|
||||
|
||||
const slipNoRes = await planApi.ensureSubpackSlipNo(id)
|
||||
const subpackSlipNo = String((slipNoRes as { data?: string }).data ?? slipNoRes ?? '').trim()
|
||||
const htmlContent = await loadSavedTemplate(
|
||||
(planId) => planApi.getSubpackSlipTemplate(planId),
|
||||
id,
|
||||
() => generateSubpackSlipTemplate(plan)
|
||||
)
|
||||
const dictStore = useDictStore()
|
||||
const userStore = useUserStore()
|
||||
await dictStore.loadAllDict()
|
||||
const userInfo = userStore.getUserInfo as { realName?: string; userName?: string } | null
|
||||
const loginUserName = String(userInfo?.realName || userInfo?.userName || '').trim()
|
||||
return replaceSubpackSlipPlaceholders(htmlContent, plan, {
|
||||
getUnitLabel: (unitCode: string) => dictStore.getLabel('crm_product_unit', unitCode) || unitCode,
|
||||
subpackSlipNo,
|
||||
orderDate: formatSubpackSlipDateIso(),
|
||||
orderUser: loginUserName,
|
||||
preparerUser: loginUserName
|
||||
})
|
||||
}
|
||||
@@ -1,18 +1,148 @@
|
||||
<template>
|
||||
<MpWebviewShell>
|
||||
<p class="mp-wv-placeholder">打印预览加载中…</p>
|
||||
<div v-if="forbidden" class="mp-wv-empty">无权限</div>
|
||||
<div v-else-if="errorText" class="mp-wv-empty">{{ errorText }}</div>
|
||||
<template v-else>
|
||||
<header class="mp-wv-header">
|
||||
<div class="mp-wv-header__row">
|
||||
<button class="mp-wv-header__back" type="button" @click="goBack">← 详情</button>
|
||||
</div>
|
||||
<h1>打印单据</h1>
|
||||
</header>
|
||||
|
||||
<p class="mp-print-hint">长按预览区可保存图片;WebView 不支持系统打印。</p>
|
||||
|
||||
<div class="mp-wv-chips">
|
||||
<button
|
||||
v-for="item in kindOptions"
|
||||
:key="item.value"
|
||||
type="button"
|
||||
class="mp-wv-chip"
|
||||
:class="{ 'is-active': kind === item.value }"
|
||||
@click="selectKind(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="kind === 'delivery' && customers.length > 1" class="mp-wv-toolbar">
|
||||
<el-select
|
||||
v-model="customerId"
|
||||
placeholder="选择送货客户"
|
||||
style="width: 100%"
|
||||
@change="reloadPreview"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in customers"
|
||||
:key="item.customerId"
|
||||
:label="item.customerName"
|
||||
:value="item.customerId"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="mp-wv-empty">正在生成单据…</div>
|
||||
<div v-else-if="blockReason" class="mp-wv-empty">{{ blockReason }}</div>
|
||||
<div v-else class="mp-print-preview" v-html="html"></div>
|
||||
</template>
|
||||
</MpWebviewShell>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import MpWebviewShell from '@/layouts/MpWebviewShell.vue'
|
||||
</script>
|
||||
import { planApi, type Plan } from '@/api/pl'
|
||||
import { canBatchPrintSlip } from '@/utils/pl-batch-print'
|
||||
import { listDeliveryPrintCustomers } from '@/utils/pl-delivery-slip-print'
|
||||
import { buildMpPlanPrintHtml, type MpPlanPrintKind } from '@/utils/mp-pl-plan-print'
|
||||
import { HttpError } from '@/utils/http/error'
|
||||
import { ApiStatus } from '@/utils/http/status'
|
||||
import '@/styles/mp-webview.scss'
|
||||
|
||||
<style scoped>
|
||||
.mp-wv-placeholder {
|
||||
margin: 24px 0 0;
|
||||
font-size: 14px;
|
||||
color: #6b7b74;
|
||||
text-align: center;
|
||||
const kindOptions: { label: string; value: MpPlanPrintKind }[] = [
|
||||
{ label: '入库单', value: 'inbound' },
|
||||
{ label: '送(提)货单', value: 'delivery' },
|
||||
{ label: '分装单', value: 'subpack' },
|
||||
{ label: '空白入库单', value: 'blank-inbound' },
|
||||
{ label: '空白送(提)货单', value: 'blank-delivery' },
|
||||
{ label: '空白分装单', value: 'blank-subpack' }
|
||||
]
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const plan = ref<Plan | null>(null)
|
||||
const kind = ref<MpPlanPrintKind>('inbound')
|
||||
const html = ref('')
|
||||
const loading = ref(false)
|
||||
const forbidden = ref(false)
|
||||
const errorText = ref('')
|
||||
const blockReason = ref('')
|
||||
const customerId = ref('')
|
||||
const customers = ref<{ customerId: string; customerName: string }[]>([])
|
||||
|
||||
function goBack() {
|
||||
if (window.history.length > 1) {
|
||||
router.back()
|
||||
return
|
||||
}
|
||||
const id = String(route.params.id || '')
|
||||
void router.replace(id ? `/m/pl/plan/${id}` : '/m/pl/plan')
|
||||
}
|
||||
</style>
|
||||
|
||||
function slipCommand(current: MpPlanPrintKind) {
|
||||
if (current === 'inbound' || current === 'delivery' || current === 'subpack') return current
|
||||
return null
|
||||
}
|
||||
|
||||
async function reloadPreview() {
|
||||
const current = kind.value
|
||||
const command = slipCommand(current)
|
||||
if (command && plan.value) {
|
||||
const check = canBatchPrintSlip([plan.value], command)
|
||||
if (!check.ok) {
|
||||
html.value = ''
|
||||
blockReason.value = check.reason || '无法打印'
|
||||
return
|
||||
}
|
||||
}
|
||||
blockReason.value = ''
|
||||
loading.value = true
|
||||
try {
|
||||
html.value = await buildMpPlanPrintHtml(current, plan.value as Plan, {
|
||||
customerId: customerId.value
|
||||
})
|
||||
} catch (error) {
|
||||
html.value = ''
|
||||
blockReason.value = error instanceof Error ? error.message : '加载单据失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function selectKind(next: MpPlanPrintKind) {
|
||||
kind.value = next
|
||||
void reloadPreview()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const id = String(route.params.id || '')
|
||||
if (!id) {
|
||||
errorText.value = '缺少计划 ID'
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await planApi.get(id)
|
||||
plan.value = ((res as { data?: Plan }).data ?? res) as Plan
|
||||
customers.value = listDeliveryPrintCustomers(plan.value)
|
||||
if (customers.value.length === 1) customerId.value = customers.value[0].customerId
|
||||
await reloadPreview()
|
||||
} catch (error) {
|
||||
if (error instanceof HttpError && error.code === ApiStatus.forbidden) {
|
||||
forbidden.value = true
|
||||
return
|
||||
}
|
||||
errorText.value = error instanceof Error ? error.message : '加载失败'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
Reference in new issue
Block a user