Compare commits

..
15 Commits
Author SHA1 Message Date
xuyueandClaude Sonnet 5 9b79cddfa6 Adjust SQL layout default split ratio
Deploy / build-and-deploy (push) Has been cancelled
Give the two data tables more default vertical space (1/4 for the
code editor instead of 3/5).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-05 00:55:27 -06:00
xuyueandClaude Sonnet 5 edd2f706ac feat: render SQL initial/result data as tables in a two-pane layout
Rewrites Content.vue to give the SQL language its own top/bottom split
(code editor over data tables) instead of sharing the horizontal
left/right layout used by other languages. SqlSection.vue now renders
the preset data and the post-run result as two side-by-side
n-data-tables, falling back to the existing error UI when a run fails.

Also registers NDataTable globally in main.ts (naive-ui components must
be explicitly registered via create()); without it n-data-table was an
inert unresolved custom element and no rows rendered.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-05 00:47:53 -06:00
xuyue 436f39d424 feat: isolate student SQL output and add result-row parsing 2026-07-05 00:37:56 -06:00
xuyueandClaude Sonnet 5 9a01d0e972 feat: structure candidate SQL table data as columns/rows
- Replace literal setupSql strings with structured columns/rows model
- Add SqlColumn interface and updated SqlTablePreset with columns/rows arrays
- Implement toSqlLiteral() for value escaping and buildSetupSql() for SQL generation
- Update sqlTable.ts to call buildSetupSql() instead of accessing setupSql field
- Maintains data parity with existing tables (students, employees, products)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-05 00:34:39 -06:00
xuyueandClaude Sonnet 5 4abd5761a0 Add implementation plan for SQL tabular data view
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-05 00:32:54 -06:00
xuyueandClaude Sonnet 5 5e38c5c796 Add design spec for tabular SQL data display
Replaces the raw-SQL-text preview/output with real data tables, and
fixes duplicate-output when a student's own SQL is itself a SELECT.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-05 00:29:20 -06:00
xuyueandClaude Sonnet 5 f8e59e9850 fix: address SQL feature code review findings
- Normalize student SQL to end with exactly one semicolon before
  appending the trailing SELECT, so a missing trailing ; no longer
  produces a confusing parse error (sqlTable.ts)
- Remove unused description field from SqlTablePreset and all presets
  (dead data, nothing in the UI reads it)
- Clear output when the candidate table selection changes, so stale
  results from the previous table don't linger (SqlSection.vue)
- Remove stray console.log(props.language) debug statement
  (CodeEditor.vue)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-05 00:16:26 -06:00
