feat: 修改代码结构

This commit is contained in:
speakeloudest 2025-08-15 09:16:02 -07:00
parent 5524d25d9a
commit 525e305f1d
17 changed files with 256 additions and 63 deletions

17
apps/admin/.env.prod Normal file
View File

@ -0,0 +1,17 @@
# Default Language
NEXT_PUBLIC_DEFAULT_LANGUAGE=zh-CN
# Site URL and API URL
NEXT_PUBLIC_SITE_URL=https://admin.ppanel.dev
NEXT_PUBLIC_API_URL=https://api.airoport.co
# Default Login User
NEXT_PUBLIC_DEFAULT_USER_EMAIL=
NEXT_PUBLIC_DEFAULT_USER_PASSWORD=
# Please put in the .env file, otherwise the i18n command will not work
# OpenAI API key and proxy URL required for i18n command (optional)
OPENAI_API_KEY=
OPENAI_PROXY_URL=

32
apps/user/.env.prod Normal file
View File

@ -0,0 +1,32 @@
# Default Language
NEXT_PUBLIC_DEFAULT_LANGUAGE=zh-CN
# Site URL and API URL
NEXT_PUBLIC_SITE_URL=https://user.ppanel.dev
NEXT_PUBLIC_API_URL=https://api.airoport.co
NEXT_PUBLIC_CDN_URL=https://cdn.jsdelivr.net
# Home Page Settings
NEXT_PUBLIC_HOME_USER_COUNT=999
NEXT_PUBLIC_HOME_SERVER_COUNT=999
NEXT_PUBLIC_HOME_LOCATION_COUNT=999
# Contact Email
NEXT_PUBLIC_EMAIL=support@ppanel.dev
# Community Links
NEXT_PUBLIC_TELEGRAM_LINK=https://t.me/ppanel
NEXT_PUBLIC_TWITTER_LINK=https://github.com/perfect-panel/ppanel-web
NEXT_PUBLIC_DISCORD_LINK=https://github.com/perfect-panel/ppanel-web
NEXT_PUBLIC_INSTAGRAM_LINK=https://github.com/perfect-panel/ppanel-web
NEXT_PUBLIC_LINKEDIN_LINK=https://github.com/perfect-panel/ppanel-web
NEXT_PUBLIC_FACEBOOK_LINK=https://github.com/perfect-panel/ppanel-web
NEXT_PUBLIC_GITHUB_LINK=https://github.com/perfect-panel/ppanel-web
# Default Login User
NEXT_PUBLIC_DEFAULT_USER_EMAIL=
NEXT_PUBLIC_DEFAULT_USER_PASSWORD=
# Please put in the .env file, otherwise the i18n command will not work
# OpenAI API key and proxy URL required for i18n command (optional)
OPENAI_API_KEY=
OPENAI_PROXY_URL=

View File

