|
@@ -0,0 +1,393 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <!-- 第一步:选择文件夹 -->
|
|
|
|
|
+ <el-dialog
|
|
|
|
|
+ v-model="dialogVisible"
|
|
|
|
|
+ :title="t('document.document.dialog.archiveDialog.title')"
|
|
|
|
|
+ width="600px"
|
|
|
|
|
+ append-to-body
|
|
|
|
|
+ @close="handleClose"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="archive-content">
|
|
|
|
|
+ <!-- 文档信息 -->
|
|
|
|
|
+ <div class="document-info" v-if="document">
|
|
|
|
|
+ <el-descriptions :column="1" size="default" border>
|
|
|
|
|
+ <el-descriptions-item :label="t('document.document.dialog.archiveDialog.documentName')">
|
|
|
|
|
+ {{ document.name }}
|
|
|
|
|
+ </el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item :label="t('document.document.dialog.archiveDialog.currentFolder')">
|
|
|
|
|
+ {{ getCurrentFolderName(document.folderId) }}
|
|
|
|
|
+ </el-descriptions-item>
|
|
|
|
|
+ </el-descriptions>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 文件夹选择 -->
|
|
|
|
|
+ <div class="folder-select-section">
|
|
|
|
|
+ <div class="section-title">{{ t('document.document.dialog.archiveDialog.selectFolder') }}</div>
|
|
|
|
|
+ <el-alert
|
|
|
|
|
+ v-if="!treeData || treeData.length === 0"
|
|
|
|
|
+ title="暂无可选文件夹"
|
|
|
|
|
+ type="warning"
|
|
|
|
|
+ :closable="false"
|
|
|
|
|
+ style="margin-bottom: 10px"
|
|
|
|
|
+ />
|
|
|
|
|
+ <el-tree
|
|
|
|
|
+ ref="treeRef"
|
|
|
|
|
+ :data="treeData"
|
|
|
|
|
+ :props="treeProps"
|
|
|
|
|
+ node-key="id"
|
|
|
|
|
+ :default-expanded-keys="defaultExpandedKeys"
|
|
|
|
|
+ :default-checked-keys="defaultCheckedKeys"
|
|
|
|
|
+ show-checkbox
|
|
|
|
|
+ check-strictly
|
|
|
|
|
+ :check-on-click-node="true"
|
|
|
|
|
+ @check="handleCheck"
|
|
|
|
|
+ class="folder-tree"
|
|
|
|
|
+ >
|
|
|
|
|
+ <template #default="{ node, data }">
|
|
|
|
|
+ <span class="custom-tree-node">
|
|
|
|
|
+ <el-icon v-if="data.type === 1"><OfficeBuilding /></el-icon>
|
|
|
|
|
+ <el-icon v-else-if="data.type === 2"><Location /></el-icon>
|
|
|
|
|
+ <el-icon v-else><Folder /></el-icon>
|
|
|
|
|
+ <span class="node-label">{{ node.label }}</span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-tree>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <div class="dialog-footer">
|
|
|
|
|
+ <el-button @click="handleCancel">{{ t('document.document.button.cancel') }}</el-button>
|
|
|
|
|
+ <el-button type="primary" @click="handleNext" :disabled="!selectedFolderId">
|
|
|
|
|
+ {{ t('document.document.button.confirm') }}
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-dialog>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 第二步:确认归档 -->
|
|
|
|
|
+ <el-dialog
|
|
|
|
|
+ v-model="confirmDialogVisible"
|
|
|
|
|
+ :title="t('document.document.dialog.archiveDialog.confirmTitle')"
|
|
|
|
|
+ width="500px"
|
|
|
|
|
+ append-to-body
|
|
|
|
|
+ @close="handleConfirmDialogClose"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="confirm-content">
|
|
|
|
|
+ <el-alert
|
|
|
|
|
+ :title="t('document.document.dialog.archiveDialog.confirmMessage')"
|
|
|
|
|
+ type="warning"
|
|
|
|
|
+ show-icon
|
|
|
|
|
+ :closable="false"
|
|
|
|
|
+ />
|
|
|
|
|
+ <div class="confirm-info" v-if="document">
|
|
|
|
|
+ <el-descriptions :column="1" size="default" border>
|
|
|
|
|
+ <el-descriptions-item :label="t('document.document.dialog.archiveDialog.documentName')">
|
|
|
|
|
+ {{ document.name }}
|
|
|
|
|
+ </el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item :label="t('document.document.dialog.archiveDialog.oldFolder')">
|
|
|
|
|
+ {{ getCurrentFolderName(document.folderId) }}
|
|
|
|
|
+ </el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item :label="t('document.document.dialog.archiveDialog.newFolder')">
|
|
|
|
|
+ {{ getSelectedFolderName() }}
|
|
|
|
|
+ </el-descriptions-item>
|
|
|
|
|
+ </el-descriptions>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <div class="dialog-footer">
|
|
|
|
|
+ <el-button @click="handleBackToSelect">{{ t('document.document.button.back') }}</el-button>
|
|
|
|
|
+ <el-button type="primary" @click="handleConfirm" :loading="loading">
|
|
|
|
|
+ {{ t('document.document.button.confirm') }}
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-dialog>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+import { ref, watch, computed } from 'vue';
|
|
|
|
|
+import { useI18n } from 'vue-i18n';
|
|
|
|
|
+import { OfficeBuilding, Location, Folder } from '@element-plus/icons-vue';
|
|
|
|
|
+
|
|
|
|
|
+interface Document {
|
|
|
|
|
+ id: number | string;
|
|
|
|
|
+ name: string;
|
|
|
|
|
+ folderId?: number | string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface FolderNode {
|
|
|
|
|
+ id: number | string;
|
|
|
|
|
+ name: string;
|
|
|
|
|
+ type?: number;
|
|
|
|
|
+ children?: FolderNode[];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface Props {
|
|
|
|
|
+ modelValue: boolean;
|
|
|
|
|
+ document?: Document | null;
|
|
|
|
|
+ treeData?: FolderNode[];
|
|
|
|
|
+ projectId?: number | string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface Emits {
|
|
|
|
|
+ (e: 'update:modelValue', value: boolean): void;
|
|
|
|
|
+ (e: 'confirm', data: { document: Document; folderId: number | string; projectId: number | string }): void;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const props = withDefaults(defineProps<Props>(), {
|
|
|
|
|
+ treeData: () => []
|
|
|
|
|
+});
|
|
|
|
|
+const emit = defineEmits<Emits>();
|
|
|
|
|
+
|
|
|
|
|
+const { t } = useI18n();
|
|
|
|
|
+
|
|
|
|
|
+const dialogVisible = ref(false);
|
|
|
|
|
+const confirmDialogVisible = ref(false);
|
|
|
|
|
+const loading = ref(false);
|
|
|
|
|
+const selectedFolderId = ref<number | string | undefined>(undefined);
|
|
|
|
|
+const treeRef = ref();
|
|
|
|
|
+const isSettingChecked = ref(false); // 添加标志位防止循环触发
|
|
|
|
|
+
|
|
|
|
|
+// 树配置
|
|
|
|
|
+const treeProps = {
|
|
|
|
|
+ children: 'children',
|
|
|
|
|
+ label: 'name'
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 默认展开的节点
|
|
|
|
|
+const defaultExpandedKeys = computed(() => {
|
|
|
|
|
+ if (props.document?.folderId) {
|
|
|
|
|
+ return [props.document.folderId];
|
|
|
|
|
+ }
|
|
|
|
|
+ return [];
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 默认选中的节点
|
|
|
|
|
+const defaultCheckedKeys = computed(() => {
|
|
|
|
|
+ if (props.document?.folderId) {
|
|
|
|
|
+ return [props.document.folderId];
|
|
|
|
|
+ }
|
|
|
|
|
+ return [];
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 监听modelValue变化
|
|
|
|
|
+watch(
|
|
|
|
|
+ () => props.modelValue,
|
|
|
|
|
+ (val) => {
|
|
|
|
|
+ dialogVisible.value = val;
|
|
|
|
|
+ if (val && props.document) {
|
|
|
|
|
+ console.log('打开归档对话框,树数据:', props.treeData);
|
|
|
|
|
+ console.log('当前文档:', props.document);
|
|
|
|
|
+ console.log('项目ID:', props.projectId);
|
|
|
|
|
+ // 默认选中当前文件夹
|
|
|
|
|
+ selectedFolderId.value = props.document.folderId;
|
|
|
|
|
+ // 设置树的选中状态
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ if (treeRef.value && props.document?.folderId) {
|
|
|
|
|
+ treeRef.value.setCheckedKeys([props.document.folderId]);
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 100);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
|
|
+// 监听dialogVisible变化
|
|
|
|
|
+watch(dialogVisible, (val) => {
|
|
|
|
|
+ if (!val && !confirmDialogVisible.value) {
|
|
|
|
|
+ // 只有在确认对话框也关闭的情况下才重置状态和通知父组件
|
|
|
|
|
+ emit('update:modelValue', val);
|
|
|
|
|
+ // 重置状态
|
|
|
|
|
+ selectedFolderId.value = undefined;
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// 处理复选框选中(实现单选效果)
|
|
|
|
|
+const handleCheck = (data: FolderNode, checked: any) => {
|
|
|
|
|
+ // 如果是程序设置的,跳过处理
|
|
|
|
|
+ if (isSettingChecked.value) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const clickedId = data.id;
|
|
|
|
|
+ const checkedKeys = checked.checkedKeys || [];
|
|
|
|
|
+
|
|
|
|
|
+ console.log('handleCheck 被调用', {
|
|
|
|
|
+ clickedId,
|
|
|
|
|
+ checkedKeys,
|
|
|
|
|
+ currentSelected: selectedFolderId.value
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 设置标志位,防止循环触发
|
|
|
|
|
+ isSettingChecked.value = true;
|
|
|
|
|
+
|
|
|
|
|
+ // 实现单选:只保留当前点击的节点
|
|
|
|
|
+ if (checkedKeys.includes(clickedId)) {
|
|
|
|
|
+ // 如果当前节点在选中列表中,说明是选中操作
|
|
|
|
|
+ selectedFolderId.value = clickedId;
|
|
|
|
|
+ console.log('选中文件夹:', clickedId);
|
|
|
|
|
+ // 清除所有选中,只保留当前节点
|
|
|
|
|
+ treeRef.value?.setCheckedKeys([clickedId]);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 如果当前节点不在选中列表中,说明是取消选中操作
|
|
|
|
|
+ // 不允许取消选中,重新选中该节点
|
|
|
|
|
+ console.log('不允许取消选中,保持选中状态');
|
|
|
|
|
+ treeRef.value?.setCheckedKeys([selectedFolderId.value || clickedId]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 延迟重置标志位
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ isSettingChecked.value = false;
|
|
|
|
|
+ }, 100);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 获取当前文件夹名称
|
|
|
|
|
+const getCurrentFolderName = (folderId: number | string | undefined): string => {
|
|
|
|
|
+ if (!folderId || !props.treeData || props.treeData.length === 0) return '-';
|
|
|
|
|
+ const folder = findFolderById(props.treeData, folderId);
|
|
|
|
|
+ return folder ? folder.name : '-';
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 获取选中的文件夹名称
|
|
|
|
|
+const getSelectedFolderName = (): string => {
|
|
|
|
|
+ if (!selectedFolderId.value || !props.treeData || props.treeData.length === 0) return '-';
|
|
|
|
|
+ const folder = findFolderById(props.treeData, selectedFolderId.value);
|
|
|
|
|
+ return folder ? folder.name : '-';
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 递归查找文件夹
|
|
|
|
|
+const findFolderById = (folders: FolderNode[] | undefined, id: number | string): FolderNode | null => {
|
|
|
|
|
+ if (!folders || folders.length === 0) return null;
|
|
|
|
|
+ for (const folder of folders) {
|
|
|
|
|
+ if (folder.id === id) {
|
|
|
|
|
+ return folder;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (folder.children && folder.children.length > 0) {
|
|
|
|
|
+ const found = findFolderById(folder.children, id);
|
|
|
|
|
+ if (found) return found;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 关闭对话框
|
|
|
|
|
+const handleClose = () => {
|
|
|
|
|
+ confirmDialogVisible.value = false;
|
|
|
|
|
+ dialogVisible.value = false;
|
|
|
|
|
+ emit('update:modelValue', false);
|
|
|
|
|
+ selectedFolderId.value = undefined;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 取消操作
|
|
|
|
|
+const handleCancel = () => {
|
|
|
|
|
+ confirmDialogVisible.value = false;
|
|
|
|
|
+ dialogVisible.value = false;
|
|
|
|
|
+ emit('update:modelValue', false);
|
|
|
|
|
+ selectedFolderId.value = undefined;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 下一步(改为直接显示确认对话框,不关闭当前对话框)
|
|
|
|
|
+const handleNext = () => {
|
|
|
|
|
+ if (!selectedFolderId.value) return;
|
|
|
|
|
+ confirmDialogVisible.value = true;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 返回选择(关闭确认对话框即可)
|
|
|
|
|
+const handleBackToSelect = () => {
|
|
|
|
|
+ confirmDialogVisible.value = false;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 确认对话框关闭
|
|
|
|
|
+const handleConfirmDialogClose = () => {
|
|
|
|
|
+ confirmDialogVisible.value = false;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 确认归档
|
|
|
|
|
+const handleConfirm = () => {
|
|
|
|
|
+ if (props.document && selectedFolderId.value && props.projectId) {
|
|
|
|
|
+ loading.value = true;
|
|
|
|
|
+ emit('confirm', {
|
|
|
|
|
+ document: props.document,
|
|
|
|
|
+ folderId: selectedFolderId.value,
|
|
|
|
|
+ projectId: props.projectId
|
|
|
|
|
+ });
|
|
|
|
|
+ // 注意:loading 和对话框关闭由父组件通过 closeLoading 和 closeDialog 方法控制
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 暴露方法供父组件调用(关闭loading)
|
|
|
|
|
+defineExpose({
|
|
|
|
|
+ closeLoading: () => {
|
|
|
|
|
+ loading.value = false;
|
|
|
|
|
+ },
|
|
|
|
|
+ closeDialog: () => {
|
|
|
|
|
+ confirmDialogVisible.value = false;
|
|
|
|
|
+ dialogVisible.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped lang="scss">
|
|
|
|
|
+.archive-content {
|
|
|
|
|
+ .document-info {
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .folder-select-section {
|
|
|
|
|
+ margin-top: 20px;
|
|
|
|
|
+
|
|
|
|
|
+ .section-title {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .folder-tree {
|
|
|
|
|
+ border: 1px solid #dcdfe6;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ padding: 10px;
|
|
|
|
|
+ max-height: 400px;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+
|
|
|
|
|
+ :deep(.el-tree-node__content) {
|
|
|
|
|
+ height: 32px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .custom-tree-node {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+
|
|
|
|
|
+ .el-icon {
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .node-label {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.confirm-content {
|
|
|
|
|
+ .el-alert {
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .confirm-info {
|
|
|
|
|
+ margin-top: 15px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dialog-footer {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: flex-end;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|