Files
phone-app------test1-/pages/CloudDatabase/CloudDatabase.vue

1576 lines
48 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.receiver || member.user_id) === userId" class="member-role" :class="currentUserRoleInTeam">
{{ currentUserRoleInTeam === 'owner' ? '创建者' : '管理员' }}
</view>
<!-- 他人owner 不可操作 -->
<view v-else-if="member.role === 'owner'" class="member-role owner">创建者</view>
<!-- 其他 admin可降级 -->
<view v-else-if="member.role === 'admin'" class="member-actions-right">
<view class="member-action-btn demote-admin" @click="toggleMemberRole(member, 'member')">降为成员</view>
<view class="member-action-btn remove-member" @click="removeTeamMemberHandler(member)">移除</view>
</view>
<!-- 普通成员可提升或移除 -->
<view v-else class="member-actions-right">
<view class="member-action-btn set-admin" @click="toggleMemberRole(member, 'admin')">设为管理</view>
<view class="member-action-btn remove-member" @click="removeTeamMemberHandler(member)">移除</view>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 邀请成员 -->
<view class="invite-section">
<view class="invite-input-wrapper">
<input class="invite-input" placeholder="输入用户名或邮箱搜索" v-model="inviteKeyword"
@confirm="searchTeamMember" />
<view class="invite-btn text-btn" @click="searchTeamMember">搜索</view>
</view>
<!-- 搜索结果 -->
<view v-if="searchResultList.length > 0" class="search-result-list">
<view class="search-result-item" v-for="user in searchResultList" :key="user._id"
@click="addTeamMemberHandler(user)">
<image v-if="user.avatar" :src="user.avatar" class="result-avatar" mode="aspectFill"></image>
<view v-else class="result-default-avatar">
<uni-icons type="person-filled" size="40rpx" color="#fff"></uni-icons>
</view>
<view class="result-info">
<text class="result-name">{{ user.username }}</text>
<text class="result-email">{{ user.email }}</text>
</view>
<view class="result-add-btn">添加</view>
</view>
</view>
<view v-else-if="inviteKeyword && searched" class="search-empty">未找到匹配用户</view>
</view>
<!-- 修改团队信息 -->
<view class="edit-team-section">
<view class="edit-team-btn" @click="openEditTeamModal">修改团队信息</view>
</view>
<!-- 删除团队 -->
<view class="delete-team-section">
<view class="delete-team-btn" @click="handleDeleteTeam">删除团队</view>
</view>
</view>
</view>
<!-- ===== 修改团队信息弹窗 ===== -->
<view v-if="showEditTeam" class="popup-overlay" @click="closeEditTeamModal">
<view class="popup-card" @click.stop>
<view class="close-popup-card" @click="closeEditTeamModal">
<view class="iconfont icon-quxiao"></view>
</view>
<view class="popup-title">修改团队信息</view>
<view class="edit-team-form">
<view class="form-label">团队名称</view>
<view class="create-folder-name">
<input v-model="editTeamName" type="text" placeholder="输入团队名称" />
</view>
<view class="form-label">团队描述</view>
<view class="create-folder-name">
<input v-model="editTeamDesc" type="text" placeholder="输入团队描述" />
</view>
<!-- <view class="form-label">团队类型</view>
<view class="create-folder-name">
<input v-model="editTeamType" type="text" placeholder="如public / private" />
</view> -->
</view>
<view class="option-wrapper">
<view class="opt-btn opt-cancel" @click="closeEditTeamModal">取消</view>
<view class="opt-btn opt-confirm" @click="confirmEditTeam">确认修改</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="showUploadPathInput" class="popup-overlay" @click="closeUploadPathInput">
<view class="popup-card" @click.stop>
<view class="close-popup-card" @click="closeUploadPathInput">
<view class="iconfont icon-quxiao"></view>
</view>
<view class="popup-title">上传文件</view>
<view class="upload-path-label">上传路径</view>
<view class="create-folder-name">
<input v-model="uploadPath" type="text" placeholder="输入上传路径,如:/myfolder" />
</view>
<view class="upload-path-hint">默认为根目录 /后端会自动创建不存在的文件夹</view>
<view class="option-wrapper">
<view class="opt-btn opt-cancel" @click="closeUploadPathInput">取消</view>
<view class="opt-btn opt-confirm" @click="confirmUploadPath">确认上传</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="handleUploadFile">
<uni-icons type="upload" size="16"></uni-icons>
<text>上传文件</text>
</view>
<!-- <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 v-if="currentTeamType || currentTeamDescription" class="team-info">
<text v-if="currentTeamType" class="team-type-tag">{{ currentTeamType }}</text>
<text v-if="currentTeamDescription" class="team-desc-text">{{ currentTeamDescription }}</text>
</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="handleUploadFile">
<uni-icons type="upload" size="16"></uni-icons>
<text>上传文件</text>
</view>
<!-- <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,
getTeamList,
getTeamAllMember,
searchUsers,
addTeamMember,
updateTeamInfo,
updateTeamMemberRole,
removeTeamMember,
uploadCloudFile,
deleteCloudFile,
deleteTeam
} from '@/utils/cloud-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 {
teamId = teamId == null ? "" : String(teamId);
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 端:复制文件到 /storage/emulated/0/yxd/
const YXD_PATH = '/storage/emulated/0/yxd/';
const ensureYxdDir = (callback) => {
plus.io.resolveLocalFileSystemURL(YXD_PATH, (dirEntry) => {
// 目录已存在
callback(dirEntry);
}, () => {
// 目录不存在,先创建
plus.io.resolveLocalFileSystemURL('/storage/emulated/0/', (rootEntry) => {
rootEntry.getDirectory('yxd', { create: true }, (dirEntry) => {
callback(dirEntry);
}, (err) => {
console.error('创建 yxd 目录失败:', JSON.stringify(err));
uni.showToast({ title: '创建目录失败', icon: 'error' });
openFileConfirm(fileName, tempPath);
});
}, (err) => {
console.error('访问外部存储根目录失败:', JSON.stringify(err));
uni.showToast({ title: '无法访问存储目录', icon: 'error' });
openFileConfirm(fileName, tempPath);
});
});
};
ensureYxdDir((yxdDirEntry) => {
plus.io.resolveLocalFileSystemURL(tempPath, (fileEntry) => {
fileEntry.copyTo(
yxdDirEntry,
fileName,
() => {
const targetPath = YXD_PATH + fileName;
console.log('文件已保存到:', targetPath);
openFileConfirm(fileName, targetPath);
},
(err) => {
console.error('复制文件失败:', JSON.stringify(err));
openFileConfirm(fileName, tempPath);
}
);
}, (err) => {
console.error('读取临时文件失败:', JSON.stringify(err));
openFileConfirm(fileName, tempPath);
});
});
// #endif
// #ifndef APP-PLUS
// 小程序/H5 端:使用 uni.saveFile 保存
uni.saveFile({
tempFilePath: tempPath,
success: (saveRes) => {
openFileConfirm(fileName, saveRes.savedFilePath);
},
fail: () => {
// saveFile 失败则直接确认打开
openFileConfirm(fileName, tempPath);
}
});
// #endif
};
// 下载成功后确认是否打开文件
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) => {
const tipText = isFolder(file) ?
`确定要删除目录"${file.name}"吗?目录内所有文件将被一并删除。` :
`确定要删除文件"${file.name}"吗?`;
uni.showModal({
title: '删除确认',
content: tipText,
confirmText: '删除',
confirmColor: '#e74c3c',
success: async (res) => {
if (res.confirm) {
uni.showLoading({
title: '删除中...'
});
try {
// 优先使用 deleteCloudFile API
await deleteCloudFile(userToken.value, file.id);
uni.hideLoading();
uni.showToast({
title: '删除成功',
icon: 'success'
});
refreshCurrentFileList();
} catch (error) {
uni.hideLoading();
// 如果 deleteCloudFile 失败,尝试用 daleteWorkspace 兜底
console.error('deleteCloudFile 失败,尝试 daleteWorkspace', error);
try {
const filePath = file.path || ('/' + file.name);
if (workspaceId.value) {
await daleteWorkspace(userToken.value, workspaceId.value, filePath);
}
uni.showToast({
title: '删除成功',
icon: 'success'
});
refreshCurrentFileList();
} catch (err2) {
console.error('删除失败:', err2);
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 showUploadPathInput = ref(false);
const uploadPath = ref('/');
const handleUploadFile = () => {
showUploadPathInput.value = true;
uploadPath.value = '/';
};
const closeUploadPathInput = () => {
showUploadPathInput.value = false;
};
const confirmUploadPath = () => {
const path = uploadPath.value.trim();
if (!path) {
uploadPath.value = '/';
}
showUploadPathInput.value = false;
chooseAndUploadFile();
};
// 选择文件并上传
const chooseAndUploadFile = () => {
// #ifdef APP-PLUS
// App 端使用系统文件选择器
plus.io.chooseFile(
(res) => {
// plus.io.chooseFile 返回格式可能是 { files: [...] } 或直接是数组
const files = Array.isArray(res) ? res : (res.files || []);
if (files.length === 0) {
uni.showToast({
title: '未选择文件',
icon: 'none'
});
return;
}
doUploadFiles(files);
},
(err) => {
if (err.code !== 11) { // 11 是用户取消
console.error('选择文件失败:', err);
}
}
);
// #endif
// #ifndef APP-PLUS
// 小程序/H5 端使用 uni.chooseFileH5 可能不支持)
uni.chooseFile({
count: 10,
type: 'all',
success: (res) => {
const tempFiles = (res.tempFiles || []).map(f => ({
tempFilePath: f.path || f.tempFilePath,
name: f.name
}));
if (tempFiles.length === 0) {
uni.showToast({
title: '未选择文件',
icon: 'none'
});
return;
}
doUploadFiles(tempFiles);
},
fail: (err) => {
console.error('选择文件失败:', err);
}
});
// #endif
};
// 执行上传
const doUploadFiles = async (files) => {
uni.showLoading({
title: '上传中...'
});
try {
// 使用用户输入的路径,默认为 /
const path = uploadPath.value.trim() || '/';
const type = currentCategory.value === 'user' ? 'user' : 'team';
const teamId = currentCategory.value === 'team' ? currentTeamId.value : '';
await uploadCloudFile(userToken.value, type, path, files, teamId);
uni.hideLoading();
uni.showToast({
title: '上传成功',
icon: 'success'
});
refreshCurrentFileList();
} catch (error) {
uni.hideLoading();
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 currentTeamDescription = computed(() => {
if (!currentTeamId.value) return '';
const team = teamGroups.value.find(g => g.id === currentTeamId.value);
return team?.description || '';
});
// 当前选中团队的类型
const currentTeamType = computed(() => {
if (!currentTeamId.value) return '';
const team = teamGroups.value.find(g => g.id === currentTeamId.value);
return team?.type || '';
});
// 当前用户在该团队中的角色(通过成员列表匹配 userId
const currentUserRoleInTeam = computed(() => {
if (!userId.value || currentTeamMembers.value.length === 0) return 'member';
const me = currentTeamMembers.value.find(m => m.receiver === userId.value || m.user_id === userId.value);
return me?.role || 'member';
});
// 当前用户是否为团队管理员admin 或 owner
const isTeamAdmin = computed(() => {
const role = currentUserRoleInTeam.value;
return role === 'admin' || role === 'owner';
});
const fetchTeamList = async () => {
try {
const token = getToken();
const groups = await getTeamList(token);
teamGroups.value = groups || [];
} catch (error) {
console.error('获取团队列表失败:', error);
teamGroups.value = [];
}
};
const selectTeam = async (teamId) => {
console.log("团队id为", teamId);
currentTeamId.value = teamId;
try {
const token = getToken();
const members = await getTeamAllMember(token, teamId);
// 映射 API 返回的成员数据到模板需要的字段
currentTeamMembers.value = (members || []).map(item => ({
id: item._id,
receiver: item.user_id,
user_id: item.user_id,
friendNickName: item.user?.username || '',
username: item.user?.username || '',
avatar: item.user?.avatar || '',
email: item.user?.email || '',
role: item.role || item.user?.role || 'member'
}));
} catch (error) {
console.error('获取团队成员失败:', error);
currentTeamMembers.value = [];
}
// 重置文件夹导航并加载团队文件
currentFolderPath.value = [];
navbarTitle.value = '云端数据';
await fetchTeamFileList(teamId);
};
const openTeamManage = async () => {
// 非管理员不能操作团队
if (currentTeamId.value) {
// 先获取成员列表判断当前用户角色
try {
const token = getToken();
const members = await getTeamAllMember(token, currentTeamId.value);
const me = (members || []).find(m => m.user_id === userId.value);
const myRole = me?.role || 'member';
if (myRole !== 'admin' && myRole !== 'owner') {
uni.showToast({ title: '仅管理员可管理团队', icon: 'none' });
return;
}
// 同时填充成员数据
currentTeamMembers.value = (members || []).map(item => ({
id: item._id,
receiver: item.user_id,
user_id: item.user_id,
friendNickName: item.user?.username || '',
username: item.user?.username || '',
avatar: item.user?.avatar || '',
email: item.user?.email || '',
role: item.role || item.user?.role || 'member'
}));
} catch (error) {
console.error('获取团队成员失败:', error);
uni.showToast({ title: '获取团队信息失败', icon: 'none' });
return;
}
} else if (teamGroups.value.length > 0) {
// 没有选中团队时,先选中第一个
await selectTeam(teamGroups.value[0].id);
// 选中后递归检查权限
showTeamManage.value = true;
// 上面 selectTeam 已填充了成员,直接判断
const me = currentTeamMembers.value.find(m => (m.receiver || m.user_id) === userId.value);
const myRole = me?.role || 'member';
if (myRole !== 'admin' && myRole !== 'owner') {
showTeamManage.value = false;
uni.showToast({ title: '仅管理员可管理团队', icon: 'none' });
return;
}
}
showTeamManage.value = true;
};
const closeTeamManage = () => {
showTeamManage.value = false;
inviteKeyword.value = '';
searchResultList.value = [];
searched.value = false;
};
// ========== 修改团队信息 ==========
const showEditTeam = ref(false);
const editTeamName = ref('');
const editTeamDesc = ref('');
const editTeamType = ref('');
const openEditTeamModal = () => {
const team = teamGroups.value.find(g => g.id === currentTeamId.value);
if (!team) {
uni.showToast({ title: '请先选择团队', icon: 'none' });
return;
}
editTeamName.value = team.name || '';
editTeamDesc.value = team.description || '';
editTeamType.value = team.type || '';
showEditTeam.value = true;
};
const closeEditTeamModal = () => {
showEditTeam.value = false;
};
const confirmEditTeam = async () => {
const name = editTeamName.value.trim();
if (!name) {
uni.showToast({ title: '请输入团队名称', icon: 'none' });
return;
}
uni.showLoading({ title: '修改中...' });
try {
const token = getToken();
await updateTeamInfo(token, currentTeamId.value, {
team_name: name,
team_description: editTeamDesc.value.trim(),
team_type: editTeamType.value.trim()
});
uni.hideLoading();
uni.showToast({ title: '修改成功', icon: 'success' });
closeEditTeamModal();
// 刷新团队列表
await fetchTeamList();
// 保持当前团队选中状态
currentTeamId.value = currentTeamId.value;
} catch (error) {
uni.hideLoading();
console.error('修改团队信息失败:', error);
uni.showToast({ title: '修改失败,请重试', icon: 'error' });
}
};
// 切换成员角色(管理员 ↔ 成员)
const toggleMemberRole = async (member, newRole) => {
const roleLabel = newRole === 'admin' ? '管理员' : '成员';
const memberName = member.friendNickName || member.username || member.name;
uni.showModal({
title: newRole === 'admin' ? '设为管理员' : '降为成员',
content: `确定将"${memberName}"${newRole === 'admin' ? '设为管理员' : '降为普通成员'}吗?`,
success: async (res) => {
if (res.confirm) {
uni.showLoading({ title: '操作中...' });
try {
const token = getToken();
const memberUserId = member.receiver || member.user_id;
await updateTeamMemberRole(token, currentTeamId.value, memberUserId, newRole);
uni.hideLoading();
uni.showToast({ title: `${newRole === 'admin' ? '设为管理员' : '降为成员'}`, icon: 'success' });
// 刷新成员列表
await refreshTeamMembers();
} catch (error) {
uni.hideLoading();
console.error('调整成员身份失败:', error);
uni.showToast({ title: '操作失败,请重试', icon: 'error' });
}
}
}
});
};
const removeTeamMemberHandler = async (member) => {
const memberName = member.friendNickName || member.username || member.name;
uni.showModal({
title: '移除成员',
content: `确定要移除"${memberName}"吗?`,
success: async (res) => {
if (res.confirm) {
uni.showLoading({ title: '移除中...' });
try {
const token = getToken();
const memberUserId = member.receiver || member.user_id;
await removeTeamMember(token, currentTeamId.value, [memberUserId]);
uni.hideLoading();
uni.showToast({ title: '已移除', icon: 'success' });
// 刷新成员列表
await refreshTeamMembers();
} catch (error) {
uni.hideLoading();
console.error('移除团队成员失败:', error);
uni.showToast({ title: '移除失败,请重试', icon: 'error' });
}
}
}
});
};
// 刷新团队成员列表
const refreshTeamMembers = async () => {
const token = getToken();
const members = await getTeamAllMember(token, currentTeamId.value);
currentTeamMembers.value = (members || []).map(item => ({
id: item._id,
receiver: item.user_id,
user_id: item.user_id,
friendNickName: item.user?.username || '',
username: item.user?.username || '',
avatar: item.user?.avatar || '',
email: item.user?.email || '',
role: item.role || 'member'
}));
};
const searchResultList = ref([]);
const searched = ref(false);
const searchTeamMember = async () => {
const keyword = inviteKeyword.value.trim();
if (!keyword) {
uni.showToast({ title: '请输入用户名或邮箱', icon: 'none' });
return;
}
try {
const token = getToken();
const users = await searchUsers(token, keyword);
searchResultList.value = users || [];
searched.value = true;
if (searchResultList.value.length === 0) {
// 未找到用户,不弹 toast由 UI 提示
}
} catch (error) {
console.error('搜索用户失败:', error);
searchResultList.value = [];
searched.value = true;
}
};
const addTeamMemberHandler = (user) => {
uni.showModal({
title: '添加成员',
content: `确定将"${user.username}"添加到当前团队吗?`,
success: async (res) => {
if (res.confirm) {
uni.showLoading({ title: '添加中...' });
try {
const token = getToken();
await addTeamMember(token, currentTeamId.value, [user._id]);
uni.hideLoading();
uni.showToast({ title: '添加成功', icon: 'success' });
// 清空搜索状态
inviteKeyword.value = '';
searchResultList.value = [];
searched.value = false;
// 刷新成员列表
await refreshTeamMembers();
} catch (error) {
uni.hideLoading();
console.error('添加团队成员失败:', error);
uni.showToast({ title: '添加失败,请重试', icon: 'error' });
}
}
}
});
};
const handleDeleteTeam = () => {
const team = teamGroups.value.find(g => g.id === currentTeamId.value);
const teamName = team ? team.name : '当前团队';
uni.showModal({
title: '删除团队',
content: `确定要删除团队"${teamName}"吗?此操作不可撤销,团队内所有文件和数据将被永久删除。`,
confirmText: '删除',
confirmColor: '#e74c3c',
success: async (res) => {
if (res.confirm) {
uni.showLoading({ title: '删除中...' });
try {
await deleteTeam(userToken.value, currentTeamId.value);
uni.hideLoading();
uni.showToast({ title: '团队已删除', icon: 'success' });
// 清除当前团队状态
currentTeamId.value = '';
currentTeamMembers.value = [];
teamFiles.value = [];
currentFolderPath.value = [];
navbarTitle.value = '云端数据';
// 刷新团队列表
await fetchTeamList();
closeTeamManage();
} catch (error) {
uni.hideLoading();
console.error('删除团队失败:', error);
uni.showToast({ title: '删除失败,请重试', icon: 'error' });
}
}
}
});
};
onMounted(async () => {
await fetchFileList();
await fetchTeamList();
});
</script>
<style scoped>
@import url("CloudDatabase.css");
</style>