openxiangda 1.0.49 → 1.0.51

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 (47) hide show
  1. package/lib/cli.js +339 -9
  2. package/lib/workspace-init.js +20 -8
  3. package/openxiangda-skills/SKILL.md +4 -3
  4. package/openxiangda-skills/skills/openxiangda-app/SKILL.md +28 -0
  5. package/openxiangda-skills/skills/openxiangda-core/SKILL.md +45 -1
  6. package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +31 -0
  7. package/package.json +7 -1
  8. package/packages/sdk/dist/runtime/index.cjs +3614 -3377
  9. package/packages/sdk/dist/runtime/index.cjs.map +1 -1
  10. package/packages/sdk/dist/runtime/index.d.mts +1 -0
  11. package/packages/sdk/dist/runtime/index.d.ts +1 -0
  12. package/packages/sdk/dist/runtime/index.mjs +3103 -2860
  13. package/packages/sdk/dist/runtime/index.mjs.map +1 -1
  14. package/packages/sdk/dist/runtime/react.cjs +236 -0
  15. package/packages/sdk/dist/runtime/react.cjs.map +1 -0
  16. package/packages/sdk/dist/runtime/react.d.mts +109 -0
  17. package/packages/sdk/dist/runtime/react.d.ts +109 -0
  18. package/packages/sdk/dist/runtime/react.mjs +222 -0
  19. package/packages/sdk/dist/runtime/react.mjs.map +1 -0
  20. package/templates/openxiangda-react-spa/.env.example +4 -0
  21. package/templates/openxiangda-react-spa/AGENTS.md +65 -0
  22. package/templates/openxiangda-react-spa/index.html +12 -0
  23. package/templates/openxiangda-react-spa/package.json +35 -0
  24. package/templates/openxiangda-react-spa/postcss.config.cjs +6 -0
  25. package/templates/openxiangda-react-spa/src/app/router.tsx +97 -0
  26. package/templates/openxiangda-react-spa/src/layouts/AdminShell.tsx +102 -0
  27. package/templates/openxiangda-react-spa/src/layouts/PublicShell.tsx +11 -0
  28. package/templates/openxiangda-react-spa/src/layouts/UserShell.tsx +22 -0
  29. package/templates/openxiangda-react-spa/src/main.tsx +12 -0
  30. package/templates/openxiangda-react-spa/src/pages/admin/RuntimeWorkspacePage.tsx +57 -0
  31. package/templates/openxiangda-react-spa/src/pages/defaults/DataRoutePage.tsx +17 -0
  32. package/templates/openxiangda-react-spa/src/pages/defaults/FilePreviewRoutePage.tsx +14 -0
  33. package/templates/openxiangda-react-spa/src/pages/defaults/FormRoutePage.tsx +62 -0
  34. package/templates/openxiangda-react-spa/src/pages/portal/UserPortalPage.tsx +27 -0
  35. package/templates/openxiangda-react-spa/src/pages/public/PublicHomePage.tsx +10 -0
  36. package/templates/openxiangda-react-spa/src/pages/states/NotFoundPage.tsx +16 -0
  37. package/templates/openxiangda-react-spa/src/resources/menus/menus.json +31 -0
  38. package/templates/openxiangda-react-spa/src/resources/permissions/page-groups/app-admin.json +8 -0
  39. package/templates/openxiangda-react-spa/src/resources/permissions/page-groups/app-user.json +8 -0
  40. package/templates/openxiangda-react-spa/src/resources/roles/roles.json +14 -0
  41. package/templates/openxiangda-react-spa/src/styles/index.css +23 -0
  42. package/templates/openxiangda-react-spa/tailwind.config.cjs +29 -0
  43. package/templates/openxiangda-react-spa/tsconfig.app.json +36 -0
  44. package/templates/openxiangda-react-spa/tsconfig.json +7 -0
  45. package/templates/openxiangda-react-spa/tsconfig.node.json +10 -0
  46. package/templates/openxiangda-react-spa/vite.config.ts +73 -0
  47. package/templates/sy-lowcode-app-workspace/src/dev/App.tsx +33 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/runtime/react/openxiangdaProvider.tsx"],"sourcesContent":["import React, {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useState,\n} from \"react\"\n\nexport interface RuntimeMenuItem {\n id: string\n name: string\n resourceCode?: string | null\n routeCode?: string | null\n path?: string | null\n type?: string\n formUuid?: string | null\n pageId?: string | null\n parentId?: string | null\n sortOrder?: number\n isHidden?: boolean\n icon?: string | null\n children?: RuntimeMenuItem[]\n [key: string]: unknown\n}\n\nexport interface RuntimePagePermissions {\n appType: string\n hasFullAccess: boolean\n roleCodes: string[]\n roleSource?: string\n menuFormUuids: string[]\n menuCodes: string[]\n routeCodes: string[]\n pathPatterns: string[]\n}\n\nexport interface RuntimeBootstrap {\n appType: string\n app?: Record<string, unknown> | null\n user?: Record<string, unknown> | null\n runtime?: {\n mode?: \"legacy\" | \"react-spa\" | string\n settings?: Record<string, unknown>\n activeReleaseId?: string | null\n activeBuildId?: string | null\n indexUrl?: string | null\n assetBaseUrl?: string | null\n }\n permissions?: RuntimePagePermissions\n menus?: RuntimeMenuItem[]\n servicePrefix?: string\n}\n\nexport interface RouteAccessResult {\n appType: string\n canAccess: boolean\n routeCode?: string\n menuCode?: string\n path?: string\n permissions?: RuntimePagePermissions\n}\n\nexport interface RuntimeRequestState<T> {\n data: T | null\n loading: boolean\n error: Error | null\n}\n\nexport interface OpenXiangdaProviderProps {\n appType?: string\n servicePrefix?: string\n fetchImpl?: typeof fetch\n children: React.ReactNode\n}\n\ninterface OpenXiangdaRuntimeStore extends RuntimeRequestState<RuntimeBootstrap> {\n appType: string\n servicePrefix: string\n fetchImpl: typeof fetch\n reload: () => Promise<void>\n}\n\nconst OpenXiangdaRuntimeContext =\n createContext<OpenXiangdaRuntimeStore | null>(null)\n\nexport const OpenXiangdaProvider: React.FC<OpenXiangdaProviderProps> = ({\n appType,\n servicePrefix = \"/service\",\n fetchImpl,\n children,\n}) => {\n const resolvedFetch = fetchImpl || fetch\n const resolvedAppType = useMemo(\n () => appType || resolveAppTypeFromLocation(),\n [appType],\n )\n const [state, setState] = useState<RuntimeRequestState<RuntimeBootstrap>>({\n data: null,\n loading: true,\n error: null,\n })\n\n const reload = useCallback(async () => {\n if (!resolvedAppType) {\n setState({\n data: null,\n loading: false,\n error: new Error(\"appType 不能为空\"),\n })\n return\n }\n setState(prev => ({ ...prev, loading: true, error: null }))\n try {\n const response = await resolvedFetch(\n buildServiceUrl(\n servicePrefix,\n `/openxiangda-api/v1/apps/${encodeURIComponent(\n resolvedAppType,\n )}/runtime/bootstrap`,\n ),\n {\n credentials: \"include\",\n headers: { accept: \"application/json\" },\n },\n )\n const payload = await response.json()\n if (!response.ok || payload?.code >= 400) {\n throw new Error(payload?.message || `Runtime bootstrap failed: ${response.status}`)\n }\n setState({\n data: payload?.data || null,\n loading: false,\n error: null,\n })\n } catch (error) {\n setState({\n data: null,\n loading: false,\n error: error instanceof Error ? error : new Error(String(error)),\n })\n }\n }, [resolvedAppType, resolvedFetch, servicePrefix])\n\n useEffect(() => {\n void reload()\n }, [reload])\n\n const value = useMemo<OpenXiangdaRuntimeStore>(\n () => ({\n ...state,\n appType: resolvedAppType,\n servicePrefix,\n fetchImpl: resolvedFetch,\n reload,\n }),\n [reload, resolvedAppType, resolvedFetch, servicePrefix, state],\n )\n\n return (\n <OpenXiangdaRuntimeContext.Provider value={value}>\n {children}\n </OpenXiangdaRuntimeContext.Provider>\n )\n}\n\nexport const useOpenXiangda = () => {\n const context = useContext(OpenXiangdaRuntimeContext)\n if (!context) {\n throw new Error(\"useOpenXiangda must be used inside OpenXiangdaProvider\")\n }\n return context\n}\n\nexport const useRuntimeBootstrap = () => useOpenXiangda()\n\nexport const useAppMenus = () => {\n const runtime = useOpenXiangda()\n return {\n ...runtime,\n data: runtime.data?.menus || [],\n }\n}\n\nexport const usePermission = () => {\n const runtime = useOpenXiangda()\n return {\n ...runtime,\n data: runtime.data?.permissions || null,\n }\n}\n\nexport interface UseCanAccessRouteInput {\n routeCode?: string\n menuCode?: string\n path?: string\n}\n\nexport const useCanAccessRoute = (input: UseCanAccessRouteInput) => {\n const runtime = useOpenXiangda()\n const [state, setState] = useState<RuntimeRequestState<RouteAccessResult>>({\n data: null,\n loading: true,\n error: null,\n })\n\n useEffect(() => {\n let disposed = false\n const check = async () => {\n const permissions = runtime.data?.permissions\n if (!runtime.appType || runtime.loading) {\n setState(prev => ({ ...prev, loading: runtime.loading }))\n return\n }\n if (permissions?.hasFullAccess) {\n setState({\n data: { appType: runtime.appType, canAccess: true, permissions },\n loading: false,\n error: null,\n })\n return\n }\n setState(prev => ({ ...prev, loading: true, error: null }))\n try {\n const response = await runtime.fetchImpl(\n buildServiceUrl(\n runtime.servicePrefix,\n `/openxiangda-api/v1/apps/${encodeURIComponent(\n runtime.appType,\n )}/runtime/routes/check`,\n ),\n {\n method: \"POST\",\n credentials: \"include\",\n headers: {\n accept: \"application/json\",\n \"content-type\": \"application/json\",\n },\n body: JSON.stringify(input),\n },\n )\n const payload = await response.json()\n if (!disposed) {\n setState({\n data: payload?.data || {\n appType: runtime.appType,\n canAccess: false,\n },\n loading: false,\n error:\n response.ok && payload?.code < 500\n ? null\n : new Error(payload?.message || `Route check failed: ${response.status}`),\n })\n }\n } catch (error) {\n if (!disposed) {\n setState({\n data: null,\n loading: false,\n error: error instanceof Error ? error : new Error(String(error)),\n })\n }\n }\n }\n void check()\n return () => {\n disposed = true\n }\n }, [\n input.menuCode,\n input.path,\n input.routeCode,\n runtime.appType,\n runtime.data?.permissions,\n runtime.fetchImpl,\n runtime.loading,\n runtime.servicePrefix,\n ])\n\n return {\n ...state,\n canAccess: Boolean(state.data?.canAccess),\n }\n}\n\nexport interface PermissionBoundaryProps extends UseCanAccessRouteInput {\n children: React.ReactNode\n fallback?: React.ReactNode\n loadingFallback?: React.ReactNode\n}\n\nexport const PermissionBoundary: React.FC<PermissionBoundaryProps> = ({\n children,\n fallback = null,\n loadingFallback = null,\n routeCode,\n menuCode,\n path,\n}) => {\n const access = useCanAccessRoute({ routeCode, menuCode, path })\n if (access.loading) return <>{loadingFallback}</>\n if (!access.canAccess) return <>{fallback}</>\n return <>{children}</>\n}\n\nconst buildServiceUrl = (servicePrefix: string, path: string) => {\n const prefix = servicePrefix.endsWith(\"/\")\n ? servicePrefix.slice(0, -1)\n : servicePrefix\n const suffix = path.startsWith(\"/\") ? path : `/${path}`\n return `${prefix}${suffix}`\n}\n\nconst resolveAppTypeFromLocation = () => {\n if (typeof window === \"undefined\") return \"\"\n const segments = window.location.pathname.split(\"/\").filter(Boolean)\n const viewIndex = segments[0] === \"view\" ? 1 : 0\n if (segments[viewIndex] === \"submit\") return segments[viewIndex + 1] || \"\"\n if (segments[viewIndex] === \"preview\") return segments[viewIndex + 1] || \"\"\n return segments[viewIndex] || \"\"\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,4 @@
