Files
qiming/qiming-mobile/subpackages/pages/chat-conversation-component/components/file-tree/file-tree.uvue

422 lines
11 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>