fix(ui): unify admin table toolbar layout with search and buttons in single row

Standardize filter bar layout across admin pages to place search/filters
on left and action buttons on right within the same row, improving
visual consistency and space utilization.
This commit is contained in:
shaw
2026-02-08 14:00:02 +08:00
parent b4ec65785d
commit b1c30df8e3
5 changed files with 114 additions and 122 deletions

View File

@@ -1,26 +1,10 @@
<template> <template>
<AppLayout> <AppLayout>
<TablePageLayout> <TablePageLayout>
<template #actions>
<div class="flex justify-end gap-3">
<button
@click="loadAnnouncements"
:disabled="loading"
class="btn btn-secondary"
:title="t('common.refresh')"
>
<Icon name="refresh" size="md" :class="loading ? 'animate-spin' : ''" />
</button>
<button @click="openCreateDialog" class="btn btn-primary">
<Icon name="plus" size="md" class="mr-1" />
{{ t('admin.announcements.createAnnouncement') }}
</button>
</div>
</template>
<template #filters> <template #filters>
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div class="flex flex-wrap items-center gap-3">
<div class="max-w-md flex-1"> <!-- Left: Search + Filters -->
<div class="flex-1 sm:max-w-64">
<input <input
v-model="searchQuery" v-model="searchQuery"
type="text" type="text"
@@ -29,13 +13,27 @@
@input="handleSearch" @input="handleSearch"
/> />
</div> </div>
<div class="flex gap-2"> <Select
<Select v-model="filters.status"
v-model="filters.status" :options="statusFilterOptions"
:options="statusFilterOptions" class="w-40"
class="w-40" @change="handleStatusChange"
@change="handleStatusChange" />
/>
<!-- Right: Action buttons -->
<div class="flex flex-1 flex-wrap items-center justify-end gap-2">
<button
@click="loadAnnouncements"
:disabled="loading"
class="btn btn-secondary"
:title="t('common.refresh')"
>
<Icon name="refresh" size="md" :class="loading ? 'animate-spin' : ''" />
</button>
<button @click="openCreateDialog" class="btn btn-primary">
<Icon name="plus" size="md" class="mr-1" />
{{ t('admin.announcements.createAnnouncement') }}
</button>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -1,26 +1,10 @@
<template> <template>
<AppLayout> <AppLayout>
<TablePageLayout> <TablePageLayout>
<template #actions>
<div class="flex justify-end gap-3">
<button
@click="loadCodes"
:disabled="loading"
class="btn btn-secondary"
:title="t('common.refresh')"
>
<Icon name="refresh" size="md" :class="loading ? 'animate-spin' : ''" />
</button>
<button @click="showCreateDialog = true" class="btn btn-primary">
<Icon name="plus" size="md" class="mr-1" />
{{ t('admin.promo.createCode') }}
</button>
</div>
</template>
<template #filters> <template #filters>
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div class="flex flex-wrap items-center gap-3">
<div class="max-w-md flex-1"> <!-- Left: Search + Filters -->
<div class="flex-1 sm:max-w-64">
<input <input
v-model="searchQuery" v-model="searchQuery"
type="text" type="text"
@@ -29,13 +13,27 @@
@input="handleSearch" @input="handleSearch"
/> />
</div> </div>
<div class="flex gap-2"> <Select
<Select v-model="filters.status"
v-model="filters.status" :options="filterStatusOptions"
:options="filterStatusOptions" class="w-36"
class="w-36" @change="loadCodes"
@change="loadCodes" />
/>
<!-- Right: Action buttons -->
<div class="flex flex-1 flex-wrap items-center justify-end gap-2">
<button
@click="loadCodes"
:disabled="loading"
class="btn btn-secondary"
:title="t('common.refresh')"
>
<Icon name="refresh" size="md" :class="loading ? 'animate-spin' : ''" />
</button>
<button @click="showCreateDialog = true" class="btn btn-primary">
<Icon name="plus" size="md" class="mr-1" />
{{ t('admin.promo.createCode') }}
</button>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -2,9 +2,42 @@
<AppLayout> <AppLayout>
<TablePageLayout> <TablePageLayout>
<template #filters> <template #filters>
<div class="space-y-3"> <div class="flex flex-wrap items-center gap-3">
<!-- Row 1: Actions --> <!-- Left: Search + Filters -->
<div class="flex flex-wrap items-center gap-3"> <div class="relative w-full sm:w-64">
<Icon
name="search"
size="md"
class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 dark:text-gray-500"
/>
<input
v-model="searchQuery"
type="text"
:placeholder="t('admin.proxies.searchProxies')"
class="input pl-10"
@input="handleSearch"
/>
</div>
<div class="w-full sm:w-40">
<Select
v-model="filters.protocol"
:options="protocolOptions"
:placeholder="t('admin.proxies.allProtocols')"
@change="loadProxies"
/>
</div>
<div class="w-full sm:w-36">
<Select
v-model="filters.status"
:options="statusOptions"
:placeholder="t('admin.proxies.allStatus')"
@change="loadProxies"
/>
</div>
<!-- Right: All action buttons -->
<div class="flex flex-1 flex-wrap items-center justify-end gap-2">
<button <button
@click="loadProxies" @click="loadProxies"
:disabled="loading" :disabled="loading"
@@ -42,41 +75,6 @@
{{ t('admin.proxies.createProxy') }} {{ t('admin.proxies.createProxy') }}
</button> </button>
</div> </div>
<!-- Row 2: Search + Filters -->
<div class="flex flex-wrap items-center gap-3">
<div class="relative w-full sm:w-64">
<Icon
name="search"
size="md"
class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 dark:text-gray-500"
/>
<input
v-model="searchQuery"
type="text"
:placeholder="t('admin.proxies.searchProxies')"
class="input pl-10"
@input="handleSearch"
/>
</div>
<div class="w-full sm:w-40">
<Select
v-model="filters.protocol"
:options="protocolOptions"
:placeholder="t('admin.proxies.allProtocols')"
@change="loadProxies"
/>
</div>
<div class="w-full sm:w-36">
<Select
v-model="filters.status"
:options="statusOptions"
:placeholder="t('admin.proxies.allStatus')"
@change="loadProxies"
/>
</div>
</div>
</div> </div>
</template> </template>

