openxiangda 1.0.73 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda",
3
- "version": "1.0.73",
3
+ "version": "1.0.74",
4
4
  "description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
5
5
  "private": false,
6
6
  "bin": {
@@ -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 = isMenuActive(location.pathname, location.search, item.path);
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">首页 / 应用</div>
245
- <div className="mt-1 truncate text-lg font-semibold text-slate-950">{appName}</div>
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
- <div className="mx-auto min-w-0 max-w-[1440px] px-4 py-5 sm:px-6">
300
- <Outlet />
301
- </div>
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
+ }
@@ -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 space-y-5">
33
- <PageHeader
34
- actions={
35
- <>
36
- <SecondaryButton>
37
- <Filter size={17} />
38
- 筛选
39
- </SecondaryButton>
40
- <SecondaryButton onClick={() => window.location.reload()}>
41
- <RefreshCw size={17} />
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 space-y-5">
114
- <PageHeader
115
- description={pageCopy.description}
116
- meta={
117
- <>
118
- <StatusPill tone={isProcessForm ? "amber" : "blue"}>{isProcessForm ? "流程办理" : "业务申请"}</StatusPill>
119
- <StatusPill tone="emerald">权限已校验</StatusPill>
120
- </>
121
- }
122
- title={pageCopy.title}
123
- />
124
- <Panel className="min-w-0 overflow-hidden">
125
- {Override ? (
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
+ }