diff --git a/.env.production b/.env.production index 52b64b0..e239e6a 100644 --- a/.env.production +++ b/.env.production @@ -5,3 +5,4 @@ VITE_ENABLE_QUERY_DEVTOOLS=false VITE_ENABLE_REQUEST_LOG=false # 客户端密钥 VITE_AUTH_TOKEN_SECRET=564d14asdasd113e46542asd6das1a2a + diff --git a/.gitignore b/.gitignore index d5ec0f6..385237f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ pnpm-debug.log* lerna-debug.log* node_modules +zihua-web.zip +zihua-web dist dist-ssr coverage diff --git a/AGENTS.md b/AGENTS.md index dcc4b76..d4b620c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ # GitNexus — Code Intelligence -This project is indexed by GitNexus as **36-character-flower** (2394 symbols, 4479 relationships, 203 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **36-character-flower** (2505 symbols, 4694 relationships, 215 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/CLAUDE.md b/CLAUDE.md index dcc4b76..d4b620c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,7 @@ # GitNexus — Code Intelligence -This project is indexed by GitNexus as **36-character-flower** (2394 symbols, 4479 relationships, 203 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **36-character-flower** (2505 symbols, 4694 relationships, 215 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/figma/img.png b/figma/img.png index 5b9db42..d016190 100644 Binary files a/figma/img.png and b/figma/img.png differ diff --git a/src/constants/auth.ts b/src/constants/auth.ts index b1d11d7..83e95bc 100644 --- a/src/constants/auth.ts +++ b/src/constants/auth.ts @@ -4,11 +4,15 @@ export const AUTH_STORAGE_KEY = 'auth-session' /** @description 认证模块调用的后端接口地址集合。 */ export const AUTH_ENDPOINTS = { login: 'api/user/login', + logout: 'api/user/logout', profile: 'api/user/profile', refreshToken: 'api/user/refreshToken', register: 'api/user/register', } as const +/** @description 后端返回该 code 表示登录态 token 无效或已过期。 */ +export const AUTH_INVALID_TOKEN_CODE = 1101 + /** @description 获取接口鉴权 auth-token 时使用的接口地址。 */ export const AUTH_TOKEN_ENDPOINT = 'api/v1/authToken' diff --git a/src/features/auth/api/auth-api.ts b/src/features/auth/api/auth-api.ts index 353d017..7301fbd 100644 --- a/src/features/auth/api/auth-api.ts +++ b/src/features/auth/api/auth-api.ts @@ -9,6 +9,8 @@ import type { AuthUserProfileDto, LoginPayload, LoginRequestDto, + LogoutPayload, + LogoutRequestDto, RefreshTokenDto, RefreshTokenRequestDto, RegisterPayload, @@ -107,6 +109,25 @@ export async function loginWithPassword( return session } +export async function logoutWithPassword( + payload: LogoutPayload, +): Promise { + const response = await api.post( + AUTH_ENDPOINTS.logout, + { + json: { + password: payload.password, + username: payload.username, + }, + }, + ) + + unwrapEnvelope( + response as ApiResponse, + 'auth.logout.errors.submitFailed', + ) +} + export async function registerWithPassword( payload: RegisterPayload, ): Promise { diff --git a/src/features/auth/api/types.ts b/src/features/auth/api/types.ts index e7a13ec..21cca30 100644 --- a/src/features/auth/api/types.ts +++ b/src/features/auth/api/types.ts @@ -56,6 +56,11 @@ export interface LoginRequestDto { username: string } +export interface LogoutRequestDto { + password: string + username: string +} + export interface RegisterRequestDto extends LoginRequestDto { invite_code: string } @@ -69,6 +74,8 @@ export interface LoginPayload { username: string } +export type LogoutPayload = LoginPayload + export interface RegisterPayload extends LoginPayload { inviteCode: string } diff --git a/src/features/auth/hooks/use-register-form.ts b/src/features/auth/hooks/use-register-form.ts index cced755..ce37895 100644 --- a/src/features/auth/hooks/use-register-form.ts +++ b/src/features/auth/hooks/use-register-form.ts @@ -16,16 +16,17 @@ interface UseRegisterFormOptions { } const REGISTER_INVITE_CODE_QUERY_PARAM = 'registerInviteCode' +const DEFAULT_REGISTER_INVITE_CODE = 'D97DBC16' function getInitialRegisterInviteCode() { if (typeof window === 'undefined') { - return '' + return DEFAULT_REGISTER_INVITE_CODE } return ( new URLSearchParams(window.location.search) .get(REGISTER_INVITE_CODE_QUERY_PARAM) - ?.trim() ?? '' + ?.trim() || DEFAULT_REGISTER_INVITE_CODE ) } diff --git a/src/features/game/components/desktop/desktop-header.tsx b/src/features/game/components/desktop/desktop-header.tsx index 7eb7f00..80c316d 100644 --- a/src/features/game/components/desktop/desktop-header.tsx +++ b/src/features/game/components/desktop/desktop-header.tsx @@ -10,7 +10,6 @@ import { VolumeX, } from 'lucide-react' import { useTranslation } from 'react-i18next' -import add from '@/assets/game/add.webp' import avatar from '@/assets/system/avatar.webp' import diamond from '@/assets/system/diamond.webp' import logo from '@/assets/system/logo.webp' diff --git a/src/features/game/components/desktop/desktop-status.tsx b/src/features/game/components/desktop/desktop-status.tsx index 1546add..52b37e6 100644 --- a/src/features/game/components/desktop/desktop-status.tsx +++ b/src/features/game/components/desktop/desktop-status.tsx @@ -13,6 +13,7 @@ import { SmartImage } from '@/components/smart-image.tsx' import { DesktopCountdown } from '@/features/game/components/desktop/desktop-countdown.tsx' import { DesktopTitle } from '@/features/game/components/desktop/desktop-title.tsx' import { useGameStatusVm } from '@/features/game/hooks/use-game-status-vm.ts' +import { cn } from '@/lib/utils.ts' export function DesktopStatusLine() { const { t } = useTranslation() @@ -20,7 +21,6 @@ export function DesktopStatusLine() { countdownMs, limitLabel, oddsLabel, - phaseDescription, phaseLabel, phaseToneClassName, roundId, @@ -40,6 +40,17 @@ export function DesktopStatusLine() { return (
+ {/**/} + {/* */} + {/*
*/} + +
+ +
-
-
- {t('gameDesktop.status.roundId')}:{roundId} -
+
-
{phaseLabel}
+
+ {phaseLabel} +
-
{phaseDescription}
+
+ +
+ {t('gameDesktop.status.roundId')}:{roundId}
-
- -
) } diff --git a/src/features/game/components/desktop/desktop-title.tsx b/src/features/game/components/desktop/desktop-title.tsx index 9398863..b9e54fb 100644 --- a/src/features/game/components/desktop/desktop-title.tsx +++ b/src/features/game/components/desktop/desktop-title.tsx @@ -1,10 +1,8 @@ -import { useTranslation } from 'react-i18next' import broadcast from '@/assets/system/broadcast.webp' import { SmartImage } from '@/components/smart-image.tsx' import { useGameSessionStore } from '@/store/game' export function DesktopTitle() { - const { t } = useTranslation() const jackpotBroadcasts = useGameSessionStore( (state) => state.jackpotBroadcasts, ) @@ -30,9 +28,6 @@ export function DesktopTitle() { alt={'broadcast'} src={broadcast} /> -
- {t('gameDesktop.title.announcement')}: -
+
+
+ +
+ + {authStatus === 'authenticated' ? ( +
+ + + +
+ ) : ( +
+ + +
+ )} +
+ +
+
+
+
+ +
+
+ {t('gameDesktop.header.systemTime')} +
+
+ {systemTimeLabel} +
+
+ + + + + + + + +
+
+ + ) +} diff --git a/src/features/game/entry/entry-page.tsx b/src/features/game/entry/entry-page.tsx index 165ec85..13b9b38 100644 --- a/src/features/game/entry/entry-page.tsx +++ b/src/features/game/entry/entry-page.tsx @@ -2,7 +2,6 @@ import { startTransition, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { getGameLobbyInit } from '@/features/game' -import { EntryNoticeGateModal } from '@/features/game/components' import { MobileEntry } from '@/features/game/entry/mobile-entry.tsx' import { PcEntry } from '@/features/game/entry/pc-entry.tsx' import { useGameRealtimeSync } from '@/features/game/hooks/use-game-realtime-sync.ts' @@ -147,8 +146,6 @@ export function EntryPage() { className="flex min-h-0 flex-1 flex-col" > {isMobile ? : } - - ) } diff --git a/src/features/game/entry/mobile-entry.tsx b/src/features/game/entry/mobile-entry.tsx index 192e7fe..0348e70 100644 --- a/src/features/game/entry/mobile-entry.tsx +++ b/src/features/game/entry/mobile-entry.tsx @@ -1,7 +1,12 @@ -import { useTranslation } from 'react-i18next' +import { MobileHeader } from '@/features/game/components/mobile/mobile-header.tsx' +import { useAutoHostingRunner } from '@/features/game/hooks/use-auto-hosting-runner.ts' export function MobileEntry() { - const { t } = useTranslation() + useAutoHostingRunner() - return
{t('gameDesktop.mobile.placeholder')}
+ return ( + <> + + + ) } diff --git a/src/features/game/entry/pc-entry.tsx b/src/features/game/entry/pc-entry.tsx index 9c055f4..fc34316 100644 --- a/src/features/game/entry/pc-entry.tsx +++ b/src/features/game/entry/pc-entry.tsx @@ -1,4 +1,4 @@ -import { DesktopHeader } from '@/features/game/components' +import { DesktopHeader, EntryNoticeGateModal } from '@/features/game/components' import { DesktopAnimal } from '@/features/game/components/desktop/desktop-animal.tsx' import { DesktopControl } from '@/features/game/components/desktop/desktop-control.tsx' import { DesktopGameHistory } from '@/features/game/components/desktop/desktop-game-history.tsx' @@ -22,9 +22,7 @@ export function PcEntry() { <>
@@ -71,6 +69,8 @@ export function PcEntry() { {/* 大奖/小奖动画展示 */} + {/* 强制弹窗 */} + ) } diff --git a/src/features/game/hooks/use-game-history-vm.ts b/src/features/game/hooks/use-game-history-vm.ts index 32e05f3..a6058bc 100644 --- a/src/features/game/hooks/use-game-history-vm.ts +++ b/src/features/game/hooks/use-game-history-vm.ts @@ -38,14 +38,18 @@ export function useGameHistoryVm() { const { i18n, t } = useTranslation() const accessToken = useAuthStore((state) => state.accessToken) const authStatus = useAuthStore((state) => state.status) - const roundId = useGameRoundStore((state) => state.round.id) - const winningCellId = useGameRoundStore((state) => state.round.winningCellId) - const lastOpenedRoundRef = useRef(null) + const revealPhase = useGameRoundStore((state) => state.revealAnimation.phase) + const revealRoundId = useGameRoundStore( + (state) => state.revealAnimation.roundId, + ) + const lastRevealedRoundRef = useRef(null) const query = useInfiniteQuery({ queryKey: ['game', 'bet-my-orders', accessToken], enabled: authStatus === 'authenticated' && Boolean(accessToken), initialPageParam: 1, + refetchOnMount: 'always', + staleTime: 0, queryFn: ({ pageParam }) => getGameBetMyOrders({ page: pageParam, @@ -63,72 +67,62 @@ export function useGameHistoryVm() { const items = useMemo( () => (query.data?.pages ?? []).flatMap((page) => - page.list.map((entry) => ({ - amountLabel: entry.total_amount, - createdAtLabel: formatCreatedTime( - entry.create_time, - i18n.resolvedLanguage ?? 'en-US', - ), - id: entry.order_no, - resultState: - entry.result_number === null - ? ('pending' satisfies HistoryResultState) - : entry.numbers.includes(entry.result_number) - ? ('win' satisfies HistoryResultState) - : ('lost' satisfies HistoryResultState), - numbersLabel: formatNumbers(entry.numbers), - numbers: entry.numbers, - orderNo: entry.order_no, - periodNo: entry.period_no, - resultNumberLabel: - entry.result_number === null - ? '--' - : String(entry.result_number).padStart(2, '0'), - winAmountLabel: entry.win_amount, - })), + page.list.map((entry) => { + const shouldHideResult = + entry.period_no === revealRoundId && revealPhase !== 'result' + const resultNumber = shouldHideResult ? null : entry.result_number + + return { + amountLabel: entry.total_amount, + createdAtLabel: formatCreatedTime( + entry.create_time, + i18n.resolvedLanguage ?? 'en-US', + ), + id: entry.order_no, + resultState: + resultNumber === null + ? ('pending' satisfies HistoryResultState) + : entry.numbers.includes(resultNumber) + ? ('win' satisfies HistoryResultState) + : ('lost' satisfies HistoryResultState), + numbersLabel: formatNumbers(entry.numbers), + numbers: entry.numbers, + orderNo: entry.order_no, + periodNo: entry.period_no, + resultNumberLabel: + resultNumber === null + ? '--' + : String(resultNumber).padStart(2, '0'), + winAmountLabel: entry.win_amount, + } + }), ), - [i18n.resolvedLanguage, query.data?.pages], + [i18n.resolvedLanguage, query.data?.pages, revealPhase, revealRoundId], ) useEffect(() => { - const openedRoundKey = - winningCellId === null || roundId.length === 0 - ? null - : `${roundId}:${winningCellId}` - - if (openedRoundKey === null) { + if (revealPhase !== 'result' || !revealRoundId) { return } - if (lastOpenedRoundRef.current === null) { - lastOpenedRoundRef.current = openedRoundKey + if (lastRevealedRoundRef.current === revealRoundId) { return } - if (lastOpenedRoundRef.current === openedRoundKey) { + if (authStatus !== 'authenticated' || query.isFetching || query.isLoading) { return } - lastOpenedRoundRef.current = openedRoundKey - - if ( - authStatus !== 'authenticated' || - items.length >= GAME_HISTORY_PAGE_SIZE || - query.isFetching || - query.isLoading - ) { - return - } + lastRevealedRoundRef.current = revealRoundId void query.refetch() }, [ authStatus, - items.length, query.isFetching, query.isLoading, query.refetch, - roundId, - winningCellId, + revealPhase, + revealRoundId, ]) return { diff --git a/src/features/game/hooks/use-game-realtime-sync.ts b/src/features/game/hooks/use-game-realtime-sync.ts index ef762d1..a6054b2 100644 --- a/src/features/game/hooks/use-game-realtime-sync.ts +++ b/src/features/game/hooks/use-game-realtime-sync.ts @@ -492,6 +492,8 @@ function applyPeriodOpenedMessage( message: GameSocketMessage, serverTime: number | null, ) { + console.log('%c[period.opened 开奖数据]', 'color: red;', message) + applyPeriodMessage(message, serverTime) const period = extractPeriodEventData(message) @@ -580,6 +582,8 @@ function applyWalletChangedMessage(message: GameSocketMessage) { } function applyJackpotHitMessage(message: GameSocketMessage) { + console.log('%c[jackpot.hit 数据]', 'color: red;', message) + const jackpotHitData = extractJackpotHitData(message) if (jackpotHitData?.hits.length) { diff --git a/src/features/game/modal/desktop/desktop-userInfo-modal.tsx b/src/features/game/modal/desktop/desktop-userInfo-modal.tsx index a66b150..4c443b2 100644 --- a/src/features/game/modal/desktop/desktop-userInfo-modal.tsx +++ b/src/features/game/modal/desktop/desktop-userInfo-modal.tsx @@ -1,7 +1,9 @@ +import { useMutation } from '@tanstack/react-query' import dayjs from 'dayjs' import { CircleUserRound, ClipboardList, + LogOut, ReceiptText, WalletCards, } from 'lucide-react' @@ -13,8 +15,10 @@ import userInfoBg from '@/assets/system/userInfo-bg.webp' import { CenterModal } from '@/components/center-modal.tsx' import { SmartBackground } from '@/components/smart-background.tsx' import { SmartImage } from '@/components/smart-image.tsx' +import { logoutWithPassword } from '@/features/auth/api/auth-api' import DesktopFinanceRecordsTab from '@/features/game/modal/desktop/desktop-finance-records-tab' import DesktopWalletRecordsTab from '@/features/game/modal/desktop/desktop-wallet-records-tab' +import { clearAuthenticatedSession } from '@/lib/auth/auth-session' import { notify } from '@/lib/notify' import { cn } from '@/lib/utils' import { useAuthStore, useModalStore } from '@/store' @@ -88,6 +92,11 @@ function DesktopUserInfoModal() { const [activeTab, setActiveTab] = useState('profile') const currentUser = useAuthStore((state) => state.currentUser) const inviteCode = currentUser?.registerInviteCode?.trim() ?? '' + const logoutUsername = + currentUser?.username ?? currentUser?.phone ?? currentUser?.name ?? '' + const logoutMutation = useMutation({ + mutationFn: logoutWithPassword, + }) useEffect(() => { if (!open) { @@ -112,6 +121,25 @@ function DesktopUserInfoModal() { } } + async function handleLogout() { + if (logoutMutation.isPending) { + return + } + + try { + await logoutMutation.mutateAsync({ + password: '', + username: logoutUsername, + }) + notify.success(t('commonUi.toast.logoutSuccess')) + } catch { + notify.warning(t('commonUi.toast.logoutLocalOnly')) + } finally { + clearAuthenticatedSession({ clearBrowserStorage: true }) + setModalOpen('desktopUserInfo', false) + } + } + return (
+ +
+ +
) : activeTab === 'financeRecords' ? ( { + return Boolean( + value && + typeof value === 'object' && + 'code' in value && + typeof value.code === 'number', + ) +} + +function getApiEnvelopeMessage(response: ApiResponse) { + return 'msg' in response && typeof response.msg === 'string' + ? response.msg + : 'message' in response && typeof response.message === 'string' + ? response.message + : API_ERROR_MESSAGES.unexpected +} + +function assertValidAuthEnvelope(data: unknown) { + if (!isApiEnvelope(data) || data.code !== AUTH_INVALID_TOKEN_CODE) { + return + } + + handleInvalidTokenSession() + + throw new ApiError({ + data, + message: getApiEnvelopeMessage(data), + status: 401, + }) +} + function unwrapEnvelopeData(response: ApiResponse) { + assertValidAuthEnvelope(response) + if (response.code === 1) { return response.data } @@ -313,6 +352,8 @@ async function request(input: string, options?: Options) { ) const data = await parseResponseBody(response) + assertValidAuthEnvelope(data) + return data as TResponse } catch (error) { if ( diff --git a/src/lib/auth/auth-session.ts b/src/lib/auth/auth-session.ts index 3cadaef..84f65e8 100644 --- a/src/lib/auth/auth-session.ts +++ b/src/lib/auth/auth-session.ts @@ -1,5 +1,9 @@ +import i18n from '@/i18n' +import { notify } from '@/lib/notify' +import { queryClient } from '@/lib/query/query-client' import type { AuthSessionInput, AuthUser } from '@/store/auth' import { useAuthStore } from '@/store/auth' +import { useModalStore } from '@/store/modal' export type CurrentUserInitializer = () => Promise export type RefreshSessionHandler = ( @@ -10,6 +14,28 @@ let currentUserInitializer: CurrentUserInitializer | null = null let refreshSessionHandler: RefreshSessionHandler | null = null let authInitializationPromise: Promise | null = null let refreshSessionPromise: Promise | null = null +let lastLoginPromptAt = 0 + +const LOGIN_PROMPT_DEDUP_MS = 1200 + +interface ClearAuthenticatedSessionOptions { + clearBrowserStorage?: boolean +} + +interface UnauthorizedSessionOptions extends ClearAuthenticatedSessionOptions { + openLoginModal?: boolean + showLoginRequiredToast?: boolean +} + +function clearBrowserStorageData() { + if (typeof localStorage !== 'undefined') { + localStorage.clear() + } + + if (typeof sessionStorage !== 'undefined') { + sessionStorage.clear() + } +} export function registerCurrentUserInitializer( initializer: CurrentUserInitializer | null, @@ -29,8 +55,55 @@ export function isAuthenticated() { return snapshot.status === 'authenticated' && Boolean(snapshot.accessToken) } -export function handleUnauthorizedSession() { +export function clearAuthenticatedSession({ + clearBrowserStorage = true, +}: ClearAuthenticatedSessionOptions = {}) { useAuthStore.getState().markUnauthorized() + queryClient.clear() + + if (clearBrowserStorage) { + clearBrowserStorageData() + } +} + +export function handleUnauthorizedSession({ + clearBrowserStorage = false, + openLoginModal = false, + showLoginRequiredToast = false, +}: UnauthorizedSessionOptions = {}) { + clearAuthenticatedSession({ clearBrowserStorage }) + + if (!openLoginModal && !showLoginRequiredToast) { + return + } + + const now = Date.now() + const shouldPrompt = now - lastLoginPromptAt > LOGIN_PROMPT_DEDUP_MS + + if (!shouldPrompt) { + return + } + + lastLoginPromptAt = now + + if (showLoginRequiredToast) { + notify.warning(i18n.t('commonUi.toast.loginRequired')) + } + + if (openLoginModal) { + const modalStore = useModalStore.getState() + + modalStore.closeAllModals() + modalStore.setModalOpen('desktopLogin', true) + } +} + +export function handleInvalidTokenSession() { + handleUnauthorizedSession({ + clearBrowserStorage: true, + openLoginModal: true, + showLoginRequiredToast: true, + }) } export async function initializeAuthSession() { diff --git a/src/locales/en-US/common.ts b/src/locales/en-US/common.ts index 726f896..a505e3b 100644 --- a/src/locales/en-US/common.ts +++ b/src/locales/en-US/common.ts @@ -160,6 +160,8 @@ export default { tel: 'Phone', registeredAt: 'Registered at', copyInviteLink: 'Copy invite link', + logout: 'Log out', + loggingOut: 'Logging out...', signature: 'My signature is as unique as my personality. This area will later display the real profile summary.', }, @@ -244,6 +246,8 @@ export default { lobbyInitFailed: 'Failed to load the game lobby', loginRequired: 'Please log in before entering the game', loginSuccess: 'Login successful', + logoutSuccess: 'Logged out', + logoutLocalOnly: 'Logout request failed. Local session was cleared.', registerSuccess: 'Registration successful', inviteLinkCopied: 'Invite link copied', inviteLinkCopyFailed: diff --git a/src/locales/id-ID/common.ts b/src/locales/id-ID/common.ts index 1eb9808..795507b 100644 --- a/src/locales/id-ID/common.ts +++ b/src/locales/id-ID/common.ts @@ -159,6 +159,8 @@ export default { tel: 'Telepon', registeredAt: 'Tanggal daftar', copyInviteLink: 'Salin tautan undangan', + logout: 'Keluar', + loggingOut: 'Keluar...', signature: 'Tanda tanganku seunik diriku. Bagian ini nantinya akan menampilkan ringkasan profil yang sebenarnya.', }, @@ -243,6 +245,8 @@ export default { lobbyInitFailed: 'Gagal memuat lobby game', loginRequired: 'Silakan masuk sebelum memasuki game', loginSuccess: 'Berhasil masuk', + logoutSuccess: 'Berhasil keluar', + logoutLocalOnly: 'Permintaan keluar gagal. Sesi lokal telah dibersihkan.', registerSuccess: 'Pendaftaran berhasil', inviteLinkCopied: 'Tautan undangan disalin', inviteLinkCopyFailed: @@ -362,7 +366,7 @@ export default { message: 'Pesan', bgm: 'BGM', id: 'ID', - fullscreen: 'Layar Penuh', + fullscreen: 'Layar', login: 'Masuk', register: 'Daftar', }, diff --git a/src/locales/ms-MY/common.ts b/src/locales/ms-MY/common.ts index 4931e8f..05d2db1 100644 --- a/src/locales/ms-MY/common.ts +++ b/src/locales/ms-MY/common.ts @@ -162,6 +162,8 @@ export default { tel: 'Telefon', registeredAt: 'Tarikh daftar', copyInviteLink: 'Salin pautan jemputan', + logout: 'Log Keluar', + loggingOut: 'Sedang log keluar...', signature: 'Tandatangan saya unik seperti personaliti saya. Bahagian ini akan memaparkan ringkasan profil sebenar kemudian.', }, @@ -246,6 +248,9 @@ export default { lobbyInitFailed: 'Gagal memuatkan lobi permainan', loginRequired: 'Sila log masuk sebelum memasuki permainan', loginSuccess: 'Log masuk berjaya', + logoutSuccess: 'Telah log keluar', + logoutLocalOnly: + 'Permintaan log keluar gagal. Sesi tempatan telah dikosongkan.', registerSuccess: 'Pendaftaran berjaya', inviteLinkCopied: 'Pautan jemputan telah disalin', inviteLinkCopyFailed: @@ -366,7 +371,7 @@ export default { message: 'Mesej', bgm: 'BGM', id: 'ID', - fullscreen: 'Skrin Penuh', + fullscreen: 'Skrin', login: 'Log Masuk', register: 'Daftar', }, diff --git a/src/locales/zh-CN/common.ts b/src/locales/zh-CN/common.ts index ac5913c..883a9a2 100644 --- a/src/locales/zh-CN/common.ts +++ b/src/locales/zh-CN/common.ts @@ -157,6 +157,8 @@ export default { tel: '电话', registeredAt: '注册时间', copyInviteLink: '复制邀请链接', + logout: '退出登录', + loggingOut: '退出中...', signature: '我的个性签名和灵魂一样独特,后续这里会接真实资料字段。', }, message: { @@ -238,6 +240,8 @@ export default { lobbyInitFailed: '游戏大厅加载失败', loginRequired: '请先登录后进入游戏', loginSuccess: '登录成功', + logoutSuccess: '已退出登录', + logoutLocalOnly: '退出接口请求失败,已清除本地登录状态', registerSuccess: '注册成功', inviteLinkCopied: '邀请链接已复制', inviteLinkCopyFailed: '邀请链接复制失败,请手动复制', diff --git a/vite.config.ts b/vite.config.ts index 2b05666..032f749 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,7 +5,6 @@ import { tanstackRouter } from '@tanstack/router-plugin/vite' import react, { reactCompilerPreset } from '@vitejs/plugin-react' import { defineConfig } from 'vite' -// https://vite.dev/config/ export default defineConfig({ resolve: { alias: { @@ -32,4 +31,7 @@ export default defineConfig({ react(), babel({ presets: [reactCompilerPreset()] }), ], + build: { + outDir: 'zihua-web', + }, })