diff --git a/frontend/src/components/common/ImageUpload.vue b/frontend/src/components/common/ImageUpload.vue new file mode 100644 index 00000000..b77ab64e --- /dev/null +++ b/frontend/src/components/common/ImageUpload.vue @@ -0,0 +1,141 @@ + + + diff --git a/frontend/src/components/layout/AppHeader.vue b/frontend/src/components/layout/AppHeader.vue index a6b4030f..ffc7c5e2 100644 --- a/frontend/src/components/layout/AppHeader.vue +++ b/frontend/src/components/layout/AppHeader.vue @@ -254,6 +254,13 @@ const displayName = computed(() => { }) const pageTitle = computed(() => { + // For custom pages, use the menu item's label instead of generic "自定义页面" + if (route.name === 'CustomPage') { + const id = route.params.id as string + const items = appStore.cachedPublicSettings?.custom_menu_items ?? [] + const menuItem = items.find((item) => item.id === id) + if (menuItem?.label) return menuItem.label + } const titleKey = route.meta.titleKey as string if (titleKey) { return t(titleKey) diff --git a/frontend/src/components/layout/AppSidebar.vue b/frontend/src/components/layout/AppSidebar.vue index 5b5db67e..40b8c8de 100644 --- a/frontend/src/components/layout/AppSidebar.vue +++ b/frontend/src/components/layout/AppSidebar.vue @@ -526,15 +526,14 @@ const userNavItems = computed((): NavItem[] => { } ] : []), + { path: '/redeem', label: t('nav.redeem'), icon: GiftIcon, hideInSimpleMode: true }, + { path: '/profile', label: t('nav.profile'), icon: UserIcon }, ...customMenuItemsForUser.value.map((item): NavItem => ({ path: `/custom/${item.id}`, label: item.label, icon: null, iconSvg: item.icon_svg, - hideInSimpleMode: true, })), - { path: '/redeem', label: t('nav.redeem'), icon: GiftIcon, hideInSimpleMode: true }, - { path: '/profile', label: t('nav.profile'), icon: UserIcon } ] return authStore.isSimpleMode ? items.filter(item => !item.hideInSimpleMode) : items }) @@ -558,15 +557,14 @@ const personalNavItems = computed((): NavItem[] => { } ] : []), + { path: '/redeem', label: t('nav.redeem'), icon: GiftIcon, hideInSimpleMode: true }, + { path: '/profile', label: t('nav.profile'), icon: UserIcon }, ...customMenuItemsForUser.value.map((item): NavItem => ({ path: `/custom/${item.id}`, label: item.label, icon: null, iconSvg: item.icon_svg, - hideInSimpleMode: true, })), - { path: '/redeem', label: t('nav.redeem'), icon: GiftIcon, hideInSimpleMode: true }, - { path: '/profile', label: t('nav.profile'), icon: UserIcon } ] return authStore.isSimpleMode ? items.filter(item => !item.hideInSimpleMode) : items }) @@ -607,22 +605,22 @@ const adminNavItems = computed((): NavItem[] => { // 简单模式下,在系统设置前插入 API密钥 if (authStore.isSimpleMode) { const filtered = baseItems.filter(item => !item.hideInSimpleMode) - // Add admin custom menu items - for (const cm of customMenuItemsForAdmin.value) { - filtered.push({ path: `/custom/${cm.id}`, label: cm.label, icon: null, iconSvg: cm.icon_svg }) - } filtered.push({ path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon }) filtered.push({ path: '/admin/data-management', label: t('nav.dataManagement'), icon: DatabaseIcon }) filtered.push({ path: '/admin/settings', label: t('nav.settings'), icon: CogIcon }) + // Add admin custom menu items after settings + for (const cm of customMenuItemsForAdmin.value) { + filtered.push({ path: `/custom/${cm.id}`, label: cm.label, icon: null, iconSvg: cm.icon_svg }) + } return filtered } baseItems.push({ path: '/admin/data-management', label: t('nav.dataManagement'), icon: DatabaseIcon }) - // Add admin custom menu items before settings + baseItems.push({ path: '/admin/settings', label: t('nav.settings'), icon: CogIcon }) + // Add admin custom menu items after settings for (const cm of customMenuItemsForAdmin.value) { baseItems.push({ path: `/custom/${cm.id}`, label: cm.label, icon: null, iconSvg: cm.icon_svg }) } - baseItems.push({ path: '/admin/settings', label: t('nav.settings'), icon: CogIcon }) return baseItems }) diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 42cf9765..7357c3f1 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -3636,6 +3636,8 @@ export default { iconSvg: 'SVG Icon', iconSvgPlaceholder: '...', iconPreview: 'Icon Preview', + uploadSvg: 'Upload SVG', + removeSvg: 'Remove', visibility: 'Visible To', visibilityUser: 'Regular Users', visibilityAdmin: 'Administrators', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index a0632fd9..9f2fb639 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -3806,6 +3806,8 @@ export default { iconSvg: 'SVG 图标', iconSvgPlaceholder: '...', iconPreview: '图标预览', + uploadSvg: '上传 SVG', + removeSvg: '清除', visibility: '可见角色', visibilityUser: '普通用户', visibilityAdmin: '管理员', diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 142828cb..08f492d4 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -428,7 +428,20 @@ router.beforeEach((to, _from, next) => { // Set page title const appStore = useAppStore() - document.title = resolveDocumentTitle(to.meta.title, appStore.siteName, to.meta.titleKey as string) + // For custom pages, use menu item label as document title + if (to.name === 'CustomPage') { + const id = to.params.id as string + const items = appStore.cachedPublicSettings?.custom_menu_items ?? [] + const menuItem = items.find((item) => item.id === id) + if (menuItem?.label) { + const siteName = appStore.siteName || 'Sub2API' + document.title = `${menuItem.label} - ${siteName}` + } else { + document.title = resolveDocumentTitle(to.meta.title, appStore.siteName, to.meta.titleKey as string) + } + } else { + document.title = resolveDocumentTitle(to.meta.title, appStore.siteName, to.meta.titleKey as string) + } // Check if route requires authentication const requiresAuth = to.meta.requiresAuth !== false // Default to true diff --git a/frontend/src/views/admin/SettingsView.vue b/frontend/src/views/admin/SettingsView.vue index 02f7f449..3a42a5b7 100644 --- a/frontend/src/views/admin/SettingsView.vue +++ b/frontend/src/views/admin/SettingsView.vue @@ -832,64 +832,14 @@ -
- -
-
- Site Logo - - - -
-
- -
-
- - -
-

- {{ t('admin.settings.site.logoHint') }} -

-

{{ logoError }}

-
-
+ @@ -1257,22 +1207,14 @@ -
- - -
- -
-
+ @@ -1390,6 +1332,7 @@ import Select from '@/components/common/Select.vue' import GroupBadge from '@/components/common/GroupBadge.vue' import GroupOptionItem from '@/components/common/GroupOptionItem.vue' import Toggle from '@/components/common/Toggle.vue' +import ImageUpload from '@/components/common/ImageUpload.vue' import { useClipboard } from '@/composables/useClipboard' import { useAppStore } from '@/stores' @@ -1402,7 +1345,6 @@ const saving = ref(false) const testingSmtp = ref(false) const sendingTestEmail = ref(false) const testEmailAddress = ref('') -const logoError = ref('') // Admin API Key 状态 const adminApiKeyLoading = ref(true) @@ -1559,44 +1501,6 @@ function moveMenuItem(index: number, direction: -1 | 1) { }) } -function handleLogoUpload(event: Event) { - const input = event.target as HTMLInputElement - const file = input.files?.[0] - logoError.value = '' - - if (!file) return - - // Check file size (300KB = 307200 bytes) - const maxSize = 300 * 1024 - if (file.size > maxSize) { - logoError.value = t('admin.settings.site.logoSizeError', { - size: (file.size / 1024).toFixed(1) - }) - input.value = '' - return - } - - // Check file type - if (!file.type.startsWith('image/')) { - logoError.value = t('admin.settings.site.logoTypeError') - input.value = '' - return - } - - // Convert to base64 - const reader = new FileReader() - reader.onload = (e) => { - form.site_logo = e.target?.result as string - } - reader.onerror = () => { - logoError.value = t('admin.settings.site.logoReadError') - } - reader.readAsDataURL(file) - - // Reset input - input.value = '' -} - async function loadSettings() { loading.value = true try { diff --git a/tmp_api_admin_orders/[id]/cancel/route.ts b/tmp_api_admin_orders/[id]/cancel/route.ts deleted file mode 100644 index 0857b4e0..00000000 --- a/tmp_api_admin_orders/[id]/cancel/route.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { verifyAdminToken, unauthorizedResponse } from '@/lib/admin-auth'; -import { adminCancelOrder, OrderError } from '@/lib/order/service'; - -export async function POST( - request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -) { - if (!verifyAdminToken(request)) return unauthorizedResponse(); - - try { - const { id } = await params; - await adminCancelOrder(id); - return NextResponse.json({ success: true }); - } catch (error) { - if (error instanceof OrderError) { - return NextResponse.json( - { error: error.message, code: error.code }, - { status: error.statusCode }, - ); - } - console.error('Admin cancel order error:', error); - return NextResponse.json({ error: '取消订单失败' }, { status: 500 }); - } -} diff --git a/tmp_api_admin_orders/[id]/retry/route.ts b/tmp_api_admin_orders/[id]/retry/route.ts deleted file mode 100644 index 07a3c0d0..00000000 --- a/tmp_api_admin_orders/[id]/retry/route.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { verifyAdminToken, unauthorizedResponse } from '@/lib/admin-auth'; -import { retryRecharge, OrderError } from '@/lib/order/service'; - -export async function POST( - request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -) { - if (!verifyAdminToken(request)) return unauthorizedResponse(); - - try { - const { id } = await params; - await retryRecharge(id); - return NextResponse.json({ success: true }); - } catch (error) { - if (error instanceof OrderError) { - return NextResponse.json( - { error: error.message, code: error.code }, - { status: error.statusCode }, - ); - } - console.error('Retry recharge error:', error); - return NextResponse.json({ error: '重试充值失败' }, { status: 500 }); - } -} diff --git a/tmp_api_admin_orders/[id]/route.ts b/tmp_api_admin_orders/[id]/route.ts deleted file mode 100644 index 941ed839..00000000 --- a/tmp_api_admin_orders/[id]/route.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { prisma } from '@/lib/db'; -import { verifyAdminToken, unauthorizedResponse } from '@/lib/admin-auth'; - -export async function GET( - request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -) { - if (!verifyAdminToken(request)) return unauthorizedResponse(); - - const { id } = await params; - - const order = await prisma.order.findUnique({ - where: { id }, - include: { - auditLogs: { - orderBy: { createdAt: 'desc' }, - }, - }, - }); - - if (!order) { - return NextResponse.json({ error: '订单不存在' }, { status: 404 }); - } - - return NextResponse.json({ - ...order, - amount: Number(order.amount), - refundAmount: order.refundAmount ? Number(order.refundAmount) : null, - }); -} diff --git a/tmp_api_admin_orders/route.ts b/tmp_api_admin_orders/route.ts deleted file mode 100644 index 110560bf..00000000 --- a/tmp_api_admin_orders/route.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { prisma } from '@/lib/db'; -import { verifyAdminToken, unauthorizedResponse } from '@/lib/admin-auth'; -import { Prisma } from '@prisma/client'; - -export async function GET(request: NextRequest) { - if (!verifyAdminToken(request)) return unauthorizedResponse(); - - const searchParams = request.nextUrl.searchParams; - const page = Math.max(1, Number(searchParams.get('page') || '1')); - const pageSize = Math.min(100, Math.max(1, Number(searchParams.get('page_size') || '20'))); - const status = searchParams.get('status'); - const userId = searchParams.get('user_id'); - const dateFrom = searchParams.get('date_from'); - const dateTo = searchParams.get('date_to'); - - const where: Prisma.OrderWhereInput = {}; - if (status) where.status = status as any; - if (userId) where.userId = Number(userId); - if (dateFrom || dateTo) { - where.createdAt = {}; - if (dateFrom) where.createdAt.gte = new Date(dateFrom); - if (dateTo) where.createdAt.lte = new Date(dateTo); - } - - const [orders, total] = await Promise.all([ - prisma.order.findMany({ - where, - orderBy: { createdAt: 'desc' }, - skip: (page - 1) * pageSize, - take: pageSize, - select: { - id: true, - userId: true, - userName: true, - userEmail: true, - amount: true, - status: true, - paymentType: true, - createdAt: true, - paidAt: true, - completedAt: true, - failedReason: true, - expiresAt: true, - }, - }), - prisma.order.count({ where }), - ]); - - return NextResponse.json({ - orders: orders.map(o => ({ - ...o, - amount: Number(o.amount), - })), - total, - page, - page_size: pageSize, - total_pages: Math.ceil(total / pageSize), - }); -} diff --git a/tmp_api_orders/[id]/cancel/route.ts b/tmp_api_orders/[id]/cancel/route.ts deleted file mode 100644 index 4e0b0dc6..00000000 --- a/tmp_api_orders/[id]/cancel/route.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { z } from 'zod'; -import { cancelOrder, OrderError } from '@/lib/order/service'; - -const cancelSchema = z.object({ - user_id: z.number().int().positive(), -}); - -export async function POST( - request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -) { - try { - const { id } = await params; - const body = await request.json(); - const parsed = cancelSchema.safeParse(body); - - if (!parsed.success) { - return NextResponse.json( - { error: '参数错误', details: parsed.error.flatten().fieldErrors }, - { status: 400 }, - ); - } - - await cancelOrder(id, parsed.data.user_id); - return NextResponse.json({ success: true }); - } catch (error) { - if (error instanceof OrderError) { - return NextResponse.json( - { error: error.message, code: error.code }, - { status: error.statusCode }, - ); - } - console.error('Cancel order error:', error); - return NextResponse.json({ error: '取消订单失败' }, { status: 500 }); - } -} diff --git a/tmp_api_orders/[id]/route.ts b/tmp_api_orders/[id]/route.ts deleted file mode 100644 index 08448607..00000000 --- a/tmp_api_orders/[id]/route.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { prisma } from '@/lib/db'; - -export async function GET( - request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -) { - const { id } = await params; - - const order = await prisma.order.findUnique({ - where: { id }, - select: { - id: true, - userId: true, - userName: true, - amount: true, - status: true, - paymentType: true, - payUrl: true, - qrCode: true, - qrCodeImg: true, - expiresAt: true, - paidAt: true, - completedAt: true, - failedReason: true, - createdAt: true, - }, - }); - - if (!order) { - return NextResponse.json({ error: '订单不存在' }, { status: 404 }); - } - - return NextResponse.json({ - order_id: order.id, - user_id: order.userId, - user_name: order.userName, - amount: Number(order.amount), - status: order.status, - payment_type: order.paymentType, - pay_url: order.payUrl, - qr_code: order.qrCode, - qr_code_img: order.qrCodeImg, - expires_at: order.expiresAt, - paid_at: order.paidAt, - completed_at: order.completedAt, - failed_reason: order.failedReason, - created_at: order.createdAt, - }); -} diff --git a/tmp_api_orders/my/route.ts b/tmp_api_orders/my/route.ts deleted file mode 100644 index 43ca2f0a..00000000 --- a/tmp_api_orders/my/route.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { prisma } from '@/lib/db'; -import { getCurrentUserByToken } from '@/lib/sub2api/client'; - -export async function GET(request: NextRequest) { - const token = request.nextUrl.searchParams.get('token')?.trim(); - if (!token) { - return NextResponse.json({ error: 'token is required' }, { status: 400 }); - } - - try { - const user = await getCurrentUserByToken(token); - const orders = await prisma.order.findMany({ - where: { userId: user.id }, - orderBy: { createdAt: 'desc' }, - take: 20, - select: { - id: true, - amount: true, - status: true, - paymentType: true, - createdAt: true, - }, - }); - - return NextResponse.json({ - user: { - id: user.id, - username: user.username, - email: user.email, - displayName: user.username || user.email || `用户 #${user.id}`, - balance: user.balance, - }, - orders: orders.map((item) => ({ - id: item.id, - amount: Number(item.amount), - status: item.status, - paymentType: item.paymentType, - createdAt: item.createdAt, - })), - }); - } catch (error) { - console.error('Get my orders error:', error); - return NextResponse.json({ error: 'unauthorized' }, { status: 401 }); - } -} diff --git a/tmp_api_orders/route.ts b/tmp_api_orders/route.ts deleted file mode 100644 index 0fd93aa4..00000000 --- a/tmp_api_orders/route.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { z } from 'zod'; -import { createOrder, OrderError } from '@/lib/order/service'; -import { getEnv } from '@/lib/config'; - -const createOrderSchema = z.object({ - user_id: z.number().int().positive(), - amount: z.number().positive(), - payment_type: z.enum(['alipay', 'wxpay']), -}); - -export async function POST(request: NextRequest) { - try { - const env = getEnv(); - const body = await request.json(); - const parsed = createOrderSchema.safeParse(body); - - if (!parsed.success) { - return NextResponse.json( - { error: '参数错误', details: parsed.error.flatten().fieldErrors }, - { status: 400 }, - ); - } - - const { user_id, amount, payment_type } = parsed.data; - - // Validate amount range - if (amount < env.MIN_RECHARGE_AMOUNT || amount > env.MAX_RECHARGE_AMOUNT) { - return NextResponse.json( - { error: `充值金额需在 ${env.MIN_RECHARGE_AMOUNT} - ${env.MAX_RECHARGE_AMOUNT} 之间` }, - { status: 400 }, - ); - } - - // Validate payment type is enabled - if (!env.ENABLED_PAYMENT_TYPES.includes(payment_type)) { - return NextResponse.json( - { error: `不支持的支付方式: ${payment_type}` }, - { status: 400 }, - ); - } - - const clientIp = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() - || request.headers.get('x-real-ip') - || '127.0.0.1'; - - const result = await createOrder({ - userId: user_id, - amount, - paymentType: payment_type, - clientIp, - }); - - return NextResponse.json(result); - } catch (error) { - if (error instanceof OrderError) { - return NextResponse.json( - { error: error.message, code: error.code }, - { status: error.statusCode }, - ); - } - console.error('Create order error:', error); - return NextResponse.json( - { error: '创建订单失败,请稍后重试' }, - { status: 500 }, - ); - } -}