xuyueandClaude Sonnet 5 62ef50fb76 Move SQL table picker next to the language selector
Previously the candidate-table dropdown lived inside SqlSection's
content pane; moved it into the header alongside SelectLanguage so
table selection is visible regardless of scroll position.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-05 00:15:02 -06:00
xuyue b30e8c9ee6 feat: add SqlSection UI wired into the desktop editor layout
Selecting SQL now shows a table picker, read-only setup-SQL preview,
and results panel instead of the Input/Output split, mirroring how
Turtle already replaces that pane with its own canvas.
2026-07-05 00:02:57 -06:00
xuyue e5b2852fd1 feat: make SQL selectable as a language with syntax highlighting 2026-07-04 23:54:16 -06:00
xuyue 89d42b8d0f feat: add candidate SQL table presets and script-building composable 2026-07-04 23:48:48 -06:00
xuyue fbe98c455b feat: add sql to LANGUAGE type, Judge0 mapping, and code cache 2026-07-04 23:45:54 -06:00
xuyueandClaude Sonnet 5 bc3308da51 Add implementation plan for SQL CRUD practice feature
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 23:43:15 -06:00
xuyueandClaude Sonnet 5 b7c19ed5d5 Add sqlparse-based formatting to the SQL CRUD practice design
Extends the existing language-agnostic /format endpoint with a sql
branch so the "整理" button works for the new SQL language too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 23:36:19 -06:00
xuyueandClaude Sonnet 5 c9eb5fa12f Add design spec for SQL CRUD practice feature
Lets students select a preset teaching table, write SQL against it,
and see the resulting table state via the existing Judge0 pipeline.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 23:34:24 -06:00
19 changed files with 1633 additions and 12 deletions
@@ -0,0 +1,651 @@
# SQL 增删改查练习功能 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Let students pick one of three preset teaching tables, write SQL against it in the existing code editor, and see the table's resulting state after execution — with no automated grading, just observation for the teacher.
**Architecture:** SQL is wired in as a new language alongside python/c/cpp/turtle. Frontend composes `<preset setup SQL> + <student SQL> + SELECT * FROM <table>;` and submits it through the existing Judge0 pipeline (`language_id=82`, SQLite) — no backend changes for execution. A small backend addition (`sqlparse`) extends the existing generic `/format` endpoint to support SQL too.
**Tech Stack:** Vue 3 + TypeScript + CodeMirror 6 (`@codemirror/lang-sql`) on the frontend; Python `sqlparse` library on the backend (FastAPI).
Spec: `codenext/docs/superpowers/specs/2026-07-04-sql-crud-practice-design.md`
## Global Constraints
- SQL execution requires **zero** `codeapinew/` changes — it reuses the existing Judge0 direct-call path (`languageToId["sql"] = 82`).
- Every run always starts from the preset's initial data (`setupSql`) — no persistent/accumulating session across runs.
- After the student's SQL runs, always append `SELECT * FROM <table>;` to show the final table state, regardless of whether the student wrote SELECT/INSERT/UPDATE/DELETE.
- No automated grading, no answer comparison, no before/after diff highlighting — just show the raw post-execution output text (reuse existing `OutputSection`, plain text).
- Mobile excludes `"sql"` from the language selector, exactly like the existing `"turtle"` exclusion (mobile has no split-pane layout to host a table-picker + preview).
- `codenext/` has no frontend test framework (no vitest/jest configured). Verify frontend tasks with `npm run build` (must succeed) plus manual browser verification via `npm run start` — do not introduce a new test runner.
- `codeapinew/` already has `pytest` + `test_formatter.py` — use real TDD (red/green) for the backend task.
- Risk carried from the spec: the self-hosted Judge0 instance's support for `language_id=82` (SQLite) has not been verified from code. This plan does not block on it, but Task 4's manual verification step is where it will surface if the image isn't enabled.
---
### Task 1: Add `sql` language plumbing (types, Judge0 id, source, cache, share whitelist)
**Files:**
- Modify: `codenext/src/types.ts:3`
- Modify: `codenext/src/templates.ts:1-33`
- Modify: `codenext/src/composables/code.ts:13-23,74-76`
**Interfaces:**
- Produces: `LANGUAGE` type now includes `"sql"`; `languageToId["sql"] === 82`; `sources.sql` (default placeholder source, used to seed `cache.code.sql` and `reset()`).
- Consumes: nothing from other tasks (foundational).
- [ ] **Step 1: Add `"sql"` to the `LANGUAGE` union**
Edit `codenext/src/types.ts:3`:
```typescript
export type LANGUAGE = "c" | "python" | "cpp" | "turtle" | "sql"
```
- [ ] **Step 2: Add the Judge0 language id and default source for SQL**
Edit `codenext/src/templates.ts`. Add a new source constant near the top (after `turtleSource`):
```typescript
const sqlSource = "-- 在这里编写你的 SQL 语句\n"
```
Update `languageToId` and `sources`:
```typescript
export const languageToId: { [key in string]: number } = {
c: 50,
cpp: 54,
java: 62,
python: 71,
sql: 82,
}
export const sources = {
c: cSource,
cpp: cppSource,
java: javaSource,
python: pythonSource,
turtle: turtleSource,
sql: sqlSource,
}
```
- [ ] **Step 3: Add `sql` to the code cache and the share-link language whitelist**
Edit `codenext/src/composables/code.ts`. In the `cache` object (around line 17-22), add:
```typescript
code: {
python: useStorage("code_python", sources["python"]),
c: useStorage("code_c", sources["c"]),
cpp: useStorage("code_cpp", sources["cpp"]),
turtle: useStorage("code_turtle", sources["turtle"]),
sql: useStorage("code_sql", sources["sql"]),
},
```
In `init()` (around line 74), extend the whitelist:
```typescript
const lang = ["python", "c", "cpp", "turtle", "sql"].includes(data.lang)
? (data.lang as LANGUAGE)
: defaultLanguage
```
- [ ] **Step 4: Verify the build still succeeds**
Run: `cd codenext && npm run build`
Expected: build completes successfully (same output shape as before — no new UI is reachable yet since `SelectLanguage.vue` doesn't offer `"sql"` until Task 3).
- [ ] **Step 5: Commit**
```bash
cd codenext
git add src/types.ts src/templates.ts src/composables/code.ts
git commit -m "feat: add sql to LANGUAGE type, Judge0 mapping, and code cache"
```
---
### Task 2: Candidate table data + `sqlTable` composable
**Files:**
- Create: `codenext/src/data/sqlTables.ts`
- Create: `codenext/src/composables/sqlTable.ts`
- Modify: `codenext/src/composables/code.ts:40-48,106-126`
**Interfaces:**
- Consumes: `LANGUAGE` type from Task 1 (already includes `"sql"`).
- Produces:
- `SqlTablePreset` interface: `{ id: string; label: string; description: string; tableName: string; setupSql: string }`
- `sqlTables: SqlTablePreset[]` (3 presets: `students`, `employees`, `products`)
- `defaultSqlTableId: string` (equals `sqlTables[0].id`, i.e. `"students"`)
- `selectedTableId: Ref<string>` (current candidate table selection, used by `SqlSection.vue` in Task 4)
- `resetSqlTableSelection(): void`
- `buildSqlScript(studentSql: string): string` — used by `run()` in this task
- [ ] **Step 1: Create the candidate table data file**
Create `codenext/src/data/sqlTables.ts`:
```typescript
export interface SqlTablePreset {
id: string
label: string
description: string
tableName: string
setupSql: string
}
const studentsSetupSql = `CREATE TABLE students (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
class TEXT NOT NULL,
score INTEGER NOT NULL
);
INSERT INTO students (id, name, class, score) VALUES
(1, '张伟', '一班', 92),
(2, '王芳', '一班', 58),
(3, '李娜', '二班', 76),
(4, '刘洋', '二班', 45),
(5, '陈静', '一班', 88),
(6, '杨帆', '三班', 63),
(7, '赵敏', '二班', 39),
(8, '孙涛', '三班', 81);`
const employeesSetupSql = `CREATE TABLE employees (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
department TEXT NOT NULL,
salary INTEGER NOT NULL,
hire_date TEXT NOT NULL
);
INSERT INTO employees (id, name, department, salary, hire_date) VALUES
(1, '周明', '技术部', 12000, '2019-03-01'),
(2, '吴倩', '市场部', 8000, '2021-07-15'),
(3, '郑凯', '技术部', 15500, '2017-11-20'),
(4, '钱多多', '财务部', 9200, '2020-01-10'),
(5, '孙丽', '市场部', 7600, '2022-05-30'),
(6, '李强', '技术部', 10800, '2023-09-01'),
(7, '林小雨', '财务部', 8900, '2018-06-12'),
(8, '黄河', '市场部', 6800, '2024-02-18');`
const productsSetupSql = `CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
category TEXT NOT NULL,
price REAL NOT NULL,
stock INTEGER NOT NULL
);
INSERT INTO products (id, name, category, price, stock) VALUES
(1, '无线鼠标', '电子产品', 59.9, 120),
(2, '机械键盘', '电子产品', 299.0, 45),
(3, '保温杯', '生活用品', 39.5, 0),
(4, '笔记本', '文具', 12.0, 300),
(5, '蓝牙耳机', '电子产品', 199.0, 0),
(6, '台灯', '生活用品', 89.0, 60),
(7, '钢笔', '文具', 25.0, 150),
(8, '充电宝', '电子产品', 129.0, 8);`
export const sqlTables: SqlTablePreset[] = [
{
id: "students",
label: "学生成绩表",
description: "适合练习按分数筛选、批量更新、删除不及格记录",
tableName: "students",
setupSql: studentsSetupSql,
},
{
id: "employees",
label: "员工工资表",
description: "适合练习按部门分组调薪、按入职日期筛选",
tableName: "employees",
setupSql: employeesSetupSql,
},
{
id: "products",
label: "商品库存表",
description: "适合练习新增商品、调整库存、下架缺货商品",
tableName: "products",
setupSql: productsSetupSql,
},
]
export const defaultSqlTableId = sqlTables[0].id
```
- [ ] **Step 2: Create the `sqlTable` composable**
Create `codenext/src/composables/sqlTable.ts`:
```typescript
import { ref } from "vue"
import { defaultSqlTableId, sqlTables } from "../data/sqlTables"
export const selectedTableId = ref(defaultSqlTableId)
export function resetSqlTableSelection() {
selectedTableId.value = defaultSqlTableId
}
export function buildSqlScript(studentSql: string) {
const table =
sqlTables.find((item) => item.id === selectedTableId.value) ??
sqlTables[0]
return `${table.setupSql}\n\n${studentSql}\n\nSELECT * FROM ${table.tableName};`
}
```
- [ ] **Step 3: Wire the composable into `code.ts`'s language switch and `run()`**
Edit `codenext/src/composables/code.ts`. Add the import at the top:
```typescript
import { buildSqlScript, resetSqlTableSelection } from "./sqlTable"
```
Update the language watcher (around line 40-48) to reset the table selection when switching into SQL:
```typescript
watch(
() => code.language,
(value: LANGUAGE) => {
cache.language.value = value
code.value = cache.code[value].value
output.value = ""
status.value = Status.NotStarted
if (value === "sql") resetSqlTableSelection()
},
)
```
Update `run()` (around line 106-126) to build the combined script for SQL:
```typescript
export async function run() {
loading.value = true
const cleanCode = code.value.trim()
if (!cleanCode) {
loading.value = false
return
}
if (code.language === "turtle") {
turtleRunId.value++
} else {
output.value = ""
status.value = Status.NotStarted
const sourceCode =
code.language === "sql" ? buildSqlScript(cleanCode) : cleanCode
const result = await submit(
{ value: sourceCode, language: code.language },
input.value.trim(),
)
output.value = result.output || ""
status.value = result.status
}
loading.value = false
}
```
- [ ] **Step 4: Verify the build still succeeds**
Run: `cd codenext && npm run build`
Expected: build completes successfully. (Behavioral verification of `buildSqlScript`/`run()` happens end-to-end in Task 4, once the UI can reach the SQL language.)
- [ ] **Step 5: Commit**
```bash
cd codenext
git add src/data/sqlTables.ts src/composables/sqlTable.ts src/composables/code.ts
git commit -m "feat: add candidate SQL table presets and script-building composable"
```
---
### Task 3: Editor & language selector UI (make SQL selectable + syntax highlighted)
**Files:**
- Modify: `codenext/package.json`
- Modify: `codenext/src/components/CodeEditor.vue:1-61`
- Modify: `codenext/src/components/SelectLanguage.vue:7-25`
- Create: `codenext/public/sql.svg`
**Interfaces:**
- Consumes: `LANGUAGE` type from Task 1 (`"sql"` already valid).
- Produces: `"sql"` becomes selectable in `SelectLanguage.vue`'s dropdown (desktop only); `CodeEditor.vue` renders SQL syntax highlighting when `language === "sql"`.
- [ ] **Step 1: Install the CodeMirror SQL language package**
Run: `cd codenext && npm install @codemirror/lang-sql`
Expected: `package.json` gains a new dependency entry under `"@codemirror/lang-sql"` (npm resolves the version; do not hand-pick a version number).
- [ ] **Step 2: Add SQL syntax highlighting to `CodeEditor.vue`**
Edit `codenext/src/components/CodeEditor.vue`. Add the import near the other `@codemirror/lang-*` imports (line 2-3):
```typescript
import { sql } from "@codemirror/lang-sql"
```
Update `langExtension` (line 56-61):
```typescript
const langExtension = computed(() => {
if (props.language === "python" || props.language === "turtle") {
return python()
}
if (props.language === "sql") {
return sql()
}
return cpp()
})
```
- [ ] **Step 3: Add the SQL option to the language selector, excluded on mobile like turtle**
Edit `codenext/src/components/SelectLanguage.vue`. Update `allLangs` and the mobile filter (line 7-18):
```typescript
const LANGS = computed(() => {
const allLangs = [
["python", "Python"],
["turtle", "海龟绘图"],
["c", "C 语言"],
["cpp", "C++"],
["sql", "SQL"],
]
if (isMobile.value) {
return allLangs.filter(([lang]) => lang !== "turtle" && lang !== "sql")
}
return allLangs
})
```
Update the mobile auto-switch watcher (line 20-25) to also cover SQL:
```typescript
// 如果当前在移动端且语言是海龟绘图或 SQL,自动切换到 Python
watch(isMobile, (mobile) => {
if (mobile && (code.language === "turtle" || code.language === "sql")) {
code.language = "python"
}
})
```
- [ ] **Step 4: Add the SQL language icon**
Create `codenext/public/sql.svg`:
```svg
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="#a9b9cb" d="M12 3c-4.97 0-9 1.343-9 3v12c0 1.657 4.03 3 9 3s9-1.343 9-3V6c0-1.657-4.03-3-9-3z"/><path fill="#7f8b99" d="M3 6v3c0 1.657 4.03 3 9 3s9-1.343 9-3V6c0 1.657-4.03 3-9 3S3 7.657 3 6z"/><path fill="#7f8b99" d="M3 11v3c0 1.657 4.03 3 9 3s9-1.343 9-3v-3c0 1.657-4.03 3-9 3s-9-1.343-9-3z"/></svg>
```
- [ ] **Step 5: Verify the build succeeds**
Run: `cd codenext && npm run build`
Expected: build completes successfully.
- [ ] **Step 6: Manually verify the language selector and syntax highlighting**
Run: `cd codenext && npm run start`, open `http://localhost:3000` in a browser.
- Open the language dropdown (desktop layout) — confirm "SQL" appears with the new icon and is selectable.
- Select SQL — confirm the editor now does SQL syntax highlighting (keywords colored) instead of C++ highlighting.
- Shrink the window to mobile width (or use device toolbar) — confirm "SQL" is absent from the dropdown, matching turtle's existing mobile behavior.
- [ ] **Step 7: Commit**
```bash
cd codenext
git add package.json package-lock.json src/components/CodeEditor.vue src/components/SelectLanguage.vue public/sql.svg
git commit -m "feat: make SQL selectable as a language with syntax highlighting"
```
---
### Task 4: `SqlSection` UI + `Content.vue` wiring (end-to-end feature)
**Files:**
- Create: `codenext/src/desktop/SqlSection.vue`
- Modify: `codenext/src/desktop/Content.vue`
**Interfaces:**
- Consumes: `selectedTableId`, `sqlTables` (Task 2); `code.language`, `run()` (Task 1/2, via `composables/code.ts`); `OutputSection.vue` (existing, unmodified).
- Produces: fully working SQL practice flow reachable from the desktop UI.
- [ ] **Step 1: Create `SqlSection.vue`**
Create `codenext/src/desktop/SqlSection.vue`:
```vue
<script lang="ts" setup>
import { computed } from "vue"
import CodeEditor from "../components/CodeEditor.vue"
import { selectedTableId } from "../composables/sqlTable"
import { sqlTables } from "../data/sqlTables"
import OutputSection from "./OutputSection.vue"
const tableOptions = computed(() =>
sqlTables.map((table) => ({ value: table.id, label: table.label })),
)
const selectedTable = computed(
() =>
sqlTables.find((table) => table.id === selectedTableId.value) ??
sqlTables[0],
)
</script>
<template>
<n-flex vertical class="sql-section">
<n-select
class="table-select"
v-model:value="selectedTableId"
:options="tableOptions"
/>
<n-split
direction="vertical"
:default-size="1 / 3"
:min="1 / 5"
:max="3 / 5"
>
<template #1>
<CodeEditor
:model-value="selectedTable.setupSql"
language="sql"
readonly
label="表结构与初始数据"
/>
</template>
<template #2>
<OutputSection />
</template>
</n-split>
</n-flex>
</template>
<style scoped>
.sql-section {
height: 100%;
}
.table-select {
margin: 12px 20px 0;
width: 160px;
}
</style>
```
- [ ] **Step 2: Wire `SqlSection` into `Content.vue`**
Edit `codenext/src/desktop/Content.vue`:
```vue
<script lang="ts" setup>
import { code } from "../composables/code"
import CodeSection from "./CodeSection.vue"
import InputSection from "./InputSection.vue"
import OutputSection from "./OutputSection.vue"
import SqlSection from "./SqlSection.vue"
import TurtleSection from "./TurtleSection.vue"
</script>
<template>
<n-layout-content class="container">
<n-split direction="horizontal" :min="1 / 3" :max="4 / 5">
<template #1>
<CodeSection />
</template>
<template #2>
<n-split
v-if="code.language !== 'turtle' && code.language !== 'sql'"
direction="vertical"
:default-size="1 / 3"
:min="1 / 5"
:max="3 / 5"
>
<template #1>
<InputSection />
</template>
<template #2>
<OutputSection />
</template>
</n-split>
<TurtleSection v-else-if="code.language === 'turtle'" />
<SqlSection v-else />
</template>
</n-split>
</n-layout-content>
</template>
<style scoped>
.container {
height: calc(100vh - 60px);
}
</style>
```
- [ ] **Step 3: Verify the build succeeds**
Run: `cd codenext && npm run build`
Expected: build completes successfully.
- [ ] **Step 4: Manually verify the full end-to-end flow**
Run: `cd codenext && npm run start`, open `http://localhost:3000`.
1. Switch language to SQL. Confirm the right pane now shows: a table dropdown (defaulting to "学生成绩表"), a read-only preview of the `students` setup SQL below it, and an (empty) output panel at the bottom.
2. In the left editor, type: `UPDATE students SET score = 100 WHERE name = '王芳';`
3. Click run (or press F5). Confirm the output panel shows the full `students` table with 王芳's score now `100` and all other rows unchanged.
4. Switch the table dropdown to "员工工资表". Confirm the preview panel updates to show the `employees` setup SQL.
5. Type `DELETE FROM employees WHERE salary < 8000;` and run. Confirm the output shows the `employees` table with the two lowest-salary rows (吴倩 8000 stays if not `<` 8000 — confirm rows below 8000, i.e. 孙丽 7600 and 黄河 6800, are gone) and the rest intact.
6. Re-run without changing anything. Confirm the result is identical each time (proves it always restarts from the preset data rather than accumulating state).
7. Type intentionally broken SQL (e.g. `SELCT * FROM students;`) and run. Confirm an error surfaces through the existing error/AI-analysis UI, the same way a C/C++ compile error would.
8. **If this step fails with a Judge0 "language not supported" error:** this surfaces the risk noted in the spec — the self-hosted Judge0 instance needs `language_id=82` (SQLite) enabled. Stop and flag this to the user; it's an infra fix, not a code fix.
- [ ] **Step 5: Commit**
```bash
cd codenext
git add src/desktop/SqlSection.vue src/desktop/Content.vue
git commit -m "feat: add SqlSection UI wired into the desktop editor layout"
```
---
### Task 5: Backend SQL formatting via `sqlparse`
**Files:**
- Modify: `codeapinew/formatter.py`
- Modify: `codeapinew/pyproject.toml`
- Modify: `codeapinew/requirements.txt`
- Modify: `codeapinew/test_formatter.py`
**Interfaces:**
- Consumes: nothing from other tasks (independent of the frontend tasks).
- Produces: `format_code(code, "sql")` returns sqlparse-formatted SQL instead of raising `FormatError`.
- [ ] **Step 1: Write the failing tests**
Add to `codeapinew/test_formatter.py`:
```python
def test_format_sql_uppercases_keywords():
result = format_code("select * from students where score>60", "sql")
assert result == "SELECT * FROM students WHERE score>60"
def test_format_sql_splits_multiple_statements():
result = format_code(
"delete from students where score<60;insert into students (id) values (9);",
"sql",
)
assert result == (
"DELETE FROM students WHERE score<60;\n\n"
"INSERT INTO students (id) VALUES (9);"
)
def test_format_sql_tolerates_syntax_errors():
result = format_code("select from where", "sql")
assert result == "SELECT FROM WHERE"
```
- [ ] **Step 2: Run the tests to verify they fail**
Run: `cd codeapinew && uv run pytest test_formatter.py -v -k sql`
Expected: 3 failures — `format_code` raises `FormatError: 不支持的语言: sql` (the `sql` branch doesn't exist yet).
- [ ] **Step 3: Add the `sqlparse` dependency**
Run: `cd codeapinew && uv add sqlparse`
Expected: `pyproject.toml` gains `sqlparse` under `dependencies`, `uv.lock` updates.
Check the resolved version:
Run: `grep sqlparse pyproject.toml uv.lock | head -5`
Add the matching pinned entry to `codeapinew/requirements.txt` (insert alphabetically, matching the existing pin style, e.g. `sqlparse==<resolved-version>`).
- [ ] **Step 4: Implement the `sql` branch in `formatter.py`**
Edit `codeapinew/formatter.py`. Add the import at the top:
```python
import sqlparse
```
Add the helper function (near `_run`, before `format_code`):
```python
def _format_with_sql(code: str) -> str:
# sqlparse 对语法错误宽容,不会抛异常,语法问题留给判题阶段反馈
# strip_whitespace 会把多条语句压成一行,先按分号拆分再逐条格式化
statements = sqlparse.split(code)
return "\n\n".join(
sqlparse.format(s, strip_whitespace=True, keyword_case="upper")
for s in statements
)
```
Add the branch inside `format_code` (before the final `raise FormatError`):
```python
if language == "sql":
return _format_with_sql(code)
```
- [ ] **Step 5: Run the tests to verify they pass**
Run: `cd codeapinew && uv run pytest test_formatter.py -v`
Expected: all tests pass, including the 3 new `sql` tests and the pre-existing python/c/cpp/unsupported-language tests (no regressions).
- [ ] **Step 6: Commit**
```bash
cd codeapinew
git add formatter.py pyproject.toml uv.lock requirements.txt test_formatter.py
git commit -m "feat: format SQL code via sqlparse in the /format endpoint"
```
@@ -0,0 +1,446 @@
# SQL 数据表格化展示 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Replace the raw-SQL-text preview and plain-text output in the SQL practice feature with real data tables, restructure the SQL layout to "code on top, two data tables side by side below", and eliminate the duplicate-output bug that occurs when a student's own SQL is itself a SELECT.
**Architecture:** Candidate table data moves from a literal SQL string to structured `columns`/`rows`; the setup SQL sent to Judge0 is generated from that structured data. The submitted script now wraps the student's own statement between `.output /dev/null` and `.output stdout` so only the final auto-appended `SELECT * FROM <table>;` ever reaches stdout — eliminating the "two copies of data" bug. `Content.vue` grows a dedicated vertical-split branch for the `sql` language (code on top, `SqlSection` below); `SqlSection.vue` becomes a horizontal split of two `n-data-table`s (initial data vs. post-run result, with the existing error UI as fallback when the run failed).
**Tech Stack:** Vue 3 + TypeScript, naive-ui's `n-data-table` (already a dependency, no new package needed).
Spec: `codenext/docs/superpowers/specs/2026-07-05-sql-data-table-view-design.md`
## Global Constraints
- No `codeapinew/` changes — this is 100% frontend, same Judge0 SQLite (`language_id=82`) submission path as before.
- `codenext/` has no frontend test framework — verify with `npm run build` (must succeed) plus manual browser verification via `npm run start`, same convention as the rest of this codebase.
- The generated setup SQL only needs to be semantically equivalent to the old literal strings, not byte-identical — e.g. a `REAL` column value written as `299` instead of `299.0` is an accepted cosmetic difference (SQLite has no functional distinction between them for a `REAL`-affinity column).
- Every preset table's first column is `id` — this plan uses that as the `n-data-table` row key. Do not add a preset without an `id` first column without revisiting this assumption.
- No automated grading, no diff highlighting — carried over from the original feature spec, unaffected by this plan.
---
### Task 1: Structured candidate-table data model
**Files:**
- Modify: `codenext/src/data/sqlTables.ts` (full rewrite)
**Interfaces:**
- Produces:
- `interface SqlColumn { name: string; type: string }`
- `interface SqlTablePreset { id: string; label: string; tableName: string; columns: SqlColumn[]; rows: (string | number)[][] }` (replaces the old `setupSql: string` field)
- `toSqlLiteral(value: string | number): string`
- `buildSetupSql(table: SqlTablePreset): string`
- `sqlTables: SqlTablePreset[]` (same 3 presets: students/employees/products, same data values, now structured)
- `defaultSqlTableId: string` (unchanged, `"students"`)
- Consumes: nothing from other tasks (foundational, same as before).
- [ ] **Step 1: Rewrite `sqlTables.ts` with the structured model**
Replace the full contents of `codenext/src/data/sqlTables.ts` with:
```typescript
export interface SqlColumn {
name: string
type: string
}
export interface SqlTablePreset {
id: string
label: string
tableName: string
columns: SqlColumn[]
rows: (string | number)[][]
}
export function toSqlLiteral(value: string | number): string {
if (typeof value === "number") return String(value)
return `'${value.replace(/'/g, "''")}'`
}
export function buildSetupSql(table: SqlTablePreset): string {
const columnDefs = table.columns
.map((column) => `${column.name} ${column.type}`)
.join(",\n ")
const columnNames = table.columns.map((column) => column.name).join(", ")
const valuesList = table.rows
.map((row) => `(${row.map(toSqlLiteral).join(", ")})`)
.join(",\n ")
return `CREATE TABLE ${table.tableName} (\n ${columnDefs}\n);\n\nINSERT INTO ${table.tableName} (${columnNames}) VALUES\n ${valuesList};`
}
export const sqlTables: SqlTablePreset[] = [
{
id: "students",
label: "学生成绩表",
tableName: "students",
columns: [
{ name: "id", type: "INTEGER PRIMARY KEY" },
{ name: "name", type: "TEXT NOT NULL" },
{ name: "class", type: "TEXT NOT NULL" },
{ name: "score", type: "INTEGER NOT NULL" },
],
rows: [
[1, "张伟", "一班", 92],
[2, "王芳", "一班", 58],
[3, "李娜", "二班", 76],
[4, "刘洋", "二班", 45],
[5, "陈静", "一班", 88],
[6, "杨帆", "三班", 63],
[7, "赵敏", "二班", 39],
[8, "孙涛", "三班", 81],
],
},
{
id: "employees",
label: "员工工资表",
tableName: "employees",
columns: [
{ name: "id", type: "INTEGER PRIMARY KEY" },
{ name: "name", type: "TEXT NOT NULL" },
{ name: "department", type: "TEXT NOT NULL" },
{ name: "salary", type: "INTEGER NOT NULL" },
{ name: "hire_date", type: "TEXT NOT NULL" },
],
rows: [
[1, "周明", "技术部", 12000, "2019-03-01"],
[2, "吴倩", "市场部", 8000, "2021-07-15"],
[3, "郑凯", "技术部", 15500, "2017-11-20"],
[4, "钱多多", "财务部", 9200, "2020-01-10"],
[5, "孙丽", "市场部", 7600, "2022-05-30"],
[6, "李强", "技术部", 10800, "2023-09-01"],
[7, "林小雨", "财务部", 8900, "2018-06-12"],
[8, "黄河", "市场部", 6800, "2024-02-18"],
],
},
{
id: "products",
label: "商品库存表",
tableName: "products",
columns: [
{ name: "id", type: "INTEGER PRIMARY KEY" },
{ name: "name", type: "TEXT NOT NULL" },
{ name: "category", type: "TEXT NOT NULL" },
{ name: "price", type: "REAL NOT NULL" },
{ name: "stock", type: "INTEGER NOT NULL" },
],
rows: [
[1, "无线鼠标", "电子产品", 59.9, 120],
[2, "机械键盘", "电子产品", 299.0, 45],
[3, "保温杯", "生活用品", 39.5, 0],
[4, "笔记本", "文具", 12.0, 300],
[5, "蓝牙耳机", "电子产品", 199.0, 0],
[6, "台灯", "生活用品", 89.0, 60],
[7, "钢笔", "文具", 25.0, 150],
[8, "充电宝", "电子产品", 129.0, 8],
],
},
]
export const defaultSqlTableId = sqlTables[0].id
```
- [ ] **Step 2: Fix the one compile error this rewrite creates**
`src/composables/sqlTable.ts` currently reads `table.setupSql`, a field that no longer exists on `SqlTablePreset` after Step 1. Make a **minimal** compatibility edit: change that one reference to `buildSetupSql(table)`, and add `buildSetupSql` to its existing import from `"../data/sqlTables"`. Do not touch anything else in `sqlTable.ts` — the rest of its rework happens in Task 2.
- [ ] **Step 3: Verify the build succeeds**
Run: `cd codenext && npm run build`
Expected: build completes successfully with no TypeScript errors.
- [ ] **Step 4: Commit**
```bash
cd codenext
git add src/data/sqlTables.ts src/composables/sqlTable.ts
git commit -m "feat: structure candidate SQL table data as columns/rows"
```
---
### Task 2: Script building with output isolation + result parsing
**Files:**
- Modify: `codenext/src/composables/sqlTable.ts` (full rewrite)
**Interfaces:**
- Consumes: `buildSetupSql`, `SqlColumn`, `defaultSqlTableId`, `sqlTables` from Task 1's `../data/sqlTables`.
- Produces:
- `selectedTableId: Ref<string>` (unchanged from before)
- `resetSqlTableSelection(): void` (unchanged from before)
- `buildSqlScript(studentSql: string): string` — same name/signature as before, new internals (wraps student SQL with `.output /dev/null` / `.output stdout`)
- `parseResultRows(output: string, columns: SqlColumn[]): Record<string, string | number>[]` (new — used by Task 3's `SqlSection.vue`)
- [ ] **Step 1: Rewrite `sqlTable.ts`**
Replace the full contents of `codenext/src/composables/sqlTable.ts` with:
```typescript
import { ref } from "vue"
import {
buildSetupSql,
defaultSqlTableId,
sqlTables,
type SqlColumn,
} from "../data/sqlTables"
export const selectedTableId = ref(defaultSqlTableId)
export function resetSqlTableSelection() {
selectedTableId.value = defaultSqlTableId
}
export function buildSqlScript(studentSql: string) {
const table =
sqlTables.find((item) => item.id === selectedTableId.value) ??
sqlTables[0]
const normalizedSql = studentSql.trim().replace(/;?\s*$/, ";")
return [
buildSetupSql(table),
".output /dev/null",
normalizedSql,
".output stdout",
`SELECT * FROM ${table.tableName};`,
].join("\n\n")
}
export function parseResultRows(
output: string,
columns: SqlColumn[],
): Record<string, string | number>[] {
return output
.split("\n")
.map((line) => line.trim())
.filter(Boolean)
.map((line) => {
const cells = line.split("|")
return Object.fromEntries(
columns.map((column, index) => [column.name, cells[index] ?? ""]),
)
})
}
```
This supersedes Task 1's minimal compatibility edit to this same file — the end state of this file is exactly the code above, nothing carried over from the old version except `selectedTableId`/`resetSqlTableSelection` (unchanged) and the `buildSqlScript` name/signature (unchanged externally, new internals).
- [ ] **Step 2: Fix the one compile error this rewrite creates**
`SqlSection.vue` currently passes `selectedTable.setupSql` (the field Task 1's Step 2 compatibility edit changed to `buildSetupSql(table)` at the call site inside `sqlTable.ts` — but `SqlSection.vue` itself still separately reads `selectedTable.setupSql` directly for its read-only preview). Make a **minimal** patch in `codenext/src/desktop/SqlSection.vue`: change that one reference to `buildSetupSql(selectedTable)`, importing `buildSetupSql` from `"../data/sqlTables"`. Do not otherwise change `SqlSection.vue`'s structure — the full rewrite happens in Task 3.
- [ ] **Step 3: Verify the build succeeds**
Run: `cd codenext && npm run build`
Expected: build completes successfully with no TypeScript errors.
- [ ] **Step 4: Commit**
```bash
cd codenext
git add src/composables/sqlTable.ts src/desktop/SqlSection.vue
git commit -m "feat: isolate student SQL output and add result-row parsing"
```
---
### Task 3: Two-table layout (Content.vue + SqlSection.vue rewrite)
**Files:**
- Modify: `codenext/src/desktop/Content.vue` (full rewrite)
- Modify: `codenext/src/desktop/SqlSection.vue` (full rewrite)
**Interfaces:**
- Consumes: `parseResultRows`, `selectedTableId` from Task 2's `../composables/sqlTable`; `sqlTables` (with `columns`/`rows`) from Task 1's `../data/sqlTables`; `output`, `status` from `../composables/code` (pre-existing, unchanged); `Status` enum from `../types` (pre-existing, unchanged).
- Produces: the full end-to-end two-table SQL UI.
- [ ] **Step 1: Rewrite `Content.vue`**
Replace the full contents of `codenext/src/desktop/Content.vue` with:
```vue
<script lang="ts" setup>
import { code } from "../composables/code"
import CodeSection from "./CodeSection.vue"
import InputSection from "./InputSection.vue"
import OutputSection from "./OutputSection.vue"
import SqlSection from "./SqlSection.vue"
import TurtleSection from "./TurtleSection.vue"
</script>
<template>
<n-layout-content class="container">
<n-split
v-if="code.language === 'sql'"
direction="vertical"
:default-size="3 / 5"
:min="1 / 4"
:max="3 / 4"
>
<template #1>
<CodeSection />
</template>
<template #2>
<SqlSection />
</template>
</n-split>
<n-split v-else direction="horizontal" :min="1 / 3" :max="4 / 5">
<template #1>
<CodeSection />
</template>
<template #2>
<n-split
v-if="code.language !== 'turtle'"
direction="vertical"
:default-size="1 / 3"
:min="1 / 5"
:max="3 / 5"
>
<template #1>
<InputSection />
</template>
<template #2>
<OutputSection />
</template>
</n-split>
<TurtleSection v-else />
</template>
</n-split>
</n-layout-content>
</template>
<style scoped>
.container {
height: calc(100vh - 60px);
}
</style>
```
Note the simplification: since the outer `v-if` now fully separates `sql` from everything else, the inner split's condition goes back to a simple `code.language !== 'turtle'` (no longer needs the `&& code.language !== 'sql'` clause, because `sql` never reaches this branch at all).
- [ ] **Step 2: Rewrite `SqlSection.vue`**
Replace the full contents of `codenext/src/desktop/SqlSection.vue` with:
```vue
<script lang="ts" setup>
import type { DataTableColumns } from "naive-ui"
import { computed, watch } from "vue"
import { output, status } from "../composables/code"
import { parseResultRows, selectedTableId } from "../composables/sqlTable"
import { sqlTables } from "../data/sqlTables"
import { Status } from "../types"
import OutputSection from "./OutputSection.vue"
const selectedTable = computed(
() =>
sqlTables.find((table) => table.id === selectedTableId.value) ??
sqlTables[0],
)
const tableColumns = computed<DataTableColumns>(() =>
selectedTable.value.columns.map((column) => ({
title: column.name,
key: column.name,
})),
)
const initialRows = computed(() =>
selectedTable.value.rows.map((row) =>
Object.fromEntries(
selectedTable.value.columns.map((column, index) => [
column.name,
row[index],
]),
),
),
)
const resultRows = computed(() =>
parseResultRows(output.value, selectedTable.value.columns),
)
watch(selectedTableId, () => {
output.value = ""
status.value = Status.NotStarted
})
</script>
<template>
<n-split
direction="horizontal"
:default-size="1 / 2"
:min="1 / 4"
:max="3 / 4"
>
<template #1>
<div class="table-panel">
<div class="panel-title">原始数据{{ selectedTable.label }}</div>
<n-data-table
size="small"
:bordered="false"
:columns="tableColumns"
:data="initialRows"
:row-key="(row: any) => row.id"
/>
</div>
</template>
<template #2>
<div class="table-panel" v-if="status === Status.Accepted">
<div class="panel-title">运行后数据</div>
<n-data-table
size="small"
:bordered="false"
:columns="tableColumns"
:data="resultRows"
:row-key="(row: any) => row.id"
/>
</div>
<OutputSection v-else />
</template>
</n-split>
</template>
<style scoped>
.table-panel {
height: 100%;
overflow: auto;
padding: 12px 20px;
box-sizing: border-box;
}
.panel-title {
font-size: 16px;
margin-bottom: 8px;
}
</style>
```
Note the `watch(selectedTableId, ...)` now resets both `output` AND `status` (the pre-existing version, from the previous plan's review-fix round, only reset `output`). This is necessary now: without resetting `status`, switching tables while the previous table's last run was `Accepted` would leave the right pane trying to render `resultRows` — which re-parses the *old* `output` text against the *newly selected* table's columns, producing garbage. Resetting `status` to `NotStarted` correctly falls back to the (also-cleared) `OutputSection` instead.
- [ ] **Step 3: Verify the build succeeds**
Run: `cd codenext && npm run build`
Expected: build completes successfully with no TypeScript/Vue errors.
- [ ] **Step 4: Manually verify the full end-to-end flow**
Run: `cd codenext && npm run start`, open `http://localhost:3000`.
1. Switch language to SQL. Confirm the layout is now top/bottom: a code editor on top (roughly 60% height), and below it two side-by-side panels — left "原始数据(学生成绩表)" showing an 8-row table with columns id/name/class/score matching the preset data, right panel empty (no run yet, falls back to the empty output box).
2. In the code editor, type `SELECT * FROM students;` (the student's own query is itself a SELECT — this is the exact scenario that used to produce duplicate output) and run.
3. **Confirm the right panel shows exactly one 8-row table** (not two copies concatenated, not 16 rows) — this is the specific bug this plan fixes. Cross-check the row count and values match the preset exactly (张伟 92, 王芳 58, ... 孙涛 81).
4. Type `UPDATE students SET score = 100 WHERE name = '王芳';` and run. Confirm the right table shows 王芳 with score 100 and all other rows unchanged, still exactly one table (not duplicated).
5. Switch the table dropdown (next to the language selector, top bar) to "员工工资表". Confirm: left panel immediately updates to the `employees` preset data (8 rows, id/name/department/salary/hire_date), right panel goes back to the empty fallback (proves the `status`/`output` reset-on-switch works — it should NOT show stale `students` data re-parsed against `employees` columns).
6. Type `DELETE FROM employees WHERE salary < 8000;` and run. Confirm the right table shows exactly 6 rows (孙丽 7600 and 黄河 6800 removed, 吴倩 8000 stays), still exactly one table.
7. Type intentionally broken SQL (e.g. `SELCT * FROM employees;`) and run. Confirm the right panel falls back to the existing error UI (运行失败 tag + 推测原因 button), NOT an empty or broken table — and the left panel's initial-data table is unaffected.
8. Resize the vertical split (drag the divider between code and tables) and the horizontal split (drag the divider between the two tables) — confirm both are draggable and don't break rendering.
9. Switch to a non-SQL language (e.g. C++) and confirm its layout (code left, input/output right) is completely unaffected by these changes.
If step 3 or 6 shows duplicated/doubled data, or step 7 shows a broken table instead of the error fallback, STOP and report BLOCKED/DONE_WITH_CONCERNS with exact details — these are the specific defects this plan exists to fix.
- [ ] **Step 5: Commit**
```bash
cd codenext
git add src/desktop/Content.vue src/desktop/SqlSection.vue
git commit -m "feat: render SQL initial/result data as tables in a two-pane layout"
```
@@ -0,0 +1,93 @@
# SQL 增删改查练习功能 设计文档
日期:2026-07-04
## 背景与目标
老师需要在现有在线代码编辑器(codenext)中考察学生的 SQL 增删改查(CRUD)能力。功能不是自动判题/评分系统,而是一个"候选数据表 + SQL 执行 + 结果展示"的沙箱:学生从几张预设的教学表中选一张,写 SQL 语句,执行后看到该表的最终状态,由老师人工判断学生写得对不对。
## 范围
- SQL 执行本身只涉及 `codenext/`(前端),复用现有的 Judge0 直连链路,与 C/C++ 的执行方式一致,不需要改动 `codeapinew/`
- 代码格式化("整理"按钮)需要给 `codeapinew/formatter.py` 新增 `sql` 分支(用 `sqlparse` 库),是本次唯一涉及后端的改动,见"代码格式化"一节。
- 不做自动判题/评分、不做前后对比高亮、不做多语句持久会话。这些是明确排除的非目标(见"已排除的方案")。
## 架构与执行流程
将 SQL 作为一门新语言接入现有语言体系(与 python / c / cpp / turtle 同级),复用 `api.ts` 中现有的 Judge0 提交链路。
每次点击"运行"时:
1. 学生已从候选表下拉框选定一张表(如 `students`)。
2. 学生在编辑器中只写自己的 SQL(如 `DELETE FROM students WHERE score < 60;`)。
3. 前端拼接完整脚本:`<该候选表的建表+初始数据 SQL>` + `\n` + `<学生的 SQL>` + `\n` + `SELECT * FROM <该表名>;`(自动追加)。
4. 整体脚本作为 source,以 `language_id = 82`Judge0 的 SQLite)提交执行。
5. 每次提交都是全新的临时 SQLite 数据库,因此天然满足"每次都从预设初始数据重新执行"的要求,不需要任何服务端状态管理。
6. Judge0 返回的 stdout(即自动追加的那条 `SELECT *` 的结果)作为执行结果展示。
**统一处理原则**:无论学生写的是 INSERT / UPDATE / DELETE 还是 SELECT,都统一在脚本末尾追加一次针对该候选表的 `SELECT * FROM 表名;` 来展示表的最终状态。这样处理逻辑不需要按语句类型分支。如果学生自己写的也是 SELECT,输出里会同时包含学生查询的结果和追加查询的表状态,两者都展示,不做去重。
## 候选数据表内容
新增配置文件 `codenext/src/data/sqlTables.ts`(格式参考现有 `templates.ts`),包含 3 张独立的单表教学数据:
1. **`students`(学生成绩表)**`id, name, class, score`,8 条示例数据,分数、班级有区分度,适合 WHERE / UPDATE / DELETE 练习。
2. **`employees`(员工工资表)**`id, name, department, salary, hire_date`,8 条示例数据,适合按部门分组、调整工资、按日期筛选。
3. **`products`(商品库存表)**`id, name, category, price, stock`,8 条示例数据,适合新增商品(INSERT)、调整库存(UPDATE)、下架缺货商品(DELETE)。
每张表都是独立单表,不带外键关联,保持题目简单易懂。每条配置包含:`{ id, label, description, tableName, setupSql, previewRows }`
## UI 改动
沿用"每种语言一套专属布局"的现有模式(Turtle 已是先例):
- **`SelectLanguage.vue`**:语言列表新增 `["sql", "SQL"]`
- **`CodeEditor.vue`**`language === "sql"` 时引入 `@codemirror/lang-sql`(新依赖)做语法高亮,替换现有 python()/cpp() 的判断分支。
- **`Content.vue`**:新增分支 `v-else-if="code.language === 'sql'"`,渲染新组件 `SqlSection.vue`,替代原有的 Input/Output 分栏(SQL 场景不需要 stdin)。
**`SqlSection.vue` 内部结构**
1. 顶部:候选表下拉选择器。
2. 表结构 + 初始数据只读预览(帮助学生了解字段和数据,避免瞎猜)。
3. 中间:复用现有 `CodeSection`(学生写 SQL 的编辑器,本身不需要改动)。
4. 底部:结果区,复用现有 `OutputSection`(纯文本展示 Judge0 返回的 stdout),不做高亮/diff,保持简单。
**其他必改点**(沿用现有语言接入的固定套路):
- `types.ts``LANGUAGE` 联合类型新增 `"sql"`
- `templates.ts``languageToId` 新增 `sql: 82`
- `composables/code.ts``cache.code` 及分享链接(`?share=`)白名单加入 `sql`
- 新增 `composables/sqlTable.ts`:管理"当前选中的候选表"这一状态(选中的表 id,随语言切换重置为默认表)。
后端 `codeapinew` 不需要任何改动。
## 代码格式化("整理"按钮)
现有"整理"按钮(`CodeSection.vue:81`)已经是语言无关的通用功能:`code.ts``format()` 调用后端 `/format` 接口,`codeapinew/formatter.py``format_code()``language` 分发到不同的格式化工具(python/turtle 用 `ruff format`c/cpp 用 `clang-format`)。前端不需要任何改动,只需给 `format_code()` 新增 `sql` 分支:
```python
def _format_with_sql(code: str) -> str:
# sqlparse 对语法错误宽容,不会抛异常,语法问题留给判题阶段反馈
# strip_whitespace 会把多条语句压成一行,先按分号拆分再逐条格式化
statements = sqlparse.split(code)
return "\n\n".join(
sqlparse.format(s, strip_whitespace=True, keyword_case="upper")
for s in statements
)
```
`format_code()` 里加入 `if language == "sql": return _format_with_sql(code)` 分支即可。需要给 `codeapinew` 新增依赖 `sqlparse`
## 已排除的方案(非目标)
- **不做自动判题/评分**:没有"标准答案"比对,考察由老师人工完成。
- **不做持久会话/状态累积**:每次运行都基于预设初始数据重新执行,不支持像真实数据库会话那样跨多次提交累积状态(若未来需要,需引入客户端 SQLite WASM,如 sql.js,这是一次单独的架构变更)。
- **不做前后对比高亮**:只展示执行后的最终表状态文本,不做逐行 diff 或颜色标注。
## 风险与待确认事项
- **需要确认自建 Judge0 实例是否已启用 `language_id = 82`(SQLite)**。这一点无法从代码层面确认,需要访问该 Judge0 实例的 `/languages` 接口核实;若未启用,需要找 Judge0 管理员开启对应语言镜像后此功能才能工作。
## 测试计划
- 手动验证:针对三张候选表分别执行 INSERT / UPDATE / DELETE / SELECT,确认输出符合预期。
- 回归验证:语言切换、分享链接(`?share=`)、localStorage 缓存机制在新增 `sql` 语言后,对其他既有语言(python/c/cpp/turtle)无影响。
- "整理"按钮:验证多语句(含分号分隔)、关键字大小写混用、含语法错误的 SQL 格式化后不报错、行为符合预期。
@@ -0,0 +1,95 @@
# SQL 数据表格化展示 设计文档
日期:2026-07-05
## 背景与目标
`2026-07-04-sql-crud-practice-design.md` 实现的 SQL 练习功能里,候选表的初始数据预览和执行结果都是纯文本/代码形式(读只 CodeEditor 显示 `CREATE TABLE`/`INSERT` 语句,输出框显示 Judge0 返回的纯文本)。但 SQL 练习真正关心的是"数据本身",纯文本不够直观。本设计把这两处都换成真正的数据表格,同时调整整体布局。
## 范围
仅涉及 `codenext/`(前端)。不需要改动 `codeapinew/` 后端或 Judge0 提交方式(仍然是同一个 Judge0 SQLite 直连链路)。
## 数据模型改造
`src/data/sqlTables.ts``SqlTablePreset` 从"一整段写死的 SQL 字符串"改为结构化数据:
```typescript
export interface SqlColumn {
name: string
type: string // 如 "INTEGER PRIMARY KEY", "TEXT NOT NULL"
}
export interface SqlTablePreset {
id: string
label: string
tableName: string
columns: SqlColumn[]
rows: (string | number)[][] // 每行一个数组,顺序对应 columns
}
```
新增两个纯函数(同文件导出):
- `toSqlLiteral(value: string | number): string` — 数字原样转字符串,字符串加单引号并转义内部单引号。
- `buildSetupSql(table: SqlTablePreset): string` — 用 `columns`/`rows` 拼出 `CREATE TABLE ... ; INSERT INTO ... VALUES ...;`,供提交 Judge0 时使用(原先这段是写死的字符串常量,现在改为运行时生成,生成结果在语义上与原常量等价)。
3 张候选表(students/employees/products)的列结构和数据值保持不变,只是从字符串形式换成结构化形式。
## 布局改造
SQL 语言不再复用其他语言共用的"左代码、右内容"横向分栏模板,改为在 `Content.vue` 里单独走一条纵向分栏分支:
```
Content.vuelanguage === "sql" 分支):
┌─────────────────────────────┐
│ CodeSection │ ← 上:SQL 输入框(全宽),default-size 3/5
├───────────────┬─────────────┤
│ 原始数据表 │ 运行后数据表 │ ← 下:SqlSection 内部横向 n-split,各占一半
└───────────────┴─────────────┘
```
- 顶层 `n-split direction="vertical"``v-if="code.language === 'sql'"``#1=CodeSection``#2=SqlSection`;其余语言维持原有 `n-split direction="horizontal"` 分支(CodeSection + 各自的右侧内容),不受影响。
- `SqlSection.vue` 内部改为横向 `n-split``#1` 渲染候选表的初始数据(`n-data-table`),`#2` 渲染运行结果(成功时 `n-data-table`,失败时回退现有 `OutputSection`)。
- 表格组件统一用 naive-ui 的 `n-data-table`(已是现有依赖,自动适配深浅色主题)。
- 顶部语言选择器旁边的候选表下拉框(`SelectSqlTable.vue`,已在上一版实现)位置不变。
## 运行结果解析与去重
**问题**:原先脚本拼接方式是 `setupSql + 学生SQL + 追加的 SELECT * FROM 表;`。如果学生自己写的语句也是 SELECT(对初学者来说非常常见,比如上来就写 `SELECT * FROM students;`),Judge0 返回的 stdout 会包含两段几乎重复的结果——学生自己查询的输出,加上自动追加查询的输出——纯文本时代不易察觉,表格化后会非常明显地表现为"重复的两组数据"。
**解决方案**:利用 sqlite3 CLI 支持在同一份输入脚本里混用点命令的特性,在学生语句执行前后临时静默输出:
```
<setupSql>
.output /dev/null
<学生的 SQL(规范化,确保以且仅以一个分号结尾)>
.output stdout
SELECT * FROM <表名>;
```
`.output /dev/null` 只重定向"查询结果"的输出目标,不影响 sqlite3 shell 报错信息(走 stderr,仍会被 Judge0 的 `redirect_stderr_to_stdout: true` 合并进 stdout)。这样无论学生写什么语句,stdout 里能看到的查询结果永远只有最后那条自动追加的 `SELECT * FROM 表;`,彻底消除重复问题,也不需要任何"猜哪一段是最终结果"的启发式解析。
`buildSqlScript``src/composables/sqlTable.ts`)改为按上述模板拼接。
**解析**(新增 `parseResultRows(output: string, columns: SqlColumn[])`,与 `buildSqlScript` 同文件):状态为 `Accepted` 时,把 `output` 按行分割、去空行,每行按 `|` 切开,按 `columns` 顺序映射成对象数组,直接喂给 `n-data-table`。状态非 `Accepted`(SQL 写错)时,右侧不解析、不渲染表格,回退显示现有的 `OutputSection`(错误文本 + "运行失败" 标签 + AI"推测原因"面板,和 C/C++ 报错一致)。
## 已排除的方案(非目标)
- 不再展示原始 `CREATE TABLE`/`INSERT` 语句文本(连列类型都不单独展示)——这是本次改造的明确取舍,学生只看数据本身,不看 DDL。
- 不改变判题/评分方式——依然是纯观察沙箱,无自动判分。
- 不改变执行方式——依然是每次从预设数据重新执行、不做多语句持久会话(沿用上一版设计)。
## 测试计划
- 手动验证(浏览器 + 真实 Judge0):
- 切换到 SQL,确认布局为上下结构(上:代码框;下:左原始数据表、右空结果表)
- 对 students 表执行 `UPDATE`,确认右侧表格只显示一份最终数据,且改动的行是对的
- 学生自己写 `SELECT * FROM students;` 再运行,确认右侧表格**不再出现重复的两组数据**(这是本次改造要修的问题)
- 切换候选表,确认左右两个表格都随之更新
- 故意写错 SQL,确认右侧回退为现有错误文本/AI 推测面板,左侧原始数据表不受影响
- 回归:确认其他语言(python/c/cpp/turtle)的布局和行为不受影响(`Content.vue` 的改动只新增了 sql 专属分支,其余分支原样保留)
+52 -3
View File
@@ -11,6 +11,7 @@
"@codemirror/autocomplete": "^6.20.2",
"@codemirror/lang-cpp": "^6.0.3",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/lang-sql": "^6.10.0",
"@vueuse/core": "^14.3.0",
"axios": "^1.16.0",
"client-zip": "2.5.0",
@@ -129,6 +130,20 @@
"@lezer/python": "^1.1.4"
}
},
"node_modules/@codemirror/lang-sql": {
"version": "6.10.0",
"resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz",
"integrity": "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.12.3",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz",
@@ -205,9 +220,9 @@
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -490,6 +505,40 @@
"@napi-rs/wasm-runtime": "1.1.4"
}
},
"node_modules/@rspack/binding-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0"
}
},
"node_modules/@rspack/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@rspack/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@rspack/binding-win32-arm64-msvc": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-2.0.2.tgz",
+1
View File
@@ -13,6 +13,7 @@
"@codemirror/autocomplete": "^6.20.2",
"@codemirror/lang-cpp": "^6.0.3",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/lang-sql": "^6.10.0",
"@vueuse/core": "^14.3.0",
"axios": "^1.16.0",
"client-zip": "2.5.0",
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="#a9b9cb" d="M12 3c-4.97 0-9 1.343-9 3v12c0 1.657 4.03 3 9 3s9-1.343 9-3V6c0-1.657-4.03-3-9-3z"/><path fill="#7f8b99" d="M3 6v3c0 1.657 4.03 3 9 3s9-1.343 9-3V6c0 1.657-4.03 3-9 3S3 7.657 3 6z"/><path fill="#7f8b99" d="M3 11v3c0 1.657 4.03 3 9 3s9-1.343 9-3v-3c0 1.657-4.03 3-9 3s-9-1.343-9-3z"/></svg>

