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 @@
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+
+
+
+
{{ hint }}
+
{{ error }}
+
+
+
+
+
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 @@
-
+ item.icon_svg = v"
+ />
@@ -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 },
- );
- }
-}