diff --git a/apps/admin/app/dashboard/settings/version/page.tsx b/apps/admin/app/dashboard/settings/version/page.tsx new file mode 100644 index 0000000..0077d77 --- /dev/null +++ b/apps/admin/app/dashboard/settings/version/page.tsx @@ -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; + +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(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({ + 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 ( +
+
+

{t('version.title')}

+ +
+ +
+ + + + ID + {t('version.platform')} + {t('version.versionNumber')} + {t('version.url')} + {t('version.force')} + {t('version.default')} + {t('version.actions')} + + + + {data?.list?.map((version: API.ApplicationVersion) => ( + + {version.id} + {version.platform} + {version.version} + + {version.url} + + {version.force_update ? t('version.yes') : t('version.no')} + {version.is_default ? t('version.yes') : t('version.no')} + +
+ + +
+
+
+ ))} + {!data?.list?.length && ( + + + {t('version.noResults')} + + + )} +
+
+
+ +
+
+ {t('version.total', { count: data?.total || 0 })} +
+
+ + + {t('version.page', { page })} + +
+
+ + + + + + {editingVersion ? t('version.edit') : t('version.createVersion')} + + +
+ +
+ ( + + {t('version.platform')} + + + + )} + /> + ( + + {t('version.versionNumber')} + + + + + + )} + /> +
+
+ ( + + {t('version.minVersion')} + + + + + + )} + /> + ( + + {t('version.downloadUrl')} + + + + + + )} + /> +
+ ( + + {t('version.descriptionField')} + + + + + + )} + /> +
+ ( + +
+ {t('version.forceUpdate')} +
+ + + +
+ )} + /> + ( + +
+ {t('version.default')} +
+ + + +
+ )} + /> + ( + +
+ {t('version.inReview')} +
+ + + +
+ )} + /> +
+ + + + + +
+
+
+ ); +} diff --git a/apps/admin/app/dashboard/system/basic-settings/currency-form.tsx b/apps/admin/app/dashboard/system/basic-settings/currency-form.tsx index ef84720..1046026 100644 --- a/apps/admin/app/dashboard/system/basic-settings/currency-form.tsx +++ b/apps/admin/app/dashboard/system/basic-settings/currency-form.tsx @@ -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; @@ -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() { )} /> + + ( + + {t('currency.fixedRate')} + + field.onChange(Number(val))} + /> + + {t('currency.fixedRateDescription')} + + + )} + /> diff --git a/apps/admin/config/navs.ts b/apps/admin/config/navs.ts index aaf318e..120b2f0 100644 --- a/apps/admin/config/navs.ts +++ b/apps/admin/config/navs.ts @@ -77,6 +77,11 @@ export const navs = [ icon: 'flat-color-icons:currency-exchange', }, { title: 'ADS Config', url: '/dashboard/ads', icon: 'flat-color-icons:electrical-sensor' }, + { + title: 'Version Management', + url: '/dashboard/settings/version', + icon: 'flat-color-icons:kindle', + }, ], }, diff --git a/apps/admin/locales/en-US/menu.json b/apps/admin/locales/en-US/menu.json index 44eb70b..4f84a04 100644 --- a/apps/admin/locales/en-US/menu.json +++ b/apps/admin/locales/en-US/menu.json @@ -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" + "Users & Support": "Users & Support", + "Version Management": "Version Management" } diff --git a/apps/admin/locales/en-US/system.json b/apps/admin/locales/en-US/system.json index c841414..145703c 100644 --- a/apps/admin/locales/en-US/system.json +++ b/apps/admin/locales/en-US/system.json @@ -18,7 +18,10 @@ "currencySymbolPlaceholder": "$", "currencyUnit": "Currency Unit", "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": { "title": "Invitation Settings", @@ -135,5 +138,35 @@ "inputPlaceholder": "Please enter", "saveSuccess": "Save Successful", "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?" } } diff --git a/apps/admin/locales/zh-CN/menu.json b/apps/admin/locales/zh-CN/menu.json index 472cd7d..6feef5b 100644 --- a/apps/admin/locales/zh-CN/menu.json +++ b/apps/admin/locales/zh-CN/menu.json @@ -1,16 +1,13 @@ { "ADS Config": "广告配置", "Announcement Management": "公告管理", - "Auth Control": "认证控制", "Balance": "余额变动", "Commerce": "商务", "Commission": "佣金记录", "Coupon Management": "优惠券管理", "Dashboard": "仪表盘", - "Document Management": "文档管理", - "Email": "邮件日志", "Gift": "赠送记录", "Login": "登录日志", @@ -23,7 +20,6 @@ "Order Management": "订单管理", "Payment Config": "支付配置", "Product Management": "商品管理", - "Register": "注册日志", "Reset Subscribe": "重置订阅", "Server Management": "服务器管理", @@ -34,10 +30,10 @@ "System": "系统", "System Config": "系统配置", "System Tool": "系统工具", - "Ticket Management": "工单管理", "Traffic Details": "流量明细", "User Detail": "用户详情", "User Management": "用户管理", - "Users & Support": "用户与支持" + "Users & Support": "用户与支持", + "Version Management": "版本管理" } diff --git a/apps/admin/locales/zh-CN/system.json b/apps/admin/locales/zh-CN/system.json index b0523a9..4825252 100644 --- a/apps/admin/locales/zh-CN/system.json +++ b/apps/admin/locales/zh-CN/system.json @@ -18,7 +18,10 @@ "currencySymbolPlaceholder": "$", "currencyUnit": "货币单位", "currencyUnitDescription": "仅用于展示使用,更改后系统中所有的货币单位都将发生变更", - "currencyUnitPlaceholder": "USD" + "currencyUnitPlaceholder": "USD", + "fixedRate": "固定汇率", + "fixedRatePlaceholder": "0", + "fixedRateDescription": "如果设置了固定汇率,将使用此值而非API获取的汇率" }, "invite": { "title": "邀请设置", @@ -137,5 +140,35 @@ "inputPlaceholder": "请输入", "saveSuccess": "保存成功", "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": "确定要删除吗?" } } diff --git a/apps/admin/services/admin/application.ts b/apps/admin/services/admin/application.ts index 0ff2011..30acbbc 100644 --- a/apps/admin/services/admin/application.ts +++ b/apps/admin/services/admin/application.ts @@ -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( + '/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( + '/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('/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( + '/v1/admin/application/version/list', + { + method: 'GET', + params: { + ...params, + }, + ...(options || {}), + }, + ); +} diff --git a/apps/admin/services/admin/typings.d.ts b/apps/admin/services/admin/typings.d.ts index a76eeb4..8e594e9 100644 --- a/apps/admin/services/admin/typings.d.ts +++ b/apps/admin/services/admin/typings.d.ts @@ -65,10 +65,53 @@ declare namespace API { type ApplicationVersion = { id: number; + platform: string; url: string; version: string; - description: string; + min_version?: string; + force_update: boolean; + description: Record; 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 = { @@ -398,6 +441,7 @@ declare namespace API { access_key: string; currency_unit: string; currency_symbol: string; + fixed_rate?: number; }; type DeleteAdsRequest = {