|
|
@@ -45,26 +45,59 @@
|
|
|
@current-change="loadList" @size-change="loadList" />
|
|
|
</div>
|
|
|
|
|
|
- <el-dialog v-model="dialogVisible" :title="dialogTitle" width="640px" destroy-on-close @closed="resetForm">
|
|
|
+ <el-dialog v-model="dialogVisible" :title="dialogTitle" width="920px" destroy-on-close @closed="resetForm">
|
|
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
|
|
|
- <el-form-item v-if="false" label="公司" prop="companyRefId">
|
|
|
- <el-select v-model="form.companyRefId" clearable filterable placeholder="请选择公司" style="width: 100%">
|
|
|
- <el-option v-for="item in companyOptions" :key="item.id" :label="item.name || item.code || `${item.id}`" :value="item.id" />
|
|
|
- </el-select>
|
|
|
+ <el-row :gutter="16">
|
|
|
+ <el-col :span="12"><el-form-item label="库位编码" prop="location"><el-input v-model="form.location" :disabled="!!editingId" /></el-form-item></el-col>
|
|
|
+ <el-col :span="12"><el-form-item label="库位说明"><el-input v-model="form.descr" /></el-form-item></el-col>
|
|
|
+ <el-col :span="12"><el-form-item label="货主/保管方"><el-input v-model="form.storer" /></el-form-item></el-col>
|
|
|
+ <el-col :span="12"><el-form-item label="库位类型"><el-input v-model="form.typed" placeholder="如 Supp" /></el-form-item></el-col>
|
|
|
+ <el-col :span="12"><el-form-item label="物理地址"><el-input v-model="form.physicalAddress" /></el-form-item></el-col>
|
|
|
+ <el-col :span="12"><el-form-item label="域编码"><el-input v-model="form.domainCode" /></el-form-item></el-col>
|
|
|
+ <el-col :span="12"><el-form-item label="启用"><el-switch v-model="form.isActive" /></el-form-item></el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <el-divider content-position="left">货架明细</el-divider>
|
|
|
+
|
|
|
+ <el-form :inline="true" class="gen-bar" @submit.prevent>
|
|
|
+ <el-form-item label="前缀">
|
|
|
+ <el-input v-model="gen.prefix" placeholder="可空" clearable style="width: 90px" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="货架序号">
|
|
|
+ <el-input-number v-model="gen.startNum" :min="1" :precision="0" :step="1" controls-position="right" placeholder="起" style="width: 110px" />
|
|
|
+ <span class="gen-sep">至</span>
|
|
|
+ <el-input-number v-model="gen.endNum" :min="1" :precision="0" :step="1" controls-position="right" placeholder="止" style="width: 110px" />
|
|
|
</el-form-item>
|
|
|
- <el-form-item v-if="false" label="工厂" prop="factoryRefId">
|
|
|
- <el-select v-model="form.factoryRefId" clearable filterable placeholder="请选择工厂" style="width: 100%" :disabled="!form.companyRefId">
|
|
|
- <el-option v-for="item in formFactoryOptions" :key="item.id" :label="item.name || item.code || `${item.id}`" :value="item.id" />
|
|
|
- </el-select>
|
|
|
+ <el-form-item label="层数">
|
|
|
+ <el-input-number v-model="gen.shelfRow" :min="1" :precision="0" :step="1" controls-position="right" style="width: 100px" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="列数">
|
|
|
+ <el-input-number v-model="gen.shelfCol" :min="1" :precision="0" :step="1" controls-position="right" style="width: 100px" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" @click="generateShelves">生成货架明细</el-button>
|
|
|
+ <el-button @click="addShelfRow">新增一行</el-button>
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="库位编码" prop="location"><el-input v-model="form.location" :disabled="!!editingId" /></el-form-item>
|
|
|
- <el-form-item label="库位说明"><el-input v-model="form.descr" /></el-form-item>
|
|
|
- <el-form-item label="货主/保管方"><el-input v-model="form.storer" /></el-form-item>
|
|
|
- <el-form-item label="库位类型"><el-input v-model="form.typed" placeholder="如 Supp" /></el-form-item>
|
|
|
- <el-form-item label="物理地址"><el-input v-model="form.physicalAddress" /></el-form-item>
|
|
|
- <el-form-item label="域编码"><el-input v-model="form.domainCode" /></el-form-item>
|
|
|
- <el-form-item label="启用"><el-switch v-model="form.isActive" /></el-form-item>
|
|
|
</el-form>
|
|
|
+
|
|
|
+ <el-table :data="shelves" border stripe size="small" max-height="300px" style="width: 100%">
|
|
|
+ <el-table-column type="index" label="#" width="50" align="center" />
|
|
|
+ <el-table-column label="货架" min-width="180">
|
|
|
+ <template #default="{ row }"><el-input v-model="row.invShelf" placeholder="货架编码" /></template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="描述" min-width="200">
|
|
|
+ <template #default="{ row }"><el-input v-model="row.descr" placeholder="描述" /></template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="区域" min-width="140">
|
|
|
+ <template #default="{ row }"><el-input v-model="row.area" placeholder="区域" /></template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="80" align="center">
|
|
|
+ <template #default="{ $index }"><el-button link type="danger" @click="removeShelfRow($index)">删除</el-button></template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ <div class="shelf-count">共 {{ shelves.length }} 条货架明细(保存上限 {{ MAX_SHELVES }} 条)</div>
|
|
|
+
|
|
|
<template #footer>
|
|
|
<el-button @click="dialogVisible = false">取消</el-button>
|
|
|
<el-button type="primary" :loading="saving" @click="submitForm">保存</el-button>
|
|
|
@@ -74,12 +107,13 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts" name="aidopS0WhLocation">
|
|
|
-import { computed, onMounted, reactive, ref, watch } from 'vue';
|
|
|
+import { computed, onMounted, reactive, ref } from 'vue';
|
|
|
import { useRoute } from 'vue-router';
|
|
|
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
|
|
|
import AidopDemoShell from '../../components/AidopDemoShell.vue';
|
|
|
-import { s0LocationsApi, type S0LocationRow, type S0LocationUpsert } from '../api/s0WarehouseApi';
|
|
|
-import { loadOrgList, type OrgOption } from '../api/s0SalesApi';
|
|
|
+import { s0LocationsApi, type S0LocationRow, type S0LocationShelfInput, type S0LocationUpsert } from '../api/s0WarehouseApi';
|
|
|
+
|
|
|
+const MAX_SHELVES = 5000;
|
|
|
|
|
|
const route = useRoute();
|
|
|
const pageTitle = computed(() => (route.meta?.title as string) || '库位维护');
|
|
|
@@ -94,6 +128,10 @@ const saving = ref(false);
|
|
|
const formRef = ref<FormInstance>();
|
|
|
const form = reactive<S0LocationUpsert>({ companyRefId: undefined, factoryRefId: undefined, domainCode: '', location: '', descr: '', storer: '', typed: '', physicalAddress: '', isActive: true });
|
|
|
|
|
|
+// 货架明细行 + 批量生成参数
|
|
|
+const shelves = ref<S0LocationShelfInput[]>([]);
|
|
|
+const gen = reactive<{ prefix: string; startNum?: number; endNum?: number; shelfRow?: number; shelfCol?: number }>({ prefix: '', startNum: undefined, endNum: undefined, shelfRow: 1, shelfCol: 1 });
|
|
|
+
|
|
|
// 单甲方私有云口径:公司/工厂不在前端启用,统一注入全局默认组织(不依赖登录账号 tenant_id)
|
|
|
const DEFAULT_COMPANY_REF_ID = '1329900200001';
|
|
|
const DEFAULT_FACTORY_REF_ID = '1329900200002';
|
|
|
@@ -104,26 +142,11 @@ function applyDefaultOrg() {
|
|
|
if (isMissingOrLegacyOrgRef(form.companyRefId)) form.companyRefId = DEFAULT_COMPANY_REF_ID;
|
|
|
if (isMissingOrLegacyOrgRef(form.factoryRefId)) form.factoryRefId = DEFAULT_FACTORY_REF_ID;
|
|
|
}
|
|
|
-const companyOptions = ref<OrgOption[]>([]);
|
|
|
-const factoryOptions = ref<OrgOption[]>([]);
|
|
|
-const formFactoryOptions = computed(() => {
|
|
|
- if (!form.companyRefId) return factoryOptions.value;
|
|
|
- return factoryOptions.value.filter((item) => item.pid === form.companyRefId);
|
|
|
-});
|
|
|
|
|
|
const rules: FormRules = {
|
|
|
location: [{ required: true, message: '请填写库位编码', trigger: 'blur' }],
|
|
|
};
|
|
|
|
|
|
-watch(
|
|
|
- () => form.companyRefId,
|
|
|
- () => {
|
|
|
- if (!formFactoryOptions.value.some((item) => item.id === form.factoryRefId)) {
|
|
|
- form.factoryRefId = undefined;
|
|
|
- }
|
|
|
- },
|
|
|
-);
|
|
|
-
|
|
|
async function loadList() {
|
|
|
loading.value = true;
|
|
|
try {
|
|
|
@@ -132,20 +155,112 @@ async function loadList() {
|
|
|
} catch { rows.value = []; total.value = 0; } finally { loading.value = false; }
|
|
|
}
|
|
|
function resetQuery() { Object.assign(query, { keyword: '', domainCode: '', typed: '', isActive: undefined, page: 1 }); void loadList(); }
|
|
|
-function resetForm() { editingId.value = null; Object.assign(form, { companyRefId: undefined, factoryRefId: undefined, domainCode: '', location: '', descr: '', storer: '', typed: '', physicalAddress: '', isActive: true }); formRef.value?.clearValidate(); }
|
|
|
+function resetForm() {
|
|
|
+ editingId.value = null;
|
|
|
+ Object.assign(form, { companyRefId: undefined, factoryRefId: undefined, domainCode: '', location: '', descr: '', storer: '', typed: '', physicalAddress: '', isActive: true });
|
|
|
+ shelves.value = [];
|
|
|
+ Object.assign(gen, { prefix: '', startNum: undefined, endNum: undefined, shelfRow: 1, shelfCol: 1 });
|
|
|
+ formRef.value?.clearValidate();
|
|
|
+}
|
|
|
function openCreate() { resetForm(); dialogTitle.value = '新增库位'; dialogVisible.value = true; }
|
|
|
-function openEdit(row: S0LocationRow) {
|
|
|
- resetForm(); editingId.value = row.id; dialogTitle.value = `编辑库位 ${row.location}`;
|
|
|
- Object.assign(form, { companyRefId: row.companyRefId, factoryRefId: row.factoryRefId, domainCode: row.domainCode ?? '', location: row.location, descr: row.descr ?? '', storer: row.storer ?? '', typed: row.typed ?? '', physicalAddress: row.physicalAddress ?? '', isActive: row.isActive });
|
|
|
+async function openEdit(row: S0LocationRow) {
|
|
|
+ resetForm();
|
|
|
+ editingId.value = row.id;
|
|
|
+ dialogTitle.value = `编辑库位 ${row.location}`;
|
|
|
+ // 编辑必须调用详情接口回显主表 + 货架明细,不再仅用列表 row 回填
|
|
|
+ try {
|
|
|
+ const detail = await s0LocationsApi.get(row.id);
|
|
|
+ Object.assign(form, { companyRefId: detail.companyRefId, factoryRefId: detail.factoryRefId, domainCode: detail.domainCode ?? '', location: detail.location, descr: detail.descr ?? '', storer: detail.storer ?? '', typed: detail.typed ?? '', physicalAddress: detail.physicalAddress ?? '', isActive: detail.isActive });
|
|
|
+ shelves.value = (detail.shelves ?? []).map((s) => ({ id: s.id, invShelf: s.invShelf, descr: s.descr ?? '', area: s.area ?? '' }));
|
|
|
+ } catch {
|
|
|
+ ElMessage.error('加载库位详情失败');
|
|
|
+ return;
|
|
|
+ }
|
|
|
dialogVisible.value = true;
|
|
|
}
|
|
|
+
|
|
|
+// ===== 批量生成 =====
|
|
|
+const pad = (v: number) => String(v).padStart(2, '0');
|
|
|
+function isPositiveInt(v: unknown): boolean {
|
|
|
+ const n = Number(v);
|
|
|
+ return Number.isFinite(n) && Number.isInteger(n) && n > 0;
|
|
|
+}
|
|
|
+// 层/列:空或 0 按 1;填写后必须为正整数,否则返回 null 表示非法
|
|
|
+function resolveDim(v: unknown): number | null {
|
|
|
+ if (v === '' || v === null || v === undefined) return 1;
|
|
|
+ const n = Number(v);
|
|
|
+ if (n === 0) return 1;
|
|
|
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n < 0) return null;
|
|
|
+ return n;
|
|
|
+}
|
|
|
+function generateShelves() {
|
|
|
+ const start = gen.startNum;
|
|
|
+ const end = gen.endNum;
|
|
|
+ if (!isPositiveInt(start)) { ElMessage.warning('请填写正整数的货架序号起始值'); return; }
|
|
|
+ if (!isPositiveInt(end)) { ElMessage.warning('请填写正整数的货架序号结束值'); return; }
|
|
|
+ if ((end as number) < (start as number)) { ElMessage.warning('货架序号结束值不能小于起始值'); return; }
|
|
|
+ const row = resolveDim(gen.shelfRow);
|
|
|
+ const col = resolveDim(gen.shelfCol);
|
|
|
+ if (row === null) { ElMessage.warning('层数必须为正整数'); return; }
|
|
|
+ if (col === null) { ElMessage.warning('列数必须为正整数'); return; }
|
|
|
+
|
|
|
+ const genCount = ((end as number) - (start as number) + 1) * row * col;
|
|
|
+ if (genCount > MAX_SHELVES) { ElMessage.warning(`本次将生成 ${genCount} 条,超过上限 ${MAX_SHELVES},请缩小货架序号范围/层数/列数`); return; }
|
|
|
+
|
|
|
+ const prefix = gen.prefix.trim();
|
|
|
+ // 已有编码集合(Trim + 不区分大小写,与后端去重口径一致)
|
|
|
+ const existing = new Set(shelves.value.map((s) => (s.invShelf ?? '').trim().toLowerCase()));
|
|
|
+ let added = 0;
|
|
|
+ let skipped = 0;
|
|
|
+ for (let i = start as number; i <= (end as number); i++) {
|
|
|
+ for (let r = 1; r <= row; r++) {
|
|
|
+ for (let c = 1; c <= col; c++) {
|
|
|
+ const invShelf = `${prefix}${pad(i)}-${pad(r)}-${pad(c)}`;
|
|
|
+ const key = invShelf.trim().toLowerCase();
|
|
|
+ if (existing.has(key)) { skipped++; continue; }
|
|
|
+ existing.add(key);
|
|
|
+ shelves.value.push({ invShelf, descr: `${i}#货架第${r}层第${c}格`, area: '' });
|
|
|
+ added++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ElMessage.success(`已生成:新增 ${added} 条,跳过重复 ${skipped} 条`);
|
|
|
+}
|
|
|
+function addShelfRow() { shelves.value.push({ invShelf: '', descr: '', area: '' }); }
|
|
|
+function removeShelfRow(index: number) { shelves.value.splice(index, 1); }
|
|
|
+
|
|
|
+// ===== 保存前校验(体验优化;后端保留同等校验)=====
|
|
|
+function validateShelvesForSubmit(): S0LocationShelfInput[] | null {
|
|
|
+ const list = shelves.value;
|
|
|
+ if (list.length > MAX_SHELVES) { ElMessage.warning(`货架明细 ${list.length} 条超过上限 ${MAX_SHELVES}`); return null; }
|
|
|
+ const seen = new Set<string>();
|
|
|
+ const cleaned: S0LocationShelfInput[] = [];
|
|
|
+ for (const s of list) {
|
|
|
+ const code = (s.invShelf ?? '').trim();
|
|
|
+ if (code.length === 0) { ElMessage.warning('存在货架编码为空的明细行,请填写货架编码或删除该行'); return null; }
|
|
|
+ if (code.length > 100) { ElMessage.warning(`货架编码「${code}」超过 100 字符`); return null; }
|
|
|
+ const descr = (s.descr ?? '').trim();
|
|
|
+ if (descr.length > 255) { ElMessage.warning(`货架「${code}」描述超过 255 字符`); return null; }
|
|
|
+ const area = (s.area ?? '').trim();
|
|
|
+ if (area.length > 100) { ElMessage.warning(`货架「${code}」区域超过 100 字符`); return null; }
|
|
|
+ const key = code.toLowerCase();
|
|
|
+ if (seen.has(key)) { ElMessage.warning(`货架编码重复:${code}`); return null; }
|
|
|
+ seen.add(key);
|
|
|
+ cleaned.push({ id: s.id, invShelf: code, descr: descr || undefined, area: area || undefined });
|
|
|
+ }
|
|
|
+ return cleaned;
|
|
|
+}
|
|
|
+
|
|
|
async function submitForm() {
|
|
|
await formRef.value?.validate();
|
|
|
+ const cleanedShelves = validateShelvesForSubmit();
|
|
|
+ if (cleanedShelves === null) return;
|
|
|
applyDefaultOrg();
|
|
|
saving.value = true;
|
|
|
try {
|
|
|
- if (editingId.value) { await s0LocationsApi.update(editingId.value, { ...form }); ElMessage.success('已保存'); }
|
|
|
- else { await s0LocationsApi.create({ ...form }); ElMessage.success('已创建'); }
|
|
|
+ const payload: S0LocationUpsert = { ...form, shelves: cleanedShelves };
|
|
|
+ if (editingId.value) { await s0LocationsApi.update(editingId.value, payload); ElMessage.success('已保存'); }
|
|
|
+ else { await s0LocationsApi.create(payload); ElMessage.success('已创建'); }
|
|
|
dialogVisible.value = false; await loadList();
|
|
|
} finally { saving.value = false; }
|
|
|
}
|
|
|
@@ -153,17 +268,15 @@ function onDelete(row: S0LocationRow) {
|
|
|
ElMessageBox.confirm(`确定删除库位「${row.location}」?`, '确认', { type: 'warning' })
|
|
|
.then(async () => { await s0LocationsApi.delete(row.id); ElMessage.success('已删除'); await loadList(); }).catch(() => {});
|
|
|
}
|
|
|
-async function loadOrgOptions() {
|
|
|
- const [companies, factories] = await Promise.all([loadOrgList('201'), loadOrgList('501')]);
|
|
|
- companyOptions.value = companies.filter((item) => item.id && item.id !== '0');
|
|
|
- factoryOptions.value = factories.filter((item) => item.id && item.id !== '0');
|
|
|
-}
|
|
|
|
|
|
-onMounted(() => { void loadList(); void loadOrgOptions(); });
|
|
|
+onMounted(() => { void loadList(); });
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
@import '/@/views/aidop/styles/aidop-demo.scss';
|
|
|
.mb12 { margin-bottom: 12px; }
|
|
|
.pager { margin-top: 12px; display: flex; justify-content: flex-end; }
|
|
|
+.gen-bar { margin-bottom: 8px; }
|
|
|
+.gen-sep { margin: 0 6px; color: var(--el-text-color-secondary); }
|
|
|
+.shelf-count { margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary); }
|
|
|
</style>
|