Merge branch 'feature/datatable-enhancements'

This commit is contained in:
shaw 2025-12-27 21:00:37 +08:00
commit 54636781ea
6 changed files with 85 additions and 31 deletions

View File

@ -1,5 +1,5 @@
<template>
<Modal :show="show" :title="t('admin.accounts.editAccount')" size="lg" @close="handleClose">
<Modal :show="show" :title="t('admin.accounts.editAccount')" size="xl" @close="handleClose">
<form v-if="account" @submit.prevent="handleSubmit" class="space-y-5">
<div>
<label class="input-label">{{ t('common.name') }}</label>

View File

@ -15,7 +15,8 @@
:key="column.key"
scope="col"
:class="[
'sticky-header-cell px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dark-400',
'sticky-header-cell py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-dark-400',
getAdaptivePaddingClass(),
{ 'cursor-pointer hover:bg-gray-100 dark:hover:bg-dark-700': column.sortable },
getStickyColumnClass(column, index)
]"
@ -81,7 +82,7 @@
<tbody class="table-body divide-y divide-gray-200 bg-white dark:divide-dark-700 dark:bg-dark-900">
<!-- Loading skeleton -->
<tr v-if="loading" v-for="i in 5" :key="i">
<td v-for="column in columns" :key="column.key" class="whitespace-nowrap px-6 py-4">
<td v-for="column in columns" :key="column.key" :class="['whitespace-nowrap py-4', getAdaptivePaddingClass()]">
<div class="animate-pulse">
<div class="h-4 w-3/4 rounded bg-gray-200 dark:bg-dark-700"></div>
</div>
@ -92,7 +93,7 @@
<tr v-else-if="!data || data.length === 0">
<td
:colspan="columns.length"
class="px-6 py-12 text-center text-gray-500 dark:text-dark-400"
:class="['py-12 text-center text-gray-500 dark:text-dark-400', getAdaptivePaddingClass()]"
>
<slot name="empty">
<div class="flex flex-col items-center">
@ -128,7 +129,8 @@
v-for="(column, colIndex) in columns"
:key="column.key"
:class="[
'whitespace-nowrap px-6 py-4 text-sm text-gray-900 dark:text-gray-100',
'whitespace-nowrap py-4 text-sm text-gray-900 dark:text-gray-100',
getAdaptivePaddingClass(),
getStickyColumnClass(column, colIndex)
]"
>
@ -165,24 +167,46 @@ const checkScrollable = () => {
const checkActionsColumnWidth = () => {
if (!tableWrapperRef.value) return
//
const actionsHeader = tableWrapperRef.value.querySelector('th:has(button[title*="Expand"], button[title*="展开"])')
if (!actionsHeader) return
//
const firstActionCell = tableWrapperRef.value.querySelector('tbody tr:first-child td:last-child')
if (!firstActionCell) return
//
const actionsContent = firstActionCell.querySelector('div')
if (!actionsContent) return
// div
const actionsContainer = firstActionCell.querySelector('div')
if (!actionsContainer) return
//
const contentWidth = actionsContent.scrollWidth
const cellWidth = (firstActionCell as HTMLElement).clientWidth
//
const wasExpanded = actionsExpanded.value
actionsExpanded.value = true
//
actionsColumnNeedsExpanding.value = contentWidth > cellWidth
// DOM
nextTick(() => {
//
const buttons = actionsContainer.querySelectorAll('button')
if (buttons.length <= 2) {
actionsColumnNeedsExpanding.value = false
actionsExpanded.value = wasExpanded
return
}
// gap
let totalWidth = 0
buttons.forEach((btn, index) => {
totalWidth += (btn as HTMLElement).offsetWidth
if (index < buttons.length - 1) {
totalWidth += 4 // gap-1 = 4px
}
})
// padding
const cellWidth = (firstActionCell as HTMLElement).clientWidth - 32 // padding
//
actionsColumnNeedsExpanding.value = totalWidth > cellWidth
//
actionsExpanded.value = wasExpanded
})
}
//
@ -219,6 +243,7 @@ interface Props {
stickyFirstColumn?: boolean
stickyActionsColumn?: boolean
expandableActions?: boolean
actionsCount?: number //
}
const props = withDefaults(defineProps<Props>(), {
@ -232,9 +257,10 @@ const sortKey = ref<string>('')
const sortOrder = ref<'asc' | 'desc'>('asc')
const actionsExpanded = ref(false)
// //
// /
// actionsExpanded checkActionsColumnWidth
watch(
[() => props.data.length, () => props.columns, actionsExpanded],
[() => props.data.length, () => props.columns],
async () => {
await nextTick()
checkScrollable()
@ -243,6 +269,12 @@ watch(
{ flush: 'post' }
)
//
watch(actionsExpanded, async () => {
await nextTick()
checkScrollable()
})
const handleSort = (key: string) => {
if (sortKey.value === key) {
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
@ -268,6 +300,12 @@ const sortedData = computed(() => {
//
const hasExpandableActions = computed(() => {
// actionsCount使
if (props.actionsCount !== undefined) {
return props.expandableActions && props.columns.some((col) => col.key === 'actions') && props.actionsCount > 2
}
// 使
return (
props.expandableActions &&
props.columns.some((col) => col.key === 'actions') &&
@ -312,6 +350,22 @@ const getStickyColumnClass = (column: Column, index: number) => {
return classes.join(' ')
}
//
const getAdaptivePaddingClass = () => {
const columnCount = props.columns.length
//
if (columnCount >= 10) {
return 'px-2' // 8px
} else if (columnCount >= 7) {
return 'px-3' // 12px
} else if (columnCount >= 5) {
return 'px-4' // 16px
} else {
return 'px-6' // 24px ()
}
}
</script>
<style scoped>

View File

@ -1172,9 +1172,9 @@ export default {
batchAdd: 'Quick Add',
batchInput: 'Proxy List',
batchInputPlaceholder:
"Enter one proxy per line in the following formats:\nsocks5://user:pass{'@'}192.168.1.1:1080\nhttp://192.168.1.1:8080\nhttps://user:pass{'@'}proxy.example.com:443",
"Enter one proxy per line in the following formats:\nsocks5://user:pass@192.168.1.1:1080\nhttp://192.168.1.1:8080\nhttps://user:pass@proxy.example.com:443",
batchInputHint:
"Supports http, https, socks5 protocols. Format: protocol://[user:pass{'@'}]host:port",
"Supports http, https, socks5 protocols. Format: protocol://[user:pass@]host:port",
parsedCount: '{count} valid',
invalidCount: '{count} invalid',
duplicateCount: '{count} duplicate',
@ -1351,12 +1351,12 @@ export default {
port: 'SMTP Port',
portPlaceholder: '587',
username: 'SMTP Username',
usernamePlaceholder: 'your-email@gmail.com',
usernamePlaceholder: "your-email{'@'}gmail.com",
password: 'SMTP Password',
passwordPlaceholder: '********',
passwordHint: 'Leave empty to keep existing password',
fromEmail: 'From Email',
fromEmailPlaceholder: 'noreply@example.com',
fromEmailPlaceholder: "noreply{'@'}example.com",
fromName: 'From Name',
fromNamePlaceholder: 'Sub2API',
useTls: 'Use TLS',
@ -1366,7 +1366,7 @@ export default {
title: 'Send Test Email',
description: 'Send a test email to verify your SMTP configuration',
recipientEmail: 'Recipient Email',
recipientEmailPlaceholder: 'test@example.com',
recipientEmailPlaceholder: "test{'@'}example.com",
sendTestEmail: 'Send Test Email',
sending: 'Sending...',
enterRecipientHint: 'Please enter a recipient email address'

View File

@ -1321,8 +1321,8 @@ export default {
batchAdd: '快捷添加',
batchInput: '代理列表',
batchInputPlaceholder:
"每行输入一个代理,支持以下格式:\nsocks5://user:pass{'@'}192.168.1.1:1080\nhttp://192.168.1.1:8080\nhttps://user:pass{'@'}proxy.example.com:443",
batchInputHint: "支持 http、https、socks5 协议,格式:协议://[用户名:密码{'@'}]主机:端口",
"每行输入一个代理,支持以下格式:\nsocks5://user:pass@192.168.1.1:1080\nhttp://192.168.1.1:8080\nhttps://user:pass@proxy.example.com:443",
batchInputHint: "支持 http、https、socks5 协议,格式:协议://[用户名:密码@]主机:端口",
parsedCount: '有效 {count} 个',
invalidCount: '无效 {count} 个',
duplicateCount: '重复 {count} 个',
@ -1549,12 +1549,12 @@ export default {
port: 'SMTP 端口',
portPlaceholder: '587',
username: 'SMTP 用户名',
usernamePlaceholder: 'your-email@gmail.com',
usernamePlaceholder: "your-email{'@'}gmail.com",
password: 'SMTP 密码',
passwordPlaceholder: '********',
passwordHint: '留空以保留现有密码',
fromEmail: '发件人邮箱',
fromEmailPlaceholder: 'noreply@example.com',
fromEmailPlaceholder: "noreply{'@'}example.com",
fromName: '发件人名称',
fromNamePlaceholder: 'Sub2API',
useTls: '使用 TLS',
@ -1564,7 +1564,7 @@ export default {
title: '发送测试邮件',
description: '发送测试邮件以验证 SMTP 配置',
recipientEmail: '收件人邮箱',
recipientEmailPlaceholder: 'test@example.com',
recipientEmailPlaceholder: "test{'@'}example.com",
sendTestEmail: '发送测试邮件',
sending: '发送中...',
enterRecipientHint: '请输入收件人邮箱地址'

View File

@ -165,7 +165,7 @@
</div>
</div>
<DataTable :columns="columns" :data="accounts" :loading="loading">
<DataTable :columns="columns" :data="accounts" :loading="loading" :actions-count="6">
<template #cell-select="{ row }">
<input
type="checkbox"

View File

@ -85,7 +85,7 @@
<!-- Users Table -->
<template #table>
<DataTable :columns="columns" :data="users" :loading="loading">
<DataTable :columns="columns" :data="users" :loading="loading" :actions-count="7">
<template #cell-email="{ value }">
<div class="flex items-center gap-2">
<div