Files
phone-app------test1-/pages/CloudDatabase/CloudDatabase.vue
2026-06-25 17:48:58 +08:00

983 lines
32 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="showTeamManage" class="popup-overlay" @click="closeTeamManage">
<view class="popup-card" @click.stop>
<view class="close-popup-card" @click="closeTeamManage">
<view class="iconfont icon-quxiao"></view>
</view>
<view class="popup-title">团队管理</view>
<scroll-view class="team-manage-body" scroll-y>
<view v-if="currentTeamMembers.length === 0" class="folder-null">暂无团队成员</view>
<view v-else class="member-list">
<view class="member-item" v-for="member in currentTeamMembers" :key="member.id || member.receiver">
<view class="member-avatar">
<image v-if="member.avatar" :src="member.avatar" mode="aspectFill"></image>
<view v-else class="default-avatar">
<uni-icons type="person-filled" size="60rpx" color="#ffffff"></uni-icons>
</view>
</view>
<view class="member-info">
<view class="member-name">{{ member.friendNickName || member.username || member.name }}</view>
<view class="member-label">{{ member.email || member.sessionId || '' }}</view>
</view>
<view class="member-actions">
<view v-if="member.role === 'owner'" class="member-role owner">创建者</view>
<view v-else-if="member.role === 'admin'" class="member-role admin">管理员</view>
<view v-else class="member-actions-right">
<view class="member-action-btn set-admin" @click="setTeamAdmin(member)">设为管理</view>
<view class="member-action-btn remove-member" @click="removeTeamMember(member)">移除</view>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 邀请成员 -->
<view class="invite-section">
<view class="invite-input-wrapper">
<input class="invite-input" placeholder="输入用户ID或名称邀请" v-model="inviteKeyword" />
<view class="invite-btn text-btn" @click="inviteTeamMember">邀请</view>
</view>
</view>
</view>
</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">新建{{ currentCategory === 'user' ? '个人' : '团队' }}目录</view>
<view class="new-folder-name">目录名称</view>
<view class="create-folder-name">
<input v-model="newFolderName" type="text" placeholder="输入目录名称" />
</view>
<view class="nf-path-label">当前路径</view>
<view class="nf-path-display">{{ currentFolderDisplayPath }}</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 v-if="showNewTable" class="popup-overlay" @click="closeNewTableModal">
<view class="popup-card" @click.stop>
<view class="close-popup-card" @click="closeNewTableModal">
<view class="iconfont icon-quxiao"></view>
</view>
<view class="popup-title">新建{{ currentCategory === 'user' ? '个人' : '团队' }}数据表</view>
<view class="new-folder-name">表名称</view>
<view class="create-folder-name">
<input v-model="newTableName" type="text" placeholder="输入表名称users" />
</view>
<view class="new-folder-name">描述</view>
<view class="create-folder-name">
<input v-model="newTableDesc" type="text" placeholder="输入表描述(可选)" />
</view>
<view class="option-wrapper">
<view class="opt-btn opt-cancel" @click="closeNewTableModal">取消</view>
<view class="opt-btn opt-confirm" @click="confirmCreateTable">确认</view>
</view>
</view>
</view>
<!-- ===== 主页面 ===== -->
<view class="cloud-db-container page-container">
<view class="cloud-db-header">
<view class="custom-navbar">
<view class="navbar-left" @click="handleBackOrUp">
<view class="iconfont icon-fanhui custom-navbar-icon"></view>
<view v-if="currentFolderPath.length > 0" class="navbar-before-title">{{ parentFolderName }}</view>
</view>
<view class="navbar-title">{{ navbarTitle }}</view>
<view class="navbar-right">
<view v-if="isSelectFolder" class="navbar-right-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="openNewFolderModal(); handleMenu()">新建文件夹</view>
</view>
</view>
<!-- 个人/团队 分类切换 -->
<view class="category-tabs">
<view class="category-tab" :class="{ active: currentCategory === 'user' }"
@click="switchCategory('user')">
个人
</view>
<view class="category-tab" :class="{ active: currentCategory === 'team' }"
@click="switchCategory('team')">
团队
</view>
</view>
</view>
<scroll-view class="cloud-db-content" scroll-y>
<!-- ========== 个人分类 ========== -->
<template v-if="currentCategory === 'user'">
<!-- 云端文件区域 -->
<view class="section-block">
<view class="section-header">
<view class="section-title">
<view class="iconfont icon-wenjianjia"></view>
<text>云端文件</text>
<text class="file-count">{{ personalFileCount > 0 ? '(' + personalFileCount + ')' : '' }}</text>
</view>
<view class="section-actions">
<view class="action-btn" @click="openNewFolderModal">
<uni-icons type="plus" size="16"></uni-icons>
<text>新建目录</text>
</view>
</view>
</view>
<view class="section-body">
<view v-if="personalFiles.length === 0" class="empty-tip">
<view class="iconfont icon-wenjianjia"></view>
<text>暂无云端文件</text>
</view>
<view v-else class="file-grid">
<view class="file-item" v-for="file in personalFiles" :key="file.id"
@click="handleFileClick(file)" @longpress="handleFileLongPress(file)">
<view class="file-icon" :class="isFolder(file) ? 'is-directory' : 'is-file'">
<view class="iconfont" :class="getFileIcon(file)"></view>
</view>
<view class="file-name">{{ file.name }}</view>
<view class="file-meta">
<text class="file-type-tag">{{ isFolder(file) ? '目录' : '文件' }}</text>
<text v-if="file.size" class="file-size">{{ formatFileSize(file.size) }}</text>
</view>
</view>
</view>
</view>
</view>
<view class="section-divider"></view>
<!-- 云端数据库区域 -->
<view class="section-block">
<view class="section-header">
<view class="section-title">
<view class="iconfont icon-shujuku"></view>
<text>云端数据库</text>
</view>
<view class="section-actions">
<view class="action-btn" @click="openNewTableModal">
<uni-icons type="plus" size="16"></uni-icons>
<text>新建表</text>
</view>
</view>
</view>
<view class="section-body">
<view v-if="personalTables.length === 0" class="empty-tip">
<view class="iconfont icon-shujuku"></view>
<text>暂无数据表</text>
</view>
<view v-else class="table-list">
<view class="table-item" v-for="table in personalTables" :key="table.id"
@click="handleTableClick(table)">
<view class="table-icon">
<uni-icons type="compose" size="22" color="#3b86ff"></uni-icons>
</view>
<view class="table-info">
<view class="table-name">{{ table.name }}</view>
<view class="table-desc">{{ table.desc || '无描述' }}</view>
</view>
<view class="table-meta">
<text class="table-rows">{{ table.rowCount || 0 }} 条记录</text>
<uni-icons type="right" size="14" color="#999"></uni-icons>
</view>
</view>
</view>
</view>
</view>
</template>
<!-- ========== 团队分类 ========== -->
<template v-if="currentCategory === 'team'">
<!-- 团队管理入口 -->
<view class="section-block team-manage-block" @click="openTeamManage">
<view class="section-header">
<view class="section-title">
<view class="iconfont icon-qunliao"></view>
<text>团队管理</text>
</view>
<view class="section-actions">
<text class="team-count">{{ teamGroups.length }} 个团队</text>
<uni-icons type="right" size="14" color="#999"></uni-icons>
</view>
</view>
</view>
<view class="section-divider"></view>
<!-- 团队选择器 -->
<view v-if="teamGroups.length > 0" class="team-selector">
<scroll-view scroll-x class="team-scroll">
<view class="team-chip" :class="{ active: currentTeamId === team.id }"
v-for="team in teamGroups" :key="team.id" @click="selectTeam(team.id)">
{{ team.name }}
</view>
</scroll-view>
</view>
<view v-if="!currentTeamId && teamGroups.length > 0" class="empty-tip select-team-tip">
<view class="iconfont icon-qunliao"></view>
<text>请选择一个团队</text>
</view>
<!-- 团队云端文件区域 -->
<view v-if="currentTeamId" class="section-block">
<view class="section-header">
<view class="section-title">
<view class="iconfont icon-wenjianjia"></view>
<text>团队云端文件</text>
</view>
<view class="section-actions">
<view class="action-btn" @click="openNewFolderModal">
<uni-icons type="plus" size="16"></uni-icons>
<text>新建目录</text>
</view>
</view>
</view>
<view class="section-body">
<view v-if="teamFiles.length === 0" class="empty-tip">
<view class="iconfont icon-wenjianjia"></view>
<text>暂无团队云端文件</text>
</view>
<view v-else class="file-grid">
<view class="file-item" v-for="file in teamFiles" :key="file.id"
@click="handleFileClick(file)" @longpress="handleFileLongPress(file)">
<view class="file-icon" :class="isFolder(file) ? 'is-directory' : 'is-file'">
<view class="iconfont" :class="getFileIcon(file)"></view>
</view>
<view class="file-name">{{ file.name }}</view>
<view class="file-meta">
<text class="file-type-tag">{{ isFolder(file) ? '目录' : '文件' }}</text>
<text v-if="file.size" class="file-size">{{ formatFileSize(file.size) }}</text>
</view>
</view>
</view>
</view>
</view>
<view v-if="currentTeamId" class="section-divider"></view>
<!-- 团队云端数据库区域 -->
<view v-if="currentTeamId" class="section-block">
<view class="section-header">
<view class="section-title">
<view class="iconfont icon-shujuku"></view>
<text>团队云端数据库</text>
</view>
<view class="section-actions">
<view class="action-btn" @click="openNewTableModal">
<uni-icons type="plus" size="16"></uni-icons>
<text>新建表</text>
</view>
</view>
</view>
<view class="section-body">
<view v-if="teamTables.length === 0" class="empty-tip">
<view class="iconfont icon-shujuku"></view>
<text>暂无团队数据表</text>
</view>
<view v-else class="table-list">
<view class="table-item" v-for="table in teamTables" :key="table.id"
@click="handleTableClick(table)">
<view class="table-icon">
<uni-icons type="compose" size="22" color="#3b86ff"></uni-icons>
</view>
<view class="table-info">
<view class="table-name">{{ table.name }}</view>
<view class="table-desc">{{ table.desc || '无描述' }}</view>
</view>
<view class="table-meta">
<text class="table-rows">{{ table.rowCount || 0 }} 条记录</text>
<uni-icons type="right" size="14" color="#999"></uni-icons>
</view>
</view>
</view>
</view>
</view>
</template>
</scroll-view>
</view>
</template>
<script setup>
import { onMounted, ref, computed } from 'vue';
import { getToken } from '@/utils/user-info.js';
import {
getUserInfo,
getCloudFileList,
getCloudFileUrl,
createWorkspaceFolder,
daleteWorkspace,
getWorkspaceFileURL
} from '@/utils/cloud-api.js';
import { getGroup } from '@/utils/friend-api.js';
const userToken = ref('');
const userId = ref('');
const workspaceId = ref('');
// 分类personal / team
const currentCategory = ref('user');
const switchCategory = (category) => {
currentCategory.value = category;
// 切换分类时重置文件夹导航
currentFolderPath.value = [];
// 切换分类时重新加载文件
loadCurrentFiles();
};
// ========== 导航栏 ==========
const navbarTitle = ref('云端数据');
const isMenuOpen = ref(false);
const isSelectFolder = ref(false);
const selectFileList = ref([]);
const handleMenu = () => {
isMenuOpen.value = !isMenuOpen.value;
};
const handleSelectFolder = () => {
isSelectFolder.value = !isSelectFolder.value;
if (!isSelectFolder.value) {
selectFileList.value = [];
}
};
const parentFolderName = computed(() => {
if (currentFolderPath.value.length === 0) return '';
if (currentFolderPath.value.length === 1) return '云端数据';
return currentFolderPath.value[currentFolderPath.value.length - 2]?.name || '云端数据';
});
// 返回 / 上级目录
const handleBackOrUp = () => {
if (currentFolderPath.value.length > 0) {
currentFolderPath.value.pop();
navbarTitle.value = currentFolderPath.value.length > 0
? currentFolderPath.value[currentFolderPath.value.length - 1].name
: '云端数据';
loadCurrentFiles();
} else {
uni.navigateBack({
delta: 1,
fail() {
uni.reLaunch({ url: '/pages/Chat/Chat' });
}
});
}
};
// ========== 云端文件 ==========
// 原始完整数据(从 API 返回的扁平列表)
const rawPersonalFileData = ref([]);
const rawTeamFileData = ref([]);
// 当前显示的文件夹列表(扁平)
const personalFiles = ref([]);
const teamFiles = ref([]);
// 文件夹导航栈
const currentFolderPath = ref([]);
// 判断是否为文件夹
const isFolder = (file) => {
return file.type === 'folder';
};
// 获取文件扩展名
const getFileExtension = (file) => {
if (isFolder(file)) return 'folder';
const name = file.name || '';
const dotIndex = name.lastIndexOf('.');
if (dotIndex === -1) return '';
return name.slice(dotIndex).toLowerCase();
};
// 获取文件图标(根据文件对象综合判断)
const getFileIcon = (file) => {
if (isFolder(file)) return 'icon-a-wenjianjiawenjian';
const ext = getFileExtension(file);
const mimeType = (file.type || '').toLowerCase();
const extMap = {
'.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',
};
if (ext && extMap[ext]) return extMap[ext];
// 根据 MIME 类型推断
if (mimeType.startsWith('image/')) return 'icon-wenjiantupian';
if (mimeType.startsWith('video/')) return 'icon-wenjianshipin';
if (mimeType.startsWith('audio/')) return 'icon-wenjianyinpin';
if (mimeType.includes('spreadsheet') || mimeType.includes('excel')) return 'icon-XLS';
if (mimeType.includes('document') || mimeType.includes('word')) return 'icon-DOC';
if (mimeType.includes('presentation') || mimeType.includes('powerpoint')) return 'icon-PPT';
if (mimeType.includes('pdf')) return 'icon-PDF';
if (mimeType.includes('zip') || mimeType.includes('rar') || mimeType.includes('tar') || mimeType.includes('gzip'))
return 'icon-wenjianyasuo';
return 'icon-qitawenjian';
};
const formatFileSize = (bytes) => {
if (!bytes) return '';
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
};
const currentFolderDisplayPath = computed(() => {
if (currentFolderPath.value.length === 0) return '根目录 (/)';
return '/' + currentFolderPath.value.map(f => f.name).join('/');
});
// 文件统计
const personalFileCount = computed(() => rawPersonalFileData.value.length);
// 从 API 数据中递归查找指定路径的文件夹内容
const findFolderByPath = (dataList, targetPath) => {
for (const item of dataList) {
if (isFolder(item) && item.path === targetPath) {
return item.children || [];
}
}
return null;
};
// 加载当前路径下的文件列表
const loadCurrentFiles = () => {
const rawData = currentCategory.value === 'user' ? rawPersonalFileData.value : rawTeamFileData.value;
if (currentFolderPath.value.length === 0) {
// 根目录API 返回的 data 数组本身就是根目录下的所有顶层项
// 不需要过滤 path因为每个顶层项的 path 是其自身路径(文件夹的 path 为 /folderName/,文件的 path 为 /
if (currentCategory.value === 'user') {
personalFiles.value = rawData;
} else {
teamFiles.value = rawData;
}
} else {
// 子目录:找到当前文件夹的 children
const currentFolder = currentFolderPath.value[currentFolderPath.value.length - 1];
const result = currentFolder.children || [];
if (currentCategory.value === 'user') {
personalFiles.value = result;
} else {
teamFiles.value = result;
}
}
};
// 获取云端文件列表
const fetchFileList = async () => {
try {
userToken.value = getToken();
const userInfo = await getUserInfo(userToken.value);
userId.value = userInfo._id;
workspaceId.value = uni.getStorageSync('workspace_id') || '';
// 获取个人文件
const personalData = await getCloudFileList(userToken.value, 'user');
rawPersonalFileData.value = personalData || [];
// 默认显示个人文件根目录API 返回的 data 数组即为根目录下的所有顶层项)
if (currentCategory.value === 'user') {
personalFiles.value = personalData || [];
}
} catch (error) {
console.error('获取文件列表失败:', error);
rawPersonalFileData.value = [];
personalFiles.value = [];
}
};
// 获取团队文件
const fetchTeamFileList = async (teamId) => {
try {
const teamData = await getCloudFileList(userToken.value, 'team', '/', teamId);
console.log('团队云端文件:', JSON.stringify(teamData));
rawTeamFileData.value = teamData || [];
teamFiles.value = teamData || [];
} catch (error) {
console.error('获取团队文件列表失败:', error);
rawTeamFileData.value = [];
teamFiles.value = [];
}
};
// 文件点击:文件夹进入,文件下载
const handleFileClick = (file) => {
if (isFolder(file)) {
// 进入文件夹
currentFolderPath.value.push({
path: file.path,
name: file.name,
children: file.children || []
});
navbarTitle.value = file.name;
loadCurrentFiles();
} else {
handleDownload(file);
}
};
// 文件长按操作
const handleFileLongPress = (file) => {
if (isSelectFolder.value) {
const idx = selectFileList.value.indexOf(file.id);
if (idx !== -1) {
selectFileList.value.splice(idx, 1);
} else {
selectFileList.value.push(file.id);
}
return;
}
const menuItems = isFolder(file) ? ['重命名', '删除'] : ['下载', '重命名', '删除'];
uni.showActionSheet({
itemList: menuItems,
success: (res) => {
if (isFolder(file)) {
switch (res.tapIndex) {
case 0: handleRename(file); break;
case 1: handleDeleteFile(file); break;
}
} else {
switch (res.tapIndex) {
case 0: handleDownload(file); break;
case 1: handleRename(file); break;
case 2: handleDeleteFile(file); break;
}
}
}
});
};
// 下载文件:通过文件 ID 获取下载 URL
const handleDownload = async (file) => {
if (isFolder(file)) {
uni.showToast({ title: '暂不支持下载目录', icon: 'none' });
return;
}
try {
// 通过文件 ID 获取下载 URL
uni.showLoading({ title: '获取下载链接...' });
const downloadUrl = await getCloudFileUrl(userToken.value, file.id);
uni.hideLoading();
if (!downloadUrl) {
uni.showToast({ title: '获取下载链接失败', icon: 'error' });
return;
}
// 下载文件
uni.showLoading({ title: '下载中...' });
uni.downloadFile({
url: downloadUrl,
success: (downloadRes) => {
uni.hideLoading();
if (downloadRes.statusCode === 200) {
const tempPath = downloadRes.tempFilePath;
// 下载成功,保存到 yxd 目录
saveFileToYxd(file.name, tempPath);
} else {
uni.showToast({ title: '下载失败', icon: 'error' });
}
},
fail: () => {
uni.hideLoading();
uni.showToast({ title: '下载失败,请重试', icon: 'error' });
}
});
} catch (error) {
uni.hideLoading();
console.error('下载失败:', error);
uni.showToast({ title: '下载失败,请重试', icon: 'error' });
}
};
// 将下载的文件保存到 yxd 目录
const saveFileToYxd = (fileName, tempPath) => {
// #ifdef APP-PLUS
// App 端:使用 plus.io 将文件复制到 /yxd/ 目录
try {
// 获取应用的私有文档目录作为 yxd 的父目录
const yxdDir = `_doc/yxd/`;
const targetPath = `${yxdDir}${fileName}`;
// 确保 yxd 目录存在
plus.io.resolveLocalFileSystemURL(yxdDir, (dirEntry) => {
// 目录已存在,复制文件
copyFileToDir(tempPath, targetPath, fileName);
}, () => {
// 目录不存在,先创建
plus.io.requestFileSystem(plus.io.PRIVATE_DOC, (fs) => {
fs.root.getDirectory('yxd', { create: true }, (dirEntry) => {
copyFileToDir(tempPath, targetPath, fileName);
}, (err) => {
console.error('创建 yxd 目录失败:', err);
uni.showToast({ title: '创建目录失败', icon: 'error' });
});
});
});
} catch (e) {
console.error('保存文件失败:', e);
uni.showToast({ title: '保存失败', icon: 'error' });
}
// #endif
// #ifndef APP-PLUS
// 小程序/H5 端:使用 uni.saveFile 保存
uni.saveFile({
tempFilePath: tempPath,
success: (saveRes) => {
openFileConfirm(fileName, saveRes.savedFilePath);
},
fail: () => {
// saveFile 失败则直接确认打开
openFileConfirm(fileName, tempPath);
}
});
// #endif
};
// App 端:将文件从临时路径复制到 yxd 目录
const copyFileToDir = (tempPath, targetPath, fileName) => {
plus.io.resolveLocalFileSystemURL(tempPath, (fileEntry) => {
fileEntry.copyTo(
plus.io.convertLocalFileSystemURL(targetPath).replace(fileName, ''),
fileName,
() => {
console.log('文件已保存到 yxd 目录:', targetPath);
// 保存成功后弹窗确认是否打开
openFileConfirm(fileName, targetPath);
},
(err) => {
console.error('复制文件失败:', err);
// 复制失败,使用临时路径
openFileConfirm(fileName, tempPath);
}
);
}, () => {
console.error('读取临时文件失败');
openFileConfirm(fileName, tempPath);
});
};
// 下载成功后确认是否打开文件
const openFileConfirm = (fileName, filePath) => {
uni.showActionSheet({
title: `${fileName} 下载完成`,
itemList: ['打开文件', '选择其他应用打开', '仅保存'],
success: (res) => {
switch (res.tapIndex) {
case 0:
// 打开文件(系统默认应用)
openFile(filePath);
break;
case 1:
// 选择其他应用打开showMenu 会弹出分享/打开方式菜单)
openFileWithMenu(filePath);
break;
case 2:
// 仅保存,不打开
uni.showToast({ title: '文件已保存到 yxd 目录', icon: 'success' });
break;
}
},
fail: () => {
// 用户取消,默认不打开
uni.showToast({ title: '文件已保存到 yxd 目录', icon: 'success' });
}
});
};
// 使用系统默认应用打开
const openFile = (filePath) => {
uni.openDocument({
filePath: filePath,
success: () => uni.showToast({ title: '打开成功', icon: 'success' }),
fail: (err) => {
console.error('打开文件失败:', err);
// 如果默认应用打开失败,尝试弹出选择菜单
openFileWithMenu(filePath);
}
});
};
// 弹出系统分享/打开方式菜单,让用户选择应用
const openFileWithMenu = (filePath) => {
// #ifdef APP-PLUS
// App 端使用 plus.runtime 打开文件,可选择应用
plus.runtime.openFile(filePath, {}, (e) => {
console.log('文件打开成功');
});
// #endif
// #ifndef APP-PLUS
// 小程序/H5 端使用 openDocument + showMenu
uni.openDocument({
filePath: filePath,
showMenu: true,
success: () => uni.showToast({ title: '打开成功', icon: 'success' }),
fail: () => uni.showToast({ title: '无法打开此文件', icon: 'error' })
});
// #endif
};
// 重命名
const handleRename = (file) => {
uni.showModal({
title: '重命名',
content: '请输入新名称',
editable: true,
placeholderText: file.name,
success: (res) => {
if (res.confirm && res.content) {
uni.showToast({ title: '重命名成功', icon: 'success' });
refreshCurrentFileList();
}
}
});
};
// 删除文件
const handleDeleteFile = (file) => {
uni.showModal({
title: '提示',
content: `确定要删除"${file.name}"吗?`,
success: async (res) => {
if (res.confirm) {
try {
const filePath = file.full_path && !file.full_path.startsWith('http')
? file.full_path
: file.path + file.name;
if (workspaceId.value) {
await daleteWorkspace(userToken.value, workspaceId.value, filePath);
}
uni.showToast({ title: '删除成功', icon: 'success' });
refreshCurrentFileList();
} catch (error) {
console.error('删除失败:', error);
uni.showToast({ title: '删除失败,请重试', icon: 'error' });
}
}
}
});
};
// 刷新当前文件列表
const refreshCurrentFileList = async () => {
if (currentCategory.value === 'user') {
await fetchFileList();
} else {
await fetchTeamFileList(currentTeamId.value);
}
};
// ========== 新建文件夹 ==========
const showNewFolder = ref(false);
const newFolderName = ref('');
const openNewFolderModal = () => {
showNewFolder.value = true;
newFolderName.value = '';
};
const closeNewFolderModal = () => {
showNewFolder.value = false;
newFolderName.value = '';
};
const confirmCreateFolder = async () => {
const name = newFolderName.value.trim();
if (!name) {
uni.showToast({ title: '请输入目录名称', icon: 'none' });
return;
}
try {
if (workspaceId.value) {
const prefix = currentFolderDisplayPath.value === '根目录 (/)'
? '/'
: currentFolderDisplayPath.value;
const dirPath = prefix + name;
await createWorkspaceFolder(userToken.value, workspaceId.value, dirPath);
}
uni.showToast({ title: '创建成功', icon: 'success' });
closeNewFolderModal();
refreshCurrentFileList();
} catch (error) {
console.error('创建文件夹失败:', error);
uni.showToast({ title: '创建失败,请重试', icon: 'error' });
}
};
// ========== 数据库表 ==========
const personalTables = ref([
{ id: '1', name: 'users', desc: '用户信息表', rowCount: 128 },
{ id: '2', name: 'messages', desc: '聊天记录表', rowCount: 2048 },
{ id: '3', name: 'settings', desc: '用户配置表', rowCount: 56 }
]);
const teamTables = ref([
{ id: 't1', name: 'team_tasks', desc: '团队任务表', rowCount: 32 },
{ id: 't2', name: 'team_docs', desc: '团队文档表', rowCount: 15 }
]);
const showNewTable = ref(false);
const newTableName = ref('');
const newTableDesc = ref('');
const openNewTableModal = () => {
showNewTable.value = true;
newTableName.value = '';
newTableDesc.value = '';
};
const closeNewTableModal = () => {
showNewTable.value = false;
newTableName.value = '';
newTableDesc.value = '';
};
const confirmCreateTable = () => {
const name = newTableName.value.trim();
if (!name) {
uni.showToast({ title: '请输入表名称', icon: 'none' });
return;
}
const newTable = {
id: Date.now().toString(),
name: name,
desc: newTableDesc.value.trim() || '无描述',
rowCount: 0
};
if (currentCategory.value === 'user') {
personalTables.value.push(newTable);
} else {
teamTables.value.push(newTable);
}
uni.showToast({ title: '创建成功', icon: 'success' });
closeNewTableModal();
};
const handleTableClick = (table) => {
uni.showToast({ title: `进入数据表:${table.name}`, icon: 'none' });
};
// ========== 团队管理 ==========
const showTeamManage = ref(false);
const teamGroups = ref([]);
const currentTeamId = ref('');
const currentTeamMembers = ref([]);
const inviteKeyword = ref('');
const fetchTeamList = async () => {
try {
if (!userId.value) {
const token = getToken();
const userInfo = await getUserInfo(token);
userId.value = userInfo._id;
}
const groups = await getGroup(userId.value);
teamGroups.value = groups || [];
} catch (error) {
console.error('获取团队列表失败:', error);
teamGroups.value = [];
}
};
const selectTeam = async (teamId) => {
currentTeamId.value = teamId;
const team = teamGroups.value.find(g => g.id === teamId);
if (team) {
currentTeamMembers.value = [
{ id: 'owner1', receiver: 'owner1', friendNickName: '创建者', username: 'Owner', role: 'owner' },
{ id: 'm1', receiver: 'm1', friendNickName: '管理员A', username: 'AdminA', role: 'admin' },
{ id: 'm2', receiver: 'm2', friendNickName: '成员B', username: 'MemberB', role: 'member' },
{ id: 'm3', receiver: 'm3', friendNickName: '成员C', username: 'MemberC', role: 'member' }
];
} else {
currentTeamMembers.value = [];
}
// 重置文件夹导航并加载团队文件
currentFolderPath.value = [];
navbarTitle.value = '云端数据';
await fetchTeamFileList(teamId);
};
const openTeamManage = () => {
showTeamManage.value = true;
if (teamGroups.value.length > 0 && !currentTeamId.value) {
selectTeam(teamGroups.value[0].id);
}
};
const closeTeamManage = () => {
showTeamManage.value = false;
inviteKeyword.value = '';
};
const setTeamAdmin = (member) => {
uni.showModal({
title: '设置管理员',
content: `确定将"${member.friendNickName || member.username}"设为管理员吗?`,
success: (res) => {
if (res.confirm) {
member.role = 'admin';
uni.showToast({ title: '已设为管理员', icon: 'success' });
}
}
});
};
const removeTeamMember = (member) => {
uni.showModal({
title: '移除成员',
content: `确定要移除"${member.friendNickName || member.username}"吗?`,
success: (res) => {
if (res.confirm) {
const index = currentTeamMembers.value.findIndex(
m => (m.id || m.receiver) === (member.id || member.receiver)
);
if (index !== -1) {
currentTeamMembers.value.splice(index, 1);
}
uni.showToast({ title: '已移除', icon: 'success' });
}
}
});
};
const inviteTeamMember = () => {
const keyword = inviteKeyword.value.trim();
if (!keyword) {
uni.showToast({ title: '请输入用户ID或名称', icon: 'none' });
return;
}
uni.showToast({ title: '邀请已发送', icon: 'success' });
inviteKeyword.value = '';
};
onMounted(async () => {
await fetchFileList();
await fetchTeamList();
});
</script>
<style scoped>
@import url("CloudDatabase.css");
</style>