openxiangda 1.0.50 → 1.0.52
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/lib/cli.js +339 -9
- package/lib/workspace-init.js +20 -8
- package/openxiangda-skills/SKILL.md +4 -3
- package/openxiangda-skills/skills/openxiangda-app/SKILL.md +28 -0
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +45 -1
- package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +31 -0
- package/package.json +7 -1
- package/packages/sdk/dist/runtime/index.cjs +3590 -3376
- package/packages/sdk/dist/runtime/index.cjs.map +1 -1
- package/packages/sdk/dist/runtime/index.d.mts +1 -0
- package/packages/sdk/dist/runtime/index.d.ts +1 -0
- package/packages/sdk/dist/runtime/index.mjs +3079 -2859
- package/packages/sdk/dist/runtime/index.mjs.map +1 -1
- package/packages/sdk/dist/runtime/react.cjs +236 -0
- package/packages/sdk/dist/runtime/react.cjs.map +1 -0
- package/packages/sdk/dist/runtime/react.d.mts +109 -0
- package/packages/sdk/dist/runtime/react.d.ts +109 -0
- package/packages/sdk/dist/runtime/react.mjs +222 -0
- package/packages/sdk/dist/runtime/react.mjs.map +1 -0
- package/packages/sdk/src/build-source/scripts/sync-schema.mjs +31 -0
- package/templates/openxiangda-react-spa/.env.example +4 -0
- package/templates/openxiangda-react-spa/AGENTS.md +69 -0
- package/templates/openxiangda-react-spa/app-workspace.config.ts +32 -0
- package/templates/openxiangda-react-spa/index.html +12 -0
- package/templates/openxiangda-react-spa/package.json +39 -0
- package/templates/openxiangda-react-spa/postcss.config.cjs +6 -0
- package/templates/openxiangda-react-spa/src/app/router.tsx +97 -0
- package/templates/openxiangda-react-spa/src/forms/.gitkeep +1 -0
- package/templates/openxiangda-react-spa/src/layouts/AdminShell.tsx +102 -0
- package/templates/openxiangda-react-spa/src/layouts/PublicShell.tsx +11 -0
- package/templates/openxiangda-react-spa/src/layouts/UserShell.tsx +22 -0
- package/templates/openxiangda-react-spa/src/main.tsx +12 -0
- package/templates/openxiangda-react-spa/src/pages/admin/RuntimeWorkspacePage.tsx +57 -0
- package/templates/openxiangda-react-spa/src/pages/defaults/DataRoutePage.tsx +17 -0
- package/templates/openxiangda-react-spa/src/pages/defaults/FilePreviewRoutePage.tsx +14 -0
- package/templates/openxiangda-react-spa/src/pages/defaults/FormRoutePage.tsx +62 -0
- package/templates/openxiangda-react-spa/src/pages/portal/UserPortalPage.tsx +27 -0
- package/templates/openxiangda-react-spa/src/pages/public/PublicHomePage.tsx +10 -0
- package/templates/openxiangda-react-spa/src/pages/states/NotFoundPage.tsx +16 -0
- package/templates/openxiangda-react-spa/src/resources/menus/menus.json +31 -0
- package/templates/openxiangda-react-spa/src/resources/permissions/page-groups/app-admin.json +8 -0
- package/templates/openxiangda-react-spa/src/resources/permissions/page-groups/app-user.json +8 -0
- package/templates/openxiangda-react-spa/src/resources/roles/roles.json +14 -0
- package/templates/openxiangda-react-spa/src/shared/form-schema.ts +135 -0
- package/templates/openxiangda-react-spa/src/styles/index.css +23 -0
- package/templates/openxiangda-react-spa/tailwind.config.cjs +29 -0
- package/templates/openxiangda-react-spa/tsconfig.app.json +36 -0
- package/templates/openxiangda-react-spa/tsconfig.json +7 -0
- package/templates/openxiangda-react-spa/tsconfig.node.json +10 -0
- package/templates/openxiangda-react-spa/vite.config.ts +73 -0
|
@@ -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":[]}
|
|
@@ -79,6 +79,36 @@ sync-schema - 将表单 Schema 同步到后端 API
|
|
|
79
79
|
`);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
function resolveWorkspaceAlias(importPath) {
|
|
83
|
+
if (!importPath.startsWith("@/")) return null;
|
|
84
|
+
const basePath = path.join(rootDir, "src", importPath.slice(2));
|
|
85
|
+
const candidates = [
|
|
86
|
+
basePath,
|
|
87
|
+
`${basePath}.ts`,
|
|
88
|
+
`${basePath}.tsx`,
|
|
89
|
+
`${basePath}.js`,
|
|
90
|
+
`${basePath}.jsx`,
|
|
91
|
+
`${basePath}.mjs`,
|
|
92
|
+
path.join(basePath, "index.ts"),
|
|
93
|
+
path.join(basePath, "index.tsx"),
|
|
94
|
+
path.join(basePath, "index.js"),
|
|
95
|
+
path.join(basePath, "index.jsx"),
|
|
96
|
+
path.join(basePath, "index.mjs"),
|
|
97
|
+
];
|
|
98
|
+
return candidates.find((candidate) => fs.existsSync(candidate)) || basePath;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function workspaceAliasPlugin() {
|
|
102
|
+
return {
|
|
103
|
+
name: "openxiangda-workspace-alias",
|
|
104
|
+
setup(build) {
|
|
105
|
+
build.onResolve({ filter: /^@\// }, (args) => ({
|
|
106
|
+
path: resolveWorkspaceAlias(args.path),
|
|
107
|
+
}));
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
82
112
|
// ---------- 表单发现 ----------
|
|
83
113
|
|
|
84
114
|
function discoverForms(filterName) {
|
|
@@ -117,6 +147,7 @@ async function loadSchema(schemaPath) {
|
|
|
117
147
|
format: "esm",
|
|
118
148
|
platform: "node",
|
|
119
149
|
target: "node18",
|
|
150
|
+
plugins: [workspaceAliasPlugin()],
|
|
120
151
|
write: false,
|
|
121
152
|
outfile: "out.mjs",
|
|
122
153
|
});
|
|
@@ -0,0 +1,69 @@
|
|
|
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 workspace publish --profile <name> --form <formCode>
|
|
22
|
+
openxiangda resource plan --profile <name>
|
|
23
|
+
openxiangda resource publish --profile <name>
|
|
24
|
+
openxiangda runtime deploy --profile <name>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`openxiangda runtime deploy` 会自动设置 `OPENXIANGDA_BUILD_ID` 和 Vite asset base,构建 `dist/`,上传 runtime release,并默认激活。不要手工改 `dist/index.html`。
|
|
28
|
+
|
|
29
|
+
完整发布顺序:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
openxiangda workspace publish --profile <name> --form <formCode>
|
|
33
|
+
openxiangda resource publish --profile <name>
|
|
34
|
+
openxiangda runtime deploy --profile <name>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
如果应用需要平台表单、流程表单、详情页或数据列表页,在 `src/forms/<formCode>/schema.ts` 定义表单 schema,并用 `openxiangda workspace publish --form <formCode>` 发布。React SPA 自身继续用 `runtime deploy` 发布。
|
|
38
|
+
|
|
39
|
+
## 权限资源
|
|
40
|
+
|
|
41
|
+
React SPA 页面需要声明菜单 code、route code 和 path pattern:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"code": "app_user_spa_pages",
|
|
46
|
+
"roles": ["app_user"],
|
|
47
|
+
"menuCodes": ["user_portal"],
|
|
48
|
+
"routeCodes": ["portal.home"],
|
|
49
|
+
"pathPatterns": ["/view/:appType/portal/*"]
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
页面中使用:
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
import { PermissionBoundary, useAppMenus, useRuntimeBootstrap } from "openxiangda/runtime/react";
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`PermissionBoundary` 只能做展示保护,不能替代后端权限。
|
|
60
|
+
|
|
61
|
+
## 默认页
|
|
62
|
+
|
|
63
|
+
- 表单提交:`/view/:appType/admin/forms/:formUuid/new`
|
|
64
|
+
- 表单详情:`/view/:appType/admin/forms/:formUuid/:formInstId`
|
|
65
|
+
- 流程详情:`/view/:appType/admin/process/:formUuid/:formInstId`
|
|
66
|
+
- 数据列表:`/view/:appType/admin/data/:formUuid`
|
|
67
|
+
- 文件预览:`/view/file-preview?ticket=...`
|
|
68
|
+
|
|
69
|
+
默认页可以替换成自定义 React route element;不要回到旧平台 `isRenderNav` 或 workbench 参数模型。
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defineAppWorkspaceConfig } from "openxiangda/build";
|
|
2
|
+
|
|
3
|
+
export default defineAppWorkspaceConfig({
|
|
4
|
+
appType: process.env.APP_TYPE || process.env.OPENXIANGDA_APP_TYPE || "APP_XXXXXXXXXXXXXXXX",
|
|
5
|
+
appName: process.env.APP_NAME || "OpenXiangda React SPA",
|
|
6
|
+
platformUrl:
|
|
7
|
+
process.env.APP_PLATFORM_URL ||
|
|
8
|
+
process.env.OPENXIANGDA_BASE_URL ||
|
|
9
|
+
"https://yida.wisejob.cn/service",
|
|
10
|
+
servicePrefix: process.env.APP_SERVICE_PREFIX || "/service",
|
|
11
|
+
appKey: process.env.APP_KEY || "",
|
|
12
|
+
appSecret: process.env.APP_SECRET || "",
|
|
13
|
+
userId: process.env.APP_USER_ID || "",
|
|
14
|
+
version: process.env.APP_VERSION || "0.1.0",
|
|
15
|
+
buildId: process.env.APP_BUILD_ID || "",
|
|
16
|
+
oss: {
|
|
17
|
+
region: process.env.APP_OSS_REGION || "oss-cn-hangzhou",
|
|
18
|
+
bucket: process.env.APP_OSS_BUCKET || "sy-app-workspace-dev",
|
|
19
|
+
accessKeyId: process.env.APP_OSS_ACCESS_KEY_ID || "",
|
|
20
|
+
accessKeySecret: process.env.APP_OSS_ACCESS_KEY_SECRET || "",
|
|
21
|
+
pathPrefix: process.env.APP_OSS_PATH_PREFIX || "app-workspace",
|
|
22
|
+
},
|
|
23
|
+
defaults: {
|
|
24
|
+
protocolVersion: process.env.APP_PAGE_PROTOCOL_VERSION || "1.0",
|
|
25
|
+
frameworkVersion: process.env.APP_FRAMEWORK_VERSION || "18.3.1",
|
|
26
|
+
cssIsolation: "none",
|
|
27
|
+
formMenuParentId: process.env.APP_FORM_MENU_PARENT_ID || "",
|
|
28
|
+
formMenuIcon: process.env.APP_FORM_MENU_ICON || "",
|
|
29
|
+
pageMenuParentId: process.env.APP_PAGE_MENU_PARENT_ID || "",
|
|
30
|
+
pageMenuIcon: process.env.APP_PAGE_MENU_ICON || "",
|
|
31
|
+
},
|
|
32
|
+
});
|
|
@@ -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,39 @@
|
|
|
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
|
+
"build:forms": "lowcode-workspace build-forms",
|
|
10
|
+
"sync-schema": "lowcode-workspace sync-schema",
|
|
11
|
+
"publish:all": "lowcode-workspace publish-all",
|
|
12
|
+
"openxiangda:publish": "lowcode-workspace publish-all",
|
|
13
|
+
"typecheck": "tsc -p tsconfig.app.json --noEmit",
|
|
14
|
+
"check": "pnpm typecheck && pnpm build",
|
|
15
|
+
"resources:plan": "openxiangda resource plan",
|
|
16
|
+
"resources:publish": "openxiangda resource publish",
|
|
17
|
+
"runtime:deploy": "openxiangda runtime deploy",
|
|
18
|
+
"deploy": "openxiangda resource publish && openxiangda runtime deploy",
|
|
19
|
+
"typegen": "openxiangda resource typegen"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
23
|
+
"lucide-react": "^0.468.0",
|
|
24
|
+
"openxiangda": "latest",
|
|
25
|
+
"react": "18.3.1",
|
|
26
|
+
"react-dom": "18.3.1",
|
|
27
|
+
"react-router-dom": "^6.30.1"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^22.0.0",
|
|
31
|
+
"@types/react": "^18.3.18",
|
|
32
|
+
"@types/react-dom": "^18.3.5",
|
|
33
|
+
"autoprefixer": "^10.4.21",
|
|
34
|
+
"postcss": "^8.5.0",
|
|
35
|
+
"tailwindcss": "^3.4.17",
|
|
36
|
+
"typescript": "^5.7.0",
|
|
37
|
+
"vite": "^6.0.0"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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,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
|
+
}
|