feat 增加文件平台相关功能

This commit is contained in:
DaxPay
2024-08-13 12:31:56 +08:00
parent 7d8ee6863e
commit 891e2aae6b
14 changed files with 675 additions and 41 deletions

View File

@@ -1,40 +1,11 @@
import { defHttp } from '@/utils/http/axios'
import { Result, UploadFileParams } from '#/axios'
import { UploadFileParams } from '#/axios'
import { UploadApiResult } from '@/api/sys/model/uploadModel'
import { getAppEnvConfig } from '@/utils/env'
import { AxiosProgressEvent } from 'axios'
const { VITE_GLOB_API_URL, VITE_GLOB_API_URL_PREFIX } = getAppEnvConfig()
/**
* 获取文件预览地址
*/
export const getFilePreviewUrl = (id) => {
return defHttp.get<Result<string>>({
url: `/file/getFilePreviewUrl`,
params: { id },
})
}
/**
* 获取文件预览地址前缀
*/
export const getFilePreviewUrlPrefix = () => {
return defHttp.get<Result<string>>({
url: `/file/getFilePreviewUrlPrefix`,
})
}
/**
* 获取文件下载地址
*/
export const getFileDownloadUrl = (id) => {
return defHttp.get<Result<string>>({
url: `/file/getFileDownloadUrl`,
params: { id },
})
}
/**
* 上传文件
* @param params

View File

@@ -58,6 +58,8 @@ async function dictDropDownNumber(dictCode: string): Promise<LabeledValue[]> {
* 字典hooks
*/
export function useDict() {
// 初始化
getDict().then()
return {
dictConvert,
dictDropDown,

View File

@@ -0,0 +1,44 @@
import { FilePlatform } from '#/store'
import { useFilePlatformStore } from '@/store/modules/filePlatform'
const platformStore = useFilePlatformStore()
/**
* 获取存储平台列表
*/
async function getFilePlatforms(): Promise<FilePlatform[]> {
const filePlatform = platformStore.getFilePlatforms
if (filePlatform.length > 0) {
return filePlatform
} else {
return await platformStore.initFilePlatform()
}
}
/**
* 获取地址
* @param fileUrl 文件保存的url地址
* @param platform 存储平台类型编码
*/
function getFileUrl(fileUrl?: string, platform?: string) {
const platforms = platformStore.getFilePlatforms
const item = platforms.filter((o) => {
return platform ? platform === o.type : o.defaultPlatform
})
if (item && item.length > 0) {
return item[0].url + fileUrl
} else {
return ''
}
}
/**
* 存储平台hooks
*/
export function useFilePlatform() {
// 初始化
getFilePlatforms().then()
return {
getFileUrl,
}
}

View File

@@ -21,6 +21,7 @@ export function useUpload(uploadUrl: string) {
const uploadAction = computed(() => {
return VITE_GLOB_API_URL + VITE_GLOB_API_URL_PREFIX + uploadUrl
})
return {
tokenHeader,
uploadAction,

View File

@@ -30,6 +30,7 @@
import { propTypes } from '@/utils/propTypes'
import { openWindow } from '@/utils'
import { createAsyncComponent } from '@/utils/factory/createAsyncComponent'
import { useFilePlatform } from '@/hooks/bootx/useFilePlatform'
type MenuEvent = 'logout' | 'doc' | 'lock' | 'api'
@@ -44,9 +45,11 @@
const { prefixCls } = useDesign('header-user-dropdown')
const { getShowDoc } = useHeaderSetting()
const userStore = useUserStore()
const { getFileUrl } = useFilePlatform()
const getUserInfo = computed(() => {
const { name = '', avatar } = userStore.getUserInfo || {}
let { name = '', avatar } = userStore.getUserInfo || {}
avatar = avatar ? getFileUrl(avatar) : ''
return { name, avatar: avatar || headerImg }
})

View File

@@ -35,10 +35,3 @@ export const useDictStore = defineStore({
},
},
})
// Need to be used outside the setup
export function useDictStoreWithOut() {
return useDictStore(store)
}
// 初始化字典
useDictStoreWithOut().initDict().then()

View File

@@ -0,0 +1,35 @@
import { defineStore } from 'pinia'
import { FilePlatform } from '#/store'
import { findAll } from '@/views/baseapi/file/platform/FilePlatform.api'
interface FilePlatformState {
filePlatform: FilePlatform[]
}
export const useFilePlatformStore = defineStore({
id: 'app-file-platform',
state: (): FilePlatformState => ({
filePlatform: [],
}),
getters: {
getFilePlatforms(): FilePlatform[] {
return this.filePlatform
},
},
actions: {
/**
* 初始化存储平台, 并列表
*/
async initFilePlatform() {
const { data } = await findAll()
this.filePlatform = data.map((o) => {
return {
type: o.type,
url: o.url,
defaultPlatform: o.defaultPlatform,
} as FilePlatform
})
console.log('初始化存储平台')
return this.filePlatform
},
},
})

View File

@@ -8,7 +8,6 @@ import { doLogout, login } from '@/api/sys/login'
import { useMessage } from '@/hooks/web/useMessage'
import { router } from '@/router'
import { h } from 'vue'
import { getFilePreviewUrlPrefix } from '@/api/common/FileUpload'
import { getUserInfo } from '@/api/sys/user'
import { userLogoutAction } from '@/store/action/userAction'
import { TOKEN_KEY, USER_INFO_KEY } from '@/enums/cacheEnum'
@@ -81,8 +80,6 @@ export const useUserStore = defineStore({
if (!this.getToken) return null
const { data: userInfo } = await getUserInfo()
// 设置头像
const { data: urlPrefix } = await getFilePreviewUrlPrefix()
userInfo.avatar = userInfo.avatar ? urlPrefix + userInfo.avatar : ''
this.setUserInfo(userInfo as any)
},
/**

View File

@@ -0,0 +1,54 @@
import { defHttp } from '@/utils/http/axios'
import { PageResult, Result } from '#/axios'
import { BaseEntity } from '#/web'
/**
* 查询列表
*/
export function findAll() {
return defHttp.get<Result<FilePlatform[]>>({
url: '/file/platform/findAll',
})
}
/**
* 更新文件存储平台地址
*/
export function updateUrl(id: string, url: string) {
return defHttp.post<Result<FilePlatform>>({
url: '/file/platform/updateUrl',
data: { id, url },
})
}
/**
* 设置默认存储平台地址
*/
export function setDefault(id: string) {
return defHttp.post<Result<FilePlatform>>({
url: '/file/platform/setDefault',
params: { id },
})
}
/**
* 文件存储平台
*/
export interface FilePlatform extends BaseEntity {
/**
* 文件存储平台名称
*/
name: string
/**
* 文件存储平台类型
*/
type: string
/**
* 访问地址
*/
url?: string
/**
* 默认平台
*/
defaultPlatform: boolean
}

View File

@@ -0,0 +1,183 @@
<template>
<div>
<div class="m-3 p-3 pt-5 bg-white">
<b-query
:query-params="model.queryParam"
:fields="fields"
@query="queryPage"
@reset="resetQueryParams"
/>
</div>
<div class="m-3 p-3 bg-white">
<vxe-toolbar ref="xToolbar" custom :refresh="{ queryMethod: queryPage }" />
<div class="h-70vh">
<vxe-table
height="auto"
row-id="id"
ref="xTable"
keep-source
:data="records"
:loading="loading"
:edit-config="{ trigger: 'click', mode: 'cell', showStatus: true }"
>
>
<vxe-column type="seq" width="60" />
<vxe-column field="defaultPlatform" title="默认平台" :width="100">
<template #default="{ row }">
<template v-if="row.defaultPlatform">
<a-tag color="red">默认</a-tag>
</template>
<template v-if="!row.defaultPlatform">
<a-button @click="setDef(row.id)" size="small" type="primary" :loading="row.loading"
>设为默认</a-button
>
</template>
</template>
</vxe-column>
<vxe-column field="type" title="类型" :width="80" />
<vxe-column field="name" title="名称" :width="120" />
<vxe-column field="url" title="平台地址" :min-width="550" :edit-render="{}">
<template #edit="scope">
<a-input v-model:value="scope.row.url" @change="updateRowStatus(scope)" />
</template>
</vxe-column>
<vxe-column fixed="right" :width="120" :showOverflow="false" title="操作">
<template #default="{ row }">
<template v-if="hasUpdateStatus(row)">
<a-space>
<a-button
@click="saveUpdateEvent(row)"
size="small"
type="primary"
:loading="row.loading"
>保存</a-button
>
<a-button @click="revertRow(row)" size="small" :loading="row.loading"
>取消</a-button
>
</a-space>
</template>
</template>
</vxe-column>
</vxe-table>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { FilePlatform, findAll, setDefault, updateUrl } from './FilePlatform.api'
import useTablePage from '@/hooks/bootx/useTablePage'
import { VxeTableInstance, VxeToolbarInstance } from 'vxe-table'
import BQuery from '/@/components/Bootx/Query/BQuery.vue'
import { QueryField } from '@/components/Bootx/Query/Query'
import { useMessage } from '@/hooks/web/useMessage'
// 使用hooks
const { resetQueryParams, model, loading } = useTablePage(queryPage)
const { createMessage, createConfirm } = useMessage()
const records = ref<FilePlatform[]>([])
// 查询条件
const fields = [
{ field: 'name', type: 'string', name: '文件名称', placeholder: '请输入文件名称' },
] as QueryField[]
const xTable = ref<VxeTableInstance>()
const xToolbar = ref<VxeToolbarInstance>()
onMounted(() => {
vxeBind()
queryPage()
})
function vxeBind() {
xTable.value?.connect(xToolbar.value as VxeToolbarInstance)
}
/**
* 查询
*/
function queryPage() {
loading.value = true
findAll().then(({ data }) => {
records.value = data
loading.value = false
})
return Promise.resolve()
}
/**
* 更新行状态
*/
function updateRowStatus(params: any) {
const $table = xTable.value
if ($table) {
return $table.updateStatus(params)
}
}
/**
* 判断是否编辑
*/
function hasUpdateStatus(row) {
const $table = xTable.value
if ($table) {
return $table.isUpdateByRow(row)
}
}
/**
* 取消编辑
*/
function revertRow(row) {
const $table = xTable.value
if ($table) {
return $table.revertData(row)
}
}
/**
* 保存编辑
*/
function saveUpdateEvent(row) {
const $table = xTable.value
if ($table) {
if ($table.isUpdateByRow(row)) {
createConfirm({
iconType: 'warning',
title: '警告',
content: '是否更新平台地址?',
onOk: () => {
updateUrl(row.id, row.url).then(() => {
createMessage.success('更新成功')
// 保存完成后将行恢复到初始状态
$table.reloadRow(row, {})
// queryPage()
})
},
})
}
}
}
/**
* 设置为默认平台
*/
function setDef(id) {
createConfirm({
iconType: 'warning',
title: '警告',
content: '注意: 设置默认存储平台,只会影响文件的默认的预览和下载,不会对上传产生影响!',
onOk: () => {
loading.value = true
setDefault(id).then(() => {
createMessage.success('设置成功')
queryPage()
})
},
})
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,113 @@
import { defHttp } from '@/utils/http/axios'
import { PageResult, Result } from '#/axios'
/**
* 分页
*/
export const page = (params) => {
return defHttp.get<Result<PageResult<UpdateFileInfo>>>({
url: '/file/page',
params,
})
}
/**
* 获取单条
*/
export const get = (id) => {
return defHttp.get<Result<UpdateFileInfo>>({
url: '/file/findById',
params: { id },
})
}
/**
* 添加
*/
export const add = (obj: UpdateFileInfo) => {
return defHttp.post({
url: '/file/add',
data: obj,
})
}
/**
* 更新
*/
export const update = (obj: UpdateFileInfo) => {
return defHttp.post({
url: '/file/update',
data: obj,
})
}
/**
* 删除
*/
export const del = (id) => {
return defHttp.delete({
url: '/file/delete',
params: { id },
})
}
/**
* 查询全部
*/
export const findAll = () => {
return defHttp.get<Result<UpdateFileInfo>>({
url: '/file/findAll',
})
}
/**
* 上传文件信息
*/
export interface UpdateFileInfo {
// id
id?: number
// 文件访问地址
url?: string
// 文件大小,单位字节
size?: string
// 文件名称
filename?: string
// 原始文件名
originalFilename?: string
// 基础存储路径
basePath?: string
// 存储路径
path?: string
// 文件扩展名
ext?: string
// MIME类型
contentType?: string
// 存储平台
platform?: string
// 缩略图访问路径
thUrl?: string
// 缩略图名称
thFilename?: string
// 缩略图大小,单位字节
thSize?: string
// 缩略图MIME类型
thContentType?: string
// 文件所属对象id
objectId?: string
// 文件所属对象类型,例如用户头像,评价图片
objectType?: string
// 文件元数据
metadata?: string
// 文件用户元数据
userMetadata?: string
// 缩略图元数据
thMetadata?: string
// 缩略图用户元数据
thUserMetadata?: string
// 附加属性
attr?: string
// 文件ACL
fileAcl?: string
// 缩略图文件ACL
thFileAcl?: string
}

View File

@@ -0,0 +1,49 @@
<template>
<basic-modal
v-bind="$attrs"
:loading="confirmLoading"
:width="1000"
title="查看"
:open="visible"
@cancel="visible = false"
>
<description :column="1" :data="data" :schema="schema" />
<template #footer>
<a-button key="cancel" @click="visible = false">取消</a-button>
</template>
</basic-modal>
</template>
<script setup lang="ts">
import BasicModal from '/@/components/Modal/src/BasicModal.vue'
import { DescItem, Description } from '@/components/Description'
import { get, UpdateFileInfo } from './FileUpload.api'
import { ref } from 'vue'
let data = ref<UpdateFileInfo>({})
let confirmLoading = ref<boolean>(false)
let visible = ref<boolean>(false)
let schema = [
{ field: 'id', label: '主键' },
{ field: 'originalFilename', label: '原始文件名' },
{ field: 'filename', label: '文件储存名称' },
{ field: 'url', label: '存储和访问路径' },
{ field: 'ext', label: '文件扩展名' },
{ field: 'contentType', label: 'MIME类型' },
{ field: 'platform', label: '存储平台' },
{ field: 'createTime', label: '创建时间' },
] as DescItem[]
/**
* 初始化数据
*/
function init(id) {
visible.value = true
get(id).then((res) => {
data.value = res.data
})
}
defineExpose({ init })
</script>
<style scoped lang="less"></style>

View File

@@ -0,0 +1,180 @@
<template>
<div>
<div class="m-3 p-3 pt-5 bg-white">
<b-query
:query-params="model.queryParam"
:fields="fields"
@query="queryPage"
@reset="resetQueryParams"
/>
</div>
<div class="m-3 p-3 bg-white">
<vxe-toolbar ref="xToolbar" custom :refresh="{ queryMethod: queryPage }">
<template #buttons>
<a-upload
name="file"
:multiple="false"
:action="uploadAction"
:headers="tokenHeader"
:showUploadList="false"
@change="handleChange"
>
<a-button preIcon="ant-design:cloud-upload-outlined" type="primary">
文件上传
</a-button>
</a-upload>
</template>
</vxe-toolbar>
<div class="h-65vh">
<vxe-table
height="auto"
row-id="id"
ref="xTable"
:data="pagination.records"
:loading="loading"
>
<vxe-column type="seq" :width="60" />
<vxe-column field="originalFilename" title="原始文件名" :min-width="180">
<template #default="{ row }">
<a-link @click="show(row)">{{ row.originalFilename }}</a-link>
</template>
</vxe-column>
<vxe-column field="ext" title="扩展名" :min-width="50" />
<vxe-column field="contentType" title="文件类型" :min-width="100" />
<vxe-column field="platform" title="存储平台" :min-width="70" />
<vxe-column field="fileSize" title="文件大小" :min-width="120" />
<vxe-column field="createTime" title="创建时间" :min-width="170" />
<vxe-column fixed="right" :width="170" :showOverflow="false" title="操作">
<template #default="{ row }">
<a href="javascript:" @click="show(row)">查看</a>
<a-divider type="vertical" />
<a href="javascript:" @click="down(row)">下载</a>
<a-divider type="vertical" />
<a href="javascript:" style="color: red" @click="remove(row)">删除</a>
</template>
</vxe-column>
</vxe-table>
</div>
<vxe-pager
size="medium"
:loading="loading"
:current-page="pagination.current"
:page-size="pagination.size"
:total="pagination.total"
@page-change="handleTableChange"
/>
</div>
<FileUploadInfo ref="fileUploadInfo" />
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import { page, del, UpdateFileInfo } from './FileUpload.api'
import useTablePage from '@/hooks/bootx/useTablePage'
import { VxeTableInstance, VxeToolbarInstance } from 'vxe-table'
import { useMessage } from '@/hooks/web/useMessage'
import { QueryField } from '@/components/Bootx/Query/Query'
import { useUpload } from '@/hooks/bootx/useUpload'
import FileUploadInfo from './FileUploadInfo.vue'
import BQuery from '@/components/Bootx/Query/BQuery.vue'
import { useFilePlatform } from '@/hooks/bootx/useFilePlatform'
import ALink from '@/components/Link/Link.vue'
// 使用hooks
const {
handleTableChange,
pageQueryResHandel,
resetQueryParams,
pagination,
pages,
model,
loading,
} = useTablePage(queryPage)
const { createMessage, createConfirm } = useMessage()
const { tokenHeader, uploadAction } = useUpload('/file/upload')
const { getFileUrl } = useFilePlatform()
// 查询条件
const fields = computed(() => {
return [{}] as QueryField[]
})
const xTable = ref<VxeTableInstance>()
const xToolbar = ref<VxeToolbarInstance>()
const fileUploadInfo = ref<any>()
onMounted(() => {
vxeBind()
queryPage()
})
function vxeBind() {
xTable.value?.connect(xToolbar.value as VxeToolbarInstance)
}
/**
* 上传完成回调
*/
function handleChange(info) {
if (info.file.status === 'done') {
if (!info.file.response.code) {
queryPage()
createMessage.success(`${info.file.name} 上传成功!`)
} else {
createMessage.error(`${info.file.response.msg}`)
}
} else if (info.file.status === 'error') {
createMessage.error('上传失败')
}
}
/**
* 分页查询
*/
function queryPage() {
loading.value = true
page({
...model.queryParam,
...pages,
}).then(({ data }) => {
pageQueryResHandel(data)
})
return Promise.resolve()
}
/**
* 查看
*/
function show(record) {
fileUploadInfo.value.init(record.id)
}
/**
* 下载
*/
function down(record: UpdateFileInfo) {
const url = getFileUrl(record.url, record.platform)
window.open(url)
}
/**
* 删除
*/
function remove(record) {
createConfirm({
iconType: 'warning',
title: '删除',
content: '是否删除该文件',
onOk: () => {
loading.value = true
del(record.id).then(() => {
createMessage.success('删除成功')
queryPage()
})
},
})
}
</script>
<style lang="less" scoped></style>

9
types/store.d.ts vendored
View File

@@ -60,3 +60,12 @@ export interface Dict {
code: string
name: string
}
/**
* 存储平台
*/
export interface FilePlatform {
type: string
url: string
defaultPlatform: boolean
}