优化
This commit is contained in:
@@ -15,7 +15,6 @@ import {
|
||||
import { listAllPlatformWarehouses, type PlatformWarehouseResource } from '../../../warehouses/api/platformWarehouses'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import DataTable from '@/shared/components/DataTable.vue'
|
||||
import { printLocationLabels } from '../utils/locationLabelPrint'
|
||||
import LocationsView from './LocationsView.vue'
|
||||
|
||||
const locationsSource = readFileSync(resolve(process.cwd(), 'src/modules/warehouse-resources/locations/pages/LocationsView.vue'), 'utf8')
|
||||
@@ -29,10 +28,6 @@ vi.mock('../api/platformLocations', () => ({
|
||||
changePlatformLocationStatus: vi.fn(),
|
||||
deletePlatformLocation: vi.fn(),
|
||||
}))
|
||||
vi.mock('../utils/locationLabelPrint', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../utils/locationLabelPrint')>()
|
||||
return { ...actual, printLocationLabels: vi.fn(() => true) }
|
||||
})
|
||||
|
||||
class ResizeObserverStub implements ResizeObserver {
|
||||
observe() {}
|
||||
@@ -182,7 +177,6 @@ describe('LocationsView', () => {
|
||||
location: availableLocation, warehouseCode: warehouse.warehouseCode, warehouseName: warehouse.warehouseName,
|
||||
})
|
||||
vi.mocked(createPlatformLocation).mockReset()
|
||||
vi.mocked(printLocationLabels).mockClear()
|
||||
})
|
||||
|
||||
it('loads only domestic warehouse candidates for querying and writes', async () => {
|
||||
@@ -242,7 +236,7 @@ describe('LocationsView', () => {
|
||||
expect(wrapper.text()).toContain('义乌收货仓')
|
||||
expect(wrapper.text()).toContain('A1-01')
|
||||
expect(wrapper.findAll('thead th').map((item) => item.text().trim())).toEqual([
|
||||
'操作', '', '库位编码', '所属仓库', '库区', '库位类型', '状态',
|
||||
'操作', '库位编码', '所属仓库', '库区', '库位类型', '状态',
|
||||
])
|
||||
})
|
||||
|
||||
@@ -286,7 +280,6 @@ describe('LocationsView', () => {
|
||||
expect(bodyRows[1]?.text()).not.toContain('详情')
|
||||
expect(bodyRows[1]?.text()).not.toContain('编辑')
|
||||
expect(bodyRows[1]?.text()).not.toContain('更多')
|
||||
expect(bodyRows[1]?.text()).toContain('打印')
|
||||
})
|
||||
|
||||
it('shows all available actions directly when a row has no more than three', async () => {
|
||||
@@ -294,7 +287,6 @@ describe('LocationsView', () => {
|
||||
const row = wrapper.find('tbody tr')
|
||||
|
||||
expect(row.text()).not.toContain('详情')
|
||||
expect(row.text()).toContain('打印')
|
||||
expect(row.text()).toContain('停用')
|
||||
expect(row.text()).not.toContain('更多')
|
||||
})
|
||||
@@ -308,7 +300,6 @@ describe('LocationsView', () => {
|
||||
|
||||
expect(row.text()).not.toContain('详情')
|
||||
expect(row.text()).toContain('编辑')
|
||||
expect(row.text()).toContain('打印')
|
||||
expect(row.text()).toContain('启用')
|
||||
expect(row.text()).toContain('删除')
|
||||
expect(row.text()).not.toContain('更多')
|
||||
@@ -347,36 +338,7 @@ describe('LocationsView', () => {
|
||||
|
||||
expect(occupiedRow?.text()).not.toContain('详情')
|
||||
expect(occupiedRow?.text()).not.toContain('编辑')
|
||||
expect(occupiedRow?.findAll('button').map((button) => button.text().trim())).toEqual(['打印'])
|
||||
})
|
||||
|
||||
it('prints one row directly and supports printing the selected current-page rows', async () => {
|
||||
const wrapper = await mountView(['platform:location:list'])
|
||||
const bodyRows = wrapper.findAll('tbody tr')
|
||||
const rowPrint = bodyRows[0]?.findAll('button').find((button) => button.text().trim() === '打印')
|
||||
|
||||
await rowPrint!.trigger('click')
|
||||
expect(printLocationLabels).toHaveBeenLastCalledWith([{
|
||||
locationCode: 'A1-01',
|
||||
locationName: 'A1 区 01 位',
|
||||
warehouseCode: 'CN-YW-01',
|
||||
warehouseName: '义乌收货仓',
|
||||
zoneCode: 'A',
|
||||
zoneName: '常规区',
|
||||
}])
|
||||
|
||||
wrapper.findComponent(DataTable).vm.$emit('selection-change', [availableLocation, occupiedLocation])
|
||||
await flushPromises()
|
||||
const batchPrint = wrapper.find('.table-panel .data-table__toolbar').findAll('button')
|
||||
.find((button) => button.text().includes('打印所选'))
|
||||
expect(batchPrint?.attributes('disabled')).toBeUndefined()
|
||||
expect(batchPrint?.text()).toContain('2')
|
||||
await batchPrint!.trigger('click')
|
||||
|
||||
expect(printLocationLabels).toHaveBeenLastCalledWith(expect.arrayContaining([
|
||||
expect.objectContaining({ locationCode: 'A1-01' }),
|
||||
expect.objectContaining({ locationCode: 'A1-02' }),
|
||||
]))
|
||||
expect(occupiedRow?.findAll('button')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('clears the warehouse on reset and immediately restores the full query', async () => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import '@/styles/adminDensity.css'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { MapPin, Plus, Printer, Save } from '@lucide/vue'
|
||||
import { MapPin, Plus, Save } from '@lucide/vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
type SavePlatformLocationRequest,
|
||||
} from '../api/platformLocations'
|
||||
import { listAllPlatformWarehouses, type PlatformWarehouseResource } from '../../../warehouses/api/platformWarehouses'
|
||||
import { printLocationLabels, type LocationLabelPrintItem } from '../utils/locationLabelPrint'
|
||||
|
||||
type EditorMode = 'create' | 'edit'
|
||||
type LocationFilters = QueryModel & {
|
||||
@@ -52,7 +51,6 @@ const warehouseError = ref<ApiErrorDetails | null>(null)
|
||||
const loading = ref(false)
|
||||
const loadError = ref<ApiErrorDetails | null>(null)
|
||||
const rows = ref<PlatformLocationResource[]>([])
|
||||
const selectedRows = ref<PlatformLocationResource[]>([])
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const total = ref(0)
|
||||
@@ -172,8 +170,7 @@ const typeLabels: Record<PlatformLocationType, string> = {
|
||||
}
|
||||
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 300, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'selection', type: 'selection', width: 48, fixed: 'left' },
|
||||
{ key: 'actions', label: '操作', width: 240, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'location', label: '库位编码', minWidth: 190, slotName: 'location' },
|
||||
{ key: 'warehouse', label: '所属仓库', minWidth: 210, slotName: 'warehouse' },
|
||||
{ key: 'zone', label: '库区', minWidth: 150, slotName: 'zone' },
|
||||
@@ -242,32 +239,6 @@ function recordScope(row: PlatformLocationResource) {
|
||||
return { warehouseId: apiId(String(row.warehouseId)) }
|
||||
}
|
||||
|
||||
function printItem(row: PlatformLocationResource): LocationLabelPrintItem {
|
||||
const warehouse = warehouses.value.find((item) => String(item.id) === String(row.warehouseId))
|
||||
return {
|
||||
locationCode: row.locationCode,
|
||||
locationName: row.locationName,
|
||||
warehouseCode: row.warehouseCode || warehouse?.warehouseCode,
|
||||
warehouseName: row.warehouseName || warehouse?.warehouseName,
|
||||
zoneCode: row.zoneCode,
|
||||
zoneName: row.zoneName,
|
||||
}
|
||||
}
|
||||
|
||||
function printRows(targets: readonly PlatformLocationResource[]) {
|
||||
if (!targets.length) {
|
||||
ElMessage.warning('请先勾选需要打印的库位')
|
||||
return
|
||||
}
|
||||
if (!printLocationLabels(targets.map(printItem))) {
|
||||
ElMessage.warning('打印窗口被浏览器拦截,请允许弹出窗口后重试')
|
||||
}
|
||||
}
|
||||
|
||||
function onSelectionChange(selection: object[]) {
|
||||
selectedRows.value = selection as PlatformLocationResource[]
|
||||
}
|
||||
|
||||
async function loadWarehouses() {
|
||||
const sequence = ++warehouseLoadSequence
|
||||
warehouseLoading.value = true
|
||||
@@ -304,12 +275,10 @@ async function loadLocations() {
|
||||
const result = await listPlatformLocations(query)
|
||||
if (sequence !== locationLoadSequence) return
|
||||
rows.value = result.records
|
||||
selectedRows.value = []
|
||||
total.value = result.total
|
||||
} catch (error) {
|
||||
if (sequence !== locationLoadSequence) return
|
||||
rows.value = []
|
||||
selectedRows.value = []
|
||||
total.value = 0
|
||||
loadError.value = getApiErrorDetails(error, '库位列表加载失败,请稍后重试')
|
||||
} finally {
|
||||
@@ -558,20 +527,10 @@ onMounted(() => {
|
||||
row-key="id"
|
||||
aria-label="库位列表"
|
||||
:empty-text="emptyDescription"
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<template #toolbar-actions>
|
||||
<el-button :disabled="loading || !selectedRows.length" @click="printRows(selectedRows)"><Printer :size="16" />打印所选<span v-if="selectedRows.length">({{ selectedRows.length }})</span></el-button>
|
||||
<el-button v-if="access.can('platform:location:create')" type="primary" @click="openCreate"><Plus :size="16" />新增库位</el-button>
|
||||
</template>
|
||||
<template #toolbar-actions><el-button v-if="access.can('platform:location:create')" type="primary" @click="openCreate"><Plus :size="16" />新增库位</el-button></template>
|
||||
<template #actions="{ row }">
|
||||
<div class="location-actions density-actions">
|
||||
<el-button
|
||||
size="small"
|
||||
class="density-action"
|
||||
:disabled="loading"
|
||||
@click="printRows([row as PlatformLocationResource])"
|
||||
>打印</el-button>
|
||||
<el-button
|
||||
v-if="canEdit(row as PlatformLocationResource)"
|
||||
size="small"
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildLocationLabelPrintHtml } from './locationLabelPrint'
|
||||
|
||||
describe('location label printing', () => {
|
||||
it('builds one 100 × 70 mm label per location with the code as the primary content', () => {
|
||||
const html = buildLocationLabelPrintHtml([
|
||||
{
|
||||
locationCode: 'A1-01',
|
||||
locationName: 'A1 区 01 位',
|
||||
warehouseName: '义乌收货仓',
|
||||
warehouseCode: 'CN-YW-01',
|
||||
zoneName: '常规区',
|
||||
zoneCode: 'A',
|
||||
},
|
||||
{ locationCode: 'B2-08', warehouseName: '深圳仓', zoneCode: 'B2' },
|
||||
])
|
||||
|
||||
expect(html).toContain('@page { size: 100mm 70mm; margin: 0; }')
|
||||
expect(html.match(/class="location-label"/g)).toHaveLength(2)
|
||||
expect(html).toContain('<div class="location-code">A1-01</div>')
|
||||
expect(html).toContain('义乌收货仓 · CN-YW-01')
|
||||
expect(html).toContain('常规区 · A')
|
||||
})
|
||||
|
||||
it('escapes server-provided label content before writing the print document', () => {
|
||||
const html = buildLocationLabelPrintHtml([{
|
||||
locationCode: '<img src=x onerror=alert(1)>',
|
||||
warehouseName: 'A&B',
|
||||
zoneName: '"危险区"',
|
||||
}])
|
||||
|
||||
expect(html).not.toContain('<img src=x')
|
||||
expect(html).toContain('<img src=x onerror=alert(1)>')
|
||||
expect(html).toContain('A&B')
|
||||
expect(html).toContain('"危险区"')
|
||||
})
|
||||
})
|
||||
@@ -1,74 +0,0 @@
|
||||
export interface LocationLabelPrintItem {
|
||||
locationCode: string
|
||||
locationName?: string | null
|
||||
warehouseCode?: string | null
|
||||
warehouseName?: string | null
|
||||
zoneCode?: string | null
|
||||
zoneName?: string | null
|
||||
}
|
||||
|
||||
function escapeHtml(value: string | null | undefined) {
|
||||
return String(value ?? '')
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''')
|
||||
}
|
||||
|
||||
function secondaryText(name: string | null | undefined, code: string | null | undefined) {
|
||||
const values = [name, code].map((value) => String(value || '').trim()).filter(Boolean)
|
||||
return [...new Set(values)].join(' · ') || '—'
|
||||
}
|
||||
|
||||
export function buildLocationLabelPrintHtml(items: readonly LocationLabelPrintItem[]) {
|
||||
const labels = items.map((item) => `
|
||||
<section class="location-label">
|
||||
<div class="label-kind">库位</div>
|
||||
<div class="location-code">${escapeHtml(item.locationCode)}</div>
|
||||
${item.locationName ? `<div class="location-name">${escapeHtml(item.locationName)}</div>` : ''}
|
||||
<dl>
|
||||
<div><dt>仓库</dt><dd>${escapeHtml(secondaryText(item.warehouseName, item.warehouseCode))}</dd></div>
|
||||
<div><dt>库区</dt><dd>${escapeHtml(secondaryText(item.zoneName, item.zoneCode))}</dd></div>
|
||||
</dl>
|
||||
</section>`).join('')
|
||||
|
||||
return `<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>库位标签打印</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; color: #111827; font-family: Arial, "Microsoft YaHei", sans-serif; }
|
||||
.location-label { width: 100mm; height: 70mm; padding: 7mm; display: flex; flex-direction: column; justify-content: center; border: 1px solid #d1d5db; break-after: page; page-break-after: always; }
|
||||
.location-label:last-child { break-after: auto; page-break-after: auto; }
|
||||
.label-kind { margin-bottom: 2mm; font-size: 11pt; font-weight: 700; letter-spacing: .24em; text-align: center; }
|
||||
.location-code { overflow-wrap: anywhere; font-size: 34pt; font-weight: 800; line-height: 1.05; text-align: center; }
|
||||
.location-name { margin-top: 2mm; overflow-wrap: anywhere; font-size: 12pt; font-weight: 600; text-align: center; }
|
||||
dl { margin: 5mm 0 0; padding-top: 3mm; border-top: 1px solid #9ca3af; font-size: 10pt; }
|
||||
dl div { display: grid; grid-template-columns: 12mm minmax(0, 1fr); gap: 2mm; margin-top: 1.5mm; }
|
||||
dt { color: #4b5563; }
|
||||
dd { margin: 0; overflow-wrap: anywhere; font-weight: 600; }
|
||||
@page { size: 100mm 70mm; margin: 0; }
|
||||
@media screen { body { display: grid; gap: 12px; justify-content: center; padding: 16px; background: #eef2f7; } .location-label { background: #fff; box-shadow: 0 4px 16px rgba(15, 23, 42, .12); } }
|
||||
@media print { .location-label { border: 0; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>${labels}</body>
|
||||
</html>`
|
||||
}
|
||||
|
||||
export function printLocationLabels(items: readonly LocationLabelPrintItem[]) {
|
||||
if (!items.length) return false
|
||||
const popup = window.open('', '_blank', 'noopener,noreferrer')
|
||||
if (!popup) return false
|
||||
popup.opener = null
|
||||
popup.document.open()
|
||||
popup.document.write(buildLocationLabelPrintHtml(items))
|
||||
popup.document.close()
|
||||
popup.focus()
|
||||
popup.print()
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user