@@ -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(),
|
||||
currency_unit: z.string().min(1),
|
||||
currency_symbol: z.string().min(1),
|
||||
fixed_rate: z.number().optional(),
|
||||
});
|
||||
|
||||
type CurrencyFormData = z.infer<typeof currencySchema>;
|
||||
@@ -62,6 +63,7 @@ export default function CurrencyConfig() {
|
||||
access_key: '',
|
||||
currency_unit: 'USD',
|
||||
currency_symbol: '$',
|
||||
fixed_rate: 0,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -170,6 +172,26 @@ export default function CurrencyConfig() {
|
||||
</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>
|
||||
</ScrollArea>
|
||||
|
||||
Reference in New Issue
Block a user