fix: 修复项目列表新增/编辑的bug

This commit is contained in:
Zzz1zz 2025-08-13 01:26:00 +08:00
parent 194e0faa72
commit ddb35b10f1
4 changed files with 92 additions and 76 deletions

View file

@ -9,21 +9,21 @@
- 支持上传进度显示 - 支持上传进度显示
- 支持图片预览 - 支持图片预览
- 支持多文件上传 - 支持多文件上传
- 上传成功后自动回传URL - 上传成功后自动回传 URL
## Props ## Props
| 参数 | 类型 | 默认值 | 说明 | | 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------| | ---------- | ------------ | ---------------- | ---------------------------- |
| modelValue | UploadFile[] | [] | 文件列表,支持v-model | | modelValue | UploadFile[] | [] | 文件列表,支持 v-model |
| uploadType | UploadType | - | 上传类型:1合同模版,2图片 | | uploadType | UploadType | - | 上传类型:1 合同模版,2 图片 |
| maxSize | number | 10 * 1024 * 1024 | 最大文件大小(字节) | | maxSize | number | 10 _ 1024 _ 1024 | 最大文件大小(字节) |
| maxCount | number | - | 最大上传数量 | | maxCount | number | - | 最大上传数量 |
## Events ## Events
| 事件名 | 参数 | 说明 | | 事件名 | 参数 | 说明 |
|--------|------|------| | ----------------- | -------------------------------------------- | ------------------ |
| update:modelValue | (value: UploadFile[]) | 文件列表更新时触发 | | update:modelValue | (value: UploadFile[]) | 文件列表更新时触发 |
| upload-success | (file: UploadFile, response: UploadResponse) | 上传成功时触发 | | upload-success | (file: UploadFile, response: UploadResponse) | 上传成功时触发 |
| upload-error | (error: Error) | 上传失败时触发 | | upload-error | (error: Error) | 上传失败时触发 |
@ -42,7 +42,8 @@ interface UploadFile {
interface UploadResponse { interface UploadResponse {
filename: string; filename: string;
size: number; size: number;
url: string; signedPutURL: string;
objectKey?: string;
} }
type UploadType = 1 | 2; // 1合同模版,2图片 type UploadType = 1 | 2; // 1合同模版,2图片
@ -63,18 +64,18 @@ type UploadType = 1 | 2; // 1合同模版,2图片
</template> </template>
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
import ImageUpload from '@/components/common/ImageUpload.vue'; import ImageUpload from '@/components/common/ImageUpload.vue';
const images = ref([]); const images = ref([]);
const handleUploadSuccess = (file, response) => { const handleUploadSuccess = (file, response) => {
console.log('上传成功:', response.url); console.log('上传成功:', response.url);
}; };
const handleUploadError = (error) => { const handleUploadError = (error) => {
console.error('上传失败:', error.message); console.error('上传失败:', error.message);
}; };
</script> </script>
``` ```
@ -110,6 +111,6 @@ const handleUploadError = (error) => {
## 注意事项 ## 注意事项
1. 组件会自动处理文件验证,包括文件大小和格式检查 1. 组件会自动处理文件验证,包括文件大小和格式检查
2. 上传成功后,文件对象会自动更新URL和状态 2. 上传成功后,文件对象会自动更新 URL 和状态
3. 支持v-model双向绑定,父组件可以直接操作文件列表 3. 支持 v-model 双向绑定,父组件可以直接操作文件列表
4. 上传类型必须从父组件传入,用于区分不同的上传场景 4. 上传类型必须从父组件传入,用于区分不同的上传场景

View file

@ -1,7 +1,7 @@
<template> <template>
<a-upload <a-upload
:list-type="listType" :list-type="listType"
:accept="'image/*'" accept="image/*"
:custom-request="customRequest" :custom-request="customRequest"
:file-list="fileList" :file-list="fileList"
:multiple="maxCount > 1" :multiple="maxCount > 1"
@ -22,48 +22,52 @@
url?: string; url?: string;
status: 'done' | 'uploading' | 'error'; status: 'done' | 'uploading' | 'error';
percent?: number; percent?: number;
objectKey?: string;
} }
interface UploadResponse { interface UploadResponse {
filename: string; filename: string;
size: number; size: number;
url: string; signedPutURL: string;
objectKey: string;
} }
const props = defineProps<{ const props = defineProps<{
modelValue: string[]; // 只暴露图片url数组 objectKeys: string[]; // 提交用的 Key 数组
signedPutURL: string[]; // 显示用 URL 数组
uploadType: UploadType; uploadType: UploadType;
maxSize?: number; // 默认10MB maxSize?: number;
maxCount?: number; // 最大上传数量,默认1(单图) maxCount?: number;
listType?: 'text' | 'picture' | 'picture-card'; // 上传样式 listType?: 'text' | 'picture' | 'picture-card';
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:modelValue', value: string[]): void; (e: 'update:objectKeys', value: string[]): void;
(e: 'upload-success', url: string): void; (e: 'update:signedPutURL', value: string[]): void;
(
e: 'upload-success',
payload: { objectKey: string; signedPutURL: string }
): void;
(e: 'upload-error', error: Error): void; (e: 'upload-error', error: Error): void;
}>(); }>();
const fileList = ref<UploadFile[]>([]); const fileList = ref<UploadFile[]>([]);
// 默认最大上传数:1(单图)
const maxCount = props.maxCount ?? 1; const maxCount = props.maxCount ?? 1;
// listType 默认值
const listType = props.listType ?? 'picture-card'; const listType = props.listType ?? 'picture-card';
// 将外部 url[] 映射为 UploadFile[] // 回显处理(编辑/刷新时)
watch( watch(
() => props.modelValue, () => [props.objectKeys, props.signedPutURL],
(newVal) => { ([keys, urls]) => {
fileList.value = (newVal || []).map((url, index) => ({ fileList.value = (keys || []).map((key, idx) => ({
uid: `${index}`, uid: `${idx}`,
name: `图片-${index + 1}`, name: key.split('/').pop() || `图片-${idx + 1}`,
url, url: urls[idx] || '',
objectKey: key,
status: 'done', status: 'done',
})); }));
}, },
{ immediate: true } { immediate: true, deep: true }
); );
// 自定义上传请求 // 自定义上传请求
@ -80,15 +84,12 @@
'image/webp', 'image/webp',
]; ];
// 文件大小校验
if (file.size > maxSize) { if (file.size > maxSize) {
onError( onError(
new Error(`文件大小不能超过${Math.round(maxSize / 1024 / 1024)}MB`) new Error(`文件大小不能超过${Math.round(maxSize / 1024 / 1024)}MB`)
); );
return; return;
} }
// 文件类型校验
if (!allowedTypes.includes(file.type)) { if (!allowedTypes.includes(file.type)) {
onError(new Error('只支持 JPG、PNG、GIF、WEBP 格式的图片')); onError(new Error('只支持 JPG、PNG、GIF、WEBP 格式的图片'));
return; return;
@ -114,44 +115,54 @@
clearInterval(progressInterval); clearInterval(progressInterval);
if (code === 200 && data) { if (code === 200 && data) {
const response = data as UploadResponse; const res = data as UploadResponse;
fileItem.status = 'done'; fileItem.status = 'done';
fileItem.url = response.url; fileItem.url = res.signedPutURL;
fileItem.objectKey = res.objectKey;
onProgress({ percent: 100 }); onProgress({ percent: 100 });
onSuccess(response); onSuccess(res);
// 新的图片 url // 更新父组件两个数组
let updatedUrls = [...props.modelValue]; let newKeys = [...props.objectKeys];
let newUrls = [...props.signedPutURL];
if (maxCount === 1) { if (maxCount === 1) {
updatedUrls = [response.url]; newKeys = [res.objectKey];
newUrls = [res.signedPutURL];
} else { } else {
updatedUrls.push(response.url); newKeys.push(res.objectKey);
// 截取至 maxCount newUrls.push(res.signedPutURL);
updatedUrls = updatedUrls.slice(0, maxCount); newKeys = newKeys.slice(0, maxCount);
newUrls = newUrls.slice(0, maxCount);
} }
emit('update:objectKeys', newKeys);
emit('update:modelValue', updatedUrls); emit('update:signedPutURL', newUrls);
emit('upload-success', response.url); emit('upload-success', {
objectKey: res.objectKey,
signedPutURL: res.signedPutURL,
});
} else { } else {
const error = new Error(message || '上传失败'); const error = new Error(message || '上传失败');
onError(error); onError(error);
emit('upload-error', error); emit('upload-error', error);
} }
} catch (error) { } catch (e) {
const err = error instanceof Error ? error : new Error('上传失败'); const err = e instanceof Error ? e : new Error('上传失败');
onError(err); onError(err);
emit('upload-error', err); emit('upload-error', err);
} }
}; };
// 同步 fileList(删除时触发) // 删除文件时同步更新父组件
const handleChange = (newFileList: UploadFile[]) => { const handleChange = (newFileList: UploadFile[]) => {
const validUrls = newFileList const newKeys = newFileList
.filter((f) => f.status === 'done' && f.objectKey)
.map((f) => f.objectKey!);
const newUrls = newFileList
.filter((f) => f.status === 'done' && f.url) .filter((f) => f.status === 'done' && f.url)
.map((f) => f.url!); .map((f) => f.url!);
emit('update:modelValue', validUrls); emit('update:objectKeys', newKeys);
emit('update:signedPutURL', newUrls);
}; };
</script> </script>

