feat: enhance device listing and improve session management
ZhenYi committed
Modified lib/service/webapi/src/assets.rs lib/service/webapi/src/assets.rs +8 −3 View lib/service/webapi/src/assets.rs
@@ -60,10 +60,15 @@
6060 "no-cache"
6161 };
6262
63 HttpResponse::Ok()
63 let mut response = HttpResponse::Ok();
64 response
6465 .insert_header((CONTENT_TYPE, asset.content_type))
65 .insert_header((CACHE_CONTROL, cache_control))
66 .body(asset.bytes)
66 .insert_header((CACHE_CONTROL, cache_control));
67 // 登录态页面(/auth、/app)不进索引,与页面内 noindex 互为补充。
68 if asset.content_type.starts_with("text/html") && fallback != LANDING_ENTRY {
69 response.insert_header(("X-Robots-Tag", "noindex, nofollow"));
70 }
71 response.body(asset.bytes)
6772 }
6873 None => HttpResponse::ServiceUnavailable()
6974 .insert_header((CONTENT_TYPE, "text/plain; charset=utf-8"))
Modified view/app/src/domains/settings/model/api.ts view/app/src/domains/settings/model/api.ts +4 −1 View view/app/src/domains/settings/model/api.ts
@@ -70,7 +70,10 @@
7070export const revokeSession = (sessionId: string) =>
7171 unwrapApi<null>(Apis.user.revoke_session({ pathParams: { session_id: sessionId } }))
7272export const revokeAllSessions = () => unwrapApi<CountResult>(Apis.user.revoke_all_sessions())
73export const listDevices = () => unwrapApi<DeviceRow[]>(Apis.account.list_devices({ params: {} }))
73export const listDevices = () =>
74 unwrapApi<{ items?: DeviceRow[] }>(Apis.account.list_devices({ params: { limit: 100 } })).then(
75 (page) => page?.items ?? [],
76 )
7477
7578/* ---- preferences ---------------------------------------------------------- */
7679/** Canonical home of the preference endpoints; features/space-management keeps
Modified view/app/src/pages/workspace/model/shell-api.ts view/app/src/pages/workspace/model/shell-api.ts +2 −1 View view/app/src/pages/workspace/model/shell-api.ts
@@ -33,7 +33,8 @@
3333 return prefs
3434 })
3535
36export const fetchMySpaces = () => unwrapApi<MySpace[]>(Apis.user.my_spaces({ params: {} }))
36export const fetchMySpaces = () =>
37 unwrapApi<{ items: MySpace[] }>(Apis.user.my_spaces({ params: { limit: 100 } })).then((page) => page?.items ?? [])
3738
3839export const fetchSpaceRooms = (spaceName: string) =>
3940 unwrapApi<RoomBrief[]>(Apis.chat.list_space_rooms({ pathParams: { space_name: spaceName } }))
Modified view/app/src/routing/router.tsx view/app/src/routing/router.tsx +1 −17 View view/app/src/routing/router.tsx
@@ -10,23 +10,7 @@
1010 { index: true, element: <Navigate to="/app" replace /> },
1111 { path: 'app', element: <Navigate to="/app/@me" replace /> },
1212 { path: 'app/invites/accept', element: <SpaceInviteRoute /> },
13 {
14 path: 'app',
15 element: <WorkspaceRoute />,
16 children: [
17 { path: 'explore' },
18 { path: '@me' },
19 { path: '@me/notify' },
20 { path: '@me/chat/:roomId?' },
21 { path: '@me/board/:boardId?' },
22 { path: '@me/repo/:repoName?' },
23 { path: ':spaceName' },
24 { path: ':spaceName/chat/:roomId?' },
25 { path: ':spaceName/board/:boardId?' },
26 { path: ':spaceName/repo/:repoName?' },
27 { path: ':spaceName/members' },
28 ],
29 },
13 { path: 'app/*', element: <WorkspaceRoute /> },
3014 { path: '*', element: <NotFoundRoute /> },
3115 ],
3216 },
Modified view/app/src/styles/index.css view/app/src/styles/index.css +5 −3 View view/app/src/styles/index.css
@@ -1,4 +1,6 @@
1@import "../../../shared/styles/fonts.css";
21@import "tailwindcss";
3@import "../../../shared/styles/theme.css";
4@source "../../";
2@import "../tokens/primitive.css";
3@import "../tokens/semantic.css";
4@import "../vendor/livekit/styles/dist/general/index.css";
5@import "./workspace.css";
6@source "../";
Deleted view/app/src/app.tsx view/app/src/app.tsx +0 −112
@@ -1,112 +0,0 @@
1import { LayoutDashboard, Moon, Sun } from 'lucide-react'
2import type { ReactNode } from 'react'
3import { useEffect, useState } from 'react'
4import { Navigate, Route, Routes } from 'react-router-dom'
5
6function useTheme() {
7 const [theme, setTheme] = useState(() => {
8 try {
9 return localStorage.getItem('auth-theme') || 'system'
10 } catch {
11 return 'system'
12 }
13 })
14 const [systemDark, setSystemDark] = useState(() => matchMedia('(prefers-color-scheme: dark)').matches)
15 const dark = theme === 'dark' || (theme === 'system' && systemDark)
16 useEffect(() => {
17 const media = matchMedia('(prefers-color-scheme: dark)')
18 const update = () => setSystemDark(media.matches)
19 media.addEventListener('change', update)
20 return () => media.removeEventListener('change', update)
21 }, [])
22 useEffect(() => {
23 document.documentElement.dataset.theme = dark ? 'dark' : 'light'
24 }, [dark])
25 function toggle() {
26 const next = dark ? 'light' : 'dark'
27 setTheme(next)
28 try {
29 localStorage.setItem('auth-theme', next)
30 } catch {}
31 }
32 return { dark, toggle }
33}
34
35function Shell({ children }: { children: ReactNode }) {
36 const { dark, toggle } = useTheme()
37 return (
38 <div className="flex min-h-dvh flex-col bg-page text-ink">
39 <header className="mx-auto flex h-16 w-full max-w-6xl items-center justify-between px-5 md:px-10">
40 <a href="/" className="inline-flex items-center gap-2.75" aria-label="GitDataAI 首页">
41 <img src="/media/gitdataai-red-panda.png" width={40} height={40} alt="" />
42 <span className="font-latin text-[19px] font-semibold tracking-[-0.4px]">GitDataAI</span>
43 </a>
44 <div className="flex items-center gap-2">
45 <button
46 type="button"
47 onClick={toggle}
48 aria-label={dark ? '切换到浅色主题' : '切换到深色主题'}
49 className="flex size-11 items-center justify-center rounded-lg text-icon hover:bg-elevated hover:text-ink"
50 >
51 {dark ? <Sun size={18} strokeWidth={1.6} /> : <Moon size={18} strokeWidth={1.6} />}
52 </button>
53 <a
54 href="/auth/login"
55 className="inline-flex min-h-10 items-center rounded-lg border border-control-border bg-field px-4 text-sm font-medium text-ink transition-colors hover:bg-elevated"
56 >
57 账户
58 </a>
59 </div>
60 </header>
61 <main className="mx-auto w-full max-w-6xl flex-1 px-5 py-10 md:px-10">{children}</main>
62 </div>
63 )
64}
65
66function HomePage() {
67 return (
68 <>
69 <div className="mb-8 flex items-center gap-3">
70 <span className="flex size-11 items-center justify-center rounded-xl bg-accent-soft text-accent">
71 <LayoutDashboard size={20} strokeWidth={1.8} />
72 </span>
73 <div>
74 <h1 className="text-2xl font-semibold tracking-[-0.4px]">工作台</h1>
75 <p className="text-sm text-muted">空间、仓库与协作的入口。</p>
76 </div>
77 </div>
78
79 <section className="rounded-xl border border-line bg-field p-6">
80 <h2 className="mb-2 text-base font-semibold">应用骨架已就绪</h2>
81 <p className="mb-4 text-sm leading-[1.7] text-muted">
82 这里将承载登录后的空间列表、通知与仓库视图。当前只初始化了路由与布局,业务模块按需接入。
83 </p>
84 <div className="flex flex-wrap gap-3">
85 <a
86 href="/auth/login"
87 className="inline-flex min-h-11 items-center rounded-lg bg-primary px-5 text-sm font-semibold text-white transition-colors hover:bg-primary-hover active:bg-primary-pressed"
88 >
89 前往登录
90 </a>
91 <a
92 href="/"
93 className="inline-flex min-h-11 items-center rounded-lg border border-control-border bg-field px-5 text-sm font-semibold text-ink transition-colors hover:bg-elevated"
94 >
95 返回首页
96 </a>
97 </div>
98 </section>
99 </>
100 )
101}
102
103export function App() {
104 return (
105 <Shell>
106 <Routes>
107 <Route path="/app" element={<HomePage />} />
108 <Route path="*" element={<Navigate to="/app" replace />} />
109 </Routes>
110 </Shell>
111 )
112}
Modified view/app/src/main.tsx view/app/src/main.tsx +15 −5 View view/app/src/main.tsx
@@ -1,12 +1,22 @@
1import { bootLocale } from '@app/i18n'
2import { initPalette, initTheme } from '@app/lib'
3import { StrictMode } from 'react'
14import { createRoot } from 'react-dom/client'
2import { BrowserRouter } from 'react-router-dom'
5import { RouterProvider } from 'react-router-dom'
6import { AppProviders } from './bootstrap'
7import { router } from './routing/router'
38import './styles/index.css'
4import { App } from './app'
59
10initTheme()
11initPalette()
12bootLocale()
13
614const root = document.getElementById('app')
715if (root)
816 createRoot(root).render(
9 <BrowserRouter>
10 <App />
11 </BrowserRouter>,
17 <StrictMode>
18 <AppProviders>
19 <RouterProvider router={router} />
20 </AppProviders>
21 </StrictMode>,
1222 )
Modified view/app/index.html view/app/index.html +17 −9 View view/app/index.html
@@ -2,21 +2,29 @@
22<html lang="zh-CN">
33 <head>
44 <meta charset="UTF-8" />
5 <link rel="icon" type="image/png" href="/media/gitdataai-red-panda.png" />
65 <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6 <title>工作台 · GitDataAI</title>
7 <meta
8 name="description"
9 content="GitDataAI 工作台:仓库、问题、合并请求与团队沟通在同一个工程空间内。"
10 />
11 <meta name="robots" content="noindex, nofollow" />
12 <link rel="icon" type="image/png" href="/media/gitdataai-red-panda.png" />
13 <link rel="manifest" href="/manifest.webmanifest" />
14 <link rel="apple-touch-icon" href="/media/gitdataai-red-panda.png" />
15 <meta name="theme-color" content="#111111" />
16 <meta name="app:region" content="EU-CENTRAL-1" />
17 <meta name="app:min-age" content="13" />
718 <meta name="referrer" content="no-referrer" />
819 <meta name="color-scheme" content="light dark" />
9 <meta name="description" content="GitDataAI 工作台:管理你的空间、仓库与协作。" />
10 <title>GitDataAI</title>
1120 <script>
1221 try {
13 const t = localStorage.getItem('auth-theme')
14 document.documentElement.dataset.theme =
15 t === 'dark' || (t !== 'light' && matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light'
22 const stored = localStorage.getItem('kernel-theme')
23 const dark =
24 stored === 'dark' || (stored !== 'light' && matchMedia('(prefers-color-scheme: dark)').matches)
25 document.documentElement.classList.toggle('dark', dark)
1626 } catch {
17 document.documentElement.dataset.theme = matchMedia('(prefers-color-scheme: dark)').matches
18 ? 'dark'
19 : 'light'
27 document.documentElement.classList.toggle('dark', matchMedia('(prefers-color-scheme: dark)').matches)
2028 }
2129 </script>
2230 </head>
Modified view/auth/src/app/routing/router.tsx view/auth/src/app/routing/router.tsx +101 −54 View view/auth/src/app/routing/router.tsx
@@ -1,6 +1,7 @@
11import type { ReactNode } from 'react'
22import { useEffect, useState } from 'react'
3import { captureCallbackUrl } from '@/entities/auth-callback'
3import { createBrowserRouter, Outlet, useLocation, useNavigate } from 'react-router-dom'
4import { registrationDraft } from '@/entities/registration'
45import type { LoginComplete } from '@/entities/session'
56import { mfaChallenge, useSession } from '@/entities/session'
67import { AccountPage } from '@/pages/account'
@@ -16,21 +17,50 @@
1617import { ResetSentPage } from '@/pages/reset-sent'
1718import { AuthError } from '@/shared/api'
1819import { routes } from '@/shared/config'
19import { t } from '@/shared/i18n'
20import { navigate, usePath } from '@/shared/lib'
20import { t, useLocale } from '@/shared/i18n'
2121import { SessionCheck } from '@/shared/ui'
22import { AppShell } from '@/widgets/app-shell'
2223
24function AuthLayout() {
25 const { pathname } = useLocation()
26 const locale = useLocale()
27 useEffect(() => {
28 document.documentElement.lang = locale
29 document.querySelector('meta[name="description"]')?.setAttribute('content', t('app.description'))
30 }, [locale])
31 // biome-ignore lint/correctness/useExhaustiveDependencies: locale 变化时需要按当前标题重算 document.title
32 useEffect(() => {
33 const heading = document.querySelector('h1')
34 document.title = `${heading?.textContent || t('app.title')} · GitDataAI`
35 heading?.focus({ preventScroll: true })
36 if (pathname !== routes.loginVerify) mfaChallenge.reset()
37 if (pathname !== routes.register && pathname !== routes.registerVerify) registrationDraft.reset()
38 if (pathname === routes.resetPassword && new URLSearchParams(location.search).has('token')) {
39 const url = new URL(location.href)
40 url.searchParams.delete('token')
41 history.replaceState(null, '', `${url.pathname}${url.search}${url.hash}`)
42 }
43 }, [pathname, locale])
44 return (
45 <AppShell>
46 <Outlet />
47 </AppShell>
48 )
49}
50
2351function LoginEntry({ children }: { children: ReactNode }) {
2452 const [unauthenticated, setUnauthenticated] = useState(false)
53 const navigate = useNavigate()
2554 const { user, loading, task } = useSession({ onUnauthenticated: () => setUnauthenticated(true) })
2655 useEffect(() => {
27 if (user) navigate(routes.account, true)
28 }, [user])
56 if (user) navigate(routes.account, { replace: true })
57 }, [user, navigate])
2958 if (unauthenticated) return <>{children}</>
3059 return <SessionCheck loading={loading} notice={task.notice} onRetry={() => location.reload()} />
3160}
3261
3362function useLoginComplete(): LoginComplete {
63 const navigate = useNavigate()
3464 return (response) => {
3565 if (response.mfa_required) {
3666 if (!response.challenge) throw new AuthError('challenge', t('mfa.missingChallenge'))
@@ -39,57 +69,74 @@
3969 return
4070 }
4171 mfaChallenge.reset()
42 navigate(routes.account, true)
72 navigate(routes.account, { replace: true })
4373 }
4474}
4575
46export function Router() {
47 const path = usePath()
48 captureCallbackUrl()
76function LoginRoute() {
4977 const onComplete = useLoginComplete()
50 let page: ReactNode
51 switch (path) {
52 case routes.home:
53 case routes.login:
54 page = (
55 <LoginEntry>
56 <LoginPage onComplete={onComplete} />
57 </LoginEntry>
58 )
59 break
60 case routes.loginCode:
61 page = (
62 <LoginEntry>
63 <LoginCodePage onComplete={onComplete} />
64 </LoginEntry>
65 )
66 break
67 case routes.loginVerify:
68 page = <LoginVerifyPage onComplete={onComplete} />
69 break
70 case routes.register:
71 page = <RegisterPage onSent={() => navigate(routes.registerVerify)} />
72 break
73 case routes.registerVerify:
74 page = <RegisterVerifyPage onComplete={() => navigate(routes.account, true)} />
75 break
76 case routes.forgotPassword:
77 page = <ForgotPasswordPage onSent={() => navigate(routes.resetSent)} />
78 break
79 case routes.resetSent:
80 page = <ResetSentPage />
81 break
82 case routes.resetPassword:
83 page = <ResetPasswordPage onComplete={() => navigate(routes.resetComplete, true)} />
84 break
85 case routes.resetComplete:
86 page = <ResetCompletePage />
87 break
88 case routes.account:
89 page = <AccountPage />
90 break
91 default:
92 page = <NotFoundPage />
93 }
94 return <div key={path + location.search}>{page}</div>
78 return (
79 <LoginEntry>
80 <LoginPage onComplete={onComplete} />
81 </LoginEntry>
82 )
9583}
84
85function LoginCodeRoute() {
86 const onComplete = useLoginComplete()
87 return (
88 <LoginEntry>
89 <LoginCodePage onComplete={onComplete} />
90 </LoginEntry>
91 )
92}
93
94function LoginVerifyRoute() {
95 const onComplete = useLoginComplete()
96 return <LoginVerifyPage onComplete={onComplete} />
97}
98
99function RegisterRoute() {
100 const navigate = useNavigate()
101 return <RegisterPage onSent={() => navigate(routes.registerVerify)} />
102}
103
104function RegisterVerifyRoute() {
105 const navigate = useNavigate()
106 return <RegisterVerifyPage onComplete={() => navigate(routes.account, { replace: true })} />
107}
108
109function ForgotPasswordRoute() {
110 const navigate = useNavigate()
111 return <ForgotPasswordPage onSent={() => navigate(routes.resetSent)} />
112}
113
114function ResetPasswordRoute() {
115 const navigate = useNavigate()
116 return <ResetPasswordPage onComplete={() => navigate(routes.resetComplete, { replace: true })} />
117}
118
119export const router = createBrowserRouter([
120 {
121 path: routes.home,
122 element: <AuthLayout />,
123 children: [
124 { index: true, element: <LoginRoute /> },
125 { path: routes.login, element: <LoginRoute /> },
126 { path: routes.loginCode, element: <LoginCodeRoute /> },
127 { path: routes.loginVerify, element: <LoginVerifyRoute /> },
128 { path: routes.register, element: <RegisterRoute /> },
129 { path: routes.registerVerify, element: <RegisterVerifyRoute /> },
130 { path: routes.forgotPassword, element: <ForgotPasswordRoute /> },
131 { path: routes.resetSent, element: <ResetSentPage /> },
132 { path: routes.resetPassword, element: <ResetPasswordRoute /> },
133 { path: routes.resetComplete, element: <ResetCompletePage /> },
134 { path: routes.account, element: <AccountPage /> },
135 ],
136 },
137 {
138 path: '*',
139 element: <AuthLayout />,
140 children: [{ index: true, element: <NotFoundPage /> }],
141 },
142])
Modified view/auth/src/pages/not-found/ui/not-found-page.tsx view/auth/src/pages/not-found/ui/not-found-page.tsx +3 −2 View view/auth/src/pages/not-found/ui/not-found-page.tsx
@@ -1,13 +1,14 @@
1import { useNavigate } from 'react-router-dom'
12import { routes } from '@/shared/config'
23import { t } from '@/shared/i18n'
3import { navigate } from '@/shared/lib'
44import { Button, Heading } from '@/shared/ui'
55import { AuthPanel } from '@/widgets/auth-panel'
66
77export function NotFoundPage() {
8 const navigate = useNavigate()
89 return (
910 <AuthPanel heading={<Heading title={t('notFound.title')} description={t('notFound.description')} />}>
10 <Button type="button" onClick={() => navigate(routes.login, true)}>
11 <Button type="button" onClick={() => navigate(routes.login, { replace: true })}>
1112 {t('common.backToLogin')}
1213 </Button>
1314 </AuthPanel>
Modified view/auth/src/pages/reset-complete/ui/reset-complete-page.tsx view/auth/src/pages/reset-complete/ui/reset-complete-page.tsx +2 −1 View view/auth/src/pages/reset-complete/ui/reset-complete-page.tsx
@@ -1,12 +1,13 @@
11import { Check, ShieldCheck } from 'lucide-react'
2import { useNavigate } from 'react-router-dom'
23import { usePasswordReset } from '@/entities/password-reset'
34import { routes } from '@/shared/config'
45import { t } from '@/shared/i18n'
5import { navigate } from '@/shared/lib'
66import { Button, FlowRequired, Heading } from '@/shared/ui'
77import { AuthPanel } from '@/widgets/auth-panel'
88
99export function ResetCompletePage() {
10 const navigate = useNavigate()
1011 const { completed } = usePasswordReset()
1112 if (!completed)
1213 return (
Modified view/auth/src/pages/reset-password/ui/reset-password-page.tsx view/auth/src/pages/reset-password/ui/reset-password-page.tsx +2 −1 View view/auth/src/pages/reset-password/ui/reset-password-page.tsx
@@ -1,13 +1,14 @@
11import { KeyRound, LockKeyhole } from 'lucide-react'
22import { useState } from 'react'
3import { useNavigate } from 'react-router-dom'
34import { ResetPasswordForm, useResetPassword } from '@/features/reset-password'
45import { routes } from '@/shared/config'
56import { t } from '@/shared/i18n'
6import { navigate } from '@/shared/lib'
77import { BackLink, Button, Heading, Link } from '@/shared/ui'
88import { AuthPanel } from '@/widgets/auth-panel'
99
1010export function ResetPasswordPage({ onComplete }: { onComplete: () => void }) {
11 const navigate = useNavigate()
1112 const [token] = useState(() => new URLSearchParams(location.search).get('token') || '')
1213 if (!token)
1314 return (
Modified view/auth/src/pages/reset-sent/ui/reset-sent-page.tsx view/auth/src/pages/reset-sent/ui/reset-sent-page.tsx +2 −1 View view/auth/src/pages/reset-sent/ui/reset-sent-page.tsx
@@ -1,12 +1,13 @@
11import { MailCheck } from 'lucide-react'
2import { useNavigate } from 'react-router-dom'
23import { ResetSentActions, useResendResetEmail } from '@/features/request-password-reset'
34import { routes } from '@/shared/config'
45import { t } from '@/shared/i18n'
5import { navigate } from '@/shared/lib'
66import { Button, FlowRequired, Heading, Link } from '@/shared/ui'
77import { AuthPanel } from '@/widgets/auth-panel'
88
99export function ResetSentPage() {
10 const navigate = useNavigate()
1011 const model = useResendResetEmail()
1112 if (!model.email)
1213 return (
Modified view/auth/src/shared/ui/organisms/flow-required.tsx view/auth/src/shared/ui/organisms/flow-required.tsx +3 −2 View view/auth/src/shared/ui/organisms/flow-required.tsx
@@ -1,5 +1,5 @@
11import type { LucideIcon } from 'lucide-react'
2import { navigate } from '@/shared/lib'
2import { useNavigate } from 'react-router-dom'
33import { Button } from '../atoms/button'
44import { Heading } from './heading'
55
@@ -18,10 +18,11 @@
1818 replace?: boolean
1919 icon?: LucideIcon
2020}) {
21 const navigate = useNavigate()
2122 return (
2223 <>
2324 <Heading title={title} description={description} icon={icon} />
24 <Button type="button" onClick={() => navigate(actionTo, replace)}>
25 <Button type="button" onClick={() => navigate(actionTo, { replace })}>
2526 {actionLabel}
2627 </Button>
2728 </>
Modified view/auth/index.html view/auth/index.html +7 −3 View view/auth/index.html
@@ -2,12 +2,16 @@
22<html lang="en">
33 <head>
44 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6 <title>Sign in · GitDataAI</title>
7 <meta
8 name="description"
9 content="Sign in to GitDataAI with one account, or create one to reach connected apps and services."
10 />
11 <meta name="robots" content="noindex, nofollow" />
512 <link rel="icon" type="image/png" href="/media/gitdataai-red-panda.png" />
6 <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
713 <meta name="referrer" content="no-referrer" />
814 <meta name="color-scheme" content="light dark" />
9 <meta name="description" content="Sign in to GitDataAI and reach every connected app and service with one account." />
10 <title>GitDataAI</title>
1115 <script>try{const t=localStorage.getItem('auth-theme');document.documentElement.dataset.theme=t==='dark'||(t!=='light'&&matchMedia('(prefers-color-scheme: dark)').matches)?'dark':'light'}catch{document.documentElement.dataset.theme=matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'}</script>
1216 </head>
1317 <body>
Modified view/index/src/styles/index.css view/index/src/styles/index.css +38 −0 View view/index/src/styles/index.css
@@ -2,3 +2,41 @@
22@import "tailwindcss";
33@import "../../../shared/styles/theme.css";
44@source "../../";
5
6/* 落地页专用装饰:光晕、网格与卡片动效,颜色全部取共享 token。 */
7.landing-aurora {
8 background: radial-gradient(
9 55% 45% at 50% 0%,
10 color-mix(in oklab, var(--auth-accent) 16%, transparent) 0%,
11 transparent 72%
12 );
13}
14
15.landing-grid {
16 background-image:
17 linear-gradient(to right, var(--auth-line) 1px, transparent 1px),
18 linear-gradient(to bottom, var(--auth-line) 1px, transparent 1px);
19 background-size: 64px 64px;
20 mask-image: radial-gradient(ellipse 70% 55% at 50% 0%, #000 35%, transparent 78%);
21 opacity: 0.55;
22}
23
24.landing-card {
25 transition:
26 transform 0.2s ease,
27 border-color 0.2s ease,
28 background-color 0.2s ease;
29}
30
31.landing-card:hover {
32 transform: translateY(-2px);
33 border-color: color-mix(in oklab, var(--auth-accent) 40%, var(--auth-line));
34}
35
36@media (prefers-reduced-motion: reduce) {
37 .landing-card,
38 .landing-card:hover {
39 transition: none;
40 transform: none;
41 }
42}
Modified view/index/src/app.tsx view/index/src/app.tsx +146 −43 View view/index/src/app.tsx
@@ -1,19 +1,32 @@
1import { Moon, Sun } from 'lucide-react'
1import { ArrowRight, GitBranch, GitPullRequest, MessagesSquare, Moon, ShieldCheck, Sun } from 'lucide-react'
2import type { LucideIcon } from 'lucide-react'
23import { useEffect, useState } from 'react'
34import { Navigate, Route, Routes } from 'react-router-dom'
45
5const features = [
6const features: { icon: LucideIcon; title: string; description: string; className: string }[] = [
7 {
8 icon: GitBranch,
9 title: 'Git 仓库',
10 description: '分支、提交、差异与 LFS 大文件由同一套服务承载,克隆与推送使用标准 Git 协议。',
11 className: 'md:col-span-4',
12 },
613 {
7 title: '统一身份',
8 description: '一个账户通行所有接入的应用与服务,不必为每个站点重复注册、反复记密码。',
14 icon: GitPullRequest,
15 title: '评审与看板',
16 description: '合并请求、Issue 与看板按空间组织,讨论和变更放在同一处。',
17 className: 'md:col-span-2',
918 },
1019 {
11 title: '安全登录',
12 description: '支持动态口令二次验证与恢复码,登录与授权行为全程留痕,异常访问及时阻断。',
20 icon: MessagesSquare,
21 title: '团队聊天',
22 description: '按空间划分的聊天房间,消息与通知直达相关成员。',
23 className: 'md:col-span-2',
1324 },
1425 {
15 title: '会话可控',
16 description: '设备会话与访问密钥集中管理,随时撤销可疑登录,账号处置不需要逐个应用排查。',
26 icon: ShieldCheck,
27 title: '统一身份',
28 description: '一个账户登录所有空间,二次验证、SSH 密钥与访问密钥集中管理,随时撤销可疑会话。',
29 className: 'md:col-span-4',
1730 },
1831]
1932
@@ -46,41 +59,126 @@
4659 return { dark, toggle }
4760}
4861
62function ProductPreview() {
63 return (
64 <div className="relative">
65 <div aria-hidden className="absolute -inset-6 rounded-[32px] bg-accent/10 blur-3xl" />
66 <div className="relative overflow-hidden rounded-2xl border border-line bg-field shadow-2xl">
67 <div className="flex items-center gap-2 border-b border-line px-4 py-3">
68 <span className="size-2.5 rounded-full bg-danger/70" />
69 <span className="size-2.5 rounded-full bg-accent/70" />
70 <span className="size-2.5 rounded-full bg-success/70" />
71 <span className="ml-2 font-latin text-xs text-muted">gitdata.ai / api</span>
72 </div>
73 <div className="grid grid-cols-[112px_1fr]">
74 <aside className="space-y-1 border-r border-line p-3 text-xs">
75 <p className="mb-2 font-medium text-ink">仓库</p>
76 <p className="rounded-md bg-page px-2 py-1.5 text-ink">api</p>
77 <p className="px-2 py-1.5 text-muted">看板</p>
78 <p className="px-2 py-1.5 text-muted">聊天</p>
79 <p className="px-2 py-1.5 text-muted">成员</p>
80 </aside>
81 <div className="space-y-2 p-4">
82 <div className="flex items-center gap-2 font-latin text-xs text-muted">
83 <GitBranch size={12} />
84 main
85 </div>
86 <div className="rounded-lg border border-line bg-page px-3 py-2 font-latin text-xs text-ink">
87 feat: 统一仓库与看板的访问校验
88 </div>
89 <div className="rounded-lg border border-line bg-page px-3 py-2 font-latin text-xs text-muted">
90 fix: 修正聊天房间通知去重
91 </div>
92 <div className="flex items-center gap-1.5 pt-1 text-xs text-success">
93 <GitPullRequest size={12} />
94 评审已通过
95 </div>
96 </div>
97 </div>
98 </div>
99 </div>
100 )
101}
102
103function FeatureCard({
104 icon: Icon,
105 title,
106 description,
107 className = '',
108}: {
109 icon: LucideIcon
110 title: string
111 description: string
112 className?: string
113}) {
114 return (
115 <article className={`landing-card rounded-2xl border border-line bg-field/60 p-6 backdrop-blur ${className}`}>
116 <span className="mb-4 flex size-10 items-center justify-center rounded-xl border border-line bg-page text-accent">
117 <Icon size={18} strokeWidth={1.8} />
118 </span>
119 <h3 className="mb-2 text-base font-semibold">{title}</h3>
120 <p className="text-sm leading-[1.7] text-muted">{description}</p>
121 </article>
122 )
123}
124
49125function LandingPage() {
50126 return (
51127 <>
52 <section className="flex flex-col items-start gap-6 py-16 md:py-24">
53 <p className="rounded-full bg-accent-soft px-3 py-1 text-xs font-medium text-accent-text">统一身份认证</p>
54 <h1 className="max-w-3xl text-4xl leading-[1.2] font-semibold tracking-[-0.8px] text-balance md:text-5xl">
55 一个账户,通达你的所有应用
56 </h1>
57 <p className="max-w-2xl text-base leading-[1.7] text-muted md:text-lg">
58 GitDataAI 为代码、协作与数据服务提供统一的登录入口、二次验证与授权管理,让身份这件事只做一次。
59 </p>
60 <div className="mt-2 flex flex-wrap items-center gap-3">
61 {/* 跨应用整页跳转:/auth 是独立的 MPA 文档,不能走客户端路由。 */}
62 <a
63 href="/auth/login"
64 className="inline-flex min-h-12 items-center justify-center rounded-lg bg-primary px-6 text-base font-semibold text-white transition-colors hover:bg-primary-hover active:bg-primary-pressed"
65 >
66 登录账户
67 </a>
68 <a
69 href="/auth/register"
70 className="inline-flex min-h-12 items-center justify-center rounded-lg border border-control-border bg-field px-6 text-base font-semibold text-ink transition-colors hover:bg-elevated"
71 >
72 创建账户
73 </a>
128 <section className="grid items-center gap-12 py-14 lg:grid-cols-[1.05fr_0.95fr] lg:gap-16 lg:py-24">
129 <div className="flex flex-col items-start gap-6">
130 <span className="inline-flex items-center gap-2 rounded-full border border-line bg-field/70 px-3 py-1 text-xs font-medium text-muted backdrop-blur">
131 <span className="size-1.5 rounded-full bg-accent" />
132 代码托管 · 评审 · 聊天
133 </span>
134 <h1 className="text-[clamp(2.4rem,5vw,4rem)] leading-[1.08] font-semibold tracking-[-1.2px] text-balance">
135 代码、协作与沟通
136 <br className="hidden sm:block" />
137 <span className="bg-gradient-to-r from-accent to-accent-text bg-clip-text text-transparent">
138 集中在一个平台
139 </span>
140 </h1>
141 <p className="max-w-xl text-base leading-[1.7] text-muted md:text-lg">
142 GitDataAI 提供 Git 仓库托管、合并请求评审、Issue 看板与团队聊天,并用一套账户打通所有空间与应用的访问权限。
143 </p>
144 <div className="mt-1 flex flex-wrap items-center gap-3">
145 {/* 跨应用整页跳转:/auth 是独立的 MPA 文档,不能走客户端路由。 */}
146 <a
147 href="/auth/login"
148 className="group inline-flex min-h-12 items-center justify-center gap-2 rounded-lg bg-primary px-6 text-base font-semibold text-white shadow-[0_10px_30px_-12px_rgba(240,77,0,0.9)] transition-colors hover:bg-primary-hover active:bg-primary-pressed"
149 >
150 登录账户
151 <ArrowRight size={18} className="transition-transform group-hover:translate-x-0.5" />
152 </a>
153 <a
154 href="/auth/register"
155 className="inline-flex min-h-12 items-center justify-center rounded-lg border border-control-border bg-field px-6 text-base font-semibold text-ink transition-colors hover:bg-elevated"
156 >
157 创建账户
158 </a>
159 </div>
74160 </div>
161 <ProductPreview />
75162 </section>
76163
77 <section className="grid gap-4 pb-20 md:grid-cols-3">
78 {features.map((feature) => (
79 <article key={feature.title} className="rounded-xl border border-line bg-field p-6">
80 <h2 className="mb-2 text-base font-semibold">{feature.title}</h2>
81 <p className="text-sm leading-[1.7] text-muted">{feature.description}</p>
82 </article>
83 ))}
164 <section className="pb-20 md:pb-28">
165 <div className="mb-10 max-w-2xl">
166 <h2 className="text-2xl font-semibold tracking-[-0.4px] md:text-3xl">从提交到讨论,都在同一个空间里</h2>
167 <p className="mt-3 text-sm leading-[1.7] text-muted md:text-base">
168 仓库、评审、看板与聊天按空间组织,权限与身份统一,减少在多个工具之间切换的上下文成本。
169 </p>
170 </div>
171 <div className="grid gap-4 md:grid-cols-6">
172 {features.map((feature) => (
173 <FeatureCard
174 key={feature.title}
175 icon={feature.icon}
176 title={feature.title}
177 description={feature.description}
178 className={feature.className}
179 />
180 ))}
181 </div>
84182 </section>
85183 </>
86184 )
@@ -92,8 +190,13 @@
92190 const privacy = import.meta.env.VITE_PRIVACY_URL
93191
94192 return (
95 <div className="flex min-h-dvh flex-col bg-page text-ink">
96 <header className="mx-auto flex h-16 w-full max-w-6xl items-center justify-between px-5 md:px-10">
193 <div className="relative flex min-h-dvh flex-col overflow-hidden bg-page text-ink">
194 <div aria-hidden className="pointer-events-none absolute inset-x-0 top-0 h-[720px]">
195 <div className="landing-aurora absolute inset-0" />
196 <div className="landing-grid absolute inset-0" />
197 </div>
198
199 <header className="relative z-10 mx-auto flex h-16 w-full max-w-6xl items-center justify-between px-5 md:px-10">
97200 <a href="/" className="inline-flex items-center gap-2.75" aria-label="GitDataAI 首页">
98201 <img src="/media/gitdataai-red-panda.png" width={40} height={40} alt="" />
99202 <span className="font-latin text-[19px] font-semibold tracking-[-0.4px]">GitDataAI</span>
@@ -103,27 +206,27 @@
103206 type="button"
104207 onClick={toggle}
105208 aria-label={dark ? '切换到浅色主题' : '切换到深色主题'}
106 className="flex size-11 items-center justify-center rounded-lg text-icon hover:bg-elevated hover:text-ink"
209 className="flex size-11 items-center justify-center rounded-lg border border-transparent text-icon transition-colors hover:border-line hover:bg-elevated hover:text-ink"
107210 >
108211 {dark ? <Sun size={18} strokeWidth={1.6} /> : <Moon size={18} strokeWidth={1.6} />}
109212 </button>
110213 <a
111214 href="/auth/login"
112 className="inline-flex min-h-10 items-center rounded-lg border border-control-border bg-field px-4 text-sm font-medium text-ink transition-colors hover:bg-elevated"
215 className="inline-flex min-h-10 items-center rounded-lg border border-control-border bg-field/70 px-4 text-sm font-medium text-ink backdrop-blur transition-colors hover:bg-elevated"
113216 >
114217 登录
115218 </a>
116219 </div>
117220 </header>
118221
119 <main className="mx-auto w-full max-w-6xl flex-1 px-5 md:px-10">
222 <main className="relative z-10 mx-auto w-full max-w-6xl flex-1 px-5 md:px-10">
120223 <Routes>
121224 <Route path="/" element={<LandingPage />} />
122225 <Route path="*" element={<Navigate to="/" replace />} />
123226 </Routes>
124227 </main>
125228
126 <footer className="border-t border-line">
229 <footer className="relative z-10 border-t border-line/70">
127230 <div className="mx-auto flex w-full max-w-6xl flex-wrap items-center justify-between gap-3 px-5 py-6 text-xs text-muted md:px-10">
128231 <span className="font-latin">© 2026 GitDataAI</span>
129232 {(terms || privacy) && (
Modified view/index/index.html view/index/index.html +52 −4 View view/index/index.html
@@ -2,15 +2,63 @@
22<html lang="zh-CN">
33 <head>
44 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6 <title>GitDataAI — Git 仓库托管、评审与团队协作</title>
7 <meta
8 name="description"
9 content="GitDataAI 提供 Git 仓库托管、合并请求评审、Issue 看板与团队聊天,并用一套账户打通所有空间与应用的访问权限。"
10 />
11 <meta name="robots" content="index, follow" />
12 <link rel="canonical" href="https://gitdata.ai/" />
513 <link rel="icon" type="image/png" href="/media/gitdataai-red-panda.png" />
6 <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
714 <meta name="referrer" content="no-referrer" />
815 <meta name="color-scheme" content="light dark" />
16
17 <meta property="og:type" content="website" />
18 <meta property="og:site_name" content="GitDataAI" />
19 <meta property="og:url" content="https://gitdata.ai/" />
20 <meta property="og:title" content="GitDataAI — Git 仓库托管、评审与团队协作" />
921 <meta
10 name="description"
11 content="GitDataAI 统一身份认证:一个账户,通达代码、协作与数据服务。"
22 property="og:description"
23 content="GitDataAI 提供 Git 仓库托管、合并请求评审、Issue 看板与团队聊天,并用一套账户打通所有空间与应用的访问权限。"
24 />
25 <meta property="og:image" content="https://gitdata.ai/media/gitdataai-red-panda.png" />
26 <meta property="og:image:width" content="400" />
27 <meta property="og:image:height" content="400" />
28 <meta property="og:locale" content="zh_CN" />
29
30 <meta name="twitter:card" content="summary" />
31 <meta name="twitter:title" content="GitDataAI — Git 仓库托管、评审与团队协作" />
32 <meta
33 name="twitter:description"
34 content="GitDataAI 提供 Git 仓库托管、合并请求评审、Issue 看板与团队聊天,并用一套账户打通所有空间与应用的访问权限。"
1235 />
13 <title>GitDataAI</title>
36 <meta name="twitter:image" content="https://gitdata.ai/media/gitdataai-red-panda.png" />
37
38 <script type="application/ld+json">
39 {
40 "@context": "https://schema.org",
41 "@graph": [
42 {
43 "@type": "Organization",
44 "@id": "https://gitdata.ai/#organization",
45 "name": "GitDataAI",
46 "url": "https://gitdata.ai/",
47 "logo": "https://gitdata.ai/media/gitdataai-red-panda.png"
48 },
49 {
50 "@type": "WebSite",
51 "@id": "https://gitdata.ai/#website",
52 "name": "GitDataAI",
53 "url": "https://gitdata.ai/",
54 "description": "Git 仓库托管、合并请求评审、Issue 看板与团队聊天,统一账户打通所有空间与应用。",
55 "inLanguage": "zh-CN",
56 "publisher": { "@id": "https://gitdata.ai/#organization" }
57 }
58 ]
59 }
60 </script>
61
1462 <script>
1563 try {
1664 const t = localStorage.getItem('auth-theme')