@allbluecn/admin-app 0.3.5 → 0.3.6

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.
Files changed (39) hide show
  1. package/package.json +11 -11
  2. package/src/components/__tests__/user-avatar.test.tsx +39 -0
  3. package/src/components/admin/admin-user-menu.tsx +2 -2
  4. package/src/components/auth/admin-login-form.tsx +2 -2
  5. package/src/components/{knowledge-public → knowledge/public}/bookmark-form-dialog.tsx +4 -4
  6. package/src/components/{knowledge-public → knowledge/public}/compliance-form-dialog.tsx +4 -4
  7. package/src/components/{knowledge-public → knowledge/public}/legal-form-dialog.tsx +4 -4
  8. package/src/components/ui/checkbox.tsx +4 -4
  9. package/src/components/user/columns.tsx +1 -1
  10. package/src/components/user-avatar.tsx +7 -4
  11. package/src/lib/__tests__/avatar-shape.test.ts +44 -0
  12. package/src/lib/auth.config.ts +1 -1
  13. package/src/lib/avatar-shape.ts +35 -0
  14. package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +10 -10
  15. package/src/routes/_nav/knowledge-public/bookmarks/route.tsx +1 -1
  16. package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +8 -8
  17. package/src/routes/_nav/knowledge-public/compliance/route.tsx +1 -1
  18. package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +8 -8
  19. package/src/routes/_nav/knowledge-public/legal/route.tsx +1 -1
  20. package/src/routes/_nav/users/$userId/route.lazy.tsx +1 -1
  21. package/src/routes/login/route.lazy.tsx +1 -1
  22. package/src/server/{knowledge-public → knowledge/public}/github-sync.ts +12 -2
  23. package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/create.ts +1 -1
  24. package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/list.ts +2 -1
  25. package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/sync-all-github.ts +3 -2
  26. package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/update.ts +3 -2
  27. package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/create.ts +1 -1
  28. package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/list.ts +2 -1
  29. package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/update.ts +3 -2
  30. package/src/server/serverFns/{knowledge-public → knowledge/public}/legal/create.ts +1 -1
  31. package/src/server/serverFns/{knowledge-public → knowledge/public}/legal/list.ts +2 -1
  32. package/src/server/serverFns/{knowledge-public → knowledge/public}/legal/update.ts +3 -2
  33. /package/src/server/{knowledge-public → knowledge/public}/canonical-id.ts +0 -0
  34. /package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/delete.ts +0 -0
  35. /package/src/server/serverFns/{knowledge-public → knowledge/public}/bookmarks/detail.ts +0 -0
  36. /package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/delete.ts +0 -0
  37. /package/src/server/serverFns/{knowledge-public → knowledge/public}/compliance/detail.ts +0 -0
  38. /package/src/server/serverFns/{knowledge-public → knowledge/public}/legal/delete.ts +0 -0
  39. /package/src/server/serverFns/{knowledge-public → knowledge/public}/legal/detail.ts +0 -0
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@allbluecn/admin-app",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "private": false,
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "type": "module",
7
7
  "dependencies": {
8
- "@allbluecn/database": "0.9.2",
9
- "@allbluecn/kernel": "0.9.1",
8
+ "@allbluecn/database": "0.9.3",
9
+ "@allbluecn/kernel": "0.10.0",
10
10
  "@auth/core": "^0.41.2",
11
11
  "@base-ui/react": "^1.8.0",
12
12
  "@fontsource-variable/inter": "^5.3.0",
@@ -21,15 +21,15 @@
21
21
  "@radix-ui/react-tooltip": "^1.2.8",
22
22
  "@sentry/tanstackstart-react": "^10.67.0",
23
23
  "@tabler/icons-react": "^3.44.0",
24
- "@tanstack/charts": "^0.16.0",
25
- "@tanstack/react-form": "2.0.0-alpha.2",
24
+ "@tanstack/charts": "^1.1.0",
25
+ "@tanstack/react-form": "2.0.0-alpha.3",
26
26
  "@tanstack/react-pacer": "^0.24.1",
27
- "@tanstack/react-query": "^5.104.1",
28
- "@tanstack/react-router": "1.170.41",
27
+ "@tanstack/react-query": "^5.104.5",
28
+ "@tanstack/react-router": "1.170.46",
29
29
  "@tanstack/react-router-ssr-query": "^1.167.3",
30
- "@tanstack/react-start": "1.168.60",
31
- "@tanstack/react-table": "^9.2.6",
32
- "@tanstack/react-virtual": "^3.14.13",
30
+ "@tanstack/react-start": "1.169.4",
31
+ "@tanstack/react-table": "^9.3.1",
32
+ "@tanstack/react-virtual": "^3.14.22",
33
33
  "@tanstack/store": "^0.11.2",
34
34
  "bcryptjs": "^3.0.3",
35
35
  "class-variance-authority": "^0.7.1",
@@ -57,7 +57,7 @@
57
57
  "@inlang/paraglide-js": "^2.25.0",
58
58
  "@inlang/plugin-message-format": "^4.4.4",
59
59
  "@tailwindcss/vite": "^4.3.3",
60
- "@tanstack/react-query-devtools": "5.104.1",
60
+ "@tanstack/react-query-devtools": "5.104.5",
61
61
  "@tanstack/react-router-devtools": "1.167.2",
62
62
  "@testing-library/jest-dom": "7.0.1",
63
63
  "@testing-library/react": "^16.3.2",
@@ -0,0 +1,39 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { describe, it, expect } from 'vitest'
19
+ import { render } from '@testing-library/react'
20
+ import { UserAvatar } from '../user-avatar'
21
+
22
+ // 与 web 端 shared-kit/avatars/__tests__/user-avatar.test.tsx 同构:
23
+ // bgShape 是 DB 字典直通的 string|null,渲染前经 toAvatarShape 归一
24
+ describe('UserAvatar bgShape 归一', () => {
25
+ it('合法 rounded 渲染 rounded-lg', () => {
26
+ const { container } = render(<UserAvatar name="张三" bgShape="rounded" />)
27
+ expect(container.firstElementChild?.className).toContain('rounded-lg')
28
+ })
29
+
30
+ it('不传 bgShape fallback 走 square(rounded-none)', () => {
31
+ const { container } = render(<UserAvatar name="张三" />)
32
+ expect(container.firstElementChild?.className).toContain('rounded-none')
33
+ })
34
+
35
+ it('非法 bgShape 不崩溃,静默归 square(rounded-none)', () => {
36
+ const { container } = render(<UserAvatar name="张三" bgShape="weird-shape" />)
37
+ expect(container.firstElementChild?.className).toContain('rounded-none')
38
+ })
39
+ })
@@ -28,7 +28,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
28
28
  import { useRouter, useMatches } from "@tanstack/react-router";
29
29
  import { signOut, useSession } from "@/lib/auth-client";
30
30
  import { UserAvatar } from "@/components/user-avatar";
31
- import type { AvatarConfig, ShapeType } from "@allbluecn/kernel";
31
+ import type { AvatarConfig } from "@allbluecn/kernel";
32
32
 
33
33
  const ROLE_LABELS: Record<string, () => string> = {
34
34
  ADMIN: m.common_common_role_ADMIN,
@@ -53,7 +53,7 @@ export function AdminUserMenu() {
53
53
  const userRole = (user as { role?: string })?.role || "ADMIN";
54
54
  const roleLabel = (ROLE_LABELS[userRole] ?? (() => m.common_login_admin()))();
55
55
  const avatarConfig = (user as { avatarConfig?: AvatarConfig })?.avatarConfig;
56
- const bgShape = (user as { bgShape?: string })?.bgShape as ShapeType | undefined;
56
+ const bgShape = (user as { bgShape?: string })?.bgShape;
57
57
  const bgColor = (user as { bgColor?: string })?.bgColor;
58
58
 
59
59
  const handleExitAdmin = async () => {
@@ -25,7 +25,7 @@ import { Field, FieldLabel } from "@/components/ui/field";
25
25
  import { Card, CardContent } from "@/components/ui/card";
26
26
  import { toast } from '@/components/ui/toast';
27
27
  import { UserAvatar } from "@/components/user-avatar";
28
- import type { AvatarConfig, ShapeType } from "@allbluecn/kernel";
28
+ import type { AvatarConfig } from "@allbluecn/kernel";
29
29
  import { AuthLayout, AuthLayoutLeft, AuthLayoutRight } from "@/components/auth/auth-layout";
30
30
  import { AuthHeader } from "@/components/auth/auth-header";
31
31
  import { AuthBrandPanel } from "@/components/auth/auth-brand-panel";
@@ -126,7 +126,7 @@ export default function AdminLoginForm({ user, isAdmin }: AdminLoginFormProps) {
126
126
  <CardContent className="flex h-full items-center gap-3 px-3 py-3">
127
127
  <UserAvatar
128
128
  avatarConfig={user?.avatarConfig}
129
- bgShape={user?.bgShape as ShapeType | undefined}
129
+ bgShape={user?.bgShape}
130
130
  bgColor={user?.bgColor}
131
131
  name={user?.name || user?.email || ""}
132
132
  size="lg"
@@ -29,9 +29,9 @@ import {
29
29
  DialogHeader,
30
30
  DialogTitle,
31
31
  } from '@/components/ui/dialog'
32
- import { adminCreateBookmarkFn } from '@/server/serverFns/knowledge-public/bookmarks/create'
33
- import { adminUpdateBookmarkFn } from '@/server/serverFns/knowledge-public/bookmarks/update'
34
- import { adminGetBookmarkDetailFn } from '@/server/serverFns/knowledge-public/bookmarks/detail'
32
+ import { adminCreateBookmarkFn } from '@/server/serverFns/knowledge/public/bookmarks/create'
33
+ import { adminUpdateBookmarkFn } from '@/server/serverFns/knowledge/public/bookmarks/update'
34
+ import { adminGetBookmarkDetailFn } from '@/server/serverFns/knowledge/public/bookmarks/detail'
35
35
 
36
36
  export interface BookmarkRow {
37
37
  id: string
@@ -80,7 +80,7 @@ export function BookmarkFormDialog({ open, onOpenChange, item, onSuccess }: Book
80
80
  if (item) {
81
81
  setLoading(true)
82
82
  adminGetBookmarkDetailFn({ data: { id: item.id } })
83
- .then((d: any) => {
83
+ .then((d: Awaited<ReturnType<typeof adminGetBookmarkDetailFn>>) => {
84
84
  setForm({
85
85
  title: d.title ?? '',
86
86
  url: d.url ?? '',
@@ -29,9 +29,9 @@ import {
29
29
  DialogHeader,
30
30
  DialogTitle,
31
31
  } from '@/components/ui/dialog'
32
- import { adminCreateComplianceFn } from '@/server/serverFns/knowledge-public/compliance/create'
33
- import { adminUpdateComplianceFn } from '@/server/serverFns/knowledge-public/compliance/update'
34
- import { adminGetComplianceDetailFn } from '@/server/serverFns/knowledge-public/compliance/detail'
32
+ import { adminCreateComplianceFn } from '@/server/serverFns/knowledge/public/compliance/create'
33
+ import { adminUpdateComplianceFn } from '@/server/serverFns/knowledge/public/compliance/update'
34
+ import { adminGetComplianceDetailFn } from '@/server/serverFns/knowledge/public/compliance/detail'
35
35
 
36
36
  export interface ComplianceRow {
37
37
  id: string
@@ -80,7 +80,7 @@ export function ComplianceFormDialog({ open, onOpenChange, item, onSuccess }: Co
80
80
  if (item) {
81
81
  setLoading(true)
82
82
  adminGetComplianceDetailFn({ data: { id: item.id } })
83
- .then((d: any) => {
83
+ .then((d: Awaited<ReturnType<typeof adminGetComplianceDetailFn>>) => {
84
84
  setForm({
85
85
  title: d.title ?? '',
86
86
  code: '',
@@ -37,9 +37,9 @@ import {
37
37
  DialogHeader,
38
38
  DialogTitle,
39
39
  } from '@/components/ui/dialog'
40
- import { adminCreateLegalFn } from '@/server/serverFns/knowledge-public/legal/create'
41
- import { adminUpdateLegalFn } from '@/server/serverFns/knowledge-public/legal/update'
42
- import { adminGetLegalDetailFn } from '@/server/serverFns/knowledge-public/legal/detail'
40
+ import { adminCreateLegalFn } from '@/server/serverFns/knowledge/public/legal/create'
41
+ import { adminUpdateLegalFn } from '@/server/serverFns/knowledge/public/legal/update'
42
+ import { adminGetLegalDetailFn } from '@/server/serverFns/knowledge/public/legal/detail'
43
43
 
44
44
  export interface LegalRow {
45
45
  id: string
@@ -106,7 +106,7 @@ export function LegalFormDialog({ open, onOpenChange, item, onSuccess }: LegalFo
106
106
  if (item) {
107
107
  setLoading(true)
108
108
  adminGetLegalDetailFn({ data: { id: item.id } })
109
- .then((d: any) => {
109
+ .then((d: Awaited<ReturnType<typeof adminGetLegalDetailFn>>) => {
110
110
  setForm({
111
111
  title: d.title ?? '',
112
112
  code: '',
@@ -1,12 +1,13 @@
1
1
 
2
2
  import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
3
3
  import { cn } from "cn"
4
- import { CheckIcon } from "lucide-react"
4
+ import { CheckIcon, MinusIcon } from "lucide-react"
5
5
 
6
- function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
6
+ function Checkbox({ className, indeterminate, ...props }: CheckboxPrimitive.Root.Props) {
7
7
  return (
8
8
  <CheckboxPrimitive.Root
9
9
  data-slot="checkbox"
10
+ indeterminate={indeterminate}
10
11
  className={cn(
11
12
  "peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input shadow-xs transition-shadow outline-none group-has-disabled/field:opacity-50 group-has-[:focus-visible]/field-label:ring-0 group-has-[:focus-visible]/field-label:not-data-checked:border-input after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground group-has-[:focus-visible]/field-label:data-checked:border-primary dark:data-checked:bg-primary",
12
13
  className
@@ -17,8 +18,7 @@ function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
17
18
  data-slot="checkbox-indicator"
18
19
  className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
19
20
  >
20
- <CheckIcon
21
- />
21
+ {indeterminate ? <MinusIcon /> : <CheckIcon />}
22
22
  </CheckboxPrimitive.Indicator>
23
23
  </CheckboxPrimitive.Root>
24
24
  )
@@ -79,7 +79,7 @@ export function getUserColumns(onView: (user: UserRow) => void): AdminColumnDef<
79
79
  <div className="flex items-center gap-2">
80
80
  <UserAvatar
81
81
  avatarConfig={user.avatarConfig as AvatarConfig | null}
82
- bgShape={user.bgShape as "square" | "rounded" | null}
82
+ bgShape={user.bgShape}
83
83
  bgColor={user.bgColor}
84
84
  name={user.username}
85
85
  size="md"
@@ -17,11 +17,13 @@
17
17
 
18
18
 
19
19
  import { NotionAvatar } from "./notion-avatar"
20
- import type { AvatarConfig, ShapeType } from "@allbluecn/kernel"
20
+ import { toAvatarShape } from "@/lib/avatar-shape"
21
+ import type { AvatarConfig } from "@allbluecn/kernel"
21
22
 
22
23
  type UserAvatarProps = {
23
24
  avatarConfig?: AvatarConfig | null
24
- bgShape?: ShapeType | null
25
+ /** DB 字典直通的 string|null,内部经 toAvatarShape 归一,非法值回落 square */
26
+ bgShape?: string | null
25
27
  bgColor?: string | null
26
28
  name?: string | null
27
29
  imageUrl?: string | null
@@ -50,14 +52,15 @@ export function UserAvatar({
50
52
  size = "md",
51
53
  }: UserAvatarProps) {
52
54
  const initial = (name || "U").charAt(0).toUpperCase()
55
+ const shape = toAvatarShape(bgShape) ?? "square"
53
56
  const shapeClass =
54
- bgShape === "circle" ? "rounded-full" : bgShape === "square" ? "rounded-none" : "rounded-lg"
57
+ shape === "circle" ? "rounded-full" : shape === "square" ? "rounded-none" : "rounded-lg"
55
58
 
56
59
  if (avatarConfig) {
57
60
  return (
58
61
  <NotionAvatar
59
62
  config={avatarConfig}
60
- shape={bgShape ?? "square"}
63
+ shape={shape}
61
64
  bgColor={bgColor ?? "#fff"}
62
65
  className={`${sizeClasses[size]} shrink-0 ${className ?? ""}`}
63
66
  />
@@ -0,0 +1,44 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { describe, it, expect } from "vitest";
19
+ import { AVATAR_SHAPES, toAvatarShape } from "../avatar-shape";
20
+
21
+ describe("AVATAR_SHAPES", () => {
22
+ it("与 kernel ShapeType 三值一致", () => {
23
+ expect([...AVATAR_SHAPES]).toEqual(["circle", "rounded", "square"]);
24
+ });
25
+ });
26
+
27
+ describe("toAvatarShape", () => {
28
+ it("三个合法值原样通过", () => {
29
+ expect(toAvatarShape("circle")).toBe("circle");
30
+ expect(toAvatarShape("rounded")).toBe("rounded");
31
+ expect(toAvatarShape("square")).toBe("square");
32
+ });
33
+
34
+ it("非法字符串返回 null", () => {
35
+ expect(toAvatarShape("weird")).toBeNull();
36
+ expect(toAvatarShape("CIRCLE")).toBeNull();
37
+ });
38
+
39
+ it("空串与空值返回 null", () => {
40
+ expect(toAvatarShape("")).toBeNull();
41
+ expect(toAvatarShape(null)).toBeNull();
42
+ expect(toAvatarShape(undefined)).toBeNull();
43
+ });
44
+ });
@@ -257,7 +257,7 @@ export const authConfig: StartAuthJSConfig = {
257
257
  user.role = token.role as string
258
258
  user.id = token.id as string
259
259
  user.avatarConfig = token.avatarConfig as Record<string, number> | undefined
260
- user.bgShape = token.bgShape as string | undefined
260
+ user.bgShape = typeof token.bgShape === "string" ? token.bgShape : undefined
261
261
  user.bgColor = token.bgColor as string | undefined
262
262
  if (token.name) user.name = token.name as string | null
263
263
  if (token.email) user.email = token.email as string | null
@@ -0,0 +1,35 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import type { ShapeType } from "@allbluecn/kernel"
19
+
20
+ export const AVATAR_SHAPES = [
21
+ "circle",
22
+ "rounded",
23
+ "square",
24
+ // satisfies 只防 kernel 收窄方向漂移(本数组塞进非 ShapeType 值会报错);
25
+ // kernel 新增形状值不会报错,会在 toAvatarShape 中静默归 null
26
+ ] as const satisfies readonly ShapeType[]
27
+
28
+ /**
29
+ * UserBrief.bgShape 是 DB 字典直通的 string|null,UI 底形只认 ShapeType:
30
+ * 运行时守卫替代断言撒谎型 cast,非法/空值返回 null(由消费端决定回落)。
31
+ * 与 web 端 shared-kit/avatars/avatar-shape.ts 同构(双端 UI 不共享,各自维护)。
32
+ */
33
+ export function toAvatarShape(v: string | null | undefined): ShapeType | null {
34
+ return AVATAR_SHAPES.find((shape) => shape === v) ?? null
35
+ }
@@ -46,9 +46,9 @@ import { toast } from '@/components/ui/toast'
46
46
  import {
47
47
  BookmarkFormDialog,
48
48
  type BookmarkRow,
49
- } from '@/components/knowledge-public/bookmark-form-dialog'
50
- import { adminDeleteBookmarkFn } from '@/server/serverFns/knowledge-public/bookmarks/delete'
51
- import { adminSyncAllGithubFn } from '@/server/serverFns/knowledge-public/bookmarks/sync-all-github'
49
+ } from '@/components/knowledge/public/bookmark-form-dialog'
50
+ import { adminDeleteBookmarkFn } from '@/server/serverFns/knowledge/public/bookmarks/delete'
51
+ import { adminSyncAllGithubFn } from '@/server/serverFns/knowledge/public/bookmarks/sync-all-github'
52
52
 
53
53
  export const Route = createLazyFileRoute('/_nav/knowledge-public/bookmarks')({
54
54
  pendingComponent: () => (
@@ -144,7 +144,7 @@ function BookmarksPage() {
144
144
  {
145
145
  accessorKey: 'title',
146
146
  header: m.common_knowledgePublic_bookmarks_columns_website(),
147
- cell: ({ row }: any) => {
147
+ cell: ({ row }) => {
148
148
  const b = row.original
149
149
  return (
150
150
  <div className="flex min-w-[180px] items-center gap-2">
@@ -164,7 +164,7 @@ function BookmarksPage() {
164
164
  {
165
165
  accessorKey: 'url',
166
166
  header: m.common_knowledgePublic_bookmarks_columns_url(),
167
- cell: ({ row }: any) => {
167
+ cell: ({ row }) => {
168
168
  const url = row.original.url
169
169
  return (
170
170
  <a
@@ -182,7 +182,7 @@ function BookmarksPage() {
182
182
  {
183
183
  accessorKey: 'categoryId',
184
184
  header: m.common_knowledgePublic_bookmarks_columns_category(),
185
- cell: ({ row }: any) =>
185
+ cell: ({ row }) =>
186
186
  row.original.categoryId ? (
187
187
  <Badge variant="outline" className="font-mono text-xs">
188
188
  {row.original.categoryId}
@@ -194,7 +194,7 @@ function BookmarksPage() {
194
194
  {
195
195
  id: 'github',
196
196
  header: 'GitHub',
197
- cell: ({ row }: any) => {
197
+ cell: ({ row }) => {
198
198
  const b = row.original
199
199
  if (!b.isGithub) return <span className="text-muted-foreground">-</span>
200
200
  return (
@@ -215,7 +215,7 @@ function BookmarksPage() {
215
215
  {
216
216
  accessorKey: 'tags',
217
217
  header: m.common_knowledgePublic_bookmarks_columns_tags(),
218
- cell: ({ row }: any) => (
218
+ cell: ({ row }) => (
219
219
  <div className="flex max-w-xs flex-wrap gap-1">
220
220
  {row.original.tags.length === 0 ? (
221
221
  <span className="text-muted-foreground">-</span>
@@ -232,7 +232,7 @@ function BookmarksPage() {
232
232
  {
233
233
  accessorKey: 'createdAt',
234
234
  header: m.common_knowledgePublic_bookmarks_columns_createdAt(),
235
- cell: ({ row }: any) => (
235
+ cell: ({ row }) => (
236
236
  <span className="text-muted-foreground">
237
237
  {new Date(row.original.createdAt).toLocaleDateString('zh-CN')}
238
238
  </span>
@@ -241,7 +241,7 @@ function BookmarksPage() {
241
241
  {
242
242
  id: 'actions',
243
243
  header: '',
244
- cell: ({ row }: any) => (
244
+ cell: ({ row }) => (
245
245
  <div className="flex items-center gap-1">
246
246
  <Button
247
247
  variant="ghost"
@@ -17,7 +17,7 @@
17
17
 
18
18
  import { createFileRoute } from '@tanstack/react-router'
19
19
  import { z } from 'zod'
20
- import { adminListBookmarksFn } from '@/server/serverFns/knowledge-public/bookmarks/list'
20
+ import { adminListBookmarksFn } from '@/server/serverFns/knowledge/public/bookmarks/list'
21
21
 
22
22
  const bookmarkSearchSchema = z.object({
23
23
  search: z.string().optional(),
@@ -28,8 +28,8 @@ import { toast } from '@/components/ui/toast'
28
28
  import {
29
29
  ComplianceFormDialog,
30
30
  type ComplianceRow,
31
- } from '@/components/knowledge-public/compliance-form-dialog'
32
- import { adminDeleteComplianceFn } from '@/server/serverFns/knowledge-public/compliance/delete'
31
+ } from '@/components/knowledge/public/compliance-form-dialog'
32
+ import { adminDeleteComplianceFn } from '@/server/serverFns/knowledge/public/compliance/delete'
33
33
 
34
34
  export const Route = createLazyFileRoute('/_nav/knowledge-public/compliance')({
35
35
  pendingComponent: () => (
@@ -90,19 +90,19 @@ function CompliancePage() {
90
90
  {
91
91
  accessorKey: 'title',
92
92
  header: m.common_knowledgePublic_compliance_columns_title(),
93
- cell: ({ row }: any) => <span className="font-medium">{row.original.title}</span>,
93
+ cell: ({ row }) => <span className="font-medium">{row.original.title}</span>,
94
94
  },
95
95
  {
96
96
  accessorKey: 'industry',
97
97
  header: m.common_knowledgePublic_compliance_columns_industry(),
98
- cell: ({ row }: any) => (
98
+ cell: ({ row }) => (
99
99
  <Badge variant="outline">{row.original.industry}</Badge>
100
100
  ),
101
101
  },
102
102
  {
103
103
  accessorKey: 'regulatoryBody',
104
104
  header: m.common_knowledgePublic_compliance_columns_regulatoryBody(),
105
- cell: ({ row }: any) => (
105
+ cell: ({ row }) => (
106
106
  <span className="text-muted-foreground">
107
107
  {row.original.regulatoryBody ?? '-'}
108
108
  </span>
@@ -111,7 +111,7 @@ function CompliancePage() {
111
111
  {
112
112
  accessorKey: 'tags',
113
113
  header: m.common_knowledgePublic_compliance_columns_tags(),
114
- cell: ({ row }: any) => (
114
+ cell: ({ row }) => (
115
115
  <div className="flex max-w-xs flex-wrap gap-1">
116
116
  {row.original.tags.length === 0 ? (
117
117
  <span className="text-muted-foreground">-</span>
@@ -128,7 +128,7 @@ function CompliancePage() {
128
128
  {
129
129
  accessorKey: 'createdAt',
130
130
  header: m.common_knowledgePublic_compliance_columns_createdAt(),
131
- cell: ({ row }: any) => (
131
+ cell: ({ row }) => (
132
132
  <span className="text-muted-foreground">
133
133
  {new Date(row.original.createdAt).toLocaleDateString('zh-CN')}
134
134
  </span>
@@ -137,7 +137,7 @@ function CompliancePage() {
137
137
  {
138
138
  id: 'actions',
139
139
  header: '',
140
- cell: ({ row }: any) => (
140
+ cell: ({ row }) => (
141
141
  <div className="flex items-center gap-1">
142
142
  <Button
143
143
  variant="ghost"
@@ -17,7 +17,7 @@
17
17
 
18
18
  import { createFileRoute } from '@tanstack/react-router'
19
19
  import { z } from 'zod'
20
- import { adminListComplianceFn } from '@/server/serverFns/knowledge-public/compliance/list'
20
+ import { adminListComplianceFn } from '@/server/serverFns/knowledge/public/compliance/list'
21
21
 
22
22
  const complianceSearchSchema = z.object({
23
23
  search: z.string().optional(),
@@ -34,8 +34,8 @@ import {
34
34
  AlertDialogTitle,
35
35
  } from '@/components/ui/alert-dialog'
36
36
  import { toast } from '@/components/ui/toast'
37
- import { LegalFormDialog, type LegalRow } from '@/components/knowledge-public/legal-form-dialog'
38
- import { adminDeleteLegalFn } from '@/server/serverFns/knowledge-public/legal/delete'
37
+ import { LegalFormDialog, type LegalRow } from '@/components/knowledge/public/legal-form-dialog'
38
+ import { adminDeleteLegalFn } from '@/server/serverFns/knowledge/public/legal/delete'
39
39
 
40
40
  export const Route = createLazyFileRoute('/_nav/knowledge-public/legal')({
41
41
  pendingComponent: () => (
@@ -96,12 +96,12 @@ function LegalPage() {
96
96
  {
97
97
  accessorKey: 'title',
98
98
  header: m.common_knowledgePublic_legal_columns_title(),
99
- cell: ({ row }: any) => <span className="font-medium">{row.original.title}</span>,
99
+ cell: ({ row }) => <span className="font-medium">{row.original.title}</span>,
100
100
  },
101
101
  {
102
102
  accessorKey: 'jurisdiction',
103
103
  header: m.common_knowledgePublic_legal_columns_jurisdiction(),
104
- cell: ({ row }: any) => (
104
+ cell: ({ row }) => (
105
105
  <Badge variant="outline">
106
106
  {row.original.jurisdiction}
107
107
  </Badge>
@@ -110,14 +110,14 @@ function LegalPage() {
110
110
  {
111
111
  accessorKey: 'category',
112
112
  header: m.common_knowledgePublic_legal_columns_category(),
113
- cell: ({ row }: any) => (
113
+ cell: ({ row }) => (
114
114
  <span className="text-muted-foreground">{row.original.category}</span>
115
115
  ),
116
116
  },
117
117
  {
118
118
  accessorKey: 'tags',
119
119
  header: m.common_knowledgePublic_legal_columns_tags(),
120
- cell: ({ row }: any) => (
120
+ cell: ({ row }) => (
121
121
  <div className="flex max-w-xs flex-wrap gap-1">
122
122
  {row.original.tags.length === 0 ? (
123
123
  <span className="text-muted-foreground">-</span>
@@ -134,7 +134,7 @@ function LegalPage() {
134
134
  {
135
135
  accessorKey: 'createdAt',
136
136
  header: m.common_knowledgePublic_legal_columns_createdAt(),
137
- cell: ({ row }: any) => (
137
+ cell: ({ row }) => (
138
138
  <span className="text-muted-foreground">
139
139
  {new Date(row.original.createdAt).toLocaleDateString('zh-CN')}
140
140
  </span>
@@ -143,7 +143,7 @@ function LegalPage() {
143
143
  {
144
144
  id: 'actions',
145
145
  header: '',
146
- cell: ({ row }: any) => (
146
+ cell: ({ row }) => (
147
147
  <div className="flex items-center gap-1">
148
148
  <Button
149
149
  variant="ghost"
@@ -17,7 +17,7 @@
17
17
 
18
18
  import { createFileRoute } from '@tanstack/react-router'
19
19
  import { z } from 'zod'
20
- import { adminListLegalFn } from '@/server/serverFns/knowledge-public/legal/list'
20
+ import { adminListLegalFn } from '@/server/serverFns/knowledge/public/legal/list'
21
21
 
22
22
  const legalSearchSchema = z.object({
23
23
  search: z.string().optional(),
@@ -132,7 +132,7 @@ function UserDetailPage() {
132
132
  avatar={
133
133
  <UserAvatar
134
134
  avatarConfig={user.avatarConfig as AvatarConfig | null}
135
- bgShape={user.bgShape as 'square' | 'rounded' | null}
135
+ bgShape={user.bgShape}
136
136
  bgColor={user.bgColor}
137
137
  name={user.username}
138
138
  size="lg"
@@ -34,7 +34,7 @@ function AdminLoginPage() {
34
34
  name: session.user.name,
35
35
  email: session.user.email,
36
36
  avatarConfig: (session.user as any).avatarConfig as AvatarConfig | undefined,
37
- bgShape: (session.user as any).bgShape as string | undefined,
37
+ bgShape: (session.user as { bgShape?: string | null }).bgShape ?? undefined,
38
38
  bgColor: (session.user as any).bgColor as string | undefined,
39
39
  }
40
40
  : null
@@ -24,6 +24,16 @@ export interface GithubData {
24
24
  versionDate: Date | null
25
25
  }
26
26
 
27
+ interface GithubRepoJson {
28
+ stargazers_count?: number
29
+ updated_at?: string
30
+ }
31
+
32
+ interface GithubReleaseJson {
33
+ tag_name?: string
34
+ published_at?: string
35
+ }
36
+
27
37
  const API = 'https://api.github.com'
28
38
  const HEADERS: HeadersInit = {
29
39
  Accept: 'application/vnd.github+json',
@@ -39,14 +49,14 @@ export async function fetchGithubData(
39
49
  ): Promise<GithubData | null> {
40
50
  const repoRes = await fetch(`${API}/repos/${owner}/${repo}`, { headers: HEADERS })
41
51
  if (!repoRes.ok) return null
42
- const repoJson: any = await repoRes.json()
52
+ const repoJson: GithubRepoJson = await repoRes.json()
43
53
 
44
54
  let latestVersion: string | null = null
45
55
  let versionDate: Date | null = null
46
56
  try {
47
57
  const relRes = await fetch(`${API}/repos/${owner}/${repo}/releases/latest`, { headers: HEADERS })
48
58
  if (relRes.ok) {
49
- const relJson: any = await relRes.json()
59
+ const relJson: GithubReleaseJson = await relRes.json()
50
60
  latestVersion = relJson.tag_name ?? null
51
61
  versionDate = relJson.published_at ? new Date(relJson.published_at) : null
52
62
  }
@@ -19,7 +19,7 @@ import { createServerFn } from '@tanstack/react-start'
19
19
  import { z } from 'zod'
20
20
  import { getPrisma } from '@allbluecn/database/get-prisma'
21
21
  import { requireAdmin } from '@/server/auth-helpers'
22
- import { bookmarkCanonicalId, parseGithubUrl } from '@/server/knowledge-public/canonical-id'
22
+ import { bookmarkCanonicalId, parseGithubUrl } from '@/server/knowledge/public/canonical-id'
23
23
 
24
24
  const createSchema = z.object({
25
25
  title: z.string().min(1).max(200),
@@ -18,6 +18,7 @@
18
18
  import { createServerFn } from '@tanstack/react-start'
19
19
  import { z } from 'zod'
20
20
  import { getPrisma } from '@allbluecn/database/get-prisma'
21
+ import { Prisma } from '@allbluecn/database'
21
22
  import { requireAdmin } from '@/server/auth-helpers'
22
23
 
23
24
  const listSchema = z.object({
@@ -33,7 +34,7 @@ export const adminListBookmarksFn = createServerFn({ method: 'GET' })
33
34
  .handler(async ({ data }) => {
34
35
  const prisma = await getPrisma()
35
36
  await requireAdmin()
36
- const where: any = { visibility: 'public', isDeleted: false }
37
+ const where: Prisma.WebsiteBookmarkWhereInput = { visibility: 'public', isDeleted: false }
37
38
  if (data.search) {
38
39
  where.OR = [
39
40
  { title: { contains: data.search, mode: 'insensitive' } },
@@ -18,8 +18,9 @@
18
18
  import { createServerFn } from '@tanstack/react-start'
19
19
  import { z } from 'zod'
20
20
  import { getPrisma } from '@allbluecn/database/get-prisma'
21
+ import { Prisma } from '@allbluecn/database'
21
22
  import { requireAdmin } from '@/server/auth-helpers'
22
- import { syncBookmarkGithub } from '@/server/knowledge-public/github-sync'
23
+ import { syncBookmarkGithub } from '@/server/knowledge/public/github-sync'
23
24
 
24
25
  const syncSchema = z.object({
25
26
  force: z.boolean().default(false),
@@ -30,7 +31,7 @@ export const adminSyncAllGithubFn = createServerFn({ method: 'POST' })
30
31
  .handler(async ({ data }) => {
31
32
  const prisma = await getPrisma()
32
33
  await requireAdmin()
33
- const where: any = { isGithub: true, isDeleted: false }
34
+ const where: Prisma.WebsiteBookmarkWhereInput = { isGithub: true, isDeleted: false }
34
35
  if (!data.force) {
35
36
  const sixHoursAgo = new Date(Date.now() - 6 * 60 * 60 * 1000)
36
37
  where.OR = [
@@ -18,8 +18,9 @@
18
18
  import { createServerFn } from '@tanstack/react-start'
19
19
  import { z } from 'zod'
20
20
  import { getPrisma } from '@allbluecn/database/get-prisma'
21
+ import { Prisma } from '@allbluecn/database'
21
22
  import { requireAdmin } from '@/server/auth-helpers'
22
- import { bookmarkCanonicalId, parseGithubUrl } from '@/server/knowledge-public/canonical-id'
23
+ import { bookmarkCanonicalId, parseGithubUrl } from '@/server/knowledge/public/canonical-id'
23
24
 
24
25
  const updateSchema = z.object({
25
26
  id: z.string(),
@@ -38,7 +39,7 @@ export const adminUpdateBookmarkFn = createServerFn({ method: 'POST' })
38
39
  const { id, ...rest } = data
39
40
  const existing = await prisma.websiteBookmark.findUnique({ where: { id } })
40
41
  if (!existing) throw new Error('未找到')
41
- const updateData: any = { ...rest }
42
+ const updateData: Prisma.WebsiteBookmarkUncheckedUpdateInput = { ...rest }
42
43
  const urlChanged = data.url !== undefined && data.url !== existing.url
43
44
  if (urlChanged && data.url) {
44
45
  const gh = parseGithubUrl(data.url)
@@ -19,7 +19,7 @@ import { createServerFn } from '@tanstack/react-start'
19
19
  import { z } from 'zod'
20
20
  import { getPrisma } from '@allbluecn/database/get-prisma'
21
21
  import { requireAdmin } from '@/server/auth-helpers'
22
- import { complianceCanonicalId } from '@/server/knowledge-public/canonical-id'
22
+ import { complianceCanonicalId } from '@/server/knowledge/public/canonical-id'
23
23
 
24
24
  const createSchema = z.object({
25
25
  title: z.string().min(1).max(200),
@@ -18,6 +18,7 @@
18
18
  import { createServerFn } from '@tanstack/react-start'
19
19
  import { z } from 'zod'
20
20
  import { getPrisma } from '@allbluecn/database/get-prisma'
21
+ import { Prisma } from '@allbluecn/database'
21
22
  import { requireAdmin } from '@/server/auth-helpers'
22
23
 
23
24
  const listSchema = z.object({
@@ -33,7 +34,7 @@ export const adminListComplianceFn = createServerFn({ method: 'GET' })
33
34
  .handler(async ({ data }) => {
34
35
  const prisma = await getPrisma()
35
36
  await requireAdmin()
36
- const where: any = { visibility: 'public', isDeleted: false }
37
+ const where: Prisma.IndustryComplianceWhereInput = { visibility: 'public', isDeleted: false }
37
38
  if (data.search) {
38
39
  where.OR = [
39
40
  { title: { contains: data.search, mode: 'insensitive' } },
@@ -18,8 +18,9 @@
18
18
  import { createServerFn } from '@tanstack/react-start'
19
19
  import { z } from 'zod'
20
20
  import { getPrisma } from '@allbluecn/database/get-prisma'
21
+ import { Prisma } from '@allbluecn/database'
21
22
  import { requireAdmin } from '@/server/auth-helpers'
22
- import { complianceCanonicalId } from '@/server/knowledge-public/canonical-id'
23
+ import { complianceCanonicalId } from '@/server/knowledge/public/canonical-id'
23
24
 
24
25
  const updateSchema = z.object({
25
26
  id: z.string(),
@@ -43,7 +44,7 @@ export const adminUpdateComplianceFn = createServerFn({ method: 'POST' })
43
44
  const { id, code, ...rest } = data
44
45
  const existing = await prisma.industryCompliance.findUnique({ where: { id } })
45
46
  if (!existing) throw new Error('未找到')
46
- const updateData: any = { ...rest }
47
+ const updateData: Prisma.IndustryComplianceUncheckedUpdateInput = { ...rest }
47
48
  if (code !== undefined || data.title !== undefined) {
48
49
  updateData.canonicalId = complianceCanonicalId({
49
50
  title: data.title ?? existing.title,
@@ -19,7 +19,7 @@ import { createServerFn } from '@tanstack/react-start'
19
19
  import { z } from 'zod'
20
20
  import { getPrisma } from '@allbluecn/database/get-prisma'
21
21
  import { requireAdmin } from '@/server/auth-helpers'
22
- import { legalCanonicalId } from '@/server/knowledge-public/canonical-id'
22
+ import { legalCanonicalId } from '@/server/knowledge/public/canonical-id'
23
23
 
24
24
  const createSchema = z.object({
25
25
  title: z.string().min(1).max(200),
@@ -18,6 +18,7 @@
18
18
  import { createServerFn } from '@tanstack/react-start'
19
19
  import { z } from 'zod'
20
20
  import { getPrisma } from '@allbluecn/database/get-prisma'
21
+ import { Prisma } from '@allbluecn/database'
21
22
  import { requireAdmin } from '@/server/auth-helpers'
22
23
 
23
24
  const listSchema = z.object({
@@ -33,7 +34,7 @@ export const adminListLegalFn = createServerFn({ method: 'GET' })
33
34
  .handler(async ({ data }) => {
34
35
  const prisma = await getPrisma()
35
36
  await requireAdmin()
36
- const where: any = { visibility: 'public', isDeleted: false }
37
+ const where: Prisma.LegalRegulationWhereInput = { visibility: 'public', isDeleted: false }
37
38
  if (data.search) {
38
39
  where.OR = [
39
40
  { title: { contains: data.search, mode: 'insensitive' } },
@@ -18,8 +18,9 @@
18
18
  import { createServerFn } from '@tanstack/react-start'
19
19
  import { z } from 'zod'
20
20
  import { getPrisma } from '@allbluecn/database/get-prisma'
21
+ import { Prisma } from '@allbluecn/database'
21
22
  import { requireAdmin } from '@/server/auth-helpers'
22
- import { legalCanonicalId } from '@/server/knowledge-public/canonical-id'
23
+ import { legalCanonicalId } from '@/server/knowledge/public/canonical-id'
23
24
 
24
25
  const updateSchema = z.object({
25
26
  id: z.string(),
@@ -45,7 +46,7 @@ export const adminUpdateLegalFn = createServerFn({ method: 'POST' })
45
46
  const { id, code, ...rest } = data
46
47
  const existing = await prisma.legalRegulation.findUnique({ where: { id } })
47
48
  if (!existing) throw new Error('未找到')
48
- const updateData: any = { ...rest }
49
+ const updateData: Prisma.LegalRegulationUncheckedUpdateInput = { ...rest }
49
50
  if (code !== undefined || data.title !== undefined) {
50
51
  updateData.canonicalId = legalCanonicalId({
51
52
  title: data.title ?? existing.title,