实验页新建、删除类

- "+ 新建类":输入类名,可以带文件夹(SQLHelpers/SQLBLL),文件夹成为命名空间的一段,
  模板和 VS 2012 的类模板一样;检查重名、非法名字
- 自己建的类可以删除(窗体、Program.cs、项目资源类不能用这个删)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-23 15:13:28 +08:00
1 parent 13c3e4c3ec
commit 42e258ddcd
3 files changed
+104 -2

No files matched your search

+15 -1
View File
@@ -3,7 +3,7 @@ import { expect, test } from 'bun:test'
import { generateDesigner, parseDesigner } from './document'
import { addControl } from './edit'
import { controlDef } from './catalog'
import { assignmentTemplate, formNameTaken, newFormFiles, nextFormName, projectNamespace, startupForm } from './templates'
import { assignmentTemplate, classPathError, formNameTaken, newClassFile, newFormFiles, nextFormName, projectNamespace, startupForm } from './templates'
const files = [
{ path: 'Program.cs', content: 'namespace 登录\n{\n static class Program\n {\n static void Main()\n {\n Application.Run(new Login());\n }\n }\n}\n' },
@@ -66,3 +66,17 @@ test('作业起点"空白工程":Program.cs 启动空的 Form1', async () => {
expect(program).toContain('Application.Run(new Form1())')
expect(files.find((f) => f.path === 'Form1.Designer.cs')!.content).not.toContain('com_port')
})
test('新建类:可以带文件夹,文件夹也是命名空间的一段;名字不能重复', () => {
const files = [{ path: 'Program.cs', content: 'namespace 智慧校园\n{\n static class Program { }\n}\n' }, { path: 'AppSet.cs', content: 'class AppSet { }' }]
expect(newClassFile('智慧校园', 'SQLHelpers/SQLHelper')).toEqual({
path: 'SQLHelpers/SQLHelper.cs',
content: 'using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace 智慧校园.SQLHelpers\n{\n class SQLHelper\n {\n }\n}\n',
})
expect(newClassFile('智慧校园', 'RecordClass.cs').path).toBe('RecordClass.cs')
expect(classPathError(files, 'SQLHelpers/SQLBLL')).toBeNull()
expect(classPathError(files, 'AppSet')).toBe('已经有 AppSet.cs 了')
expect(classPathError(files, 'Other/AppSet')).toBe('已经有名为“AppSet”的类或窗体')
expect(classPathError(files, '1abc')).toContain('不能以数字开头')
expect(classPathError(files, 'Properties/X')).toContain('平台管理')
})
+36
View File
@@ -22,6 +22,42 @@ export function formNameTaken(files: SourceFile[], name: string): boolean {
return files.some((f) => f.path === `${name}.cs` || f.path === `${name}.Designer.cs` || new RegExp(`(?<![\\p{L}\\p{N}_])class\\s+${name}(?![\\p{L}\\p{N}_])`, 'u').test(f.content))
}
/**
* 新建类时输入的名字有什么问题(没有问题返回 null)。可以带文件夹,如 SQLHelpers/SQLHelper:
* 和 VS 一样,文件放进文件夹,文件夹也是命名空间的一段。
*/
export function classPathError(files: SourceFile[], input: string): string | null {
const path = input.trim().replace(/\\/g, '/').replace(/\.cs$/i, '')
if (!path) return '请输入类名'
const parts = path.split('/')
if (parts.some((p) => !/^[\p{L}_][\p{L}\p{N}_]*$/u.test(p))) return '类名和文件夹名只能用字母、汉字、数字、下划线,且不能以数字开头'
if (parts[0] === 'Properties') return 'Properties 文件夹由平台管理,请换一个文件夹'
if (files.some((f) => f.path.toLowerCase() === `${path}.cs`.toLowerCase())) return `已经有 ${path}.cs 了`
if (formNameTaken(files, parts.at(-1)!)) return `已经有名为“${parts.at(-1)}”的类或窗体`
return null
}
/** 新建类(VS 的"添加 → 类"):和 VS 2012 的模板一样 */
export function newClassFile(namespace: string, input: string): SourceFile {
const path = input.trim().replace(/\\/g, '/').replace(/\.cs$/i, '')
const parts = path.split('/')
const name = parts.pop()!
const content = `using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ${[namespace, ...parts].join('.')}
{
class ${name}
{
}
}
`
return { path: `${path}.cs`, content }
}
/** Program.cs 里 Application.Run(new X()) 启动的窗体类名 */
export function startupForm(files: SourceFile[]): string | undefined {
const program = files.find((f) => f.path === 'Program.cs')
+53 -1
View File
@@ -10,7 +10,7 @@ import FormDesigner from '../designer/FormDesigner.vue'
import { createDesignerHistory } from '../designer/history'
import { gallery, loadGallery, projectResources, resourcePrefix, resourcesFile, RESOURCES_FILE } from '../designer/resources'
import ResourcesDialog from '../designer/ResourcesDialog.vue'
import { formNameTaken, isFormDesigner, newFormFiles, nextFormName, projectNamespace, startupForm } from '../designer/templates'
import { classPathError, formNameTaken, isFormDesigner, newClassFile, newFormFiles, nextFormName, projectNamespace, startupForm } from '../designer/templates'
import CodeEditor from '../editor/CodeEditor.vue'
import { api } from '../api'
import { currentUser, logout } from '../auth'
@@ -293,6 +293,41 @@ function saveResources(names: string[]) {
filesChanged()
}
// ---------- 新建、删除类 ----------
const newClassOpen = ref(false)
const newClassName = ref('')
const newClassError = computed(() => classPathError(files.value, newClassName.value))
function openNewClass(show: boolean) {
newClassOpen.value = show
if (show) newClassName.value = ''
}
/** 新建类:和 VS 的"添加 → 类"一样,可以带文件夹(SQLHelpers/SQLHelper) */
function createClass() {
if (newClassError.value) return
const created = newClassFile(projectNamespace(files.value), newClassName.value)
files.value.push(created)
filesChanged()
newClassOpen.value = false
activeTab.value = created.path
}
/** 当前标签页的普通类文件能否删除:窗体文件、Program.cs、平台生成的资源类不在这里删 */
const deletableClass = computed(() => {
const path = activeTab.value
if (!path || activeForm.value || path === 'Program.cs' || path.startsWith('Properties/') || !path.endsWith('.cs')) return undefined
return forms.value.some((f) => f.code === path || f.designer === path) ? undefined : path
})
function deleteClass() {
const path = deletableClass.value
if (!path) return
files.value = files.value.filter((f) => f.path !== path)
filesChanged()
activeTab.value = forms.value[0]?.tab ?? files.value[0]?.path
}
// ---------- 新建、删除窗体 ----------
const newFormOpen = ref(false)
const newFormName = ref('')
@@ -467,6 +502,23 @@ function run() {
<NButton size="small" type="primary" :disabled="!!newFormError" @click="createForm">创建</NButton>
</div>
</NPopover>
<NPopover trigger="click" placement="bottom-end" :show="newClassOpen" @update:show="openNewClass">
<template #trigger>
<NButton size="small" quaternary>+ 新建类</NButton>
</template>
<div class="new-form">
<div>类名(也是文件名;放进文件夹时写成 SQLHelpers/SQLHelper)</div>
<NInput v-model:value="newClassName" size="small" placeholder="例如:AppSet" @keydown.enter="createClass" />
<div v-if="newClassError && newClassName.trim()" class="new-form-error">{{ newClassError }}</div>
<NButton size="small" type="primary" :disabled="!!newClassError" @click="createClass">创建</NButton>
</div>
</NPopover>
<NPopconfirm v-if="deletableClass" positive-text="删除" negative-text="取消" @positive-click="deleteClass">
<template #trigger>
<NButton size="small" quaternary>删除类</NButton>
</template>
将删除 {{ deletableClass }},代码里用到它的地方需要自己修改。确定删除吗?
</NPopconfirm>
<NButton size="small" quaternary title="相当于 VS 的“项目属性 → 资源”:添加图片和声音,代码里用 Properties.Resources.名字" @click="resourcesOpen = true">
项目资源{{ resourceNames.length ? `(${resourceNames.length})` : '' }}
</NButton>