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