openxiangda 1.0.62 → 1.0.63
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/package.json +1 -1
- package/packages/sdk/dist/runtime/index.cjs +276 -13
- package/packages/sdk/dist/runtime/index.cjs.map +1 -1
- package/packages/sdk/dist/runtime/index.d.mts +1 -1
- package/packages/sdk/dist/runtime/index.d.ts +1 -1
- package/packages/sdk/dist/runtime/index.mjs +276 -13
- package/packages/sdk/dist/runtime/index.mjs.map +1 -1
- package/packages/sdk/dist/runtime/react.cjs +277 -13
- package/packages/sdk/dist/runtime/react.cjs.map +1 -1
- package/packages/sdk/dist/runtime/react.d.mts +60 -7
- package/packages/sdk/dist/runtime/react.d.ts +60 -7
- package/packages/sdk/dist/runtime/react.mjs +277 -13
- package/packages/sdk/dist/runtime/react.mjs.map +1 -1
- package/templates/openxiangda-react-spa/src/app/router.tsx +12 -1
- package/templates/openxiangda-react-spa/src/layouts/AdminShell.tsx +374 -96
- package/templates/openxiangda-react-spa/src/layouts/PublicShell.tsx +25 -3
- package/templates/openxiangda-react-spa/src/layouts/UserShell.tsx +101 -22
- package/templates/openxiangda-react-spa/src/pages/admin/RuntimeWorkspacePage.tsx +203 -41
- package/templates/openxiangda-react-spa/src/pages/defaults/DataRoutePage.tsx +80 -11
- package/templates/openxiangda-react-spa/src/pages/defaults/FilePreviewRoutePage.tsx +67 -14
- package/templates/openxiangda-react-spa/src/pages/defaults/FormRoutePage.tsx +153 -30
- package/templates/openxiangda-react-spa/src/pages/portal/UserPortalPage.tsx +53 -14
- package/templates/openxiangda-react-spa/src/pages/public/PublicHomePage.tsx +46 -6
- package/templates/openxiangda-react-spa/src/pages/states/NotFoundPage.tsx +26 -11
- package/templates/openxiangda-react-spa/src/shared/mac-admin.tsx +332 -0
- package/templates/openxiangda-react-spa/src/styles/index.css +42 -4
- package/templates/openxiangda-react-spa/tailwind.config.cjs +8 -0
- package/templates/openxiangda-react-spa/vite.config.ts +31 -0
|
@@ -1,126 +1,336 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import {
|
|
2
|
+
Bell,
|
|
3
|
+
ChevronDown,
|
|
4
|
+
ChevronRight,
|
|
5
|
+
CircleHelp,
|
|
6
|
+
Database,
|
|
7
|
+
FileText,
|
|
8
|
+
FolderOpen,
|
|
9
|
+
Globe2,
|
|
10
|
+
Home,
|
|
11
|
+
LayoutDashboard,
|
|
12
|
+
LogOut,
|
|
13
|
+
Menu,
|
|
14
|
+
Rocket,
|
|
15
|
+
Search,
|
|
16
|
+
Settings,
|
|
17
|
+
Shield,
|
|
18
|
+
Sparkles,
|
|
19
|
+
Upload,
|
|
20
|
+
UserCircle,
|
|
21
|
+
Workflow,
|
|
22
|
+
X,
|
|
23
|
+
} from "lucide-react";
|
|
24
|
+
import { useEffect, useMemo, useState } from "react";
|
|
25
|
+
import { Link, Outlet, useLocation, useParams } from "react-router-dom";
|
|
26
|
+
import {
|
|
27
|
+
PermissionBoundary,
|
|
28
|
+
useAppMenus,
|
|
29
|
+
useRuntimeAuth,
|
|
30
|
+
useRuntimeBootstrap,
|
|
31
|
+
type PermissionBoundaryFallbackState,
|
|
32
|
+
} from "openxiangda/runtime/react";
|
|
5
33
|
|
|
6
|
-
|
|
34
|
+
import {
|
|
35
|
+
MacPrimaryButton,
|
|
36
|
+
MacSecondaryButton,
|
|
37
|
+
MacStatePage,
|
|
38
|
+
MacStatusPill,
|
|
39
|
+
cn,
|
|
40
|
+
} from "@/shared/mac-admin";
|
|
41
|
+
|
|
42
|
+
type MenuItem = {
|
|
43
|
+
name: string;
|
|
44
|
+
path: string;
|
|
45
|
+
icon: typeof LayoutDashboard;
|
|
46
|
+
hint?: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type MenuGroup = {
|
|
50
|
+
title: string;
|
|
51
|
+
items: MenuItem[];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const fallbackNavigation: MenuItem[] = [
|
|
55
|
+
{ hint: "应用总览", icon: LayoutDashboard, name: "运行时工作台", path: "admin" },
|
|
56
|
+
{ hint: "普通用户入口", icon: Home, name: "用户门户", path: "portal" },
|
|
57
|
+
{ hint: "匿名访问链路", icon: Globe2, name: "公开访问", path: "public" },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const systemNavigation: MenuGroup[] = [
|
|
7
61
|
{
|
|
8
|
-
title: "
|
|
62
|
+
title: "表单与流程",
|
|
9
63
|
items: [
|
|
10
|
-
{
|
|
11
|
-
{
|
|
64
|
+
{ hint: "默认提交页", icon: FileText, name: "表单模板", path: "admin?panel=forms" },
|
|
65
|
+
{ hint: "流程发起和详情", icon: Workflow, name: "流程模板", path: "admin?panel=workflow" },
|
|
66
|
+
{ hint: "数据管理列表", icon: Database, name: "数据列表", path: "admin?panel=data" },
|
|
67
|
+
{ hint: "平台文件 ticket", icon: Upload, name: "文件预览", path: "admin?panel=files" },
|
|
12
68
|
],
|
|
13
69
|
},
|
|
14
70
|
{
|
|
15
|
-
title: "
|
|
71
|
+
title: "权限与发布",
|
|
16
72
|
items: [
|
|
17
|
-
{ name: "AI 验证", path: "admin?panel=verification"
|
|
18
|
-
{ name: "角色权限", path: "admin?panel=permission"
|
|
73
|
+
{ hint: "真实用户验证", icon: Sparkles, name: "AI 验证", path: "admin?panel=verification" },
|
|
74
|
+
{ hint: "页面 code 与角色", icon: Shield, name: "角色权限", path: "admin?panel=permission" },
|
|
75
|
+
{ hint: "版本与部署", icon: Rocket, name: "发布版本", path: "admin?panel=releases" },
|
|
76
|
+
{ hint: "运行参数", icon: Settings, name: "系统设置", path: "admin?panel=settings" },
|
|
19
77
|
],
|
|
20
78
|
},
|
|
21
79
|
];
|
|
22
80
|
|
|
23
81
|
export function AdminShell() {
|
|
24
82
|
const { appType = "" } = useParams();
|
|
83
|
+
const location = useLocation();
|
|
25
84
|
const bootstrap = useRuntimeBootstrap();
|
|
26
85
|
const menus = useAppMenus();
|
|
86
|
+
const auth = useRuntimeAuth();
|
|
27
87
|
const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>({});
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
88
|
+
const [mobileOpen, setMobileOpen] = useState(false);
|
|
89
|
+
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
|
90
|
+
const [loggingOut, setLoggingOut] = useState(false);
|
|
91
|
+
|
|
92
|
+
const appName = String(bootstrap.data?.app?.name || "OpenXiangda 应用");
|
|
93
|
+
const runtimeMode = String(bootstrap.data?.runtime?.mode || "react-spa");
|
|
94
|
+
const userName = String(
|
|
95
|
+
bootstrap.data?.user?.name ||
|
|
96
|
+
bootstrap.data?.user?.nickName ||
|
|
97
|
+
bootstrap.data?.user?.id ||
|
|
98
|
+
"未登录",
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const groups = useMemo<MenuGroup[]>(() => {
|
|
102
|
+
const platformItems = menus.data.length
|
|
103
|
+
? menus.data.flatMap(item => {
|
|
104
|
+
const children = item.children?.length ? item.children : [item];
|
|
105
|
+
return children
|
|
106
|
+
.filter(child => !child.isHidden)
|
|
107
|
+
.map(child => ({
|
|
108
|
+
hint: child.routeCode || child.resourceCode || undefined,
|
|
109
|
+
icon: resolveMenuIcon(String(child.name || child.path || "")),
|
|
110
|
+
name: child.name,
|
|
111
|
+
path: resolveMenuPath(appType, child.path || "admin"),
|
|
112
|
+
}));
|
|
113
|
+
})
|
|
114
|
+
: fallbackNavigation.map(item => ({
|
|
115
|
+
...item,
|
|
116
|
+
path: resolveMenuPath(appType, item.path),
|
|
117
|
+
}));
|
|
118
|
+
|
|
119
|
+
return [
|
|
120
|
+
{ title: "应用导航", items: platformItems },
|
|
121
|
+
...systemNavigation.map(group => ({
|
|
31
122
|
...group,
|
|
32
123
|
items: group.items.map(item => ({
|
|
33
124
|
...item,
|
|
34
125
|
path: resolveMenuPath(appType, item.path),
|
|
35
126
|
})),
|
|
36
|
-
}));
|
|
37
|
-
}
|
|
38
|
-
if (menus.data.every(item => !item.children?.length)) {
|
|
39
|
-
return [
|
|
40
|
-
{
|
|
41
|
-
title: "应用导航",
|
|
42
|
-
items: menus.data.map(item => ({
|
|
43
|
-
name: item.name,
|
|
44
|
-
path: resolveMenuPath(appType, item.path || "admin"),
|
|
45
|
-
icon: LayoutDashboard,
|
|
46
|
-
})),
|
|
47
|
-
},
|
|
48
|
-
];
|
|
49
|
-
}
|
|
50
|
-
return menus.data.map(group => ({
|
|
51
|
-
title: group.name,
|
|
52
|
-
items: (group.children?.length ? group.children : [group]).map(item => ({
|
|
53
|
-
name: item.name,
|
|
54
|
-
path: resolveMenuPath(appType, item.path || "admin"),
|
|
55
|
-
icon: LayoutDashboard,
|
|
56
127
|
})),
|
|
57
|
-
|
|
128
|
+
];
|
|
58
129
|
}, [appType, menus.data]);
|
|
59
130
|
|
|
131
|
+
const handleLogout = async () => {
|
|
132
|
+
setLoggingOut(true);
|
|
133
|
+
await auth.logoutAndRedirect({ replace: true });
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const sidebar = (
|
|
137
|
+
<aside className="flex h-full min-h-0 flex-col border-r border-white/70 bg-white/[0.84] backdrop-blur-xl">
|
|
138
|
+
<div className="flex h-20 shrink-0 items-center gap-3 border-b border-slate-200/70 px-5">
|
|
139
|
+
<Link className="grid h-12 w-12 place-items-center rounded-2xl bg-slate-950 text-sm font-semibold text-white shadow-lg shadow-slate-300/70" to={`/view/${appType}/admin`}>
|
|
140
|
+
OX
|
|
141
|
+
</Link>
|
|
142
|
+
<div className="min-w-0">
|
|
143
|
+
<Link className="block truncate text-base font-semibold text-slate-950" to={`/view/${appType}/admin`}>
|
|
144
|
+
OpenXiangda
|
|
145
|
+
</Link>
|
|
146
|
+
<div className="mt-0.5 truncate text-xs text-slate-500">React SPA Runtime</div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<nav className="ox-scrollbar min-h-0 flex-1 space-y-4 overflow-y-auto px-4 py-5">
|
|
151
|
+
{groups.map(group => {
|
|
152
|
+
const collapsed = collapsedGroups[group.title];
|
|
153
|
+
return (
|
|
154
|
+
<section key={group.title}>
|
|
155
|
+
<button
|
|
156
|
+
className="flex w-full items-center justify-between rounded-xl px-2 py-2 text-xs font-semibold text-slate-500 transition hover:bg-slate-100/80"
|
|
157
|
+
onClick={() => setCollapsedGroups(prev => ({ ...prev, [group.title]: !collapsed }))}
|
|
158
|
+
type="button"
|
|
159
|
+
>
|
|
160
|
+
<span>{group.title}</span>
|
|
161
|
+
{collapsed ? <ChevronRight size={15} /> : <ChevronDown size={15} />}
|
|
162
|
+
</button>
|
|
163
|
+
{!collapsed ? (
|
|
164
|
+
<div className="mt-1 space-y-1.5">
|
|
165
|
+
{group.items.map(item => {
|
|
166
|
+
const active = isMenuActive(location.pathname, location.search, item.path);
|
|
167
|
+
return (
|
|
168
|
+
<Link
|
|
169
|
+
className={cn(
|
|
170
|
+
"group relative flex items-center gap-3 rounded-2xl px-3 py-3 text-sm transition",
|
|
171
|
+
active
|
|
172
|
+
? "bg-blue-50 text-blue-700 shadow-sm ring-1 ring-blue-100"
|
|
173
|
+
: "text-slate-600 hover:bg-slate-100/80 hover:text-slate-950",
|
|
174
|
+
)}
|
|
175
|
+
key={`${group.title}-${item.name}-${item.path}`}
|
|
176
|
+
onClick={() => setMobileOpen(false)}
|
|
177
|
+
to={item.path}
|
|
178
|
+
>
|
|
179
|
+
<span
|
|
180
|
+
className={cn(
|
|
181
|
+
"absolute left-0 top-3 h-8 w-1 rounded-r-full transition",
|
|
182
|
+
active ? "bg-blue-600" : "bg-transparent",
|
|
183
|
+
)}
|
|
184
|
+
/>
|
|
185
|
+
<span
|
|
186
|
+
className={cn(
|
|
187
|
+
"grid h-9 w-9 shrink-0 place-items-center rounded-xl transition",
|
|
188
|
+
active ? "bg-white text-blue-700" : "bg-white/70 text-slate-500 group-hover:text-slate-700",
|
|
189
|
+
)}
|
|
190
|
+
>
|
|
191
|
+
<item.icon size={18} />
|
|
192
|
+
</span>
|
|
193
|
+
<span className="min-w-0 flex-1">
|
|
194
|
+
<span className="block truncate font-medium">{item.name}</span>
|
|
195
|
+
{item.hint ? (
|
|
196
|
+
<span className="mt-0.5 block truncate text-xs text-slate-400">{item.hint}</span>
|
|
197
|
+
) : null}
|
|
198
|
+
</span>
|
|
199
|
+
</Link>
|
|
200
|
+
);
|
|
201
|
+
})}
|
|
202
|
+
</div>
|
|
203
|
+
) : null}
|
|
204
|
+
</section>
|
|
205
|
+
);
|
|
206
|
+
})}
|
|
207
|
+
</nav>
|
|
208
|
+
|
|
209
|
+
<div className="shrink-0 border-t border-slate-200/70 p-4">
|
|
210
|
+
<div className="rounded-2xl bg-slate-950 p-4 text-white shadow-lg shadow-slate-300/60">
|
|
211
|
+
<div className="text-xs text-slate-300">Runtime Build</div>
|
|
212
|
+
<div className="mt-1 truncate text-sm font-semibold">
|
|
213
|
+
{String(bootstrap.data?.runtime?.activeBuildId || "local-dev")}
|
|
214
|
+
</div>
|
|
215
|
+
<div className="mt-3 flex flex-wrap gap-2">
|
|
216
|
+
<span className="rounded-full bg-white/[0.12] px-2.5 py-1 text-xs text-slate-100">{runtimeMode}</span>
|
|
217
|
+
<span className="rounded-full bg-emerald-400/18 px-2.5 py-1 text-xs text-emerald-100">online</span>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
</aside>
|
|
222
|
+
);
|
|
223
|
+
|
|
60
224
|
return (
|
|
61
225
|
<PermissionBoundary
|
|
62
|
-
|
|
226
|
+
fallback={state => <RuntimePermissionState appType={appType} state={state} />}
|
|
227
|
+
loadingFallback={<RuntimeLoadingState />}
|
|
63
228
|
menuCode="runtime_workspace"
|
|
64
229
|
path={`/view/${appType}/admin`}
|
|
65
|
-
|
|
66
|
-
fallback={<ShellState title="无权访问管理后台" description="请切换到有权限的账号,或联系管理员开通页面权限。" />}
|
|
230
|
+
routeCode="admin.dashboard"
|
|
67
231
|
>
|
|
68
|
-
<div className="min-h-screen bg-[
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
232
|
+
<div className="min-h-screen overflow-x-hidden bg-[linear-gradient(180deg,#eef2f7_0%,#f8fafc_48%,#edf2f7_100%)] text-slate-950">
|
|
233
|
+
<div className="fixed inset-y-0 left-0 z-30 hidden w-80 lg:block">{sidebar}</div>
|
|
234
|
+
|
|
235
|
+
{mobileOpen ? (
|
|
236
|
+
<div className="fixed inset-0 z-40 lg:hidden">
|
|
237
|
+
<button
|
|
238
|
+
aria-label="关闭导航"
|
|
239
|
+
className="absolute inset-0 bg-slate-950/35"
|
|
240
|
+
onClick={() => setMobileOpen(false)}
|
|
241
|
+
type="button"
|
|
242
|
+
/>
|
|
243
|
+
<div className="relative h-full w-[min(22rem,88vw)]">{sidebar}</div>
|
|
244
|
+
</div>
|
|
245
|
+
) : null}
|
|
246
|
+
|
|
247
|
+
<main className="min-w-0 lg:pl-80">
|
|
248
|
+
<header className="sticky top-0 z-20 border-b border-white/70 bg-white/[0.78] backdrop-blur-xl">
|
|
249
|
+
<div className="flex h-20 min-w-0 items-center justify-between gap-3 px-4 sm:px-6">
|
|
250
|
+
<div className="flex min-w-0 items-center gap-3">
|
|
251
|
+
<button
|
|
252
|
+
aria-label="打开导航"
|
|
253
|
+
className="grid h-10 w-10 shrink-0 place-items-center rounded-xl bg-slate-100 text-slate-700 lg:hidden"
|
|
254
|
+
onClick={() => setMobileOpen(true)}
|
|
255
|
+
type="button"
|
|
256
|
+
>
|
|
257
|
+
<Menu size={20} />
|
|
258
|
+
</button>
|
|
259
|
+
<div className="min-w-0">
|
|
260
|
+
<div className="truncate text-lg font-semibold text-slate-950">{appName}</div>
|
|
261
|
+
<div className="mt-1 flex min-w-0 items-center gap-2 text-xs text-slate-500">
|
|
262
|
+
<span className="truncate">/view/{appType || "APP"}/admin</span>
|
|
263
|
+
<span className="hidden sm:inline">·</span>
|
|
264
|
+
<span className="hidden sm:inline">{runtimeMode}</span>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
<div className="hidden min-w-0 max-w-xl flex-1 items-center rounded-2xl border border-slate-200 bg-slate-50/90 px-3 py-2 text-sm text-slate-400 xl:flex">
|
|
270
|
+
<Search className="mr-2 shrink-0" size={17} />
|
|
271
|
+
<span className="truncate">搜索菜单、表单、页面 code</span>
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
275
|
+
<MacStatusPill tone="emerald">wisejob</MacStatusPill>
|
|
276
|
+
<button className="relative grid h-10 w-10 place-items-center rounded-xl bg-white text-slate-600 shadow-sm ring-1 ring-slate-200 transition hover:bg-slate-50" type="button">
|
|
277
|
+
<Bell size={18} />
|
|
278
|
+
<span className="absolute right-2 top-2 h-2 w-2 rounded-full bg-rose-500 ring-2 ring-white" />
|
|
279
|
+
</button>
|
|
280
|
+
<button className="hidden h-10 w-10 place-items-center rounded-xl bg-white text-slate-600 shadow-sm ring-1 ring-slate-200 transition hover:bg-slate-50 sm:grid" type="button">
|
|
281
|
+
<CircleHelp size={18} />
|
|
282
|
+
</button>
|
|
283
|
+
<div className="relative">
|
|
82
284
|
<button
|
|
83
|
-
className="flex
|
|
84
|
-
onClick={() =>
|
|
285
|
+
className="flex h-10 items-center gap-2 rounded-full bg-white py-1 pl-1 pr-3 text-sm font-medium text-slate-700 shadow-sm ring-1 ring-slate-200 transition hover:bg-slate-50"
|
|
286
|
+
onClick={() => setUserMenuOpen(open => !open)}
|
|
85
287
|
type="button"
|
|
86
288
|
>
|
|
87
|
-
<span>
|
|
88
|
-
|
|
289
|
+
<span className="grid h-8 w-8 place-items-center rounded-full bg-blue-600 text-xs font-semibold text-white">
|
|
290
|
+
{userName.slice(0, 1).toUpperCase()}
|
|
291
|
+
</span>
|
|
292
|
+
<span className="hidden max-w-24 truncate sm:block">{userName}</span>
|
|
293
|
+
<ChevronDown size={15} />
|
|
89
294
|
</button>
|
|
90
|
-
{
|
|
91
|
-
<div className="mt-
|
|
92
|
-
|
|
93
|
-
<
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
295
|
+
{userMenuOpen ? (
|
|
296
|
+
<div className="absolute right-0 mt-3 w-72 overflow-hidden rounded-2xl border border-slate-200 bg-white p-2 shadow-[0_24px_70px_rgba(15,23,42,0.16)]">
|
|
297
|
+
<div className="flex gap-3 rounded-xl bg-slate-50 p-3">
|
|
298
|
+
<div className="grid h-10 w-10 shrink-0 place-items-center rounded-xl bg-blue-600 text-white">
|
|
299
|
+
<UserCircle size={21} />
|
|
300
|
+
</div>
|
|
301
|
+
<div className="min-w-0">
|
|
302
|
+
<div className="truncate text-sm font-semibold text-slate-950">{userName}</div>
|
|
303
|
+
<div className="mt-1 truncate text-xs text-slate-500">{String(bootstrap.data?.user?.id || "current-user")}</div>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
<button
|
|
307
|
+
className="mt-2 flex w-full items-center gap-2 rounded-xl px-3 py-2 text-left text-sm text-slate-600 transition hover:bg-slate-50"
|
|
308
|
+
onClick={() => {
|
|
309
|
+
setUserMenuOpen(false);
|
|
310
|
+
void bootstrap.reload();
|
|
311
|
+
}}
|
|
312
|
+
type="button"
|
|
313
|
+
>
|
|
314
|
+
<FolderOpen size={17} />
|
|
315
|
+
重新加载上下文
|
|
316
|
+
</button>
|
|
317
|
+
<button
|
|
318
|
+
className="flex w-full items-center gap-2 rounded-xl px-3 py-2 text-left text-sm text-rose-600 transition hover:bg-rose-50"
|
|
319
|
+
disabled={loggingOut}
|
|
320
|
+
onClick={() => void handleLogout()}
|
|
321
|
+
type="button"
|
|
322
|
+
>
|
|
323
|
+
<LogOut size={17} />
|
|
324
|
+
{loggingOut ? "正在退出" : "退出登录"}
|
|
325
|
+
</button>
|
|
106
326
|
</div>
|
|
107
|
-
)}
|
|
108
|
-
</
|
|
109
|
-
|
|
110
|
-
})}
|
|
111
|
-
</nav>
|
|
112
|
-
</aside>
|
|
113
|
-
<main className="lg:pl-72">
|
|
114
|
-
<header className="sticky top-0 z-10 flex h-16 items-center justify-between border-b border-slate-200 bg-white/85 px-5 backdrop-blur">
|
|
115
|
-
<div>
|
|
116
|
-
<div className="text-sm font-semibold">{String(bootstrap.data?.app?.name || "应用工作台")}</div>
|
|
117
|
-
<div className="text-xs text-slate-500">runtime: {bootstrap.data?.runtime?.mode || "loading"}</div>
|
|
118
|
-
</div>
|
|
119
|
-
<div className="rounded-full border border-slate-200 bg-white px-3 py-1 text-xs text-slate-600">
|
|
120
|
-
{String(bootstrap.data?.user?.name || bootstrap.data?.user?.id || "未登录")}
|
|
327
|
+
) : null}
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
121
330
|
</div>
|
|
122
331
|
</header>
|
|
123
|
-
|
|
332
|
+
|
|
333
|
+
<div className="mx-auto min-w-0 max-w-[1600px] px-4 py-5 sm:px-6">
|
|
124
334
|
<Outlet />
|
|
125
335
|
</div>
|
|
126
336
|
</main>
|
|
@@ -129,19 +339,87 @@ export function AdminShell() {
|
|
|
129
339
|
);
|
|
130
340
|
}
|
|
131
341
|
|
|
342
|
+
function RuntimePermissionState({
|
|
343
|
+
appType,
|
|
344
|
+
state,
|
|
345
|
+
}: {
|
|
346
|
+
appType: string;
|
|
347
|
+
state: PermissionBoundaryFallbackState;
|
|
348
|
+
}) {
|
|
349
|
+
const auth = useRuntimeAuth();
|
|
350
|
+
|
|
351
|
+
useEffect(() => {
|
|
352
|
+
if (state.errorType === "unauthenticated") {
|
|
353
|
+
void auth.redirectToLogin({ replace: true });
|
|
354
|
+
}
|
|
355
|
+
}, [auth, state.errorType]);
|
|
356
|
+
|
|
357
|
+
if (state.errorType === "unauthenticated") {
|
|
358
|
+
return (
|
|
359
|
+
<MacStatePage
|
|
360
|
+
actions={
|
|
361
|
+
<MacPrimaryButton onClick={() => void auth.redirectToLogin({ replace: true })}>
|
|
362
|
+
立即登录
|
|
363
|
+
</MacPrimaryButton>
|
|
364
|
+
}
|
|
365
|
+
description="当前浏览器没有有效登录态,正在为你跳转到平台登录页。"
|
|
366
|
+
fullScreen
|
|
367
|
+
icon={<LogOut size={24} />}
|
|
368
|
+
status="401"
|
|
369
|
+
title="需要登录"
|
|
370
|
+
/>
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return (
|
|
375
|
+
<MacStatePage
|
|
376
|
+
actions={
|
|
377
|
+
<>
|
|
378
|
+
<MacSecondaryButton onClick={() => window.history.back()}>返回上一页</MacSecondaryButton>
|
|
379
|
+
<MacPrimaryButton onClick={() => window.location.assign(`/view/${appType}/portal`)}>
|
|
380
|
+
打开用户门户
|
|
381
|
+
</MacPrimaryButton>
|
|
382
|
+
</>
|
|
383
|
+
}
|
|
384
|
+
description={state.message || "请切换到有权限的账号,或联系管理员开通页面权限。"}
|
|
385
|
+
fullScreen
|
|
386
|
+
icon={<Shield size={24} />}
|
|
387
|
+
status="403"
|
|
388
|
+
title="无权访问管理后台"
|
|
389
|
+
/>
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function RuntimeLoadingState() {
|
|
394
|
+
return (
|
|
395
|
+
<MacStatePage
|
|
396
|
+
description="正在读取应用、用户和页面权限信息。"
|
|
397
|
+
fullScreen
|
|
398
|
+
icon={<Rocket size={24} />}
|
|
399
|
+
status="LOADING"
|
|
400
|
+
title="正在进入工作区"
|
|
401
|
+
/>
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
132
405
|
function resolveMenuPath(appType: string, rawPath: string) {
|
|
133
406
|
const path = rawPath.replace(/:appType/g, appType).replace(/\/{2,}/g, "/");
|
|
134
407
|
if (path.startsWith("/")) return path;
|
|
135
408
|
return `/view/${appType}/${path}`.replace(/\/{2,}/g, "/");
|
|
136
409
|
}
|
|
137
410
|
|
|
138
|
-
function
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
411
|
+
function resolveMenuIcon(value: string): typeof LayoutDashboard {
|
|
412
|
+
const normalized = value.toLowerCase();
|
|
413
|
+
if (normalized.includes("用户") || normalized.includes("portal")) return Home;
|
|
414
|
+
if (normalized.includes("公开") || normalized.includes("public")) return Globe2;
|
|
415
|
+
if (normalized.includes("数据") || normalized.includes("data")) return Database;
|
|
416
|
+
if (normalized.includes("流程") || normalized.includes("process")) return Workflow;
|
|
417
|
+
if (normalized.includes("表单") || normalized.includes("form")) return FileText;
|
|
418
|
+
return LayoutDashboard;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function isMenuActive(pathname: string, search: string, target: string) {
|
|
422
|
+
const current = `${pathname}${search}`;
|
|
423
|
+
if (target.includes("?")) return current === target;
|
|
424
|
+
return pathname === target || pathname.startsWith(`${target}/`);
|
|
147
425
|
}
|
|
@@ -1,9 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Globe2, ShieldCheck } from "lucide-react";
|
|
2
|
+
import { Link, Outlet, useParams } from "react-router-dom";
|
|
3
|
+
|
|
4
|
+
import { MacStatusPill } from "@/shared/mac-admin";
|
|
2
5
|
|
|
3
6
|
export function PublicShell() {
|
|
7
|
+
const { appType = "" } = useParams();
|
|
8
|
+
|
|
4
9
|
return (
|
|
5
|
-
<div className="min-h-screen bg-[
|
|
6
|
-
<
|
|
10
|
+
<div className="min-h-screen overflow-x-hidden bg-[linear-gradient(180deg,#f8fafc_0%,#eef2f7_100%)]">
|
|
11
|
+
<header className="border-b border-white/70 bg-white/[0.78] backdrop-blur-xl">
|
|
12
|
+
<div className="mx-auto flex max-w-6xl items-center justify-between gap-3 px-5 py-4">
|
|
13
|
+
<Link className="flex min-w-0 items-center gap-3" to={`/view/${appType}/public`}>
|
|
14
|
+
<span className="grid h-11 w-11 shrink-0 place-items-center rounded-2xl bg-slate-950 text-white">
|
|
15
|
+
<Globe2 size={22} />
|
|
16
|
+
</span>
|
|
17
|
+
<span className="min-w-0">
|
|
18
|
+
<span className="block truncate text-base font-semibold text-slate-950">公开访问</span>
|
|
19
|
+
<span className="block truncate text-xs text-slate-500">Public Runtime</span>
|
|
20
|
+
</span>
|
|
21
|
+
</Link>
|
|
22
|
+
<MacStatusPill tone="blue">
|
|
23
|
+
<ShieldCheck size={13} />
|
|
24
|
+
ticket / guest
|
|
25
|
+
</MacStatusPill>
|
|
26
|
+
</div>
|
|
27
|
+
</header>
|
|
28
|
+
<main className="mx-auto min-w-0 max-w-6xl px-5 py-8">
|
|
7
29
|
<Outlet />
|
|
8
30
|
</main>
|
|
9
31
|
</div>
|