增加专家在平板上查看评审资料功能,前端页面为单独入口url:project/review-tablet
parent
ca0cbe2ac9
commit
8f893c0afd
|
|
@ -19,8 +19,11 @@ export interface ReviewProjectItemVO {
|
||||||
export interface ReviewMeetingSaveReqVO {
|
export interface ReviewMeetingSaveReqVO {
|
||||||
id?: number
|
id?: number
|
||||||
name: string
|
name: string
|
||||||
startTime: string | number
|
startTime?: string | number
|
||||||
endTime: string | number
|
endTime?: string | number
|
||||||
|
materialViewStartTime?: string | number
|
||||||
|
materialViewEndTime?: string | number
|
||||||
|
materialViewRemark?: string
|
||||||
location: string
|
location: string
|
||||||
expertIds: number[]
|
expertIds: number[]
|
||||||
projects?: ReviewProjectItemVO[]
|
projects?: ReviewProjectItemVO[]
|
||||||
|
|
@ -41,6 +44,9 @@ export interface ReviewMeetingRespVO {
|
||||||
name: string
|
name: string
|
||||||
startTime: string
|
startTime: string
|
||||||
endTime: string
|
endTime: string
|
||||||
|
materialViewStartTime?: string
|
||||||
|
materialViewEndTime?: string
|
||||||
|
materialViewRemark?: string
|
||||||
location: string
|
location: string
|
||||||
status: number // 0-草稿 1-已邀约 2-已结束 3-已取消
|
status: number // 0-草稿 1-已邀约 2-已结束 3-已取消
|
||||||
expertIds: number[]
|
expertIds: number[]
|
||||||
|
|
@ -111,4 +117,3 @@ export const importProjectsFromExcel = (file: File) => {
|
||||||
/** 下载导入模板 */
|
/** 下载导入模板 */
|
||||||
export const getImportTemplate = () =>
|
export const getImportTemplate = () =>
|
||||||
request.download({ url: '/project/review-meeting/get-import-template' })
|
request.download({ url: '/project/review-meeting/get-import-template' })
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
import request from '@/config/axios'
|
||||||
|
|
||||||
|
export interface ReviewTabletCatalogVO {
|
||||||
|
reviewMeetingId: number
|
||||||
|
meetingName: string
|
||||||
|
meetingStartTime: string
|
||||||
|
meetingEndTime: string
|
||||||
|
reviewMeetingProjectId: number
|
||||||
|
seqNo: number
|
||||||
|
agendaCategory: string
|
||||||
|
projectTitle: string
|
||||||
|
reporter: string
|
||||||
|
reporterUnit: string
|
||||||
|
fileCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReviewTabletOpenUrlVO {
|
||||||
|
fileId: number
|
||||||
|
fileName: string
|
||||||
|
fileType: string
|
||||||
|
openUrl: string
|
||||||
|
visitUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTodayCatalog = () =>
|
||||||
|
request.get({ url: '/project/review-tablet/catalog/today' })
|
||||||
|
|
||||||
|
export const getProjectFiles = (reviewMeetingProjectId: number) =>
|
||||||
|
request.get({
|
||||||
|
url: '/project/review-tablet/project-files',
|
||||||
|
params: { reviewMeetingProjectId }
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getFileOpenUrl = (fileId: number): Promise<ReviewTabletOpenUrlVO> =>
|
||||||
|
request.get({
|
||||||
|
url: '/project/review-tablet/file-open-url',
|
||||||
|
params: { fileId }
|
||||||
|
})
|
||||||
|
|
@ -694,6 +694,18 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/project/review-tablet',
|
||||||
|
component: () => import('@/views/review/tablet/index.vue'),
|
||||||
|
name: 'ReviewTablet',
|
||||||
|
meta: {
|
||||||
|
title: '专家评审资料',
|
||||||
|
noCache: true,
|
||||||
|
hidden: true,
|
||||||
|
canTo: true,
|
||||||
|
noTagsView: true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/:pathMatch(.*)*',
|
path: '/:pathMatch(.*)*',
|
||||||
component: () => import('@/views/Error/404.vue'),
|
component: () => import('@/views/Error/404.vue'),
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,23 @@
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="资料查看时限" prop="materialViewTimeRange">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="formData.materialViewTimeRange"
|
||||||
|
type="datetimerange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始时间"
|
||||||
|
end-placeholder="结束时间"
|
||||||
|
format="YYYY-MM-DD HH:mm"
|
||||||
|
value-format="x"
|
||||||
|
:disabled="isView"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
<el-row :gutter="16">
|
<el-row :gutter="16">
|
||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<el-form-item label="参会专家" prop="expertIds">
|
<el-form-item label="参会专家" prop="expertIds">
|
||||||
|
|
@ -94,6 +111,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed } from 'vue'
|
import { ref, reactive, computed } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import type { FormRules } from 'element-plus'
|
||||||
import type { UploadFile } from 'element-plus'
|
import type { UploadFile } from 'element-plus'
|
||||||
import {
|
import {
|
||||||
createReviewMeeting,
|
createReviewMeeting,
|
||||||
|
|
@ -118,20 +136,27 @@ const dialogTitle = computed(() =>
|
||||||
formType.value === 'create' ? '新增会议邀约' : formType.value === 'update' ? '编辑评审会议' : '查看评审会议'
|
formType.value === 'create' ? '新增会议邀约' : formType.value === 'update' ? '编辑评审会议' : '查看评审会议'
|
||||||
)
|
)
|
||||||
|
|
||||||
type FormData = ReviewMeetingSaveReqVO & { meetingTimeRange?: string[] }
|
type FormData = ReviewMeetingSaveReqVO & {
|
||||||
|
meetingTimeRange?: any[]
|
||||||
|
materialViewTimeRange?: any[]
|
||||||
|
}
|
||||||
|
|
||||||
const formData = reactive<FormData>({
|
const formData = reactive<FormData>({
|
||||||
id: undefined,
|
id: undefined,
|
||||||
name: '',
|
name: '',
|
||||||
startTime: '',
|
startTime: undefined,
|
||||||
endTime: '',
|
endTime: undefined,
|
||||||
location: '',
|
location: '',
|
||||||
|
materialViewStartTime: undefined,
|
||||||
|
materialViewEndTime: undefined,
|
||||||
|
materialViewRemark: undefined,
|
||||||
expertIds: [],
|
expertIds: [],
|
||||||
meetingTimeRange: undefined,
|
meetingTimeRange: undefined,
|
||||||
|
materialViewTimeRange: undefined,
|
||||||
projects: []
|
projects: []
|
||||||
})
|
})
|
||||||
|
|
||||||
const rules = {
|
const rules: FormRules = {
|
||||||
name: [{ required: true, message: '会议标题不能为空', trigger: 'blur' }],
|
name: [{ required: true, message: '会议标题不能为空', trigger: 'blur' }],
|
||||||
meetingTimeRange: [{ required: true, message: '会议时间不能为空', trigger: 'change' }],
|
meetingTimeRange: [{ required: true, message: '会议时间不能为空', trigger: 'change' }],
|
||||||
location: [{ required: true, message: '会议地点不能为空', trigger: 'blur' }],
|
location: [{ required: true, message: '会议地点不能为空', trigger: 'blur' }],
|
||||||
|
|
@ -161,6 +186,12 @@ const open = async (type: 'create' | 'update' | 'view', id?: number) => {
|
||||||
new Date(detail.endTime.replace(' ', 'T')).getTime()
|
new Date(detail.endTime.replace(' ', 'T')).getTime()
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
if (detail.materialViewStartTime && detail.materialViewEndTime) {
|
||||||
|
formData.materialViewTimeRange = [
|
||||||
|
new Date(detail.materialViewStartTime.replace(' ', 'T')).getTime(),
|
||||||
|
new Date(detail.materialViewEndTime.replace(' ', 'T')).getTime()
|
||||||
|
]
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
formLoading.value = false
|
formLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
@ -170,11 +201,15 @@ const open = async (type: 'create' | 'update' | 'view', id?: number) => {
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
formData.id = undefined
|
formData.id = undefined
|
||||||
formData.name = ''
|
formData.name = ''
|
||||||
formData.startTime = ''
|
formData.startTime = undefined
|
||||||
formData.endTime = ''
|
formData.endTime = undefined
|
||||||
formData.location = ''
|
formData.location = ''
|
||||||
|
formData.materialViewStartTime = undefined
|
||||||
|
formData.materialViewEndTime = undefined
|
||||||
|
formData.materialViewRemark = undefined
|
||||||
formData.expertIds = []
|
formData.expertIds = []
|
||||||
formData.meetingTimeRange = undefined
|
formData.meetingTimeRange = undefined
|
||||||
|
formData.materialViewTimeRange = undefined
|
||||||
formData.projects = []
|
formData.projects = []
|
||||||
formRef.value?.resetFields()
|
formRef.value?.resetFields()
|
||||||
}
|
}
|
||||||
|
|
@ -213,6 +248,17 @@ const submitForm = async () => {
|
||||||
formData.startTime = formData.meetingTimeRange[0]
|
formData.startTime = formData.meetingTimeRange[0]
|
||||||
formData.endTime = formData.meetingTimeRange[1]
|
formData.endTime = formData.meetingTimeRange[1]
|
||||||
}
|
}
|
||||||
|
if (formData.materialViewTimeRange?.length === 2) {
|
||||||
|
formData.materialViewStartTime = formData.materialViewTimeRange[0]
|
||||||
|
formData.materialViewEndTime = formData.materialViewTimeRange[1]
|
||||||
|
} else if (formData.meetingTimeRange?.length === 2) {
|
||||||
|
// 默认与会议时间一致,便于管理员快速配置
|
||||||
|
formData.materialViewStartTime = formData.meetingTimeRange[0]
|
||||||
|
formData.materialViewEndTime = formData.meetingTimeRange[1]
|
||||||
|
} else {
|
||||||
|
formData.materialViewStartTime = undefined
|
||||||
|
formData.materialViewEndTime = undefined
|
||||||
|
}
|
||||||
formLoading.value = true
|
formLoading.value = true
|
||||||
try {
|
try {
|
||||||
const submitData = { ...formData }
|
const submitData = { ...formData }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,582 @@
|
||||||
|
<template>
|
||||||
|
<div class="tablet-full-page" v-loading="pageLoading">
|
||||||
|
<header class="page-toolbar">
|
||||||
|
<div class="title-wrap">
|
||||||
|
<h2>专家评审资料</h2>
|
||||||
|
<span>当日项目编目 · {{ projectCount }} 项</span>
|
||||||
|
</div>
|
||||||
|
<div class="tool-wrap">
|
||||||
|
<el-input
|
||||||
|
v-model="keyword"
|
||||||
|
clearable
|
||||||
|
placeholder="搜索项目/资料"
|
||||||
|
class="search-input"
|
||||||
|
/>
|
||||||
|
<el-button :icon="Refresh" @click="loadCatalog">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="page-body">
|
||||||
|
<aside class="directory-panel">
|
||||||
|
<div class="panel-title">项目目录</div>
|
||||||
|
<el-empty v-if="treeData.length === 0" description="当前时段暂无可查看资料" />
|
||||||
|
<el-tree
|
||||||
|
v-else
|
||||||
|
ref="treeRef"
|
||||||
|
class="catalog-tree"
|
||||||
|
:data="treeData"
|
||||||
|
node-key="id"
|
||||||
|
highlight-current
|
||||||
|
:expand-on-click-node="false"
|
||||||
|
:default-expanded-keys="expandedKeys"
|
||||||
|
:props="treeProps"
|
||||||
|
:filter-node-method="filterNode"
|
||||||
|
@node-expand="handleNodeExpand"
|
||||||
|
@node-collapse="handleNodeCollapse"
|
||||||
|
@node-click="handleNodeClick"
|
||||||
|
>
|
||||||
|
<template #default="{ data }">
|
||||||
|
<div v-if="data.type === 'project'" class="tree-project">
|
||||||
|
<div class="node-left">
|
||||||
|
<el-icon class="node-icon folder-icon"><Folder /></el-icon>
|
||||||
|
<span class="node-name">{{ data.label }}</span>
|
||||||
|
</div>
|
||||||
|
<el-tag size="small" type="info">{{ data.fileCount }} 份</el-tag>
|
||||||
|
</div>
|
||||||
|
<div v-else class="tree-file">
|
||||||
|
<div class="node-left">
|
||||||
|
<el-icon class="node-icon file-icon"><Document /></el-icon>
|
||||||
|
<span class="node-name">{{ data.label }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="file-type">{{ data.fileType?.toUpperCase() }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-tree>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section class="preview-panel">
|
||||||
|
<div class="preview-header">
|
||||||
|
<div class="preview-meta">
|
||||||
|
<div class="preview-title">{{ activeFileName || '请选择左侧资料进行预览' }}</div>
|
||||||
|
<div class="preview-sub">{{ activeFileDesc }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="preview-actions">
|
||||||
|
<el-button :disabled="!activeFileId" @click="refreshPreview">刷新预览</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:icon="Link"
|
||||||
|
:disabled="!previewUrl"
|
||||||
|
@click="openInNewWindow"
|
||||||
|
>
|
||||||
|
新窗口打开
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="preview-body" v-loading="previewLoading">
|
||||||
|
<div v-if="!activeFileId && !previewLoading" class="welcome-panel">
|
||||||
|
<div class="welcome-title">欢迎各位专家莅临指导工作</div>
|
||||||
|
<div class="welcome-sub">请从左侧目录中选择评审资料开始查阅</div>
|
||||||
|
</div>
|
||||||
|
<el-result v-else-if="previewError" icon="error" title="预览加载失败" :sub-title="previewError">
|
||||||
|
<template #extra>
|
||||||
|
<el-button type="primary" :disabled="!activeFileId" @click="refreshPreview">重试</el-button>
|
||||||
|
<el-button :disabled="!previewUrl" @click="openInNewWindow">新窗口打开</el-button>
|
||||||
|
</template>
|
||||||
|
</el-result>
|
||||||
|
<iframe
|
||||||
|
v-else-if="previewUrl"
|
||||||
|
:src="previewUrl"
|
||||||
|
class="preview-iframe"
|
||||||
|
frameborder="0"
|
||||||
|
allowfullscreen
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, nextTick, ref, watch } from 'vue'
|
||||||
|
import { Document, Folder, Link, Refresh } from '@element-plus/icons-vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { formatDate } from '@/utils/formatTime'
|
||||||
|
import {
|
||||||
|
getFileOpenUrl,
|
||||||
|
getProjectFiles,
|
||||||
|
getTodayCatalog,
|
||||||
|
type ReviewTabletCatalogVO,
|
||||||
|
type ReviewTabletOpenUrlVO
|
||||||
|
} from '@/api/review/tablet'
|
||||||
|
import type { ReviewMeetingFileRespVO } from '@/api/review/project'
|
||||||
|
|
||||||
|
defineOptions({ name: 'ReviewTablet' })
|
||||||
|
|
||||||
|
type TreeNode = ProjectTreeNode | FileTreeNode
|
||||||
|
|
||||||
|
interface ProjectTreeNode {
|
||||||
|
id: string
|
||||||
|
type: 'project'
|
||||||
|
label: string
|
||||||
|
projectId: number
|
||||||
|
meetingName: string
|
||||||
|
fileCount: number
|
||||||
|
seqNo: number
|
||||||
|
loaded: boolean
|
||||||
|
loading: boolean
|
||||||
|
source: ReviewTabletCatalogVO
|
||||||
|
children: FileTreeNode[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FileTreeNode {
|
||||||
|
id: string
|
||||||
|
type: 'file'
|
||||||
|
label: string
|
||||||
|
projectId: number
|
||||||
|
fileId: number
|
||||||
|
fileType: string
|
||||||
|
fileSize: number
|
||||||
|
createTime: string
|
||||||
|
source: ReviewMeetingFileRespVO
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageLoading = ref(false)
|
||||||
|
const previewLoading = ref(false)
|
||||||
|
const previewError = ref('')
|
||||||
|
const keyword = ref('')
|
||||||
|
|
||||||
|
const treeRef = ref<any>()
|
||||||
|
const treeData = ref<ProjectTreeNode[]>([])
|
||||||
|
const expandedKeys = ref<string[]>([])
|
||||||
|
const fileCache = ref<Record<number, ReviewMeetingFileRespVO[]>>({})
|
||||||
|
|
||||||
|
const activeProjectId = ref<number>()
|
||||||
|
const activeFileId = ref<number>()
|
||||||
|
const activeFileNode = ref<FileTreeNode>()
|
||||||
|
const previewUrl = ref('')
|
||||||
|
const previewPayload = ref<ReviewTabletOpenUrlVO>()
|
||||||
|
|
||||||
|
const treeProps = {
|
||||||
|
label: 'label',
|
||||||
|
children: 'children'
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectCount = computed(() => treeData.value.length)
|
||||||
|
const activeFileName = computed(() => activeFileNode.value?.label || previewPayload.value?.fileName || '')
|
||||||
|
const activeFileDesc = computed(() => {
|
||||||
|
if (!activeFileNode.value) return ''
|
||||||
|
const segments: string[] = []
|
||||||
|
if (activeFileNode.value.fileType) {
|
||||||
|
segments.push(activeFileNode.value.fileType.toUpperCase())
|
||||||
|
}
|
||||||
|
if (activeFileNode.value.fileSize) {
|
||||||
|
segments.push(formatFileSize(activeFileNode.value.fileSize))
|
||||||
|
}
|
||||||
|
if (activeFileNode.value.createTime) {
|
||||||
|
segments.push(`上传于 ${formatDate(new Date(activeFileNode.value.createTime), 'YYYY-MM-DD HH:mm')}`)
|
||||||
|
}
|
||||||
|
return segments.join(' · ')
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(keyword, (value) => {
|
||||||
|
treeRef.value?.filter(value.trim())
|
||||||
|
})
|
||||||
|
|
||||||
|
const filterNode = (value: string, data: TreeNode): boolean => {
|
||||||
|
if (!value) return true
|
||||||
|
const keywordValue = value.toLowerCase()
|
||||||
|
if (data.type === 'project') {
|
||||||
|
const projectMatched = [data.label, data.meetingName].join('|').toLowerCase().includes(keywordValue)
|
||||||
|
const childMatched = data.children.some((item) => item.label.toLowerCase().includes(keywordValue))
|
||||||
|
return projectMatched || childMatched
|
||||||
|
}
|
||||||
|
return [data.label, data.fileType].join('|').toLowerCase().includes(keywordValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildProjectNode = (item: ReviewTabletCatalogVO): ProjectTreeNode => ({
|
||||||
|
id: `project-${item.reviewMeetingProjectId}`,
|
||||||
|
type: 'project',
|
||||||
|
label: item.projectTitle,
|
||||||
|
projectId: item.reviewMeetingProjectId,
|
||||||
|
meetingName: item.meetingName,
|
||||||
|
fileCount: item.fileCount,
|
||||||
|
seqNo: item.seqNo,
|
||||||
|
loaded: false,
|
||||||
|
loading: false,
|
||||||
|
source: item,
|
||||||
|
children: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const toFileNode = (file: ReviewMeetingFileRespVO, projectId: number): FileTreeNode => ({
|
||||||
|
id: `file-${projectId}-${file.id}`,
|
||||||
|
type: 'file',
|
||||||
|
label: file.fileName,
|
||||||
|
projectId,
|
||||||
|
fileId: file.id,
|
||||||
|
fileType: file.fileType,
|
||||||
|
fileSize: file.fileSize,
|
||||||
|
createTime: file.createTime,
|
||||||
|
source: file
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadProjectFiles = async (projectNode: ProjectTreeNode) => {
|
||||||
|
if (projectNode.loaded || projectNode.loading) return
|
||||||
|
const cachedFiles = fileCache.value[projectNode.projectId]
|
||||||
|
if (cachedFiles) {
|
||||||
|
projectNode.children = cachedFiles.map((item) => toFileNode(item, projectNode.projectId))
|
||||||
|
projectNode.loaded = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
projectNode.loading = true
|
||||||
|
try {
|
||||||
|
const files = await getProjectFiles(projectNode.projectId)
|
||||||
|
fileCache.value[projectNode.projectId] = files
|
||||||
|
projectNode.children = files.map((item) => toFileNode(item, projectNode.projectId))
|
||||||
|
projectNode.loaded = true
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('加载项目资料失败,请稍后重试')
|
||||||
|
throw new Error('load-project-files-failed')
|
||||||
|
} finally {
|
||||||
|
projectNode.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadPreview = async (fileId: number) => {
|
||||||
|
previewLoading.value = true
|
||||||
|
previewError.value = ''
|
||||||
|
try {
|
||||||
|
const payload = await getFileOpenUrl(fileId)
|
||||||
|
previewPayload.value = payload
|
||||||
|
if (!payload?.openUrl) {
|
||||||
|
previewUrl.value = ''
|
||||||
|
previewError.value = '未获取到预览地址'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
previewUrl.value = payload.openUrl
|
||||||
|
} catch {
|
||||||
|
previewUrl.value = ''
|
||||||
|
previewError.value = '预览服务暂不可用,请稍后重试'
|
||||||
|
} finally {
|
||||||
|
previewLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectFileNode = async (fileNode: FileTreeNode) => {
|
||||||
|
activeProjectId.value = fileNode.projectId
|
||||||
|
activeFileId.value = fileNode.fileId
|
||||||
|
activeFileNode.value = fileNode
|
||||||
|
await loadPreview(fileNode.fileId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const openInNewWindow = () => {
|
||||||
|
const url = previewPayload.value?.openUrl || previewUrl.value
|
||||||
|
if (!url) {
|
||||||
|
ElMessage.warning('暂无可打开的预览地址')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
window.open(url, '_blank', 'noopener,noreferrer')
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshPreview = async () => {
|
||||||
|
if (!activeFileId.value) return
|
||||||
|
await loadPreview(activeFileId.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNodeExpand = async (data: TreeNode) => {
|
||||||
|
if (data.type !== 'project') return
|
||||||
|
if (!expandedKeys.value.includes(data.id)) {
|
||||||
|
expandedKeys.value.push(data.id)
|
||||||
|
}
|
||||||
|
await loadProjectFiles(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNodeCollapse = (data: TreeNode) => {
|
||||||
|
if (data.type !== 'project') return
|
||||||
|
expandedKeys.value = expandedKeys.value.filter((id) => id !== data.id)
|
||||||
|
if (expandedKeys.value.length > 0) return
|
||||||
|
|
||||||
|
activeProjectId.value = undefined
|
||||||
|
activeFileId.value = undefined
|
||||||
|
activeFileNode.value = undefined
|
||||||
|
previewUrl.value = ''
|
||||||
|
previewPayload.value = undefined
|
||||||
|
previewError.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNodeClick = async (data: TreeNode) => {
|
||||||
|
if (data.type === 'project') {
|
||||||
|
activeProjectId.value = data.projectId
|
||||||
|
await loadProjectFiles(data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await selectFileNode(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadCatalog = async () => {
|
||||||
|
pageLoading.value = true
|
||||||
|
previewError.value = ''
|
||||||
|
previewUrl.value = ''
|
||||||
|
previewPayload.value = undefined
|
||||||
|
activeFileId.value = undefined
|
||||||
|
activeFileNode.value = undefined
|
||||||
|
activeProjectId.value = undefined
|
||||||
|
try {
|
||||||
|
const catalog = await getTodayCatalog()
|
||||||
|
treeData.value = catalog
|
||||||
|
.sort((a, b) => (a.seqNo || 0) - (b.seqNo || 0))
|
||||||
|
.map((item) => buildProjectNode(item))
|
||||||
|
fileCache.value = {}
|
||||||
|
expandedKeys.value = []
|
||||||
|
if (treeData.value.length === 0) return
|
||||||
|
|
||||||
|
expandedKeys.value = []
|
||||||
|
await nextTick()
|
||||||
|
treeRef.value?.setCurrentKey(undefined)
|
||||||
|
} finally {
|
||||||
|
pageLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatFileSize = (bytes: number): string => {
|
||||||
|
if (!bytes) return '-'
|
||||||
|
if (bytes < 1024) return `${bytes} B`
|
||||||
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||||
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
||||||
|
}
|
||||||
|
|
||||||
|
loadCatalog()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tablet-full-page {
|
||||||
|
height: 100vh;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
background: #eef2f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-toolbar {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-wrap h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-wrap span {
|
||||||
|
display: block;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 360px minmax(0, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.directory-panel,
|
||||||
|
.preview-panel {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.directory-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
padding: 6px 8px 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-tree {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-project,
|
||||||
|
.tree-file {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-left {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-name {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-icon {
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-type {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-header {
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-meta {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-sub {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-panel {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #1f2937;
|
||||||
|
text-align: center;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-title {
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-sub {
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 0;
|
||||||
|
display: block;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tree-node__content) {
|
||||||
|
height: 42px;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tree-node__content:hover) {
|
||||||
|
background: #eff6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.tablet-full-page {
|
||||||
|
height: auto;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-toolbar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-body {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.directory-panel {
|
||||||
|
max-height: 42vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel {
|
||||||
|
min-height: 56vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in New Issue