diff --git a/src/api/review/tablet.ts b/src/api/review/tablet.ts index dbcccb281..de94289d9 100644 --- a/src/api/review/tablet.ts +++ b/src/api/review/tablet.ts @@ -45,3 +45,8 @@ export const getFileOpenUrl = (fileId: number): Promise = url: '/project/review-tablet/file-open-url', params: { fileId } }) + +export const getAgendaOpenUrl = (): Promise => + request.get({ + url: '/project/review-tablet/agenda-open-url' + }) diff --git a/src/views/review/tablet/index.vue b/src/views/review/tablet/index.vue index 04e2863f3..e87dcbc01 100644 --- a/src/views/review/tablet/index.vue +++ b/src/views/review/tablet/index.vue @@ -15,7 +15,17 @@
专家评审资料
-
当前评审会议编目:{{ projectCount }}篇
+
+
当前评审会议编目:{{ projectCount }}篇
+ 今日议程 +
- + + + {{ data.label }}
- {{ data.fileType?.toUpperCase() }}
@@ -72,12 +83,10 @@
-
{{ activeFileName || '请选择左侧资料进行预览' }}
-
{{ - activeFileDesc || '预览区支持 PDF/Office 等资料在线查看' - }}
+
{{ centerTitle }}
+
{{ centerSub }}
- 刷新预览
-
-
-
今日议程
-
{{ agendaMeetingLabel }}
-
-
-
-
项目信息
-
-
-
-
{{ item.seqNoText }}
-
-
{{ item.projectTitle }}
-
- {{ item.agendaCategory || '未设置议程分类' }} - {{ item.reporter || '未填写汇报人' }} - {{ item.reporterUnit }} -
-
-
-
-
-
-
-
专家信息
-
-
-
{{ expert.name }}
-
- {{ expert.organization }} - {{ expert.title }} - {{ expert.position }} - {{ expert.remark }} -
-
-
-
暂无专家信息
-
-
-
-
+
欢迎查阅评审资料
请从左侧目录中选择文件开始浏览
@@ -427,14 +395,18 @@ import { ArrowLeftBold, ArrowRightBold, Close, + DataBoard, Document, + Files, Folder, InfoFilled, Link, Loading, + Picture, Promotion, Refresh, Search, + Tickets, WarningFilled } from '@element-plus/icons-vue' import { ElMessage, ElMessageBox } from 'element-plus' @@ -449,6 +421,7 @@ import { import { getFileOpenUrl, getProjectFiles, + getAgendaOpenUrl, getActiveCatalog, type ReviewTabletCatalogVO, type ReviewTabletOpenUrlVO @@ -518,6 +491,7 @@ const pageLoading = ref(false) const previewLoading = ref(false) const previewError = ref('') const keyword = ref('') +const agendaLoading = ref(false) const leftCollapsed = ref(false) const rightCollapsed = ref(false) const sideTab = ref('ai') @@ -532,6 +506,7 @@ const fileCache = ref>({}) const activeProjectId = ref() const activeFileId = ref() const activeFileNode = ref() +const isAgendaPreview = ref(false) const previewUrl = ref('') const previewPayload = ref() const previewFrameLoading = ref(false) @@ -541,65 +516,19 @@ const previewRequestToken = ref(0) const treeProps = { label: 'label', children: 'children' } const projectCount = computed(() => treeData.value.length) -const showAgendaOverview = computed( - () => - !previewLoading.value && - !activeFileId.value && - treeData.value.length > 0 && - expandedKeys.value.length === 0 -) -const agendaOverviewList = computed(() => - treeData.value.map((item) => ({ - id: item.id, - projectTitle: item.label, - agendaCategory: item.source.agendaCategory, - expertInfos: (item.source.expertInfos || []).map((expert, index) => ({ - key: `${item.id}-${index}-${expert.name || 'expert'}`, - name: expert.name, - organization: expert.organization, - title: expert.title, - position: expert.position, - remark: expert.remark ? `备注:${expert.remark}` : '' - })), - reporter: item.source.reporter, - reporterUnit: item.source.reporterUnit, - seqNoText: item.seqNo ? String(item.seqNo).padStart(2, '0') : '--' - })) -) -const agendaExpertInfos = computed(() => { - const seen = new Set() - return agendaOverviewList.value - .flatMap((item) => item.expertInfos) - .filter((expert) => { - const key = [ - expert.name, - expert.organization, - expert.title, - expert.position, - expert.remark - ].join('|') - if (seen.has(key)) return false - seen.add(key) - return true - }) -}) -const agendaMeetingLabel = computed(() => { - const first = treeData.value[0]?.source - if (!first) return '当日评审项目总览' - const timeSegments = [first.meetingName] - if (first.meetingStartTime && first.meetingEndTime) { - timeSegments.push( - `${formatDate(new Date(first.meetingStartTime), 'HH:mm')} - ${formatDate( - new Date(first.meetingEndTime), - 'HH:mm' - )}` - ) - } - return timeSegments.filter(Boolean).join(' · ') -}) const activeFileName = computed( () => activeFileNode.value?.label || previewPayload.value?.fileName || '' ) +const isFilePreviewing = computed(() => !!activeFileId.value && !isAgendaPreview.value) +const centerTitle = computed(() => + isAgendaPreview.value ? '今日议程' : activeFileName.value || '请选择左侧资料进行预览' +) +const centerSub = computed(() => + isAgendaPreview.value + ? '' + : activeFileDesc.value || '预览区支持 PDF/Office 等资料在线查看' +) +const canRefreshPreview = computed(() => isAgendaPreview.value || !!activeFileId.value) const activeProjectTitle = computed(() => { if (!activeProjectId.value) return '' return ( @@ -828,7 +757,50 @@ const loadPreview = async (fileId: number) => { } } +const loadAgendaPreview = async () => { + const requestToken = ++previewRequestToken.value + clearPreviewFrameTimeout() + previewLoading.value = true + previewFrameLoading.value = true + previewError.value = '' + previewUrl.value = '' + previewPayload.value = undefined + previewFrameKey.value += 1 + agendaLoading.value = true + try { + const openUrl = await getAgendaOpenUrl() + if (requestToken !== previewRequestToken.value) return + if (!openUrl) { + previewFrameLoading.value = false + previewError.value = '当前会议暂无议程文件,请先在会议新建阶段生成议程' + return + } + previewUrl.value = openUrl + startPreviewFrameTimeout(requestToken) + } catch { + if (requestToken !== previewRequestToken.value) return + previewFrameLoading.value = false + previewError.value = '议程文件预览加载失败,请稍后重试' + } finally { + agendaLoading.value = false + if (requestToken === previewRequestToken.value) { + previewLoading.value = false + } + } +} + +const openTodayAgenda = async () => { + if (treeData.value.length === 0) return + isAgendaPreview.value = true + activeFileId.value = undefined + activeFileNode.value = undefined + await nextTick() + treeRef.value?.setCurrentKey(undefined) + await loadAgendaPreview() +} + const selectFileNode = async (fileNode: FileTreeNode) => { + isAgendaPreview.value = false activeProjectId.value = fileNode.projectId activeFileId.value = fileNode.fileId activeFileNode.value = fileNode @@ -845,6 +817,10 @@ const openInNewWindow = () => { } const refreshPreview = async () => { + if (isAgendaPreview.value) { + await loadAgendaPreview() + return + } if (activeFileId.value) await loadPreview(activeFileId.value) } @@ -878,6 +854,7 @@ const handleNodeExpand = async (data: TreeNode) => { } const resetPreviewSelection = async () => { + isAgendaPreview.value = false clearPreviewFrameTimeout() previewRequestToken.value += 1 previewError.value = '' @@ -899,12 +876,22 @@ const handleNodeCollapse = (data: TreeNode) => { } } -const handleNodeClick = async (data: TreeNode) => { +const handleNodeClick = async (data: TreeNode, node: any) => { if (data.type === 'project') { persistCurrentProjectAiState() const switched = activeProjectId.value !== data.projectId activeProjectId.value = data.projectId - await loadProjectFiles(data) + const isExpanded = !!node?.expanded + if (!isExpanded) { + await loadProjectFiles(data) + } + if (node) { + if (isExpanded) { + node.collapse() + } else { + node.expand() + } + } if (isAiTabActive.value && (switched || !aiSummary.value)) { restoreOrLoadProjectAi(data.projectId) } @@ -924,6 +911,7 @@ const loadCatalog = async () => { clearPreviewFrameTimeout() clearOfficeWarmupTimers() previewRequestToken.value += 1 + isAgendaPreview.value = false previewError.value = '' previewUrl.value = '' previewPayload.value = undefined @@ -1241,6 +1229,26 @@ const formatFileSize = (bytes: number): string => { const isOfficeFile = (fileType?: string) => ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes((fileType || '').toLowerCase()) +const resolveFileIcon = (fileType?: string) => { + const normalized = (fileType || '').toLowerCase() + if (['ppt', 'pptx'].includes(normalized)) return Tickets + if (['xls', 'xlsx', 'csv'].includes(normalized)) return DataBoard + if (['doc', 'docx', 'txt', 'rtf'].includes(normalized)) return Document + if (normalized === 'pdf') return Files + if (['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'].includes(normalized)) return Picture + return Document +} + +const resolveFileIconClass = (fileType?: string) => { + const normalized = (fileType || '').toLowerCase() + if (['ppt', 'pptx'].includes(normalized)) return 'file-icon--ppt' + if (['xls', 'xlsx', 'csv'].includes(normalized)) return 'file-icon--excel' + if (['doc', 'docx', 'txt', 'rtf'].includes(normalized)) return 'file-icon--doc' + if (normalized === 'pdf') return 'file-icon--pdf' + if (['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'].includes(normalized)) return 'file-icon--image' + return '' +} + const clearOfficeWarmupTimers = () => { officeWarmupTimers.forEach((timer) => clearTimeout(timer)) officeWarmupTimers = [] @@ -1426,9 +1434,63 @@ loadCatalog() } .left-sub { - margin-top: 8px; color: var(--tablet-text-soft); font-size: 14px; + line-height: 1.4; +} + +.left-sub-row { + margin-top: 8px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.agenda-entry-btn { + flex-shrink: 0; + height: 30px; + padding: 0 14px; + border-radius: 6px; + border: 1px solid #b8cbea; + background: #dbe7fa; + color: #2d5ca3; + font-size: 13px; + font-weight: 600; + letter-spacing: 0.3px; + box-shadow: + 0 5px 12px rgba(60, 99, 182, 0.16), + inset 0 1px 0 rgba(255, 255, 255, 0.88); + transition: + transform 0.18s ease, + box-shadow 0.18s ease, + border-color 0.18s ease, + color 0.18s ease, + background 0.18s ease; +} + +.agenda-entry-btn:hover:not(.is-disabled) { + border-color: #9eb9e2; + color: #244b88; + background: #cfe0fa; + box-shadow: + 0 7px 16px rgba(45, 92, 163, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.9); + transform: translateY(-1px); +} + +.agenda-entry-btn:active:not(.is-disabled) { + transform: translateY(0); + box-shadow: + 0 4px 10px rgba(45, 92, 163, 0.16), + inset 0 1px 0 rgba(255, 255, 255, 0.8); +} + +.agenda-entry-btn.is-disabled { + border-color: var(--tablet-border); + background: var(--tablet-surface-muted); + color: #8695ab; + box-shadow: none; } .left-search { @@ -1487,17 +1549,15 @@ loadCatalog() } .node-label { - overflow: hidden; - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; - line-clamp: 2; + display: block; + overflow: visible; white-space: normal; line-height: 1.4; font-size: 15px; font-weight: 500; color: var(--tablet-text); - word-break: break-all; + word-break: break-word; + overflow-wrap: anywhere; } .node-icon { @@ -1515,6 +1575,26 @@ loadCatalog() color: #5d7395; } +.file-icon--doc { + color: #2f67be; +} + +.file-icon--ppt { + color: #d86a34; +} + +.file-icon--excel { + color: #2f9d62; +} + +.file-icon--pdf { + color: #cc4a4a; +} + +.file-icon--image { + color: #7b5ac8; +} + .node-badge { min-width: 20px; padding: 2px 6px; @@ -1527,14 +1607,6 @@ loadCatalog() flex-shrink: 0; } -.node-ext { - font-size: 12px; - line-height: 1.3; - color: var(--tablet-text-soft); - flex-shrink: 0; - margin-top: 2px; -} - .center-header { padding: 14px 16px 10px; border-bottom: 1px solid var(--tablet-border); @@ -1543,7 +1615,7 @@ loadCatalog() .center-title { font-size: 42px; - line-height: 1; + line-height: 1.1; color: var(--tablet-text); font-weight: 700; white-space: nowrap; @@ -1551,6 +1623,16 @@ loadCatalog() text-overflow: ellipsis; } +.center-title.is-file-title { + font-size: clamp(20px, 2.2vw, 30px); + line-height: 1.3; + white-space: normal; + overflow: visible; + text-overflow: clip; + overflow-wrap: anywhere; + word-break: break-word; +} + .center-sub { margin-top: 10px; color: var(--tablet-text-soft); @@ -1577,169 +1659,6 @@ loadCatalog() position: relative; } -.agenda-overview { - height: 100%; - box-sizing: border-box; - padding: 18px 20px; - display: flex; - flex-direction: column; - gap: 12px; - overflow-y: auto; - background: - radial-gradient(circle at top right, rgba(196, 213, 239, 0.4), transparent 28%), - linear-gradient(180deg, var(--tablet-panel-soft) 0%, var(--tablet-panel) 100%); -} - -.agenda-head { - display: flex; - flex-direction: column; - gap: 6px; -} - -.agenda-title { - font-size: 24px; - font-weight: 700; - color: var(--tablet-text); -} - -.agenda-sub { - font-size: 13px; - color: var(--tablet-text-soft); -} - -.agenda-list { - min-width: 0; - display: flex; - flex-direction: column; - gap: 6px; -} - -.agenda-content { - display: grid; - grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr); - gap: 14px; - align-items: stretch; - min-height: 0; -} - -.agenda-project { - min-width: 0; - background: var(--tablet-panel); - border-radius: 14px; -} - -.agenda-projects-panel, -.agenda-experts-panel { - min-width: 0; - box-sizing: border-box; - padding: 10px 12px; - border-radius: 12px; - border: 1px solid var(--tablet-border); - box-shadow: 0 8px 24px rgba(139, 158, 191, 0.06); -} - -.agenda-projects-panel { - height: 100%; - background: var(--tablet-panel); -} - -.agenda-experts-panel { - background: var(--tablet-panel); -} - -.agenda-item { - min-width: 0; -} - -.agenda-panel-title { - margin-bottom: 8px; - font-size: 13px; - font-weight: 700; - color: var(--tablet-accent); -} - -.agenda-card { - display: grid; - grid-template-columns: 48px minmax(0, 1fr); - gap: 10px; - align-items: start; - padding: 8px 10px; - border-radius: 10px; - border: 1px solid var(--tablet-border-strong); - background: var(--tablet-surface-strong); -} - -.agenda-index { - height: 28px; - border-radius: 8px; - background: linear-gradient(135deg, #d5e3f8, #e5edf9); - color: var(--tablet-accent); - font-size: 14px; - font-weight: 700; - display: flex; - align-items: center; - justify-content: center; -} - -.agenda-main { - min-width: 0; - display: flex; - flex-direction: column; - gap: 3px; -} - -.agenda-name { - font-size: 15px; - line-height: 1.35; - color: var(--tablet-text); - font-weight: 600; - word-break: break-word; -} - -.agenda-meta { - display: flex; - flex-wrap: wrap; - gap: 4px 10px; - color: var(--tablet-text-soft); - font-size: 11px; - line-height: 1.4; -} - -.agenda-experts { - display: flex; - flex-direction: column; - gap: 6px; -} - -.agenda-expert { - padding: 8px 10px; - border-radius: 8px; - background: #e3ebf7; - border: 1px solid var(--tablet-border-strong); -} - -.agenda-expert-name { - font-size: 13px; - font-weight: 700; - color: var(--tablet-text); -} - -.agenda-expert-meta { - margin-top: 3px; - display: flex; - flex-wrap: wrap; - gap: 4px 10px; - color: var(--tablet-text-soft); - font-size: 11px; - line-height: 1.5; -} - -.agenda-experts-empty { - color: var(--tablet-text-soft); - font-size: 12px; - line-height: 1.5; -} - .preview-iframe { width: 100%; height: 100%; @@ -2253,6 +2172,11 @@ loadCatalog() padding-right: 6px; } +:deep(.catalog-tree .el-tree-node__expand-icon) { + display: none; + pointer-events: none; +} + :deep(.el-tree-node__content:hover) { background: var(--tablet-surface-muted); } @@ -2350,6 +2274,13 @@ loadCatalog() padding: 10px 8px; } +.tablet-shell.is-touch-tablet .agenda-entry-btn { + height: 34px; + padding: 0 16px; + font-size: 14px; + border-radius: 7px; +} + .tablet-shell.is-touch-tablet .center-header { padding: 12px 14px 10px; } @@ -2358,6 +2289,10 @@ loadCatalog() font-size: clamp(22px, 2.8vw, 34px); } +.tablet-shell.is-touch-tablet .center-title.is-file-title { + font-size: clamp(18px, 2.1vw, 24px); +} + .tablet-shell.is-touch-tablet .center-sub { margin-top: 8px; font-size: clamp(14px, 1.8vw, 19px); @@ -2368,32 +2303,6 @@ loadCatalog() gap: 6px; } -.tablet-shell.is-touch-tablet .agenda-overview { - padding: 14px 14px calc(16px + env(safe-area-inset-bottom, 0px)); -} - -.tablet-shell.is-touch-tablet .agenda-content { - gap: 10px; -} - -.tablet-shell.is-touch-tablet .agenda-card { - grid-template-columns: 42px minmax(0, 1fr); - gap: 8px; -} - -.tablet-shell.is-touch-tablet .agenda-index { - height: 26px; - font-size: 13px; -} - -.tablet-shell.is-touch-tablet .agenda-name { - font-size: 14px; -} - -.tablet-shell.is-touch-tablet .agenda-meta { - font-size: 12px; -} - .tablet-shell.is-touch-tablet .side-tabbar { padding: 8px; } @@ -2443,15 +2352,6 @@ loadCatalog() font-size: 16px; } -.tablet-shell.is-compact-tablet .agenda-content { - grid-template-columns: minmax(0, 1fr); -} - -.tablet-shell.is-compact-tablet .agenda-projects-panel, -.tablet-shell.is-compact-tablet .agenda-experts-panel { - padding: 10px; -} - .tablet-shell.is-compact-tablet .ai-summary-wrap { max-height: 188px; } @@ -2460,6 +2360,10 @@ loadCatalog() font-size: clamp(20px, 2.4vw, 28px); } +.tablet-shell.is-dense-tablet .center-title.is-file-title { + font-size: clamp(16px, 2vw, 21px); +} + .tablet-shell.is-dense-tablet .center-sub { font-size: 14px; } @@ -2468,8 +2372,7 @@ loadCatalog() font-size: 13px; } -.tablet-shell.is-dense-tablet .left-sub, -.tablet-shell.is-dense-tablet .agenda-sub { +.tablet-shell.is-dense-tablet .left-sub { font-size: 12px; }