@oj-module/cli 0.2.8 → 0.2.14
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/devkit/README.md +70 -24
- package/devkit/framework-dev.md +14 -5
- package/devkit/manual.md +9 -6
- package/devkit/module-dev-and-cdn-playground.md +197 -0
- package/devkit/sql-builder-missing-capabilities-pr.md +38 -4
- package/package.json +2 -2
- package/shell-dist/assets/runtime.js +130 -45
- package/shell-dist/index.html +2 -2
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/build-backend.test.ts +64 -0
- package/src/__tests__/versions.test.ts +26 -0
- package/src/build.ts +9 -0
- package/src/bundle.ts +8 -5
- package/src/contract/__tests__/market-client.integration.test.ts +13 -10
- package/src/index.ts +6 -0
- package/src/init.ts +22 -0
- package/src/lifecycle/__tests__/bundle.test.ts +3 -3
- package/src/market/__tests__/bootstrap.test.ts +204 -0
- package/src/market/__tests__/build-index.test.ts +154 -0
- package/src/market/__tests__/cdn-layout.test.ts +96 -0
- package/src/market/__tests__/failure-playbook.test.ts +33 -0
- package/src/market/__tests__/fixture.test.ts +48 -0
- package/src/market/__tests__/publish.test.ts +109 -0
- package/src/market/__tests__/report.test.ts +51 -0
- package/src/market/__tests__/setup.test.ts +114 -0
- package/src/market/__tests__/verify.test.ts +111 -0
- package/src/market/bootstrap.ts +255 -0
- package/src/market/build-index.ts +159 -0
- package/src/market/cdn-layout.ts +122 -0
- package/src/market/command.ts +139 -0
- package/src/market/failure-playbook.ts +57 -0
- package/src/market/fixture.ts +138 -0
- package/src/market/index-builder.ts +76 -0
- package/src/market/publish.ts +156 -0
- package/src/market/report.ts +67 -0
- package/src/market/scan.ts +99 -0
- package/src/market/setup.ts +144 -0
- package/src/market/trial.ts +121 -0
- package/src/market/verify.ts +298 -0
- package/src/market/walkthrough.ts +506 -0
- package/src/oj.ts +3 -1
- package/src/preview-config.ts +14 -0
- package/src/preview.ts +4 -3
- package/src/sign.ts +21 -6
- package/src/usage.ts +19 -0
- package/src/vendor.ts +4 -2
- package/src/versions.ts +28 -0
- package/templates/api/.ojm-api-exempt.json +6 -2
- package/templates/api/config.yaml +20 -0
- package/templates/api/src/_platform/_lifecycle/steps/extract.ts +8 -8
- package/templates/api/src/_platform/_lifecycle/uninstall.ts +21 -53
- package/templates/api/src/_platform/_shared/service/domain/contacts.ts +15 -10
- package/templates/api/src/_platform/_shared/service/domain/solution-perms.ts +8 -6
- package/templates/api/src/_platform/_shared/service/infra/oplog.ts +16 -23
- package/templates/api/src/_platform/_shared/service/infra/query.ts +18 -0
- package/templates/api/src/_platform/_shared/service/infra/rate.ts +4 -1
- package/templates/api/src/_platform/fixtures/l1-test-user.sql +13 -0
- package/templates/api/src/_platform/manifest.yaml +1 -0
- package/templates/api/src/_platform/migrations/0013__users_profile.sql +4 -0
- package/templates/api/src/_platform/schema.yaml +12 -0
- package/templates/api/src/auth/_shared/session.ts +2 -2
- package/templates/api/src/demo/todos/api.ts +8 -9
- package/templates/api/src/notification/_shared/notify.ts +64 -26
- package/templates/api/src/notification/api.ts +1 -1
- package/templates/api/src/notification/manifest.yaml +1 -0
- package/templates/api/src/notification/migrations/0004__notifications_broadcast_hidden.sql +12 -0
- package/templates/api/src/notification/schema.yaml +7 -0
- package/templates/api/src/notification/seed.sql +5 -4
- package/templates/api/src/personal-center/profile/api.ts +63 -0
- package/templates/api/src/web/user-info/api.ts +3 -1
- package/templates/api/tests/fixture.ts +8 -16
- package/templates/api/tests/l1-builder.test.ts +91 -0
- package/templates/api/tests/lifecycle/installer.spec.ts +1 -4
- package/templates/api/tests/lifecycle/market-api.spec.ts +1 -3
- package/templates/api/tests/lifecycle/steps-fs.spec.ts +5 -8
- package/templates/api/tests/lifecycle/uninstall.spec.ts +28 -88
- package/templates/api/tests/query-utils.spec.ts +85 -1
- package/templates/api/tests/rbac-settings.spec.ts +64 -19
- package/templates/cdn/README.md +48 -0
- package/templates/global.d.ts +36 -2
- package/templates/init.include.yaml +3 -2
- package/templates/openapi.json +55 -10
- package/templates/web/src/notification/entry.ts +46 -9
- package/templates/web/src/notification/locales/en-US.json +19 -0
- package/templates/web/src/notification/locales/zh-CN.json +19 -0
- package/templates/web/src/notification/pages/center/index.tsx +131 -0
- package/templates/web/src/oj-market/entry.ts +8 -2
- package/templates/web/src/personal-center/__client/api.schemas.ts +9 -0
- package/templates/web/src/personal-center/__client/api.ts +25 -1
- package/templates/web/src/personal-center/entry.ts +5 -0
- package/templates/web/src/personal-center/pages/my-profile/index.tsx +25 -3
- package/templates/web/src/system/entry.ts +2 -1
- package/templates/web.config.ts +10 -7
- package/vendor/host-versions.json +3 -3
- package/templates/api/src/notifications/api.ts +0 -29
- package/templates/api/src/notifications/manifest.yaml +0 -5
- package/templates/web/src/notifications/__client/api.schemas.ts +0 -8
- package/templates/web/src/notifications/__client/api.ts +0 -80
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"menu": {
|
|
3
|
+
"notificationCenter": "Notifications"
|
|
4
|
+
},
|
|
5
|
+
"center": {
|
|
6
|
+
"tabAll": "All",
|
|
7
|
+
"category": {
|
|
8
|
+
"notice": "Notice",
|
|
9
|
+
"pm": "Direct",
|
|
10
|
+
"mention": "Mentions"
|
|
11
|
+
},
|
|
12
|
+
"markAllRead": "Mark all as read",
|
|
13
|
+
"clear": "Clear",
|
|
14
|
+
"clearConfirm": "Clear all notifications? (Broadcasts are hidden for you only)",
|
|
15
|
+
"empty": "No notifications",
|
|
16
|
+
"scopeBroadcast": "Broadcast",
|
|
17
|
+
"scopeUser": "Personal"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"menu": {
|
|
3
|
+
"notificationCenter": "通知中心"
|
|
4
|
+
},
|
|
5
|
+
"center": {
|
|
6
|
+
"tabAll": "全部",
|
|
7
|
+
"category": {
|
|
8
|
+
"notice": "通知",
|
|
9
|
+
"pm": "私信",
|
|
10
|
+
"mention": "@我"
|
|
11
|
+
},
|
|
12
|
+
"markAllRead": "全部已读",
|
|
13
|
+
"clear": "清空",
|
|
14
|
+
"clearConfirm": "确定清空所有通知吗?(广播仅对您隐藏,不影响他人)",
|
|
15
|
+
"empty": "暂无通知",
|
|
16
|
+
"scopeBroadcast": "广播",
|
|
17
|
+
"scopeUser": "个人"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { NoticeCategory } from "@oj-module/runtime/notify";
|
|
2
|
+
|
|
3
|
+
import { CheckOutlined, ClearOutlined } from "@ant-design/icons";
|
|
4
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
import { App, Avatar, Button, Empty, List, Popconfirm, Segmented, Tag, Tooltip } from "antd";
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { useTranslation } from "react-i18next";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
notificationNotificationsClearPost,
|
|
11
|
+
notificationNotificationsGet,
|
|
12
|
+
notificationNotificationsReadAllPost,
|
|
13
|
+
notificationNotificationsReadPost,
|
|
14
|
+
} from "../../__client/api";
|
|
15
|
+
|
|
16
|
+
type Item = Awaited<ReturnType<typeof notificationNotificationsGet>>[number];
|
|
17
|
+
type CatFilter = "all" | NoticeCategory;
|
|
18
|
+
|
|
19
|
+
const CAT_COLOR: Record<NoticeCategory, string> = {
|
|
20
|
+
notice: "blue",
|
|
21
|
+
pm: "magenta",
|
|
22
|
+
mention: "orange",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 通知中心(N3 落地):全量消息页,铃铛弹层「查看全部」跳转至此。
|
|
27
|
+
* 按类型(notice/pm/mention)Tab 过滤;单条点击标记已读并跳深链;
|
|
28
|
+
* 全部已读/清空与铃铛弹层同一数据路径(/api/notification/*)。
|
|
29
|
+
*/
|
|
30
|
+
export default function NotificationCenter() {
|
|
31
|
+
const { t } = useTranslation("notification");
|
|
32
|
+
const { message } = App.useApp();
|
|
33
|
+
const queryClient = useQueryClient();
|
|
34
|
+
const [cat, setCat] = useState<CatFilter>("all");
|
|
35
|
+
|
|
36
|
+
const { data, isPending } = useQuery({
|
|
37
|
+
queryKey: ["notification-center"],
|
|
38
|
+
queryFn: notificationNotificationsGet,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const invalidate = () => queryClient.invalidateQueries({ queryKey: ["notification-center"] });
|
|
42
|
+
|
|
43
|
+
const readMut = useMutation({
|
|
44
|
+
mutationFn: (id: string) => notificationNotificationsReadPost({ id }),
|
|
45
|
+
onSettled: invalidate,
|
|
46
|
+
onError: e => message.error(String(e)),
|
|
47
|
+
});
|
|
48
|
+
const readAllMut = useMutation({
|
|
49
|
+
mutationFn: notificationNotificationsReadAllPost,
|
|
50
|
+
onSettled: invalidate,
|
|
51
|
+
onError: e => message.error(String(e)),
|
|
52
|
+
});
|
|
53
|
+
const clearMut = useMutation({
|
|
54
|
+
mutationFn: notificationNotificationsClearPost,
|
|
55
|
+
onSettled: invalidate,
|
|
56
|
+
onError: e => message.error(String(e)),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const items = (data ?? []).filter(x => cat === "all" || x.category === cat);
|
|
60
|
+
|
|
61
|
+
const handleClick = (item: Item) => {
|
|
62
|
+
if (!item.isRead)
|
|
63
|
+
readMut.mutate(item.id);
|
|
64
|
+
// 深链与铃铛弹层同约定:标记已读后新标签打开
|
|
65
|
+
if (item.link)
|
|
66
|
+
window.open(item.link, "_blank", "noopener");
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const hasUnread = (data ?? []).some(x => !x.isRead);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="max-w-2xl mx-auto p-4">
|
|
73
|
+
<div className="flex items-center justify-between mb-3">
|
|
74
|
+
<Segmented
|
|
75
|
+
value={cat}
|
|
76
|
+
onChange={v => setCat(v as CatFilter)}
|
|
77
|
+
options={[
|
|
78
|
+
{ label: t("center.tabAll"), value: "all" },
|
|
79
|
+
{ label: t("center.category.notice"), value: "notice" },
|
|
80
|
+
{ label: t("center.category.pm"), value: "pm" },
|
|
81
|
+
{ label: t("center.category.mention"), value: "mention" },
|
|
82
|
+
]}
|
|
83
|
+
/>
|
|
84
|
+
<div className="flex items-center gap-2">
|
|
85
|
+
<Tooltip title={hasUnread ? t("center.markAllRead") : null}>
|
|
86
|
+
<Button
|
|
87
|
+
disabled={!hasUnread || readAllMut.isPending}
|
|
88
|
+
icon={<CheckOutlined />}
|
|
89
|
+
onClick={() => readAllMut.mutate()}
|
|
90
|
+
>
|
|
91
|
+
{t("center.markAllRead")}
|
|
92
|
+
</Button>
|
|
93
|
+
</Tooltip>
|
|
94
|
+
<Popconfirm title={t("center.clearConfirm")} onConfirm={() => clearMut.mutate()}>
|
|
95
|
+
<Button danger disabled={!(data ?? []).length || clearMut.isPending} icon={<ClearOutlined />}>
|
|
96
|
+
{t("center.clear")}
|
|
97
|
+
</Button>
|
|
98
|
+
</Popconfirm>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
<List
|
|
102
|
+
loading={isPending}
|
|
103
|
+
itemLayout="horizontal"
|
|
104
|
+
locale={{ emptyText: <Empty description={t("center.empty")} /> }}
|
|
105
|
+
dataSource={items}
|
|
106
|
+
renderItem={item => (
|
|
107
|
+
<List.Item className="cursor-pointer hover:bg-gray-100" onClick={() => handleClick(item)}>
|
|
108
|
+
<List.Item.Meta
|
|
109
|
+
avatar={<Avatar src={item.avatar || undefined}>{item.title.slice(0, 1)}</Avatar>}
|
|
110
|
+
title={(
|
|
111
|
+
<span className="flex items-center gap-2">
|
|
112
|
+
{!item.isRead && <span className="w-2 h-2 rounded bg-blue-600 inline-block shrink-0" />}
|
|
113
|
+
<span className="font-semibold">{item.title}</span>
|
|
114
|
+
<Tag color={CAT_COLOR[item.category ?? "notice"]}>
|
|
115
|
+
{t(`center.category.${item.category ?? "notice"}`)}
|
|
116
|
+
</Tag>
|
|
117
|
+
</span>
|
|
118
|
+
)}
|
|
119
|
+
description={(
|
|
120
|
+
<div className="flex flex-col gap-1">
|
|
121
|
+
<span>{item.message}</span>
|
|
122
|
+
<span className="text-xs text-muted-foreground">{item.date}</span>
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
/>
|
|
126
|
+
</List.Item>
|
|
127
|
+
)}
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { defineModule } from "@oj-module/runtime";
|
|
1
|
+
import { defineModule, OutsidePageIcon } from "@oj-module/runtime";
|
|
2
|
+
import { createElement } from "react";
|
|
2
3
|
import MarketApp from "./pages/MarketApp";
|
|
3
4
|
|
|
4
5
|
// 方案市场前端模块(第一方专用,R29:消费 /api/_platform/market/* 编排面,第三方方案不得仿照此调用面)。
|
|
@@ -10,7 +11,12 @@ export default defineModule({
|
|
|
10
11
|
routes: [
|
|
11
12
|
{
|
|
12
13
|
path: "/oj-market",
|
|
13
|
-
handle: {
|
|
14
|
+
handle: {
|
|
15
|
+
layout: "container",
|
|
16
|
+
order: 200,
|
|
17
|
+
title: "oj-market:menu.main",
|
|
18
|
+
icon: createElement(OutsidePageIcon),
|
|
19
|
+
},
|
|
14
20
|
children: [
|
|
15
21
|
{
|
|
16
22
|
index: true,
|
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
import { z } from "@oj-module/runtime";
|
|
5
5
|
|
|
6
6
|
export const schemas = {
|
|
7
|
+
"personalCenterProfilePost": {
|
|
8
|
+
body: z.object({
|
|
9
|
+
"username": z.string().optional(),
|
|
10
|
+
"email": z.string().optional(),
|
|
11
|
+
"phoneNumber": z.string().optional(),
|
|
12
|
+
"description": z.string().optional()
|
|
13
|
+
}),
|
|
14
|
+
data: z.null(),
|
|
15
|
+
},
|
|
7
16
|
"personalCenterUploadPost": {
|
|
8
17
|
data: z.string(),
|
|
9
18
|
},
|
|
@@ -33,7 +33,7 @@ export function createPersonalCenterClient(ctx: ModuleContext) {
|
|
|
33
33
|
}
|
|
34
34
|
ctx.register.apiPrefix(API_PREFIX);
|
|
35
35
|
bindRequest(ctx.utils.request);
|
|
36
|
-
return { personalCenterUploadPost };
|
|
36
|
+
return { personalCenterProfilePost, personalCenterUploadPost };
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
@@ -64,6 +64,30 @@ async function toApiError(e: unknown): Promise<unknown> {
|
|
|
64
64
|
return e;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
export type PersonalCenterProfilePostBody = z.input<(typeof schemas)["personalCenterProfilePost"]["body"]>;
|
|
68
|
+
export type PersonalCenterProfilePostData = z.infer<(typeof schemas)["personalCenterProfilePost"]["data"]>;
|
|
69
|
+
|
|
70
|
+
export async function personalCenterProfilePost(body: PersonalCenterProfilePostBody): Promise<PersonalCenterProfilePostData> {
|
|
71
|
+
const client = ensureReq();
|
|
72
|
+
try {
|
|
73
|
+
const env = await client.post(`personal-center/profile`, { json: body }).json<OjEnvelope<PersonalCenterProfilePostData>>();
|
|
74
|
+
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
75
|
+
if (typeof env.code === "number" && env.code !== 0)
|
|
76
|
+
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
77
|
+
const data = env.data as PersonalCenterProfilePostData;
|
|
78
|
+
if (import.meta.env.DEV) {
|
|
79
|
+
const { schemas } = await import("./api.schemas");
|
|
80
|
+
const r = schemas["personalCenterProfilePost"].data.safeParse(data);
|
|
81
|
+
if (!r.success)
|
|
82
|
+
throw new ContractApiError(-1, `[契约违例] personalCenterProfilePost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
83
|
+
}
|
|
84
|
+
return data;
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
throw await toApiError(e);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
67
91
|
export type PersonalCenterUploadPostData = z.infer<(typeof schemas)["personalCenterUploadPost"]["data"]>;
|
|
68
92
|
|
|
69
93
|
export async function personalCenterUploadPost(): Promise<PersonalCenterUploadPostData> {
|
|
@@ -3,6 +3,8 @@ import { ProfileCardIcon, RiAccountCircleLine, useAuthStore } from "@oj-module/r
|
|
|
3
3
|
|
|
4
4
|
import { createElement, lazy } from "react";
|
|
5
5
|
|
|
6
|
+
import { createPersonalCenterClient } from "./__client/api";
|
|
7
|
+
|
|
6
8
|
const MyProfile = lazy(() => import("./pages/my-profile"));
|
|
7
9
|
|
|
8
10
|
const routes: AppRouteRecordRaw[] = [
|
|
@@ -40,6 +42,9 @@ const mod: ModuleDefinition = {
|
|
|
40
42
|
// P4-3:接管头像上传端点(D9)。antd Upload 直连 action,headers 在上传瞬间
|
|
41
43
|
// 取当前 token 注入 Bearer(token 存 runtime auth store,跨渲染期读 getState)。
|
|
42
44
|
async onInit(ctx) {
|
|
45
|
+
// 构造即接线:登记契约前缀 /personal-center + 绑定 scoped request,
|
|
46
|
+
// 页面直接 import 的 personalCenterProfilePost 才有可用的请求通道
|
|
47
|
+
createPersonalCenterClient(ctx);
|
|
43
48
|
const provider: UploadApiProvider = {
|
|
44
49
|
action: "/api/personal-center/upload",
|
|
45
50
|
headers: (): Record<string, string> => {
|
|
@@ -5,10 +5,13 @@ import {
|
|
|
5
5
|
ProFormTextArea,
|
|
6
6
|
} from "@ant-design/pro-components";
|
|
7
7
|
import { BasicContent, FormAvatarItem, useUserStore } from "@oj-module/runtime";
|
|
8
|
-
import { Form, Input } from "antd";
|
|
8
|
+
import { App, Form, Input } from "antd";
|
|
9
|
+
|
|
10
|
+
import { personalCenterProfilePost } from "../../__client/api";
|
|
9
11
|
|
|
10
12
|
export default function Profile() {
|
|
11
13
|
const currentUser = useUserStore();
|
|
14
|
+
const { message } = App.useApp();
|
|
12
15
|
const getAvatarURL = () => {
|
|
13
16
|
if (currentUser) {
|
|
14
17
|
if (currentUser.avatar) {
|
|
@@ -20,8 +23,27 @@ export default function Profile() {
|
|
|
20
23
|
return "";
|
|
21
24
|
};
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
// 真实入库:POST /api/personal-center/profile(只传资料四字段,avatar 走 upload 通道),
|
|
27
|
+
// 成功后重拉 user-info 刷新全局 store(表单 initialValues 随之更新)。
|
|
28
|
+
const handleFinish = async (values: {
|
|
29
|
+
username?: string
|
|
30
|
+
email?: string
|
|
31
|
+
phoneNumber?: string
|
|
32
|
+
description?: string
|
|
33
|
+
}) => {
|
|
34
|
+
try {
|
|
35
|
+
await personalCenterProfilePost({
|
|
36
|
+
username: values.username,
|
|
37
|
+
email: values.email,
|
|
38
|
+
phoneNumber: values.phoneNumber,
|
|
39
|
+
description: values.description,
|
|
40
|
+
});
|
|
41
|
+
await useUserStore.getState().getUserInfo();
|
|
42
|
+
message.success("更新基本信息成功");
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
message.error(`更新基本信息失败:${e instanceof Error ? e.message : String(e)}`);
|
|
46
|
+
}
|
|
25
47
|
};
|
|
26
48
|
|
|
27
49
|
return (
|
package/templates/web.config.ts
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
* 模块工程配置:共享依赖一律不进 dependencies(宿主 importmap 提供),
|
|
3
3
|
* 这里只登记模块清单(entry 相对本工程根)。
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* 注意:这里的登记顺序只影响模块加载顺序,**不决定侧边栏导航顺序**——
|
|
6
|
+
* 菜单顺序由各模块 entry 路由的 handle.order 决定(≥0 主菜单区升序;
|
|
7
|
+
* <0 底部固定区,pin 在侧边栏底部;同 order 按根 path 字母序)。
|
|
8
|
+
* `home` 必须第一个登记(shell 的 "/" 重定向到它)。
|
|
6
9
|
*/
|
|
7
10
|
const allModules = [
|
|
8
11
|
// /home 必须是首个模块:shell 的 "/" → VITE_BASE_HOME_PATH(=/home)重定向
|
|
9
12
|
{ name: "home", entry: "web/src/home/entry.ts" },
|
|
10
|
-
{ name: "demo", entry: "web/src/demo/entry.ts" },
|
|
13
|
+
// { name: "demo", entry: "web/src/demo/entry.ts" },
|
|
11
14
|
// /login 路由由模块提供:shell 宿主不挂 runtime 内置 baseRoutes
|
|
12
15
|
{ name: "login", entry: "web/src/login/entry.ts" },
|
|
13
16
|
// 个人中心:runtime 用户菜单固定导航 /personal-center/my-profile,缺则点击落空
|
|
@@ -25,11 +28,11 @@ const allModules = [
|
|
|
25
28
|
// 用于把 templates 当可运行 demo 直接跑;其文件不在 `ojm init` 的
|
|
26
29
|
// init.include.yaml 白名单里,不拷入新工程,故本区间连同这些条目会被
|
|
27
30
|
// init 一并剥离,脚手架产出的配置不会残留指向缺失文件的死条目。
|
|
28
|
-
{ name: "about", entry: "web/src/about/entry.ts" },
|
|
29
|
-
{ name: "access", entry: "web/src/access/entry.ts" },
|
|
30
|
-
{ name: "exception", entry: "web/src/exception/entry.ts" },
|
|
31
|
-
{ name: "outside", entry: "web/src/outside/entry.ts" },
|
|
32
|
-
{ name: "route-nest", entry: "web/src/route-nest/entry.ts" },
|
|
31
|
+
// { name: "about", entry: "web/src/about/entry.ts" },
|
|
32
|
+
// { name: "access", entry: "web/src/access/entry.ts" },
|
|
33
|
+
// { name: "exception", entry: "web/src/exception/entry.ts" },
|
|
34
|
+
// { name: "outside", entry: "web/src/outside/entry.ts" },
|
|
35
|
+
// { name: "route-nest", entry: "web/src/route-nest/entry.ts" },
|
|
33
36
|
// #endregion demo-only
|
|
34
37
|
];
|
|
35
38
|
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
"react-router": "7.18.2",
|
|
9
9
|
"react-router/dom": "7.18.2",
|
|
10
10
|
"@tanstack/react-query": "5.102.8",
|
|
11
|
-
"@oj-module/runtime": "0.2.
|
|
12
|
-
"@oj-module/runtime/contract": "0.2.
|
|
13
|
-
"@oj-module/runtime/contract/errors": "0.2.
|
|
11
|
+
"@oj-module/runtime": "0.2.14",
|
|
12
|
+
"@oj-module/runtime/contract": "0.2.14",
|
|
13
|
+
"@oj-module/runtime/contract/errors": "0.2.14",
|
|
14
14
|
"@plane/kit": "0.1.20",
|
|
15
15
|
"@plane/editor": "0.1.20",
|
|
16
16
|
"antd": "6.6.1",
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
// GET /api/notifications —— 通知列表(runtime 通知铃的内置 root 级兜底端点)。
|
|
2
|
-
//
|
|
3
|
-
// runtime 的 fetchNotifications 在未注册 notificationsApi provider 时,直接
|
|
4
|
-
// `request.get("notifications")` → 本端点;返回 NotificationItem[](信封内 data)。
|
|
5
|
-
// 查 notifications 表,is_read(0/1) 映射为布尔 isRead;Bearer 守卫保护。
|
|
6
|
-
//
|
|
7
|
-
// G5:本目录只保留只读兜底——表结构/种子迁归 api/src/notification/ 模块
|
|
8
|
-
// (评审 b3 双模块同表冲突决策);查询须返回 id(G4 NotificationItem.id 必填)。
|
|
9
|
-
//
|
|
10
|
-
// 注意:root 级路径无法写进业务契约(defineApi 要求 route 以 "/" 开头),
|
|
11
|
-
// 故本模块已在 api/.ojm-api-exempt.json 豁免,ojm api --check 不会误报。
|
|
12
|
-
export default {
|
|
13
|
-
async get() {
|
|
14
|
-
try {
|
|
15
|
-
const rows = await db.table("notifications").select(["id", "avatar", "date", "is_read", "message", "title"]).orderBy([{ field: "id", dir: "desc" }]).all();
|
|
16
|
-
json.ok(rows.map(r => ({
|
|
17
|
-
id: Number(r.id),
|
|
18
|
-
avatar: String(r.avatar),
|
|
19
|
-
date: String(r.date),
|
|
20
|
-
isRead: Number(r.is_read) === 1,
|
|
21
|
-
message: String(r.message),
|
|
22
|
-
title: String(r.title),
|
|
23
|
-
})));
|
|
24
|
-
}
|
|
25
|
-
catch (e) {
|
|
26
|
-
json.fail(500, String(e));
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
};
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
/* eslint-disable */
|
|
2
|
-
// 生成物:ojm api 从契约生成,勿手改(改动请改契约文件后重跑 ojm api)
|
|
3
|
-
import { ContractApiError } from "@oj-module/runtime/contract/errors";
|
|
4
|
-
import type { ScopedRequestLike } from "@oj-module/runtime/contract/errors";
|
|
5
|
-
import type { ModuleContext } from "@oj-module/runtime";
|
|
6
|
-
import type { z } from "@oj-module/runtime";
|
|
7
|
-
import type { schemas } from "./api.schemas";
|
|
8
|
-
|
|
9
|
-
/** oj 信封(AC-D16):code=0 成功;非 0 时 HTTP status=code,由 toApiError 归一为 ContractApiError */
|
|
10
|
-
interface OjEnvelope<T> { code: number, msg?: string, data?: T }
|
|
11
|
-
|
|
12
|
-
let req: ScopedRequestLike | undefined;
|
|
13
|
-
|
|
14
|
-
/** 线上 API 前缀(ojm api 从契约抽取,唯一真源,勿手改):请求基址 + 前缀登记用 */
|
|
15
|
-
export const API_PREFIX = "/notifications";
|
|
16
|
-
|
|
17
|
-
/** 认领身份前缀(契约 apiPrefix,字面等于模块目录名):跨模块认领护栏对比基准 */
|
|
18
|
-
export const MODULE_PREFIX = "/notifications";
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* 模块 entry 的 onInit 里一行创建(构造即 install):完成 apiPrefix 登记 +
|
|
22
|
-
* scoped request 绑定,返回绑好前缀的端点函数集合。一模块一 client;
|
|
23
|
-
* 重复构造幂等安全(前缀重复登记同值、request 重绑同值)。
|
|
24
|
-
*/
|
|
25
|
-
export function createNotificationsClient(ctx: ModuleContext) {
|
|
26
|
-
// 跨模块认领护栏(DEV):工厂知道自己该属于谁(MODULE_PREFIX = 契约 apiPrefix),
|
|
27
|
-
// ctx 又带 module.name——名实不符说明别的模块在替本模块创建 client,会共享 request
|
|
28
|
-
// 单槽、卸载互相耦合;跨模块需求应由本模块创建后经 provider 暴露。
|
|
29
|
-
// 注意对比基准是 MODULE_PREFIX(目录名)而非 API_PREFIX——D-M8 映射时线上前缀
|
|
30
|
-
// 属于别的命名空间,认领身份仍按目录名。
|
|
31
|
-
if (import.meta.env.DEV && ctx.module.name !== MODULE_PREFIX.slice(1)) {
|
|
32
|
-
console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${MODULE_PREFIX.slice(1)}" 模块在自身 onInit 创建,跨模块需求经 provider 暴露。`);
|
|
33
|
-
}
|
|
34
|
-
ctx.register.apiPrefix(API_PREFIX);
|
|
35
|
-
bindRequest(ctx.utils.request);
|
|
36
|
-
return { notificationsGet };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* @deprecated 请改用 createNotificationsClient(ctx)——工厂构造即完成
|
|
41
|
-
* 前缀登记 + request 绑定,不会出现「登记了前缀没绑 request」的半接线状态。
|
|
42
|
-
*/
|
|
43
|
-
export function bindRequest(r: ScopedRequestLike): void {
|
|
44
|
-
req = r;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function ensureReq(): ScopedRequestLike {
|
|
48
|
-
if (!req)
|
|
49
|
-
throw new ContractApiError(-1, "[ojm-api] 请求未绑定——请在模块 entry.ts 的 onInit 里调用 createNotificationsClient(ctx)(或 bindRequest(ctx.utils.request))。");
|
|
50
|
-
return req;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** ky HTTPError → ContractApiError(错误体为信封时取 code/msg);契约违例与原错误原样透传 */
|
|
54
|
-
async function toApiError(e: unknown): Promise<unknown> {
|
|
55
|
-
if (e instanceof ContractApiError)
|
|
56
|
-
return e;
|
|
57
|
-
// ky 2.x 构造 HTTPError 时已用 getReader() 消费掉原 response 体(res.clone().json() 按规范抛 TypeError,
|
|
58
|
-
// 体方法一律失效),解析好的错误体在公开字段 error.data——故读 error.data,勿改回 clone().json()。
|
|
59
|
-
const err = e as { data?: { code?: unknown, msg?: string }, response?: Response } | null;
|
|
60
|
-
const res = err?.response;
|
|
61
|
-
const d = err?.data;
|
|
62
|
-
if (res instanceof Response && d && typeof d.code === "number")
|
|
63
|
-
return new ContractApiError(d.code, d.msg ?? res.statusText);
|
|
64
|
-
return e;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export async function notificationsGet(): Promise<unknown> {
|
|
68
|
-
const client = ensureReq();
|
|
69
|
-
try {
|
|
70
|
-
const env = await client.get(`notifications`).json<OjEnvelope<unknown>>();
|
|
71
|
-
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
72
|
-
if (typeof env.code === "number" && env.code !== 0)
|
|
73
|
-
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
74
|
-
const data = env.data as unknown;
|
|
75
|
-
return data;
|
|
76
|
-
}
|
|
77
|
-
catch (e) {
|
|
78
|
-
throw await toApiError(e);
|
|
79
|
-
}
|
|
80
|
-
}
|