openxiangda 1.0.57 → 1.0.59

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.
@@ -1,7 +1,7 @@
1
1
  import { ChevronDown, ChevronRight, Home, LayoutDashboard, Shield, Sparkles } from "lucide-react";
2
2
  import { useMemo, useState } from "react";
3
3
  import { Link, NavLink, Outlet, useParams } from "react-router-dom";
4
- import { useAppMenus, useRuntimeBootstrap } from "openxiangda/runtime/react";
4
+ import { PermissionBoundary, useAppMenus, useRuntimeBootstrap } from "openxiangda/runtime/react";
5
5
 
6
6
  const fallbackGroups = [
7
7
  {
@@ -26,77 +26,122 @@ export function AdminShell() {
26
26
  const menus = useAppMenus();
27
27
  const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>({});
28
28
  const groups = useMemo(() => {
29
- if (!menus.data.length) return fallbackGroups;
29
+ if (!menus.data.length) {
30
+ return fallbackGroups.map(group => ({
31
+ ...group,
32
+ items: group.items.map(item => ({
33
+ ...item,
34
+ path: resolveMenuPath(appType, item.path),
35
+ })),
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
+ }
30
50
  return menus.data.map(group => ({
31
51
  title: group.name,
32
52
  items: (group.children?.length ? group.children : [group]).map(item => ({
33
53
  name: item.name,
34
- path: item.path || "admin",
54
+ path: resolveMenuPath(appType, item.path || "admin"),
35
55
  icon: LayoutDashboard,
36
56
  })),
37
57
  }));
38
- }, [menus.data]);
58
+ }, [appType, menus.data]);
39
59
 
40
60
  return (
41
- <div className="min-h-screen bg-[#f6f7f9] text-graphite">
42
- <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">
43
- <Link to={`/view/${appType}/admin`} className="mb-6 flex items-center gap-3 px-2">
44
- <span className="grid h-9 w-9 place-items-center rounded-lg bg-blue-600 text-sm font-semibold text-white">OX</span>
45
- <span>
46
- <span className="block text-sm font-semibold">OpenXiangda</span>
47
- <span className="block text-xs text-slate-500">React SPA</span>
48
- </span>
49
- </Link>
50
- <nav className="space-y-3">
51
- {groups.map(group => {
52
- const collapsed = collapsedGroups[group.title];
53
- return (
54
- <section key={group.title}>
55
- <button
56
- 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"
57
- onClick={() => setCollapsedGroups(prev => ({ ...prev, [group.title]: !collapsed }))}
58
- type="button"
59
- >
60
- <span>{group.title}</span>
61
- {collapsed ? <ChevronRight size={14} /> : <ChevronDown size={14} />}
62
- </button>
63
- {!collapsed && (
64
- <div className="mt-1 space-y-1">
65
- {group.items.map(item => (
66
- <NavLink
67
- className={({ isActive }) =>
68
- `relative flex items-center gap-2 rounded-md px-3 py-2 text-sm ${
69
- 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"
70
- }`
71
- }
72
- key={`${group.title}-${item.name}`}
73
- to={`/view/${appType}/${item.path}`}
74
- >
75
- <item.icon size={16} />
76
- <span className="truncate">{item.name}</span>
77
- </NavLink>
78
- ))}
79
- </div>
80
- )}
81
- </section>
82
- );
83
- })}
84
- </nav>
85
- </aside>
86
- <main className="lg:pl-72">
87
- <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">
88
- <div>
89
- <div className="text-sm font-semibold">{String(bootstrap.data?.app?.name || "应用工作台")}</div>
90
- <div className="text-xs text-slate-500">runtime: {bootstrap.data?.runtime?.mode || "loading"}</div>
61
+ <PermissionBoundary
62
+ routeCode="admin.dashboard"
63
+ menuCode="runtime_workspace"
64
+ path={`/view/${appType}/admin`}
65
+ loadingFallback={<ShellState title="正在校验权限" description="请稍候" />}
66
+ fallback={<ShellState title="无权访问管理后台" description="请切换到有权限的账号,或联系管理员开通页面权限。" />}
67
+ >
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}>
82
+ <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 }))}
85
+ type="button"
86
+ >
87
+ <span>{group.title}</span>
88
+ {collapsed ? <ChevronRight size={14} /> : <ChevronDown size={14} />}
89
+ </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
+ ))}
106
+ </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 || "未登录")}
121
+ </div>
122
+ </header>
123
+ <div className="p-5">
124
+ <Outlet />
91
125
  </div>
92
- <div className="rounded-full border border-slate-200 bg-white px-3 py-1 text-xs text-slate-600">
93
- {String(bootstrap.data?.user?.name || bootstrap.data?.user?.id || "未登录")}
94
- </div>
95
- </header>
96
- <div className="p-5">
97
- <Outlet />
98
- </div>
99
- </main>
126
+ </main>
127
+ </div>
128
+ </PermissionBoundary>
129
+ );
130
+ }
131
+
132
+ function resolveMenuPath(appType: string, rawPath: string) {
133
+ const path = rawPath.replace(/:appType/g, appType).replace(/\/{2,}/g, "/");
134
+ if (path.startsWith("/")) return path;
135
+ return `/view/${appType}/${path}`.replace(/\/{2,}/g, "/");
136
+ }
137
+
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>
100
145
  </div>
101
146
  );
102
147
  }
@@ -1,22 +1,50 @@
1
1
  import { Link, Outlet, useParams } from "react-router-dom";
2
+ import { PermissionBoundary, useCanAccessRoute } from "openxiangda/runtime/react";
2
3
 
