| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212 |
- <template>
- <div>
- <div class="flex flex-wrap">
- <template v-if="limit == 1">
- <div
- class="rounded cursor-pointer overflow-hidden relative border border-solid border-color image-wrap mr-[10px]"
- :class="{ 'rounded-full': type == 'avatar' }"
- :style="style"
- >
- <div class="w-full h-full relative" v-if="imagesData && imagesData.length > 0 && imagesData[0] != ''">
- <div class="w-full h-full flex items-center justify-center">
- <el-image class="w-full h-full" :src="imagesData[0].indexOf('data:image') != -1 ? imagesData[0] : img(imagesData[0])"></el-image>
- </div>
- <div class="absolute z-[1] flex items-center justify-center w-full h-full inset-0 bg-black bg-opacity-60 operation">
- <icon name="element ZoomIn" color="#fff" size="18px" class="mr-[10px]" @click="previewImage(imagesData, 0)" />
- <icon name="element Delete" color="#fff" size="18px" @click.stop="removeImage" />
- </div>
- </div>
- <div class="w-full h-full flex items-center justify-center flex-col content-wrap" v-else @click="openDialog">
- <icon name="element Plus" size="20px" color="var(--el-text-color-secondary)" />
- <div class="leading-none text-xs mt-[10px] text-secondary">{{ imageText || '上传图片' }}</div>
- </div>
- </div>
- </template>
- <template v-else>
- <div class="flex flex-wrap" ref="imgListRef">
- <template v-for="(item, index) in imagesData" :key="item + index">
- <div
- v-if="item && item != ''"
- class="rounded cursor-pointer overflow-hidden relative border border-solid border-color image-wrap mr-[10px] mb-[10px]"
- :style="style"
- >
- <div class="w-full h-full relative">
- <div class="w-full h-full flex items-center justify-center">
- <el-image :src="img(item)" fit="contain"></el-image>
- </div>
- <div class="absolute z-[1] flex flex-col items-center justify-center w-full h-full inset-0 bg-black bg-opacity-60 operation">
- <div class="flex items-center justify-center mb-[6px]">
- <icon name="element ZoomIn" color="#fff" size="18px" class="mr-[10px]" @click.stop="previewImage(imagesData, index)" />
- <icon name="element Delete" color="#fff" size="18px" @click.stop="removeImage(index)" />
- </div>
- <div class="flex items-center justify-center gap-[8px]">
- <el-icon
- :size="16"
- :style="{ color: Number(index) === 0 ? 'rgba(255,255,255,0.3)' : '#fff', cursor: Number(index) === 0 ? 'not-allowed' : 'pointer' }"
- :title="'向左移动'"
- @click.stop="Number(index) > 0 && moveImage(Number(index), Number(index) - 1)"
- ><ArrowLeft /></el-icon>
- <el-icon
- :size="16"
- :style="{ color: Number(index) === imagesData.length - 1 ? 'rgba(255,255,255,0.3)' : '#fff', cursor: Number(index) === imagesData.length - 1 ? 'not-allowed' : 'pointer' }"
- :title="'向右移动'"
- @click.stop="Number(index) < imagesData.length - 1 && moveImage(Number(index), Number(index) + 1)"
- ><ArrowRight /></el-icon>
- </div>
- </div>
- </div>
- </div>
- </template>
- <div
- class="rounded cursor-pointer overflow-hidden relative border border-solid border-color image-wrap mr-[10px] mb-[10px]"
- :style="style"
- v-if="imagesData.length < limit"
- >
- <div class="w-full h-full flex items-center justify-center flex-col content-wrap" @click="openDialog">
- <icon name="element Plus" size="20px" color="var(--el-text-color-secondary)" />
- <div class="leading-none text-xs mt-[10px] text-secondary">{{ imageText || '上传图片' }}</div>
- </div>
- </div>
- </div>
- </template>
- </div>
- <!-- 选择图片 -->
- <el-dialog
- v-model="dialogVisible"
- title="选择图片"
- width="1400"
- :close-on-click-modal="false"
- class="file-selector-dialog"
- :before-close="closeDialog"
- >
- <div class="dialog-bos">
- <!-- 工具栏 -->
- <div class="toolbar">
- <div class="toolbar-left">
- <el-upload
- ref="uploadRef"
- :action="uploadUrl"
- :headers="uploadHeaders"
- :before-upload="beforeUpload"
- :on-success="onUploadSuccess"
- :on-error="onUploadError"
- :show-file-list="false"
- :accept="getUploadFileAccept()"
- >
- <el-button type="primary">
- <el-icon><Plus /></el-icon>
- 上传图片
- </el-button>
- </el-upload>
- </div>
- <div class="toolbar-right">
- <el-input v-model="queryParams.name" placeholder="请输入图片名称" style="width: 200px" clearable @input="handleSearch">
- <template #prefix>
- <el-icon><Search /></el-icon>
- </template>
- </el-input>
- <div class="view-toggle">
- <el-button :type="viewMode === 'grid' ? 'primary' : 'default'" size="small" @click="viewMode = 'grid'">
- <el-icon><Grid /></el-icon>
- </el-button>
- <el-button :type="viewMode === 'list' ? 'primary' : 'default'" size="small" @click="viewMode = 'list'">
- <el-icon><List /></el-icon>
- </el-button>
- </div>
- </div>
- </div>
- <div class="content-wrapper">
- <!-- 左侧分类导航 -->
- <div class="sidebar">
- <!-- 全部文件 -->
- <div @click="handleTreeNodeClick({ id: '' })" class="category-item" :class="{ active: queryParams.categoryId == '' }">
- <el-icon class="category-icon"><Folder /></el-icon>
- <span>全部图片</span>
- <el-dropdown trigger="click" @click.stop class="node-actions">
- <el-icon class="more-icon"><MoreFilled /></el-icon>
- <template #dropdown>
- <el-dropdown-menu>
- <el-dropdown-item @click="openClassify({}, 'add')" command="addRoot">添加分类</el-dropdown-item>
- </el-dropdown-menu>
- </template>
- </el-dropdown>
- </div>
- <!-- 树形控件 -->
- <el-tree
- ref="categoryTreeRef"
- :data="filteredCategoryTree"
- :props="treeProps"
- :expand-on-click-node="false"
- :current-node-key="queryParams.categoryId"
- node-key="id"
- @node-click="handleTreeNodeClick"
- class="category-tree"
- >
- <template #default="{ data }">
- <div class="tree-node-content">
- <el-icon class="category-icon"><Folder /></el-icon>
- <span class="node-label">{{ data.name }}</span>
- <el-dropdown trigger="click" @click.stop class="node-actions pr-[10px]">
- <el-icon class="more-icon"><MoreFilled /></el-icon>
- <template #dropdown>
- <el-dropdown-menu>
- <el-dropdown-item @click="openClassify(data, 'add')">添加分类</el-dropdown-item>
- <el-dropdown-item @click="openClassify(data, 'edit')">编辑分类</el-dropdown-item>
- <el-dropdown-item @click="closeClassify(data)">删除分类</el-dropdown-item>
- </el-dropdown-menu>
- </template>
- </el-dropdown>
- </div>
- </template>
- </el-tree>
- </div>
- <!-- 右侧文件展示区 -->
- <div class="content-area">
- <!-- 网格视图 -->
- <div v-if="viewMode === 'grid'" class="file-grid">
- <div
- v-for="(file, index) in fileList"
- :key="index"
- class="file-item"
- :class="{
- selected: selectedFiles.some((row: any) => row.id == file.id)
- }"
- @click="toggleFileSelection(file)"
- >
- <div class="file-wrapper">
- <el-image :src="getImageUrl(file)" fit="cover" class="file-thumbnail" :preview-disabled="true" lazy>
- <template #error>
- <div class="file-error">
- <el-icon size="24" color="#c0c4cc">
- <Picture />
- </el-icon>
- </div>
- </template>
- <template #placeholder>
- <div class="file-loading">
- <el-icon size="24" color="#409eff">
- <Loading />
- </el-icon>
- </div>
- </template>
- </el-image>
- <div class="file-checkbox">
- <el-checkbox :model-value="selectedFiles.some((row: any) => row.id == file.id)" />
- </div>
- </div>
- <div class="file-info">
- <div class="file-name">{{ file.name || file.originalName }}</div>
- <div class="file-actions">
- <el-button
- link
- size="small"
- @click.stop="
- previewImage(
- fileList.map((row: any) => row.url),
- index
- )
- "
- >预览</el-button
- >
- <el-button link size="small" @click.stop="handleRename(file)">重命名</el-button>
- </div>
- </div>
- </div>
- </div>
- <!-- 列表视图 -->
- <div v-else class="file-list">
- <!-- @selection-change="handleSelectionChange" -->
- <el-table height="600" v-loading="loading" :data="fileList" style="width: 100%">
- <!-- <el-table-column type="selection" width="55" /> -->
- <el-table-column label="预览" width="80">
- <template #default="{ row }">
- <el-image :src="getImageUrl(row)" style="width: 50px; height: 50px; border-radius: 4px" fit="cover" :preview-disabled="true" lazy>
- <template #error>
- <div class="list-image-error">
- <el-icon size="20" color="#c0c4cc">
- <Picture />
- </el-icon>
- </div>
- </template>
- <template #placeholder>
- <div class="list-image-loading">
- <el-icon size="20" color="#409eff">
- <Loading />
- </el-icon>
- </div>
- </template>
- </el-image>
- </template>
- </el-table-column>
- <el-table-column label="文件名" prop="name" min-width="200">
- <template #default="{ row }">
- {{ row.name || row.originalName }}
- </template>
- </el-table-column>
- <el-table-column label="大小" width="100">
- <template #default="{ row }">
- {{ formatFileSize(row.size) }}
- </template>
- </el-table-column>
- <el-table-column label="类型" prop="type" width="120" />
- <el-table-column label="上传时间" width="180">
- <template #default="{ row }">
- {{ formatTime(row.createTime) }}
- </template>
- </el-table-column>
- <el-table-column label="操作" width="150" fixed="right">
- <template #default="scope">
- <el-button
- link
- @click="
- previewImage(
- fileList.map((row: any) => row.url),
- scope.$index
- )
- "
- >预览</el-button
- >
- <el-button link @click="handleRename(scope.row)">重命名</el-button>
- </template>
- </el-table-column>
- </el-table>
- </div>
- <!-- 分页 -->
- <div class="pagination">
- <el-pagination
- v-model:current-page="queryParams.pageNum"
- v-model:page-size="queryParams.pageSize"
- :total="total"
- :page-sizes="[21, 28, 35, 42]"
- layout="total, sizes, prev, pager, next, jumper"
- @size-change="getList"
- @current-change="getList"
- />
- </div>
- </div>
- </div>
- </div>
- <template #footer>
- <div class="dialog-footer">
- <el-button @click="closeDialog">取消</el-button>
- <el-button type="primary" @click="confirmSelection" :disabled="selectedFiles.length > 0 ? false : true">
- 确认选择({{ selectedFiles.length }})
- </el-button>
- </div>
- </template>
- </el-dialog>
- <!-- 添加编辑分类 -->
- <el-dialog v-model="classifyDialog.dialog" :title="classifyDialog.title" width="600">
- <el-form ref="categoryFormRef" :model="categoryForm" :rules="categoryRules" label-width="100px">
- <!-- 分类名称 -->
- <el-form-item label="分类名称" prop="name" required>
- <el-input v-model="categoryForm.name" placeholder="请输入分类名称" />
- </el-form-item>
- </el-form>
- <template #footer>
- <div class="dialog-footer">
- <el-button @click="classifyDialog.dialog = false">取消</el-button>
- <el-button type="primary" @click="submitCategory" :loading="classifyDialog.loading">确定</el-button>
- </div>
- </template>
- </el-dialog>
- <!-- 重命名对话框 -->
- <el-dialog v-model="renameDialogVisible" title="重命名文件" width="400px" append-to-body :close-on-click-modal="false">
- <el-form ref="renameFormRef" :model="renameForm" label-width="80px">
- <el-form-item label="原文件名">
- <el-input v-model="renameForm.originalName" readonly />
- </el-form-item>
- <el-form-item
- label="新文件名"
- prop="name"
- :rules="[
- { required: true, message: '请输入新文件名', trigger: 'blur' },
- { min: 1, max: 100, message: '文件名长度在 1 到 100 个字符', trigger: 'blur' }
- ]"
- >
- <el-input v-model="renameForm.name" placeholder="请输入新文件名" />
- </el-form-item>
- </el-form>
- <template #footer>
- <div class="dialog-footer">
- <el-button @click="renameDialogVisible = false">取消</el-button>
- <el-button type="primary" @click="submitRename">确定</el-button>
- </div>
- </template>
- </el-dialog>
- <!-- 图片放大 -->
- <el-image-viewer
- :url-list="previewImageList"
- v-if="imageViewer.show"
- @close="imageViewer.show = false"
- :initial-index="imageViewer.index"
- :zoom-rate="1"
- />
- </div>
- </template>
- <script lang="ts" setup>
- import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue';
- import { img } from '@/utils/common';
- import { listFileInfo, delFileInfo, addFileInfo, updateDownloadCount, updateFileInfo } from '@/api/file/info';
- import { listFileCategoryTree, addFileCategory, updateFileCategory, delFileCategory } from '@/api/file/category';
- import { globalHeaders } from '@/utils/request';
- const props = defineProps({
- type: {
- type: String,
- default: 'image'
- },
- modelValue: {
- type: String || Array,
- default: ''
- },
- width: {
- type: String,
- default: '100px'
- },
- height: {
- type: String,
- default: '100px'
- },
- imageText: {
- type: String
- },
- limit: {
- type: Number,
- default: 1
- }
- });
- const imagesData = ref<any>([]);
- const previewImageList = ref<any>([]);
- watch(
- () => props.modelValue,
- () => {
- if (props.limit == 1) {
- imagesData.value = [props.modelValue];
- } else {
- if (Array.isArray(props.modelValue)) {
- imagesData.value = props.modelValue;
- } else {
- imagesData.value = props.modelValue.split(',');
- imagesData.value = imagesData.value.filter((item: any) => item !== '');
- }
- }
- },
- { immediate: true }
- );
- const emit = defineEmits(['update:modelValue', 'change']);
- const dialogVisible = ref<any>(false);
- const viewMode = ref<any>('grid');
- // 图片列表
- const loading = ref<any>(false);
- const fileList = ref([]);
- const total = ref(0);
- const selectedFiles = ref([]);
- // 查询参数
- const queryParams = ref<any>({
- pageNum: 1,
- pageSize: 20,
- name: null,
- categoryId: '',
- categoryType: 1
- });
- // 分类相关数据
- const filteredCategoryTree = ref<any>([]);
- const categoryTreeRef = ref<any>(null);
- const treeProps = {
- label: 'name',
- children: 'children'
- };
- const classifyDialog = ref<any>({
- dialog: false,
- title: '添加分类',
- loading: false
- });
- // 分类表单数据
- const categoryFormRef = ref<any>(null);
- const categoryForm = ref({
- id: null,
- name: '',
- code: '',
- parentId: null,
- type: 1,
- sort: 1,
- description: '',
- status: 0
- });
- // 分类表单验证规则
- const categoryRules = ref<any>({
- name: [
- { required: true, message: '请输入分类名称', trigger: 'blur' },
- { min: 2, max: 50, message: '分类名称长度在 2 到 50 个字符', trigger: 'blur' }
- ]
- });
- // 重命名相关数据
- const renameDialogVisible = ref(false);
- const renameForm = ref({
- id: null,
- name: '',
- originalName: '',
- currentFile: null
- });
- const renameFormRef = ref();
- // 上传配置
- const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload');
- const uploadHeaders = ref(globalHeaders());
- const uploadFileList = ref([]);
- // 打开弹窗
- const openDialog = () => {
- getCategoryTree();
- getList();
- selectedFiles.value = [];
- dialogVisible.value = true;
- };
- //关闭弹窗
- const closeDialog = () => {
- dialogVisible.value = false;
- };
- //确定
- const confirmSelection = () => {
- if (props.limit == 1) {
- emit('update:modelValue', selectedFiles.value[0].url);
- emit('change', selectedFiles.value[0]);
- } else {
- const result = selectedFiles.value.map((item: any) => item.url);
- let resultArray = [];
- let resultString = '';
- if (Array.isArray(props.modelValue)) {
- resultArray = [...result, ...imagesData.value];
- emit('update:modelValue', resultArray);
- emit('change', resultArray);
- } else {
- resultString = imagesData.value + ',' + result.join(',');
- emit('update:modelValue', resultString);
- emit('change', resultString);
- }
- }
- closeDialog();
- };
- // 搜索文件
- const handleSearch = () => {
- queryParams.value.pageNum = 1;
- getList();
- };
- // 获取文件列表
- const getList = async () => {
- try {
- loading.value = true;
- const response = (await listFileInfo(queryParams.value)) as any;
- const data = response?.data ?? response;
- if (data && data.rows) {
- fileList.value = data.rows as any[];
- total.value = data.total || 0;
- } else {
- fileList.value = [];
- total.value = 0;
- }
- } catch (error) {
- console.error('获取文件列表失败:', error);
- ElMessage.error('获取文件列表失败');
- fileList.value = [];
- total.value = 0;
- } finally {
- loading.value = false;
- }
- };
- const handleSelectionChange = (res: any) => {};
- // 切换文件选择状态
- const toggleFileSelection = (res: any) => {
- if (props.limit == 1) {
- selectedFiles.value = [res];
- } else {
- // 多选模式,实现切换逻辑
- const index = selectedFiles.value.findIndex((item: any) => item.id === res.id);
- // 计算当前总选择数量(已选文件 + 已上传图片)
- const currentTotalCount = selectedFiles.value.length + imagesData.value.length;
- if (index > -1) {
- // 如果已选中,则取消选中(删除)
- selectedFiles.value.splice(index, 1);
- } else {
- // 如果未选中,检查是否超过限制
- if (currentTotalCount >= props.limit) {
- ElMessage.warning(`最多只能选择 ${props.limit} 张图片`);
- return;
- }
- // 未超过限制,添加选中
- selectedFiles.value.push(res);
- }
- }
- };
- // 获取图片URL
- const getImageUrl = (file) => {
- // 优先使用url字段
- if (file.url) {
- // 如果是完整的URL,直接返回
- if (file.url.startsWith('http://') || file.url.startsWith('https://')) {
- return file.url;
- }
- // 如果是相对路径,添加基础URL
- if (file.url.startsWith('/')) {
- return import.meta.env.VITE_APP_BASE_API + file.url;
- }
- return file.url;
- }
- // 备选方案:使用path字段
- if (file.path) {
- if (file.path.startsWith('http://') || file.path.startsWith('https://')) {
- return file.path;
- }
- if (file.path.startsWith('/')) {
- return import.meta.env.VITE_APP_BASE_API + file.path;
- }
- return file.path;
- }
- // 如果都没有,返回空字符串,触发错误处理
- return '';
- };
- // 获取分类树
- const getCategoryTree = () => {
- listFileCategoryTree().then((res) => {
- if (res.code == 200) {
- if (res.data.length > 0) {
- res.data.forEach((item: any) => {
- if (item.type == 1 && item.code == 'IMAGE') {
- filteredCategoryTree.value = item.children;
- }
- });
- }
- }
- });
- };
- // 添加分类
- const openClassify = (res: any, type: any) => {
- console.log(res, 'res');
- if (type == 'add') {
- classifyDialog.value.title = '添加分类';
- categoryForm.value.name = '';
- categoryForm.value.id = null;
- if (res.id) {
- categoryForm.value.parentId = res.id;
- } else {
- categoryForm.value.parentId = 1;
- }
- } else {
- classifyDialog.value.title = '编辑分类';
- categoryForm.value.name = res.name;
- categoryForm.value.id = res.id;
- categoryForm.value.parentId = null;
- }
- classifyDialog.value.dialog = true;
- };
- // 新增编辑分类
- const submitCategory = async () => {
- try {
- await categoryFormRef.value?.validate();
- classifyDialog.value.loading = true;
- const categoryData = {
- ...categoryForm.value,
- tenantId: '000000'
- };
- if (categoryForm.value.id) {
- await updateFileCategory(categoryData);
- } else {
- await addFileCategory(categoryData);
- }
- ElMessage.success(categoryForm.value.id ? '更新成功' : '添加成功');
- classifyDialog.value.loading = false;
- getCategoryTree();
- } catch (error) {
- console.error('保存分类失败:', error);
- ElMessage.error('保存分类失败');
- } finally {
- classifyDialog.value.loading = false;
- classifyDialog.value.dialog = false;
- }
- };
- // 删除分类
- const closeClassify = async (data: any) => {
- if (data.children && data.children.length > 0) {
- ElMessage.warning('该分类下有子分类,请先删除子分类');
- return;
- }
- try {
- await ElMessageBox.confirm(`确定要删除分类"${data.name}"吗?`, '提示', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning'
- } as any);
- await delFileCategory(data.id);
- ElMessage.success('删除成功');
- getCategoryTree();
- } catch (error) {
- if (error !== 'cancel') {
- console.error('删除分类失败:', error);
- ElMessage.error('删除分类失败');
- }
- }
- };
- // 树节点点击事件
- const handleTreeNodeClick = (res: any) => {
- queryParams.value.categoryId = res.id;
- if (res.id == '') {
- categoryTreeRef.value.setCurrentKey(null);
- }
- handleSearch();
- };
- // 图片放大
- const imageViewer = reactive({
- show: false,
- index: 0
- });
- const previewImage = (list: any, index: any) => {
- previewImageList.value = list;
- imageViewer.index = index ? index : 0;
- imageViewer.show = true;
- };
- /**
- * 移动图片位置
- * @param fromIndex 原位置
- * @param toIndex 目标位置
- */
- const moveImage = (fromIndex: number, toIndex: number) => {
- const list = [...imagesData.value];
- const [item] = list.splice(fromIndex, 1);
- list.splice(toIndex, 0, item);
- if (Array.isArray(props.modelValue)) {
- emit('update:modelValue', list);
- emit('change', list);
- } else {
- emit('update:modelValue', list.join(','));
- emit('change', list.join(','));
- }
- };
- /**
- * 删除图片
- * @param index
- */
- const removeImage = (index?: any) => {
- if (props.limit == 1) {
- emit('update:modelValue', '');
- emit('change', '');
- } else {
- const list = [...imagesData.value];
- list.splice(index, 1);
- if (Array.isArray(props.modelValue)) {
- emit('update:modelValue', list);
- emit('change', list);
- } else {
- emit('update:modelValue', list.join(','));
- emit('change', list.join(','));
- }
- }
- };
- // 重命名文件
- const handleRename = (file: any) => {
- renameForm.value = {
- id: file.id,
- name: '',
- originalName: file.name || file.originalName || '',
- currentFile: file // 保存完整的文件信息
- };
- renameDialogVisible.value = true;
- };
- // 提交重命名
- const submitRename = async () => {
- try {
- await renameFormRef.value?.validate();
- if (!renameForm.value.name.trim()) {
- ElMessage.error('请输入新文件名');
- return;
- }
- if (renameForm.value.name === renameForm.value.originalName) {
- ElMessage.warning('新文件名与原文件名相同');
- return;
- }
- // 调用重命名API
- const file = renameForm.value.currentFile;
- await updateFileInfo({
- id: renameForm.value.id,
- name: renameForm.value.name.trim(),
- originalName: file.originalName,
- path: file.path,
- url: file.url,
- size: file.size,
- type: file.type,
- extension: file.extension,
- categoryId: file.categoryId,
- description: file.description
- });
- ElMessage.success('重命名成功');
- renameDialogVisible.value = false;
- // 刷新文件列表
- handleSearch();
- console.log('重命名文件:', {
- id: renameForm.value.id,
- oldName: renameForm.value.originalName,
- newName: renameForm.value.name.trim()
- });
- } catch (error) {
- console.error('重命名失败:', error);
- ElMessage.error('重命名失败');
- }
- };
- // 格式化文件大小
- const formatFileSize = (size) => {
- if (!size) return '0 B';
- const units = ['B', 'KB', 'MB', 'GB'];
- let index = 0;
- let fileSize = size;
- while (fileSize >= 1024 && index < units.length - 1) {
- fileSize /= 1024;
- index++;
- }
- return fileSize.toFixed(2) + ' ' + units[index];
- };
- // 格式化时间
- const formatTime = (time) => {
- if (!time) return '';
- return new Date(time).toLocaleString();
- };
- // 上传前检查
- const beforeUpload = (file) => {
- // 获取准确的MIME类型
- const actualMimeType = getFileMimeType(file);
- const fileName = file.name || '';
- const extension = fileName.split('.').pop()?.toLowerCase();
- let isValidType = false;
- let fileTypeText = '';
- isValidType = actualMimeType.startsWith('image/') || ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(extension);
- fileTypeText = '图片';
- if (!isValidType) {
- ElMessage.error(`只能上传${fileTypeText}文件! 检测到的文件类型: ${actualMimeType}`);
- return false;
- }
- // 检查文件大小 (50MB)
- const isLtSize = file.size / 1024 / 1024 < 50;
- if (!isLtSize) {
- ElMessage.error('上传文件大小不能超过 50MB!');
- return false;
- }
- // 非图片文件或非图片分类,直接上传
- return true;
- };
- // 上传成功
- const onUploadSuccess = (response, file) => {
- if (response.code === 200) {
- // 获取文件MIME类型和扩展名
- const mimeType = getFileMimeType(file);
- const fileName = file.name || '';
- const extension = fileName.split('.').pop()?.toLowerCase() || '';
- const datas = {
- categoryId: queryParams.value.categoryId,
- categoryType: 1,
- description: '',
- downloadCount: 0,
- extension: extension,
- isPublic: 1,
- name: file.name,
- originalName: file.name,
- ossId: response.data?.ossId || '',
- path: response.data?.url || '',
- size: file.size,
- status: 0,
- type: mimeType,
- uploadStatus: 1,
- url: response.data?.url || '',
- viewCount: 0
- };
- addFileInfo(datas).then((res) => {
- if (res.code == 200) {
- ElMessage.success('文件上传成功');
- handleSearch();
- }
- });
- } else {
- ElMessage.error('上传失败:' + response.msg);
- }
- };
- // 上传失败
- const onUploadError = (error) => {
- console.error('上传失败:', error);
- ElMessage.error('上传失败');
- };
- // 获取上传文件接受类型
- const getUploadFileAccept = () => {
- return '.jpg,.jpeg,.png,.gif,.bmp,.webp';
- };
- // 获取文件MIME类型
- const getFileMimeType = (file) => {
- // 优先使用浏览器检测的MIME类型
- if (file.type) {
- return file.type;
- }
- // 如果浏览器无法检测,根据文件扩展名推断
- const fileName = file.name || '';
- const extension = fileName.split('.').pop()?.toLowerCase();
- const mimeTypeMap = {
- // 图片类型
- 'jpg': 'image/jpeg',
- 'jpeg': 'image/jpeg',
- 'png': 'image/png',
- 'gif': 'image/gif',
- 'bmp': 'image/bmp',
- 'webp': 'image/webp',
- 'svg': 'image/svg+xml',
- // 视频类型
- 'mp4': 'video/mp4',
- 'avi': 'video/x-msvideo',
- 'mov': 'video/quicktime',
- 'wmv': 'video/x-ms-wmv',
- 'flv': 'video/x-flv',
- 'mkv': 'video/x-matroska',
- 'webm': 'video/webm',
- // 音频类型
- 'mp3': 'audio/mpeg',
- 'wav': 'audio/wav',
- 'flac': 'audio/flac',
- 'aac': 'audio/aac',
- 'ogg': 'audio/ogg',
- 'm4a': 'audio/mp4',
- // 文档类型
- 'pdf': 'application/pdf',
- 'doc': 'application/msword',
- 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
- 'xls': 'application/vnd.ms-excel',
- 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
- 'ppt': 'application/vnd.ms-powerpoint',
- 'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
- 'txt': 'text/plain',
- // 压缩文件
- 'zip': 'application/zip',
- 'rar': 'application/vnd.rar',
- '7z': 'application/x-7z-compressed',
- 'tar': 'application/x-tar',
- 'gz': 'application/gzip',
- // 其他常见类型
- 'json': 'application/json',
- 'xml': 'application/xml',
- 'csv': 'text/csv',
- 'html': 'text/html',
- 'css': 'text/css',
- 'js': 'application/javascript'
- };
- return mimeTypeMap[extension] || 'application/octet-stream';
- };
- const style = computed(() => {
- return {
- width: props.width,
- height: props.height
- };
- });
- </script>
- <style lang="scss" scoped>
- .image-wrap {
- .operation {
- display: none;
- }
- &:hover {
- .operation {
- display: flex;
- }
- }
- }
- .border-color {
- border-color: #e5e7eb;
- }
- .dialog-bos {
- height: 750px;
- background: #f5f7fa;
- padding: 10px;
- border-radius: 0 0 8px 8px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- margin-left: 0px !important;
- display: flex;
- flex-direction: column;
- .toolbar {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 10px;
- padding: 16px 20px;
- background: white;
- .toolbar-left {
- display: flex;
- align-items: center;
- gap: 12px;
- }
- .toolbar-right {
- display: flex;
- align-items: center;
- gap: 12px;
- .view-toggle {
- display: flex;
- gap: 0;
- }
- }
- }
- .content-wrapper {
- display: flex;
- gap: 20px;
- height: 0;
- flex: 1;
- .sidebar {
- width: 280px;
- background: white;
- border-radius: 8px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- overflow: auto;
- display: flex;
- flex-direction: column;
- height: 100%;
- padding: 10px;
- .category-item {
- display: flex;
- align-items: center;
- padding: 0px 15px;
- border-radius: 6px;
- cursor: pointer;
- margin-bottom: 8px;
- background: #f5f7fa;
- transition: background-color 0.3s;
- height: 40px;
- .category-icon {
- margin-right: 10px;
- font-size: 18px;
- }
- &.active {
- background: #409eff;
- color: white;
- .more-icon {
- color: rgba(255, 255, 255, 0.8);
- &:hover {
- color: white;
- background: rgba(255, 255, 255, 0.2);
- }
- }
- }
- }
- /* 树形控件样式 */
- .category-tree {
- background: transparent;
- :deep(.el-tree-node__content) {
- height: 40px;
- border-radius: 6px;
- margin-bottom: 4px;
- background: #f5f7fa;
- transition: background-color 0.3s;
- }
- :deep(.el-tree-node__content:hover) {
- background: #ebeef5;
- }
- :deep(.el-tree-node.is-current > .el-tree-node__content) {
- background: #409eff;
- color: white;
- .more-icon {
- color: rgba(255, 255, 255, 0.8);
- &:hover {
- color: white;
- background: rgba(255, 255, 255, 0.2);
- }
- }
- }
- :deep(.el-tree-node.is-current > .el-tree-node__content:hover) {
- background: #66b1ff;
- }
- }
- .tree-node-content {
- display: flex;
- align-items: center;
- width: 100%;
- padding: 0 5px;
- .category-icon {
- margin-right: 8px;
- font-size: 16px;
- }
- .node-label {
- flex: 1;
- font-size: 14px;
- }
- }
- .node-actions {
- margin-left: auto;
- .more-icon {
- font-size: 16px;
- color: #909399;
- cursor: pointer;
- transition: color 0.3s;
- border-radius: 2px;
- &:hover {
- color: #409eff;
- background: rgba(64, 158, 255, 0.1);
- }
- }
- }
- }
- .content-area {
- flex: 1;
- display: flex;
- flex-direction: column;
- height: 100%;
- overflow: hidden;
- .file-grid {
- display: flex;
- flex-wrap: wrap;
- gap: 15px 10px;
- padding: 10px;
- flex: 1;
- overflow-y: auto;
- min-height: 0;
- .file-item {
- flex: 0 0 calc((100% - 30px) / 4);
- overflow: hidden;
- position: relative;
- cursor: pointer;
- border: 1px solid #ebeef5;
- border-radius: 8px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- transition:
- transform 0.3s ease,
- box-shadow 0.3s ease;
- height: 223px;
- &:hover {
- transform: translateY(-5px);
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
- }
- &.selected {
- border: 2px solid #409eff;
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
- }
- .file-wrapper {
- position: relative;
- width: 100%;
- height: 150px; /* Fixed height for grid view */
- overflow: hidden;
- .file-thumbnail {
- width: 100%;
- height: 100%;
- object-fit: cover;
- .file-error {
- display: flex;
- justify-content: center;
- align-items: center;
- width: 100%;
- height: 100%;
- background: #f5f7fa;
- color: #c0c4cc;
- }
- .file-loading {
- display: flex;
- justify-content: center;
- align-items: center;
- width: 100%;
- height: 100%;
- background: #f0f9ff;
- color: #409eff;
- }
- }
- .file-checkbox {
- position: absolute;
- top: 8px;
- right: 8px;
- z-index: 10;
- }
- }
- .file-info {
- padding: 5px 5px 10px 5px;
- background: #f5f7fa;
- border-top: 1px solid #ebeef5;
- border-radius: 0 0 8px 8px;
- .file-name {
- font-size: 14px;
- color: #303133;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- margin-bottom: 5px;
- }
- .file-actions {
- display: flex;
- justify-content: space-around;
- gap: 8px;
- }
- }
- }
- }
- }
- .pagination {
- display: flex;
- justify-content: center;
- padding: 10px;
- background: white;
- flex-shrink: 0;
- }
- }
- }
- </style>
|