2.7.9beta (#1993)
* 媒体库增加分类,图库多选择时优化。 * 重构 JWT token 生成,使用 `New()` 函数代替直接创建实例 * 上传组件支持查看大图 (#1982) * 将参数缓存,媒体库增加分类,图库多选择时优化。 (#1978) * 媒体库增加分类,图库多选择时优化。 *修复文件上传进度显示bug&按钮样式优化 (#1986) * fix:添加内部 iframe 展示网页,优化 permission 代码 * 俩个uuid库合并一个,更新库到当前版本。 * 优化关于我们界面 * feat: 个人中心头像调整,媒体库兼容性调整。 * feat: 自动化代码前端页面美化,多余按钮收入专家模式 * feat: 增加单独生成server功能 * feat: 限制单独生成前后端的情况下的细节配置 * feat: 修复全选失败报错的问题 --------- Co-authored-by: task <121913992@qq.com> Co-authored-by: Feng.YJ <32027253+huiyifyj@users.noreply.github.com> Co-authored-by: will0523 <dygsunshine@163.com> Co-authored-by: task <ms.yangdan@gmail.com> Co-authored-by: sslee <57312216+GIS142857@users.noreply.github.com> Co-authored-by: bypanghu <bypanghu@163.com> Co-authored-by: Azir <2075125282@qq.com> Co-authored-by: piexlMax(奇淼 <qimiaojiangjizhao@gmail.com> Co-authored-by: krank <emosick@qq.com>
This commit is contained in:
26
web/src/api/attachmentCategory.js
Normal file
26
web/src/api/attachmentCategory.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import service from '@/utils/request'
|
||||
// 分类列表
|
||||
export const getCategoryList = () => {
|
||||
return service({
|
||||
url: '/attachmentCategory/getCategoryList',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
// 添加/编辑分类
|
||||
export const addCategory = (data) => {
|
||||
return service({
|
||||
url: '/attachmentCategory/addCategory',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除分类
|
||||
export const deleteCategory = (data) => {
|
||||
return service({
|
||||
url: '/attachmentCategory/deleteCategory',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
@@ -1,120 +0,0 @@
|
||||
<template>
|
||||
<div class="profile-avatar relative w-[120px] h-[120px] rounded-full overflow-hidden cursor-pointer group">
|
||||
<img
|
||||
v-if="modelValue"
|
||||
class="w-full h-full object-cover"
|
||||
:src="getUrl(modelValue)"
|
||||
alt="头像"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="w-full h-full flex flex-col items-center justify-center bg-gray-100 dark:bg-slate-700"
|
||||
>
|
||||
<el-icon class="text-2xl text-gray-400">
|
||||
<avatar />
|
||||
</el-icon>
|
||||
<span class="mt-2 text-sm text-gray-400">点击上传</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="absolute inset-0 bg-black/30 opacity-0 group-hover:opacity-100 flex items-center justify-center transition-all duration-300"
|
||||
@click="chooseFile"
|
||||
>
|
||||
<div class="text-center text-white">
|
||||
<el-icon class="text-2xl"><camera-filled /></el-icon>
|
||||
<div class="text-xs mt-1">更换头像</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
class="hidden"
|
||||
accept="image/*"
|
||||
@change="handleFileChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getUrl } from '@/utils/image'
|
||||
import service from '@/utils/request'
|
||||
|
||||
defineOptions({
|
||||
name: 'ProfileAvatar'
|
||||
})
|
||||
|
||||
const { modelValue } = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const fileInput = ref(null)
|
||||
|
||||
const chooseFile = () => {
|
||||
fileInput.value.click()
|
||||
}
|
||||
|
||||
const handleFileChange = async (e) => {
|
||||
const file = e.target.files[0]
|
||||
if (!file) return
|
||||
|
||||
// 验证文件类型
|
||||
if (!file.type.includes('image/')) {
|
||||
ElMessage.error('请选择图片文件')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证文件大小(限制为2MB)
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
ElMessage.error('图片大小不能超过2MB')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
const res = await service({
|
||||
url: '/fileUploadAndDownload/upload',
|
||||
method: 'post',
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
|
||||
if (res.code === 0 && res.data?.file?.url) {
|
||||
emit('update:modelValue', res.data.file.url)
|
||||
ElMessage.success('头像上传成功')
|
||||
} else {
|
||||
ElMessage.error(res.msg || '上传失败')
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error('头像上传失败,请重试')
|
||||
} finally {
|
||||
// 清空input,确保可以重复选择同一文件
|
||||
e.target.value = ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.profile-avatar {
|
||||
img {
|
||||
@apply transition-transform duration-300;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
img {
|
||||
@apply scale-110;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -54,7 +54,8 @@
|
||||
const options = reactive([])
|
||||
const deepMenus = (menus) => {
|
||||
const arr = []
|
||||
menus.forEach((menu) => {
|
||||
menus?.forEach((menu) => {
|
||||
if (!menu?.children) return
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
arr.push(...deepMenus(menu.children))
|
||||
} else {
|
||||
@@ -77,7 +78,7 @@
|
||||
label: '跳转',
|
||||
children: []
|
||||
}
|
||||
const menus = deepMenus(routerStore.asyncRouters[0].children)
|
||||
const menus = deepMenus(routerStore.asyncRouters[0]?.children || [])
|
||||
option.children.push(...menus)
|
||||
options.push(option)
|
||||
}
|
||||
|
@@ -1,59 +1,68 @@
|
||||
<template>
|
||||
<div
|
||||
class="w-40 h-40 relative rounded border border-dashed border-gray-300 overflow-hidden cursor-pointer group"
|
||||
class="w-40 h-40 relative rounded border border-dashed border-gray-300 cursor-pointer group"
|
||||
:class="rounded ? 'rounded-full' : ''"
|
||||
>
|
||||
<el-icon
|
||||
v-if="isVideoExt(model || '')"
|
||||
:size="32"
|
||||
class="absolute top-[calc(50%-16px)] left-[calc(50%-16px)]"
|
||||
>
|
||||
<VideoPlay />
|
||||
</el-icon>
|
||||
<video
|
||||
v-if="isVideoExt(model || '')"
|
||||
class="w-full h-full object-cover"
|
||||
muted
|
||||
preload="metadata"
|
||||
>
|
||||
<source :src="getUrl(model) + '#t=1'" />
|
||||
</video>
|
||||
<div class="w-full h-full overflow-hidden" :class="rounded ? 'rounded-full' : ''">
|
||||
<el-icon
|
||||
v-if="isVideoExt(model || '')"
|
||||
:size="32"
|
||||
class="absolute top-[calc(50%-16px)] left-[calc(50%-16px)]"
|
||||
>
|
||||
<VideoPlay />
|
||||
</el-icon>
|
||||
<video
|
||||
v-if="isVideoExt(model || '')"
|
||||
class="w-full h-full object-cover"
|
||||
muted
|
||||
preload="metadata"
|
||||
>
|
||||
<source :src="getUrl(model) + '#t=1'" />
|
||||
</video>
|
||||
|
||||
<img
|
||||
v-if="model && !isVideoExt(model)"
|
||||
class="w-full h-full"
|
||||
:src="getUrl(model)"
|
||||
alt="图片"
|
||||
/>
|
||||
<div
|
||||
v-if="model"
|
||||
class="left-0 top-0 hidden text-gray-600 group-hover:bg-gray-600 group-hover:bg-opacity-30 w-full h-full group-hover:flex justify-center items-center absolute z-10"
|
||||
@click="deleteItem"
|
||||
>
|
||||
<el-icon>
|
||||
<delete />
|
||||
</el-icon>
|
||||
删除
|
||||
<el-image
|
||||
v-if="model && !isVideoExt(model)"
|
||||
class="w-full h-full"
|
||||
:src="imgUrl"
|
||||
:preview-src-list="srcList"
|
||||
fit="cover"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="text-gray-600 group-hover:bg-gray-200 group-hover:opacity-60 w-full h-full flex justify-center items-center"
|
||||
@click="chooseItem"
|
||||
>
|
||||
<el-icon>
|
||||
<plus />
|
||||
</el-icon>
|
||||
上传
|
||||
</div>
|
||||
</div>
|
||||
<!-- 删除按钮在外层容器中 -->
|
||||
<div
|
||||
v-else
|
||||
class="text-gray-600 group-hover:bg-gray-400 w-full h-full flex justify-center items-center"
|
||||
@click="chooseItem"
|
||||
v-if="model"
|
||||
class="right-0 top-0 hidden text-gray-400 group-hover:flex justify-center items-center absolute z-10"
|
||||
@click="deleteItem"
|
||||
>
|
||||
<el-icon>
|
||||
<plus />
|
||||
<el-icon :size="24">
|
||||
<CircleCloseFilled />
|
||||
</el-icon>
|
||||
上传
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { getUrl, isVideoExt } from '@/utils/image'
|
||||
import { Delete, Plus } from '@element-plus/icons-vue'
|
||||
import { CircleCloseFilled, Plus } from '@element-plus/icons-vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
model: {
|
||||
default: '',
|
||||
type: String
|
||||
},
|
||||
rounded: {
|
||||
default: false,
|
||||
type: Boolean
|
||||
}
|
||||
})
|
||||
|
||||
@@ -66,4 +75,12 @@
|
||||
const deleteItem = () => {
|
||||
emits('deleteItem')
|
||||
}
|
||||
|
||||
const imgUrl = computed(() => {
|
||||
return getUrl(props.model)
|
||||
})
|
||||
|
||||
const srcList = computed(() => {
|
||||
return imgUrl.value ? [imgUrl.value] : []
|
||||
})
|
||||
</script>
|
||||
|
@@ -1,290 +1,452 @@
|
||||
<template>
|
||||
<div>
|
||||
<selectComponent
|
||||
v-if="!props.multiple"
|
||||
:model="model"
|
||||
@chooseItem="openChooseImg"
|
||||
@deleteItem="openChooseImg"
|
||||
/>
|
||||
<selectComponent :rounded="rounded" v-if="!props.multiple" :model="model" @chooseItem="openChooseImg" @deleteItem="openChooseImg" />
|
||||
<div v-else class="w-full gap-4 flex flex-wrap">
|
||||
<selectComponent
|
||||
v-for="(item, index) in model"
|
||||
:key="index"
|
||||
:model="item"
|
||||
@chooseItem="openChooseImg"
|
||||
@deleteItem="deleteImg(index)"
|
||||
<selectComponent :rounded="rounded" v-for="(item, index) in model" :key="index" :model="item" @chooseItem="openChooseImg"
|
||||
@deleteItem="deleteImg(index)"
|
||||
/>
|
||||
<selectComponent
|
||||
v-if="
|
||||
model?.length < props.maxUpdateCount || props.maxUpdateCount === 0
|
||||
"
|
||||
@chooseItem="openChooseImg"
|
||||
@deleteItem="openChooseImg"
|
||||
<selectComponent :rounded="rounded" v-if="model.length < props.maxUpdateCount || props.maxUpdateCount === 0"
|
||||
@chooseItem="openChooseImg" @deleteItem="openChooseImg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-drawer v-model="drawer" title="媒体库" :size="appStore.drawerSize">
|
||||
<warning-bar title="点击“文件名/备注”可以编辑文件名或者备注内容。" />
|
||||
<div class="gva-btn-list gap-2">
|
||||
<upload-common :image-common="imageCommon" @on-success="getImageList" />
|
||||
<upload-image
|
||||
:image-url="imageUrl"
|
||||
:file-size="512"
|
||||
:max-w-h="1080"
|
||||
@on-success="getImageList"
|
||||
/>
|
||||
<el-input
|
||||
v-model="search.keyword"
|
||||
class="keyword"
|
||||
placeholder="请输入文件名或备注"
|
||||
/>
|
||||
<el-button type="primary" icon="search" @click="getImageList">
|
||||
查询
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<div v-for="(item, key) in picList" :key="key" class="w-40">
|
||||
<div
|
||||
class="w-40 h-40 border rounded overflow-hidden border-dashed border-gray-300 cursor-pointer relative group"
|
||||
>
|
||||
<el-image
|
||||
:key="key"
|
||||
:src="getUrl(item.url)"
|
||||
fit="cover"
|
||||
class="w-full h-full relative"
|
||||
@click="chooseImg(item.url)"
|
||||
<el-drawer v-model="drawer" title="媒体库" :size="880">
|
||||
<div class="flex">
|
||||
<div class="w-64" style="border-right: solid 1px var(--el-border-color);">
|
||||
<el-scrollbar style="height: calc(100vh - 110px)">
|
||||
<el-tree
|
||||
:data="categories"
|
||||
node-key="id"
|
||||
:props="defaultProps"
|
||||
@node-click="handleNodeClick"
|
||||
default-expand-all
|
||||
>
|
||||
<template #error>
|
||||
<el-icon
|
||||
v-if="isVideoExt(item.url || '')"
|
||||
:size="32"
|
||||
class="absolute top-[calc(50%-16px)] left-[calc(50%-16px)]"
|
||||
>
|
||||
<VideoPlay />
|
||||
</el-icon>
|
||||
<video
|
||||
v-if="isVideoExt(item.url || '')"
|
||||
class="w-full h-full object-cover"
|
||||
muted
|
||||
preload="metadata"
|
||||
@click="chooseImg(item.url)"
|
||||
>
|
||||
<source :src="getUrl(item.url) + '#t=1'" />
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
<div
|
||||
v-else
|
||||
class="w-full h-full object-cover flex items-center justify-center"
|
||||
>
|
||||
<el-icon :size="32">
|
||||
<icon-picture />
|
||||
<template #default="{ node, data }">
|
||||
<div class="w-36" :class="search.classId === data.ID ? 'text-blue-500 font-bold' : ''">{{ data.name }}
|
||||
</div>
|
||||
<el-dropdown>
|
||||
<el-icon class="ml-3 text-right" v-if="data.ID > 0"><MoreFilled /></el-icon>
|
||||
<el-icon class="ml-3 text-right mt-1" v-else><Plus /></el-icon>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="addCategoryFun(data)">添加分类</el-dropdown-item>
|
||||
<el-dropdown-item @click="editCategory(data)" v-if="data.ID > 0">编辑分类</el-dropdown-item>
|
||||
<el-dropdown-item @click="deleteCategoryFun(data.ID)" v-if="data.ID > 0">删除分类</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<div class="ml-4 image-library">
|
||||
<warning-bar title="点击“文件名”可以编辑;选择的类别即是上传的类别。" />
|
||||
<div class="gva-btn-list gap-2">
|
||||
<el-button @click="useSelectedImages" type="danger" :disabled="selectedImages.length === 0" :icon="ArrowLeftBold">确认所选</el-button>
|
||||
<upload-common :image-common="imageCommon" :classId="search.classId" @on-success="onSuccess" />
|
||||
<upload-image :image-url="imageUrl" :file-size="2048" :max-w-h="1080" :classId="search.classId" @on-success="onSuccess" />
|
||||
<el-input v-model.trim="search.keyword" class="w-52" placeholder="请输入文件名或备注" clearable />
|
||||
<el-button type="primary" icon="search" @click="onSubmit"> 查询</el-button>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<div v-for="(item,key) in picList" :key="key" class="w-40">
|
||||
<div class="w-40 h-40 border rounded overflow-hidden border-dashed border-gray-300 cursor-pointer relative group">
|
||||
<el-image :key="key" :src="getUrl(item.url)" fit="cover" class="w-full h-full relative" @click="toggleImageSelection(item)" :class="{ selected: isSelected(item) }">
|
||||
<template #error>
|
||||
<el-icon v-if="isVideoExt(item.url || '')" :size="32" class="absolute top-[calc(50%-16px)] left-[calc(50%-16px)]">
|
||||
<VideoPlay />
|
||||
</el-icon>
|
||||
<video v-if="isVideoExt(item.url || '')"
|
||||
class="w-full h-full object-cover"
|
||||
muted
|
||||
preload="metadata"
|
||||
@click="toggleImageSelection(item)"
|
||||
:class="{ selected: isSelected(item) }"
|
||||
>
|
||||
<source :src="getUrl(item.url) + '#t=1'">
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
<div v-else class="w-full h-full object-cover flex items-center justify-center">
|
||||
<el-icon :size="32">
|
||||
<icon-picture />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div class="absolute -right-1 top-1 w-8 h-8 group-hover:inline-block hidden" @click="deleteCheck(item)">
|
||||
<el-icon :size="18">
|
||||
<CloseBold />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div
|
||||
class="absolute -right-1 top-1 w-8 h-8 group-hover:inline-block hidden"
|
||||
@click="deleteCheck(item)"
|
||||
>
|
||||
<el-icon :size="16"><CircleClose /></el-icon>
|
||||
</div>
|
||||
<div class="overflow-hidden text-nowrap overflow-ellipsis text-center w-full cursor-pointer" @click="editFileNameFunc(item)">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="overflow-hidden text-nowrap overflow-ellipsis text-center w-full"
|
||||
@click="editFileNameFunc(item)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<el-pagination
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
class="justify-center"
|
||||
layout="total, prev, pager, next, jumper"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<el-pagination
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
:style="{ 'justify-content': 'center' }"
|
||||
layout="total, prev, pager, next, jumper"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</el-drawer>
|
||||
|
||||
|
||||
<!-- 添加分类弹窗 -->
|
||||
<el-dialog v-model="categoryDialogVisible" @close="closeAddCategoryDialog" width="520"
|
||||
:title="(categoryFormData.ID === 0 ? '添加' : '编辑') + '分类'"
|
||||
draggable
|
||||
>
|
||||
<el-form ref="categoryForm" :rules="rules" :model="categoryFormData" label-width="80px">
|
||||
<el-form-item label="上级分类">
|
||||
<el-tree-select
|
||||
v-model="categoryFormData.pid"
|
||||
:data="categories"
|
||||
check-strictly
|
||||
:props="defaultProps"
|
||||
:render-after-expand="false"
|
||||
style="width: 240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="分类名称" prop="name">
|
||||
<el-input v-model.trim="categoryFormData.name" placeholder="分类名称"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="closeAddCategoryDialog">取消</el-button>
|
||||
<el-button type="primary" @click="confirmAddCategory">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getUrl, isVideoExt } from '@/utils/image'
|
||||
import { ref } from 'vue'
|
||||
import {
|
||||
getFileList,
|
||||
editFileName,
|
||||
deleteFile
|
||||
} from '@/api/fileUploadAndDownload'
|
||||
import UploadImage from '@/components/upload/image.vue'
|
||||
import UploadCommon from '@/components/upload/common.vue'
|
||||
import WarningBar from '@/components/warningBar/warningBar.vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Picture as IconPicture } from '@element-plus/icons-vue'
|
||||
import selectComponent from '@/components/selectImage/selectComponent.vue'
|
||||
import { useAppStore } from "@/pinia";
|
||||
import { getUrl, isVideoExt } from '@/utils/image'
|
||||
import { ref } from 'vue'
|
||||
import { getFileList, editFileName, deleteFile } from '@/api/fileUploadAndDownload'
|
||||
import UploadImage from '@/components/upload/image.vue'
|
||||
import UploadCommon from '@/components/upload/common.vue'
|
||||
import WarningBar from '@/components/warningBar/warningBar.vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
ArrowLeftBold,
|
||||
CloseBold,
|
||||
MoreFilled,
|
||||
Picture as IconPicture,
|
||||
Plus,
|
||||
VideoPlay
|
||||
} from '@element-plus/icons-vue'
|
||||
import selectComponent from '@/components/selectImage/selectComponent.vue'
|
||||
import { addCategory, deleteCategory, getCategoryList } from '@/api/attachmentCategory'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const imageUrl = ref('')
|
||||
const imageCommon = ref('')
|
||||
|
||||
const imageUrl = ref('')
|
||||
const imageCommon = ref('')
|
||||
const search = ref({
|
||||
keyword: null,
|
||||
classId: 0
|
||||
})
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
const pageSize = ref(20)
|
||||
|
||||
const search = ref({})
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
const pageSize = ref(20)
|
||||
const model = defineModel({ type: [String, Array] })
|
||||
|
||||
const model = defineModel({ type: [String, Array] })
|
||||
const props = defineProps({
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
fileType: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
maxUpdateCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
rounded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
fileType: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
maxUpdateCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
const deleteImg = (index) => {
|
||||
model.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
getImageList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getImageList()
|
||||
}
|
||||
|
||||
const onSubmit = () => {
|
||||
search.value.classId = 0
|
||||
page.value = 1
|
||||
getImageList()
|
||||
}
|
||||
|
||||
const editFileNameFunc = async(row) => {
|
||||
ElMessageBox.prompt('请输入文件名或者备注', '编辑', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern: /\S/,
|
||||
inputErrorMessage: '不能为空',
|
||||
inputValue: row.name
|
||||
}).then(async({ value }) => {
|
||||
row.name = value
|
||||
const res = await editFileName(row)
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '编辑成功!'
|
||||
})
|
||||
await getImageList()
|
||||
}
|
||||
})
|
||||
|
||||
const deleteImg = (index) => {
|
||||
model.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
getImageList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getImageList()
|
||||
}
|
||||
|
||||
const editFileNameFunc = async (row) => {
|
||||
ElMessageBox.prompt('请输入文件名或者备注', '编辑', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern: /\S/,
|
||||
inputErrorMessage: '不能为空',
|
||||
inputValue: row.name
|
||||
}).catch(() => {
|
||||
ElMessage({
|
||||
type: 'info',
|
||||
message: '取消修改'
|
||||
})
|
||||
.then(async ({ value }) => {
|
||||
row.name = value
|
||||
const res = await editFileName(row)
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '编辑成功!'
|
||||
})
|
||||
getImageList()
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage({
|
||||
type: 'info',
|
||||
message: '取消修改'
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const drawer = ref(false)
|
||||
const picList = ref([])
|
||||
const drawer = ref(false)
|
||||
const picList = ref([])
|
||||
|
||||
const imageTypeList = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp']
|
||||
const videoTyteList = [
|
||||
'mp4',
|
||||
'avi',
|
||||
'rmvb',
|
||||
'rm',
|
||||
'asf',
|
||||
'divx',
|
||||
'mpg',
|
||||
'mpeg',
|
||||
'mpe',
|
||||
'wmv',
|
||||
'mkv',
|
||||
'vob'
|
||||
]
|
||||
const imageTypeList = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg']
|
||||
const videoTypeList = ['mp4', 'avi', 'rmvb', 'rm', 'asf', 'divx', 'mpg', 'mpeg', 'mpe', 'wmv', 'mkv', 'vob']
|
||||
|
||||
const listObj = {
|
||||
image: imageTypeList,
|
||||
video: videoTyteList
|
||||
}
|
||||
const listObj = {
|
||||
image: imageTypeList,
|
||||
video: videoTypeList
|
||||
}
|
||||
|
||||
const chooseImg = (url) => {
|
||||
if (props.fileType) {
|
||||
const typeSuccess = listObj[props.fileType].some((item) => {
|
||||
if (url?.toLowerCase().includes(item)) {
|
||||
return true
|
||||
}
|
||||
})
|
||||
if (!typeSuccess) {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '当前类型不支持使用'
|
||||
})
|
||||
return
|
||||
const chooseImg = (url) => {
|
||||
if (props.fileType) {
|
||||
const typeSuccess = listObj[props.fileType].some(item => {
|
||||
if (url?.toLowerCase().includes(item)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if (props.multiple) {
|
||||
model.value.push(url)
|
||||
} else {
|
||||
model.value = url
|
||||
}
|
||||
drawer.value = false
|
||||
}
|
||||
|
||||
const openChooseImg = async () => {
|
||||
if (model.value && !props.multiple) {
|
||||
model.value = ''
|
||||
})
|
||||
if (!typeSuccess) {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '当前类型不支持使用'
|
||||
})
|
||||
return
|
||||
}
|
||||
await getImageList()
|
||||
drawer.value = true
|
||||
}
|
||||
//if (props.multiple) {
|
||||
// model.value.push(url)
|
||||
//} else {
|
||||
model.value = url
|
||||
//}
|
||||
drawer.value = false
|
||||
}
|
||||
|
||||
const getImageList = async () => {
|
||||
const res = await getFileList({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
...search.value
|
||||
})
|
||||
const openChooseImg = async() => {
|
||||
if (model.value && !props.multiple) {
|
||||
model.value = ''
|
||||
return
|
||||
}
|
||||
await getImageList()
|
||||
await fetchCategories()
|
||||
drawer.value = true
|
||||
}
|
||||
|
||||
const getImageList = async() => {
|
||||
const res = await getFileList({ page: page.value, pageSize: pageSize.value, ...search.value })
|
||||
if (res.code === 0) {
|
||||
picList.value = res.data.list
|
||||
total.value = res.data.total
|
||||
page.value = res.data.page
|
||||
pageSize.value = res.data.pageSize
|
||||
}
|
||||
}
|
||||
|
||||
const deleteCheck = (item) => {
|
||||
ElMessageBox.confirm('是否删除该文件', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async() => {
|
||||
const res = await deleteFile(item)
|
||||
if (res.code === 0) {
|
||||
picList.value = res.data.list
|
||||
total.value = res.data.total
|
||||
page.value = res.data.page
|
||||
pageSize.value = res.data.pageSize
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '删除成功!'
|
||||
})
|
||||
await getImageList()
|
||||
}
|
||||
}
|
||||
|
||||
const deleteCheck = (item) => {
|
||||
ElMessageBox.confirm('是否删除该文件', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).catch(() => {
|
||||
ElMessage({
|
||||
type: 'info',
|
||||
message: '已取消删除'
|
||||
})
|
||||
.then(async () => {
|
||||
const res = await deleteFile(item)
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '删除成功!'
|
||||
})
|
||||
getImageList()
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage({
|
||||
type: 'info',
|
||||
message: '已取消删除'
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
children: 'children',
|
||||
label: 'name',
|
||||
value: 'ID'
|
||||
}
|
||||
|
||||
const categories = ref([])
|
||||
const fetchCategories = async() => {
|
||||
const res = await getCategoryList()
|
||||
let data = {
|
||||
name: '全部分类',
|
||||
ID: 0,
|
||||
pid: 0,
|
||||
children:[]
|
||||
}
|
||||
if (res.code === 0) {
|
||||
categories.value = res.data || []
|
||||
categories.value.unshift(data)
|
||||
}
|
||||
}
|
||||
|
||||
const handleNodeClick = (node) => {
|
||||
search.value.keyword = null
|
||||
search.value.classId = node.ID
|
||||
page.value = 1
|
||||
getImageList()
|
||||
}
|
||||
|
||||
const onSuccess = () => {
|
||||
search.value.keyword = null
|
||||
page.value = 1
|
||||
getImageList()
|
||||
}
|
||||
|
||||
const categoryDialogVisible = ref(false)
|
||||
const categoryFormData = ref({
|
||||
ID: 0,
|
||||
pid: 0,
|
||||
name: ''
|
||||
})
|
||||
|
||||
const categoryForm = ref(null)
|
||||
const rules = ref({
|
||||
name: [
|
||||
{ required: true, message: '请输入分类名称', trigger: 'blur' },
|
||||
{ max: 20, message: '最多20位字符', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const addCategoryFun = (category) => {
|
||||
categoryDialogVisible.value = true
|
||||
categoryFormData.value.ID = 0
|
||||
categoryFormData.value.pid = category.ID
|
||||
}
|
||||
|
||||
const editCategory = (category) => {
|
||||
categoryFormData.value = {
|
||||
ID: category.ID,
|
||||
pid: category.pid,
|
||||
name: category.name
|
||||
}
|
||||
categoryDialogVisible.value = true
|
||||
}
|
||||
|
||||
const deleteCategoryFun = async(id) => {
|
||||
const res = await deleteCategory({ id: id })
|
||||
if (res.code === 0) {
|
||||
ElMessage.success({ type: 'success', message: '删除成功' })
|
||||
await fetchCategories()
|
||||
}
|
||||
}
|
||||
|
||||
const confirmAddCategory = async() => {
|
||||
categoryForm.value.validate(async valid => {
|
||||
if (valid) {
|
||||
const res = await addCategory(categoryFormData.value)
|
||||
if (res.code === 0) {
|
||||
ElMessage({ type: 'success', message: '操作成功' })
|
||||
await fetchCategories()
|
||||
closeAddCategoryDialog()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const closeAddCategoryDialog = () => {
|
||||
categoryDialogVisible.value = false
|
||||
categoryFormData.value = {
|
||||
ID: 0,
|
||||
pid: 0,
|
||||
name: ''
|
||||
}
|
||||
}
|
||||
|
||||
const selectedImages = ref([])
|
||||
|
||||
const toggleImageSelection = (item) => {
|
||||
if (props.multiple === false) {
|
||||
chooseImg(item.url)
|
||||
return
|
||||
}
|
||||
const index = selectedImages.value.findIndex(img => img.ID === item.ID)
|
||||
if (index > -1) {
|
||||
selectedImages.value.splice(index, 1)
|
||||
} else {
|
||||
selectedImages.value.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
const isSelected = (item) => {
|
||||
return selectedImages.value.some(img => img.ID === item.ID)
|
||||
}
|
||||
|
||||
const useSelectedImages = () => {
|
||||
selectedImages.value.forEach((item) => {
|
||||
model.value.push(item.url)
|
||||
})
|
||||
drawer.value = false
|
||||
selectedImages.value = []
|
||||
}
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
.selected {
|
||||
border: 3px solid #409eff;
|
||||
}
|
||||
|
||||
.image-library {
|
||||
width: 605px;
|
||||
}
|
||||
|
||||
.selected:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border: 10px solid #409eff;
|
||||
}
|
||||
|
||||
.selected:after {
|
||||
content: "";
|
||||
width: 9px;
|
||||
height: 14px;
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
top: 0;
|
||||
border: 3px solid #fff;
|
||||
border-top-color: transparent;
|
||||
border-left-color: transparent;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
</style>
|
||||
|
@@ -6,10 +6,11 @@
|
||||
:on-error="uploadError"
|
||||
:on-success="uploadSuccess"
|
||||
:show-file-list="false"
|
||||
:data="{'classId': props.classId}"
|
||||
multiple
|
||||
class="upload-btn"
|
||||
>
|
||||
<el-button type="primary">普通上传</el-button>
|
||||
<el-button type="primary" :icon="Upload">普通上传</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
</template>
|
||||
@@ -19,11 +20,19 @@
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { isVideoMime, isImageMime } from '@/utils/image'
|
||||
import { getBaseUrl } from '@/utils/format'
|
||||
import {Upload} from "@element-plus/icons-vue";
|
||||
|
||||
defineOptions({
|
||||
name: 'UploadCommon'
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
classId: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['on-success'])
|
||||
|
||||
const fullscreenLoading = ref(false)
|
||||
|
@@ -6,8 +6,9 @@
|
||||
:on-success="handleImageSuccess"
|
||||
:before-upload="beforeImageUpload"
|
||||
:multiple="false"
|
||||
:data="{'classId': props.classId}"
|
||||
>
|
||||
<el-button type="primary">压缩上传</el-button>
|
||||
<el-button type="primary" :icon="Upload">压缩上传</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
</template>
|
||||
@@ -16,6 +17,7 @@
|
||||
import ImageCompress from '@/utils/image'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getBaseUrl } from '@/utils/format'
|
||||
import {Upload} from "@element-plus/icons-vue";
|
||||
|
||||
defineOptions({
|
||||
name: 'UploadImage'
|
||||
@@ -34,6 +36,10 @@
|
||||
maxWH: {
|
||||
type: Number,
|
||||
default: 1920 // 图片长宽上限
|
||||
},
|
||||
classId: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
|
@@ -27,6 +27,7 @@
|
||||
"/src/view/layout/aside/normalMode.vue": "GvaAside",
|
||||
"/src/view/layout/header/index.vue": "Index",
|
||||
"/src/view/layout/header/tools.vue": "Tools",
|
||||
"/src/view/layout/iframe.vue": "GvaLayoutIframe",
|
||||
"/src/view/layout/index.vue": "GvaLayout",
|
||||
"/src/view/layout/screenfull/index.vue": "Screenfull",
|
||||
"/src/view/layout/search/search.vue": "BtnBox",
|
||||
|
@@ -4,134 +4,143 @@ import getPageTitle from '@/utils/page'
|
||||
import router from '@/router'
|
||||
import Nprogress from 'nprogress'
|
||||
import 'nprogress/nprogress.css'
|
||||
Nprogress.configure({ showSpinner: false, ease: 'ease', speed: 500 })
|
||||
|
||||
const whiteList = ['Login', 'Init']
|
||||
// 配置 NProgress
|
||||
Nprogress.configure({
|
||||
showSpinner: false,
|
||||
ease: 'ease',
|
||||
speed: 500
|
||||
})
|
||||
|
||||
const getRouter = async (userStore) => {
|
||||
const routerStore = useRouterStore()
|
||||
await routerStore.SetAsyncRouter()
|
||||
await userStore.GetUserInfo()
|
||||
const asyncRouters = routerStore.asyncRouters
|
||||
asyncRouters.forEach((asyncRouter) => {
|
||||
router.addRoute(asyncRouter)
|
||||
})
|
||||
}
|
||||
// 白名单路由
|
||||
const WHITE_LIST = ['Login', 'Init']
|
||||
|
||||
const removeLoading = () => {
|
||||
const element = document.getElementById('gva-loading-box')
|
||||
if (element) {
|
||||
element.remove()
|
||||
// 处理路由加载
|
||||
const setupRouter = async (userStore) => {
|
||||
try {
|
||||
const routerStore = useRouterStore()
|
||||
await Promise.all([routerStore.SetAsyncRouter(), userStore.GetUserInfo()])
|
||||
|
||||
routerStore.asyncRouters.forEach((route) => router.addRoute(route))
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Setup router failed:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleKeepAlive(to) {
|
||||
if (to.matched.some((item) => item.meta.keepAlive)) {
|
||||
if (to.matched && to.matched.length > 2) {
|
||||
for (let i = 1; i < to.matched.length; i++) {
|
||||
const element = to.matched[i - 1]
|
||||
if (element.name === 'layout') {
|
||||
to.matched.splice(i, 1)
|
||||
await handleKeepAlive(to)
|
||||
}
|
||||
// 如果没有按需加载完成则等待加载
|
||||
if (typeof element.components.default === 'function') {
|
||||
await element.components.default()
|
||||
await handleKeepAlive(to)
|
||||
}
|
||||
// 移除加载动画
|
||||
const removeLoading = () => {
|
||||
const element = document.getElementById('gva-loading-box')
|
||||
element?.remove()
|
||||
}
|
||||
|
||||
// 处理组件缓存
|
||||
const handleKeepAlive = async (to) => {
|
||||
if (!to.matched.some((item) => item.meta.keepAlive)) return
|
||||
|
||||
if (to.matched?.length > 2) {
|
||||
for (let i = 1; i < to.matched.length; i++) {
|
||||
const element = to.matched[i - 1]
|
||||
|
||||
if (element.name === 'layout') {
|
||||
to.matched.splice(i, 1)
|
||||
await handleKeepAlive(to)
|
||||
continue
|
||||
}
|
||||
|
||||
if (typeof element.components.default === 'function') {
|
||||
await element.components.default()
|
||||
await handleKeepAlive(to)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理路由重定向
|
||||
const handleRedirect = (to, userStore) => {
|
||||
if (router.hasRoute(userStore.userInfo.authority.defaultRouter)) {
|
||||
return { ...to, replace: true }
|
||||
}
|
||||
return { path: '/layout/404' }
|
||||
}
|
||||
|
||||
// 路由守卫
|
||||
router.beforeEach(async (to, from) => {
|
||||
const routerStore = useRouterStore()
|
||||
Nprogress.start()
|
||||
const userStore = useUserStore()
|
||||
to.meta.matched = [...to.matched]
|
||||
handleKeepAlive(to)
|
||||
const routerStore = useRouterStore()
|
||||
const token = userStore.token
|
||||
// 在白名单中的判断情况
|
||||
|
||||
Nprogress.start()
|
||||
|
||||
// 处理元数据和缓存
|
||||
to.meta.matched = [...to.matched]
|
||||
await handleKeepAlive(to)
|
||||
|
||||
// 设置页面标题
|
||||
document.title = getPageTitle(to.meta.title, to)
|
||||
|
||||
if (to.meta.client) {
|
||||
return true
|
||||
}
|
||||
if (whiteList.indexOf(to.name) > -1) {
|
||||
if (token) {
|
||||
if (!routerStore.asyncRouterFlag && whiteList.indexOf(from.name) < 0) {
|
||||
await getRouter(userStore)
|
||||
}
|
||||
// token 可以解析但是却是不存在的用户 id 或角色 id 会导致无限调用
|
||||
if (userStore.userInfo?.authority?.defaultRouter != null) {
|
||||
if (router.hasRoute(userStore.userInfo.authority.defaultRouter)) {
|
||||
return { name: userStore.userInfo.authority.defaultRouter }
|
||||
} else {
|
||||
return { path: '/layout/404' }
|
||||
}
|
||||
} else {
|
||||
// 强制退出账号
|
||||
userStore.ClearStorage()
|
||||
return {
|
||||
name: 'Login',
|
||||
query: {
|
||||
redirect: document.location.hash
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return true
|
||||
|
||||
// 白名单路由处理
|
||||
if (WHITE_LIST.includes(to.name)) {
|
||||
if (
|
||||
token &&
|
||||
!routerStore.asyncRouterFlag &&
|
||||
!WHITE_LIST.includes(from.name)
|
||||
) {
|
||||
await setupRouter(userStore)
|
||||
}
|
||||
} else {
|
||||
// 不在白名单中并且已经登录的时候
|
||||
if (token) {
|
||||
if (sessionStorage.getItem('needToHome') === 'true') {
|
||||
sessionStorage.removeItem('needToHome')
|
||||
return { path: '/' }
|
||||
}
|
||||
// 添加flag防止多次获取动态路由和栈溢出
|
||||
if (!routerStore.asyncRouterFlag && whiteList.indexOf(from.name) < 0) {
|
||||
await getRouter(userStore)
|
||||
if (userStore.token) {
|
||||
if (router.hasRoute(userStore.userInfo.authority.defaultRouter)) {
|
||||
return { ...to, replace: true }
|
||||
} else {
|
||||
return { path: '/layout/404' }
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
name: 'Login',
|
||||
query: { redirect: to.href }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (to.matched.length) {
|
||||
return true
|
||||
} else {
|
||||
return { path: '/layout/404' }
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 需要登录的路由处理
|
||||
if (token) {
|
||||
// 处理需要跳转到首页的情况
|
||||
if (sessionStorage.getItem('needToHome') === 'true') {
|
||||
sessionStorage.removeItem('needToHome')
|
||||
return { path: '/' }
|
||||
}
|
||||
// 不在白名单中并且未登录的时候
|
||||
if (!token) {
|
||||
|
||||
// 处理异步路由
|
||||
if (!routerStore.asyncRouterFlag && !WHITE_LIST.includes(from.name)) {
|
||||
const setupSuccess = await setupRouter(userStore)
|
||||
|
||||
if (setupSuccess && userStore.token) {
|
||||
return handleRedirect(to, userStore)
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'Login',
|
||||
query: {
|
||||
redirect: document.location.hash
|
||||
}
|
||||
query: { redirect: to.href }
|
||||
}
|
||||
}
|
||||
|
||||
return to.matched.length ? true : { path: '/layout/404' }
|
||||
}
|
||||
|
||||
// 未登录跳转登录页
|
||||
return {
|
||||
name: 'Login',
|
||||
query: {
|
||||
redirect: document.location.hash
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 路由加载完成
|
||||
router.afterEach(() => {
|
||||
// 路由加载完成后关闭进度条
|
||||
document.getElementsByClassName('main-cont main-right')[0]?.scrollTo(0, 0)
|
||||
document.querySelector('.main-cont.main-right')?.scrollTo(0, 0)
|
||||
Nprogress.done()
|
||||
})
|
||||
|
||||
router.onError(() => {
|
||||
// 路由发生错误后销毁进度条
|
||||
// 路由错误处理
|
||||
router.onError((error) => {
|
||||
console.error('Router error:', error)
|
||||
Nprogress.remove()
|
||||
})
|
||||
|
||||
// 移除初始加载动画
|
||||
removeLoading()
|
||||
|
31
web/src/pinia/modules/params.js
Normal file
31
web/src/pinia/modules/params.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { getSysParam } from '@/api/sysParams'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useParamsStore = defineStore('params', () => {
|
||||
const paramsMap = ref({})
|
||||
|
||||
const setParamsMap = (paramsRes) => {
|
||||
paramsMap.value = { ...paramsMap.value, ...paramsRes }
|
||||
}
|
||||
|
||||
const getParams = async(key) => {
|
||||
if (paramsMap.value[key] && paramsMap.value[key].length) {
|
||||
return paramsMap.value[key]
|
||||
} else {
|
||||
const res = await getSysParam({ key })
|
||||
if (res.code === 0) {
|
||||
const paramsRes = {}
|
||||
paramsRes[key] = res.data.value
|
||||
setParamsMap(paramsRes)
|
||||
return paramsMap.value[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
paramsMap,
|
||||
setParamsMap,
|
||||
getParams
|
||||
}
|
||||
})
|
@@ -21,7 +21,7 @@ const routes = [
|
||||
closeTab: true
|
||||
},
|
||||
component: () => import('@/view/error/index.vue')
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { useDictionaryStore } from '@/pinia/modules/dictionary'
|
||||
import { getSysParam } from '@/api/sysParams'
|
||||
// 获取字典方法 使用示例 getDict('sex').then(res) 或者 async函数下 const res = await getDict('sex')
|
||||
export const getDict = async (type) => {
|
||||
const dictionaryStore = useDictionaryStore()
|
||||
@@ -25,10 +24,3 @@ export const showDictLabel = (
|
||||
})
|
||||
return Reflect.has(dictMap, code) ? dictMap[code] : ''
|
||||
}
|
||||
|
||||
export const getParams = async (key) => {
|
||||
const res = await getSysParam({ key })
|
||||
if (res.code === 0) {
|
||||
return res.data.value
|
||||
}
|
||||
}
|
||||
|
14
web/src/utils/params.js
Normal file
14
web/src/utils/params.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useParamsStore } from '@/pinia/modules/params'
|
||||
/*
|
||||
* 获取参数方法 使用示例 getParams('key').then(res) 或者 async函数下 const res = await getParams('key')
|
||||
* const res = ref('')
|
||||
* const fun = async () => {
|
||||
* res.value = await getParams('test')
|
||||
* }
|
||||
* fun()
|
||||
*/
|
||||
export const getParams = async(key) => {
|
||||
const paramsStore = useParamsStore()
|
||||
await paramsStore.getParams(key)
|
||||
return paramsStore.paramsMap[key]
|
||||
}
|
@@ -1,34 +1,29 @@
|
||||
<template>
|
||||
<div class="mt-2">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<div class="flex flex-col md:flex-row gap-4">
|
||||
<div class="w-full md:w-1/2">
|
||||
<el-card class="min-w-96">
|
||||
<template #header>
|
||||
<el-divider>gin-vue-admin</el-divider>
|
||||
</template>
|
||||
<div>
|
||||
<el-row>
|
||||
<el-col :span="8" :offset="8">
|
||||
<a href="https://github.com/flipped-aurora/gin-vue-admin">
|
||||
<div class="w-full flex items-center justify-center">
|
||||
<a href="https://github.com/flipped-aurora/gin-vue-admin">
|
||||
<img
|
||||
class="org-img dom-center"
|
||||
src="@/assets/logo.png"
|
||||
alt="gin-vue-admin"
|
||||
/>
|
||||
</a>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="8">
|
||||
<a href="https://github.com/flipped-aurora/gin-vue-admin">
|
||||
</div>
|
||||
<div class="w-full flex items-center justify-around">
|
||||
<a href="https://github.com/flipped-aurora/gin-vue-admin">
|
||||
<img
|
||||
class="dom-center"
|
||||
src="https://img.shields.io/github/watchers/flipped-aurora/gin-vue-admin.svg?label=Watch"
|
||||
alt=""
|
||||
/>
|
||||
</a>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<a href="https://github.com/flipped-aurora/gin-vue-admin">
|
||||
<img
|
||||
class="dom-center"
|
||||
@@ -36,8 +31,6 @@
|
||||
alt=""
|
||||
/>
|
||||
</a>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<a href="https://github.com/flipped-aurora/gin-vue-admin">
|
||||
<img
|
||||
class="dom-center"
|
||||
@@ -45,17 +38,15 @@
|
||||
alt=""
|
||||
/>
|
||||
</a>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card style="margin-top: 20px">
|
||||
<el-card class="min-w-96 mt-5">
|
||||
<template #header>
|
||||
<div>flipped-aurora团队</div>
|
||||
</template>
|
||||
<div>
|
||||
<el-row>
|
||||
<el-col :span="8" :offset="8">
|
||||
<div class="w-full flex items-center justify-center">
|
||||
<a href="https://github.com/flipped-aurora">
|
||||
<img
|
||||
class="org-img dom-center"
|
||||
@@ -63,17 +54,13 @@
|
||||
alt="flipped-aurora"
|
||||
/>
|
||||
</a>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div
|
||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mt-4"
|
||||
>
|
||||
<div v-for="(item, index) in members" :key="index" :span="8">
|
||||
<a :href="item.html_url" class="flex items-center">
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-4 mt-4">
|
||||
<div v-for="(item, index) in members" :key="index" class="min-h-10 flex items-center">
|
||||
<a :href="item.html_url" class="flex items-center group">
|
||||
<img class="w-8 h-8 rounded-full" :src="item.avatar_url" />
|
||||
<el-link
|
||||
class="text-blue-700 ml-2 text-xl font-bold font-sans"
|
||||
style=""
|
||||
class="text-blue-700 ml-2 text-lg font-bold font-sans break-all"
|
||||
>{{ item.login }}</el-link
|
||||
>
|
||||
</a>
|
||||
@@ -81,13 +68,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
</div>
|
||||
<div class="w-full md:w-1/2">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div>提交记录</div>
|
||||
</template>
|
||||
<div>
|
||||
<div class="h-[calc(100vh-300px)] overflow-y-auto">
|
||||
<el-timeline>
|
||||
<el-timeline-item
|
||||
v-for="(item, index) in dataTimeline"
|
||||
@@ -102,12 +89,14 @@
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</div>
|
||||
<div class="w-full flex items-center justify-center">
|
||||
<el-button class="load-more" type="primary" link @click="loadMore">
|
||||
Load more
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -155,10 +144,6 @@
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.load-more {
|
||||
margin-left: 120px;
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
float: left;
|
||||
height: 40px;
|
||||
|
@@ -2,19 +2,19 @@
|
||||
<div
|
||||
class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-7 py-2 gap-4 md:gap-2 gva-container2"
|
||||
>
|
||||
<gva-card custom-class="col-span-1 lg:col-span-2 h-32">
|
||||
<gva-card custom-class="col-span-1 lg:col-span-2 ">
|
||||
<gva-chart :type="1" title="访问人数" />
|
||||
</gva-card>
|
||||
<gva-card custom-class="col-span-1 lg:col-span-2 h-32 ">
|
||||
<gva-card custom-class="col-span-1 lg:col-span-2 ">
|
||||
<gva-chart :type="2" title="新增客户" />
|
||||
</gva-card>
|
||||
<gva-card custom-class="col-span-1 lg:col-span-2 h-32">
|
||||
<gva-card custom-class="col-span-1 lg:col-span-2 ">
|
||||
<gva-chart :type="3" title="解决数量" />
|
||||
</gva-card>
|
||||
<gva-card
|
||||
title="快捷功能"
|
||||
show-action
|
||||
custom-class="col-start-1 md:col-start-3 lg:col-start-7 row-span-2 h-38"
|
||||
custom-class="col-start-1 md:col-start-3 lg:col-start-7 row-span-2 "
|
||||
>
|
||||
<gva-quick-link />
|
||||
</gva-card>
|
||||
|
@@ -3,25 +3,27 @@
|
||||
<div class="gva-table-box">
|
||||
<el-divider content-position="left">大文件上传</el-divider>
|
||||
<form id="fromCont" method="post">
|
||||
<div class="fileUpload" @click="inputChange">
|
||||
选择文件
|
||||
<input
|
||||
v-show="false"
|
||||
id="file"
|
||||
ref="FileInput"
|
||||
multiple="multiple"
|
||||
type="file"
|
||||
@change="choseFile"
|
||||
/>
|
||||
<!-- 新增按钮容器,使用 Flexbox 对齐按钮 -->
|
||||
<div class="button-container">
|
||||
<div class="fileUpload" @click="inputChange">
|
||||
<span class="takeFile">选择文件</span>
|
||||
<input
|
||||
v-show="false"
|
||||
id="file"
|
||||
ref="FileInput"
|
||||
multiple="multiple"
|
||||
type="file"
|
||||
@change="choseFile"
|
||||
/>
|
||||
</div>
|
||||
<el-button
|
||||
:disabled="limitFileSize"
|
||||
type="primary"
|
||||
class="uploadBtn"
|
||||
@click="getFile"
|
||||
>上传文件</el-button>
|
||||
</div>
|
||||
</form>
|
||||
<el-button
|
||||
:disabled="limitFileSize"
|
||||
type="primary"
|
||||
class="uploadBtn"
|
||||
@click="getFile"
|
||||
>上传文件</el-button
|
||||
>
|
||||
<div class="el-upload__tip">请上传不超过5MB的文件</div>
|
||||
<div class="list">
|
||||
<transition name="list" tag="p">
|
||||
@@ -48,247 +50,291 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SparkMD5 from 'spark-md5'
|
||||
import {
|
||||
findFile,
|
||||
breakpointContinueFinish,
|
||||
removeChunk,
|
||||
breakpointContinue
|
||||
} from '@/api/breakpoint'
|
||||
import { ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import SparkMD5 from 'spark-md5'
|
||||
import {
|
||||
findFile,
|
||||
breakpointContinueFinish,
|
||||
removeChunk,
|
||||
breakpointContinue
|
||||
} from '@/api/breakpoint'
|
||||
import { ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
defineOptions({
|
||||
name: 'BreakPoint'
|
||||
})
|
||||
defineOptions({
|
||||
name: 'BreakPoint'
|
||||
})
|
||||
|
||||
const file = ref(null)
|
||||
const fileMd5 = ref('')
|
||||
const formDataList = ref([])
|
||||
const waitUpLoad = ref([])
|
||||
const waitNum = ref(NaN)
|
||||
const limitFileSize = ref(false)
|
||||
const percentage = ref(0)
|
||||
const percentageFlage = ref(true)
|
||||
const file = ref(null)
|
||||
const fileMd5 = ref('')
|
||||
const formDataList = ref([])
|
||||
const waitUpLoad = ref([])
|
||||
const waitNum = ref(NaN)
|
||||
const limitFileSize = ref(false)
|
||||
const percentage = ref(0)
|
||||
const percentageFlage = ref(true)
|
||||
|
||||
// 选中文件的函数
|
||||
const choseFile = async (e) => {
|
||||
// 点击选择文件后取消 直接return
|
||||
if (!e.target.files.length) {
|
||||
return
|
||||
}
|
||||
const fileR = new FileReader() // 创建一个reader用来读取文件流
|
||||
const fileInput = e.target.files[0] // 获取当前文件
|
||||
const maxSize = 5 * 1024 * 1024
|
||||
file.value = fileInput // file 丢全局方便后面用 可以改进为func传参形式
|
||||
percentage.value = 0
|
||||
if (file.value.size < maxSize) {
|
||||
fileR.readAsArrayBuffer(file.value) // 把文件读成ArrayBuffer 主要为了保持跟后端的流一致
|
||||
fileR.onload = async (e) => {
|
||||
// 读成arrayBuffer的回调 e 为方法自带参数 相当于 dom的e 流存在e.target.result 中
|
||||
const blob = e.target.result
|
||||
const spark = new SparkMD5.ArrayBuffer() // 创建md5制造工具 (md5用于检测文件一致性 这里不懂就打电话问我)
|
||||
spark.append(blob) // 文件流丢进工具
|
||||
fileMd5.value = spark.end() // 工具结束 产生一个a 总文件的md5
|
||||
const FileSliceCap = 1 * 1024 * 1024 // 分片字节数
|
||||
let start = 0 // 定义分片开始切的地方
|
||||
let end = 0 // 每片结束切的地方a
|
||||
let i = 0 // 第几片
|
||||
formDataList.value = [] // 分片存储的一个池子 丢全局
|
||||
while (end < file.value.size) {
|
||||
// 当结尾数字大于文件总size的时候 结束切片
|
||||
start = i * FileSliceCap // 计算每片开始位置
|
||||
end = (i + 1) * FileSliceCap // 计算每片结束位置
|
||||
var fileSlice = file.value.slice(start, end) // 开始切 file.slice 为 h5方法 对文件切片 参数为 起止字节数
|
||||
const formData = new window.FormData() // 创建FormData用于存储传给后端的信息
|
||||
formData.append('fileMd5', fileMd5.value) // 存储总文件的Md5 让后端知道自己是谁的切片
|
||||
formData.append('file', fileSlice) // 当前的切片
|
||||
formData.append('chunkNumber', i) // 当前是第几片
|
||||
formData.append('fileName', file.value.name) // 当前文件的文件名 用于后端文件切片的命名 formData.appen 为 formData对象添加参数的方法
|
||||
formDataList.value.push({ key: i, formData }) // 把当前切片信息 自己是第几片 存入我们方才准备好的池子
|
||||
i++
|
||||
}
|
||||
const params = {
|
||||
fileName: file.value.name,
|
||||
fileMd5: fileMd5.value,
|
||||
chunkTotal: formDataList.value.length
|
||||
}
|
||||
const res = await findFile(params)
|
||||
// 全部切完以后 发一个请求给后端 拉当前文件后台存储的切片信息 用于检测有多少上传成功的切片
|
||||
const finishList = res.data.file.ExaFileChunk // 上传成功的切片
|
||||
const IsFinish = res.data.file.IsFinish // 是否是同文件不同命 (文件md5相同 文件名不同 则默认是同一个文件但是不同文件名 此时后台数据库只需要拷贝一下数据库文件即可 不需要上传文件 即秒传功能)
|
||||
if (!IsFinish) {
|
||||
// 当是断点续传时候
|
||||
waitUpLoad.value = formDataList.value.filter((all) => {
|
||||
return !(
|
||||
finishList &&
|
||||
finishList.some((fi) => fi.FileChunkNumber === all.key)
|
||||
) // 找出需要上传的切片
|
||||
})
|
||||
} else {
|
||||
waitUpLoad.value = [] // 秒传则没有需要上传的切片
|
||||
ElMessage.success('文件已秒传')
|
||||
}
|
||||
waitNum.value = waitUpLoad.value.length // 记录长度用于百分比展示
|
||||
// 选中文件的函数
|
||||
const choseFile = async (e) => {
|
||||
// 点击选择文件后取消 直接return
|
||||
if (!e.target.files.length) {
|
||||
return
|
||||
}
|
||||
const fileR = new FileReader() // 创建一个reader用来读取文件流
|
||||
const fileInput = e.target.files[0] // 获取当前文件
|
||||
const maxSize = 5 * 1024 * 1024
|
||||
file.value = fileInput // file 丢全局方便后面用 可以改进为func传参形式
|
||||
percentage.value = 0
|
||||
if (file.value.size < maxSize) {
|
||||
fileR.readAsArrayBuffer(file.value) // 把文件读成ArrayBuffer 主要为了保持跟后端的流一致
|
||||
fileR.onload = async (e) => {
|
||||
// 读成arrayBuffer的回调 e 为方法自带参数 相当于 dom的e 流存在e.target.result 中
|
||||
const blob = e.target.result
|
||||
const spark = new SparkMD5.ArrayBuffer() // 创建md5制造工具 (md5用于检测文件一致性 这里不懂就打电话问我)
|
||||
spark.append(blob) // 文件流丢进工具
|
||||
fileMd5.value = spark.end() // 工具结束 产生一个a 总文件的md5
|
||||
const FileSliceCap = 1 * 1024 * 1024 // 分片字节数
|
||||
let start = 0 // 定义分片开始切的地方
|
||||
let end = 0 // 每片结束切的地方a
|
||||
let i = 0 // 第几片
|
||||
formDataList.value = [] // 分片存储的一个池子 丢全局
|
||||
while (end < file.value.size) {
|
||||
// 当结尾数字大于文件总size的时候 结束切片
|
||||
start = i * FileSliceCap // 计算每片开始位置
|
||||
end = (i + 1) * FileSliceCap // 计算每片结束位置
|
||||
var fileSlice = file.value.slice(start, end) // 开始切 file.slice 为 h5方法 对文件切片 参数为 起止字节数
|
||||
const formData = new window.FormData() // 创建FormData用于存储传给后端的信息
|
||||
formData.append('fileMd5', fileMd5.value) // 存储总文件的Md5 让后端知道自己是谁的切片
|
||||
formData.append('file', fileSlice) // 当前的切片
|
||||
formData.append('chunkNumber', i) // 当前是第几片
|
||||
formData.append('fileName', file.value.name) // 当前文件的文件名 用于后端文件切片的命名 formData.appen 为 formData对象添加参数的方法
|
||||
formDataList.value.push({ key: i, formData }) // 把当前切片信息 自己是第几片 存入我们方才准备好的池子
|
||||
i++
|
||||
}
|
||||
} else {
|
||||
limitFileSize.value = true
|
||||
ElMessage('请上传小于5M文件')
|
||||
}
|
||||
}
|
||||
|
||||
const getFile = () => {
|
||||
// 确定按钮
|
||||
if (file.value === null) {
|
||||
ElMessage('请先上传文件')
|
||||
return
|
||||
}
|
||||
if (percentage.value === 100) {
|
||||
percentageFlage.value = false
|
||||
}
|
||||
sliceFile() // 上传切片
|
||||
}
|
||||
|
||||
const sliceFile = () => {
|
||||
waitUpLoad.value &&
|
||||
waitUpLoad.value.forEach((item) => {
|
||||
// 需要上传的切片
|
||||
item.formData.append('chunkTotal', formDataList.value.length) // 切片总数携带给后台 总有用的
|
||||
const fileR = new FileReader() // 功能同上
|
||||
const fileF = item.formData.get('file')
|
||||
fileR.readAsArrayBuffer(fileF)
|
||||
fileR.onload = (e) => {
|
||||
const spark = new SparkMD5.ArrayBuffer()
|
||||
spark.append(e.target.result)
|
||||
item.formData.append('chunkMd5', spark.end()) // 获取当前切片md5 后端用于验证切片完整性
|
||||
upLoadFileSlice(item)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => waitNum.value,
|
||||
() => {
|
||||
percentage.value = Math.floor(
|
||||
((formDataList.value.length - waitNum.value) /
|
||||
formDataList.value.length) *
|
||||
100
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const upLoadFileSlice = async (item) => {
|
||||
// 切片上传
|
||||
const fileRe = await breakpointContinue(item.formData)
|
||||
if (fileRe.code !== 0) {
|
||||
return
|
||||
}
|
||||
waitNum.value-- // 百分数增加
|
||||
if (waitNum.value === 0) {
|
||||
// 切片传完以后 合成文件
|
||||
const params = {
|
||||
fileName: file.value.name,
|
||||
fileMd5: fileMd5.value
|
||||
fileMd5: fileMd5.value,
|
||||
chunkTotal: formDataList.value.length
|
||||
}
|
||||
const res = await breakpointContinueFinish(params)
|
||||
if (res.code === 0) {
|
||||
// 合成文件过后 删除缓存切片
|
||||
const params = {
|
||||
fileName: file.value.name,
|
||||
fileMd5: fileMd5.value,
|
||||
filePath: res.data.filePath
|
||||
}
|
||||
ElMessage.success('上传成功')
|
||||
await removeChunk(params)
|
||||
const res = await findFile(params)
|
||||
// 全部切完以后 发一个请求给后端 拉当前文件后台存储的切片信息 用于检测有多少上传成功的切片
|
||||
const finishList = res.data.file.ExaFileChunk // 上传成功的切片
|
||||
const IsFinish = res.data.file.IsFinish // 是否是同文件不同命 (文件md5相同 文件名不同 则默认是同一个文件但是不同文件名 此时后台数据库只需要拷贝一下数据库文件即可 不需要上传文件 即秒传功能)
|
||||
if (!IsFinish) {
|
||||
// 当是断点续传时候
|
||||
waitUpLoad.value = formDataList.value.filter((all) => {
|
||||
return !(
|
||||
finishList &&
|
||||
finishList.some((fi) => fi.FileChunkNumber === all.key)
|
||||
) // 找出需要上传的切片
|
||||
})
|
||||
} else {
|
||||
waitUpLoad.value = [] // 秒传则没有需要上传的切片
|
||||
ElMessage.success('文件已秒传!')
|
||||
}
|
||||
waitNum.value = waitUpLoad.value.length // 记录长度用于百分比展示
|
||||
}
|
||||
} else {
|
||||
limitFileSize.value = true
|
||||
ElMessage('请上传小于5M文件!')
|
||||
}
|
||||
}
|
||||
|
||||
const getFile = () => {
|
||||
// 确定按钮
|
||||
if (file.value === null) {
|
||||
ElMessage('请先上传文件!')
|
||||
return
|
||||
}
|
||||
// 检查文件上传进度
|
||||
if (percentage.value === 100) {
|
||||
ElMessage.success('上传已完成!') // 添加提示消息
|
||||
percentageFlage.value = false
|
||||
return // 如果进度已完成,阻止继续执行后续代码
|
||||
}
|
||||
// 如果文件未上传完成,继续上传切片
|
||||
sliceFile() // 上传切片
|
||||
}
|
||||
|
||||
const sliceFile = () => {
|
||||
waitUpLoad.value &&
|
||||
waitUpLoad.value.forEach((item) => {
|
||||
// 需要上传的切片
|
||||
item.formData.append('chunkTotal', formDataList.value.length) // 切片总数携带给后台 总有用的
|
||||
const fileR = new FileReader() // 功能同上
|
||||
const fileF = item.formData.get('file')
|
||||
fileR.readAsArrayBuffer(fileF)
|
||||
fileR.onload = (e) => {
|
||||
const spark = new SparkMD5.ArrayBuffer()
|
||||
spark.append(e.target.result)
|
||||
item.formData.append('chunkMd5', spark.end()) // 获取当前切片md5 后端用于验证切片完整性
|
||||
upLoadFileSlice(item)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => waitNum.value,
|
||||
() => {
|
||||
percentage.value = Math.floor(
|
||||
((formDataList.value.length - waitNum.value) /
|
||||
formDataList.value.length) *
|
||||
100
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const upLoadFileSlice = async (item) => {
|
||||
// 切片上传
|
||||
const fileRe = await breakpointContinue(item.formData)
|
||||
if (fileRe.code !== 0) {
|
||||
return
|
||||
}
|
||||
waitNum.value-- // 百分数增加
|
||||
if (waitNum.value === 0) {
|
||||
// 切片传完以后 合成文件
|
||||
const params = {
|
||||
fileName: file.value.name,
|
||||
fileMd5: fileMd5.value
|
||||
}
|
||||
const res = await breakpointContinueFinish(params)
|
||||
if (res.code === 0) {
|
||||
// 合成文件过后 删除缓存切片
|
||||
const params = {
|
||||
fileName: file.value.name,
|
||||
fileMd5: fileMd5.value,
|
||||
filePath: res.data.filePath
|
||||
}
|
||||
ElMessage.success('上传成功')
|
||||
await removeChunk(params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const FileInput = ref(null)
|
||||
const inputChange = () => {
|
||||
FileInput.value.dispatchEvent(new MouseEvent('click'))
|
||||
}
|
||||
const FileInput = ref(null)
|
||||
const inputChange = () => {
|
||||
FileInput.value.dispatchEvent(new MouseEvent('click'))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
h3 {
|
||||
margin: 40px 0 0;
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
li {
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
}
|
||||
a {
|
||||
color: #42b983;
|
||||
}
|
||||
#fromCont {
|
||||
display: inline-block;
|
||||
}
|
||||
.fileUpload {
|
||||
padding: 3px 10px;
|
||||
font-size: 12px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
border: 1px solid #c1c1c1;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
input {
|
||||
position: absolute;
|
||||
font-size: 100px;
|
||||
right: 0;
|
||||
top: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.fileName {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin: 6px 15px 0 15px;
|
||||
}
|
||||
.uploadBtn {
|
||||
position: relative;
|
||||
top: -10px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
.tips {
|
||||
margin-top: 30px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #606266;
|
||||
}
|
||||
.el-divider {
|
||||
margin: 0 0 30px 0;
|
||||
}
|
||||
h3 {
|
||||
margin: 40px 0 0;
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
li {
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
}
|
||||
a {
|
||||
color: #42b983;
|
||||
}
|
||||
#fromCont {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-top: 15px;
|
||||
.gva-table-box {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.fileUpload,
|
||||
.uploadBtn {
|
||||
width: 90px;
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
font-size: 14px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fileUpload {
|
||||
padding: 0 15px;
|
||||
background-color: #007bff;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease-in-out;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.uploadBtn {
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.fileUpload:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.uploadBtn:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
|
||||
.fileUpload:active,
|
||||
.uploadBtn:active {
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
.fileUpload input {
|
||||
position: relative;
|
||||
font-size: 100px;
|
||||
right: 0;
|
||||
top: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.fileName {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin: 6px 15px 0 15px;
|
||||
}
|
||||
.tips {
|
||||
margin-top: 30px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #606266;
|
||||
}
|
||||
.el-divider {
|
||||
margin: 0 0 30px 0;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-top: 15px;
|
||||
}
|
||||
.list-item {
|
||||
display: block;
|
||||
margin-right: 10px;
|
||||
color: #606266;
|
||||
line-height: 25px;
|
||||
margin-bottom: 5px;
|
||||
width: 40%;
|
||||
.percentage {
|
||||
float: right;
|
||||
}
|
||||
.list-item {
|
||||
display: block;
|
||||
margin-right: 10px;
|
||||
color: #606266;
|
||||
line-height: 25px;
|
||||
margin-bottom: 5px;
|
||||
width: 40%;
|
||||
.percentage {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 1s;
|
||||
}
|
||||
.list-enter, .list-leave-to
|
||||
/* .list-leave-active for below version 2.1.8 */ {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
</style>
|
||||
}
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 1s;
|
||||
}
|
||||
.list-enter, .list-leave-to
|
||||
/* .list-leave-active for below version 2.1.8 */ {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
</style>
|
@@ -1,161 +1,233 @@
|
||||
<template>
|
||||
<div v-loading.fullscreen.lock="fullscreenLoading">
|
||||
<div class="gva-table-box">
|
||||
<warning-bar title="点击“文件名/备注”可以编辑文件名或者备注内容。" />
|
||||
<div class="gva-btn-list gap-3">
|
||||
<upload-common :image-common="imageCommon" @on-success="getTableData" />
|
||||
<upload-image
|
||||
:image-url="imageUrl"
|
||||
:file-size="512"
|
||||
:max-w-h="1080"
|
||||
@on-success="getTableData"
|
||||
/>
|
||||
<el-button type="primary" icon="upload" @click="importUrlFunc">
|
||||
导入URL
|
||||
</el-button>
|
||||
<el-input
|
||||
v-model="search.keyword"
|
||||
class="w-72"
|
||||
placeholder="请输入文件名或备注"
|
||||
/>
|
||||
<el-button type="primary" icon="search" @click="getTableData"
|
||||
>查询</el-button
|
||||
>
|
||||
<div class="flex gap-4 p-2">
|
||||
<div class="flex-none w-64 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded p-4">
|
||||
<el-scrollbar style="height: calc(100vh - 300px)">
|
||||
<el-tree
|
||||
:data="categories"
|
||||
node-key="id"
|
||||
:props="defaultProps"
|
||||
@node-click="handleNodeClick"
|
||||
default-expand-all
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<div class="w-36" :class="search.classId === data.ID ? 'text-blue-500 font-bold' : ''">{{ data.name }}
|
||||
</div>
|
||||
<el-dropdown>
|
||||
<el-icon class="ml-3 text-right" v-if="data.ID > 0"><MoreFilled /></el-icon>
|
||||
<el-icon class="ml-3 text-right mt-1" v-else><Plus /></el-icon>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="addCategoryFun(data)">添加分类</el-dropdown-item>
|
||||
<el-dropdown-item @click="editCategory(data)" v-if="data.ID > 0">编辑分类</el-dropdown-item>
|
||||
<el-dropdown-item @click="deleteCategoryFun(data.ID)" v-if="data.ID > 0">删除分类</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<div class="flex-1 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900">
|
||||
<div class="gva-table-box mt-0 mb-0">
|
||||
<warning-bar title="点击“文件名”可以编辑;选择的类别即是上传的类别。" />
|
||||
<div class="gva-btn-list gap-3">
|
||||
<upload-common :image-common="imageCommon" :classId="search.classId" @on-success="onSuccess" />
|
||||
<upload-image
|
||||
:image-url="imageUrl"
|
||||
:file-size="512"
|
||||
:max-w-h="1080"
|
||||
:classId="search.classId"
|
||||
@on-success="onSuccess"
|
||||
/>
|
||||
<el-button type="primary" icon="upload" @click="importUrlFunc">
|
||||
导入URL
|
||||
</el-button>
|
||||
<el-input
|
||||
v-model="search.keyword"
|
||||
class="w-72"
|
||||
placeholder="请输入文件名或备注"
|
||||
/>
|
||||
<el-button type="primary" icon="search" @click="onSubmit"
|
||||
>查询
|
||||
</el-button
|
||||
>
|
||||
</div>
|
||||
|
||||
<el-table :data="tableData">
|
||||
<el-table-column align="left" label="预览" width="100">
|
||||
<template #default="scope">
|
||||
<CustomPic pic-type="file" :pic-src="scope.row.url" preview />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="日期" prop="UpdatedAt" width="180">
|
||||
<template #default="scope">
|
||||
<div>{{ formatDate(scope.row.UpdatedAt) }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="文件名/备注"
|
||||
prop="name"
|
||||
width="180"
|
||||
>
|
||||
<template #default="scope">
|
||||
<div class="name" @click="editFileNameFunc(scope.row)">
|
||||
{{ scope.row.name }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="链接" prop="url" min-width="300" />
|
||||
<el-table-column align="left" label="标签" prop="tag" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag
|
||||
:type="scope.row.tag?.toLowerCase() === 'jpg' ? 'info' : 'success'"
|
||||
disable-transitions
|
||||
>{{ scope.row.tag }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="操作" width="160">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
icon="download"
|
||||
type="primary"
|
||||
link
|
||||
@click="downloadFile(scope.row)"
|
||||
>下载</el-button
|
||||
<el-table :data="tableData">
|
||||
<el-table-column align="left" label="预览" width="100">
|
||||
<template #default="scope">
|
||||
<CustomPic pic-type="file" :pic-src="scope.row.url" preview/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="日期" prop="UpdatedAt" width="180">
|
||||
<template #default="scope">
|
||||
<div>{{ formatDate(scope.row.UpdatedAt) }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="文件名/备注"
|
||||
prop="name"
|
||||
width="180"
|
||||
>
|
||||
<el-button
|
||||
icon="delete"
|
||||
type="primary"
|
||||
link
|
||||
@click="deleteFileFunc(scope.row)"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="gva-pagination">
|
||||
<el-pagination
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 30, 50, 100]"
|
||||
:style="{ float: 'right', padding: '20px' }"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
<template #default="scope">
|
||||
<div class="cursor-pointer" @click="editFileNameFunc(scope.row)">
|
||||
{{ scope.row.name }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="链接" prop="url" min-width="300"/>
|
||||
<el-table-column align="left" label="标签" prop="tag" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag
|
||||
:type="scope.row.tag?.toLowerCase() === 'jpg' ? 'info' : 'success'"
|
||||
disable-transitions
|
||||
>{{ scope.row.tag }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="操作" width="160">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
icon="download"
|
||||
type="primary"
|
||||
link
|
||||
@click="downloadFile(scope.row)"
|
||||
>下载
|
||||
</el-button
|
||||
>
|
||||
<el-button
|
||||
icon="delete"
|
||||
type="primary"
|
||||
link
|
||||
@click="deleteFileFunc(scope.row)"
|
||||
>删除
|
||||
</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="gva-pagination">
|
||||
<el-pagination
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 30, 50, 100]"
|
||||
:style="{ float: 'right', padding: '20px' }"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加分类弹窗 -->
|
||||
<el-dialog v-model="categoryDialogVisible" @close="closeAddCategoryDialog" width="520"
|
||||
:title="(categoryFormData.ID === 0 ? '添加' : '编辑') + '分类'"
|
||||
draggable
|
||||
>
|
||||
<el-form ref="categoryForm" :rules="rules" :model="categoryFormData" label-width="80px">
|
||||
<el-form-item label="上级分类">
|
||||
<el-tree-select
|
||||
v-model="categoryFormData.pid"
|
||||
:data="categories"
|
||||
check-strictly
|
||||
:props="defaultProps"
|
||||
:render-after-expand="false"
|
||||
style="width: 240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="分类名称" prop="name">
|
||||
<el-input v-model.trim="categoryFormData.name" placeholder="分类名称"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="closeAddCategoryDialog">取消</el-button>
|
||||
<el-button type="primary" @click="confirmAddCategory">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
getFileList,
|
||||
deleteFile,
|
||||
editFileName,
|
||||
importURL
|
||||
} from '@/api/fileUploadAndDownload'
|
||||
import { downloadImage } from '@/utils/downloadImg'
|
||||
import CustomPic from '@/components/customPic/index.vue'
|
||||
import UploadImage from '@/components/upload/image.vue'
|
||||
import UploadCommon from '@/components/upload/common.vue'
|
||||
import { CreateUUID, formatDate } from '@/utils/format'
|
||||
import WarningBar from '@/components/warningBar/warningBar.vue'
|
||||
import {
|
||||
getFileList,
|
||||
deleteFile,
|
||||
editFileName,
|
||||
importURL
|
||||
} from '@/api/fileUploadAndDownload'
|
||||
import {downloadImage} from '@/utils/downloadImg'
|
||||
import CustomPic from '@/components/customPic/index.vue'
|
||||
import UploadImage from '@/components/upload/image.vue'
|
||||
import UploadCommon from '@/components/upload/common.vue'
|
||||
import {CreateUUID, formatDate} from '@/utils/format'
|
||||
import WarningBar from '@/components/warningBar/warningBar.vue'
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {ref} from 'vue'
|
||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||
import {addCategory, deleteCategory, getCategoryList} from "@/api/attachmentCategory";
|
||||
|
||||
defineOptions({
|
||||
name: 'Upload'
|
||||
})
|
||||
defineOptions({
|
||||
name: 'Upload'
|
||||
})
|
||||
|
||||
const path = ref(import.meta.env.VITE_BASE_API)
|
||||
const path = ref(import.meta.env.VITE_BASE_API)
|
||||
|
||||
const imageUrl = ref('')
|
||||
const imageCommon = ref('')
|
||||
const imageUrl = ref('')
|
||||
const imageCommon = ref('')
|
||||
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
const pageSize = ref(10)
|
||||
const search = ref({})
|
||||
const tableData = ref([])
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
const pageSize = ref(10)
|
||||
const search = ref({
|
||||
keyword: null,
|
||||
classId: 0
|
||||
})
|
||||
const tableData = ref([])
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 查询
|
||||
const getTableData = async () => {
|
||||
const table = await getFileList({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
...search.value
|
||||
})
|
||||
if (table.code === 0) {
|
||||
tableData.value = table.data.list
|
||||
total.value = table.data.total
|
||||
page.value = table.data.page
|
||||
pageSize.value = table.data.pageSize
|
||||
}
|
||||
}
|
||||
// 分页
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const deleteFileFunc = async (row) => {
|
||||
ElMessageBox.confirm('此操作将永久删除文件, 是否继续?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const onSubmit = () => {
|
||||
search.value.classId = 0
|
||||
page.value = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 查询
|
||||
const getTableData = async () => {
|
||||
const table = await getFileList({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
...search.value
|
||||
})
|
||||
if (table.code === 0) {
|
||||
tableData.value = table.data.list
|
||||
total.value = table.data.total
|
||||
page.value = table.data.page
|
||||
pageSize.value = table.data.pageSize
|
||||
}
|
||||
}
|
||||
getTableData()
|
||||
|
||||
const deleteFileFunc = async (row) => {
|
||||
ElMessageBox.confirm('此操作将永久删除文件, 是否继续?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
const res = await deleteFile(row)
|
||||
if (res.code === 0) {
|
||||
@@ -175,30 +247,30 @@
|
||||
message: '已取消删除'
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const downloadFile = (row) => {
|
||||
if (row.url.indexOf('http://') > -1 || row.url.indexOf('https://') > -1) {
|
||||
downloadImage(row.url, row.name)
|
||||
} else {
|
||||
downloadImage(path.value + '/' + row.url, row.name)
|
||||
}
|
||||
const downloadFile = (row) => {
|
||||
if (row.url.indexOf('http://') > -1 || row.url.indexOf('https://') > -1) {
|
||||
downloadImage(row.url, row.name)
|
||||
} else {
|
||||
downloadImage(path.value + '/' + row.url, row.name)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑文件名或者备注
|
||||
* @param row
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const editFileNameFunc = async (row) => {
|
||||
ElMessageBox.prompt('请输入文件名或者备注', '编辑', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern: /\S/,
|
||||
inputErrorMessage: '不能为空',
|
||||
inputValue: row.name
|
||||
})
|
||||
.then(async ({ value }) => {
|
||||
/**
|
||||
* 编辑文件名或者备注
|
||||
* @param row
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const editFileNameFunc = async (row) => {
|
||||
ElMessageBox.prompt('请输入文件名或者备注', '编辑', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern: /\S/,
|
||||
inputErrorMessage: '不能为空',
|
||||
inputValue: row.name
|
||||
})
|
||||
.then(async ({value}) => {
|
||||
row.name = value
|
||||
// console.log(row)
|
||||
const res = await editFileName(row)
|
||||
@@ -216,22 +288,22 @@
|
||||
message: '取消修改'
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入URL
|
||||
*/
|
||||
const importUrlFunc = () => {
|
||||
ElMessageBox.prompt('格式:文件名|链接或者仅链接。', '导入', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputType: 'textarea',
|
||||
inputPlaceholder:
|
||||
/**
|
||||
* 导入URL
|
||||
*/
|
||||
const importUrlFunc = () => {
|
||||
ElMessageBox.prompt('格式:文件名|链接或者仅链接。', '导入', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputType: 'textarea',
|
||||
inputPlaceholder:
|
||||
'我的图片|https://my-oss.com/my.png\nhttps://my-oss.com/my_1.png',
|
||||
inputPattern: /\S/,
|
||||
inputErrorMessage: '不能为空'
|
||||
})
|
||||
.then(async ({ value }) => {
|
||||
inputPattern: /\S/,
|
||||
inputErrorMessage: '不能为空'
|
||||
})
|
||||
.then(async ({value}) => {
|
||||
let data = value.split('\n')
|
||||
let importData = []
|
||||
data.forEach((item) => {
|
||||
@@ -249,6 +321,7 @@
|
||||
importData.push({
|
||||
name: name,
|
||||
url: url,
|
||||
classId: search.value.classId,
|
||||
tag: url.substring(url.lastIndexOf('.') + 1),
|
||||
key: CreateUUID()
|
||||
})
|
||||
@@ -270,11 +343,101 @@
|
||||
message: '取消导入'
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
}
|
||||
|
||||
<style scoped>
|
||||
.name {
|
||||
cursor: pointer;
|
||||
const onSuccess = () => {
|
||||
search.value.keyword = null
|
||||
page.value = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
children: 'children',
|
||||
label: 'name',
|
||||
value: 'ID'
|
||||
}
|
||||
|
||||
const categories = ref([])
|
||||
const fetchCategories = async () => {
|
||||
const res = await getCategoryList()
|
||||
let data = {
|
||||
name: '全部分类',
|
||||
ID: 0,
|
||||
pid: 0,
|
||||
children:[]
|
||||
}
|
||||
</style>
|
||||
if (res.code === 0) {
|
||||
categories.value = res.data || []
|
||||
categories.value.unshift(data)
|
||||
}
|
||||
}
|
||||
|
||||
const handleNodeClick = (node) => {
|
||||
search.value.keyword = null
|
||||
search.value.classId = node.ID
|
||||
page.value = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const categoryDialogVisible = ref(false)
|
||||
const categoryFormData = ref({
|
||||
ID: 0,
|
||||
pid: 0,
|
||||
name: ''
|
||||
})
|
||||
|
||||
const categoryForm = ref(null)
|
||||
const rules = ref({
|
||||
name: [
|
||||
{required: true, message: '请输入分类名称', trigger: 'blur'},
|
||||
{max: 20, message: '最多20位字符', trigger: 'blur'}
|
||||
]
|
||||
})
|
||||
|
||||
const addCategoryFun = (category) => {
|
||||
categoryDialogVisible.value = true
|
||||
categoryFormData.value.ID = 0
|
||||
categoryFormData.value.pid = category.ID
|
||||
}
|
||||
|
||||
const editCategory = (category) => {
|
||||
categoryFormData.value = {
|
||||
ID: category.ID,
|
||||
pid: category.pid,
|
||||
name: category.name
|
||||
}
|
||||
categoryDialogVisible.value = true
|
||||
}
|
||||
|
||||
const deleteCategoryFun = async (id) => {
|
||||
const res = await deleteCategory({id: id})
|
||||
if (res.code === 0) {
|
||||
ElMessage.success({type: 'success', message: '删除成功'})
|
||||
await fetchCategories()
|
||||
}
|
||||
}
|
||||
|
||||
const confirmAddCategory = async () => {
|
||||
categoryForm.value.validate(async valid => {
|
||||
if (valid) {
|
||||
const res = await addCategory(categoryFormData.value)
|
||||
if (res.code === 0) {
|
||||
ElMessage({type: 'success', message: '操作成功'})
|
||||
await fetchCategories()
|
||||
closeAddCategoryDialog()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const closeAddCategoryDialog = () => {
|
||||
categoryDialogVisible.value = false
|
||||
categoryFormData.value = {
|
||||
ID: 0,
|
||||
pid: 0,
|
||||
name: ''
|
||||
}
|
||||
}
|
||||
|
||||
fetchCategories()
|
||||
</script>
|
||||
|
@@ -95,7 +95,6 @@
|
||||
return config.value.layout_side_collapsed_width
|
||||
}
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
active.value = route.meta.activeName || route.name
|
||||
})
|
||||
@@ -123,8 +122,10 @@
|
||||
})
|
||||
if (index === route.name) return
|
||||
if (index.indexOf('http://') > -1 || index.indexOf('https://') > -1) {
|
||||
window.open(index)
|
||||
} else {
|
||||
window.open(index, '_blank')
|
||||
return
|
||||
}
|
||||
|
||||
if (!top) {
|
||||
router.push({ name: index, query, params })
|
||||
return
|
||||
@@ -136,7 +137,7 @@
|
||||
}
|
||||
const firstMenu = leftMenu.find((item) => !item.hidden && item.path.indexOf("http://") === -1 && item.path.indexOf("https://") === -1)
|
||||
router.push({ name: firstMenu.name, query, params })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const toggleCollapse = () => {
|
||||
|
@@ -40,6 +40,10 @@
|
||||
const isCollapse = ref(false)
|
||||
const active = ref('')
|
||||
watchEffect(() => {
|
||||
if (route.name === 'Iframe') {
|
||||
active.value = decodeURIComponent(route.query.url)
|
||||
return
|
||||
}
|
||||
active.value = route.meta.activeName || route.name
|
||||
})
|
||||
|
||||
@@ -66,7 +70,18 @@
|
||||
})
|
||||
if (index === route.name) return
|
||||
if (index.indexOf('http://') > -1 || index.indexOf('https://') > -1) {
|
||||
window.open(index)
|
||||
if (index === 'Iframe') {
|
||||
query.url = decodeURIComponent(index)
|
||||
router.push({
|
||||
name: 'Iframe',
|
||||
query,
|
||||
params
|
||||
})
|
||||
return
|
||||
} else {
|
||||
window.open(index, '_blank')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
router.push({ name: index, query, params })
|
||||
}
|
||||
|
@@ -15,7 +15,7 @@
|
||||
unique-opened
|
||||
@select="selectMenuItem"
|
||||
>
|
||||
<template v-for="item in routerStore.asyncRouters[0].children">
|
||||
<template v-for="item in routerStore.asyncRouters[0]?.children || []">
|
||||
<aside-component
|
||||
v-if="!item.hidden"
|
||||
:key="item.name"
|
||||
@@ -65,6 +65,10 @@
|
||||
}
|
||||
})
|
||||
watchEffect(() => {
|
||||
if (route.name === 'Iframe') {
|
||||
active.value = decodeURIComponent(route.query.url)
|
||||
return
|
||||
}
|
||||
active.value = route.meta.activeName || route.name
|
||||
})
|
||||
|
||||
@@ -91,7 +95,18 @@
|
||||
})
|
||||
if (index === route.name) return
|
||||
if (index.indexOf('http://') > -1 || index.indexOf('https://') > -1) {
|
||||
window.open(index)
|
||||
if (index === 'Iframe') {
|
||||
query.url = decodeURIComponent(index)
|
||||
router.push({
|
||||
name: 'Iframe',
|
||||
query,
|
||||
params
|
||||
})
|
||||
return
|
||||
} else {
|
||||
window.open(index, '_blank')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
router.push({ name: index, query, params })
|
||||
}
|
||||
|
70
web/src/view/layout/iframe.vue
Normal file
70
web/src/view/layout/iframe.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div
|
||||
class="bg-gray-50 text-slate-700 dark:text-slate-500 dark:bg-slate-800 w-screen h-screen"
|
||||
>
|
||||
<iframe
|
||||
v-if="reloadFlag"
|
||||
id="gva-base-load-dom"
|
||||
class="gva-body-h bg-gray-50 dark:bg-slate-800 w-full border-t border-gray-200 dark:border-slate-700"
|
||||
src="https://www.gin-vue-admin.com"
|
||||
></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import useResponsive from '@/hooks/responsive'
|
||||
import { emitter } from '@/utils/bus.js'
|
||||
import { ref, onMounted, nextTick, reactive, watchEffect } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useUserStore } from '@/pinia/modules/user'
|
||||
import { useAppStore } from '@/pinia'
|
||||
import { storeToRefs } from 'pinia'
|
||||
const appStore = useAppStore()
|
||||
const { isDark } = storeToRefs(appStore)
|
||||
|
||||
defineOptions({
|
||||
name: 'GvaLayoutIframe'
|
||||
})
|
||||
|
||||
useResponsive(true)
|
||||
const font = reactive({
|
||||
color: 'rgba(0, 0, 0, .15)'
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
font.color = isDark.value ? 'rgba(255,255,255, .15)' : 'rgba(0, 0, 0, .15)'
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
onMounted(() => {
|
||||
// 挂载一些通用的事件
|
||||
emitter.on('reload', reload)
|
||||
if (userStore.loadingInstance) {
|
||||
userStore.loadingInstance.close()
|
||||
}
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const reloadFlag = ref(true)
|
||||
let reloadTimer = null
|
||||
const reload = async () => {
|
||||
if (reloadTimer) {
|
||||
window.clearTimeout(reloadTimer)
|
||||
}
|
||||
reloadTimer = window.setTimeout(async () => {
|
||||
if (route.meta.keepAlive) {
|
||||
reloadFlag.value = false
|
||||
await nextTick()
|
||||
reloadFlag.value = true
|
||||
} else {
|
||||
const title = route.meta.title
|
||||
router.push({ name: 'Reload', params: { title } })
|
||||
}
|
||||
}, 400)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
@@ -74,8 +74,7 @@
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
font.color =
|
||||
isDark.value ? 'rgba(255,255,255, .15)' : 'rgba(0, 0, 0, .15)'
|
||||
font.color = isDark.value ? 'rgba(255,255,255, .15)' : 'rgba(0, 0, 0, .15)'
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
@@ -12,18 +12,24 @@
|
||||
<div class="flex flex-col lg:flex-row items-start gap-8">
|
||||
<!-- 左侧头像 -->
|
||||
<div class="profile-avatar-wrapper flex-shrink-0 mx-auto lg:mx-0">
|
||||
<ProfileAvatar
|
||||
v-model="userStore.userInfo.headerImg"
|
||||
@update:modelValue="handleAvatarChange"
|
||||
<SelectImage
|
||||
v-model="userStore.userInfo.headerImg"
|
||||
file-type="image"
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 右侧信息 -->
|
||||
<div class="flex-1 pt-20 w-full">
|
||||
<div class="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-4">
|
||||
<div>
|
||||
<div class="flex-1 pt-12 lg:pt-20 w-full">
|
||||
<div
|
||||
class="flex flex-col lg:flex-row items-start lg:items-start justify-between gap-4"
|
||||
>
|
||||
<div class="lg:mt-4">
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<div v-if="!editFlag" class="text-2xl font-bold flex items-center gap-3 text-gray-800 dark:text-gray-100">
|
||||
<div
|
||||
v-if="!editFlag"
|
||||
class="text-2xl font-bold flex items-center gap-3 text-gray-800 dark:text-gray-100"
|
||||
>
|
||||
{{ userStore.userInfo.nickName }}
|
||||
<el-icon
|
||||
class="cursor-pointer text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors duration-200"
|
||||
@@ -32,29 +38,27 @@
|
||||
<edit />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div v-else class="flex items-center gap-3">
|
||||
<el-input
|
||||
v-model="nickName"
|
||||
class="w-48"
|
||||
size="large"
|
||||
/>
|
||||
<el-button type="success" circle @click="enterEdit">
|
||||
<el-icon><check /></el-icon>
|
||||
<div v-else class="flex items-center">
|
||||
<el-input v-model="nickName" class="w-48 mr-4" />
|
||||
<el-button type="primary" plain @click="enterEdit">
|
||||
确认
|
||||
</el-button>
|
||||
<el-button type="danger" circle @click="closeEdit">
|
||||
<el-icon><close /></el-icon>
|
||||
<el-button type="danger" plain @click="closeEdit">
|
||||
取消
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col lg:flex-row items-start lg:items-center gap-4 lg:gap-8 text-gray-500 dark:text-gray-400">
|
||||
<div
|
||||
class="flex flex-col lg:flex-row items-start lg:items-center gap-4 lg:gap-8 text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-icon><location /></el-icon>
|
||||
<span>中国·北京市·朝阳区</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-icon><office-building /></el-icon>
|
||||
<span>北京反转极光科技有限公司</span>
|
||||
<span>北京翻转极光科技有限公司</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-icon><user /></el-icon>
|
||||
@@ -63,15 +67,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 mt-4 lg:mt-0">
|
||||
<el-button type="primary" plain>
|
||||
<el-icon><message /></el-icon>
|
||||
<div class="flex gap-4 mt-4">
|
||||
<el-button type="primary" plain icon="message">
|
||||
发送消息
|
||||
</el-button>
|
||||
<el-button>
|
||||
<el-icon><share /></el-icon>
|
||||
分享主页
|
||||
</el-button>
|
||||
<el-button icon="share"> 分享主页 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,13 +83,17 @@
|
||||
<div class="grid lg:grid-cols-12 md:grid-cols-1 gap-8">
|
||||
<!-- 左侧信息栏 -->
|
||||
<div class="lg:col-span-4">
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 mb-6 profile-card">
|
||||
<div
|
||||
class="bg-white dark:bg-slate-800 rounded-xl p-6 mb-6 profile-card"
|
||||
>
|
||||
<h2 class="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<el-icon class="text-blue-500"><info-filled /></el-icon>
|
||||
基本信息
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-3 text-gray-600 dark:text-gray-300">
|
||||
<div
|
||||
class="flex items-center gap-1 lg:gap-3 text-gray-600 dark:text-gray-300"
|
||||
>
|
||||
<el-icon class="text-blue-500"><phone /></el-icon>
|
||||
<span class="font-medium">手机号码:</span>
|
||||
<span>{{ userStore.userInfo.phone || '未设置' }}</span>
|
||||
@@ -102,9 +106,11 @@
|
||||
修改
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-gray-600 dark:text-gray-300">
|
||||
<div
|
||||
class="flex items-center gap-1 lg:gap-3 text-gray-600 dark:text-gray-300"
|
||||
>
|
||||
<el-icon class="text-green-500"><message /></el-icon>
|
||||
<span class="font-medium">邮箱地址:</span>
|
||||
<span class="font-medium flex-shrink-0">邮箱地址:</span>
|
||||
<span>{{ userStore.userInfo.email || '未设置' }}</span>
|
||||
<el-button
|
||||
link
|
||||
@@ -115,7 +121,9 @@
|
||||
修改
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-gray-600 dark:text-gray-300">
|
||||
<div
|
||||
class="flex items-center gap-1 lg:gap-3 text-gray-600 dark:text-gray-300"
|
||||
>
|
||||
<el-icon class="text-purple-500"><lock /></el-icon>
|
||||
<span class="font-medium">账号密码:</span>
|
||||
<span>已设置</span>
|
||||
@@ -162,19 +170,35 @@
|
||||
</template>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 lg:gap-6 py-6">
|
||||
<div class="stat-card">
|
||||
<div class="text-2xl lg:text-4xl font-bold text-blue-500 mb-2">138</div>
|
||||
<div
|
||||
class="text-2xl lg:text-4xl font-bold text-blue-500 mb-2"
|
||||
>
|
||||
138
|
||||
</div>
|
||||
<div class="text-gray-500 text-sm">项目参与</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="text-2xl lg:text-4xl font-bold text-green-500 mb-2">2.3k</div>
|
||||
<div
|
||||
class="text-2xl lg:text-4xl font-bold text-green-500 mb-2"
|
||||
>
|
||||
2.3k
|
||||
</div>
|
||||
<div class="text-gray-500 text-sm">代码提交</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="text-2xl lg:text-4xl font-bold text-purple-500 mb-2">95%</div>
|
||||
<div
|
||||
class="text-2xl lg:text-4xl font-bold text-purple-500 mb-2"
|
||||
>
|
||||
95%
|
||||
</div>
|
||||
<div class="text-gray-500 text-sm">任务完成</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="text-2xl lg:text-4xl font-bold text-yellow-500 mb-2">12</div>
|
||||
<div
|
||||
class="text-2xl lg:text-4xl font-bold text-yellow-500 mb-2"
|
||||
>
|
||||
12
|
||||
</div>
|
||||
<div class="text-gray-500 text-sm">获得勋章</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -196,7 +220,9 @@
|
||||
:hollow="true"
|
||||
class="pb-6"
|
||||
>
|
||||
<h3 class="text-base font-medium mb-1">{{ activity.title }}</h3>
|
||||
<h3 class="text-base font-medium mb-1">
|
||||
{{ activity.title }}
|
||||
</h3>
|
||||
<p class="text-gray-500 text-sm">{{ activity.content }}</p>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
@@ -258,7 +284,7 @@
|
||||
<div class="flex gap-4">
|
||||
<el-input
|
||||
v-model="phoneForm.code"
|
||||
placeholder="请输入验证码"
|
||||
placeholder="请输入验证码[模拟]"
|
||||
class="flex-1"
|
||||
>
|
||||
<template #prefix>
|
||||
@@ -302,7 +328,7 @@
|
||||
<div class="flex gap-4">
|
||||
<el-input
|
||||
v-model="emailForm.code"
|
||||
placeholder="请输入验证码"
|
||||
placeholder="请输入验证码[模拟]"
|
||||
class="flex-1"
|
||||
>
|
||||
<template #prefix>
|
||||
@@ -332,11 +358,10 @@
|
||||
|
||||
<script setup>
|
||||
import { setSelfInfo, changePassword } from '@/api/user.js'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/pinia/modules/user'
|
||||
import ProfileAvatar from '@/components/Avatar/ProfileAvatar.vue'
|
||||
|
||||
import SelectImage from '@/components/selectImage/selectImage.vue'
|
||||
defineOptions({
|
||||
name: 'Person'
|
||||
})
|
||||
@@ -486,12 +511,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
const handleAvatarChange = async (newUrl) => {
|
||||
const res = await setSelfInfo({ headerImg: newUrl })
|
||||
watch(() => userStore.userInfo.headerImg, async(val) => {
|
||||
const res = await setSelfInfo({ headerImg: val })
|
||||
if (res.code === 0) {
|
||||
userStore.ResetUserInfo({ headerImg: newUrl })
|
||||
userStore.ResetUserInfo({ headerImg: val })
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '设置成功',
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 添加活动数据
|
||||
const activities = [
|
||||
|
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<warning-bar title="获取参数且缓存方法已在前端utils/params 已经封装完成 不必自己书写 使用方法查看文件内注释" />
|
||||
<div class="gva-search-box">
|
||||
<el-form
|
||||
ref="elSearchFormRef"
|
||||
@@ -231,7 +232,7 @@
|
||||
<p class="mb-2 text-sm text-gray-600">
|
||||
前端可以通过引入
|
||||
<code class="bg-blue-100 px-1 py-0.5 rounded"
|
||||
>import { getParams } from '@/utils/dictionary'</code
|
||||
>import { getParams } from '@/utils/params'</code
|
||||
>
|
||||
然后通过
|
||||
<code class="bg-blue-100 px-1 py-0.5 rounded"
|
||||
@@ -297,6 +298,7 @@
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ref, reactive } from 'vue'
|
||||
import WarningBar from "@/components/warningBar/warningBar.vue";
|
||||
|
||||
defineOptions({
|
||||
name: 'SysParams'
|
||||
|
@@ -115,5 +115,5 @@
|
||||
ElMessage.success('复制成功')
|
||||
}
|
||||
|
||||
defineExpose({ copy })
|
||||
defineExpose({ copy, selectText })
|
||||
</script>
|
||||
|
@@ -329,142 +329,159 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="3">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<el-tooltip
|
||||
content="注:会自动在结构体global.Model其中包含主键和软删除相关操作配置"
|
||||
placement="bottom"
|
||||
effect="light"
|
||||
>
|
||||
<div>
|
||||
使用GVA结构 <el-icon><QuestionFilled /></el-icon>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-checkbox v-model="form.gvaModel" @change="useGva" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="3">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<el-tooltip
|
||||
content="注:把自动生成的API注册进数据库"
|
||||
placement="bottom"
|
||||
effect="light"
|
||||
>
|
||||
<div>
|
||||
自动创建API <el-icon><QuestionFilled /></el-icon>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-checkbox v-model="form.autoCreateApiToSql" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="3">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<el-tooltip
|
||||
content="注:把自动生成的菜单注册进数据库"
|
||||
placement="bottom"
|
||||
effect="light"
|
||||
>
|
||||
<div>
|
||||
自动创建菜单 <el-icon><QuestionFilled /></el-icon>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-checkbox v-model="form.autoCreateMenuToSql" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="3">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<el-tooltip
|
||||
content="注:自动同步数据库表结构,如果不需要可以选择关闭。"
|
||||
placement="bottom"
|
||||
effect="light"
|
||||
>
|
||||
<div>
|
||||
同步表结构 <el-icon><QuestionFilled /></el-icon>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-checkbox v-model="form.autoMigrate" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="3">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<el-tooltip
|
||||
content="注:会自动产生页面内的按钮权限配置,若不在角色管理中进行按钮分配则按钮不可见"
|
||||
placement="bottom"
|
||||
effect="light"
|
||||
>
|
||||
<div>
|
||||
创建按钮权限 <el-icon><QuestionFilled /></el-icon>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-checkbox v-model="form.autoCreateBtnAuth" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="3">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<el-tooltip
|
||||
content="注:会自动在结构体添加 created_by updated_by deleted_by,方便用户进行资源权限控制"
|
||||
placement="bottom"
|
||||
effect="light"
|
||||
>
|
||||
<div>
|
||||
创建资源标识 <el-icon><QuestionFilled /></el-icon>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-checkbox v-model="form.autoCreateResource" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="3">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<el-tooltip
|
||||
content="注:使用基础模板将不会生成任何结构体和CURD,仅仅配置enter等属性方便自行开发非CURD逻辑"
|
||||
placement="bottom"
|
||||
effect="light"
|
||||
>
|
||||
<div>
|
||||
基础模板 <el-icon><QuestionFilled /></el-icon>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-checkbox v-model="form.onlyTemplate" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="9">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<el-tooltip
|
||||
content="注:会自动创建parentID来进行父子关系关联,仅支持主键为int类型"
|
||||
placement="bottom"
|
||||
effect="light"
|
||||
>
|
||||
<div>
|
||||
树型结构 <el-icon><QuestionFilled /></el-icon>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<div class="flex gap-2 items-center">
|
||||
<el-checkbox v-model="form.isTree" />
|
||||
<el-input v-model="form.treeJson" :disabled="!form.isTree" placeholder="前端展示json属性"></el-input>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="gva-search-box">
|
||||
<el-collapse class="no-border-collapse">
|
||||
<el-collapse-item>
|
||||
<template #title>
|
||||
<div class="text-lg text-gray-600 font-normal">
|
||||
专家模式
|
||||
</div>
|
||||
</template>
|
||||
<template #icon="{ isActive }">
|
||||
<span class="text-lg ml-auto mr-4 font-normal">
|
||||
{{ isActive ? '收起' : '展开' }}
|
||||
</span>
|
||||
</template>
|
||||
<div class="p-4">
|
||||
<!-- 基础设置组 -->
|
||||
<div class="border-b border-gray-200 last:border-0">
|
||||
<h3 class="text-lg font-medium mb-4 text-gray-700">基础设置</h3>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="3">
|
||||
<el-tooltip
|
||||
content="注:会自动在结构体global.Model其中包含主键和软删除相关操作配置"
|
||||
placement="top"
|
||||
effect="light"
|
||||
>
|
||||
<el-form-item label="使用GVA结构">
|
||||
<el-checkbox v-model="form.gvaModel" @change="useGva" />
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="3">
|
||||
<el-tooltip
|
||||
content="注:会自动产生页面内的按钮权限配置,若不在角色管理中进行按钮分配则按钮不可见"
|
||||
placement="top"
|
||||
effect="light"
|
||||
>
|
||||
<el-form-item label="创建按钮权限">
|
||||
<el-checkbox :disabled="!form.generateWeb" v-model="form.autoCreateBtnAuth" />
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="3">
|
||||
<el-form-item label="生成前端">
|
||||
<el-checkbox v-model="form.generateWeb" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="3">
|
||||
<el-form-item label="生成后端">
|
||||
<el-checkbox v-model="form.generateServer" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 自动化设置组 -->
|
||||
<div class="border-b border-gray-200 last:border-0">
|
||||
<h3 class="text-lg font-medium mb-4 text-gray-700">自动化设置</h3>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="3">
|
||||
<el-tooltip
|
||||
content="注:把自动生成的API注册进数据库"
|
||||
placement="top"
|
||||
effect="light"
|
||||
>
|
||||
<el-form-item label="自动创建API">
|
||||
<el-checkbox :disabled="!form.generateServer" v-model="form.autoCreateApiToSql" />
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="3">
|
||||
<el-tooltip
|
||||
content="注:把自动生成的菜单注册进数据库"
|
||||
placement="top"
|
||||
effect="light"
|
||||
>
|
||||
<el-form-item label="自动创建菜单">
|
||||
<el-checkbox :disabled="!form.generateWeb" v-model="form.autoCreateMenuToSql" />
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="3">
|
||||
<el-tooltip
|
||||
content="注:自动同步数据库表结构,如果不需要可以选择关闭"
|
||||
placement="top"
|
||||
effect="light"
|
||||
>
|
||||
<el-form-item label="同步表结构">
|
||||
<el-checkbox :disabled="!form.generateServer" v-model="form.autoMigrate" />
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 高级设置组 -->
|
||||
<div class="border-b border-gray-200 last:border-0">
|
||||
<h3 class="text-lg font-medium mb-4 text-gray-700">高级设置</h3>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="3">
|
||||
<el-tooltip
|
||||
content="注:会自动在结构体添加 created_by updated_by deleted_by,方便用户进行资源权限控制"
|
||||
placement="top"
|
||||
effect="light"
|
||||
>
|
||||
<el-form-item label="创建资源标识">
|
||||
<el-checkbox v-model="form.autoCreateResource" />
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="3">
|
||||
<el-tooltip
|
||||
content="注:使用基础模板将不会生成任何结构体和CURD,仅仅配置enter等属性方便自行开发非CURD逻辑"
|
||||
placement="top"
|
||||
effect="light"
|
||||
>
|
||||
<el-form-item label="基础模板">
|
||||
<el-checkbox v-model="form.onlyTemplate" />
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 树形结构设置 -->
|
||||
<div class="last:pb-0">
|
||||
<h3 class="text-lg font-medium mb-4 text-gray-700">树形结构设置</h3>
|
||||
<el-row :gutter="20" align="middle">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="树型结构">
|
||||
<div class="flex items-center gap-4">
|
||||
<el-tooltip
|
||||
content="注:会自动创建parentID来进行父子关系关联,仅支持主键为int类型"
|
||||
placement="top"
|
||||
effect="light"
|
||||
>
|
||||
<el-checkbox v-model="form.isTree" />
|
||||
</el-tooltip>
|
||||
<el-input
|
||||
v-model="form.treeJson"
|
||||
:disabled="!form.isTree"
|
||||
placeholder="前端展示json属性"
|
||||
class="flex-1"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
<!-- 组件列表 -->
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list">
|
||||
@@ -929,6 +946,10 @@
|
||||
for (let key in json) {
|
||||
form.value[key] = json[key]
|
||||
}
|
||||
|
||||
form.value.generateServer = true
|
||||
form.value.generateWeb = true
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1118,6 +1139,8 @@
|
||||
autoCreateResource: false,
|
||||
onlyTemplate: false,
|
||||
isTree: false,
|
||||
generateWeb:true,
|
||||
generateServer:true,
|
||||
treeJson: "",
|
||||
fields: []
|
||||
})
|
||||
@@ -1239,6 +1262,13 @@
|
||||
})
|
||||
return false
|
||||
}
|
||||
if(!form.value.generateWeb && !form.value.generateServer){
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '请至少选择一个生成项'
|
||||
})
|
||||
return false
|
||||
}
|
||||
if (!form.value.onlyTemplate) {
|
||||
if (form.value.fields.length <= 0) {
|
||||
ElMessage({
|
||||
@@ -1403,6 +1433,8 @@
|
||||
form.value.abbreviation = toLowerCase(tbHump)
|
||||
form.value.description = tbHump + '表'
|
||||
form.value.autoCreateApiToSql = true
|
||||
form.value.generateServer = true
|
||||
form.value.generateWeb = true
|
||||
form.value.fields = []
|
||||
res.data.columns &&
|
||||
res.data.columns.forEach((item) => {
|
||||
@@ -1520,6 +1552,20 @@
|
||||
}
|
||||
)
|
||||
|
||||
watch(()=>form.value.generateServer,()=>{
|
||||
if(!form.value.generateServer){
|
||||
form.value.autoCreateApiToSql = false
|
||||
form.value.autoMigrate = false
|
||||
}
|
||||
})
|
||||
|
||||
watch(()=>form.value.generateWeb,()=>{
|
||||
if(!form.value.generateWeb){
|
||||
form.value.autoCreateMenuToSql = false
|
||||
form.value.autoCreateBtnAuth = false
|
||||
}
|
||||
})
|
||||
|
||||
const catchData = () => {
|
||||
window.sessionStorage.setItem('autoCode', JSON.stringify(form.value))
|
||||
}
|
||||
@@ -1607,3 +1653,18 @@
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.no-border-collapse{
|
||||
@apply border-none;
|
||||
.el-collapse-item__header{
|
||||
@apply border-none;
|
||||
}
|
||||
.el-collapse-item__wrap{
|
||||
@apply border-none;
|
||||
}
|
||||
.el-collapse-item__content{
|
||||
@apply pb-0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
Reference in New Issue
Block a user