feat: 添加参考价格列 + 修复公司筛选跨域问题 + CLIP模型持久化

## 新增功能
- material_base 表新增 reference_price 列(NUMERIC(10,2))
- 基础信息 list.vue / buyOdoo.vue 页面增加「参考价格」列展示和编辑
- 新增 material_list:referencePrice 权限元素,支持按角色控制可见性

## Bug 修复
- 入库三页面(buy/product/semi)公司筛选:非超管用户 company=ALL 不再传给旧版后端
- 基础信息两页面(list/buyOdoo):buyOdoo 增加 v-if=isSuperAdmin 与 list.vue 行为统一
- company=ALL 默认值在各页面 getList/fetchData 中自动过滤,兼容旧版后端

## 运维优化
- docker-compose.prod.yml 增加 models_prod 卷挂载,CLIP模型持久化免重复上传
- deploy_code.sh 增加 models_prod 目录检查与模型文件存在性告警
This commit is contained in:
yueli
2026-07-13 17:30:52 +08:00
parent 4f5965db02
commit 760f78e016
10 changed files with 112 additions and 24 deletions

View File

@ -21,6 +21,7 @@
</el-input>
<el-select
v-if="isSuperAdmin"
v-model="queryParams.company"
placeholder="所属公司"
clearable
@ -29,6 +30,7 @@
style="width: 120px; margin-right: 10px;"
@change="handleQuery"
>
<el-option label="全部 (跨域)" value="ALL" />
<el-option v-for="item in companyOptions" :key="item" :label="item" :value="item" />
</el-select>
@ -194,6 +196,7 @@
<el-checkbox v-if="hasColPermission('files')" v-model="columns.files.visible" label="资料" />
<el-checkbox v-if="hasColPermission('isEnabled')" v-model="columns.isEnabled.visible" label="状态" />
<el-checkbox v-if="hasColPermission('isInspectionRequired')" v-model="columns.isInspectionRequired.visible" label="强制质检" />
<el-checkbox v-if="hasColPermission('referencePrice')" v-model="columns.referencePrice.visible" label="参考价格" />
<el-checkbox v-if="hasColPermission('warningStatus')" v-model="columns.warningStatus.visible" label="预警状态" />
</div>
</el-popover>
@ -330,6 +333,12 @@
</el-tag>
</template>
</el-table-column>
<el-table-column v-if="columns.referencePrice.visible" prop="referencePrice" label="参考价格" min-width="120" align="center" sortable="custom">
<template #default="scope">
<span v-if="scope.row.referencePrice != null" class="money-text">{{ scope.row.referencePrice?.toFixed(2) }}</span>
<span v-else style="color: #ccc;">-</span>
</template>
</el-table-column>
<el-table-column v-if="columns.warningStatus.visible" label="预警状态" width="120" align="center">
<template #default="{ row }">
<template v-if="row.warningStatus === 2">
@ -491,6 +500,14 @@
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="参考价格" prop="referencePrice" v-if="hasFieldPermission('referencePrice')">
<el-input-number v-model="form.referencePrice" :precision="2" :min="0" controls-position="right" style="width: 100%" placeholder="请输入参考价格" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="产品图" prop="generalImage" v-if="hasFieldPermission('files')">
<div class="upload-container" id="upload-generalImage">
<el-upload
@ -701,6 +718,7 @@ import ImageSearchDialog from '@/components/ImageSearchDialog.vue';
import { imageSearch as imageSearchApi, type ImageSearchItem } from '@/api/common/upload';
const userStore = useUserStore();
const isSuperAdmin = computed(() => userStore.role === 'SUPER_ADMIN');
// --- 类型定义 ---
interface MaterialBaseVO {
@ -723,6 +741,7 @@ interface MaterialBaseVO {
warningOrdered?: boolean;
warningRedEmails?: string;
warningYellowEmails?: string;
referencePrice?: number;
}
interface QueryParams {
@ -770,7 +789,8 @@ const fieldOptions = computed(() => {
{ value: 'spec', label: '规格型号', perm: 'material_list:spec' },
{ value: 'unit', label: '单位', perm: 'material_list:unit' },
{ value: 'inventoryCount', label: '库存数', perm: 'material_list:inventoryCount' },
{ value: 'availableCount', label: '可用数', perm: 'material_list:availableCount' }
{ value: 'availableCount', label: '可用数', perm: 'material_list:availableCount' },
{ value: 'referencePrice', label: '参考价格', perm: 'material_list:referencePrice' }
];
return allFields.filter(item => userStore.hasPermission(item.perm));
});
@ -803,7 +823,7 @@ const queryParams = reactive<QueryParams>({
searchField: 'all',
category: '',
type: '',
company: '',
company: 'ALL',
isEnabled: undefined,
orderByColumn: '',
isAsc: undefined,
@ -925,7 +945,7 @@ const columns = reactive({
commonName: { visible: true }, category: { visible: true }, type: { visible: true },
spec: { visible: true }, unit: { visible: true }, inventory: { visible: true },
available: { visible: true }, files: { visible: true }, isEnabled: { visible: true },
isInspectionRequired: { visible: true }, warningStatus: { visible: true }
isInspectionRequired: { visible: true }, referencePrice: { visible: true }, warningStatus: { visible: true }
});
const permissionMap: Record<string, string> = {
@ -933,7 +953,8 @@ const permissionMap: Record<string, string> = {
commonName: 'material_list:commonName', category: 'material_list:category', type: 'material_list:type',
spec: 'material_list:spec', unit: 'material_list:unit', inventory: 'material_list:inventoryCount',
available: 'material_list:availableCount', files: 'material_list:files', isEnabled: 'material_list:isEnabled',
isInspectionRequired: 'material_list:operation', warningStatus: 'material_list:view_warning'
isInspectionRequired: 'material_list:operation', referencePrice: 'material_list:referencePrice',
warningStatus: 'material_list:view_warning'
};
const getStorageKey = () => `MOM_BASIC_INFO_COLS_${userStore.username || 'DEFAULT'}`;
@ -1024,6 +1045,7 @@ const formRef = ref<FormInstance>();
const initForm = {
id: undefined, companyName: '', name: '', commonName: '', category: '', type: '', spec: '', unit: '',
visibilityLevel: 0, generalManual: [] as string[], generalImage: [] as string[], isEnabled: true,
referencePrice: undefined as number | undefined,
productImageRemark: '', manualLinkRemark: ''
};
const form = ref({...initForm});
@ -1096,6 +1118,10 @@ const getList = () => {
...queryParams,
advancedFilters: JSON.stringify(queryParams.advancedFilters || [])
};
// 兼容旧版后端company=ALL 时不传过滤参数
if (params.company === 'ALL') {
delete params.company
}
listMaterialBase(params).then((response: any) => {
if (response && response.data) {
tableData.value = response.data.items;
@ -1133,7 +1159,7 @@ const handleInputSearch = () => {
};
const handleSortChange = ({ column, prop, order }: any) => {
const sortableColumns = ['inventoryCount', 'availableCount', 'companyName', 'name', 'commonName', 'category', 'type', 'spec', 'unit'];
const sortableColumns = ['inventoryCount', 'availableCount', 'referencePrice', 'companyName', 'name', 'commonName', 'category', 'type', 'spec', 'unit'];
if (prop && sortableColumns.includes(prop)) {
queryParams.orderByColumn = prop;
queryParams.isAsc = order === 'ascending' ? 'asc' : order === 'descending' ? 'desc' : undefined;
@ -1147,7 +1173,7 @@ const handleSortChange = ({ column, prop, order }: any) => {
const handleQuery = () => { getList(); };
const resetQuery = () => {
queryParams.keyword = ''; queryParams.searchField = 'all'; queryParams.category = '';
queryParams.type = ''; queryParams.company = ''; queryParams.isEnabled = undefined;
queryParams.type = ''; queryParams.company = isSuperAdmin.value ? 'ALL' : ''; queryParams.isEnabled = undefined;
queryParams.orderByColumn = ''; queryParams.isAsc = undefined; queryParams.has_stock = '';
selectedItems.value = []; groupSelections.value = {};
Object.values(tableRefs.value).forEach(t => t?.clearSelection?.());