优化二维码池筛选和日期中文化

This commit is contained in:
yml2213
2026-06-19 19:16:52 +08:00
parent f4343ed8a5
commit fcf0230e04
5 changed files with 408 additions and 35 deletions
@@ -2,6 +2,8 @@ import { apiClient } from '@/shared/api/client'
import type { ApiResponse, PaginatedResult } from '@/shared/types/types'
export type QrCodeStatus = 'unused' | 'used' | 'disabled'
export type QrCodeBooleanFilter = 'true' | 'false'
export type QrCodeExpireStatus = 'valid' | 'expired' | 'permanent' | 'expiring_soon'
export interface ChatQrCode {
id: number
@@ -34,6 +36,12 @@ export interface QrCodeOcrResult {
export interface QrCodeListQuery {
status?: QrCodeStatus
keyword?: string
wecom_renamed?: QrCodeBooleanFilter
bound?: QrCodeBooleanFilter
expire_status?: QrCodeExpireStatus
created_start?: string
created_end?: string
page?: number
limit?: number
}
@@ -63,7 +71,17 @@ export async function fetchQrCodes(query: QrCodeListQuery = {}) {
const { data } = await apiClient.get<ApiResponse<PaginatedResult<ChatQrCode>>>(
'/admin/chats/qrcodes',
{
params: { status: query.status || undefined, page: query.page || 1, limit: query.limit || 20 },
params: {
status: query.status || undefined,
keyword: query.keyword?.trim() || undefined,
wecom_renamed: query.wecom_renamed || undefined,
bound: query.bound || undefined,
expire_status: query.expire_status || undefined,
created_start: query.created_start || undefined,
created_end: query.created_end || undefined,
page: query.page || 1,
limit: query.limit || 20,
},
silent: true,
}
)
@@ -21,6 +21,8 @@ import {
updateQrCode,
type ChatQrCode,
type CreateQrCodePayload,
type QrCodeBooleanFilter,
type QrCodeExpireStatus,
type QrCodeStatus,
type QrCodeStats,
} from '@/features/admin/api/adminQrCodes'
@@ -40,7 +42,24 @@ const canManageQrCodes = computed(() => adminSession.hasPermission('qrcode:manag
const currentPage = ref(1)
const currentPageSize = ref(20)
const total = ref(0)
const statusFilter = ref<QrCodeStatus | ''>('')
type QrCodeFilters = {
keyword: string
status: QrCodeStatus | ''
wecom_renamed: QrCodeBooleanFilter | ''
bound: QrCodeBooleanFilter | ''
expire_status: QrCodeExpireStatus | ''
created_range: string[]
}
const filters = reactive<QrCodeFilters>({
keyword: '',
status: '',
wecom_renamed: '',
bound: '',
expire_status: '',
created_range: [],
})
// 上传弹窗
const uploadVisible = ref(false)
@@ -81,6 +100,23 @@ const statusOptions: { label: string; value: QrCodeStatus | ''; type: string }[]
{ label: '已停用', value: 'disabled', type: 'danger' },
]
const renameFilterOptions: { label: string; value: QrCodeBooleanFilter }[] = [
{ label: '已改名', value: 'true' },
{ label: '未改名', value: 'false' },
]
const boundFilterOptions: { label: string; value: QrCodeBooleanFilter }[] = [
{ label: '已绑定', value: 'true' },
{ label: '未绑定', value: 'false' },
]
const expireStatusOptions: { label: string; value: QrCodeExpireStatus }[] = [
{ label: '未过期', value: 'valid' },
{ label: '即将过期', value: 'expiring_soon' },
{ label: '已过期', value: 'expired' },
{ label: '永久有效', value: 'permanent' },
]
const statusMap = computed(() => {
const m: Record<string, { label: string; type: string }> = {}
statusOptions.forEach(o => {
@@ -104,11 +140,25 @@ const selectedRows = ref<ChatQrCode[]>([])
const selectedCount = computed(() => selectedRows.value.length)
const selectedIssuedCount = computed(() => selectedRows.value.filter(qrcodeWasIssued).length)
function filterDateToISO(value?: string) {
if (!value) return undefined
const date = new Date(value)
if (Number.isNaN(date.getTime())) return undefined
return date.toISOString()
}
async function loadList() {
loading.value = true
try {
const [createdStart, createdEnd] = filters.created_range || []
const res = await fetchQrCodes({
status: statusFilter.value || undefined,
status: filters.status || undefined,
keyword: filters.keyword.trim() || undefined,
wecom_renamed: filters.wecom_renamed || undefined,
bound: filters.bound || undefined,
expire_status: filters.expire_status || undefined,
created_start: filterDateToISO(createdStart),
created_end: filterDateToISO(createdEnd),
page: currentPage.value,
limit: currentPageSize.value,
})
@@ -133,6 +183,16 @@ function handleFilter() {
loadList()
}
function resetFilters() {
filters.keyword = ''
filters.status = ''
filters.wecom_renamed = ''
filters.bound = ''
filters.expire_status = ''
filters.created_range = []
handleFilter()
}
function handlePageChange() {
loadList()
}
@@ -452,11 +512,20 @@ onMounted(reloadAll)
<el-card shadow="never" class="toolbar-card">
<div class="toolbar">
<div class="filter-area">
<el-input
v-model="filters.keyword"
clearable
class="keyword-filter"
:prefix-icon="Search"
placeholder="群名 / 备注 / 绑定群 / ID"
@clear="handleFilter"
@keyup.enter="handleFilter"
/>
<el-select
v-model="statusFilter"
v-model="filters.status"
placeholder="状态筛选"
clearable
style="width: 140px"
class="compact-filter"
@change="handleFilter"
>
<el-option
@@ -466,7 +535,61 @@ onMounted(reloadAll)
:value="opt.value"
/>
</el-select>
<el-button :icon="Search" @click="handleFilter">筛选</el-button>
<el-select
v-model="filters.wecom_renamed"
placeholder="企微改名"
clearable
class="compact-filter"
@change="handleFilter"
>
<el-option
v-for="opt in renameFilterOptions"
:key="`rename-${opt.value}`"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<el-select
v-model="filters.bound"
placeholder="绑定群"
clearable
class="compact-filter"
@change="handleFilter"
>
<el-option
v-for="opt in boundFilterOptions"
:key="`bound-${opt.value}`"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<el-select
v-model="filters.expire_status"
placeholder="过期状态"
clearable
class="compact-filter"
@change="handleFilter"
>
<el-option
v-for="opt in expireStatusOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<el-date-picker
v-model="filters.created_range"
type="datetimerange"
range-separator=""
start-placeholder="上传开始"
end-placeholder="上传结束"
format="YYYY-MM-DD HH:mm"
value-format="YYYY-MM-DDTHH:mm:ss"
class="date-range-filter"
@change="handleFilter"
/>
<el-button type="primary" :icon="Search" @click="handleFilter">筛选</el-button>
<el-button @click="resetFilters">重置</el-button>
</div>
<div class="action-area">
<span v-if="selectedCount" class="selection-summary">已选 {{ selectedCount }} </span>
@@ -833,7 +956,7 @@ onMounted(reloadAll)
.toolbar {
display: flex;
align-items: center;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
@@ -845,6 +968,29 @@ onMounted(reloadAll)
align-items: center;
}
.filter-area {
flex: 1 1 auto;
flex-wrap: wrap;
}
.action-area {
flex: 0 0 auto;
flex-wrap: wrap;
justify-content: flex-end;
}
.keyword-filter {
width: 240px;
}
.compact-filter {
width: 128px;
}
.date-range-filter {
width: 330px;
}
.qrcode-table {
border-radius: 10px;
box-shadow: none;
@@ -992,6 +1138,30 @@ onMounted(reloadAll)
padding: 4px 6px;
}
@media (max-width: 1280px) {
.toolbar {
flex-direction: column;
align-items: stretch;
}
.action-area {
justify-content: flex-start;
}
}
@media (max-width: 720px) {
.keyword-filter,
.compact-filter,
.date-range-filter {
width: 100%;
}
.filter-area,
.action-area {
align-items: stretch;
}
}
.upload-preview {
display: flex;
flex-direction: column;