🐛 fix: Remove GroupTable and related components, simplify SubscribeTable and update language handling in subscription forms
This commit is contained in:
parent
4563c570ac
commit
1ab9b39e8a
@ -1,143 +0,0 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@workspace/ui/components/form';
|
||||
import { ScrollArea } from '@workspace/ui/components/scroll-area';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@workspace/ui/components/sheet';
|
||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
interface GroupFormProps<T> {
|
||||
onSubmit: (data: T) => Promise<boolean> | boolean;
|
||||
initialValues?: T;
|
||||
loading?: boolean;
|
||||
trigger: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export default function GroupForm<T extends Record<string, any>>({
|
||||
onSubmit,
|
||||
initialValues,
|
||||
loading,
|
||||
trigger,
|
||||
title,
|
||||
}: GroupFormProps<T>) {
|
||||
const t = useTranslations('product');
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const form = useForm({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
...initialValues,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form?.reset(initialValues);
|
||||
}, [form, initialValues]);
|
||||
|
||||
async function handleSubmit(data: { [x: string]: any }) {
|
||||
const bool = await onSubmit(data as T);
|
||||
|
||||
if (bool) setOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
onClick={() => {
|
||||
form.reset();
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
{trigger}
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className='w-[500px] max-w-full md:max-w-screen-md'>
|
||||
<SheetHeader>
|
||||
<SheetTitle>{title}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))]'>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className='space-y-4 px-6 pt-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='name'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('group.form.name')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
{...field}
|
||||
onValueChange={(value) => {
|
||||
form.setValue(field.name, value);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='description'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('group.form.description')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
{...field}
|
||||
onValueChange={(value) => {
|
||||
form.setValue(field.name, value);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</ScrollArea>
|
||||
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
|
||||
<Button
|
||||
variant='outline'
|
||||
disabled={loading}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{t('group.form.cancel')}
|
||||
</Button>
|
||||
<Button disabled={loading} onClick={form.handleSubmit(handleSubmit)}>
|
||||
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}{' '}
|
||||
{t('group.form.confirm')}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@ -1,141 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { ProTable, ProTableActions } from '@/components/pro-table';
|
||||
import {
|
||||
batchDeleteSubscribeGroup,
|
||||
createSubscribeGroup,
|
||||
deleteSubscribeGroup,
|
||||
getSubscribeGroupList,
|
||||
updateSubscribeGroup,
|
||||
} from '@/services/admin/subscribe';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
|
||||
import { formatDate } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useRef, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import GroupForm from './form';
|
||||
|
||||
const GroupTable = () => {
|
||||
const t = useTranslations('product');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const ref = useRef<ProTableActions>(null);
|
||||
|
||||
return (
|
||||
<ProTable<API.SubscribeGroup, any>
|
||||
action={ref}
|
||||
header={{
|
||||
title: t('group.title'),
|
||||
toolbar: (
|
||||
<GroupForm<API.CreateSubscribeGroupRequest>
|
||||
trigger={t('group.create')}
|
||||
title={t('group.createSubscribeGroup')}
|
||||
loading={loading}
|
||||
onSubmit={async (values) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await createSubscribeGroup(values);
|
||||
toast.success(t('group.createSuccess'));
|
||||
ref.current?.refresh();
|
||||
setLoading(false);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: t('group.name'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: t('group.description'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'updated_at',
|
||||
header: t('group.updatedAt'),
|
||||
cell: ({ row }) => formatDate(row.getValue('updated_at')),
|
||||
},
|
||||
]}
|
||||
request={async () => {
|
||||
const { data } = await getSubscribeGroupList();
|
||||
return {
|
||||
list: data.data?.list || [],
|
||||
total: data.data?.total || 0,
|
||||
};
|
||||
}}
|
||||
actions={{
|
||||
render: (row) => [
|
||||
<GroupForm<API.SubscribeGroup>
|
||||
key='edit'
|
||||
trigger={t('group.edit')}
|
||||
title={t('group.editSubscribeGroup')}
|
||||
loading={loading}
|
||||
initialValues={row}
|
||||
onSubmit={async (values) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await updateSubscribeGroup({
|
||||
...row,
|
||||
...values,
|
||||
});
|
||||
toast.success(t('group.updateSuccess'));
|
||||
ref.current?.refresh();
|
||||
setLoading(false);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
}}
|
||||
/>,
|
||||
<ConfirmButton
|
||||
key='delete'
|
||||
trigger={<Button variant='destructive'>{t('group.delete')}</Button>}
|
||||
title={t('group.confirmDelete')}
|
||||
description={t('group.deleteWarning')}
|
||||
onConfirm={async () => {
|
||||
await deleteSubscribeGroup({
|
||||
id: row.id,
|
||||
});
|
||||
toast.success(t('group.deleteSuccess'));
|
||||
ref.current?.refresh();
|
||||
}}
|
||||
cancelText={t('group.cancel')}
|
||||
confirmText={t('group.confirm')}
|
||||
/>,
|
||||
],
|
||||
batchRender(rows) {
|
||||
return [
|
||||
<ConfirmButton
|
||||
key='delete'
|
||||
trigger={<Button variant='destructive'>{t('group.delete')}</Button>}
|
||||
title={t('group.confirmDelete')}
|
||||
description={t('group.deleteWarning')}
|
||||
onConfirm={async () => {
|
||||
await batchDeleteSubscribeGroup({
|
||||
ids: rows.map((item) => item.id),
|
||||
});
|
||||
toast.success(t('group.deleteSuccess'));
|
||||
ref.current?.refresh();
|
||||
}}
|
||||
cancelText={t('group.cancel')}
|
||||
confirmText={t('group.confirm')}
|
||||
/>,
|
||||
];
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupTable;
|
||||
@ -1,25 +1,9 @@
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||
|
||||
import GroupTable from './group/table';
|
||||
import SubscribeTable from './subscribe-table';
|
||||
|
||||
export default async function Page() {
|
||||
const t = await getTranslations('product');
|
||||
|
||||
return (
|
||||
<Tabs defaultValue='subscribe'>
|
||||
<TabsList>
|
||||
<TabsTrigger value='subscribe'>{t('tabs.subscribe')}</TabsTrigger>
|
||||
<TabsTrigger value='group'>{t('tabs.subscribeGroup')}</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value='subscribe'>
|
||||
<SubscribeTable />
|
||||
</TabsContent>
|
||||
<TabsContent value='group'>
|
||||
<GroupTable />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
return <SubscribeTable />;
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { filterNodeList, queryNodeTag } from '@/services/admin/server';
|
||||
import { getSubscribeGroupList } from '@/services/admin/subscribe';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
@ -62,6 +61,7 @@ const defaultValues = {
|
||||
traffic: 0,
|
||||
quota: 0,
|
||||
discount: [],
|
||||
language: '',
|
||||
node_tags: [],
|
||||
nodes: [],
|
||||
unit_time: 'Month',
|
||||
@ -102,8 +102,7 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
||||
device_limit: z.number().optional(),
|
||||
traffic: z.number().optional(),
|
||||
quota: z.number().optional(),
|
||||
group_id: z.number().optional().nullish(),
|
||||
// Use tags as group identifiers; accept string (tag) or number (legacy id)
|
||||
language: z.string().optional(),
|
||||
node_tags: z.array(z.string()).optional(),
|
||||
nodes: z.array(z.number()).optional(),
|
||||
deduction_ratio: z.number().optional(),
|
||||
@ -230,14 +229,6 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
||||
if (bool) setOpen(false);
|
||||
}
|
||||
|
||||
const { data: group } = useQuery({
|
||||
queryKey: ['getSubscribeGroupList'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getSubscribeGroupList();
|
||||
return data.data?.list as API.SubscribeGroup[];
|
||||
},
|
||||
});
|
||||
|
||||
const { data: nodes } = useQuery({
|
||||
queryKey: ['filterNodeListAll'],
|
||||
queryFn: async () => {
|
||||
@ -328,22 +319,20 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='group_id'
|
||||
name='language'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('form.groupId')}</FormLabel>
|
||||
<FormLabel>
|
||||
{t('form.language')}
|
||||
<span className='text-muted-foreground ml-1 text-[0.8rem]'>
|
||||
{t('form.languageDescription')}
|
||||
</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Combobox<number, false>
|
||||
placeholder={t('form.selectSubscribeGroup')}
|
||||
<EnhancedInput
|
||||
{...field}
|
||||
value={field.value ?? undefined}
|
||||
onChange={(value) => {
|
||||
form.setValue(field.name, value || 0);
|
||||
}}
|
||||
options={group?.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}))}
|
||||
placeholder={t('form.languagePlaceholder')}
|
||||
onValueChange={(v) => form.setValue(field.name, v as string)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
|
||||
@ -6,12 +6,10 @@ import {
|
||||
batchDeleteSubscribe,
|
||||
createSubscribe,
|
||||
deleteSubscribe,
|
||||
getSubscribeGroupList,
|
||||
getSubscribeList,
|
||||
subscribeSort,
|
||||
updateSubscribe,
|
||||
} from '@/services/admin/subscribe';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Badge } from '@workspace/ui/components/badge';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
@ -24,16 +22,6 @@ import SubscribeForm from './subscribe-form';
|
||||
export default function SubscribeTable() {
|
||||
const t = useTranslations('product');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { data: groups } = useQuery({
|
||||
queryKey: ['getSubscribeGroupList', 'all'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getSubscribeGroupList({
|
||||
page: 1,
|
||||
size: 9999,
|
||||
});
|
||||
return data.data?.list as API.SubscribeGroup[];
|
||||
},
|
||||
});
|
||||
const ref = useRef<ProTableActions>(null);
|
||||
return (
|
||||
<ProTable<API.SubscribeItem, { group_id: number; query: string }>
|
||||
@ -67,14 +55,6 @@ export default function SubscribeTable() {
|
||||
),
|
||||
}}
|
||||
params={[
|
||||
{
|
||||
key: 'group_id',
|
||||
placeholder: t('subscribeGroup'),
|
||||
options: groups?.map((item) => ({
|
||||
label: item.name,
|
||||
value: String(item.id),
|
||||
})),
|
||||
},
|
||||
{
|
||||
key: 'search',
|
||||
},
|
||||
@ -176,11 +156,11 @@ export default function SubscribeTable() {
|
||||
cell: ({ row }) => <Display type='number' value={row.getValue('quota')} unlimited />,
|
||||
},
|
||||
{
|
||||
accessorKey: 'group_id',
|
||||
header: t('subscribeGroup'),
|
||||
accessorKey: 'language',
|
||||
header: t('language'),
|
||||
cell: ({ row }) => {
|
||||
const name = groups?.find((group) => group.id === row.getValue('group_id'))?.name;
|
||||
return name ? <Badge variant='outline'>{name}</Badge> : '--';
|
||||
const language = row.getValue('language') as string;
|
||||
return language ? <Badge variant='outline'>{language}</Badge> : '--';
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -35,8 +35,10 @@
|
||||
"discountPercent": "Discount Percentage",
|
||||
"discount_price": "Discount Price",
|
||||
"duration": "Duration (months)",
|
||||
"groupId": "Subscription Group",
|
||||
"inventory": "Subscription Limit",
|
||||
"language": "Language",
|
||||
"languageDescription": "Leave empty for default without language restriction",
|
||||
"languagePlaceholder": "Language identifier for the subscription, e.g., en-US, zh-CN",
|
||||
"monthlyReset": "Monthly Reset",
|
||||
"name": "Name",
|
||||
"noLimit": "No Limit",
|
||||
@ -51,7 +53,6 @@
|
||||
"resetCycle": "Reset Cycle",
|
||||
"resetOn1st": "Reset on the 1st",
|
||||
"selectResetCycle": "Please select a reset cycle",
|
||||
"selectSubscribeGroup": "Select Subscription Group",
|
||||
"selectUnitTime": "Please select a unit of time",
|
||||
"node": "Node",
|
||||
"nodeGroup": "Node Group",
|
||||
@ -61,32 +62,8 @@
|
||||
"unitPrice": "Unit Price",
|
||||
"unitTime": "Unit Time"
|
||||
},
|
||||
"group": {
|
||||
"actions": "Actions",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"confirmDelete": "Are you sure you want to delete?",
|
||||
"create": "Create",
|
||||
"createSubscribeGroup": "Create Subscription Group",
|
||||
"createSuccess": "Create Successful",
|
||||
"delete": "Delete",
|
||||
"deleteSuccess": "Delete Successful",
|
||||
"deleteWarning": "Data cannot be recovered after deletion. Please proceed with caution.",
|
||||
"description": "Description",
|
||||
"edit": "Edit",
|
||||
"editSubscribeGroup": "Edit Subscription Group",
|
||||
"form": {
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"description": "Description",
|
||||
"name": "Name"
|
||||
},
|
||||
"name": "Name",
|
||||
"title": "Subscription Group List",
|
||||
"updateSuccess": "Update Successful",
|
||||
"updatedAt": "Updated At"
|
||||
},
|
||||
"inventory": "Subscription Limit",
|
||||
"language": "Language",
|
||||
"name": "Name",
|
||||
"quota": "Purchase Limit/Time",
|
||||
"replacement": "Reset Price/Time",
|
||||
@ -94,11 +71,6 @@
|
||||
"show": "Display",
|
||||
"sold": "Subscription Count",
|
||||
"subscribe": "Subscribe",
|
||||
"subscribeGroup": "Subscription Group",
|
||||
"tabs": {
|
||||
"subscribe": "Subscribe",
|
||||
"subscribeGroup": "Subscription Group"
|
||||
},
|
||||
"traffic": "Traffic",
|
||||
"unitPrice": "Unit Price",
|
||||
"updateSuccess": "Update Successful"
|
||||
|
||||
@ -35,8 +35,10 @@
|
||||
"discountPercent": "折扣比",
|
||||
"discount_price": "折扣价格",
|
||||
"duration": "时长(月)",
|
||||
"groupId": "订阅组",
|
||||
"inventory": "订阅限制",
|
||||
"language": "语言",
|
||||
"languageDescription": "不填写则为没有语言限制的默认选项",
|
||||
"languagePlaceholder": "订阅产品支持的语言标识符,如:en-US, zh-CN",
|
||||
"monthlyReset": "按月重置",
|
||||
"name": "名称",
|
||||
"noLimit": "无限制",
|
||||
@ -51,7 +53,6 @@
|
||||
"resetCycle": "重置周期",
|
||||
"resetOn1st": "1号重置",
|
||||
"selectResetCycle": "请选择重置周期",
|
||||
"selectSubscribeGroup": "请选择订阅组",
|
||||
"selectUnitTime": "请选择单位时间",
|
||||
"node": "节点",
|
||||
"nodeGroup": "节点组",
|
||||
@ -61,32 +62,8 @@
|
||||
"unitPrice": "单价",
|
||||
"unitTime": "单位时间"
|
||||
},
|
||||
"group": {
|
||||
"actions": "操作",
|
||||
"cancel": "取消",
|
||||
"confirm": "确认",
|
||||
"confirmDelete": "确定删除吗?",
|
||||
"create": "创建",
|
||||
"createSubscribeGroup": "新建订阅组",
|
||||
"createSuccess": "创建成功",
|
||||
"delete": "删除",
|
||||
"deleteSuccess": "删除成功",
|
||||
"deleteWarning": "删除后数据无法恢复,请谨慎操作。",
|
||||
"description": "描述",
|
||||
"edit": "编辑",
|
||||
"editSubscribeGroup": "编辑订阅组",
|
||||
"form": {
|
||||
"cancel": "取消",
|
||||
"confirm": "确认",
|
||||
"description": "描述",
|
||||
"name": "名称"
|
||||
},
|
||||
"name": "名称",
|
||||
"title": "订阅组列表",
|
||||
"updateSuccess": "更新成功",
|
||||
"updatedAt": "更新时间"
|
||||
},
|
||||
"inventory": "订阅限制",
|
||||
"language": "语言",
|
||||
"name": "名称",
|
||||
"quota": "限购/次",
|
||||
"replacement": "重置价格/次",
|
||||
@ -94,13 +71,6 @@
|
||||
"show": "显示",
|
||||
"sold": "订阅数量",
|
||||
"subscribe": "订阅",
|
||||
"subscribeGroup": "订阅组",
|
||||
"tabs": {
|
||||
"subscribe": "订阅",
|
||||
"subscribeApp": "应用配置",
|
||||
"subscribeConfig": "订阅配置",
|
||||
"subscribeGroup": "订阅组"
|
||||
},
|
||||
"traffic": "流量",
|
||||
"unitPrice": "单价",
|
||||
"updateSuccess": "更新成功"
|
||||
|
||||
@ -191,14 +191,6 @@ export async function updateSubscribeConfig(
|
||||
});
|
||||
}
|
||||
|
||||
/** Get subscribe type GET /v1/admin/system/subscribe_type */
|
||||
export async function getSubscribeType(options?: { [key: string]: any }) {
|
||||
return request<API.Response & { data?: API.SubscribeType }>('/v1/admin/system/subscribe_type', {
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
});
|
||||
}
|
||||
|
||||
/** Get Team of Service Config GET /v1/admin/system/tos_config */
|
||||
export async function getTosConfig(options?: { [key: string]: any }) {
|
||||
return request<API.Response & { data?: API.TosConfig }>('/v1/admin/system/tos_config', {
|
||||
|
||||
12
apps/admin/services/admin/typings.d.ts
vendored
12
apps/admin/services/admin/typings.d.ts
vendored
@ -325,6 +325,7 @@ declare namespace API {
|
||||
|
||||
type CreateSubscribeRequest = {
|
||||
name: string;
|
||||
language: string;
|
||||
description: string;
|
||||
unit_price: number;
|
||||
unit_time: string;
|
||||
@ -335,7 +336,6 @@ declare namespace API {
|
||||
speed_limit: number;
|
||||
device_limit: number;
|
||||
quota: number;
|
||||
group_id: number;
|
||||
nodes: number[];
|
||||
node_tags: string[];
|
||||
show: boolean;
|
||||
@ -1045,14 +1045,14 @@ declare namespace API {
|
||||
type GetSubscribeListParams = {
|
||||
page: number;
|
||||
size: number;
|
||||
group_id?: number;
|
||||
language?: string;
|
||||
search?: string;
|
||||
};
|
||||
|
||||
type GetSubscribeListRequest = {
|
||||
page: number;
|
||||
size: number;
|
||||
group_id?: number;
|
||||
language?: string;
|
||||
search?: string;
|
||||
};
|
||||
|
||||
@ -1829,6 +1829,7 @@ declare namespace API {
|
||||
type Subscribe = {
|
||||
id: number;
|
||||
name: string;
|
||||
language: string;
|
||||
description: string;
|
||||
unit_price: number;
|
||||
unit_time: string;
|
||||
@ -1839,7 +1840,6 @@ declare namespace API {
|
||||
speed_limit: number;
|
||||
device_limit: number;
|
||||
quota: number;
|
||||
group_id: number;
|
||||
nodes: number[];
|
||||
node_tags: string[];
|
||||
show: boolean;
|
||||
@ -1893,6 +1893,7 @@ declare namespace API {
|
||||
type SubscribeItem = {
|
||||
id?: number;
|
||||
name?: string;
|
||||
language?: string;
|
||||
description?: string;
|
||||
unit_price?: number;
|
||||
unit_time?: string;
|
||||
@ -1903,7 +1904,6 @@ declare namespace API {
|
||||
speed_limit?: number;
|
||||
device_limit?: number;
|
||||
quota?: number;
|
||||
group_id?: number;
|
||||
nodes?: number[];
|
||||
node_tags?: string[];
|
||||
show?: boolean;
|
||||
@ -2145,6 +2145,7 @@ declare namespace API {
|
||||
type UpdateSubscribeRequest = {
|
||||
id: number;
|
||||
name: string;
|
||||
language: string;
|
||||
description: string;
|
||||
unit_price: number;
|
||||
unit_time: string;
|
||||
@ -2155,7 +2156,6 @@ declare namespace API {
|
||||
speed_limit: number;
|
||||
device_limit: number;
|
||||
quota: number;
|
||||
group_id: number;
|
||||
nodes: number[];
|
||||
node_tags: string[];
|
||||
show: boolean;
|
||||
|
||||
2
apps/admin/services/common/typings.d.ts
vendored
2
apps/admin/services/common/typings.d.ts
vendored
@ -731,6 +731,7 @@ declare namespace API {
|
||||
type Subscribe = {
|
||||
id: number;
|
||||
name: string;
|
||||
language: string;
|
||||
description: string;
|
||||
unit_price: number;
|
||||
unit_time: string;
|
||||
@ -741,7 +742,6 @@ declare namespace API {
|
||||
speed_limit: number;
|
||||
device_limit: number;
|
||||
quota: number;
|
||||
group_id: number;
|
||||
nodes: number[];
|
||||
node_tags: string[];
|
||||
show: boolean;
|
||||
|
||||
@ -1,15 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { Display } from '@/components/display';
|
||||
import { querySubscribeGroupList, querySubscribeList } from '@/services/user/subscribe';
|
||||
import { querySubscribeList } from '@/services/user/subscribe';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { Card, CardContent, CardFooter, CardHeader } from '@workspace/ui/components/card';
|
||||
import { Separator } from '@workspace/ui/components/separator';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { cn } from '@workspace/ui/lib/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useLocale, useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Empty } from '@/components/empty';
|
||||
@ -18,125 +17,102 @@ import Purchase from '@/components/subscribe/purchase';
|
||||
|
||||
export default function Page() {
|
||||
const t = useTranslations('subscribe');
|
||||
const locale = useLocale();
|
||||
const [subscribe, setSubscribe] = useState<API.Subscribe>();
|
||||
|
||||
const [group, setGroup] = useState<string>('');
|
||||
|
||||
const { data: groups } = useQuery({
|
||||
queryKey: ['querySubscribeGroupList'],
|
||||
queryFn: async () => {
|
||||
const { data } = await querySubscribeGroupList();
|
||||
return data.data?.list || [];
|
||||
},
|
||||
});
|
||||
|
||||
const { data } = useQuery({
|
||||
queryKey: ['querySubscribeList'],
|
||||
queryKey: ['querySubscribeList', locale],
|
||||
queryFn: async () => {
|
||||
const { data } = await querySubscribeList();
|
||||
console.log('Fetching subscription list...');
|
||||
const { data } = await querySubscribeList({ language: locale });
|
||||
return data.data?.list || [];
|
||||
},
|
||||
});
|
||||
|
||||
const filteredData = data?.filter((item) => item.show);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs value={group} onValueChange={setGroup} className='space-y-4'>
|
||||
{groups && groups.length > 0 && (
|
||||
<>
|
||||
<h1 className='text-muted-foreground w-full'>{t('category')}</h1>
|
||||
<TabsList>
|
||||
<TabsTrigger value=''>{t('all')}</TabsTrigger>
|
||||
{groups.map((group) => (
|
||||
<TabsTrigger key={group.id} value={String(group.id)}>
|
||||
{group.name}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
<h2 className='text-muted-foreground w-full'>{t('products')}</h2>
|
||||
</>
|
||||
)}
|
||||
<div className='space-y-4'>
|
||||
<div className='grid gap-4 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3'>
|
||||
{data
|
||||
?.filter((item) => item.show)
|
||||
?.filter((item) => (group ? item.group_id === Number(group) : true))
|
||||
?.map((item) => (
|
||||
<Card className='flex flex-col' key={item.id}>
|
||||
<CardHeader className='bg-muted/50 text-xl font-medium'>{item.name}</CardHeader>
|
||||
<CardContent className='flex flex-grow flex-col gap-3 p-6 *:!text-sm'>
|
||||
{/* <div className='font-semibold'>{t('productDescription')}</div> */}
|
||||
<ul className='flex flex-grow flex-col gap-3'>
|
||||
{(() => {
|
||||
let parsedDescription;
|
||||
try {
|
||||
parsedDescription = JSON.parse(item.description);
|
||||
} catch {
|
||||
parsedDescription = { description: '', features: [] };
|
||||
}
|
||||
{filteredData?.map((item) => (
|
||||
<Card className='flex flex-col' key={item.id}>
|
||||
<CardHeader className='bg-muted/50 text-xl font-medium'>{item.name}</CardHeader>
|
||||
<CardContent className='flex flex-grow flex-col gap-3 p-6 *:!text-sm'>
|
||||
{/* <div className='font-semibold'>{t('productDescription')}</div> */}
|
||||
<ul className='flex flex-grow flex-col gap-3'>
|
||||
{(() => {
|
||||
let parsedDescription;
|
||||
try {
|
||||
parsedDescription = JSON.parse(item.description);
|
||||
} catch {
|
||||
parsedDescription = { description: '', features: [] };
|
||||
}
|
||||
|
||||
const { description, features } = parsedDescription;
|
||||
return (
|
||||
<>
|
||||
{description && <li className='text-muted-foreground'>{description}</li>}
|
||||
{features?.map(
|
||||
(
|
||||
feature: {
|
||||
icon: string;
|
||||
label: string;
|
||||
type: 'default' | 'success' | 'destructive';
|
||||
},
|
||||
index: number,
|
||||
) => (
|
||||
<li
|
||||
className={cn('flex items-center gap-1', {
|
||||
'text-muted-foreground line-through':
|
||||
feature.type === 'destructive',
|
||||
})}
|
||||
key={index}
|
||||
>
|
||||
{feature.icon && (
|
||||
<Icon
|
||||
icon={feature.icon}
|
||||
className={cn('text-primary size-5', {
|
||||
'text-green-500': feature.type === 'success',
|
||||
'text-destructive': feature.type === 'destructive',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{feature.label}
|
||||
</li>
|
||||
),
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</ul>
|
||||
<SubscribeDetail
|
||||
subscribe={{
|
||||
...item,
|
||||
name: undefined,
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
<Separator />
|
||||
<CardFooter className='relative mt-2 flex flex-col gap-2'>
|
||||
<h2 className='pb-5 text-2xl font-semibold sm:text-3xl'>
|
||||
<Display type='currency' value={item.unit_price} />
|
||||
<span className='text-base font-medium'>/{t(item.unit_time || 'Month')}</span>
|
||||
</h2>
|
||||
<Button
|
||||
className='absolute bottom-0 w-full rounded-b-xl rounded-t-none'
|
||||
onClick={() => {
|
||||
setSubscribe(item);
|
||||
}}
|
||||
>
|
||||
{t('buy')}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
const { description, features } = parsedDescription;
|
||||
return (
|
||||
<>
|
||||
{description && <li className='text-muted-foreground'>{description}</li>}
|
||||
{features?.map(
|
||||
(
|
||||
feature: {
|
||||
icon: string;
|
||||
label: string;
|
||||
type: 'default' | 'success' | 'destructive';
|
||||
},
|
||||
index: number,
|
||||
) => (
|
||||
<li
|
||||
className={cn('flex items-center gap-1', {
|
||||
'text-muted-foreground line-through':
|
||||
feature.type === 'destructive',
|
||||
})}
|
||||
key={index}
|
||||
>
|
||||
{feature.icon && (
|
||||
<Icon
|
||||
icon={feature.icon}
|
||||
className={cn('text-primary size-5', {
|
||||
'text-green-500': feature.type === 'success',
|
||||
'text-destructive': feature.type === 'destructive',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{feature.label}
|
||||
</li>
|
||||
),
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</ul>
|
||||
<SubscribeDetail
|
||||
subscribe={{
|
||||
...item,
|
||||
name: undefined,
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
<Separator />
|
||||
<CardFooter className='relative mt-2 flex flex-col gap-2'>
|
||||
<h2 className='pb-5 text-2xl font-semibold sm:text-3xl'>
|
||||
<Display type='currency' value={item.unit_price} />
|
||||
<span className='text-base font-medium'>/{t(item.unit_time || 'Month')}</span>
|
||||
</h2>
|
||||
<Button
|
||||
className='absolute bottom-0 w-full rounded-b-xl rounded-t-none'
|
||||
onClick={() => {
|
||||
setSubscribe(item);
|
||||
}}
|
||||
>
|
||||
{t('buy')}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
{data?.length === 0 && <Empty />}
|
||||
</Tabs>
|
||||
{filteredData?.length === 0 && <Empty />}
|
||||
</div>
|
||||
<Purchase subscribe={subscribe} setSubscribe={setSubscribe} />
|
||||
</>
|
||||
);
|
||||
|
||||
2
apps/user/services/common/typings.d.ts
vendored
2
apps/user/services/common/typings.d.ts
vendored
@ -731,6 +731,7 @@ declare namespace API {
|
||||
type Subscribe = {
|
||||
id: number;
|
||||
name: string;
|
||||
language: string;
|
||||
description: string;
|
||||
unit_price: number;
|
||||
unit_time: string;
|
||||
@ -741,7 +742,6 @@ declare namespace API {
|
||||
speed_limit: number;
|
||||
device_limit: number;
|
||||
quota: number;
|
||||
group_id: number;
|
||||
nodes: number[];
|
||||
node_tags: string[];
|
||||
show: boolean;
|
||||
|
||||
@ -2,23 +2,19 @@
|
||||
/* eslint-disable */
|
||||
import request from '@/utils/request';
|
||||
|
||||
/** Get subscribe group list GET /v1/public/subscribe/group/list */
|
||||
export async function querySubscribeGroupList(options?: { [key: string]: any }) {
|
||||
return request<API.Response & { data?: API.QuerySubscribeGroupListResponse }>(
|
||||
'/v1/public/subscribe/group/list',
|
||||
{
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** Get subscribe list GET /v1/public/subscribe/list */
|
||||
export async function querySubscribeList(options?: { [key: string]: any }) {
|
||||
export async function querySubscribeList(
|
||||
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
|
||||
params: API.QuerySubscribeListParams,
|
||||
options?: { [key: string]: any },
|
||||
) {
|
||||
return request<API.Response & { data?: API.QuerySubscribeListResponse }>(
|
||||
'/v1/public/subscribe/list',
|
||||
{
|
||||
method: 'GET',
|
||||
params: {
|
||||
...params,
|
||||
},
|
||||
...(options || {}),
|
||||
},
|
||||
);
|
||||
|
||||
10
apps/user/services/user/typings.d.ts
vendored
10
apps/user/services/user/typings.d.ts
vendored
@ -642,6 +642,14 @@ declare namespace API {
|
||||
total: number;
|
||||
};
|
||||
|
||||
type QuerySubscribeListParams = {
|
||||
language: string;
|
||||
};
|
||||
|
||||
type QuerySubscribeListRequest = {
|
||||
language: string;
|
||||
};
|
||||
|
||||
type QuerySubscribeListResponse = {
|
||||
list: Subscribe[];
|
||||
total: number;
|
||||
@ -821,6 +829,7 @@ declare namespace API {
|
||||
type Subscribe = {
|
||||
id: number;
|
||||
name: string;
|
||||
language: string;
|
||||
description: string;
|
||||
unit_price: number;
|
||||
unit_time: string;
|
||||
@ -831,7 +840,6 @@ declare namespace API {
|
||||
speed_limit: number;
|
||||
device_limit: number;
|
||||
quota: number;
|
||||
group_id: number;
|
||||
nodes: number[];
|
||||
node_tags: string[];
|
||||
show: boolean;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user