dsh-contextos-devops 0.0.0-stage → 1.48.31
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/LICENSE +21 -0
- package/README.md +207 -2
- package/assets/avatar.jpg +0 -0
- package/assets/client.css +3316 -0
- package/assets/icon-180.png +0 -0
- package/build/client.mjs +64 -0
- package/client.js +7622 -0
- package/cordis.patch.yml +3 -0
- package/icon.svg +15 -0
- package/index.js +345 -0
- package/lib/base/env.js +22 -0
- package/lib/base/field.js +25 -0
- package/lib/base/http.js +140 -0
- package/lib/base/platform.js +33 -0
- package/lib/base/registry.js +186 -0
- package/lib/base/state.js +66 -0
- package/lib/base/table.js +47 -0
- package/lib/base/texttable.js +26 -0
- package/lib/base/url.js +178 -0
- package/lib/core/README.md +17 -0
- package/lib/core/apps.js +675 -0
- package/lib/core/auth.js +110 -0
- package/lib/core/avatar.js +289 -0
- package/lib/core/backup.js +105 -0
- package/lib/core/codegen.js +401 -0
- package/lib/core/copilot.js +419 -0
- package/lib/core/data.js +157 -0
- package/lib/core/device.js +148 -0
- package/lib/core/files.js +152 -0
- package/lib/core/gobuild.js +63 -0
- package/lib/core/iam.js +122 -0
- package/lib/core/identity.js +22 -0
- package/lib/core/nodes.js +627 -0
- package/lib/core/ops.js +306 -0
- package/lib/core/project.js +393 -0
- package/lib/core/spaces.js +406 -0
- package/lib/core/terminal.js +137 -0
- package/lib/core/terminal2.js +141 -0
- package/lib/core/users.js +51 -0
- package/lib/core/vocab.js +158 -0
- package/lib/misc/async.js +15 -0
- package/lib/misc/shell.js +27 -0
- package/lib/misc/static.js +33 -0
- package/locale/en.json +10 -0
- package/locale/zh.json +10 -0
- package/package.json +91 -4
- package/src/client/README.md +37 -0
- package/src/client/parts/00-prelude.js +315 -0
- package/src/client/parts/05-bootstrap.js +61 -0
- package/src/client/parts/08-store.js +85 -0
- package/src/client/parts/10-ui.js +188 -0
- package/src/client/parts/12-debug.js +361 -0
- package/src/client/parts/15-layout.js +86 -0
- package/src/client/parts/20-devworkspace.js +789 -0
- package/src/client/parts/25-editor-ui.js +28 -0
- package/src/client/parts/30-modals.js +85 -0
- package/src/client/parts/32-cards.js +176 -0
- package/src/client/parts/34-forms.js +176 -0
- package/src/client/parts/36-data.js +267 -0
- package/src/client/parts/40-project.js +430 -0
- package/src/client/parts/45-map.js +109 -0
- package/src/client/parts/46-pm.js +158 -0
- package/src/client/parts/47-three.js +324 -0
- package/src/client/parts/49-msg.js +167 -0
- package/src/client/parts/50-chat.js +1422 -0
- package/src/client/parts/51-pmselection.js +31 -0
- package/src/client/parts/52-pmdata.js +29 -0
- package/src/client/parts/55-pm.js +146 -0
- package/src/client/parts/58-config.js +381 -0
- package/src/client/parts/59-monitor.js +262 -0
- package/src/client/parts/60-page.js +1518 -0
- package/src/client/styles/kit.css +20 -0
- package/src/client/styles/main.css +3294 -0
- package/src/client/styles/tokens.css +2 -0
- package/vendor/leaflet/leaflet.css +661 -0
- package/vendor/leaflet/leaflet.js +6 -0
- package/vendor/three/three.module.js +53044 -0
- package/vendor/xterm/addon-fit.js +2 -0
- package/vendor/xterm/xterm.css +218 -0
- package/vendor/xterm/xterm.js +2 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function gitClass(x) { return (x === 'A' || x === '?' || x === '??' ? 'add' : x === 'D' ? 'del' : x === 'U' || x === 'UU' ? 'unt' : 'mod'); }
|
|
2
|
+
// Shared editor chrome. Keep the explorer's Git groups and file tabs out of
|
|
3
|
+
// the page component so new editor features compose these primitives.
|
|
4
|
+
function GitSections({ git, gitOpen, setGitOpen, gitHistOpen, setGitHistOpen, activePath, openFile, setStatus, zh }) {
|
|
5
|
+
if (!git) return null;
|
|
6
|
+
return h('div', { className: 'cx-git-sections' },
|
|
7
|
+
git.changed ? h('div', { className: 'cx-tree-recent' },
|
|
8
|
+
h('div', { className: 'cx-tree-sect cx-tree-sect-top', onClick: () => setGitOpen((v) => !v) }, h('span', { className: 'chev', dangerouslySetInnerHTML: chevHtml(gitOpen) }), zh ? 'GIT 变更' : 'GIT CHANGES',
|
|
9
|
+
h('span', { className: 'cx-git-count' }, String(git.changed))),
|
|
10
|
+
gitOpen ? Object.keys(git.files).slice(0, 60).map((fp) => h('div', { key: 'g:' + fp, className: `cx-tree-item${activePath === fp ? ' on' : ''}`, style: { paddingLeft: 6 }, onClick: () => openFile(fp) },
|
|
11
|
+
h('span', { className: 'chev' }),
|
|
12
|
+
h('span', { className: 'fic', dangerouslySetInnerHTML: fileIconHtml(fp) }),
|
|
13
|
+
h('span', { className: 'nm' }, fp.split('/').pop()),
|
|
14
|
+
h('span', { className: `cx-git-badge ${gitClass(git.files[fp])}` }, git.files[fp] === '??' ? 'U' : git.files[fp]))) : null) : null,
|
|
15
|
+
git.history && git.history.length ? h('div', { className: 'cx-tree-recent' },
|
|
16
|
+
h('div', { className: 'cx-tree-sect cx-tree-sect-top cx-git-history-head', onClick: () => setGitHistOpen((v) => !v) }, h('span', { className: 'chev', dangerouslySetInnerHTML: chevHtml(gitHistOpen) }), zh ? 'GIT 历史' : 'GIT HISTORY',
|
|
17
|
+
h('span', { className: 'cx-git-count' }, String(git.history.length))),
|
|
18
|
+
gitHistOpen ? git.history.slice(0, 30).map((c) => h('div', { key: 'h:' + c.hash, className: 'cx-tree-item cx-git-log', style: { paddingLeft: 6 }, title: `${c.hash} ${c.subject} · ${c.author} · ${c.when}`, onClick: () => { try { navigator.clipboard.writeText(c.hash); } catch {} setStatus(`${c.hash} ${c.subject}`); } },
|
|
19
|
+
h('span', { className: 'h' }, c.hash),
|
|
20
|
+
h('span', { className: 'nm' }, c.subject),
|
|
21
|
+
h('span', { className: 'when' }, c.when))) : null) : null);
|
|
22
|
+
}
|
|
23
|
+
function EditorTabs({ tabs, activePath, setActivePath, closeTab }) {
|
|
24
|
+
return h('div', { className: 'cx-tabs' }, (tabs || []).map((t) => h('div', { key: t.path, className: `cx-tab${t.path === activePath ? ' on' : ''}`, onClick: () => setActivePath(t.path), title: t.path },
|
|
25
|
+
h('span', { className: 'ico', dangerouslySetInnerHTML: fileIconHtml(t.name, 12) }),
|
|
26
|
+
h('span', { className: 'nm' }, t.name + (t.dirty ? ' ●' : '')),
|
|
27
|
+
h(IconButton, { name: 'close', className: 'x', onClick: (e) => { e.stopPropagation(); closeTab(t.path); } }))));
|
|
28
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
function DropSelect({ value, onChange, options, title, className }) {
|
|
2
|
+
const [open, setOpen] = useState(false);
|
|
3
|
+
const [pos, setPos] = useState(null);
|
|
4
|
+
const btnRef = useRef(null);
|
|
5
|
+
const lbRef = useRef(null);
|
|
6
|
+
const openMenu = (e) => { e.stopPropagation(); setOpen((v) => !v); };
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
if (!open) return;
|
|
9
|
+
const close = () => setOpen(false);
|
|
10
|
+
window.addEventListener('click', close);
|
|
11
|
+
window.addEventListener('scroll', close, true);
|
|
12
|
+
return () => { window.removeEventListener('click', close); window.removeEventListener('scroll', close, true); };
|
|
13
|
+
}, [open]);
|
|
14
|
+
const cur = (options || []).find((o) => o.value === value);
|
|
15
|
+
return h('span', { className: `cx-drop${className ? ' ' + className : ''}` },
|
|
16
|
+
h('button', { ref: btnRef, type: 'button', className: `cx-drop-btn${open ? ' on' : ''}`, title, onClick: openMenu },
|
|
17
|
+
h('span', { className: 'lb' }, cur ? cur.label : ''),
|
|
18
|
+
h('span', { className: 'cv' }, h(UiIcon, { name: 'chevronDown', size: 12 }))),
|
|
19
|
+
open ? h('div', { className: 'cx-drop-pop', onClick: (e) => e.stopPropagation() },
|
|
20
|
+
h(MenuList, { items: (options || []).map((o) => ({ label: o.label, icon: o.icon, on: o.value === value, go: () => { setOpen(false); onChange(o.value); } })), onPick: () => setOpen(false) })) : null);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function MenuButton({ items, disabled }) {
|
|
24
|
+
const [open, setOpen] = useState(false);
|
|
25
|
+
const fire = (e) => {
|
|
26
|
+
e.stopPropagation();
|
|
27
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
28
|
+
try {
|
|
29
|
+
window.dispatchEvent(new CustomEvent('cx-float-menu', { detail: {
|
|
30
|
+
x: Math.round(r.left),
|
|
31
|
+
right: Math.round((window.innerWidth || 1200) - r.right),
|
|
32
|
+
y: Math.round(Math.min(r.bottom + 4, (window.innerHeight || 800) - 300)),
|
|
33
|
+
title: '更多',
|
|
34
|
+
icon: 'more',
|
|
35
|
+
items: (items || []).map((it) => (it.sep ? { sep: true } : { label: T(it.label), icon: it.icon || 'chevronRight', url: it.url || '', danger: !!it.danger, open: !!it.open, openable: !!it.openable, go: () => it.action() })),
|
|
36
|
+
} }));
|
|
37
|
+
} catch {}
|
|
38
|
+
};
|
|
39
|
+
return h('span', { className: 'cx-menu-wrap' },
|
|
40
|
+
h(IconButton, { name: 'more', className: 'cx-btn xs', title: '⋯', disabled, onClick: fire }));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function Modal({ title, desc, onClose, children }) {
|
|
44
|
+
return h('div', { className: 'cx-modal', onMouseDown: (e) => { if (e.target === e.currentTarget) onClose(); } },
|
|
45
|
+
h('div', { className: 'cx-card' }, h('h2', null, title), desc ? h('div', { className: 'desc' }, desc) : null, children));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function SceneModal({ scene, onClose, onSave }) {
|
|
49
|
+
const p = scene?.params || {};
|
|
50
|
+
const tzDefault = (typeof Intl !== 'undefined' && Intl.DateTimeFormat().resolvedOptions().timeZone) || 'Asia/Shanghai';
|
|
51
|
+
const [name, setName] = useState(scene?.name || '');
|
|
52
|
+
const [theme, setTheme] = useState(p.theme || 'system');
|
|
53
|
+
const [lang, setLang] = useState(p.lang || 'system');
|
|
54
|
+
const [timezone, setTimezone] = useState(p.timezone || tzDefault);
|
|
55
|
+
const [location, setLocation] = useState(p.location || '');
|
|
56
|
+
return h(Modal, { title: '赛博世界设置 / CYBER WORLD', desc: '这些参数属于当前赛博世界:切换赛博世界会自动跟随;没设置的用系统默认。', onClose },
|
|
57
|
+
h('div', { className: 'cx-form' },
|
|
58
|
+
h('label', { className: 'cx-field full' }, '名称', h('input', { className: 'cx-input', value: name, placeholder: '例如 我的赛博世界', onChange: (e) => setName(e.target.value) })),
|
|
59
|
+
h('label', { className: 'cx-field full' }, '主题', h(DropSelect, { value: theme, onChange: setTheme, options: [{ value: 'system', label: '跟随系统' }, { value: 'light', label: '浅色' }, { value: 'dark', label: '深色' }] })),
|
|
60
|
+
h('label', { className: 'cx-field full' }, '语言', h(DropSelect, { value: lang, onChange: setLang, options: [{ value: 'system', label: '跟随系统' }, { value: 'zh', label: '中文' }, { value: 'en', label: 'English' }] })),
|
|
61
|
+
h('label', { className: 'cx-field full' }, '时区', h('input', { className: 'cx-input', value: timezone, placeholder: tzDefault, onChange: (e) => setTimezone(e.target.value) })),
|
|
62
|
+
h('label', { className: 'cx-field full' }, '地点', h('input', { className: 'cx-input', value: location, placeholder: '例如 深圳市', onChange: (e) => setLocation(e.target.value) }))),
|
|
63
|
+
h('div', { className: 'cx-foot' },
|
|
64
|
+
h('button', { className: 'cx-btn', onClick: onClose }, T('取消')),
|
|
65
|
+
h('button', { className: 'cx-btn pri', onClick: () => onSave({ name: name.trim(), theme, lang, timezone, location }) }, T('保存'))));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function AvatarEditModal({ avatar, onClose, onSave }) {
|
|
69
|
+
const [form, setForm] = useState({
|
|
70
|
+
name: avatar?.name || '', city: avatar?.city || '', company: avatar?.company || '',
|
|
71
|
+
persona: avatar?.persona || '', preferences: avatar?.preferences || '', style: avatar?.style || '',
|
|
72
|
+
});
|
|
73
|
+
const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value }));
|
|
74
|
+
return h(Modal, { title: '修改分身 / AVATAR', desc: '这些信息属于当前数字分身,创建实例与对话上下文都会带上。', onClose },
|
|
75
|
+
h('div', { className: 'cx-form' },
|
|
76
|
+
h('label', { className: 'cx-field full' }, '名称', h('input', { className: 'cx-input', value: form.name, onChange: set('name') })),
|
|
77
|
+
h('label', { className: 'cx-field full' }, '城市', h('input', { className: 'cx-input', value: form.city, onChange: set('city') })),
|
|
78
|
+
h('label', { className: 'cx-field full' }, '公司', h('input', { className: 'cx-input', value: form.company, onChange: set('company') })),
|
|
79
|
+
h('label', { className: 'cx-field full' }, '人格', h('input', { className: 'cx-input', value: form.persona, onChange: set('persona'), placeholder: '专业、亲和、直接' })),
|
|
80
|
+
h('label', { className: 'cx-field full' }, '偏好', h('input', { className: 'cx-input', value: form.preferences, onChange: set('preferences'), placeholder: '技术、产品、效率' })),
|
|
81
|
+
h('label', { className: 'cx-field full' }, '风格', h('input', { className: 'cx-input', value: form.style, onChange: set('style'), placeholder: '简洁、结构化' }))),
|
|
82
|
+
h('div', { className: 'cx-foot' },
|
|
83
|
+
h('button', { className: 'cx-btn', onClick: onClose }, T('取消')),
|
|
84
|
+
h('button', { className: 'cx-btn pri', onClick: () => onSave(form) }, T('保存'))));
|
|
85
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// 节点/空间通用的常用工具(都是宿主自带页面)
|
|
2
|
+
// key, 中文, 英文, 尾段, 图标
|
|
3
|
+
const CARD_TOOLS = [
|
|
4
|
+
['desktop', '应用桌面', 'App Desktop', '', 'monitor'],
|
|
5
|
+
['portal', '官方网站', 'Website', '', 'web'],
|
|
6
|
+
['admin', '管理后台', 'Admin', '', 'manage'],
|
|
7
|
+
['vimer', '编程工具', 'Code', '', 'dev'],
|
|
8
|
+
['runtime', '运行环境', 'Runtime', '', 'pulse'], // /c/runtime
|
|
9
|
+
['xterm', '命令终端', 'Terminal', '/type/sh', 'terminal'], // 直接进 shell,不停在会话列表
|
|
10
|
+
];
|
|
11
|
+
// 首页由节点/空间自己的代码决定,插件只负责「打开首页」;这里只列固定工具
|
|
12
|
+
// 副标题用的时间:MM-DD HH:mm
|
|
13
|
+
const cardTime = (v) => {
|
|
14
|
+
const s = String(v || '').replace('T', ' ').replace(/\..*$/, '');
|
|
15
|
+
const m = s.match(/^(?:\d{4}-)?(\d{2}-\d{2})[ T](\d{2}:\d{2})/);
|
|
16
|
+
return m ? `${m[1]} ${m[2]}` : (s || '');
|
|
17
|
+
};
|
|
18
|
+
// 版本号兜底:老接口会把整张 /c/version 表塞进 version,这里只取 vX.Y.Z-xxx
|
|
19
|
+
const cardVer = (v) => {
|
|
20
|
+
const raw = String(v || '').trim();
|
|
21
|
+
if (!raw) return '';
|
|
22
|
+
if (!/\s/.test(raw)) return raw;
|
|
23
|
+
const m = raw.match(/v\d+\.\d+\.\d+(?:-[0-9a-z.]+)+/i);
|
|
24
|
+
return m ? m[0] : '';
|
|
25
|
+
};
|
|
26
|
+
const toolItems = (onTool, urlOf, locale, isOpen = null) => (onTool ? CARD_TOOLS.map(([key, zh, en, tail, icon]) => {
|
|
27
|
+
const url = urlOf(key, tail || '');
|
|
28
|
+
return {
|
|
29
|
+
label: locale === 'zh' ? zh : en,
|
|
30
|
+
icon,
|
|
31
|
+
url,
|
|
32
|
+
openable: !!isOpen,
|
|
33
|
+
open: isOpen ? !!isOpen(url) : false,
|
|
34
|
+
action: () => onTool(url, locale === 'zh' ? zh : en),
|
|
35
|
+
};
|
|
36
|
+
}) : []);
|
|
37
|
+
|
|
38
|
+
function NodeCard({ n, active, busy, onSelect, onStart, onStop, onDelete, onOpen, onIde, onTool, onHome, onAuth, isOpen, tone = () => '', locale }) {
|
|
39
|
+
const on = n.state === 'running';
|
|
40
|
+
return h('div', { className: `cx-node${active ? ' on' : ''}`, onClick: () => onSelect(n.port) },
|
|
41
|
+
h(EntityTitle, {
|
|
42
|
+
image: n.image,
|
|
43
|
+
icon: 'server',
|
|
44
|
+
title: n.name || `node-${n.port}`,
|
|
45
|
+
status: on ? (locale === 'zh' ? '运行中' : 'RUNNING') : (locale === 'zh' ? '离线' : 'OFFLINE'),
|
|
46
|
+
statusTone: on ? 'ok' : 'off',
|
|
47
|
+
// 端口 · 代码版本 · 编译时间 · 程序大小
|
|
48
|
+
// 端口 · 版本 · 编译时间 · 大小 · 进程 · 部署方式
|
|
49
|
+
meta: [`:${n.port}`, cardVer(n.version) || '—',
|
|
50
|
+
cardTime(n.builtAt || (n.runtime && n.runtime.builtAt)) || '—',
|
|
51
|
+
n.sizeText || (n.runtime && n.runtime.sizeText) || (n.size ? Math.round(n.size / 1048576) + 'M' : '—'),
|
|
52
|
+
n.pid ? `pid ${n.pid}` : '',
|
|
53
|
+
n.source || ''].filter(Boolean).join(' · '),
|
|
54
|
+
}),
|
|
55
|
+
h('div', { className: 'cx-node-actions' },
|
|
56
|
+
on
|
|
57
|
+
? h('button', { className: 'cx-btn xs', onClick: (e) => { e.stopPropagation(); onOpen(n.port); } }, locale === 'zh' ? '首页' : 'HOME')
|
|
58
|
+
: h('button', { className: 'cx-btn xs pri', onClick: (e) => { e.stopPropagation(); onStart(n.port); } }, locale === 'zh' ? '启动' : 'START'),
|
|
59
|
+
h('button', { className: 'cx-btn xs', disabled: busy, onClick: (e) => { e.stopPropagation(); onIde && onIde(n.port, ''); } }, locale === 'zh' ? '编程' : 'CODE'),
|
|
60
|
+
on
|
|
61
|
+
? h(MenuButton, { disabled: busy, items: [
|
|
62
|
+
...(onHome ? [{ label: locale === 'zh' ? '打开首页' : 'Open home', icon: 'home', action: () => onHome() }] : []),
|
|
63
|
+
...toolItems(onTool, (k, tail = '') => `http://127.0.0.1:${n.port}/c/${k}${tail}`, locale, isOpen),
|
|
64
|
+
...(onAuth ? [{ label: locale === 'zh' ? '登录凭证' : 'Credentials', icon: 'lock', action: () => onAuth(n.port) }] : []),
|
|
65
|
+
{ sep: true }, { label: '停止', icon: 'stop', danger: tone('stop') === 'danger', action: () => onStop(n.port) }, { label: '删除', icon: 'trash', danger: true, action: () => onDelete(n.port) }] })
|
|
66
|
+
: h(MenuButton, { disabled: busy, items: [
|
|
67
|
+
...(onHome ? [{ label: locale === 'zh' ? '打开首页' : 'Open home', icon: 'home', action: () => onHome() }] : []),
|
|
68
|
+
...toolItems(onTool, (k, tail = '') => `http://127.0.0.1:${n.port}/c/${k}${tail}`, locale, isOpen),
|
|
69
|
+
...(onAuth ? [{ label: locale === 'zh' ? '登录凭证' : 'Credentials', icon: 'lock', action: () => onAuth(n.port) }] : []),
|
|
70
|
+
{ sep: true },
|
|
71
|
+
{ label: '删除', icon: 'trash', danger: tone('remove') === 'danger', action: () => onDelete(n.port) },
|
|
72
|
+
] })));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function SpaceCard({ s, active, busy, node, onSelect, onOpen, onStart, onStop, onDelete, onIde, onTool, onHome, isOpen, tone = () => '', locale }) {
|
|
76
|
+
const on = String(s.status || '').trim() === 'start';
|
|
77
|
+
const iconUrl = (() => {
|
|
78
|
+
const raw = String(s.icons || '').trim();
|
|
79
|
+
if (!raw) return '';
|
|
80
|
+
if (/^https?:\/\//i.test(raw)) return raw;
|
|
81
|
+
const p = raw.startsWith('/') ? raw : `/${raw}`;
|
|
82
|
+
return node ? `http://127.0.0.1:${node}${p}` : p;
|
|
83
|
+
})();
|
|
84
|
+
return h('div', { className: `cx-space${active ? ' on' : ''}`, onClick: () => onSelect(s.name) },
|
|
85
|
+
h(EntityTitle, {
|
|
86
|
+
image: iconUrl,
|
|
87
|
+
icon: 'space',
|
|
88
|
+
title: [s.help, s.name].filter(Boolean).join(' '),
|
|
89
|
+
status: on ? (locale === 'zh' ? '运行中' : 'start') : (locale === 'zh' ? '已停止' : 'stop'),
|
|
90
|
+
statusTone: on ? 'ok' : 'off',
|
|
91
|
+
// 代码版本 · 编译时间 · 程序大小
|
|
92
|
+
meta: [cardVer(s.version) || '—', cardTime(s.builtAt) || '—', s.size || '—'].join(' · '),
|
|
93
|
+
}),
|
|
94
|
+
h('div', { className: 'cx-space-actions' },
|
|
95
|
+
on ? h('button', { className: 'cx-btn xs pri', disabled: busy, onClick: (e) => { e.stopPropagation(); onOpen(s.name); } }, locale === 'zh' ? '首页' : 'OPEN') : h('button', { className: 'cx-btn xs pri', disabled: busy, onClick: (e) => { e.stopPropagation(); onStart(s.name); } }, locale === 'zh' ? '启动' : 'START'),
|
|
96
|
+
h('button', { className: 'cx-btn xs', disabled: busy, onClick: (e) => { e.stopPropagation(); onIde && onIde(node, s.name); } }, locale === 'zh' ? '编程' : 'CODE'),
|
|
97
|
+
h(MenuButton, { disabled: busy, items: [
|
|
98
|
+
...(onHome ? [{ label: locale === 'zh' ? '打开首页' : 'Open home', icon: 'home', action: () => onHome() }] : []),
|
|
99
|
+
...toolItems(onTool, (k, tail = '') => `http://127.0.0.1:${node}/s/${encodeURIComponent(s.name)}/c/${k}${tail}`, locale, isOpen),
|
|
100
|
+
...(on ? [{ sep: true }, { label: '停止', icon: 'stop', danger: tone('stop') === 'danger', action: () => onStop(s.name) }] : []),
|
|
101
|
+
{ label: '删除', icon: 'trash', danger: true, action: () => onDelete(s.name) },
|
|
102
|
+
] })));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function ItemIcon({ item, name, size, src, port }) {
|
|
106
|
+
const raw = String(src || (item && (item.icons || item.icon || item.image)) || '').trim();
|
|
107
|
+
const iconSrc = !raw ? ''
|
|
108
|
+
: /^(https?:|data:)/i.test(raw) ? raw
|
|
109
|
+
: raw.startsWith('/') && port ? `http://127.0.0.1:${port}${raw}`
|
|
110
|
+
: '';
|
|
111
|
+
const label = String(name || (item && (item.title || item.name)) || '?').trim().slice(0, 1) || '?';
|
|
112
|
+
let hue = 0;
|
|
113
|
+
const sname = String((item && (item.name || item.command)) || label);
|
|
114
|
+
for (let i = 0; i < sname.length; i += 1) hue = (hue * 31 + sname.charCodeAt(i)) % 360;
|
|
115
|
+
const px = size || 26;
|
|
116
|
+
if (iconSrc && /^(https?:|\/)/.test(iconSrc)) {
|
|
117
|
+
return h('span', { className: 'cx-ai', style: { width: px, height: px } }, h('img', { src: iconSrc, alt: '' }));
|
|
118
|
+
}
|
|
119
|
+
return h('span', { className: 'cx-ai', style: { width: px, height: px, background: `linear-gradient(135deg, hsl(${hue} 72% 42%), hsl(${(hue + 42) % 360} 70% 26%))` } }, label.toUpperCase());
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function AppRow({ a, active, onSelect, onOpen, locale, node }) {
|
|
123
|
+
return h('div', { className: `cx-app${active ? ' on' : ''}`, onClick: () => (onSelect ? onSelect(a) : onOpen(a)) },
|
|
124
|
+
h(EntityTitle, {
|
|
125
|
+
image: a.place_avatar || a.icon ? (/^https?:/i.test(a.place_avatar || a.icon) ? (a.place_avatar || a.icon) : `http://127.0.0.1:${node}${a.place_avatar || a.icon}`) : '',
|
|
126
|
+
icon: 'app',
|
|
127
|
+
title: a.title || a.name,
|
|
128
|
+
after: cardTime(a.updated) || '',
|
|
129
|
+
meta: [a.city_name, a.company_name, a.service_name].filter(Boolean).join(' · ') || a.command,
|
|
130
|
+
}),
|
|
131
|
+
h('div', { className: 'aa' },
|
|
132
|
+
h('button', { className: 'cx-btn xs pri', onClick: (e) => { e.stopPropagation(); onOpen(a); } }, locale === 'zh' ? '打开' : 'OPEN')));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function StoreRow({ s, onCreate, busy, onAction, onCode, onConfig, locale, node }) {
|
|
136
|
+
const online = s.status === 'online';
|
|
137
|
+
const acts = s.actions || ['create', 'online', 'offline', 'code', 'conf'];
|
|
138
|
+
const has = (x) => acts.includes(x);
|
|
139
|
+
const label = (x) => locale === 'zh' ? ({ create: '创建', online: '上架', offline: '下架', code: '编程', conf: '配置' }[x] || x) : ({ create: 'CREATE', online: 'Online', offline: 'Offline', code: 'Code', conf: 'Config' }[x] || x);
|
|
140
|
+
return h('div', { className: 'cx-store' },
|
|
141
|
+
h('div', { className: 'cx-store-main', onClick: () => !busy && onCreate(s) },
|
|
142
|
+
h(ItemIcon, { item: s, name: s.name, size: 26, src: s.icon, port: node }),
|
|
143
|
+
h('div', { style: { flex: 1, minWidth: 0 } },
|
|
144
|
+
h('div', { className: 'cx-store-titleline' },
|
|
145
|
+
h('span', { className: 'sn' }, s.name),
|
|
146
|
+
h('span', { className: `cx-status-r ${online ? 'on' : 'off'}` }, s.status_label || s.status)),
|
|
147
|
+
h('div', { className: 'ss' }, `${s.space} · ${s.command}`))),
|
|
148
|
+
h('div', { className: 'cx-store-actions' },
|
|
149
|
+
has('create') ? h('button', { className: 'cx-btn xs pri', disabled: busy, onClick: (e) => { e.stopPropagation(); onCreate(s); } }, busy ? '…' : label('create')) : null,
|
|
150
|
+
has('online') ? h('button', { className: 'cx-btn xs', disabled: busy, onClick: (e) => { e.stopPropagation(); onAction(s, 'online'); } }, label('online')) : null,
|
|
151
|
+
has('offline') ? h('button', { className: 'cx-btn xs danger', disabled: busy, onClick: (e) => { e.stopPropagation(); onAction(s, 'offline'); } }, label('offline')) : null,
|
|
152
|
+
(has('code') || has('conf') || has('more')) ? h(MenuButton, { disabled: busy, items: [
|
|
153
|
+
has('code') ? { label: label('code'), icon: 'dev', action: () => onCode(s) } : null,
|
|
154
|
+
has('conf') ? { label: label('conf'), icon: 'setting', action: () => onConfig(s) } : null,
|
|
155
|
+
has('more') ? { label: locale === 'zh' ? '更多' : 'More', icon: 'more', action: () => onAction(s, 'more') } : null,
|
|
156
|
+
].filter(Boolean) }) : null));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function OfficialSpaces({ node, onDeploy, locale }) {
|
|
160
|
+
const [keys, setKeys] = useState(['community', 'enterprise']);
|
|
161
|
+
const items = OFFICIAL;
|
|
162
|
+
const toggle = (k) => { if (k === 'community' || k === 'enterprise') return; setKeys((xs) => xs.includes(k) ? xs.filter((x) => x !== k) : [...xs, k]); };
|
|
163
|
+
return h('div', { className: 'cx-official' },
|
|
164
|
+
h('div', { className: 'oh' }, locale === 'zh' ? '一键部署官方空间 / OFFICIAL SPACES' : 'OFFICIAL SPACES / One-click deploy'),
|
|
165
|
+
items.map(([k, label, en]) => {
|
|
166
|
+
const forced = k === 'community' || k === 'enterprise';
|
|
167
|
+
const on = forced || keys.includes(k);
|
|
168
|
+
return h('label', { key: k, className: `cx-official-row${forced ? ' forced' : ''}` },
|
|
169
|
+
h('input', { type: 'checkbox', checked: on, disabled: forced, onChange: () => toggle(k) }),
|
|
170
|
+
h('span', { className: 'cx-space-ico', dangerouslySetInnerHTML: spaceIconHtml(label, 14) }),
|
|
171
|
+
h('span', { className: 'cx-space-name' }, label),
|
|
172
|
+
h('em', { className: 'cx-space-en' }, en || ''),
|
|
173
|
+
forced ? h('i', null, T('默认')) : null);
|
|
174
|
+
}),
|
|
175
|
+
h('button', { className: 'cx-btn xs pri cx-deploy-btn', style: { marginTop: 8 }, disabled: !node || !keys.length, onClick: () => onDeploy(keys) }, locale === 'zh' ? `部署 (${keys.length})` : `DEPLOY (${keys.length})`));
|
|
176
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
function DeployModal({ onClose, onDone, onScan }) {
|
|
2
|
+
const [port, setPort] = useState('9188');
|
|
3
|
+
const [name, setName] = useState('');
|
|
4
|
+
const [mode, setMode] = useState('auto');
|
|
5
|
+
const [parent, setParent] = useState('');
|
|
6
|
+
const [repo, setRepo] = useState('');
|
|
7
|
+
const [sourceBin, setSourceBin] = useState('');
|
|
8
|
+
const [spaceKeys, setSpaceKeys] = useState(['community', 'enterprise']);
|
|
9
|
+
const [busy, setBusy] = useState(false);
|
|
10
|
+
const [err, setErr] = useState('');
|
|
11
|
+
const go = async () => {
|
|
12
|
+
setBusy(true); setErr('');
|
|
13
|
+
try { await api('deploy', { port: Number(port), name, mode, parent, repo, sourceBin }); await api('officialSpaces', { node: Number(port), keys: spaceKeys }); onDone(); }
|
|
14
|
+
catch (e) { setErr(String(e?.message || e)); } finally { setBusy(false); }
|
|
15
|
+
};
|
|
16
|
+
return h(Modal, { title: '部署服务节点 / DEPLOY NODE', desc: 'auto=先 clone 代码库,有 git+go 编译,否则复制或下载 ice.bin;默认选中云社区、云办公。', onClose },
|
|
17
|
+
h('div', { className: 'cx-form' },
|
|
18
|
+
h('label', { className: 'cx-field' }, '部署方式 / MODE', h('select', { className: 'cx-select', value: mode, onChange: (e) => setMode(e.target.value) },
|
|
19
|
+
h('option', { value: 'auto' }, 'auto / 克隆后编译/复制/下载'), h('option', { value: 'copy' }, 'copy / 本机复制'), h('option', { value: 'download' }, 'download / 下载'), h('option', { value: 'source' }, 'source / 源码构建'))),
|
|
20
|
+
h('label', { className: 'cx-field' }, '端口 / PORT', h('input', { className: 'cx-input', value: port, onChange: (e) => setPort(e.target.value) })),
|
|
21
|
+
h('label', { className: 'cx-field' }, '名称 / NAME', h('input', { className: 'cx-input', value: name, placeholder: 'ContextOS-prod', onChange: (e) => setName(e.target.value) })),
|
|
22
|
+
(mode === 'download' || mode === 'source') ? h('label', { className: 'cx-field' }, '父节点 / PARENT', h('input', { className: 'cx-input', value: parent, placeholder: 'https://shylinux.com', onChange: (e) => setParent(e.target.value) })) : null,
|
|
23
|
+
mode === 'source' ? h('label', { className: 'cx-field full' }, '代码仓库 / REPO', h('input', { className: 'cx-input', value: repo, placeholder: 'https://shylinux.com/x/ContextOS', onChange: (e) => setRepo(e.target.value) })) : null,
|
|
24
|
+
mode === 'copy' ? h('label', { className: 'cx-field full' }, '节点镜像 / SOURCE BIN', h('input', { className: 'cx-input', value: sourceBin, placeholder: '留空用 usr/publish,或 /path/bin/ice.bin', onChange: (e) => setSourceBin(e.target.value) })) : null,
|
|
25
|
+
h('label', { className: 'cx-field full' }, '标准空间 / SPACES', h(StandardSpaces, { value: spaceKeys, onChange: setSpaceKeys }))),
|
|
26
|
+
err ? h(ErrorBox, { text: err }) : null,
|
|
27
|
+
h('div', { className: 'cx-foot' },
|
|
28
|
+
onScan ? h('button', { className: 'cx-btn', style: { marginRight: 'auto' }, onClick: onScan, title: '扫描远程主机 / WiFi / 蓝牙' }, '扫描设备') : null,
|
|
29
|
+
h('button', { className: 'cx-btn', onClick: onClose }, T('取消')),
|
|
30
|
+
h('button', { className: 'cx-btn pri', disabled: busy, onClick: go }, busy ? '部署中…' : '部署')));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function SpaceModal({ node, onClose, onDone }) {
|
|
34
|
+
const [name, setName] = useState('project');
|
|
35
|
+
const [busy, setBusy] = useState(false);
|
|
36
|
+
const [err, setErr] = useState('');
|
|
37
|
+
const go = async () => {
|
|
38
|
+
setBusy(true); setErr('');
|
|
39
|
+
try {
|
|
40
|
+
const created = await api('createSpace', { node: Number(node), name });
|
|
41
|
+
await api('initSpace', { node: Number(node), space: created.name });
|
|
42
|
+
onDone(created.name);
|
|
43
|
+
} catch (e) { setErr(String(e?.message || e)); } finally { setBusy(false); }
|
|
44
|
+
};
|
|
45
|
+
return h(Modal, { title: '创建项目空间 / CREATE WORKSPACE', desc: '不指定 repos:dream create 建空空间 → web.code.git.status init 生成 .git / src/main.go / go.mod 骨架。', onClose },
|
|
46
|
+
h('div', { className: 'cx-form' }, h('label', { className: 'cx-field full' }, '空间名称 / SPACE NAME', h('input', { className: 'cx-input', value: name, placeholder: 'project', onChange: (e) => setName(e.target.value) }))),
|
|
47
|
+
h('div', { className: 'desc' }, '空间名只用字母、数字、下划线;服务端自动加 YYYYMMDD- 前缀。'),
|
|
48
|
+
err ? h(ErrorBox, { text: err }) : null,
|
|
49
|
+
h('div', { className: 'cx-foot' }, h('button', { className: 'cx-btn', onClick: onClose }, T('取消')), h('button', { className: 'cx-btn pri', disabled: busy, onClick: go }, busy ? '创建中…' : '创建并初始化')));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function AppModal({ node, space, onClose, onDone }) {
|
|
53
|
+
const [name, setName] = useState('yuangong');
|
|
54
|
+
const [help, setHelp] = useState('员工管理');
|
|
55
|
+
const [place, setPlace] = useState('employee');
|
|
56
|
+
const [value, setValue] = useState('profile,position,skill,experience,education,record');
|
|
57
|
+
const [trans, setTrans] = useState('员工信息,工作岗位,专业技能,工作经验,教育经历,事件记录');
|
|
58
|
+
const [busy, setBusy] = useState(false);
|
|
59
|
+
const [err, setErr] = useState('');
|
|
60
|
+
const go = async () => {
|
|
61
|
+
setBusy(true); setErr('');
|
|
62
|
+
try {
|
|
63
|
+
const r = await api('autogenApp', { node: Number(node), space, name, help, place, value, trans });
|
|
64
|
+
onDone(r);
|
|
65
|
+
} catch (e) { setErr(String(e?.message || e)); } finally { setBusy(false); }
|
|
66
|
+
};
|
|
67
|
+
return h(Modal, { title: '生成应用 / GENERATE APP', desc: '云社区 service/autogen 按应用平台风格生成:portal.json + 五角色 + 列表/创建 + 看板;随后 compile。', onClose },
|
|
68
|
+
h('div', { className: 'cx-form' },
|
|
69
|
+
h('label', { className: 'cx-field' }, '应用名称 / APP NAME', h('input', { className: 'cx-input', value: name, onChange: (e) => setName(e.target.value) })),
|
|
70
|
+
h('label', { className: 'cx-field' }, '标题 / TITLE', h('input', { className: 'cx-input', value: help, onChange: (e) => setHelp(e.target.value) })),
|
|
71
|
+
h('label', { className: 'cx-field' }, '场景 / PLACE', h('input', { className: 'cx-input', value: place, onChange: (e) => setPlace(e.target.value) })),
|
|
72
|
+
h('label', { className: 'cx-field' }, '字段 / VALUE (逗号分隔)', h('input', { className: 'cx-input', value: value, onChange: (e) => setValue(e.target.value) })),
|
|
73
|
+
h('label', { className: 'cx-field full' }, '翻译 / TRANS(中文)', h('input', { className: 'cx-input', value: trans, onChange: (e) => setTrans(e.target.value) }))),
|
|
74
|
+
err ? h(ErrorBox, { text: err }) : null,
|
|
75
|
+
h('div', { className: 'cx-foot' }, h('button', { className: 'cx-btn', onClick: onClose }, T('取消')), h('button', { className: 'cx-btn pri', disabled: busy, onClick: go }, busy ? '生成中…' : '生成并编译')));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function CodeModal({ node, space, app, onClose }) {
|
|
79
|
+
const [files, setFiles] = useState([]);
|
|
80
|
+
const [showMore, setShowMore] = useState(false);
|
|
81
|
+
const [pm, setPm] = useState(null);
|
|
82
|
+
const [targetId, setTargetId] = useState('');
|
|
83
|
+
const [planId, setPlanId] = useState('');
|
|
84
|
+
const [taskId, setTaskId] = useState('');
|
|
85
|
+
const [reportId, setReportId] = useState('');
|
|
86
|
+
const [feedbackId, setFeedbackId] = useState('');
|
|
87
|
+
const bootCal = cxRead('cal', {});
|
|
88
|
+
const [calStart, setCalStart] = useState(bootCal.start || '');
|
|
89
|
+
const [calEnd, setCalEnd] = useState(bootCal.end || '');
|
|
90
|
+
useEffect(() => cxWrite('cal', { ...cxRead('cal', {}), start: calStart, end: calEnd }), [calStart, calEnd]);
|
|
91
|
+
const [planStatusFilter, setPlanStatusFilter] = useState([]);
|
|
92
|
+
const [taskStatusFilter, setTaskStatusFilter] = useState([]);
|
|
93
|
+
const [reportTypeFilter, setReportTypeFilter] = useState([]);
|
|
94
|
+
const [feedbackStatusFilter, setFeedbackStatusFilter] = useState([]);
|
|
95
|
+
const [pmForm, setPmForm] = useState(null);
|
|
96
|
+
const sessionStateRef = useRef({ key: '', ready: false, msgs: [] });
|
|
97
|
+
const seedingRef = useRef(false);
|
|
98
|
+
const [file, setFile] = useState('portal.go');
|
|
99
|
+
const [content, setContent] = useState('');
|
|
100
|
+
const [msg, setMsg] = useState('');
|
|
101
|
+
const [busy, setBusy] = useState(false);
|
|
102
|
+
const [err, setErr] = useState('');
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
let alive = true;
|
|
105
|
+
api('listDir', { node, space, path: `src/${app.name}` }).then((d) => {
|
|
106
|
+
if (!alive) return;
|
|
107
|
+
const list = (d.entries || []).filter((e) => e.type === 'file' && /\.(go|json|js|css|md)$/.test(e.name));
|
|
108
|
+
setFiles(list);
|
|
109
|
+
const pick = list.find((e) => e.name === 'portal.go') || list[0];
|
|
110
|
+
if (pick) setFile(pick.name);
|
|
111
|
+
}).catch((e) => setErr(String(e?.message || e)));
|
|
112
|
+
return () => { alive = false; };
|
|
113
|
+
}, [node, space, app.name]);
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (!file) return;
|
|
116
|
+
let alive = true;
|
|
117
|
+
api('readFile', { node, space, path: `src/${app.name}/${file}` }).then((d) => { if (alive) setContent(d.content || ''); }).catch((e) => { if (alive) setErr(String(e?.message || e)); });
|
|
118
|
+
return () => { alive = false; };
|
|
119
|
+
}, [node, space, app.name, file]);
|
|
120
|
+
const save = async (build) => {
|
|
121
|
+
setBusy(true); setErr(''); setMsg('');
|
|
122
|
+
try {
|
|
123
|
+
await api('writeFile', { node, space, path: `src/${app.name}/${file}`, content });
|
|
124
|
+
setMsg('saved');
|
|
125
|
+
if (build) { await api('buildSpace', { node, space }); setMsg('saved + built'); }
|
|
126
|
+
} catch (e) { setErr(String(e?.message || e)); } finally { setBusy(false); }
|
|
127
|
+
};
|
|
128
|
+
return h(Modal, { title: `CODE / ${app.title || app.name}`, desc: `编辑 ${space}/src/${app.name};保存后可编译。`, onClose },
|
|
129
|
+
h('div', { className: 'cx-form' },
|
|
130
|
+
h('label', { className: 'cx-field full' }, 'FILE', h('select', { className: 'cx-select', value: file, onChange: (e) => setFile(e.target.value) }, files.map((f) => h('option', { key: f.name, value: f.name }, f.name)))),
|
|
131
|
+
h('label', { className: 'cx-field full' }, 'CONTENT', h('textarea', { className: 'cx-area', style: { minHeight: 300 }, value: content, spellCheck: false, onChange: (e) => setContent(e.target.value) }))),
|
|
132
|
+
msg ? h('div', { className: 'desc' }, msg) : null,
|
|
133
|
+
err ? h(ErrorBox, { text: err }) : null,
|
|
134
|
+
h('div', { className: 'cx-foot' }, h('button', { className: 'cx-btn', onClick: onClose }, T('关闭')), h('button', { className: 'cx-btn', disabled: busy, onClick: () => save(false) }, busy ? '…' : T('保存')), h('button', { className: 'cx-btn pri', disabled: busy, onClick: () => save(true) }, T('保存并编译'))));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function InstanceModal({ node, space, app, avatar, onClose, onDone }) {
|
|
138
|
+
const [name, setName] = useState(app.title || '我的应用');
|
|
139
|
+
const [city, setCity] = useState(avatar?.city || '深圳市');
|
|
140
|
+
const [company, setCompany] = useState(avatar?.company || '云轩领航');
|
|
141
|
+
const [busy, setBusy] = useState(false);
|
|
142
|
+
const [err, setErr] = useState('');
|
|
143
|
+
const go = async () => {
|
|
144
|
+
setBusy(true); setErr('');
|
|
145
|
+
try { const r = await api('createInstance', { node, space, command: app.command, place: app.place || '', name, city, company, uid: cxActorUid(avatar) }); onDone(r); }
|
|
146
|
+
catch (e) { setErr(String(e?.message || e)); } finally { setBusy(false); }
|
|
147
|
+
};
|
|
148
|
+
return h(Modal, { title: `创建应用实例 / ${app.title || app.name}`, desc: `在 ${space} 用 ${app.command} 建实例;实例行会出现在该用户的 recent 列表。`, onClose },
|
|
149
|
+
h('div', { className: 'cx-form' },
|
|
150
|
+
h('label', { className: 'cx-field full' }, '实例名称 / INSTANCE NAME', h('input', { className: 'cx-input', value: name, onChange: (e) => setName(e.target.value) })),
|
|
151
|
+
h('label', { className: 'cx-field' }, '城市 / CITY', h('input', { className: 'cx-input', value: city, onChange: (e) => setCity(e.target.value) })),
|
|
152
|
+
h('label', { className: 'cx-field' }, '公司 / COMPANY', h('input', { className: 'cx-input', value: company, onChange: (e) => setCompany(e.target.value) }))),
|
|
153
|
+
err ? h(ErrorBox, { text: err }) : null,
|
|
154
|
+
h('div', { className: 'cx-foot' }, h('button', { className: 'cx-btn', onClick: onClose }, T('取消')), h('button', { className: 'cx-btn pri', disabled: busy, onClick: go }, busy ? '创建中…' : '创建应用实例')));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function StoreInstanceModal({ node, service, avatar, onClose, onDone }) {
|
|
158
|
+
const [city, setCity] = useState(avatar?.city || '深圳市');
|
|
159
|
+
const [company, setCompany] = useState(avatar?.company || '云轩领航');
|
|
160
|
+
const [name, setName] = useState(String(service.name || '').split(/\s+/)[0] || '我的应用');
|
|
161
|
+
const [busy, setBusy] = useState(false);
|
|
162
|
+
const [err, setErr] = useState('');
|
|
163
|
+
const go = async () => {
|
|
164
|
+
setBusy(true); setErr('');
|
|
165
|
+
try { const r = await api('createStoreInstance', { node, service, city, company, name, uid: avatar?.uid || '' }); onDone(r); }
|
|
166
|
+
catch (e) { setErr(String(e?.message || e)); } finally { setBusy(false); }
|
|
167
|
+
};
|
|
168
|
+
return h(Modal, { title: `创建应用实例 / ${service.name}`, desc: '从应用商店模板创建实例:城市 + 公司 + 应用名称三个参数。', onClose },
|
|
169
|
+
h('div', { className: 'cx-form' },
|
|
170
|
+
h('label', { className: 'cx-field full' }, '城市名称 / CITY', h('input', { className: 'cx-input', value: city, onChange: (e) => setCity(e.target.value) })),
|
|
171
|
+
h('label', { className: 'cx-field full' }, '公司名称 / COMPANY', h('input', { className: 'cx-input', value: company, onChange: (e) => setCompany(e.target.value) })),
|
|
172
|
+
h('label', { className: 'cx-field full' }, '应用名称 / APP NAME', h('input', { className: 'cx-input', value: name, onChange: (e) => setName(e.target.value) }))),
|
|
173
|
+
h('div', { className: 'desc' }, `space: ${service.space} · command: ${service.command}`),
|
|
174
|
+
err ? h(ErrorBox, { text: err }) : null,
|
|
175
|
+
h('div', { className: 'cx-foot' }, h('button', { className: 'cx-btn', onClick: onClose }, T('取消')), h('button', { className: 'cx-btn pri', disabled: busy, onClick: go }, busy ? '创建中…' : '创建应用实例')));
|
|
176
|
+
}
|