数据库页面的对象资源管理器简化成“库 → 表”两层:去掉“数据库”“表”“列”三层文件夹和 emoji;头部“+ 新建库”和刷新,每个库下面“+ 新建表”;点表名打开编辑前 200 行(已经开着就切过去),库和表右边的 ⋯ 和右键是同一个菜单(新建表 / 新建查询 / 删除数据库;编辑数据 / 查询前 1000 行 / 设计表结构 / 删除表),⋯ 打开的菜单出在按钮下面;鼠标停在表名上显示列。顶部去掉重复的“新建数据库”按钮;使用说明、数据库截图和设计方案同步
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b292d38ba7
commit
f9a2739353
5 files changed
+159
-127
No files matched your search
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 21 KiB |
+147
-121
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// 对象资源管理器(仿 SSMS):数据库 → 库 → 表 → 列。右键菜单里的操作交给页面处理。
|
||||
// 对象资源管理器:只有“库 → 表”两层。点表名打开编辑数据,其他操作在 ⋯ 里(右键也是同一个菜单,照书上右键操作也行)。
|
||||
import { NDropdown, type DropdownOption } from 'naive-ui'
|
||||
import { nextTick, reactive, ref } from 'vue'
|
||||
import type { ExplorerDatabase, ExplorerTable } from './sql'
|
||||
@@ -18,22 +18,25 @@ export type ExplorerAction =
|
||||
defineProps<{ databases: ExplorerDatabase[] }>()
|
||||
const emit = defineEmits<{ action: [action: ExplorerAction] }>()
|
||||
|
||||
/** 展开的节点:root、db:库名、tables:库名、table:库名/表名、columns:库名/表名 */
|
||||
const expanded = reactive(new Set<string>(['root']))
|
||||
const toggle = (key: string) => (expanded.has(key) ? expanded.delete(key) : expanded.add(key))
|
||||
/** 收起的库(默认都展开,学生的库不多) */
|
||||
const collapsed = reactive(new Set<string>())
|
||||
const toggle = (name: string) => (collapsed.has(name) ? collapsed.delete(name) : collapsed.add(name))
|
||||
|
||||
// ---------- 右键菜单 ----------
|
||||
// ---------- 菜单(⋯ 按钮和右键共用) ----------
|
||||
const menu = ref<{ x: number; y: number; options: DropdownOption[]; actions: Map<string, ExplorerAction> } | null>(null)
|
||||
|
||||
function openMenu(event: MouseEvent, items: [string, ExplorerAction][]) {
|
||||
function openMenu(event: MouseEvent, items: [string, ExplorerAction | null][]) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
// 右键:菜单出在鼠标处;点 ⋯(也可能是键盘按的,没有鼠标坐标):出在按钮下面
|
||||
const anchor = event.type === 'contextmenu' ? null : (event.currentTarget as HTMLElement).getBoundingClientRect()
|
||||
menu.value = null
|
||||
void nextTick(() => {
|
||||
menu.value = {
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
options: items.map(([label], i) => (label === '-' ? { type: 'divider', key: `d${i}` } : { label, key: String(i) })),
|
||||
actions: new Map(items.map(([, action], i) => [String(i), action])),
|
||||
x: anchor ? anchor.left : event.clientX,
|
||||
y: anchor ? anchor.bottom : event.clientY,
|
||||
options: items.map(([label, action], i) => (action ? { label, key: String(i) } : { type: 'divider', key: `d${i}` })),
|
||||
actions: new Map(items.flatMap(([, action], i) => (action ? [[String(i), action] as const] : []))),
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -44,107 +47,68 @@ function choose(key: string) {
|
||||
if (action) emit('action', action)
|
||||
}
|
||||
|
||||
const REFRESH: ExplorerAction = { kind: 'refresh' }
|
||||
|
||||
function rootMenu(event: MouseEvent) {
|
||||
openMenu(event, [
|
||||
['新建数据库…', { kind: 'newDatabase' }],
|
||||
['-', REFRESH],
|
||||
['刷新', REFRESH],
|
||||
])
|
||||
}
|
||||
|
||||
function databaseMenu(event: MouseEvent, database: string) {
|
||||
openMenu(event, [
|
||||
['新建表', { kind: 'newTable', database }],
|
||||
['新建查询', { kind: 'newQuery', database }],
|
||||
['新建表…', { kind: 'newTable', database }],
|
||||
['-', REFRESH],
|
||||
['删除', { kind: 'dropDatabase', database }],
|
||||
['刷新', REFRESH],
|
||||
])
|
||||
}
|
||||
|
||||
function tablesMenu(event: MouseEvent, database: string) {
|
||||
openMenu(event, [
|
||||
['新建表…', { kind: 'newTable', database }],
|
||||
['-', REFRESH],
|
||||
['刷新', REFRESH],
|
||||
['-', null],
|
||||
['删除数据库', { kind: 'dropDatabase', database }],
|
||||
])
|
||||
}
|
||||
|
||||
function tableMenu(event: MouseEvent, database: string, table: ExplorerTable) {
|
||||
openMenu(event, [
|
||||
['新建表…', { kind: 'newTable', database }],
|
||||
['设计', { kind: 'design', database, table }],
|
||||
['选择前 1000 行', { kind: 'selectRows', database, table }],
|
||||
['编辑前 200 行', { kind: 'editRows', database, table }],
|
||||
['-', REFRESH],
|
||||
['删除', { kind: 'dropTable', database, table }],
|
||||
['刷新', REFRESH],
|
||||
['编辑数据(前 200 行)', { kind: 'editRows', database, table }],
|
||||
['查询数据(前 1000 行)', { kind: 'selectRows', database, table }],
|
||||
['设计表结构', { kind: 'design', database, table }],
|
||||
['-', null],
|
||||
['删除表', { kind: 'dropTable', database, table }],
|
||||
])
|
||||
}
|
||||
|
||||
function columnText(column: ExplorerTable['columns'][number]) {
|
||||
const flags = [column.primaryKey ? 'PK' : '', column.type, column.nullable ? 'null' : 'not null'].filter(Boolean)
|
||||
return `${column.name} (${flags.join(', ')})`
|
||||
/** 鼠标停在表名上显示的列:每列一行“列名 类型 主键 / 可空” */
|
||||
function columnsTip(table: ExplorerTable) {
|
||||
const lines = table.columns.map((c) => [c.name, c.type, c.primaryKey ? '主键' : '', c.nullable ? '可空' : ''].filter(Boolean).join(' '))
|
||||
return `点一下编辑数据\n\n${lines.join('\n')}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="explorer">
|
||||
<div class="title">对象资源管理器</div>
|
||||
<ul class="tree">
|
||||
<li>
|
||||
<div class="node" @click="toggle('root')" @contextmenu="rootMenu">
|
||||
<span class="arrow">{{ expanded.has('root') ? '▾' : '▸' }}</span
|
||||
>📁 数据库
|
||||
<div class="head">
|
||||
<span class="title">数据库</span>
|
||||
<button class="head-btn" title="刷新" @click="emit('action', { kind: 'refresh' })">↻</button>
|
||||
<button class="head-btn add" @click="emit('action', { kind: 'newDatabase' })">+ 新建库</button>
|
||||
</div>
|
||||
|
||||
<div class="tree">
|
||||
<p v-if="databases.length === 0" class="empty">还没有数据库。点上面的“+ 新建库”,或者在页面顶部“恢复实验初始数据”。</p>
|
||||
|
||||
<div v-for="db in databases" :key="db.name" class="db">
|
||||
<div class="row db-row" @click="toggle(db.name)" @contextmenu="databaseMenu($event, db.name)">
|
||||
<span class="arrow">{{ collapsed.has(db.name) ? '▸' : '▾' }}</span>
|
||||
<span class="name">{{ db.name }}</span>
|
||||
<span class="count">{{ db.tables.length }} 张表</span>
|
||||
<button class="more" title="更多操作" @click="databaseMenu($event, db.name)">⋯</button>
|
||||
</div>
|
||||
<ul v-if="expanded.has('root')">
|
||||
<li v-if="databases.length === 0" class="empty">(还没有数据库:右键"数据库"新建,或在上方恢复实验初始数据)</li>
|
||||
<li v-for="db in databases" :key="db.name">
|
||||
<div class="node" @click="toggle(`db:${db.name}`)" @contextmenu="databaseMenu($event, db.name)">
|
||||
<span class="arrow">{{ expanded.has(`db:${db.name}`) ? '▾' : '▸' }}</span
|
||||
>🛢 {{ db.name }}
|
||||
</div>
|
||||
<ul v-if="expanded.has(`db:${db.name}`)">
|
||||
<li>
|
||||
<div class="node" @click="toggle(`tables:${db.name}`)" @contextmenu="tablesMenu($event, db.name)">
|
||||
<span class="arrow">{{ expanded.has(`tables:${db.name}`) ? '▾' : '▸' }}</span
|
||||
>📁 表
|
||||
</div>
|
||||
<ul v-if="expanded.has(`tables:${db.name}`)">
|
||||
<li v-if="db.tables.length === 0" class="empty">(没有表)</li>
|
||||
<li v-for="table in db.tables" :key="table.name">
|
||||
<div
|
||||
class="node"
|
||||
title="右键:编辑前 200 行、设计、删除"
|
||||
@click="toggle(`table:${db.name}/${table.name}`)"
|
||||
@contextmenu="tableMenu($event, db.name, table)"
|
||||
>
|
||||
<span class="arrow">{{ expanded.has(`table:${db.name}/${table.name}`) ? '▾' : '▸' }}</span
|
||||
>▦ dbo.{{ table.name }}
|
||||
</div>
|
||||
<ul v-if="expanded.has(`table:${db.name}/${table.name}`)">
|
||||
<li>
|
||||
<div class="node" @click="toggle(`columns:${db.name}/${table.name}`)">
|
||||
<span class="arrow">{{ expanded.has(`columns:${db.name}/${table.name}`) ? '▾' : '▸' }}</span
|
||||
>📁 列
|
||||
</div>
|
||||
<ul v-if="expanded.has(`columns:${db.name}/${table.name}`)">
|
||||
<li v-for="column in table.columns" :key="column.name" class="node leaf">
|
||||
{{ column.primaryKey ? '🔑' : '▫' }} {{ columnText(column) }}
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<template v-if="!collapsed.has(db.name)">
|
||||
<div
|
||||
v-for="table in db.tables"
|
||||
:key="table.name"
|
||||
class="row table-row"
|
||||
:title="columnsTip(table)"
|
||||
@click="emit('action', { kind: 'editRows', database: db.name, table })"
|
||||
@contextmenu="tableMenu($event, db.name, table)"
|
||||
>
|
||||
<span class="name">{{ table.name }}</span>
|
||||
<button class="more" title="更多操作" @click="tableMenu($event, db.name, table)">⋯</button>
|
||||
</div>
|
||||
<div class="row new-table" @click="emit('action', { kind: 'newTable', database: db.name })">+ 新建表</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NDropdown
|
||||
v-if="menu"
|
||||
show
|
||||
@@ -169,54 +133,116 @@ function columnText(column: ExplorerTable['columns'][number]) {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 6px 8px;
|
||||
font-weight: 600;
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 8px 6px 12px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.tree,
|
||||
.tree ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
.title {
|
||||
flex: 1;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.head-btn {
|
||||
padding: 2px 8px;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: #4b5563;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.head-btn:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.head-btn.add {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.tree {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 4px 0;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.tree ul {
|
||||
padding-left: 16px;
|
||||
.db + .db {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.node {
|
||||
padding: 2px 6px;
|
||||
white-space: nowrap;
|
||||
cursor: default;
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 28px;
|
||||
padding: 0 4px 0 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.node:hover {
|
||||
background: #e5f1fb;
|
||||
}
|
||||
|
||||
.leaf {
|
||||
padding-left: 20px;
|
||||
color: #374151;
|
||||
.row:hover {
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
width: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 2px 6px 2px 22px;
|
||||
.name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.db-row .name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.count {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.table-row,
|
||||
.new-table {
|
||||
padding-left: 26px;
|
||||
}
|
||||
|
||||
.new-table {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
/* ⋯ 平时浅一点,鼠标移到这一行再变深 */
|
||||
.more {
|
||||
padding: 0 6px;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: #c0c4cc;
|
||||
font: inherit;
|
||||
line-height: 22px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.row:hover .more {
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.more:hover {
|
||||
background: #dbeafe;
|
||||
}
|
||||
|
||||
.empty {
|
||||
margin: 8px 6px;
|
||||
color: #9ca3af;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
@@ -76,9 +76,16 @@ async function onAction(action: ExplorerAction) {
|
||||
case 'design':
|
||||
open({ kind: 'design', title: `${action.database} - dbo.${action.table.name}`, database: action.database, table: action.table })
|
||||
break
|
||||
case 'editRows':
|
||||
case 'editRows': {
|
||||
// 点表名打开编辑数据;这张表已经开着就切过去
|
||||
const opened = tabs.value.find((t) => t.kind === 'edit' && t.database === action.database && t.table.name === action.table.name)
|
||||
if (opened) {
|
||||
active.value = opened.key
|
||||
break
|
||||
}
|
||||
open({ kind: 'edit', title: `${action.database} - dbo.${action.table.name}`, database: action.database, table: action.table })
|
||||
break
|
||||
}
|
||||
case 'selectRows': {
|
||||
// 和 SSMS 生成的语句格式一样
|
||||
const columns = action.table.columns.map((c) => quoteName(c.name)).join('\n ,')
|
||||
@@ -177,7 +184,6 @@ onMounted(refresh)
|
||||
<RouterLink to="/" class="nav-link">← 返回实验</RouterLink>
|
||||
<strong>数据库</strong>
|
||||
<NButton size="small" type="primary" @click="newQuery()">新建查询</NButton>
|
||||
<NButton size="small" @click="creatingDatabase = ''">新建数据库</NButton>
|
||||
<NButton size="small" @click="openRestore">恢复实验初始数据</NButton>
|
||||
<span class="spacer" />
|
||||
<span class="user">{{ currentUser?.name }}的数据库(只有自己能看到)</span>
|
||||
@@ -200,7 +206,7 @@ onMounted(refresh)
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
<div v-else class="empty">
|
||||
<p>右键左边的"数据库"新建数据库;右键表可以编辑数据、设计表结构。</p>
|
||||
<p>左边是你的数据库:点“+ 新建库”“+ 新建表”建库建表,点表名编辑表里的数据,表右边的 ⋯ 里还能查询、设计表结构、删除。</p>
|
||||
<p>也可以点"新建查询",用 SQL 语句建库、建表、增删改查。</p>
|
||||
<p>运行要用数据库的实验(教材项目 6 的任务 2、3,项目 8)时,平台会自动建好需要的库;把实验库恢复成初始数据,点"恢复实验初始数据"。</p>
|
||||
</div>
|
||||
|
||||
@@ -141,14 +141,14 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
|
||||
|
||||
<h3>7. 数据库(项目 6、8)</h3>
|
||||
<ul>
|
||||
<li>点右上角的<b>数据库</b>(在新标签页打开),代替书上的 SSMS:右键新建数据库、新建表,<b>新建查询</b>写 SQL(输入时会提示关键字、表名和列名,写"表名."或"别名."列出这张表的列),按 F5 执行。</li>
|
||||
<li>点右上角的<b>数据库</b>(在新标签页打开),代替书上的 SSMS:左边点“+ 新建库”“+ 新建表”,点表名编辑表里的数据,表右边的 ⋯(或右键)里能查询、设计表结构、删除;<b>新建查询</b>写 SQL(输入时会提示关键字、表名和列名,写"表名."或"别名."列出这张表的列),按 F5 执行。</li>
|
||||
<li>每个人有自己的一套数据库,只有自己能看到,一直保留。程序里的连接字符串只看数据库名,服务器名、账号密码照书上写就行。</li>
|
||||
<li>数据弄乱了:点<b>恢复实验初始数据</b>,选实验(默认是刚才做的实验),把这个实验用到的库换回初始数据(其他库不动)。</li>
|
||||
<li>点<b>← 返回实验</b>回到刚才做的实验。</li>
|
||||
</ul>
|
||||
<figure class="shot">
|
||||
<a href="/help-img/db.webp" title="点击放大" @click.prevent="zoom"><img src="/help-img/db.webp" alt="" loading="lazy" /></a>
|
||||
<figcaption>数据库页面:左边是对象资源管理器,右边写 SQL、按 F5 执行,下面是结果</figcaption>
|
||||
<figcaption>数据库页面:左边是你的库和表(点表名编辑数据),右边写 SQL、按 F5 执行,下面是结果</figcaption>
|
||||
</figure>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in new issue
Block a user