chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -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>