* 媒体库增加分类,图库多选择时优化。

* 重构 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:
PiexlMax(奇淼
2025-02-13 15:25:10 +08:00
committed by GitHub
parent ea39d83907
commit d40c815760
61 changed files with 2288 additions and 1529 deletions

View 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
})
}

View File

@@ -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>

View File

@@ -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)
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)

View File

@@ -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
}
})

View File

@@ -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",

View File

@@ -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()

View 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
}
})

View File

@@ -21,7 +21,7 @@ const routes = [
closeTab: true
},
component: () => import('@/view/error/index.vue')
}
},
]
const router = createRouter({

View File

@@ -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
View 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]
}

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 = () => {

View File

@@ -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 })
}

View File

@@ -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 })
}

View 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>

View File

@@ -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()

View File

@@ -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 = [

View File

@@ -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'

View File

@@ -115,5 +115,5 @@
ElMessage.success('复制成功')
}
defineExpose({ copy })
defineExpose({ copy, selectText })
</script>

View File

@@ -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>