View file

@ -72,15 +72,16 @@ export type ProjectListRes = HttpResponse<{
projectsList?: ProjectItem[]; projectsList?: ProjectItem[];
}>; }>;
export interface CreateProjectParam { export interface CreateProjectParam {
id?: number; id?: number | null;
type: ProjectType; // 项目类型:1写字楼,2商业综合体,3园区 type: ProjectType; // 项目类型:1写字楼,2商业综合体,3园区
name: string; // 楼栋名称 name: string; // 楼栋名称
province_id: number | null; // 省份id province_id: number | null; // 省份id
city_id: number | null; // 城市id city_id: number | null; // 城市id
district_id: number | null; // 区域id district_id: number | null; // 区域id
address: string; // 详细地址 address: string; // 详细地址
images: string[]; // 图片数组 objectKeys: string[]; // 存储给后端的 key
buildings: BuildingItem[]; // 楼栋数据 signedPutURL: string[]; // 存储已签名 URL
buildings: BuildingItem[];
} }
export interface BuildingInfo extends BaseEntity { export interface BuildingInfo extends BaseEntity {
projectId: number; // 项目id projectId: number; // 项目id

View file

@ -104,12 +104,13 @@
</a-col> </a-col>
</a-row> </a-row>
</a-form-item> </a-form-item>
<a-form-item label="项目图片" field="images"> <a-form-item label="项目图片" field="objectKeys">
<ImageUpload <ImageUpload
v-model="form.images" v-model:object-keys="form.objectKeys"
v-model:signed-put-u-r-l="form.signedPutURL"
:upload-type="2" :upload-type="2"
:max-count="5" :max-count="5"
:list-type="'picture-card'" list-type="picture-card"
/> />
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
@ -214,13 +215,15 @@
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
const defaultForm: CreateProjectParam = { const defaultForm: CreateProjectParam = {
id: null,
type: 1 as ProjectType, // 项目类型:1写字楼,2商业综合体,3园区 type: 1 as ProjectType, // 项目类型:1写字楼,2商业综合体,3园区
name: '', // 楼栋名称 name: '', // 楼栋名称
province_id: null, // 省份id province_id: null, // 省份id
city_id: null, // 城市id city_id: null, // 城市id
district_id: null, // 区域id district_id: null, // 区域id
address: '', // 详细地址 address: '', // 详细地址
images: [], // 图片数组 objectKeys: [], // 图片数组
signedPutURL: [], // 图片数组
buildings: [createEmptyBuilding()], // 楼栋数据 buildings: [createEmptyBuilding()], // 楼栋数据
}; };
const form = reactive<CreateProjectParam>({ ...defaultForm }); const form = reactive<CreateProjectParam>({ ...defaultForm });
@ -306,7 +309,7 @@
}); });
} }
if (record?.imageUrls?.length) { if (record?.imageUrls?.length) {
record.images = record?.imageUrls; record.objectKeys = record?.imageUrls;
} }
form.name = record.name; form.name = record.name;
Object.keys(form).forEach((key) => { Object.keys(form).forEach((key) => {
@ -330,7 +333,6 @@
}, },
{ immediate: true } { immediate: true }
); );
const handleOk = async () => { const handleOk = async () => {
if (isLoading.value) return; if (isLoading.value) return;
try { try {
@ -345,9 +347,10 @@
id: item?.id, id: item?.id,
}; };
}); });
const params = { const params: any = {
...form, ...form,
}; };
delete params.signedPutURL;
const res = props?.isEdit const res = props?.isEdit
? await updateProject(params) ? await updateProject(params)
: await createProject(params); : await createProject(params);