diff --git a/RuoYi-Vue/easycode-web/src/utils/appModuleOutline.js b/RuoYi-Vue/easycode-web/src/utils/appModuleOutline.js new file mode 100644 index 0000000..93ce1fa --- /dev/null +++ b/RuoYi-Vue/easycode-web/src/utils/appModuleOutline.js @@ -0,0 +1,131 @@ +const DEFAULT_OUTLINE_TITLE = '系统模块设计' +const FALLBACK_GROUP_TITLE = '未分组模块' + +function cleanText(value, fallback = '') { + const text = String(value ?? '').trim() + return text || fallback +} + +function textFromFields(value, fallback = '') { + if (!value || typeof value !== 'object') return cleanText(value, fallback) + return cleanText(value.title ?? value.name ?? value.code, fallback) +} + +function menuTitle(menu, index) { + return cleanText(menu?.name ?? menu?.title ?? menu?.code, `功能${index + 1}`) +} + +function warning(code, line) { + return line === undefined ? { code } : { code, line } +} + +function createGroup(title) { + return { + title, + items: [] + } +} + +export function normalizeModuleOutline(outline = {}) { + const groups = Array.isArray(outline.groups) ? outline.groups : [] + return { + title: textFromFields(outline), + groups: groups.map((group) => ({ + title: textFromFields(group), + items: (Array.isArray(group?.items) ? group.items : []).map((item) => ({ + title: textFromFields(item) + })).filter((item) => item.title) + })).filter((group) => group.title || group.items.length) + } +} + +export function parseModuleOutlineText(text = '') { + const warnings = [] + const lines = String(text ?? '').replace(/\t/g, ' ').split(/\r?\n/) + const contentLines = lines + .map((line, index) => ({ line, number: index + 1, title: line.trim() })) + .filter((entry) => entry.title) + + if (!contentLines.length) { + return { + outline: { title: '', groups: [] }, + warnings: [warning('EMPTY_OUTLINE')] + } + } + + const outline = { + title: contentLines[0].title, + groups: [] + } + let currentGroup = null + + contentLines.slice(1).forEach((entry) => { + const indent = entry.line.match(/^ */)?.[0].length ?? 0 + + if (indent < 2) { + currentGroup = createGroup(entry.title) + outline.groups.push(currentGroup) + warnings.push(warning('GROUP_WITHOUT_INDENT', entry.number)) + return + } + + if (indent < 4) { + currentGroup = createGroup(entry.title) + outline.groups.push(currentGroup) + return + } + + if (!currentGroup) { + currentGroup = createGroup(FALLBACK_GROUP_TITLE) + outline.groups.push(currentGroup) + warnings.push(warning('LEAF_WITHOUT_GROUP', entry.number)) + } + currentGroup.items.push({ title: entry.title }) + }) + + return { + outline: normalizeModuleOutline(outline), + warnings + } +} + +export function blueprintToModuleOutline(blueprint = {}, options = {}) { + const title = cleanText(options.projectName, cleanText(blueprint.projectName, DEFAULT_OUTLINE_TITLE)) + const groups = [ + { + title: '管理员端', + items: (Array.isArray(blueprint.adminMenus) ? blueprint.adminMenus : []).map((menu, index) => ({ + title: menuTitle(menu, index) + })).filter((item) => item.title) + }, + { + title: '普通用户端', + items: (Array.isArray(blueprint.frontendMenus) ? blueprint.frontendMenus : []).map((menu, index) => ({ + title: menuTitle(menu, index) + })).filter((item) => item.title) + } + ].filter((group) => group.items.length) + + return normalizeModuleOutline({ + title, + groups + }) +} + +export function moduleOutlineToText(outline = {}) { + const normalized = normalizeModuleOutline(outline) + const lines = [] + + if (normalized.title) { + lines.push(normalized.title) + } + + normalized.groups.forEach((group) => { + lines.push(` ${group.title}`) + group.items.forEach((item) => { + lines.push(` ${item.title}`) + }) + }) + + return lines.join('\n') +} diff --git a/RuoYi-Vue/easycode-web/src/utils/appModuleOutline.test.mjs b/RuoYi-Vue/easycode-web/src/utils/appModuleOutline.test.mjs new file mode 100644 index 0000000..f47230b --- /dev/null +++ b/RuoYi-Vue/easycode-web/src/utils/appModuleOutline.test.mjs @@ -0,0 +1,108 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { + blueprintToModuleOutline, + moduleOutlineToText, + normalizeModuleOutline, + parseModuleOutlineText +} from './appModuleOutline.js' + +const studentClubText = `学生社团管理系统 + 管理员端 + 注册登录 + 用户管理 + 社团管理 + 成员管理 + 活动管理 + 公告管理 + 系统管理 + 普通用户端 + 注册登录 + 社团浏览 + 社团申请 + 活动浏览 + 活动报名 + 个人中心` + +test('parseModuleOutlineText parses the reference student club outline', () => { + const result = parseModuleOutlineText(studentClubText) + + assert.deepEqual(result.warnings, []) + assert.equal(result.outline.title, '学生社团管理系统') + assert.deepEqual(result.outline.groups.map((group) => group.title), ['管理员端', '普通用户端']) + assert.deepEqual(result.outline.groups[0].items.map((item) => item.title), [ + '注册登录', + '用户管理', + '社团管理', + '成员管理', + '活动管理', + '公告管理', + '系统管理' + ]) + assert.deepEqual(result.outline.groups[1].items.map((item) => item.title), [ + '注册登录', + '社团浏览', + '社团申请', + '活动浏览', + '活动报名', + '个人中心' + ]) +}) + +test('parseModuleOutlineText normalizes tabs as two-space indentation', () => { + const result = parseModuleOutlineText('图书借阅系统\n\t管理端\n\t\t图书管理') + + assert.equal(result.outline.title, '图书借阅系统') + assert.equal(result.outline.groups[0].title, '管理端') + assert.equal(result.outline.groups[0].items[0].title, '图书管理') +}) + +test('parseModuleOutlineText accepts imperfect indentation with warnings', () => { + const result = parseModuleOutlineText('客户管理系统\n客户模块\n 客户列表\n 客户详情') + + assert.equal(result.outline.title, '客户管理系统') + assert.equal(result.outline.groups[0].title, '客户模块') + assert.deepEqual(result.outline.groups[0].items.map((item) => item.title), ['客户列表', '客户详情']) + assert.equal(result.warnings.some((warning) => warning.code === 'GROUP_WITHOUT_INDENT'), true) +}) + +test('parseModuleOutlineText returns empty outline warnings for blank input', () => { + const result = parseModuleOutlineText(' \n\t') + + assert.equal(result.outline.title, '') + assert.deepEqual(result.outline.groups, []) + assert.equal(result.warnings.some((warning) => warning.code === 'EMPTY_OUTLINE'), true) +}) + +test('blueprintToModuleOutline maps project menus into standard groups', () => { + const outline = blueprintToModuleOutline({ + projectName: '社团系统', + adminMenus: [ + { code: 'users', name: '用户管理' }, + { code: 'clubs', title: '社团管理' } + ], + frontendMenus: [ + { code: 'browse', name: '社团浏览' }, + { code: 'profile' } + ] + }, { + projectName: '学生社团管理系统' + }) + + assert.equal(outline.title, '学生社团管理系统') + assert.deepEqual(outline.groups.map((group) => group.title), ['管理员端', '普通用户端']) + assert.deepEqual(outline.groups[0].items.map((item) => item.title), ['用户管理', '社团管理']) + assert.deepEqual(outline.groups[1].items.map((item) => item.title), ['社团浏览', 'profile']) +}) + +test('moduleOutlineToText serializes normalized outlines with two-space levels', () => { + const outline = normalizeModuleOutline({ + title: '学生社团管理系统', + groups: [ + { title: '管理员端', items: [{ title: '用户管理' }] }, + { title: '普通用户端', items: [{ title: '社团浏览' }] } + ] + }) + + assert.equal(moduleOutlineToText(outline), '学生社团管理系统\n 管理员端\n 用户管理\n 普通用户端\n 社团浏览') +})