feat: add module outline parser
This commit is contained in:
131
RuoYi-Vue/easycode-web/src/utils/appModuleOutline.js
Normal file
131
RuoYi-Vue/easycode-web/src/utils/appModuleOutline.js
Normal file
@@ -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')
|
||||||
|
}
|
||||||
108
RuoYi-Vue/easycode-web/src/utils/appModuleOutline.test.mjs
Normal file
108
RuoYi-Vue/easycode-web/src/utils/appModuleOutline.test.mjs
Normal file
@@ -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 社团浏览')
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user