View File

@@ -1,34 +1,18 @@
<template> <template>
<AppLayout> <AppLayout>
<TablePageLayout> <TablePageLayout>
<template #actions>
<div class="flex justify-end gap-3">
<button
@click="loadCodes"
:disabled="loading"
class="btn btn-secondary"
:title="t('common.refresh')"
>
<Icon name="refresh" size="md" :class="loading ? 'animate-spin' : ''" />
</button>
<button @click="showGenerateDialog = true" class="btn btn-primary">
{{ t('admin.redeem.generateCodes') }}
</button>
</div>
</template>
<template #filters> <template #filters>
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div class="flex flex-wrap items-center gap-3">
<div class="max-w-md flex-1"> <!-- Left: Search + Filters -->
<input <div class="flex-1 sm:max-w-64">
v-model="searchQuery" <input
type="text" v-model="searchQuery"
:placeholder="t('admin.redeem.searchCodes')" type="text"
class="input" :placeholder="t('admin.redeem.searchCodes')"
@input="handleSearch" class="input"
/> @input="handleSearch"
/>
</div> </div>
<div class="flex gap-2">
<Select <Select
v-model="filters.type" v-model="filters.type"
:options="filterTypeOptions" :options="filterTypeOptions"
@@ -41,9 +25,23 @@
class="w-36" class="w-36"
@change="loadCodes" @change="loadCodes"
/> />
<button @click="handleExportCodes" class="btn btn-secondary">
{{ t('admin.redeem.exportCsv') }} <!-- Right: Action buttons -->
</button> <div class="flex flex-1 flex-wrap items-center justify-end gap-2">
<button
@click="loadCodes"
:disabled="loading"
class="btn btn-secondary"
:title="t('common.refresh')"
>
<Icon name="refresh" size="md" :class="loading ? 'animate-spin' : ''" />
</button>
<button @click="handleExportCodes" class="btn btn-secondary">
{{ t('admin.redeem.exportCsv') }}
</button>
<button @click="showGenerateDialog = true" class="btn btn-primary">
{{ t('admin.redeem.generateCodes') }}
</button>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -3,9 +3,9 @@
<TablePageLayout> <TablePageLayout>
<!-- Single Row: Search, Filters, and Actions --> <!-- Single Row: Search, Filters, and Actions -->
<template #filters> <template #filters>
<div class="flex w-full flex-col gap-3 md:flex-row md:flex-wrap-reverse md:items-center md:justify-between md:gap-4"> <div class="flex flex-wrap items-center gap-3">
<!-- Left: Search + Active Filters --> <!-- Left: Search + Active Filters -->
<div class="flex min-w-[280px] flex-1 flex-wrap content-start items-center gap-3 md:order-1"> <div class="flex flex-1 flex-wrap items-center gap-3">
<!-- Search Box --> <!-- Search Box -->
<div class="relative w-full md:w-64"> <div class="relative w-full md:w-64">
<Icon <Icon
@@ -100,7 +100,7 @@
</div> </div>
<!-- Right: Actions and Settings --> <!-- Right: Actions and Settings -->
<div class="flex w-full items-center justify-between gap-2 md:order-2 md:ml-auto md:max-w-full md:flex-wrap md:justify-end md:gap-3"> <div class="flex flex-wrap items-center justify-end gap-2">
<!-- Mobile: Secondary buttons (icon only) --> <!-- Mobile: Secondary buttons (icon only) -->
<div class="flex items-center gap-2 md:contents"> <div class="flex items-center gap-2 md:contents">
<!-- Refresh Button --> <!-- Refresh Button -->