Files
phone-app------test1-/pages/WorkSpace/WorkSpace.vue
2026-08-03 09:05:21 +08:00

828 lines
24 KiB
Vue
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>
<view class="status-bar"></view>
<!-- ===== 新建文件夹设置弹窗 ===== -->
<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 ">
<view class="iconfont icon-fanhui custom-navbar-icon"></view>
<view class="navbar-before-title workspace-text">{{isSelectFolder?'全选':navbarBeforeTitle}}</view>
</view>
<view class="navbar-title workspace-text">{{navbarTitle}}</view>
<view class="navbar-right">
<view v-if="isSelectFolder" class="navbar-right-text workspace-text" @click="handleSelectFolder()">
完成</view>
<view v-else class="iconfont icon-gengduo" :class="isMenuOpen ?'menu-open':'custom-navbar-icon'"
@click="handleMenu"></view>
</view>
<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" @click="handleUploadFile();handleMenu()">上传文件</view>
<view class="solid-line"></view>
<view class="menu-card-item" @click="newWorkspaceFolder();handleMenu()">新建文件夹</view>
</view>
</view>
<view class="search-file-warpper">
<view class="search-file">
<view class="iconfont icon-sousuo"></view>
<input class="search-file-input" placeholder="搜索" @focus="handleSearchFocus"
v-model="searchKeyword" />
</view>
<view v-if="isSearchFocus" class="cancel-search" @click="handleSearchFocus">取消</view>
</view>
<!-- 标签切换 -->
<view class="workspace-tabs">
<view class="tab-item" :class="{ active: activeTab === 'workspace' }" @click="switchTab('workspace')">
工作区
</view>
<view class="tab-item" :class="{ active: activeTab === 'downloads' }" @click="switchTab('downloads')">
下载列表
<text v-if="downloadList.length > 0" class="tab-badge">{{ downloadList.length }}</text>
</view>
</view>
</view>
<!-- ===== 工作区文件 ===== -->
<view class="workspace-content" v-if="activeTab === 'workspace'">
<view class="folder-null" v-if="currentFolderList.length===0">
<view class="iconfont icon-wenjianjia"></view>
文件夹为空
</view>
<view class="folder-grid" v-if="currentFolderList.length>0">
<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 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.path)}">
<uni-icons v-if="selectFileList.includes(folder.path)" type="checkmarkempty"
color="#fff"></uni-icons>
</view>
</view>
</view>
</view>
<!-- ===== 下载列表 ===== -->
<view class="download-content" v-if="activeTab === 'downloads'">
<view v-if="downloadList.length === 0" class="download-empty">
<text class="iconfont icon-xiazai"></text>
<text>暂无下载文件</text>
</view>
<view v-else class="download-list">
<view class="download-header">
<text>已下载 {{ downloadList.length }} 个文件</text>
<text class="clear-btn" @click="handleClearDownloads">清空</text>
</view>
<view class="download-item" v-for="item in downloadList" :key="item.id" @click="openDownloadFile(item)">
<view class="di-icon">
<text class="iconfont" :class="getFileIcon(item.name)"></text>
</view>
<view class="di-info">
<text class="di-name">{{ item.name }}</text>
<text class="di-meta">{{ formatDownloadTime(item.time) }} · {{ formatDownloadSize(item.size) }}</text>
<text class="di-path">{{ item.displayPath || item.path }}</text>
</view>
<view class="di-delete" @click.stop="handleDeleteDownload(item.id)">
<text class="iconfont icon-del"></text>
</view>
</view>
</view>
</view>
<view v-if="isSelectFolder" class="workspace-footer" :class="{'folder-actions':selectFileList.length>0}">
<view class="iconfont icon-shangchuan" @click="handleUploadFile"></view>
<!-- <view class="iconfont icon-fuzhi"></view> -->
<!-- <view class="iconfont icon-wenjian"></view> -->
<view class="iconfont icon-del"></view>
<!-- <view class="iconfont icon-gengduo"></view> -->
</view>
</view>
</template>
<script setup>
import {
onMounted,
ref,
computed
} from 'vue';
import {
getWorkspaceId,
getToken
} from '@/utils/user-info.js'
import {
chooseFile
} from '@/uni_modules/lime-choose-file';
import {
getWorkspaceList,
createWorkspaceFolder,
daleteWorkspace,
getWorkspaceFileURL,
uploadFileToShortStorage,
workspaceUploadFileByURL,
updateWorkspaceFileName
} from '@/utils/cloud-api.js'
import { openFile as openFileNative } from '@/utils/fileOpener.js'
import {
getDownloadList,
saveDownload,
deleteDownload,
clearAllDownloads,
formatFileSize,
formatTime
} from '@/utils/downloadManager.js'
const UserToken = ref('')
const workspaceId = ref('')
const navbarBeforeTitle = ref('');
const navbarTitle = ref('工作区');
const activeTab = ref('workspace')
const downloadList = ref([])
// 选择文件模式
const isSelectFolder = ref(false);
const selectFileList = ref([]);
const handleSelectFolder = () => {
if (isSelectFolder.value) {
selectFileList.value = []
}
isSelectFolder.value = !isSelectFolder.value;
}
const selectFolder = (id) => {
const index = selectFileList.value.indexOf(id);
if (index !== -1) {
selectFileList.value.splice(index, 1);
} else {
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 handleUploadFile = () => {
chooseFile({
count: 10,
type: 'all',
success: async (res) => {
const files = res.tempFiles || [];
if (files.length === 0) {
uni.showToast({
title: '未选择文件',
icon: 'none'
});
return;
}
await doUploadWorkspaceFiles(files);
},
fail: (err) => {
console.error('选择文件失败:', err);
}
});
};
const doUploadWorkspaceFiles = async (files) => {
uni.showLoading({
title: '上传中...'
});
try {
const dirPath = folderStack.value.length > 0 ?
'./' + folderStack.value.map(f => f.name).join('/') + '/' :
'/';
// 1. 一次性上传所有文件到短存云端,获取 URL 数组
const results = await uploadFileToShortStorage(
workspaceId.value,
files,
dirPath
);
// results: [{ url, success }, ...]
// 2. 收集成功的 URL批量上传到工作区
const successUrls = results
.filter(r => r.success)
.map(r => r.url);
if (successUrls.length === 0) {
const errors = results.filter(r => !r.success).map(r => r.error).join('; ');
throw new Error(errors || '所有文件上传失败');
}
await workspaceUploadFileByURL(
UserToken.value,
workspaceId.value,
successUrls,
dirPath
);
uni.hideLoading();
const total = files.length;
const failed = total - successUrls.length;
const msg = failed > 0 ?
`上传完成:成功 ${successUrls.length},失败 ${failed}` :
`成功上传 ${total} 个文件`;
uni.showToast({
title: msg,
icon: successUrls.length > 0 ? 'success' : 'error'
});
// 刷新文件列表
initCurrentFolderList();
} catch (error) {
uni.hideLoading();
console.error('上传文件失败:', error);
uni.showToast({
title: typeof error === 'string' ? error : '上传失败,请重试',
icon: 'error'
});
}
};
// 当前文件夹路径
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;
const isWyxd = folder.name && folder.name.toLowerCase().endsWith('.wyxd');
const itemList = isWyxd ? ['下载', '删除', 'WYXD查看器', 'AI修改'] : ['下载', '删除'];
uni.showActionSheet({
title: folder.name,
itemList: itemList,
success: (res) => {
if (isWyxd) {
switch (res.tapIndex) {
case 0:
handleDownload(folder);
break;
case 1:
handleDelete(folder);
break;
case 2:
openWyxdViewer(folder);
break;
case 3: // 'AI修改',
handleAiModify(folder);
break;
}
} else {
switch (res.tapIndex) {
case 0:
handleDownload(folder);
break;
case 1:
handleDelete(folder);
break;
}
}
}
})
}
// 重命名
const handleRename = async (folder) => {
uni.showModal({
title: '重命名',
content: '请输入新名称',
editable: true,
placeholderText: folder.name,
success: async (res) => {
if (res.confirm && res.content) {
try {
uni.showLoading({
title: '重命名中...'
});
// 构建文件路径:当前目录 + 文件名
const dirPath = folderStack.value.length > 0 ?
'./' + folderStack.value.map(f => f.name).join('/') + '/' :
'/';
const filePath = dirPath + folder.name;
await updateWorkspaceFileName(
workspaceId.value,
filePath,
res.content
);
uni.hideLoading();
uni.showToast({
title: '重命名成功',
icon: 'success'
});
// 刷新文件列表
initCurrentFolderList();
} catch (error) {
uni.hideLoading();
console.error('重命名失败:', error);
uni.showToast({
title: typeof error === 'string' ? error : '重命名失败,请重试',
icon: 'error'
});
}
}
}
});
};
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;
const fileSize = fileInfo.size || 0;
uni.showLoading({
title: '下载中...'
});
uni.downloadFile({
url: downloadUrl,
name: fileName,
success: (downloadRes) => {
uni.hideLoading();
if (downloadRes.statusCode === 200) {
const tempPath = downloadRes.tempFilePath;
// 持久化保存到本地
saveDownload(tempPath, fileName, fileSize).then((record) => {
downloadList.value.unshift(record);
uni.showToast({ title: '下载成功', icon: 'success' });
}).catch(() => {
// 保存失败不影响打开
});
// 下载成功后打开文件
openFile(tempPath);
} 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 openFile = (filePath) => {
openFileNative(filePath, () => {
uni.showToast({ title: '无法打开此文件', icon: 'error' });
});
};
// ===== 下载列表相关 =====
const switchTab = (tab) => {
activeTab.value = tab
if (tab === 'downloads') {
loadDownloads()
}
}
const loadDownloads = () => {
downloadList.value = getDownloadList()
}
const openDownloadFile = (item) => {
// #ifdef APP-PLUS
// 检查公共目录下文件是否存在
plus.io.resolveLocalFileSystemURL(
item.path,
() => {
// 文件存在,直接打开
openFile(item.path)
},
() => {
// 文件已被手动删除
uni.showToast({ title: '文件已失效,将移除记录', icon: 'none' })
handleDeleteDownload(item.id)
}
)
// #endif
// #ifndef APP-PLUS
uni.getSavedFileList({
success: (res) => {
const found = res.fileList.find(f => f.filePath === item.path)
if (found) {
openFile(item.path)
} else {
uni.showToast({ title: '文件已失效,将移除记录', icon: 'none' })
handleDeleteDownload(item.id)
}
},
fail: () => openFile(item.path)
})
// #endif
}
const handleDeleteDownload = (id) => {
uni.showModal({
title: '删除文件',
content: '确定要删除此下载文件吗?',
success: (res) => {
if (res.confirm) {
deleteDownload(id).then(() => {
downloadList.value = getDownloadList()
uni.showToast({ title: '已删除', icon: 'success' })
}).catch(() => {
uni.showToast({ title: '删除失败', icon: 'error' })
})
}
}
})
}
const handleClearDownloads = () => {
if (downloadList.value.length === 0) return
uni.showModal({
title: '清空下载',
content: '确定要清空所有下载文件吗?此操作不可撤销。',
success: (res) => {
if (res.confirm) {
clearAllDownloads().then(() => {
downloadList.value = []
uni.showToast({ title: '已清空', icon: 'success' })
})
}
}
})
}
const formatDownloadSize = formatFileSize
const formatDownloadTime = formatTime
// const getFileIcon = (fileName) => {
// const ext = fileName.split('.').pop().toLowerCase()
// const iconMap = {
// pdf: 'icon-PDF',
// doc: 'icon-word', docx: 'icon-word',
// xls: 'icon-excel', xlsx: 'icon-excel',
// ppt: 'icon-PPD', pptx: 'icon-PPD',
// zip: 'icon-yasuobao', rar: 'icon-yasuobao', '7z': 'icon-yasuobao',
// jpg: 'icon-tupian', jpeg: 'icon-tupian', png: 'icon-tupian', gif: 'icon-tupian',
// mp3: 'icon-yinle', wav: 'icon-yinle', flac: 'icon-yinle',
// mp4: 'icon-shipin', avi: 'icon-shipin', mov: 'icon-shipin',
// txt: 'icon-txt', md: 'icon-txt',
// }
// return iconMap[ext] || 'icon-weizhiwenjian'
// }
// WYXD 查看器入口
const openWyxdViewer = (folder) => {
const filePath = folder.path.startsWith('./') ? folder.path.slice(2) : folder.path;
console.log("解析文件路径:",filePath);
console.log("folder",folder);
console.log("encode",encodeURIComponent(filePath));
console.log("encode",encodeURIComponent(folder.name));
uni.navigateTo({
url: '/pages/WYXD/WYXD?workspaceId=' + encodeURIComponent(workspaceId.value)
+ '&filePath=' + encodeURIComponent(filePath)
+ '&fileName=' + encodeURIComponent(folder.name)
+ '&userToken=' + encodeURIComponent(UserToken.value)
});
};
// AI修改入口跳转对话页并传入 WYXD 信息
const handleAiModify = (folder) => {
const filePath = folder.path.startsWith('./') ? folder.path.slice(2) : folder.path;
uni.navigateTo({
url: '/pages/Chat/Chat?wyxdWorkspaceId=' + encodeURIComponent(workspaceId.value)
+ '&wyxdFilePath=' + encodeURIComponent(filePath)
+ '&wyxdFileName=' + encodeURIComponent(folder.name)
+ '&wyxdUserToken=' + encodeURIComponent(UserToken.value)
});
};
// 删除
const handleDelete = (folder) => {
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) => {
// console.log(id);
// }
// 菜单
const isMenuOpen = ref(false)
const handleMenu = () => {
isMenuOpen.value = !isMenuOpen.value;
}
// 搜索框
const searchKeyword = ref('')
const isSearchFocus = ref(false)
const handleSearchFocus = () => {
isSearchFocus.value = !isSearchFocus.value
}
// 根据文件后缀返回对应图标
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-PDF1',
'.doc': 'icon-word',
'.docx': 'icon-word',
'.xls': 'icon-execl',
'.xlsx': 'icon-execl',
'.ppt': 'icon-PPT1',
'.pptx': 'icon-PPT1',
'.txt': 'icon-TXT1',
'.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-daima',
'.java': 'icon-daima',
'.json': 'icon-JSON',
};
return iconMap[ext?.toLowerCase()] || 'icon-qitalingyu';
};
const allFoldersData = ref({})
// 文件夹路径栈
const folderStack = ref([]);
// 当前显示的文件夹列表
const currentFolderList = ref([]);
const getCurrentFolderList = () => {
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 || [];
}
// 递归在树中按 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;
};
// 从根节点的 children 开始搜索
const currentFolder = findFolderByPath(allFoldersData.value?.children || [], currentPath);
return currentFolder ? currentFolder.children : [];
}
// 初始化当前文件夹列表
const initCurrentFolderList = async () => {
workspaceId.value = getWorkspaceId();
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') {
handleLongPress(folder)
return
}
folderStack.value.push({
path: folder.path,
name: folder.name
});
navbarTitle.value = folder.name;
navbarBeforeTitle.value = folderStack.value.length > 1 ? folderStack.value[folderStack.value.length - 2].name :
'工作区';
currentFolderList.value = getCurrentFolderList();
searchKeyword.value = '';
isSearchFocus.value = false;
};
// 全选/取消全选
const handleSelectAll = () => {
if (selectFileList.value.length === currentFolderList.value.length) {
selectFileList.value = [];
} else {
selectFileList.value = currentFolderList.value.map(f => f.path);
}
};
// 返回上一级
const handleBackOrCheck = () => {
if (isSelectFolder.value) {
// 选择模式下点击全选/取消全选
handleSelectAll();
return;
}
if (folderStack.value.length > 0) {
folderStack.value.pop();
if (folderStack.value.length === 0) {
navbarTitle.value = '工作区';
navbarBeforeTitle.value = null;
} else {
navbarTitle.value = folderStack.value[folderStack.value.length - 1].name;
navbarBeforeTitle.value = folderStack.value.length > 1 ?
folderStack.value[folderStack.value.length - 2].name :
'工作区';
}
initCurrentFolderList();
// 返回后清空搜索
searchKeyword.value = '';
isSearchFocus.value = false;
} else {
// 已在根目录,返回到聊天界面
uni.navigateBack({
delta: 1,
fail() {
console.log("返回失败,进入兜底跳转")
uni.reLaunch({
url: '/pages/Chat/Chat'
})
}
})
}
};
onMounted(() => {
initCurrentFolderList();
loadDownloads();
})
</script>
<style scoped>
@import url("WorkSpace.css");
</style>