openxiangda 1.0.71 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda",
3
- "version": "1.0.71",
3
+ "version": "1.0.72",
4
4
  "description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
5
5
  "private": false,
6
6
  "bin": {
@@ -49,7 +49,7 @@ React SPA 页面需要声明菜单 code、route code 和 path pattern。默认
49
49
  ```json
50
50
  {
51
51
  "code": "admin_dashboard",
52
- "name": "首页",
52
+ "name": "工作台",
53
53
  "routeCode": "admin.dashboard",
54
54
  "path": "/view/:appType/admin"
55
55
  }
@@ -1,10 +1,12 @@
1
- import type { ComponentType } from "react";
1
+ import type { ComponentType, SVGProps } from "react";
2
2
  import { Home } from "lucide-react";
3
3
 
4
4
  export type StarterNavigationItem = {
5
5
  code?: string;
6
6
  hint?: string;
7
- icon: ComponentType<{ size?: string | number; className?: string }>;
7
+ icon: ComponentType<
8
+ SVGProps<SVGSVGElement> & { size?: string | number; strokeWidth?: string | number }
9
+ >;
8
10
  name: string;
9
11
  path: string;
10
12
  routeCode?: string;
@@ -27,13 +29,12 @@ export function buildStarterAdminNavigation({
27
29
  }: BuildStarterNavigationOptions): StarterNavigationGroup[] {
28
30
  return [
29
31
  {
30
- title: "应用",
32
+ title: "应用工作台",
31
33
  items: [
32
34
  {
33
35
  code: "admin_dashboard",
34
- hint: "默认首页",
35
36
  icon: Home,
36
- name: "首页",
37
+ name: "工作台",
37
38
  path: viewPath(appType, "admin"),
38
39
  routeCode: "admin.dashboard",
39
40
  },
@@ -1,5 +1,6 @@
1
1
  export const starterBrand = {
2
2
  fallbackName: "业务应用",
3
3
  logoText: "A",
4
+ name: "OpenXiangda",
4
5
  subtitle: "React SPA Starter",
5
6
  };
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  ChevronDown,
3
+ ChevronLeft,
3
4
  ChevronRight,
4
5
  LogOut,
5
6
  Menu,
6
7
  RefreshCw,
7
8
  Shield,
8
- UserCircle,
9
9
  X,
10
10
  } from "lucide-react";
11
11
  import { useEffect, useMemo, useState } from "react";
@@ -49,6 +49,7 @@ export function AdminShell() {
49
49
  const auth = useRuntimeAuth();
50
50
  const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>({});
51
51
  const [mobileOpen, setMobileOpen] = useState(false);
52
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
52
53
  const [userMenuOpen, setUserMenuOpen] = useState(false);
53
54
  const [loggingOut, setLoggingOut] = useState(false);
54
55
 
@@ -59,6 +60,19 @@ export function AdminShell() {
59
60
  bootstrap.data?.user?.id ||
60
61
  "当前用户",
61
62
  );
63
+ const userAvatar = String(
64
+ bootstrap.data?.user?.avatar ||
65
+ bootstrap.data?.user?.avatarUrl ||
66
+ bootstrap.data?.user?.photoUrl ||
67
+ "",
68
+ );
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
+ "平台用户",
75
+ );
62
76
 
63
77
  const menuCodes = useMemo(() => collectMenuCodes(menus.data), [menus.data]);
64
78
  const groups = useMemo<StarterNavigationGroup[]>(
@@ -77,47 +91,60 @@ export function AdminShell() {
77
91
  await auth.logoutAndRedirect({ replace: true });
78
92
  };
79
93
 
80
- const sidebar = (
81
- <aside className="flex h-full min-h-0 flex-col border-r border-white/70 bg-white/[0.86] backdrop-blur-xl">
82
- <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")}>
83
97
  <Link
84
- className="grid h-11 w-11 place-items-center rounded-2xl bg-[linear-gradient(135deg,#2563eb,#172554)] 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"
85
100
  to={`/view/${appType}/admin`}
86
101
  >
87
- {appName.slice(0, 1) || starterBrand.logoText}
102
+ <OpenXiangdaMark />
88
103
  </Link>
89
- <div className="min-w-0">
90
- <Link className="block truncate text-base font-semibold text-slate-950" to={`/view/${appType}/admin`}>
91
- {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}
92
107
  </Link>
93
- <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>
94
109
  </div>
95
110
  </div>
96
111
 
97
- <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")}>
98
113
  {groups.map(group => {
99
114
  const collapsed = collapsedGroups[group.title];
100
115
  return (
101
116
  <section key={group.title}>
102
117
  <button
103
- 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
+ )}
104
123
  onClick={() => setCollapsedGroups(prev => ({ ...prev, [group.title]: !collapsed }))}
105
124
  type="button"
106
125
  >
107
- <span>{group.title}</span>
108
- {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
+ )}
109
134
  </button>
110
135
  {!collapsed ? (
111
- <div className="mt-1 space-y-1.5">
136
+ <div className={cn("mt-1 space-y-1", compact && "mt-2")}>
112
137
  {group.items.map(item => {
113
138
  const active = isMenuActive(location.pathname, location.search, item.path);
114
139
  return (
115
140
  <Link
141
+ aria-label={item.name}
116
142
  className={cn(
117
- "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",
118
145
  active
119
- ? "bg-blue-50 text-blue-700 shadow-sm ring-1 ring-blue-100"
120
- : "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",
121
148
  )}
122
149
  key={`${group.title}-${item.name}-${item.path}`}
123
150
  onClick={() => setMobileOpen(false)}
@@ -125,24 +152,20 @@ export function AdminShell() {
125
152
  >
126
153
  <span
127
154
  className={cn(
128
- "absolute left-0 top-3 h-8 w-1 rounded-r-full transition",
129
- 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",
130
158
  )}
131
159
  />
132
160
  <span
133
161
  className={cn(
134
- "grid h-9 w-9 shrink-0 place-items-center rounded-xl transition",
135
- 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",
136
164
  )}
137
165
  >
138
- <item.icon size={18} />
139
- </span>
140
- <span className="min-w-0 flex-1">
141
- <span className="block truncate font-medium">{item.name}</span>
142
- {item.hint ? (
143
- <span className="mt-0.5 block truncate text-xs text-slate-400">{item.hint}</span>
144
- ) : null}
166
+ <item.icon size={17} strokeWidth={2} />
145
167
  </span>
168
+ <span className={cn("min-w-0 flex-1 truncate font-medium", compact && "sr-only")}>{item.name}</span>
146
169
  </Link>
147
170
  );
148
171
  })}
@@ -152,6 +175,21 @@ export function AdminShell() {
152
175
  );
153
176
  })}
154
177
  </nav>
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>
192
+ </div>
155
193
  </aside>
156
194
  );
157
195
 
@@ -164,7 +202,9 @@ export function AdminShell() {
164
202
  routeCode="admin.dashboard"
165
203
  >
166
204
  <div className="min-h-screen overflow-x-hidden bg-[linear-gradient(180deg,#edf4ff_0%,#f8fafc_42%,#f1f5f9_100%)] text-slate-950">
167
- <div className="fixed inset-y-0 left-0 z-30 hidden w-64 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>
168
208
 
169
209
  {mobileOpen ? (
170
210
  <div className="fixed inset-0 z-40 lg:hidden">
@@ -183,12 +223,12 @@ export function AdminShell() {
183
223
  >
184
224
  <X size={19} />
185
225
  </button>
186
- {sidebar}
226
+ {sidebar(false)}
187
227
  </div>
188
228
  </div>
189
229
  ) : null}
190
230
 
191
- <main className="min-w-0 lg:pl-64">
231
+ <main className={cn("min-w-0 transition-[padding] duration-200", sidebarCollapsed ? "lg:pl-[76px]" : "lg:pl-60")}>
192
232
  <header className="sticky top-0 z-20 border-b border-white/70 bg-white/[0.8] backdrop-blur-xl">
193
233
  <div className="flex h-20 min-w-0 items-center justify-between gap-3 px-4 sm:px-6">
194
234
  <div className="flex min-w-0 items-center gap-3">
@@ -209,25 +249,24 @@ export function AdminShell() {
209
249
  <div className="flex shrink-0 items-center">
210
250
  <div className="relative">
211
251
  <button
212
- 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"
213
253
  onClick={() => setUserMenuOpen(open => !open)}
214
254
  type="button"
215
255
  >
216
- <span className="grid h-8 w-8 place-items-center rounded-full bg-blue-600 text-xs font-semibold text-white">
217
- {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>
218
260
  </span>
219
- <span className="hidden max-w-24 truncate sm:block">{userName}</span>
220
- <ChevronDown size={15} />
261
+ <ChevronDown className="text-slate-500" size={16} />
221
262
  </button>
222
263
  {userMenuOpen ? (
223
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)]">
224
- <div className="flex gap-3 rounded-xl bg-slate-50 p-3">
225
- <div className="grid h-10 w-10 shrink-0 place-items-center rounded-xl bg-blue-600 text-white">
226
- <UserCircle size={21} />
227
- </div>
265
+ <div className="flex items-center gap-3 rounded-xl bg-slate-50 p-3">
266
+ <UserAvatar name={userName} src={userAvatar} size="lg" />
228
267
  <div className="min-w-0">
229
268
  <div className="truncate text-sm font-semibold text-slate-950">{userName}</div>
230
- <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>
231
270
  </div>
232
271
  </div>
233
272
  <button
@@ -266,6 +305,61 @@ export function AdminShell() {
266
305
  );
267
306
  }
268
307
 
308
+ function UserAvatar({
309
+ name,
310
+ size = "md",
311
+ src,
312
+ }: {
313
+ name: string;
314
+ size?: "md" | "lg";
315
+ src?: string;
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
+
269
363
  function AdminPermissionState({ state }: { state: PermissionBoundaryFallbackState }) {
270
364
  const auth = useRuntimeAuth();
271
365
 
@@ -2,7 +2,7 @@
2
2
  "menus": [
3
3
  {
4
4
  "code": "admin_dashboard",
5
- "name": "首页",
5
+ "name": "工作台",
6
6
  "type": "nav",
7
7
  "routeCode": "admin.dashboard",
8
8
  "path": "/view/:appType/admin",