Files
ShiErFeng/shierfeng-fastapi-frontend/src/views/master/site/index.vue
T
yujiangandClaude Opus 5.5 167df4adb8 Feat: 前端改版 Web 端界面与业务作业流程
按设计稿改版登录后的全部页面,登录页与 Logo 资源未改动。

- 全局外壳:浅色侧边栏与分组菜单、左下角用户卡片、Ctrl/⌘K 搜索面板、
  测试模式提示条、深色模式配色、窄屏适配
- 共用组件:页头 PageHeader、托盘流程条 FlowStepper、扫码反馈 ScanFeedback、
  缩短单号 DocNo、托盘档案链接 PalletCodeLink、图标 SfIcon、上级资料选择
  MasterRefSelect,以及编号段解析等工具
- 工作台重做;新增托盘档案页;托盘追溯增加单托盘入口,报表托盘号可跳转档案
- 基础资料维护:各页加指标条、名称代替 ID、详情面板;三步式批量导入预览;
  分配用户对话框标注增减;7 个新增/编辑对话框统一布局
- 业务管理:发货出库、仓库入库、工厂装填、客户现场使用、退回仓库改为
  列表 + 详情面板 + 全屏作业区;写入流程、幂等键与防串单逻辑保持不变
- 修复入库页扫码反馈条因同名状态变量遮蔽组件而不显示的问题,并新增契约检查
- 新增入库粘贴解析、退回、装填的行为测试与现场使用页新流程测试,
  同步更新业务与工作台契约脚本

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 05:03:22 -07:00

