资源管理
This commit is contained in:
@@ -79,6 +79,8 @@ report:
|
|||||||
dateFormat: yyyy年MM月dd日
|
dateFormat: yyyy年MM月dd日
|
||||||
data:
|
data:
|
||||||
homeDir: {{APP_DATA_PATH}}\data
|
homeDir: {{APP_DATA_PATH}}\data
|
||||||
|
resource:
|
||||||
|
videoDir: {{APP_DATA_PATH}}\resources\videos
|
||||||
qr:
|
qr:
|
||||||
cloud: http://pqmcc.com:18082/api/file
|
cloud: http://pqmcc.com:18082/api/file
|
||||||
dev:
|
dev:
|
||||||
|
|||||||
14
frontend/src/api/resourceManage/index.ts
Normal file
14
frontend/src/api/resourceManage/index.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import http from '@/api'
|
||||||
|
import type { ResourceManage } from '@/api/resourceManage/interface'
|
||||||
|
|
||||||
|
export const getResourceManageList = (params: ResourceManage.ReqResourceManageParams) => {
|
||||||
|
return http.post<ResourceManage.ResResourceManagePage>('/resourceManage/list', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addResourceManage = (params: FormData) => {
|
||||||
|
return http.upload('/resourceManage/add', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getResourceManagePlayUrl = (id: string) => {
|
||||||
|
return http.get<ResourceManage.PlayVO>(`/resourceManage/play?id=${id}`)
|
||||||
|
}
|
||||||
28
frontend/src/api/resourceManage/interface/index.ts
Normal file
28
frontend/src/api/resourceManage/interface/index.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import type { ReqPage, ResPage } from '@/api/interface'
|
||||||
|
|
||||||
|
export namespace ResourceManage {
|
||||||
|
export interface ReqResourceManageParams extends ReqPage {
|
||||||
|
name?: string
|
||||||
|
fileName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResResourceManage {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
fileName: string
|
||||||
|
fileSize: number
|
||||||
|
relativePath: string
|
||||||
|
remark: string
|
||||||
|
state: number
|
||||||
|
createBy?: string | null
|
||||||
|
createTime?: string | null
|
||||||
|
updateBy?: string | null
|
||||||
|
updateTime?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResResourceManagePage extends ResPage<ResResourceManage> {}
|
||||||
|
|
||||||
|
export interface PlayVO {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
title="新增资源"
|
||||||
|
width="520px"
|
||||||
|
:destroy-on-close="true"
|
||||||
|
:close-on-click-modal="!submitting"
|
||||||
|
draggable
|
||||||
|
>
|
||||||
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
|
||||||
|
<el-form-item label="资源名称" prop="name">
|
||||||
|
<el-input v-model="form.name" maxlength="250" placeholder="请输入资源名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注" prop="remark">
|
||||||
|
<el-input v-model="form.remark" maxlength="250" placeholder="请输入备注" type="textarea" :rows="3" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="文件" prop="file">
|
||||||
|
<el-upload
|
||||||
|
ref="uploadRef"
|
||||||
|
action="#"
|
||||||
|
:auto-upload="false"
|
||||||
|
:limit="1"
|
||||||
|
accept=".mp4,video/mp4"
|
||||||
|
:file-list="fileList"
|
||||||
|
:on-change="handleFileChange"
|
||||||
|
:on-remove="handleFileRemove"
|
||||||
|
:on-exceed="handleExceed"
|
||||||
|
>
|
||||||
|
<el-button type="primary" :icon="Upload">选择文件</el-button>
|
||||||
|
<template #tip>
|
||||||
|
<div class="el-upload__tip">仅支持 MP4 文件,最大 250MB</div>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button :disabled="submitting" @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="submit">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref } from 'vue'
|
||||||
|
import {
|
||||||
|
ElMessage,
|
||||||
|
genFileId,
|
||||||
|
type FormInstance,
|
||||||
|
type FormRules,
|
||||||
|
type UploadFile,
|
||||||
|
type UploadInstance,
|
||||||
|
type UploadProps,
|
||||||
|
type UploadRawFile
|
||||||
|
} from 'element-plus'
|
||||||
|
import { Upload } from '@element-plus/icons-vue'
|
||||||
|
import { addResourceManage } from '@/api/resourceManage'
|
||||||
|
|
||||||
|
const MAX_FILE_SIZE = 250 * 1024 * 1024
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
refreshTable?: () => void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const uploadRef = ref<UploadInstance>()
|
||||||
|
const fileList = ref<UploadFile[]>([])
|
||||||
|
|
||||||
|
const form = reactive<{
|
||||||
|
name: string
|
||||||
|
remark: string
|
||||||
|
file: File | null
|
||||||
|
}>({
|
||||||
|
name: '',
|
||||||
|
remark: '',
|
||||||
|
file: null
|
||||||
|
})
|
||||||
|
|
||||||
|
const validateFile = (_rule: unknown, value: File | null, callback: (error?: Error) => void) => {
|
||||||
|
if (!value) {
|
||||||
|
callback(new Error('请选择 MP4 文件'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
|
||||||
|
const rules = reactive<FormRules>({
|
||||||
|
name: [{ required: true, message: '请输入资源名称', trigger: 'blur' }],
|
||||||
|
remark: [{ required: true, message: '请输入备注', trigger: 'blur' }],
|
||||||
|
file: [{ validator: validateFile, trigger: 'change' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
const open = () => {
|
||||||
|
form.name = ''
|
||||||
|
form.remark = ''
|
||||||
|
form.file = null
|
||||||
|
fileList.value = []
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidMp4 = (file: File) => {
|
||||||
|
return file.name.toLowerCase().endsWith('.mp4') && (!file.type || file.type === 'video/mp4')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileChange: UploadProps['onChange'] = uploadFile => {
|
||||||
|
const raw = uploadFile.raw
|
||||||
|
if (!raw) return
|
||||||
|
if (!isValidMp4(raw)) {
|
||||||
|
ElMessage.error('仅支持上传 MP4 文件')
|
||||||
|
fileList.value = []
|
||||||
|
form.file = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (raw.size > MAX_FILE_SIZE) {
|
||||||
|
ElMessage.error('文件大小不能超过 250MB')
|
||||||
|
fileList.value = []
|
||||||
|
form.file = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fileList.value = [uploadFile]
|
||||||
|
form.file = raw
|
||||||
|
formRef.value?.validateField('file')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileRemove = () => {
|
||||||
|
form.file = null
|
||||||
|
fileList.value = []
|
||||||
|
formRef.value?.validateField('file')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleExceed: UploadProps['onExceed'] = files => {
|
||||||
|
uploadRef.value?.clearFiles()
|
||||||
|
const file = files[0] as UploadRawFile
|
||||||
|
file.uid = genFileId()
|
||||||
|
uploadRef.value?.handleStart(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
if (!formRef.value) return
|
||||||
|
await formRef.value.validate()
|
||||||
|
if (!form.file) return
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('name', form.name.trim())
|
||||||
|
formData.append('remark', form.remark.trim())
|
||||||
|
formData.append('file', form.file)
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
await addResourceManage(formData)
|
||||||
|
ElMessage.success('新增成功')
|
||||||
|
dialogVisible.value = false
|
||||||
|
props.refreshTable?.()
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
open
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="dialogVisible" title="播放视频" width="820px" :destroy-on-close="true" @closed="clearVideo">
|
||||||
|
<video ref="videoRef" class="resource-player" :src="videoUrl" controls autoplay />
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { nextTick, ref } from 'vue'
|
||||||
|
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const videoUrl = ref('')
|
||||||
|
const videoRef = ref<HTMLVideoElement>()
|
||||||
|
|
||||||
|
const open = async (url: string) => {
|
||||||
|
videoUrl.value = url
|
||||||
|
dialogVisible.value = true
|
||||||
|
await nextTick()
|
||||||
|
videoRef.value?.load()
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearVideo = () => {
|
||||||
|
if (videoRef.value) {
|
||||||
|
videoRef.value.pause()
|
||||||
|
videoRef.value.removeAttribute('src')
|
||||||
|
videoRef.value.load()
|
||||||
|
}
|
||||||
|
videoUrl.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
open
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.resource-player {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 68vh;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
126
frontend/src/views/resourceManage/index.vue
Normal file
126
frontend/src/views/resourceManage/index.vue
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<template>
|
||||||
|
<div class="table-box">
|
||||||
|
<ProTable ref="proTable" :columns="columns" :request-api="getTableList">
|
||||||
|
<template #tableHeader>
|
||||||
|
<el-button v-auth.resourceManage="'add'" type="primary" :icon="CirclePlus" @click="openAddDialog">
|
||||||
|
新增
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
<template #operation="scope">
|
||||||
|
<el-button
|
||||||
|
v-auth.resourceManage="'play'"
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
:icon="VideoPlay"
|
||||||
|
@click="handlePlay(scope.row)"
|
||||||
|
>
|
||||||
|
播放
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</ProTable>
|
||||||
|
</div>
|
||||||
|
<ResourceManagePopup ref="resourceManagePopup" :refresh-table="proTable?.getTableList" />
|
||||||
|
<ResourcePlayerDialog ref="resourcePlayerDialog" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="tsx" name="resourceManage">
|
||||||
|
import { reactive, ref } from 'vue'
|
||||||
|
import { CirclePlus, VideoPlay } from '@element-plus/icons-vue'
|
||||||
|
import ProTable from '@/components/ProTable/index.vue'
|
||||||
|
import type { ColumnProps, ProTableInstance } from '@/components/ProTable/interface'
|
||||||
|
import type { ResourceManage } from '@/api/resourceManage/interface'
|
||||||
|
import { getResourceManageList, getResourceManagePlayUrl } from '@/api/resourceManage'
|
||||||
|
import ResourceManagePopup from './components/resourceManagePopup.vue'
|
||||||
|
import ResourcePlayerDialog from './components/resourcePlayerDialog.vue'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'resourceManage'
|
||||||
|
})
|
||||||
|
|
||||||
|
const proTable = ref<ProTableInstance>()
|
||||||
|
const resourceManagePopup = ref()
|
||||||
|
const resourcePlayerDialog = ref()
|
||||||
|
|
||||||
|
const getTableList = async (params: ResourceManage.ReqResourceManageParams) => {
|
||||||
|
return getResourceManageList(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatFileSize = (size?: number) => {
|
||||||
|
if (!size && size !== 0) return ''
|
||||||
|
if (size < 1024) return `${size} B`
|
||||||
|
const kb = size / 1024
|
||||||
|
if (kb < 1024) return `${kb.toFixed(2)} KB`
|
||||||
|
return `${(kb / 1024).toFixed(2)} MB`
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDateTime = (value?: string | null) => {
|
||||||
|
if (!value) return ''
|
||||||
|
const date = new Date(value)
|
||||||
|
if (Number.isNaN(date.getTime())) return value
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeStreamUrl = (url: string) => {
|
||||||
|
if (/^https?:\/\//i.test(url)) return url
|
||||||
|
const baseUrl = import.meta.env.VITE_API_URL as string
|
||||||
|
const normalizedBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl
|
||||||
|
const normalizedUrl = url.startsWith('/') ? url : `/${url}`
|
||||||
|
return `${normalizedBase}${normalizedUrl}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = reactive<ColumnProps<ResourceManage.ResResourceManage>[]>([
|
||||||
|
{ type: 'index', fixed: 'left', width: 70, label: '序号' },
|
||||||
|
{
|
||||||
|
prop: 'name',
|
||||||
|
label: '资源名称',
|
||||||
|
minWidth: 160,
|
||||||
|
search: { el: 'input' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'fileName',
|
||||||
|
label: '文件名',
|
||||||
|
minWidth: 220,
|
||||||
|
search: { el: 'input' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'fileSize',
|
||||||
|
label: '文件大小',
|
||||||
|
width: 120,
|
||||||
|
render: scope => formatFileSize(scope.row.fileSize)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'relativePath',
|
||||||
|
label: '路径',
|
||||||
|
minWidth: 260,
|
||||||
|
showOverflowTooltip: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'remark',
|
||||||
|
label: '备注',
|
||||||
|
minWidth: 180,
|
||||||
|
showOverflowTooltip: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'createTime',
|
||||||
|
label: '上传时间',
|
||||||
|
width: 180,
|
||||||
|
render: scope => formatDateTime(scope.row.createTime)
|
||||||
|
},
|
||||||
|
{ prop: 'operation', label: '操作', fixed: 'right', width: 100 }
|
||||||
|
])
|
||||||
|
|
||||||
|
const openAddDialog = () => {
|
||||||
|
resourceManagePopup.value?.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePlay = async (row: ResourceManage.ResResourceManage) => {
|
||||||
|
const { data } = await getResourceManagePlayUrl(row.id)
|
||||||
|
resourcePlayerDialog.value?.open(normalizeStreamUrl(data.url))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user