This commit is contained in:
wuzilong committed 2026-09-17 09:10:10 +08:00
1 parent a0da6427ef
commit 86694f739f
471 files changed
+62075 -44805

No files matched your search

+66
View File
@@ -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)
}
+21 -9
View File
@@ -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
+9 -2
View File
@@ -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)
+189 -9
View File
@@ -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 }
}
+54
View File
@@ -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) || '未生成'
}
-128
View File
@@ -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
})
}
+115 -9
View File
@@ -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
}
+5 -2
View File
@@ -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 -5
View File
@@ -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)
}
/**