fix: merge general improvements from release branch
Backend: - gateway_handler: pass subject.UserID instead of int64(0) for user-level routing - setting_handler: add missing BalanceLowNotifyRechargeURL to UpdateSettings response - openai_gateway_service: use applyAccountStatsCost for account stats pricing integration - embed_on: add local file override (data/public/) for embedded frontend assets Frontend: - useTableSelection: add batchUpdate method for batch operations - AccountsView: virtual scrolling params, Set-based isSelected, swipe virtualization - ProxiesView: add batchUpdate to selection and swipe-select - BulkEditAccountModal: fix submit handler to prevent event object as argument - SettingsView: move payload construction outside try block - i18n: add general translation keys (saved, deleted, view, validation, allowUserRefund) - api/client: reorder error fields for consistency - stores/payment: clarify pollOrderStatus JSDoc
This commit is contained in:
@@ -270,9 +270,9 @@ apiClient.interceptors.response.use(
|
||||
return Promise.reject({
|
||||
status,
|
||||
code: apiData.code,
|
||||
reason: apiData.reason,
|
||||
error: apiData.error,
|
||||
message: apiData.message || apiData.detail || error.message,
|
||||
reason: apiData.reason,
|
||||
metadata: apiData.metadata,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
width="wide"
|
||||
@close="handleClose"
|
||||
>
|
||||
<form id="bulk-edit-account-form" class="space-y-5" @submit.prevent="handleSubmit">
|
||||
<form id="bulk-edit-account-form" class="space-y-5" @submit.prevent="() => handleSubmit()">
|
||||
<!-- Info -->
|
||||
<div class="rounded-lg bg-blue-50 p-4 dark:bg-blue-900/20">
|
||||
<p class="text-sm text-blue-700 dark:text-blue-400">
|
||||
|
||||
@@ -76,6 +76,12 @@ export function useTableSelection<T>({ rows, getId }: UseTableSelectionOptions<T
|
||||
replaceSelectedSet(next)
|
||||
}
|
||||
|
||||
const batchUpdate = (updater: (draft: Set<number>) => void) => {
|
||||
const draft = new Set(selectedSet.value)
|
||||
updater(draft)
|
||||
replaceSelectedSet(draft)
|
||||
}
|
||||
|
||||
const selectVisible = () => {
|
||||
toggleVisible(true)
|
||||
}
|
||||
@@ -93,6 +99,7 @@ export function useTableSelection<T>({ rows, getId }: UseTableSelectionOptions<T
|
||||
clear,
|
||||
removeMany,
|
||||
toggleVisible,
|
||||
selectVisible
|
||||
selectVisible,
|
||||
batchUpdate
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,6 +247,8 @@ export default {
|
||||
loading: 'Loading...',
|
||||
justNow: 'just now',
|
||||
save: 'Save',
|
||||
saved: 'Saved successfully',
|
||||
deleted: 'Deleted successfully',
|
||||
cancel: 'Cancel',
|
||||
delete: 'Delete',
|
||||
edit: 'Edit',
|
||||
@@ -304,6 +306,7 @@ export default {
|
||||
saving: 'Saving...',
|
||||
selectedCount: '({count} selected)',
|
||||
refresh: 'Refresh',
|
||||
view: 'View',
|
||||
settings: 'Settings',
|
||||
chooseFile: 'Choose File',
|
||||
notAvailable: 'N/A',
|
||||
@@ -5487,6 +5490,7 @@ export default {
|
||||
refundSuccess: 'Refund successful',
|
||||
refundInfo: 'Refund Info',
|
||||
refundEnabled: 'Refund Enabled',
|
||||
allowUserRefund: 'Allow User Refund',
|
||||
alreadyRefunded: 'Already Refunded',
|
||||
deductBalance: 'Deduct Balance',
|
||||
deductBalanceHint: 'Subtract recharged amount from user balance',
|
||||
@@ -5556,6 +5560,9 @@ export default {
|
||||
tabPlanConfig: 'Plan Configuration',
|
||||
tabUserSubs: 'User Subscriptions',
|
||||
selectGroup: 'Select a group',
|
||||
groupRequired: 'Please select a subscription group',
|
||||
priceRequired: 'Price must be greater than 0',
|
||||
validityDaysRequired: 'Validity days must be greater than 0',
|
||||
groupMissing: 'Missing',
|
||||
groupInfo: 'Group Info',
|
||||
platform: 'Platform',
|
||||
|
||||
@@ -247,6 +247,8 @@ export default {
|
||||
loading: '加载中...',
|
||||
justNow: '刚刚',
|
||||
save: '保存',
|
||||
saved: '保存成功',
|
||||
deleted: '删除成功',
|
||||
cancel: '取消',
|
||||
delete: '删除',
|
||||
edit: '编辑',
|
||||
@@ -304,6 +306,7 @@ export default {
|
||||
saving: '保存中...',
|
||||
selectedCount: '(已选 {count} 个)',
|
||||
refresh: '刷新',
|
||||
view: '查看',
|
||||
settings: '设置',
|
||||
chooseFile: '选择文件',
|
||||
notAvailable: '不可用',
|
||||
@@ -5744,6 +5747,9 @@ export default {
|
||||
tabPlanConfig: '套餐配置',
|
||||
tabUserSubs: '用户订阅',
|
||||
selectGroup: '请选择分组',
|
||||
groupRequired: '请选择订阅分组',
|
||||
priceRequired: '价格必须大于 0',
|
||||
validityDaysRequired: '有效期天数必须大于 0',
|
||||
groupMissing: '缺失',
|
||||
groupInfo: '分组信息',
|
||||
platform: '平台',
|
||||
|
||||
@@ -66,7 +66,7 @@ export const usePaymentStore = defineStore('payment', () => {
|
||||
return response.data
|
||||
}
|
||||
|
||||
/** Poll order status by ID */
|
||||
/** Poll order status by ID (read-only, no upstream check) */
|
||||
async function pollOrderStatus(orderId: number): Promise<PaymentOrder | null> {
|
||||
try {
|
||||
const response = await paymentAPI.getOrder(orderId)
|
||||
|
||||
@@ -144,6 +144,7 @@
|
||||
<AccountBulkActionsBar :selected-ids="selIds" @delete="handleBulkDelete" @reset-status="handleBulkResetStatus" @refresh-token="handleBulkRefreshToken" @edit="showBulkEdit = true" @clear="clearSelection" @select-page="selectPage" @toggle-schedulable="handleBulkToggleSchedulable" />
|
||||
<div ref="accountTableRef" class="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<DataTable
|
||||
ref="dataTableRef"
|
||||
:columns="cols"
|
||||
:data="accounts"
|
||||
:loading="loading"
|
||||
@@ -153,6 +154,8 @@
|
||||
default-sort-key="name"
|
||||
default-sort-order="asc"
|
||||
:sort-storage-key="ACCOUNT_SORT_STORAGE_KEY"
|
||||
:estimate-row-height="72"
|
||||
:overscan="5"
|
||||
>
|
||||
<template #header-select>
|
||||
<input
|
||||
@@ -164,7 +167,7 @@
|
||||
/>
|
||||
</template>
|
||||
<template #cell-select="{ row }">
|
||||
<input type="checkbox" :checked="selIds.includes(row.id)" @change="toggleSel(row.id)" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500" />
|
||||
<input type="checkbox" :checked="isSelected(row.id)" @change="toggleSel(row.id)" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500" />
|
||||
</template>
|
||||
<template #cell-name="{ row, value }">
|
||||
<div class="flex flex-col">
|
||||
@@ -197,7 +200,9 @@
|
||||
<AccountCapacityCell :account="row" />
|
||||
</template>
|
||||
<template #cell-status="{ row }">
|
||||
<AccountStatusIndicator :account="row" @show-temp-unsched="handleShowTempUnsched" />
|
||||
<div class="flex items-center gap-1.5">
|
||||
<AccountStatusIndicator :account="row" @show-temp-unsched="handleShowTempUnsched" />
|
||||
</div>
|
||||
</template>
|
||||
<template #cell-schedulable="{ row }">
|
||||
<button @click="handleToggleSchedulable(row)" :disabled="togglingSchedulable === row.id" class="relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:focus:ring-offset-dark-800" :class="[row.schedulable ? 'bg-primary-500 hover:bg-primary-600' : 'bg-gray-200 hover:bg-gray-300 dark:bg-dark-600 dark:hover:bg-dark-500']" :title="row.schedulable ? t('admin.accounts.schedulableEnabled') : t('admin.accounts.schedulableDisabled')">
|
||||
@@ -313,7 +318,7 @@ import { useAppStore } from '@/stores/app'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import { useTableLoader } from '@/composables/useTableLoader'
|
||||
import { useSwipeSelect } from '@/composables/useSwipeSelect'
|
||||
import { useSwipeSelect, type SwipeSelectVirtualContext } from '@/composables/useSwipeSelect'
|
||||
import { useTableSelection } from '@/composables/useTableSelection'
|
||||
import AppLayout from '@/components/layout/AppLayout.vue'
|
||||
import TablePageLayout from '@/components/layout/TablePageLayout.vue'
|
||||
@@ -351,6 +356,7 @@ const authStore = useAuthStore()
|
||||
const proxies = ref<AccountProxy[]>([])
|
||||
const groups = ref<AdminGroup[]>([])
|
||||
const accountTableRef = ref<HTMLElement | null>(null)
|
||||
const dataTableRef = ref<InstanceType<typeof DataTable> | null>(null)
|
||||
const selPlatforms = computed<AccountPlatform[]>(() => {
|
||||
const platforms = new Set(
|
||||
accounts.value
|
||||
@@ -650,17 +656,25 @@ const {
|
||||
clear: clearSelection,
|
||||
removeMany: removeSelectedAccounts,
|
||||
toggleVisible,
|
||||
selectVisible: selectPage
|
||||
selectVisible: selectPage,
|
||||
batchUpdate
|
||||
} = useTableSelection<Account>({
|
||||
rows: accounts,
|
||||
getId: (account) => account.id
|
||||
})
|
||||
|
||||
const swipeVirtualContext: SwipeSelectVirtualContext = {
|
||||
getVirtualizer: () => dataTableRef.value?.virtualizer ?? null,
|
||||
getSortedData: () => dataTableRef.value?.sortedData ?? accounts.value,
|
||||
getRowId: (row: any) => row.id,
|
||||
}
|
||||
|
||||
useSwipeSelect(accountTableRef, {
|
||||
isSelected,
|
||||
select,
|
||||
deselect
|
||||
})
|
||||
deselect,
|
||||
batchUpdate
|
||||
}, swipeVirtualContext)
|
||||
|
||||
const resetAutoRefreshCache = () => {
|
||||
autoRefreshETag.value = null
|
||||
|
||||
@@ -985,7 +985,8 @@ const {
|
||||
deselect,
|
||||
clear: clearSelectedProxies,
|
||||
removeMany: removeSelectedProxies,
|
||||
toggleVisible
|
||||
toggleVisible,
|
||||
batchUpdate
|
||||
} = useTableSelection<Proxy>({
|
||||
rows: proxies,
|
||||
getId: (proxy) => proxy.id
|
||||
@@ -993,7 +994,8 @@ const {
|
||||
useSwipeSelect(proxyTableRef, {
|
||||
isSelected,
|
||||
select,
|
||||
deselect
|
||||
deselect,
|
||||
batchUpdate
|
||||
})
|
||||
const accountsProxy = ref<Proxy | null>(null)
|
||||
const proxyAccounts = ref<ProxyAccountSummary[]>([])
|
||||
|
||||
@@ -4116,12 +4116,13 @@ async function handleToggleField(provider: ProviderInstance, field: 'enabled' |
|
||||
if (field === 'enabled') newValue = !provider.enabled
|
||||
else if (field === 'refund_enabled') newValue = !provider.refund_enabled
|
||||
else newValue = !provider.allow_user_refund
|
||||
|
||||
const payload: Record<string, boolean> = { [field]: newValue }
|
||||
// Cascade: turning off refund_enabled also turns off allow_user_refund
|
||||
if (field === 'refund_enabled' && !newValue) {
|
||||
payload.allow_user_refund = false
|
||||
}
|
||||
try {
|
||||
const payload: Record<string, boolean> = { [field]: newValue }
|
||||
// Cascade: turning off refund_enabled also disables allow_user_refund
|
||||
if (field === 'refund_enabled' && !newValue) {
|
||||
payload.allow_user_refund = false
|
||||
}
|
||||
await adminAPI.payment.updateProvider(provider.id, payload)
|
||||
if (field === 'enabled') provider.enabled = newValue
|
||||
else if (field === 'refund_enabled') {
|
||||
|
||||
Reference in New Issue
Block a user