feat(web): migrate application shell to TanStack Start
- replace the legacy client-only Vite bootstrap with TanStack Router routes, Start SSR, and Nitro output - compose the reusable layout, navigation, appearance, chats, and notifications blocks with application-owned data - persist UI preferences through server functions and initialize theme, sidebar, and breakpoint state before hydration - add React Query, hotkey, toaster, and responsive dashboard integration - add application-owned Lingui configuration and catalogs, mount the runtime provider, and lazily enable I18nDevtool in development - remove superseded HTML, main entry, theme provider, and application ESLint configuration
This commit is contained in:
@@ -1,22 +0,0 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"sourceLocale": "en",
|
||||
"locales": ["en", "zh-Hans"],
|
||||
"catalogPath": "src/locales/{locale}/messages",
|
||||
"include": ["src", "../../packages/blocks/src", "../../packages/ui/src"],
|
||||
"exclude": ["**/*.test.{ts,tsx}", "**/node_modules/**"]
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>vite-monorepo</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,4 @@
|
||||
import project from "./i18n.config.json" with { type: "json" }
|
||||
import { createLinguiConfig } from "@workspace/i18n/lingui-config"
|
||||
|
||||
export default createLinguiConfig(project)
|
||||
+29
-13
@@ -4,32 +4,48 @@
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint",
|
||||
"dev": "vite dev",
|
||||
"build": "vite build && tsc -b",
|
||||
"format": "prettier --write \"**/*.{ts,tsx}\"",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"preview": "vite preview"
|
||||
"i18n": "workspace-i18n",
|
||||
"i18n:compile": "workspace-i18n compile",
|
||||
"i18n:extract": "workspace-i18n extract",
|
||||
"i18n:ui": "workspace-i18n ui",
|
||||
"typecheck": "tsc -b",
|
||||
"preview": "vite preview",
|
||||
"start": "node .output/server/index.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.6.0",
|
||||
"@base-ui/utils": "^0.3.1",
|
||||
"@floating-ui/utils": "^0.2.12",
|
||||
"@hugeicons/core-free-icons": "^4.2.3",
|
||||
"@hugeicons/react": "^1.1.9",
|
||||
"@iconify/react": "^6.0.2",
|
||||
"@lingui/core": "^6",
|
||||
"@tanstack/react-form": "^1.33.2",
|
||||
"@tanstack/react-hotkeys": "^0.10.0",
|
||||
"@tanstack/react-query": "^5.101.4",
|
||||
"@tanstack/react-router": "^1.170.18",
|
||||
"@tanstack/react-start": "^1.168.32",
|
||||
"@workspace/blocks": "workspace:*",
|
||||
"@workspace/i18n": "workspace:*",
|
||||
"@workspace/ui": "workspace:*",
|
||||
"lucide-react": "^1.27.0",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6"
|
||||
"react-dom": "^19.2.6",
|
||||
"sonner": "^2.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10",
|
||||
"@tailwindcss/vite": "^4",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/node": "^24",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@vitejs/plugin-react": "^6",
|
||||
"eslint": "^10",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17",
|
||||
"nitro": "^3.0.260610-beta",
|
||||
"typescript": "~6",
|
||||
"typescript-eslint": "^8",
|
||||
"vite": "^8"
|
||||
"vite": "^8",
|
||||
"vitest": "^4.1.10"
|
||||
}
|
||||
}
|
||||
|
||||
+19
-14
@@ -1,19 +1,24 @@
|
||||
import { Button } from "@workspace/ui/components/button"
|
||||
import { AppLayout } from "@workspace/blocks/layout"
|
||||
|
||||
import { chatThreads } from "./data/chats"
|
||||
import { primaryNavigationGroups } from "./data/navigation"
|
||||
import {
|
||||
fetchMockNotifications,
|
||||
notificationsQueryKey,
|
||||
} from "./data/notifications"
|
||||
import { DashboardPage } from "./pages/dashboard-page"
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<div className="flex min-h-svh p-6">
|
||||
<div className="flex max-w-md min-w-0 flex-col gap-4 text-sm leading-loose">
|
||||
<div>
|
||||
<h1 className="font-medium">Project ready!</h1>
|
||||
<p>You may now add components and start building.</p>
|
||||
<p>We've already added the button component for you.</p>
|
||||
<Button className="mt-2">Button</Button>
|
||||
</div>
|
||||
<div className="text-muted-foreground font-mono text-xs">
|
||||
(Press <kbd>d</kbd> to toggle dark mode)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AppLayout
|
||||
chatThreads={chatThreads}
|
||||
navigationGroups={primaryNavigationGroups}
|
||||
notifications={{
|
||||
queryFn: fetchMockNotifications,
|
||||
queryKey: notificationsQueryKey,
|
||||
}}
|
||||
>
|
||||
<DashboardPage />
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
import * as React from "react"
|
||||
|
||||
type Theme = "dark" | "light" | "system"
|
||||
type ResolvedTheme = "dark" | "light"
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: React.ReactNode
|
||||
defaultTheme?: Theme
|
||||
storageKey?: string
|
||||
disableTransitionOnChange?: boolean
|
||||
}
|
||||
|
||||
type ThemeProviderState = {
|
||||
theme: Theme
|
||||
setTheme: (theme: Theme) => void
|
||||
}
|
||||
|
||||
const COLOR_SCHEME_QUERY = "(prefers-color-scheme: dark)"
|
||||
const THEME_VALUES: Theme[] = ["dark", "light", "system"]
|
||||
|
||||
const ThemeProviderContext = React.createContext<
|
||||
ThemeProviderState | undefined
|
||||
>(undefined)
|
||||
|
||||
function isTheme(value: string | null): value is Theme {
|
||||
if (value === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
return THEME_VALUES.includes(value as Theme)
|
||||
}
|
||||
|
||||
function getSystemTheme(): ResolvedTheme {
|
||||
if (window.matchMedia(COLOR_SCHEME_QUERY).matches) {
|
||||
return "dark"
|
||||
}
|
||||
|
||||
return "light"
|
||||
}
|
||||
|
||||
function disableTransitionsTemporarily() {
|
||||
const style = document.createElement("style")
|
||||
style.appendChild(
|
||||
document.createTextNode(
|
||||
"*,*::before,*::after{-webkit-transition:none!important;transition:none!important}"
|
||||
)
|
||||
)
|
||||
document.head.appendChild(style)
|
||||
|
||||
return () => {
|
||||
window.getComputedStyle(document.body)
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
style.remove()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function isEditableTarget(target: EventTarget | null) {
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (target.isContentEditable) {
|
||||
return true
|
||||
}
|
||||
|
||||
const editableParent = target.closest(
|
||||
"input, textarea, select, [contenteditable='true']"
|
||||
)
|
||||
if (editableParent) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = "system",
|
||||
storageKey = "theme",
|
||||
disableTransitionOnChange = true,
|
||||
...props
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setThemeState] = React.useState<Theme>(() => {
|
||||
const storedTheme = localStorage.getItem(storageKey)
|
||||
if (isTheme(storedTheme)) {
|
||||
return storedTheme
|
||||
}
|
||||
|
||||
return defaultTheme
|
||||
})
|
||||
|
||||
const setTheme = React.useCallback(
|
||||
(nextTheme: Theme) => {
|
||||
localStorage.setItem(storageKey, nextTheme)
|
||||
setThemeState(nextTheme)
|
||||
},
|
||||
[storageKey]
|
||||
)
|
||||
|
||||
const applyTheme = React.useCallback(
|
||||
(nextTheme: Theme) => {
|
||||
const root = document.documentElement
|
||||
const resolvedTheme =
|
||||
nextTheme === "system" ? getSystemTheme() : nextTheme
|
||||
const restoreTransitions = disableTransitionOnChange
|
||||
? disableTransitionsTemporarily()
|
||||
: null
|
||||
|
||||
root.classList.remove("light", "dark")
|
||||
root.classList.add(resolvedTheme)
|
||||
|
||||
if (restoreTransitions) {
|
||||
restoreTransitions()
|
||||
}
|
||||
},
|
||||
[disableTransitionOnChange]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
applyTheme(theme)
|
||||
|
||||
if (theme !== "system") {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const mediaQuery = window.matchMedia(COLOR_SCHEME_QUERY)
|
||||
const handleChange = () => {
|
||||
applyTheme("system")
|
||||
}
|
||||
|
||||
mediaQuery.addEventListener("change", handleChange)
|
||||
|
||||
return () => {
|
||||
mediaQuery.removeEventListener("change", handleChange)
|
||||
}
|
||||
}, [theme, applyTheme])
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.repeat) {
|
||||
return
|
||||
}
|
||||
|
||||
if (event.metaKey || event.ctrlKey || event.altKey) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isEditableTarget(event.target)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key.toLowerCase() !== "d") {
|
||||
return
|
||||
}
|
||||
|
||||
setThemeState((currentTheme) => {
|
||||
const nextTheme =
|
||||
currentTheme === "dark"
|
||||
? "light"
|
||||
: currentTheme === "light"
|
||||
? "dark"
|
||||
: getSystemTheme() === "dark"
|
||||
? "light"
|
||||
: "dark"
|
||||
|
||||
localStorage.setItem(storageKey, nextTheme)
|
||||
return nextTheme
|
||||
})
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown)
|
||||
}
|
||||
}, [storageKey])
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleStorageChange = (event: StorageEvent) => {
|
||||
if (event.storageArea !== localStorage) {
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key !== storageKey) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isTheme(event.newValue)) {
|
||||
setThemeState(event.newValue)
|
||||
return
|
||||
}
|
||||
|
||||
setThemeState(defaultTheme)
|
||||
}
|
||||
|
||||
window.addEventListener("storage", handleStorageChange)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("storage", handleStorageChange)
|
||||
}
|
||||
}, [defaultTheme, storageKey])
|
||||
|
||||
const value = React.useMemo(
|
||||
() => ({
|
||||
theme,
|
||||
setTheme,
|
||||
}),
|
||||
[theme, setTheme]
|
||||
)
|
||||
|
||||
return (
|
||||
<ThemeProviderContext.Provider {...props} value={value}>
|
||||
{children}
|
||||
</ThemeProviderContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = React.useContext(ThemeProviderContext)
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error("useTheme must be used within a ThemeProvider")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
"use client"
|
||||
|
||||
import { useUiState } from "@workspace/blocks/appearance"
|
||||
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
||||
import {
|
||||
CircleCheckIcon,
|
||||
InfoIcon,
|
||||
TriangleAlertIcon,
|
||||
OctagonXIcon,
|
||||
Loader2Icon,
|
||||
} from "lucide-react"
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const [themeMode] = useUiState("theme-mode")
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={themeMode}
|
||||
className="toaster group"
|
||||
icons={{
|
||||
success: <CircleCheckIcon className="size-4" />,
|
||||
info: <InfoIcon className="size-4" />,
|
||||
warning: <TriangleAlertIcon className="size-4" />,
|
||||
error: <OctagonXIcon className="size-4" />,
|
||||
loading: <Loader2Icon className="size-4 animate-spin" />,
|
||||
}}
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
"--border-radius": "var(--radius)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast: "cn-toast",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { ChatThread } from "@workspace/blocks/chats"
|
||||
|
||||
export const chatThreads: readonly ChatThread[] = [
|
||||
{
|
||||
avatarUrl:
|
||||
"https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/images/mock/avatar/avatar-1.webp",
|
||||
name: "Jayvion Simon",
|
||||
presence: "offline",
|
||||
},
|
||||
{
|
||||
avatarUrl:
|
||||
"https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/images/mock/avatar/avatar-2.webp",
|
||||
name: "Lucian Obrien",
|
||||
presence: "online",
|
||||
},
|
||||
{
|
||||
avatarUrl:
|
||||
"https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/images/mock/avatar/avatar-3.webp",
|
||||
name: "Deja Brady",
|
||||
},
|
||||
{
|
||||
avatarUrl:
|
||||
"https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/images/mock/avatar/avatar-4.webp",
|
||||
name: "Harrison Stein",
|
||||
},
|
||||
{
|
||||
avatarUrl:
|
||||
"https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/images/mock/avatar/avatar-5.webp",
|
||||
name: "Reece Chung",
|
||||
},
|
||||
{
|
||||
avatarUrl:
|
||||
"https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/images/mock/avatar/avatar-6.webp",
|
||||
name: "Cristopher Cardenas",
|
||||
},
|
||||
{
|
||||
avatarUrl:
|
||||
"https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/images/mock/avatar/avatar-8.webp",
|
||||
name: "Melanie Noble",
|
||||
},
|
||||
{
|
||||
avatarUrl:
|
||||
"https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/images/mock/avatar/avatar-9.webp",
|
||||
name: "Chase Day",
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,594 @@
|
||||
import {
|
||||
Alert02Icon,
|
||||
Analytics01Icon,
|
||||
AnalyticsUpIcon,
|
||||
AppWindowIcon,
|
||||
ArrowDataTransferHorizontalIcon,
|
||||
BubbleChatIcon,
|
||||
Calculator01Icon,
|
||||
CallMissed01Icon,
|
||||
CancelCircleIcon,
|
||||
Chart01Icon,
|
||||
ChartIncreaseIcon,
|
||||
CheckmarkBadge01Icon,
|
||||
CheckmarkCircle01Icon,
|
||||
ClipboardListIcon,
|
||||
Clock01Icon,
|
||||
CodeSquareIcon,
|
||||
Compass01Icon,
|
||||
CreditCardIcon,
|
||||
CrownIcon,
|
||||
Delete02Icon,
|
||||
DeliveryTruck01Icon,
|
||||
Discount01Icon,
|
||||
File01Icon,
|
||||
GalleryHorizontalIcon,
|
||||
GiftIcon,
|
||||
HistoryIcon,
|
||||
IdentityCardIcon,
|
||||
InboxDownloadIcon,
|
||||
InboxUploadIcon,
|
||||
Invoice03Icon,
|
||||
Key01Icon,
|
||||
LinkCircleIcon,
|
||||
Location01Icon,
|
||||
LockKeyIcon,
|
||||
Medal01Icon,
|
||||
MoneySendSquareIcon,
|
||||
Note01Icon,
|
||||
Package02Icon,
|
||||
PackageIcon,
|
||||
Refresh01Icon,
|
||||
RoadLocation01Icon,
|
||||
SecurityWarningIcon,
|
||||
Settings01Icon,
|
||||
Settings03Icon,
|
||||
Share01Icon,
|
||||
ShoppingBag01Icon,
|
||||
ShoppingCart01Icon,
|
||||
StarCircleIcon,
|
||||
StarIcon,
|
||||
Store01Icon,
|
||||
Structure01Icon,
|
||||
Task01Icon,
|
||||
Ticket01Icon,
|
||||
UserGroupIcon,
|
||||
UserShield01Icon,
|
||||
Wallet01Icon,
|
||||
WalletCardsIcon,
|
||||
} from "@hugeicons/core-free-icons"
|
||||
|
||||
import type { NavigationGroup } from "@workspace/blocks/navigation"
|
||||
|
||||
export const primaryNavigationGroups: readonly NavigationGroup[] = [
|
||||
{
|
||||
id: "overview",
|
||||
label: "概览",
|
||||
items: [
|
||||
{
|
||||
id: "overview-dashboard",
|
||||
to: "/",
|
||||
label: "经营总览",
|
||||
icon: Analytics01Icon,
|
||||
},
|
||||
{
|
||||
id: "overview-realtime",
|
||||
to: "/analytics/realtime",
|
||||
label: "实时监控",
|
||||
icon: AnalyticsUpIcon,
|
||||
},
|
||||
{
|
||||
id: "overview-tasks",
|
||||
to: "/workspace/tasks",
|
||||
label: "待办中心",
|
||||
icon: Task01Icon,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "catalog-and-inventory",
|
||||
label: "商品与库存",
|
||||
items: [
|
||||
{
|
||||
id: "catalog-products",
|
||||
label: "商品中心",
|
||||
icon: ShoppingBag01Icon,
|
||||
items: [
|
||||
{
|
||||
id: "catalog-products-list",
|
||||
to: "/catalog/products/list",
|
||||
label: "商品列表",
|
||||
icon: ShoppingBag01Icon,
|
||||
},
|
||||
{
|
||||
id: "catalog-products-drafts",
|
||||
to: "/catalog/products/drafts",
|
||||
label: "商品草稿",
|
||||
icon: File01Icon,
|
||||
},
|
||||
{
|
||||
id: "catalog-products-bundles",
|
||||
to: "/catalog/products/bundles",
|
||||
label: "组合商品",
|
||||
icon: Package02Icon,
|
||||
},
|
||||
{
|
||||
id: "catalog-products-attributes",
|
||||
label: "商品属性",
|
||||
icon: Settings03Icon,
|
||||
items: [
|
||||
{
|
||||
id: "catalog-product-specifications",
|
||||
to: "/catalog/products/attributes/specifications",
|
||||
label: "规格模板",
|
||||
icon: ClipboardListIcon,
|
||||
},
|
||||
{
|
||||
id: "catalog-product-brands",
|
||||
to: "/catalog/products/attributes/brands",
|
||||
label: "品牌资料",
|
||||
icon: StarIcon,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "catalog-categories",
|
||||
to: "/catalog/categories",
|
||||
label: "分类目录",
|
||||
icon: ClipboardListIcon,
|
||||
items: [
|
||||
{
|
||||
id: "catalog-category-tree",
|
||||
to: "/catalog/categories/tree",
|
||||
label: "分类树",
|
||||
icon: Structure01Icon,
|
||||
},
|
||||
{
|
||||
id: "catalog-category-navigation",
|
||||
to: "/catalog/categories/navigation",
|
||||
label: "导航分类",
|
||||
icon: Compass01Icon,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "inventory-center",
|
||||
to: "/inventory",
|
||||
label: "库存中心",
|
||||
icon: PackageIcon,
|
||||
items: [
|
||||
{
|
||||
id: "inventory-overview",
|
||||
to: "/inventory/overview",
|
||||
label: "库存总览",
|
||||
icon: Chart01Icon,
|
||||
},
|
||||
{
|
||||
id: "inventory-inbound",
|
||||
to: "/inventory/inbound",
|
||||
label: "入库单",
|
||||
icon: InboxDownloadIcon,
|
||||
},
|
||||
{
|
||||
id: "inventory-outbound",
|
||||
to: "/inventory/outbound",
|
||||
label: "出库单",
|
||||
icon: InboxUploadIcon,
|
||||
},
|
||||
{
|
||||
id: "inventory-transfers",
|
||||
to: "/inventory/transfers",
|
||||
label: "调拨单",
|
||||
icon: ArrowDataTransferHorizontalIcon,
|
||||
},
|
||||
{
|
||||
id: "inventory-alerts",
|
||||
to: "/inventory/alerts",
|
||||
label: "库存预警",
|
||||
icon: Alert02Icon,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "content-assets",
|
||||
to: "/content",
|
||||
label: "内容素材",
|
||||
icon: File01Icon,
|
||||
items: [
|
||||
{
|
||||
id: "content-articles",
|
||||
to: "/content/articles",
|
||||
label: "内容文章",
|
||||
icon: File01Icon,
|
||||
},
|
||||
{
|
||||
id: "content-media",
|
||||
to: "/content/media",
|
||||
label: "媒体资源",
|
||||
icon: GalleryHorizontalIcon,
|
||||
},
|
||||
{
|
||||
id: "content-pages",
|
||||
to: "/content/pages",
|
||||
label: "自定义页面",
|
||||
icon: AppWindowIcon,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "trade-and-fulfillment",
|
||||
label: "交易与履约",
|
||||
items: [
|
||||
{
|
||||
id: "trade-orders",
|
||||
to: "/trade/orders",
|
||||
label: "订单中心",
|
||||
icon: ShoppingCart01Icon,
|
||||
items: [
|
||||
{
|
||||
id: "trade-orders-awaiting-payment",
|
||||
to: "/trade/orders/awaiting-payment",
|
||||
label: "待付款订单",
|
||||
icon: WalletCardsIcon,
|
||||
},
|
||||
{
|
||||
id: "trade-orders-awaiting-shipment",
|
||||
to: "/trade/orders/awaiting-shipment",
|
||||
label: "待发货订单",
|
||||
icon: PackageIcon,
|
||||
},
|
||||
{
|
||||
id: "trade-orders-shipped",
|
||||
to: "/trade/orders/shipped",
|
||||
label: "配送中订单",
|
||||
icon: DeliveryTruck01Icon,
|
||||
},
|
||||
{
|
||||
id: "trade-orders-completed",
|
||||
to: "/trade/orders/completed",
|
||||
label: "已完成订单",
|
||||
icon: CheckmarkCircle01Icon,
|
||||
},
|
||||
{
|
||||
id: "trade-orders-canceled",
|
||||
to: "/trade/orders/canceled",
|
||||
label: "已取消订单",
|
||||
icon: CancelCircleIcon,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "fulfillment-center",
|
||||
to: "/fulfillment",
|
||||
label: "配送履约",
|
||||
icon: DeliveryTruck01Icon,
|
||||
items: [
|
||||
{
|
||||
id: "fulfillment-deliveries",
|
||||
to: "/fulfillment/deliveries",
|
||||
label: "配送任务",
|
||||
icon: Location01Icon,
|
||||
},
|
||||
{
|
||||
id: "fulfillment-pickups",
|
||||
to: "/fulfillment/pickups",
|
||||
label: "自提核销",
|
||||
icon: Store01Icon,
|
||||
},
|
||||
{
|
||||
id: "fulfillment-freight-rules",
|
||||
to: "/fulfillment/freight-rules",
|
||||
label: "运费规则",
|
||||
icon: RoadLocation01Icon,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "after-sales-center",
|
||||
to: "/after-sales",
|
||||
label: "售后中心",
|
||||
icon: CallMissed01Icon,
|
||||
items: [
|
||||
{
|
||||
id: "after-sales-returns",
|
||||
to: "/after-sales/returns",
|
||||
label: "退货申请",
|
||||
icon: Refresh01Icon,
|
||||
},
|
||||
{
|
||||
id: "after-sales-refunds",
|
||||
to: "/after-sales/refunds",
|
||||
label: "退款审核",
|
||||
icon: MoneySendSquareIcon,
|
||||
},
|
||||
{
|
||||
id: "after-sales-disputes",
|
||||
to: "/after-sales/disputes",
|
||||
label: "交易纠纷",
|
||||
icon: SecurityWarningIcon,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "customers-and-marketing",
|
||||
label: "客户与营销",
|
||||
items: [
|
||||
{
|
||||
id: "customer-center",
|
||||
to: "/customers",
|
||||
label: "客户中心",
|
||||
icon: IdentityCardIcon,
|
||||
items: [
|
||||
{
|
||||
id: "customer-profiles",
|
||||
to: "/customers/profiles",
|
||||
label: "客户档案",
|
||||
icon: IdentityCardIcon,
|
||||
},
|
||||
{
|
||||
id: "customer-segments",
|
||||
to: "/customers/segments",
|
||||
label: "客户分群",
|
||||
icon: UserGroupIcon,
|
||||
},
|
||||
{
|
||||
id: "customer-tiers",
|
||||
to: "/customers/tiers",
|
||||
label: "客户等级",
|
||||
icon: CrownIcon,
|
||||
},
|
||||
{
|
||||
id: "customer-risk-list",
|
||||
to: "/customers/risk-list",
|
||||
label: "风险名单",
|
||||
icon: SecurityWarningIcon,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "marketing-campaigns",
|
||||
to: "/marketing/campaigns",
|
||||
label: "营销活动",
|
||||
icon: Discount01Icon,
|
||||
items: [
|
||||
{
|
||||
id: "marketing-coupons",
|
||||
to: "/marketing/coupons",
|
||||
label: "优惠券管理",
|
||||
icon: Ticket01Icon,
|
||||
},
|
||||
{
|
||||
id: "marketing-flash-sales",
|
||||
to: "/marketing/flash-sales",
|
||||
label: "限时折扣",
|
||||
icon: Clock01Icon,
|
||||
},
|
||||
{
|
||||
id: "marketing-gift-promotions",
|
||||
to: "/marketing/gift-promotions",
|
||||
label: "满赠活动",
|
||||
icon: GiftIcon,
|
||||
},
|
||||
{
|
||||
id: "marketing-referrals",
|
||||
to: "/marketing/referrals",
|
||||
label: "邀请有礼",
|
||||
icon: Share01Icon,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "loyalty-center",
|
||||
to: "/loyalty",
|
||||
label: "会员权益",
|
||||
icon: Medal01Icon,
|
||||
items: [
|
||||
{
|
||||
id: "loyalty-points",
|
||||
to: "/loyalty/points",
|
||||
label: "积分规则",
|
||||
icon: StarCircleIcon,
|
||||
},
|
||||
{
|
||||
id: "loyalty-stored-value",
|
||||
to: "/loyalty/stored-value",
|
||||
label: "储值账户",
|
||||
icon: Wallet01Icon,
|
||||
},
|
||||
{
|
||||
id: "loyalty-benefits",
|
||||
to: "/loyalty/benefits",
|
||||
label: "权益配置",
|
||||
icon: CheckmarkBadge01Icon,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "data-and-finance",
|
||||
label: "数据与财务",
|
||||
items: [
|
||||
{
|
||||
id: "report-center",
|
||||
to: "/reports",
|
||||
label: "数据报表",
|
||||
icon: Chart01Icon,
|
||||
items: [
|
||||
{
|
||||
id: "report-sales",
|
||||
to: "/reports/sales",
|
||||
label: "销售分析",
|
||||
icon: AnalyticsUpIcon,
|
||||
},
|
||||
{
|
||||
id: "report-products",
|
||||
to: "/reports/products",
|
||||
label: "商品分析",
|
||||
icon: ShoppingBag01Icon,
|
||||
},
|
||||
{
|
||||
id: "report-customers",
|
||||
to: "/reports/customers",
|
||||
label: "客群分析",
|
||||
icon: UserGroupIcon,
|
||||
},
|
||||
{
|
||||
id: "report-channels",
|
||||
to: "/reports/channels",
|
||||
label: "渠道分析",
|
||||
icon: ChartIncreaseIcon,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "finance-center",
|
||||
to: "/finance",
|
||||
label: "财务中心",
|
||||
icon: WalletCardsIcon,
|
||||
items: [
|
||||
{
|
||||
id: "finance-settlements",
|
||||
to: "/finance/settlements",
|
||||
label: "结算账单",
|
||||
icon: Invoice03Icon,
|
||||
},
|
||||
{
|
||||
id: "finance-reconciliation",
|
||||
to: "/finance/reconciliation",
|
||||
label: "对账管理",
|
||||
icon: Calculator01Icon,
|
||||
},
|
||||
{
|
||||
id: "finance-invoices",
|
||||
to: "/finance/invoices",
|
||||
label: "发票管理",
|
||||
icon: File01Icon,
|
||||
},
|
||||
{
|
||||
id: "finance-payouts",
|
||||
to: "/finance/payouts",
|
||||
label: "打款记录",
|
||||
icon: MoneySendSquareIcon,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "system-management",
|
||||
label: "系统管理",
|
||||
items: [
|
||||
{
|
||||
id: "system-members",
|
||||
to: "/system/members",
|
||||
label: "组织成员",
|
||||
icon: UserShield01Icon,
|
||||
items: [
|
||||
{
|
||||
id: "system-members-admins",
|
||||
to: "/system/members/admins",
|
||||
label: "管理员账号",
|
||||
icon: UserShield01Icon,
|
||||
},
|
||||
{
|
||||
id: "system-members-roles",
|
||||
to: "/system/members/roles",
|
||||
label: "角色管理",
|
||||
icon: UserGroupIcon,
|
||||
},
|
||||
{
|
||||
id: "system-members-permissions",
|
||||
to: "/system/members/permissions",
|
||||
label: "权限策略",
|
||||
icon: LockKeyIcon,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "system-notifications",
|
||||
to: "/system/notifications",
|
||||
label: "消息中心",
|
||||
icon: BubbleChatIcon,
|
||||
items: [
|
||||
{
|
||||
id: "system-notification-templates",
|
||||
to: "/system/notifications/templates",
|
||||
label: "通知模板",
|
||||
icon: File01Icon,
|
||||
},
|
||||
{
|
||||
id: "system-notification-logs",
|
||||
to: "/system/notifications/logs",
|
||||
label: "发送记录",
|
||||
icon: HistoryIcon,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "system-audit",
|
||||
to: "/system/audit",
|
||||
label: "操作审计",
|
||||
icon: Note01Icon,
|
||||
},
|
||||
{
|
||||
id: "system-integrations",
|
||||
to: "/system/integrations",
|
||||
label: "开放集成",
|
||||
icon: CodeSquareIcon,
|
||||
items: [
|
||||
{
|
||||
id: "system-integration-api-keys",
|
||||
to: "/system/integrations/api-keys",
|
||||
label: "API 密钥",
|
||||
icon: Key01Icon,
|
||||
},
|
||||
{
|
||||
id: "system-integration-webhooks",
|
||||
to: "/system/integrations/webhooks",
|
||||
label: "Webhook 配置",
|
||||
icon: LinkCircleIcon,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "system-settings",
|
||||
to: "/system-configs",
|
||||
label: "系统配置",
|
||||
icon: Settings01Icon,
|
||||
items: [
|
||||
{
|
||||
id: "system-settings-store",
|
||||
to: "/system-configs/store",
|
||||
label: "门店资料",
|
||||
icon: Store01Icon,
|
||||
},
|
||||
{
|
||||
id: "system-settings-payments",
|
||||
to: "/system-configs/payments",
|
||||
label: "支付渠道",
|
||||
icon: CreditCardIcon,
|
||||
},
|
||||
{
|
||||
id: "system-settings-shipping",
|
||||
to: "/system-configs/shipping",
|
||||
label: "配送设置",
|
||||
icon: DeliveryTruck01Icon,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "system-recycle-bin",
|
||||
to: "/system/recycle-bin",
|
||||
label: "回收站",
|
||||
icon: Delete02Icon,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,226 @@
|
||||
import {
|
||||
DeliveryTruck01Icon,
|
||||
Mail01Icon,
|
||||
Message01Icon,
|
||||
PackageIcon,
|
||||
} from "@hugeicons/core-free-icons"
|
||||
import type { QueryFunctionContext } from "@tanstack/react-query"
|
||||
|
||||
import type { Notification } from "@workspace/blocks/notifications"
|
||||
|
||||
const RANDOM_USER_API_URL =
|
||||
"https://randomuser.me/api/1.4/?results=4&inc=name,email,picture&nat=au,ca,de,fr,gb,nl,nz,us&noinfo"
|
||||
|
||||
interface RandomUser {
|
||||
email: string
|
||||
name: {
|
||||
first: string
|
||||
last: string
|
||||
}
|
||||
picture: {
|
||||
medium: string
|
||||
}
|
||||
}
|
||||
|
||||
const HOUR_IN_MILLISECONDS = 60 * 60 * 1000
|
||||
|
||||
function getCreatedAt(hoursAgo: number) {
|
||||
return new Date(Date.now() - hoursAgo * HOUR_IN_MILLISECONDS).toISOString()
|
||||
}
|
||||
|
||||
export const fallbackNotifications: readonly Notification[] = [
|
||||
{
|
||||
id: "friend-request-deja-brady",
|
||||
title: "Deja Brady sent you a friend request",
|
||||
createdAt: getCreatedAt(11),
|
||||
category: "Communication",
|
||||
isRead: false,
|
||||
media: {
|
||||
kind: "avatar",
|
||||
src: "https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/images/mock/avatar/avatar-2.webp",
|
||||
alt: "Deja Brady",
|
||||
},
|
||||
actions: [
|
||||
{ id: "accept", label: "Accept" },
|
||||
{ id: "decline", label: "Decline", intent: "neutral" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "mention-minimal-ui",
|
||||
title: "Jayvon Hull mentioned you in Minimal UI",
|
||||
createdAt: getCreatedAt(24),
|
||||
category: "Project UI",
|
||||
to: "/workspace/tasks",
|
||||
isRead: false,
|
||||
media: {
|
||||
kind: "avatar",
|
||||
src: "https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/images/mock/avatar/avatar-3.webp",
|
||||
alt: "Jayvon Hull",
|
||||
},
|
||||
actions: [{ id: "view", label: "View", intent: "neutral" }],
|
||||
},
|
||||
{
|
||||
id: "order-placed",
|
||||
title: "Your order is placed",
|
||||
description: "The order is confirmed and waiting for shipping.",
|
||||
createdAt: getCreatedAt(24 * 6),
|
||||
category: "Order",
|
||||
isRead: true,
|
||||
media: {
|
||||
kind: "icon",
|
||||
icon: PackageIcon,
|
||||
tone: "warning",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "order-shipping",
|
||||
title: "Delivery in progress",
|
||||
description: "Your order has left the warehouse and is being shipped.",
|
||||
createdAt: getCreatedAt(24 * 7),
|
||||
category: "Order",
|
||||
isRead: true,
|
||||
media: {
|
||||
kind: "icon",
|
||||
icon: DeliveryTruck01Icon,
|
||||
tone: "success",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "unread-messages",
|
||||
title: "You have 5 unread messages",
|
||||
createdAt: getCreatedAt(24 * 8),
|
||||
category: "Communication",
|
||||
to: "/workspace/tasks",
|
||||
isRead: true,
|
||||
media: {
|
||||
kind: "icon",
|
||||
icon: Message01Icon,
|
||||
tone: "info",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "new-mail",
|
||||
title: "You have new mail",
|
||||
createdAt: getCreatedAt(24 * 9),
|
||||
category: "Communication",
|
||||
isRead: true,
|
||||
media: {
|
||||
kind: "icon",
|
||||
icon: Mail01Icon,
|
||||
tone: "destructive",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export const notificationsQueryKey = ["notifications"] as const
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null
|
||||
}
|
||||
|
||||
function isRandomUser(value: unknown): value is RandomUser {
|
||||
if (!isRecord(value) || !isRecord(value.name) || !isRecord(value.picture)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
typeof value.email === "string" &&
|
||||
typeof value.name.first === "string" &&
|
||||
typeof value.name.last === "string" &&
|
||||
typeof value.picture.medium === "string"
|
||||
)
|
||||
}
|
||||
|
||||
function getUserName(user: RandomUser) {
|
||||
return `${user.name.first} ${user.name.last}`
|
||||
}
|
||||
|
||||
function getAvatarMedia(user: RandomUser) {
|
||||
return {
|
||||
alt: getUserName(user),
|
||||
kind: "avatar" as const,
|
||||
src: user.picture.medium,
|
||||
}
|
||||
}
|
||||
|
||||
function createRandomUserNotifications(
|
||||
users: readonly RandomUser[]
|
||||
): readonly Notification[] {
|
||||
const [friend, mention, commenter, collaborator] = users
|
||||
|
||||
if (!friend || !mention || !commenter || !collaborator) {
|
||||
throw new Error("Random User API returned too few users")
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: `friend-request:${friend.email}`,
|
||||
title: `${getUserName(friend)} sent you a friend request`,
|
||||
createdAt: getCreatedAt(11),
|
||||
category: "Communication",
|
||||
isRead: false,
|
||||
media: getAvatarMedia(friend),
|
||||
actions: [
|
||||
{ id: "accept", label: "Accept" },
|
||||
{ id: "decline", label: "Decline", intent: "neutral" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: `mention:${mention.email}`,
|
||||
title: `${getUserName(mention)} mentioned you in Minimal UI`,
|
||||
createdAt: getCreatedAt(24),
|
||||
category: "Project UI",
|
||||
to: "/workspace/tasks",
|
||||
isRead: false,
|
||||
media: getAvatarMedia(mention),
|
||||
actions: [{ id: "view", label: "View", intent: "neutral" }],
|
||||
},
|
||||
{
|
||||
id: `comment:${commenter.email}`,
|
||||
title: `${getUserName(commenter)} commented on your task`,
|
||||
description: "Can you review the latest changes?",
|
||||
createdAt: getCreatedAt(24 * 2),
|
||||
category: "Project UI",
|
||||
to: "/workspace/tasks",
|
||||
isRead: false,
|
||||
media: getAvatarMedia(commenter),
|
||||
},
|
||||
{
|
||||
id: `collaborator:${collaborator.email}`,
|
||||
title: `${getUserName(collaborator)} joined your workspace`,
|
||||
createdAt: getCreatedAt(24 * 4),
|
||||
category: "Team",
|
||||
isRead: true,
|
||||
media: getAvatarMedia(collaborator),
|
||||
},
|
||||
...fallbackNotifications.slice(2),
|
||||
]
|
||||
}
|
||||
|
||||
export async function fetchMockNotifications({
|
||||
signal,
|
||||
}: QueryFunctionContext): Promise<readonly Notification[]> {
|
||||
try {
|
||||
const response = await fetch(RANDOM_USER_API_URL, { signal })
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Random User API responded with ${response.status}`)
|
||||
}
|
||||
|
||||
const payload: unknown = await response.json()
|
||||
|
||||
if (!isRecord(payload) || !Array.isArray(payload.results)) {
|
||||
throw new Error("Random User API returned an invalid response")
|
||||
}
|
||||
|
||||
const users = payload.results.filter(isRandomUser)
|
||||
|
||||
return createRandomUserNotifications(users)
|
||||
} catch (error) {
|
||||
if (signal.aborted) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return fallbackNotifications
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { lazy, Suspense, type PropsWithChildren } from "react"
|
||||
import { I18nProvider, type LocaleDefinition } from "@workspace/i18n"
|
||||
|
||||
import { messages as englishMessages } from "@/locales/en/messages"
|
||||
import { messages as simplifiedChineseMessages } from "@/locales/zh-Hans/messages"
|
||||
|
||||
const I18nDevtool = import.meta.env.DEV
|
||||
? lazy(() =>
|
||||
import("@workspace/i18n/devtool").then((module) => ({
|
||||
default: module.I18nDevtool,
|
||||
}))
|
||||
)
|
||||
: null
|
||||
|
||||
export const appLocales = [
|
||||
{
|
||||
label: "English",
|
||||
locale: "en",
|
||||
},
|
||||
{
|
||||
label: "简体中文",
|
||||
locale: "zh-Hans",
|
||||
},
|
||||
] as const satisfies readonly LocaleDefinition[]
|
||||
|
||||
export type AppLocale = (typeof appLocales)[number]["locale"]
|
||||
|
||||
const appCatalogs = {
|
||||
en: englishMessages,
|
||||
"zh-Hans": simplifiedChineseMessages,
|
||||
}
|
||||
|
||||
export interface AppI18nProviderProps extends PropsWithChildren {
|
||||
locale?: AppLocale
|
||||
}
|
||||
|
||||
export function AppI18nProvider({
|
||||
children,
|
||||
locale = "zh-Hans",
|
||||
}: AppI18nProviderProps) {
|
||||
return (
|
||||
<I18nProvider catalogs={appCatalogs} locale={locale} locales={appLocales}>
|
||||
{children}
|
||||
{I18nDevtool && (
|
||||
<Suspense fallback={null}>
|
||||
<I18nDevtool dark={"ui\\:dark"} />
|
||||
</Suspense>
|
||||
)}
|
||||
</I18nProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { QueryClient } from "@tanstack/react-query"
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 1000 * 60 * 5,
|
||||
gcTime: 1000 * 60 * 10,
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: @lingui/cli\n"
|
||||
"Language: en\n"
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
@@ -0,0 +1,2 @@
|
||||
/*eslint-disable*/ import type { Messages } from "@lingui/core"
|
||||
export const messages = JSON.parse("{}") as Messages
|
||||
@@ -0,0 +1,14 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: @lingui/cli\n"
|
||||
"Language: zh-Hans\n"
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
@@ -0,0 +1,2 @@
|
||||
/*eslint-disable*/ import type { Messages } from "@lingui/core"
|
||||
export const messages = JSON.parse("{}") as Messages
|
||||
@@ -1,14 +0,0 @@
|
||||
import { StrictMode } from "react"
|
||||
import { createRoot } from "react-dom/client"
|
||||
|
||||
import "@workspace/ui/globals.css"
|
||||
import { App } from "./App.tsx"
|
||||
import { ThemeProvider } from "@/components/theme-provider.tsx"
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<ThemeProvider>
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</StrictMode>
|
||||
)
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Button } from "@workspace/ui/components/button"
|
||||
import { Slider } from "@workspace/ui/components/slider"
|
||||
import { Switch } from "@workspace/ui/components/switch"
|
||||
|
||||
export function DashboardPage() {
|
||||
return (
|
||||
<>
|
||||
<img
|
||||
className="h-auto w-full"
|
||||
src="https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/background/background-3-blur.webp"
|
||||
/>
|
||||
<div className="flex max-w-md min-w-0 flex-col gap-4 text-sm leading-loose">
|
||||
<div>
|
||||
<h1 className="font-medium">Project ready!</h1>
|
||||
<p>You may now add components and start building.</p>
|
||||
<p>We've already added the button component for you.</p>
|
||||
<Button variant="ghost" className="mt-2">
|
||||
Button
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Switch />
|
||||
<Slider
|
||||
defaultValue={[75]}
|
||||
max={100}
|
||||
step={1}
|
||||
className="mx-auto w-full max-w-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="font-mono text-xs text-muted-foreground">
|
||||
(Press <kbd>Mod+D</kbd> to toggle dark mode)
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="mr-2">breakpoint:</span>
|
||||
<span className="hidden mobile:inline">mobile</span>
|
||||
<span className="hidden tablet:inline">tablet</span>
|
||||
<span className="hidden desktop:inline">desktop</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="mr-2">collapsed:</span>
|
||||
<span className="hidden collapsed:inline">true</span>
|
||||
<span className="inline collapsed:hidden">false</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// This file was automatically generated by TanStack Router.
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as AppRouteImport } from './routes/_app'
|
||||
import { Route as AppIndexRouteImport } from './routes/_app.index'
|
||||
import { Route as AppSplatRouteImport } from './routes/_app.$'
|
||||
|
||||
const AppRoute = AppRouteImport.update({
|
||||
id: '/_app',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AppIndexRoute = AppIndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => AppRoute,
|
||||
} as any)
|
||||
const AppSplatRoute = AppSplatRouteImport.update({
|
||||
id: '/$',
|
||||
path: '/$',
|
||||
getParentRoute: () => AppRoute,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof AppIndexRoute
|
||||
'/$': typeof AppSplatRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/$': typeof AppSplatRoute
|
||||
'/': typeof AppIndexRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/_app': typeof AppRouteWithChildren
|
||||
'/_app/$': typeof AppSplatRoute
|
||||
'/_app/': typeof AppIndexRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/' | '/$'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/$' | '/'
|
||||
id: '__root__' | '/_app' | '/_app/$' | '/_app/'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
AppRoute: typeof AppRouteWithChildren
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/_app': {
|
||||
id: '/_app'
|
||||
path: ''
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof AppRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/_app/': {
|
||||
id: '/_app/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof AppIndexRouteImport
|
||||
parentRoute: typeof AppRoute
|
||||
}
|
||||
'/_app/$': {
|
||||
id: '/_app/$'
|
||||
path: '/$'
|
||||
fullPath: '/$'
|
||||
preLoaderRoute: typeof AppSplatRouteImport
|
||||
parentRoute: typeof AppRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface AppRouteChildren {
|
||||
AppSplatRoute: typeof AppSplatRoute
|
||||
AppIndexRoute: typeof AppIndexRoute
|
||||
}
|
||||
|
||||
const AppRouteChildren: AppRouteChildren = {
|
||||
AppSplatRoute: AppSplatRoute,
|
||||
AppIndexRoute: AppIndexRoute,
|
||||
}
|
||||
|
||||
const AppRouteWithChildren = AppRoute._addFileChildren(AppRouteChildren)
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
AppRoute: AppRouteWithChildren,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
|
||||
import type { getRouter } from './router.tsx'
|
||||
import type { createStart } from '@tanstack/react-start'
|
||||
declare module '@tanstack/react-start' {
|
||||
interface Register {
|
||||
ssr: true
|
||||
router: Awaited<ReturnType<typeof getRouter>>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createRouter } from "@tanstack/react-router"
|
||||
|
||||
import { routeTree } from "./route-tree.gen"
|
||||
|
||||
export function getRouter() {
|
||||
return createRouter({
|
||||
routeTree,
|
||||
defaultPreload: "intent",
|
||||
scrollRestoration: true,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import type { ReactNode } from "react"
|
||||
import {
|
||||
HeadContent,
|
||||
Outlet,
|
||||
Scripts,
|
||||
createRootRoute,
|
||||
} from "@tanstack/react-router"
|
||||
import {
|
||||
BreakpointReactor,
|
||||
getBreakpointInitializationScript,
|
||||
} from "@workspace/ui/hooks/use-breakpoint"
|
||||
|
||||
import appCss from "@/styles/globals.css?url"
|
||||
import {
|
||||
AppearanceController,
|
||||
buildThemeStyleSheet,
|
||||
THEME_STYLE_ELEMENT_ID,
|
||||
type UiState,
|
||||
UiStateProvider,
|
||||
} from "@workspace/blocks/appearance"
|
||||
import { SidebarStateProvider } from "@workspace/blocks/layout"
|
||||
import { getUiState, setUiState } from "@/server/ui-state.functions"
|
||||
import { HotkeysProvider } from "@tanstack/react-hotkeys"
|
||||
import { TooltipProvider } from "@workspace/ui/components/tooltip"
|
||||
import { QueryClientProvider } from "@tanstack/react-query"
|
||||
import { AppI18nProvider } from "@/i18n/app-i18n-provider"
|
||||
import { queryClient } from "@/lib/query-client"
|
||||
|
||||
export const Route = createRootRoute({
|
||||
loader: () => getUiState(),
|
||||
head: () => ({
|
||||
meta: [
|
||||
{ charSet: "utf-8" },
|
||||
{
|
||||
name: "viewport",
|
||||
content: "width=device-width, initial-scale=1",
|
||||
},
|
||||
{ title: "Simple React App Kit" },
|
||||
],
|
||||
links: [
|
||||
{
|
||||
rel: "stylesheet",
|
||||
href: appCss,
|
||||
},
|
||||
],
|
||||
}),
|
||||
component: RootComponent,
|
||||
})
|
||||
|
||||
function RootComponent() {
|
||||
const initialState = Route.useLoaderData()
|
||||
|
||||
return (
|
||||
<RootDocument initialState={initialState}>
|
||||
<AppI18nProvider>
|
||||
<BreakpointReactor />
|
||||
<UiStateProvider
|
||||
initialState={initialState}
|
||||
onStateChange={(update) => setUiState({ data: update })}
|
||||
>
|
||||
<SidebarStateProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<HotkeysProvider>
|
||||
<TooltipProvider>
|
||||
<AppearanceController />
|
||||
<Outlet />
|
||||
</TooltipProvider>
|
||||
</HotkeysProvider>
|
||||
</QueryClientProvider>
|
||||
</SidebarStateProvider>
|
||||
</UiStateProvider>
|
||||
</AppI18nProvider>
|
||||
</RootDocument>
|
||||
)
|
||||
}
|
||||
|
||||
function getUiInitializationScript(state: UiState) {
|
||||
return `(() => {
|
||||
const theme = ${JSON.stringify(state["theme-mode"])};
|
||||
const resolvedTheme =
|
||||
theme === "system"
|
||||
? window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: theme;
|
||||
const root = document.documentElement;
|
||||
root.classList.remove("ui:light", "ui:dark");
|
||||
root.classList.add("ui:" + resolvedTheme);
|
||||
root.classList.toggle("ui:compacted", ${state["main-compacted"]});
|
||||
root.classList.toggle("ui:collapsed", ${state["sidebar-collapsed"]});
|
||||
root.style.colorScheme = resolvedTheme;
|
||||
})();`
|
||||
}
|
||||
|
||||
function RootDocument({
|
||||
children,
|
||||
initialState,
|
||||
}: Readonly<{
|
||||
children: ReactNode
|
||||
initialState: UiState
|
||||
}>) {
|
||||
const themeMode = initialState["theme-mode"]
|
||||
const className = [
|
||||
themeMode === "system" ? null : `ui:${themeMode}`,
|
||||
initialState["main-compacted"] ? "ui:compacted" : null,
|
||||
initialState["sidebar-collapsed"] ? "ui:collapsed" : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
|
||||
return (
|
||||
<html
|
||||
lang="zh-CN"
|
||||
suppressHydrationWarning
|
||||
className={className || undefined}
|
||||
style={themeMode === "system" ? undefined : { colorScheme: themeMode }}
|
||||
>
|
||||
<head>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: getBreakpointInitializationScript(),
|
||||
}}
|
||||
/>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: getUiInitializationScript(initialState),
|
||||
}}
|
||||
/>
|
||||
<HeadContent />
|
||||
<style
|
||||
id={THEME_STYLE_ELEMENT_ID}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: buildThemeStyleSheet(
|
||||
initialState["theme-light"],
|
||||
initialState["theme-dark"]
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
{children}
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
|
||||
import { DashboardPage } from "@/pages/dashboard-page"
|
||||
|
||||
export const Route = createFileRoute("/_app/$")({
|
||||
component: DashboardPage,
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
|
||||
import { DashboardPage } from "@/pages/dashboard-page"
|
||||
|
||||
export const Route = createFileRoute("/_app/")({
|
||||
component: DashboardPage,
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Outlet, createFileRoute } from "@tanstack/react-router"
|
||||
|
||||
import { AppLayout } from "@workspace/blocks/layout"
|
||||
|
||||
import { chatThreads } from "@/data/chats"
|
||||
import { primaryNavigationGroups } from "@/data/navigation"
|
||||
import {
|
||||
fetchMockNotifications,
|
||||
notificationsQueryKey,
|
||||
} from "@/data/notifications"
|
||||
|
||||
export const Route = createFileRoute("/_app")({
|
||||
component: ApplicationLayoutRoute,
|
||||
})
|
||||
|
||||
function ApplicationLayoutRoute() {
|
||||
return (
|
||||
<AppLayout
|
||||
chatThreads={chatThreads}
|
||||
navigationGroups={primaryNavigationGroups}
|
||||
notifications={{
|
||||
queryFn: fetchMockNotifications,
|
||||
queryKey: notificationsQueryKey,
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { createServerFn } from "@tanstack/react-start"
|
||||
import { isUiStateUpdate } from "@workspace/blocks/appearance"
|
||||
import { readUiState, writeUiState } from "./ui-state.server"
|
||||
|
||||
export const getUiState = createServerFn({ method: "GET" }).handler(() => {
|
||||
return readUiState()
|
||||
})
|
||||
|
||||
export const setUiState = createServerFn({ method: "POST" })
|
||||
.validator((value: unknown) => {
|
||||
if (!isUiStateUpdate(value)) {
|
||||
throw new Error("Invalid UI state update")
|
||||
}
|
||||
return value
|
||||
})
|
||||
.handler(({ data }) => {
|
||||
writeUiState(data.key, data.value)
|
||||
})
|
||||
@@ -0,0 +1,39 @@
|
||||
import "@tanstack/react-start/server-only"
|
||||
import { getCookie, setCookie } from "@tanstack/react-start/server"
|
||||
import {
|
||||
type UiState,
|
||||
type UiStateKey,
|
||||
uiStateDefinitions,
|
||||
} from "@workspace/blocks/appearance"
|
||||
|
||||
const LEGACY_THEME_COOKIE = "simple-shop-admin-theme"
|
||||
|
||||
export function readUiState(): UiState {
|
||||
return {
|
||||
"main-compacted": uiStateDefinitions["main-compacted"].parse(
|
||||
getCookie(uiStateDefinitions["main-compacted"].cookie)
|
||||
),
|
||||
"sidebar-collapsed": uiStateDefinitions["sidebar-collapsed"].parse(
|
||||
getCookie(uiStateDefinitions["sidebar-collapsed"].cookie)
|
||||
),
|
||||
"theme-mode": uiStateDefinitions["theme-mode"].parse(
|
||||
getCookie(uiStateDefinitions["theme-mode"].cookie) ??
|
||||
getCookie(LEGACY_THEME_COOKIE)
|
||||
),
|
||||
"theme-light": uiStateDefinitions["theme-light"].parse(
|
||||
getCookie(uiStateDefinitions["theme-light"].cookie)
|
||||
),
|
||||
"theme-dark": uiStateDefinitions["theme-dark"].parse(
|
||||
getCookie(uiStateDefinitions["theme-dark"].cookie)
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
export function writeUiState<K extends UiStateKey>(key: K, value: UiState[K]) {
|
||||
const definition = uiStateDefinitions[key]
|
||||
setCookie(definition.cookie, definition.serialize(value), {
|
||||
path: "/",
|
||||
sameSite: "lax",
|
||||
maxAge: 60 * 60 * 24 * 365,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
@import "@workspace/ui/globals.css";
|
||||
@import "@workspace/blocks/globals.css";
|
||||
|
||||
:root {
|
||||
--ui-content-top-gap: --spacing(2);
|
||||
--ui-header-height: --spacing(18);
|
||||
--ui-sidebar-collapsed-width: --spacing(22);
|
||||
--ui-sidebar-width: --spacing(74);
|
||||
}
|
||||
|
||||
.ui\:mobile {
|
||||
--ui-header-height: --spacing(16);
|
||||
--ui-sidebar-width: 0px;
|
||||
}
|
||||
|
||||
.ui\:tablet,
|
||||
.ui\:collapsed:not(.ui\:mobile) {
|
||||
--ui-sidebar-width: var(--ui-sidebar-collapsed-width);
|
||||
}
|
||||
|
||||
.ui\:theme-changing *,
|
||||
.ui\:theme-changing *::before,
|
||||
.ui\:theme-changing *::after {
|
||||
-webkit-transition: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
@keyframes ui-show-scroll-divider {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@utility scroll-top-divider {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@supports (animation-timeline: scroll()) {
|
||||
.scroll-top-divider {
|
||||
animation: ui-show-scroll-divider 1s steps(1, end) both;
|
||||
animation-range: 0px var(--ui-content-top-gap, 1px);
|
||||
animation-timeline: scroll(root block);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"verbatimModuleSyntax": false,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
@@ -23,6 +23,7 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@workspace/blocks/*": ["../../packages/blocks/src/*"],
|
||||
"@workspace/ui/*": ["../../packages/ui/src/*"]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@workspace/blocks/*": ["../../packages/blocks/src/*"],
|
||||
"@workspace/ui/*": ["../../packages/ui/src/*"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
@@ -20,5 +21,5 @@
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
"include": ["vite.config.ts", "lingui.config.ts"]
|
||||
}
|
||||
|
||||
+23
-1
@@ -1,14 +1,36 @@
|
||||
import path from "path"
|
||||
import tailwindcss from "@tailwindcss/vite"
|
||||
import { tanstackStart } from "@tanstack/react-start/plugin/vite"
|
||||
import { i18n } from "@workspace/i18n/vite"
|
||||
import react from "@vitejs/plugin-react"
|
||||
import { nitro } from "nitro/vite"
|
||||
import { defineConfig } from "vite"
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
tsconfigPaths: true,
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ["use-sync-external-store/shim/with-selector"],
|
||||
},
|
||||
plugins: [
|
||||
i18n(),
|
||||
tailwindcss(),
|
||||
tanstackStart({
|
||||
router: {
|
||||
generatedRouteTree: "route-tree.gen.ts",
|
||||
},
|
||||
srcDirectory: "src",
|
||||
}),
|
||||
react(),
|
||||
nitro(),
|
||||
],
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
port: 31573,
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user