document.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. <template>
  2. <div>
  3. <el-card shadow="never">
  4. <template #header>
  5. <div class="flex justify-between items-center">
  6. <span class="text-lg font-bold">文档管理</span>
  7. <el-button type="primary" @click="handleBack">返回项目列表</el-button>
  8. </div>
  9. </template>
  10. <div class="content-wrapper">
  11. <div class="tree-container">
  12. <div class="tree-header">
  13. <el-button v-hasPermi="['document:folder:add']" type="primary" icon="Plus" size="small" @click="handleAddFolder">新建文件夹</el-button>
  14. </div>
  15. <el-scrollbar class="tree-scrollbar">
  16. <el-tree
  17. v-loading="loading"
  18. :data="treeData"
  19. :props="treeProps"
  20. node-key="id"
  21. default-expand-all
  22. :expand-on-click-node="false"
  23. >
  24. <template #default="{ node, data }">
  25. <span class="custom-tree-node">
  26. <el-icon>
  27. <Folder v-if="data.type === 0" />
  28. <Location v-else-if="data.type === 1" />
  29. <OfficeBuilding v-else-if="data.type === 2" />
  30. <Document v-else />
  31. </el-icon>
  32. <span class="node-label">{{ node.label }}</span>
  33. <span class="node-actions">
  34. <el-dropdown trigger="click" @command="handleCommand($event, data)">
  35. <span class="el-dropdown-link">
  36. <el-icon><MoreFilled /></el-icon>
  37. </span>
  38. <template #dropdown>
  39. <el-dropdown-menu>
  40. <el-dropdown-item command="add" v-hasPermi="['document:folder:add']">添加子节点</el-dropdown-item>
  41. <el-dropdown-item command="edit" v-hasPermi="['document:folder:edit']">编辑</el-dropdown-item>
  42. <el-dropdown-item command="delete" v-hasPermi="['document:folder:remove']">删除</el-dropdown-item>
  43. </el-dropdown-menu>
  44. </template>
  45. </el-dropdown>
  46. </span>
  47. </span>
  48. </template>
  49. </el-tree>
  50. </el-scrollbar>
  51. </div>
  52. <div class="content-container">
  53. <el-empty description="文档内容展示区域">
  54. </el-empty>
  55. </div>
  56. </div>
  57. </el-card>
  58. <!-- 添加文件夹对话框 -->
  59. <el-dialog v-model="dialog.visible" :title="dialog.title" width="600px" append-to-body>
  60. <el-form ref="folderFormRef" :model="form" :rules="rules" label-width="100px">
  61. <el-form-item label="名称" prop="name">
  62. <el-input v-model="form.name" placeholder="请输入名称" clearable />
  63. </el-form-item>
  64. <el-form-item label="类型" prop="type">
  65. <el-select v-model="form.type" placeholder="请选择类型" style="width: 100%;">
  66. <el-option
  67. v-for="typeOption in getAvailableTypes()"
  68. :key="typeOption.value"
  69. :label="typeOption.label"
  70. :value="typeOption.value"
  71. />
  72. </el-select>
  73. </el-form-item>
  74. <el-form-item label="限制层级" prop="restrictionLevel">
  75. <el-input-number v-model="form.restrictionLevel" :min="-1" style="width: 100%;" placeholder="请输入限制层级" />
  76. </el-form-item>
  77. <el-form-item label="备注" prop="note">
  78. <el-input v-model="form.note" type="textarea" :rows="4" placeholder="请输入备注" />
  79. </el-form-item>
  80. </el-form>
  81. <template #footer>
  82. <div class="dialog-footer">
  83. <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
  84. <el-button @click="cancel">取 消</el-button>
  85. </div>
  86. </template>
  87. </el-dialog>
  88. </div>
  89. </template>
  90. <script setup lang="ts">
  91. import { ref, reactive, onMounted, getCurrentInstance } from 'vue';
  92. import { listFolder, addFolder, delFolder, getFolder, updateFolder } from '@/api/document/folder';
  93. import { FolderListVO, FolderForm } from '@/api/document/folder/types';
  94. import { Folder, Document, Edit, Delete, Plus, MoreFilled, Location, OfficeBuilding } from '@element-plus/icons-vue';
  95. import { ElMessage, ElMessageBox } from 'element-plus';
  96. import type { FormInstance } from 'element-plus';
  97. import type { ComponentInternalInstance } from 'vue';
  98. interface Props {
  99. projectId?: number | string;
  100. }
  101. const props = defineProps<Props>();
  102. const emit = defineEmits<{
  103. back: [];
  104. }>();
  105. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  106. // 数据定义
  107. const loading = ref(false);
  108. const buttonLoading = ref(false);
  109. const treeData = ref<FolderListVO[]>([]);
  110. const folderFormRef = ref<FormInstance>();
  111. // 对话框
  112. const dialog = reactive({
  113. visible: false,
  114. title: '',
  115. isEdit: false
  116. });
  117. // 当前操作的节点
  118. const currentNode = ref<FolderListVO | null>(null);
  119. // 表单初始数据
  120. const initFormData: FolderForm = {
  121. id: undefined,
  122. projectId: undefined,
  123. parentId: undefined,
  124. type: 0,
  125. name: '',
  126. status: 0,
  127. note: '',
  128. restrictionLevel: -1
  129. };
  130. // 表单数据
  131. const form = ref<FolderForm>({ ...initFormData });
  132. // 表单验证规则
  133. const rules = {
  134. name: [
  135. { required: true, message: '请输入名称', trigger: 'blur' }
  136. ],
  137. type: [
  138. { required: true, message: '请选择类型', trigger: 'change' }
  139. ]
  140. };
  141. // 树形组件配置
  142. const treeProps = {
  143. children: 'children',
  144. label: 'name'
  145. };
  146. // 获取文件夹列表
  147. const getList = async () => {
  148. if (!props.projectId) {
  149. ElMessage.warning('项目ID不存在');
  150. return;
  151. }
  152. loading.value = true;
  153. try {
  154. const res = await listFolder({ projectId: props.projectId } as any);
  155. treeData.value = res.data || [];
  156. } catch (error) {
  157. ElMessage.error('获取文件夹列表失败');
  158. console.error(error);
  159. } finally {
  160. loading.value = false;
  161. }
  162. };
  163. // 返回项目列表
  164. const handleBack = () => {
  165. emit('back');
  166. };
  167. // 表单重置
  168. const reset = () => {
  169. form.value = { ...initFormData };
  170. folderFormRef.value?.resetFields();
  171. };
  172. // 取消按钮
  173. const cancel = () => {
  174. reset();
  175. dialog.visible = false;
  176. };
  177. // 新建文件夹(顶级)
  178. const handleAddFolder = () => {
  179. reset();
  180. currentNode.value = null;
  181. form.value.projectId = props.projectId;
  182. form.value.parentId = undefined;
  183. dialog.visible = true;
  184. dialog.title = '新增文件夹';
  185. dialog.isEdit = false;
  186. };
  187. // 新增子节点
  188. const handleAddChild = (data: FolderListVO) => {
  189. reset();
  190. currentNode.value = data;
  191. form.value.projectId = props.projectId;
  192. form.value.parentId = data.id;
  193. dialog.visible = true;
  194. dialog.title = '新增子节点';
  195. dialog.isEdit = false;
  196. };
  197. // 获取可选类型(根据父节点类型限制)
  198. const getAvailableTypes = () => {
  199. if (!currentNode.value) {
  200. // 顶级节点,可以选择所有类型
  201. return [
  202. { label: '文件夹', value: 0 },
  203. { label: '国家', value: 1 },
  204. { label: '中心', value: 2 }
  205. ];
  206. }
  207. const parentType = currentNode.value.type;
  208. if (parentType === 1 || parentType === 2) {
  209. // 父节点是国家或中心,子节点只能是中心或文件夹
  210. return [
  211. { label: '文件夹', value: 0 },
  212. { label: '中心', value: 2 }
  213. ];
  214. } else {
  215. // 父节点是文件夹,子节点只能是文件夹
  216. return [
  217. { label: '文件夹', value: 0 }
  218. ];
  219. }
  220. };
  221. // 下拉菜单命令处理
  222. const handleCommand = (command: string, data: FolderListVO) => {
  223. if (command === 'add') {
  224. handleAddChild(data);
  225. } else if (command === 'edit') {
  226. handleEdit(data);
  227. } else if (command === 'delete') {
  228. handleDelete(data);
  229. }
  230. };
  231. // 提交表单
  232. const submitForm = () => {
  233. folderFormRef.value?.validate(async (valid: boolean) => {
  234. if (valid) {
  235. // 如果是编辑,显示确认对话框
  236. if (dialog.isEdit) {
  237. try {
  238. const typeLabel = form.value.type === 0 ? '文件夹' : form.value.type === 1 ? '国家' : '中心';
  239. const confirmMessage = `
  240. <div style="text-align: left;">
  241. <p><strong>名称:</strong>${form.value.name}</p>
  242. <p><strong>类型:</strong>${typeLabel}</p>
  243. <p><strong>限制层级:</strong>${form.value.restrictionLevel}</p>
  244. <p><strong>备注:</strong>${form.value.note || '无'}</p>
  245. </div>
  246. `;
  247. await ElMessageBox.confirm(confirmMessage, '确认修改信息', {
  248. confirmButtonText: '确认',
  249. cancelButtonText: '取消',
  250. type: 'warning',
  251. dangerouslyUseHTMLString: true
  252. });
  253. } catch {
  254. return; // 用户取消
  255. }
  256. }
  257. buttonLoading.value = true;
  258. try {
  259. if (dialog.isEdit) {
  260. await updateFolder(form.value);
  261. proxy?.$modal.msgSuccess('修改成功');
  262. } else {
  263. await addFolder(form.value);
  264. proxy?.$modal.msgSuccess('新增成功');
  265. }
  266. dialog.visible = false;
  267. await getList();
  268. } catch (error) {
  269. console.error(dialog.isEdit ? '修改失败' : '新增失败', error);
  270. } finally {
  271. buttonLoading.value = false;
  272. }
  273. }
  274. });
  275. };
  276. // 编辑
  277. const handleEdit = async (data: FolderListVO) => {
  278. reset();
  279. loading.value = true;
  280. try {
  281. const res = await getFolder(data.id);
  282. Object.assign(form.value, res.data);
  283. currentNode.value = null; // 编辑时不限制类型
  284. dialog.visible = true;
  285. dialog.title = '修改文件夹';
  286. dialog.isEdit = true;
  287. } catch (error) {
  288. ElMessage.error('获取文件夹信息失败');
  289. console.error(error);
  290. } finally {
  291. loading.value = false;
  292. }
  293. };
  294. // 删除
  295. const handleDelete = async (data: FolderListVO) => {
  296. // 检查是否有子节点
  297. if (data.children && data.children.length > 0) {
  298. ElMessage.warning('该文件夹下存在子节点,无法删除');
  299. return;
  300. }
  301. try {
  302. await ElMessageBox.confirm(`确认删除 "${data.name}" 吗?`, '提示', {
  303. confirmButtonText: '确定',
  304. cancelButtonText: '取消',
  305. type: 'warning'
  306. });
  307. loading.value = true;
  308. await delFolder(data.id);
  309. ElMessage.success('删除成功');
  310. await getList();
  311. } catch (error: any) {
  312. // 用户取消删除或删除失败
  313. if (error !== 'cancel') {
  314. console.error('删除失败:', error);
  315. }
  316. } finally {
  317. loading.value = false;
  318. }
  319. };
  320. // 初始化
  321. onMounted(() => {
  322. getList();
  323. });
  324. </script>
  325. <style scoped lang="scss">
  326. .flex {
  327. display: flex;
  328. }
  329. .justify-between {
  330. justify-content: space-between;
  331. }
  332. .items-center {
  333. align-items: center;
  334. }
  335. .text-lg {
  336. font-size: 1.125rem;
  337. }
  338. .font-bold {
  339. font-weight: 700;
  340. }
  341. .content-wrapper {
  342. display: flex;
  343. height: calc(100vh - 250px);
  344. min-height: 500px;
  345. }
  346. .tree-container {
  347. width: 300px;
  348. border-right: 1px solid #e4e7ed;
  349. display: flex;
  350. flex-direction: column;
  351. }
  352. .tree-header {
  353. padding: 10px;
  354. border-bottom: 1px solid #e4e7ed;
  355. }
  356. .tree-scrollbar {
  357. flex: 1;
  358. overflow: hidden;
  359. :deep(.el-scrollbar__view) {
  360. padding: 10px;
  361. }
  362. }
  363. .custom-tree-node {
  364. flex: 1;
  365. display: flex;
  366. align-items: center;
  367. font-size: 14px;
  368. padding-right: 8px;
  369. .el-icon {
  370. margin-right: 8px;
  371. font-size: 16px;
  372. }
  373. .node-label {
  374. flex: 1;
  375. overflow: hidden;
  376. text-overflow: ellipsis;
  377. white-space: nowrap;
  378. }
  379. .node-actions {
  380. display: none;
  381. }
  382. &:hover .node-actions {
  383. display: inline-flex;
  384. gap: 4px;
  385. }
  386. }
  387. .el-dropdown-link {
  388. cursor: pointer;
  389. display: flex;
  390. align-items: center;
  391. font-size: 16px;
  392. &:hover {
  393. color: var(--el-color-primary);
  394. }
  395. }
  396. .content-container {
  397. flex: 1;
  398. padding: 20px;
  399. overflow: auto;
  400. }
  401. .detail-content {
  402. max-width: 800px;
  403. }
  404. </style>