Initial commit: G3FO recurring investment schedule frontend

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
justin
2026-10-10 09:21:25 +08:00
co-authored by Cursor
commit 9ed161d93e
69 changed files with 10097 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
export const APP_LOCALES = ['zh-CN', 'zh-TW', 'en-US'] as const
export type AppLocale = (typeof APP_LOCALES)[number]
export const DEFAULT_LOCALE: AppLocale = 'zh-TW'
export function isAppLocale(value: string | null): value is AppLocale {
return value !== null && (APP_LOCALES as readonly string[]).includes(value)
}
+41
View File
@@ -0,0 +1,41 @@
import type { LocationQuery } from 'vue-router'
export const URL_PARAM_CLIENT_ID = 'clientId'
export const URL_PARAM_ACCOUNT_ID = 'accountId'
export interface UrlAccountContext {
clientId: string | null
accountId: string | null
}
function readQueryValue(query: LocationQuery, key: string): string | null {
const raw = query[key]
if (raw === undefined || raw === null) {
return null
}
const value = Array.isArray(raw) ? raw[0] : raw
if (value === undefined || value === null || value === '') {
return null
}
return value
}
function readSearchParam(search: string, key: string): string | null {
const params = new URLSearchParams(search.startsWith('?') ? search.slice(1) : search)
const value = params.get(key)
return value && value.trim() !== '' ? value : null
}
export function parseAccountContextFromSearch(search: string): UrlAccountContext {
return {
clientId: readSearchParam(search, URL_PARAM_CLIENT_ID),
accountId: readSearchParam(search, URL_PARAM_ACCOUNT_ID),
}
}
export function parseAccountContextFromQuery(query: LocationQuery): UrlAccountContext {
return {
clientId: readQueryValue(query, URL_PARAM_CLIENT_ID),
accountId: readQueryValue(query, URL_PARAM_ACCOUNT_ID),
}
}
+88
View File
@@ -0,0 +1,88 @@
import type { LocationQuery } from 'vue-router'
import { DEFAULT_LOCALE, isAppLocale, type AppLocale } from '@/config/appLocale'
import { APP_THEMES, DEFAULT_THEME, type AppTheme } from '@/theme/types'
export const URL_PARAM_LANG = 'lang'
export const URL_PARAM_THEME = 'theme'
export interface AppPreferences {
locale: AppLocale
theme: AppTheme
}
function readQueryValue(query: LocationQuery, key: string): string | undefined {
const raw = query[key]
if (raw === undefined || raw === null) {
return undefined
}
const value = Array.isArray(raw) ? raw[0] : raw
if (value === undefined || value === null || value === '') {
return undefined
}
return value
}
function normalizeTheme(value: string | undefined): AppTheme {
if (!value) {
return DEFAULT_THEME
}
const lower = value.toLowerCase()
if (lower === 'day' || lower === 'light') {
return 'light'
}
if (lower === 'night' || lower === 'dark') {
return 'dark'
}
return APP_THEMES.includes(lower as AppTheme) ? (lower as AppTheme) : DEFAULT_THEME
}
function normalizeLocale(value: string | undefined): AppLocale {
if (!value) {
return DEFAULT_LOCALE
}
const normalized = value.replace('_', '-')
if (isAppLocale(normalized)) {
return normalized
}
return DEFAULT_LOCALE
}
export function parsePreferencesFromSearch(search: string): AppPreferences {
const params = new URLSearchParams(search.startsWith('?') ? search.slice(1) : search)
const lang = params.get(URL_PARAM_LANG) ?? params.get('locale')
const theme = params.get(URL_PARAM_THEME)
return {
locale: normalizeLocale(lang ?? undefined),
theme: normalizeTheme(theme ?? undefined),
}
}
export function parsePreferencesFromQuery(query: LocationQuery): AppPreferences {
const lang = readQueryValue(query, URL_PARAM_LANG) ?? readQueryValue(query, 'locale')
const theme = readQueryValue(query, URL_PARAM_THEME)
return {
locale: normalizeLocale(lang),
theme: normalizeTheme(theme),
}
}
export function preferencesToQuery(
preferences: AppPreferences,
existing: LocationQuery = {},
): Record<string, string> {
const next: Record<string, string> = {}
for (const [key, value] of Object.entries(existing)) {
if (key === URL_PARAM_LANG || key === 'locale' || key === URL_PARAM_THEME) {
continue
}
if (value !== undefined && value !== null) {
const v = Array.isArray(value) ? value[0] : value
if (v !== undefined && v !== null && v !== '') {
next[key] = v
}
}
}
next[URL_PARAM_LANG] = preferences.locale
next[URL_PARAM_THEME] = preferences.theme
return next
}