提交
This commit is contained in:
1 parent
a0da6427ef
commit
86694f739f
471 files changed
+62075
-44805
No files matched your search
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 复制文本到剪贴板。
|
||||
* 小程序 web-view / H5 iframe 常被 Permissions Policy 拦截 Clipboard API,
|
||||
* 需回退到 execCommand,必要时再交给壳页复制。
|
||||
*/
|
||||
import { MP_WEBVIEW_COPY_TEXT } from '@/utils/mp-webview'
|
||||
|
||||
function fallbackExecCommandCopy(text: string): boolean {
|
||||
try {
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.value = text
|
||||
textarea.setAttribute('readonly', '')
|
||||
textarea.style.position = 'fixed'
|
||||
textarea.style.top = '0'
|
||||
textarea.style.left = '0'
|
||||
textarea.style.width = '1px'
|
||||
textarea.style.height = '1px'
|
||||
textarea.style.padding = '0'
|
||||
textarea.style.border = 'none'
|
||||
textarea.style.outline = 'none'
|
||||
textarea.style.boxShadow = 'none'
|
||||
textarea.style.background = 'transparent'
|
||||
textarea.style.opacity = '0'
|
||||
document.body.appendChild(textarea)
|
||||
textarea.focus()
|
||||
textarea.select()
|
||||
textarea.setSelectionRange(0, textarea.value.length)
|
||||
const ok = document.execCommand('copy')
|
||||
document.body.removeChild(textarea)
|
||||
return ok
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** H5:内嵌 iframe 被策略拦截时,交给壳页(父窗口)执行复制 */
|
||||
function tryCopyViaMpWebviewHost(text: string): boolean {
|
||||
if (typeof window === 'undefined') return false
|
||||
if (!window.parent || window.parent === window) return false
|
||||
try {
|
||||
window.parent.postMessage({ type: MP_WEBVIEW_COPY_TEXT, text }, '*')
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function copyTextToClipboard(text: string): Promise<boolean> {
|
||||
const value = String(text ?? '')
|
||||
if (!value) return false
|
||||
|
||||
try {
|
||||
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(value)
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
/* Permissions Policy / insecure context → fallback */
|
||||
}
|
||||
|
||||
if (typeof document !== 'undefined' && fallbackExecCommandCopy(value)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return tryCopyViaMpWebviewHost(value)
|
||||
}
|
||||
@@ -32,45 +32,57 @@ function isSegmentSiteRecord(record: SiteSummaryRecord) {
|
||||
return (record.businessType || '').includes('按段')
|
||||
}
|
||||
|
||||
function formatStopProductLine(product: PlanStopProduct) {
|
||||
function formatStopProductLine(
|
||||
product: PlanStopProduct,
|
||||
getUnitLabel?: (unit: string) => string
|
||||
) {
|
||||
const name = (product.productName || '').trim() || '—'
|
||||
const qty = product.planQty
|
||||
const unit = (product.unit || '吨').trim()
|
||||
const unitRaw = (product.unit || '').trim()
|
||||
const unit = unitRaw
|
||||
? (getUnitLabel ? getUnitLabel(unitRaw) || unitRaw : unitRaw)
|
||||
: '吨'
|
||||
if (qty == null) return name
|
||||
return `${name} ${qty}${unit}`
|
||||
}
|
||||
|
||||
function isSiteVisibleStop(customerName?: string | null, generateDibang?: number | null) {
|
||||
return isPlInternalCustomerName(customerName) && Number(generateDibang || 0) === 1
|
||||
/** 现场页/简述可见段:仅内部客户(山峡/云栋),与是否勾选生成地磅单无关。 */
|
||||
function isSiteVisibleStop(customerName?: string | null) {
|
||||
return isPlInternalCustomerName(customerName)
|
||||
}
|
||||
|
||||
export function buildSiteStopBlocksFromPlan(
|
||||
plan?: {
|
||||
load?: PlanLoad | null
|
||||
deliveries?: PlanDelivery[] | null
|
||||
} | null
|
||||
} | null,
|
||||
getUnitLabel?: (unit: string) => string
|
||||
): SiteSummaryStopBlock[] {
|
||||
if (!plan) return []
|
||||
const blocks: SiteSummaryStopBlock[] = []
|
||||
const load = plan.load
|
||||
if (load && isSiteVisibleStop(load.customerName, load.generateDibang)) {
|
||||
if (load && isSiteVisibleStop(load.customerName)) {
|
||||
blocks.push({
|
||||
label: '装货',
|
||||
customerName: load.customerName || undefined,
|
||||
businessType: load.dibangDirection || undefined,
|
||||
address: load.address || undefined,
|
||||
productLines: (load.products || []).map(formatStopProductLine).filter(Boolean)
|
||||
productLines: (load.products || [])
|
||||
.map((product) => formatStopProductLine(product, getUnitLabel))
|
||||
.filter(Boolean)
|
||||
})
|
||||
}
|
||||
for (const delivery of plan.deliveries || []) {
|
||||
if (!isSiteVisibleStop(delivery.customerName, delivery.generateDibang)) continue
|
||||
if (!isSiteVisibleStop(delivery.customerName)) continue
|
||||
const seq = delivery.seq != null ? Number(delivery.seq) : 0
|
||||
blocks.push({
|
||||
label: seq > 0 ? `送货 #${seq}` : '送货',
|
||||
customerName: delivery.customerName || undefined,
|
||||
businessType: delivery.dibangDirection || undefined,
|
||||
address: delivery.address || undefined,
|
||||
productLines: (delivery.products || []).map(formatStopProductLine).filter(Boolean)
|
||||
productLines: (delivery.products || [])
|
||||
.map((product) => formatStopProductLine(product, getUnitLabel))
|
||||
.filter(Boolean)
|
||||
})
|
||||
}
|
||||
return blocks
|
||||
|
||||
@@ -22,6 +22,7 @@ import { $t } from '@/locales'
|
||||
import { BaseResponse } from '@/types'
|
||||
import { decryptJson, deriveKey, encryptJson, randomNonce } from './crypto'
|
||||
import { fetchRefreshToken } from '@/api/auth'
|
||||
import { isMpWebviewAuthBooting } from '@/utils/mp-webview'
|
||||
|
||||
/** 请求配置常量 */
|
||||
const REQUEST_TIMEOUT = 60000
|
||||
@@ -163,10 +164,12 @@ axiosInstance.interceptors.request.use(
|
||||
}
|
||||
)
|
||||
|
||||
/** 登录/公钥/登出等认证接口:业务 401 不得走 refresh/再登出(主动退出时尤其勿弹「未授权」) */
|
||||
/** 登录/公钥/登出/WebView 换票等:业务 401 不得走 refresh/再登出 */
|
||||
function isPublicAuthApiUrl(url?: string): boolean {
|
||||
if (!url) return false
|
||||
return /\/web\/auth\/(login|login-rsa-public-key|logout)(?:\?|$)/.test(url)
|
||||
return /\/web\/auth\/(login|login-rsa-public-key|logout|mp-webview-exchange|mp-webview-ticket)(?:\?|$)/.test(
|
||||
url
|
||||
)
|
||||
}
|
||||
|
||||
function isBlobResponseType(config?: AxiosRequestConfig): boolean {
|
||||
@@ -311,6 +314,10 @@ async function handleUnauthorizedResponse(
|
||||
|
||||
/** 带 3 秒防抖的登出提示 */
|
||||
function triggerLogoutWithDebounce(message?: string) {
|
||||
// 小程序 WebView 换票完成前,父页 onMounted 可能抢先打业务 API;此时登出会冲掉即将写入的 JWT
|
||||
if (isMpWebviewAuthBooting()) {
|
||||
throw createHttpError(message || $t('httpMsg.unauthorized'), ApiStatus.unauthorized)
|
||||
}
|
||||
if (isUnauthorizedErrorShown) return
|
||||
isUnauthorizedErrorShown = true
|
||||
const error = createHttpError(message || $t('httpMsg.unauthorized'), ApiStatus.unauthorized)
|
||||
|
||||
@@ -1,10 +1,83 @@
|
||||
import type { Plan } from '@/api/pl'
|
||||
import type { Plan, PlanProduct, PlanStopProduct } from '@/api/pl'
|
||||
import {
|
||||
getRequiredCertSlots,
|
||||
isHazardProduct,
|
||||
isProductCertSlotFilled,
|
||||
resolveProductHazardFlags
|
||||
} from '@/composables/usePlToxicProductCertCheck'
|
||||
import { isPlInternalCustomerName } from '@/utils/pl-internal-customer'
|
||||
import {
|
||||
formatPlVehiclePlateDisplay,
|
||||
getPlanProductActualQuantity,
|
||||
getPlPlanFollowStatusLabel,
|
||||
getPlPlanFollowStatusTagType,
|
||||
getPlPlanTypeLabel
|
||||
getPlPlanTypeLabel,
|
||||
normalizePlPersonnelSummaryDisplay
|
||||
} from '@/utils/pl-display'
|
||||
import {
|
||||
flattenPlanListRows,
|
||||
formatStopDibangLabel,
|
||||
getStopAddress,
|
||||
getStopContractLines,
|
||||
getStopCustomerName,
|
||||
getStopKindLabel,
|
||||
getStopProducts,
|
||||
getStopTime,
|
||||
resolveStopCertProducts,
|
||||
type PlPlanListDisplayRow
|
||||
} from '@/utils/pl-plan-list-display-rows'
|
||||
import {
|
||||
formatMpPlanYmd,
|
||||
formatMpPlanYmdList,
|
||||
resolveMpPackageSpecLabel,
|
||||
resolveMpProductUnitLabel
|
||||
} from '@/utils/mp-pl-plan-display'
|
||||
|
||||
export type MpPlanCardProductLine = {
|
||||
name: string
|
||||
isToxic: boolean
|
||||
isExplosive: boolean
|
||||
spec: string
|
||||
packageSpec: string
|
||||
unit: string
|
||||
planQty: string
|
||||
actualQty: string
|
||||
inboundQty: string
|
||||
outboundQty: string
|
||||
}
|
||||
|
||||
export type MpPlanCardStopBlock = {
|
||||
key: string
|
||||
kindLabel: string
|
||||
customer: string
|
||||
customerInternal: boolean
|
||||
address: string
|
||||
time: string
|
||||
addressTime: string
|
||||
contracts: string
|
||||
dibang: string
|
||||
attachments: string
|
||||
products: MpPlanCardProductLine[]
|
||||
}
|
||||
|
||||
export type MpPlanCard = {
|
||||
id: string
|
||||
planNo: string
|
||||
typeLabel: string
|
||||
planDate: string
|
||||
deliveryTimes: string
|
||||
statusLabel: string
|
||||
statusTagType: ReturnType<typeof getPlPlanFollowStatusTagType>
|
||||
plate: string
|
||||
people: string
|
||||
product: string
|
||||
supplier: string
|
||||
buyer: string
|
||||
partyLine: string
|
||||
follower: string
|
||||
arrangeCopy: string
|
||||
stops: MpPlanCardStopBlock[]
|
||||
}
|
||||
|
||||
export function formatPartyLine(supplier?: string | null, buyer?: string | null) {
|
||||
const s = (supplier || '').trim() || '—'
|
||||
@@ -15,9 +88,7 @@ export function formatPartyLine(supplier?: string | null, buyer?: string | null)
|
||||
function productSummary(plan: Plan): string {
|
||||
const extra = (plan as Plan & { productSummary?: string }).productSummary
|
||||
if (extra) return extra
|
||||
const names = (plan.products || [])
|
||||
.map((item) => item.productName)
|
||||
.filter(Boolean)
|
||||
const names = (plan.products || []).map((item) => item.productName).filter(Boolean)
|
||||
if (names.length) return names.join(' · ')
|
||||
const parties = [plan.supplierName, plan.customerName || plan.destinationName]
|
||||
.filter(Boolean)
|
||||
@@ -25,22 +96,131 @@ function productSummary(plan: Plan): string {
|
||||
return parties || '—'
|
||||
}
|
||||
|
||||
export function buildPlanCard(plan: Plan) {
|
||||
function formatQty(value: unknown): string {
|
||||
if (value == null || value === '') return '—'
|
||||
const num = Number(value)
|
||||
if (!Number.isFinite(num)) return '—'
|
||||
return String(num)
|
||||
}
|
||||
|
||||
function resolveUnitLabel(unit?: string | null): string {
|
||||
return resolveMpProductUnitLabel(unit)
|
||||
}
|
||||
|
||||
function resolveLinkedPlanProduct(
|
||||
plan: Plan,
|
||||
stopProduct: PlanStopProduct
|
||||
): PlanProduct | undefined {
|
||||
const planProducts = plan.products || []
|
||||
return (
|
||||
planProducts.find(
|
||||
(item) =>
|
||||
stopProduct.planProductId &&
|
||||
item.id &&
|
||||
String(item.id) === String(stopProduct.planProductId)
|
||||
) || planProducts.find((item) => String(item.productId) === String(stopProduct.productId))
|
||||
)
|
||||
}
|
||||
|
||||
function formatStopProductLine(
|
||||
plan: Plan,
|
||||
row: PlPlanListDisplayRow,
|
||||
stopProduct: PlanStopProduct
|
||||
): MpPlanCardProductLine {
|
||||
const linked = resolveLinkedPlanProduct(plan, stopProduct)
|
||||
const hazard = linked
|
||||
? resolveProductHazardFlags(linked)
|
||||
: { isToxic: false, isExplosive: false }
|
||||
const name =
|
||||
(linked?.productName || '').trim() || stopProduct.productName || '—'
|
||||
const actualFromStop =
|
||||
row.kind === 'load' ? stopProduct.inboundQty : stopProduct.outboundQty
|
||||
const actualQty =
|
||||
actualFromStop != null && actualFromStop !== ''
|
||||
? formatQty(actualFromStop)
|
||||
: linked
|
||||
? getPlanProductActualQuantity(linked)
|
||||
: '—'
|
||||
|
||||
return {
|
||||
name,
|
||||
isToxic: hazard.isToxic,
|
||||
isExplosive: hazard.isExplosive,
|
||||
spec: stopProduct.model || linked?.model || '—',
|
||||
packageSpec: resolveMpPackageSpecLabel(stopProduct.packageSpec || linked?.packageSpec),
|
||||
unit: resolveUnitLabel(stopProduct.unit || linked?.unit),
|
||||
planQty: formatQty(stopProduct.planQty),
|
||||
actualQty,
|
||||
inboundQty: formatQty(stopProduct.inboundQty),
|
||||
outboundQty: formatQty(stopProduct.outboundQty)
|
||||
}
|
||||
}
|
||||
|
||||
function formatAttachments(plan: Plan, row: PlPlanListDisplayRow): string {
|
||||
const certProducts = resolveStopCertProducts(plan, getStopProducts(row)).filter((p) =>
|
||||
isHazardProduct(p)
|
||||
)
|
||||
if (!certProducts.length) return '—'
|
||||
return certProducts
|
||||
.map((product) => {
|
||||
const slots = getRequiredCertSlots(product)
|
||||
.map((slot) => {
|
||||
const filled = isProductCertSlotFilled(product, slot.key)
|
||||
return `${slot.label}(${filled ? '有' : '无'})`
|
||||
})
|
||||
.join(' ')
|
||||
const name = product.productName || '—'
|
||||
return slots ? `${name} ${slots}` : name
|
||||
})
|
||||
.join(';')
|
||||
}
|
||||
|
||||
function formatContracts(row: PlPlanListDisplayRow): string {
|
||||
const lines = getStopContractLines(row)
|
||||
if (!lines.length) return '—'
|
||||
return lines.map((line) => `${line.side}:${line.no}`).join(' · ')
|
||||
}
|
||||
|
||||
function buildStopBlocks(plan: Plan): MpPlanCardStopBlock[] {
|
||||
return flattenPlanListRows([plan]).map((row) => {
|
||||
const address = getStopAddress(row)
|
||||
const time = getStopTime(row)
|
||||
const customer = getStopCustomerName(row)
|
||||
return {
|
||||
key: row.rowKey,
|
||||
kindLabel: getStopKindLabel(row),
|
||||
customer,
|
||||
customerInternal: isPlInternalCustomerName(customer),
|
||||
address,
|
||||
time,
|
||||
addressTime: `${address} / ${time}`,
|
||||
contracts: formatContracts(row),
|
||||
dibang: formatStopDibangLabel(row.stop) || '—',
|
||||
attachments: formatAttachments(plan, row),
|
||||
products: getStopProducts(row).map((p) => formatStopProductLine(plan, row, p))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function buildPlanCard(plan: Plan): MpPlanCard {
|
||||
const supplier = plan.supplierName || '—'
|
||||
const buyer = plan.customerName || plan.destinationName || '—'
|
||||
return {
|
||||
id: plan.id,
|
||||
planNo: plan.planNo || '—',
|
||||
typeLabel: getPlPlanTypeLabel(plan.planType || ''),
|
||||
planDate: plan.planDate || '—',
|
||||
planDate: formatMpPlanYmd(plan.planDate),
|
||||
deliveryTimes: formatMpPlanYmdList(plan.deliveryTimes),
|
||||
statusLabel: getPlPlanFollowStatusLabel(plan.followStatus),
|
||||
statusTagType: getPlPlanFollowStatusTagType(plan.followStatus),
|
||||
plate: formatPlVehiclePlateDisplay(plan.licensePlate, plan.trailerNo) || '—',
|
||||
people: plan.personnelSummary || '—',
|
||||
people: normalizePlPersonnelSummaryDisplay(plan.personnelSummary) || '—',
|
||||
product: productSummary(plan),
|
||||
supplier,
|
||||
buyer,
|
||||
partyLine: formatPartyLine(plan.supplierName, plan.customerName || plan.destinationName),
|
||||
follower: plan.followUserName || '—'
|
||||
follower: plan.followUserName || '—',
|
||||
arrangeCopy: plan.canCopyPoundQrcode ? '可复制磅单码' : '—',
|
||||
stops: buildStopBlocks(plan)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
export type MpDetailActionKind =
|
||||
| 'edit'
|
||||
| 'arrange'
|
||||
| 'copy-driver'
|
||||
| 'copy-site'
|
||||
| 'copy-pound-qrcode'
|
||||
| 'copy'
|
||||
| 'wait'
|
||||
| 'cancel-wait'
|
||||
| 'print'
|
||||
| 'void'
|
||||
|
||||
export type MpDetailAction = {
|
||||
@@ -13,11 +15,18 @@ export type MpDetailAction = {
|
||||
danger?: boolean
|
||||
}
|
||||
|
||||
/** 主栏最多 2 个:优先 edit、arrange;其余进 more */
|
||||
/**
|
||||
* 主栏最多 2 个:优先 edit、arrange;其余进 more。
|
||||
* 若无可展示主操作,再从 more 补足最多 2 个,避免底栏空白。
|
||||
* 安排后复制类(司机/现场/磅单码)始终进 more(详情页另有显眼入口)。
|
||||
*/
|
||||
export function splitMpPlanDetailActions(input: {
|
||||
canEdit: boolean
|
||||
canArrange: boolean
|
||||
arrangeLabel: string
|
||||
canCopyDriver?: boolean
|
||||
canCopySite?: boolean
|
||||
canCopyPoundQrcode?: boolean
|
||||
canWait: boolean
|
||||
canCancelWait: boolean
|
||||
canVoid: boolean
|
||||
@@ -30,15 +39,95 @@ export function splitMpPlanDetailActions(input: {
|
||||
primary.push({ kind: 'arrange', label: input.arrangeLabel })
|
||||
}
|
||||
|
||||
if (input.canCopyDriver) more.push({ kind: 'copy-driver', label: '复制给司机' })
|
||||
if (input.canCopySite) more.push({ kind: 'copy-site', label: '复制给现场' })
|
||||
if (input.canCopyPoundQrcode) more.push({ kind: 'copy-pound-qrcode', label: '复制磅单二维码' })
|
||||
|
||||
more.push({ kind: 'copy', label: '复制' })
|
||||
if (input.canWait) more.push({ kind: 'wait', label: '等客户通知' })
|
||||
if (input.canCancelWait) more.push({ kind: 'cancel-wait', label: '取消等通知' })
|
||||
more.push({ kind: 'print', label: '打印单据' })
|
||||
if (input.canVoid) more.push({ kind: 'void', label: '作废', danger: true })
|
||||
|
||||
while (primary.length > 2) more.unshift(primary.pop()!)
|
||||
while (primary.length < 2 && more.length) {
|
||||
primary.push(more.shift()!)
|
||||
|
||||
// 仅当主栏完全为空时补位;优先补非复制类业务操作,避免把「复制给司机」挤到主栏
|
||||
if (primary.length === 0) {
|
||||
const preferKinds: MpDetailActionKind[] = ['copy', 'wait', 'cancel-wait', 'void']
|
||||
for (const kind of preferKinds) {
|
||||
if (primary.length >= 2) break
|
||||
const idx = more.findIndex((a) => a.kind === kind)
|
||||
if (idx < 0) continue
|
||||
primary.push(more.splice(idx, 1)[0])
|
||||
}
|
||||
while (primary.length < 2 && more.length) {
|
||||
primary.push(more.shift()!)
|
||||
}
|
||||
}
|
||||
|
||||
return { primary, more }
|
||||
}
|
||||
|
||||
export type MpFormFooterActionKey =
|
||||
| 'cancel'
|
||||
| 'draft'
|
||||
| 'save'
|
||||
| 'saveAndArrange'
|
||||
| 'rearrange'
|
||||
|
||||
export type MpFormFooterAction = {
|
||||
key: MpFormFooterActionKey
|
||||
label: string
|
||||
type?: 'primary' | 'default'
|
||||
ghost?: boolean
|
||||
}
|
||||
|
||||
/** 表单底栏:暂存 / 创建(保存) / 主操作 外显;取消进更多 */
|
||||
export function splitMpPlanFormFooterActions(input: {
|
||||
showDraft: boolean
|
||||
showSave: boolean
|
||||
showSaveAndArrange: boolean
|
||||
showRearrange: boolean
|
||||
saveLabel: string
|
||||
saveAndArrangeLabel: string
|
||||
}): { primary: MpFormFooterAction[]; more: MpFormFooterAction[] } {
|
||||
const more: MpFormFooterAction[] = [{ key: 'cancel', label: '取消' }]
|
||||
const primary: MpFormFooterAction[] = []
|
||||
const hasStrongPrimary = input.showSaveAndArrange || input.showRearrange
|
||||
|
||||
if (input.showDraft) {
|
||||
primary.push({ key: 'draft', label: '暂存' })
|
||||
}
|
||||
if (input.showSave) {
|
||||
primary.push({
|
||||
key: 'save',
|
||||
label: input.saveLabel,
|
||||
type: hasStrongPrimary ? 'default' : 'primary',
|
||||
ghost: hasStrongPrimary
|
||||
})
|
||||
}
|
||||
if (input.showRearrange) {
|
||||
primary.push({ key: 'rearrange', label: '调整运输', type: 'primary' })
|
||||
} else if (input.showSaveAndArrange) {
|
||||
primary.push({
|
||||
key: 'saveAndArrange',
|
||||
label: input.saveAndArrangeLabel,
|
||||
type: 'primary'
|
||||
})
|
||||
}
|
||||
|
||||
// 主栏最多 3 个,多出的收进更多(取消仍排最前)
|
||||
while (primary.length > 3) {
|
||||
const overflow = primary.pop()!
|
||||
more.splice(1, 0, overflow)
|
||||
}
|
||||
|
||||
if (primary.length === 0 && more.length > 1) {
|
||||
const first = more.find((a) => a.key !== 'cancel') || more[0]
|
||||
if (first) {
|
||||
more.splice(more.indexOf(first), 1)
|
||||
primary.push({ ...first, type: first.type || 'primary' })
|
||||
}
|
||||
}
|
||||
|
||||
return { primary, more }
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { PlanDelivery, PlanLoad } from '@/api/pl'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { formatStopDibangLabel } from '@/utils/pl-plan-list-display-rows'
|
||||
|
||||
/** 移动端运输计划:日期只展示到年月日 */
|
||||
export function formatMpPlanYmd(value?: string | number | Date | null): string {
|
||||
if (value == null || value === '') return '—'
|
||||
const text = formatDate(value)
|
||||
if (text) return text
|
||||
const raw = String(value).trim()
|
||||
return raw.slice(0, 10) || '—'
|
||||
}
|
||||
|
||||
export function formatMpPlanYmdList(values?: Array<string | null | undefined> | null): string {
|
||||
const parts = (values || [])
|
||||
.map((item) => formatMpPlanYmd(item))
|
||||
.filter((item) => item && item !== '—')
|
||||
return parts.length ? [...new Set(parts)].join('、') : '—'
|
||||
}
|
||||
|
||||
export function resolveMpDictLabel(
|
||||
dictType: string,
|
||||
value?: string | null,
|
||||
emptyFallback = '—'
|
||||
): string {
|
||||
const raw = String(value || '').trim()
|
||||
if (!raw) return emptyFallback
|
||||
const dictStore = useDictStore()
|
||||
return dictStore.getLabel(dictType, raw) || raw
|
||||
}
|
||||
|
||||
/** 产品单位:dun → 吨 */
|
||||
export function resolveMpProductUnitLabel(unit?: string | null): string {
|
||||
return resolveMpDictLabel('crm_product_unit', unit)
|
||||
}
|
||||
|
||||
/** 包装规格字典 */
|
||||
export function resolveMpPackageSpecLabel(spec?: string | null): string {
|
||||
return resolveMpDictLabel('crm_product_gg', spec)
|
||||
}
|
||||
|
||||
/** 送货方式字典 */
|
||||
export function resolveMpDeliveryMethodLabel(method?: string | null): string {
|
||||
return resolveMpDictLabel('crm_shfs', method)
|
||||
}
|
||||
|
||||
/** 地磅:未生成 / 出库 / 入库(不用 outbound 等编码) */
|
||||
export function formatMpStopDibangText(
|
||||
stop: Pick<PlanLoad | PlanDelivery, 'generateDibang' | 'dibangDirection'> | null | undefined
|
||||
): string {
|
||||
if (!stop) return '—'
|
||||
return formatStopDibangLabel(stop as PlanLoad | PlanDelivery) || '未生成'
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
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,35 +1,141 @@
|
||||
export const MP_WEBVIEW_PATH_PREFIX = '/m/'
|
||||
export const MP_PLAN_LIST_PATH = '/m/pl/plan'
|
||||
export const MINI_WORKBENCH_PATH = '/pages/workbench/workbench'
|
||||
/** H5 iframe / uni 壳页约定:内嵌页请求返回工作台 */
|
||||
export const MP_WEBVIEW_GO_WORKBENCH = 'MP_WEBVIEW_GO_WORKBENCH'
|
||||
/** H5 iframe:内嵌页 Clipboard API 被拦截时,请壳页代为写入剪贴板 */
|
||||
export const MP_WEBVIEW_COPY_TEXT = 'MP_WEBVIEW_COPY_TEXT'
|
||||
const MP_RETURN_ORIGIN_KEY = 'mp_webview_return_origin'
|
||||
|
||||
/** MpWebviewShell 换票 / 就绪引导中:业务请求若 401 不得触发登出(与换票竞态) */
|
||||
let mpWebviewAuthBootDepth = 0
|
||||
|
||||
export function beginMpWebviewAuthBoot(): void {
|
||||
mpWebviewAuthBootDepth += 1
|
||||
}
|
||||
|
||||
export function endMpWebviewAuthBoot(): void {
|
||||
mpWebviewAuthBootDepth = Math.max(0, mpWebviewAuthBootDepth - 1)
|
||||
}
|
||||
|
||||
export function isMpWebviewAuthBooting(): boolean {
|
||||
return mpWebviewAuthBootDepth > 0
|
||||
}
|
||||
|
||||
export function isMpWebviewPath(path: string): boolean {
|
||||
return (path || '').startsWith(MP_WEBVIEW_PATH_PREFIX)
|
||||
}
|
||||
|
||||
export function getWxMiniProgram(): {
|
||||
reLaunch?: (opt: { url: string }) => void
|
||||
navigateBack?: (opt?: { delta?: number }) => void
|
||||
} | null {
|
||||
type WxMiniProgramApi = {
|
||||
reLaunch?: (opt: { url: string; fail?: () => void }) => void
|
||||
navigateBack?: (opt?: { delta?: number; fail?: () => void }) => void
|
||||
getEnv?: (cb: (res: { miniprogram?: boolean }) => void) => void
|
||||
}
|
||||
|
||||
export function getWxMiniProgram(): WxMiniProgramApi | null {
|
||||
if (typeof window === 'undefined') return null
|
||||
const wxObj = (window as unknown as { wx?: { miniProgram?: unknown } }).wx
|
||||
const mini = wxObj?.miniProgram as
|
||||
| { reLaunch?: (opt: { url: string }) => void; navigateBack?: (opt?: { delta?: number }) => void }
|
||||
| undefined
|
||||
const mini = wxObj?.miniProgram as WxMiniProgramApi | undefined
|
||||
return mini || null
|
||||
}
|
||||
|
||||
/** 真机 web-view 内才为 true;勿信任 jweixin 在普通浏览器里挂上的空壳 API */
|
||||
export function isInMiniProgramWebView(): boolean {
|
||||
if (typeof window === 'undefined') return false
|
||||
const w = window as unknown as { __wxjs_environment?: string }
|
||||
if (w.__wxjs_environment === 'miniprogram') return true
|
||||
const ua = typeof navigator !== 'undefined' ? navigator.userAgent || '' : ''
|
||||
return /miniProgram/i.test(ua) && /MicroMessenger/i.test(ua)
|
||||
}
|
||||
|
||||
/** 记住小程序壳 origin,供 H5 iframe 顶栏返回工作台(跨域无法读 parent.location) */
|
||||
export function rememberMpReturnOrigin(origin?: string): void {
|
||||
if (typeof sessionStorage === 'undefined') return
|
||||
const value = String(origin || '').trim().replace(/\/$/, '')
|
||||
if (!value || !/^https?:\/\//i.test(value)) return
|
||||
try {
|
||||
sessionStorage.setItem(MP_RETURN_ORIGIN_KEY, value)
|
||||
} catch {
|
||||
/* ignore quota */
|
||||
}
|
||||
}
|
||||
|
||||
function readMpReturnOrigin(): string {
|
||||
if (typeof sessionStorage === 'undefined') return ''
|
||||
try {
|
||||
return String(sessionStorage.getItem(MP_RETURN_ORIGIN_KEY) || '').trim()
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function postGoWorkbenchToHost(): boolean {
|
||||
if (typeof window === 'undefined') return false
|
||||
const payload = { type: MP_WEBVIEW_GO_WORKBENCH }
|
||||
let posted = false
|
||||
try {
|
||||
if (window.parent && window.parent !== window) {
|
||||
window.parent.postMessage(payload, '*')
|
||||
posted = true
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
try {
|
||||
if (window.top && window.top !== window && window.top !== window.parent) {
|
||||
window.top.postMessage(payload, '*')
|
||||
posted = true
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return posted
|
||||
}
|
||||
|
||||
function navigateTopToMiniWorkbench(): boolean {
|
||||
const origin = readMpReturnOrigin()
|
||||
if (!origin || typeof window === 'undefined') return false
|
||||
try {
|
||||
const target = `${origin}${MINI_WORKBENCH_PATH}`
|
||||
if (window.top && window.top !== window) {
|
||||
window.top.location.href = target
|
||||
return true
|
||||
}
|
||||
window.location.href = target
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** 列表顶栏「返回」:回到小程序工作台(web-view 多用 redirectTo 打开,须 reLaunch) */
|
||||
export function goToMiniWorkbench(): void {
|
||||
const mini = getWxMiniProgram()
|
||||
if (isInMiniProgramWebView() && mini?.reLaunch) {
|
||||
mini.reLaunch({ url: MINI_WORKBENCH_PATH })
|
||||
return
|
||||
}
|
||||
// uni-app H5:web-view 多为 iframe,由壳页监听 postMessage;再兜底顶层跳转
|
||||
postGoWorkbenchToHost()
|
||||
navigateTopToMiniWorkbench()
|
||||
}
|
||||
|
||||
export function redirectToMiniLogin(): void {
|
||||
const mini = getWxMiniProgram()
|
||||
if (mini?.reLaunch) {
|
||||
if (isInMiniProgramWebView() && mini?.reLaunch) {
|
||||
mini.reLaunch({ url: '/pages/login/login' })
|
||||
return
|
||||
}
|
||||
postGoWorkbenchToHost()
|
||||
// 非小程序:只展示空态,禁止跳管理端登录
|
||||
}
|
||||
|
||||
export function stripMpTicketFromUrl(
|
||||
path: string,
|
||||
_path: string,
|
||||
query: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
const next = { ...query }
|
||||
delete next.mpTicket
|
||||
delete next.mpReturnOrigin
|
||||
return next
|
||||
}
|
||||
@@ -546,15 +546,18 @@ export function getPlRechargeTypeTagType(type: string) {
|
||||
export function getPlFuelCategoryLabel(category: string) {
|
||||
const map: Record<string, string> = {
|
||||
bailian: '百链',
|
||||
gas_station: '加油站'
|
||||
gas_station: '加油站',
|
||||
shanxia_factory: '山峡'
|
||||
}
|
||||
return map[category] || category || '—'
|
||||
}
|
||||
|
||||
export function getPlFuelCategoryTagType(category: string) {
|
||||
// 注意:本项目 global 样式下 primary 与 success 同为翠绿,不可并用区分类别
|
||||
const map: Record<string, string> = {
|
||||
bailian: 'primary',
|
||||
gas_station: 'success'
|
||||
gas_station: 'warning',
|
||||
shanxia_factory: 'info'
|
||||
}
|
||||
return map[category] || 'info'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { ComputedRef, InjectionKey } from 'vue'
|
||||
|
||||
/** 计划表单 page 变体下,装/送货等库选择走移动端 Sheet(字符串 key,避免 Symbol 多实例 inject 失效) */
|
||||
export const PL_PLAN_FORM_MOBILE_PICKERS_KEY: InjectionKey<ComputedRef<boolean>> =
|
||||
'plPlanFormMobilePickers' as unknown as InjectionKey<ComputedRef<boolean>>
|
||||
@@ -11,6 +11,7 @@ import NProgress from 'nprogress'
|
||||
import 'nprogress/nprogress.css'
|
||||
import i18n, { $t } from '@/locales'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { isMpWebviewPath } from '@/utils/mp-webview'
|
||||
|
||||
/** 历史遗留登录路径(与 RoutesAlias.Login 等价) */
|
||||
const LEGACY_LOGIN_PATHS = new Set([RoutesAlias.Login, '/login'])
|
||||
@@ -36,11 +37,16 @@ export const configureNProgress = () => {
|
||||
*/
|
||||
export const setPageTitle = (to: RouteLocationNormalized): void => {
|
||||
const { title } = to.meta
|
||||
if (title) {
|
||||
setTimeout(() => {
|
||||
document.title = `${formatMenuTitle(String(title))} - ${AppConfig.systemInfo.name}`
|
||||
}, 150)
|
||||
}
|
||||
if (!title) return
|
||||
setTimeout(() => {
|
||||
const pageTitle = formatMenuTitle(String(title))
|
||||
// 小程序 web-view 原生顶栏会显示 document.title;勿拼系统名,且内嵌页用空标题避免双标题
|
||||
if (isMpWebviewPath(to.path) || to.meta?.isMpWebview) {
|
||||
document.title = '\u200B'
|
||||
return
|
||||
}
|
||||
document.title = `${pageTitle} - ${AppConfig.systemInfo.name}`
|
||||
}, 150)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user