diff --git a/web/src/designer/templates.test.ts b/web/src/designer/templates.test.ts index 2a41651..75515a3 100644 --- a/web/src/designer/templates.test.ts +++ b/web/src/designer/templates.test.ts @@ -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('平台管理') +}) diff --git a/web/src/designer/templates.ts b/web/src/designer/templates.ts index 1422732..378c27f 100644 --- a/web/src/designer/templates.ts +++ b/web/src/designer/templates.ts @@ -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{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') diff --git a/web/src/pages/WorkbenchPage.vue b/web/src/pages/WorkbenchPage.vue index 112a0e2..de16083 100644 --- a/web/src/pages/WorkbenchPage.vue +++ b/web/src/pages/WorkbenchPage.vue @@ -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() { 创建 + + +
+
类名(也是文件名;放进文件夹时写成 SQLHelpers/SQLHelper)
+ +
{{ newClassError }}
+ 创建 +
+
+ + + 将删除 {{ deletableClass }},代码里用到它的地方需要自己修改。确定删除吗? + 项目资源{{ resourceNames.length ? `(${resourceNames.length})` : '' }}