feat: 将产品图/说明书备注字段从JSON数组中分离为独立数据库列

- 数据库新增 product_image_remark 和 manual_link_remark 两个TEXT列
- 备注文字不再与上传文件URL混存在 generalImage/generalManual JSON数组中
- 前端输入框改为textarea,绑定独立的备注字段
- 修复 handlePasteLink 忽略field参数的bug
- 新增 isInternalFile 辅助函数,用于区分上传文件与纯文本
- 同步修改 list.vue 和 buyOdoo.vue 两个页面
This commit is contained in:
yueli
2026-07-10 10:52:23 +08:00
parent 4fb5a96c4c
commit ffafa635bb
4 changed files with 61 additions and 62 deletions

View File

@ -510,13 +510,13 @@
</div>
</div>
<el-input
v-model="imageExternalUrl"
placeholder="如有外部图片链接请在此输入"
v-model="form.productImageRemark"
type="textarea"
:rows="1"
placeholder="请输入产品图备注信息"
style="margin-top: 8px;"
clearable
>
<template #prefix><el-icon><Link /></el-icon></template>
</el-input>
/>
<div style="color: #409EFF; font-size: 12px; margin-top: 4px;">支持将鼠标悬停于虚线框内通过 Ctrl+V 粘贴图片快速上传</div>
</el-form-item>
@ -564,13 +564,13 @@
</div>
</div>
<el-input
v-model="manualExternalUrl"
placeholder="如有外部说明书链接请在此输入"
v-model="form.manualLinkRemark"
type="textarea"
:rows="1"
placeholder="请输入说明书备注信息"
style="margin-top: 8px;"
clearable
>
<template #prefix><el-icon><Link /></el-icon></template>
</el-input>
/>
<div style="color: #409EFF; font-size: 12px; margin-top: 4px;">支持将鼠标悬停于虚线框内通过 Ctrl+V 粘贴图片快速上传</div>
</el-form-item>
@ -674,7 +674,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted, nextTick, computed, watch } from 'vue';
import { Plus, Document, DocumentCopy, Refresh, Setting, Rank, Camera, Link, Download, Bell, CircleCheck, Files, ZoomIn, Delete, Picture, FolderOpened } from '@element-plus/icons-vue';
import { Plus, Document, DocumentCopy, Refresh, Setting, Rank, Camera, Download, Bell, CircleCheck, Files, ZoomIn, Delete, Picture, FolderOpened } from '@element-plus/icons-vue';
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus';
import { useUserStore } from '@/stores/user';
@ -786,8 +786,6 @@ const operatorOptions = ref([
const fileListImage = ref<any[]>([]);
const fileListManual = ref<any[]>([]);
const imageExternalUrl = ref('');
const manualExternalUrl = ref('');
const dialogVisibleImage = ref(false);
const dialogImageUrl = ref('');
const cameraDialogVisible = ref(false);
@ -1025,7 +1023,8 @@ const dialog = reactive({ visible: false, title: '' });
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
visibilityLevel: 0, generalManual: [] as string[], generalImage: [] as string[], isEnabled: true,
productImageRemark: '', manualLinkRemark: ''
};
const form = ref({...initForm});
@ -1176,13 +1175,12 @@ const handleEdit = (row: MaterialBaseVO) => {
} else { tempCategoryPrefix.value = []; tempCategorySuffix.value = ''; }
const images = row.generalImage || []; const manuals = row.generalManual || [];
const imgFiles = images.filter(u => !isExternalLink(u)); const imgLinks = images.filter(u => isExternalLink(u));
const manualFiles = manuals.filter(u => !isExternalLink(u)); const manualLinks = manuals.filter(u => isExternalLink(u));
// 只显示真正的上传文件,备注字段已由 Object.assign 自动回显
const imgFiles = images.filter(u => isInternalFile(u));
const manualFiles = manuals.filter(u => isInternalFile(u));
fileListImage.value = imgFiles.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }));
imageExternalUrl.value = imgLinks.length > 0 ? imgLinks[0] : '';
fileListManual.value = manualFiles.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }));
manualExternalUrl.value = manualLinks.length > 0 ? manualLinks[0] : '';
});
};
@ -1210,7 +1208,7 @@ const isArraysEqual = (a: any[], b: any[]): boolean => {
const buildPartialPayload = (current: any, original: any): any => {
const payload: any = { id: current.id };
const compareFields = ['name', 'commonName', 'category', 'type', 'spec', 'unit', 'visibilityLevel', 'isEnabled', 'isInspectionRequired', 'generalImage', 'generalManual', 'companyName'];
const compareFields = ['name', 'commonName', 'category', 'type', 'spec', 'unit', 'visibilityLevel', 'isEnabled', 'isInspectionRequired', 'generalImage', 'generalManual', 'companyName', 'productImageRemark', 'manualLinkRemark'];
for (const key of compareFields) {
const currentVal = current[key]; const originalVal = original[key];
if (Array.isArray(currentVal) && Array.isArray(originalVal)) { if (!isArraysEqual(currentVal, originalVal)) payload[key] = currentVal; }
@ -1228,10 +1226,9 @@ const submitForm = async () => {
const isDuplicate = await checkDuplicate(form.value.name, form.value.spec);
if (isDuplicate) { submitLoading.value = false; return; }
const finalImageList = form.value.generalImage.filter(item => !isExternalLink(item));
if (imageExternalUrl.value) finalImageList.push(imageExternalUrl.value);
const finalManualList = form.value.generalManual.filter(item => !isExternalLink(item));
if (manualExternalUrl.value) finalManualList.push(manualExternalUrl.value);
// 只保留上传文件,过滤掉旧数据中混入的备注文字和外部链接
const finalImageList = form.value.generalImage.filter(item => isInternalFile(item));
const finalManualList = form.value.generalManual.filter(item => isInternalFile(item));
const prefixStr = tempCategoryPrefix.value.join('/'); const suffixStr = tempCategorySuffix.value.trim();
let fullCategory = '';
@ -1272,7 +1269,7 @@ const resetForm = () => {
form.value = JSON.parse(JSON.stringify(initForm));
fileListImage.value = []; fileListManual.value = [];
tempCategoryPrefix.value = []; tempCategorySuffix.value = '';
imageExternalUrl.value = ''; manualExternalUrl.value = ''; originalForm.value = null;
originalForm.value = null;
if (formRef.value) formRef.value.resetFields();
};
@ -1337,6 +1334,7 @@ const tableRowClassName = ({ row }: { row: MaterialBaseVO }) => {
const getImageUrl = (url: string) => { return !url ? '' : (url.startsWith('http') ? url : url) }
const isExternalLink = (str: string) => { return str && (str.startsWith('http://') || str.startsWith('https://')) && !str.includes('/api/v1/common/files') }
const isInternalFile = (str: string) => { return str && (str.includes('/api/v1/common/files') || /\.(jpg|jpeg|png|gif|webp|bmp|pdf|zip|rar|7z)$/i.test(str)) }
const isImageFile = (url: string) => { return /\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(url) }
const isCompressedFile = (url: string) => { return /\.(zip|rar|7z)$/i.test(url) }
const getImagesOnly = (list: string[]) => { return !list ? [] : list.filter(item => !isExternalLink(item) && isImageFile(item)) }
@ -1378,9 +1376,9 @@ const customUpload = async (options: any, targetField: 'generalImage' | 'general
} catch (e) { ElMessage.error('网络错误'); onError(e) } finally { isUploading.value = false }
}
const handlePasteLink = (link: string, field: string) => { imageExternalUrl.value = link }
usePasteUpload(customUpload, 'generalImage', '#upload-generalImage', handlePasteLink)
usePasteUpload(customUpload, 'generalManual', '#upload-generalManual', handlePasteLink)
// 粘贴上传处理器(仅处理实际图片文件,不再将链接自动填入备注框)
usePasteUpload(customUpload, 'generalImage', '#upload-generalImage')
usePasteUpload(customUpload, 'generalManual', '#upload-generalManual')
const handleRemoveImage = async (uploadFile: any, targetField: 'generalImage' | 'generalManual') => {
const fileName = uploadFile.name || uploadFile.url?.split('/').pop() || '此文件'

View File

@ -505,13 +505,13 @@
</div>
</div>
<el-input
v-model="imageExternalUrl"
placeholder="如有外部图片链接请在此输入"
v-model="form.productImageRemark"
type="textarea"
:rows="1"
placeholder="请输入产品图备注信息"
style="margin-top: 8px;"
clearable
>
<template #prefix><el-icon><Link /></el-icon></template>
</el-input>
/>
<div style="color: #409EFF; font-size: 12px; margin-top: 4px;">支持将鼠标悬停于虚线框内通过 Ctrl+V 粘贴图片快速上传</div>
</el-form-item>
@ -559,13 +559,13 @@
</div>
</div>
<el-input
v-model="manualExternalUrl"
placeholder="如有外部说明书链接请在此输入"
v-model="form.manualLinkRemark"
type="textarea"
:rows="1"
placeholder="请输入说明书备注信息"
style="margin-top: 8px;"
clearable
>
<template #prefix><el-icon><Link /></el-icon></template>
</el-input>
/>
<div style="color: #409EFF; font-size: 12px; margin-top: 4px;">支持将鼠标悬停于虚线框内通过 Ctrl+V 粘贴图片快速上传</div>
</el-form-item>
@ -672,7 +672,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted, nextTick, computed, watch } from 'vue';
import { Plus, Document, DocumentCopy, Refresh, Setting, Rank, Camera, Link, Download, Bell, CircleCheck, Files, ZoomIn, Delete, Picture } from '@element-plus/icons-vue';
import { Plus, Document, DocumentCopy, Refresh, Setting, Rank, Camera, Download, Bell, CircleCheck, Files, ZoomIn, Delete, Picture } from '@element-plus/icons-vue';
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus';
import { useUserStore } from '@/stores/user';
@ -787,8 +787,6 @@ const operatorOptions = ref([
// 文件上传相关
const fileListImage = ref<any[]>([]);
const fileListManual = ref<any[]>([]);
const imageExternalUrl = ref('');
const manualExternalUrl = ref('');
const dialogVisibleImage = ref(false);
const dialogImageUrl = ref('');
const cameraDialogVisible = ref(false);
@ -1101,7 +1099,9 @@ const initForm = {
visibilityLevel: 0,
generalManual: [] as string[],
generalImage: [] as string[],
isEnabled: true // 已修改为默认 true
isEnabled: true, // 已修改为默认 true
productImageRemark: '',
manualLinkRemark: '',
};
const form = ref({...initForm});
@ -1357,16 +1357,12 @@ const handleEdit = (row: MaterialBaseVO) => {
const images = row.generalImage || [];
const manuals = row.generalManual || [];
const imgFiles = images.filter(u => !isExternalLink(u));
const imgLinks = images.filter(u => isExternalLink(u));
const manualFiles = manuals.filter(u => !isExternalLink(u));
const manualLinks = manuals.filter(u => isExternalLink(u));
// 只显示真正的上传文件,备注字段已由 Object.assign 自动回显到 form.productImageRemark / form.manualLinkRemark
const imgFiles = images.filter(u => isInternalFile(u));
const manualFiles = manuals.filter(u => isInternalFile(u));
fileListImage.value = imgFiles.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }));
imageExternalUrl.value = imgLinks.length > 0 ? imgLinks[0] : '';
fileListManual.value = manualFiles.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }));
manualExternalUrl.value = manualLinks.length > 0 ? manualLinks[0] : '';
});
};
@ -1414,7 +1410,7 @@ const isArraysEqual = (a: any[], b: any[]): boolean => {
const buildPartialPayload = (current: any, original: any): any => {
const payload: any = { id: current.id };
const compareFields = ['name', 'commonName', 'category', 'type', 'spec', 'unit', 'visibilityLevel', 'isEnabled', 'isInspectionRequired', 'generalImage', 'generalManual', 'companyName'];
const compareFields = ['name', 'commonName', 'category', 'type', 'spec', 'unit', 'visibilityLevel', 'isEnabled', 'isInspectionRequired', 'generalImage', 'generalManual', 'companyName', 'productImageRemark', 'manualLinkRemark'];
for (const key of compareFields) {
const currentVal = current[key];
@ -1445,11 +1441,9 @@ const submitForm = async () => {
return;
}
const finalImageList = form.value.generalImage.filter(item => !isExternalLink(item));
if (imageExternalUrl.value) finalImageList.push(imageExternalUrl.value);
const finalManualList = form.value.generalManual.filter(item => !isExternalLink(item));
if (manualExternalUrl.value) finalManualList.push(manualExternalUrl.value);
// 只保留上传文件,过滤掉旧数据中混入的备注文字和外部链接
const finalImageList = form.value.generalImage.filter(item => isInternalFile(item));
const finalManualList = form.value.generalManual.filter(item => isInternalFile(item));
const prefixStr = tempCategoryPrefix.value.join('/');
const suffixStr = tempCategorySuffix.value.trim();
@ -1535,8 +1529,6 @@ const resetForm = () => {
tempCategoryPrefix.value = [];
tempCategorySuffix.value = '';
imageExternalUrl.value = '';
manualExternalUrl.value = '';
originalForm.value = null;
if (formRef.value) formRef.value.resetFields();
};
@ -1690,6 +1682,7 @@ const tableRowClassName = ({ row }: { row: MaterialBaseVO }) => {
const getImageUrl = (url: string) => { return !url ? '' : (url.startsWith('http') ? url : url) }
const isExternalLink = (str: string) => { return str && (str.startsWith('http://') || str.startsWith('https://')) && !str.includes('/api/v1/common/files') }
const isInternalFile = (str: string) => { return str && (str.includes('/api/v1/common/files') || /\.(jpg|jpeg|png|gif|webp|bmp|pdf|zip|rar|7z)$/i.test(str)) }
const isImageFile = (url: string) => { return /\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(url) }
const isCompressedFile = (url: string) => { return /\.(zip|rar|7z)$/i.test(url) }
const getImagesOnly = (list: string[]) => { return !list ? [] : list.filter(item => !isExternalLink(item) && isImageFile(item)) }
@ -1766,12 +1759,9 @@ const customUpload = async (options: any, targetField: 'generalImage' | 'general
finally { isUploading.value = false }
}
// 粘贴上传处理器(PC 端:鼠标悬停 + Ctrl+V 直接粘贴图片
const handlePasteLink = (link: string, field: string) => {
imageExternalUrl.value = link
}
usePasteUpload(customUpload, 'generalImage', '#upload-generalImage', handlePasteLink)
usePasteUpload(customUpload, 'generalManual', '#upload-generalManual', handlePasteLink)
// 粘贴上传处理器(仅处理实际图片文件,不再将链接自动填入备注框
usePasteUpload(customUpload, 'generalImage', '#upload-generalImage')
usePasteUpload(customUpload, 'generalManual', '#upload-generalManual')
const handleRemoveImage = async (uploadFile: any, targetField: 'generalImage' | 'generalManual') => {
const fileName = uploadFile.name || uploadFile.url?.split('/').pop() || '此文件'