前端部分更新
This commit is contained in:
@@ -0,0 +1,596 @@
|
||||
<template>
|
||||
<div class="mod-weather__real-time-monitoring" v-loading="state.loading">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2 class="page-title">631气象信息</h2>
|
||||
</div>
|
||||
<div class="page-header-actions">
|
||||
<el-tag type="info">层级 {{ state.selectedPath.length }} 级</el-tag>
|
||||
<el-tag type="success">文件 {{ state.fileItems.length }}</el-tag>
|
||||
<el-button type="primary" plain icon="Refresh" @click="refresh">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-alert v-if="state.errorMessage" :title="state.errorMessage" type="error" :closable="false" show-icon class="mb-16" />
|
||||
|
||||
<div class="dept-bar" v-if="levelRows.length">
|
||||
<div class="level-row" v-for="(row, rowIdx) in levelRows" :key="rowIdx">
|
||||
<span class="level-label" v-if="row.label">{{ row.label }}</span>
|
||||
<div class="level-tabs">
|
||||
<div v-for="tab in row.tabs" :key="tab.id" class="level-tab" :class="[{ 'is-active': row.selectedId === tab.id }, `level-${rowIdx}`]" @click="selectTab(rowIdx, tab)">
|
||||
{{ tab.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-body" v-if="state.fileItems.length">
|
||||
<aside class="file-list-panel">
|
||||
<div v-for="(item, index) in state.fileItems" :key="item.anchorId" class="file-list-item" :class="{ 'is-active': state.selectedFileIndex === index }" @click="selectFile(index)">
|
||||
<div class="file-list-item__thumb">
|
||||
<span class="file-list-item__icon file-list-item__icon--txt">文</span>
|
||||
</div>
|
||||
<span class="file-list-item__name">{{ item.displayName || `文件-${item.fileId}` }}</span>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="preview-panel" v-if="previewItem">
|
||||
<div class="preview-panel__header">
|
||||
<h3 class="preview-panel__title">{{ previewItem.displayName || `文件-${previewItem.fileId}` }}</h3>
|
||||
<el-button type="primary" plain size="small" :loading="state.copyingId === previewItem.anchorId" @click="copyText(previewItem)"> 复制全部文本 </el-button>
|
||||
</div>
|
||||
|
||||
<div class="preview-panel__body">
|
||||
<div class="preview-panel__text preview-panel__text--full">{{ previewItem.content || "暂无文本内容" }}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="preview-panel preview-panel--empty" v-else>
|
||||
<el-empty description="请在左侧选择要预览的文件" :image-size="120" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-else-if="!state.loading && !state.errorMessage" description="暂无可供展示的文件" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import app from "@/constants/app";
|
||||
import baseService from "@/service/baseService";
|
||||
import { copyToClipboard } from "@/utils/utils";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { computed, onMounted, reactive } from "vue";
|
||||
|
||||
interface DeptNode {
|
||||
id: string;
|
||||
name: string;
|
||||
children?: DeptNode[];
|
||||
}
|
||||
|
||||
interface DeptTab {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface DeptFileGroupVO {
|
||||
deptId: string;
|
||||
deptName: string;
|
||||
fileList: { displayName: string; fileId: string; type: string; content: string }[];
|
||||
}
|
||||
|
||||
interface FileItem {
|
||||
deptId: string;
|
||||
deptName: string;
|
||||
displayName: string;
|
||||
fileId: string;
|
||||
type: string;
|
||||
content: string;
|
||||
anchorId: string;
|
||||
}
|
||||
|
||||
const IMAGE_EXTS = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg"];
|
||||
|
||||
const isImageFile = (type: string) => {
|
||||
if (!type) return true;
|
||||
const lower = type.toLowerCase();
|
||||
return IMAGE_EXTS.some((ext) => lower.includes(ext));
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
loading: true,
|
||||
errorMessage: "",
|
||||
fileItems: [] as FileItem[],
|
||||
copyingId: "",
|
||||
deptTreeData: [] as DeptNode[],
|
||||
selectedPath: [] as string[],
|
||||
selectedFileIndex: 0
|
||||
});
|
||||
|
||||
const previewItem = computed(() => {
|
||||
if (state.selectedFileIndex >= 0 && state.selectedFileIndex < state.fileItems.length) {
|
||||
return state.fileItems[state.selectedFileIndex];
|
||||
}
|
||||
return state.fileItems.length > 0 ? state.fileItems[0] : null;
|
||||
});
|
||||
|
||||
const transformGroups = (list: DeptFileGroupVO[]) => {
|
||||
const items: FileItem[] = [];
|
||||
|
||||
list.forEach((group) => {
|
||||
(group.fileList || []).forEach((file) => {
|
||||
if (isImageFile(file.type)) return;
|
||||
items.push({
|
||||
deptId: group.deptId,
|
||||
deptName: group.deptName,
|
||||
displayName: file.displayName,
|
||||
fileId: file.fileId,
|
||||
type: file.type,
|
||||
content: file.content || "",
|
||||
anchorId: `txt-${file.fileId}`
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
state.fileItems = items;
|
||||
state.selectedFileIndex = 0;
|
||||
};
|
||||
|
||||
const activeDeptId = computed(() => {
|
||||
if (state.selectedPath.length > 0) {
|
||||
return state.selectedPath[state.selectedPath.length - 1];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
interface LevelRow {
|
||||
tabs: DeptTab[];
|
||||
selectedId: string | null;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const LEVEL_LABELS = ["一级", "二级", "三级", "四级", "五级", "六级"];
|
||||
|
||||
const levelRows = computed<LevelRow[]>(() => {
|
||||
const rows: LevelRow[] = [];
|
||||
let nodes: DeptNode[] = state.deptTreeData;
|
||||
|
||||
for (let i = 0; i < state.selectedPath.length; i++) {
|
||||
const selId = state.selectedPath[i];
|
||||
rows.push({
|
||||
tabs: nodes.map((n) => ({ id: n.id, name: n.name })),
|
||||
selectedId: selId,
|
||||
label: LEVEL_LABELS[i] || `${i + 1}级`
|
||||
});
|
||||
const selNode = nodes.find((n) => n.id === selId);
|
||||
if (selNode?.children?.length) {
|
||||
nodes = selNode.children;
|
||||
} else {
|
||||
nodes = [];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (nodes.length > 0) {
|
||||
rows.push({
|
||||
tabs: nodes.map((n) => ({ id: n.id, name: n.name })),
|
||||
selectedId: null,
|
||||
label: LEVEL_LABELS[rows.length] || `${rows.length + 1}级`
|
||||
});
|
||||
}
|
||||
|
||||
return rows;
|
||||
});
|
||||
|
||||
const loadData = async (deptId: string) => {
|
||||
state.loading = true;
|
||||
state.errorMessage = "";
|
||||
try {
|
||||
const res = await baseService.get("/filescan/record/tree", { deptId });
|
||||
transformGroups((res.data || []) as DeptFileGroupVO[]);
|
||||
selectFile(0);
|
||||
} catch (error: any) {
|
||||
state.errorMessage = error?.message || "加载失败";
|
||||
state.fileItems = [];
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadDeptTree = async (): Promise<string | null> => {
|
||||
try {
|
||||
const res = await baseService.get("/sys/dept/list");
|
||||
const treeData = (res.data || []) as DeptNode[];
|
||||
state.deptTreeData = treeData;
|
||||
|
||||
if (!treeData.length) return null;
|
||||
|
||||
const firstRoot = treeData[0];
|
||||
const firstChild = firstRoot.children?.length ? firstRoot.children[0] : firstRoot;
|
||||
|
||||
const buildPath = (nodes: DeptNode[], targetId: string, path: string[]): string[] | null => {
|
||||
for (const node of nodes) {
|
||||
const newPath = [...path, node.id];
|
||||
if (node.id === targetId) return newPath;
|
||||
if (node.children?.length) {
|
||||
const found = buildPath(node.children, targetId, newPath);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const path = buildPath(treeData, firstChild.id, []);
|
||||
state.selectedPath = path || [firstRoot.id];
|
||||
return firstChild.id;
|
||||
} catch {
|
||||
state.deptTreeData = [];
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const selectTab = (levelIdx: number, tab: DeptTab) => {
|
||||
state.selectedPath = [...state.selectedPath.slice(0, levelIdx), tab.id];
|
||||
state.fileItems = [];
|
||||
state.selectedFileIndex = 0;
|
||||
loadData(tab.id);
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
if (activeDeptId.value !== null) {
|
||||
loadData(activeDeptId.value);
|
||||
}
|
||||
};
|
||||
|
||||
const selectFile = (index: number) => {
|
||||
state.selectedFileIndex = index;
|
||||
};
|
||||
|
||||
const copyText = (item: FileItem) => {
|
||||
state.copyingId = item.anchorId;
|
||||
copyToClipboard(item.content || "");
|
||||
ElMessage.success("文本已复制成功");
|
||||
state.copyingId = "";
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
const deptId = await loadDeptTree();
|
||||
if (deptId !== null) {
|
||||
loadData(deptId);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mod-weather__real-time-monitoring {
|
||||
min-height: calc(100vh - 140px);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
margin: 8px 0 0;
|
||||
color: #909399;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.page-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.mb-16 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* ==== 部门选择栏 ==== */
|
||||
.dept-bar {
|
||||
margin-bottom: 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
padding: 10px 16px 8px;
|
||||
}
|
||||
|
||||
.level-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.level-row:last-child {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.level-label {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
flex-shrink: 0;
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
.level-tabs {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.level-tab {
|
||||
padding: 4px 16px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
background: #f5f7fa;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: color 0.2s, background 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.level-tab + .level-tab {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.level-tab:hover {
|
||||
color: var(--el-color-primary);
|
||||
background: #ecf5ff;
|
||||
}
|
||||
|
||||
.level-tab.is-active {
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.level-tab.is-active.level-0 {
|
||||
background: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.level-tab.is-active.level-1 {
|
||||
background: var(--el-color-primary-light-3);
|
||||
border-color: var(--el-color-primary-light-3);
|
||||
}
|
||||
|
||||
.level-tab.is-active.level-2 {
|
||||
background: var(--el-color-success);
|
||||
border-color: var(--el-color-success);
|
||||
}
|
||||
|
||||
.level-tab.is-active.level-3 {
|
||||
background: var(--el-color-warning);
|
||||
border-color: var(--el-color-warning);
|
||||
}
|
||||
|
||||
.level-tab.is-active.level-4 {
|
||||
background: var(--el-color-danger);
|
||||
border-color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.level-tab.is-active.level-5 {
|
||||
background: #8b5cf6;
|
||||
border-color: #8b5cf6;
|
||||
}
|
||||
|
||||
/* ==== 主体两栏 ==== */
|
||||
.page-body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* ==== 左侧文件列表 ==== */
|
||||
.file-list-panel {
|
||||
position: sticky;
|
||||
top: 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
max-height: calc(100vh - 320px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.file-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.file-list-item:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.file-list-item.is-active {
|
||||
background: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.file-list-item__thumb {
|
||||
flex-shrink: 0;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: #f0f2f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.file-list-item__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.file-list-item__icon {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.file-list-item__icon--img {
|
||||
background: var(--el-color-success);
|
||||
}
|
||||
|
||||
.file-list-item__icon--txt {
|
||||
background: var(--el-color-warning);
|
||||
}
|
||||
|
||||
.file-list-item__icon--other {
|
||||
background: #c0c4cc;
|
||||
}
|
||||
|
||||
.file-list-item__name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ==== 右侧预览面板 ==== */
|
||||
.preview-panel {
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.preview-panel--empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.preview-panel__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.preview-panel__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.preview-panel__title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
color: #303133;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.preview-panel__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.preview-panel__loading {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
padding: 60px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.preview-zoom-area {
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background: #f5f7fa;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.preview-panel__img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 60vh;
|
||||
border-radius: 8px;
|
||||
user-select: none;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.preview-panel__text--full {
|
||||
min-height: 200px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.preview-panel__text {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
line-height: 1.8;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
padding: 12px 14px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.image-card__error,
|
||||
.image-card__unsupported {
|
||||
color: #e6a23c;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
padding: 12px 14px;
|
||||
background: #fdf6ec;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* ==== 响应式 ==== */
|
||||
@media (max-width: 992px) {
|
||||
.page-body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-header,
|
||||
.preview-panel__header,
|
||||
.preview-panel__actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.level-tab {
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user