mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-02 08:31:35 +00:00
feat: complete batch image workflow
This commit is contained in:
@@ -20,7 +20,7 @@
|
||||
<button
|
||||
v-if="showCloseButton"
|
||||
@click="emit('close')"
|
||||
class="-mr-2 rounded-xl p-2 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 dark:text-dark-500 dark:hover:bg-dark-700 dark:hover:text-dark-300"
|
||||
class="-mr-2 rounded-xl p-2 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/30 focus-visible:ring-offset-2 dark:text-dark-500 dark:hover:bg-dark-700 dark:hover:text-dark-300 dark:focus-visible:ring-offset-dark-900"
|
||||
aria-label="Close modal"
|
||||
>
|
||||
<Icon name="x" size="md" />
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
:sort-key="sortKey"
|
||||
:sort-order="sortOrder"
|
||||
>
|
||||
<div class="flex items-center space-x-1">
|
||||
<div :class="['flex items-center space-x-1', getHeaderContentAlignmentClass(column)]">
|
||||
<span>{{ column.label }}</span>
|
||||
<span
|
||||
v-if="column.sortable"
|
||||
@@ -251,6 +251,11 @@ const checkScrollable = () => {
|
||||
|
||||
// 检查操作列是否需要展开
|
||||
const checkActionsColumnWidth = () => {
|
||||
if (!props.expandableActions) {
|
||||
actionsColumnNeedsExpanding.value = false
|
||||
actionsExpanded.value = false
|
||||
return
|
||||
}
|
||||
if (!tableWrapperRef.value) return
|
||||
|
||||
// 查找第一行的操作列单元格
|
||||
@@ -478,6 +483,13 @@ const getColumnAriaSort = (key: string) => {
|
||||
return sortOrder.value === 'asc' ? 'ascending' : 'descending'
|
||||
}
|
||||
|
||||
const getHeaderContentAlignmentClass = (column: Column) => {
|
||||
const className = column.class || ''
|
||||
if (className.includes('text-center')) return 'justify-center'
|
||||
if (className.includes('text-right')) return 'justify-end'
|
||||
return 'justify-start'
|
||||
}
|
||||
|
||||
const isNullishOrEmpty = (value: any) => value === null || value === undefined || value === ''
|
||||
|
||||
const toFiniteNumberOrNull = (value: any): number | null => {
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<!-- Balance Display -->
|
||||
<div
|
||||
v-if="user"
|
||||
class="hidden items-center gap-2 rounded-xl bg-primary-50 px-3 py-1.5 dark:bg-primary-900/20 sm:flex"
|
||||
class="group relative hidden items-center gap-2 rounded-xl bg-primary-50 px-3 py-1.5 dark:bg-primary-900/20 sm:flex"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 text-primary-600 dark:text-primary-400"
|
||||
@@ -63,8 +63,32 @@
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-sm font-semibold text-primary-700 dark:text-primary-300">
|
||||
${{ user.balance?.toFixed(2) || '0.00' }}
|
||||
{{ formatHeaderMoney(availableBalance) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="frozenBalance > 0"
|
||||
class="rounded-full bg-amber-100 px-1.5 py-0.5 text-xs font-medium text-amber-700 dark:bg-amber-900/40 dark:text-amber-200"
|
||||
>
|
||||
{{ balanceFrozenLabel }}
|
||||
</span>
|
||||
<div
|
||||
class="pointer-events-none absolute right-0 top-full mt-2 hidden w-56 rounded-lg border border-gray-200 bg-white p-3 text-xs shadow-lg group-hover:block dark:border-dark-700 dark:bg-dark-800"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-500 dark:text-dark-400">{{ balanceAvailableText }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ formatHeaderMoney(availableBalance) }}</span>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center justify-between">
|
||||
<span class="text-gray-500 dark:text-dark-400">{{ balanceFrozenText }}</span>
|
||||
<span class="font-medium text-amber-700 dark:text-amber-200">{{ formatHeaderMoney(frozenBalance) }}</span>
|
||||
</div>
|
||||
<div class="mt-2 border-t border-gray-100 pt-2 dark:border-dark-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-500 dark:text-dark-400">{{ balanceTotalText }}</span>
|
||||
<span class="font-semibold text-gray-900 dark:text-white">{{ formatHeaderMoney(totalBalance) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Dropdown -->
|
||||
@@ -111,7 +135,10 @@
|
||||
{{ t('common.balance') }}
|
||||
</div>
|
||||
<div class="text-sm font-semibold text-primary-600 dark:text-primary-400">
|
||||
${{ user.balance?.toFixed(2) || '0.00' }}
|
||||
{{ formatHeaderMoney(availableBalance) }}
|
||||
</div>
|
||||
<div v-if="frozenBalance > 0" class="mt-1 text-xs text-amber-600 dark:text-amber-300">
|
||||
{{ balanceFrozenText }} {{ formatHeaderMoney(frozenBalance) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -237,6 +264,13 @@ const dropdownRef = ref<HTMLElement | null>(null)
|
||||
const contactInfo = computed(() => appStore.contactInfo)
|
||||
const docUrl = computed(() => appStore.docUrl)
|
||||
const avatarUrl = computed(() => user.value?.avatar_url?.trim() || '')
|
||||
const availableBalance = computed(() => Number(user.value?.balance || 0))
|
||||
const frozenBalance = computed(() => Number(user.value?.frozen_balance || 0))
|
||||
const totalBalance = computed(() => availableBalance.value + frozenBalance.value)
|
||||
const balanceAvailableText = computed(() => t('common.availableBalance') === 'common.availableBalance' ? '可用余额' : t('common.availableBalance'))
|
||||
const balanceFrozenText = computed(() => t('common.frozenBalance') === 'common.frozenBalance' ? '冻结金额' : t('common.frozenBalance'))
|
||||
const balanceTotalText = computed(() => t('common.totalBalance') === 'common.totalBalance' ? '总余额' : t('common.totalBalance'))
|
||||
const balanceFrozenLabel = computed(() => `${balanceFrozenText.value} ${formatHeaderMoney(frozenBalance.value)}`)
|
||||
|
||||
// 只在标准模式的管理员下显示新手引导按钮
|
||||
const showOnboardingButton = computed(() => {
|
||||
@@ -314,6 +348,11 @@ function handleReplayGuide() {
|
||||
onboardingStore.replay()
|
||||
}
|
||||
|
||||
function formatHeaderMoney(value: number) {
|
||||
if (!Number.isFinite(value)) return '$0.00'
|
||||
return `$${value.toFixed(2)}`
|
||||
}
|
||||
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
|
||||
closeDropdown()
|
||||
|
||||
@@ -278,6 +278,26 @@ const KeyIcon = {
|
||||
)
|
||||
}
|
||||
|
||||
const BatchImageIcon = {
|
||||
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: 'M6.827 6.175A2.31 2.31 0 015.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 00-1.134-.175 2.31 2.31 0 01-1.64-1.055l-.822-1.316a2.25 2.25 0 00-1.906-1.059H9.554a2.25 2.25 0 00-1.906 1.059l-.821 1.316z'
|
||||
}),
|
||||
h('path', {
|
||||
'stroke-linecap': 'round',
|
||||
'stroke-linejoin': 'round',
|
||||
d: 'M16.5 12.75a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zM18.75 10.5h.008v.008h-.008V10.5z'
|
||||
})
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
const ChartIcon = {
|
||||
render: () =>
|
||||
h(
|
||||
@@ -666,6 +686,7 @@ function buildSelfNavItems(withDashboard: boolean): NavItem[] {
|
||||
}
|
||||
items.push(
|
||||
{ path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon },
|
||||
{ path: '/batch-image', label: t('nav.batchImage'), icon: BatchImageIcon, hideInSimpleMode: true },
|
||||
{ path: '/usage', label: t('nav.usage'), icon: ChartIcon, hideInSimpleMode: true },
|
||||
{ path: '/available-channels', label: t('nav.availableChannels'), icon: ChannelIcon, hideInSimpleMode: true, featureFlag: flagAvailableChannels },
|
||||
{ path: '/monitor', label: t('nav.channelStatus'), icon: SignalIcon, featureFlag: flagChannelMonitor },
|
||||
|
||||
@@ -34,6 +34,21 @@
|
||||
/>
|
||||
</button>
|
||||
|
||||
<button @click="router.push('/batch-image')" class="group flex w-full items-center gap-4 rounded-xl bg-gray-50 p-4 text-left transition-all duration-200 hover:bg-gray-100 dark:bg-dark-800/50 dark:hover:bg-dark-800">
|
||||
<div class="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-xl bg-sky-100 transition-transform group-hover:scale-105 dark:bg-sky-900/30">
|
||||
<Icon name="sparkles" size="lg" class="text-sky-600 dark:text-sky-400" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">{{ t('dashboard.batchImageAgent') }}</p>
|
||||
<p class="text-xs text-gray-500 dark:text-dark-400">{{ t('dashboard.batchImageAgentDesc') }}</p>
|
||||
</div>
|
||||
<Icon
|
||||
name="chevronRight"
|
||||
size="md"
|
||||
class="text-gray-400 transition-colors group-hover:text-sky-500 dark:text-dark-500"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<button @click="router.push('/redeem')" class="group flex w-full items-center gap-4 rounded-xl bg-gray-50 p-4 text-left transition-all duration-200 hover:bg-gray-100 dark:bg-dark-800/50 dark:hover:bg-dark-800">
|
||||
<div class="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-xl bg-amber-100 transition-transform group-hover:scale-105 dark:bg-amber-900/30">
|
||||
<Icon name="gift" size="lg" class="text-amber-600 dark:text-amber-400" />
|
||||
|
||||
Reference in New Issue
Block a user