522 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="app-container master-page master-site">
<page-header>
<template #actions>
<el-row :gutter="10" class="mb8 master-toolbar">
<template v-if="isMasterWriteAllowed">
<el-col v-if="userStore.admin === true && !testBatchStore.isTestMode && selectedIds.length" :span="1.5">
<el-button icon="FolderAdd" :disabled="!selectedIds.length" @click="openAdoptionDialog" v-hasPermi="['system:testbatch:adopt']">纳入测试批次</el-button>
</el-col>
<el-col :span="1.5" class="mp-primary-col"><el-button type="primary" icon="Plus" @click="handleAdd" v-hasPermi="['master:site:add']">新增现场</el-button></el-col>
</template>
<master-excel-actions resource="site" :allow-import="isMasterWriteAllowed" :query-params="queryParams" :selected-ids="selectedIds" @imported="getList" />
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList" />
</el-row>
</template>
</page-header>
<section class="mp-kpis" aria-label="本页现场概况">
<div class="mp-kpi">
<span class="mp-kpi__label">现场</span>
<span class="mp-kpi__value">{{ formatCount(total) }}</span>
<span class="mp-kpi__hint">{{ routeFilterLabel || '全部客户' }}</span>
</div>
<div class="mp-kpi">
<span class="mp-kpi__label">本页在用托盘</span>
<span class="mp-kpi__value">{{ formatCount(pageTotals.pallets) }}</span>
<span class="mp-kpi__hint">未使用 {{ formatCount(pageTotals.unused) }} · 部分使用 {{ formatCount(pageTotals.partial) }} · 空托 {{ formatCount(pageTotals.empty) }}</span>
</div>
<div class="mp-kpi">
<span class="mp-kpi__label">本页在途</span>
<span class="mp-kpi__value">{{ formatCount(pageTotals.transit) }}</span>
<span class="mp-kpi__hint">已发货、待现场扫码收货</span>
</div>
<div class="mp-kpi">
<span class="mp-kpi__label">未分配用户的现场</span>
<span class="mp-kpi__value" :class="{ 'is-warn': pageTotals.unassigned }">{{ formatCount(pageTotals.unassigned) }}</span>
<span class="mp-kpi__hint">没有授权用户时,现场无法收货与登记使用</span>
</div>
</section>
<el-form ref="queryRef" v-show="showSearch" class="master-query page-filter mp-filter" :model="queryParams" :inline="true" @submit.prevent>
<el-form-item prop="siteName" class="mp-filter__search">
<el-input v-model="queryParams.siteName" placeholder="搜索现场名称" prefix-icon="Search" clearable maxlength="100" @keyup.enter="handleQuery" @clear="handleQuery" />
</el-form-item>
<el-form-item label="编码" prop="siteCode">
<el-input v-model="queryParams.siteCode" placeholder="现场编码" clearable maxlength="64" style="width: 150px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="客户ID" prop="customerId">
<el-input-number v-model="queryParams.customerId" :min="1" :step="1" step-strictly :controls="false" placeholder="全部" style="width: 110px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="状态" prop="status" class="mp-filter__select">
<el-select v-model="queryParams.status" placeholder="全部" clearable @change="handleQuery">
<el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery" v-hasPermi="['master:site:list']">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery" v-hasPermi="['master:site:list']">重置</el-button>
</el-form-item>
<span v-if="routeFilterLabel" class="site-filter-chip">
{{ routeFilterLabel }}
<button type="button" aria-label="清除筛选" @click="clearRouteFilter">×</button>
</span>
<span class="mp-filter__summary">共 {{ formatCount(total) }} 个现场</span>
</el-form>
<el-alert v-if="loadError" :title="loadError" type="error" :closable="false" show-icon class="mb8" />
<el-table class="master-table data-table" v-loading="loading" empty-text="暂无数据" :data="siteList" :row-class-name="siteRowClass" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="44" align="center" />
<el-table-column label="现场" min-width="200">
<template #default="scope">
<span class="mp-name-cell__text">
<strong>{{ scope.row.siteName }} <span class="mp-mono mp-muted">{{ scope.row.siteCode }}</span></strong>
<small>{{ scope.row.address || '未登记地址' }}</small>
</span>
</template>
</el-table-column>
<el-table-column label="所属客户" min-width="150" :show-overflow-tooltip="true">
<template #default="scope">
<span class="mp-name-cell__text">
<strong>{{ scope.row.customerName || `客户 #${scope.row.customerId}` }}</strong>
<small class="mp-mono">{{ scope.row.customerCode || '—' }}</small>
</span>
</template>
</el-table-column>
<el-table-column label="产线 / 隧道" width="110" align="right">
<template #default="scope"><span class="mp-num">{{ scope.row.lineCount ?? 0 }} / {{ scope.row.tunnelCount ?? 0 }}</span></template>
</el-table-column>
<el-table-column label="现场托盘" min-width="190">
<template #default="scope">
<span class="site-pallets">
<span class="mp-stack" role="img" :aria-label="sitePalletLabel(scope.row)">
<span :style="{ flex: scope.row.unusedPalletCount || 0, background: 'var(--sf-site)' }"></span>
<span :style="{ flex: scope.row.partialPalletCount || 0, background: 'var(--sf-transit)' }"></span>
<span :style="{ flex: scope.row.emptyPalletCount || 0, background: 'var(--sf-neutral-dot)' }"></span>
</span>
<small class="mp-num">{{ sitePalletCount(scope.row) }} 托在用<template v-if="scope.row.emptyPalletCount"> · <span class="site-pallets__warn">空托 {{ scope.row.emptyPalletCount }}</span></template><template v-if="scope.row.transitPalletCount"> · 在途 {{ scope.row.transitPalletCount }}</template></small>
</span>
</template>
</el-table-column>
<el-table-column label="授权用户" min-width="150">
<template #default="scope">
<span v-if="scope.row.userCount" class="site-users">
<span class="mp-avatars">
<span v-for="user in (scope.row.users || []).slice(0, 3)" :key="user.userId" class="mp-avatar" :style="{ '--mp-avatar': avatarTint(user.userId) }" :title="userDisplayName(user)">{{ personInitial(user) }}</span>
</span>
<small class="mp-muted">{{ scope.row.userCount }} 人</small>
</span>
<span v-else class="mp-pill is-warn">未分配</span>
</template>
</el-table-column>
<el-table-column label="状态" prop="status" width="84">
<template #default="scope"><dict-tag :options="sys_normal_disable" :value="scope.row.status" /></template>
</el-table-column>
<el-table-column v-if="isMasterWriteAllowed" label="操作" width="150" align="right" class-name="small-padding fixed-width nowrap-cell">
<template #default="scope">
<el-button link type="primary" icon="User" @click="handleAssign(scope.row)" v-hasPermi="['master:site:assign']">分配</el-button>
<el-button
v-if="proxy.$auth.hasPermiAnd(['master:site:query', 'master:site:edit'])"
link type="primary" icon="Edit" :disabled="detailLoading" @click="handleUpdate(scope.row)" v-hasPermi="['master:site:edit']"
>修改</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0" :total="total"
v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList"
/>
<test-batch-adoption-dialog ref="adoptionDialogRef" :root-type="'site'" :resource-ids="selectedIds" @success="getList" />
<el-dialog
v-model="open"
:title="title"
width="min(620px, 94vw)"
append-to-body
class="sf-form-dialog"
:close-on-click-modal="!submitLoading"
:close-on-press-escape="!submitLoading"
:show-close="!submitLoading"
:before-close="handleDialogBeforeClose"
>
<p class="sf-form-intro">现场是发货的目的地,也是使用登记与退回的发起地。现场编码在同一客户下唯一。</p>
<el-alert v-if="formError" :title="formError" type="error" :closable="false" show-icon class="mb8" />
<el-form ref="siteRef" :model="form" :rules="rules" label-position="top" class="sf-form">
<el-form-item label="所属客户" prop="customerId">
<master-ref-select v-model="form.customerId" kind="客户" :options="customerOptions" :loading="customerOptionLoading" :failed="customerOptionFailed" :current-label="formCustomerName" placeholder="搜索客户名称或编码" />
<div class="sf-form-hint">只列出状态正常的客户</div>
</el-form-item>
<div class="sf-form-grid">
<el-form-item label="现场编码" prop="siteCode">
<el-input v-model="form.siteCode" class="sf-form-mono" placeholder="如 S-0012-01" maxlength="64" />
<div class="sf-form-hint">同一客户下唯一,导入产线和隧道时用它关联现场</div>
</el-form-item>
<el-form-item label="现场名称" prop="siteName">
<el-input v-model="form.siteName" placeholder="如 南厂、二号基地" maxlength="100" />
<div class="sf-form-hint">发货、收货与报表中显示</div>
</el-form-item>
</div>
<el-form-item label="地址" prop="address">
<el-input v-model="form.address" type="textarea" :rows="2" maxlength="500" show-word-limit placeholder="选填,便于司机送货与现场核对" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status" class="sf-choice">
<el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value" border>
<strong>{{ dict.label }}</strong><small>{{ STATUS_HINTS[dict.value] }}</small>
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="选填" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer sf-form-footer">
<span class="sf-form-footer__note">{{ formMetaText }}</span>
<el-button :disabled="submitLoading" @click="cancel">取消</el-button>
<el-button v-if="!form.siteId" type="primary" :loading="submitLoading" @click="submitCreate" v-hasPermi="['master:site:add']">创建现场</el-button>
<el-button v-else type="primary" :loading="submitLoading" @click="submitUpdate" v-hasPermi="['master:site:edit']">保存修改</el-button>
</div>
</template>
</el-dialog>
<business-user-assign-dialog ref="assignDialogRef" @ok="getList" />
</div>
</template>
<style scoped>
.master-query :deep(.el-form-item) { margin-right:0; } .master-query :deep(.el-form-item__content) { min-width:0; } .master-query :deep(.el-input), .master-query :deep(.el-input-number), .master-query :deep(.el-select) { width:100% !important; min-width:0; }
.master-table :deep(.el-table__cell) { padding:10px 0; } .master-table :deep(.fixed-width) { position:sticky; right:0; z-index:2; background:var(--el-bg-color); }
.master-table :deep(.is-muted-row .cell) { opacity:.62; }
.site-pallets { display:flex; flex-direction:column; gap:6px; max-width:200px; }
.site-pallets small { color:var(--sf-text-3, #5b616e); font-size:12px; }
.site-pallets__warn { color:var(--sf-warn-fg, #b54708); }
.site-users { display:inline-flex; align-items:center; gap:8px; }
.site-filter-chip { display:inline-flex; align-items:center; gap:6px; height:28px; margin:0 0 10px; padding:0 6px 0 10px; border-radius:7px; background:var(--sf-accent-soft, #e6f3f5); color:var(--sf-accent-strong, #006a78); font-size:12.5px; }
.site-filter-chip button { width:20px; height:20px; border:0; border-radius:5px; background:transparent; color:inherit; font-size:15px; line-height:1; cursor:pointer; }
.site-filter-chip button:hover { background:rgba(0,0,0,.06); }
</style>
<script setup name="Site">
import BusinessUserAssignDialog from "@/components/BusinessUserAssignDialog";
import { addSite, getSite, getSiteCustomerOptions, listSite, updateSite } from "@/api/master/site";
import TestBatchAdoptionDialog from '@/components/TestBatchAdoptionDialog'
import MasterRefSelect from '@/components/MasterRefSelect'
import useUserStore from '@/store/modules/user'
import useTestBatchStore from '@/store/modules/testBatch'
import { avatarTint, formatCount, personInitial, userDisplayName } from '@/utils/masterDisplay'
import { positiveId, useMasterRouteIntent } from '@/utils/masterRouteIntent'
const { proxy } = getCurrentInstance();
const userStore = useUserStore()
const testBatchStore = useTestBatchStore()
const isMasterWriteAllowed = computed(() => userStore.admin === true || !testBatchStore.isTestMode)
const adoptionDialogRef = ref()
const { sys_normal_disable } = proxy.useDict("sys_normal_disable");
const siteList = ref([]);
const showSearch = ref(true);
const selectedIds = ref([]);
const selectedRows = ref([]);
const assignDialogRef = ref();
const loading = ref(false);
const detailLoading = ref(false);
const submitLoading = ref(false);
const loadError = ref("");
const formError = ref("");
const open = ref(false);
const single = ref(true);
const total = ref(0);
const title = ref("");
let listGeneration = 0;
let detailGeneration = 0;
// 状态选项的后果说明,与服务端校验保持一致
const STATUS_HINTS = { '0': '可发货、收货核对、登记使用与退回', '1': '发货、收货、使用登记与退回都会被拒绝;历史记录保留' };
const formMeta = ref(null);
const formMetaText = computed(() => formMeta.value
? [`最后修改${formMeta.value.by ? ` ${formMeta.value.by}` : ''}`, proxy.parseTime(formMeta.value.time, '{y}-{m}-{d} {h}:{i}')].filter(Boolean).join(' · ')
: '带 * 的为必填项');
// 从客户详情跳转来的筛选与待办动作
const routeFilter = ref({ customerName: '', siteName: '' })
const routeCustomerNames = ref({})
let pendingAssignSiteId
function validateRequiredText(rule, value, callback) {
const labels = { siteCode: "现场编码", siteName: "现场名称" };
if (typeof value !== "string" || value.trim() === "") {
callback(new Error(`${labels[rule.field]}不能为空`));
return;
}
callback();
}
function validatePositiveInteger(rule, value, callback) {
if (!Number.isInteger(value) || value <= 0) {
callback(new Error("客户ID必须为正整数"));
return;
}
callback();
}
const data = reactive({
form: {},
queryParams: { pageNum: 1, pageSize: 10, customerId: undefined, siteCode: undefined, siteName: undefined, status: undefined, ids: undefined },
rules: {
customerId: [{ required: true, validator: validatePositiveInteger, trigger: "change" }],
siteCode: [{ required: true, validator: validateRequiredText, trigger: "blur" }],
siteName: [{ required: true, validator: validateRequiredText, trigger: "blur" }],
status: [{ required: true, message: "请选择状态", trigger: "change" }]
}
});
const { form, queryParams, rules } = toRefs(data);
const pageTotals = computed(() => siteList.value.reduce((totals, row) => {
totals.unused += row.unusedPalletCount || 0
totals.partial += row.partialPalletCount || 0
totals.empty += row.emptyPalletCount || 0
totals.transit += row.transitPalletCount || 0
totals.pallets += sitePalletCount(row)
if (!row.userCount && row.status !== '1') totals.unassigned += 1
return totals
}, { unused: 0, partial: 0, empty: 0, transit: 0, pallets: 0, unassigned: 0 }))
const routeFilterLabel = computed(() => {
if (queryParams.value.ids) return `仅显示:${routeFilter.value.siteName || '指定现场'}`
if (queryParams.value.customerId && routeFilter.value.customerName) return `客户:${routeFilter.value.customerName}`
return ''
})
// 已知客户名:来自当前列表行与跳转参数,用于表单里回显所属客户
const knownCustomerNames = computed(() => {
const names = { ...routeCustomerNames.value }
siteList.value.forEach((row) => {
if (row.customerName) names[row.customerId] = `${row.customerName}${row.customerCode ? ` · ${row.customerCode}` : ''}`
})
return names
})
const formCustomerName = computed(() => knownCustomerNames.value[form.value.customerId] || '')
function isCurrentListRequest(listContext) {
return listContext.generation === listGeneration;
}
function getList() {
const listContext = {
generation: ++listGeneration,
query: { ...queryParams.value }
};
loading.value = true;
loadError.value = "";
listSite(listContext.query).then((response) => {
if (!isCurrentListRequest(listContext)) {
return;
}
siteList.value = response.rows;
total.value = response.total;
runPendingAssign({ listLoaded: true });
}).catch(() => {
if (!isCurrentListRequest(listContext)) {
return;
}
siteList.value = [];
total.value = 0;
loadError.value = "客户现场列表加载失败,请稍后重试";
}).finally(() => {
if (!isCurrentListRequest(listContext)) {
return;
}
loading.value = false;
});
}
function reset() {
formError.value = "";
formMeta.value = null;
form.value = { siteId: undefined, customerId: undefined, siteCode: "", siteName: "", address: "", status: "0", remark: "" };
proxy.resetForm("siteRef");
}
function invalidateDetailRequest() {
detailGeneration += 1;
detailLoading.value = false;
}
function isCurrentDetailRequest(detailContext) {
return detailContext.generation === detailGeneration;
}
function cancel() {
if (submitLoading.value) return;
invalidateDetailRequest();
open.value = false;
reset();
}
function handleDialogBeforeClose(done) {
if (submitLoading.value) return;
invalidateDetailRequest();
reset();
done();
}
function handleQuery() { queryParams.value.pageNum = 1; getList(); }
function resetQuery() { proxy.resetForm("queryRef"); queryParams.value.ids = undefined; routeFilter.value = { customerName: '', siteName: '' }; handleQuery(); }
function handleSelectionChange(selection) {
selectedRows.value = selection;
selectedIds.value = selection.map((item) => item.siteId);
single.value = selectedIds.value.length !== 1;
}
function openAdoptionDialog() { adoptionDialogRef.value?.open() }
function handleAdd() {
invalidateDetailRequest();
reset();
title.value = "新增客户现场";
open.value = true;
}
function handleUpdate(row = {}) {
reset();
const siteId = row.siteId || selectedIds.value[0];
const detailContext = {
generation: ++detailGeneration,
resourceId: siteId
};
detailLoading.value = true;
getSite(detailContext.resourceId).then((response) => {
if (!isCurrentDetailRequest(detailContext)) {
return;
}
const detail = response.data;
formMeta.value = { by: detail.updateBy || detail.createBy, time: detail.updateTime || detail.createTime };
form.value = {
siteId: detail.siteId,
customerId: detail.customerId,
siteCode: detail.siteCode,
siteName: detail.siteName,
address: detail.address || "",
status: detail.status,
remark: detail.remark || ""
};
title.value = "修改客户现场";
open.value = true;
}).catch(() => {
if (!isCurrentDetailRequest(detailContext)) {
return;
}
loadError.value = "客户现场详情加载失败,请稍后重试";
}).finally(() => {
if (!isCurrentDetailRequest(detailContext)) {
return;
}
detailLoading.value = false;
});
}
function handleAssign(row = {}) {
if (!row.siteId) row = selectedRows.value[0];
if (!row) return;
assignDialogRef.value.show({
resourceType: "site",
resourceId: row.siteId,
resourceName: row.siteName
});
}
function afterSubmit(response) {
proxy.$modal.msgSuccess(response.msg || "操作成功");
open.value = false;
getList();
}
function submitCreate() {
proxy.$refs.siteRef.validate((valid) => {
if (!valid) return;
submitLoading.value = true;
formError.value = "";
addSite({
customerId: form.value.customerId,
siteCode: form.value.siteCode.trim(),
siteName: form.value.siteName.trim(),
address: form.value.address?.trim() || undefined,
status: form.value.status,
remark: form.value.remark?.trim() || undefined
}).then(afterSubmit).catch(() => {
formError.value = "新增失败,请根据提示修正后重试";
}).finally(() => { submitLoading.value = false; });
});
}
function submitUpdate() {
proxy.$refs.siteRef.validate((valid) => {
if (!valid) return;
submitLoading.value = true;
formError.value = "";
updateSite({
siteId: form.value.siteId,
customerId: form.value.customerId,
siteCode: form.value.siteCode.trim(),
siteName: form.value.siteName.trim(),
address: form.value.address?.trim() || undefined,
status: form.value.status,
remark: form.value.remark?.trim() || undefined
}).then(afterSubmit).catch(() => {
formError.value = "修改失败,请根据提示修正后重试";
}).finally(() => { submitLoading.value = false; });
});
}
function siteRowClass({ row }) {
return row.status === '1' ? 'is-muted-row' : ''
}
function sitePalletCount(row) {
return (row.unusedPalletCount || 0) + (row.partialPalletCount || 0) + (row.emptyPalletCount || 0)
}
function sitePalletLabel(row) {
return `未使用 ${row.unusedPalletCount || 0} 托,部分使用 ${row.partialPalletCount || 0} 托,空托 ${row.emptyPalletCount || 0} 托`
}
function clearRouteFilter() {
queryParams.value.customerId = undefined
queryParams.value.ids = undefined
routeFilter.value = { customerName: '', siteName: '' }
handleQuery()
}
// 列表请求进行中时保留待办,等这次列表返回后再打开分配窗口
function runPendingAssign({ listLoaded = false } = {}) {
if (!pendingAssignSiteId || (loading.value && !listLoaded)) return
const row = siteList.value.find((item) => item.siteId === pendingAssignSiteId)
pendingAssignSiteId = undefined
if (row && isMasterWriteAllowed.value && proxy.$auth.hasPermiAnd(['master:site:assign'])) handleAssign(row)
}
// 表单里的上级客户选项:每次打开弹窗时刷新,加载失败时退回为直接输入 ID
const customerOptions = ref([]);
const customerOptionLoading = ref(false);
const customerOptionFailed = ref(false);
let customerOptionGeneration = 0;
function loadCustomerOptions() {
const generation = ++customerOptionGeneration;
customerOptionLoading.value = true;
customerOptionFailed.value = false;
getSiteCustomerOptions().then((response) => {
if (generation !== customerOptionGeneration) return;
customerOptions.value = Array.isArray(response.data) ? response.data : [];
}).catch(() => {
if (generation !== customerOptionGeneration) return;
customerOptions.value = [];
customerOptionFailed.value = true;
}).finally(() => {
if (generation === customerOptionGeneration) customerOptionLoading.value = false;
});
}
watch(open, (visible) => {
if (visible) loadCustomerOptions();
});
const consumeRouteIntent = useMasterRouteIntent((intent, { initial }) => {
const customerId = positiveId(intent.customerId)
const siteId = positiveId(intent.siteId)
const nextIds = siteId ? String(siteId) : undefined
const changed = customerId !== queryParams.value.customerId || nextIds !== queryParams.value.ids
if (customerId || siteId) {
queryParams.value.customerId = customerId
queryParams.value.ids = nextIds
routeFilter.value = { customerName: String(intent.customerName || ''), siteName: String(intent.siteName || '') }
if (customerId && intent.customerName) routeCustomerNames.value = { ...routeCustomerNames.value, [customerId]: String(intent.customerName) }
}
if (intent.action === 'assign' && siteId) pendingAssignSiteId = siteId
if (intent.action === 'add' && isMasterWriteAllowed.value && proxy.$auth.hasPermiAnd(['master:site:add'])) {
handleAdd()
form.value.customerId = customerId
}
if (!initial && changed) handleQuery()
else if (!initial) runPendingAssign()
})
consumeRouteIntent({ initial: true })
getList();
</script>