<template>
<div class="app-container">
<el-row :gutter="20">
<!-- 项目树菜单 -->
<el-col :span="6" :xs="24">
<div class="head-container">
<el-input
v-model="projectName"
placeholder="请输入项目名称"
clearable
size="mini"
prefix-icon="el-icon-search"
style="margin-bottom: 20px"
@input="filterProjectTree"
/>
</div>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="info"
plain
icon="el-icon-sort"
size="mini"
@click="toggleExpandAll"
>{{ isExpandAll ? '折叠' : '展开' }}</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-folder-add"
size="mini"
@click="handleAddProject"
v-hasPermi="['cms:project:add']"
>新增项目</el-button>
</el-col>
</el-row>
<el-tree
v-if="refreshTable"
style="margin-top: 0.8rem;"
:data="filteredProjectTreeData"
:props="defaultProps"
:expand-on-click-node="false"
:filter-node-method="filterNode"
:highlight-current="true"
:default-expand-all="isExpandAll"
ref="projectTree"
empty-text="加载中,请稍候"
node-key="id"
@node-click="handleProjectClick"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<div class="tree-node-content">
<el-icon class="tree-icon" :size="16">
<svg-icon v-if="node.expanded" icon-class="folder-open" />
<svg-icon v-else icon-class="folder" />
</el-icon>
<div v-if="node.label && node.label.length > 25" class="node-label">
<el-tooltip :show-after="300" :content="node.label" placement="top-start">
<span>{{ ellipsis(node.label, 25) }}</span>
</el-tooltip>
</div>
<div v-else class="node-label">
<span>{{ node.label }}</span>
</div>
</div>
<span class="node-actions">
<el-button
type="text"
size="mini"
icon="el-icon-edit"
v-hasPermi="['cms:project:update']"
@click.stop="() => handleEditProject(data)"
></el-button>
<el-button
type="text"
size="mini"
icon="el-icon-plus"
v-hasPermi="['cms:project:add']"
@click.stop="() => handleAddSubProject(data)"
></el-button>
<el-button
type="text"
size="mini"
icon="el-icon-delete"
v-hasPermi="['cms:project:delete']"
@click.stop="() => handleDeleteProject(data)"
></el-button>
</span>
</span>
</el-tree>
</el-col>
<!-- 文档区域 -->
<el-col :span="18" :xs="24">
<!-- 文档列表视图 -->
<div v-if="activeView === 'list'" class="doc-list-container">
<el-form :model="docQueryParams" ref="docQueryForm" :inline="true" label-width="68px">
<el-form-item label="文档标题" prop="title">
<el-input
v-model="docQueryParams.title"
placeholder="请输入文档标题"
clearable
size="mini"
@keyup.enter.native="getDocumentList"
prefix-icon="el-icon-search"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="docQueryParams.status" placeholder="状态" size="mini" clearable>
<el-option label="编制中✍" value="0" />
<el-option label="待评审✊" value="1" />
<el-option label="已评审👍" value="2" />
<el-option label="修改中🔧" value="3" />
<el-option label="开发中💪" value="4" />
<el-option label="已完成开发🆗" value="5" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="getDocumentList">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetDocQuery">重置</el-button>
</el-form-item>
</el-form>
<div class="batch-actions">
<el-button
type="success"
icon="el-icon-download"
size="mini"
:disabled="selectedDocs.length === 0"
@click="handleBatchExport"
>
批量导出 ({{ selectedDocs.length }})
</el-button>
</div>
<el-table
v-loading="docLoading"
:data="documentList"
highlight-current-row
@row-click="handleRowClick"
@selection-change="handleSelectionChange"
ref="docTable"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" type="index" width="50" align="center" />
<el-table-column label="编号" align="center" prop="serialNum" />
<el-table-column label="标题" align="center" prop="title" class-name="small-padding fixed-width" width="200" :show-overflow-tooltip="true" sortable >
<template slot-scope="scope">
<div v-show="scope.row.title.startsWith('[诊断项]')">{{scope.row.title}}<span style="color: #303133;font-family: 'Arial', sans-serif;font-size: 12px;font-weight: normal;font-style: italic;">(v{{scope.row.articleVersion}}版本)</span></div>
<el-button v-show="!scope.row.title.startsWith('[诊断项]')" size="normal" type="text" icon="el-icon-tickets" @click="getArticleInfo(scope.row.id)">{{scope.row.title}}<span style="color: #303133;font-family: 'Arial', sans-serif;font-size: 12px;font-weight: normal;font-style: italic;">(v{{scope.row.articleVersion}}版本)</span></el-button>
</template>
</el-table-column>
<el-table-column label="进度" prop="status" width="100">
<template slot-scope="scope">
<el-tag v-if="scope.row.status === '0'" type="info" size="small">编制中✍</el-tag>
<el-tag v-if="scope.row.status === '1'" type="warning" size="small">待评审✊</el-tag>
<el-tag v-if="scope.row.status === '2'" type="success" size="small">已评审👍</el-tag>
<el-tag v-if="scope.row.status === '3'" type="success" size="small">修改中🔧</el-tag>
<el-tag v-if="scope.row.status === '4'" type="success" size="small">开发中💪</el-tag>
<el-tag v-if="scope.row.status === '5'" type="success" size="small">已完成开发🆗</el-tag>
</template>
</el-table-column>
<el-table-column label="创建人" prop="createBy" width="100" />
<el-table-column label="负责人" prop="director" width="100"/>
<el-table-column label="创建时间" prop="createTime" width="140" />
<el-table-column label="操作" width="180" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click.stop="handleEditDocument(scope.row)"></el-button>
<el-button
size="mini"
type="text"
icon="el-icon-download"
@click.stop="handleExportHttp(scope.row, true)"
title="导出详细"
></el-button>
<el-button
size="mini"
type="text"
icon="el-icon-download"
@click.stop="handleExportHttp(scope.row, false)"
title="导出简版"
></el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click.stop="handleDeleteDocument(scope.row)" v-hasPermi="['cms:project:deleteArticleFromProject']"></el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="docTotal>0"
:total="docTotal"
:page.sync="docQueryParams.pageNum"
:limit.sync="docQueryParams.pageSize"
@pagination="getDocumentList"
/>
</div>
<!-- 文档详情视图 -->
<div v-else-if="activeView === 'detail'" class="doc-detail-container">
<div class="doc-header">
<el-button type="text" icon="el-icon-back" @click="backToList">返回文档列表</el-button>
<h2 class="doc-title">
<el-icon class="title-icon"><svg-icon icon-class="document" /></el-icon>
{{ currentDocument.title }}
</h2>
</div>
<!-- 富文本编辑器 -->
<el-form :model="currentDocument" ref="docForm" label-width="80px">
<el-form-item label="文档内容">
<Tinymce :height='600' v-model='currentDocument.content'></Tinymce>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-check" @click="saveDocument">保存</el-button>
<el-button icon="el-icon-close" @click="backToList">取消</el-button>
</el-form-item>
</el-form>
</div>
</el-col>
</el-row>
<!-- 项目编辑对话框 -->
<el-dialog :title="projectDialogTitle" :visible.sync="projectDialogVisible" width="50%">
<el-form :model="projectForm" ref="projectForm" label-width="100px">
<el-form-item label="项目名称" prop="name" required>
<el-input v-model="projectForm.name" placeholder="请输入项目名称" prefix-icon="el-icon-folder" />
</el-form-item>
<el-form-item label="上级项目" prop="parentId">
<treeselect
v-model="projectForm.parentId"
:options="projectTreeData"
:normalizer="normalizer"
placeholder="选择上级项目"
/>
</el-form-item>
<el-form-item label="项目描述" prop="description">
<el-input type="textarea" v-model="projectForm.description" :rows="3" />
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="projectDialogVisible = false">取消</el-button>
<el-button type="primary" icon="el-icon-check" @click="saveProject">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
getData,
updateData
} from "@/api/cms/data";
import {
getProjectTree,
getDocuments,
saveProject,
updateProject,
deleteProject,
removeMenusFromProject
} from "@/api/cms/articleProject";
import Tinymce from '@/components/Tinymce';
import Treeselect from "@riophae/vue-treeselect";
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
import axios from 'axios';
export default {
name: "ProjectManagement",
components: { Treeselect, Tinymce },
data() {
return {
// 项目树相关数据
projectName: '',
projectTreeData: [],
filteredProjectTreeData: [],
refreshTable: true,
isExpandAll: true,
defaultProps: {
children: "children",
label: "label"
},
// 项目对话框相关
projectDialogVisible: false,
projectDialogTitle: '',
projectForm: {
id: null,
name: '',
parentId: null,
description: ''
},
// 文档列表相关
activeView: 'list', // 'list' 或 'detail'
docQueryParams: {
serialNums: null,
title: '',
status: '',
pageNum: 1,
pageSize: 10
},
documentList: [],
docTotal: 0,
docLoading: false,
selectedDocs: [], // 选中的文档
ids: [], // 选中文档ID集合
// 文档详情相关
currentDocument: {
id: null,
menuId: null,
title: '',
content: '',
status: '0'
},
// 当前选中的项目ID(用于删除操作)
currentProjectId: null,
// 导出相关数据
form: {
ids: [], // 导出的文档ID集合
notesExportFlag: 1 // 默认导出详细
}
};
},
created() {
this.getProjectTree();
},
methods: {
// ================= 项目树方法 =================
ellipsis(text, maxLength) {
return text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
},
toggleExpandAll() {
this.refreshTable = false;
this.isExpandAll = !this.isExpandAll;
this.$nextTick(() => {
this.refreshTable = true;
});
},
filterNode(value, data) {
if (!value) return true;
return data.label.toLowerCase().includes(value.toLowerCase());
},
filterProjectTree() {
this.$refs.projectTree.filter(this.projectName);
},
handleProjectClick(data) {
// 保存当前选中的项目ID
this.currentProjectId = data.id;
// 将serialNums字符串转换为数组
const serialNumsArray = data.serialNums ? data.serialNums.split(',').map(id => id.trim()) : [];
// 将数组转换回逗号分隔的字符串用于查询
this.docQueryParams.serialNums = serialNumsArray.join(',');
this.getDocumentList();
},
// ================= 项目管理方法 =================
handleAddProject() {
this.projectForm = {
id: null,
name: '',
parentId: null,
description: ''
};
this.projectDialogTitle = '新增项目';
this.projectDialogVisible = true;
},
handleAddSubProject(data) {
this.projectForm = {
id: null,
name: '',
parentId: data.id,
description: ''
};
this.projectDialogTitle = '新增子项目';
this.projectDialogVisible = true;
},
handleEditProject(data) {
this.projectForm = {
id: data.id,
name: data.name,
parentId: data.parentId,
description: data.description || ''
};
this.projectDialogTitle = '编辑项目';
this.projectDialogVisible = true;
},
// 递归收集项目ID
collectProjectIds(node) {
let ids = [node.id];
if (node.children && node.children.length > 0) {
node.children.forEach(child => {
ids = ids.concat(this.collectProjectIds(child));
});
}
return ids;
},
// 修改后的删除项目方法
handleDeleteProject(data) {
this.$confirm(`确定删除项目 "${data.name}" 及其所有子项目吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 递归收集所有节点ID
const ids = this.collectProjectIds(data);
// 调用批量删除API
deleteProject(ids).then(response => {
if (response.code === 200) {
this.$message.success('删除成功');
this.getProjectTree();
} else {
this.$message.error(response.msg || '删除失败');
}
}).catch(error => {
this.$message.error('删除失败: ' + error.message);
});
});
},
saveProject() {
this.$refs.projectForm.validate(valid => {
if (valid) {
const saveMethod = this.projectForm.id ? updateProject : saveProject;
saveMethod(this.projectForm).then(response => {
if (response.code === 200) {
this.$message.success('保存成功');
this.projectDialogVisible = false;
this.getProjectTree();
} else {
this.$message.error(response.msg || '保存失败');
}
}).catch(error => {
this.$message.error('保存失败: ' + error.message);
});
}
});
},
normalizer(node) {
return {
id: node.id,
label: node.name,
children: node.children && node.children.length > 0 ? node.children : undefined
};
},
// 获取项目树数据
getProjectTree() {
getProjectTree().then(response => {
if (response.code === 200 && response.data) {
// 处理根节点
const rootNode = response.data;
// 转换数据结构
this.projectTreeData = this.transformTreeData([rootNode]);
this.filteredProjectTreeData = [...this.projectTreeData];
// 默认展开根节点
this.$nextTick(() => {
if (this.projectTreeData.length > 0) {
this.$refs.projectTree.setCurrentKey(rootNode.id);
this.handleProjectClick(rootNode);
}
});
} else {
this.$message.error('获取项目树失败: ' + (response.msg || '未知错误'));
}
}).catch(error => {
console.error("获取项目树失败:", error);
this.$message.error('获取项目树失败: ' + error.message);
});
},
// 转换数据结构为el-tree需要的格式
transformTreeData(nodes) {
if (!nodes || !Array.isArray(nodes)) return [];
return nodes.map(node => ({
id: node.id,
label: node.name,
name: node.name,
parentId: node.parentId,
serialNums: node.serialNums,
description: node.description,
createBy: node.createBy,
createTime: node.createTime,
updateBy: node.updateBy,
updateTime: node.updateTime,
children: this.transformTreeData(node.children || []),
rawData: node
}));
},
// ================= 文档管理方法 =================
// 在 methods 中添加递归收集 serialNum 的方法
collectAllSerialNums(node) {
let serialNums = [];
// 添加当前节点的 serialNum
if (node.serialNums) {
const ids = node.serialNums.split(',').map(id => id.trim());
serialNums = [...serialNums, ...ids];
}
// 递归处理子节点
if (node.children && node.children.length > 0) {
node.children.forEach(child => {
serialNums = [...serialNums, ...this.collectAllSerialNums(child)];
});
}
return serialNums;
},
// 修改后的 getDocumentList 方法
getDocumentList() {
// 查找当前选中的项目节点
const currentNode = this.$refs.projectTree.getNode(this.currentProjectId);
if (!currentNode || !currentNode.data) {
this.$message.error('未找到选中的项目');
return;
}
// 收集当前节点及其所有子节点的 serialNum
const allSerialNums = this.collectAllSerialNums(currentNode.data);
// 去重并转换为逗号分隔的字符串
const uniqueSerialNums = [...new Set(allSerialNums)].join(',');
// 如果没有找到任何 serialNum,显示提示信息
if (!uniqueSerialNums) {
this.documentList = [];
this.docTotal = 0;
this.$message.info('当前项目及其子项目没有关联任何文档');
return;
}
// 更新查询参数
this.docQueryParams.serialNums = uniqueSerialNums;
this.docLoading = true;
getDocuments(this.docQueryParams).then(response => {
if (response.code === 200) {
this.documentList = response.rows;
this.docTotal = response.total;
this.selectedDocs = []; // 清空选择
this.ids = []; // 清空ID集合
} else {
this.$message.error(response.msg || '获取文档列表失败');
}
this.docLoading = false;
}).catch(error => {
this.$message.error('获取文档列表失败: ' + error.message);
this.docLoading = false;
});
},
resetDocQuery() {
this.docQueryParams.title = '';
this.docQueryParams.status = '';
this.getDocumentList();
},
handleEditDocument(row) {
getData(row.id).then(response => {
if (response.code === 200) {
this.currentDocument = {
id: response.data.id,
menuId: response.data.menuId,
title: response.data.title,
content: response.data.content,
status: response.data.status
};
this.activeView = 'detail';
} else {
this.$message.error(response.msg || '获取文档详情失败');
}
}).catch(error => {
this.$message.error('获取文档详情失败: ' + error.message);
});
},
handleDeleteDocument(row) {
this.$confirm(`确定从项目中移除文档 "${row.title}" 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 使用removeMenusFromProject API从项目中移除文档
// 参数: { projectId: 当前项目ID, menusIds: [文档ID] }
const params = {
projectId: this.currentProjectId,
menusIds: [row.id] // 使用文档ID数组
};
removeMenusFromProject(params).then(response => {
if (response.code === 200) {
this.$message.success('文档已从项目中移除');
this.getDocumentList();
} else {
this.$message.error(response.msg || '移除文档失败');
}
}).catch(error => {
this.$message.error('移除文档失败: ' + error.message);
});
});
},
handleRowClick(row) {
this.handleEditDocument(row);
},
backToList() {
this.activeView = 'list';
},
saveDocument() {
const saveMethod = this.currentDocument.id ? updateData : addData;
saveMethod(this.currentDocument).then(response => {
if (response.code === 200) {
this.$message.success('保存成功');
this.getDocumentList();
this.backToList();
} else {
this.$message.error(response.msg || '保存失败');
}
}).catch(error => {
this.$message.error('保存失败: ' + error.message);
});
},
// ================= 导出功能方法 =================
// 多选处理
handleSelectionChange(selection) {
this.selectedDocs = selection;
this.ids = selection.map(item => item.id);
},
// 导出文档(单篇)
handleExportHttp(row, isDetail) {
const params = {
ids: row.id,
notesExportFlag: isDetail ? 1 : 0
};
this.download(
'cms/data/createArticleOutputHttp',
params,
`word文档_${row.title}_${new Date().getTime()}.docx`,
{ timeout: 60000 }
);
},
// 批量导出文档
handleBatchExport() {
if (this.selectedDocs.length === 0) {
this.$message.warning('请选择要导出的文档');
return;
}
// 弹出选择导出类型的对话框
this.$confirm('请选择导出方式', '提示', {
distinguishCancelAndClose: true,
confirmButtonText: '导出详细',
cancelButtonText: '导出简版',
type: 'info'
}).then(() => {
// 导出详细
this.batchExportHttp(true);
}).catch(action => {
if (action === 'cancel') {
// 导出简版
this.batchExportHttp(false);
}
});
},
// 批量导出文档实现
batchExportHttp(isDetail) {
const params = {
ids: this.ids,
notesExportFlag: isDetail ? 1 : 0
};
this.download(
'cms/data/createArticleOutputHttp',
params,
`批量文档_${new Date().getTime()}.zip`,
{ timeout: 120000 } // 批量导出可能需要更长时间
);
},
// 下载方法实现
download(url, params, fileName, config = {}) {
// 显示加载提示
const loading = this.$loading({
lock: true,
text: '正在生成文档,请稍候...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
// 发送请求
axios.get(url, {
params: params,
responseType: 'blob',
timeout: config.timeout || 30000,
headers: {
'Authorization': `Bearer ${this.$store.getters.token}`
}
})
.then(response => {
// 创建下载链接
const blob = new Blob([response.data], { type: response.headers['content-type'] });
const downloadUrl = window.URL.createObjectURL(blob);
// 创建下载链接
const link = document.createElement('a');
link.href = downloadUrl;
link.download = fileName;
document.body.appendChild(link);
// 触发下载
link.click();
// 清理资源
window.URL.revokeObjectURL(downloadUrl);
document.body.removeChild(link);
this.$message.success('文档导出成功');
})
.catch(error => {
console.error('导出失败:', error);
// 处理错误响应
if (error.response) {
if (error.response.status === 500) {
this.$message.error('导出失败:服务器内部错误');
} else if (error.response.status === 401) {
this.$message.error('导出失败:未授权访问');
} else if (error.response.status === 404) {
this.$message.error('导出失败:API接口不存在');
} else {
this.$message.error(`导出失败:服务器错误 (${error.response.status})`);
}
} else if (error.message.includes('timeout')) {
this.$message.error('导出超时,请稍后再试');
} else {
this.$message.error('导出失败:' + (error.message || '未知错误'));
}
})
.finally(() => {
loading.close();
});
},
// 排序格式化方法
sortableFormatter(row, column) {
if (this.ids.includes(row.id)) {
return 1;
} else {
return 2;
}
},
// 跳转到文章详情页
getArticleInfo(articleId) {
// id加密
const articleIdStr = EncryptJs(articleId, "f1827100d08ff039", "ed363078893c0329");
console.log('阅读的文档跳转加密后的ID:'+articleIdStr)
let routeUrl = this.$router.resolve({
path: '/cms/doucumentView',
query: {
id: articleIdStr
}
});
window.open(routeUrl.href, '_blank');
}
}
};
</script>
<style scoped>
.app-container {
padding: 20px;
background-color: #f5f7fa;
}
.head-container {
padding: 10px;
background-color: #ffffff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
.tree-node-content {
display: flex;
align-items: center;
}
.tree-icon {
margin-right: 8px;
color: #409EFF;
}
.node-label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.node-actions {
display: flex;
align-items: center;
}
.doc-list-container {
background-color: #ffffff;
padding: 20px;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.batch-actions {
margin-bottom: 15px;
}
.doc-detail-container {
background-color: #ffffff;
padding: 20px;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.doc-header {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.doc-title {
display: flex;
align-items: center;
margin-left: 15px;
margin-bottom: 0;
font-size: 18px;
color: #303133;
}
.title-icon {
margin-right: 10px;
color: #409EFF;
}
.doc-title .doc-icon {
margin-right: 8px;
color: #909399;
}
.el-tree {
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 10px;
max-height: 70vh;
overflow-y: auto;
background-color: #ffffff;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.custom-tree-node .el-button {
padding: 4px;
margin-left: 5px;
}
.el-table {
margin-top: 10px;
border-radius: 4px;
overflow: hidden;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.el-form-item {
margin-bottom: 18px;
}
.el-tag {
margin: 2px;
}
/* 响应式调整 */
@media (max-width: 768px) {
.el-col-xs-24 {
width: 100%;
margin-bottom: 20px;
}
.doc-list-container, .doc-detail-container {
padding: 10px;
}
.doc-header h2 {
font-size: 16px;
}
}
</style>
点击表格的时候没有触发@click并且帮我优化树的ui,加上图标,帮我修复并给我正确的完整的代码
最新发布