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>
<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