数据库页面的对象资源管理器简化成“库 → 表”两层:去掉“数据库”“表”“列”三层文件夹和 emoji;头部“+ 新建库”和刷新,每个库下面“+ 新建表”;点表名打开编辑前 200 行(已经开着就切过去),库和表右边的 ⋯ 和右键是同一个菜单(新建表 / 新建查询 / 删除数据库;编辑数据 / 查询前 1000 行 / 设计表结构 / 删除表),⋯ 打开的菜单出在按钮下面;鼠标停在表名上显示列。顶部去掉重复的“新建数据库”按钮;使用说明、数据库截图和设计方案同步
CI/CD / test (push) Canceled after 0s
CI/CD / deploy (push) Canceled after 0s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-27 15:23:42 +08:00
1 parent b292d38ba7
commit f9a2739353
5 files changed
+153 -121

No files matched your search

+1 -1
View File
@@ -289,7 +289,7 @@ flowchart TB
- **SqlClient 仿冒**(`runtime/Sim.Data`):SqlConnection / SqlCommand / SqlDataReader / SqlDataAdapter / SqlCommandBuilder / SqlParameter,DataSet、DataTable、DataView 直接用 .NET 自带的。连接字符串只按 Initial Catalog 打开学生自己的同名库(服务器名、账号密码不检查,书上的长服务器名、"." 都能连上),库不存在时报 4060。
- **T-SQL 转写**:ScriptDom(SQL Server 2008 语法)解析后按白名单逐个节点转写成 SQLite 语句,不认识的语法报"平台暂不支持",生成的 SQL 里只有平台写出的关键字和 tsql_ 函数。表用 STRICT 模式保存,原始的 T-SQL 列类型记在元数据表里,读出时还原成 SQL Server 返回的 .NET 类型(int、DateTime、bool……);varchar(n) 超长、类型不对、NULL、重复主键都报和 SQL Server 一样的错误号和中文信息。`insert into 表 values (...)` 不写列名时跳过自增列(照书上写时值的顺序和列顺序不一致,数据会存进错的列,和真实情况一样);日期统一存成 yyyy-MM-dd HH:mm:ss,书上拼接的 `DateTime.Now.ToString()`(2024/1/16 9:05:03)在插入和 BETWEEN 比较时都转换;支持 TOP(含 UPDATE / DELETE TOP)、CONVERT 样式 120 等、year / getdate / dateadd / datediff、`+` 按类型连接或相加、参数化查询、@@IDENTITY、多条语句。SqlCommandBuilder 根据带中文别名的查询生成 UPDATE。
- **DataGridView**:绑定 DataView / DataTable / DataSet,自动生成列;在单元格里修改的值写回 DataTable,配合 SqlDataAdapter.Update 保存;当前行 ▶、新增行 *、选中行、CurrentRow.Cells[0].Value;类型转换失败时和 WinForms 一样触发 DataError。**DateTimePicker**:Long / Short / Time / Custom 格式与中文 Windows 一致,弹出浏览器的日期选择框。设计器工具箱加"数据表格""日期时间选择器"。
- **数据库页面**(代替 SSMS,`/database`):对象资源管理器(库 → 表 → 列,右键新建数据库、新建表、设计、选择前 1000 行、编辑前 200 行、删除);查询窗口(F5 执行、选中部分只执行选中的、USE 切换库,结果网格和消息栏的格式与 SSMS 一致,每批最多 5 秒);编辑前 200 行(改单元格即 UPDATE、* 行新增、右键删除,没有主键的表按所有列的原值定位);表设计器(列名、类型、允许 Null、右键设主键、标识列,"查看 SQL 脚本"显示对应的 CREATE TABLE);"恢复实验初始数据"按实验的 `database.sql` 替换实验用到的库,其他库不动。这些操作都生成 T-SQL 交给常驻的 SQL 服务执行,和学生程序走同一套转写。
- **数据库页面**(代替 SSMS,`/database`):对象资源管理器(只有库 → 表两层,比 SSMS 简单:“+ 新建库”“+ 新建表”,点表名编辑前 200 行,⋯ 和右键是同一个菜单:查询前 1000 行、设计表结构、删除;鼠标停在表名上显示列);查询窗口(F5 执行、选中部分只执行选中的、USE 切换库,结果网格和消息栏的格式与 SSMS 一致,每批最多 5 秒);编辑前 200 行(改单元格即 UPDATE、* 行新增、右键删除,没有主键的表按所有列的原值定位);表设计器(列名、类型、允许 Null、右键设主键、标识列,"查看 SQL 脚本"显示对应的 CREATE TABLE);"恢复实验初始数据"按实验的 `database.sql` 替换实验用到的库,其他库不动。这些操作都生成 T-SQL 交给常驻的 SQL 服务执行,和学生程序走同一套转写。
- **评分**:每个实验可以带 `database.sql`,评分时每个检查项用一份新建的实验库,不碰学生自己的数据;评分规则加"点 / 修改表格单元格""在日期时间选择器里选日期""表格里有 / 没有某行"。新增示例项目 6 任务 2、3(任务 1 在数据库页面上完成),共 15 个。
- 实测:用到数据库的学生程序比不用的多约 11 MB 内存(PSS 17 → 28 MB)。另外修正了平台上的时间格式:Linux 的中文区域数据是 09:05:03,改成和中文 Windows 一样的 9:05:03。
Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 21 KiB

+141 -115
View File
@@ -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>
<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 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(`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">
<template v-if="!collapsed.has(db.name)">
<div
class="node"
title="右键:编辑前 200 行、设计、删除"
@click="toggle(`table:${db.name}/${table.name}`)"
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="arrow">{{ expanded.has(`table:${db.name}/${table.name}`) ? '▾' : '▸' }}</span
>▦ dbo.{{ table.name }}
<span class="name">{{ table.name }}</span>
<button class="more" title="更多操作" @click="tableMenu($event, db.name, table)">⋯</button>
</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 class="row new-table" @click="emit('action', { kind: 'newTable', database: db.name })">+ 新建表</div>
</template>
</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>
</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>
+9 -3
View File
@@ -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>
+2 -2
View File
@@ -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>