|
|
@@ -34,65 +34,78 @@
|
|
|
<section class="upload-section">
|
|
|
<h2 class="block-heading">上传封面图</h2>
|
|
|
<div class="upload-layout">
|
|
|
- <div
|
|
|
- class="upload-slot"
|
|
|
- :class="{ filled: !!draftCover, uploading: uploading }"
|
|
|
- :role="draftCover ? undefined : 'button'"
|
|
|
- :tabindex="draftCover ? -1 : 0"
|
|
|
- @click="onUploadSlotClick"
|
|
|
- @keydown.enter.prevent="onUploadSlotKeydown"
|
|
|
- >
|
|
|
+ <div class="upload-slots-column">
|
|
|
<input
|
|
|
ref="fileInputRef"
|
|
|
type="file"
|
|
|
class="upload-input-hidden"
|
|
|
:accept="fileAcceptAttr"
|
|
|
+ :multiple="allowMultipleOnInput"
|
|
|
@change="onFileInputChange"
|
|
|
/>
|
|
|
- <template v-if="!draftCover">
|
|
|
- <el-icon class="upload-slot__plus">
|
|
|
- <Plus />
|
|
|
- </el-icon>
|
|
|
- <div class="upload-slot__primary">上传图片/视频 {{ slotCountTip }}</div>
|
|
|
- <div class="upload-slot__hint">建议尺寸 1242*640px;图片不超过 20MB,视频不超过 500MB</div>
|
|
|
- </template>
|
|
|
- <template v-else-if="draftCover.isVideo">
|
|
|
- <div class="upload-slot__preview upload-slot__preview--video">
|
|
|
- <video
|
|
|
- :src="draftCover.url"
|
|
|
- class="upload-slot__thumb-v"
|
|
|
- muted
|
|
|
- playsinline
|
|
|
- preload="metadata"
|
|
|
- :poster="draftCover.posterUrl || undefined"
|
|
|
- />
|
|
|
- <div class="upload-slot__hover-overlay">
|
|
|
- <button type="button" class="upload-slot__action" @click.stop="openCoverPreview">
|
|
|
- <el-icon><View /></el-icon>
|
|
|
- <span>查看</span>
|
|
|
- </button>
|
|
|
- <button type="button" class="upload-slot__action" @click.stop="clearDraft">
|
|
|
- <el-icon><Delete /></el-icon>
|
|
|
- <span>删除</span>
|
|
|
- </button>
|
|
|
- </div>
|
|
|
+ <div class="upload-slots-row">
|
|
|
+ <div
|
|
|
+ v-for="(item, index) in draftCovers"
|
|
|
+ :key="'cover-' + index + '-' + (item.url || '')"
|
|
|
+ class="upload-slot upload-slot--grid filled"
|
|
|
+ :class="{ uploading: uploading }"
|
|
|
+ >
|
|
|
+ <template v-if="item.isVideo">
|
|
|
+ <div class="upload-slot__preview upload-slot__preview--video">
|
|
|
+ <video
|
|
|
+ :src="item.url"
|
|
|
+ class="upload-slot__thumb-v"
|
|
|
+ muted
|
|
|
+ playsinline
|
|
|
+ preload="metadata"
|
|
|
+ :poster="item.posterUrl || undefined"
|
|
|
+ />
|
|
|
+ <div class="upload-slot__hover-overlay">
|
|
|
+ <button type="button" class="upload-slot__action" @click.stop="openCoverPreview(index)">
|
|
|
+ <el-icon><View /></el-icon>
|
|
|
+ <span>查看</span>
|
|
|
+ </button>
|
|
|
+ <button type="button" class="upload-slot__action" @click.stop="removeDraftAt(index)">
|
|
|
+ <el-icon><Delete /></el-icon>
|
|
|
+ <span>删除</span>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <div class="upload-slot__preview">
|
|
|
+ <img :src="item.url" alt="已选封面图" class="upload-slot__thumb" />
|
|
|
+ <div class="upload-slot__hover-overlay">
|
|
|
+ <button type="button" class="upload-slot__action" @click.stop="openCoverPreview(index)">
|
|
|
+ <el-icon><View /></el-icon>
|
|
|
+ <span>查看</span>
|
|
|
+ </button>
|
|
|
+ <button type="button" class="upload-slot__action" @click.stop="removeDraftAt(index)">
|
|
|
+ <el-icon><Delete /></el-icon>
|
|
|
+ <span>删除</span>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
</div>
|
|
|
- </template>
|
|
|
- <template v-else>
|
|
|
- <div class="upload-slot__preview">
|
|
|
- <img :src="draftCover.url" alt="已选封面图" class="upload-slot__thumb" />
|
|
|
- <div class="upload-slot__hover-overlay">
|
|
|
- <button type="button" class="upload-slot__action" @click.stop="openCoverPreview">
|
|
|
- <el-icon><View /></el-icon>
|
|
|
- <span>查看</span>
|
|
|
- </button>
|
|
|
- <button type="button" class="upload-slot__action" @click.stop="clearDraft">
|
|
|
- <el-icon><Delete /></el-icon>
|
|
|
- <span>删除</span>
|
|
|
- </button>
|
|
|
+
|
|
|
+ <div
|
|
|
+ v-if="showAddCoverSlot"
|
|
|
+ class="upload-slot upload-slot--grid"
|
|
|
+ :class="{ uploading: uploading }"
|
|
|
+ role="button"
|
|
|
+ tabindex="0"
|
|
|
+ @click="onAddCoverSlotClick"
|
|
|
+ @keydown.enter.prevent="onAddCoverSlotKeydown"
|
|
|
+ >
|
|
|
+ <el-icon class="upload-slot__plus">
|
|
|
+ <Plus />
|
|
|
+ </el-icon>
|
|
|
+ <div class="upload-slot__primary">
|
|
|
+ {{ addSlotPrimaryText }}
|
|
|
</div>
|
|
|
</div>
|
|
|
- </template>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
<ul class="instruction-list">
|
|
|
@@ -139,6 +152,8 @@ import PcVideoPreviewDialog from "@/components/pcMediaPreview/PcVideoPreviewDial
|
|
|
|
|
|
/** 与商户端封面 imgType 一致 */
|
|
|
const IMG_TYPE_COVER = 38;
|
|
|
+/** 与商户端一致:纯图模式最多 5 张,与 1 个视频二选一 */
|
|
|
+const COVER_MAX_IMAGES = 5;
|
|
|
|
|
|
const IMAGE_MAX_MB = 20;
|
|
|
const VIDEO_MAX_MB = 500;
|
|
|
@@ -188,7 +203,7 @@ const loading = ref(false);
|
|
|
const uploading = ref(false);
|
|
|
const saving = ref(false);
|
|
|
|
|
|
-const draftCover = ref<DraftCover | null>(null);
|
|
|
+const draftCovers = ref<DraftCover[]>([]);
|
|
|
|
|
|
const fileInputRef = ref<HTMLInputElement | null>(null);
|
|
|
/** 图片预览(与 storeHeadMap 一致);视频用弹窗 + 快照避免删除后空引用 */
|
|
|
@@ -197,22 +212,43 @@ const coverImagePreviewUrls = ref<string[]>([]);
|
|
|
const coverImagePreviewInitialIndex = ref(0);
|
|
|
const coverVideoPreviewVisible = ref(false);
|
|
|
const previewTarget = ref<DraftCover | null>(null);
|
|
|
-const slotCountTip = computed(() => (draftCover.value ? `(1/1)` : `(0/1)`));
|
|
|
+
|
|
|
+const draftHasVideo = computed(() => draftCovers.value.some(d => d.isVideo));
|
|
|
+
|
|
|
+const showAddCoverSlot = computed(() => !draftHasVideo.value && draftCovers.value.length < COVER_MAX_IMAGES);
|
|
|
+
|
|
|
+/** 仅已有图片时显示张数上限;空态不显示 (0/5) */
|
|
|
+const addSlotPrimaryText = computed(() => {
|
|
|
+ const n = draftCovers.value.length;
|
|
|
+ if (n > 0) {
|
|
|
+ return `上传图片 (${n}/${COVER_MAX_IMAGES})`;
|
|
|
+ }
|
|
|
+ return "上传图片或视频";
|
|
|
+});
|
|
|
+
|
|
|
+const fileAcceptAttr = computed(() => {
|
|
|
+ if (draftHasVideo.value) return "";
|
|
|
+ if (draftCovers.value.length > 0) {
|
|
|
+ return "image/jpeg,image/png,image/webp,.jpg,.jpeg,.png,.webp";
|
|
|
+ }
|
|
|
+ return "image/jpeg,image/png,image/webp,video/mp4,video/quicktime,video/webm,.mp4,.MOV,.mov,.webm,.m4v";
|
|
|
+});
|
|
|
+
|
|
|
+/** 已有图且未满时允许多选图片一次加入;空列表也可多选图片;视频与图二选一时选视频为单文件 */
|
|
|
+const allowMultipleOnInput = computed(() => !draftHasVideo.value && draftCovers.value.length < COVER_MAX_IMAGES);
|
|
|
|
|
|
const previewReviews = ref(1853);
|
|
|
const storeNameDisplay = ref("示例门店");
|
|
|
|
|
|
-const fileAcceptAttr = "image/jpeg,image/png,image/webp,video/mp4,video/quicktime,video/webm,.mp4,.MOV,.mov,.webm,.m4v";
|
|
|
-
|
|
|
const instructionLines = [
|
|
|
- "封面图将展示在门店列表、搜索结果等位置,是用户对门店的第一印象",
|
|
|
- "支持JPG、PNG、WebP格式图片,或MP4、MOV格式视频",
|
|
|
- "图片建议包含门店招牌、环境或特色产品,清晰美观无水印"
|
|
|
+ "封面请二选一:上传 1 个视频,或上传最多 5 张图片;保存后仍可重新上传替换。",
|
|
|
+ "视频建议不超过 500MB,避免视频出现显示不全问题。",
|
|
|
+ "建议上传4:3尺寸的图片,图片建议不超过 20MB,避免图片出现显示不全问题。"
|
|
|
];
|
|
|
|
|
|
const canSubmit = computed(() => {
|
|
|
- const u = draftCover.value?.url?.trim();
|
|
|
- return Boolean(u && !uploading.value);
|
|
|
+ if (uploading.value) return false;
|
|
|
+ return draftCovers.value.some(d => Boolean(d.url?.trim()));
|
|
|
});
|
|
|
|
|
|
const fetchStoreDetail = async () => {
|
|
|
@@ -261,7 +297,7 @@ const fetchCoverDraft = async () => {
|
|
|
const userInfo: any = localGet("geeker-user")?.userInfo || {};
|
|
|
const storeId = userInfo.storeId;
|
|
|
if (!storeId) {
|
|
|
- draftCover.value = null;
|
|
|
+ draftCovers.value = [];
|
|
|
return;
|
|
|
}
|
|
|
loading.value = true;
|
|
|
@@ -273,40 +309,55 @@ const fetchCoverDraft = async () => {
|
|
|
const sorted = [...imgList].sort(
|
|
|
(a: unknown, b: unknown) => (Number((a as StoreImgRow)?.imgSort) || 0) - (Number((b as StoreImgRow)?.imgSort) || 0)
|
|
|
);
|
|
|
- const firstRow = sorted[0];
|
|
|
- draftCover.value = firstRow ? rowToDraft(firstRow as StoreImgRow) : null;
|
|
|
+ if (!sorted.length) {
|
|
|
+ draftCovers.value = [];
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const firstDraft = rowToDraft(sorted[0] as StoreImgRow);
|
|
|
+ if (firstDraft?.isVideo) {
|
|
|
+ draftCovers.value = [firstDraft];
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ draftCovers.value = sorted
|
|
|
+ .slice(0, COVER_MAX_IMAGES)
|
|
|
+ .map(row => rowToDraft(row as StoreImgRow))
|
|
|
+ .filter((x): x is DraftCover => x != null);
|
|
|
} else {
|
|
|
- draftCover.value = null;
|
|
|
+ draftCovers.value = [];
|
|
|
}
|
|
|
} catch {
|
|
|
- draftCover.value = null;
|
|
|
+ draftCovers.value = [];
|
|
|
} finally {
|
|
|
loading.value = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
function openFilePicker() {
|
|
|
- if (uploading.value || saving.value || draftCover.value) return;
|
|
|
+ if (uploading.value || saving.value) return;
|
|
|
+ if (draftHasVideo.value) return;
|
|
|
+ if (draftCovers.value.length >= COVER_MAX_IMAGES) return;
|
|
|
fileInputRef.value?.click();
|
|
|
}
|
|
|
|
|
|
-function onUploadSlotClick() {
|
|
|
+function onAddCoverSlotClick() {
|
|
|
openFilePicker();
|
|
|
}
|
|
|
|
|
|
-function onUploadSlotKeydown() {
|
|
|
+function onAddCoverSlotKeydown() {
|
|
|
openFilePicker();
|
|
|
}
|
|
|
|
|
|
-function openCoverPreview() {
|
|
|
- const d = draftCover.value;
|
|
|
+function openCoverPreview(index: number) {
|
|
|
+ const d = draftCovers.value[index];
|
|
|
if (!d) return;
|
|
|
if (d.isVideo) {
|
|
|
previewTarget.value = { ...d };
|
|
|
coverVideoPreviewVisible.value = true;
|
|
|
} else {
|
|
|
- coverImagePreviewUrls.value = [d.url];
|
|
|
- coverImagePreviewInitialIndex.value = 0;
|
|
|
+ const urls = draftCovers.value.filter(x => !x.isVideo).map(x => x.url);
|
|
|
+ coverImagePreviewUrls.value = urls;
|
|
|
+ const ii = urls.indexOf(d.url);
|
|
|
+ coverImagePreviewInitialIndex.value = ii >= 0 ? ii : 0;
|
|
|
coverImagePreviewVisible.value = true;
|
|
|
}
|
|
|
}
|
|
|
@@ -316,30 +367,28 @@ function onCoverVideoPreviewClosed() {
|
|
|
previewTarget.value = null;
|
|
|
}
|
|
|
|
|
|
-async function ingestPickedFile(file: File): Promise<boolean> {
|
|
|
+function isFileVideo(file: File): boolean {
|
|
|
const name = String(file.name || "").toLowerCase();
|
|
|
- const type = String(file.type || "");
|
|
|
-
|
|
|
- let isVideo = /^video\//i.test(type) || /\.(mp4|mov|m4v|webm|3gp)$/i.test(name);
|
|
|
-
|
|
|
- const imgOk = type === "image/jpeg" || type === "image/png" || type === "image/webp" || /\.(jpe?g|png|webp)$/i.test(name);
|
|
|
+ return /^video\//i.test(String(file.type || "")) || /\.(mp4|mov|m4v|webm|3gp)$/i.test(name);
|
|
|
+}
|
|
|
|
|
|
- if (!imgOk && !isVideo) {
|
|
|
- ElMessage.warning("仅支持 JPG、PNG、WebP、MP4、MOV");
|
|
|
- return false;
|
|
|
- }
|
|
|
+function isFileImage(file: File): boolean {
|
|
|
+ const name = String(file.name || "").toLowerCase();
|
|
|
+ const type = String(file.type || "");
|
|
|
+ return type === "image/jpeg" || type === "image/png" || type === "image/webp" || /\.(jpe?g|png|webp)$/i.test(name);
|
|
|
+}
|
|
|
|
|
|
+async function uploadOneToDraft(file: File, isVideo: boolean): Promise<DraftCover | null> {
|
|
|
const maxMb = isVideo ? VIDEO_MAX_MB : IMAGE_MAX_MB;
|
|
|
const sizeMb = file.size / (1024 * 1024);
|
|
|
if (sizeMb > maxMb) {
|
|
|
ElMessage.warning(`文件不能超过 ${maxMb}MB`);
|
|
|
- return false;
|
|
|
+ return null;
|
|
|
}
|
|
|
|
|
|
uploading.value = true;
|
|
|
try {
|
|
|
const urls = await uploadFilesToOss([file], isVideo ? "video" : "image", {
|
|
|
- /** 保留全局 PopupLoading 进度;上传完成不 toast,由本页统一提示(上传服务已含合规校验) */
|
|
|
uploadSuccessMessage: null
|
|
|
});
|
|
|
const url = urls[0];
|
|
|
@@ -350,48 +399,122 @@ async function ingestPickedFile(file: File): Promise<boolean> {
|
|
|
posterUrl = tryOssVideoSnapshotCoverUrl(url);
|
|
|
}
|
|
|
|
|
|
- draftCover.value = {
|
|
|
+ return {
|
|
|
id: undefined,
|
|
|
url,
|
|
|
isVideo,
|
|
|
...(posterUrl ? { posterUrl } : {})
|
|
|
};
|
|
|
- ElMessage.success("上传成功");
|
|
|
- return true;
|
|
|
} catch (e: unknown) {
|
|
|
if (isUploadUserCancelledError(e)) {
|
|
|
- return false;
|
|
|
+ return null;
|
|
|
}
|
|
|
if (isUploadApiErrorAlreadyMessaged(e)) {
|
|
|
- return false;
|
|
|
+ return null;
|
|
|
}
|
|
|
const msg =
|
|
|
typeof e === "object" && e && "message" in e && typeof (e as { message?: unknown }).message === "string"
|
|
|
? String((e as Error).message)
|
|
|
: "上传失败";
|
|
|
ElMessage.error(msg);
|
|
|
- return false;
|
|
|
+ return null;
|
|
|
} finally {
|
|
|
uploading.value = false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+async function ingestSingleVideoFromFile(file: File): Promise<boolean> {
|
|
|
+ if (!isFileVideo(file)) {
|
|
|
+ ElMessage.warning("仅支持 MP4、MOV 等视频格式");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ const draft = await uploadOneToDraft(file, true);
|
|
|
+ if (!draft) return false;
|
|
|
+ draftCovers.value = [draft];
|
|
|
+ ElMessage.success("上传成功");
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+async function ingestSingleImageAppend(file: File): Promise<boolean> {
|
|
|
+ if (!isFileImage(file)) {
|
|
|
+ ElMessage.warning("仅支持 JPG、PNG、WebP 图片");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (draftHasVideo.value) {
|
|
|
+ ElMessage.warning("请先删除视频后再上传图片");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (draftCovers.value.length >= COVER_MAX_IMAGES) {
|
|
|
+ ElMessage.warning(`最多上传 ${COVER_MAX_IMAGES} 张图片`);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ const draft = await uploadOneToDraft(file, false);
|
|
|
+ if (!draft) return false;
|
|
|
+ draftCovers.value = [...draftCovers.value, draft];
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+async function ingestPickedFiles(files: File[]): Promise<void> {
|
|
|
+ const list = files.filter(Boolean);
|
|
|
+ if (!list.length) return;
|
|
|
+
|
|
|
+ const firstVideo = list.find(isFileVideo);
|
|
|
+ if (firstVideo) {
|
|
|
+ if (list.some(f => isFileImage(f)) || list.filter(isFileVideo).length > 1) {
|
|
|
+ ElMessage.info("已选择视频,将仅保留该视频作为封面(与图片二选一)");
|
|
|
+ }
|
|
|
+ await ingestSingleVideoFromFile(firstVideo);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (draftHasVideo.value) {
|
|
|
+ ElMessage.warning("请先删除视频后再上传图片");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const remaining = COVER_MAX_IMAGES - draftCovers.value.length;
|
|
|
+ if (remaining <= 0) {
|
|
|
+ ElMessage.warning(`最多上传 ${COVER_MAX_IMAGES} 张图片`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const imageFiles = list.filter(isFileImage).slice(0, remaining);
|
|
|
+ if (!imageFiles.length) {
|
|
|
+ ElMessage.warning("仅支持 JPG、PNG、WebP、MP4、MOV");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ let okCount = 0;
|
|
|
+ for (const file of imageFiles) {
|
|
|
+ if (draftCovers.value.length >= COVER_MAX_IMAGES) break;
|
|
|
+ const ok = await ingestSingleImageAppend(file);
|
|
|
+ if (ok) okCount += 1;
|
|
|
+ else break;
|
|
|
+ }
|
|
|
+ if (okCount > 1) {
|
|
|
+ ElMessage.success(`已上传 ${okCount} 张图片`);
|
|
|
+ } else if (okCount === 1) {
|
|
|
+ ElMessage.success("上传成功");
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
async function onFileInputChange(ev: Event): Promise<void> {
|
|
|
const input = ev.target as HTMLInputElement;
|
|
|
- const file = input.files?.[0];
|
|
|
+ const files = Array.from(input.files || []);
|
|
|
input.value = "";
|
|
|
- if (!file) return;
|
|
|
- await ingestPickedFile(file);
|
|
|
+ if (!files.length) return;
|
|
|
+ await ingestPickedFiles(files);
|
|
|
}
|
|
|
|
|
|
-function clearDraft() {
|
|
|
- draftCover.value = null;
|
|
|
+function removeDraftAt(index: number) {
|
|
|
+ const next = draftCovers.value.filter((_, i) => i !== index);
|
|
|
+ draftCovers.value = next;
|
|
|
}
|
|
|
|
|
|
async function onSave() {
|
|
|
if (saving.value) return;
|
|
|
if (!canSubmit.value) {
|
|
|
- ElMessage.warning("请先上传一张图片或一段视频(仅 1 条)");
|
|
|
+ ElMessage.warning("请先上传封面:1 个视频或最多 5 张图片");
|
|
|
return;
|
|
|
}
|
|
|
const sidRaw = localGet("geeker-user")?.userInfo?.storeId;
|
|
|
@@ -403,17 +526,15 @@ async function onSave() {
|
|
|
|
|
|
saving.value = true;
|
|
|
try {
|
|
|
- const dc = draftCover.value!;
|
|
|
- const storeImgList = [
|
|
|
- {
|
|
|
- id: dc.id,
|
|
|
- imgUrl: dc.url,
|
|
|
- imgType: IMG_TYPE_COVER,
|
|
|
- imgSort: 1,
|
|
|
- storeId: numericStoreId,
|
|
|
- isExtract: 0
|
|
|
- }
|
|
|
- ];
|
|
|
+ const list = draftCovers.value;
|
|
|
+ const storeImgList = list.map((dc, index) => ({
|
|
|
+ id: dc.id,
|
|
|
+ imgUrl: dc.url,
|
|
|
+ imgType: IMG_TYPE_COVER,
|
|
|
+ imgSort: index + 1,
|
|
|
+ storeId: numericStoreId,
|
|
|
+ isExtract: 0
|
|
|
+ }));
|
|
|
|
|
|
const res: any = await saveStoreHeadImg({
|
|
|
storeImgList,
|
|
|
@@ -574,6 +695,16 @@ onMounted(async () => {
|
|
|
gap: 32px 40px;
|
|
|
align-items: flex-start;
|
|
|
}
|
|
|
+.upload-slots-column {
|
|
|
+ flex: 0 1 auto;
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+.upload-slots-row {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 16px;
|
|
|
+ align-items: stretch;
|
|
|
+}
|
|
|
.upload-slot {
|
|
|
box-sizing: border-box;
|
|
|
display: flex;
|
|
|
@@ -685,6 +816,51 @@ onMounted(async () => {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+.upload-slot.upload-slot--grid {
|
|
|
+ box-sizing: border-box;
|
|
|
+ display: flex;
|
|
|
+ flex-shrink: 0;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ width: 240px;
|
|
|
+ height: 180px;
|
|
|
+ min-height: 180px;
|
|
|
+ max-height: 180px;
|
|
|
+ padding: 6px 8px;
|
|
|
+ overflow: hidden;
|
|
|
+ border-radius: 10px;
|
|
|
+ .upload-slot__plus {
|
|
|
+ font-size: 22px;
|
|
|
+ }
|
|
|
+ .upload-slot__primary {
|
|
|
+ margin-top: 2px;
|
|
|
+ font-size: 11px;
|
|
|
+ line-height: 1.3;
|
|
|
+ color: #909399;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+ &.filled {
|
|
|
+ justify-content: stretch;
|
|
|
+ padding: 0;
|
|
|
+ }
|
|
|
+ &.filled .upload-slot__preview {
|
|
|
+ flex: 1 1 auto;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ min-height: 0;
|
|
|
+ max-height: none;
|
|
|
+ border-radius: 10px;
|
|
|
+ }
|
|
|
+ &.filled .upload-slot__thumb,
|
|
|
+ &.filled .upload-slot__thumb-v {
|
|
|
+ display: block;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ max-height: none;
|
|
|
+ object-fit: cover;
|
|
|
+ }
|
|
|
+}
|
|
|
.upload-input-hidden {
|
|
|
position: absolute;
|
|
|
width: 0;
|
|
|
@@ -767,5 +943,12 @@ onMounted(async () => {
|
|
|
width: 100%;
|
|
|
max-width: 360px;
|
|
|
}
|
|
|
+ .upload-slot.upload-slot--grid {
|
|
|
+ width: calc((100% - 16px) / 2);
|
|
|
+ max-width: 240px;
|
|
|
+ height: 180px;
|
|
|
+ min-height: 180px;
|
|
|
+ max-height: 180px;
|
|
|
+ }
|
|
|
}
|
|
|
</style>
|