openxiangda 1.0.70 → 1.0.72
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/templates/openxiangda-react-spa/AGENTS.md +6 -8
- package/templates/openxiangda-react-spa/src/app/navigation.ts +8 -111
- package/templates/openxiangda-react-spa/src/app/router.tsx +2 -54
- package/templates/openxiangda-react-spa/src/app/starter-content.ts +3 -179
- package/templates/openxiangda-react-spa/src/layouts/AdminShell.tsx +147 -109
- package/templates/openxiangda-react-spa/src/pages/admin/AdminDashboardPage.tsx +15 -181
- package/templates/openxiangda-react-spa/src/resources/menus/menus.json +2 -47
- package/templates/openxiangda-react-spa/src/resources/permissions/page-groups/app-admin.json +3 -17
- package/templates/openxiangda-react-spa/src/resources/roles/roles.json +1 -6
- package/templates/openxiangda-react-spa/src/layouts/PublicShell.tsx +0 -33
- package/templates/openxiangda-react-spa/src/layouts/UserShell.tsx +0 -129
- package/templates/openxiangda-react-spa/src/pages/admin/DataCenterPage.tsx +0 -96
- package/templates/openxiangda-react-spa/src/pages/admin/ServiceCenterPage.tsx +0 -100
- package/templates/openxiangda-react-spa/src/pages/admin/TaskCenterPage.tsx +0 -135
- package/templates/openxiangda-react-spa/src/pages/portal/UserPortalPage.tsx +0 -71
- package/templates/openxiangda-react-spa/src/pages/public/PublicHomePage.tsx +0 -49
- package/templates/openxiangda-react-spa/src/resources/permissions/page-groups/app-user.json +0 -8
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
-
CalendarDays,
|
|
3
2
|
ChevronDown,
|
|
3
|
+
ChevronLeft,
|
|
4
4
|
ChevronRight,
|
|
5
5
|
LogOut,
|
|
6
6
|
Menu,
|
|
7
7
|
RefreshCw,
|
|
8
8
|
Shield,
|
|
9
|
-
UserCircle,
|
|
10
9
|
X,
|
|
11
10
|
} from "lucide-react";
|
|
12
11
|
import { useEffect, useMemo, useState } from "react";
|
|
@@ -25,12 +24,10 @@ import {
|
|
|
25
24
|
type StarterNavigationGroup,
|
|
26
25
|
} from "@/app/navigation";
|
|
27
26
|
import { starterBrand } from "@/app/starter-content";
|
|
28
|
-
import { runtimeDefaultRoutes } from "@/runtime/default-routes";
|
|
29
27
|
import {
|
|
30
28
|
PrimaryButton,
|
|
31
29
|
SecondaryButton,
|
|
32
30
|
StatePage,
|
|
33
|
-
StatusPill,
|
|
34
31
|
cn,
|
|
35
32
|
} from "@/shared/ui";
|
|
36
33
|
|
|
@@ -52,6 +49,7 @@ export function AdminShell() {
|
|
|
52
49
|
const auth = useRuntimeAuth();
|
|
53
50
|
const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>({});
|
|
54
51
|
const [mobileOpen, setMobileOpen] = useState(false);
|
|
52
|
+
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
55
53
|
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
|
56
54
|
const [loggingOut, setLoggingOut] = useState(false);
|
|
57
55
|
|
|
@@ -62,26 +60,19 @@ export function AdminShell() {
|
|
|
62
60
|
bootstrap.data?.user?.id ||
|
|
63
61
|
"当前用户",
|
|
64
62
|
);
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const primaryReceiptFormUuid = useMemo(
|
|
72
|
-
() =>
|
|
73
|
-
runtimeDefaultRoutes.formSubmit?.formUuid ||
|
|
74
|
-
findMenuFormUuid(menus.data, ["receipt", "form"]),
|
|
75
|
-
[menus.data],
|
|
63
|
+
const userAvatar = String(
|
|
64
|
+
bootstrap.data?.user?.avatar ||
|
|
65
|
+
bootstrap.data?.user?.avatarUrl ||
|
|
66
|
+
bootstrap.data?.user?.photoUrl ||
|
|
67
|
+
"",
|
|
76
68
|
);
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
69
|
+
const userRole = String(
|
|
70
|
+
bootstrap.data?.user?.roleName ||
|
|
71
|
+
bootstrap.data?.user?.title ||
|
|
72
|
+
bootstrap.data?.user?.position ||
|
|
73
|
+
bootstrap.data?.user?.departmentName ||
|
|
74
|
+
"平台用户",
|
|
82
75
|
);
|
|
83
|
-
const primaryDataFormUuid =
|
|
84
|
-
runtimeDefaultRoutes.dataManageList?.formUuid || primaryReceiptFormUuid;
|
|
85
76
|
|
|
86
77
|
const menuCodes = useMemo(() => collectMenuCodes(menus.data), [menus.data]);
|
|
87
78
|
const groups = useMemo<StarterNavigationGroup[]>(
|
|
@@ -89,13 +80,10 @@ export function AdminShell() {
|
|
|
89
80
|
filterNavigationByMenuCodes(
|
|
90
81
|
buildStarterAdminNavigation({
|
|
91
82
|
appType,
|
|
92
|
-
dataFormUuid: primaryDataFormUuid,
|
|
93
|
-
processFormUuid: primaryProcessFormUuid,
|
|
94
|
-
receiptFormUuid: primaryReceiptFormUuid,
|
|
95
83
|
}),
|
|
96
84
|
menuCodes,
|
|
97
85
|
),
|
|
98
|
-
[appType, menuCodes
|
|
86
|
+
[appType, menuCodes],
|
|
99
87
|
);
|
|
100
88
|
|
|
101
89
|
const handleLogout = async () => {
|
|
@@ -103,47 +91,60 @@ export function AdminShell() {
|
|
|
103
91
|
await auth.logoutAndRedirect({ replace: true });
|
|
104
92
|
};
|
|
105
93
|
|
|
106
|
-
const sidebar = (
|
|
107
|
-
<aside className="flex h-full min-h-0 flex-col border-r border-
|
|
108
|
-
<div className="flex h-
|
|
94
|
+
const sidebar = (compact = false) => (
|
|
95
|
+
<aside className="flex h-full min-h-0 flex-col border-r border-slate-200 bg-white">
|
|
96
|
+
<div className={cn("flex h-[68px] shrink-0 items-center gap-3", compact ? "justify-center px-3" : "px-5")}>
|
|
109
97
|
<Link
|
|
110
|
-
|
|
98
|
+
aria-label={starterBrand.name}
|
|
99
|
+
className="grid h-9 w-9 shrink-0 place-items-center text-blue-600"
|
|
111
100
|
to={`/view/${appType}/admin`}
|
|
112
101
|
>
|
|
113
|
-
|
|
102
|
+
<OpenXiangdaMark />
|
|
114
103
|
</Link>
|
|
115
|
-
<div className="min-w-0">
|
|
116
|
-
<Link className="block truncate text-
|
|
117
|
-
{
|
|
104
|
+
<div className={cn("min-w-0", compact && "hidden")}>
|
|
105
|
+
<Link className="block truncate text-[15px] font-semibold leading-5 text-slate-950" to={`/view/${appType}/admin`}>
|
|
106
|
+
{starterBrand.name}
|
|
118
107
|
</Link>
|
|
119
|
-
<div className="mt-0.5 truncate text-xs text-slate-500">{starterBrand.subtitle}</div>
|
|
108
|
+
<div className="mt-0.5 truncate text-xs leading-4 text-slate-500">{starterBrand.subtitle}</div>
|
|
120
109
|
</div>
|
|
121
110
|
</div>
|
|
122
111
|
|
|
123
|
-
<nav className="ox-scrollbar min-h-0 flex-1
|
|
112
|
+
<nav className={cn("ox-scrollbar min-h-0 flex-1 overflow-y-auto py-5", compact ? "px-2" : "space-y-7 px-3")}>
|
|
124
113
|
{groups.map(group => {
|
|
125
114
|
const collapsed = collapsedGroups[group.title];
|
|
126
115
|
return (
|
|
127
116
|
<section key={group.title}>
|
|
128
117
|
<button
|
|
129
|
-
|
|
118
|
+
aria-label={group.title}
|
|
119
|
+
className={cn(
|
|
120
|
+
"flex h-10 w-full items-center rounded-lg text-[13px] font-semibold text-slate-800 transition hover:bg-slate-50",
|
|
121
|
+
compact ? "justify-center px-0" : "justify-between px-2.5",
|
|
122
|
+
)}
|
|
130
123
|
onClick={() => setCollapsedGroups(prev => ({ ...prev, [group.title]: !collapsed }))}
|
|
131
124
|
type="button"
|
|
132
125
|
>
|
|
133
|
-
<span>{group.title}</span>
|
|
134
|
-
{
|
|
126
|
+
<span className={cn(compact && "sr-only")}>{group.title}</span>
|
|
127
|
+
{compact ? (
|
|
128
|
+
<Menu className="text-slate-500" size={16} strokeWidth={2} />
|
|
129
|
+
) : collapsed ? (
|
|
130
|
+
<ChevronRight className="text-slate-500" size={16} strokeWidth={2} />
|
|
131
|
+
) : (
|
|
132
|
+
<ChevronDown className="text-slate-500" size={16} strokeWidth={2} />
|
|
133
|
+
)}
|
|
135
134
|
</button>
|
|
136
135
|
{!collapsed ? (
|
|
137
|
-
<div className="mt-1 space-y-1
|
|
136
|
+
<div className={cn("mt-1 space-y-1", compact && "mt-2")}>
|
|
138
137
|
{group.items.map(item => {
|
|
139
138
|
const active = isMenuActive(location.pathname, location.search, item.path);
|
|
140
139
|
return (
|
|
141
140
|
<Link
|
|
141
|
+
aria-label={item.name}
|
|
142
142
|
className={cn(
|
|
143
|
-
"group relative flex items-center
|
|
143
|
+
"group relative flex h-10 items-center rounded-lg text-sm transition",
|
|
144
|
+
compact ? "justify-center px-0" : "gap-3 px-3",
|
|
144
145
|
active
|
|
145
|
-
? "bg-
|
|
146
|
-
: "text-slate-600 hover:bg-slate-
|
|
146
|
+
? "bg-[#eef6ff] text-[#1677ff]"
|
|
147
|
+
: "text-slate-600 hover:bg-slate-50 hover:text-slate-900",
|
|
147
148
|
)}
|
|
148
149
|
key={`${group.title}-${item.name}-${item.path}`}
|
|
149
150
|
onClick={() => setMobileOpen(false)}
|
|
@@ -151,24 +152,20 @@ export function AdminShell() {
|
|
|
151
152
|
>
|
|
152
153
|
<span
|
|
153
154
|
className={cn(
|
|
154
|
-
"absolute left-
|
|
155
|
-
active ? "bg-
|
|
155
|
+
"absolute -left-3 top-1 h-8 w-1 rounded-r-full transition",
|
|
156
|
+
active ? "bg-[#1677ff]" : "bg-transparent",
|
|
157
|
+
compact && "-left-2",
|
|
156
158
|
)}
|
|
157
159
|
/>
|
|
158
160
|
<span
|
|
159
161
|
className={cn(
|
|
160
|
-
"grid h-
|
|
161
|
-
active ? "
|
|
162
|
+
"grid h-5 w-5 shrink-0 place-items-center transition",
|
|
163
|
+
active ? "text-[#1677ff]" : "text-slate-500 group-hover:text-slate-700",
|
|
162
164
|
)}
|
|
163
165
|
>
|
|
164
|
-
<item.icon size={
|
|
165
|
-
</span>
|
|
166
|
-
<span className="min-w-0 flex-1">
|
|
167
|
-
<span className="block truncate font-medium">{item.name}</span>
|
|
168
|
-
{item.hint ? (
|
|
169
|
-
<span className="mt-0.5 block truncate text-xs text-slate-400">{item.hint}</span>
|
|
170
|
-
) : null}
|
|
166
|
+
<item.icon size={17} strokeWidth={2} />
|
|
171
167
|
</span>
|
|
168
|
+
<span className={cn("min-w-0 flex-1 truncate font-medium", compact && "sr-only")}>{item.name}</span>
|
|
172
169
|
</Link>
|
|
173
170
|
);
|
|
174
171
|
})}
|
|
@@ -178,28 +175,36 @@ export function AdminShell() {
|
|
|
178
175
|
);
|
|
179
176
|
})}
|
|
180
177
|
</nav>
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
178
|
+
<div className="shrink-0 px-3 py-4">
|
|
179
|
+
<button
|
|
180
|
+
className={cn(
|
|
181
|
+
"flex h-10 w-full items-center gap-3 rounded-lg text-sm font-medium text-slate-600 transition hover:bg-slate-50 hover:text-slate-950",
|
|
182
|
+
compact ? "justify-center px-0" : "px-2.5",
|
|
183
|
+
)}
|
|
184
|
+
onClick={() => setSidebarCollapsed(value => !value)}
|
|
185
|
+
type="button"
|
|
186
|
+
>
|
|
187
|
+
<span className="grid h-7 w-7 shrink-0 place-items-center rounded-lg bg-slate-50 text-slate-600 ring-1 ring-slate-200">
|
|
188
|
+
{compact ? <ChevronRight size={17} /> : <ChevronLeft size={17} />}
|
|
189
|
+
</span>
|
|
190
|
+
<span className={cn("truncate", compact && "sr-only")}>{compact ? "展开侧边栏" : "收起侧边栏"}</span>
|
|
191
|
+
</button>
|
|
189
192
|
</div>
|
|
190
193
|
</aside>
|
|
191
194
|
);
|
|
192
195
|
|
|
193
196
|
return (
|
|
194
197
|
<PermissionBoundary
|
|
195
|
-
fallback={state => <AdminPermissionState
|
|
198
|
+
fallback={state => <AdminPermissionState state={state} />}
|
|
196
199
|
loadingFallback={<AdminLoadingState />}
|
|
197
200
|
menuCode="admin_dashboard"
|
|
198
201
|
path={`/view/${appType}/admin`}
|
|
199
202
|
routeCode="admin.dashboard"
|
|
200
203
|
>
|
|
201
204
|
<div className="min-h-screen overflow-x-hidden bg-[linear-gradient(180deg,#edf4ff_0%,#f8fafc_42%,#f1f5f9_100%)] text-slate-950">
|
|
202
|
-
<div className="fixed inset-y-0 left-0 z-30 hidden w-
|
|
205
|
+
<div className={cn("fixed inset-y-0 left-0 z-30 hidden lg:block", sidebarCollapsed ? "w-[76px]" : "w-60")}>
|
|
206
|
+
{sidebar(sidebarCollapsed)}
|
|
207
|
+
</div>
|
|
203
208
|
|
|
204
209
|
{mobileOpen ? (
|
|
205
210
|
<div className="fixed inset-0 z-40 lg:hidden">
|
|
@@ -218,12 +223,12 @@ export function AdminShell() {
|
|
|
218
223
|
>
|
|
219
224
|
<X size={19} />
|
|
220
225
|
</button>
|
|
221
|
-
{sidebar}
|
|
226
|
+
{sidebar(false)}
|
|
222
227
|
</div>
|
|
223
228
|
</div>
|
|
224
229
|
) : null}
|
|
225
230
|
|
|
226
|
-
<main className="min-w-0 lg:pl-
|
|
231
|
+
<main className={cn("min-w-0 transition-[padding] duration-200", sidebarCollapsed ? "lg:pl-[76px]" : "lg:pl-60")}>
|
|
227
232
|
<header className="sticky top-0 z-20 border-b border-white/70 bg-white/[0.8] backdrop-blur-xl">
|
|
228
233
|
<div className="flex h-20 min-w-0 items-center justify-between gap-3 px-4 sm:px-6">
|
|
229
234
|
<div className="flex min-w-0 items-center gap-3">
|
|
@@ -236,37 +241,32 @@ export function AdminShell() {
|
|
|
236
241
|
<Menu size={20} />
|
|
237
242
|
</button>
|
|
238
243
|
<div className="min-w-0">
|
|
239
|
-
<div className="truncate text-
|
|
240
|
-
<div className="mt-1 truncate text-
|
|
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>
|
|
241
246
|
</div>
|
|
242
247
|
</div>
|
|
243
248
|
|
|
244
|
-
<div className="flex shrink-0 items-center
|
|
245
|
-
<StatusPill tone="blue">
|
|
246
|
-
<CalendarDays size={13} />
|
|
247
|
-
{todayText}
|
|
248
|
-
</StatusPill>
|
|
249
|
+
<div className="flex shrink-0 items-center">
|
|
249
250
|
<div className="relative">
|
|
250
251
|
<button
|
|
251
|
-
className="flex h-
|
|
252
|
+
className="flex h-12 items-center gap-3 rounded-2xl px-2 py-1.5 text-left transition hover:bg-white/80"
|
|
252
253
|
onClick={() => setUserMenuOpen(open => !open)}
|
|
253
254
|
type="button"
|
|
254
255
|
>
|
|
255
|
-
<
|
|
256
|
-
|
|
256
|
+
<UserAvatar name={userName} src={userAvatar} />
|
|
257
|
+
<span className="hidden min-w-0 sm:block">
|
|
258
|
+
<span className="block max-w-28 truncate text-sm font-semibold leading-5 text-slate-950">{userName}</span>
|
|
259
|
+
<span className="block max-w-28 truncate text-xs leading-4 text-slate-500">{userRole}</span>
|
|
257
260
|
</span>
|
|
258
|
-
<
|
|
259
|
-
<ChevronDown size={15} />
|
|
261
|
+
<ChevronDown className="text-slate-500" size={16} />
|
|
260
262
|
</button>
|
|
261
263
|
{userMenuOpen ? (
|
|
262
264
|
<div className="absolute right-0 mt-3 w-72 overflow-hidden rounded-2xl border border-slate-200 bg-white p-2 shadow-[0_24px_70px_rgba(15,23,42,0.16)]">
|
|
263
|
-
<div className="flex gap-3 rounded-xl bg-slate-50 p-3">
|
|
264
|
-
<
|
|
265
|
-
<UserCircle size={21} />
|
|
266
|
-
</div>
|
|
265
|
+
<div className="flex items-center gap-3 rounded-xl bg-slate-50 p-3">
|
|
266
|
+
<UserAvatar name={userName} src={userAvatar} size="lg" />
|
|
267
267
|
<div className="min-w-0">
|
|
268
268
|
<div className="truncate text-sm font-semibold text-slate-950">{userName}</div>
|
|
269
|
-
<div className="mt-
|
|
269
|
+
<div className="mt-0.5 truncate text-xs text-slate-500">{userRole}</div>
|
|
270
270
|
</div>
|
|
271
271
|
</div>
|
|
272
272
|
<button
|
|
@@ -296,7 +296,7 @@ export function AdminShell() {
|
|
|
296
296
|
</div>
|
|
297
297
|
</header>
|
|
298
298
|
|
|
299
|
-
<div className="mx-auto min-w-0 max-w-[
|
|
299
|
+
<div className="mx-auto min-w-0 max-w-[1440px] px-4 py-5 sm:px-6">
|
|
300
300
|
<Outlet />
|
|
301
301
|
</div>
|
|
302
302
|
</main>
|
|
@@ -305,13 +305,62 @@ export function AdminShell() {
|
|
|
305
305
|
);
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
-
function
|
|
309
|
-
|
|
310
|
-
|
|
308
|
+
function UserAvatar({
|
|
309
|
+
name,
|
|
310
|
+
size = "md",
|
|
311
|
+
src,
|
|
311
312
|
}: {
|
|
312
|
-
|
|
313
|
-
|
|
313
|
+
name: string;
|
|
314
|
+
size?: "md" | "lg";
|
|
315
|
+
src?: string;
|
|
314
316
|
}) {
|
|
317
|
+
const className = cn(
|
|
318
|
+
"shrink-0 overflow-hidden rounded-full bg-[linear-gradient(135deg,#2563eb_0%,#38bdf8_100%)] text-white shadow-sm ring-2 ring-white",
|
|
319
|
+
size === "lg" ? "h-11 w-11" : "h-9 w-9",
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
if (src) {
|
|
323
|
+
return (
|
|
324
|
+
<img
|
|
325
|
+
alt=""
|
|
326
|
+
className={cn(className, "object-cover")}
|
|
327
|
+
referrerPolicy="no-referrer"
|
|
328
|
+
src={src}
|
|
329
|
+
/>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return (
|
|
334
|
+
<span className={cn(className, "grid place-items-center text-sm font-semibold")}>
|
|
335
|
+
{name.slice(0, 1).toUpperCase()}
|
|
336
|
+
</span>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function OpenXiangdaMark() {
|
|
341
|
+
return (
|
|
342
|
+
<svg
|
|
343
|
+
aria-hidden="true"
|
|
344
|
+
className="h-8 w-8"
|
|
345
|
+
fill="none"
|
|
346
|
+
viewBox="0 0 36 36"
|
|
347
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
348
|
+
>
|
|
349
|
+
<path
|
|
350
|
+
d="M18 3.5 32.5 31H26L18 15.7 10 31H3.5L18 3.5Z"
|
|
351
|
+
fill="currentColor"
|
|
352
|
+
/>
|
|
353
|
+
<path d="M18 20.2 23.4 31H12.6L18 20.2Z" fill="white" />
|
|
354
|
+
<path
|
|
355
|
+
d="M18 9.5 8.2 31H3.5L18 3.5l14.5 27.5h-4.7L18 9.5Z"
|
|
356
|
+
fill="currentColor"
|
|
357
|
+
opacity="0.86"
|
|
358
|
+
/>
|
|
359
|
+
</svg>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function AdminPermissionState({ state }: { state: PermissionBoundaryFallbackState }) {
|
|
315
364
|
const auth = useRuntimeAuth();
|
|
316
365
|
|
|
317
366
|
useEffect(() => {
|
|
@@ -342,8 +391,8 @@ function AdminPermissionState({
|
|
|
342
391
|
actions={
|
|
343
392
|
<>
|
|
344
393
|
<SecondaryButton onClick={() => window.history.back()}>返回上一页</SecondaryButton>
|
|
345
|
-
<PrimaryButton onClick={() =>
|
|
346
|
-
|
|
394
|
+
<PrimaryButton onClick={() => void auth.redirectToLogin({ replace: true })}>
|
|
395
|
+
切换账号
|
|
347
396
|
</PrimaryButton>
|
|
348
397
|
</>
|
|
349
398
|
}
|
|
@@ -381,23 +430,12 @@ function collectMenuCodes(items: PlatformMenuLike[]): Set<string> {
|
|
|
381
430
|
return codes;
|
|
382
431
|
}
|
|
383
432
|
|
|
384
|
-
function findMenuFormUuid(
|
|
385
|
-
items: PlatformMenuLike[],
|
|
386
|
-
types: string[],
|
|
387
|
-
): string | undefined {
|
|
388
|
-
for (const item of items) {
|
|
389
|
-
const type = String(item.type || "").toLowerCase();
|
|
390
|
-
if (item.formUuid && types.some(candidate => type.includes(candidate))) {
|
|
391
|
-
return item.formUuid;
|
|
392
|
-
}
|
|
393
|
-
const nested = findMenuFormUuid(item.children || [], types);
|
|
394
|
-
if (nested) return nested;
|
|
395
|
-
}
|
|
396
|
-
return undefined;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
433
|
function isMenuActive(pathname: string, search: string, target: string) {
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
434
|
+
const normalize = (value: string) =>
|
|
435
|
+
value
|
|
436
|
+
.replace(/[?#].*$/, "")
|
|
437
|
+
.replace(/\/+$/, "")
|
|
438
|
+
.replace(/\/{2,}/g, "/");
|
|
439
|
+
if (target.includes("?")) return `${pathname}${search}` === target;
|
|
440
|
+
return normalize(pathname) === normalize(target);
|
|
403
441
|
}
|
|
@@ -1,193 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ArrowRight,
|
|
3
|
-
BarChart3,
|
|
4
|
-
ClipboardList,
|
|
5
|
-
Database,
|
|
6
|
-
FileText,
|
|
7
|
-
Inbox,
|
|
8
|
-
Sparkles,
|
|
9
|
-
Workflow,
|
|
10
|
-
} from "lucide-react";
|
|
11
|
-
import { useMemo } from "react";
|
|
12
|
-
import { useParams } from "react-router-dom";
|
|
1
|
+
import { Home } from "lucide-react";
|
|
13
2
|
|
|
14
|
-
import {
|
|
15
|
-
dataPath,
|
|
16
|
-
formPath,
|
|
17
|
-
processPath,
|
|
18
|
-
viewPath,
|
|
19
|
-
} from "@/app/navigation";
|
|
20
|
-
import {
|
|
21
|
-
dashboardHero,
|
|
22
|
-
dashboardMetrics,
|
|
23
|
-
quickActions,
|
|
24
|
-
recentActivities,
|
|
25
|
-
taskItems,
|
|
26
|
-
trendOverview,
|
|
27
|
-
} from "@/app/starter-content";
|
|
28
|
-
import { runtimeDefaultRoutes } from "@/runtime/default-routes";
|
|
29
|
-
import {
|
|
30
|
-
ListItem,
|
|
31
|
-
MetricCard,
|
|
32
|
-
PageHeader,
|
|
33
|
-
Panel,
|
|
34
|
-
PrimaryButton,
|
|
35
|
-
SecondaryButton,
|
|
36
|
-
StatusPill,
|
|
37
|
-
TrendBars,
|
|
38
|
-
} from "@/shared/ui";
|
|
39
|
-
|
|
40
|
-
const metricIcons = [Inbox, FileText, Database, BarChart3];
|
|
3
|
+
import { PageHeader, Panel, StatusPill } from "@/shared/ui";
|
|
41
4
|
|
|
42
5
|
export function AdminDashboardPage() {
|
|
43
|
-
const { appType = "" } = useParams();
|
|
44
|
-
const actionPaths = useActionPaths(appType);
|
|
45
|
-
|
|
46
6
|
return (
|
|
47
7
|
<div className="min-w-0 space-y-5">
|
|
48
8
|
<PageHeader
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
<Database size={17} />
|
|
53
|
-
查看数据
|
|
54
|
-
</SecondaryButton>
|
|
55
|
-
<PrimaryButton onClick={() => window.location.assign(actionPaths.form)}>
|
|
56
|
-
<FileText size={17} />
|
|
57
|
-
发起申请
|
|
58
|
-
</PrimaryButton>
|
|
59
|
-
</>
|
|
60
|
-
}
|
|
61
|
-
description={dashboardHero.description}
|
|
62
|
-
meta={
|
|
63
|
-
<>
|
|
64
|
-
<StatusPill tone="blue">业务办理</StatusPill>
|
|
65
|
-
<StatusPill tone="emerald">数据可查</StatusPill>
|
|
66
|
-
<StatusPill tone="amber">待办提醒</StatusPill>
|
|
67
|
-
</>
|
|
68
|
-
}
|
|
69
|
-
title={dashboardHero.title}
|
|
9
|
+
description="这是应用默认首页。你可以在这里接入真实业务模块、数据看板或常用操作。"
|
|
10
|
+
meta={<StatusPill tone="blue">默认首页</StatusPill>}
|
|
11
|
+
title="首页"
|
|
70
12
|
/>
|
|
71
13
|
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
icon={metricIcons[index]}
|
|
77
|
-
key={metric.label}
|
|
78
|
-
label={metric.label}
|
|
79
|
-
tone={metric.tone}
|
|
80
|
-
value={metric.value}
|
|
81
|
-
/>
|
|
82
|
-
))}
|
|
83
|
-
</section>
|
|
84
|
-
|
|
85
|
-
<section className="grid gap-5 xl:grid-cols-[minmax(0,1.35fr)_minmax(320px,0.65fr)]">
|
|
86
|
-
<Panel
|
|
87
|
-
action={<StatusPill tone="emerald">持续更新</StatusPill>}
|
|
88
|
-
description={trendOverview.description}
|
|
89
|
-
title={trendOverview.title}
|
|
90
|
-
>
|
|
91
|
-
<div className="grid gap-5 lg:grid-cols-[minmax(0,1fr)_240px]">
|
|
92
|
-
<TrendBars values={trendOverview.values} tone="blue" />
|
|
93
|
-
<div className="grid gap-3">
|
|
94
|
-
{trendOverview.summary.map(item => (
|
|
95
|
-
<div
|
|
96
|
-
className="rounded-2xl bg-slate-50 px-4 py-3 ring-1 ring-slate-200/70"
|
|
97
|
-
key={item.label}
|
|
98
|
-
>
|
|
99
|
-
<div className="text-xs text-slate-500">{item.label}</div>
|
|
100
|
-
<div className="mt-1 text-lg font-semibold text-slate-950">{item.value}</div>
|
|
101
|
-
</div>
|
|
102
|
-
))}
|
|
103
|
-
</div>
|
|
104
|
-
</div>
|
|
105
|
-
</Panel>
|
|
106
|
-
|
|
107
|
-
<Panel title="快捷入口" description="把高频业务放在这里,减少用户查找成本。">
|
|
108
|
-
<div className="space-y-3">
|
|
109
|
-
{quickActions.map(action => (
|
|
110
|
-
<button
|
|
111
|
-
className="block w-full text-left"
|
|
112
|
-
key={action.label}
|
|
113
|
-
onClick={() => window.location.assign(actionPaths[action.type])}
|
|
114
|
-
type="button"
|
|
115
|
-
>
|
|
116
|
-
<ListItem
|
|
117
|
-
icon={resolveActionIcon(action.type)}
|
|
118
|
-
tone={action.tone}
|
|
119
|
-
>
|
|
120
|
-
<div className="flex items-center justify-between gap-3">
|
|
121
|
-
<div className="min-w-0">
|
|
122
|
-
<div className="truncate text-sm font-semibold text-slate-950">{action.label}</div>
|
|
123
|
-
<div className="mt-1 truncate text-xs text-slate-500">{action.desc}</div>
|
|
124
|
-
</div>
|
|
125
|
-
<ArrowRight className="shrink-0 text-slate-400" size={17} />
|
|
126
|
-
</div>
|
|
127
|
-
</ListItem>
|
|
128
|
-
</button>
|
|
129
|
-
))}
|
|
130
|
-
</div>
|
|
131
|
-
</Panel>
|
|
132
|
-
</section>
|
|
133
|
-
|
|
134
|
-
<section className="grid gap-5 xl:grid-cols-[minmax(0,1fr)_360px]">
|
|
135
|
-
<Panel title="待办事项" description="优先处理即将到期和影响业务推进的事项。">
|
|
136
|
-
<div className="space-y-3">
|
|
137
|
-
{taskItems.map(item => (
|
|
138
|
-
<ListItem
|
|
139
|
-
icon={<ClipboardList size={17} />}
|
|
140
|
-
key={item.title}
|
|
141
|
-
tone={item.priority === "high" ? "rose" : item.priority === "medium" ? "amber" : "blue"}
|
|
142
|
-
>
|
|
143
|
-
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
144
|
-
<div className="min-w-0">
|
|
145
|
-
<div className="truncate text-sm font-semibold text-slate-950">{item.title}</div>
|
|
146
|
-
<div className="mt-1 text-xs text-slate-500">
|
|
147
|
-
{item.assignee} · {item.due}
|
|
148
|
-
</div>
|
|
149
|
-
</div>
|
|
150
|
-
<StatusPill tone={item.priority === "high" ? "rose" : "amber"}>{item.status}</StatusPill>
|
|
151
|
-
</div>
|
|
152
|
-
</ListItem>
|
|
153
|
-
))}
|
|
14
|
+
<Panel className="min-h-[420px]">
|
|
15
|
+
<div className="flex min-h-[360px] flex-col items-center justify-center rounded-2xl border border-dashed border-slate-200 bg-white/60 px-6 text-center">
|
|
16
|
+
<div className="grid h-14 w-14 place-items-center rounded-2xl bg-blue-50 text-blue-700 ring-1 ring-blue-100">
|
|
17
|
+
<Home size={24} />
|
|
154
18
|
</div>
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
icon={<Sparkles size={17} />}
|
|
162
|
-
key={activity}
|
|
163
|
-
tone={index % 2 === 0 ? "emerald" : "violet"}
|
|
164
|
-
>
|
|
165
|
-
<div className="text-sm leading-6 text-slate-600">{activity}</div>
|
|
166
|
-
</ListItem>
|
|
167
|
-
))}
|
|
168
|
-
</div>
|
|
169
|
-
</Panel>
|
|
170
|
-
</section>
|
|
19
|
+
<h2 className="mt-5 text-lg font-semibold text-slate-950">默认首页</h2>
|
|
20
|
+
<p className="mt-2 max-w-md text-sm leading-6 text-slate-500">
|
|
21
|
+
保留应用框架和登录账号信息,页面内容由后续业务开发自行接入。
|
|
22
|
+
</p>
|
|
23
|
+
</div>
|
|
24
|
+
</Panel>
|
|
171
25
|
</div>
|
|
172
26
|
);
|
|
173
27
|
}
|
|
174
|
-
|
|
175
|
-
function useActionPaths(appType: string) {
|
|
176
|
-
return useMemo(
|
|
177
|
-
() => ({
|
|
178
|
-
data: dataPath(appType, runtimeDefaultRoutes.dataManageList?.formUuid || runtimeDefaultRoutes.formSubmit?.formUuid),
|
|
179
|
-
form: formPath(appType, runtimeDefaultRoutes.formSubmit?.formUuid),
|
|
180
|
-
portal: viewPath(appType, "portal"),
|
|
181
|
-
process: processPath(appType, runtimeDefaultRoutes.processSubmit?.formUuid),
|
|
182
|
-
tasks: viewPath(appType, "admin/tasks"),
|
|
183
|
-
}),
|
|
184
|
-
[appType],
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function resolveActionIcon(type: (typeof quickActions)[number]["type"]) {
|
|
189
|
-
if (type === "form") return <FileText size={17} />;
|
|
190
|
-
if (type === "tasks") return <Inbox size={17} />;
|
|
191
|
-
if (type === "data") return <Database size={17} />;
|
|
192
|
-
return <Workflow size={17} />;
|
|
193
|
-
}
|