422 lines
11 KiB
Plaintext
422 lines
11 KiB
Plaintext
<template>
|
||
<drawer-popup
|
||
ref="popupFileTreeRef"
|
||
direction="bottom"
|
||
height="85vh"
|
||
:title="t('Mobile.Common.workspace')"
|
||
@update-visible="updateVisible"
|
||
>
|
||
<view class="file-tree">
|
||
<view class="file-tree-wrapper" ref="fileTreeWrapperRef">
|
||
<!-- <text class="test-long-text">噶事根深蒂固萨达噶是的gsasag工地萨嘎搜嘎十多个噶事根深蒂固萨达噶是的gsasag工地萨嘎搜嘎十多个噶事根深蒂固萨达噶是的gsasag工地萨嘎搜嘎十多个噶事根深蒂固萨达噶是的gsasag工地萨嘎搜嘎十多个噶事根深蒂固萨达噶是的gsasag工地萨嘎搜嘎十多个噶事根深蒂固萨达噶是的gsasag工地萨嘎搜嘎十多个噶事根深蒂固萨达噶是的gsasag工地萨嘎搜嘎十多个噶事根深蒂固萨达噶是的gsasag工地萨嘎搜嘎十多个噶事根深蒂固萨达噶是的gsasag工地萨嘎搜嘎十多个</text> -->
|
||
|
||
<!-- 加载中, 刷新时,不显示加载图标 -->
|
||
<template v-if="isLoading && !files?.length">
|
||
<view class="file-tree-loading">
|
||
<image
|
||
class="icon-loading-image"
|
||
src="@/static/assets/icon_loading.svg"
|
||
mode="widthFix"
|
||
alt=""
|
||
/>
|
||
</view>
|
||
</template>
|
||
<!-- 有文件时,显示文件树 -->
|
||
<template v-else-if="!!files?.length">
|
||
<file-tree-node
|
||
v-for="node in files"
|
||
:key="node.id"
|
||
:node="node"
|
||
:level="0"
|
||
:expanded-folders="expandedFolders"
|
||
:selected-file-id="selectedFileId"
|
||
@toggle-folder="handleToggleFolder"
|
||
@select-file="handleSelectFile"
|
||
/>
|
||
</template>
|
||
<!-- 没有文件时,显示空状态 -->
|
||
<template v-else>
|
||
<view class="file-tree-empty">
|
||
<text>{{ t("Mobile.FileTree.noFiles") }}</text>
|
||
</view>
|
||
</template>
|
||
</view>
|
||
<view
|
||
class="download-button"
|
||
v-if="!isLoading && files?.length > 0"
|
||
:style="{
|
||
bottom: buttonPosition.bottom + 'rpx',
|
||
right: buttonPosition.right + 'rpx',
|
||
}"
|
||
@click="handleDownload"
|
||
@touchstart="handleTouchStart"
|
||
@touchmove="handleTouchMove"
|
||
@touchend="handleTouchEnd"
|
||
>
|
||
<text class="download-icon iconfont icon-a-Arrowdown"></text>
|
||
</view>
|
||
</view>
|
||
</drawer-popup>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { onMounted } from "vue";
|
||
import DrawerPopup from "@/components/drawer-popup/drawer-popup.uvue";
|
||
import type { FileNode } from "@/types/interfaces/agent";
|
||
import FileTreeNode from "./file-tree-node.uvue";
|
||
import { apiDownloadAllFiles } from "@/servers/agentDev.uts";
|
||
import { API_BASE_URL } from "@/constants/config";
|
||
import { SUCCESS_CODE } from "@/constants/codes.constants";
|
||
import { useI18n } from "@/utils/i18n";
|
||
|
||
const { t } = useI18n();
|
||
|
||
// #ifdef H5 || WEB
|
||
import { exportWholeProjectZipH5 } from "@/subpackages/utils/fileTree";
|
||
// #endif
|
||
// #ifdef MP-WEIXIN || MP-ALIPAY || MP-BAIDU || MP-TOUTIAO || MP-QQ
|
||
import { downloadFileInMiniProgram } from "@/subpackages/utils/fileTree";
|
||
// #endif
|
||
|
||
// 文件树弹窗引用
|
||
const popupFileTreeRef = ref<any>(null);
|
||
// 文件树包装器引用
|
||
const fileTreeWrapperRef = ref<any>(null);
|
||
// 已展开的文件夹ID集合
|
||
const expandedFolders = ref<Set<string>>(new Set());
|
||
// 选中的文件ID
|
||
const selectedFileId = ref<string>("");
|
||
|
||
// 下载按钮位置
|
||
const buttonPosition = ref<{ bottom: number; right: number }>({
|
||
bottom: 50,
|
||
right: 50,
|
||
});
|
||
|
||
// 拖拽相关状态
|
||
const isDragging = ref<boolean>(false);
|
||
const dragStartPos = ref<{ x: number; y: number }>({ x: 0, y: 0 });
|
||
const buttonStartPos = ref<{ bottom: number; right: number }>({
|
||
bottom: 50,
|
||
right: 50,
|
||
});
|
||
|
||
const props = withDefaults(
|
||
defineProps<{
|
||
cId: number;
|
||
files: FileNode[];
|
||
isLoading: boolean;
|
||
}>(),
|
||
{
|
||
cId: 0,
|
||
files: [],
|
||
isLoading: false,
|
||
},
|
||
);
|
||
|
||
const emit = defineEmits(["update-visible"]);
|
||
|
||
// 处理文件树弹窗可见性变化
|
||
const updateVisible = (value: boolean) => {
|
||
emit("update-visible", value);
|
||
};
|
||
|
||
// 导出项目
|
||
const handleExportProject = async () => {
|
||
// 检查项目ID是否有效
|
||
if (!props.cId) {
|
||
uni.showToast({
|
||
title: t("Mobile.FileTree.invalidConversationId"),
|
||
icon: "none",
|
||
});
|
||
return;
|
||
}
|
||
|
||
try {
|
||
uni.showLoading({ title: t("Mobile.FileTree.preparingDownload") });
|
||
|
||
// #ifdef H5 || WEB
|
||
// H5 环境:使用 blob 下载
|
||
const result = await apiDownloadAllFiles(props.cId);
|
||
const filename = `chat-${props.cId}.zip`;
|
||
await exportWholeProjectZipH5(
|
||
result.data as Blob,
|
||
result.headers,
|
||
filename,
|
||
);
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: t("Mobile.FileTree.exportSuccess"),
|
||
icon: "success",
|
||
});
|
||
// #endif
|
||
|
||
// #ifdef MP-WEIXIN || MP-ALIPAY || MP-BAIDU || MP-TOUTIAO || MP-QQ
|
||
// 小程序环境:直接使用下载地址(后端已打包成zip)
|
||
let downloadUrl = `${API_BASE_URL}/api/computer/static/download-all-files?cId=${props.cId}`;
|
||
const filename = `chat-${props.cId}.zip`;
|
||
|
||
// 下载并保存zip文件,返回保存路径(函数内部已弹窗提示用户)
|
||
const savedFilePath = await downloadFileInMiniProgram(
|
||
downloadUrl,
|
||
filename,
|
||
);
|
||
console.log("文件保存路径:", savedFilePath);
|
||
// #endif
|
||
} catch (error) {
|
||
uni.hideLoading();
|
||
// 改进错误处理,兼容不同的错误格式
|
||
const errorMessage =
|
||
(error as any)?.message || t("Mobile.FileTree.exportUnknownError");
|
||
|
||
uni.showToast({
|
||
title: t("Mobile.FileTree.exportFailed", { message: errorMessage }),
|
||
icon: "none",
|
||
});
|
||
console.error("导出失败:", error);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 下载文件
|
||
*/
|
||
const handleDownload = () => {
|
||
if (isDragging.value) {
|
||
return; // 如果正在拖拽,不触发下载
|
||
}
|
||
handleExportProject();
|
||
};
|
||
|
||
/**
|
||
* 加载按钮位置
|
||
*/
|
||
const loadButtonPosition = () => {
|
||
try {
|
||
const saved = uni.getStorageSync("file-tree-download-button-position");
|
||
if (saved) {
|
||
buttonPosition.value = saved;
|
||
}
|
||
} catch (e) {
|
||
console.error("加载按钮位置失败:", e);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 保存按钮位置
|
||
*/
|
||
const saveButtonPosition = () => {
|
||
try {
|
||
uni.setStorageSync(
|
||
"file-tree-download-button-position",
|
||
buttonPosition.value,
|
||
);
|
||
} catch (e) {
|
||
console.error("保存按钮位置失败:", e);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 触摸开始
|
||
*/
|
||
const handleTouchStart = (e: any) => {
|
||
const touch = e.touches?.[0];
|
||
if (!touch) return;
|
||
|
||
// 使用 clientX/clientY 获取触摸坐标
|
||
dragStartPos.value = {
|
||
x: touch.clientX,
|
||
y: touch.clientY,
|
||
};
|
||
buttonStartPos.value = { ...buttonPosition.value };
|
||
isDragging.value = false;
|
||
};
|
||
|
||
/**
|
||
* 触摸移动
|
||
*/
|
||
const handleTouchMove = (e: any) => {
|
||
const touch = e.touches?.[0];
|
||
if (!touch) return;
|
||
|
||
const currentX = touch.clientX;
|
||
const currentY = touch.clientY;
|
||
const deltaX = currentX - dragStartPos.value.x;
|
||
const deltaY = currentY - dragStartPos.value.y;
|
||
|
||
// 如果移动距离超过10px,认为是拖拽
|
||
if (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10) {
|
||
isDragging.value = true;
|
||
}
|
||
|
||
if (isDragging.value) {
|
||
// 获取系统信息,计算屏幕尺寸
|
||
const systemInfo = uni.getSystemInfoSync();
|
||
const screenWidth = systemInfo.windowWidth;
|
||
const screenHeight = systemInfo.windowHeight;
|
||
|
||
// 将px转换为rpx (1rpx = screenWidth / 750)
|
||
const rpxRatio = screenWidth / 750;
|
||
|
||
// 计算新位置
|
||
// deltaX: 向右移动为正,所以 right 应该减小
|
||
// deltaY: 向下移动为正,所以 bottom 应该减小(因为 bottom 是从底部向上的距离)
|
||
let newRight = buttonStartPos.value.right - deltaX / rpxRatio;
|
||
let newBottom = buttonStartPos.value.bottom - deltaY / rpxRatio;
|
||
|
||
// 限制在屏幕范围内
|
||
const buttonWidth = 120; // 按钮宽度 120rpx
|
||
const buttonHeight = 120; // 按钮高度 120rpx
|
||
const minRight = 0;
|
||
const maxRight = screenWidth / rpxRatio - buttonWidth;
|
||
const minBottom = 0;
|
||
const maxBottom = screenHeight / rpxRatio - buttonHeight;
|
||
|
||
newRight = Math.max(minRight, Math.min(maxRight, newRight));
|
||
newBottom = Math.max(minBottom, Math.min(maxBottom, newBottom));
|
||
|
||
buttonPosition.value = {
|
||
right: newRight,
|
||
bottom: newBottom,
|
||
};
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 触摸结束
|
||
*/
|
||
const handleTouchEnd = () => {
|
||
if (isDragging.value) {
|
||
saveButtonPosition();
|
||
}
|
||
isDragging.value = false;
|
||
};
|
||
|
||
// 组件挂载时加载位置
|
||
onMounted(() => {
|
||
loadButtonPosition();
|
||
});
|
||
|
||
/**
|
||
* 切换文件夹展开/收起
|
||
*/
|
||
const handleToggleFolder = (folderId: string) => {
|
||
const newExpanded = new Set(expandedFolders.value);
|
||
if (newExpanded.has(folderId)) {
|
||
newExpanded.delete(folderId);
|
||
} else {
|
||
newExpanded.add(folderId);
|
||
}
|
||
expandedFolders.value = newExpanded;
|
||
};
|
||
|
||
/**
|
||
* 选择文件
|
||
*/
|
||
const handleSelectFile = (fileNode: FileNode) => {
|
||
selectedFileId.value = fileNode.id;
|
||
if (fileNode?.isLink) {
|
||
uni.showToast({
|
||
title: t("Mobile.FileTree.linkFileNotSupported"),
|
||
icon: "none",
|
||
});
|
||
return;
|
||
}
|
||
|
||
uni.navigateTo({
|
||
url:
|
||
"/subpackages/pages/file-preview-page/file-preview-page?cId=" +
|
||
props.cId +
|
||
"&fileProxyUrl=" +
|
||
encodeURIComponent(fileNode.fileProxyUrl),
|
||
});
|
||
};
|
||
|
||
// 打开文件树弹窗
|
||
const open = () => {
|
||
popupFileTreeRef.value?.open();
|
||
};
|
||
|
||
// 关闭文件树弹窗
|
||
const close = () => {
|
||
popupFileTreeRef.value?.close();
|
||
};
|
||
|
||
// 暴露文件树弹窗引用
|
||
defineExpose({
|
||
open,
|
||
close,
|
||
});
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.file-tree {
|
||
position: relative;
|
||
height: 100%;
|
||
width: 100%;
|
||
overflow-x: scroll;
|
||
overflow-y: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
|
||
.file-tree-wrapper {
|
||
display: inline-block;
|
||
padding-bottom: 20rpx;
|
||
width: max-content;
|
||
min-width: 100%;
|
||
}
|
||
|
||
.test-long-text {
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.file-tree-loading {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 100%;
|
||
|
||
.icon-loading-image {
|
||
width: 48rpx;
|
||
height: 48rpx;
|
||
margin-right: 10rpx;
|
||
-webkit-touch-callout: none !important;
|
||
-webkit-user-select: none !important;
|
||
user-select: none !important;
|
||
}
|
||
}
|
||
|
||
.file-tree-empty {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 100%;
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.download-button {
|
||
position: fixed;
|
||
width: 120rpx;
|
||
height: 120rpx;
|
||
background-color: rgba(0, 0, 0, 0.6);
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 999;
|
||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
|
||
transition: background-color 0.2s;
|
||
touch-action: none;
|
||
|
||
&:active {
|
||
background-color: rgba(0, 0, 0, 0.8);
|
||
}
|
||
|
||
.download-icon {
|
||
font-size: 48rpx;
|
||
color: #fff;
|
||
line-height: 1;
|
||
}
|
||
}
|
||
}
|
||
</style>
|