修复Vue Admin布局中RouterView未绑定唯一key的问题,解决RBAC系统资源页面切换后内容不刷新的ISSUE-001故障,新增routeViewKey工具函数区分路由实例 完善路由配置,新增admin空路径路由,修复路由命名空路径告警 新增媒体管理、运维发布/重置页面,补全全站路由注册与动态路由过滤逻辑 新增玩法、线索、管家、系统资源管理的全套工具函数、业务组件及单元测试用例 修复后端app/seed.py中媒体资源重复检测未考虑会话内pending资源的bug,新增对应测试验证修复逻辑 修正alembic迁移版本0015的依赖关系,删除废弃的旧迁移文件 优化后端系统接口的字典序列化逻辑,简化重复代码 添加QA验收报告与相关测试截图,完善项目测试覆盖
65 lines
3.2 KiB
Vue
65 lines
3.2 KiB
Vue
<script setup lang="ts">
|
|
import { ref } from "vue";
|
|
import { ElMessage } from "element-plus";
|
|
import { uploadMediaAsset } from "@/api/client";
|
|
|
|
const props = defineProps<{ modelValue: string[]; disabled: boolean }>();
|
|
const emit = defineEmits<{ (event: "update:modelValue", value: string[]): void }>();
|
|
const input = ref<HTMLInputElement | null>(null);
|
|
const uploading = ref(false);
|
|
const error = ref("");
|
|
const allowedTypes = new Set(["image/jpeg", "image/png", "image/webp", "image/gif"]);
|
|
|
|
function openPicker() { input.value?.click(); }
|
|
|
|
async function uploadFiles(event: Event) {
|
|
const target = event.target as HTMLInputElement;
|
|
const files = Array.from(target.files ?? []);
|
|
if (!files.length) return;
|
|
error.value = "";
|
|
uploading.value = true;
|
|
try {
|
|
let next = [...props.modelValue];
|
|
for (const file of files) {
|
|
if (!allowedTypes.has(file.type)) throw new Error("仅支持 JPG、PNG、WEBP 或 GIF 图片");
|
|
if (file.size > 5 * 1024 * 1024) throw new Error("单张图片不能超过 5MB");
|
|
const asset = await uploadMediaAsset(file, "wanfa-detail");
|
|
next = [...next, asset.url];
|
|
emit("update:modelValue", next);
|
|
}
|
|
ElMessage.success(`已上传 ${files.length} 张详情图片`);
|
|
} catch (uploadError) {
|
|
error.value = uploadError instanceof Error ? uploadError.message : "图片上传失败,请重试";
|
|
} finally {
|
|
uploading.value = false;
|
|
target.value = "";
|
|
}
|
|
}
|
|
|
|
function remove(index: number) { emit("update:modelValue", props.modelValue.filter((_, itemIndex) => itemIndex !== index)); }
|
|
|
|
function move(index: number, direction: -1 | 1) {
|
|
const nextIndex = index + direction;
|
|
if (nextIndex < 0 || nextIndex >= props.modelValue.length) return;
|
|
const next = [...props.modelValue];
|
|
[next[index], next[nextIndex]] = [next[nextIndex], next[index]];
|
|
emit("update:modelValue", next);
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="gallery-editor">
|
|
<div class="gallery-editor-heading"><div><strong>详情图片</strong><span>上传后按当前顺序保存到详情记录。</span></div><el-button type="primary" plain :disabled="disabled || uploading" :loading="uploading" @click="openPicker">上传图片</el-button></div>
|
|
<input ref="input" class="media-file-input" type="file" accept="image/jpeg,image/png,image/webp,image/gif" multiple :disabled="disabled || uploading" @change="uploadFiles" />
|
|
<el-alert v-if="error" :title="error" type="error" :closable="false" show-icon />
|
|
<el-empty v-if="!modelValue.length" :image-size="56" description="暂无详情图片" />
|
|
<div v-else class="gallery-grid">
|
|
<div v-for="(image, index) in modelValue" :key="`${image}-${index}`" class="gallery-item">
|
|
<el-image :src="image" fit="cover" :preview-src-list="modelValue" />
|
|
<small>{{ index + 1 }}. {{ image }}</small>
|
|
<div class="gallery-actions"><el-button link size="small" :disabled="disabled || uploading || index === 0" @click="move(index, -1)">上移</el-button><el-button link size="small" :disabled="disabled || uploading || index === modelValue.length - 1" @click="move(index, 1)">下移</el-button><el-button link size="small" type="danger" :disabled="disabled || uploading" @click="remove(index)">移除</el-button></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|