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.
Files changed (28) hide show
  1. package/package.json +1 -1
  2. package/packages/sdk/dist/runtime/index.cjs +276 -13
  3. package/packages/sdk/dist/runtime/index.cjs.map +1 -1
  4. package/packages/sdk/dist/runtime/index.d.mts +1 -1
  5. package/packages/sdk/dist/runtime/index.d.ts +1 -1
  6. package/packages/sdk/dist/runtime/index.mjs +276 -13
  7. package/packages/sdk/dist/runtime/index.mjs.map +1 -1
  8. package/packages/sdk/dist/runtime/react.cjs +277 -13
  9. package/packages/sdk/dist/runtime/react.cjs.map +1 -1
  10. package/packages/sdk/dist/runtime/react.d.mts +60 -7
  11. package/packages/sdk/dist/runtime/react.d.ts +60 -7
  12. package/packages/sdk/dist/runtime/react.mjs +277 -13
  13. package/packages/sdk/dist/runtime/react.mjs.map +1 -1
  14. package/templates/openxiangda-react-spa/src/app/router.tsx +12 -1
  15. package/templates/openxiangda-react-spa/src/layouts/AdminShell.tsx +374 -96
  16. package/templates/openxiangda-react-spa/src/layouts/PublicShell.tsx +25 -3
  17. package/templates/openxiangda-react-spa/src/layouts/UserShell.tsx +101 -22
  18. package/templates/openxiangda-react-spa/src/pages/admin/RuntimeWorkspacePage.tsx +203 -41
  19. package/templates/openxiangda-react-spa/src/pages/defaults/DataRoutePage.tsx +80 -11
  20. package/templates/openxiangda-react-spa/src/pages/defaults/FilePreviewRoutePage.tsx +67 -14
  21. package/templates/openxiangda-react-spa/src/pages/defaults/FormRoutePage.tsx +153 -30
  22. package/templates/openxiangda-react-spa/src/pages/portal/UserPortalPage.tsx +53 -14
  23. package/templates/openxiangda-react-spa/src/pages/public/PublicHomePage.tsx +46 -6
  24. package/templates/openxiangda-react-spa/src/pages/states/NotFoundPage.tsx +26 -11
  25. package/templates/openxiangda-react-spa/src/shared/mac-admin.tsx +332 -0
  26. package/templates/openxiangda-react-spa/src/styles/index.css +42 -4
  27. package/templates/openxiangda-react-spa/tailwind.config.cjs +8 -0
  28. package/templates/openxiangda-react-spa/vite.config.ts +31 -0
@@ -1,126 +1,336 @@
1
- import { ChevronDown, ChevronRight, Home, LayoutDashboard, Shield, Sparkles } from "lucide-react";
2
- import { useMemo, useState } from "react";
3
- import { Link, NavLink, Outlet, useParams } from "react-router-dom";
4
- import { PermissionBoundary, useAppMenus, useRuntimeBootstrap } from "openxiangda/runtime/react";
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
- const fallbackGroups = [
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
- { name: "工作台", path: "admin", icon: LayoutDashboard },
11
- { name: "用户门户", path: "portal", icon: Home },
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", icon: Sparkles },
18
- { name: "角色权限", path: "admin?panel=permission", icon: Shield },
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 groups = useMemo(() => {
29
- if (!menus.data.length) {
30
- return fallbackGroups.map(group => ({
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
- routeCode="admin.dashboard"
226
+ fallback={state => <RuntimePermissionState appType={appType} state={state} />}
227
+ loadingFallback={<RuntimeLoadingState />}
63
228
  menuCode="runtime_workspace"
64
229
  path={`/view/${appType}/admin`}
65
- loadingFallback={<ShellState title="正在校验权限" description="请稍候" />}
66
- fallback={<ShellState title="无权访问管理后台" description="请切换到有权限的账号,或联系管理员开通页面权限。" />}
230
+ routeCode="admin.dashboard"
67
231
  >
68
- <div className="min-h-screen bg-[#f6f7f9] text-graphite">
69
- <aside className="fixed inset-y-0 left-0 z-20 hidden w-72 border-r border-slate-200 bg-white/90 px-4 py-5 backdrop-blur lg:block">
70
- <Link to={`/view/${appType}/admin`} className="mb-6 flex items-center gap-3 px-2">
71
- <span className="grid h-9 w-9 place-items-center rounded-lg bg-blue-600 text-sm font-semibold text-white">OX</span>
72
- <span>
73
- <span className="block text-sm font-semibold">OpenXiangda</span>
74
- <span className="block text-xs text-slate-500">React SPA</span>
75
- </span>
76
- </Link>
77
- <nav className="space-y-3">
78
- {groups.map(group => {
79
- const collapsed = collapsedGroups[group.title];
80
- return (
81
- <section key={group.title}>
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 w-full items-center justify-between rounded-md px-2 py-2 text-xs font-medium text-slate-500 hover:bg-slate-50"
84
- onClick={() => setCollapsedGroups(prev => ({ ...prev, [group.title]: !collapsed }))}
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>{group.title}</span>
88
- {collapsed ? <ChevronRight size={14} /> : <ChevronDown size={14} />}
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
- {!collapsed && (
91
- <div className="mt-1 space-y-1">
92
- {group.items.map(item => (
93
- <NavLink
94
- className={({ isActive }) =>
95
- `relative flex items-center gap-2 rounded-md px-3 py-2 text-sm ${
96
- isActive ? "bg-blue-50 text-blue-700 before:absolute before:left-0 before:h-5 before:w-0.5 before:rounded-full before:bg-blue-600" : "text-slate-600 hover:bg-slate-50"
97
- }`
98
- }
99
- key={`${group.title}-${item.name}`}
100
- to={item.path}
101
- >
102
- <item.icon size={16} />
103
- <span className="truncate">{item.name}</span>
104
- </NavLink>
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
- </section>
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
- <div className="p-5">
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 ShellState({ title, description }: { title: string; description: string }) {
139
- return (
140
- <div className="grid min-h-screen place-items-center bg-[#f6f7f9] px-6">
141
- <div className="w-full max-w-md rounded-lg border border-slate-200 bg-white p-6 shadow-sm">
142
- <div className="text-base font-semibold text-slate-950">{title}</div>
143
- <div className="mt-2 text-sm leading-6 text-slate-500">{description}</div>
144
- </div>
145
- </div>
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 { Outlet } from "react-router-dom";
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-[#f6f7f9]">
6
- <main className="mx-auto max-w-4xl px-5 py-10">
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>