After

Width:  |  Height:  |  Size: 399 B

+4 -1
View File
@@ -1,6 +1,7 @@
<script lang="ts" setup>
import { cpp } from "@codemirror/lang-cpp"
import { python } from "@codemirror/lang-python"
import { sql } from "@codemirror/lang-sql"
import { EditorState } from "@codemirror/state"
import { EditorView } from "@codemirror/view"
import { autocompletion, completeAnyWord } from "@codemirror/autocomplete"
@@ -57,11 +58,13 @@ const langExtension = computed(() => {
if (props.language === "python" || props.language === "turtle") {
return python()
}
if (props.language === "sql") {
return sql()
}
return cpp()
})
const enhanceAutoCompletion = computed(() => {
console.log(props.language)
return autocompletion({
override: [enhanceCompletion(props.language), completeAnyWord],
})
+4 -3
View File
@@ -10,16 +10,17 @@ const LANGS = computed(() => {
["turtle", "海龟绘图"],
["c", "C 语言"],
["cpp", "C++"],
["sql", "SQL"],
]
if (isMobile.value) {
return allLangs.filter(([lang]) => lang !== "turtle")
return allLangs.filter(([lang]) => lang !== "turtle" && lang !== "sql")
}
return allLangs
})
// 如果当前在移动端且语言是海龟绘图,自动切换到 Python
// 如果当前在移动端且语言是海龟绘图或 SQL,自动切换到 Python
watch(isMobile, (mobile) => {
if (mobile && code.language === "turtle") {
if (mobile && (code.language === "turtle" || code.language === "sql")) {
code.language = "python"
}
})
+22
View File
@@ -0,0 +1,22 @@
<script lang="ts" setup>
import type { SelectOption } from "naive-ui"
import { computed } from "vue"
import { selectedTableId } from "../composables/sqlTable"
import { sqlTables } from "../data/sqlTables"
const tableOptions = computed<SelectOption[]>(() =>
sqlTables.map((table) => ({ value: table.id, label: table.label })),
)
</script>
<template>
<n-select
class="select"
:options="tableOptions"
v-model:value="selectedTableId"
/>
</template>
<style scoped>
.select {
width: 125px;
}
</style>
+7 -2
View File
@@ -7,6 +7,7 @@ import { sources } from "../templates"
import { Cache, Code, LANGUAGE, Status } from "../types"
import { atou, utoa } from "../utils"
import { isMobile } from "./breakpoints"
import { buildSqlScript, resetSqlTableSelection } from "./sqlTable"
const defaultLanguage = "python"
@@ -19,6 +20,7 @@ const cache: Cache = {
c: useStorage("code_c", sources["c"]),
cpp: useStorage("code_cpp", sources["cpp"]),
turtle: useStorage("code_turtle", sources["turtle"]),
sql: useStorage("code_sql", sources["sql"]),
},
}
@@ -44,6 +46,7 @@ watch(
code.value = cache.code[value].value
output.value = ""
status.value = Status.NotStarted
if (value === "sql") resetSqlTableSelection()
},
)
@@ -71,7 +74,7 @@ export async function init() {
if (base64) {
try {
const data = JSON.parse(atou(base64))
const lang = ["python", "c", "cpp", "turtle"].includes(data.lang)
const lang = ["python", "c", "cpp", "turtle", "sql"].includes(data.lang)
? (data.lang as LANGUAGE)
: defaultLanguage
const sharedCode = data.code ?? sources[lang]
@@ -115,8 +118,10 @@ export async function run() {
} else {
output.value = ""
status.value = Status.NotStarted
const sourceCode =
code.language === "sql" ? buildSqlScript(cleanCode) : cleanCode
const result = await submit(
{ value: cleanCode, language: code.language },
{ value: sourceCode, language: code.language },
input.value.trim(),
)
output.value = result.output || ""
+43
View File
@@ -0,0 +1,43 @@
import { ref } from "vue"
import {
buildSetupSql,
defaultSqlTableId,
sqlTables,
type SqlColumn,
} from "../data/sqlTables"
export const selectedTableId = ref(defaultSqlTableId)
export function resetSqlTableSelection() {
selectedTableId.value = defaultSqlTableId
}
export function buildSqlScript(studentSql: string) {
const table =
sqlTables.find((item) => item.id === selectedTableId.value) ??
sqlTables[0]
const normalizedSql = studentSql.trim().replace(/;?\s*$/, ";")
return [
buildSetupSql(table),
".output /dev/null",
normalizedSql,
".output stdout",
`SELECT * FROM ${table.tableName};`,
].join("\n\n")
}
export function parseResultRows(
output: string,
columns: SqlColumn[],
): Record<string, string | number>[] {
return output
.split("\n")
.map((line) => line.trim())
.filter(Boolean)
.map((line) => {
const cells = line.split("|")
return Object.fromEntries(
columns.map((column, index) => [column.name, cells[index] ?? ""]),
)
})
}
+98
View File
@@ -0,0 +1,98 @@
export interface SqlColumn {
name: string
type: string
}
export interface SqlTablePreset {
id: string
label: string
tableName: string
columns: SqlColumn[]
rows: (string | number)[][]
}
export function toSqlLiteral(value: string | number): string {
if (typeof value === "number") return String(value)
return `'${value.replace(/'/g, "''")}'`
}
export function buildSetupSql(table: SqlTablePreset): string {
const columnDefs = table.columns
.map((column) => `${column.name} ${column.type}`)
.join(",\n ")
const columnNames = table.columns.map((column) => column.name).join(", ")
const valuesList = table.rows
.map((row) => `(${row.map(toSqlLiteral).join(", ")})`)
.join(",\n ")
return `CREATE TABLE ${table.tableName} (\n ${columnDefs}\n);\n\nINSERT INTO ${table.tableName} (${columnNames}) VALUES\n ${valuesList};`
}
export const sqlTables: SqlTablePreset[] = [
{
id: "students",
label: "学生成绩表",
tableName: "students",
columns: [
{ name: "id", type: "INTEGER PRIMARY KEY" },
{ name: "name", type: "TEXT NOT NULL" },
{ name: "class", type: "TEXT NOT NULL" },
{ name: "score", type: "INTEGER NOT NULL" },
],
rows: [
[1, "张伟", "一班", 92],
[2, "王芳", "一班", 58],
[3, "李娜", "二班", 76],
[4, "刘洋", "二班", 45],
[5, "陈静", "一班", 88],
[6, "杨帆", "三班", 63],
[7, "赵敏", "二班", 39],
[8, "孙涛", "三班", 81],
],
},
{
id: "employees",
label: "员工工资表",
tableName: "employees",
columns: [
{ name: "id", type: "INTEGER PRIMARY KEY" },
{ name: "name", type: "TEXT NOT NULL" },
{ name: "department", type: "TEXT NOT NULL" },
{ name: "salary", type: "INTEGER NOT NULL" },
{ name: "hire_date", type: "TEXT NOT NULL" },
],
rows: [
[1, "周明", "技术部", 12000, "2019-03-01"],
[2, "吴倩", "市场部", 8000, "2021-07-15"],
[3, "郑凯", "技术部", 15500, "2017-11-20"],
[4, "钱多多", "财务部", 9200, "2020-01-10"],
[5, "孙丽", "市场部", 7600, "2022-05-30"],
[6, "李强", "技术部", 10800, "2023-09-01"],
[7, "林小雨", "财务部", 8900, "2018-06-12"],
[8, "黄河", "市场部", 6800, "2024-02-18"],
],
},
{
id: "products",
label: "商品库存表",
tableName: "products",
columns: [
{ name: "id", type: "INTEGER PRIMARY KEY" },
{ name: "name", type: "TEXT NOT NULL" },
{ name: "category", type: "TEXT NOT NULL" },
{ name: "price", type: "REAL NOT NULL" },
{ name: "stock", type: "INTEGER NOT NULL" },
],
rows: [
[1, "无线鼠标", "电子产品", 59.9, 120],
[2, "机械键盘", "电子产品", 299.0, 45],
[3, "保温杯", "生活用品", 39.5, 0],
[4, "笔记本", "文具", 12.0, 300],
[5, "蓝牙耳机", "电子产品", 199.0, 0],
[6, "台灯", "生活用品", 89.0, 60],
[7, "钢笔", "文具", 25.0, 150],
[8, "充电宝", "电子产品", 129.0, 8],
],
},
]
export const defaultSqlTableId = sqlTables[0].id
+16 -1
View File
@@ -3,12 +3,27 @@ import { code } from "../composables/code"
import CodeSection from "./CodeSection.vue"
import InputSection from "./InputSection.vue"
import OutputSection from "./OutputSection.vue"
import SqlSection from "./SqlSection.vue"
import TurtleSection from "./TurtleSection.vue"
</script>
<template>
<n-layout-content class="container">
<n-split direction="horizontal" :min="1 / 3" :max="4 / 5">
<n-split
v-if="code.language === 'sql'"
direction="vertical"
:default-size="1 / 4"
:min="1 / 4"
:max="3 / 4"
>
<template #1>
<CodeSection />
</template>
<template #2>
<SqlSection />
</template>
</n-split>
<n-split v-else direction="horizontal" :min="1 / 3" :max="4 / 5">
<template #1>
<CodeSection />
</template>
+3 -1
View File
@@ -2,8 +2,9 @@
import { Icon } from "@iconify/vue"
import { useMessage } from "naive-ui"
import SelectLanguage from "../components/SelectLanguage.vue"
import SelectSqlTable from "../components/SelectSqlTable.vue"
import ThemeButton from "../components/ThemeButton.vue"
import { loading, run, share, size } from "../composables/code"
import { code, loading, run, share, size } from "../composables/code"
const message = useMessage()
@@ -34,6 +35,7 @@ function handleShare() {
<template #prefix>字号</template>
</n-input-number>
<SelectLanguage />
<SelectSqlTable v-if="code.language === 'sql'" />
<n-button type="primary" @click="run" :disabled="loading">
运行 (F5)
</n-button>
+90
View File
@@ -0,0 +1,90 @@
<script lang="ts" setup>
import type { DataTableColumns } from "naive-ui"
import { computed, watch } from "vue"
import { output, status } from "../composables/code"
import { parseResultRows, selectedTableId } from "../composables/sqlTable"
import { sqlTables } from "../data/sqlTables"
import { Status } from "../types"
import OutputSection from "./OutputSection.vue"
const selectedTable = computed(
() =>
sqlTables.find((table) => table.id === selectedTableId.value) ??
sqlTables[0],
)
const tableColumns = computed<DataTableColumns>(() =>
selectedTable.value.columns.map((column) => ({
title: column.name,
key: column.name,
})),
)
const initialRows = computed(() =>
selectedTable.value.rows.map((row) =>
Object.fromEntries(
selectedTable.value.columns.map((column, index) => [
column.name,
row[index],
]),
),
),
)
const resultRows = computed(() =>
parseResultRows(output.value, selectedTable.value.columns),
)
watch(selectedTableId, () => {
output.value = ""
status.value = Status.NotStarted
})
</script>
<template>
<n-split
direction="horizontal"
:default-size="1 / 2"
:min="1 / 4"
:max="3 / 4"
>
<template #1>
<div class="table-panel">
<div class="panel-title">原始数据{{ selectedTable.label }}</div>
<n-data-table
size="small"
:bordered="false"
:columns="tableColumns"
:data="initialRows"
:row-key="(row: any) => row.id"
/>
</div>
</template>
<template #2>
<div class="table-panel" v-if="status === Status.Accepted">
<div class="panel-title">运行后数据</div>
<n-data-table
size="small"
:bordered="false"
:columns="tableColumns"
:data="resultRows"
:row-key="(row: any) => row.id"
/>
</div>
<OutputSection v-else />
</template>
</n-split>
</template>
<style scoped>
.table-panel {
height: 100%;
overflow: auto;
padding: 12px 20px;
box-sizing: border-box;
}
.panel-title {
font-size: 16px;
margin-bottom: 8px;
}
</style>
+2
View File
@@ -5,6 +5,7 @@ import {
NCollapse,
NCollapseItem,
NConfigProvider,
NDataTable,
NDropdown,
NFlex,
NIcon,
@@ -40,6 +41,7 @@ const naive = create({
NCollapse,
NCollapseItem,
NConfigProvider,
NDataTable,
NMessageProvider,
NLayout,
NLayoutHeader,
+4
View File
@@ -17,11 +17,14 @@ for i in range(4):
turtle.done()`
const sqlSource = "-- 在这里编写你的 SQL 语句\n"
export const languageToId: { [key in string]: number } = {
c: 50,
cpp: 54,
java: 62,
python: 71,
sql: 82,
}
export const sources = {
@@ -30,4 +33,5 @@ export const sources = {
java: javaSource,
python: pythonSource,
turtle: turtleSource,
sql: sqlSource,
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { RemovableRef } from "@vueuse/core"
export type LANGUAGE = "c" | "python" | "cpp" | "turtle"
export type LANGUAGE = "c" | "python" | "cpp" | "turtle" | "sql"
export interface Code {
value: string