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

1563 lines
49 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="showNewDb" class="popup-overlay" @click="closeNewDbModal">
<view class="popup-card" @click.stop>
<view class="close-popup-card" @click="closeNewDbModal">
<view class="iconfont icon-quxiao"></view>
</view>
<view class="popup-title">新建个人数据库</view>
<view class="new-folder-name">数据库名称</view>
<view class="create-folder-name">
<input v-model="newDbName" type="text" placeholder="输入数据库名称" />
</view>
<view class="option-wrapper">
<view class="opt-btn opt-cancel" @click="closeNewDbModal">取消</view>
<view class="opt-btn opt-confirm" @click="confirmCreateDb">确认</view>
</view>
</view>
</view>
<!-- ===== 上传数据库文件弹窗 ===== -->
<view v-if="showUploadDb" class="popup-overlay" @click="closeUploadDbModal">
<view class="popup-card" @click.stop>
<view class="close-popup-card" @click="closeUploadDbModal">
<view class="iconfont icon-quxiao"></view>
</view>
<view class="popup-title">上传数据库文件</view>
<view class="upload-db-hint">支持 .db / .sqlite / .sqlite3 格式的 SQLite 数据库文件</view>
<view class="upload-file-area" @click="handlePickDbFile">
<view v-if="uploadDbFileName" class="selected-file">{{ uploadDbFileName }}</view>
<view v-else class="upload-hint">点击选择数据库文件</view>
</view>
<view class="option-wrapper">
<view class="opt-btn opt-cancel" @click="closeUploadDbModal">取消</view>
<view class="opt-btn opt-confirm" @click="confirmUploadDb">确认上传</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>
<text class="file-count">{{ !isDbLoading && personalDatabases.length > 0 ? '(' + personalDatabases.length + ')' : '' }}</text>
</view>
<view class="section-actions">
<view class="action-btn" @click="openUploadDbModal">
<uni-icons type="upload" size="16"></uni-icons>
<text>上传数据库</text>
</view>
<view class="action-btn" @click="openNewDbModal">
<uni-icons type="plus" size="16"></uni-icons>
<text>新建数据库</text>
</view>
</view>
</view>
<view class="section-body">
<!-- 加载中 -->
<view v-if="isDbLoading" class="loading-area">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="personalDatabases.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="db in personalDatabases" :key="db.database_id"
@click="handleDbClick(db)" @longpress="handleDeleteDb(db)">
<view class="table-icon">
<uni-icons type="compose" size="22" color="#3b86ff"></uni-icons>
</view>
<view class="table-info">
<view class="table-name">
{{ db.database_name }}
<text class="db-tag" :class="db.team_id ? 'tag-team' : 'tag-personal'">
{{ db.team_id ? '团队共享' : '个人私有' }}
</text>
</view>
<view class="table-desc">{{ db.table_count || 0 }} 张表 · {{ getAccessLevelLabel(db.access_level) }}</view>
</view>
<view class="table-meta">
<text class="table-rows">{{ formatDbTime(db.modified_time) }}</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>
<view class="section-body">
<!-- 加载中 -->
<view v-if="isDbLoading" class="loading-area">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="teamDatabases.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="db in teamDatabases" :key="db.database_id"
@click="handleDbClick(db)">
<view class="table-icon">
<uni-icons type="compose" size="22" color="#3b86ff"></uni-icons>
</view>
<view class="table-info">
<view class="table-name">
{{ db.database_name }}
<text class="db-tag tag-team">团队共享</text>
</view>
<view class="table-desc">{{ db.table_count || 0 }} 张表 · {{ getAccessLevelLabel(db.access_level) }}</view>
</view>
<view class="table-meta">
<text class="table-rows">{{ formatDbTime(db.modified_time) }}</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 { chooseFile } from '@/uni_modules/lime-choose-file';
import {
getUserInfo,
getCloudFileList,
getCloudFileUrl,
createWorkspaceFolder,
daleteWorkspace,
getWorkspaceFileURL,
getTeamList,
getTeamAllMember,
searchUsers,
addTeamMember,
updateTeamInfo,
updateTeamMemberRole,
removeTeamMember,
uploadCloudFile,
deleteCloudFile,
deleteTeam,
getCloudDbList,
createCloudDb,
uploadCloudDbFile,
deleteCloudDb
} 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;
// 下载成功,直接打开文件
openFile(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'
});
}
};
// 使用系统默认应用打开
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 = () => {
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);
}
});
};
// 执行上传
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 allDatabases = ref([]); // API 返回的全部数据库列表
const isDbLoading = ref(false); // 数据库加载中
// 个人展示全部数据库(个人私有 + 归属团队的),团队只展示属于当前选中团队的
const personalDatabases = computed(() => allDatabases.value);
const teamDatabases = computed(() => allDatabases.value.filter(db => db.team_id && String(db.team_id) === String(currentTeamId.value)));
const showNewDb = ref(false);
const newDbName = ref('');
// 获取数据库列表
const fetchDbList = async () => {
isDbLoading.value = true;
try {
const token = getToken();
const list = await getCloudDbList(token);
allDatabases.value = list || [];
} catch (error) {
console.error('获取数据库列表失败:', error);
allDatabases.value = [];
} finally {
isDbLoading.value = false;
}
};
const openNewDbModal = () => {
showNewDb.value = true;
newDbName.value = '';
};
const closeNewDbModal = () => {
showNewDb.value = false;
newDbName.value = '';
};
const confirmCreateDb = async () => {
const name = newDbName.value.trim();
if (!name) {
uni.showToast({ title: '请输入数据库名称', icon: 'none' });
return;
}
uni.showLoading({ title: '创建中...' });
try {
const token = getToken();
await createCloudDb(token, name);
uni.hideLoading();
uni.showToast({ title: '创建成功', icon: 'success' });
closeNewDbModal();
await fetchDbList();
} catch (error) {
uni.hideLoading();
console.error('创建数据库失败:', error);
uni.showToast({ title: '创建失败,请重试', icon: 'error' });
}
};
// ========== 上传数据库文件 ==========
const showUploadDb = ref(false);
const uploadDbFileName = ref('');
const uploadDbFilePath = ref('');
const openUploadDbModal = () => {
showUploadDb.value = true;
uploadDbFileName.value = '';
uploadDbFilePath.value = '';
};
const closeUploadDbModal = () => {
showUploadDb.value = false;
};
const handlePickDbFile = () => {
chooseFile({
count: 1,
type: 'all',
success: (res) => {
const file = res.tempFiles[0];
uploadDbFileName.value = file.name || 'unknown';
uploadDbFilePath.value = file.path;
},
fail: (err) => {
console.error('选择文件失败:', err);
uni.showToast({ title: '选择文件失败', icon: 'none' });
}
});
};
const confirmUploadDb = async () => {
if (!uploadDbFilePath.value) {
uni.showToast({ title: '请选择数据库文件', icon: 'none' });
return;
}
// 校验文件后缀
const ext = uploadDbFileName.value.split('.').pop()?.toLowerCase();
const allowedExts = ['db', 'sqlite', 'sqlite3'];
if (!allowedExts.includes(ext)) {
uni.showToast({ title: '仅支持 .db / .sqlite / .sqlite3 文件', icon: 'none' });
return;
}
uni.showLoading({ title: '上传中...' });
try {
const token = getToken();
await uploadCloudDbFile(token, uploadDbFilePath.value);
uni.hideLoading();
uni.showToast({ title: '上传成功', icon: 'success' });
closeUploadDbModal();
await fetchDbList();
} catch (error) {
uni.hideLoading();
console.error('上传数据库失败:', error);
uni.showToast({ title: '上传失败,请重试', icon: 'error' });
}
};
const handleDeleteDb = (db) => {
uni.showModal({
title: '删除数据库',
content: `确定要删除数据库"${db.database_name}"吗?此操作不可恢复。`,
success: async (res) => {
if (res.confirm) {
uni.showLoading({ title: '删除中...' });
try {
const token = getToken();
await deleteCloudDb(token, db.database_id);
uni.hideLoading();
uni.showToast({ title: '删除成功', icon: 'success' });
await fetchDbList();
} catch (error) {
uni.hideLoading();
console.error('删除数据库失败:', error);
uni.showToast({ title: '删除失败,请重试', icon: 'error' });
}
}
}
});
};
const handleDbClick = (db) => {
uni.navigateTo({
url: `/pages/CloudDbDetail/CloudDbDetail?databaseId=${db.database_id}&databaseName=${encodeURIComponent(db.database_name)}&teamId=${db.team_id || ''}`
});
};
const formatDbTime = (timestamp) => {
if (!timestamp) return '';
const date = new Date(timestamp * 1000);
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
};
const getAccessLevelLabel = (level) => {
const labels = { 0: '无权限', 1: '只读', 2: '读写', 3: '管理' };
return labels[level] || '未知';
};
// ========== 团队管理 ==========
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();
await fetchDbList();
});
</script>
<style scoped>
@import url("CloudDatabase.css");
</style>