diff --git a/apps/user/app/(main)/(content)/(user)/layout.tsx b/apps/user/app/(main)/(content)/(user)/layout.tsx index 4755f96..f9733e9 100644 --- a/apps/user/app/(main)/(content)/(user)/layout.tsx +++ b/apps/user/app/(main)/(content)/(user)/layout.tsx @@ -7,8 +7,7 @@ import { SidebarLeft } from './sidebar-left'; export default async function DashboardLayout({ children }: { children: React.ReactNode }) { const cookieStore = await cookies(); - const defaultOpen = cookieStore.get('sidebar:state')?.value === 'true'; - + const defaultOpen = cookieStore.get('sidebar:state')?.value !== 'false'; // 默认 true,除非明确为 'false' return ( diff --git a/apps/user/app/auth/LoginDialogContext.tsx b/apps/user/app/auth/LoginDialogContext.tsx index 17d9f9c..f7b7948 100644 --- a/apps/user/app/auth/LoginDialogContext.tsx +++ b/apps/user/app/auth/LoginDialogContext.tsx @@ -1,8 +1,6 @@ 'use client'; import EmailAuthForm from '@/app/auth/email/auth-form'; -import CloseSvg from '@/components/CustomIcon/icons/close.svg'; import { Dialog, DialogContent, DialogTitle } from '@workspace/airo-ui/components/dialog'; -import Image from 'next/image'; import { createContext, forwardRef, @@ -66,11 +64,7 @@ const LoginDialog = forwardRef((props, ref) => { return ( - } - closeClassName='right-[40px] top-[30px] font-bold text-black opacity-100 focus:ring-0 focus:ring-offset-0' - > + Login
diff --git a/apps/user/public/svg-icon/affiliate.svg b/apps/user/assets/svg-icon/affiliate.svg similarity index 100% rename from apps/user/public/svg-icon/affiliate.svg rename to apps/user/assets/svg-icon/affiliate.svg diff --git a/apps/user/public/svg-icon/copy.svg b/apps/user/assets/svg-icon/copy.svg similarity index 100% rename from apps/user/public/svg-icon/copy.svg rename to apps/user/assets/svg-icon/copy.svg diff --git a/apps/user/public/svg-icon/dashboard.svg b/apps/user/assets/svg-icon/dashboard.svg similarity index 100% rename from apps/user/public/svg-icon/dashboard.svg rename to apps/user/assets/svg-icon/dashboard.svg diff --git a/apps/user/public/svg-icon/document.svg b/apps/user/assets/svg-icon/document.svg similarity index 100% rename from apps/user/public/svg-icon/document.svg rename to apps/user/assets/svg-icon/document.svg diff --git a/apps/user/public/svg-icon/exit.svg b/apps/user/assets/svg-icon/exit.svg similarity index 100% rename from apps/user/public/svg-icon/exit.svg rename to apps/user/assets/svg-icon/exit.svg diff --git a/apps/user/public/svg-icon/notes.svg b/apps/user/assets/svg-icon/notes.svg similarity index 100% rename from apps/user/public/svg-icon/notes.svg rename to apps/user/assets/svg-icon/notes.svg diff --git a/apps/user/public/svg-icon/profile.svg b/apps/user/assets/svg-icon/profile.svg similarity index 100% rename from apps/user/public/svg-icon/profile.svg rename to apps/user/assets/svg-icon/profile.svg diff --git a/apps/user/public/svg-icon/qrcode.svg b/apps/user/assets/svg-icon/qrcode.svg similarity index 100% rename from apps/user/public/svg-icon/qrcode.svg rename to apps/user/assets/svg-icon/qrcode.svg diff --git a/apps/user/public/svg-icon/shop.svg b/apps/user/assets/svg-icon/shop.svg similarity index 100% rename from apps/user/public/svg-icon/shop.svg rename to apps/user/assets/svg-icon/shop.svg diff --git a/apps/user/public/svg-icon/ticket.svg b/apps/user/assets/svg-icon/ticket.svg similarity index 100% rename from apps/user/public/svg-icon/ticket.svg rename to apps/user/assets/svg-icon/ticket.svg diff --git a/apps/user/public/svg-icon/wallet.svg b/apps/user/assets/svg-icon/wallet.svg similarity index 100% rename from apps/user/public/svg-icon/wallet.svg rename to apps/user/assets/svg-icon/wallet.svg diff --git a/apps/user/components/SvgIcon.tsx b/apps/user/components/SvgIcon.tsx index bd1db95..f74e089 100644 --- a/apps/user/components/SvgIcon.tsx +++ b/apps/user/components/SvgIcon.tsx @@ -1,3 +1,4 @@ +'use client'; import { useEffect, useState } from 'react'; const SvgIcon = ({ name, ...props }) => { @@ -6,7 +7,7 @@ const SvgIcon = ({ name, ...props }) => { useEffect(() => { let isMounted = true; - import(`public/svg-icon/${name}.svg`) + import(`@/assets/svg-icon/${name}.svg`) .then((module) => { if (isMounted) { setIcon(() => module.default); diff --git a/apps/user/components/main/HomeContent.tsx b/apps/user/components/main/HomeContent.tsx index dbbd38a..3cc7321 100644 --- a/apps/user/components/main/HomeContent.tsx +++ b/apps/user/components/main/HomeContent.tsx @@ -1,21 +1,23 @@ 'use client'; import { Button } from '@workspace/airo-ui/components/button'; +import { useTranslations } from 'next-intl'; import { useRef } from 'react'; import OfferDialog, { OfferDialogRef } from './OfferDialog/index'; export default function HomeContent() { const dialogRef = useRef(null); + const t = useTranslations('components.home'); return (
{/* 大标题 */}

- 连接 + {t('connect')}
- 任何时间 + {t('anytime')}
- 任何地点 + {t('anywhere')}

{/* 副标题 */}
@@ -23,16 +25,16 @@ export default function HomeContent() { AiroPort - 提供极稳,极简,极速的网络服务 + {t('serviceSlogan')}

-

获取订阅地址,开始顶级的私密网络体验

+

{t('getSubscription')}

{/* 按钮 */} diff --git a/apps/user/components/main/OfferDialog/TabContent.tsx b/apps/user/components/main/OfferDialog/TabContent.tsx index df034aa..152c776 100644 --- a/apps/user/components/main/OfferDialog/TabContent.tsx +++ b/apps/user/components/main/OfferDialog/TabContent.tsx @@ -1,3 +1,4 @@ +import { useTranslations } from 'next-intl'; import React from 'react'; import { PlanList } from './index'; import { ProcessedPlanData } from './types'; @@ -23,6 +24,7 @@ export const TabContent: React.FC = ({ onSubscribe, firstPlanCardRef, }) => { + const t = useTranslations('components.offerDialog'); return (
{tabValue === 'year' && ( @@ -32,7 +34,7 @@ export const TabContent: React.FC = ({ tabValue={tabValue} error={error} onRetry={onRetry} - emptyMessage='暂无年付套餐' + emptyMessage={t('noYearlyPlan')} onSubscribe={onSubscribe} firstPlanCardRef={firstPlanCardRef} /> @@ -44,7 +46,7 @@ export const TabContent: React.FC = ({ isLoading={isLoading} error={error} onRetry={onRetry} - emptyMessage='暂无月付套餐' + emptyMessage={t('noMonthlyPlan')} onSubscribe={onSubscribe} firstPlanCardRef={firstPlanCardRef} /> diff --git a/apps/user/components/main/OfferDialog/index.tsx b/apps/user/components/main/OfferDialog/index.tsx index 180174c..2e5486f 100644 --- a/apps/user/components/main/OfferDialog/index.tsx +++ b/apps/user/components/main/OfferDialog/index.tsx @@ -1,11 +1,9 @@ -import CloseSvg from '@/components/CustomIcon/icons/close.svg'; import { getSubscription } from '@/services/user/portal'; import { useQuery } from '@tanstack/react-query'; import { Dialog, DialogContent, DialogTitle } from '@workspace/airo-ui/components/dialog'; import { ScrollArea } from '@workspace/airo-ui/components/scroll-area'; import { Tabs, TabsList, TabsTrigger } from '@workspace/airo-ui/components/tabs'; import { unitConversion } from '@workspace/airo-ui/utils'; -import Image from 'next/image'; import { forwardRef, useCallback, @@ -20,25 +18,31 @@ import { TabContent } from './TabContent'; import { ProcessedPlanData } from './types'; // 加载状态组件 -const LoadingState = () => ( -
-
-

加载中...

-
-); +const LoadingState = () => { + const t = useTranslations('components.offerDialog'); + return ( +
+
+

{t('loading')}

+
+ ); +}; // 错误状态组件 -const ErrorState = ({ onRetry }: { onRetry: () => void }) => ( -
-

加载失败,请重试

- -
-); +const ErrorState = ({ onRetry }: { onRetry: () => void }) => { + const t = useTranslations('components.offerDialog'); + return ( +
+

{t('loadFailed')}

+ +
+ ); +}; // 空状态组件 const EmptyState = ({ message }: { message: string }) => ( @@ -48,34 +52,41 @@ const EmptyState = ({ message }: { message: string }) => ( ); // 价格显示组件 -const PriceDisplay = ({ plan }: { plan: ProcessedPlanData }) => ( -
-
- {plan.origin_price && ( - - ${plan.origin_price} +const PriceDisplay = ({ plan }: { plan: ProcessedPlanData }) => { + const t = useTranslations('components.offerDialog'); + return ( +
+
+ {plan.origin_price && ( + + ${plan.origin_price} + + )} + + ${plan.discount_price} + + {t('perYear')} + +
+ {plan.origin_price && ( +

{t('yearlyDiscount')}

)} - - ${plan.discount_price} - - /年
- {plan.origin_price && ( -

年付享受8折优惠

- )} -
-); + ); +}; import { useLoginDialog } from '@/app/auth/LoginDialogContext'; import { Display } from '@/components/display'; import Purchase from '@/components/subscribe/purchase'; import useGlobalStore from '@/config/use-global'; import { useTranslations } from 'next-intl'; + // 订阅按钮组件 const SubscribeButton = ({ onClick }: { onClick?: () => void }) => { const { user } = useGlobalStore(); const { openLoginDialog } = useLoginDialog(); + const t = useTranslations('components.offerDialog'); function handleClick() { console.log('click', user); @@ -92,7 +103,7 @@ const SubscribeButton = ({ onClick }: { onClick?: () => void }) => { onClick={handleClick} className='h-10 w-full rounded-full bg-[#0F2C53] text-sm font-medium text-white shadow-md transition-all duration-300 hover:bg-[#225BA9] sm:h-10 sm:text-sm md:h-[40px] md:text-[14px]' > - 订阅 + {t('subscribe')} ); }; @@ -111,7 +122,8 @@ const StarRating = ({ rating, maxRating = 5 }: { rating: number; maxRating?: num // 功能列表组件 const FeatureList = ({ plan }: { plan: ProcessedPlanData }) => { const t = useTranslations('subscribe.detail'); - const features = [{ label: '可用节点', value: plan.features?.nodes || '11' }]; + const tOffer = useTranslations('components.offerDialog'); + const features = [{ label: tOffer('availableNodes'), value: plan.features?.nodes || '11' }]; return (
@@ -154,7 +166,7 @@ const FeatureList = ({ plan }: { plan: ProcessedPlanData }) => {
  • - 网络稳定指数: + {tOffer('networkStabilityIndex')}
    @@ -246,6 +258,7 @@ export interface OfferDialogRef { } const OfferDialog = forwardRef((props, ref) => { + const t = useTranslations('components.offerDialog'); const [open, setOpen] = useState(false); const [tabValue, setTabValue] = useState('year'); const [selectedPlan, setSelectedPlan] = useState(null); @@ -379,17 +392,13 @@ const OfferDialog = forwardRef((props, ref) => { } - closeClassName={ - 'right-6 top-6 font-bold text-black opacity-100 focus:ring-0 focus:ring-offset-0' - } >
    - 选择套餐 + {t('selectPlan')}
    - 选择最适合您的服务套餐 + {t('selectYourPlan')}
    ((props, ref) => { } value='year' > - 年付套餐 + {t('yearlyPlan')} ((props, ref) => { } value='month' > - 月付套餐 + {t('monthlyPlan')} diff --git a/apps/user/components/subscribe/billing.tsx b/apps/user/components/subscribe/billing.tsx index 788438a..37e9819 100644 --- a/apps/user/components/subscribe/billing.tsx +++ b/apps/user/components/subscribe/billing.tsx @@ -1,79 +1,49 @@ 'use client'; -import { Display } from '@/components/display'; -import { Separator } from '@workspace/airo-ui/components/separator'; +import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card'; +import { Separator } from '@workspace/ui/components/separator'; import { useTranslations } from 'next-intl'; -interface SubscribeBillingProps { - order?: Partial< - API.OrderDetail & { - unit_price: number; - unit_time: number; - subscribe_discount: number; - } - >; -} - -export function SubscribeBilling({ order }: Readonly) { - const t = useTranslations('subscribe'); - +export function SubscribeBilling({ order }: { order: API.Order }) { + const t = useTranslations('subscribe.billing'); + const t_c = useTranslations('components.billing'); return ( - <> -
    {t('billing.billingTitle')}
    -
      -
    • - 套餐时长 - - {order?.quantity === 1 ? '30天' : ''} - {order?.quantity === 12 ? '365天' : ''} - -
    • - {order?.type && [1, 2].includes(order?.type) && ( -
    • - {t('billing.duration')} - - {order?.quantity || 1} {t(order?.unit_time || 'Month')} + + + {t('billingTitle')} + + +
      +
      + {t('productDiscount')} + -¥ {order?.discount_amount} +
      +
      + {t('couponDiscount')} + -¥ {order?.coupon_discount_amount} +
      +
      + {t_c('planDuration')} + + {order?.quantity === 1 ? t_c('30days') : ''} + {order?.quantity === 12 ? t_c('365days') : ''} -
    • - )} -
    • - {t('billing.price')} - - - -
    • -
    • - {t('billing.productDiscount')} - - - -
    • -
    • - {t('billing.couponDiscount')} - - - -
    • -
    • - {t('billing.fee')} - - - -
    • -
    • - {t('billing.gift')} - - - -
    • -
    - -
    - 支付金额 - - - -
    - +
    +
    + {t('gift')} + -¥ {order?.gift_balance_deduction_amount} +
    +
    + {t('fee')} + ¥ {order?.fee} +
    + +
    + {t('total')} + ¥ {order?.total_amount} +
    +
  • + + ); } diff --git a/apps/user/components/user-nav.tsx b/apps/user/components/user-nav.tsx index 0c7b273..4277ba1 100644 --- a/apps/user/components/user-nav.tsx +++ b/apps/user/components/user-nav.tsx @@ -10,6 +10,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@workspace/airo-ui/components/dropdown-menu'; +import { useSidebar } from '@workspace/airo-ui/components/sidebar'; import { Icon } from '@workspace/airo-ui/custom-components/icon'; import { useIsMobile } from '@workspace/airo-ui/hooks/use-mobile'; import { useTranslations } from 'next-intl'; @@ -20,7 +21,7 @@ export function UserNav({ from = '' }: { from?: string }) { const { user, setUser } = useGlobalStore(); const router = useRouter(); const pathname = usePathname(); - // const { toggleSidebar } = useSidebar(); + const { toggleSidebar } = useSidebar(); const isMobile = useIsMobile(); if (user) { return ( @@ -97,7 +98,7 @@ export function UserNav({ from = '' }: { from?: string }) { data-active={pathname === item.url} onClick={() => { if (pathname === item.url) return; - /* toggleSidebar();*/ + toggleSidebar(); router.push(`${item.url}`); }} className='flex cursor-pointer items-center gap-3 rounded-full bg-white px-5 py-2 text-base font-medium focus:bg-[#0F2C53] focus:text-white data-[active=true]:bg-[#0F2C53] data-[active=true]:text-white md:text-xl' diff --git a/apps/user/locales/en-US/components.json b/apps/user/locales/en-US/components.json new file mode 100644 index 0000000..1374cdd --- /dev/null +++ b/apps/user/locales/en-US/components.json @@ -0,0 +1,36 @@ +{ + "billing": { + "planDuration": "Plan Duration", + "30days": "30 Days", + "365days": "365 Days", + "paymentAmount": "Payment Amount" + }, + "home": { + "connect": "Connect", + "anytime": "Anytime", + "anywhere": "Anywhere", + "serviceSlogan": "Providing extremely stable, simple, and fast network services", + "getSubscription": "Get your subscription URL and start your premium private network experience", + "viewSubscriptionPlans": "View Subscription Plans" + }, + "language": { + "japanese": "日本語", + "simplifiedChinese": "Simplified Chinese" + }, + "offerDialog": { + "loading": "Loading...", + "loadFailed": "Failed to load, please try again", + "reload": "Reload", + "perYear": "/year", + "yearlyDiscount": "Enjoy a 20% discount on annual payment", + "subscribe": "Subscribe", + "availableNodes": "Available Nodes", + "networkStabilityIndex": "Network Stability Index:", + "selectPlan": "Select Plan", + "selectYourPlan": "Select the service plan that suits you best", + "yearlyPlan": "Yearly Plan", + "monthlyPlan": "Monthly Plan", + "noYearlyPlan": "No yearly plans available", + "noMonthlyPlan": "No monthly plans available" + } +} diff --git a/apps/user/locales/request.ts b/apps/user/locales/request.ts index 6081675..6211d18 100644 --- a/apps/user/locales/request.ts +++ b/apps/user/locales/request.ts @@ -24,6 +24,7 @@ export default getRequestConfig(async () => { ticket: (await import(`./${locale}/ticket.json`)).default, document: (await import(`./${locale}/document.json`)).default, affiliate: (await import(`./${locale}/affiliate.json`)).default, + components: (await import(`./${locale}/components.json`)).default, }; return { diff --git a/apps/user/locales/zh-CN/components.json b/apps/user/locales/zh-CN/components.json new file mode 100644 index 0000000..b010600 --- /dev/null +++ b/apps/user/locales/zh-CN/components.json @@ -0,0 +1,36 @@ +{ + "billing": { + "planDuration": "套餐时长", + "30days": "30天", + "365days": "365天", + "paymentAmount": "支付金额" + }, + "home": { + "connect": "连接", + "anytime": "任何时间", + "anywhere": "任何地点", + "serviceSlogan": "提供极稳,极简,极速的网络服务", + "getSubscription": "获取订阅地址,开始顶级的私密网络体验", + "viewSubscriptionPlans": "查看订阅套餐" + }, + "language": { + "japanese": "日本語", + "simplifiedChinese": "简体中文" + }, + "offerDialog": { + "loading": "加载中...", + "loadFailed": "加载失败,请重试", + "reload": "重新加载", + "perYear": "/年", + "yearlyDiscount": "年付享受8折优惠", + "subscribe": "订阅", + "availableNodes": "可用节点", + "networkStabilityIndex": "网络稳定指数:", + "selectPlan": "选择套餐", + "selectYourPlan": "选择最适合您的服务套餐", + "yearlyPlan": "年付套餐", + "monthlyPlan": "月付套餐", + "noYearlyPlan": "暂无年付套餐", + "noMonthlyPlan": "暂无月付套餐" + } +} diff --git a/apps/user/next.config.ts b/apps/user/next.config.ts index f7eb808..17fa2ca 100644 --- a/apps/user/next.config.ts +++ b/apps/user/next.config.ts @@ -21,9 +21,34 @@ const nextConfig: NextConfig = { }, ], }, + webpack(config) { + // Grab the existing rule that handles SVG imports + const fileLoaderRule = config.module.rules.find((rule) => rule.test?.test?.('.svg')); + + config.module.rules.push( + // Reapply the existing rule, but only for svg imports ending in ?url + { + ...fileLoaderRule, + test: /\.svg$/i, + resourceQuery: /url/, // *.svg?url + }, + // Convert all other *.svg imports to React components + { + test: /\.svg$/i, + issuer: fileLoaderRule.issuer, + resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] }, // exclude if *.svg?url + use: ['@svgr/webpack'], + }, + ); + + // Modify the file loader rule to ignore *.svg, since we have it handled now. + fileLoaderRule.exclude = /\.svg$/i; + + return config; + }, turbopack: { rules: { - './public/svg-icon/*.svg': { + '*.svg': { loaders: ['@svgr/webpack'], as: '*.js', }, diff --git a/apps/user/components/CustomIcon/icons/close.svg b/packages/airo-ui/src/components/close.svg similarity index 100% rename from apps/user/components/CustomIcon/icons/close.svg rename to packages/airo-ui/src/components/close.svg diff --git a/packages/airo-ui/src/components/dialog.tsx b/packages/airo-ui/src/components/dialog.tsx index a0c6cd0..2c748d0 100644 --- a/packages/airo-ui/src/components/dialog.tsx +++ b/packages/airo-ui/src/components/dialog.tsx @@ -3,9 +3,8 @@ import * as DialogPrimitive from '@radix-ui/react-dialog'; import * as React from 'react'; -import CloseSvg from '@/components/CustomIcon/icons/close.svg'; import { cn } from '@workspace/airo-ui/lib/utils'; -import Image from 'next/image'; +import CloseSvg from './close.svg'; const Dialog = DialogPrimitive.Root; @@ -50,10 +49,12 @@ const DialogContent = React.forwardRef< {children} - {'close'} +
    + +
    Close