实现ai对话(电脑同步)

This commit is contained in:
2026-06-06 18:10:29 +08:00
parent dd4975fd2c
commit d284789240
23 changed files with 3768 additions and 1548 deletions

View File

@@ -475,6 +475,10 @@
// 跳转到工作区
const goWorkSpace = () => {
// 保存当前会话id确保从工作区返回时能恢复
if (currentSessionId.value) {
uni.setStorageSync('currentSessionId', currentSessionId.value)
}
uni.navigateTo({
url: '/pages/WorkSpace/WorkSpace'
})
@@ -732,6 +736,9 @@
// })
// }
// }
// AI 回复超时定时器
let aiResponseTimer = null
// 发送AI消息
const sendAIMessage = async () => {
try {
@@ -750,7 +757,16 @@
}
socketStore.send(data)
isThinking.value = true
await waitForAIResponse();
// 3分钟超时
aiResponseTimer = setTimeout(() => {
uni.showToast({
title: 'AI回复超时请重试',
icon: 'none'
})
isThinking.value = false
textMessage.value = ''
uploadedFiles.value = []
}, 180000)
} catch (error) {
isThinking.value = false
uni.showToast({
@@ -760,43 +776,20 @@
}
}
// 等待ai回复信息并保存到数据库
const waitForAIResponse = () => {
return new Promise((resolve, reject) => {
// 设置超时
const timeout = setTimeout(() => {
unwatch()
reject(new Error('等待AI回复超时'));
}, 180000); // 3分钟超时
const finish = async () => {
try {
console.log("ai返回的消息", socketStore.messageString);
console.log('AI回复结束刷新消息列表');
await takeConversationMessages();
resolve();
} catch (error) {
console.error('保存AI回复失败', error);
reject(error);
} finally {
isThinking.value = false
textMessage.value = ''
uploadedFiles.value = []
unwatch();
clearTimeout(timeout);
}
}
// 监听 isThinkingfalse → true 时不做处理true → false 时表示 AI 回复结束
const unwatch = watch(() => socketStore.isThinking, (newVal, oldVal) => {
console.log("isThinking 变化:", oldVal, "→", newVal);
// AI 回复结束:从 true 变为 false且已有回复内容
if (oldVal && !newVal && socketStore.messageString) {
finish()
}
})
})
}
// 全局监听 socketStore.isThinkingtrue → false 时表示 AI 回复结束
watch(() => socketStore.isThinking, (newVal, oldVal) => {
console.log("isThinking 变化:", oldVal, "→", newVal);
// AI 回复结束:从 true 变为 false且已有回复内容
if (oldVal && !newVal && socketStore.messageString) {
clearTimeout(aiResponseTimer)
console.log("ai返回的消息", socketStore.messageString);
console.log('AI回复结束刷新消息列表');
takeConversationMessages()
isThinking.value = false
textMessage.value = ''
uploadedFiles.value = []
}
})
const textMessage = ref('')
@@ -829,11 +822,13 @@
console.log("token:", userToken.value);
UserConversations.value = await getUserConversations(userToken.value) || [];
console.log("UserConversations:", UserConversations.value);
// 获取列表第一个会话
if (UserConversations.value.length > 0) {
currentSessionId.value = UserConversations.value[0]._id; // 假设会话对象有 id 字段
// 优先恢复已保存的会话id避免刷新到列表第一个
const savedSessionId = getCurrentSessionId();
if (savedSessionId && UserConversations.value.some(c => c._id === savedSessionId)) {
currentSessionId.value = savedSessionId;
} else if (UserConversations.value.length > 0) {
currentSessionId.value = UserConversations.value[0]._id;
} else {
// 如果没有会话,可以创建一个默认会话或设为空
currentSessionId.value = '';
}
console.log('保存会话id', currentSessionId.value);

View File

@@ -1,3 +1,87 @@
/* 新建文件夹弹窗样式 */
.create-folder-name {
display: flex;
width: 100%;
padding: 10rpx 20rpx;
box-sizing: border-box;
align-items: center;
background-color: rgba(194, 191, 211, 0.1);
border-radius: 10rpx;
}
.create-folder-name input {
box-sizing: border-box;
width: 100%;
}
.create-folder-name.has-value {
background-color: #ffffff;
border: 2rpx solid #aaaaff;
box-shadow: 0 0 15rpx #aaaaff;
}
/* 新建文件夹弹窗内部样式 */
.new-folder-name {
font-size: 28rpx;
color: #666;
margin-bottom: 12rpx;
}
.nf-path-label {
font-size: 26rpx;
color: #999;
margin-top: 24rpx;
margin-bottom: 8rpx;
}
.nf-path-display {
font-size: 28rpx;
color: #333;
padding: 12rpx 20rpx;
background: #f5f5f5;
border-radius: 10rpx;
word-break: break-all;
}
.nf-path-preview {
font-size: 28rpx;
color: #0073ff;
padding: 12rpx 20rpx;
background: rgba(0, 115, 255, 0.05);
border: 2rpx dashed rgba(0, 115, 255, 0.3);
border-radius: 10rpx;
word-break: break-all;
}
.option-wrapper {
display: flex;
justify-content: space-between;
gap: 20rpx;
margin-top: 40rpx;
}
.opt-btn {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
padding: 24rpx 0;
border-radius: 12rpx;
font-size: 30rpx;
}
.opt-cancel {
background: #f5f5f5;
color: #666;
}
.opt-confirm {
background: linear-gradient(135deg, #0073ff, #3ab0ff);
color: #fff;
}
.workspace-container {
display: flex;
flex-direction: column;
@@ -145,6 +229,7 @@
transform: translateX(-50%);
font-size: 32rpx;
}
.folder-null .iconfont {
font-size: 180rpx;
}
@@ -152,7 +237,7 @@
.folder-grid {
width: 100%;
display: grid;
grid-template-columns: repeat(3,1fr);
grid-template-columns: repeat(3, 1fr);
padding: 20rpx;
box-sizing: border-box;
gap: 20rpx;
@@ -165,6 +250,7 @@
justify-content: center;
align-items: center;
position: relative;
min-width: 0;
}
.folder-item.select-folder {
@@ -172,6 +258,19 @@
background: rgba(9, 9, 9, 0.1);
}
.folder-item .folder-name {
max-width: 150rpx;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
text-align: center;
line-height: 40rpx;
min-width: 0;
}
.folder-item-style {
font-size: 160rpx !important;
font-weight: 10rpx !important;
@@ -216,4 +315,4 @@
.workspace-footer .iconfont {
font-size: 40rpx !important;
}
}

View File

@@ -1,7 +1,40 @@
<template>
<view class="status-bar"></view>
<view class="workspace-container page-container">
<!-- ===== 新建文件夹设置弹窗 ===== -->
<view v-if="showNewFolder" class="popup-overlay" @click="closeNewFolderModal">
<view class="popup-card" @click.stop>
<!-- 关闭按钮 -->
<view class="close-popup-card" @click="closeNewFolderModal">
<view class="iconfont icon-quxiao"></view>
</view>
<!-- 标题 -->
<view class="popup-title">新建工作区目录</view>
<!-- 目录名称输入 -->
<view class="new-folder-name">目录名称</view>
<view class="create-folder-name" :class="{'has-value': isNFNFocused || newFolderName}">
<input v-model="newFolderName" @focus="isNFNFocused=true"
@blur="isNFNFocused=false" type="text" placeholder="输入目录名称,如:项目资料/设计图" />
</view>
<!-- 当前路径 -->
<view class="nf-path-label">当前路径</view>
<view class="nf-path-display">{{ currentFolderPath }}</view>
<!-- 路径预览 -->
<view class="nf-path-label">路径预览</view>
<view class="nf-path-preview">{{ folderPathPreview }}</view>
<!-- 底部按钮 -->
<view class="option-wrapper">
<view class="opt-btn opt-cancel" @click="closeNewFolderModal">取消</view>
<view class="opt-btn opt-confirm" @click="confirmCreateFolder">确认</view>
</view>
</view>
</view>
<view class="workspace-container page-container">
<view class="workspace-header">
<view class="custom-navbar">
<view class="navbar-left" @click="handleBackOrCheck ">
@@ -19,7 +52,7 @@
<view v-if="isMenuOpen" class="menu-card">
<view class="menu-card-item" @click="handleSelectFolder();handleMenu()">选择</view>
<view class="solid-line"></view>
<view class="menu-card-item">新建文件夹</view>
<view class="menu-card-item" @click="newWorkspaceFolder();handleMenu()">新建文件夹</view>
</view>
</view>
<view class="search-file-warpper">
@@ -38,15 +71,17 @@
文件夹为空
</view>
<view class="folder-grid" v-if="currentFolderList.length>0">
<view class="folder-item" :class="{'select-folder':selectFileList.includes(folder.id)}"
v-for="folder in currentFolderList" :key="folder.id"
@click="isSelectFolder?selectFolder(folder.id):handleFolderClick(folder)"
<view class="folder-item" :class="{'select-folder':selectFileList.includes(folder.path)}"
v-for="folder in currentFolderList" :key="folder.path"
@click="isSelectFolder?selectFolder(folder.path):handleFolderClick(folder)"
@longpress="handleLongPress(folder)">
<view class="iconfont icon-a-wenjianjiawenjian folder-item-style"></view>
<view v-if="folder.type === 'directory'"
class="iconfont icon-a-wenjianjiawenjian folder-item-style"></view>
<view v-else class="iconfont folder-item-style" :class="getFileIcon(folder.extension)"></view>
<view class="folder-name">{{folder.name}}</view>
<view v-if="isSelectFolder" class="folder-checkbox"
:class="{'select-folder':selectFileList.includes(folder.id)}">
<uni-icons v-if="selectFileList.includes(folder.id)" type="checkmarkempty"
:class="{'select-folder':selectFileList.includes(folder.path)}">
<uni-icons v-if="selectFileList.includes(folder.path)" type="checkmarkempty"
color="#fff"></uni-icons>
</view>
</view>
@@ -65,11 +100,21 @@
<script setup>
import {
onMounted,
ref
ref,
computed
} from 'vue';
import {getWorkspaceId} from '@/utils/user-info.js'
import {getWorkspaceList} from '@/utils/cloud-api.js'
import {
getWorkspaceId,
getToken
} from '@/utils/user-info.js'
import {
getWorkspaceList,
createWorkspaceFolder,
daleteWorkspace,
getWorkspaceFileURL
} from '@/utils/cloud-api.js'
const UserToken = ref('')
const workspaceId = ref('')
const navbarBeforeTitle = ref('');
const navbarTitle = ref('工作区');
@@ -91,66 +136,247 @@
selectFileList.value.push(id);
}
}
// 新建文件夹
const showNewFolder = ref(false)
const isNFNFocused = ref(false)
const newFolderName = ref('')
const closeNewFolderModal = () => {
showNewFolder.value = false
newFolderName.value = ''
}
const newWorkspaceFolder = () => {
showNewFolder.value = true
}
// 当前文件夹路径
const currentFolderPath = computed(() => {
if (folderStack.value.length === 0) return '根目录'
return '/' + folderStack.value.map(f => f.name).join('/')
})
// 路径预览:./ + 当前路径 + 新文件夹名
const folderPathPreview = computed(() => {
const folderPath = folderStack.value.map(f => f.name).join('/')
const name = newFolderName.value.trim()
if (name) {
return folderPath ? `./${folderPath}/${name}` : `./${name}`
}
return folderPath ? `./${folderPath}/` : './'
})
// 确认创建文件夹
const confirmCreateFolder = async () => {
const name = newFolderName.value.trim()
if (!name) {
uni.showToast({ title: '请输入目录名称', icon: 'none' })
return
}
try {
// 拼接路径:./ 开头,如 ./项目资料/设计图
const folderPath = folderStack.value.map(f => f.name).join('/')
const dirPath = folderPath ? `./${folderPath}/${name}` : `./${name}`
await createWorkspaceFolder(UserToken.value, workspaceId.value, dirPath)
uni.showToast({ title: '创建成功', icon: 'success' })
closeNewFolderModal()
initCurrentFolderList()
} catch (error) {
console.error('创建文件夹失败:', error)
uni.showToast({ title: '创建失败,请重试', icon: 'error' })
}
}
// 长按文件
const handleLongPress = (folder) => {
if (isSelectFolder.value) return;
uni.showActionSheet({
itemList:['重命名','删除','下载','压缩'],
itemList: ['重命名', '删除', '下载', '压缩'],
success: (res) => {
switch(res.tapIndex) {
switch (res.tapIndex) {
case 0:
handleRename(folder);
break;
handleRename(folder);
break;
case 1:
handleDelete(folder);
break;
handleDelete(folder);
break;
case 2:
handleDownload(folder);
break;
handleDownload(folder);
break;
case 3:
handleDownload(folder);
break;
handleDownload(folder);
break;
}
}
})
}
// 重命名
const handleRename = (folder) => {
uni.showModal({
title: '重命名',
content: '请输入新名称',
editable: true,
placeholderText: folder.name,
success: (res) => {
if (res.confirm && res.content) {
// 更新文件夹名称
updateFolderName(folder.id, res.content);
}
}
});
uni.showModal({
title: '重命名',
content: '请输入新名称',
editable: true,
placeholderText: folder.name,
success: (res) => {
if (res.confirm && res.content) {
// 更新文件夹名称
updateFolderName(folder.id, res.content);
}
}
});
};
// // 下载文件
// const handleDownload = async (folder) => {
// // 目录不能下载
// if (folder.type === 'directory') {
// uni.showToast({ title: '暂不支持下载目录', icon: 'none' });
// return;
// }
// try {
// uni.showLoading({ title: '获取下载链接...' });
// // 构建文件路径,去掉开头的 ./
// const filePath = folder.path.startsWith('./') ? folder.path.slice(2) : folder.path;
// const result = await getWorkspaceFileURL(UserToken.value, workspaceId.value, filePath);
// uni.hideLoading();
// // 接口返回数组,取第一个文件的 url
// const fileInfo = Array.isArray(result) ? result[0] : result;
// if (!fileInfo || !fileInfo.url) {
// uni.showToast({ title: '获取下载链接失败', icon: 'error' });
// return;
// }
// const downloadUrl = fileInfo.url;
// const fileName = folder.name;
// uni.showLoading({ title: '下载中...' });
// uni.downloadFile({
// url: downloadUrl,
// success: (downloadRes) => {
// uni.hideLoading();
// if (downloadRes.statusCode === 200) {
// // 保存到手机相册/文件
// uni.saveFile({
// tempFilePath: downloadRes.tempFilePath,
// success: (saveRes) => {
// uni.showToast({ title: '下载成功', icon: 'success' });
// console.log('文件已保存到:', saveRes.savedFilePath);
// },
// fail: (err) => {
// console.error('保存文件失败:', err);
// uni.showToast({ title: '保存文件失败', icon: 'error' });
// }
// });
// } else {
// uni.showToast({ title: '下载失败', icon: 'error' });
// }
// },
// fail: (err) => {
// uni.hideLoading();
// console.error('下载文件失败:', err);
// uni.showToast({ title: '下载失败,请重试', icon: 'error' });
// }
// });
// } catch (error) {
// uni.hideLoading();
// console.error('获取下载链接失败:', error);
// uni.showToast({ title: '获取下载链接失败,请重试', icon: 'error' });
// }
// };
const handleDownload = async (folder) => {
// 目录不能下载
if (folder.type === 'directory') {
uni.showToast({ title: '暂不支持下载目录', icon: 'none' });
return;
}
// 移动
const handleDownload = (folder) => {
// 可以选择跳转到移动页面或显示选择器
uni.showToast({
title: '移动功能开发中',
icon: 'none'
});
try {
uni.showLoading({ title: '获取下载链接...' });
// 构建文件路径,去掉开头的 ./
const filePath = folder.path.startsWith('./') ? folder.path.slice(2) : folder.path;
const result = await getWorkspaceFileURL(UserToken.value, workspaceId.value, filePath);
uni.hideLoading();
// 接口返回数组,取第一个文件的 url
const fileInfo = Array.isArray(result) ? result[0] : result;
if (!fileInfo || !fileInfo.url) {
uni.showToast({ title: '获取下载链接失败', icon: 'error' });
return;
}
const downloadUrl = fileInfo.url;
const fileName = folder.name;
uni.showLoading({ title: '下载中...' });
uni.downloadFile({
url: downloadUrl,
name: fileName, // 重要:指定文件名,保证打开正常
success: (downloadRes) => {
uni.hideLoading();
if (downloadRes.statusCode === 200) {
// 下载成功 → 直接打开文件
uni.openDocument({
filePath: downloadRes.tempFilePath,
showMenu: true, // 右上角显示 分享/保存 按钮
success: () => {
uni.showToast({
title: '打开成功',
icon: 'success'
});
},
fail: (err) => {
console.error('打开失败:', err);
uni.showToast({
title: '无法打开此文件',
icon: 'error'
});
}
});
} else {
uni.showToast({ title: '下载失败', icon: 'error' });
}
},
fail: (err) => {
uni.hideLoading();
console.error('下载文件失败:', err);
uni.showToast({ title: '下载失败,请重试', icon: 'error' });
}
});
} catch (error) {
uni.hideLoading();
console.error('获取下载链接失败:', error);
uni.showToast({ title: '获取下载链接失败,请重试', icon: 'error' });
}
};
// 删除
const handleDelete = (folder) => {
uni.showModal({
title: '提示',
content: `确定要删除文件夹"${folder.name}"吗?`,
success: (res) => {
if (res.confirm) {
// deleteFolder(folder.id);
console.log('确认删除文件夹');
}
}
});
uni.showModal({
title: '提示',
content: `确定要删除"${folder.name}"吗?`,
success: async (res) => {
if (res.confirm) {
try {
// 路径加 ./ 前缀
const filePath = `./${folder.path}`
console.log("删除的是:",filePath);
await daleteWorkspace(UserToken.value, workspaceId.value, filePath)
uni.showToast({ title: '删除成功', icon: 'success' })
initCurrentFolderList()
} catch (error) {
console.error('删除失败:', error)
uni.showToast({ title: '删除失败,请重试', icon: 'error' })
}
}
}
});
};
// const handleFolderClick = (id) => {
@@ -170,160 +396,102 @@
isSearchFocus.value = !isSearchFocus.value
}
const allFoldersData = ref({
// 根目录下的文件夹
'root': [{
id: 1,
name: '文档资料',
parentId: 'root',
children: [{
id: 11,
name: '工作文档',
parentId: 1,
children: []
},
{
id: 12,
name: '学习笔记',
parentId: 1,
children: []
},
{
id: 13,
name: '合同模板',
parentId: 1,
children: []
}
]
},
{
id: 2,
name: '图片素材',
parentId: 'root',
children: [{
id: 21,
name: '风景图片',
parentId: 2,
children: []
},
{
id: 22,
name: '人物照片',
parentId: 2,
children: []
},
{
id: 23,
name: 'UI图标',
parentId: 2,
children: []
}
]
},
{
id: 3,
name: '视频文件',
parentId: 'root',
children: [{
id: 31,
name: '教程视频',
parentId: 3,
children: []
},
{
id: 32,
name: '会议录像',
parentId: 3,
children: []
}
]
},
{
id: 4,
name: '项目代码',
parentId: 'root',
children: [{
id: 41,
name: '前端项目',
parentId: 4,
children: []
},
{
id: 42,
name: '后端服务',
parentId: 4,
children: []
}
]
},
{
id: 5,
name: '安装包',
parentId: 'root',
children: []
},
{
id: 6,
name: '备份文件',
parentId: 'root',
children: []
},
{
id: 7,
name: '临时文件',
parentId: 'root',
children: []
},
{
id: 8,
name: '个人收藏',
parentId: 'root',
children: []
}
]
});
// 根据文件后缀返回对应图标
const getFileIcon = (ext) => {
const iconMap = {
'.png': 'icon-wenjiantupian',
'.jpg': 'icon-wenjiantupian',
'.jpeg': 'icon-wenjiantupian',
'.gif': 'icon-wenjiantupian',
'.svg': 'icon-SVG',
'.bmp': 'icon-wenjiantupian',
'.webp': 'icon-wenjiantupian',
'.mp4': 'icon-wenjianshipin',
'.avi': 'icon-wenjianshipin',
'.mov': 'icon-wenjianshipin',
'.wmv': 'icon-wenjianshipin',
'.flv': 'icon-wenjianshipin',
'.mkv': 'icon-wenjianshipin',
'.mp3': 'icon-wenjianyinpin',
'.wav': 'icon-wenjianyinpin',
'.flac': 'icon-wenjianyinpin',
'.aac': 'icon-wenjianyinpin',
'.pdf': 'icon-PDF',
'.doc': 'icon-DOC',
'.docx': 'icon-DOC',
'.xls': 'icon-XLS',
'.xlsx': 'icon-XLS',
'.ppt': 'icon-PPT',
'.pptx': 'icon-PPT',
'.txt': 'icon-TXT',
'.md': 'icon-file-markdown-fill',
'.zip': 'icon-wenjianyasuo',
'.rar': 'icon-wenjianyasuo',
'.7z': 'icon-wenjianyasuo',
'.tar': 'icon-wenjianyasuo',
'.gz': 'icon-wenjianyasuo',
'.js': 'icon-JS',
'.ts': 'icon-daimawenjia',
'.vue': 'icon-daimawenjia',
'.html': 'icon-HTML',
'.css': 'icon-CSS',
'.py': 'icon-daimawenjian',
'.java': 'icon-daimawenjian',
'.json': 'icon-JSON',
};
return iconMap[ext?.toLowerCase()] || 'icon-qitawenjian';
};
const allFoldersData = ref({})
// 文件夹路径栈
const folderStack = ref([]);
// 当前显示的文件夹列表
const currentFolderList = ref([]);
const getCurrentFolderList = () => {
const currentPath = folderStack.value.length > 0 ? folderStack.value[folderStack.value.length - 1].id : 'root';
if (currentPath === "root") {
return allFoldersData.value['root'] || [];
console.log("当前路径为:", JSON.stringify(folderStack.value));
// 当前路径:栈为空时在根目录,路径为 "."
const currentPath = folderStack.value.length > 0 ? folderStack.value[folderStack.value.length - 1].path : '.';
// 在根目录下,直接返回根节点的 children
if (currentPath === '.') {
return allFoldersData.value?.children || [];
}
const findFolderById = (folders, id) => {
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;
// 递归在树中按 path 查找目标节点
const findFolderByPath = (list, targetPath) => {
for (const item of list) {
if (item.path === targetPath) return item;
if (item.type === 'directory' && item.children?.length) {
const res = findFolderByPath(item.children, targetPath);
if (res) return res;
}
}
return null;
};
const currentFolder = findFolderById(allFoldersData.value['root'], currentPath);
// 从根节点的 children 开始搜索
const currentFolder = findFolderByPath(allFoldersData.value?.children || [], currentPath);
return currentFolder ? currentFolder.children : [];
}
// 初始化当前文件夹列表
const initCurrentFolderList = () => {
const initCurrentFolderList = async () => {
workspaceId.value = getWorkspaceId();
console.log("getWorkspaceId:",workspaceId.value);
UserToken.value = getToken();
console.log("getWorkspaceId:", workspaceId.value);
allFoldersData.value = await getWorkspaceList(UserToken.value, workspaceId.value);
currentFolderList.value = getCurrentFolderList();
};
// 处理文件夹点击(非选择模式下进入文件夹)
// 处理文件夹点击(非选择模式下进入文件夹,仅目录可点击进入
const handleFolderClick = (folder) => {
if (folder.type !== 'directory') return;
folderStack.value.push({
id: folder.id,
path: folder.path,
name: folder.name
});
navbarTitle.value = folder.name;
navbarBeforeTitle.value = folderStack.value.length > 1 ? folderStack.value[folderStack.value.length - 2].name :
'工作区';
initCurrentFolderList();
currentFolderList.value = getCurrentFolderList();
searchKeyword.value = '';
isSearchFocus.value = false;
};
@@ -333,7 +501,7 @@
if (selectFileList.value.length === currentFolderList.value.length) {
selectFileList.value = [];
} else {
selectFileList.value = currentFolderList.value.map(f => f.id);
selectFileList.value = currentFolderList.value.map(f => f.path);
}
};