Merge branch 'feature/datatable-enhancements'

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

View File

@@ -1,5 +1,5 @@
<template> <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"> <form v-if="account" @submit.prevent="handleSubmit" class="space-y-5">
<div> <div>
<label class="input-label">{{ t('common.name') }}</label> <label class="input-label">{{ t('common.name') }}</label>

View File

@@ -15,7 +15,8 @@
:key="column.key" :key="column.key"
scope="col" scope="col"
:class="[ :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 }, { 'cursor-pointer hover:bg-gray-100 dark:hover:bg-dark-700': column.sortable },
getStickyColumnClass(column, index) 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"> <tbody class="table-body divide-y divide-gray-200 bg-white dark:divide-dark-700 dark:bg-dark-900">
<!-- Loading skeleton --> <!-- Loading skeleton -->
<tr v-if="loading" v-for="i in 5" :key="i"> <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="animate-pulse">
<div class="h-4 w-3/4 rounded bg-gray-200 dark:bg-dark-700"></div> <div class="h-4 w-3/4 rounded bg-gray-200 dark:bg-dark-700"></div>
</div> </div>
@@ -92,7 +93,7 @@
<tr v-else-if="!data || data.length === 0"> <tr v-else-if="!data || data.length === 0">
<td <td
:colspan="columns.length" :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"> <slot name="empty">
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
@@ -128,7 +129,8 @@
v-for="(column, colIndex) in columns" v-for="(column, colIndex) in columns"
:key="column.key" :key="column.key"
:class="[ :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) getStickyColumnClass(column, colIndex)
]" ]"
> >
@@ -165,24 +167,46 @@ const checkScrollable = () => {
const checkActionsColumnWidth = () => { const checkActionsColumnWidth = () => {
if (!tableWrapperRef.value) return 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') const firstActionCell = tableWrapperRef.value.querySelector('tbody tr:first-child td:last-child')
if (!firstActionCell) return if (!firstActionCell) return
// 获取操作列内容的实际宽度 // 查找操作列内容的容器div
const actionsContent = firstActionCell.querySelector('div') const actionsContainer = firstActionCell.querySelector('div')
if (!actionsContent) return if (!actionsContainer) return
// 比较内容宽度和单元格宽度 // 临时展开以测量完整宽度
const contentWidth = actionsContent.scrollWidth const wasExpanded = actionsExpanded.value
const cellWidth = (firstActionCell as HTMLElement).clientWidth actionsExpanded.value = true
// 如果内容宽度超过单元格宽度,说明需要展开 // 等待DOM更新
actionsColumnNeedsExpanding.value = contentWidth > cellWidth 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 stickyFirstColumn?: boolean
stickyActionsColumn?: boolean stickyActionsColumn?: boolean
expandableActions?: boolean expandableActions?: boolean
actionsCount?: number // 操作按钮总数,用于判断是否需要展开功能
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@@ -232,9 +257,10 @@ const sortKey = ref<string>('')
const sortOrder = ref<'asc' | 'desc'>('asc') const sortOrder = ref<'asc' | 'desc'>('asc')
const actionsExpanded = ref(false) const actionsExpanded = ref(false)
// 数据/列/展开状态变化时重新检查滚动状态 // 数据/列变化时重新检查滚动状态
// 注意:不能监听 actionsExpanded因为 checkActionsColumnWidth 会临时修改它,会导致无限循环
watch( watch(
[() => props.data.length, () => props.columns, actionsExpanded], [() => props.data.length, () => props.columns],
async () => { async () => {
await nextTick() await nextTick()
checkScrollable() checkScrollable()
@@ -243,6 +269,12 @@ watch(
{ flush: 'post' } { flush: 'post' }
) )
// 单独监听展开状态变化,只更新滚动状态
watch(actionsExpanded, async () => {
await nextTick()
checkScrollable()
})
const handleSort = (key: string) => { const handleSort = (key: string) => {
if (sortKey.value === key) { if (sortKey.value === key) {
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc' sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
@@ -268,6 +300,12 @@ const sortedData = computed(() => {
// 检查是否有可展开的操作列 // 检查是否有可展开的操作列
const hasExpandableActions = computed(() => { const hasExpandableActions = computed(() => {
// 如果明确指定了actionsCount使用它来判断
if (props.actionsCount !== undefined) {
return props.expandableActions && props.columns.some((col) => col.key === 'actions') && props.actionsCount > 2
}
// 否则使用原来的检测逻辑
return ( return (
props.expandableActions && props.expandableActions &&
props.columns.some((col) => col.key === 'actions') && props.columns.some((col) => col.key === 'actions') &&
@@ -312,6 +350,22 @@ const getStickyColumnClass = (column: Column, index: number) => {
return classes.join(' ') 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> </script>
<style scoped> <style scoped>

View File

@@ -1172,9 +1172,9 @@ export default {
batchAdd: 'Quick Add', batchAdd: 'Quick Add',
batchInput: 'Proxy List', batchInput: 'Proxy List',
batchInputPlaceholder: 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: 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', parsedCount: '{count} valid',
invalidCount: '{count} invalid', invalidCount: '{count} invalid',
duplicateCount: '{count} duplicate', duplicateCount: '{count} duplicate',
@@ -1351,12 +1351,12 @@ export default {
port: 'SMTP Port', port: 'SMTP Port',
portPlaceholder: '587', portPlaceholder: '587',
username: 'SMTP Username', username: 'SMTP Username',
usernamePlaceholder: 'your-email@gmail.com', usernamePlaceholder: "your-email{'@'}gmail.com",
password: 'SMTP Password', password: 'SMTP Password',
passwordPlaceholder: '********', passwordPlaceholder: '********',
passwordHint: 'Leave empty to keep existing password', passwordHint: 'Leave empty to keep existing password',
fromEmail: 'From Email', fromEmail: 'From Email',
fromEmailPlaceholder: 'noreply@example.com', fromEmailPlaceholder: "noreply{'@'}example.com",
fromName: 'From Name', fromName: 'From Name',
fromNamePlaceholder: 'Sub2API', fromNamePlaceholder: 'Sub2API',
useTls: 'Use TLS', useTls: 'Use TLS',
@@ -1366,7 +1366,7 @@ export default {
title: 'Send Test Email', title: 'Send Test Email',
description: 'Send a test email to verify your SMTP configuration', description: 'Send a test email to verify your SMTP configuration',
recipientEmail: 'Recipient Email', recipientEmail: 'Recipient Email',
recipientEmailPlaceholder: 'test@example.com', recipientEmailPlaceholder: "test{'@'}example.com",
sendTestEmail: 'Send Test Email', sendTestEmail: 'Send Test Email',
sending: 'Sending...', sending: 'Sending...',
enterRecipientHint: 'Please enter a recipient email address' enterRecipientHint: 'Please enter a recipient email address'

View File

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

View File

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

View File

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