效果:
advertise image支持图片上传,最多只能上传一张,上传的图片支持删除
完整代码:
<template>
<div class="ad-edit-dialog">
<el-dialog
:model-value="modelValue"
title="Advertisement"
width="580px"
@close="onClose"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="150px">
<el-form-item label="advertising position" prop="positionKey">
<el-select
v-model="form.positionKey"
placeholder="Select position"
style="width: 300px"
>
<el-option
v-for="p in positions"
:key="p.key"
:label="p.label"
:value="p.key"
/>
</el-select>
</el-form-item>
<el-form-item label="Link Type" prop="linkType">
<el-select
v-model="form.linkType"
placeholder="Select type"
style="width: 300px"
>
<el-option v-for="t in linkTypes" :key="t" :label="t" :value="t" />
</el-select>
</el-form-item>
<el-form-item label="Link" prop="link">
<el-input
v-model="form.link"
placeholder="Please input"
style="width: 300px"
/>
</el-form-item>
<el-form-item label="advertise Image">
<div class="upload-row">
<el-upload
class="upload-one"
:auto-upload="false"
:limit="1"
:file-list="fileList"
:on-change="onFileChange"
:on-remove="onRemove"
:on-exceed="onExceed"
list-type="picture-card"
>
<el-icon><Plus /></el-icon>
</el-upload>
<div class="sample">
<div>示例尺寸:{{ sampleSize }}</div>
<div v-if="actualSize">实际尺寸:{{ actualSize }}</div>
</div>
</div>
</el-form-item>
<el-form-item label="Promote Plus" prop="promotePlus">
<el-select v-model="form.promotePlus" style="width: 300px">
<el-option label="Yes" value="Yes" />
<el-option label="No" value="No" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="onClose">Cancel</el-button>
<el-button type="primary" @click="onConfirmClick">Confirm</el-button>
</span>
</template>
<ConfirmDialog v-model="confirmVisible" @confirm="submit" />
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from "vue";
import { Plus } from "@element-plus/icons-vue";
import ConfirmDialog from "./ConfirmDialog.vue";
export interface PositionItem {
key: string;
label: string;
sampleSize: string;
}
export interface AdFormModel {
id?: number;
positionKey: string;
linkType: "Internal" | "External";
link: string;
imageUrl?: string;
promotePlus: "Yes" | "No";
}
const props = defineProps<{
modelValue: boolean;
model?: Partial<AdFormModel>;
positions: PositionItem[];
linkTypes: Array<"Internal" | "External">;
}>();
const emits = defineEmits<{
(e: "update:modelValue", v: boolean): void;
(e: "submit", v: AdFormModel): void;
(e: "cancel"): void;
}>();
const formRef = ref();
const form = ref<AdFormModel>({
positionKey: "",
linkType: "Internal",
link: "",
imageUrl: "",
promotePlus: "Yes",
});
const fileList = ref<any[]>([]);
const actualSize = ref("");
watch(
() => props.model,
(v) => {
form.value = {
id: v?.id,
positionKey: v?.positionKey || "",
linkType: (v?.linkType as any) || "Internal",
link: v?.link || "",
imageUrl: v?.imageUrl,
promotePlus: (v?.promotePlus as any) || "Yes",
};
fileList.value = form.value.imageUrl
? [{ name: "image", url: form.value.imageUrl } as any]
: [];
actualSize.value = "";
},
{ immediate: true }
);
const rules = {
positionKey: [{ required: true, message: "Required", trigger: "change" }],
linkType: [{ required: true, message: "Required", trigger: "change" }],
link: [
{ required: true, message: "Required", trigger: "blur" },
{ validator: validateLink, trigger: "blur" },
],
promotePlus: [{ required: true, message: "Required", trigger: "change" }],
} as const;
function validateLink(_: any, value: string, cb: any) {
if (!value) return cb();
if (form.value.linkType === "External") {
const ok = /^https?:\/\//i.test(value);
return ok ? cb() : cb(new Error("Must start with http(s)://"));
}
cb();
}
const sampleSize = computed(() => {
const p = props.positions.find((p) => p.key === form.value.positionKey);
return p?.sampleSize || "-";
});
function onFileChange(file: any, files: any[]) {
if (files.length > 1) files.splice(0, files.length - 1);
const raw = file.raw;
if (raw) {
const url = URL.createObjectURL(raw);
form.value.imageUrl = url;
fileList.value = [{ name: raw.name, url } as any];
getImageSize(url).then((size) => {
actualSize.value = `${size.width}×${size.height}`;
});
}
}
function onRemove() {
form.value.imageUrl = "";
fileList.value = [];
actualSize.value = "";
}
function onExceed(files: any) {
onRemove();
onFileChange({ raw: files[0] }, [files[0]]);
}
function getImageSize(src: string): Promise<{ width: number; height: number }> {
return new Promise((resolve) => {
const img = new Image();
img.onload = () =>
resolve({ width: img.naturalWidth, height: img.naturalHeight });
img.src = src;
});
}
const confirmVisible = ref(false);
function onConfirmClick() {
formRef.value.validate((valid: boolean) => {
if (!valid) return;
confirmVisible.value = true;
});
}
function submit() {
emits("submit", { ...form.value });
emits("update:modelValue", false);
}
function onClose() {
emits("update:modelValue", false);
emits("cancel");
}
</script>
<style scoped lang="scss">
.upload-row {
display: flex;
flex-direction: column;
gap: 16px;
.upload-one {
display: flex;
}
}
.sample {
display: flex;
gap: 15px;
color: #6b7280;
font-size: 12px;
}
// 弹窗标题样式
.ad-edit-dialog :deep(.el-dialog__header) {
background: #f9fafb;
margin: 0;
padding: 16px;
border-bottom: 1px solid #e5e7eb;
}
.ad-edit-dialog :deep(.el-dialog__body) {
padding: 24px 24px 0 24px;
}
</style>
组件封装: SingleImageUpload.vue
<template>
<div class="single-image-upload">
<el-upload
class="upload-one"
:auto-upload="false"
list-type="picture-card"
:limit="1"
:file-list="internalList"
:on-change="onChange"
:on-remove="onRemove"
:on-exceed="onExceed"
v-bind="$attrs"
>
<slot>
<el-icon><Plus /></el-icon>
</slot>
</el-upload>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { Plus } from '@element-plus/icons-vue'
const props = defineProps<{ modelValue?: string }>()
const emits = defineEmits<{
(e: 'update:modelValue', v: string): void
(e: 'size', size: { width: number; height: number } | null): void
}>()
const internalList = ref<any[]>([])
watch(() => props.modelValue, (v) => {
internalList.value = v ? [{ name: 'image', url: v } as any] : []
}, { immediate: true })
function onChange(file: any, files: any[]) {
if (files.length > 1) files.splice(0, files.length - 1)
const raw = file.raw
if (raw) {
const url = URL.createObjectURL(raw)
internalList.value = [{ name: raw.name, url } as any]
emits('update:modelValue', url)
getImageSize(url).then(size => emits('size', size))
}
}
function onRemove() {
internalList.value = []
emits('update:modelValue', '')
emits('size', null)
}
function onExceed(files: any[]) {
onRemove()
onChange({ raw: files[0] }, [files[0]])
}
function getImageSize(src: string): Promise<{ width: number; height: number }> {
return new Promise((resolve) => {
const img = new Image()
img.onload = () => resolve({ width: img.naturalWidth, height: img.naturalHeight })
img.src = src
})
}
</script>
<style scoped lang="scss">
.upload-one {
display: inline-flex;
}
</style>
父组件:
<template>
<div class="ad-edit-dialog">
<el-dialog
:model-value="modelValue"
title="Advertisement"
width="580px"
@close="onClose"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="150px">
<el-form-item label="advertising position" prop="positionKey">
<el-select
v-model="form.positionKey"
placeholder="Select position"
style="width: 300px"
>
<el-option
v-for="p in positions"
:key="p.key"
:label="p.label"
:value="p.key"
/>
</el-select>
</el-form-item>
<el-form-item label="Link Type" prop="linkType">
<el-select
v-model="form.linkType"
placeholder="Select type"
style="width: 300px"
>
<el-option v-for="t in linkTypes" :key="t" :label="t" :value="t" />
</el-select>
</el-form-item>
<el-form-item label="Link" prop="link">
<el-input
v-model="form.link"
placeholder="Please input"
style="width: 300px"
/>
</el-form-item>
<el-form-item label="advertise Image">
<div class="upload-row">
<SingleImageUpload v-model="form.imageUrl" @size="onSize" />
<div class="sample">
<div>示例尺寸:{{ sampleSize }}</div>
<div v-if="actualSize">实际尺寸:{{ actualSize }}</div>
</div>
</div>
</el-form-item>
<el-form-item label="Promote Plus" prop="promotePlus">
<el-select v-model="form.promotePlus" style="width: 300px">
<el-option label="Yes" value="Yes" />
<el-option label="No" value="No" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="onClose">Cancel</el-button>
<el-button type="primary" @click="onConfirmClick">Confirm</el-button>
</span>
</template>
<ConfirmDialog v-model="confirmVisible" @confirm="submit" />
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from "vue";
import ConfirmDialog from "./ConfirmDialog.vue";
import SingleImageUpload from "./SingleImageUpload.vue";
export interface PositionItem {
key: string;
label: string;
sampleSize: string;
}
export interface AdFormModel {
id?: number;
positionKey: string;
linkType: "Internal" | "External";
link: string;
imageUrl?: string;
promotePlus: "Yes" | "No";
}
const props = defineProps<{
modelValue: boolean;
model?: Partial<AdFormModel>;
positions: PositionItem[];
linkTypes: Array<"Internal" | "External">;
}>();
const emits = defineEmits<{
(e: "update:modelValue", v: boolean): void;
(e: "submit", v: AdFormModel): void;
(e: "cancel"): void;
}>();
const formRef = ref();
const form = ref<AdFormModel>({
positionKey: "",
linkType: "Internal",
link: "",
imageUrl: "",
promotePlus: "Yes",
});
const actualSize = ref("");
watch(
() => props.model,
(v) => {
form.value = {
id: v?.id,
positionKey: v?.positionKey || "",
linkType: (v?.linkType as any) || "Internal",
link: v?.link || "",
imageUrl: v?.imageUrl,
promotePlus: (v?.promotePlus as any) || "Yes",
};
actualSize.value = "";
},
{ immediate: true }
);
const rules = {
positionKey: [{ required: true, message: "Required", trigger: "change" }],
linkType: [{ required: true, message: "Required", trigger: "change" }],
link: [
{ required: true, message: "Required", trigger: "blur" },
{ validator: validateLink, trigger: "blur" },
],
promotePlus: [{ required: true, message: "Required", trigger: "change" }],
} as const;
function validateLink(_: any, value: string, cb: any) {
if (!value) return cb();
if (form.value.linkType === "External") {
const ok = /^https?:\/\//i.test(value);
return ok ? cb() : cb(new Error("Must start with http(s)://"));
}
cb();
}
const sampleSize = computed(() => {
const p = props.positions.find((p) => p.key === form.value.positionKey);
return p?.sampleSize || "-";
});
function onSize(size: { width: number; height: number } | null) {
actualSize.value = size ? `${size.width}×${size.height}` : "";
}
const confirmVisible = ref(false);
function onConfirmClick() {
formRef.value.validate((valid: boolean) => {
if (!valid) return;
confirmVisible.value = true;
});
}
function submit() {
emits("submit", { ...form.value });
emits("update:modelValue", false);
}
function onClose() {
emits("update:modelValue", false);
emits("cancel");
}
</script>
<style scoped lang="scss">
.upload-row {
display: flex;
flex-direction: column;
gap: 16px;
.upload-one {
display: flex;
}
}
.sample {
display: flex;
gap: 15px;
color: #6b7280;
font-size: 12px;
}
// 弹窗标题样式
.ad-edit-dialog :deep(.el-dialog__header) {
background: #f9fafb;
margin: 0;
padding: 16px;
border-bottom: 1px solid #e5e7eb;
}
.ad-edit-dialog :deep(.el-dialog__body) {
padding: 24px 24px 0 24px;
}
</style>
============================支持上传多张图片=============================== 父组件:
<MultipleImageUpload
v-model="formData.image"
:limit="5"
:multiple="true"
accept="image/jpeg,image/png"
/>
// formData.image 是个数组
子组件:
<template>
<div class="multiple-image-upload">
<el-upload
class="upload-multiple"
:auto-upload="false"
list-type="picture-card"
:limit="limit"
:file-list="internalList"
:on-change="onChange"
:on-remove="onRemove"
:on-exceed="onExceed"
v-bind="$attrs"
>
<slot>
<el-icon><Plus /></el-icon>
</slot>
</el-upload>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { Plus } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
const props = defineProps<{
modelValue?: string | string[]
limit?: number
multiple?: boolean
}>()
const emits = defineEmits<{
(e: 'update:modelValue', v: string | string[]): void
(e: 'size', size: { width: number; height: number }[]): void
}>()
const internalList = ref<any[]>([])
const limit = props.limit || 10
watch(() => props.modelValue, (v) => {
if (Array.isArray(v)) {
internalList.value = v.map((url, index) => ({ name: `image-${index}`, url } as any))
} else if (v) {
internalList.value = [{ name: 'image', url: v } as any]
} else {
internalList.value = []
}
}, { immediate: true })
function onChange(_file: any, files: any[]) {
const urls: string[] = []
files.forEach((f: any) => {
const url = f.url || (f.raw ? URL.createObjectURL(f.raw) : '')
if (url) {
urls.push(url)
}
})
internalList.value = files.map((f: any, index: number) => {
const url = f.url || (f.raw ? URL.createObjectURL(f.raw) : '')
return { name: `image-${index}`, url } as any
})
// 根据multiple属性决定返回格式
const result = props.multiple ? urls : (urls.length === 1 ? urls[0] : urls)
emits('update:modelValue', result)
// 延迟触发size事件,确保所有图片都加载完成
Promise.all(files.map(f => {
const src = f.url || (f.raw ? URL.createObjectURL(f.raw) : '')
return src ? getImageSize(src) : Promise.resolve({ width: 0, height: 0 })
}))
.then(allSizes => emits('size', allSizes))
}
function onRemove(_file: any, files: any[]) {
const urls: string[] = []
files.forEach((f: any) => {
const url = f.url || (f.raw ? URL.createObjectURL(f.raw) : '')
if (url) {
urls.push(url)
}
})
internalList.value = files.map((f: any, index: number) => {
const url = f.url || (f.raw ? URL.createObjectURL(f.raw) : '')
return { name: `image-${index}`, url } as any
})
// 根据multiple属性决定返回格式
const result = props.multiple ? urls : (urls.length === 0 ? '' : (urls.length === 1 ? urls[0] : urls))
emits('update:modelValue', result)
}
function onExceed(_files: any[]) {
ElMessage.warning(`最多只能上传 ${limit} 张图片`)
}
function getImageSize(src: string): Promise<{ width: number; height: number }> {
return new Promise((resolve) => {
const img = new Image()
img.onload = () => resolve({ width: img.naturalWidth, height: img.naturalHeight })
img.src = src
})
}
</script>
<style scoped lang="scss">
.upload-multiple {
display: inline-flex;
}
</style>