增加专家在平板上查看评审资料功能,前端页面为单独入口url:project/review-tablet
parent
ca0cbe2ac9
commit
8f893c0afd
|
|
@ -19,8 +19,11 @@ export interface ReviewProjectItemVO {
|
|||
export interface ReviewMeetingSaveReqVO {
|
||||
id?: number
|
||||
name: string
|
||||
startTime: string | number
|
||||
endTime: string | number
|
||||
startTime?: string | number
|
||||
endTime?: string | number
|
||||
materialViewStartTime?: string | number
|
||||
materialViewEndTime?: string | number
|
||||
materialViewRemark?: string
|
||||
location: string
|
||||
expertIds: number[]
|
||||
projects?: ReviewProjectItemVO[]
|
||||
|
|
@ -41,6 +44,9 @@ export interface ReviewMeetingRespVO {
|
|||
name: string
|
||||
startTime: string
|
||||
endTime: string
|
||||
materialViewStartTime?: string
|
||||
materialViewEndTime?: string
|
||||
materialViewRemark?: string
|
||||
location: string
|
||||
status: number // 0-草稿 1-已邀约 2-已结束 3-已取消
|
||||
expertIds: number[]
|
||||
|
|
@ -111,4 +117,3 @@ export const importProjectsFromExcel = (file: File) => {
|
|||
/** 下载导入模板 */
|
||||
export const getImportTemplate = () =>
|
||||
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(.*)*',
|
||||
component: () => import('@/views/Error/404.vue'),
|
||||
|
|
|
|||
|
|
@ -33,6 +33,23 @@
|
|||
</el-form-item>
|
||||
</el-col>
|
||||
</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-col :span="24">
|
||||
<el-form-item label="参会专家" prop="expertIds">
|
||||
|
|
@ -94,6 +111,7 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import type { UploadFile } from 'element-plus'
|
||||
import {
|
||||
createReviewMeeting,
|
||||
|
|
@ -118,20 +136,27 @@ const dialogTitle = computed(() =>
|
|||
formType.value === 'create' ? '新增会议邀约' : formType.value === 'update' ? '编辑评审会议' : '查看评审会议'
|
||||
)
|
||||
|
||||
type FormData = ReviewMeetingSaveReqVO & { meetingTimeRange?: string[] }
|
||||
type FormData = ReviewMeetingSaveReqVO & {
|
||||
meetingTimeRange?: any[]
|
||||
materialViewTimeRange?: any[]
|
||||
}
|
||||
|
||||
const formData = reactive<FormData>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
startTime: undefined,
|
||||
endTime: undefined,
|
||||
location: '',
|
||||
materialViewStartTime: undefined,
|
||||
materialViewEndTime: undefined,
|
||||
materialViewRemark: undefined,
|
||||
expertIds: [],
|
||||
meetingTimeRange: undefined,
|
||||
materialViewTimeRange: undefined,
|
||||
projects: []
|
||||
})
|
||||
|
||||
const rules = {
|
||||
const rules: FormRules = {
|
||||
name: [{ required: true, message: '会议标题不能为空', trigger: 'blur' }],
|
||||
meetingTimeRange: [{ required: true, message: '会议时间不能为空', trigger: 'change' }],
|
||||
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()
|
||||
]
|
||||
}
|
||||
if (detail.materialViewStartTime && detail.materialViewEndTime) {
|
||||
formData.materialViewTimeRange = [
|
||||
new Date(detail.materialViewStartTime.replace(' ', 'T')).getTime(),
|
||||
new Date(detail.materialViewEndTime.replace(' ', 'T')).getTime()
|
||||
]
|
||||
}
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
|
|
@ -170,11 +201,15 @@ const open = async (type: 'create' | 'update' | 'view', id?: number) => {
|
|||
const resetForm = () => {
|
||||
formData.id = undefined
|
||||
formData.name = ''
|
||||
formData.startTime = ''
|
||||
formData.endTime = ''
|
||||
formData.startTime = undefined
|
||||
formData.endTime = undefined
|
||||
formData.location = ''
|
||||
formData.materialViewStartTime = undefined
|
||||
formData.materialViewEndTime = undefined
|
||||
formData.materialViewRemark = undefined
|
||||
formData.expertIds = []
|
||||
formData.meetingTimeRange = undefined
|
||||
formData.materialViewTimeRange = undefined
|
||||
formData.projects = []
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
|
@ -213,6 +248,17 @@ const submitForm = async () => {
|
|||
formData.startTime = formData.meetingTimeRange[0]
|
||||
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
|
||||
try {
|
||||
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