diff --git a/admin-snack/src/api/file.ts b/admin-snack/src/api/file.ts index f953e80..a01592d 100644 --- a/admin-snack/src/api/file.ts +++ b/admin-snack/src/api/file.ts @@ -2,15 +2,57 @@ import { request } from '@/utils/request' /** * 通用文件上传 + * + * @param file 要上传的文件 + * @param bizType 业务类型:avatar / admin-avatar / product / sku / banner / notice / chat / common + * @returns 上传结果(含 url/path/id) */ -export function uploadFileApi(file: File, folder = 'common') { +export function uploadFileApi(file: File, bizType = 'common') { const formData = new FormData() formData.append('file', file) - formData.append('folder', folder) - return request<{ url: string; name: string; size: number }>({ - url: '/api/admin/file/upload', + return request<{ + id: number + name: string + url: string + path: string + size: number + contentType: string + bizType: string + createTime: string + }>({ + url: '/api/file/upload', method: 'POST', + params: { bizType }, data: formData, + // 注意:axios 上传文件时不要手动设 Content-Type,让浏览器自动加 boundary headers: { 'Content-Type': 'multipart/form-data' } }) } + +/** 管理员头像上传(带登录态) */ +export function uploadAdminAvatarApi(file: File) { + return uploadFileApi(file, 'admin-avatar') +} + +/** 商品图片上传 */ +export function uploadProductImageApi(file: File) { + return uploadFileApi(file, 'product') +} + +/** 分类图标上传 */ +export function uploadCategoryIconApi(file: File) { + return uploadFileApi(file, 'category') +} + +/** 轮播图上传 */ +export function uploadBannerImageApi(file: File) { + return uploadFileApi(file, 'banner') +} + +/** 删除文件 */ +export function deleteFileApi(id: number) { + return request({ + url: `/api/file/${id}`, + method: 'DELETE' + }) +} diff --git a/admin-snack/src/components/business/ImageUploader.vue b/admin-snack/src/components/business/ImageUploader.vue new file mode 100644 index 0000000..6edff7f --- /dev/null +++ b/admin-snack/src/components/business/ImageUploader.vue @@ -0,0 +1,187 @@ + + + + + diff --git a/admin-snack/src/views/product/list.vue b/admin-snack/src/views/product/list.vue index 8432fd2..394f000 100644 --- a/admin-snack/src/views/product/list.vue +++ b/admin-snack/src/views/product/list.vue @@ -52,6 +52,8 @@ import type { StockAdjustPayload } from '@/api/product' import { getCategoryTreeApi, getCategoryOptionsApi } from '@/api/product' +import ImageUploader from '@/components/business/ImageUploader.vue' +import { Plus } from '@element-plus/icons-vue' import type { CategoryOption } from '@/api/product' import { formatDate } from '@/utils/date' import SearchBar from '@/components/common/SearchBar.vue' @@ -183,8 +185,8 @@ interface FormState { categoryId: number | undefined brand: string mainImage: string - /** 副图:换行分隔的 URL 列表(保存时转 JSON 数组字符串) */ - subImagesText: string + /** 副图:URL 列表(保存时由前端序列化为 JSON 数组字符串) */ + subImages: string[] detail: string originPrice: number | undefined status: 0 | 1 @@ -202,7 +204,7 @@ const form = reactive({ categoryId: undefined, brand: '', mainImage: '', - subImagesText: '', + subImages: [], detail: '', originPrice: undefined, status: 1, @@ -211,6 +213,21 @@ const form = reactive({ skuList: [emptySku()] }) +/** 副图独立 state 方便多图 UI 操作 */ +const subImagesList = ref([]) + +function addSubImage() { + subImagesList.value.push('') +} + +function updateSubImage(idx: number, url: string | undefined) { + if (url) { + subImagesList.value[idx] = url + } else { + subImagesList.value.splice(idx, 1) + } +} + const rules = computed>(() => ({ name: [ { required: true, message: '请输入商品名称', trigger: 'blur' }, @@ -262,7 +279,7 @@ async function openCreate() { categoryId: undefined, brand: '', mainImage: '', - subImagesText: '', + subImages: [], detail: '', originPrice: undefined, status: 1, @@ -270,6 +287,7 @@ async function openCreate() { isNew: 0, skuList: [emptySku()] }) + subImagesList.value = [] dialogVisible.value = true } @@ -284,13 +302,14 @@ async function openEdit(row: ProductItem) { detail = null } const d = detail ?? (row as unknown as ProductDetail) + const subImgs: string[] = d.subImages || [] Object.assign(form, { id: d.id, name: d.name ?? row.name, categoryId: d.categoryId ?? row.categoryId, brand: d.brand ?? '', mainImage: d.mainImage ?? row.mainImage ?? '', - subImagesText: (d.subImages || []).join('\n'), + subImages: subImgs, detail: d.detail ?? '', originPrice: d.originPrice, status: (d.status ?? row.status) as 0 | 1, @@ -309,6 +328,7 @@ async function openEdit(row: ProductItem) { })) : [emptySku()] }) + subImagesList.value = [...subImgs] dialogVisible.value = true } @@ -318,11 +338,8 @@ async function handleSubmit() { if (!valid) return submitting.value = true try { - // 副图:换行 / 空白分隔的 URL 串 → JSON 数组字符串 - const subImagesArr = form.subImagesText - .split(/[\n,]/g) - .map((s) => s.trim()) - .filter((s) => s.length > 0) + // 副图:从独立 state 取(ImageUploader 直接操作) + const subImagesArr = subImagesList.value.filter((s) => !!s) const payload: ProductSavePayload = { id: form.id, name: form.name, @@ -626,20 +643,35 @@ watch(categoryOptions, (v) => { - - + + - - - - +
+
+ +
+ + 添加副图 + +
+
{