From 6e042a90361bb68e7a641af3aea30f9bea7716cf Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期一, 30 三月 2026 17:45:18 +0800
Subject: [PATCH] feat: add loc basic info page
---
rsf-design/src/router/adapters/backendMenuAdapter.js | 1
rsf-design/src/views/basic-info/loc/modules/loc-detail-drawer.vue | 80 ++
rsf-design/src/views/basic-info/loc/modules/loc-dialog.vue | 378 ++++++++++
rsf-design/src/views/basic-info/loc/locTable.columns.js | 153 ++++
rsf-design/src/router/routes/staticRoutes.js | 10
rsf-design/src/api/loc.js | 248 +++++++
rsf-design/tests/basic-info-loc-page-contract.test.mjs | 266 +++++++
rsf-design/src/views/basic-info/loc/index.vue | 437 ++++++++++++
rsf-design/src/views/basic-info/loc/locPage.helpers.js | 477 +++++++++++++
9 files changed, 2,050 insertions(+), 0 deletions(-)
diff --git a/rsf-design/src/api/loc.js b/rsf-design/src/api/loc.js
new file mode 100644
index 0000000..4213715
--- /dev/null
+++ b/rsf-design/src/api/loc.js
@@ -0,0 +1,248 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const parsed = Number(value)
+ return Number.isNaN(parsed) ? fallback : parsed
+}
+
+function normalizeTypeIds(typeIds = []) {
+ if (Array.isArray(typeIds)) {
+ return typeIds
+ .map((item) => normalizeNumber(item, void 0))
+ .filter((item) => item !== void 0 && item !== null)
+ }
+
+ if (typeof typeIds === 'string' && typeIds.trim()) {
+ return typeIds
+ .split(',')
+ .map((item) => normalizeNumber(item, void 0))
+ .filter((item) => item !== void 0 && item !== null)
+ }
+
+ return []
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildLocPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildLocSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ warehouseId:
+ params.warehouseId !== undefined && params.warehouseId !== null && params.warehouseId !== ''
+ ? Number(params.warehouseId)
+ : void 0,
+ areaId:
+ params.areaId !== undefined && params.areaId !== null && params.areaId !== ''
+ ? Number(params.areaId)
+ : void 0,
+ code: normalizeText(params.code),
+ useStatus: normalizeText(params.useStatus),
+ row:
+ params.row !== undefined && params.row !== null && params.row !== ''
+ ? Number(params.row)
+ : void 0,
+ col:
+ params.col !== undefined && params.col !== null && params.col !== ''
+ ? Number(params.col)
+ : void 0,
+ lev:
+ params.lev !== undefined && params.lev !== null && params.lev !== ''
+ ? Number(params.lev)
+ : void 0,
+ channel:
+ params.channel !== undefined && params.channel !== null && params.channel !== ''
+ ? Number(params.channel)
+ : void 0,
+ status:
+ params.status !== undefined && params.status !== null && params.status !== ''
+ ? Number(params.status)
+ : void 0,
+ barcode: normalizeText(params.barcode),
+ memo: normalizeText(params.memo)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildLocSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== undefined && formData.id !== null && formData.id !== ''
+ ? { id: Number(formData.id) }
+ : {}),
+ ...(formData.version !== undefined && formData.version !== null && formData.version !== ''
+ ? { version: Number(formData.version) }
+ : {}),
+ ...(formData.warehouseId !== undefined && formData.warehouseId !== null && formData.warehouseId !== ''
+ ? { warehouseId: Number(formData.warehouseId) }
+ : {}),
+ ...(formData.areaId !== undefined && formData.areaId !== null && formData.areaId !== ''
+ ? { areaId: Number(formData.areaId) }
+ : {}),
+ code: normalizeText(formData.code) || '',
+ typeIds: normalizeTypeIds(formData.typeIds),
+ ...(formData.flagLogic !== undefined && formData.flagLogic !== null && formData.flagLogic !== ''
+ ? { flagLogic: Number(formData.flagLogic) }
+ : {}),
+ fucAtrrs: normalizeText(formData.fucAtrrs) || '',
+ barcode: normalizeText(formData.barcode) || '',
+ unit: normalizeText(formData.unit) || '',
+ ...(formData.length !== undefined && formData.length !== null && formData.length !== ''
+ ? { length: Number(formData.length) }
+ : {}),
+ ...(formData.height !== undefined && formData.height !== null && formData.height !== ''
+ ? { height: Number(formData.height) }
+ : {}),
+ ...(formData.width !== undefined && formData.width !== null && formData.width !== ''
+ ? { width: Number(formData.width) }
+ : {}),
+ ...(formData.row !== undefined && formData.row !== null && formData.row !== ''
+ ? { row: Number(formData.row) }
+ : {}),
+ ...(formData.col !== undefined && formData.col !== null && formData.col !== ''
+ ? { col: Number(formData.col) }
+ : {}),
+ ...(formData.lev !== undefined && formData.lev !== null && formData.lev !== ''
+ ? { lev: Number(formData.lev) }
+ : {}),
+ ...(formData.channel !== undefined && formData.channel !== null && formData.channel !== ''
+ ? { channel: Number(formData.channel) }
+ : {}),
+ ...(formData.maxParts !== undefined && formData.maxParts !== null && formData.maxParts !== ''
+ ? { maxParts: Number(formData.maxParts) }
+ : {}),
+ ...(formData.maxPack !== undefined && formData.maxPack !== null && formData.maxPack !== ''
+ ? { maxPack: Number(formData.maxPack) }
+ : {}),
+ useStatus: normalizeText(formData.useStatus) || 'O',
+ ...(formData.flagLabelMange !== undefined && formData.flagLabelMange !== null && formData.flagLabelMange !== ''
+ ? { flagLabelMange: Number(formData.flagLabelMange) }
+ : {}),
+ locAttrs: normalizeText(formData.locAttrs) || '',
+ status:
+ formData.status !== undefined && formData.status !== null && formData.status !== ''
+ ? Number(formData.status)
+ : 1,
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+export function fetchLocPage(params = {}) {
+ return request.post({
+ url: '/loc/page',
+ params: buildLocPageParams(params)
+ })
+}
+
+export function fetchGetLocDetail(id) {
+ return request.get({
+ url: `/loc/${id}`
+ })
+}
+
+export function fetchGetLocMany(ids) {
+ return request.post({
+ url: `/loc/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchSaveLoc(params = {}) {
+ return request.post({
+ url: '/loc/save',
+ params: buildLocSavePayload(params)
+ })
+}
+
+export function fetchUpdateLoc(params = {}) {
+ return request.post({
+ url: '/loc/update',
+ params: buildLocSavePayload(params)
+ })
+}
+
+export function fetchDeleteLoc(ids) {
+ return request.post({
+ url: `/loc/remove/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchLocQuery(condition = '') {
+ return request.post({
+ url: '/loc/query',
+ params: {
+ condition: normalizeText(condition)
+ }
+ })
+}
+
+export function fetchLocTypeList() {
+ return request.post({
+ url: '/locType/list',
+ data: {}
+ })
+}
+
+export function fetchWarehouseList() {
+ return request.post({
+ url: '/warehouse/list',
+ data: {}
+ })
+}
+
+export function fetchWarehouseAreasList() {
+ return request.post({
+ url: '/warehouseAreas/list',
+ data: {}
+ })
+}
+
+export async function fetchExportLocReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/loc/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/router/adapters/backendMenuAdapter.js b/rsf-design/src/router/adapters/backendMenuAdapter.js
index bc62f9d..cabcb5d 100644
--- a/rsf-design/src/router/adapters/backendMenuAdapter.js
+++ b/rsf-design/src/router/adapters/backendMenuAdapter.js
@@ -23,6 +23,7 @@
basContainer: '/basic-info/bas-container',
warehouse: '/basic-info/warehouse',
warehouseAreas: '/basic-info/warehouse-areas',
+ loc: '/basic-info/loc',
warehouseStock: '/stock/warehouse-stock',
warehouseAreasItem: '/stock/warehouse-areas-item',
qlyInspect: '/manager/qly-inspect',
diff --git a/rsf-design/src/router/routes/staticRoutes.js b/rsf-design/src/router/routes/staticRoutes.js
index 4988184..13cebd2 100644
--- a/rsf-design/src/router/routes/staticRoutes.js
+++ b/rsf-design/src/router/routes/staticRoutes.js
@@ -80,6 +80,16 @@
icon: 'ri:layout-grid-line',
keepAlive: false
}
+ },
+ {
+ path: 'loc',
+ name: 'Loc',
+ component: () => import('@views/basic-info/loc/index.vue'),
+ meta: {
+ title: 'menu.loc',
+ icon: 'ri:map-pin-2-line',
+ keepAlive: false
+ }
}
]
},
diff --git a/rsf-design/src/views/basic-info/loc/index.vue b/rsf-design/src/views/basic-info/loc/index.vue
new file mode 100644
index 0000000..c4287c6
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc/index.vue
@@ -0,0 +1,437 @@
+<template>
+ <div class="loc-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板搴撲綅</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <LocDialog
+ v-model:visible="dialogVisible"
+ :dialog-type="dialogType"
+ :loc-data="currentLocData"
+ :warehouse-options="warehouseOptions"
+ :area-options="areaOptions"
+ :loc-type-options="locTypeOptions"
+ @submit="handleDialogSubmit"
+ />
+
+ <LocDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { fetchWarehouseAreasList, fetchWarehouseList } from '@/api/warehouse-areas'
+ import {
+ fetchDeleteLoc,
+ fetchExportLocReport,
+ fetchGetLocDetail,
+ fetchGetLocMany,
+ fetchLocPage,
+ fetchLocTypeList,
+ fetchSaveLoc,
+ fetchUpdateLoc
+ } from '@/api/loc'
+ import LocDialog from './modules/loc-dialog.vue'
+ import LocDetailDrawer from './modules/loc-detail-drawer.vue'
+ import { createLocTableColumns } from './locTable.columns'
+ import {
+ buildLocDialogModel,
+ buildLocPageQueryParams,
+ buildLocPrintRows,
+ buildLocReportMeta,
+ buildLocSavePayload,
+ buildLocSearchParams,
+ createLocSearchState,
+ getLocPaginationKey,
+ getLocStatusOptions,
+ getLocUseStatusOptions,
+ normalizeLocListRow,
+ resolveLocAreaOptions,
+ resolveLocTypeOptions,
+ resolveLocWarehouseOptions,
+ LOC_REPORT_STYLE,
+ LOC_REPORT_TITLE
+ } from './locPage.helpers'
+
+ defineOptions({ name: 'Loc' })
+
+ const { hasAuth } = useAuth()
+ const userStore = useUserStore()
+
+ const searchForm = ref(createLocSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ const warehouseOptions = ref([])
+ const areaOptions = ref([])
+ const locTypeOptions = ref([])
+ let handleDeleteAction = null
+
+ const reportTitle = LOC_REPORT_TITLE
+ const reportQueryParams = computed(() => buildLocSearchParams(searchForm.value))
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ簱浣嶅彿/瀹瑰櫒缂栫爜/澶囨敞'
+ }
+ },
+ {
+ label: '浠撳簱',
+ key: 'warehouseId',
+ type: 'select',
+ props: {
+ clearable: true,
+ filterable: true,
+ options: warehouseOptions.value
+ }
+ },
+ {
+ label: '搴撳尯',
+ key: 'areaId',
+ type: 'select',
+ props: {
+ clearable: true,
+ filterable: true,
+ options: areaOptions.value.filter((item) => {
+ if (!searchForm.value.warehouseId) {
+ return true
+ }
+ if (item?.warehouseId === undefined || item?.warehouseId === null) {
+ return true
+ }
+ return Number(item.warehouseId) === Number(searchForm.value.warehouseId)
+ })
+ }
+ },
+ {
+ label: '搴撲綅鍙�',
+ key: 'code',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ簱浣嶅彿'
+ }
+ },
+ {
+ label: '浣跨敤鐘舵��',
+ key: 'useStatus',
+ type: 'select',
+ props: {
+ clearable: true,
+ filterable: true,
+ options: getLocUseStatusOptions()
+ }
+ },
+ {
+ label: '鎺�',
+ key: 'row',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユ帓'
+ }
+ },
+ {
+ label: '鍒�',
+ key: 'col',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ュ垪'
+ }
+ },
+ {
+ label: '灞�',
+ key: 'lev',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ュ眰'
+ }
+ },
+ {
+ label: '宸烽亾',
+ key: 'channel',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ュ贩閬�'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getLocStatusOptions()
+ }
+ },
+ {
+ label: '瀹瑰櫒缂栫爜',
+ key: 'barcode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ鍣ㄧ紪鐮�'
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ }
+ ])
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ const detail = await guardRequestWithMessage(fetchGetLocDetail(row.id), {}, {
+ timeoutMessage: '搴撲綅璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ detailData.value = normalizeLocListRow(detail)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇搴撲綅璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ const detail = await guardRequestWithMessage(fetchGetLocDetail(row.id), {}, {
+ timeoutMessage: '搴撲綅璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ showDialog('edit', detail)
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇搴撲綅璇︽儏澶辫触')
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ } = useTable({
+ core: {
+ apiFn: fetchLocPage,
+ apiParams: buildLocPageQueryParams(searchForm.value),
+ paginationKey: getLocPaginationKey(),
+ columnsFactory: () =>
+ createLocTableColumns({
+ handleView: openDetail,
+ handleEdit: hasAuth('update') ? openEditDialog : null,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null,
+ canEdit: hasAuth('update'),
+ canDelete: hasAuth('delete')
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeLocListRow(item))
+ }
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentLocData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => buildLocDialogModel(),
+ buildEditModel: (record) => buildLocDialogModel(record),
+ buildSavePayload: (formData) => buildLocSavePayload(formData),
+ saveRequest: fetchSaveLoc,
+ updateRequest: fetchUpdateLoc,
+ deleteRequest: fetchDeleteLoc,
+ entityName: '搴撲綅',
+ resolveRecordLabel: (record) => record?.code || record?.barcode || record?.id,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+ handleDeleteAction = handleDelete
+
+ const buildPreviewDialogMeta = (rows) => {
+ const now = new Date()
+ return {
+ reportTitle,
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length
+ }
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ const response = Array.isArray(payload?.ids) && payload.ids.length > 0
+ ? await fetchGetLocMany(payload.ids)
+ : await fetchLocPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ return defaultResponseAdapter(response).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'loc.xlsx',
+ requestExport: (payload) =>
+ fetchExportLocReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildLocPrintRows(records),
+ buildPreviewMeta: (rows) => buildPreviewDialogMeta(rows)
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildLocReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ titleAlign: LOC_REPORT_STYLE.titleAlign,
+ titleLevel: LOC_REPORT_STYLE.titleLevel
+ })
+ )
+
+ async function loadWarehouseOptions() {
+ const response = await guardRequestWithMessage(fetchWarehouseList(), [], {
+ timeoutMessage: '浠撳簱閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ warehouseOptions.value = resolveLocWarehouseOptions(defaultResponseAdapter(response).records)
+ }
+
+ async function loadAreaOptions() {
+ const response = await guardRequestWithMessage(fetchWarehouseAreasList(), [], {
+ timeoutMessage: '搴撳尯閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ areaOptions.value = resolveLocAreaOptions(defaultResponseAdapter(response).records)
+ }
+
+ async function loadLocTypeOptions() {
+ const response = await guardRequestWithMessage(fetchLocTypeList(), [], {
+ timeoutMessage: '搴撲綅绫诲瀷閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ locTypeOptions.value = resolveLocTypeOptions(defaultResponseAdapter(response).records)
+ }
+
+ function handleSearch(params) {
+ replaceSearchParams(buildLocSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createLocSearchState())
+ resetSearchParams()
+ }
+
+ onMounted(async () => {
+ await Promise.all([loadWarehouseOptions(), loadAreaOptions(), loadLocTypeOptions()])
+ })
+</script>
diff --git a/rsf-design/src/views/basic-info/loc/locPage.helpers.js b/rsf-design/src/views/basic-info/loc/locPage.helpers.js
new file mode 100644
index 0000000..333ddf8
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc/locPage.helpers.js
@@ -0,0 +1,477 @@
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success', bool: true },
+ 0: { text: '鍐荤粨', type: 'danger', bool: false }
+}
+
+const USE_STATUS_META = {
+ O: { text: '绌哄簱', type: 'success' },
+ D: { text: '绌烘澘', type: 'info' },
+ R: { text: '棰勭害鍑哄簱', type: 'warning' },
+ S: { text: '棰勭害鍏ュ簱', type: 'warning' },
+ X: { text: '绂佺敤', type: 'danger' },
+ F: { text: '鍦ㄥ簱', type: 'primary' }
+}
+
+export const LOC_REPORT_TITLE = '搴撲綅鎶ヨ〃'
+export const LOC_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'portrait',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const parsed = Number(value)
+ return Number.isNaN(parsed) ? fallback : parsed
+}
+
+function normalizeFlagText(value) {
+ if (value === 1 || value === '1' || value === true || value === '鏄�') {
+ return '鏄�'
+ }
+ if (value === 0 || value === '0' || value === false || value === '鍚�') {
+ return '鍚�'
+ }
+ return normalizeText(value) || '--'
+}
+
+function normalizeTypeIds(typeIds = []) {
+ if (Array.isArray(typeIds)) {
+ return typeIds
+ .map((item) => normalizeNumber(item, void 0))
+ .filter((item) => item !== void 0 && item !== null)
+ }
+
+ if (typeof typeIds === 'string' && typeIds.trim()) {
+ return typeIds
+ .split(',')
+ .map((item) => normalizeNumber(item, void 0))
+ .filter((item) => item !== void 0 && item !== null)
+ }
+
+ return []
+}
+
+export function createLocSearchState() {
+ return {
+ condition: '',
+ warehouseId: '',
+ areaId: '',
+ code: '',
+ useStatus: '',
+ row: '',
+ col: '',
+ lev: '',
+ channel: '',
+ status: '',
+ barcode: '',
+ memo: ''
+ }
+}
+
+export function createLocFormState() {
+ return {
+ id: void 0,
+ version: void 0,
+ warehouseId: void 0,
+ areaId: void 0,
+ code: '',
+ typeIds: [],
+ flagLogic: 0,
+ fucAtrrs: '',
+ barcode: '',
+ unit: '',
+ length: void 0,
+ height: void 0,
+ width: void 0,
+ row: void 0,
+ col: void 0,
+ lev: void 0,
+ channel: void 0,
+ maxParts: void 0,
+ maxPack: void 0,
+ useStatus: 'O',
+ flagLabelMange: 0,
+ locAttrs: '',
+ status: 1,
+ memo: ''
+ }
+}
+
+export function getLocPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getLocStatusOptions() {
+ return [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+}
+
+export function getLocUseStatusOptions() {
+ return [
+ { label: '绌哄簱', value: 'O' },
+ { label: '绌烘澘', value: 'D' },
+ { label: '棰勭害鍑哄簱', value: 'R' },
+ { label: '棰勭害鍏ュ簱', value: 'S' },
+ { label: '绂佺敤', value: 'X' },
+ { label: '鍦ㄥ簱', value: 'F' }
+ ]
+}
+
+export function getLocBinaryOptions() {
+ return [
+ { label: '鍚�', value: 0 },
+ { label: '鏄�', value: 1 }
+ ]
+}
+
+export function getLocStatusMeta(status) {
+ if (status === true || Number(status) === 1) {
+ return STATUS_META[1]
+ }
+ if (status === false || Number(status) === 0) {
+ return STATUS_META[0]
+ }
+ return { text: '鏈煡', type: 'info', bool: false }
+}
+
+export function getLocUseStatusMeta(useStatus) {
+ if (!useStatus) {
+ return { text: '鏈煡', type: 'info' }
+ }
+ return USE_STATUS_META[String(useStatus).trim()] || { text: String(useStatus), type: 'info' }
+}
+
+export function buildLocSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ warehouseId:
+ params.warehouseId !== undefined && params.warehouseId !== null && params.warehouseId !== ''
+ ? Number(params.warehouseId)
+ : void 0,
+ areaId:
+ params.areaId !== undefined && params.areaId !== null && params.areaId !== ''
+ ? Number(params.areaId)
+ : void 0,
+ code: normalizeText(params.code),
+ useStatus: normalizeText(params.useStatus),
+ row:
+ params.row !== undefined && params.row !== null && params.row !== ''
+ ? Number(params.row)
+ : void 0,
+ col:
+ params.col !== undefined && params.col !== null && params.col !== ''
+ ? Number(params.col)
+ : void 0,
+ lev:
+ params.lev !== undefined && params.lev !== null && params.lev !== ''
+ ? Number(params.lev)
+ : void 0,
+ channel:
+ params.channel !== undefined && params.channel !== null && params.channel !== ''
+ ? Number(params.channel)
+ : void 0,
+ status:
+ params.status !== undefined && params.status !== null && params.status !== ''
+ ? Number(params.status)
+ : void 0,
+ barcode: normalizeText(params.barcode),
+ memo: normalizeText(params.memo)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildLocPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildLocSearchParams(params)
+ }
+}
+
+export function buildLocSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== undefined && formData.id !== null && formData.id !== ''
+ ? { id: Number(formData.id) }
+ : {}),
+ ...(formData.version !== undefined && formData.version !== null && formData.version !== ''
+ ? { version: Number(formData.version) }
+ : {}),
+ ...(formData.warehouseId !== undefined && formData.warehouseId !== null && formData.warehouseId !== ''
+ ? { warehouseId: Number(formData.warehouseId) }
+ : {}),
+ ...(formData.areaId !== undefined && formData.areaId !== null && formData.areaId !== ''
+ ? { areaId: Number(formData.areaId) }
+ : {}),
+ code: normalizeText(formData.code) || '',
+ typeIds: normalizeTypeIds(formData.typeIds),
+ ...(formData.flagLogic !== undefined && formData.flagLogic !== null && formData.flagLogic !== ''
+ ? { flagLogic: Number(formData.flagLogic) }
+ : {}),
+ fucAtrrs: normalizeText(formData.fucAtrrs) || '',
+ barcode: normalizeText(formData.barcode) || '',
+ unit: normalizeText(formData.unit) || '',
+ ...(formData.length !== undefined && formData.length !== null && formData.length !== ''
+ ? { length: Number(formData.length) }
+ : {}),
+ ...(formData.height !== undefined && formData.height !== null && formData.height !== ''
+ ? { height: Number(formData.height) }
+ : {}),
+ ...(formData.width !== undefined && formData.width !== null && formData.width !== ''
+ ? { width: Number(formData.width) }
+ : {}),
+ ...(formData.row !== undefined && formData.row !== null && formData.row !== ''
+ ? { row: Number(formData.row) }
+ : {}),
+ ...(formData.col !== undefined && formData.col !== null && formData.col !== ''
+ ? { col: Number(formData.col) }
+ : {}),
+ ...(formData.lev !== undefined && formData.lev !== null && formData.lev !== ''
+ ? { lev: Number(formData.lev) }
+ : {}),
+ ...(formData.channel !== undefined && formData.channel !== null && formData.channel !== ''
+ ? { channel: Number(formData.channel) }
+ : {}),
+ ...(formData.maxParts !== undefined && formData.maxParts !== null && formData.maxParts !== ''
+ ? { maxParts: Number(formData.maxParts) }
+ : {}),
+ ...(formData.maxPack !== undefined && formData.maxPack !== null && formData.maxPack !== ''
+ ? { maxPack: Number(formData.maxPack) }
+ : {}),
+ useStatus: normalizeText(formData.useStatus) || 'O',
+ ...(formData.flagLabelMange !== undefined && formData.flagLabelMange !== null && formData.flagLabelMange !== ''
+ ? { flagLabelMange: Number(formData.flagLabelMange) }
+ : {}),
+ locAttrs: normalizeText(formData.locAttrs) || '',
+ status:
+ formData.status !== undefined && formData.status !== null && formData.status !== ''
+ ? Number(formData.status)
+ : 1,
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+export function buildLocDialogModel(record = {}) {
+ return {
+ ...createLocFormState(),
+ ...(record.id !== undefined && record.id !== null && record.id !== '' ? { id: Number(record.id) } : {}),
+ ...(record.version !== undefined && record.version !== null && record.version !== ''
+ ? { version: Number(record.version) }
+ : {}),
+ warehouseId:
+ record.warehouseId !== undefined && record.warehouseId !== null && record.warehouseId !== ''
+ ? Number(record.warehouseId)
+ : void 0,
+ areaId:
+ record.areaId !== undefined && record.areaId !== null && record.areaId !== ''
+ ? Number(record.areaId)
+ : void 0,
+ code: normalizeText(record.code || ''),
+ typeIds: normalizeTypeIds(record.typeIds ?? record.type ?? ''),
+ flagLogic:
+ record.flagLogic !== undefined && record.flagLogic !== null && record.flagLogic !== ''
+ ? Number(record.flagLogic)
+ : 0,
+ fucAtrrs: normalizeText(record.fucAtrrs || ''),
+ barcode: normalizeText(record.barcode || ''),
+ unit: normalizeText(record.unit || ''),
+ length:
+ record.length !== undefined && record.length !== null && record.length !== ''
+ ? Number(record.length)
+ : void 0,
+ height:
+ record.height !== undefined && record.height !== null && record.height !== ''
+ ? Number(record.height)
+ : void 0,
+ width:
+ record.width !== undefined && record.width !== null && record.width !== ''
+ ? Number(record.width)
+ : void 0,
+ row:
+ record.row !== undefined && record.row !== null && record.row !== ''
+ ? Number(record.row)
+ : void 0,
+ col:
+ record.col !== undefined && record.col !== null && record.col !== ''
+ ? Number(record.col)
+ : void 0,
+ lev:
+ record.lev !== undefined && record.lev !== null && record.lev !== ''
+ ? Number(record.lev)
+ : void 0,
+ channel:
+ record.channel !== undefined && record.channel !== null && record.channel !== ''
+ ? Number(record.channel)
+ : void 0,
+ maxParts:
+ record.maxParts !== undefined && record.maxParts !== null && record.maxParts !== ''
+ ? Number(record.maxParts)
+ : void 0,
+ maxPack:
+ record.maxPack !== undefined && record.maxPack !== null && record.maxPack !== ''
+ ? Number(record.maxPack)
+ : void 0,
+ useStatus: normalizeText(record.useStatus || 'O') || 'O',
+ flagLabelMange:
+ record.flagLabelMange !== undefined && record.flagLabelMange !== null && record.flagLabelMange !== ''
+ ? Number(record.flagLabelMange)
+ : 0,
+ locAttrs: normalizeText(record.locAttrs || ''),
+ status: record.status !== undefined && record.status !== null ? Number(record.status) : 1,
+ memo: normalizeText(record.memo || '')
+ }
+}
+
+export function normalizeLocDetailRecord(record = {}) {
+ const statusMeta = getLocStatusMeta(record.statusBool ?? record.status)
+ const useStatusMeta = getLocUseStatusMeta(record.useStatus)
+ const typeIds = normalizeTypeIds(record.typeIds ?? record.type ?? '')
+ return {
+ ...record,
+ warehouseName: normalizeText(record.warehouseId$ || record.warehouseName || ''),
+ areaName: normalizeText(record.areaId$ || record.areaName || ''),
+ typeIds,
+ typeIdsText: normalizeText(record.typeIds$ || record.type$ || record.typeText || record.type || ''),
+ code: normalizeText(record.code || ''),
+ barcode: normalizeText(record.barcode || ''),
+ unit: normalizeText(record.unit || ''),
+ fucAtrrs: normalizeText(record.fucAtrrs || ''),
+ locAttrs: normalizeText(record.locAttrs || ''),
+ memo: normalizeText(record.memo || ''),
+ flagLogicText: normalizeFlagText(record.flagLogic),
+ flagLabelMangeText: normalizeFlagText(record.flagLabelMange),
+ useStatusText: useStatusMeta.text,
+ useStatusType: useStatusMeta.type,
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+ row: record.row !== undefined && record.row !== null ? Number(record.row) : void 0,
+ col: record.col !== undefined && record.col !== null ? Number(record.col) : void 0,
+ lev: record.lev !== undefined && record.lev !== null ? Number(record.lev) : void 0,
+ channel: record.channel !== undefined && record.channel !== null ? Number(record.channel) : void 0,
+ length: record.length !== undefined && record.length !== null ? Number(record.length) : void 0,
+ height: record.height !== undefined && record.height !== null ? Number(record.height) : void 0,
+ width: record.width !== undefined && record.width !== null ? Number(record.width) : void 0,
+ maxParts: record.maxParts !== undefined && record.maxParts !== null ? Number(record.maxParts) : void 0,
+ maxPack: record.maxPack !== undefined && record.maxPack !== null ? Number(record.maxPack) : void 0,
+ createByText: normalizeText(record.createBy$ || record.createByText || ''),
+ createTimeText: normalizeText(record.createTime$ || record.createTime || ''),
+ updateByText: normalizeText(record.updateBy$ || record.updateByText || ''),
+ updateTimeText: normalizeText(record.updateTime$ || record.updateTime || '')
+ }
+}
+
+export function normalizeLocListRow(record = {}) {
+ return normalizeLocDetailRecord(record)
+}
+
+export function buildLocPrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeLocListRow(record))
+}
+
+export function buildLocReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = LOC_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: LOC_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...LOC_REPORT_STYLE,
+ orientation
+ }
+ }
+}
+
+export function resolveLocWarehouseOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+
+ return records
+ .map((item) => {
+ if (!item || typeof item !== 'object') {
+ return null
+ }
+ const value = item.id ?? item.value
+ if (value === void 0 || value === null || value === '') {
+ return null
+ }
+ return {
+ value: Number(value),
+ label: normalizeText(item.name || item.code || `浠撳簱 ${value}`)
+ }
+ })
+ .filter(Boolean)
+}
+
+export function resolveLocAreaOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+
+ return records
+ .map((item) => {
+ if (!item || typeof item !== 'object') {
+ return null
+ }
+ const value = item.id ?? item.value
+ if (value === void 0 || value === null || value === '') {
+ return null
+ }
+ return {
+ value: Number(value),
+ label: normalizeText(item.name || item.code || `搴撳尯 ${value}`),
+ warehouseId:
+ item.warehouseId !== undefined && item.warehouseId !== null && item.warehouseId !== ''
+ ? Number(item.warehouseId)
+ : void 0
+ }
+ })
+ .filter(Boolean)
+}
+
+export function resolveLocTypeOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+
+ return records
+ .map((item) => {
+ if (!item || typeof item !== 'object') {
+ return null
+ }
+ const value = item.id ?? item.value
+ if (value === void 0 || value === null || value === '') {
+ return null
+ }
+ return {
+ value: Number(value),
+ label: normalizeText(item.name || item.code || item.label || `绫诲瀷 ${value}`)
+ }
+ })
+ .filter(Boolean)
+}
diff --git a/rsf-design/src/views/basic-info/loc/locTable.columns.js b/rsf-design/src/views/basic-info/loc/locTable.columns.js
new file mode 100644
index 0000000..5817307
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc/locTable.columns.js
@@ -0,0 +1,153 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import { getLocStatusMeta, getLocUseStatusMeta } from './locPage.helpers'
+
+export function createLocTableColumns({
+ handleView,
+ handleEdit,
+ handleDelete,
+ canEdit = true,
+ canDelete = true
+} = {}) {
+ const operations = [{ key: 'view', label: '璇︽儏', icon: 'ri:eye-line' }]
+
+ if (canEdit && handleEdit) {
+ operations.push({ key: 'edit', label: '缂栬緫', icon: 'ri:pencil-line' })
+ }
+
+ if (canDelete && handleDelete) {
+ operations.push({ key: 'delete', label: '鍒犻櫎', icon: 'ri:delete-bin-5-line', color: 'var(--art-error)' })
+ }
+
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'code',
+ label: '搴撲綅鍙�',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.code || '--'
+ },
+ {
+ prop: 'warehouseName',
+ label: '浠撳簱',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.warehouseName || row.warehouseId$ || '--'
+ },
+ {
+ prop: 'areaName',
+ label: '搴撳尯',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.areaName || row.areaId$ || '--'
+ },
+ {
+ prop: 'typeIdsText',
+ label: '搴撲綅绫诲瀷',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.typeIdsText || '--'
+ },
+ {
+ prop: 'row',
+ label: '鎺�',
+ width: 80,
+ align: 'center',
+ formatter: (row) => row.row ?? '--'
+ },
+ {
+ prop: 'col',
+ label: '鍒�',
+ width: 80,
+ align: 'center',
+ formatter: (row) => row.col ?? '--'
+ },
+ {
+ prop: 'lev',
+ label: '灞�',
+ width: 80,
+ align: 'center',
+ formatter: (row) => row.lev ?? '--'
+ },
+ {
+ prop: 'channel',
+ label: '宸烽亾',
+ width: 90,
+ align: 'center',
+ formatter: (row) => row.channel ?? '--'
+ },
+ {
+ prop: 'useStatus',
+ label: '浣跨敤鐘舵��',
+ width: 110,
+ align: 'center',
+ formatter: (row) => {
+ const statusMeta = getLocUseStatusMeta(row.useStatus)
+ return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
+ }
+ },
+ {
+ prop: 'flagLogicText',
+ label: '铏氭嫙搴撲綅',
+ width: 110,
+ align: 'center',
+ formatter: (row) => row.flagLogicText || '--'
+ },
+ {
+ prop: 'flagLabelMangeText',
+ label: '鏍囩绠$悊',
+ width: 110,
+ align: 'center',
+ formatter: (row) => row.flagLabelMangeText || '--'
+ },
+ {
+ prop: 'barcode',
+ label: '瀹瑰櫒缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.barcode || '--'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 100,
+ align: 'center',
+ formatter: (row) => {
+ const statusMeta = getLocStatusMeta(row.statusBool ?? row.status)
+ return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
+ }
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || row.updateTime$ || '--'
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.memo || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 160,
+ align: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: operations,
+ onClick: (item) => {
+ if (item.key === 'view') handleView?.(row)
+ if (item.key === 'edit') handleEdit?.(row)
+ if (item.key === 'delete') handleDelete?.(row)
+ }
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/basic-info/loc/modules/loc-detail-drawer.vue b/rsf-design/src/views/basic-info/loc/modules/loc-detail-drawer.vue
new file mode 100644
index 0000000..24d1c24
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc/modules/loc-detail-drawer.vue
@@ -0,0 +1,80 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="搴撲綅璇︽儏"
+ size="960px"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
+ <div v-if="loading" class="py-6">
+ <ElSkeleton :rows="14" animated />
+ </div>
+ <div v-else class="space-y-4">
+ <ElDescriptions title="鍩虹淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="浠撳簱">{{ detail.warehouseName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撳尯">{{ detail.areaName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撲綅鍙�">{{ detail.code || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撲綅绫诲瀷">{{ detail.typeIdsText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浣跨敤鐘舵��">
+ <ElTag :type="detail.useStatusType || 'info'" effect="light">
+ {{ detail.useStatusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">
+ {{ detail.statusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="瀹瑰櫒缂栫爜">{{ detail.barcode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀛樻斁鍗曚綅">{{ detail.unit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="浣嶇疆淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鎺�">{{ detail.row ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒�">{{ detail.col ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="灞�">{{ detail.lev ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸烽亾">{{ detail.channel ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="铏氭嫙搴撲綅">{{ detail.flagLogicText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏍囩绠$悊">{{ detail.flagLabelMangeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="闀�">{{ detail.length ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀹�">{{ detail.width ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="楂�">{{ detail.height ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏈�澶ч浂浠舵暟">{{ detail.maxParts ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏈�澶у寘瑁呮暟">{{ detail.maxPack ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍔熻兘灞炴��" :span="2">{{ detail.fucAtrrs || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="灞炴��" :span="2">{{ detail.locAttrs || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="瀹¤淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const visible = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+
+ function handleVisibleChange(value) {
+ visible.value = value
+ }
+</script>
diff --git a/rsf-design/src/views/basic-info/loc/modules/loc-dialog.vue b/rsf-design/src/views/basic-info/loc/modules/loc-dialog.vue
new file mode 100644
index 0000000..a3a88c2
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc/modules/loc-dialog.vue
@@ -0,0 +1,378 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="980px"
+ align-center
+ destroy-on-close
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import { computed, nextTick, reactive, ref, watch } from 'vue'
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import {
+ buildLocDialogModel,
+ createLocFormState,
+ getLocBinaryOptions,
+ getLocStatusOptions,
+ getLocUseStatusOptions
+ } from '../locPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ dialogType: { type: String, default: 'add' },
+ locData: { type: Object, default: () => ({}) },
+ warehouseOptions: { type: Array, default: () => [] },
+ areaOptions: { type: Array, default: () => [] },
+ locTypeOptions: { type: Array, default: () => [] }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createLocFormState())
+
+ const isEdit = computed(() => props.dialogType === 'edit')
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫搴撲綅' : '鏂板搴撲綅'))
+
+ const filteredAreaOptions = computed(() => {
+ if (!form.warehouseId) {
+ return props.areaOptions
+ }
+ return (props.areaOptions || []).filter((item) => {
+ if (item?.warehouseId === undefined || item?.warehouseId === null) {
+ return true
+ }
+ return Number(item.warehouseId) === Number(form.warehouseId)
+ })
+ })
+
+ const rules = computed(() => ({
+ warehouseId: [{ required: true, message: '璇烽�夋嫨浠撳簱', trigger: 'change' }],
+ areaId: [{ required: true, message: '璇烽�夋嫨搴撳尯', trigger: 'change' }],
+ code: [{ required: true, message: '璇疯緭鍏ュ簱浣嶅彿', trigger: 'blur' }],
+ typeIds: [{ type: 'array', required: true, message: '璇烽�夋嫨搴撲綅绫诲瀷', trigger: 'change' }],
+ row: [{ required: true, message: '璇疯緭鍏ユ帓', trigger: 'change' }],
+ col: [{ required: true, message: '璇疯緭鍏ュ垪', trigger: 'change' }],
+ lev: [{ required: true, message: '璇疯緭鍏ュ眰', trigger: 'change' }],
+ useStatus: [{ required: true, message: '璇烽�夋嫨浣跨敤鐘舵��', trigger: 'change' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '浠撳簱',
+ key: 'warehouseId',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨浠撳簱',
+ clearable: true,
+ filterable: true,
+ options: props.warehouseOptions
+ }
+ },
+ {
+ label: '搴撳尯',
+ key: 'areaId',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨搴撳尯',
+ clearable: true,
+ filterable: true,
+ options: filteredAreaOptions.value
+ }
+ },
+ {
+ label: '搴撲綅鍙�',
+ key: 'code',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ュ簱浣嶅彿',
+ clearable: true
+ }
+ },
+ {
+ label: '搴撲綅绫诲瀷',
+ key: 'typeIds',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨搴撲綅绫诲瀷',
+ clearable: true,
+ multiple: true,
+ collapseTags: true,
+ filterable: true,
+ options: props.locTypeOptions
+ }
+ },
+ {
+ label: '浣跨敤鐘舵��',
+ key: 'useStatus',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨浣跨敤鐘舵��',
+ clearable: true,
+ filterable: true,
+ options: getLocUseStatusOptions()
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鐘舵��',
+ clearable: true,
+ options: getLocStatusOptions()
+ }
+ },
+ {
+ label: '鎺�',
+ key: 'row',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユ帓'
+ }
+ },
+ {
+ label: '鍒�',
+ key: 'col',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ュ垪'
+ }
+ },
+ {
+ label: '灞�',
+ key: 'lev',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ュ眰'
+ }
+ },
+ {
+ label: '宸烽亾',
+ key: 'channel',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ュ贩閬�'
+ }
+ },
+ {
+ label: '瀹瑰櫒缂栫爜',
+ key: 'barcode',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ュ鍣ㄧ紪鐮�',
+ clearable: true
+ }
+ },
+ {
+ label: '瀛樻斁鍗曚綅',
+ key: 'unit',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ュ瓨鏀惧崟浣�',
+ clearable: true
+ }
+ },
+ {
+ label: '铏氭嫙搴撲綅',
+ key: 'flagLogic',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨铏氭嫙搴撲綅',
+ clearable: true,
+ options: getLocBinaryOptions()
+ }
+ },
+ {
+ label: '鏍囩绠$悊',
+ key: 'flagLabelMange',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鏍囩绠$悊',
+ clearable: true,
+ options: getLocBinaryOptions()
+ }
+ },
+ {
+ label: '闀�',
+ key: 'length',
+ type: 'number',
+ props: {
+ min: 0,
+ step: 0.01,
+ precision: 2,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ラ暱'
+ }
+ },
+ {
+ label: '瀹�',
+ key: 'width',
+ type: 'number',
+ props: {
+ min: 0,
+ step: 0.01,
+ precision: 2,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ュ'
+ }
+ },
+ {
+ label: '楂�',
+ key: 'height',
+ type: 'number',
+ props: {
+ min: 0,
+ step: 0.01,
+ precision: 2,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ラ珮'
+ }
+ },
+ {
+ label: '鏈�澶ч浂浠舵暟',
+ key: 'maxParts',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユ渶澶ч浂浠舵暟'
+ }
+ },
+ {
+ label: '鏈�澶у寘瑁呮暟',
+ key: 'maxPack',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユ渶澶у寘瑁呮暟'
+ }
+ },
+ {
+ label: '鍔熻兘灞炴��',
+ key: 'fucAtrrs',
+ type: 'input',
+ span: 24,
+ props: {
+ placeholder: '璇疯緭鍏ュ姛鑳藉睘鎬�',
+ clearable: true
+ }
+ },
+ {
+ label: '灞炴��',
+ key: 'locAttrs',
+ type: 'input',
+ span: 24,
+ props: {
+ placeholder: '璇疯緭鍏ュ睘鎬�',
+ clearable: true
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�',
+ clearable: true
+ }
+ }
+ ])
+
+ const loadFormData = () => {
+ Object.assign(form, buildLocDialogModel(props.locData))
+ }
+
+ const resetForm = () => {
+ Object.assign(form, createLocFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ const handleCancel = () => {
+ emit('update:visible', false)
+ }
+
+ const handleClosed = () => {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => {
+ formRef.value?.clearValidate?.()
+ })
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.locData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+
+ watch(
+ () => form.warehouseId,
+ () => {
+ if (!form.areaId) {
+ return
+ }
+ const available = filteredAreaOptions.value.some((item) => Number(item.value) === Number(form.areaId))
+ if (!available) {
+ form.areaId = void 0
+ }
+ }
+ )
+</script>
diff --git a/rsf-design/tests/basic-info-loc-page-contract.test.mjs b/rsf-design/tests/basic-info-loc-page-contract.test.mjs
new file mode 100644
index 0000000..a7f5dee
--- /dev/null
+++ b/rsf-design/tests/basic-info-loc-page-contract.test.mjs
@@ -0,0 +1,266 @@
+import assert from 'node:assert/strict'
+import { readFileSync } from 'node:fs'
+import test from 'node:test'
+
+const pageModuleUrl = new URL('../src/views/basic-info/loc/index.vue', import.meta.url)
+const helpersModuleUrl = new URL('../src/views/basic-info/loc/locPage.helpers.js', import.meta.url)
+const columnsModuleUrl = new URL('../src/views/basic-info/loc/locTable.columns.js', import.meta.url)
+const apiModuleUrl = new URL('../src/api/loc.js', import.meta.url)
+const backendMenuAdapterUrl = new URL('../src/router/adapters/backendMenuAdapter.js', import.meta.url)
+const staticRoutesUrl = new URL('../src/router/routes/staticRoutes.js', import.meta.url)
+
+test('loc api exposes the dedicated basic-info backend contract', async () => {
+ const apiSource = readFileSync(apiModuleUrl, 'utf8')
+
+ assert.match(apiSource, /fetchLocPage/)
+ assert.match(apiSource, /fetchGetLocDetail/)
+ assert.match(apiSource, /fetchGetLocMany/)
+ assert.match(apiSource, /fetchSaveLoc/)
+ assert.match(apiSource, /fetchUpdateLoc/)
+ assert.match(apiSource, /fetchDeleteLoc/)
+ assert.match(apiSource, /fetchLocQuery/)
+ assert.match(apiSource, /fetchLocTypeList/)
+ assert.match(apiSource, /fetchWarehouseList/)
+ assert.match(apiSource, /fetchWarehouseAreasList/)
+ assert.match(apiSource, /fetchExportLocReport/)
+ assert.match(apiSource, /url:\s*'\/loc\/page'/)
+ assert.match(apiSource, /url:\s*'\/loc\/save'/)
+ assert.match(apiSource, /url:\s*'\/loc\/update'/)
+ assert.match(apiSource, /url:\s*`\/loc\/remove\/\$\{normalizeIds\(ids\)\}`/)
+ assert.match(apiSource, /url:\s*'\/locType\/list'/)
+})
+
+test('loc helpers keep page, save and detail contracts stable', async () => {
+ const helpers = await import(helpersModuleUrl)
+
+ assert.deepEqual(helpers.createLocSearchState(), {
+ condition: '',
+ warehouseId: '',
+ areaId: '',
+ code: '',
+ useStatus: '',
+ row: '',
+ col: '',
+ lev: '',
+ channel: '',
+ status: '',
+ barcode: '',
+ memo: ''
+ })
+
+ assert.deepEqual(helpers.getLocPaginationKey(), {
+ current: 'current',
+ size: 'pageSize'
+ })
+
+ assert.deepEqual(
+ helpers.buildLocPageQueryParams({
+ current: 2,
+ pageSize: 30,
+ condition: ' A鍖� ',
+ warehouseId: '8',
+ areaId: '9',
+ code: ' LOC-01 ',
+ useStatus: ' O ',
+ row: '1',
+ col: '2',
+ lev: '3',
+ channel: '4',
+ status: '1',
+ barcode: ' BOX-01 ',
+ memo: ' memo '
+ }),
+ {
+ current: 2,
+ pageSize: 30,
+ condition: 'A鍖�',
+ warehouseId: 8,
+ areaId: 9,
+ code: 'LOC-01',
+ useStatus: 'O',
+ row: 1,
+ col: 2,
+ lev: 3,
+ channel: 4,
+ status: 1,
+ barcode: 'BOX-01',
+ memo: 'memo'
+ }
+ )
+
+ assert.deepEqual(
+ helpers.buildLocSavePayload({
+ id: '8',
+ version: '3',
+ warehouseId: '5',
+ areaId: '6',
+ code: ' LOC-01 ',
+ typeIds: ['1', '2'],
+ flagLogic: 1,
+ fucAtrrs: ' 鍔熻兘 ',
+ barcode: ' BOX-01 ',
+ unit: ' 绠� ',
+ length: '1.2',
+ height: '2.3',
+ width: '3.4',
+ row: '1',
+ col: '2',
+ lev: '3',
+ channel: '4',
+ maxParts: '10',
+ maxPack: '20',
+ useStatus: 'F',
+ flagLabelMange: 0,
+ locAttrs: ' 灞炴�� ',
+ status: '',
+ memo: ' 澶囨敞 '
+ }),
+ {
+ id: 8,
+ version: 3,
+ warehouseId: 5,
+ areaId: 6,
+ code: 'LOC-01',
+ typeIds: [1, 2],
+ flagLogic: 1,
+ fucAtrrs: '鍔熻兘',
+ barcode: 'BOX-01',
+ unit: '绠�',
+ length: 1.2,
+ height: 2.3,
+ width: 3.4,
+ row: 1,
+ col: 2,
+ lev: 3,
+ channel: 4,
+ maxParts: 10,
+ maxPack: 20,
+ useStatus: 'F',
+ flagLabelMange: 0,
+ locAttrs: '灞炴��',
+ status: 1,
+ memo: '澶囨敞'
+ }
+ )
+
+ const detail = helpers.normalizeLocDetailRecord({
+ id: 1,
+ warehouseId: 4,
+ warehouseId$: '涓讳粨',
+ areaId: 8,
+ areaId$: 'A鍖�',
+ type: '1,2',
+ typeIds$: '楂樺簱浣�,涓簱浣�',
+ useStatus: 'O',
+ status: 1,
+ flagLogic: 1,
+ flagLabelMange: 0,
+ code: ' LOC-01 ',
+ barcode: ' BOX-01 ',
+ unit: ' 绠� ',
+ row: 1,
+ col: 2,
+ lev: 3,
+ channel: 4,
+ length: 1.2,
+ height: 2.3,
+ width: 3.4,
+ maxParts: 10,
+ maxPack: 20,
+ memo: ' memo ',
+ createBy$: 'ROOT',
+ updateBy$: 'ROOT',
+ createTime$: '2026-03-30 10:00:00',
+ updateTime$: '2026-03-30 10:10:00'
+ })
+
+ assert.equal(detail.warehouseName, '涓讳粨')
+ assert.equal(detail.areaName, 'A鍖�')
+ assert.equal(detail.typeIdsText, '楂樺簱浣�,涓簱浣�')
+ assert.equal(detail.useStatusText, '绌哄簱')
+ assert.equal(detail.statusText, '姝e父')
+ assert.equal(detail.flagLogicText, '鏄�')
+ assert.equal(detail.flagLabelMangeText, '鍚�')
+ assert.equal(detail.memo, 'memo')
+
+ assert.deepEqual(helpers.buildLocPrintRows([{ id: 2, code: 'LOC-02', useStatus: 'F', status: 0 }]), [
+ {
+ id: 2,
+ code: 'LOC-02',
+ useStatus: 'F',
+ status: 0,
+ warehouseName: '',
+ areaName: '',
+ typeIds: [],
+ typeIdsText: '',
+ barcode: '',
+ unit: '',
+ fucAtrrs: '',
+ locAttrs: '',
+ memo: '',
+ flagLogicText: '--',
+ flagLabelMangeText: '--',
+ useStatusText: '鍦ㄥ簱',
+ useStatusType: 'primary',
+ statusText: '鍐荤粨',
+ statusType: 'danger',
+ statusBool: false,
+ row: void 0,
+ col: void 0,
+ lev: void 0,
+ channel: void 0,
+ length: void 0,
+ height: void 0,
+ width: void 0,
+ maxParts: void 0,
+ maxPack: void 0,
+ createByText: '',
+ createTimeText: '',
+ updateByText: '',
+ updateTimeText: ''
+ }
+ ])
+
+ assert.deepEqual(helpers.buildLocReportMeta({ count: 12 }), {
+ reportTitle: '搴撲綅鎶ヨ〃',
+ reportDate: undefined,
+ printedAt: undefined,
+ operator: undefined,
+ count: 12,
+ reportStyle: {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'portrait',
+ density: 'compact',
+ showSequence: true
+ }
+ })
+})
+
+test('loc page skeleton uses ArtDesignPro list page structure', async () => {
+ const pageSource = readFileSync(pageModuleUrl, 'utf8')
+ const columnsSource = readFileSync(columnsModuleUrl, 'utf8')
+
+ assert.match(pageSource, /ArtSearchBar/)
+ assert.match(pageSource, /ArtTableHeader/)
+ assert.match(pageSource, /ListExportPrint/)
+ assert.match(pageSource, /LocDialog/)
+ assert.match(pageSource, /LocDetailDrawer/)
+ assert.match(pageSource, /useCrudPage/)
+ assert.match(pageSource, /usePrintExportPage/)
+ assert.match(columnsSource, /label:\s*'搴撲綅鍙�'/)
+ assert.match(columnsSource, /label:\s*'浠撳簱'/)
+ assert.match(columnsSource, /label:\s*'搴撳尯'/)
+ assert.match(columnsSource, /label:\s*'搴撲綅绫诲瀷'/)
+ assert.match(columnsSource, /label:\s*'浣跨敤鐘舵��'/)
+})
+
+test('backend menu adapter and static routes expose the loc page', async () => {
+ const backendMenuAdapterSource = readFileSync(backendMenuAdapterUrl, 'utf8')
+ const staticRoutesSource = readFileSync(staticRoutesUrl, 'utf8')
+
+ assert.match(backendMenuAdapterSource, /loc:\s*'\/basic-info\/loc'/)
+ assert.match(staticRoutesSource, /path:\s*'loc'/)
+ assert.match(staticRoutesSource, /title:\s*'menu\.loc'/)
+ assert.match(staticRoutesSource, /basic-info\/loc\/index\.vue/)
+})
--
Gitblit v1.9.1