Feat: 前端改版 Web 端界面与业务作业流程
按设计稿改版登录后的全部页面,登录页与 Logo 资源未改动。 - 全局外壳:浅色侧边栏与分组菜单、左下角用户卡片、Ctrl/⌘K 搜索面板、 测试模式提示条、深色模式配色、窄屏适配 - 共用组件:页头 PageHeader、托盘流程条 FlowStepper、扫码反馈 ScanFeedback、 缩短单号 DocNo、托盘档案链接 PalletCodeLink、图标 SfIcon、上级资料选择 MasterRefSelect,以及编号段解析等工具 - 工作台重做;新增托盘档案页;托盘追溯增加单托盘入口,报表托盘号可跳转档案 - 基础资料维护:各页加指标条、名称代替 ID、详情面板;三步式批量导入预览; 分配用户对话框标注增减;7 个新增/编辑对话框统一布局 - 业务管理:发货出库、仓库入库、工厂装填、客户现场使用、退回仓库改为 列表 + 详情面板 + 全屏作业区;写入流程、幂等键与防串单逻辑保持不变 - 修复入库页扫码反馈条因同名状态变量遮蔽组件而不显示的问题,并新增契约检查 - 新增入库粘贴解析、退回、装填的行为测试与现场使用页新流程测试, 同步更新业务与工作台契约脚本 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,52 @@
|
||||
<template>
|
||||
<div class="app-container master-page">
|
||||
<el-form ref="queryRef" v-show="showSearch" class="master-query page-filter" :model="queryParams" :inline="true">
|
||||
<el-form-item label="客户编码" prop="customerCode">
|
||||
<el-input v-model="queryParams.customerCode" placeholder="请输入客户编码" clearable maxlength="64" @keyup.enter="handleQuery" />
|
||||
<div class="app-container master-page master-customer">
|
||||
<page-header title="客户与现场">
|
||||
<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:customer:add']">新增客户</el-button></el-col>
|
||||
</template>
|
||||
<master-excel-actions resource="customer" :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">{{ disabledCustomerCount === undefined ? '合作中与已停用' : `合作中 ${formatCount(Math.max(0, total - disabledCustomerCount))} · 已停用 ${formatCount(disabledCustomerCount)}` }}</span>
|
||||
</div>
|
||||
<div class="mp-kpi">
|
||||
<span class="mp-kpi__label">本页现场</span>
|
||||
<span class="mp-kpi__value">{{ formatCount(pageTotals.sites) }}</span>
|
||||
<span class="mp-kpi__hint">产线 {{ formatCount(pageTotals.lines) }} 条 · 隧道 {{ formatCount(pageTotals.tunnels) }} 条</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.transit) }} 托</span>
|
||||
</div>
|
||||
<div class="mp-kpi">
|
||||
<span class="mp-kpi__label">未建现场的客户</span>
|
||||
<span class="mp-kpi__value" :class="{ 'is-warn': pageTotals.withoutSite }">{{ formatCount(pageTotals.withoutSite) }}</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="customerName" class="mp-filter__search">
|
||||
<el-input v-model="queryParams.customerName" placeholder="搜索客户名称" prefix-icon="Search" clearable maxlength="100" @keyup.enter="handleQuery" @clear="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="客户名称" prop="customerName">
|
||||
<el-input v-model="queryParams.customerName" placeholder="请输入客户名称" clearable maxlength="100" @keyup.enter="handleQuery" />
|
||||
<el-form-item label="编码" prop="customerCode">
|
||||
<el-input v-model="queryParams.customerCode" placeholder="客户编码" clearable maxlength="64" style="width: 150px" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 160px">
|
||||
<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>
|
||||
@@ -16,86 +54,220 @@
|
||||
<el-button type="primary" icon="Search" @click="handleQuery" v-hasPermi="['master:customer:list']">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery" v-hasPermi="['master:customer:list']">重置</el-button>
|
||||
</el-form-item>
|
||||
<span class="mp-filter__summary">{{ formatCount(total) }} 个客户 · 本页 {{ formatCount(pageSiteCount) }} 个现场</span>
|
||||
</el-form>
|
||||
|
||||
<el-row :gutter="10" class="mb8 master-toolbar">
|
||||
<template v-if="isMasterWriteAllowed">
|
||||
<el-col :span="1.5"><el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['master:customer:add']">新增</el-button></el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
v-if="proxy.$auth.hasPermiAnd(['master:customer:query', 'master:customer:edit'])"
|
||||
type="success" plain icon="Edit" :disabled="single || detailLoading" :loading="detailLoading"
|
||||
@click="handleUpdate()" v-hasPermi="['master:customer:edit']"
|
||||
>修改</el-button>
|
||||
</el-col>
|
||||
<el-col v-if="userStore.admin === true && !testBatchStore.isTestMode" :span="1.5">
|
||||
<el-button type="warning" plain icon="FolderAdd" :disabled="!selectedIds.length" @click="openAdoptionDialog" v-hasPermi="['system:testbatch:adopt']">纳入测试批次</el-button>
|
||||
</el-col>
|
||||
</template>
|
||||
<master-excel-actions resource="customer" :allow-import="isMasterWriteAllowed" :query-params="queryParams" :selected-ids="selectedIds" @imported="getList" />
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList" />
|
||||
</el-row>
|
||||
|
||||
<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="customerList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="客户ID" prop="customerId" align="center" />
|
||||
<el-table-column label="客户编码" prop="customerCode" align="center" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="客户名称" prop="customerName" align="center" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="状态" prop="status" align="center" width="100">
|
||||
<template #default="scope"><dict-tag :options="sys_normal_disable" :value="scope.row.status" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" prop="createTime" align="center" width="180">
|
||||
<template #default="scope"><span>{{ parseTime(scope.row.createTime) }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="isMasterWriteAllowed" label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-if="proxy.$auth.hasPermiAnd(['master:customer:query', 'master:customer:edit'])"
|
||||
link type="primary" icon="Edit" :disabled="detailLoading" @click="handleUpdate(scope.row)" v-hasPermi="['master:customer:edit']"
|
||||
>修改</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="mp-workspace" :class="{ 'is-wide-panel': panelRow }">
|
||||
<div class="mp-workspace__list">
|
||||
<el-table
|
||||
class="master-table data-table"
|
||||
v-loading="loading"
|
||||
empty-text="暂无数据"
|
||||
:data="customerList"
|
||||
:row-class-name="customerRowClass"
|
||||
@row-click="handleRowClick"
|
||||
@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">
|
||||
<span class="mp-initial" :class="{ 'is-off': scope.row.status === '1' }">{{ orgInitial(scope.row.customerName) }}</span>
|
||||
<span class="mp-name-cell__text">
|
||||
<strong>{{ scope.row.customerName }}</strong>
|
||||
<small><span class="mp-mono">{{ scope.row.customerCode }}</span> · {{ scope.row.siteCount ?? 0 }} 个现场</small>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="在用托盘" width="96" align="right">
|
||||
<template #default="scope">
|
||||
<span class="customer-pallets">
|
||||
<strong class="mp-num">{{ formatCount(scope.row.sitePalletCount ?? 0) }}</strong>
|
||||
<small v-if="scope.row.transitPalletCount">在途 {{ formatCount(scope.row.transitPalletCount) }}</small>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="!panelRow" label="层级" min-width="170">
|
||||
<template #default="scope">
|
||||
<span class="mp-count-cell"><span><strong>{{ scope.row.lineCount ?? 0 }}</strong> 条产线</span><span><strong>{{ scope.row.tunnelCount ?? 0 }}</strong> 条隧道</span></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="!panelRow" label="授权用户" width="96" align="right">
|
||||
<template #default="scope"><span class="mp-num">{{ scope.row.userCount ?? 0 }} 人</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 && !panelRow" label="操作" width="90" align="right" class-name="small-padding fixed-width nowrap-cell">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-if="proxy.$auth.hasPermiAnd(['master:customer:query', 'master:customer:edit'])"
|
||||
link type="primary" icon="Edit" :disabled="detailLoading" @click="handleUpdate(scope.row)" v-hasPermi="['master:customer: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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<section v-if="panelRow" class="mp-panel customer-panel" aria-label="客户详情">
|
||||
<div class="mp-panel__head">
|
||||
<span class="mp-initial is-lg" :class="{ 'is-off': panelRow.status === '1' }">{{ orgInitial(panelRow.customerName) }}</span>
|
||||
<div class="mp-panel__title">
|
||||
<span class="mp-panel__name">
|
||||
{{ panelRow.customerName }}
|
||||
<span class="mp-mono mp-muted">{{ panelRow.customerCode }}</span>
|
||||
<span class="mp-pill" :class="panelRow.status === '1' ? '' : 'is-ok'">{{ panelRow.status === '1' ? '停用' : '正常合作' }}</span>
|
||||
</span>
|
||||
<span class="mp-panel__sub">{{ panelRow.remark || '暂无备注' }}</span>
|
||||
</div>
|
||||
<span class="customer-panel__actions">
|
||||
<template v-if="isMasterWriteAllowed">
|
||||
<el-button
|
||||
v-if="proxy.$auth.hasPermiAnd(['master:customer:query', 'master:customer:edit'])"
|
||||
icon="Edit" :disabled="detailLoading" @click="handleUpdate(panelRow)" v-hasPermi="['master:customer:edit']"
|
||||
>编辑客户</el-button>
|
||||
</template>
|
||||
<el-button v-if="sitePath && isMasterWriteAllowed" icon="Plus" @click="goMaster('site', { customerId: panelRow.customerId, customerName: panelRow.customerName, action: 'add' })">新增现场</el-button>
|
||||
<button type="button" class="mp-panel__close" aria-label="关闭详情" @click="closePanel">×</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-loading="panelLoading" class="mp-panel__body">
|
||||
<div class="mp-stats customer-panel__stats">
|
||||
<div><span>现场</span><strong>{{ panelTotals.sites }}</strong></div>
|
||||
<div><span>产线</span><strong>{{ panelTotals.lines }}</strong></div>
|
||||
<div><span>隧道</span><strong>{{ panelTotals.tunnels }}</strong></div>
|
||||
<div><span>在用托盘</span><strong>{{ formatCount(panelTotals.pallets) }}</strong></div>
|
||||
<div><span>授权用户</span><strong>{{ panelTotals.users }}</strong></div>
|
||||
</div>
|
||||
|
||||
<template v-if="canQueryCustomer">
|
||||
<div class="mp-section__head">
|
||||
<strong>现场</strong>
|
||||
<button v-if="panelSites.length" type="button" class="mp-link" @click="toggleAllSites">{{ allSitesExpanded ? '全部收起' : '全部展开' }}</button>
|
||||
</div>
|
||||
<p v-if="!panelLoading && !panelSites.length" class="mp-empty">还没有现场。现场是发货的目的地,新增后即可安排发货。</p>
|
||||
<section v-for="site in panelSites" :key="site.siteId" class="customer-site" :class="{ 'is-open': expandedSites.includes(site.siteId) }">
|
||||
<button type="button" class="customer-site__head" :aria-expanded="expandedSites.includes(site.siteId)" @click="toggleSite(site.siteId)">
|
||||
<span class="customer-site__chevron" aria-hidden="true">›</span>
|
||||
<span class="customer-site__title">
|
||||
<span>
|
||||
<strong>{{ site.siteName }}</strong>
|
||||
<span class="mp-mono mp-muted">{{ site.siteCode }}</span>
|
||||
<span v-if="site.status === '1'" class="mp-pill is-outline">停用</span>
|
||||
<span v-if="site.emptyPalletCount" class="mp-pill is-warn">空托 {{ site.emptyPalletCount }} 待退回</span>
|
||||
</span>
|
||||
<small>{{ site.address || '未登记地址' }}</small>
|
||||
</span>
|
||||
<span class="mp-count-cell customer-site__counts">
|
||||
<span>{{ site.lines.length }} 条产线</span>
|
||||
<span>{{ site.tunnels.length }} 条隧道</span>
|
||||
<span><strong>{{ sitePalletCount(site) }}</strong> 托在用</span>
|
||||
</span>
|
||||
<span class="mp-avatars" :aria-label="`授权用户 ${site.users.length} 人`">
|
||||
<span v-for="user in site.users.slice(0, 3)" :key="user.userId" class="mp-avatar" :style="{ '--mp-avatar': avatarTint(user.userId) }" :title="userDisplayName(user)">{{ personInitial(user) }}</span>
|
||||
<span v-if="site.users.length > 3" class="mp-avatar is-more">+{{ site.users.length - 3 }}</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div v-if="expandedSites.includes(site.siteId)" class="customer-site__body">
|
||||
<div class="customer-site__col">
|
||||
<span class="customer-site__col-head"><strong>产线</strong><button v-if="linePath && isMasterWriteAllowed" type="button" class="mp-link" @click="goMaster('line', { siteId: site.siteId, siteName: site.siteName, action: 'add' })">+ 新增</button></span>
|
||||
<span v-for="line in site.lines" :key="line.lineId" class="customer-site__row">
|
||||
<span>{{ line.lineName }}</span>
|
||||
<span class="mp-mono mp-muted">{{ line.lineCode }}</span>
|
||||
<span class="mp-muted mp-num customer-site__note">{{ line.status === '1' ? '停用' : line.activePalletCount ? `${line.activePalletCount} 托在线` : '空闲' }}</span>
|
||||
</span>
|
||||
<span v-if="!site.lines.length" class="mp-muted customer-site__none">未登记产线</span>
|
||||
</div>
|
||||
<div class="customer-site__col">
|
||||
<span class="customer-site__col-head"><strong>隧道</strong><button v-if="tunnelPath && isMasterWriteAllowed" type="button" class="mp-link" @click="goMaster('tunnel', { siteId: site.siteId, siteName: site.siteName, action: 'add' })">+ 新增</button></span>
|
||||
<span v-for="tunnel in site.tunnels" :key="tunnel.tunnelId" class="customer-site__tunnel">
|
||||
<span class="customer-site__row">
|
||||
<span class="mp-mono">{{ tunnel.tunnelCode }}</span>
|
||||
<span>{{ tunnel.tunnelName }}</span>
|
||||
<span class="mp-pill customer-site__note" :class="tunnel.currentTraceNo ? 'is-site' : ''">{{ tunnel.status === '1' ? '停用' : tunnel.currentTraceNo ? '有物料' : '空闲' }}</span>
|
||||
</span>
|
||||
<small v-if="tunnel.currentTraceNo" class="mp-muted mp-num">溯源号 <span class="mp-mono">{{ shortTrace(tunnel.currentTraceNo) }}</span><template v-if="tunnel.currentTraceStartedAt"> · {{ parseTime(tunnel.currentTraceStartedAt, '{m}-{d}') }} 起</template> · {{ tunnel.activePalletCount }} 托</small>
|
||||
</span>
|
||||
<span v-if="!site.tunnels.length" class="mp-muted customer-site__none">未登记隧道</span>
|
||||
</div>
|
||||
<div class="customer-site__col">
|
||||
<span class="customer-site__col-head"><strong>现场托盘</strong><span class="mp-muted mp-num">剩余 {{ formatCount(site.remainingPackageQty) }} 包</span></span>
|
||||
<span class="mp-stack" role="img" :aria-label="`未使用 ${site.unusedPalletCount} 托,部分使用 ${site.partialPalletCount} 托,空托 ${site.emptyPalletCount} 托`">
|
||||
<span :style="{ flex: site.unusedPalletCount, background: 'var(--sf-site)' }"></span>
|
||||
<span :style="{ flex: site.partialPalletCount, background: 'var(--sf-transit)' }"></span>
|
||||
<span :style="{ flex: site.emptyPalletCount, background: 'var(--sf-neutral-dot)' }"></span>
|
||||
</span>
|
||||
<span class="mp-legend">
|
||||
<span><i style="background: var(--sf-site)"></i>未使用<b>{{ site.unusedPalletCount }}</b></span>
|
||||
<span><i style="background: var(--sf-transit)"></i>部分使用<b>{{ site.partialPalletCount }}</b></span>
|
||||
<span><i style="background: var(--sf-neutral-dot)"></i>空托<b>{{ site.emptyPalletCount }}</b></span>
|
||||
</span>
|
||||
<small v-if="site.transitPalletCount" class="mp-muted mp-num">另有 {{ site.transitPalletCount }} 托在途</small>
|
||||
<span class="customer-site__users">
|
||||
<span class="mp-muted">授权用户 {{ site.users.length ? site.users.map(userDisplayName).join('、') : '未分配' }}</span>
|
||||
<button v-if="sitePath && isMasterWriteAllowed" type="button" class="mp-link" @click="goMaster('site', { siteId: site.siteId, siteName: site.siteName, action: 'assign' })">分配</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<p v-else class="mp-empty">需要客户查询权限才能查看现场、产线与隧道层级。</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<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="'customer'" :resource-ids="selectedIds" @success="getList" />
|
||||
|
||||
<el-dialog
|
||||
v-model="open"
|
||||
:title="title"
|
||||
width="min(520px, 92vw)"
|
||||
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="customerRef" :model="form" :rules="rules" label-width="90px">
|
||||
<el-form-item label="客户编码" prop="customerCode">
|
||||
<el-input v-model="form.customerCode" placeholder="请输入客户编码" maxlength="64" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="客户名称" prop="customerName">
|
||||
<el-input v-model="form.customerName" placeholder="请输入客户名称" maxlength="100" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form ref="customerRef" :model="form" :rules="rules" label-position="top" class="sf-form">
|
||||
<div class="sf-form-grid">
|
||||
<el-form-item label="客户编码" prop="customerCode">
|
||||
<el-input v-model="form.customerCode" class="sf-form-mono" placeholder="如 C-0012" maxlength="64" />
|
||||
<div class="sf-form-hint">全局唯一,导入现场时用它关联客户</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户名称" prop="customerName">
|
||||
<el-input v-model="form.customerName" placeholder="对外的客户全称或简称" maxlength="100" />
|
||||
<div class="sf-form-hint">发货单、报表与客户详情中显示</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
|
||||
<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-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">
|
||||
<el-button v-if="!form.customerId" type="primary" :loading="submitLoading" @click="submitCreate" v-hasPermi="['master:customer:add']">确 定</el-button>
|
||||
<el-button v-else type="primary" :loading="submitLoading" @click="submitUpdate" v-hasPermi="['master:customer:edit']">确 定</el-button>
|
||||
<el-button :disabled="submitLoading" @click="cancel">取 消</el-button>
|
||||
<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.customerId" type="primary" :loading="submitLoading" @click="submitCreate" v-hasPermi="['master:customer:add']">创建客户</el-button>
|
||||
<el-button v-else type="primary" :loading="submitLoading" @click="submitUpdate" v-hasPermi="['master:customer:edit']">保存修改</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -103,17 +275,41 @@
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.master-query { display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:0 14px; padding:16px 18px 4px; margin-bottom:14px; background:var(--el-bg-color); border:1px solid var(--el-border-color-lighter); border-radius:8px; }
|
||||
.master-query :deep(.el-form-item) { margin-right:0; width:100%; }
|
||||
.master-query :deep(.el-form-item__content) { flex:1; 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-query :deep(.el-form-item:last-child) { align-self:end; }
|
||||
.master-toolbar { display:flex; flex-wrap:wrap; gap:8px; }
|
||||
.master-toolbar :deep(.el-col) { width:auto; }
|
||||
.master-table :deep(.el-table__header th) { background:var(--el-fill-color-light); font-weight:600; }
|
||||
.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); box-shadow:-4px 0 8px rgba(0,0,0,.04); }
|
||||
@media (max-width:768px) { .master-query { grid-template-columns:1fr; padding:14px 14px 2px; } .master-query :deep(.el-form-item:last-child) { margin-bottom:12px; } .master-toolbar :deep(.el-button) { margin-left:0; } }
|
||||
.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); }
|
||||
.customer-pallets { display:inline-flex; flex-direction:column; align-items:flex-end; line-height:1.3; }
|
||||
.customer-pallets strong { font-weight:600; }
|
||||
.customer-pallets small { color:var(--sf-text-4, #6b717d); font-size:12px; }
|
||||
.customer-panel__actions { display:flex; flex:none; align-items:center; gap:8px; }
|
||||
.customer-panel__actions .el-button + .el-button { margin-left:0; }
|
||||
.master-customer .customer-panel__stats.mp-stats { grid-template-columns:repeat(5, minmax(0, 1fr)); }
|
||||
.customer-site { overflow:hidden; border:1px solid var(--sf-border, #e7e9ec); border-radius:10px; }
|
||||
.customer-site__head { display:flex; align-items:center; gap:12px; width:100%; padding:12px 14px; border:0; background:transparent; color:inherit; font:inherit; text-align:left; cursor:pointer; }
|
||||
.customer-site__head:hover { background:var(--sf-surface-2, #f7f8f9); }
|
||||
.customer-site__chevron { flex:none; width:12px; color:var(--sf-text-4, #6b717d); font-size:18px; line-height:1; transition:transform .15s ease; }
|
||||
.customer-site.is-open .customer-site__chevron { transform:rotate(90deg); }
|
||||
.customer-site__title { display:flex; flex:1; flex-direction:column; gap:2px; min-width:0; }
|
||||
.customer-site__title > span { display:flex; flex-wrap:wrap; align-items:center; gap:8px; }
|
||||
.customer-site__title strong { font-size:14px; font-weight:600; }
|
||||
.customer-site__title small { overflow:hidden; color:var(--sf-text-4, #6b717d); font-size:12.5px; white-space:nowrap; text-overflow:ellipsis; }
|
||||
.customer-site__counts { flex:none; }
|
||||
.customer-site__body { display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:16px 24px; padding:4px 16px 16px 38px; }
|
||||
.customer-site__col { display:flex; flex-direction:column; gap:8px; min-width:0; }
|
||||
.customer-site__col-head { display:flex; align-items:baseline; justify-content:space-between; gap:8px; font-size:12.5px; }
|
||||
.customer-site__col-head strong { color:var(--sf-text-3, #5b616e); font-size:12.5px; font-weight:500; }
|
||||
.customer-site__row { display:flex; align-items:center; gap:8px; min-width:0; font-size:13px; }
|
||||
.customer-site__note { margin-left:auto; font-size:12px; }
|
||||
.customer-site__tunnel { display:flex; flex-direction:column; gap:2px; }
|
||||
.customer-site__tunnel small, .customer-site__col > small { font-size:12px; }
|
||||
.customer-site__none { font-size:12.5px; }
|
||||
.customer-site__users { display:flex; align-items:baseline; justify-content:space-between; gap:8px; padding-top:8px; border-top:1px solid var(--sf-border-soft, #eef0f2); font-size:12.5px; }
|
||||
@media (max-width:768px) {
|
||||
.master-customer .customer-panel__stats.mp-stats { grid-template-columns:repeat(3, minmax(0, 1fr)); }
|
||||
.customer-site__head { flex-wrap:wrap; }
|
||||
.customer-site__counts { width:100%; padding-left:24px; }
|
||||
.customer-site__body { padding-left:16px; }
|
||||
.mp-panel__head { flex-wrap:wrap; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup name="Customer">
|
||||
@@ -121,6 +317,7 @@ import { addCustomer, getCustomer, listCustomer, updateCustomer } from "@/api/ma
|
||||
import TestBatchAdoptionDialog from '@/components/TestBatchAdoptionDialog'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
import useTestBatchStore from '@/store/modules/testBatch'
|
||||
import { avatarTint, formatCount, masterRoutePath, orgInitial, personInitial, userDisplayName } from '@/utils/masterDisplay'
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
const userStore = useUserStore()
|
||||
@@ -142,6 +339,50 @@ 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 panelRow = ref(null)
|
||||
const panelDetail = ref(null)
|
||||
const panelLoading = ref(false)
|
||||
const expandedSites = ref([])
|
||||
let panelGeneration = 0
|
||||
const canQueryCustomer = computed(() => proxy.$auth.hasPermiAnd(['master:customer:query']))
|
||||
const sitePath = computed(() => masterRoutePath('site'))
|
||||
const linePath = computed(() => masterRoutePath('line'))
|
||||
const tunnelPath = computed(() => masterRoutePath('tunnel'))
|
||||
const pageSiteCount = computed(() => customerList.value.reduce((sum, row) => sum + (row.siteCount || 0), 0))
|
||||
const disabledCustomerCount = ref(undefined)
|
||||
let statusCountGeneration = 0
|
||||
const pageTotals = computed(() => customerList.value.reduce((totals, row) => {
|
||||
totals.sites += row.siteCount || 0
|
||||
totals.lines += row.lineCount || 0
|
||||
totals.tunnels += row.tunnelCount || 0
|
||||
totals.pallets += row.sitePalletCount || 0
|
||||
totals.transit += row.transitPalletCount || 0
|
||||
if (!row.siteCount && row.status !== '1') totals.withoutSite += 1
|
||||
return totals
|
||||
}, { sites: 0, lines: 0, tunnels: 0, pallets: 0, transit: 0, withoutSite: 0 }))
|
||||
const panelSites = computed(() => panelDetail.value?.sites || [])
|
||||
const allSitesExpanded = computed(() => panelSites.value.length > 0 && panelSites.value.every((site) => expandedSites.value.includes(site.siteId)))
|
||||
const panelTotals = computed(() => {
|
||||
const row = panelRow.value || {}
|
||||
if (!panelDetail.value) {
|
||||
return { sites: row.siteCount ?? 0, lines: row.lineCount ?? 0, tunnels: row.tunnelCount ?? 0, pallets: row.sitePalletCount ?? 0, users: row.userCount ?? 0 }
|
||||
}
|
||||
const users = new Set(panelSites.value.flatMap((site) => site.users.map((user) => user.userId)))
|
||||
return {
|
||||
sites: panelSites.value.length,
|
||||
lines: panelSites.value.reduce((sum, site) => sum + site.lines.length, 0),
|
||||
tunnels: panelSites.value.reduce((sum, site) => sum + site.tunnels.length, 0),
|
||||
pallets: panelSites.value.reduce((sum, site) => sum + sitePalletCount(site), 0),
|
||||
users: users.size
|
||||
}
|
||||
})
|
||||
|
||||
function validateRequiredText(rule, value, callback) {
|
||||
if (typeof value !== "string" || value.trim() === "") {
|
||||
@@ -179,6 +420,8 @@ function getList() {
|
||||
}
|
||||
customerList.value = response.rows;
|
||||
total.value = response.total;
|
||||
syncPanel();
|
||||
loadDisabledCount(listContext.query);
|
||||
}).catch(() => {
|
||||
if (!isCurrentListRequest(listContext)) {
|
||||
return;
|
||||
@@ -196,6 +439,7 @@ function getList() {
|
||||
|
||||
function reset() {
|
||||
formError.value = "";
|
||||
formMeta.value = null;
|
||||
form.value = { customerId: undefined, customerCode: "", customerName: "", status: "0", remark: "" };
|
||||
proxy.resetForm("customerRef");
|
||||
}
|
||||
@@ -246,6 +490,7 @@ function handleUpdate(row = {}) {
|
||||
return;
|
||||
}
|
||||
const detail = response.data;
|
||||
formMeta.value = { by: detail.updateBy || detail.createBy, time: detail.updateTime || detail.createTime };
|
||||
form.value = {
|
||||
customerId: detail.customerId,
|
||||
customerCode: detail.customerCode,
|
||||
@@ -304,5 +549,89 @@ function submitUpdate() {
|
||||
}).finally(() => { submitLoading.value = false; });
|
||||
});
|
||||
}
|
||||
|
||||
// 停用客户数随当前筛选条件变化;已按状态筛选时无需再查
|
||||
function loadDisabledCount(query) {
|
||||
const generation = ++statusCountGeneration
|
||||
if (query.status) {
|
||||
disabledCustomerCount.value = query.status === '1' ? total.value : 0
|
||||
return
|
||||
}
|
||||
listCustomer({ ...query, pageNum: 1, pageSize: 1, status: '1' }).then((response) => {
|
||||
if (generation === statusCountGeneration) disabledCustomerCount.value = response.total
|
||||
}).catch(() => {
|
||||
if (generation === statusCountGeneration) disabledCustomerCount.value = undefined
|
||||
})
|
||||
}
|
||||
|
||||
// ---------- 客户详情:现场层级 ----------
|
||||
function customerRowClass({ row }) {
|
||||
return row.customerId === panelRow.value?.customerId ? 'is-panel-row' : ''
|
||||
}
|
||||
function handleRowClick(row, column, event) {
|
||||
if (column?.type === 'selection' || event?.target?.closest?.('button, a, .el-checkbox')) return
|
||||
openPanel(row)
|
||||
}
|
||||
// 列表刷新后保留当前客户;首次进入时默认打开第一位客户
|
||||
function syncPanel() {
|
||||
const current = panelRow.value && customerList.value.find((row) => row.customerId === panelRow.value.customerId)
|
||||
if (current) {
|
||||
panelRow.value = current
|
||||
loadPanelDetail(current, { keepExpanded: true })
|
||||
} else if (customerList.value.length && window.matchMedia?.('(min-width: 1024px)').matches !== false) {
|
||||
openPanel(customerList.value[0])
|
||||
} else {
|
||||
closePanel()
|
||||
}
|
||||
}
|
||||
function openPanel(row) {
|
||||
panelRow.value = row
|
||||
loadPanelDetail(row)
|
||||
}
|
||||
function closePanel() {
|
||||
panelGeneration += 1
|
||||
panelRow.value = null
|
||||
panelDetail.value = null
|
||||
panelLoading.value = false
|
||||
}
|
||||
function loadPanelDetail(row, { keepExpanded = false } = {}) {
|
||||
const generation = ++panelGeneration
|
||||
if (!keepExpanded) {
|
||||
panelDetail.value = null
|
||||
expandedSites.value = []
|
||||
}
|
||||
if (!canQueryCustomer.value) return
|
||||
panelLoading.value = true
|
||||
getCustomer(row.customerId).then((response) => {
|
||||
if (generation !== panelGeneration) return
|
||||
panelDetail.value = response.data || null
|
||||
if (!keepExpanded && panelSites.value.length) expandedSites.value = [panelSites.value[0].siteId]
|
||||
}).catch(() => {
|
||||
if (generation !== panelGeneration) return
|
||||
panelDetail.value = null
|
||||
}).finally(() => {
|
||||
if (generation === panelGeneration) panelLoading.value = false
|
||||
})
|
||||
}
|
||||
function toggleSite(siteId) {
|
||||
expandedSites.value = expandedSites.value.includes(siteId)
|
||||
? expandedSites.value.filter((id) => id !== siteId)
|
||||
: [...expandedSites.value, siteId]
|
||||
}
|
||||
function toggleAllSites() {
|
||||
expandedSites.value = allSitesExpanded.value ? [] : panelSites.value.map((site) => site.siteId)
|
||||
}
|
||||
function sitePalletCount(site) {
|
||||
return (site.unusedPalletCount || 0) + (site.partialPalletCount || 0) + (site.emptyPalletCount || 0)
|
||||
}
|
||||
function shortTrace(traceNo) {
|
||||
const text = String(traceNo || '')
|
||||
return text.length > 14 ? `${text.slice(0, 6)}…${text.slice(-4)}` : text
|
||||
}
|
||||
// 现场、产线、隧道由各自页面维护,这里带着上级 ID 跳转过去
|
||||
function goMaster(segment, query) {
|
||||
const path = masterRoutePath(segment)
|
||||
if (path) proxy.$router.push({ path, query })
|
||||
}
|
||||
getList();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user