| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465 |
- import assert from 'node:assert/strict'
- import { readFileSync } from 'node:fs'
- import test from 'node:test'
- import ts from 'typescript'
- import { parse } from '@vue/compiler-sfc'
- import { ref } from 'vue'
- import { formatBrandConstraintLabel } from '../src/utils/crm-product-brand'
- import { useServerColumnFilters } from '../src/composables/useServerColumnFilters'
- import { plPlanColumnFilterDefs } from '../src/views/pl/plan/columnFilterDefs'
- import { plPlanListTabToFilters } from '../src/utils/pl-display'
- import {
- applyMergedCustomerQueryParams,
- applyContractSideQueryParams
- } from '../src/utils/pl-plan-pair-list-query'
- import { facetPlanDateParams } from '../src/views/m/pl/plan-cert/model'
- function productLine() {
- const source = ts.createSourceFile(
- 'card.ts',
- readFileSync(new URL('../src/utils/mp-pl-plan-card.ts', import.meta.url), 'utf8'),
- ts.ScriptTarget.Latest,
- true
- )
- const names = [
- 'formatStopProductLine',
- 'resolveLinkedPlanProduct',
- 'formatQty',
- 'resolveUnitLabel'
- ]
- const code = ts.transpile(
- source.statements
- .filter((node) => ts.isFunctionDeclaration(node) && names.includes(node.name!.text))
- .map((node) => node.getText(source))
- .join('\n'),
- { target: ts.ScriptTarget.ESNext }
- )
- const env = {
- formatBrandConstraintLabel,
- resolveProductHazardFlags: () => ({ isToxic: false, isExplosive: false }),
- getPlanProductActualQuantity: () => '0',
- resolveMpPackageSpecLabel: (v: string) => v || '—',
- resolveMpProductUnitLabel: (v: string) => v || '—'
- }
- return new Function(...Object.keys(env), `${code};return formatStopProductLine`)(
- ...Object.values(env)
- )
- }
- test('mobile list product brands and aliases follow the exact plan line before product ID fallback', () => {
- const format = productLine()
- const plan = {
- products: [
- {
- id: 'first',
- productId: 'p',
- productName: '标准名称',
- brandMode: 'required',
- brandName: '品牌甲',
- aliasName: '别名甲'
- },
- {
- id: 'second',
- productId: 'p',
- productName: '标准名称',
- brandMode: 'required',
- brandName: '品牌乙',
- aliasName: ' 别名乙 '
- }
- ]
- }
- const second = format(
- plan,
- { kind: 'load' },
- { productId: 'p', planProductId: 'second', productName: '段上别名', planQty: 2 }
- )
- assert.equal(second.name, '标准名称')
- assert.equal(second.brand, '品牌乙')
- assert.equal(second.alias, '别名乙')
- assert.equal(second.planQty, '2')
- const fallback = format(plan, { kind: 'delivery' }, { productId: 'p' })
- assert.equal(fallback.brand, '品牌甲')
- assert.equal(fallback.alias, '别名甲')
- })
- test('mobile product metadata uses the Web brand constraint label and empty placeholders', () => {
- const format = productLine()
- for (const brandMode of ['any', 'required', undefined]) {
- const product = { id: 'line', productId: 'p', brandMode, brandName: '', aliasName: '' }
- const line = format({ products: [product] }, { kind: 'load' }, { planProductId: 'line' })
- assert.equal(line.brand, formatBrandConstraintLabel(brandMode, ''))
- assert.equal(line.alias, '—')
- }
- const missing = format({ products: [] }, { kind: 'load' }, { productId: 'missing' })
- assert.equal(missing.brand, '—')
- assert.equal(missing.alias, '—')
- })
- for (const metadata of [
- { aliasName: '送货别名', brandMode: 'any', brandName: '' },
- { aliasName: '', brandMode: '', brandName: '' },
- {},
- { aliasName: '另一段别名', brandMode: 'required', brandName: '另一段品牌' }
- ]) {
- test(`segment metadata overrides the same root product, including explicit empty values: ${JSON.stringify(metadata)}`, () => {
- const format = productLine()
- const root = {
- id: 'root',
- productId: 'p',
- productName: '标准名称',
- aliasName: '装货根别名',
- brandMode: 'required',
- brandName: '装货根品牌'
- }
- const plan = { products: [root] }
- const load = format(plan, { kind: 'load' }, { productId: 'p', planProductId: 'root' })
- const delivery = format(
- plan,
- { kind: 'delivery' },
- {
- productId: 'p',
- planProductId: 'root',
- mobileProductMeta: metadata
- }
- )
- assert.equal(load.alias, '装货根别名')
- assert.equal(load.brand, '装货根品牌')
- assert.equal(delivery.alias, metadata.aliasName || '—')
- assert.equal(delivery.brand, formatBrandConstraintLabel(metadata.brandMode, metadata.brandName))
- assert.equal(root.aliasName, '装货根别名')
- assert.equal(root.brandName, '装货根品牌')
- })
- }
- function queryFunction(path: string, name: string, bindings: Record<string, any>) {
- const ast = ts.createSourceFile(
- 'query.ts',
- parse(readFileSync(new URL(path, import.meta.url), 'utf8')).descriptor.scriptSetup!.content,
- ts.ScriptTarget.Latest,
- true
- )
- const fn = ast.statements.find(
- (node) => ts.isFunctionDeclaration(node) && node.name?.text === name
- )!
- return new Function(
- ...Object.keys(bindings),
- `${ts.transpile(fn.getText(ast), { target: ts.ScriptTarget.ESNext })};return ${name}`
- )(...Object.values(bindings))
- }
- for (const tab of ['pending_arrange', 'all', 'inbound', 'outbound']) {
- test(`mobile ${tab} list query matches the Web keyword, filter, tab, sort and contract-side parameters`, () => {
- const state = useServerColumnFilters(plPlanColumnFilterDefs)
- Object.assign(state.filters, {
- followStatus: { selected: ['accepted'], text: '' },
- planType: { selected: ['direct'], text: '' },
- planNo: { selected: ['PO1', 'PO2'], text: '' },
- planDate: { from: '2026-10-01', to: '2026-10-10' },
- deliveryTimes: { from: '2026-10-02', to: '2026-10-11' },
- loadCustomer: { selected: ['供方'], text: '' },
- deliveryCustomer: { selected: ['需方'], text: '' },
- productContract: { selected: ['load', 'CT1'], text: '' },
- personnelSummary: { text: '司机', selected: [] },
- planQty: { op: 'between', min: 1, max: 9 }
- })
- const keyword = ref(' 搜索关键词 '),
- activeTab = ref(tab),
- dateRange = ref(['2026-09-01', '2026-09-30'])
- const sort = { sortField: 'deliveryTimes', sortOrder: 'asc' }
- const bindings = {
- ...state,
- keyword,
- activeTab,
- dateRange,
- sort,
- pageSize: 20,
- plPlanListTabToFilters,
- applyMergedCustomerQueryParams,
- applyContractSideQueryParams,
- isStockList: ref(false),
- quickSearch: keyword,
- activePlanTypeTab: activeTab,
- toSortQueryParams: () => sort
- }
- const mobile = queryFunction('../src/views/m/pl/plan/index.vue', 'buildParams', bindings)()
- const web = queryFunction('../src/views/pl/plan/index.vue', 'buildListQueryParams', bindings)()
- assert.deepEqual(mobile, { ...web, pageSize: 20 })
- })
- }
- test('plan facets preserve independent load and delivery customer restrictions from the draft', () => {
- const state = useServerColumnFilters(plPlanColumnFilterDefs)
- Object.assign(state.filters, {
- loadCustomer: { text: '', selected: ['装货甲'] },
- deliveryCustomer: { text: '', selected: ['送货乙'] }
- })
- const source = ts.createSourceFile(
- 'filter.ts',
- parse(readFileSync(new URL('../src/views/m/pl/plan/PlanFilters.vue', import.meta.url), 'utf8'))
- .descriptor.scriptSetup!.content,
- ts.ScriptTarget.Latest,
- true
- )
- const node = source.statements.find(
- (node) =>
- ts.isVariableStatement(node) &&
- node.declarationList.declarations.some((d) => d.name.getText(source) === 'picker')
- )!
- const env = {
- defs: plPlanColumnFilterDefs,
- draft: state.filters,
- toQueryParams: state.toQueryParams,
- props: {
- context: {
- followStatus: 'accepted',
- page: 2,
- loadCustomersIn: '旧装货',
- deliveryCustomersIn: '旧送货'
- },
- filters: {}
- },
- userStore: {},
- useMobileCertFacet: (config: any) => config,
- fetchPlanColumnFacetValues: () => {},
- applyMergedCustomerQueryParams,
- applyContractSideQueryParams,
- facetPlanDateParams
- }
- const picker = new Function(
- ...Object.keys(env),
- `${ts.transpile(node.getText(source), { target: ts.ScriptTarget.ESNext })};return picker`
- )(...Object.values(env))
- const q = picker.context()
- assert.equal(q.loadCustomersIn, '装货甲')
- assert.equal(q.deliveryCustomersIn, '送货乙')
- assert.equal(q.stopCustomers, undefined)
- assert.equal(q.followStatus, 'accepted')
- })
- import * as Vue from 'vue'
- import { useMobileCertFacet } from '../src/views/m/pl/plan-cert/useMobileCertFacet'
- import {
- withPlanContractIoFacetOptions,
- PL_PLAN_CONTRACT_SIDE_FILTER_OPTIONS
- } from '../src/utils/pl-plan-contract-display'
- test('plan filter pickers confirm drafts, keep server query semantics and reset all filters', async () => {
- const source = ts.createSourceFile(
- 'filters.ts',
- parse(readFileSync(new URL('../src/views/m/pl/plan/PlanFilters.vue', import.meta.url), 'utf8'))
- .descriptor.scriptSetup!.content,
- ts.ScriptTarget.Latest,
- true
- )
- const code = source.statements
- .filter((node) => !ts.isImportDeclaration(node))
- .map((node) => node.getText(source))
- .join('\n')
- const props = Vue.reactive({
- modelValue: false,
- dateOnly: false,
- filters: {} as Record<string, any>,
- sort: { sortField: '', sortOrder: 'desc' },
- context: { followStatus: 'accepted' }
- })
- const events: any[] = [],
- requests: any[] = [],
- warnings: string[] = []
- const bindings = {
- ...Object.fromEntries(
- Object.entries(Vue).filter(([key]) => /^[a-zA-Z_$][\w$]*$/.test(key) && key !== 'default')
- ),
- defs: plPlanColumnFilterDefs,
- useServerColumnFilters,
- useMobileCertFacet,
- applyMergedCustomerQueryParams,
- applyContractSideQueryParams,
- facetPlanDateParams,
- withPlanContractIoFacetOptions,
- PL_PLAN_CONTRACT_SIDE_FILTER_OPTIONS,
- defineProps: () => props,
- defineEmits:
- () =>
- (...args: any[]) =>
- events.push(args),
- useUserStore: () => ({ accessToken: 'test', getUserInfo: { id: 'U' } }),
- fetchPlanColumnFacetValues: async (query: any) => {
- requests.push(query)
- return { list: [{ value: 'P1', label: '计划一' }] }
- },
- ElMessage: { warning: (text: string) => warnings.push(text) }
- }
- const scope = Vue.effectScope()
- const h = scope.run(() =>
- new Function(
- ...Object.keys(bindings),
- ts.transpile(code, { target: ts.ScriptTarget.ESNext }) +
- ';return {picker,draft,confirmPicker,enumSummary,openSingle,confirmSingle,setNumberValue,openDate,confirmDate,toQueryParams,apply,resetAndApply,singleOpen,dateOpen,visibleDateDefs}'
- )(...Object.values(bindings))
- )
- const flush = async () => {
- for (let i = 0; i < 10; i++) await Promise.resolve()
- }
- try {
- props.modelValue = true
- await Vue.nextTick()
- h.picker.open('planType', [])
- await flush()
- assert.equal(requests.length, 0, 'fixed options do not call the remote facet API')
- h.picker.toggle('inbound')
- h.picker.close()
- assert.equal(h.draft.planType, undefined)
- h.picker.open('planType', [])
- await flush()
- h.picker.toggle('inbound')
- h.confirmPicker()
- assert.equal(h.enumSummary('planType'), '入库单')
- h.picker.open('planNo', [])
- await flush()
- assert.equal(requests[0].followStatus, 'accepted')
- h.picker.toggle('P1')
- h.confirmPicker()
- assert.equal(h.enumSummary('planNo'), '计划一')
- h.openSingle('numberOp', 'planQty')
- h.confirmSingle({ selectedValues: ['gte'] })
- h.setNumberValue('planQty', 'value', '10')
- h.openDate('planDate', 'from')
- h.confirmDate({ selectedValues: ['2026', '10', '24'] })
- h.openSingle('sortField')
- h.confirmSingle({ selectedValues: ['planDate'] })
- assert.deepEqual(h.toQueryParams(), {
- planNoIn: 'P1',
- planTypeIn: 'inbound',
- planDateFrom: '2026-10-24',
- productPlanQtyOp: 'gte',
- productPlanQty: '10'
- })
- h.apply()
- assert.deepEqual(events.find((event) => event[0] === 'apply').slice(2), [
- { sortField: 'planDate', sortOrder: 'desc' },
- false
- ])
- h.openDate('planDate', 'to')
- h.confirmDate({ selectedValues: ['2026', '10', '23'] })
- h.apply()
- assert.equal(warnings.length, 1)
- h.openSingle('sortOrder')
- h.openDate('planDate', 'to')
- props.modelValue = false
- await Vue.nextTick()
- assert.equal(h.singleOpen.value, false)
- assert.equal(h.dateOpen.value, false)
- h.resetAndApply()
- assert.deepEqual(events.filter((event) => event[0] === 'apply').at(-1), [
- 'apply',
- {},
- { sortField: '', sortOrder: 'desc' },
- true
- ])
- props.modelValue = false
- await Vue.nextTick()
- props.dateOnly = true
- props.filters = {
- planNo: { selected: ['P1'] },
- planDate: { from: '2026-10-24', to: '2026-10-25' }
- }
- props.sort = { sortField: 'createTime', sortOrder: 'asc' }
- props.modelValue = true
- await Vue.nextTick()
- assert.deepEqual(
- h.visibleDateDefs.value.map((d: any) => d.prop),
- ['planDate']
- )
- h.openDate('planDate', 'from')
- h.confirmDate({ selectedValues: ['2026', '10', '23'] })
- assert.equal(
- props.filters.planDate.from,
- '2026-10-24',
- 'date edits stay in draft until confirmed'
- )
- h.resetAndApply()
- assert.deepEqual(events.filter((event) => event[0] === 'apply').at(-1), [
- 'apply',
- { planNo: { selected: ['P1'] } },
- { sortField: 'createTime', sortOrder: 'asc' },
- true
- ])
- } finally {
- scope.stop()
- }
- })
- test('date entry opens only date fields and preserves applied quick dates until confirmed', () => {
- const source = ts.createSourceFile(
- 'list.ts',
- parse(readFileSync(new URL('../src/views/m/pl/plan/index.vue', import.meta.url), 'utf8'))
- .descriptor.scriptSetup!.content,
- ts.ScriptTarget.Latest,
- true
- )
- const names = [
- 'rangeFilters',
- 'onRangeOpen',
- 'onMoreOpen',
- 'onDateDropdownChange',
- 'pickDate',
- 'applyFilters'
- ]
- const code = source.statements
- .filter((node) =>
- ts.isFunctionDeclaration(node)
- ? names.includes(node.name!.text)
- : ts.isVariableStatement(node) &&
- node.declarationList.declarations.some((d) => names.includes(d.name.getText(source)))
- )
- .map((node) => node.getText(source))
- .join('\n')
- let reloads = 0
- const bindings = {
- computed: Vue.computed,
- filters: Vue.reactive<Record<string, any>>({ planNo: { selected: ['P1'] } }),
- dateShortcut: ref('today'),
- dateRange: ref(['2026-10-24', '2026-10-24']),
- customFrom: ref(''),
- customTo: ref(''),
- customDateLabel: ref(''),
- dateOnly: ref(false),
- filterOpen: ref(false),
- rangeDropdown: ref(null),
- moreDropdown: ref(null),
- sort: { sortField: 'createTime', sortOrder: 'asc' },
- reload: () => reloads++,
- closeFilter: () => {},
- rangeFor: () => ['2026-10-25', '2026-10-25']
- }
- const h = new Function(
- ...Object.keys(bindings),
- ts.transpile(code, { target: ts.ScriptTarget.ESNext }) +
- ';return {rangeFilters,onRangeOpen,onMoreOpen,onDateDropdownChange,pickDate,applyFilters}'
- )(...Object.values(bindings))
- h.onRangeOpen()
- assert.equal(bindings.dateOnly.value, true)
- assert.equal(bindings.filterOpen.value, true)
- assert.equal(bindings.dateShortcut.value, 'today')
- assert.equal(bindings.filters.planDate, undefined)
- assert.deepEqual(h.rangeFilters.value.planDate, { from: '2026-10-24', to: '2026-10-24' })
- bindings.filterOpen.value = false
- h.onDateDropdownChange('custom')
- assert.equal(bindings.dateShortcut.value, 'today')
- assert.equal(reloads, 0)
- h.applyFilters(
- { ...bindings.filters, planDate: { from: '2026-10-20', to: '2026-10-22' } },
- { ...bindings.sort }
- )
- assert.equal(bindings.dateShortcut.value, 'custom')
- assert.deepEqual(bindings.dateRange.value, [])
- assert.equal(bindings.customDateLabel.value, '2026-10-20 ~ 2026-10-22')
- h.onMoreOpen()
- assert.equal(bindings.dateOnly.value, false)
- h.pickDate('custom')
- assert.equal(bindings.filters.planDate.from, '2026-10-20')
- h.pickDate('tomorrow')
- assert.equal(bindings.filters.planDate, undefined)
- assert.deepEqual(bindings.dateRange.value, ['2026-10-25', '2026-10-25'])
- assert.deepEqual(bindings.filters.planNo, { selected: ['P1'] })
- })
|