Compare commits

..

25 Commits

Author SHA1 Message Date
shanshanzhong147 b5f1c19877 👷 ci: [admin-only] 更新后台
CI / build (20.15.1) (push) Successful in 21m56s
2026-02-09 06:46:21 -08:00
shanshanzhong147 71cfb29f25 👷 ci: [admin-only] 更新后台 2026-02-09 06:35:00 -08:00
shanshanzhong147 2d73e00e32 feat: 1 (fix #1) 2026-02-09 06:29:59 -08:00
shanshanzhong147 9aa958b6b4 feat: X 2026-02-09 06:25:22 -08:00
shanshanzhong147 38e0078e1d 👷 ci: [admin-only] 触发后台构建 2026-02-09 05:00:09 -08:00
shanshanzhong147 5231a42f87 👷 ci: [admin-only] 更新后台构建配置 2026-02-09 04:56:04 -08:00
shanshanzhong147 64496ba566 feat: Banben 2026-02-07 19:48:49 -08:00
shanshanzhong147 75e7e43465 feat: Banben 2026-02-07 19:47:16 -08:00
shanshanzhong147 efc47cf8bd feat(根系版本): 版本 2026-02-07 19:27:21 -08:00
sepakeloudest a8832d4777 更新package.json 2026-02-07 22:21:04 -05:00
sepakeloudest b1d246d1ac 更新 package.json 2026-02-07 22:19:05 -05:00
shanshanzhong147 3c0e544c97 feat: Gengx 2026-02-06 20:26:46 -08:00
shanshanzhong147 7df01e454c feat: 修改配置文件 2026-02-06 20:16:21 -08:00
sepakeloudest d05cd8ed3d 更新 README.md
CI / build (20.15.1) (push) Has been cancelled
2026-02-02 23:56:16 -05:00
shanshanzhong147 5ddf9ade01 feat: 2 (#2)
CI / build (20.15.1) (push) Failing after 19m30s
2026-01-27 20:14:52 -08:00
shanshanzhong147 3b93b95177 🐛 fix: Ci
CI / build (20.15.1) (push) Has been cancelled
2026-01-27 20:11:44 -08:00
shanshanzhong147 57b841525d feat(ci): Ci
CI / build (20.15.1) (push) Failing after 14m59s
2026-01-05 04:20:01 -08:00
shanshanzhong147 1e147c8298 feat: Ci
CI / build (20.15.1) (push) Failing after 14m49s
2026-01-05 03:55:10 -08:00
shanshanzhong147 7419d8ebcd feat: 0
CI / build (20.15.1) (push) Failing after 14m41s
2026-01-05 03:24:27 -08:00
shanshanzhong147 56a955ae81 feat: 1
CI / build (20.15.1) (push) Successful in 14m4s
2026-01-05 03:04:09 -08:00
shanshanzhong147 e3aa52af01 merge(main): 合并 develop 到 main 并同步 CI/Docker 修复
CI / build (20.15.1) (push) Failing after 16m25s
2026-01-03 22:03:52 -08:00
sepakeloudest b31c70a5c1 🐛 fix: 修改样式 2025-12-15 18:30:29 -08:00
sepakeloudest 75db379624 🐛 fix: 修改样式 2025-12-15 18:29:56 -08:00
sepakeloudest c8401af672 🐛 fix: 更新next版本 2025-12-07 05:59:23 -08:00
semantic-release-bot ea3964ebe5 🔖 chore(release): v1.6.1 [skip ci]
## [1.6.1](https://github.com/perfect-panel/ppanel-web/compare/v1.6.0...v1.6.1) (2025-11-05)

### 🐛 Bug Fixes

* Fixing issues with generating standard and quantum-resistant encryption keys ([5eac6a9](https://github.com/perfect-panel/ppanel-web/commit/5eac6a9))
2025-11-05 04:45:55 +00:00
25 changed files with 894 additions and 71 deletions
+3 -5
View File
@@ -5,12 +5,10 @@ on:
branches: branches:
- main - main
- develop - develop
- cicd
pull_request: pull_request:
branches: branches:
- main - main
- develop - develop
- cicd
env: env:
DOMAIN_URL: git.kxsw.us DOMAIN_URL: git.kxsw.us
@@ -523,7 +521,7 @@ jobs:
# 安全停止和移除容器 # 安全停止和移除容器
echo "检查现有容器状态..." echo "检查现有容器状态..."
CONTAINER_NAME="ppanel-admin-web" CONTAINER_NAME="fastvpn-admin-web"
# 检查容器是否存在 # 检查容器是否存在
if docker ps -aq -f name=$CONTAINER_NAME | grep -q .; then if docker ps -aq -f name=$CONTAINER_NAME | grep -q .; then
@@ -574,7 +572,7 @@ jobs:
echo "启动新容器..." echo "启动新容器..."
docker run -d \ docker run -d \
--add-host api.airoport.co:103.150.215.40 \ --network host \
--name fastvpn-admin-web \ --name fastvpn-admin-web \
--restart unless-stopped \ --restart unless-stopped \
-p 3001:3000 \ -p 3001:3000 \
@@ -724,7 +722,7 @@ jobs:
echo "启动新容器..." echo "启动新容器..."
docker run -d \ docker run -d \
--add-host api.airoport.co:103.150.215.40 \ --network host \
--name fastvpn-user-web \ --name fastvpn-user-web \
--restart unless-stopped \ --restart unless-stopped \
-p 3002:3000 \ -p 3002:3000 \
+10
View File
@@ -1,3 +1,13 @@
<a name="readme-top"></a>
# Changelog
## [1.6.1](https://github.com/perfect-panel/ppanel-web/compare/v1.6.0...v1.6.1) (2025-11-05)
### 🐛 Bug Fixes
* Fixing issues with generating standard and quantum-resistant encryption keys ([5eac6a9](https://github.com/perfect-panel/ppanel-web/commit/5eac6a9))
<a name="readme-top"></a> <a name="readme-top"></a>
# Changelog # Changelog
+1 -1
View File
@@ -4,7 +4,7 @@
<img width="160" src="https://raw.githubusercontent.com/perfect-panel/ppanel-assets/refs/heads/main/logo.svg"> <img width="160" src="https://raw.githubusercontent.com/perfect-panel/ppanel-assets/refs/heads/main/logo.svg">
<h1>PPanel web</h1> <h1>PPanel web hifastvpn</h1>
This is a PPanel web powered by PPanel This is a PPanel web powered by PPanel
+1 -1
View File
@@ -6,7 +6,7 @@
<h1>PPanel 前端</h1> <h1>PPanel 前端</h1>
这是由 PPanel 提供支持的前端 这是由 PPanel 提供支持的前端1
[英文](./README.md) [英文](./README.md)
· ·
@@ -87,6 +87,7 @@ export default function EmailSettingsForm() {
const form = useForm<EmailSettingsFormData>({ const form = useForm<EmailSettingsFormData>({
resolver: zodResolver(emailSettingsSchema), resolver: zodResolver(emailSettingsSchema),
shouldUnregister: false,
defaultValues: { defaultValues: {
id: 0, id: 0,
method: 'email', method: 'email',
@@ -416,8 +417,8 @@ export default function EmailSettingsForm() {
<FormControl> <FormControl>
<HTMLEditor <HTMLEditor
placeholder={t('email.inputPlaceholder')} placeholder={t('email.inputPlaceholder')}
value={field.value} value={field.value ?? ''}
onBlur={field.onChange} onChange={field.onChange}
/> />
</FormControl> </FormControl>
<div className='mt-4 space-y-2 border-t pt-4'> <div className='mt-4 space-y-2 border-t pt-4'>
@@ -480,8 +481,8 @@ export default function EmailSettingsForm() {
<FormControl> <FormControl>
<HTMLEditor <HTMLEditor
placeholder={t('email.inputPlaceholder')} placeholder={t('email.inputPlaceholder')}
value={field.value} value={field.value ?? ''}
onBlur={field.onChange} onChange={field.onChange}
/> />
</FormControl> </FormControl>
<div className='mt-4 space-y-2 border-t pt-4'> <div className='mt-4 space-y-2 border-t pt-4'>
@@ -525,8 +526,8 @@ export default function EmailSettingsForm() {
<FormControl> <FormControl>
<HTMLEditor <HTMLEditor
placeholder={t('email.inputPlaceholder')} placeholder={t('email.inputPlaceholder')}
value={field.value} value={field.value ?? ''}
onBlur={field.onChange} onChange={field.onChange}
/> />
</FormControl> </FormControl>
<div className='mt-4 space-y-2 border-t pt-4'> <div className='mt-4 space-y-2 border-t pt-4'>
@@ -580,8 +581,8 @@ export default function EmailSettingsForm() {
<FormControl> <FormControl>
<HTMLEditor <HTMLEditor
placeholder={t('email.inputPlaceholder')} placeholder={t('email.inputPlaceholder')}
value={field.value} value={field.value ?? ''}
onBlur={field.onChange} onChange={field.onChange}
/> />
</FormControl> </FormControl>
<div className='mt-4 space-y-2 border-t pt-4'> <div className='mt-4 space-y-2 border-t pt-4'>
@@ -0,0 +1,450 @@
'use client';
import {
createAppVersion,
deleteAppVersion,
getAppVersionList,
updateAppVersion,
} from '@/services/admin/application';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Button } from '@workspace/ui/components/button';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@workspace/ui/components/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@workspace/ui/components/form';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@workspace/ui/components/select';
import { Switch } from '@workspace/ui/components/switch';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@workspace/ui/components/table';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { Icon } from '@workspace/ui/custom-components/icon';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { z } from 'zod';
const versionSchema = z.object({
id: z.number().optional(),
platform: z.string().min(1),
version: z.string().min(1),
min_version: z.string().optional(),
url: z.string().url(),
description: z.string().optional(),
force_update: z.boolean(),
is_default: z.boolean(),
is_in_review: z.boolean(),
});
type VersionFormData = z.infer<typeof versionSchema>;
export default function VersionPage() {
const t = useTranslations('system');
const queryClient = useQueryClient();
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [open, setOpen] = useState(false);
const [editingVersion, setEditingVersion] = useState<API.ApplicationVersion | null>(null);
const { data } = useQuery({
queryKey: ['app-versions', page, pageSize],
queryFn: async () => {
const res = await getAppVersionList({ page, size: pageSize });
return res.data?.data;
},
});
const form = useForm<VersionFormData>({
resolver: zodResolver(versionSchema),
defaultValues: {
platform: 'android',
version: '',
min_version: '',
url: '',
description: '',
force_update: false,
is_default: false,
is_in_review: false,
},
});
const createMutation = useMutation({
mutationFn: createAppVersion,
onSuccess: () => {
toast.success(t('common.saveSuccess'));
setOpen(false);
form.reset();
queryClient.invalidateQueries({ queryKey: ['app-versions'] });
},
onError: () => {
toast.error(t('common.saveFailed'));
},
});
const updateMutation = useMutation({
mutationFn: updateAppVersion,
onSuccess: () => {
toast.success(t('common.saveSuccess'));
setOpen(false);
setEditingVersion(null);
form.reset();
queryClient.invalidateQueries({ queryKey: ['app-versions'] });
},
onError: () => {
toast.error(t('common.saveFailed'));
},
});
const deleteMutation = useMutation({
mutationFn: deleteAppVersion,
onSuccess: () => {
toast.success(t('common.saveSuccess'));
queryClient.invalidateQueries({ queryKey: ['app-versions'] });
},
onError: () => {
toast.error(t('common.saveFailed'));
},
});
const onSubmit = (values: VersionFormData) => {
const payload = {
...values,
description: JSON.stringify({ 'en-US': values.description, 'zh-CN': values.description }),
};
if (editingVersion) {
updateMutation.mutate({ ...payload, id: editingVersion.id } as API.UpdateAppVersionRequest);
} else {
createMutation.mutate(payload as API.CreateAppVersionRequest);
}
};
const handleEdit = (version: API.ApplicationVersion) => {
setEditingVersion(version);
let desc = '';
if (version.description && typeof version.description === 'object') {
desc = Object.values(version.description)[0] || '';
} else if (typeof version.description === 'string') {
try {
const parsed = JSON.parse(version.description);
desc = (Object.values(parsed)[0] as string) || '';
} catch (e) {
desc = version.description;
}
}
form.reset({
platform: version.platform,
version: version.version,
min_version: version.min_version,
url: version.url,
description: desc,
force_update: version.force_update,
is_default: version.is_default,
is_in_review: version.is_in_review,
});
setOpen(true);
};
const handleDelete = (id: number) => {
if (confirm(t('version.confirmDelete'))) {
deleteMutation.mutate({ id });
}
};
const handleOpenChange = (open: boolean) => {
setOpen(open);
if (!open) {
setEditingVersion(null);
form.reset({
platform: 'android',
version: '',
min_version: '',
url: '',
description: '',
force_update: false,
is_default: false,
is_in_review: false,
});
}
};
return (
<div className='space-y-4'>
<div className='flex items-center justify-between'>
<h2 className='text-2xl font-bold tracking-tight'>{t('version.title')}</h2>
<Button onClick={() => handleOpenChange(true)}>
<Icon icon='mdi:plus' className='mr-2 h-4 w-4' />
{t('version.create')}
</Button>
</div>
<div className='rounded-md border'>
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>{t('version.platform')}</TableHead>
<TableHead>{t('version.versionNumber')}</TableHead>
<TableHead>{t('version.url')}</TableHead>
<TableHead>{t('version.force')}</TableHead>
<TableHead>{t('version.default')}</TableHead>
<TableHead>{t('version.actions')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data?.list?.map((version: API.ApplicationVersion) => (
<TableRow key={version.id}>
<TableCell>{version.id}</TableCell>
<TableCell>{version.platform}</TableCell>
<TableCell>{version.version}</TableCell>
<TableCell className='max-w-[200px] truncate' title={version.url}>
{version.url}
</TableCell>
<TableCell>{version.force_update ? t('version.yes') : t('version.no')}</TableCell>
<TableCell>{version.is_default ? t('version.yes') : t('version.no')}</TableCell>
<TableCell>
<div className='flex space-x-2'>
<Button variant='ghost' size='icon' onClick={() => handleEdit(version)}>
<Icon icon='mdi:pencil' className='h-4 w-4' />
</Button>
<Button variant='ghost' size='icon' onClick={() => handleDelete(version.id)}>
<Icon icon='mdi:delete' className='h-4 w-4 text-red-500' />
</Button>
</div>
</TableCell>
</TableRow>
))}
{!data?.list?.length && (
<TableRow>
<TableCell colSpan={7} className='h-24 text-center'>
{t('version.noResults')}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className='flex items-center justify-between py-2'>
<div className='text-muted-foreground text-sm'>
{t('version.total', { count: data?.total || 0 })}
</div>
<div className='flex items-center space-x-2'>
<Select value={String(pageSize)} onValueChange={(val) => setPageSize(Number(val))}>
<SelectTrigger className='w-[80px]'>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value='10'>10</SelectItem>
<SelectItem value='20'>20</SelectItem>
<SelectItem value='50'>50</SelectItem>
</SelectContent>
</Select>
<Button
variant='outline'
size='sm'
onClick={() => setPage(Math.max(1, page - 1))}
disabled={page <= 1}
>
{t('version.previous')}
</Button>
<span className='text-sm'>{t('version.page', { page })}</span>
<Button
variant='outline'
size='sm'
onClick={() => setPage(page + 1)}
disabled={!data?.list?.length || data.list.length < pageSize}
>
{t('version.next')}
</Button>
</div>
</div>
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className='sm:max-w-[600px]'>
<DialogHeader>
<DialogTitle>
{editingVersion ? t('version.edit') : t('version.createVersion')}
</DialogTitle>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'>
<div className='grid grid-cols-2 gap-4'>
<FormField
control={form.control}
name='platform'
render={({ field }) => (
<FormItem>
<FormLabel>{t('version.platform')}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('version.platformPlaceholder')} />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value='android'>Android</SelectItem>
<SelectItem value='ios'>iOS</SelectItem>
<SelectItem value='windows'>Windows</SelectItem>
<SelectItem value='macos'>macOS</SelectItem>
<SelectItem value='linux'>Linux</SelectItem>
<SelectItem value='harmony'>Harmony</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='version'
render={({ field }) => (
<FormItem>
<FormLabel>{t('version.versionNumber')}</FormLabel>
<FormControl>
<EnhancedInput
value={field.value}
onValueChange={field.onChange}
placeholder={t('version.versionPlaceholder')}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className='grid grid-cols-2 gap-4'>
<FormField
control={form.control}
name='min_version'
render={({ field }) => (
<FormItem>
<FormLabel>{t('version.minVersion')}</FormLabel>
<FormControl>
<EnhancedInput
value={field.value}
onValueChange={field.onChange}
placeholder={t('version.versionPlaceholder')}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='url'
render={({ field }) => (
<FormItem>
<FormLabel>{t('version.downloadUrl')}</FormLabel>
<FormControl>
<EnhancedInput
value={field.value}
onValueChange={field.onChange}
placeholder='https://...'
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name='description'
render={({ field }) => (
<FormItem>
<FormLabel>{t('version.descriptionField')}</FormLabel>
<FormControl>
<EnhancedInput
value={field.value}
onValueChange={field.onChange}
placeholder={t('version.descriptionPlaceholder')}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className='flex flex-row gap-4'>
<FormField
control={form.control}
name='force_update'
render={({ field }) => (
<FormItem className='flex flex-1 flex-row items-center justify-between rounded-lg border p-3 shadow-sm'>
<div className='space-y-0.5'>
<FormLabel>{t('version.forceUpdate')}</FormLabel>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name='is_default'
render={({ field }) => (
<FormItem className='flex flex-1 flex-row items-center justify-between rounded-lg border p-3 shadow-sm'>
<div className='space-y-0.5'>
<FormLabel>{t('version.default')}</FormLabel>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name='is_in_review'
render={({ field }) => (
<FormItem className='flex flex-1 flex-row items-center justify-between rounded-lg border p-3 shadow-sm'>
<div className='space-y-0.5'>
<FormLabel>{t('version.inReview')}</FormLabel>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
</div>
<DialogFooter>
<Button type='submit'>
{editingVersion ? t('version.update') : t('version.create')}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
</div>
);
}
@@ -38,6 +38,7 @@ const currencySchema = z.object({
access_key: z.string().optional(), access_key: z.string().optional(),
currency_unit: z.string().min(1), currency_unit: z.string().min(1),
currency_symbol: z.string().min(1), currency_symbol: z.string().min(1),
fixed_rate: z.number().optional(),
}); });
type CurrencyFormData = z.infer<typeof currencySchema>; type CurrencyFormData = z.infer<typeof currencySchema>;
@@ -62,6 +63,7 @@ export default function CurrencyConfig() {
access_key: '', access_key: '',
currency_unit: 'USD', currency_unit: 'USD',
currency_symbol: '$', currency_symbol: '$',
fixed_rate: 0,
}, },
}); });
@@ -170,6 +172,26 @@ export default function CurrencyConfig() {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name='fixed_rate'
render={({ field }) => (
<FormItem>
<FormLabel>{t('currency.fixedRate')}</FormLabel>
<FormControl>
<EnhancedInput
type='number'
placeholder={t('currency.fixedRatePlaceholder', { defaultValue: '0' })}
value={field.value}
onValueChange={(val) => field.onChange(Number(val))}
/>
</FormControl>
<FormDescription>{t('currency.fixedRateDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form> </form>
</Form> </Form>
</ScrollArea> </ScrollArea>
+177 -13
View File
@@ -1,6 +1,5 @@
'use client'; 'use client';
import { Display } from '@/components/display';
import { ProTable, ProTableActions } from '@/components/pro-table'; import { ProTable, ProTableActions } from '@/components/pro-table';
import { import {
createUser, createUser,
@@ -12,7 +11,6 @@ import {
import { useSubscribe } from '@/store/subscribe'; import { useSubscribe } from '@/store/subscribe';
import { formatDate } from '@/utils/common'; import { formatDate } from '@/utils/common';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Badge } from '@workspace/ui/components/badge';
import { Button } from '@workspace/ui/components/button'; import { Button } from '@workspace/ui/components/button';
import { import {
DropdownMenu, DropdownMenu,
@@ -20,6 +18,13 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@workspace/ui/components/dropdown-menu'; } from '@workspace/ui/components/dropdown-menu';
import { Input } from '@workspace/ui/components/input';
import {
Popover,
PopoverClose,
PopoverContent,
PopoverTrigger,
} from '@workspace/ui/components/popover';
import { ScrollArea } from '@workspace/ui/components/scroll-area'; import { ScrollArea } from '@workspace/ui/components/scroll-area';
import { import {
Sheet, Sheet,
@@ -31,6 +36,7 @@ import {
import { Switch } from '@workspace/ui/components/switch'; import { Switch } from '@workspace/ui/components/switch';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button'; import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
import { FilePenLine } from 'lucide-react';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import Link from 'next/link'; import Link from 'next/link';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
@@ -38,11 +44,69 @@ import { useRef, useState } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { UserDetail } from './user-detail'; import { UserDetail } from './user-detail';
import UserForm from './user-form'; import UserForm from './user-form';
import { AuthMethodsForm } from './user-profile/auth-methods-form';
import { BasicInfoForm } from './user-profile/basic-info-form'; import { BasicInfoForm } from './user-profile/basic-info-form';
import { NotifySettingsForm } from './user-profile/notify-settings-form'; import { NotifySettingsForm } from './user-profile/notify-settings-form';
import UserSubscription from './user-subscription'; import UserSubscription from './user-subscription';
function getDeviceTypeInfo(userAgent = '') {
let deviceType = 'Unknown';
const ua = userAgent.toLowerCase();
if (ua.includes('android')) {
deviceType = 'Android';
} else if (ua.includes('iphone') || ua.includes('ios')) {
deviceType = 'iPhone';
} else if (ua.includes('ipad')) {
deviceType = 'iPad';
} else if (ua.includes('mac os') || ua.includes('mac')) {
deviceType = 'Mac';
} else if (ua.includes('windows')) {
deviceType = 'Windows';
} else if (ua.includes('linux')) {
deviceType = 'Linux';
}
return { deviceType };
}
// 为 RemarkForm 组件定义 props 类型
interface RemarkFormProps {
initialRemark?: string | null;
onSave: (remark: string) => void;
CloseComponent: React.ComponentType<{ asChild?: boolean; children: React.ReactNode }>;
}
// 新的子组件,在管理它自己的备注状态
const RemarkForm: React.FC<RemarkFormProps> = ({ onSave, initialRemark, CloseComponent }) => {
const [remark, setRemark] = useState<string>(initialRemark ?? '');
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setRemark(event.target.value);
};
const handleSaveClick = () => {
onSave(remark);
};
return (
<>
<div className='mb-2 text-sm font-semibold'></div>
<Input
type='text'
value={remark}
onChange={handleInputChange}
placeholder='在此输入备注...'
className='w-full'
/>
<CloseComponent asChild>
<Button onClick={handleSaveClick} variant='default' size={'sm'} className={'mt-2'}>
</Button>
</CloseComponent>
</>
);
};
export default function Page() { export default function Page() {
const t = useTranslations('user'); const t = useTranslations('user');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -56,6 +120,7 @@ export default function Page() {
user_id: sp.get('user_id') || undefined, user_id: sp.get('user_id') || undefined,
subscribe_id: sp.get('subscribe_id') || undefined, subscribe_id: sp.get('subscribe_id') || undefined,
user_subscribe_id: sp.get('user_subscribe_id') || undefined, user_subscribe_id: sp.get('user_subscribe_id') || undefined,
device_id: sp.get('device_id') || undefined,
}; };
return ( return (
@@ -128,20 +193,96 @@ export default function Page() {
}, },
{ {
accessorKey: 'auth_methods', accessorKey: 'auth_methods',
header: t('userName'), header: '绑定邮箱',
cell: ({ row }) => { cell: ({ row }) => {
const method = row.original.auth_methods?.[0]; const method = row.original.auth_methods;
return ( return (
<div> <div>
<Badge className='mr-1 uppercase' title={method?.verified ? t('verified') : ''}> <Popover>
{method?.auth_type} <PopoverTrigger>
</Badge> <div className={'flex items-center'}>
{method?.auth_identifier} {method?.find((v) => v.auth_type === 'email')?.auth_identifier || '待绑定'}
{row.original?.remark ? `${row.original.remark}` : ''}
<FilePenLine size={14} className={'text-primary ml-2'} />
</div>
</PopoverTrigger>
<PopoverContent className={'w-64'}>
<RemarkForm
initialRemark={row.original.remark}
CloseComponent={PopoverClose}
onSave={async (remark) => {
const {
auth_methods,
user_devices,
enable_balance_notify,
enable_login_notify,
enable_subscribe_notify,
enable_trade_notify,
updated_at,
created_at,
id,
...rest
} = row.original;
await updateUserBasicInfo({
user_id: id,
...rest,
remark,
} as unknown as API.UpdateUserBasiceInfoRequest);
toast.success(t('updateSuccess'));
ref.current?.refresh();
}}
/>
</PopoverContent>
</Popover>
</div> </div>
); );
}, },
}, },
{ {
accessorKey: 'user_devices',
header: '绑定设备',
cell: ({ row }) => {
const devices = row?.original.user_devices ?? [];
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
{devices.map((v, index) => {
const { deviceType } = getDeviceTypeInfo(v.user_agent);
return (
<div key={v.id + '_wrapper'}>
<div
style={{
padding: '4px 6px',
background: '#f8f8f8',
borderRadius: '4px',
border: '1px solid #e0e0e0',
fontSize: '12px',
lineHeight: '16px',
}}
>
<div style={{ fontWeight: 500 }}>
ID{v.id}{deviceType}
</div>
</div>
{index !== devices.length - 1 && (
<div
style={{
height: '1px',
background: '#eee',
margin: '4px 0',
}}
></div>
)}
</div>
);
})}
</div>
);
},
},
/*{
accessorKey: 'balance', accessorKey: 'balance',
header: t('balance'), header: t('balance'),
cell: ({ row }) => <Display type='currency' value={row.getValue('balance')} />, cell: ({ row }) => <Display type='currency' value={row.getValue('balance')} />,
@@ -155,12 +296,31 @@ export default function Page() {
accessorKey: 'commission', accessorKey: 'commission',
header: t('commission'), header: t('commission'),
cell: ({ row }) => <Display type='currency' value={row.getValue('commission')} />, cell: ({ row }) => <Display type='currency' value={row.getValue('commission')} />,
}, },*/
{ {
accessorKey: 'refer_code', accessorKey: 'refer_code',
header: t('inviteCode'), header: t('inviteCode'),
cell: ({ row }) => row.getValue('refer_code') || '--', cell: ({ row }) => row.getValue('refer_code') || '--',
}, },
{
accessorKey: 'last_login_time',
header: '最后登录时间',
cell: ({ row }) => {
const v = (row.original as any)?.last_login_time;
if (!v) return '---';
const ts = Number(v);
const ms = ts < 1e12 ? ts * 1000 : ts;
return formatDate(ms) as any;
},
},
{
accessorKey: 'member_status',
header: '会员状态',
cell: ({ row }) => {
const v = (row.original as any)?.member_status;
return <span className='text-sm'>{v ?? '---'}</span>;
},
},
{ {
accessorKey: 'referer_id', accessorKey: 'referer_id',
header: t('referer'), header: t('referer'),
@@ -203,6 +363,10 @@ export default function Page() {
key: 'user_subscribe_id', key: 'user_subscribe_id',
placeholder: t('subscriptionId'), placeholder: t('subscriptionId'),
}, },
{
key: 'device_id',
placeholder: '设备id',
},
]} ]}
actions={{ actions={{
render: (row) => { render: (row) => {
@@ -281,7 +445,7 @@ function ProfileSheet({ userId }: { userId: number }) {
<TabsList className='mb-3'> <TabsList className='mb-3'>
<TabsTrigger value='basic'>{t('basicInfoTitle')}</TabsTrigger> <TabsTrigger value='basic'>{t('basicInfoTitle')}</TabsTrigger>
<TabsTrigger value='notify'>{t('notifySettingsTitle')}</TabsTrigger> <TabsTrigger value='notify'>{t('notifySettingsTitle')}</TabsTrigger>
<TabsTrigger value='auth'>{t('authMethodsTitle')}</TabsTrigger> {/*<TabsTrigger value='auth'>{t('authMethodsTitle')}</TabsTrigger>*/}
</TabsList> </TabsList>
<TabsContent value='basic' className='mt-0'> <TabsContent value='basic' className='mt-0'>
<BasicInfoForm user={user} refetch={refetch as any} /> <BasicInfoForm user={user} refetch={refetch as any} />
@@ -289,9 +453,9 @@ function ProfileSheet({ userId }: { userId: number }) {
<TabsContent value='notify' className='mt-0'> <TabsContent value='notify' className='mt-0'>
<NotifySettingsForm user={user} refetch={refetch as any} /> <NotifySettingsForm user={user} refetch={refetch as any} />
</TabsContent> </TabsContent>
<TabsContent value='auth' className='mt-0'> {/*<TabsContent value='auth' className='mt-0'>
<AuthMethodsForm user={user} refetch={refetch as any} /> <AuthMethodsForm user={user} refetch={refetch as any} />
</TabsContent> </TabsContent>*/}
</Tabs> </Tabs>
</ScrollArea> </ScrollArea>
)} )}
@@ -148,7 +148,8 @@ export function UserDetail({ id }: { id: number }) {
const identifier = const identifier =
data?.auth_methods.find((m) => m.auth_type === 'email')?.auth_identifier || data?.auth_methods.find((m) => m.auth_type === 'email')?.auth_identifier ||
data?.auth_methods[0]?.auth_identifier; `设备Id${data?.user_devices[0]?.id}` ||
'账号不存在';
return ( return (
<HoverCard> <HoverCard>
+5
View File
@@ -77,6 +77,11 @@ export const navs = [
icon: 'flat-color-icons:currency-exchange', icon: 'flat-color-icons:currency-exchange',
}, },
{ title: 'ADS Config', url: '/dashboard/ads', icon: 'flat-color-icons:electrical-sensor' }, { title: 'ADS Config', url: '/dashboard/ads', icon: 'flat-color-icons:electrical-sensor' },
{
title: 'Version Management',
url: '/dashboard/settings/version',
icon: 'flat-color-icons:kindle',
},
], ],
}, },
+2 -6
View File
@@ -1,16 +1,13 @@
{ {
"ADS Config": "ADS Config", "ADS Config": "ADS Config",
"Announcement Management": "Announcement Management", "Announcement Management": "Announcement Management",
"Auth Control": "Auth Control", "Auth Control": "Auth Control",
"Balance": "Balance", "Balance": "Balance",
"Commerce": "Commerce", "Commerce": "Commerce",
"Commission": "Commission", "Commission": "Commission",
"Coupon Management": "Coupon Management", "Coupon Management": "Coupon Management",
"Dashboard": "Dashboard", "Dashboard": "Dashboard",
"Document Management": "Document Management", "Document Management": "Document Management",
"Email": "Email", "Email": "Email",
"Gift": "Gift", "Gift": "Gift",
"Login": "Login", "Login": "Login",
@@ -23,7 +20,6 @@
"Order Management": "Order Management", "Order Management": "Order Management",
"Payment Config": "Payment Config", "Payment Config": "Payment Config",
"Product Management": "Product Management", "Product Management": "Product Management",
"Register": "Register", "Register": "Register",
"Reset Subscribe": "Reset Subscribe", "Reset Subscribe": "Reset Subscribe",
"Server Management": "Server Management", "Server Management": "Server Management",
@@ -34,10 +30,10 @@
"System": "System", "System": "System",
"System Config": "System Config", "System Config": "System Config",
"System Tool": "System Tool", "System Tool": "System Tool",
"Ticket Management": "Ticket Management", "Ticket Management": "Ticket Management",
"Traffic Details": "Traffic Details", "Traffic Details": "Traffic Details",
"User Detail": "User Detail", "User Detail": "User Detail",
"User Management": "User Management", "User Management": "User Management",
"Users & Support": "Users & Support" "Users & Support": "Users & Support",
"Version Management": "Version Management"
} }
+34 -1
View File
@@ -18,7 +18,10 @@
"currencySymbolPlaceholder": "$", "currencySymbolPlaceholder": "$",
"currencyUnit": "Currency Unit", "currencyUnit": "Currency Unit",
"currencyUnitDescription": "Used for display purposes only; changing this will affect all currency units in the system", "currencyUnitDescription": "Used for display purposes only; changing this will affect all currency units in the system",
"currencyUnitPlaceholder": "USD" "currencyUnitPlaceholder": "USD",
"fixedRate": "Fixed Exchange Rate",
"fixedRatePlaceholder": "0",
"fixedRateDescription": "If a fixed rate is set, it will be used instead of the API rate"
}, },
"invite": { "invite": {
"title": "Invitation Settings", "title": "Invitation Settings",
@@ -135,5 +138,35 @@
"inputPlaceholder": "Please enter", "inputPlaceholder": "Please enter",
"saveSuccess": "Save Successful", "saveSuccess": "Save Successful",
"saveFailed": "Save Failed" "saveFailed": "Save Failed"
},
"version": {
"title": "Version Management",
"description": "Manage app versions for all platforms",
"create": "Create",
"edit": "Edit Version",
"createVersion": "Create Version",
"platform": "Platform",
"platformPlaceholder": "Select platform",
"versionNumber": "Version",
"versionPlaceholder": "1.0.0",
"minVersion": "Min Version",
"downloadUrl": "Download URL",
"descriptionField": "Description",
"descriptionPlaceholder": "Update description...",
"forceUpdate": "Force Update",
"default": "Default",
"inReview": "In Review",
"actions": "Actions",
"url": "URL",
"force": "Force",
"total": "Total: {count} items",
"previous": "Previous",
"next": "Next",
"page": "Page {page}",
"noResults": "No results.",
"yes": "Yes",
"no": "No",
"update": "Update",
"confirmDelete": "Are you sure you want to delete?"
} }
} }
+2 -6
View File
@@ -1,16 +1,13 @@
{ {
"ADS Config": "广告配置", "ADS Config": "广告配置",
"Announcement Management": "公告管理", "Announcement Management": "公告管理",
"Auth Control": "认证控制", "Auth Control": "认证控制",
"Balance": "余额变动", "Balance": "余额变动",
"Commerce": "商务", "Commerce": "商务",
"Commission": "佣金记录", "Commission": "佣金记录",
"Coupon Management": "优惠券管理", "Coupon Management": "优惠券管理",
"Dashboard": "仪表盘", "Dashboard": "仪表盘",
"Document Management": "文档管理", "Document Management": "文档管理",
"Email": "邮件日志", "Email": "邮件日志",
"Gift": "赠送记录", "Gift": "赠送记录",
"Login": "登录日志", "Login": "登录日志",
@@ -23,7 +20,6 @@
"Order Management": "订单管理", "Order Management": "订单管理",
"Payment Config": "支付配置", "Payment Config": "支付配置",
"Product Management": "商品管理", "Product Management": "商品管理",
"Register": "注册日志", "Register": "注册日志",
"Reset Subscribe": "重置订阅", "Reset Subscribe": "重置订阅",
"Server Management": "服务器管理", "Server Management": "服务器管理",
@@ -34,10 +30,10 @@
"System": "系统", "System": "系统",
"System Config": "系统配置", "System Config": "系统配置",
"System Tool": "系统工具", "System Tool": "系统工具",
"Ticket Management": "工单管理", "Ticket Management": "工单管理",
"Traffic Details": "流量明细", "Traffic Details": "流量明细",
"User Detail": "用户详情", "User Detail": "用户详情",
"User Management": "用户管理", "User Management": "用户管理",
"Users & Support": "用户与支持" "Users & Support": "用户与支持",
"Version Management": "版本管理"
} }
+34 -1
View File
@@ -18,7 +18,10 @@
"currencySymbolPlaceholder": "$", "currencySymbolPlaceholder": "$",
"currencyUnit": "货币单位", "currencyUnit": "货币单位",
"currencyUnitDescription": "仅用于展示使用,更改后系统中所有的货币单位都将发生变更", "currencyUnitDescription": "仅用于展示使用,更改后系统中所有的货币单位都将发生变更",
"currencyUnitPlaceholder": "USD" "currencyUnitPlaceholder": "USD",
"fixedRate": "固定汇率",
"fixedRatePlaceholder": "0",
"fixedRateDescription": "如果设置了固定汇率,将使用此值而非API获取的汇率"
}, },
"invite": { "invite": {
"title": "邀请设置", "title": "邀请设置",
@@ -137,5 +140,35 @@
"inputPlaceholder": "请输入", "inputPlaceholder": "请输入",
"saveSuccess": "保存成功", "saveSuccess": "保存成功",
"saveFailed": "保存失败" "saveFailed": "保存失败"
},
"version": {
"title": "版本管理",
"description": "管理各平台应用版本信息",
"create": "创建",
"edit": "编辑版本",
"createVersion": "创建版本",
"platform": "平台",
"platformPlaceholder": "选择平台",
"versionNumber": "版本号",
"versionPlaceholder": "1.0.0",
"minVersion": "最低版本",
"downloadUrl": "下载链接",
"descriptionField": "描述",
"descriptionPlaceholder": "更新说明...",
"forceUpdate": "强制更新",
"default": "默认版本",
"inReview": "审核中",
"actions": "操作",
"url": "下载地址",
"force": "强制",
"total": "共 {count} 条",
"previous": "上一页",
"next": "下一页",
"page": "第 {page} 页",
"noResults": "暂无数据",
"yes": "是",
"no": "否",
"update": "更新",
"confirmDelete": "确定要删除吗?"
} }
} }
+1 -1
View File
@@ -21,7 +21,7 @@
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"mlkem-wasm": "^0.0.6", "mlkem-wasm": "^0.0.6",
"nanoid": "^5.1.5", "nanoid": "^5.1.5",
"next": "^15.5.2", "next": "15.5.7",
"next-intl": "^3.26.3", "next-intl": "^3.26.3",
"next-runtime-env": "^3.3.0", "next-runtime-env": "^3.3.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
+68
View File
@@ -85,3 +85,71 @@ export async function getSubscribeApplicationList(
}, },
); );
} }
/** Create App Version POST /v1/admin/application/version */
export async function createAppVersion(
body: API.CreateAppVersionRequest,
options?: { [key: string]: any },
) {
return request<API.Response & { data?: API.ApplicationVersion }>(
'/v1/admin/application/version',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
},
);
}
/** Update App Version PUT /v1/admin/application/version */
export async function updateAppVersion(
body: API.UpdateAppVersionRequest,
options?: { [key: string]: any },
) {
return request<API.Response & { data?: API.ApplicationVersion }>(
'/v1/admin/application/version',
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
},
);
}
/** Delete App Version DELETE /v1/admin/application/version */
export async function deleteAppVersion(
body: API.DeleteAppVersionRequest,
options?: { [key: string]: any },
) {
return request<API.Response & { data?: any }>('/v1/admin/application/version', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** Get App Version List GET /v1/admin/application/version/list */
export async function getAppVersionList(
params: API.GetAppVersionListParams,
options?: { [key: string]: any },
) {
return request<API.Response & { data?: API.GetAppVersionListResponse }>(
'/v1/admin/application/version/list',
{
method: 'GET',
params: {
...params,
},
...(options || {}),
},
);
}
+46 -1
View File
@@ -65,10 +65,53 @@ declare namespace API {
type ApplicationVersion = { type ApplicationVersion = {
id: number; id: number;
platform: string;
url: string; url: string;
version: string; version: string;
description: string; min_version?: string;
force_update: boolean;
description: Record<string, string>;
is_default: boolean; is_default: boolean;
is_in_review: boolean;
created_at: number;
};
type CreateAppVersionRequest = {
platform: string;
version: string;
min_version?: string;
force_update?: boolean;
description?: string;
url: string;
is_default?: boolean;
is_in_review?: boolean;
};
type UpdateAppVersionRequest = {
id: number;
platform: string;
version: string;
min_version?: string;
force_update?: boolean;
description?: string;
url: string;
is_default?: boolean;
is_in_review?: boolean;
};
type DeleteAppVersionRequest = {
id: number;
};
type GetAppVersionListParams = {
page?: number;
size?: number;
platform?: string;
};
type GetAppVersionListResponse = {
total: number;
list: ApplicationVersion[];
}; };
type AppUserSubcbribe = { type AppUserSubcbribe = {
@@ -398,6 +441,7 @@ declare namespace API {
access_key: string; access_key: string;
currency_unit: string; currency_unit: string;
currency_symbol: string; currency_symbol: string;
fixed_rate?: number;
}; };
type DeleteAdsRequest = { type DeleteAdsRequest = {
@@ -2360,6 +2404,7 @@ declare namespace API {
id: number; id: number;
avatar: string; avatar: string;
balance: number; balance: number;
remark: string;
commission: number; commission: number;
referral_percentage: number; referral_percentage: number;
only_first_purchase: boolean; only_first_purchase: boolean;
+1 -1
View File
@@ -20,7 +20,7 @@
"framer-motion": "^12.23.12", "framer-motion": "^12.23.12",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"lucide-react": "^0.542.0", "lucide-react": "^0.542.0",
"next": "^15.5.2", "next": "15.5.7",
"next-intl": "^3.26.3", "next-intl": "^3.26.3",
"next-runtime-env": "^3.3.0", "next-runtime-env": "^3.3.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
+12 -12
View File
@@ -115,7 +115,7 @@
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"mlkem-wasm": "^0.0.6", "mlkem-wasm": "^0.0.6",
"nanoid": "^5.1.5", "nanoid": "^5.1.5",
"next": "^15.5.2", "next": "15.5.7",
"next-intl": "^3.26.3", "next-intl": "^3.26.3",
"next-runtime-env": "^3.3.0", "next-runtime-env": "^3.3.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
@@ -151,7 +151,7 @@
"framer-motion": "^12.23.12", "framer-motion": "^12.23.12",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"lucide-react": "^0.542.0", "lucide-react": "^0.542.0",
"next": "^15.5.2", "next": "15.5.7",
"next-intl": "^3.26.3", "next-intl": "^3.26.3",
"next-runtime-env": "^3.3.0", "next-runtime-env": "^3.3.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
@@ -528,27 +528,27 @@
"@netlify/plugin-nextjs": ["@netlify/plugin-nextjs@5.14.0", "", {}, "sha512-8WEnVm88Ed6v6j/D0iqXDSnAW8Mf9P2CDVgBG3x2+vkiEI48X6Na7KnoQRyi6oulgA6foHIPWqgQIClsPiW20A=="], "@netlify/plugin-nextjs": ["@netlify/plugin-nextjs@5.14.0", "", {}, "sha512-8WEnVm88Ed6v6j/D0iqXDSnAW8Mf9P2CDVgBG3x2+vkiEI48X6Na7KnoQRyi6oulgA6foHIPWqgQIClsPiW20A=="],
"@next/env": ["@next/env@15.5.6", "", {}, "sha512-3qBGRW+sCGzgbpc5TS1a0p7eNxnOarGVQhZxfvTdnV0gFI61lX7QNtQ4V1TSREctXzYn5NetbUsLvyqwLFJM6Q=="], "@next/env": ["@next/env@15.5.7", "", {}, "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg=="],
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.5.6", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-YxDvsT2fwy1j5gMqk3ppXlsgDopHnkM4BoxSVASbvvgh5zgsK8lvWerDzPip8k3WVzsTZ1O7A7si1KNfN4OZfQ=="], "@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.5.6", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-YxDvsT2fwy1j5gMqk3ppXlsgDopHnkM4BoxSVASbvvgh5zgsK8lvWerDzPip8k3WVzsTZ1O7A7si1KNfN4OZfQ=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ES3nRz7N+L5Umz4KoGfZ4XX6gwHplwPhioVRc25+QNsDa7RtUF/z8wJcbuQ2Tffm5RZwuN2A063eapoJ1u4nPg=="], "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.5.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-JIGcytAyk9LQp2/nuVZPAtj8uaJ/zZhsKOASTjxDug0SPU9LAM3wy6nPU735M1OqacR4U20LHVF5v5Wnl9ptTA=="], "@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.5.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-qvz4SVKQ0P3/Im9zcS2RmfFL/UCQnsJKJwQSkissbngnB/12c6bZTCB0gHTexz1s6d/mD0+egPKXAIRFVS7hQg=="], "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.5.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-FsbGVw3SJz1hZlvnWD+T6GFgV9/NYDeLTNQB2MXoPN5u9VA9OEDy6fJEfePfsUKAhJufFbZLgp0cPxMuV6SV0w=="], "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.5.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-3QnHGFWlnvAgyxFxt2Ny8PTpXtQD7kVEeaFat5oPAHHI192WKYB+VIKZijtHLGdBBvc16tiAkPTDmQNOQ0dyrA=="], "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.5.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-OsGX148sL+TqMK9YFaPFPoIaJKbFJJxFzkXZljIgA9hjMjdruKht6xDCEv1HLtlLNfkx3c5w2GLKhj7veBQizQ=="], "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.5.7", "", { "os": "linux", "cpu": "x64" }, "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-ONOMrqWxdzXDJNh2n60H6gGyKed42Ieu6UTVPZteXpuKbLZTH4G4eBMsr5qWgOBA+s7F+uB4OJbZnrkEDnZ5Fg=="], "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.5.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ=="],
"@next/swc-win32-ia32-msvc": ["@next/swc-win32-ia32-msvc@14.2.33", "", { "os": "win32", "cpu": "ia32" }, "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q=="], "@next/swc-win32-ia32-msvc": ["@next/swc-win32-ia32-msvc@14.2.33", "", { "os": "win32", "cpu": "ia32" }, "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-pxK4VIjFRx1MY92UycLOOw7dTdvccWsNETQ0kDHkBlcFH1GrTLUjSiHU1ohrznnux6TqRHgv5oflhfIWZwVROQ=="], "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.7", "", { "os": "win32", "cpu": "x64" }, "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw=="],
"@noble/curves": ["@noble/curves@2.0.1", "", { "dependencies": { "@noble/hashes": "2.0.1" } }, "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw=="], "@noble/curves": ["@noble/curves@2.0.1", "", { "dependencies": { "@noble/hashes": "2.0.1" } }, "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw=="],
@@ -1918,7 +1918,7 @@
"netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="], "netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="],
"next": ["next@15.5.6", "", { "dependencies": { "@next/env": "15.5.6", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.6", "@next/swc-darwin-x64": "15.5.6", "@next/swc-linux-arm64-gnu": "15.5.6", "@next/swc-linux-arm64-musl": "15.5.6", "@next/swc-linux-x64-gnu": "15.5.6", "@next/swc-linux-x64-musl": "15.5.6", "@next/swc-win32-arm64-msvc": "15.5.6", "@next/swc-win32-x64-msvc": "15.5.6", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-zTxsnI3LQo3c9HSdSf91O1jMNsEzIXDShXd4wVdg9y5shwLqBXi4ZtUUJyB86KGVSJLZx0PFONvO54aheGX8QQ=="], "next": ["next@15.5.7", "", { "dependencies": { "@next/env": "15.5.7", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.7", "@next/swc-darwin-x64": "15.5.7", "@next/swc-linux-arm64-gnu": "15.5.7", "@next/swc-linux-arm64-musl": "15.5.7", "@next/swc-linux-x64-gnu": "15.5.7", "@next/swc-linux-x64-musl": "15.5.7", "@next/swc-win32-arm64-msvc": "15.5.7", "@next/swc-win32-x64-msvc": "15.5.7", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ=="],
"next-intl": ["next-intl@3.26.5", "", { "dependencies": { "@formatjs/intl-localematcher": "^0.5.4", "negotiator": "^1.0.0", "use-intl": "^3.26.5" }, "peerDependencies": { "next": "^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" } }, "sha512-EQlCIfY0jOhRldiFxwSXG+ImwkQtDEfQeSOEQp6ieAGSLWGlgjdb/Ck/O7wMfC430ZHGeUKVKax8KGusTPKCgg=="], "next-intl": ["next-intl@3.26.5", "", { "dependencies": { "@formatjs/intl-localematcher": "^0.5.4", "negotiator": "^1.0.0", "use-intl": "^3.26.5" }, "peerDependencies": { "next": "^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" } }, "sha512-EQlCIfY0jOhRldiFxwSXG+ImwkQtDEfQeSOEQp6ieAGSLWGlgjdb/Ck/O7wMfC430ZHGeUKVKax8KGusTPKCgg=="],
+7 -1
View File
@@ -5,7 +5,13 @@ services:
context: ../ context: ../
dockerfile: ./docker/ppanel-admin-web/Dockerfile dockerfile: ./docker/ppanel-admin-web/Dockerfile
environment: environment:
NEXT_PUBLIC_API_URL: 'https://api.ppanel.dev' NEXT_PUBLIC_API_URL: 'https://api.hifast.biz'
NEXT_PUBLIC_DEFAULT_LANGUAGE: 'en-US'
NEXT_PUBLIC_SITE_URL: 'https://4d3vsw8.88xgaen.hifast.biz'
NEXT_PUBLIC_DEFAULT_USER_EMAIL: 'admin@ppanel.dev'
NEXT_PUBLIC_DEFAULT_USER_PASSWORD: 'password'
# 使用 host 模式可解决网络连接问题,但需注意端口冲突(默认 3000)
# network_mode: "host"
restart: always restart: always
ports: ports:
- 3001:3000 - 3001:3000
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "fastvpn-web", "name": "ppanel-web",
"version": "1.6.0", "version": "1.6.4",
"private": true, "private": true,
"homepage": "https://github.com/perfect-panel/fastvpn-web", "homepage": "https://github.com/perfect-panel/fastvpn-web",
"bugs": { "bugs": {
+2 -1
View File
@@ -10,6 +10,7 @@ const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger; const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverAnchor = PopoverPrimitive.Anchor; const PopoverAnchor = PopoverPrimitive.Anchor;
const PopoverClose = PopoverPrimitive.Close;
const PopoverContent = React.forwardRef< const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>, React.ElementRef<typeof PopoverPrimitive.Content>,
@@ -30,4 +31,4 @@ const PopoverContent = React.forwardRef<
)); ));
PopoverContent.displayName = PopoverPrimitive.Content.displayName; PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger }; export { Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger };
@@ -109,7 +109,7 @@ export function ProTable<
const [rowCount, setRowCount] = useState<number>(0); const [rowCount, setRowCount] = useState<number>(0);
const [pagination, setPagination] = useState({ const [pagination, setPagination] = useState({
pageIndex: 0, pageIndex: 0,
pageSize: 10, pageSize: 200,
}); });
const loading = useRef(false); const loading = useRef(false);
+2 -8
View File
@@ -35,16 +35,10 @@ else
fi fi
# Set up pre-commit hook # Set up pre-commit hook
setup_husky_hook "pre-commit" "#!/bin/sh setup_husky_hook "pre-commit" "npx --no-install lint-staged"
. \"\$(dirname \"\$0\")/_/husky.sh\"
npx --no-install lint-staged"
# Set up commit-msg hook # Set up commit-msg hook
setup_husky_hook "commit-msg" "#!/bin/sh setup_husky_hook "commit-msg" "npx --no-install commitlint --edit \"\$1\""
. \"\$(dirname \"\$0\")/_/husky.sh\"
npx --no-install commitlint --edit \"\$1\""
# Function to globally install an npm package if not installed # Function to globally install an npm package if not installed
install_global_package() { install_global_package() {