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
|
@@ -2,57 +2,12 @@
|
|
|
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",
|
|
9
|
-
"icon": "LayoutDashboard",
|
|
10
|
-
"sortOrder": 10
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
"code": "task_center",
|
|
14
|
-
"name": "待办中心",
|
|
15
|
-
"type": "nav",
|
|
16
|
-
"routeCode": "admin.tasks",
|
|
17
|
-
"path": "/view/:appType/admin/tasks",
|
|
18
|
-
"icon": "Inbox",
|
|
19
|
-
"sortOrder": 20
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
"code": "service_center",
|
|
23
|
-
"name": "服务入口",
|
|
24
|
-
"type": "nav",
|
|
25
|
-
"routeCode": "admin.services",
|
|
26
|
-
"path": "/view/:appType/admin/services",
|
|
27
|
-
"icon": "ClipboardList",
|
|
28
|
-
"sortOrder": 30
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"code": "data_center",
|
|
32
|
-
"name": "数据中心",
|
|
33
|
-
"type": "nav",
|
|
34
|
-
"routeCode": "admin.data",
|
|
35
|
-
"path": "/view/:appType/admin/data",
|
|
36
|
-
"icon": "Database",
|
|
37
|
-
"sortOrder": 40
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
"code": "user_portal",
|
|
41
|
-
"name": "用户门户",
|
|
42
|
-
"type": "nav",
|
|
43
|
-
"routeCode": "portal.home",
|
|
44
|
-
"path": "/view/:appType/portal",
|
|
45
9
|
"icon": "Home",
|
|
46
|
-
"sortOrder":
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
"code": "public_home",
|
|
50
|
-
"name": "公开服务",
|
|
51
|
-
"type": "nav",
|
|
52
|
-
"routeCode": "public.home",
|
|
53
|
-
"path": "/view/:appType/public",
|
|
54
|
-
"icon": "Globe",
|
|
55
|
-
"sortOrder": 60
|
|
10
|
+
"sortOrder": 10
|
|
56
11
|
}
|
|
57
12
|
]
|
|
58
13
|
}
|
package/templates/openxiangda-react-spa/src/resources/permissions/page-groups/app-admin.json
CHANGED
|
@@ -2,21 +2,7 @@
|
|
|
2
2
|
"code": "app_admin_spa_pages",
|
|
3
3
|
"name": "应用管理员页面权限",
|
|
4
4
|
"roles": ["app_admin"],
|
|
5
|
-
"menuCodes": [
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
"service_center",
|
|
9
|
-
"data_center",
|
|
10
|
-
"user_portal",
|
|
11
|
-
"public_home"
|
|
12
|
-
],
|
|
13
|
-
"routeCodes": [
|
|
14
|
-
"admin.dashboard",
|
|
15
|
-
"admin.tasks",
|
|
16
|
-
"admin.services",
|
|
17
|
-
"admin.data",
|
|
18
|
-
"portal.home",
|
|
19
|
-
"public.home"
|
|
20
|
-
],
|
|
21
|
-
"pathPatterns": ["/view/:appType/admin/*", "/view/:appType/portal/*", "/view/:appType/public/*"]
|
|
5
|
+
"menuCodes": ["admin_dashboard"],
|
|
6
|
+
"routeCodes": ["admin.dashboard"],
|
|
7
|
+
"pathPatterns": ["/view/:appType/admin", "/view/:appType/admin/*"]
|
|
22
8
|
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { Globe2, ShieldCheck } from "lucide-react";
|
|
2
|
-
import { Link, Outlet, useParams } from "react-router-dom";
|
|
3
|
-
|
|
4
|
-
import { StatusPill } from "@/shared/ui";
|
|
5
|
-
|
|
6
|
-
export function PublicShell() {
|
|
7
|
-
const { appType = "" } = useParams();
|
|
8
|
-
|
|
9
|
-
return (
|
|
10
|
-
<div className="min-h-screen overflow-x-hidden bg-[linear-gradient(180deg,#f8fafc_0%,#eef2f7_100%)]">
|
|
11
|
-
<header className="border-b border-white/70 bg-white/[0.78] backdrop-blur-xl">
|
|
12
|
-
<div className="mx-auto flex max-w-6xl items-center justify-between gap-3 px-5 py-4">
|
|
13
|
-
<Link className="flex min-w-0 items-center gap-3" to={`/view/${appType}/public`}>
|
|
14
|
-
<span className="grid h-11 w-11 shrink-0 place-items-center rounded-2xl bg-slate-950 text-white">
|
|
15
|
-
<Globe2 size={22} />
|
|
16
|
-
</span>
|
|
17
|
-
<span className="min-w-0">
|
|
18
|
-
<span className="block truncate text-base font-semibold text-slate-950">公开服务</span>
|
|
19
|
-
<span className="block truncate text-xs text-slate-500">无需登录的服务入口</span>
|
|
20
|
-
</span>
|
|
21
|
-
</Link>
|
|
22
|
-
<StatusPill tone="blue">
|
|
23
|
-
<ShieldCheck size={13} />
|
|
24
|
-
安全访问
|
|
25
|
-
</StatusPill>
|
|
26
|
-
</div>
|
|
27
|
-
</header>
|
|
28
|
-
<main className="mx-auto min-w-0 max-w-6xl px-5 py-8">
|
|
29
|
-
<Outlet />
|
|
30
|
-
</main>
|
|
31
|
-
</div>
|
|
32
|
-
);
|
|
33
|
-
}
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { Home, LogIn, Shield, Sparkles } from "lucide-react";
|
|
2
|
-
import { useEffect } from "react";
|
|
3
|
-
import { Link, Outlet, useParams } from "react-router-dom";
|
|
4
|
-
import {
|
|
5
|
-
PermissionBoundary,
|
|
6
|
-
useCanAccessRoute,
|
|
7
|
-
useRuntimeAuth,
|
|
8
|
-
useRuntimeBootstrap,
|
|
9
|
-
type PermissionBoundaryFallbackState,
|
|
10
|
-
} from "openxiangda/runtime/react";
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
PrimaryButton,
|
|
14
|
-
SecondaryButton,
|
|
15
|
-
StatePage,
|
|
16
|
-
StatusPill,
|
|
17
|
-
} from "@/shared/ui";
|
|
18
|
-
|
|
19
|
-
export function UserShell() {
|
|
20
|
-
const { appType = "" } = useParams();
|
|
21
|
-
const bootstrap = useRuntimeBootstrap();
|
|
22
|
-
const adminAccess = useCanAccessRoute({
|
|
23
|
-
routeCode: "admin.dashboard",
|
|
24
|
-
menuCode: "admin_dashboard",
|
|
25
|
-
path: `/view/${appType}/admin`,
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
return (
|
|
29
|
-
<PermissionBoundary
|
|
30
|
-
fallback={state => <UserAccessState appType={appType} state={state} />}
|
|
31
|
-
loadingFallback={<UserLoadingState />}
|
|
32
|
-
menuCode="user_portal"
|
|
33
|
-
path={`/view/${appType}/portal`}
|
|
34
|
-
routeCode="portal.home"
|
|
35
|
-
>
|
|
36
|
-
<div className="min-h-screen overflow-x-hidden bg-[linear-gradient(180deg,#eef2f7_0%,#f8fafc_58%,#eef2f7_100%)]">
|
|
37
|
-
<header className="border-b border-white/70 bg-white/[0.78] backdrop-blur-xl">
|
|
38
|
-
<div className="mx-auto flex h-20 max-w-6xl items-center justify-between gap-3 px-5 py-4">
|
|
39
|
-
<Link className="flex min-w-0 items-center gap-3" to={`/view/${appType}/portal`}>
|
|
40
|
-
<span className="grid h-11 w-11 shrink-0 place-items-center rounded-2xl bg-slate-950 text-sm font-semibold text-white shadow-lg shadow-slate-300/70">
|
|
41
|
-
OX
|
|
42
|
-
</span>
|
|
43
|
-
<span className="min-w-0">
|
|
44
|
-
<span className="block truncate text-base font-semibold text-slate-950">
|
|
45
|
-
{String(bootstrap.data?.app?.name || "用户门户")}
|
|
46
|
-
</span>
|
|
47
|
-
<span className="block truncate text-xs text-slate-500">服务门户</span>
|
|
48
|
-
</span>
|
|
49
|
-
</Link>
|
|
50
|
-
<div className="flex shrink-0 items-center gap-2">
|
|
51
|
-
<StatusPill tone="emerald">已登录</StatusPill>
|
|
52
|
-
{adminAccess.canAccess ? (
|
|
53
|
-
<Link
|
|
54
|
-
className="inline-flex h-10 items-center gap-2 rounded-xl bg-white px-3 text-sm font-semibold text-slate-700 shadow-sm ring-1 ring-slate-200 transition hover:bg-slate-50"
|
|
55
|
-
to={`/view/${appType}/admin`}
|
|
56
|
-
>
|
|
57
|
-
<Home size={17} />
|
|
58
|
-
管理后台
|
|
59
|
-
</Link>
|
|
60
|
-
) : null}
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
</header>
|
|
64
|
-
<main className="mx-auto min-w-0 max-w-6xl px-5 py-6">
|
|
65
|
-
<Outlet />
|
|
66
|
-
</main>
|
|
67
|
-
</div>
|
|
68
|
-
</PermissionBoundary>
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function UserAccessState({
|
|
73
|
-
appType,
|
|
74
|
-
state,
|
|
75
|
-
}: {
|
|
76
|
-
appType: string;
|
|
77
|
-
state: PermissionBoundaryFallbackState;
|
|
78
|
-
}) {
|
|
79
|
-
const auth = useRuntimeAuth();
|
|
80
|
-
|
|
81
|
-
useEffect(() => {
|
|
82
|
-
if (state.errorType === "unauthenticated") {
|
|
83
|
-
void auth.redirectToLogin({ replace: true });
|
|
84
|
-
}
|
|
85
|
-
}, [auth, state.errorType]);
|
|
86
|
-
|
|
87
|
-
if (state.errorType === "unauthenticated") {
|
|
88
|
-
return (
|
|
89
|
-
<StatePage
|
|
90
|
-
actions={<PrimaryButton onClick={() => void auth.redirectToLogin({ replace: true })}>去登录</PrimaryButton>}
|
|
91
|
-
description="当前没有有效登录态,正在跳转到登录页。"
|
|
92
|
-
fullScreen
|
|
93
|
-
icon={<LogIn size={24} />}
|
|
94
|
-
status="401"
|
|
95
|
-
title="需要登录"
|
|
96
|
-
/>
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return (
|
|
101
|
-
<StatePage
|
|
102
|
-
actions={
|
|
103
|
-
<>
|
|
104
|
-
<SecondaryButton onClick={() => window.history.back()}>返回上一页</SecondaryButton>
|
|
105
|
-
<PrimaryButton onClick={() => window.location.assign(`/view/${appType}/public`)}>
|
|
106
|
-
打开公开页
|
|
107
|
-
</PrimaryButton>
|
|
108
|
-
</>
|
|
109
|
-
}
|
|
110
|
-
description={state.message || "请联系管理员开通用户门户页面权限。"}
|
|
111
|
-
fullScreen
|
|
112
|
-
icon={<Shield size={24} />}
|
|
113
|
-
status="403"
|
|
114
|
-
title="无权访问用户门户"
|
|
115
|
-
/>
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function UserLoadingState() {
|
|
120
|
-
return (
|
|
121
|
-
<StatePage
|
|
122
|
-
description="正在确认当前用户、菜单和页面权限。"
|
|
123
|
-
fullScreen
|
|
124
|
-
icon={<Sparkles size={24} />}
|
|
125
|
-
status="LOADING"
|
|
126
|
-
title="正在进入用户门户"
|
|
127
|
-
/>
|
|
128
|
-
);
|
|
129
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { ArrowRight, BarChart3, Database, Filter, RefreshCw, Search } from "lucide-react";
|
|
2
|
-
import { useMemo } from "react";
|
|
3
|
-
import { useParams } from "react-router-dom";
|
|
4
|
-
|
|
5
|
-
import { dataPath } from "@/app/navigation";
|
|
6
|
-
import { dataRows } from "@/app/starter-content";
|
|
7
|
-
import { runtimeDefaultRoutes } from "@/runtime/default-routes";
|
|
8
|
-
import {
|
|
9
|
-
ListItem,
|
|
10
|
-
MetricCard,
|
|
11
|
-
PageHeader,
|
|
12
|
-
Panel,
|
|
13
|
-
PrimaryButton,
|
|
14
|
-
SecondaryButton,
|
|
15
|
-
StatusPill,
|
|
16
|
-
TrendBars,
|
|
17
|
-
} from "@/shared/ui";
|
|
18
|
-
|
|
19
|
-
export function DataCenterPage() {
|
|
20
|
-
const { appType = "" } = useParams();
|
|
21
|
-
const dataFormUuid = runtimeDefaultRoutes.dataManageList?.formUuid || runtimeDefaultRoutes.formSubmit?.formUuid;
|
|
22
|
-
const listPath = useMemo(
|
|
23
|
-
() => dataPath(appType, dataFormUuid),
|
|
24
|
-
[appType, dataFormUuid],
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
return (
|
|
28
|
-
<div className="min-w-0 space-y-5">
|
|
29
|
-
<PageHeader
|
|
30
|
-
actions={
|
|
31
|
-
<>
|
|
32
|
-
<SecondaryButton>
|
|
33
|
-
<Filter size={17} />
|
|
34
|
-
筛选
|
|
35
|
-
</SecondaryButton>
|
|
36
|
-
{dataFormUuid ? (
|
|
37
|
-
<PrimaryButton onClick={() => window.location.assign(listPath)}>
|
|
38
|
-
<Database size={17} />
|
|
39
|
-
打开数据列表
|
|
40
|
-
</PrimaryButton>
|
|
41
|
-
) : (
|
|
42
|
-
<StatusPill tone="amber">配置后可用</StatusPill>
|
|
43
|
-
)}
|
|
44
|
-
</>
|
|
45
|
-
}
|
|
46
|
-
description="汇总关键数据、业务趋势和常用查询入口,帮助团队快速掌握当前进展。"
|
|
47
|
-
meta={
|
|
48
|
-
<>
|
|
49
|
-
<StatusPill tone="emerald">权限数据</StatusPill>
|
|
50
|
-
<StatusPill tone="blue">业务台账</StatusPill>
|
|
51
|
-
<StatusPill tone="violet">趋势分析</StatusPill>
|
|
52
|
-
</>
|
|
53
|
-
}
|
|
54
|
-
title="数据中心"
|
|
55
|
-
/>
|
|
56
|
-
|
|
57
|
-
<section className="grid gap-4 md:grid-cols-3">
|
|
58
|
-
{dataRows.map((row, index) => (
|
|
59
|
-
<MetricCard
|
|
60
|
-
caption={row.trend}
|
|
61
|
-
icon={index === 0 ? BarChart3 : index === 1 ? RefreshCw : Database}
|
|
62
|
-
key={row.label}
|
|
63
|
-
label={row.label}
|
|
64
|
-
tone={index === 0 ? "blue" : index === 1 ? "amber" : "emerald"}
|
|
65
|
-
value={row.count}
|
|
66
|
-
/>
|
|
67
|
-
))}
|
|
68
|
-
</section>
|
|
69
|
-
|
|
70
|
-
<section className="grid gap-5 xl:grid-cols-[minmax(0,1fr)_360px]">
|
|
71
|
-
<Panel title="业务趋势" description="用于呈现关键业务数据的变化节奏。">
|
|
72
|
-
<TrendBars values={[24, 36, 42, 39, 58, 64, 72, 81]} tone="emerald" />
|
|
73
|
-
</Panel>
|
|
74
|
-
|
|
75
|
-
<Panel title="常用查询">
|
|
76
|
-
<button
|
|
77
|
-
className="block w-full text-left disabled:cursor-not-allowed disabled:opacity-70"
|
|
78
|
-
disabled={!dataFormUuid}
|
|
79
|
-
onClick={() => dataFormUuid && window.location.assign(listPath)}
|
|
80
|
-
type="button"
|
|
81
|
-
>
|
|
82
|
-
<ListItem icon={<Search size={17} />} tone="blue">
|
|
83
|
-
<div className="flex items-center justify-between gap-3">
|
|
84
|
-
<div className="min-w-0">
|
|
85
|
-
<div className="truncate text-sm font-semibold text-slate-950">业务数据列表</div>
|
|
86
|
-
<div className="mt-1 truncate text-xs text-slate-500">查看当前账号有权限访问的数据。</div>
|
|
87
|
-
</div>
|
|
88
|
-
<ArrowRight className="shrink-0 text-slate-400" size={17} />
|
|
89
|
-
</div>
|
|
90
|
-
</ListItem>
|
|
91
|
-
</button>
|
|
92
|
-
</Panel>
|
|
93
|
-
</section>
|
|
94
|
-
</div>
|
|
95
|
-
);
|
|
96
|
-
}
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { ArrowRight, Database, FileText, Home, Workflow } from "lucide-react";
|
|
2
|
-
import { useMemo } from "react";
|
|
3
|
-
import { useParams } from "react-router-dom";
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
dataPath,
|
|
7
|
-
formPath,
|
|
8
|
-
processPath,
|
|
9
|
-
viewPath,
|
|
10
|
-
} from "@/app/navigation";
|
|
11
|
-
import { serviceEntries } from "@/app/starter-content";
|
|
12
|
-
import { runtimeDefaultRoutes } from "@/runtime/default-routes";
|
|
13
|
-
import {
|
|
14
|
-
ListItem,
|
|
15
|
-
PageHeader,
|
|
16
|
-
Panel,
|
|
17
|
-
PrimaryButton,
|
|
18
|
-
StatusPill,
|
|
19
|
-
} from "@/shared/ui";
|
|
20
|
-
|
|
21
|
-
export function ServiceCenterPage() {
|
|
22
|
-
const { appType = "" } = useParams();
|
|
23
|
-
const paths = useMemo(
|
|
24
|
-
() => ({
|
|
25
|
-
data: dataPath(appType, runtimeDefaultRoutes.dataManageList?.formUuid || runtimeDefaultRoutes.formSubmit?.formUuid),
|
|
26
|
-
form: formPath(appType, runtimeDefaultRoutes.formSubmit?.formUuid),
|
|
27
|
-
portal: viewPath(appType, "portal"),
|
|
28
|
-
process: processPath(appType, runtimeDefaultRoutes.processSubmit?.formUuid),
|
|
29
|
-
}),
|
|
30
|
-
[appType],
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<div className="min-w-0 space-y-5">
|
|
35
|
-
<PageHeader
|
|
36
|
-
actions={
|
|
37
|
-
<PrimaryButton onClick={() => window.location.assign(paths.form)}>
|
|
38
|
-
<FileText size={17} />
|
|
39
|
-
发起申请
|
|
40
|
-
</PrimaryButton>
|
|
41
|
-
}
|
|
42
|
-
description="把常用表单、流程、数据和门户入口集中起来,形成清晰的业务办理路径。"
|
|
43
|
-
meta={
|
|
44
|
-
<>
|
|
45
|
-
<StatusPill tone="blue">申请登记</StatusPill>
|
|
46
|
-
<StatusPill tone="amber">流程办理</StatusPill>
|
|
47
|
-
<StatusPill tone="emerald">数据查询</StatusPill>
|
|
48
|
-
</>
|
|
49
|
-
}
|
|
50
|
-
title="服务入口"
|
|
51
|
-
/>
|
|
52
|
-
|
|
53
|
-
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
|
54
|
-
{serviceEntries.map(entry => (
|
|
55
|
-
<button
|
|
56
|
-
className="block text-left"
|
|
57
|
-
key={entry.label}
|
|
58
|
-
onClick={() => window.location.assign(paths[entry.type])}
|
|
59
|
-
type="button"
|
|
60
|
-
>
|
|
61
|
-
<div className="h-full rounded-2xl border border-white/70 bg-white/[0.86] p-5 shadow-[0_18px_55px_rgba(15,23,42,0.06)] ring-1 ring-slate-900/5 transition hover:-translate-y-0.5 hover:shadow-[0_24px_70px_rgba(15,23,42,0.10)]">
|
|
62
|
-
<div className="flex items-start justify-between gap-3">
|
|
63
|
-
<span className="grid h-11 w-11 place-items-center rounded-2xl bg-blue-50 text-blue-700">
|
|
64
|
-
{resolveServiceIcon(entry.type)}
|
|
65
|
-
</span>
|
|
66
|
-
<ArrowRight className="text-slate-300" size={18} />
|
|
67
|
-
</div>
|
|
68
|
-
<div className="mt-5 text-base font-semibold text-slate-950">{entry.label}</div>
|
|
69
|
-
<div className="mt-2 text-sm leading-6 text-slate-500">{entry.desc}</div>
|
|
70
|
-
</div>
|
|
71
|
-
</button>
|
|
72
|
-
))}
|
|
73
|
-
</section>
|
|
74
|
-
|
|
75
|
-
<Panel title="服务组织方式" description="不同服务按办理方式归类,用户可以更快找到需要发起或查询的业务。">
|
|
76
|
-
<div className="grid gap-3 lg:grid-cols-3">
|
|
77
|
-
<ListItem icon={<FileText size={17} />} tone="blue">
|
|
78
|
-
<div className="text-sm font-semibold text-slate-950">表单入口</div>
|
|
79
|
-
<div className="mt-1 text-xs leading-5 text-slate-500">适合标准登记、收集和提交场景。</div>
|
|
80
|
-
</ListItem>
|
|
81
|
-
<ListItem icon={<Workflow size={17} />} tone="amber">
|
|
82
|
-
<div className="text-sm font-semibold text-slate-950">流程入口</div>
|
|
83
|
-
<div className="mt-1 text-xs leading-5 text-slate-500">适合审批、办理、抄送和多角色协同。</div>
|
|
84
|
-
</ListItem>
|
|
85
|
-
<ListItem icon={<Database size={17} />} tone="emerald">
|
|
86
|
-
<div className="text-sm font-semibold text-slate-950">数据入口</div>
|
|
87
|
-
<div className="mt-1 text-xs leading-5 text-slate-500">适合查询台账、跟进状态和查看历史记录。</div>
|
|
88
|
-
</ListItem>
|
|
89
|
-
</div>
|
|
90
|
-
</Panel>
|
|
91
|
-
</div>
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function resolveServiceIcon(type: (typeof serviceEntries)[number]["type"]) {
|
|
96
|
-
if (type === "form") return <FileText size={21} />;
|
|
97
|
-
if (type === "process") return <Workflow size={21} />;
|
|
98
|
-
if (type === "data") return <Database size={21} />;
|
|
99
|
-
return <Home size={21} />;
|
|
100
|
-
}
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import { CheckCircle2, Clock3, Filter, Inbox, RefreshCw } from "lucide-react";
|
|
2
|
-
import { useMemo, useState } from "react";
|
|
3
|
-
|
|
4
|
-
import { taskItems } from "@/app/starter-content";
|
|
5
|
-
import {
|
|
6
|
-
ListItem,
|
|
7
|
-
PageHeader,
|
|
8
|
-
Panel,
|
|
9
|
-
SecondaryButton,
|
|
10
|
-
StatusPill,
|
|
11
|
-
cn,
|
|
12
|
-
} from "@/shared/ui";
|
|
13
|
-
|
|
14
|
-
type TaskFilter = "all" | "pending" | "done";
|
|
15
|
-
|
|
16
|
-
const doneItems = [
|
|
17
|
-
{
|
|
18
|
-
assignee: "系统运营组",
|
|
19
|
-
due: "昨天",
|
|
20
|
-
priority: "low" as const,
|
|
21
|
-
status: "已完成",
|
|
22
|
-
title: "确认服务入口可访问",
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
assignee: "数据管理员",
|
|
26
|
-
due: "本周一",
|
|
27
|
-
priority: "low" as const,
|
|
28
|
-
status: "已完成",
|
|
29
|
-
title: "完成数据台账月度归档",
|
|
30
|
-
},
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
export function TaskCenterPage() {
|
|
34
|
-
const [filter, setFilter] = useState<TaskFilter>("all");
|
|
35
|
-
const tasks = useMemo(() => {
|
|
36
|
-
if (filter === "pending") return taskItems;
|
|
37
|
-
if (filter === "done") return doneItems;
|
|
38
|
-
return [...taskItems, ...doneItems];
|
|
39
|
-
}, [filter]);
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<div className="min-w-0 space-y-5">
|
|
43
|
-
<PageHeader
|
|
44
|
-
actions={
|
|
45
|
-
<>
|
|
46
|
-
<SecondaryButton>
|
|
47
|
-
<Filter size={17} />
|
|
48
|
-
筛选
|
|
49
|
-
</SecondaryButton>
|
|
50
|
-
<SecondaryButton onClick={() => window.location.reload()}>
|
|
51
|
-
<RefreshCw size={17} />
|
|
52
|
-
刷新
|
|
53
|
-
</SecondaryButton>
|
|
54
|
-
</>
|
|
55
|
-
}
|
|
56
|
-
description="集中查看当前账号需要处理、跟进和确认的业务事项。"
|
|
57
|
-
meta={
|
|
58
|
-
<>
|
|
59
|
-
<StatusPill tone="amber">待处理 {taskItems.length}</StatusPill>
|
|
60
|
-
<StatusPill tone="emerald">已完成 {doneItems.length}</StatusPill>
|
|
61
|
-
</>
|
|
62
|
-
}
|
|
63
|
-
title="待办中心"
|
|
64
|
-
/>
|
|
65
|
-
|
|
66
|
-
<Panel>
|
|
67
|
-
<div className="mb-4 flex flex-wrap gap-2">
|
|
68
|
-
{[
|
|
69
|
-
{ label: "全部", value: "all" as const },
|
|
70
|
-
{ label: "待处理", value: "pending" as const },
|
|
71
|
-
{ label: "已完成", value: "done" as const },
|
|
72
|
-
].map(item => (
|
|
73
|
-
<button
|
|
74
|
-
className={cn(
|
|
75
|
-
"h-9 rounded-full px-4 text-sm font-semibold transition",
|
|
76
|
-
filter === item.value
|
|
77
|
-
? "bg-slate-950 text-white shadow-lg shadow-slate-300/60"
|
|
78
|
-
: "bg-slate-100 text-slate-600 hover:bg-slate-200/80",
|
|
79
|
-
)}
|
|
80
|
-
key={item.value}
|
|
81
|
-
onClick={() => setFilter(item.value)}
|
|
82
|
-
type="button"
|
|
83
|
-
>
|
|
84
|
-
{item.label}
|
|
85
|
-
</button>
|
|
86
|
-
))}
|
|
87
|
-
</div>
|
|
88
|
-
|
|
89
|
-
<div className="space-y-3">
|
|
90
|
-
{tasks.map(item => {
|
|
91
|
-
const done = item.status === "已完成";
|
|
92
|
-
return (
|
|
93
|
-
<ListItem
|
|
94
|
-
icon={done ? <CheckCircle2 size={17} /> : <Clock3 size={17} />}
|
|
95
|
-
key={`${item.title}-${item.status}`}
|
|
96
|
-
tone={done ? "emerald" : item.priority === "high" ? "rose" : "amber"}
|
|
97
|
-
>
|
|
98
|
-
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
|
99
|
-
<div className="min-w-0">
|
|
100
|
-
<div className="truncate text-sm font-semibold text-slate-950">{item.title}</div>
|
|
101
|
-
<div className="mt-1 flex flex-wrap gap-2 text-xs text-slate-500">
|
|
102
|
-
<span>{item.assignee}</span>
|
|
103
|
-
<span>·</span>
|
|
104
|
-
<span>{item.due}</span>
|
|
105
|
-
</div>
|
|
106
|
-
</div>
|
|
107
|
-
<StatusPill tone={done ? "emerald" : item.priority === "high" ? "rose" : "amber"}>
|
|
108
|
-
{item.status}
|
|
109
|
-
</StatusPill>
|
|
110
|
-
</div>
|
|
111
|
-
</ListItem>
|
|
112
|
-
);
|
|
113
|
-
})}
|
|
114
|
-
</div>
|
|
115
|
-
</Panel>
|
|
116
|
-
|
|
117
|
-
<Panel title="办理建议" description="优先处理高优先级和临近截止时间的事项,保持流程顺畅。">
|
|
118
|
-
<div className="grid gap-3 md:grid-cols-3">
|
|
119
|
-
<ListItem icon={<Inbox size={17} />} tone="blue">
|
|
120
|
-
<div className="text-sm font-semibold text-slate-950">统一入口</div>
|
|
121
|
-
<div className="mt-1 text-xs leading-5 text-slate-500">把审批、补充资料和确认事项集中到一个页面。</div>
|
|
122
|
-
</ListItem>
|
|
123
|
-
<ListItem icon={<Clock3 size={17} />} tone="amber">
|
|
124
|
-
<div className="text-sm font-semibold text-slate-950">按时推进</div>
|
|
125
|
-
<div className="mt-1 text-xs leading-5 text-slate-500">使用截止时间和状态提醒帮助用户判断优先级。</div>
|
|
126
|
-
</ListItem>
|
|
127
|
-
<ListItem icon={<CheckCircle2 size={17} />} tone="emerald">
|
|
128
|
-
<div className="text-sm font-semibold text-slate-950">闭环记录</div>
|
|
129
|
-
<div className="mt-1 text-xs leading-5 text-slate-500">完成事项保留处理痕迹,便于后续查询。</div>
|
|
130
|
-
</ListItem>
|
|
131
|
-
</div>
|
|
132
|
-
</Panel>
|
|
133
|
-
</div>
|
|
134
|
-
);
|
|
135
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { ArrowRight, FileText, Inbox, Search, ShieldCheck } from "lucide-react";
|
|
2
|
-
|
|
3
|
-
import { portalEntries, recentActivities } from "@/app/starter-content";
|
|
4
|
-
import {
|
|
5
|
-
ListItem,
|
|
6
|
-
MetricCard,
|
|
7
|
-
PageHeader,
|
|
8
|
-
Panel,
|
|
9
|
-
StatusPill,
|
|
10
|
-
} from "@/shared/ui";
|
|
11
|
-
|
|
12
|
-
const icons = [FileText, Inbox, Search];
|
|
13
|
-
|
|
14
|
-
export function UserPortalPage() {
|
|
15
|
-
return (
|
|
16
|
-
<div className="min-w-0 space-y-5">
|
|
17
|
-
<PageHeader
|
|
18
|
-
description="普通用户可以在这里发起申请、查看事项进度,并查询自己有权限访问的数据。"
|
|
19
|
-
meta={
|
|
20
|
-
<>
|
|
21
|
-
<StatusPill tone="emerald">已登录</StatusPill>
|
|
22
|
-
<StatusPill tone="blue">服务入口</StatusPill>
|
|
23
|
-
<StatusPill tone="violet">我的事项</StatusPill>
|
|
24
|
-
</>
|
|
25
|
-
}
|
|
26
|
-
title="用户门户"
|
|
27
|
-
/>
|
|
28
|
-
|
|
29
|
-
<section className="grid gap-4 md:grid-cols-3">
|
|
30
|
-
{portalEntries.map((entry, index) => (
|
|
31
|
-
<MetricCard
|
|
32
|
-
caption={entry.desc}
|
|
33
|
-
icon={icons[index]}
|
|
34
|
-
key={entry.label}
|
|
35
|
-
label={entry.label}
|
|
36
|
-
tone={entry.tone}
|
|
37
|
-
value={<span className="text-base">进入</span>}
|
|
38
|
-
/>
|
|
39
|
-
))}
|
|
40
|
-
</section>
|
|
41
|
-
|
|
42
|
-
<section className="grid gap-5 lg:grid-cols-[minmax(0,1fr)_360px]">
|
|
43
|
-
<Panel title="我的事项" description="常用事项会集中展示,便于持续跟进。">
|
|
44
|
-
<div className="grid gap-3 md:grid-cols-2">
|
|
45
|
-
{portalEntries.map(entry => (
|
|
46
|
-
<ListItem key={entry.label} tone={entry.tone}>
|
|
47
|
-
<div className="flex items-center justify-between gap-3">
|
|
48
|
-
<div className="min-w-0">
|
|
49
|
-
<div className="truncate text-sm font-semibold text-slate-950">{entry.label}</div>
|
|
50
|
-
<div className="mt-1 truncate text-xs text-slate-500">{entry.desc}</div>
|
|
51
|
-
</div>
|
|
52
|
-
<ArrowRight className="shrink-0 text-slate-400" size={17} />
|
|
53
|
-
</div>
|
|
54
|
-
</ListItem>
|
|
55
|
-
))}
|
|
56
|
-
</div>
|
|
57
|
-
</Panel>
|
|
58
|
-
|
|
59
|
-
<Panel title="最近动态">
|
|
60
|
-
<div className="space-y-3">
|
|
61
|
-
{recentActivities.slice(0, 3).map(activity => (
|
|
62
|
-
<ListItem icon={<ShieldCheck size={17} />} key={activity} tone="emerald">
|
|
63
|
-
<div className="text-sm leading-6 text-slate-600">{activity}</div>
|
|
64
|
-
</ListItem>
|
|
65
|
-
))}
|
|
66
|
-
</div>
|
|
67
|
-
</Panel>
|
|
68
|
-
</section>
|
|
69
|
-
</div>
|
|
70
|
-
);
|
|
71
|
-
}
|