mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-13 22:46:18 +00:00
fix(payment): resolve PR audit issues
- Add payment navigation to AppSidebar (user orders + admin payment menu group with collapse) - Add 5 missing nav i18n keys (myOrders, orderManagement, paymentDashboard, paymentConfig, paymentPlans) - Renumber payment migrations 090-100 → 092-102 to avoid conflict with upstream 090/091 - Remove non-payment sora_client_enabled change, restore upstream purchase_subscription fields - Remove extra 'data' from SettingsTab type union
This commit is contained in:
1 parent
63d1860dc0
commit
e1547d7835
14 files changed
+182
-33
No files matched your search
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -38,7 +38,8 @@ export interface SystemSettings {
|
|||||||
doc_url: string
|
doc_url: string
|
||||||
home_content: string
|
home_content: string
|
||||||
hide_ccs_import_button: boolean
|
hide_ccs_import_button: boolean
|
||||||
sora_client_enabled: boolean
|
purchase_subscription_enabled: boolean
|
||||||
|
purchase_subscription_url: string
|
||||||
backend_mode_enabled: boolean
|
backend_mode_enabled: boolean
|
||||||
custom_menu_items: CustomMenuItem[]
|
custom_menu_items: CustomMenuItem[]
|
||||||
custom_endpoints: CustomEndpoint[]
|
custom_endpoints: CustomEndpoint[]
|
||||||
@@ -155,6 +156,8 @@ export interface UpdateSettingsRequest {
|
|||||||
doc_url?: string
|
doc_url?: string
|
||||||
home_content?: string
|
home_content?: string
|
||||||
hide_ccs_import_button?: boolean
|
hide_ccs_import_button?: boolean
|
||||||
|
purchase_subscription_enabled?: boolean
|
||||||
|
purchase_subscription_url?: string
|
||||||
backend_mode_enabled?: boolean
|
backend_mode_enabled?: boolean
|
||||||
custom_menu_items?: CustomMenuItem[]
|
custom_menu_items?: CustomMenuItem[]
|
||||||
custom_endpoints?: CustomEndpoint[]
|
custom_endpoints?: CustomEndpoint[]
|
||||||
|
|||||||
@@ -29,30 +29,64 @@
|
|||||||
<template v-if="isAdmin">
|
<template v-if="isAdmin">
|
||||||
<!-- Admin Section -->
|
<!-- Admin Section -->
|
||||||
<div class="sidebar-section">
|
<div class="sidebar-section">
|
||||||
<router-link
|
<template v-for="item in adminNavItems" :key="item.path">
|
||||||
v-for="item in adminNavItems"
|
<!-- Collapsible group (has children) -->
|
||||||
:key="item.path"
|
<template v-if="item.children?.length">
|
||||||
:to="item.path"
|
<button
|
||||||
class="sidebar-link mb-1"
|
type="button"
|
||||||
:class="{ 'sidebar-link-active': isActive(item.path) }"
|
class="sidebar-link mb-1 w-full"
|
||||||
:title="sidebarCollapsed ? item.label : undefined"
|
:class="{ 'sidebar-link-active': isGroupActive(item) && !isGroupExpanded(item) }"
|
||||||
:id="
|
:title="sidebarCollapsed ? item.label : undefined"
|
||||||
item.path === '/admin/accounts'
|
@click="sidebarCollapsed ? undefined : toggleGroup(item)"
|
||||||
? 'sidebar-channel-manage'
|
>
|
||||||
: item.path === '/admin/groups'
|
<component :is="item.icon" class="h-5 w-5 flex-shrink-0" />
|
||||||
? 'sidebar-group-manage'
|
<transition name="fade">
|
||||||
: item.path === '/admin/redeem'
|
<span v-if="!sidebarCollapsed" class="flex flex-1 items-center justify-between">
|
||||||
? 'sidebar-wallet'
|
<span>{{ item.label }}</span>
|
||||||
: undefined
|
<ChevronDownIcon class="h-4 w-4 flex-shrink-0 transition-transform duration-200" :class="isGroupExpanded(item) ? 'rotate-180' : ''" />
|
||||||
"
|
</span>
|
||||||
@click="handleMenuItemClick(item.path)"
|
</transition>
|
||||||
>
|
</button>
|
||||||
<span v-if="item.iconSvg" class="h-5 w-5 flex-shrink-0 sidebar-svg-icon" v-html="sanitizeSvg(item.iconSvg)"></span>
|
<!-- Children -->
|
||||||
<component v-else :is="item.icon" class="h-5 w-5 flex-shrink-0" />
|
<div v-if="!sidebarCollapsed && isGroupExpanded(item)" class="mb-1 ml-4 border-l border-gray-200 pl-2 dark:border-dark-600">
|
||||||
<transition name="fade">
|
<router-link
|
||||||
<span v-if="!sidebarCollapsed">{{ item.label }}</span>
|
v-for="child in item.children"
|
||||||
</transition>
|
:key="child.path"
|
||||||
</router-link>
|
:to="child.path"
|
||||||
|
class="sidebar-link mb-0.5 py-1.5 text-sm"
|
||||||
|
:class="{ 'sidebar-link-active': route.path === child.path }"
|
||||||
|
@click="handleMenuItemClick(child.path)"
|
||||||
|
>
|
||||||
|
<component :is="child.icon" class="h-4 w-4 flex-shrink-0" />
|
||||||
|
<span>{{ child.label }}</span>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<!-- Normal item (no children) -->
|
||||||
|
<router-link
|
||||||
|
v-else
|
||||||
|
:to="item.path"
|
||||||
|
class="sidebar-link mb-1"
|
||||||
|
:class="{ 'sidebar-link-active': isActive(item.path) }"
|
||||||
|
:title="sidebarCollapsed ? item.label : undefined"
|
||||||
|
:id="
|
||||||
|
item.path === '/admin/accounts'
|
||||||
|
? 'sidebar-channel-manage'
|
||||||
|
: item.path === '/admin/groups'
|
||||||
|
? 'sidebar-group-manage'
|
||||||
|
: item.path === '/admin/redeem'
|
||||||
|
? 'sidebar-wallet'
|
||||||
|
: undefined
|
||||||
|
"
|
||||||
|
@click="handleMenuItemClick(item.path)"
|
||||||
|
>
|
||||||
|
<span v-if="item.iconSvg" class="h-5 w-5 flex-shrink-0 sidebar-svg-icon" v-html="sanitizeSvg(item.iconSvg)"></span>
|
||||||
|
<component v-else :is="item.icon" class="h-5 w-5 flex-shrink-0" />
|
||||||
|
<transition name="fade">
|
||||||
|
<span v-if="!sidebarCollapsed">{{ item.label }}</span>
|
||||||
|
</transition>
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Personal Section for Admin (hidden in simple mode) -->
|
<!-- Personal Section for Admin (hidden in simple mode) -->
|
||||||
@@ -160,6 +194,7 @@ interface NavItem {
|
|||||||
icon: unknown
|
icon: unknown
|
||||||
iconSvg?: string
|
iconSvg?: string
|
||||||
hideInSimpleMode?: boolean
|
hideInSimpleMode?: boolean
|
||||||
|
children?: NavItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -175,6 +210,9 @@ const mobileOpen = computed(() => appStore.mobileOpen)
|
|||||||
const isAdmin = computed(() => authStore.isAdmin)
|
const isAdmin = computed(() => authStore.isAdmin)
|
||||||
const isDark = ref(document.documentElement.classList.contains('dark'))
|
const isDark = ref(document.documentElement.classList.contains('dark'))
|
||||||
|
|
||||||
|
// Track which parent nav groups are expanded
|
||||||
|
const expandedGroups = ref<Set<string>>(new Set())
|
||||||
|
|
||||||
// Site settings from appStore (cached, no flicker)
|
// Site settings from appStore (cached, no flicker)
|
||||||
const siteName = computed(() => appStore.siteName)
|
const siteName = computed(() => appStore.siteName)
|
||||||
const siteLogo = computed(() => appStore.siteLogo)
|
const siteLogo = computed(() => appStore.siteLogo)
|
||||||
@@ -467,6 +505,36 @@ const ChevronDoubleLeftIcon = {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const OrderIcon = {
|
||||||
|
render: () =>
|
||||||
|
h(
|
||||||
|
'svg',
|
||||||
|
{ fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', 'stroke-width': '1.5' },
|
||||||
|
[
|
||||||
|
h('path', {
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round',
|
||||||
|
d: 'M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15a2.25 2.25 0 012.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z'
|
||||||
|
})
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const OrderListIcon = {
|
||||||
|
render: () =>
|
||||||
|
h(
|
||||||
|
'svg',
|
||||||
|
{ fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', 'stroke-width': '1.5' },
|
||||||
|
[
|
||||||
|
h('path', {
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round',
|
||||||
|
d: 'M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z'
|
||||||
|
})
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const ChevronDoubleRightIcon = {
|
const ChevronDoubleRightIcon = {
|
||||||
render: () =>
|
render: () =>
|
||||||
h(
|
h(
|
||||||
@@ -482,6 +550,21 @@ const ChevronDoubleRightIcon = {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ChevronDownIcon = {
|
||||||
|
render: () =>
|
||||||
|
h(
|
||||||
|
'svg',
|
||||||
|
{ fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', 'stroke-width': '1.5' },
|
||||||
|
[
|
||||||
|
h('path', {
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round',
|
||||||
|
d: 'm19.5 8.25-7.5 7.5-7.5-7.5'
|
||||||
|
})
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// User navigation items (for regular users)
|
// User navigation items (for regular users)
|
||||||
const userNavItems = computed((): NavItem[] => {
|
const userNavItems = computed((): NavItem[] => {
|
||||||
const items: NavItem[] = [
|
const items: NavItem[] = [
|
||||||
@@ -489,14 +572,24 @@ const userNavItems = computed((): NavItem[] => {
|
|||||||
{ path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon },
|
{ path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon },
|
||||||
{ path: '/usage', label: t('nav.usage'), icon: ChartIcon, hideInSimpleMode: true },
|
{ path: '/usage', label: t('nav.usage'), icon: ChartIcon, hideInSimpleMode: true },
|
||||||
{ path: '/subscriptions', label: t('nav.mySubscriptions'), icon: CreditCardIcon, hideInSimpleMode: true },
|
{ path: '/subscriptions', label: t('nav.mySubscriptions'), icon: CreditCardIcon, hideInSimpleMode: true },
|
||||||
...(appStore.cachedPublicSettings?.purchase_subscription_enabled
|
...(appStore.cachedPublicSettings?.payment_enabled
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
path: '/purchase',
|
path: '/purchase',
|
||||||
label: t('nav.buySubscription'),
|
label: t('nav.buySubscription'),
|
||||||
icon: RechargeSubscriptionIcon,
|
icon: RechargeSubscriptionIcon,
|
||||||
hideInSimpleMode: true
|
hideInSimpleMode: true
|
||||||
}
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(appStore.cachedPublicSettings?.payment_enabled
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
path: '/orders',
|
||||||
|
label: t('nav.myOrders'),
|
||||||
|
icon: OrderListIcon,
|
||||||
|
hideInSimpleMode: true
|
||||||
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
{ path: '/redeem', label: t('nav.redeem'), icon: GiftIcon, hideInSimpleMode: true },
|
{ path: '/redeem', label: t('nav.redeem'), icon: GiftIcon, hideInSimpleMode: true },
|
||||||
@@ -517,14 +610,24 @@ const personalNavItems = computed((): NavItem[] => {
|
|||||||
{ path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon },
|
{ path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon },
|
||||||
{ path: '/usage', label: t('nav.usage'), icon: ChartIcon, hideInSimpleMode: true },
|
{ path: '/usage', label: t('nav.usage'), icon: ChartIcon, hideInSimpleMode: true },
|
||||||
{ path: '/subscriptions', label: t('nav.mySubscriptions'), icon: CreditCardIcon, hideInSimpleMode: true },
|
{ path: '/subscriptions', label: t('nav.mySubscriptions'), icon: CreditCardIcon, hideInSimpleMode: true },
|
||||||
...(appStore.cachedPublicSettings?.purchase_subscription_enabled
|
...(appStore.cachedPublicSettings?.payment_enabled
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
path: '/purchase',
|
path: '/purchase',
|
||||||
label: t('nav.buySubscription'),
|
label: t('nav.buySubscription'),
|
||||||
icon: RechargeSubscriptionIcon,
|
icon: RechargeSubscriptionIcon,
|
||||||
hideInSimpleMode: true
|
hideInSimpleMode: true
|
||||||
}
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(appStore.cachedPublicSettings?.payment_enabled
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
path: '/orders',
|
||||||
|
label: t('nav.myOrders'),
|
||||||
|
icon: OrderListIcon,
|
||||||
|
hideInSimpleMode: true
|
||||||
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
{ path: '/redeem', label: t('nav.redeem'), icon: GiftIcon, hideInSimpleMode: true },
|
{ path: '/redeem', label: t('nav.redeem'), icon: GiftIcon, hideInSimpleMode: true },
|
||||||
@@ -569,6 +672,21 @@ const adminNavItems = computed((): NavItem[] => {
|
|||||||
{ path: '/admin/proxies', label: t('nav.proxies'), icon: ServerIcon },
|
{ path: '/admin/proxies', label: t('nav.proxies'), icon: ServerIcon },
|
||||||
{ path: '/admin/redeem', label: t('nav.redeemCodes'), icon: TicketIcon, hideInSimpleMode: true },
|
{ path: '/admin/redeem', label: t('nav.redeemCodes'), icon: TicketIcon, hideInSimpleMode: true },
|
||||||
{ path: '/admin/promo-codes', label: t('nav.promoCodes'), icon: GiftIcon, hideInSimpleMode: true },
|
{ path: '/admin/promo-codes', label: t('nav.promoCodes'), icon: GiftIcon, hideInSimpleMode: true },
|
||||||
|
...(adminSettingsStore.paymentEnabled
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
path: '/admin/orders',
|
||||||
|
label: t('nav.orderManagement'),
|
||||||
|
icon: OrderIcon,
|
||||||
|
hideInSimpleMode: true,
|
||||||
|
children: [
|
||||||
|
{ path: '/admin/orders/dashboard', label: t('nav.paymentDashboard'), icon: ChartIcon },
|
||||||
|
{ path: '/admin/orders', label: t('nav.orderManagement'), icon: OrderIcon },
|
||||||
|
{ path: '/admin/orders/plans', label: t('nav.paymentPlans'), icon: CreditCardIcon },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{ path: '/admin/usage', label: t('nav.usage'), icon: ChartIcon }
|
{ path: '/admin/usage', label: t('nav.usage'), icon: ChartIcon }
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -630,6 +748,23 @@ function isActive(path: string): boolean {
|
|||||||
return route.path === path || route.path.startsWith(path + '/')
|
return route.path === path || route.path.startsWith(path + '/')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isGroupActive(item: NavItem): boolean {
|
||||||
|
if (!item.children) return false
|
||||||
|
return item.children.some(child => route.path === child.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isGroupExpanded(item: NavItem): boolean {
|
||||||
|
return expandedGroups.value.has(item.path) || isGroupActive(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleGroup(item: NavItem) {
|
||||||
|
if (expandedGroups.value.has(item.path)) {
|
||||||
|
expandedGroups.value.delete(item.path)
|
||||||
|
} else {
|
||||||
|
expandedGroups.value.add(item.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize theme
|
// Initialize theme
|
||||||
const savedTheme = localStorage.getItem('theme')
|
const savedTheme = localStorage.getItem('theme')
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -353,7 +353,12 @@ export default {
|
|||||||
mySubscriptions: 'My Subscriptions',
|
mySubscriptions: 'My Subscriptions',
|
||||||
buySubscription: 'Recharge / Subscription',
|
buySubscription: 'Recharge / Subscription',
|
||||||
docs: 'Docs',
|
docs: 'Docs',
|
||||||
sora: 'Sora Studio'
|
sora: 'Sora Studio',
|
||||||
|
myOrders: 'My Orders',
|
||||||
|
orderManagement: 'Orders',
|
||||||
|
paymentDashboard: 'Payment Dashboard',
|
||||||
|
paymentConfig: 'Payment Config',
|
||||||
|
paymentPlans: 'Plans'
|
||||||
},
|
},
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
|
|||||||
@@ -353,7 +353,12 @@ export default {
|
|||||||
mySubscriptions: '我的订阅',
|
mySubscriptions: '我的订阅',
|
||||||
buySubscription: '充值/订阅',
|
buySubscription: '充值/订阅',
|
||||||
docs: '文档',
|
docs: '文档',
|
||||||
sora: 'Sora 创作'
|
sora: 'Sora 创作',
|
||||||
|
myOrders: '我的订单',
|
||||||
|
orderManagement: '订单管理',
|
||||||
|
paymentDashboard: '支付概览',
|
||||||
|
paymentConfig: '支付配置',
|
||||||
|
paymentPlans: '订阅套餐'
|
||||||
},
|
},
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
|
|||||||
@@ -2498,7 +2498,7 @@ const { t } = useI18n()
|
|||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
const adminSettingsStore = useAdminSettingsStore()
|
const adminSettingsStore = useAdminSettingsStore()
|
||||||
|
|
||||||
type SettingsTab = 'general' | 'security' | 'users' | 'gateway' | 'payment' | 'email' | 'backup' | 'data'
|
type SettingsTab = 'general' | 'security' | 'users' | 'gateway' | 'payment' | 'email' | 'backup'
|
||||||
const activeTab = ref<SettingsTab>('general')
|
const activeTab = ref<SettingsTab>('general')
|
||||||
const settingsTabs = [
|
const settingsTabs = [
|
||||||
{ key: 'general' as SettingsTab, icon: 'home' as const },
|
{ key: 'general' as SettingsTab, icon: 'home' as const },
|
||||||
@@ -2613,7 +2613,8 @@ const form = reactive<SettingsForm>({
|
|||||||
backend_mode_enabled: false,
|
backend_mode_enabled: false,
|
||||||
hide_ccs_import_button: false,
|
hide_ccs_import_button: false,
|
||||||
payment_enabled: false, payment_min_amount: 1, payment_max_amount: 10000, payment_daily_limit: 50000, payment_max_pending_orders: 3, payment_order_timeout_minutes: 30, payment_balance_disabled: false, payment_enabled_types: [], payment_help_image_url: '', payment_help_text: '', payment_product_name_prefix: '', payment_product_name_suffix: '', payment_load_balance_strategy: 'round-robin', payment_cancel_rate_limit_enabled: false, payment_cancel_rate_limit_max: 10, payment_cancel_rate_limit_window: 1, payment_cancel_rate_limit_unit: 'day', payment_cancel_rate_limit_window_mode: 'rolling',
|
payment_enabled: false, payment_min_amount: 1, payment_max_amount: 10000, payment_daily_limit: 50000, payment_max_pending_orders: 3, payment_order_timeout_minutes: 30, payment_balance_disabled: false, payment_enabled_types: [], payment_help_image_url: '', payment_help_text: '', payment_product_name_prefix: '', payment_product_name_suffix: '', payment_load_balance_strategy: 'round-robin', payment_cancel_rate_limit_enabled: false, payment_cancel_rate_limit_max: 10, payment_cancel_rate_limit_window: 1, payment_cancel_rate_limit_unit: 'day', payment_cancel_rate_limit_window_mode: 'rolling',
|
||||||
sora_client_enabled: false,
|
purchase_subscription_enabled: false,
|
||||||
|
purchase_subscription_url: '',
|
||||||
custom_menu_items: [] as Array<{id: string; label: string; icon_svg: string; url: string; visibility: 'user' | 'admin'; sort_order: number}>,
|
custom_menu_items: [] as Array<{id: string; label: string; icon_svg: string; url: string; visibility: 'user' | 'admin'; sort_order: number}>,
|
||||||
custom_endpoints: [] as Array<{name: string; endpoint: string; description: string}>,
|
custom_endpoints: [] as Array<{name: string; endpoint: string; description: string}>,
|
||||||
frontend_url: '',
|
frontend_url: '',
|
||||||
|
|||||||
Reference in new issue
Block a user