openxiangda 1.0.72 → 1.0.74
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/components/index.cjs +0 -1
- package/packages/sdk/dist/components/index.cjs.map +1 -1
- package/packages/sdk/dist/components/index.mjs +0 -1
- package/packages/sdk/dist/components/index.mjs.map +1 -1
- package/packages/sdk/dist/runtime/index.cjs +0 -1
- package/packages/sdk/dist/runtime/index.cjs.map +1 -1
- package/packages/sdk/dist/runtime/index.mjs +0 -1
- package/packages/sdk/dist/runtime/index.mjs.map +1 -1
- package/packages/sdk/dist/styles/tailwind-preset.cjs +45 -1
- package/packages/sdk/dist/styles/tailwind-preset.cjs.map +1 -1
- package/packages/sdk/dist/styles/tailwind-preset.mjs +45 -1
- package/packages/sdk/dist/styles/tailwind-preset.mjs.map +1 -1
- package/templates/openxiangda-react-spa/src/layouts/AdminShell.tsx +114 -8
- package/templates/openxiangda-react-spa/src/main.tsx +1 -1
- package/templates/openxiangda-react-spa/src/pages/defaults/DataRoutePage.tsx +11 -76
- package/templates/openxiangda-react-spa/src/pages/defaults/FormRoutePage.tsx +32 -28
- package/templates/openxiangda-react-spa/src/styles/index.css +44 -0
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
Shield,
|
|
9
9
|
X,
|
|
10
10
|
} from "lucide-react";
|
|
11
|
-
import { useEffect, useMemo, useState } from "react";
|
|
11
|
+
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
|
12
12
|
import { Link, Outlet, useLocation, useParams } from "react-router-dom";
|
|
13
13
|
import {
|
|
14
14
|
PermissionBoundary,
|
|
@@ -41,6 +41,24 @@ type PlatformMenuLike = {
|
|
|
41
41
|
type?: string | null;
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
+
type AdminPageMeta = {
|
|
45
|
+
breadcrumbs?: string[];
|
|
46
|
+
title?: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type NavigationState = {
|
|
50
|
+
activePath?: string;
|
|
51
|
+
breadcrumbs: string[];
|
|
52
|
+
title: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const AdminPageMetaContext = createContext<((meta: AdminPageMeta | null) => void) | null>(null);
|
|
56
|
+
const noopSetAdminPageMeta = () => undefined;
|
|
57
|
+
|
|
58
|
+
export function useAdminPageMetaController() {
|
|
59
|
+
return useContext(AdminPageMetaContext) ?? noopSetAdminPageMeta;
|
|
60
|
+
}
|
|
61
|
+
|
|
44
62
|
export function AdminShell() {
|
|
45
63
|
const { appType = "" } = useParams();
|
|
46
64
|
const location = useLocation();
|
|
@@ -52,8 +70,8 @@ export function AdminShell() {
|
|
|
52
70
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
53
71
|
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
|
54
72
|
const [loggingOut, setLoggingOut] = useState(false);
|
|
73
|
+
const [pageMeta, setPageMeta] = useState<AdminPageMeta | null>(null);
|
|
55
74
|
|
|
56
|
-
const appName = String(bootstrap.data?.app?.name || starterBrand.fallbackName);
|
|
57
75
|
const userName = String(
|
|
58
76
|
bootstrap.data?.user?.name ||
|
|
59
77
|
bootstrap.data?.user?.nickName ||
|
|
@@ -85,6 +103,15 @@ export function AdminShell() {
|
|
|
85
103
|
),
|
|
86
104
|
[appType, menuCodes],
|
|
87
105
|
);
|
|
106
|
+
const navigationState = useMemo(
|
|
107
|
+
() => resolveNavigationState(groups, location.pathname, location.search, appType),
|
|
108
|
+
[appType, groups, location.pathname, location.search],
|
|
109
|
+
);
|
|
110
|
+
const headerTitle = pageMeta?.title || navigationState.title || starterBrand.fallbackName;
|
|
111
|
+
const breadcrumbs =
|
|
112
|
+
pageMeta?.breadcrumbs && pageMeta.breadcrumbs.length > 0
|
|
113
|
+
? pageMeta.breadcrumbs
|
|
114
|
+
: navigationState.breadcrumbs;
|
|
88
115
|
|
|
89
116
|
const handleLogout = async () => {
|
|
90
117
|
setLoggingOut(true);
|
|
@@ -135,7 +162,7 @@ export function AdminShell() {
|
|
|
135
162
|
{!collapsed ? (
|
|
136
163
|
<div className={cn("mt-1 space-y-1", compact && "mt-2")}>
|
|
137
164
|
{group.items.map(item => {
|
|
138
|
-
const active =
|
|
165
|
+
const active = navigationState.activePath === item.path;
|
|
139
166
|
return (
|
|
140
167
|
<Link
|
|
141
168
|
aria-label={item.name}
|
|
@@ -241,8 +268,10 @@ export function AdminShell() {
|
|
|
241
268
|
<Menu size={20} />
|
|
242
269
|
</button>
|
|
243
270
|
<div className="min-w-0">
|
|
244
|
-
<div className="truncate text-xs font-medium text-slate-500"
|
|
245
|
-
|
|
271
|
+
<div className="truncate text-xs font-medium text-slate-500">
|
|
272
|
+
{breadcrumbs.join(" / ")}
|
|
273
|
+
</div>
|
|
274
|
+
<div className="mt-1 truncate text-lg font-semibold text-slate-950">{headerTitle}</div>
|
|
246
275
|
</div>
|
|
247
276
|
</div>
|
|
248
277
|
|
|
@@ -296,9 +325,11 @@ export function AdminShell() {
|
|
|
296
325
|
</div>
|
|
297
326
|
</header>
|
|
298
327
|
|
|
299
|
-
<
|
|
300
|
-
<
|
|
301
|
-
|
|
328
|
+
<AdminPageMetaContext.Provider value={setPageMeta}>
|
|
329
|
+
<div className="ox-admin-content mx-auto min-w-0 max-w-[1440px] px-4 py-5 sm:px-6">
|
|
330
|
+
<Outlet />
|
|
331
|
+
</div>
|
|
332
|
+
</AdminPageMetaContext.Provider>
|
|
302
333
|
</main>
|
|
303
334
|
</div>
|
|
304
335
|
</PermissionBoundary>
|
|
@@ -439,3 +470,78 @@ function isMenuActive(pathname: string, search: string, target: string) {
|
|
|
439
470
|
if (target.includes("?")) return `${pathname}${search}` === target;
|
|
440
471
|
return normalize(pathname) === normalize(target);
|
|
441
472
|
}
|
|
473
|
+
|
|
474
|
+
function resolveNavigationState(
|
|
475
|
+
groups: StarterNavigationGroup[],
|
|
476
|
+
pathname: string,
|
|
477
|
+
search: string,
|
|
478
|
+
appType: string,
|
|
479
|
+
): NavigationState {
|
|
480
|
+
const fallback: NavigationState = {
|
|
481
|
+
breadcrumbs: ["首页"],
|
|
482
|
+
title: starterBrand.fallbackName,
|
|
483
|
+
};
|
|
484
|
+
const entries = groups.flatMap(group => group.items.map(item => ({ group, item })));
|
|
485
|
+
const exact = entries.find(({ item }) => isMenuActive(pathname, search, item.path));
|
|
486
|
+
if (exact) {
|
|
487
|
+
return {
|
|
488
|
+
activePath: exact.item.path,
|
|
489
|
+
breadcrumbs: ["首页", exact.group.title, exact.item.name],
|
|
490
|
+
title: exact.item.name,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const route = parseAdminRoute(pathname, appType);
|
|
495
|
+
if (!route) return fallback;
|
|
496
|
+
|
|
497
|
+
const related = findRelatedNavigationEntry(entries, route);
|
|
498
|
+
if (related) {
|
|
499
|
+
const title =
|
|
500
|
+
route.kind === "form-detail" || route.kind === "process-detail"
|
|
501
|
+
? `${related.item.name}详情`
|
|
502
|
+
: related.item.name;
|
|
503
|
+
return {
|
|
504
|
+
activePath: related.item.path,
|
|
505
|
+
breadcrumbs: ["首页", related.group.title, related.item.name],
|
|
506
|
+
title,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (route.kind === "data") {
|
|
511
|
+
return { breadcrumbs: ["首页", "数据管理"], title: "业务数据" };
|
|
512
|
+
}
|
|
513
|
+
if (route.kind === "form-new") {
|
|
514
|
+
return { breadcrumbs: ["首页", "业务办理"], title: "发起申请" };
|
|
515
|
+
}
|
|
516
|
+
if (route.kind === "form-detail") {
|
|
517
|
+
return { breadcrumbs: ["首页", "业务记录"], title: "记录详情" };
|
|
518
|
+
}
|
|
519
|
+
return { breadcrumbs: ["首页", "流程办理"], title: "流程详情" };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function parseAdminRoute(pathname: string, appType: string) {
|
|
523
|
+
const escapedAppType = appType.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
524
|
+
const prefix = new RegExp(`^/view/${escapedAppType}/admin/`);
|
|
525
|
+
if (!prefix.test(pathname)) return null;
|
|
526
|
+
const rest = pathname.replace(prefix, "");
|
|
527
|
+
const [kind, formUuid, tail] = rest.split("/");
|
|
528
|
+
if (kind === "data" && formUuid) return { formUuid, kind: "data" as const };
|
|
529
|
+
if (kind === "forms" && formUuid && tail === "new") return { formUuid, kind: "form-new" as const };
|
|
530
|
+
if (kind === "forms" && formUuid) return { formUuid, kind: "form-detail" as const };
|
|
531
|
+
if (kind === "process" && formUuid) return { formUuid, kind: "process-detail" as const };
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function findRelatedNavigationEntry(
|
|
536
|
+
entries: Array<{ group: StarterNavigationGroup; item: StarterNavigationGroup["items"][number] }>,
|
|
537
|
+
route: NonNullable<ReturnType<typeof parseAdminRoute>>,
|
|
538
|
+
) {
|
|
539
|
+
const formPathNeedle = `/forms/${route.formUuid}/new`;
|
|
540
|
+
const dataPathNeedle = `/data/${route.formUuid}`;
|
|
541
|
+
if (route.kind === "data") return entries.find(({ item }) => item.path.includes(dataPathNeedle));
|
|
542
|
+
if (route.kind === "form-new") return entries.find(({ item }) => item.path.includes(formPathNeedle));
|
|
543
|
+
return (
|
|
544
|
+
entries.find(({ item }) => item.path.includes(dataPathNeedle)) ||
|
|
545
|
+
entries.find(({ item }) => item.path.includes(formPathNeedle))
|
|
546
|
+
);
|
|
547
|
+
}
|
|
@@ -3,7 +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/
|
|
6
|
+
import "antd-mobile/bundle/style.css";
|
|
7
7
|
import "./styles/index.css";
|
|
8
8
|
|
|
9
9
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
|
@@ -1,15 +1,6 @@
|
|
|
1
|
-
import { Database, FilePlus2, Filter, RefreshCw, Rows3 } from "lucide-react";
|
|
2
|
-
import type { ReactNode } from "react";
|
|
3
1
|
import { useParams } from "react-router-dom";
|
|
4
2
|
import { DataManagementList } from "openxiangda";
|
|
5
3
|
|
|
6
|
-
import {
|
|
7
|
-
PageHeader,
|
|
8
|
-
Panel,
|
|
9
|
-
PrimaryButton,
|
|
10
|
-
SecondaryButton,
|
|
11
|
-
StatusPill,
|
|
12
|
-
} from "@/shared/ui";
|
|
13
4
|
import {
|
|
14
5
|
defaultPageOverrides,
|
|
15
6
|
resolveDefaultPageOverride,
|
|
@@ -29,73 +20,17 @@ export function DataRoutePage() {
|
|
|
29
20
|
);
|
|
30
21
|
|
|
31
22
|
return (
|
|
32
|
-
<div className="min-w-0
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
</SecondaryButton>
|
|
44
|
-
<PrimaryButton onClick={() => window.location.assign(`/view/${appType}/admin/forms/${formUuid}/new`)}>
|
|
45
|
-
<FilePlus2 size={17} />
|
|
46
|
-
新增数据
|
|
47
|
-
</PrimaryButton>
|
|
48
|
-
</>
|
|
49
|
-
}
|
|
50
|
-
description="查看、筛选和维护当前账号有权限访问的业务数据。"
|
|
51
|
-
meta={
|
|
52
|
-
<>
|
|
53
|
-
<StatusPill tone="blue">业务台账</StatusPill>
|
|
54
|
-
<StatusPill tone="emerald">权限数据</StatusPill>
|
|
55
|
-
<StatusPill tone="amber">详情联动</StatusPill>
|
|
56
|
-
</>
|
|
57
|
-
}
|
|
58
|
-
title="业务数据"
|
|
59
|
-
/>
|
|
60
|
-
<section className="grid gap-4 md:grid-cols-3">
|
|
61
|
-
<DataSummary icon={<Database size={18} />} label="当前表单" value={formUuid || "--"} />
|
|
62
|
-
<DataSummary icon={<Rows3 size={18} />} label="列表状态" value="可查询" />
|
|
63
|
-
<DataSummary icon={<Filter size={18} />} label="数据范围" value="按权限展示" />
|
|
64
|
-
</section>
|
|
65
|
-
<Panel className="min-w-0 overflow-hidden p-0">
|
|
66
|
-
<div className="min-w-0 overflow-hidden">
|
|
67
|
-
{Override ? (
|
|
68
|
-
<Override
|
|
69
|
-
appType={appType}
|
|
70
|
-
defaultNode={defaultNode}
|
|
71
|
-
formUuid={formUuid}
|
|
72
|
-
kind="data-manage-list"
|
|
73
|
-
/>
|
|
74
|
-
) : (
|
|
75
|
-
defaultNode
|
|
76
|
-
)}
|
|
77
|
-
</div>
|
|
78
|
-
</Panel>
|
|
79
|
-
</div>
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function DataSummary({
|
|
84
|
-
icon,
|
|
85
|
-
label,
|
|
86
|
-
value,
|
|
87
|
-
}: {
|
|
88
|
-
icon: ReactNode;
|
|
89
|
-
label: string;
|
|
90
|
-
value: string;
|
|
91
|
-
}) {
|
|
92
|
-
return (
|
|
93
|
-
<div className="rounded-2xl border border-white/70 bg-white/[0.82] p-4 shadow-[0_16px_45px_rgba(15,23,42,0.05)]">
|
|
94
|
-
<div className="flex items-center gap-2 text-sm text-slate-500">
|
|
95
|
-
<span className="text-blue-600">{icon}</span>
|
|
96
|
-
{label}
|
|
97
|
-
</div>
|
|
98
|
-
<div className="mt-2 truncate text-base font-semibold text-slate-950">{value}</div>
|
|
23
|
+
<div className="ox-default-data-route min-w-0 overflow-hidden">
|
|
24
|
+
{Override ? (
|
|
25
|
+
<Override
|
|
26
|
+
appType={appType}
|
|
27
|
+
defaultNode={defaultNode}
|
|
28
|
+
formUuid={formUuid}
|
|
29
|
+
kind="data-manage-list"
|
|
30
|
+
/>
|
|
31
|
+
) : (
|
|
32
|
+
defaultNode
|
|
33
|
+
)}
|
|
99
34
|
</div>
|
|
100
35
|
);
|
|
101
36
|
}
|
|
@@ -5,12 +5,10 @@ import { StandardFormPage } from "openxiangda";
|
|
|
5
5
|
import { normalizeRuntimeFormSchema } from "openxiangda/runtime";
|
|
6
6
|
import { useOpenXiangda, useRuntimeAuth } from "openxiangda/runtime/react";
|
|
7
7
|
|
|
8
|
+
import { useAdminPageMetaController } from "@/layouts/AdminShell";
|
|
8
9
|
import {
|
|
9
|
-
PageHeader,
|
|
10
|
-
Panel,
|
|
11
10
|
PrimaryButton,
|
|
12
11
|
StatePage,
|
|
13
|
-
StatusPill,
|
|
14
12
|
} from "@/shared/ui";
|
|
15
13
|
import {
|
|
16
14
|
defaultPageOverrides,
|
|
@@ -30,6 +28,7 @@ export function FormRoutePage({ mode }: { mode: Mode }) {
|
|
|
30
28
|
const { appType = "", formUuid = "", formInstId = "" } = useParams();
|
|
31
29
|
const navigate = useNavigate();
|
|
32
30
|
const runtime = useOpenXiangda();
|
|
31
|
+
const setAdminPageMeta = useAdminPageMetaController();
|
|
33
32
|
const [schema, setSchema] = useState<any>(null);
|
|
34
33
|
const [error, setError] = useState<PageError | null>(null);
|
|
35
34
|
|
|
@@ -87,6 +86,16 @@ export function FormRoutePage({ mode }: { mode: Mode }) {
|
|
|
87
86
|
? "process-submit"
|
|
88
87
|
: "form-submit";
|
|
89
88
|
const pageCopy = useMemo(() => resolvePageCopy(overrideKind, mode), [mode, overrideKind]);
|
|
89
|
+
const pageTitle = useMemo(
|
|
90
|
+
() => resolvePageTitle(schema, pageCopy.title, overrideKind),
|
|
91
|
+
[overrideKind, pageCopy.title, schema],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!schema) return undefined;
|
|
96
|
+
setAdminPageMeta({ title: pageTitle });
|
|
97
|
+
return () => setAdminPageMeta(null);
|
|
98
|
+
}, [pageTitle, schema, setAdminPageMeta]);
|
|
90
99
|
|
|
91
100
|
if (error) return <DefaultErrorState error={error} />;
|
|
92
101
|
if (!schema) return <DefaultLoadingState description="正在读取页面配置和当前用户权限。" title="正在加载" />;
|
|
@@ -110,31 +119,19 @@ export function FormRoutePage({ mode }: { mode: Mode }) {
|
|
|
110
119
|
);
|
|
111
120
|
|
|
112
121
|
return (
|
|
113
|
-
<div className="min-w-0
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
<Override
|
|
127
|
-
appType={appType}
|
|
128
|
-
defaultNode={defaultNode}
|
|
129
|
-
formInstId={formInstId}
|
|
130
|
-
formUuid={formUuid}
|
|
131
|
-
kind={overrideKind}
|
|
132
|
-
schema={schema}
|
|
133
|
-
/>
|
|
134
|
-
) : (
|
|
135
|
-
defaultNode
|
|
136
|
-
)}
|
|
137
|
-
</Panel>
|
|
122
|
+
<div className="ox-default-form-route min-w-0">
|
|
123
|
+
{Override ? (
|
|
124
|
+
<Override
|
|
125
|
+
appType={appType}
|
|
126
|
+
defaultNode={defaultNode}
|
|
127
|
+
formInstId={formInstId}
|
|
128
|
+
formUuid={formUuid}
|
|
129
|
+
kind={overrideKind}
|
|
130
|
+
schema={schema}
|
|
131
|
+
/>
|
|
132
|
+
) : (
|
|
133
|
+
defaultNode
|
|
134
|
+
)}
|
|
138
135
|
</div>
|
|
139
136
|
);
|
|
140
137
|
}
|
|
@@ -206,6 +203,13 @@ function resolvePageCopy(kind: DefaultPageKind, mode: Mode) {
|
|
|
206
203
|
};
|
|
207
204
|
}
|
|
208
205
|
|
|
206
|
+
function resolvePageTitle(schema: any, fallback: string, kind: DefaultPageKind) {
|
|
207
|
+
const title = String(schema?.formMeta?.title || schema?.title || schema?.template?.title || "").trim();
|
|
208
|
+
if (!title) return fallback;
|
|
209
|
+
if (kind === "form-detail" || kind === "process-detail") return `${title}详情`;
|
|
210
|
+
return title;
|
|
211
|
+
}
|
|
212
|
+
|
|
209
213
|
function createPageError(status: number | undefined, code: number | string | undefined, message: string): PageError {
|
|
210
214
|
const normalizedCode = typeof code === "string" ? Number(code) : code;
|
|
211
215
|
if (status === 401 || normalizedCode === 401) return { message, status, type: "unauthenticated" };
|
|
@@ -59,3 +59,47 @@ a {
|
|
|
59
59
|
.ox-scrollbar::-webkit-scrollbar-track {
|
|
60
60
|
background: transparent;
|
|
61
61
|
}
|
|
62
|
+
|
|
63
|
+
.ox-admin-content :where(.sy-runtime-page) {
|
|
64
|
+
min-height: auto;
|
|
65
|
+
background: transparent;
|
|
66
|
+
color: inherit;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.ox-admin-content :where(.sy-runtime-page__content) {
|
|
70
|
+
max-width: 100% !important;
|
|
71
|
+
padding: 0 !important;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.ox-admin-content :where(.sy-submit-page) {
|
|
75
|
+
gap: 0;
|
|
76
|
+
min-height: auto;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.ox-admin-content :where(.sy-submit-card, .sy-detail-card) {
|
|
80
|
+
border: 1px solid rgb(226 232 240 / 0.86);
|
|
81
|
+
border-radius: 20px;
|
|
82
|
+
background: rgb(255 255 255 / 0.9);
|
|
83
|
+
box-shadow: 0 18px 55px rgb(15 23 42 / 0.06);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.ox-admin-content :where(.sy-submit-card) {
|
|
87
|
+
min-height: auto;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.ox-admin-content :where(.sy-detail-main) {
|
|
91
|
+
max-width: 100%;
|
|
92
|
+
padding: 0 0 32px;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.ox-admin-content :where(.sy-detail-header) {
|
|
96
|
+
display: none;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.ox-admin-content :where(.sy-detail-page) {
|
|
100
|
+
background: transparent;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.ox-default-data-route :where(.sy-data-management-list, .sy-runtime-page) {
|
|
104
|
+
min-width: 0;
|
|
105
|
+
}
|