<template>
|
<el-card class="archive-management-card">
|
<div class="left">
|
<div class="left-content">
|
<div class="tree-header">
|
<h3>文档管理</h3>
|
<el-button type="primary" size="small" @click="append('')" icon="Plus"
|
>新增</el-button
|
>
|
</div>
|
|
<!-- 搜索框 -->
|
<div class="search-box">
|
<el-input
|
v-model="filterText"
|
placeholder="输入关键字进行搜索"
|
size="small"
|
clearable
|
@input="handleFilter"
|
>
|
<template #prefix>
|
<el-icon><Search /></el-icon>
|
</template>
|
</el-input>
|
</div>
|
|
<div class="tree-container">
|
<el-tree
|
ref="treeRef"
|
:data="treeData"
|
:props="props"
|
:filter-node-method="filterNode"
|
:expand-on-click-node="false"
|
:default-expand-all="false"
|
node-key="id"
|
@node-click="handleNodeClick"
|
class="custom-tree"
|
>
|
<template #default="{ node, data }">
|
<div class="tree-node-content" @dblclick="headerDbClick(data)">
|
<div class="node-icon">
|
<el-icon
|
v-if="!node.isLeaf"
|
:class="{ expanded: node.expanded }"
|
>
|
<Folder />
|
</el-icon>
|
<el-icon v-else>
|
<Document />
|
</el-icon>
|
</div>
|
|
<div class="node-label">
|
<span v-if="!data.isEdit" class="label-text">{{
|
node.label
|
}}</span>
|
<el-input
|
v-else
|
:ref="(el) => setInputRef(el, data)"
|
placeholder="请输入节点名称"
|
v-model="newName"
|
@blur="($event) => handleInputBlur($event, data, node)"
|
@keyup.enter="
|
($event) => handleInputBlur($event, data, node)
|
"
|
size="small"
|
class="tree-input"
|
autofocus
|
/>
|
</div>
|
<div class="node-actions" v-show="!data.isEdit">
|
<el-button
|
link
|
size="small"
|
@click.stop="append(data)"
|
icon="Plus"
|
title="新增子节点"
|
></el-button>
|
<el-button
|
link
|
size="small"
|
@click.stop="remove(node, data)"
|
icon="Delete"
|
title="删除"
|
></el-button>
|
</div>
|
</div>
|
</template>
|
</el-tree>
|
</div>
|
</div>
|
</div>
|
<div class="right">
|
<el-row :gutter="24">
|
<el-col :span="2" :offset="20"
|
><el-button :icon="Delete" type="danger" @click="delHandler">删除</el-button></el-col
|
>
|
<el-col :span="2"
|
><el-button
|
:icon="Plus"
|
type="primary"
|
@click="add"
|
:disabled="!tableData.length"
|
>新增</el-button
|
></el-col
|
>
|
</el-row>
|
<ETable
|
:maxHeight="1200"
|
:loading="loading"
|
:table-data="tableData"
|
:columns="columns"
|
@selection-change="handleSelectionChange"
|
@edit="handleEdit"
|
:show-selection="true"
|
:border="true"
|
>
|
</ETable>
|
<Pagination
|
:total="total"
|
:page="queryParams.current"
|
:limit="queryParams.pageSize"
|
:show-total="true"
|
@pagination="handlePageChange"
|
:layout="'total, prev, pager, next, jumper'"
|
></Pagination>
|
</div>
|
<archiveDialog
|
v-model:centerDialogVisible="dialogVisible"
|
@centerDialogVisible="centerDialogVisible"
|
:row="row"
|
@submitForm="submitForm"
|
>
|
</archiveDialog>
|
</el-card>
|
</template>
|
<script setup>
|
import { onMounted, ref, nextTick, reactive } from "vue";
|
import ETable from "@/components/Table/ETable.vue";
|
import { ElButton, ElInput, ElIcon, ElMessage } from "element-plus";
|
import archiveDialog from "./mould/archiveDialog.vue";
|
import Pagination from "@/components/Pagination/index.vue";
|
import {
|
Plus,
|
Search,
|
Folder,
|
Document,
|
Delete,
|
} from "@element-plus/icons-vue";
|
import {
|
getTree,
|
addOrEditTree,
|
delTree,
|
getArchiveList,
|
addOrEditArchive,
|
delArchive,
|
} from "@/api/archiveManagement";
|
const dialogVisible = ref(false); // 控制归档对话框显示
|
const loading = ref(false);
|
const tableData = ref([]);
|
const treeData = ref([]);
|
const newName = ref("");
|
const inputRefs = ref(new Map()); // 存储输入框引用
|
const filterText = ref(""); // 搜索关键字
|
const treeRef = ref(); // 树组件引用
|
const total = ref(0); // 总记录数
|
const columns = [
|
{ prop: "name", label: "名称", minWidth: 180 },
|
{ prop: "type", label: "类型", minWidth: 120 },
|
{ prop: "status", label: "状态", minWidth: 100 },
|
];
|
const selectedRows = reactive([]); // 存储选中行数据
|
const handleSelectionChange = (selection) => {
|
selectedRows.splice(0, selectedRows.length, ...selection);
|
};
|
const queryParams = reactive({
|
searchText: "",
|
current: 1,
|
pageSize: 10, // 固定每页10条
|
treeId: null, // 当前树节点ID
|
});
|
// 搜索过滤功能
|
const handleFilter = () => {
|
treeRef.value?.filter(filterText.value);
|
};
|
const row = ref({}); // 当前选中行数据
|
const filterNode = (value, data) => {
|
if (!value) return true;
|
return data.name?.toLowerCase().includes(value.toLowerCase());
|
};
|
const submitForm = async (res) => {
|
console.log("提交表单回调:", res);
|
|
if (res && res.code === 200) {
|
ElMessage.success("操作成功");
|
// 刷新列表数据
|
await getArchiveListData();
|
} else {
|
ElMessage.error("操作失败: " + (res?.message || "未知错误"));
|
}
|
}
|
const centerDialogVisible = (val) => {
|
console.log(val);
|
};
|
// 处理节点点击
|
const handleNodeClick = async (data) => {
|
console.log("点击节点:", data);
|
// 切换节点时重置到第一页
|
queryParams.current = 1;
|
queryParams.treeId = data.id;
|
getArchiveListData();
|
};
|
// add
|
const add = () => {
|
row.value = {}; // 清空行数据,确保是新增模式
|
dialogVisible.value = true;
|
newName.value = ""; // 清空输入框
|
};
|
// 处理分页变化
|
const handlePageChange = ({ page }) => {
|
console.log("分页变化:", { page });
|
queryParams.current = page;
|
// pageSize 固定为20,不再从参数中获取
|
getArchiveListData();
|
};
|
|
const getArchiveListData = async () => {
|
try {
|
loading.value = true;
|
console.log("获取归档列表数据", {
|
treeId: queryParams.treeId,
|
current: queryParams.current,
|
pageSize: queryParams.pageSize,
|
});
|
|
let res = await getArchiveList({
|
treeId: queryParams.treeId,
|
current: queryParams.current,
|
size: queryParams.pageSize,
|
});
|
|
if (res.code !== 200) {
|
ElMessage.error("获取数据失败: " + res.message);
|
tableData.value = [];
|
total.value = 0;
|
return;
|
}
|
|
tableData.value = res.data?.records || res.data || [];
|
total.value = res.data?.total || 0;
|
// 确保分页参数正确更新
|
if (res.data?.current) {
|
queryParams.current = res.data.current;
|
}
|
// pageSize 固定为20,不从后端获取
|
|
console.log("数据更新完成:", {
|
total: total.value,
|
current: queryParams.current,
|
pageSize: queryParams.pageSize,
|
records: tableData.value.length,
|
});
|
} catch (error) {
|
console.error("获取归档列表失败:", error);
|
ElMessage.error("获取数据失败");
|
tableData.value = [];
|
total.value = 0;
|
} finally {
|
loading.value = false;
|
}
|
};
|
const delHandler = async () => {
|
if (selectedRows.length === 0) {
|
ElMessage.warning("请选择要删除的数据");
|
return;
|
}
|
try {
|
const ids = selectedRows.map((row) => row.id);
|
console.log(ids)
|
const { code, msg } = await delArchive(ids);
|
if (code !== 200) {
|
ElMessage.warning("删除失败: " + msg);
|
} else {
|
ElMessage.success("删除成功");
|
// 删除成功后重新获取数据
|
await getArchiveListData();
|
}
|
} catch (error) {
|
console.error("删除归档失败:", error);
|
ElMessage.error("删除归档失败");
|
}
|
};
|
// 双击编辑节点
|
const headerDbClick = (comeTreeData) => {
|
comeTreeData.isEdit = true;
|
newName.value = comeTreeData.name;
|
nextTick(() => {
|
const inputEl = inputRefs.value.get(comeTreeData.id || comeTreeData);
|
if (inputEl) {
|
inputEl.focus();
|
inputEl.select();
|
}
|
});
|
};
|
|
// 设置输入框引用的方法
|
const setInputRef = (el, data) => {
|
if (el) {
|
inputRefs.value.set(data.id || data, el);
|
if (data.isEdit) {
|
nextTick(() => {
|
// el.focus();
|
// el.select();
|
});
|
}
|
}
|
};
|
|
// 处理输入框失焦
|
const handleInputBlur = async (event, comeTreeData, node) => {
|
if (!comeTreeData.isEdit) return; // 如果不是编辑状态,直接返回
|
if (event.relatedTarget && event.relatedTarget.tagName === "BUTTON") {
|
return;
|
}
|
comeTreeData.isEdit = false;
|
const newValue = newName.value.trim();
|
if (comeTreeData.name === newValue) {
|
console.log("没有修改内容");
|
return;
|
}
|
if (newValue === "") {
|
console.warn("输入不能为空");
|
newName.value = comeTreeData.name || "新节点";
|
return;
|
}
|
try {
|
comeTreeData.name = newValue;
|
// 获取父节点的id - 通过 node 参数更准确地获取
|
let parentId = null;
|
if (node && node.parent && node.parent.data) {
|
parentId = node.parent.data.id;
|
}
|
await addOrEditTree({
|
name: newValue,
|
parentId: parentId || null, // 如果没有父节点,则为 null
|
});
|
} catch (error) {
|
console.error("存储失败", error);
|
comeTreeData.name = comeTreeData.name || "新节点";
|
}
|
console.log("保存成功:", newValue);
|
};
|
|
onMounted(async () => {
|
await getList();
|
});
|
const props = {
|
label: "name",
|
children: "children", // 改为 children 以匹配标准结构
|
isLeaf: "leaf",
|
};
|
|
const remove = async (node, data) => {
|
console.log("删除节点:", data);
|
if (!data || !data.id) {
|
console.warn("无法删除未定义或无效的节点");
|
return;
|
}
|
console.log("删除节点 ID:", data.id);
|
let { code, msg } = await delTree([data.id]);
|
if (code !== 200) {
|
ElMessage.warning("删除失败, " + msg);
|
} else {
|
ElMessage.success("删除成功");
|
}
|
await getList();
|
};
|
|
const append = async (data) => {
|
if (data === "") {
|
// 新增根节点
|
console.log("新增根节点");
|
const newNode = {
|
id: Date.now(),
|
name: "新节点",
|
isEdit: true,
|
};
|
treeData.value.push(newNode);
|
newName.value = "新节点";
|
|
nextTick(() => {
|
const inputEl = inputRefs.value.get(newNode.id || newNode);
|
if (inputEl) {
|
inputEl.focus();
|
inputEl.select();
|
}
|
});
|
} else {
|
const hasChildren = data.children && data.children.length > 0;
|
const nodeKey = data.id || data;
|
const node = treeRef.value?.getNode(nodeKey);
|
const isExpanded = node?.expanded; // 如果有子级且未展开,先展开节点
|
if (hasChildren && !isExpanded) {
|
console.log(treeRef.value, "展开节点", nodeKey);
|
if (
|
treeRef.value &&
|
treeRef.value.store &&
|
treeRef.value.store.nodesMap[nodeKey]
|
) {
|
treeRef.value.store.nodesMap[nodeKey].expanded = true;
|
}
|
}
|
|
const newNode = {
|
id: Date.now(),
|
name: "新子节点",
|
isEdit: true,
|
};
|
|
if (!data.children) {
|
data.children = [];
|
}
|
data.children.push(newNode);
|
newName.value = "新子节点";
|
|
// 根据是否需要展开来决定延迟时间
|
const delay = hasChildren && !isExpanded ? 200 : 50;
|
|
// 等待DOM更新完成后再聚焦
|
nextTick(() => {
|
setTimeout(() => {
|
const inputEl = inputRefs.value.get(newNode.id || newNode);
|
if (inputEl) {
|
inputEl.focus();
|
inputEl.select();
|
|
// 滚动到新增的节点位置
|
inputEl.$el?.scrollIntoView?.({
|
behavior: "smooth",
|
block: "nearest",
|
});
|
}
|
}, delay);
|
});
|
}
|
};
|
|
const handleEdit = (rows) => {
|
row.value = rows;
|
dialogVisible.value = true;
|
console.log("编辑节点:", row.value);
|
};
|
|
// 移除懒加载,直接获取数据
|
const getList = async () => {
|
try {
|
let res = await getTree();
|
if (res.code === 200) {
|
treeData.value = res.data?.records || res.data || [];
|
} else {
|
console.error("Failed to fetch tree data:", res.message);
|
treeData.value = [];
|
}
|
} catch (error) {
|
console.error("获取树形数据失败:", error);
|
treeData.value = [];
|
}
|
};
|
</script>
|
<style scoped lang="scss">
|
.custom-tree-node {
|
flex: 1;
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
font-size: 14px;
|
padding-right: 8px;
|
}
|
|
// 树形菜单样式
|
.tree-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 16px;
|
padding-bottom: 12px;
|
border-bottom: 1px solid #e4e7ed;
|
|
h3 {
|
margin: 0;
|
font-size: 16px;
|
font-weight: 600;
|
color: #303133;
|
}
|
}
|
|
.search-box {
|
margin-bottom: 16px;
|
|
.el-input {
|
border-radius: 6px;
|
|
:deep(.el-input__wrapper) {
|
border-radius: 6px;
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
&:hover {
|
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.15);
|
}
|
|
&.is-focus {
|
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
|
}
|
}
|
}
|
}
|
|
.tree-container {
|
flex: 1;
|
overflow-y: auto;
|
border: 1px solid #dcdfe6;
|
border-radius: 8px;
|
background: #fff;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
.custom-tree {
|
padding: 8px;
|
background: transparent;
|
|
:deep(.el-tree-node) {
|
.el-tree-node__content {
|
height: 36px;
|
padding: 0 8px;
|
border-radius: 6px;
|
margin: 2px 0;
|
transition: all 0.2s ease;
|
|
&:hover {
|
background-color: #f0f9ff;
|
}
|
|
&.is-current {
|
background-color: #e6f7ff;
|
border: 1px solid #91d5ff;
|
}
|
}
|
|
.el-tree-node__expand-icon {
|
color: #606266;
|
font-size: 14px;
|
padding: 6px;
|
|
&.expanded {
|
transform: rotate(90deg);
|
}
|
|
&.is-leaf {
|
color: transparent;
|
}
|
}
|
}
|
}
|
}
|
|
.tree-node-content {
|
display: flex;
|
align-items: center;
|
width: 100%;
|
padding: 4px 0;
|
|
.node-icon {
|
margin-right: 8px;
|
color: #faad14;
|
display: flex;
|
align-items: center;
|
|
.el-icon {
|
font-size: 16px;
|
|
&.expanded {
|
color: #1890ff;
|
}
|
}
|
}
|
|
.node-label {
|
flex: 1;
|
min-width: 0;
|
|
.label-text {
|
font-size: 14px;
|
color: #303133;
|
cursor: pointer;
|
display: block;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
|
&:hover {
|
color: #1890ff;
|
}
|
}
|
}
|
|
.node-actions {
|
opacity: 0;
|
transition: opacity 0.2s ease;
|
display: flex;
|
|
.el-button {
|
padding: 4px;
|
margin-left: 4px;
|
color: #909399;
|
min-height: auto;
|
|
&:hover {
|
color: #1890ff;
|
background-color: #f0f9ff;
|
}
|
|
&.el-button--text:hover {
|
background-color: #f0f9ff;
|
}
|
}
|
}
|
|
&:hover .node-actions {
|
opacity: 1;
|
}
|
}
|
|
// 输入框样式美化
|
.tree-input {
|
flex: 1;
|
|
:deep(.el-input__wrapper) {
|
border-radius: 4px;
|
border: 1px solid #d9d9d9;
|
transition: all 0.2s ease;
|
|
&:hover {
|
border-color: #40a9ff;
|
}
|
|
&.is-focus {
|
border-color: #1890ff;
|
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
}
|
}
|
|
:deep(.el-input__inner) {
|
padding: 4px 8px;
|
font-size: 14px;
|
color: #303133;
|
|
&::placeholder {
|
color: #bfbfbf;
|
}
|
}
|
}
|
.el-card {
|
width: calc(100% - 40px);
|
height: calc(100vh - 130px);
|
margin: 20px;
|
box-sizing: border-box;
|
.left {
|
width: 30%;
|
height: calc(100vh - 160px);
|
background-color: #fafafa;
|
padding: 16px;
|
float: left;
|
box-sizing: border-box;
|
border-radius: 8px;
|
|
.left-content {
|
width: 100%;
|
height: 100%;
|
display: flex;
|
flex-direction: column;
|
}
|
}
|
.right {
|
width: 70%;
|
height: calc(100vh - 160px);
|
padding: 0px 10px;
|
float: left;
|
}
|
}
|
.archive-management-card {
|
margin: 0;
|
}
|
</style>
|