chore: initialize qiming workspace repository
This commit is contained in:
@@ -0,0 +1,421 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user