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.
@@ -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 todayText = new Intl.DateTimeFormat("zh-CN", {
66
- day: "numeric",
67
- month: "long",
68
- weekday: "long",
69
- }).format(new Date());
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 primaryProcessFormUuid = useMemo(
78
- () =>
79
- runtimeDefaultRoutes.processSubmit?.formUuid ||
80
- findMenuFormUuid(menus.data, ["process", "workflow"]),
81
- [menus.data],
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, primaryDataFormUuid, primaryProcessFormUuid, primaryReceiptFormUuid],
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-white/70 bg-white/[0.86] backdrop-blur-xl">
108
- <div className="flex h-20 shrink-0 items-center gap-3 border-b border-slate-200/70 px-5">
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
- className="grid h-12 w-12 place-items-center rounded-2xl bg-[linear-gradient(135deg,#2563eb,#0f172a)] text-base font-semibold text-white shadow-lg shadow-blue-200/70"
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
- {appName.slice(0, 1) || starterBrand.logoText}
102
+ <OpenXiangdaMark />
114
103
  </Link>
115
- <div className="min-w-0">
116
- <Link className="block truncate text-base font-semibold text-slate-950" to={`/view/${appType}/admin`}>
117
- {appName}
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 space-y-4 overflow-y-auto px-4 py-5">
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
- className="flex w-full items-center justify-between rounded-xl px-2 py-2 text-xs font-semibold text-slate-500 transition hover:bg-slate-100/80"
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
- {collapsed ? <ChevronRight size={15} /> : <ChevronDown size={15} />}
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.5">
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 gap-3 rounded-2xl px-3 py-3 text-sm transition",
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-blue-50 text-blue-700 shadow-sm ring-1 ring-blue-100"
146
- : "text-slate-600 hover:bg-slate-100/80 hover:text-slate-950",
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-0 top-3 h-8 w-1 rounded-r-full transition",
155
- active ? "bg-blue-600" : "bg-transparent",
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-9 w-9 shrink-0 place-items-center rounded-xl transition",
161
- active ? "bg-white text-blue-700" : "bg-white/70 text-slate-500 group-hover:text-slate-700",
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={18} />
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
- <div className="shrink-0 border-t border-slate-200/70 p-4">
183
- <div className="rounded-2xl bg-[linear-gradient(135deg,#eff6ff,#ecfeff)] p-4 ring-1 ring-blue-100">
184
- <div className="text-xs font-semibold text-blue-700">今日提醒</div>
185
- <div className="mt-1 text-sm leading-5 text-slate-600">
186
- 关注待办事项和业务数据变化,保持关键流程及时推进。
187
- </div>
188
- </div>
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 appType={appType} state={state} />}
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-80 lg:block">{sidebar}</div>
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-80">
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-lg font-semibold text-slate-950">{appName}</div>
240
- <div className="mt-1 truncate text-xs text-slate-500">欢迎回来,{userName}</div>
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 gap-2">
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-10 items-center gap-2 rounded-full bg-white py-1 pl-1 pr-3 text-sm font-medium text-slate-700 shadow-sm ring-1 ring-slate-200 transition hover:bg-slate-50"
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
- <span className="grid h-8 w-8 place-items-center rounded-full bg-blue-600 text-xs font-semibold text-white">
256
- {userName.slice(0, 1).toUpperCase()}
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
- <span className="hidden max-w-24 truncate sm:block">{userName}</span>
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
- <div className="grid h-10 w-10 shrink-0 place-items-center rounded-xl bg-blue-600 text-white">
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-1 truncate text-xs text-slate-500">{String(bootstrap.data?.user?.id || "current-user")}</div>
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-[1500px] px-4 py-5 sm:px-6">
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 AdminPermissionState({
309
- appType,
310
- state,
308
+ function UserAvatar({
309
+ name,
310
+ size = "md",
311
+ src,
311
312
  }: {
312
- appType: string;
313
- state: PermissionBoundaryFallbackState;
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={() => window.location.assign(`/view/${appType}/portal`)}>
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 current = `${pathname}${search}`;
401
- if (target.includes("?")) return current === target;
402
- return pathname === target || pathname.startsWith(`${target}/`);
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
- actions={
50
- <>
51
- <SecondaryButton onClick={() => window.location.assign(actionPaths.data)}>
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
- <section className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
73
- {dashboardMetrics.map((metric, index) => (
74
- <MetricCard
75
- caption={metric.caption}
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
- </Panel>
156
-
157
- <Panel title="最近动态">
158
- <div className="space-y-3">
159
- {recentActivities.map((activity, index) => (
160
- <ListItem
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
- }