@ -18,8 +18,8 @@ export function Header() {
const pathname = usePathname();
const items = useMemo(() => findNavByUrl(pathname), [pathname]);
return (
<header className='box-content flex h-[84px] items-center justify-between gap-2 px-4 pt-4 md:hidden'>
<SidebarTrigger />
<header className='flex h-[84px] w-full items-center justify-end gap-2 px-4 pt-4 md:hidden'>
<SidebarTrigger className={'fixed left-4'} />
<Breadcrumb>
<BreadcrumbList className={'text-[36px] font-semibold'}>
{items.map((item, index) => {

View File

@ -75,8 +75,8 @@ export default function Content() {
const { data: orderData } = useQuery({
queryKey: ['orderData'],
queryFn: async () => {
const { data } = await queryOrderList({ status: 5, page: 1, size: 10 });
return data?.[0] ?? {};
const { data } = await queryOrderList({ status: 5, page: 1, size: 1 });
return data?.data?.list?.[0] ?? {};
},
});

View File

@ -13,8 +13,8 @@ export default async function DashboardLayout({ children }: { children: React.Re
<SidebarLeft className='w-[288px] border-r-0 bg-transparent lg:flex' />
<SidebarInset className='relative flex-grow overflow-hidden'>
{/*<LanguageSwitch />*/}
<Header />
<div className='h-[calc(100vh-84px)] flex-grow gap-4 overflow-auto p-4 pt-0 sm:pt-9'>
<div className='h-screen flex-grow gap-4 overflow-auto p-4 sm:pt-9'>
<Header />
{children}
</div>
</SidebarInset>

View File

@ -5,6 +5,7 @@ import { updateUserNotify } from '@/services/user/user';
import { zodResolver } from '@hookform/resolvers/zod';
import { Button } from '@workspace/airo-ui/components/button';
import { Card } from '@workspace/airo-ui/components/card';
import { useEffect } from 'react';
import { AiroButton } from '@workspace/airo-ui/components/AiroButton';
import {
@ -39,6 +40,17 @@ export default function NotifySettings() {
enable_trade_notify: user?.enable_trade_notify ?? false,
},
});
// 监听 user 变化,更新表单
useEffect(() => {
if (user) {
form.reset({
enable_balance_notify: user.enable_balance_notify ?? false,
enable_login_notify: user.enable_login_notify ?? false,
enable_subscribe_notify: user.enable_subscribe_notify ?? false,
enable_trade_notify: user.enable_trade_notify ?? false,
});
}
}, [user, form]);
async function onSubmit(data: z.infer<typeof FormSchema>) {
await updateUserNotify(data);

View File

@ -0,0 +1,60 @@
import { Display } from '@/components/display';
import { Empty } from '@/components/empty';
import { ProList, ProListActions } from '@/components/pro-list';
import { queryUserBalanceLog } from '@/services/user/user';
import { Card, CardContent } from '@workspace/airo-ui/components/card';
import { formatDate } from '@workspace/airo-ui/utils';
import { useTranslations } from 'next-intl';
import { useRef } from 'react';
const Table: React.FC<{}> = () => {
const ref = useRef<ProListActions>(null);
const t = useTranslations('wallet');
return (
<ProList<API.UserBalanceLog, Record<string, unknown>>
action={ref}
request={async (pagination, filter) => {
const response = await queryUserBalanceLog({ ...pagination, ...filter });
return {
list: response.data.data?.list || [],
total: response.data.data?.total || 0,
};
}}
renderItem={(item) => {
return (
<Card className='rounded-[32px] px-[20px] sm:px-[55px]'>
<CardContent className='px-0 py-3 text-[10px] sm:p-3 sm:text-sm'>
<ul className='grid grid-cols-4 gap-3 *:flex *:flex-col'>
<li className='font-semibold'>
<span className='text-[#225BA9]'>{t('createdAt')}</span>
<time>{formatDate(item.created_at)}</time>
</li>
<li className='font-semibold'>
<span className='text-[#225BA9]'>{t('type.0')}</span>
<span>{t(`type.${item.type}`)}</span>
</li>
<li className='font-semibold'>
<span className='text-[#225BA9]'>{t('amount')}</span>
<span>
<Display type='currency' value={item.amount} />
</span>
</li>
<li className='font-semibold'>
<span className='text-[#225BA9]'>{t('balance')}</span>
<span>
<Display type='currency' value={item.balance} />
</span>
</li>
</ul>
</CardContent>
</Card>
);
}}
empty={<Empty />}
/>
);
};
export default Table;

View File

@ -0,0 +1,106 @@
import { queryUserBalanceLog } from '@/services/user/user';
import { useQuery } from '@tanstack/react-query';
import { AiroButton } from '@workspace/airo-ui/components/AiroButton';
import { Card, CardContent } from '@workspace/airo-ui/components/card';
import {
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@workspace/airo-ui/components/dialog';
import { default as Airo_Empty } from '@workspace/airo-ui/custom-components/empty';
import { formatDate } from '@workspace/airo-ui/utils';
import { Dialog } from '@workspace/ui/components/dialog';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { Display } from '@/components/display';
import Table from '../Table/Table';
const WalletDialog: React.FC<{}> = () => {
const [open, setOpen] = useState(false);
const t = useTranslations('affiliate');
const tWallet = useTranslations('wallet');
const { data } = useQuery({
queryKey: ['walletDialogList'],
queryFn: async () => {
const data = await queryUserBalanceLog({
page: 1,
size: 4,
} as API.QueryUserAffiliateListRequest);
console.log('data', data?.data.data);
return data?.data?.data?.list || [];
},
});
return (
<Card className='order-2 rounded-[20px] border border-[#EAEAEA] bg-gradient-to-b from-white to-[#EAEAEA] p-6 md:order-none'>
<div className='mb-4 flex items-center justify-between'>
<h3 className='font-medium text-[#666666] sm:text-xl'>{tWallet('title')}</h3>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<AiroButton variant={'dangerLink'} className={'min-w-0 px-1 text-sm text-[#225BA9]'}>
{t('more')}
</AiroButton>
</DialogTrigger>
<DialogContent className='sm:w-[675px]'>
<DialogHeader>
<DialogTitle className='text-left text-2xl sm:text-4xl'>
{tWallet('title')}
</DialogTitle>
</DialogHeader>
<Table />
</DialogContent>
</Dialog>
</div>
<div className='space-y-2 sm:space-y-4'>
{data?.length ? (
<div className='relative space-y-3'>
<div
className={
'absolute bottom-0 left-0 right-0 h-[60px] bg-white/30 backdrop-blur-[1px]'
}
></div>
{data?.map((item) => {
return (
<Card className='rounded-[15px] px-[20px] sm:rounded-[32px] sm:px-[55px]'>
<CardContent className='px-0 py-3 text-[10px] sm:p-3 sm:text-sm'>
<ul className='grid grid-cols-4 gap-3 *:flex *:flex-col'>
<li className='font-semibold'>
<span className='text-[#225BA9]'>{tWallet('createdAt')}</span>
<time>{formatDate(item.created_at)}</time>
</li>
<li className='font-semibold'>
<span className='text-[#225BA9]'>{tWallet('type.0')}</span>
<span>{tWallet(`type.${item.type}`)}</span>
</li>
<li className='font-semibold'>
<span className='text-[#225BA9]'>{tWallet('amount')}</span>
<span>
<Display type='currency' value={item.amount} />
</span>
</li>
<li className='font-semibold'>
<span className='text-[#225BA9]'>{tWallet('balance')}</span>
<span>
<Display type='currency' value={item.balance} />
</span>
</li>
</ul>
</CardContent>
</Card>
);
})}
</div>
) : (
<Airo_Empty className={'py-0'} description={t('noInvitationRecords')} />
)}
</div>
</Card>
);
};
export default WalletDialog;

View File

@ -1,27 +1,23 @@
'use client';
import { Display } from '@/components/display';
import { ProList, ProListActions } from '@/components/pro-list';
import useGlobalStore from '@/config/use-global';
import { queryUserBalanceLog } from '@/services/user/user';
import { Card, CardContent } from '@workspace/airo-ui/components/card';
import { useTranslations } from 'next-intl';
import { useRef } from 'react';
import { Empty } from '@/components/empty';
import Recharge from '@/components/subscribe/recharge';
import SvgIcon from '@/components/SvgIcon';
import { Button } from '@workspace/airo-ui/components/button';
import { formatDate } from '@workspace/airo-ui/utils';
import Link from 'next/link';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import { toast } from 'sonner';
import Table from './components/Table/Table';
import WalletDialog from './components/WalletDialog/WalletDialog';
export default function Page() {
const t = useTranslations('wallet');
const dashboardT = useTranslations('dashboard');
const { user } = useGlobalStore();
const ref = useRef<ProListActions>(null);
const totalAssets = (user?.balance || 0) + (user?.commission || 0) + (user?.gift_amount || 0);
return (
<>
@ -92,48 +88,12 @@ export default function Page() {
</div>
</CardContent>
</Card>
<ProList<API.UserBalanceLog, Record<string, unknown>>
action={ref}
request={async (pagination, filter) => {
const response = await queryUserBalanceLog({ ...pagination, ...filter });
return {
list: response.data.data?.list || [],
total: response.data.data?.total || 0,
};
}}
renderItem={(item) => {
return (
<Card className='rounded-[32px] px-[20px] sm:px-[55px]'>
<CardContent className='px-0 py-3 text-[10px] sm:p-3 sm:text-sm'>
<ul className='grid grid-cols-4 gap-3 *:flex *:flex-col'>
<li className='font-semibold'>
<span className='text-[#225BA9]'>{t('createdAt')}</span>
<time>{formatDate(item.created_at)}</time>
</li>
<li className='font-semibold'>
<span className='text-[#225BA9]'>{t('type.0')}</span>
<span>{t(`type.${item.type}`)}</span>
</li>
<li className='font-semibold'>
<span className='text-[#225BA9]'>{t('amount')}</span>
<span>
<Display type='currency' value={item.amount} />
</span>
</li>
<li className='font-semibold'>
<span className='text-[#225BA9]'>{t('balance')}</span>
<span>
<Display type='currency' value={item.balance} />
</span>
</li>
</ul>
</CardContent>
</Card>
);
}}
empty={<Empty />}
/>
<div className={'hidden sm:block'}>
<Table />
</div>
<div className={'mt-2.5 sm:hidden'}>
<WalletDialog />
</div>
</>
);
}

View File

@ -56,7 +56,7 @@ export default function LanguageSwitch() {
}}
>
<SvgIcon
name={locale === 'en-US' ? 'language' : 'language-zh'}
name={locale === 'en-US' ? 'language-zh' : 'language'}
className={'cursor-pointer text-[#EAEAEA] hover:text-[#B5C9E2]'}
/>
<span className='sr-only'>{languages[locale as keyof typeof languages]}</span>

View File

@ -75,7 +75,6 @@ const Purchase = forwardRef<PurchaseDialogRef, PurchaseProps>((props, ref) => {
queryKey: ['preCreateOrder', subscribe?.id, params.quantity],
queryFn: async () => {
try {
console.log('123123', subscribe);
const { data } = await preCreateOrder({
...params,
subscribe_id: subscribe?.id as number,

View File

@ -8,6 +8,7 @@
"giftAmount": "Girt Amount",
"referralCode": "Referral Code",
"referralDetails": "Referral Details",
"title": "Invite Records",
"totalAssets": "Total Assets",
"type": {
"0": "Type",

View File

@ -8,6 +8,7 @@
"giftAmount": "赠送金额",
"referralCode": "返佣邀请码",
"referralDetails": "返佣详情",
"title": "邀请记录",
"totalAssets": "资产概览",
"type": {
"0": "类型",

View File

@ -32,11 +32,17 @@ export async function queryUserAffiliateList(
}
/** Query User Balance Log GET /v1/public/user/balance_log */
export async function queryUserBalanceLog(options?: { [key: string]: any }) {
export async function queryUserBalanceLog(
params: API.QueryUserAffiliateListParams,
options?: { [key: string]: any },
) {
return request<API.Response & { data?: API.QueryUserBalanceLogListResponse }>(
'/v1/public/user/balance_log',
{
method: 'GET',
params: {
...params,
},
...(options || {}),
},
);

View File

@ -7,7 +7,6 @@ import { getAuthorization, Logout } from './common';
async function handleError(response: any) {
const code = response.data?.code;
console.log(1111111, code);
if ([40002, 40003, 40004, 40005].includes(code)) {
if (isBrowser()) {
const t = await getTranslations('common');

View File

@ -14,10 +14,10 @@ PROJECTS=(
)
# Step 1: Install dependencies
#bun install || {
# echo "Dependency installation failed"
# exit 1
#}
bun install || {
echo "Dependency installation failed"
exit 1
}
# Step 2: Build each project using Turbo
for ITEM in "${PROJECTS[@]}"; do

View File

@ -33,7 +33,7 @@ for ITEM in "${PROJECTS[@]}"; do
cp -r $PROJECT_PATH/.next/static $PROJECT_BUILD_DIR/$PROJECT_PATH/.next/
cp -r $PROJECT_PATH/public $PROJECT_BUILD_DIR/$PROJECT_PATH/
cp -r $PROJECT_PATH/.env.template $PROJECT_BUILD_DIR/$PROJECT_PATH/.env.template
cp -r $PROJECT_PATH/.env $PROJECT_BUILD_DIR/$PROJECT_PATH/.env
cp -f $PROJECT_PATH/.env.prod $PROJECT_BUILD_DIR/$PROJECT_PATH/.env
# Generate ecosystem.config.js for the project
ECOSYSTEM_CONFIG="$PROJECT_BUILD_DIR/ecosystem.config.js"