Files
2026-07-02 18:01:56 +08:00

715 lines
20 KiB
Vue
Raw Permalink 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="cloud-db-detail-wrapper">
<view class="status-bar"></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">导入文件到数据库</view>
<view class="new-folder-name">表名称</view>
<view class="create-folder-name">
<input v-model="importTableName" type="text" placeholder="输入表名称" />
</view>
<view class="new-folder-name">文件</view>
<view class="upload-file-area" @click="handlePickFile">
<view v-if="importFileName" class="selected-file">{{ importFileName }}</view>
<view v-else class="upload-hint">点击选择文件支持 CSV/Excel/SQLite</view>
</view>
<view class="option-wrapper">
<view class="opt-btn opt-cancel" @click="closeNewTableModal">取消</view>
<view class="opt-btn opt-confirm" @click="confirmImportFile">确认导入</view>
</view>
</view>
</view>
<!-- ===== 权限管理弹窗 ===== -->
<view v-if="showPermission" class="popup-overlay" @click="closePermissionModal">
<view class="popup-card" @click.stop>
<view class="close-popup-card" @click="closePermissionModal">
<view class="iconfont icon-quxiao"></view>
</view>
<view class="popup-title">权限管理</view>
<scroll-view class="permission-body" scroll-y>
<view v-if="permissionList.length === 0" class="folder-null">暂无权限记录</view>
<view v-else class="permission-list">
<view class="permission-item" v-for="perm in permissionList" :key="perm._id || perm.id">
<view class="perm-info">
<!-- <text class="perm-name">{{ perm.name }}</text> -->
<text class="perm-type">{{ perm.permType === 'team_id' ? '团队' : '个人' }}</text>
</view>
<view class="perm-level">{{ getAccessLevelLabel(perm.access_level) }}</view>
<view class="perm-actions">
<view class="perm-action-btn" @click="changePermission(perm, 'change')">修改</view>
<view class="perm-action-btn delete" @click="changePermission(perm, 'delete')">移除</view>
</view>
</view>
</view>
</scroll-view>
<view class="close-permission-btn" @click="closePermissionModal">关闭</view>
</view>
</view>
<!-- ===== 移交团队弹窗 ===== -->
<view v-if="showTransfer" class="popup-overlay" @click="closeTransferModal">
<view class="popup-card" @click.stop>
<view class="close-popup-card" @click="closeTransferModal">
<view class="iconfont icon-quxiao"></view>
</view>
<view class="popup-title">移交数据库到团队</view>
<scroll-view class="transfer-body" scroll-y>
<view v-if="teamList.length === 0" class="folder-null">暂无可用团队</view>
<view v-else class="transfer-list">
<view class="transfer-item" v-for="team in teamList" :key="team.id"
:class="{ active: selectedTeamId === team.id }" @click="selectedTeamId = team.id">
<text class="transfer-name">{{ team.name }}</text>
<uni-icons v-if="selectedTeamId === team.id" type="checkmarkempty" size="20"
color="#3b86ff"></uni-icons>
</view>
</view>
</scroll-view>
<view class="option-wrapper">
<view class="opt-btn opt-cancel" @click="closeTransferModal">取消</view>
<view class="opt-btn opt-confirm" @click="confirmTransfer">确认移交</view>
</view>
</view>
</view>
<!-- ===== 主页面 ===== -->
<view class="cloud-db-detail page-container">
<view class="cloud-db-header">
<view class="custom-navbar">
<view class="navbar-left" @click="handleBack">
<view class="iconfont icon-fanhui custom-navbar-icon"></view>
</view>
<view class="navbar-title">{{ databaseName }}</view>
<view class="navbar-right">
<view class="iconfont icon-gengduo custom-navbar-icon" @click="handleMenu"></view>
</view>
</view>
<!-- 下拉菜单 -->
<view v-if="isMenuOpen" class="menu-card">
<view class="menu-card-item" @click="openImportModal(); handleMenu()">导入文件</view>
<view class="solid-line"></view>
<view class="menu-card-item" @click="openPermissionModal(); handleMenu()">权限管理</view>
<view v-if="!teamId" class="solid-line"></view>
<view v-if="!teamId" class="menu-card-item" @click="openTransferModal(); handleMenu()">移交团队</view>
</view>
</view>
<scroll-view class="cloud-db-content" scroll-y>
<!-- 数据库基本信息 -->
<view class="db-info-bar">
<text class="db-info-text">{{ dbTableCount }} 张表</text>
<text class="db-info-text">权限{{ getAccessLevelLabel(dbAccessLevel) }}</text>
</view>
<!-- 表列表 -->
<view class="section-block">
<view class="section-header">
<view class="section-title">
<view class="iconfont icon-biaoge"></view>
<text>数据表</text>
</view>
</view>
<view class="section-body">
<!-- 加载中 -->
<view v-if="isTableLoading" class="loading-area">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="tableList.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 tableList" :key="table.table_name"
@click="selectTable(table)" @longpress="handleDeleteTable(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.table_name }}</view>
<view class="table-desc">{{ table.row_count || 0 }} 条记录</view>
</view>
<view class="table-meta">
<uni-icons type="right" size="14" color="#999"></uni-icons>
</view>
</view>
</view>
</view>
</view>
<!-- 表数据查看 -->
<view v-if="currentTable" class="section-block">
<view class="section-divider"></view>
<view class="section-header">
<view class="section-title">
<view class="iconfont icon-biaoge"></view>
<text>{{ currentTable.table_name }}</text>
</view>
<view class="section-actions">
<text class="row-count">{{ tableData.length }} / {{ currentTable.row_count || 0 }} </text>
</view>
</view>
<view class="section-body">
<view v-if="tableData.length === 0" class="empty-tip">
<text>暂无数据</text>
</view>
<view v-else class="data-table-wrapper">
<scroll-view scroll-x class="data-table-scroll">
<view class="data-table">
<!-- 表头 -->
<view class="data-row data-header">
<view class="data-cell index-cell">#</view>
<view v-for="col in tableColumns" :key="col" class="data-cell">
{{ col }}
</view>
</view>
<!-- 数据行 -->
<view class="data-row" v-for="(row, rowIdx) in tableData"
:key="row._row_id || rowIdx" @click="openEditRow(row)">
<view class="data-cell index-cell">{{ rowIdx + 1 }}</view>
<view v-for="col in tableColumns" :key="col" class="data-cell">
{{ formatCellValue(row[col]) }}
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup>
import {
onMounted,
ref,
computed
} from 'vue';
import {
getToken,
getUserId
} from '@/utils/user-info.js';
import {
chooseFile
} from '@/uni_modules/lime-choose-file';
import {
getCloudDbTables,
deleteCloudDbTable,
getCloudDbTableData,
updateCloudDbTableData,
importFileToCloudDb,
getCloudDbPermission,
deleteCloudDbPermission,
updateCloudDbPermission,
dbTransferToTeam,
getTeamList
} from '@/utils/cloud-api.js';
// 声明 props 以避免 "Extraneous non-props attributes" 警告
const props = defineProps({
databaseId: {
type: String,
default: ''
},
databaseName: {
type: String,
default: ''
},
teamId: {
type: String,
default: ''
}
});
const databaseId = ref(props.databaseId || '');
const databaseName = ref(props.databaseName || '');
const teamId = ref(props.teamId || '');
const dbAccessLevel = ref(2);
const dbTableCount = ref(0);
const tableList = ref([]);
const isTableLoading = ref(false);
const currentTable = ref(null);
const tableData = ref([]);
const tableColumns = computed(() => {
// 如果有表数据,从数据中提取列名
if (tableData.value.length > 0) {
const keys = new Set();
tableData.value.forEach(row => Object.keys(row).forEach(k => keys.add(k)));
return Array.from(keys).filter(k => k !== '_row_id' && k !== '_id');
}
// 否则使用表结构中的 columns
if (currentTable.value?.columns) {
return currentTable.value.columns.map(c => c.name).filter(n => n !== '_row_id');
}
return [];
});
// 菜单
const isMenuOpen = ref(false);
const handleMenu = () => {
isMenuOpen.value = !isMenuOpen.value;
};
const handleBack = () => {
uni.navigateBack();
};
// ========== 数据加载 ==========
const fetchTables = async () => {
isTableLoading.value = true;
try {
const token = getToken();
const tables = await getCloudDbTables(token, databaseId.value, true);
// API 返回 { tables: [...] } 格式,字段名为 name / columns / row_count
// const tables = result?.tables || result || [];
tableList.value = (tables || []).map(t => ({
table_name: t.name,
row_count: t.row_count || 0,
columns: t.columns || []
}));
dbTableCount.value = tableList.value.length;
} catch (error) {
console.error('获取表列表失败:', error);
tableList.value = [];
} finally {
isTableLoading.value = false;
}
};
const selectTable = async (table) => {
currentTable.value = table;
tableData.value = [];
uni.showLoading({
title: '加载中...'
});
try {
const token = getToken();
const data = await getCloudDbTableData(token, databaseId.value, table.table_name);
tableData.value = data || [];
uni.hideLoading();
} catch (err) {
uni.hideLoading();
uni.showToast({
title: typeof err === 'string' ? err : '加载表数据失败',
icon: 'none'
});
tableData.value = [];
}
};
const handleDeleteTable = (table) => {
uni.showModal({
title: '删除表',
content: `确定要删除表"${table.table_name}"吗?此操作不可恢复。`,
success: async (res) => {
if (res.confirm) {
uni.showLoading({
title: '删除中...'
});
try {
const token = getToken();
await deleteCloudDbTable(token, databaseId.value, table.table_name);
uni.hideLoading();
uni.showToast({
title: '删除成功',
icon: 'success'
});
if (currentTable.value?.table_name === table.table_name) {
currentTable.value = null;
tableData.value = [];
}
await fetchTables();
} catch (error) {
uni.hideLoading();
console.error('删除表失败:', error);
uni.showToast({
title: '删除失败,请重试',
icon: 'error'
});
}
}
}
});
};
// ========== 数据编辑 ==========
const openEditRow = (row) => {
const rowId = row._row_id || row._id;
if (!rowId) {
uni.showToast({
title: '无法识别该行',
icon: 'none'
});
return;
}
// 收集可编辑字段
const columns = tableColumns.value;
if (columns.length === 0) {
uni.showToast({
title: '无可用列',
icon: 'none'
});
return;
}
// 简化:弹出编辑第一个字段
const firstCol = columns[0];
uni.showModal({
title: `编辑 ${firstCol}`,
editable: true,
placeholderText: row[firstCol] || '',
content: '',
success: async (res) => {
if (res.confirm && res.content !== undefined) {
uni.showLoading({
title: '更新中...'
});
try {
const token = getToken();
await updateCloudDbTableData(
token,
databaseId.value,
currentTable.value.table_name, {
_row_id: rowId
}, {
[firstCol]: res.content
}
);
uni.hideLoading();
uni.showToast({
title: '更新成功',
icon: 'success'
});
// 更新本地数据
row[firstCol] = res.content;
} catch (error) {
uni.hideLoading();
console.error('更新失败:', error);
uni.showToast({
title: error,
icon: 'error'
});
}
}
}
});
};
// ========== 导入文件 ==========
const showNewTable = ref(false);
const importTableName = ref('');
const importFileName = ref('');
const importFilePath = ref('');
const openImportModal = () => {
showNewTable.value = true;
importTableName.value = '';
importFileName.value = '';
importFilePath.value = '';
};
const closeNewTableModal = () => {
showNewTable.value = false;
};
const handlePickFile = () => {
chooseFile({
count: 1,
type: 'all',
success: (res) => {
const file = res.tempFiles[0];
importFileName.value = file.name || 'unknown';
importFilePath.value = file.path;
},
fail: (err) => {
console.error('选择文件失败:', err);
uni.showToast({
title: '选择文件失败',
icon: 'none'
});
}
});
};
const confirmImportFile = async () => {
const tableName = importTableName.value.trim() || "";
// if (!tableName) {
// uni.showToast({
// title: '请输入表名称',
// icon: 'none'
// });
// return;
// }
if (!importFilePath.value) {
uni.showToast({
title: '请选择文件',
icon: 'none'
});
return
}
uni.showLoading({
title: '导入中...'
});
try {
const token = getToken();
await importFileToCloudDb(token, false, databaseId.value, importFilePath.value, tableName);
uni.hideLoading();
uni.showToast({
title: '导入成功',
icon: 'success'
});
closeNewTableModal();
await fetchTables();
} catch (error) {
uni.hideLoading();
console.error('导入失败:', error);
if (error === "table_exists"){
uni.showToast({
title: '文件已存在',
icon: 'error'
});
}
else{
uni.showToast({
title: '导入失败,请重试',
icon: 'error'
});
}
}
};
// ========== 权限管理 ==========
const showPermission = ref(false);
const permissionList = ref([]);
const openPermissionModal = async () => {
showPermission.value = true;
uni.showLoading({
title: '加载中...'
});
try {
const token = getToken();
// 如果有 teamId 则查团队权限,否则查用户权限(传空字符串后端自动识别当前用户)
let result;
if (teamId.value) {
result = await getCloudDbPermission(token, databaseId.value, 'team_id', teamId.value);
} else {
result = await getCloudDbPermission(token, databaseId.value, 'user_id', getUserId());
}
const rawList = Array.isArray(result) ? result : [];
// 归一化:根据数据中是否存在 team 对象或 team_id 字段来判断类型
permissionList.value = rawList.map(item => {
if (item.team) {
// team_id 返回格式:团队权限
return {
_id: item._id,
database_id: item.database_id,
access_level: item.access_level,
permType: 'team_id',
id: item.team._id || item.team.id,
name: item.team.name
};
} else if (item.team_id) {
// user_id 返回格式但有 team_id说明数据库归属团队显示为团队
return {
database_id: item.database_id,
access_level: item.access_level,
permType: 'team_id',
id: item.team_id,
name: item.database?.database_name || ''
};
} else {
// user_id 返回格式且无 team_id真正的个人数据库
return {
database_id: item.database_id,
access_level: item.access_level,
permType: 'user_id',
id: item.user_id || item.database?.user_id || getUserId(),
name: item.database?.database_name || ''
};
}
});
} catch (error) {
console.error('获取权限失败:', error);
permissionList.value = [];
}
uni.hideLoading();
};
const closePermissionModal = () => {
showPermission.value = false;
};
const changePermission = (perm, action) => {
if (action === 'delete') {
uni.showModal({
title: '移除权限',
content: `确定要移除"${perm.name}"的权限吗?`,
success: async (res) => {
if (res.confirm) {
uni.showLoading({
title: '移除中...'
});
try {
const token = getToken();
await deleteCloudDbPermission(token, databaseId.value, perm.permType, perm.id);
uni.hideLoading();
uni.showToast({
title: '已移除',
icon: 'success'
});
await openPermissionModal();
} catch (error) {
uni.hideLoading();
console.error('移除权限失败:', error);
uni.showToast({
title: '操作失败,请重试',
icon: 'error'
});
}
}
}
});
} else {
// 修改权限级别
const levels = ['无权限访问', '仅访问', '管理员完全控制', '团队完全控制'];
const currentLevel = perm.access_level || 0;
uni.showActionSheet({
title: '选择权限级别',
itemList: levels.map((label, idx) => `${idx}. ${label}`),
success: async (res) => {
const newLevel = res.tapIndex;
if (newLevel === currentLevel) return;
uni.showLoading({
title: '更新中...'
});
try {
const token = getToken();
await updateCloudDbPermission(token, databaseId.value, perm.permType, perm.id,
newLevel);
uni.hideLoading();
uni.showToast({
title: '已更新',
icon: 'success'
});
await openPermissionModal();
} catch (error) {
uni.hideLoading();
console.error('更新权限失败:', error);
uni.showToast({
title: '操作失败,请重试',
icon: 'error'
});
}
}
});
}
};
// ========== 移交团队 ==========
const showTransfer = ref(false);
const teamList = ref([]);
const selectedTeamId = ref('');
const openTransferModal = async () => {
showTransfer.value = true;
selectedTeamId.value = '';
try {
const token = getToken();
const teams = await getTeamList(token);
teamList.value = teams || [];
} catch (error) {
console.error('获取团队列表失败:', error);
teamList.value = [];
}
};
const closeTransferModal = () => {
showTransfer.value = false;
};
const confirmTransfer = async () => {
if (!selectedTeamId.value) {
uni.showToast({
title: '请选择目标团队',
icon: 'none'
});
return;
}
uni.showModal({
title: '确认移交',
content: '移交后数据库将归属于该团队,确定继续吗?',
success: async (res) => {
if (res.confirm) {
uni.showLoading({
title: '移交中...'
});
try {
const token = getToken();
await dbTransferToTeam(token, databaseId.value, selectedTeamId.value);
uni.hideLoading();
uni.showToast({
title: '移交成功',
icon: 'success'
});
closeTransferModal();
setTimeout(() => {
uni.navigateBack();
}, 1000);
} catch (error) {
uni.hideLoading();
console.error('移交失败:', error);
uni.showToast({
title: '移交失败,请重试',
icon: 'error'
});
}
}
}
});
};
// ========== 工具函数 ==========
const getAccessLevelLabel = (level) => {
const labels = {
0: '无权限访问',
1: '仅访问',
2: '管理员完全控制',
3: '团队完全控制'
};
return labels[level] || '未知';
};
const formatCellValue = (val) => {
if (val === null || val === undefined) return '';
if (typeof val === 'object') return JSON.stringify(val);
return String(val);
};
onMounted(() => {
// 从路由参数获取数据
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
const options = currentPage.$page?.options || currentPage.options || {};
databaseId.value = options.databaseId || '';
databaseName.value = decodeURIComponent(options.databaseName || '数据库详情');
teamId.value = options.teamId || '';
if (databaseId.value) {
fetchTables();
}
});
</script>
<style scoped>
@import url("CloudDbDetail.css");
</style>