3
4
  export function UserShell() {
4
5
  const { appType = "" } = useParams();
6
+ const adminAccess = useCanAccessRoute({
7
+ routeCode: "admin.dashboard",
8
+ menuCode: "runtime_workspace",
9
+ path: `/view/${appType}/admin`,
10
+ });
11
+
12
+ return (
13
+ <PermissionBoundary
14
+ routeCode="portal.home"
15
+ menuCode="user_portal"
16
+ path={`/view/${appType}/portal`}
17
+ loadingFallback={<ShellState title="正在校验权限" description="请稍候" />}
18
+ fallback={<ShellState title="无权访问用户门户" description="请切换到有权限的账号,或联系管理员开通页面权限。" />}
19
+ >
20
+ <div className="min-h-screen bg-[#f6f7f9]">
21
+ <header className="border-b border-slate-200 bg-white">
22
+ <div className="mx-auto flex max-w-6xl items-center justify-between px-5 py-4">
23
+ <Link className="text-sm font-semibold text-slate-900" to={`/view/${appType}/portal`}>
24
+ OpenXiangda Portal
25
+ </Link>
26
+ {adminAccess.canAccess ? (
27
+ <Link className="text-sm text-blue-700" to={`/view/${appType}/admin`}>
28
+ 管理后台
29
+ </Link>
30
+ ) : null}
31
+ </div>
32
+ </header>
33
+ <main className="mx-auto max-w-6xl px-5 py-6">
34
+ <Outlet />
35
+ </main>
36
+ </div>
37
+ </PermissionBoundary>
38
+ );
39
+ }
40
+
41
+ function ShellState({ title, description }: { title: string; description: string }) {
5
42
  return (
6
- <div className="min-h-screen bg-[#f6f7f9]">
7
- <header className="border-b border-slate-200 bg-white">
8
- <div className="mx-auto flex max-w-6xl items-center justify-between px-5 py-4">
9
- <Link className="text-sm font-semibold text-slate-900" to={`/view/${appType}/portal`}>
10
- OpenXiangda Portal
11
- </Link>
12
- <Link className="text-sm text-blue-700" to={`/view/${appType}/admin`}>
13
- 管理后台
14
- </Link>
15
- </div>
16
- </header>
17
- <main className="mx-auto max-w-6xl px-5 py-6">
18
- <Outlet />
19
- </main>
43
+ <div className="grid min-h-screen place-items-center bg-[#f6f7f9] px-6">
44
+ <div className="w-full max-w-md rounded-lg border border-slate-200 bg-white p-6 shadow-sm">
45
+ <div className="text-base font-semibold text-slate-950">{title}</div>
46
+ <div className="mt-2 text-sm leading-6 text-slate-500">{description}</div>
47
+ </div>
20
48
  </div>
21
49
  );
22
50
  }
@@ -3,6 +3,7 @@ import ReactDOM from "react-dom/client";
3
3
  import { RouterProvider } from "react-router-dom";
4
4
 
5
5
  import { router } from "./app/router";
6
+ import "antd-mobile/es/global";
6
7
  import "./styles/index.css";
7
8
 
8
9
  ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
@@ -1,6 +1,7 @@
1
1
  import { useEffect, useState } from "react";
2
2
  import { useNavigate, useParams } from "react-router-dom";
3
3
  import { StandardFormPage } from "openxiangda";
4
+ import { normalizeRuntimeFormSchema } from "openxiangda/runtime";
4
5
  import { useOpenXiangda } from "openxiangda/runtime/react";
5
6
 
6
7
  type Mode = "submit" | "detail" | "process";
@@ -24,7 +25,11 @@ export function FormRoutePage({ mode }: { mode: Mode }) {
24
25
  if (!response.ok || payload?.code >= 400) {
25
26
  throw new Error(payload?.message || "表单 schema 加载失败");
26
27
  }
27
- if (!disposed) setSchema(payload?.data?.schema || payload?.data || payload);
28
+ const normalizedSchema = normalizeRuntimeFormSchema(payload, { appType, formUuid });
29
+ if (!normalizedSchema) {
30
+ throw new Error("表单 schema 格式暂不支持或没有可渲染字段");
31
+ }
32
+ if (!disposed) setSchema(normalizedSchema);
28
33
  } catch (err) {
29
34
  if (!disposed) setError(err instanceof Error ? err.message : String(err));
30
35
  }
@@ -38,14 +43,24 @@ export function FormRoutePage({ mode }: { mode: Mode }) {
38
43
  if (error) return <DefaultState title="加载失败" description={error} />;
39
44
  if (!schema) return <DefaultState title="加载中" description="正在读取表单配置" />;
40
45
 
46
+ const formType = String(schema?.template?.formType || schema?.formMeta?.formType || "").toLowerCase();
47
+ const isProcessForm = mode === "process" || formType === "process" || formType === "flow";
48
+ const pageMode = isProcessForm ? "process" : mode === "detail" ? "detail" : "submit";
49
+
41
50
  return (
42
51
  <div className="ox-panel p-5">
43
52
  <StandardFormPage
44
53
  appType={appType}
45
54
  formInstanceId={formInstId}
46
55
  formUuid={formUuid}
47
- mode={mode === "process" ? "process" : mode === "detail" ? "detail" : "submit"}
48
- onSubmitSuccess={id => navigate(`/view/${appType}/admin/forms/${formUuid}/${id}`)}
56
+ mode={pageMode}
57
+ onSubmitSuccess={id =>
58
+ navigate(
59
+ isProcessForm
60
+ ? `/view/${appType}/admin/process/${formUuid}/${id}`
61
+ : `/view/${appType}/admin/forms/${formUuid}/${id}`,
62
+ )
63
+ }
49
64
  schema={schema}
50
65
  />
51
66
  </div>