@yss123/cage-admin 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -3
- package/dist/bootstrap.d.ts +27 -1
- package/dist/bootstrap.js +52 -16
- package/dist/bootstrap.js.map +1 -1
- package/dist/catalog.d.ts +48 -0
- package/dist/catalog.js +96 -0
- package/dist/catalog.js.map +1 -0
- package/dist/diagnostics.d.ts +14 -0
- package/dist/diagnostics.js +163 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/domain/constraints.d.ts +23 -0
- package/dist/domain/constraints.js +27 -0
- package/dist/domain/constraints.js.map +1 -1
- package/dist/domain/index.d.ts +5 -0
- package/dist/domain/index.js +5 -0
- package/dist/domain/index.js.map +1 -1
- package/dist/domain/org.d.ts +230 -149
- package/dist/domain/org.js +98 -4
- package/dist/domain/org.js.map +1 -1
- package/dist/http.d.ts +13 -1
- package/dist/http.js +28 -13
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/package.json +9 -5
- package/ui/dist/app/shell.js +135 -0
- package/ui/dist/app.js +34 -0
- package/ui/dist/modules/audit/audit-page.js +107 -0
- package/ui/dist/modules/audit/index.js +5 -0
- package/ui/dist/modules/auth/index.js +5 -0
- package/ui/dist/modules/auth/login-page.js +38 -0
- package/ui/dist/modules/models/index.js +5 -0
- package/ui/dist/modules/models/models-page.js +60 -0
- package/ui/dist/modules/operations/index.js +6 -0
- package/ui/dist/modules/operations/operations-page.js +110 -0
- package/ui/dist/modules/operations/sync-page.js +60 -0
- package/ui/dist/modules/org/index.js +5 -0
- package/ui/dist/modules/org/org-page.js +243 -0
- package/ui/dist/modules/personalities/index.js +5 -0
- package/ui/dist/modules/personalities/personalities-page.js +161 -0
- package/ui/dist/modules/roles/index.js +5 -0
- package/ui/dist/modules/roles/roles-page.js +348 -0
- package/ui/dist/modules/tasks/index.js +5 -0
- package/ui/dist/modules/tasks/tasks-page.js +68 -0
- package/ui/dist/shared/api.js +166 -0
- package/ui/dist/shared/dom.js +139 -0
- package/ui/dist/shared/labels.js +84 -0
- package/ui/dist/shared/navigation.js +14 -0
- package/ui/dist/shared/types.js +1 -0
- package/ui/index.html +12 -10
- package/ui/style.css +1657 -41
- package/ui/app.js +0 -339
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/** 统一用 DOM/textContent 构建界面;业务名称、错误、审计差异不会被当作 HTML 执行。 */
|
|
2
|
+
/**
|
|
3
|
+
* 创建指定标签并递归追加 children,最后赋属性以保证 select 能匹配已插入的 option。
|
|
4
|
+
* value/checked 使用 DOM 属性,aria 属性保留字符串语义;事件键去除 on 前缀后注册监听。
|
|
5
|
+
* 返回对应标签类型的未挂载节点,不做输入业务校验或权限判断。
|
|
6
|
+
*/
|
|
7
|
+
export function h(tag, attrs = {}, ...children) {
|
|
8
|
+
const element = document.createElement(tag);
|
|
9
|
+
const append = (child) => {
|
|
10
|
+
if (Array.isArray(child))
|
|
11
|
+
child.forEach(append);
|
|
12
|
+
else if (child !== null && child !== undefined && child !== false)
|
|
13
|
+
element.append(child instanceof Node ? child : document.createTextNode(String(child)));
|
|
14
|
+
};
|
|
15
|
+
children.forEach(append);
|
|
16
|
+
// select 的 value 必须在 option 加入后赋值,否则浏览器会丢失当前选择。
|
|
17
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
18
|
+
if (value === null || value === undefined)
|
|
19
|
+
continue;
|
|
20
|
+
if (key.startsWith('on'))
|
|
21
|
+
element.addEventListener(key.slice(2), value);
|
|
22
|
+
else if (key === 'class')
|
|
23
|
+
element.className = String(value);
|
|
24
|
+
else if (key === 'value')
|
|
25
|
+
element.value = String(value);
|
|
26
|
+
else if (key === 'checked')
|
|
27
|
+
element.checked = Boolean(value);
|
|
28
|
+
else if (key.startsWith('aria-'))
|
|
29
|
+
element.setAttribute(key, String(value));
|
|
30
|
+
else if (value !== false)
|
|
31
|
+
element.setAttribute(key, value === true ? '' : String(value));
|
|
32
|
+
}
|
|
33
|
+
return element;
|
|
34
|
+
}
|
|
35
|
+
const paths = {
|
|
36
|
+
grid: ['M3 3h7v7H3zM14 3h7v7h-7zM3 14h7v7H3zM14 14h7v7h-7z'],
|
|
37
|
+
shield: ['M12 3 3 7v5c0 5 9 9 9 9s9-4 9-9V7z', 'm8 12 3 3 5-6'],
|
|
38
|
+
users: [
|
|
39
|
+
'M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2',
|
|
40
|
+
'M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8',
|
|
41
|
+
'M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75',
|
|
42
|
+
],
|
|
43
|
+
inbox: ['M4 4h16l2 12v4H2v-4z', 'M2 16h6l2 3h4l2-3h6'],
|
|
44
|
+
search: ['M21 21l-5-5', 'M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16'],
|
|
45
|
+
clock: ['M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20', 'M12 6v6l4 2'],
|
|
46
|
+
link: ['M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-2 2', 'M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l2-2'],
|
|
47
|
+
file: ['M14 2H4v20h16V8z', 'M14 2v6h6M8 13h8M8 17h5'],
|
|
48
|
+
arrow: ['M7 17 17 7M7 7h10v10'],
|
|
49
|
+
plus: ['M12 5v14M5 12h14'],
|
|
50
|
+
check: ['m5 12 4 4L19 6'],
|
|
51
|
+
chevron: ['m9 5 7 7-7 7'],
|
|
52
|
+
logout: ['M9 21H3V3h6M9 12h12m-4-4 4 4-4 4'],
|
|
53
|
+
refresh: ['M20 7a9 9 0 1 0 1 10M20 3v5h-5'],
|
|
54
|
+
info: ['M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20', 'M12 11v6M12 7h.01'],
|
|
55
|
+
lock: ['M5 10h14v11H5zM8 10V7a4 4 0 0 1 8 0v3M12 14v3'],
|
|
56
|
+
};
|
|
57
|
+
/** 线条图标是本地 SVG,无字体、CDN 或图片下载依赖。 */
|
|
58
|
+
/**
|
|
59
|
+
* 从本地 paths 表生成装饰性 SVG,继承 currentColor 并设置 aria-hidden。
|
|
60
|
+
* name 必须来自已定义图标键;此函数不访问网络或加载外部 SVG。
|
|
61
|
+
*/
|
|
62
|
+
export function icon(name) {
|
|
63
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
64
|
+
for (const [key, value] of Object.entries({
|
|
65
|
+
viewBox: '0 0 24 24',
|
|
66
|
+
fill: 'none',
|
|
67
|
+
stroke: 'currentColor',
|
|
68
|
+
'stroke-width': '1.7',
|
|
69
|
+
'stroke-linecap': 'round',
|
|
70
|
+
'stroke-linejoin': 'round',
|
|
71
|
+
'aria-hidden': 'true',
|
|
72
|
+
class: 'icon',
|
|
73
|
+
}))
|
|
74
|
+
svg.setAttribute(key, value);
|
|
75
|
+
for (const d of paths[name]) {
|
|
76
|
+
const path = document.createElementNS(svg.namespaceURI, 'path');
|
|
77
|
+
path.setAttribute('d', d);
|
|
78
|
+
svg.append(path);
|
|
79
|
+
}
|
|
80
|
+
return svg;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 把表单标签、控件和可选提示组合为一个 label,控件状态仍由调用页面维护。
|
|
84
|
+
*/
|
|
85
|
+
export const field = (label, control, hint) => h('label', { class: 'field' }, h('span', {}, label), control, hint && h('small', {}, hint));
|
|
86
|
+
/**
|
|
87
|
+
* 将存在的时间字符串按浏览器本地时区显示为中文日期时间,null 显示占位符。
|
|
88
|
+
*/
|
|
89
|
+
export const formatTime = (time) => time ? new Date(time).toLocaleString('zh-CN', { hour12: false }) : '—';
|
|
90
|
+
/**
|
|
91
|
+
* 生成状态徽标,tone 选择既有 CSS 色调;text 作为文本插入,徽标不判断业务终态。
|
|
92
|
+
*/
|
|
93
|
+
export const badge = (text, tone = 'neutral') => h('span', { class: `badge ${tone}` }, text);
|
|
94
|
+
/**
|
|
95
|
+
* 以列标题与单元格渲染函数构造表格;空行集显示跨列提示。
|
|
96
|
+
* rows 是调用方已取得的数据,不在这里分页、过滤或请求服务器;单元格仍通过安全 DOM 子项追加。
|
|
97
|
+
*/
|
|
98
|
+
export function table(columns, rows, empty = '暂无记录') {
|
|
99
|
+
return h('div', { class: 'table-wrap' }, h('table', {}, h('thead', {}, h('tr', {}, columns.map(([name]) => h('th', { scope: 'col' }, name)))), h('tbody', {}, rows.length
|
|
100
|
+
? rows.map((row) => h('tr', {}, columns.map(([, cell]) => h('td', {}, cell(row)))))
|
|
101
|
+
: h('tr', {}, h('td', { colspan: columns.length, class: 'table-empty' }, empty)))));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* 构造含图标、标题和说明的空态块;由调用方区分无记录、无权限和未配置等原因。
|
|
105
|
+
*/
|
|
106
|
+
export function emptyState(title, message, glyph = 'inbox') {
|
|
107
|
+
return h('div', { class: 'empty-state' }, h('span', { class: 'empty-icon' }, icon(glyph)), h('h3', {}, title), h('p', {}, message));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* 组合带标题、可选说明和内容的 section,返回未挂载面板供页面自由布局。
|
|
111
|
+
*/
|
|
112
|
+
export function panel(title, description, ...children) {
|
|
113
|
+
return h('section', { class: 'panel' }, h('div', { class: 'panel-heading' }, h('div', {}, h('h2', {}, title), description && h('p', {}, description))), children);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* 创建搜索输入并将每次 input 的当前值交给 onInput。
|
|
117
|
+
* 默认不防抖、不请求服务器,调用者决定本地过滤或发起查询。
|
|
118
|
+
*/
|
|
119
|
+
export function searchInput(placeholder, onInput, value = '') {
|
|
120
|
+
const input = h('input', {
|
|
121
|
+
type: 'search',
|
|
122
|
+
placeholder,
|
|
123
|
+
'aria-label': placeholder,
|
|
124
|
+
value,
|
|
125
|
+
oninput: () => onInput(input.value),
|
|
126
|
+
});
|
|
127
|
+
return h('div', { class: 'search-field' }, icon('search'), input);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* 把原生 dialog 挂到 document.body 并以模态方式打开,返回实例便于提交后关闭。
|
|
131
|
+
* close 事件只监听一次并移除节点,避免已关闭表单长期留在 DOM;不会替表单提交或保存草稿。
|
|
132
|
+
*/
|
|
133
|
+
export function modal(title, content) {
|
|
134
|
+
const dialog = h('dialog', { class: 'modal' }, h('div', { class: 'modal-heading' }, h('h2', {}, title), h('button', { type: 'button', class: 'icon-button', 'aria-label': '关闭窗口', onclick: () => dialog.close() }, '×')), content);
|
|
135
|
+
dialog.addEventListener('close', () => dialog.remove(), { once: true });
|
|
136
|
+
document.body.append(dialog);
|
|
137
|
+
dialog.showModal();
|
|
138
|
+
return dialog;
|
|
139
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 把目录技术标识映射为系统管理页面的中文展示文字。
|
|
3
|
+
* 目录由应用启动阶段加载,函数按现有 state.catalog 查找;默认文案只影响显示,不改能力标识、状态或授权规则。
|
|
4
|
+
*/
|
|
5
|
+
import { state } from './api.js';
|
|
6
|
+
/**
|
|
7
|
+
* 范围组合子的固定名称和说明,供角色范围编辑器复用。实际可见记录仍由聚合字段与服务端授权决定。
|
|
8
|
+
*/
|
|
9
|
+
export const scopes = {
|
|
10
|
+
self: { title: '仅本人', hint: '仅查看归属于自己的记录' },
|
|
11
|
+
dept: { title: '本部门', hint: '查看本人所在部门的记录' },
|
|
12
|
+
deptSubtree: { title: '本部门及下级', hint: '查看本人部门及下属部门的记录' },
|
|
13
|
+
deptSet: { title: '指定部门', hint: '仅查看所选部门的记录' },
|
|
14
|
+
all: { title: '全部数据', hint: '查看当前租户内的全部记录' },
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* 按聚合标识查找中文 label/description,没有独立中文名时返回通用对象名。
|
|
18
|
+
*/
|
|
19
|
+
export function objectName(name) {
|
|
20
|
+
const object = state.catalog.aggregates.find((item) => item.name === name);
|
|
21
|
+
return object?.label && object.label !== name ? object.label : object?.description || '业务对象';
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 先使用能力目录解释操作名,再处理不属于目录的登录与运维审计动作。
|
|
25
|
+
* 找不到映射时返回通用名称,原始 capability 仍应保留供诊断。
|
|
26
|
+
*/
|
|
27
|
+
export function capabilityName(capability) {
|
|
28
|
+
const item = state.catalog.capabilities.find((item) => item.capability === capability);
|
|
29
|
+
if (item)
|
|
30
|
+
return item.aggregate ? item.label.replaceAll(item.aggregate, objectName(item.aggregate)) : item.label;
|
|
31
|
+
// 这些审计操作不属于领域能力目录,单独命名,避免登录和运维都显示成“业务操作”。
|
|
32
|
+
const operations = {
|
|
33
|
+
'auth.login': '账号登录',
|
|
34
|
+
'deadLetter.replay': '重新投递事件',
|
|
35
|
+
'deadLetter.discard': '丢弃死信事件',
|
|
36
|
+
'job.enable': '启用定时任务',
|
|
37
|
+
'job.disable': '停用定时任务',
|
|
38
|
+
};
|
|
39
|
+
return operations[capability] || '业务操作';
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 为不能由 user 直接调用的命令生成流程来源提示;含 user 或没有调用身份限制时返回空串。
|
|
43
|
+
*/
|
|
44
|
+
export function invocation(capability) {
|
|
45
|
+
if (!capability.invokedBy || capability.invokedBy.includes('user'))
|
|
46
|
+
return '';
|
|
47
|
+
const names = {
|
|
48
|
+
process: '审批流程',
|
|
49
|
+
external: '外部系统',
|
|
50
|
+
job: '定时任务',
|
|
51
|
+
system: '系统内部',
|
|
52
|
+
reaction: '自动反应',
|
|
53
|
+
};
|
|
54
|
+
return capability.invokedBy.map((item) => names[item] || '自动流程').join(' / ') + '调用';
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 组织与运行记录的常见状态文案表;展示映射不推断事件已完成或远端调用成功。
|
|
58
|
+
*/
|
|
59
|
+
export const statusNames = {
|
|
60
|
+
active: '已启用',
|
|
61
|
+
disabled: '已停用',
|
|
62
|
+
archived: '已停用',
|
|
63
|
+
pending: '等待处理',
|
|
64
|
+
processing: '处理中',
|
|
65
|
+
running: '执行中',
|
|
66
|
+
done: '已完成',
|
|
67
|
+
ok: '成功',
|
|
68
|
+
failed: '失败',
|
|
69
|
+
error: '失败',
|
|
70
|
+
dead: '已进入死信',
|
|
71
|
+
retry: '等待重试',
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* 返回常见状态的中文说明,未知值显示待确认,避免将新状态误写为成功。
|
|
75
|
+
*/
|
|
76
|
+
export const statusName = (status) => statusNames[status] || '待确认';
|
|
77
|
+
/**
|
|
78
|
+
* 从项目图谱 labels 查事件中文名,缺配置时只给通用说明。
|
|
79
|
+
*/
|
|
80
|
+
export const eventName = (name) => state.catalog.labels[`event:${name}`] || '业务事件';
|
|
81
|
+
/**
|
|
82
|
+
* 从当前目录查定时任务展示名;不读取调度执行状态或任务结果。
|
|
83
|
+
*/
|
|
84
|
+
export const jobName = (name) => state.catalog.jobs.find((item) => item.name === name)?.label || '定时任务';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 管理模块共享的导航意图出口,避免页面反向依赖应用壳。
|
|
3
|
+
* 改变 hash 触发壳的 hashchange;当前页面再次导航则派发刷新事件重新读取数据。
|
|
4
|
+
*/
|
|
5
|
+
// 事件让模块只发出导航意图,不反向导入应用壳。
|
|
6
|
+
/**
|
|
7
|
+
* 进入指定管理页面或刷新同一页面。page 是应用壳识别的页面键,不是任意外部 URL。
|
|
8
|
+
*/
|
|
9
|
+
export function navigate(page) {
|
|
10
|
+
if (location.hash !== `#/${page}`)
|
|
11
|
+
location.hash = `#/${page}`;
|
|
12
|
+
else
|
|
13
|
+
window.dispatchEvent(new Event('cage:refresh'));
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/ui/index.html
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
|
+
<!-- 系统管理浏览器文档入口。应用脚本挂载到 app,通知层由共享反馈工具管理;页面数据通过认证后的接口加载。 -->
|
|
2
3
|
<html lang="zh-CN">
|
|
3
|
-
<head>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
</head>
|
|
9
|
-
<body>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<title>系统管理</title>
|
|
8
|
+
<link rel="stylesheet" href="./style.css" />
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="app"></div>
|
|
12
|
+
<div id="notifications" aria-live="polite"></div>
|
|
13
|
+
<script type="module" src="./dist/app.js"></script>
|
|
14
|
+
</body>
|
|
13
15
|
</html>
|