feat: complete batch image workflow

This commit is contained in:
Turtle_Li
2026-07-06 12:22:04 +08:00
parent a994fbd77a
commit 8fab636998
96 changed files with 8290 additions and 279 deletions
@@ -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" />
+13 -1
View File
@@ -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 => {
+42 -3
View File
@@ -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" />