1
+ OPENXIANGDA_BASE_URL=https://your-platform.example.com
2
+ OPENXIANGDA_APP_TYPE=APP_XXXX
3
+ APP_SERVICE_PREFIX=/service
4
+ OPENXIANGDA_DEV_PORT=5174
@@ -0,0 +1,65 @@
1
+ # AGENTS.md — OpenXiangda React SPA 工作区
2
+
3
+ 本工作区是标准 React SPA 应用,不是旧 `sy-lowcode-app-workspace` 单页面上传模式。
4
+
5
+ ## 开发原则
6
+
7
+ - 使用 React Router 管理路由,路由定义在 `src/app/router.tsx`。
8
+ - 使用 Tailwind CSS 表达样式,不依赖平台 theme tokens。
9
+ - 使用 `OpenXiangdaProvider` 包裹应用运行时,平台能力通过 SDK hooks 获取。
10
+ - 不在页面里硬编码角色判断。菜单可见和 route 访问都由后端权限接口返回。
11
+ - 页面、表单、字段、数据范围、流程动作、文件、连接器权限以后端为准。
12
+ - 本地开发通过 Vite `/service` 代理远端平台,保留 HttpOnly Cookie。
13
+
14
+ ## 常用命令
15
+
16
+ ```bash
17
+ pnpm install
18
+ pnpm dev
19
+ pnpm typecheck
20
+ pnpm build
21
+ openxiangda resource plan --profile <name>
22
+ openxiangda resource publish --profile <name>
23
+ openxiangda runtime deploy --profile <name>
24
+ ```
25
+
26
+ `openxiangda runtime deploy` 会自动设置 `OPENXIANGDA_BUILD_ID` 和 Vite asset base,构建 `dist/`,上传 runtime release,并默认激活。不要手工改 `dist/index.html`。
27
+
28
+ 完整发布顺序:
29
+
30
+ ```bash
31
+ openxiangda resource publish --profile <name>
32
+ openxiangda runtime deploy --profile <name>
33
+ ```
34
+
35
+ ## 权限资源
36
+
37
+ React SPA 页面需要声明菜单 code、route code 和 path pattern:
38
+
39
+ ```json
40
+ {
41
+ "code": "app_user_spa_pages",
42
+ "roles": ["app_user"],
43
+ "menuCodes": ["user_portal"],
44
+ "routeCodes": ["portal.home"],
45
+ "pathPatterns": ["/view/:appType/portal/*"]
46
+ }
47
+ ```
48
+
49
+ 页面中使用:
50
+
51
+ ```tsx
52
+ import { PermissionBoundary, useAppMenus, useRuntimeBootstrap } from "openxiangda/runtime/react";
53
+ ```
54
+
55
+ `PermissionBoundary` 只能做展示保护,不能替代后端权限。
56
+
57
+ ## 默认页
58
+
59
+ - 表单提交:`/view/:appType/admin/forms/:formUuid/new`
60
+ - 表单详情:`/view/:appType/admin/forms/:formUuid/:formInstId`
61
+ - 流程详情:`/view/:appType/admin/process/:formUuid/:formInstId`
62
+ - 数据列表:`/view/:appType/admin/data/:formUuid`
63
+ - 文件预览:`/view/file-preview?ticket=...`
64
+
65
+ 默认页可以替换成自定义 React route element;不要回到旧平台 `isRenderNav` 或 workbench 参数模型。
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>OpenXiangda React SPA</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "__WORKSPACE_PACKAGE_NAME__",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "typecheck": "tsc -p tsconfig.app.json --noEmit",
10
+ "check": "pnpm typecheck && pnpm build",
11
+ "resources:plan": "openxiangda resource plan",
12
+ "resources:publish": "openxiangda resource publish",
13
+ "runtime:deploy": "openxiangda runtime deploy",
14
+ "deploy": "openxiangda resource publish && openxiangda runtime deploy",
15
+ "typegen": "openxiangda resource typegen"
16
+ },
17
+ "dependencies": {
18
+ "@vitejs/plugin-react": "^4.3.0",
19
+ "lucide-react": "^0.468.0",
20
+ "openxiangda": "latest",
21
+ "react": "18.3.1",
22
+ "react-dom": "18.3.1",
23
+ "react-router-dom": "^6.30.1"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^22.0.0",
27
+ "@types/react": "^18.3.18",
28
+ "@types/react-dom": "^18.3.5",
29
+ "autoprefixer": "^10.4.21",
30
+ "postcss": "^8.5.0",
31
+ "tailwindcss": "^3.4.17",
32
+ "typescript": "^5.7.0",
33
+ "vite": "^6.0.0"
34
+ }
35
+ }
@@ -0,0 +1,6 @@
1
+ const tailwindcss = require("tailwindcss");
2
+ const autoprefixer = require("autoprefixer");
3
+
4
+ module.exports = {
5
+ plugins: [tailwindcss(), autoprefixer()],
6
+ };
@@ -0,0 +1,97 @@
1
+ import {
2
+ createBrowserRouter,
3
+ Navigate,
4
+ Outlet,
5
+ useParams,
6
+ } from "react-router-dom";
7
+ import { lazy, Suspense, type ReactNode } from "react";
8
+ import { OpenXiangdaProvider } from "openxiangda/runtime/react";
9
+
10
+ import { AdminShell } from "@/layouts/AdminShell";
11
+ import { PublicShell } from "@/layouts/PublicShell";
12
+ import { UserShell } from "@/layouts/UserShell";
13
+ import { RuntimeWorkspacePage } from "@/pages/admin/RuntimeWorkspacePage";
14
+ import { UserPortalPage } from "@/pages/portal/UserPortalPage";
15
+ import { PublicHomePage } from "@/pages/public/PublicHomePage";
16
+ import { NotFoundPage } from "@/pages/states/NotFoundPage";
17
+
18
+ const servicePrefix = process.env.APP_SERVICE_PREFIX || "/service";
19
+ const DataRoutePage = lazy(() =>
20
+ import("@/pages/defaults/DataRoutePage").then(module => ({
21
+ default: module.DataRoutePage,
22
+ })),
23
+ );
24
+ const FilePreviewRoutePage = lazy(() =>
25
+ import("@/pages/defaults/FilePreviewRoutePage").then(module => ({
26
+ default: module.FilePreviewRoutePage,
27
+ })),
28
+ );
29
+ const FormRoutePage = lazy(() =>
30
+ import("@/pages/defaults/FormRoutePage").then(module => ({
31
+ default: module.FormRoutePage,
32
+ })),
33
+ );
34
+
35
+ const routeElement = (element: ReactNode) => (
36
+ <Suspense fallback={<div className="ox-panel p-6 text-sm text-slate-500">加载中</div>}>
37
+ {element}
38
+ </Suspense>
39
+ );
40
+
41
+ function RuntimeRoot() {
42
+ const { appType = process.env.OPENXIANGDA_APP_TYPE || "" } = useParams();
43
+ return (
44
+ <OpenXiangdaProvider appType={appType} servicePrefix={servicePrefix}>
45
+ <Outlet />
46
+ </OpenXiangdaProvider>
47
+ );
48
+ }
49
+
50
+ export const router = createBrowserRouter([
51
+ {
52
+ path: "/view/file-preview",
53
+ element: routeElement(<FilePreviewRoutePage />),
54
+ },
55
+ {
56
+ path: "/view/submit/:appType/:formUuid",
57
+ element: <RuntimeRoot />,
58
+ children: [{ index: true, element: routeElement(<FormRoutePage mode="submit" />) }],
59
+ },
60
+ {
61
+ path: "/view/:appType",
62
+ element: <RuntimeRoot />,
63
+ children: [
64
+ { index: true, element: <Navigate to="admin" replace /> },
65
+ {
66
+ path: "admin",
67
+ element: <AdminShell />,
68
+ children: [
69
+ { index: true, element: <RuntimeWorkspacePage /> },
70
+ { path: "data/:formUuid", element: routeElement(<DataRoutePage />) },
71
+ { path: "forms/:formUuid/new", element: routeElement(<FormRoutePage mode="submit" />) },
72
+ { path: "forms/:formUuid/:formInstId", element: routeElement(<FormRoutePage mode="detail" />) },
73
+ { path: "process/:formUuid/:formInstId", element: routeElement(<FormRoutePage mode="process" />) },
74
+ ],
75
+ },
76
+ {
77
+ path: "portal",
78
+ element: <UserShell />,
79
+ children: [
80
+ { index: true, element: <UserPortalPage /> },
81
+ { path: "forms/:formUuid/new", element: routeElement(<FormRoutePage mode="submit" />) },
82
+ { path: "data/:formUuid", element: routeElement(<DataRoutePage />) },
83
+ ],
84
+ },
85
+ {
86
+ path: "public",
87
+ element: <PublicShell />,
88
+ children: [
89
+ { index: true, element: <PublicHomePage /> },
90
+ { path: "forms/:formUuid/new", element: routeElement(<FormRoutePage mode="submit" />) },
91
+ ],
92
+ },
93
+ { path: "*", element: <NotFoundPage /> },
94
+ ],
95
+ },
96
+ { path: "*", element: <NotFoundPage /> },
97
+ ]);
@@ -0,0 +1,102 @@
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 { useAppMenus, useRuntimeBootstrap } from "openxiangda/runtime/react";
5
+
6
+ const fallbackGroups = [
7
+ {
8
+ title: "应用工作台",
9
+ items: [
10
+ { name: "工作台", path: "admin", icon: LayoutDashboard },
11
+ { name: "用户门户", path: "portal", icon: Home },
12
+ ],
13
+ },
14
+ {
15
+ title: "权限与系统",
16
+ items: [
17
+ { name: "AI 验证", path: "admin?panel=verification", icon: Sparkles },
18
+ { name: "角色权限", path: "admin?panel=permission", icon: Shield },
19
+ ],
20
+ },
21
+ ];
22
+
23
+ export function AdminShell() {
24
+ const { appType = "" } = useParams();
25
+ const bootstrap = useRuntimeBootstrap();
26
+ const menus = useAppMenus();
27
+ const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>({});
28
+ const groups = useMemo(() => {
29
+ if (!menus.data.length) return fallbackGroups;
30
+ return menus.data.map(group => ({
31
+ title: group.name,
32
+ items: (group.children?.length ? group.children : [group]).map(item => ({
33
+ name: item.name,
34
+ path: item.path || "admin",
35
+ icon: LayoutDashboard,
36
+ })),
37
+ }));
38
+ }, [menus.data]);
39
+
40
+ 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>
91
+ </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>
100
+ </div>
101
+ );
102
+ }
@@ -0,0 +1,11 @@
1
+ import { Outlet } from "react-router-dom";
2
+
3
+ export function PublicShell() {
4
+ return (
5
+ <div className="min-h-screen bg-[#f6f7f9]">
6
+ <main className="mx-auto max-w-4xl px-5 py-10">
7
+ <Outlet />
8
+ </main>
9
+ </div>
10
+ );
11
+ }
@@ -0,0 +1,22 @@
1
+ import { Link, Outlet, useParams } from "react-router-dom";
2
+
3
+ export function UserShell() {
4
+ const { appType = "" } = useParams();
5
+ 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>
20
+ </div>
21
+ );
22
+ }
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+ import ReactDOM from "react-dom/client";
3
+ import { RouterProvider } from "react-router-dom";
4
+
5
+ import { router } from "./app/router";
6
+ import "./styles/index.css";
7
+
8
+ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
9
+ <React.StrictMode>
10
+ <RouterProvider router={router} />
11
+ </React.StrictMode>,
12
+ );
@@ -0,0 +1,57 @@
1
+ import { Activity, CheckCircle2, Cookie, ShieldCheck } from "lucide-react";
2
+ import { useRuntimeBootstrap } from "openxiangda/runtime/react";
3
+
4
+ const cards = [
5
+ { label: "SDK 连接", value: "已接入", tone: "text-green-700", icon: CheckCircle2 },
6
+ { label: "Cookie / Proxy", value: "同域代理", tone: "text-blue-700", icon: Cookie },
7
+ { label: "后端权限", value: "后端兜底", tone: "text-amber-700", icon: ShieldCheck },
8
+ { label: "运行状态", value: "React SPA", tone: "text-slate-700", icon: Activity },
9
+ ];
10
+
11
+ export function RuntimeWorkspacePage() {
12
+ const runtime = useRuntimeBootstrap();
13
+ return (
14
+ <div className="space-y-5">
15
+ <section className="ox-panel p-5">
16
+ <div className="flex flex-wrap items-start justify-between gap-4">
17
+ <div>
18
+ <h1 className="text-xl font-semibold text-slate-950">运行时工作台</h1>
19
+ <p className="mt-1 text-sm text-slate-500">
20
+ 应用路由、布局和页面组织由当前 React SPA 接管。
21
+ </p>
22
+ </div>
23
+ <button className="rounded-md bg-blue-600 px-3 py-2 text-sm font-medium text-white hover:bg-blue-700" type="button">
24
+ 发布版本
25
+ </button>
26
+ </div>
27
+ </section>
28
+ <section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
29
+ {cards.map(card => (
30
+ <div className="ox-panel p-4" key={card.label}>
31
+ <div className={`mb-4 inline-flex rounded-md bg-slate-50 p-2 ${card.tone}`}>
32
+ <card.icon size={18} />
33
+ </div>
34
+ <div className="text-sm text-slate-500">{card.label}</div>
35
+ <div className="mt-1 text-lg font-semibold text-slate-950">{card.value}</div>
36
+ </div>
37
+ ))}
38
+ </section>
39
+ <section className="grid gap-4 xl:grid-cols-[1.2fr_0.8fr]">
40
+ <div className="ox-panel p-5">
41
+ <h2 className="text-sm font-semibold text-slate-950">Runtime Bootstrap</h2>
42
+ <pre className="mt-4 max-h-80 overflow-auto rounded-md bg-slate-950 p-4 text-xs leading-6 text-slate-100">
43
+ {JSON.stringify(runtime.data, null, 2)}
44
+ </pre>
45
+ </div>
46
+ <div className="ox-panel p-5">
47
+ <h2 className="text-sm font-semibold text-slate-950">最近活动</h2>
48
+ <div className="mt-4 space-y-3 text-sm text-slate-600">
49
+ <div className="rounded-md border border-slate-200 p-3">加载应用菜单和当前用户权限。</div>
50
+ <div className="rounded-md border border-slate-200 p-3">默认页可直接挂载到 React Router。</div>
51
+ <div className="rounded-md border border-slate-200 p-3">AI 验证登录使用真实用户身份。</div>
52
+ </div>
53
+ </div>
54
+ </section>
55
+ </div>
56
+ );
57
+ }
@@ -0,0 +1,17 @@
1
+ import { useParams } from "react-router-dom";
2
+ import { DataManagementList } from "openxiangda";
3
+
4
+ export function DataRoutePage() {
5
+ const { appType = "", formUuid = "" } = useParams();
6
+ return (
7
+ <div className="ox-panel overflow-hidden">
8
+ <DataManagementList
9
+ appType={appType}
10
+ detailBasePath={`/view/${appType}/admin/forms/${formUuid}`}
11
+ formUuid={formUuid}
12
+ fullHeight={false}
13
+ title="数据列表"
14
+ />
15
+ </div>
16
+ );
17
+ }
@@ -0,0 +1,14 @@
1
+ export function FilePreviewRoutePage() {
2
+ const ticket = new URLSearchParams(window.location.search).get("ticket") || "";
3
+ return (
4
+ <main className="min-h-screen bg-[#f6f7f9] p-5">
5
+ <section className="ox-panel mx-auto max-w-4xl p-6">
6
+ <h1 className="text-lg font-semibold text-slate-950">文件预览</h1>
7
+ <p className="mt-2 text-sm text-slate-500">ticket: {ticket || "未提供"}</p>
8
+ <div className="mt-6 rounded-md border border-dashed border-slate-300 bg-slate-50 p-8 text-center text-sm text-slate-500">
9
+ 文件预览组件将在平台文件元数据接口稳定后接入。
10
+ </div>
11
+ </section>
12
+ </main>
13
+ );
14
+ }
@@ -0,0 +1,62 @@
1
+ import { useEffect, useState } from "react";
2
+ import { useNavigate, useParams } from "react-router-dom";
3
+ import { StandardFormPage } from "openxiangda";
4
+ import { useOpenXiangda } from "openxiangda/runtime/react";
5
+
6
+ type Mode = "submit" | "detail" | "process";
7
+
8
+ export function FormRoutePage({ mode }: { mode: Mode }) {
9
+ const { appType = "", formUuid = "", formInstId = "" } = useParams();
10
+ const navigate = useNavigate();
11
+ const runtime = useOpenXiangda();
12
+ const [schema, setSchema] = useState<any>(null);
13
+ const [error, setError] = useState<string>("");
14
+
15
+ useEffect(() => {
16
+ let disposed = false;
17
+ const load = async () => {
18
+ try {
19
+ const response = await runtime.fetchImpl(
20
+ `${runtime.servicePrefix}/openxiangda-api/v1/apps/${encodeURIComponent(appType)}/forms/${encodeURIComponent(formUuid)}`,
21
+ { credentials: "include", headers: { accept: "application/json" } },
22
+ );
23
+ const payload = await response.json();
24
+ if (!response.ok || payload?.code >= 400) {
25
+ throw new Error(payload?.message || "表单 schema 加载失败");
26
+ }
27
+ if (!disposed) setSchema(payload?.data?.schema || payload?.data || payload);
28
+ } catch (err) {
29
+ if (!disposed) setError(err instanceof Error ? err.message : String(err));
30
+ }
31
+ };
32
+ void load();
33
+ return () => {
34
+ disposed = true;
35
+ };
36
+ }, [appType, formUuid, runtime.fetchImpl, runtime.servicePrefix]);
37
+
38
+ if (error) return <DefaultState title="加载失败" description={error} />;
39
+ if (!schema) return <DefaultState title="加载中" description="正在读取表单配置" />;
40
+
41
+ return (
42
+ <div className="ox-panel p-5">
43
+ <StandardFormPage
44
+ appType={appType}
45
+ formInstanceId={formInstId}
46
+ formUuid={formUuid}
47
+ mode={mode === "process" ? "process" : mode === "detail" ? "detail" : "submit"}
48
+ onSubmitSuccess={id => navigate(`/view/${appType}/admin/forms/${formUuid}/${id}`)}
49
+ schema={schema}
50
+ />
51
+ </div>
52
+ );
53
+ }
54
+
55
+ function DefaultState({ title, description }: { title: string; description: string }) {
56
+ return (
57
+ <div className="ox-panel p-8">
58
+ <div className="text-base font-semibold text-slate-950">{title}</div>
59
+ <div className="mt-2 text-sm text-slate-500">{description}</div>
60
+ </div>
61
+ );
62
+ }
@@ -0,0 +1,27 @@
1
+ import { FileText, Inbox, Search } from "lucide-react";
2
+
3
+ const entries = [
4
+ { title: "发起申请", desc: "打开常用表单和流程", icon: FileText },
5
+ { title: "我的待办", desc: "处理审批与办理任务", icon: Inbox },
6
+ { title: "数据查询", desc: "查看有权限的数据列表", icon: Search },
7
+ ];
8
+
9
+ export function UserPortalPage() {
10
+ return (
11
+ <div className="space-y-5">
12
+ <section className="ox-panel p-5">
13
+ <h1 className="text-xl font-semibold text-slate-950">用户门户</h1>
14
+ <p className="mt-1 text-sm text-slate-500">面向普通用户的入口页,和后台共享同一套权限与 SDK。</p>
15
+ </section>
16
+ <section className="grid gap-4 md:grid-cols-3">
17
+ {entries.map(entry => (
18
+ <div className="ox-panel p-5" key={entry.title}>
19
+ <entry.icon className="text-blue-600" size={20} />
20
+ <div className="mt-4 text-sm font-semibold text-slate-950">{entry.title}</div>
21
+ <div className="mt-1 text-sm text-slate-500">{entry.desc}</div>
22
+ </div>
23
+ ))}
24
+ </section>
25
+ </div>
26
+ );
27
+ }
@@ -0,0 +1,10 @@
1
+ export function PublicHomePage() {
2
+ return (
3
+ <section className="ox-panel p-6">
4
+ <h1 className="text-xl font-semibold text-slate-950">公开访问</h1>
5
+ <p className="mt-2 text-sm text-slate-500">
6
+ 公开表单、公开查询和 ticket 状态页都应在 PublicShell 下实现,不依赖旧平台导航参数。
7
+ </p>
8
+ </section>
9
+ );
10
+ }
@@ -0,0 +1,16 @@
1
+ import { Link } from "react-router-dom";
2
+
3
+ export function NotFoundPage() {
4
+ return (
5
+ <main className="grid min-h-screen place-items-center bg-[#f6f7f9] p-5">
6
+ <section className="ox-panel max-w-md p-8 text-center">
7
+ <div className="text-sm font-semibold text-blue-700">404</div>
8
+ <h1 className="mt-2 text-xl font-semibold text-slate-950">页面不存在</h1>
9
+ <p className="mt-2 text-sm text-slate-500">请检查当前 React Router 路由或平台 routeCode 配置。</p>
10
+ <Link className="mt-5 inline-flex rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white" to="/">
11
+ 返回入口
12
+ </Link>
13
+ </section>
14
+ </main>
15
+ );
16
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "menus": [
3
+ {
4
+ "code": "runtime_workspace",
5
+ "name": "运行时工作台",
6
+ "type": "nav",
7
+ "routeCode": "admin.dashboard",
8
+ "path": "/view/:appType/admin",
9
+ "icon": "LayoutDashboard",
10
+ "sortOrder": 10
11
+ },
12
+ {
13
+ "code": "user_portal",
14
+ "name": "用户门户",
15
+ "type": "nav",
16
+ "routeCode": "portal.home",
17
+ "path": "/view/:appType/portal",
18
+ "icon": "Home",
19
+ "sortOrder": 20
20
+ },
21
+ {
22
+ "code": "public_home",
23
+ "name": "公开访问",
24
+ "type": "nav",
25
+ "routeCode": "public.home",
26
+ "path": "/view/:appType/public",
27
+ "icon": "Globe",
28
+ "sortOrder": 30
29
+ }
30
+ ]
31
+ }