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:
parent
c14d739360
commit
63f539b382
@ -1071,6 +1071,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
|||||||
EnableCCHSigning: updatedSettings.EnableCCHSigning,
|
EnableCCHSigning: updatedSettings.EnableCCHSigning,
|
||||||
BalanceLowNotifyEnabled: updatedSettings.BalanceLowNotifyEnabled,
|
BalanceLowNotifyEnabled: updatedSettings.BalanceLowNotifyEnabled,
|
||||||
BalanceLowNotifyThreshold: updatedSettings.BalanceLowNotifyThreshold,
|
BalanceLowNotifyThreshold: updatedSettings.BalanceLowNotifyThreshold,
|
||||||
|
BalanceLowNotifyRechargeURL: updatedSettings.BalanceLowNotifyRechargeURL,
|
||||||
AccountQuotaNotifyEnabled: updatedSettings.AccountQuotaNotifyEnabled,
|
AccountQuotaNotifyEnabled: updatedSettings.AccountQuotaNotifyEnabled,
|
||||||
AccountQuotaNotifyEmails: dto.NotifyEmailEntriesFromService(updatedSettings.AccountQuotaNotifyEmails),
|
AccountQuotaNotifyEmails: dto.NotifyEmailEntriesFromService(updatedSettings.AccountQuotaNotifyEmails),
|
||||||
PaymentEnabled: updatedPaymentCfg.Enabled,
|
PaymentEnabled: updatedPaymentCfg.Enabled,
|
||||||
|
|||||||
@ -522,7 +522,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
|||||||
|
|
||||||
for {
|
for {
|
||||||
// 选择支持该模型的账号
|
// 选择支持该模型的账号
|
||||||
selection, err := h.gatewayService.SelectAccountWithLoadAwareness(c.Request.Context(), currentAPIKey.GroupID, sessionKey, reqModel, fs.FailedAccountIDs, parsedReq.MetadataUserID, int64(0))
|
selection, err := h.gatewayService.SelectAccountWithLoadAwareness(c.Request.Context(), currentAPIKey.GroupID, sessionKey, reqModel, fs.FailedAccountIDs, parsedReq.MetadataUserID, subject.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if len(fs.FailedAccountIDs) == 0 {
|
if len(fs.FailedAccountIDs) == 0 {
|
||||||
h.handleStreamingAwareError(c, http.StatusServiceUnavailable, "api_error", "No available accounts: "+err.Error(), streamStarted)
|
h.handleStreamingAwareError(c, http.StatusServiceUnavailable, "api_error", "No available accounts: "+err.Error(), streamStarted)
|
||||||
|
|||||||
@ -4575,14 +4575,9 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
|
|||||||
|
|
||||||
// 计算账号统计定价费用(使用最终上游模型匹配自定义规则)
|
// 计算账号统计定价费用(使用最终上游模型匹配自定义规则)
|
||||||
if apiKey.GroupID != nil {
|
if apiKey.GroupID != nil {
|
||||||
statsModel := result.UpstreamModel
|
applyAccountStatsCost(ctx, usageLog, s.channelService, s.billingService,
|
||||||
if statsModel == "" {
|
account.ID, *apiKey.GroupID, result.UpstreamModel, result.Model,
|
||||||
statsModel = result.Model
|
tokens, cost.TotalCost,
|
||||||
}
|
|
||||||
usageLog.AccountStatsCost = resolveAccountStatsCost(
|
|
||||||
ctx, s.channelService, s.billingService,
|
|
||||||
account.ID, *apiKey.GroupID, statsModel,
|
|
||||||
tokens, 1, cost.TotalCost,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,8 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -32,11 +34,12 @@ type PublicSettingsProvider interface {
|
|||||||
|
|
||||||
// FrontendServer serves the embedded frontend with settings injection
|
// FrontendServer serves the embedded frontend with settings injection
|
||||||
type FrontendServer struct {
|
type FrontendServer struct {
|
||||||
distFS fs.FS
|
distFS fs.FS
|
||||||
fileServer http.Handler
|
fileServer http.Handler
|
||||||
baseHTML []byte
|
baseHTML []byte
|
||||||
cache *HTMLCache
|
cache *HTMLCache
|
||||||
settings PublicSettingsProvider
|
settings PublicSettingsProvider
|
||||||
|
overrideDir string // local file override directory
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFrontendServer creates a new frontend server with settings injection
|
// NewFrontendServer creates a new frontend server with settings injection
|
||||||
@ -62,11 +65,12 @@ func NewFrontendServer(settingsProvider PublicSettingsProvider) (*FrontendServer
|
|||||||
cache.SetBaseHTML(baseHTML)
|
cache.SetBaseHTML(baseHTML)
|
||||||
|
|
||||||
return &FrontendServer{
|
return &FrontendServer{
|
||||||
distFS: distFS,
|
distFS: distFS,
|
||||||
fileServer: http.FileServer(http.FS(distFS)),
|
fileServer: http.FileServer(http.FS(distFS)),
|
||||||
baseHTML: baseHTML,
|
baseHTML: baseHTML,
|
||||||
cache: cache,
|
cache: cache,
|
||||||
settings: settingsProvider,
|
settings: settingsProvider,
|
||||||
|
overrideDir: filepath.Join("data", "public"),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,6 +103,11 @@ func (s *FrontendServer) Middleware() gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try local override first
|
||||||
|
if s.tryServeOverride(c, cleanPath) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Serve static files normally
|
// Serve static files normally
|
||||||
s.fileServer.ServeHTTP(c.Writer, c.Request)
|
s.fileServer.ServeHTTP(c.Writer, c.Request)
|
||||||
c.Abort()
|
c.Abort()
|
||||||
@ -114,6 +123,22 @@ func (s *FrontendServer) fileExists(path string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tryServeOverride checks if a local override file exists and serves it.
|
||||||
|
// Files in overrideDir take precedence over embedded files.
|
||||||
|
func (s *FrontendServer) tryServeOverride(c *gin.Context, cleanPath string) bool {
|
||||||
|
if s.overrideDir == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
filePath := filepath.Join(s.overrideDir, filepath.Clean("/"+cleanPath))
|
||||||
|
info, err := os.Stat(filePath)
|
||||||
|
if err != nil || info.IsDir() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
c.File(filePath)
|
||||||
|
c.Abort()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (s *FrontendServer) serveIndexHTML(c *gin.Context) {
|
func (s *FrontendServer) serveIndexHTML(c *gin.Context) {
|
||||||
// Get nonce from context (generated by SecurityHeaders middleware)
|
// Get nonce from context (generated by SecurityHeaders middleware)
|
||||||
nonce := middleware.GetNonceFromContext(c)
|
nonce := middleware.GetNonceFromContext(c)
|
||||||
@ -226,6 +251,7 @@ func ServeEmbeddedFrontend() gin.HandlerFunc {
|
|||||||
panic("failed to get dist subdirectory: " + err.Error())
|
panic("failed to get dist subdirectory: " + err.Error())
|
||||||
}
|
}
|
||||||
fileServer := http.FileServer(http.FS(distFS))
|
fileServer := http.FileServer(http.FS(distFS))
|
||||||
|
overrideDir := filepath.Join("data", "public")
|
||||||
|
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
path := c.Request.URL.Path
|
path := c.Request.URL.Path
|
||||||
@ -242,6 +268,10 @@ func ServeEmbeddedFrontend() gin.HandlerFunc {
|
|||||||
|
|
||||||
if file, err := distFS.Open(cleanPath); err == nil {
|
if file, err := distFS.Open(cleanPath); err == nil {
|
||||||
_ = file.Close()
|
_ = file.Close()
|
||||||
|
// Try local override first
|
||||||
|
if tryServeOverrideFile(c, overrideDir, cleanPath) {
|
||||||
|
return
|
||||||
|
}
|
||||||
fileServer.ServeHTTP(c.Writer, c.Request)
|
fileServer.ServeHTTP(c.Writer, c.Request)
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
@ -251,6 +281,21 @@ func ServeEmbeddedFrontend() gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tryServeOverrideFile is a standalone version of tryServeOverride for legacy usage.
|
||||||
|
func tryServeOverrideFile(c *gin.Context, overrideDir, cleanPath string) bool {
|
||||||
|
if overrideDir == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
filePath := filepath.Join(overrideDir, filepath.Clean("/"+cleanPath))
|
||||||
|
info, err := os.Stat(filePath)
|
||||||
|
if err != nil || info.IsDir() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
c.File(filePath)
|
||||||
|
c.Abort()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func shouldBypassEmbeddedFrontend(path string) bool {
|
func shouldBypassEmbeddedFrontend(path string) bool {
|
||||||
trimmed := strings.TrimSpace(path)
|
trimmed := strings.TrimSpace(path)
|
||||||
return strings.HasPrefix(trimmed, "/api/") ||
|
return strings.HasPrefix(trimmed, "/api/") ||
|
||||||
|
|||||||
@ -270,9 +270,9 @@ apiClient.interceptors.response.use(
|
|||||||
return Promise.reject({
|
return Promise.reject({
|
||||||
status,
|
status,
|
||||||
code: apiData.code,
|
code: apiData.code,
|
||||||
|
reason: apiData.reason,
|
||||||
error: apiData.error,
|
error: apiData.error,
|
||||||
message: apiData.message || apiData.detail || error.message,
|
message: apiData.message || apiData.detail || error.message,
|
||||||
reason: apiData.reason,
|
|
||||||
metadata: apiData.metadata,
|
metadata: apiData.metadata,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
width="wide"
|
width="wide"
|
||||||
@close="handleClose"
|
@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 -->
|
<!-- Info -->
|
||||||
<div class="rounded-lg bg-blue-50 p-4 dark:bg-blue-900/20">
|
<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">
|
<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)
|
replaceSelectedSet(next)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const batchUpdate = (updater: (draft: Set<number>) => void) => {
|
||||||
|
const draft = new Set(selectedSet.value)
|
||||||
|
updater(draft)
|
||||||
|
replaceSelectedSet(draft)
|
||||||
|
}
|
||||||
|
|
||||||
const selectVisible = () => {
|
const selectVisible = () => {
|
||||||
toggleVisible(true)
|
toggleVisible(true)
|
||||||
}
|
}
|
||||||
@ -93,6 +99,7 @@ export function useTableSelection<T>({ rows, getId }: UseTableSelectionOptions<T
|
|||||||
clear,
|
clear,
|
||||||
removeMany,
|
removeMany,
|
||||||
toggleVisible,
|
toggleVisible,
|
||||||
selectVisible
|
selectVisible,
|
||||||
|
batchUpdate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -247,6 +247,8 @@ export default {
|
|||||||
loading: 'Loading...',
|
loading: 'Loading...',
|
||||||
justNow: 'just now',
|
justNow: 'just now',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
|
saved: 'Saved successfully',
|
||||||
|
deleted: 'Deleted successfully',
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
edit: 'Edit',
|
edit: 'Edit',
|
||||||
@ -304,6 +306,7 @@ export default {
|
|||||||
saving: 'Saving...',
|
saving: 'Saving...',
|
||||||
selectedCount: '({count} selected)',
|
selectedCount: '({count} selected)',
|
||||||
refresh: 'Refresh',
|
refresh: 'Refresh',
|
||||||
|
view: 'View',
|
||||||
settings: 'Settings',
|
settings: 'Settings',
|
||||||
chooseFile: 'Choose File',
|
chooseFile: 'Choose File',
|
||||||
notAvailable: 'N/A',
|
notAvailable: 'N/A',
|
||||||
@ -5487,6 +5490,7 @@ export default {
|
|||||||
refundSuccess: 'Refund successful',
|
refundSuccess: 'Refund successful',
|
||||||
refundInfo: 'Refund Info',
|
refundInfo: 'Refund Info',
|
||||||
refundEnabled: 'Refund Enabled',
|
refundEnabled: 'Refund Enabled',
|
||||||
|
allowUserRefund: 'Allow User Refund',
|
||||||
alreadyRefunded: 'Already Refunded',
|
alreadyRefunded: 'Already Refunded',
|
||||||
deductBalance: 'Deduct Balance',
|
deductBalance: 'Deduct Balance',
|
||||||
deductBalanceHint: 'Subtract recharged amount from user balance',
|
deductBalanceHint: 'Subtract recharged amount from user balance',
|
||||||
@ -5556,6 +5560,9 @@ export default {
|
|||||||
tabPlanConfig: 'Plan Configuration',
|
tabPlanConfig: 'Plan Configuration',
|
||||||
tabUserSubs: 'User Subscriptions',
|
tabUserSubs: 'User Subscriptions',
|
||||||
selectGroup: 'Select a group',
|
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',
|
groupMissing: 'Missing',
|
||||||
groupInfo: 'Group Info',
|
groupInfo: 'Group Info',
|
||||||
platform: 'Platform',
|
platform: 'Platform',
|
||||||
|
|||||||
@ -247,6 +247,8 @@ export default {
|
|||||||
loading: '加载中...',
|
loading: '加载中...',
|
||||||
justNow: '刚刚',
|
justNow: '刚刚',
|
||||||
save: '保存',
|
save: '保存',
|
||||||
|
saved: '保存成功',
|
||||||
|
deleted: '删除成功',
|
||||||
cancel: '取消',
|
cancel: '取消',
|
||||||
delete: '删除',
|
delete: '删除',
|
||||||
edit: '编辑',
|
edit: '编辑',
|
||||||
@ -304,6 +306,7 @@ export default {
|
|||||||
saving: '保存中...',
|
saving: '保存中...',
|
||||||
selectedCount: '(已选 {count} 个)',
|
selectedCount: '(已选 {count} 个)',
|
||||||
refresh: '刷新',
|
refresh: '刷新',
|
||||||
|
view: '查看',
|
||||||
settings: '设置',
|
settings: '设置',
|
||||||
chooseFile: '选择文件',
|
chooseFile: '选择文件',
|
||||||
notAvailable: '不可用',
|
notAvailable: '不可用',
|
||||||
@ -5744,6 +5747,9 @@ export default {
|
|||||||
tabPlanConfig: '套餐配置',
|
tabPlanConfig: '套餐配置',
|
||||||
tabUserSubs: '用户订阅',
|
tabUserSubs: '用户订阅',
|
||||||
selectGroup: '请选择分组',
|
selectGroup: '请选择分组',
|
||||||
|
groupRequired: '请选择订阅分组',
|
||||||
|
priceRequired: '价格必须大于 0',
|
||||||
|
validityDaysRequired: '有效期天数必须大于 0',
|
||||||
groupMissing: '缺失',
|
groupMissing: '缺失',
|
||||||
groupInfo: '分组信息',
|
groupInfo: '分组信息',
|
||||||
platform: '平台',
|
platform: '平台',
|
||||||
|
|||||||
@ -66,7 +66,7 @@ export const usePaymentStore = defineStore('payment', () => {
|
|||||||
return response.data
|
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> {
|
async function pollOrderStatus(orderId: number): Promise<PaymentOrder | null> {
|
||||||
try {
|
try {
|
||||||
const response = await paymentAPI.getOrder(orderId)
|
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" />
|
<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">
|
<div ref="accountTableRef" class="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||||
<DataTable
|
<DataTable
|
||||||
|
ref="dataTableRef"
|
||||||
:columns="cols"
|
:columns="cols"
|
||||||
:data="accounts"
|
:data="accounts"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@ -153,6 +154,8 @@
|
|||||||
default-sort-key="name"
|
default-sort-key="name"
|
||||||
default-sort-order="asc"
|
default-sort-order="asc"
|
||||||
:sort-storage-key="ACCOUNT_SORT_STORAGE_KEY"
|
:sort-storage-key="ACCOUNT_SORT_STORAGE_KEY"
|
||||||
|
:estimate-row-height="72"
|
||||||
|
:overscan="5"
|
||||||
>
|
>
|
||||||
<template #header-select>
|
<template #header-select>
|
||||||
<input
|
<input
|
||||||
@ -164,7 +167,7 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #cell-select="{ row }">
|
<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>
|
||||||
<template #cell-name="{ row, value }">
|
<template #cell-name="{ row, value }">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
@ -197,7 +200,9 @@
|
|||||||
<AccountCapacityCell :account="row" />
|
<AccountCapacityCell :account="row" />
|
||||||
</template>
|
</template>
|
||||||
<template #cell-status="{ row }">
|
<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>
|
||||||
<template #cell-schedulable="{ row }">
|
<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')">
|
<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 { useAuthStore } from '@/stores/auth'
|
||||||
import { adminAPI } from '@/api/admin'
|
import { adminAPI } from '@/api/admin'
|
||||||
import { useTableLoader } from '@/composables/useTableLoader'
|
import { useTableLoader } from '@/composables/useTableLoader'
|
||||||
import { useSwipeSelect } from '@/composables/useSwipeSelect'
|
import { useSwipeSelect, type SwipeSelectVirtualContext } from '@/composables/useSwipeSelect'
|
||||||
import { useTableSelection } from '@/composables/useTableSelection'
|
import { useTableSelection } from '@/composables/useTableSelection'
|
||||||
import AppLayout from '@/components/layout/AppLayout.vue'
|
import AppLayout from '@/components/layout/AppLayout.vue'
|
||||||
import TablePageLayout from '@/components/layout/TablePageLayout.vue'
|
import TablePageLayout from '@/components/layout/TablePageLayout.vue'
|
||||||
@ -351,6 +356,7 @@ const authStore = useAuthStore()
|
|||||||
const proxies = ref<AccountProxy[]>([])
|
const proxies = ref<AccountProxy[]>([])
|
||||||
const groups = ref<AdminGroup[]>([])
|
const groups = ref<AdminGroup[]>([])
|
||||||
const accountTableRef = ref<HTMLElement | null>(null)
|
const accountTableRef = ref<HTMLElement | null>(null)
|
||||||
|
const dataTableRef = ref<InstanceType<typeof DataTable> | null>(null)
|
||||||
const selPlatforms = computed<AccountPlatform[]>(() => {
|
const selPlatforms = computed<AccountPlatform[]>(() => {
|
||||||
const platforms = new Set(
|
const platforms = new Set(
|
||||||
accounts.value
|
accounts.value
|
||||||
@ -650,17 +656,25 @@ const {
|
|||||||
clear: clearSelection,
|
clear: clearSelection,
|
||||||
removeMany: removeSelectedAccounts,
|
removeMany: removeSelectedAccounts,
|
||||||
toggleVisible,
|
toggleVisible,
|
||||||
selectVisible: selectPage
|
selectVisible: selectPage,
|
||||||
|
batchUpdate
|
||||||
} = useTableSelection<Account>({
|
} = useTableSelection<Account>({
|
||||||
rows: accounts,
|
rows: accounts,
|
||||||
getId: (account) => account.id
|
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, {
|
useSwipeSelect(accountTableRef, {
|
||||||
isSelected,
|
isSelected,
|
||||||
select,
|
select,
|
||||||
deselect
|
deselect,
|
||||||
})
|
batchUpdate
|
||||||
|
}, swipeVirtualContext)
|
||||||
|
|
||||||
const resetAutoRefreshCache = () => {
|
const resetAutoRefreshCache = () => {
|
||||||
autoRefreshETag.value = null
|
autoRefreshETag.value = null
|
||||||
|
|||||||
@ -985,7 +985,8 @@ const {
|
|||||||
deselect,
|
deselect,
|
||||||
clear: clearSelectedProxies,
|
clear: clearSelectedProxies,
|
||||||
removeMany: removeSelectedProxies,
|
removeMany: removeSelectedProxies,
|
||||||
toggleVisible
|
toggleVisible,
|
||||||
|
batchUpdate
|
||||||
} = useTableSelection<Proxy>({
|
} = useTableSelection<Proxy>({
|
||||||
rows: proxies,
|
rows: proxies,
|
||||||
getId: (proxy) => proxy.id
|
getId: (proxy) => proxy.id
|
||||||
@ -993,7 +994,8 @@ const {
|
|||||||
useSwipeSelect(proxyTableRef, {
|
useSwipeSelect(proxyTableRef, {
|
||||||
isSelected,
|
isSelected,
|
||||||
select,
|
select,
|
||||||
deselect
|
deselect,
|
||||||
|
batchUpdate
|
||||||
})
|
})
|
||||||
const accountsProxy = ref<Proxy | null>(null)
|
const accountsProxy = ref<Proxy | null>(null)
|
||||||
const proxyAccounts = ref<ProxyAccountSummary[]>([])
|
const proxyAccounts = ref<ProxyAccountSummary[]>([])
|
||||||
|
|||||||
@ -4116,12 +4116,13 @@ async function handleToggleField(provider: ProviderInstance, field: 'enabled' |
|
|||||||
if (field === 'enabled') newValue = !provider.enabled
|
if (field === 'enabled') newValue = !provider.enabled
|
||||||
else if (field === 'refund_enabled') newValue = !provider.refund_enabled
|
else if (field === 'refund_enabled') newValue = !provider.refund_enabled
|
||||||
else newValue = !provider.allow_user_refund
|
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 {
|
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)
|
await adminAPI.payment.updateProvider(provider.id, payload)
|
||||||
if (field === 'enabled') provider.enabled = newValue
|
if (field === 'enabled') provider.enabled = newValue
|
||||||
else if (field === 'refund_enabled') {
|
else if (field === 'refund_enabled') {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user