zhou zhou
2 天以前 6e042a90361bb68e7a641af3aea30f9bea7716cf
feat: add loc basic info page
7个文件已添加
2个文件已修改
2050 ■■■■■ 已修改文件
rsf-design/src/api/loc.js 248 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/router/adapters/backendMenuAdapter.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/router/routes/staticRoutes.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/basic-info/loc/index.vue 437 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/basic-info/loc/locPage.helpers.js 477 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/basic-info/loc/locTable.columns.js 153 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/basic-info/loc/modules/loc-detail-drawer.vue 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/basic-info/loc/modules/loc-dialog.vue 378 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/tests/basic-info-loc-page-contract.test.mjs 266 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/api/loc.js
New file
@@ -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)
  })
}
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',
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
        }
      }
    ]
  },
rsf-design/src/views/basic-info/loc/index.vue
New file
@@ -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>
rsf-design/src/views/basic-info/loc/locPage.helpers.js
New file
@@ -0,0 +1,477 @@
const STATUS_META = {
  1: { text: '正常', 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: '正常', 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)
}
rsf-design/src/views/basic-info/loc/locTable.columns.js
New file
@@ -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)
          }
        })
    }
  ]
}
rsf-design/src/views/basic-info/loc/modules/loc-detail-drawer.vue
New file
@@ -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>
rsf-design/src/views/basic-info/loc/modules/loc-dialog.vue
New file
@@ -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>
rsf-design/tests/basic-info-loc-page-contract.test.mjs
New file
@@ -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, '正常')
  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/)
})