feat(admin): 分站二维码池支持搜索筛选并记住上次选择的分站
- 工具栏对齐主站二维码池:关键词搜索、绑定群、过期状态、上传时间范围 - fetchSiteQrCodes 补充传递 bound/expire_status/created_start/created_end - 按管理员维度记忆上次查看的分站,站点失效时回退到首个站点
This commit is contained in:
@@ -182,6 +182,10 @@ export async function fetchSiteQrCodes(siteId: number, query: QrCodeListQuery =
|
||||
params: {
|
||||
status: query.status || undefined,
|
||||
keyword: query.keyword?.trim() || 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,
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Delete, Plus, Refresh, UploadFilled } from '@element-plus/icons-vue'
|
||||
import { Delete, Plus, Refresh, Search, UploadFilled } from '@element-plus/icons-vue'
|
||||
|
||||
import {
|
||||
batchCreateSiteQrCodes,
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
updateSiteQrCode,
|
||||
type ChatQrCode,
|
||||
type CreateQrCodePayload,
|
||||
type QrCodeBooleanFilter,
|
||||
type QrCodeExpireStatus,
|
||||
type QrCodeStats,
|
||||
type QrCodeStatus,
|
||||
} from '@/features/admin/api/adminQrCodes'
|
||||
@@ -38,7 +40,38 @@ const qrcodes = ref<ChatQrCode[]>([])
|
||||
const total = ref(0)
|
||||
const currentPage = ref(1)
|
||||
const currentPageSize = ref(20)
|
||||
const statusFilter = ref<QrCodeStatus | ''>('')
|
||||
const filters = reactive<{
|
||||
keyword: string
|
||||
status: QrCodeStatus | ''
|
||||
bound: QrCodeBooleanFilter | ''
|
||||
expire_status: QrCodeExpireStatus | ''
|
||||
created_range: string[]
|
||||
}>({
|
||||
keyword: '',
|
||||
status: '',
|
||||
bound: '',
|
||||
expire_status: '',
|
||||
created_range: [],
|
||||
})
|
||||
|
||||
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' },
|
||||
]
|
||||
|
||||
function filterDateToISO(value?: string) {
|
||||
if (!value) return undefined
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return undefined
|
||||
return date.toISOString()
|
||||
}
|
||||
const stats = ref<QrCodeStats>({
|
||||
unused_count: 0,
|
||||
used_count: 0,
|
||||
@@ -76,13 +109,38 @@ const maxBatchUploadCount = 20
|
||||
|
||||
const selectedSite = computed(() => sites.value.find(site => site.id === siteId.value))
|
||||
|
||||
// 记住每个管理员上次查看的分站,下次进页面直接落到该站点
|
||||
const siteMemoryKey = computed(() => `admin_distro_qrcode_last_site_${adminSession.adminId || 0}`)
|
||||
|
||||
function readRememberedSiteId() {
|
||||
try {
|
||||
const raw = localStorage.getItem(siteMemoryKey.value)
|
||||
const parsed = Number(raw)
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
function rememberSiteId(id?: number) {
|
||||
try {
|
||||
if (id) localStorage.setItem(siteMemoryKey.value, String(id))
|
||||
else localStorage.removeItem(siteMemoryKey.value)
|
||||
} catch {
|
||||
// localStorage 不可用(隐私模式等)时静默降级,不影响使用
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSites() {
|
||||
try {
|
||||
sites.value = await fetchDistributionSites()
|
||||
const firstSite = sites.value[0]
|
||||
if (!siteId.value && firstSite) {
|
||||
siteId.value = firstSite.id
|
||||
}
|
||||
const rememberedId = readRememberedSiteId()
|
||||
const rememberedSite = rememberedId
|
||||
? sites.value.find(site => site.id === rememberedId)
|
||||
: undefined
|
||||
// 记忆站点已不存在(被删除/无权限)时回退到第一个站点
|
||||
siteId.value = rememberedSite?.id ?? sites.value[0]?.id
|
||||
if (siteId.value) rememberSiteId(siteId.value)
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '加载分销站点失败'))
|
||||
}
|
||||
@@ -98,7 +156,12 @@ async function loadPool() {
|
||||
try {
|
||||
const [list, nextStats] = await Promise.all([
|
||||
fetchSiteQrCodes(siteId.value, {
|
||||
status: statusFilter.value || undefined,
|
||||
status: filters.status || undefined,
|
||||
keyword: filters.keyword.trim() || undefined,
|
||||
bound: filters.bound || undefined,
|
||||
expire_status: filters.expire_status || undefined,
|
||||
created_start: filterDateToISO(filters.created_range?.[0]),
|
||||
created_end: filterDateToISO(filters.created_range?.[1]),
|
||||
page: currentPage.value,
|
||||
limit: currentPageSize.value,
|
||||
}),
|
||||
@@ -113,6 +176,7 @@ async function loadPool() {
|
||||
}
|
||||
|
||||
function handleSiteChange() {
|
||||
rememberSiteId(siteId.value)
|
||||
currentPage.value = 1
|
||||
loadPool()
|
||||
}
|
||||
@@ -122,6 +186,15 @@ function handleFilter() {
|
||||
loadPool()
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
filters.keyword = ''
|
||||
filters.status = ''
|
||||
filters.bound = ''
|
||||
filters.expire_status = ''
|
||||
filters.created_range = []
|
||||
handleFilter()
|
||||
}
|
||||
|
||||
async function recognizeGroupName(file: File) {
|
||||
const { job_id } = await submitOCRJob(file)
|
||||
const delays = [1000, 2000, 3000, 3000, 3000]
|
||||
@@ -294,7 +367,7 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="statusFilter"
|
||||
v-model="filters.status"
|
||||
placeholder="状态"
|
||||
clearable
|
||||
class="status-select"
|
||||
@@ -307,6 +380,56 @@ onBeforeUnmount(() => {
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="filters.keyword"
|
||||
clearable
|
||||
class="keyword-input"
|
||||
:prefix-icon="Search"
|
||||
placeholder="群名 / 备注 / 绑定群 / ID"
|
||||
@clear="handleFilter"
|
||||
@keyup.enter="handleFilter"
|
||||
/>
|
||||
<el-select
|
||||
v-model="filters.bound"
|
||||
placeholder="绑定群"
|
||||
clearable
|
||||
class="compact-select"
|
||||
@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-select"
|
||||
@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>
|
||||
<el-button :icon="Refresh" @click="loadPool">刷新</el-button>
|
||||
</div>
|
||||
|
||||
@@ -466,12 +589,25 @@ onBeforeUnmount(() => {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.site-select {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.status-select {
|
||||
width: 140px;
|
||||
.status-select,
|
||||
.compact-select {
|
||||
width: 130px;
|
||||
}
|
||||
|
||||
.keyword-input {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.date-range-filter {
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.stats div {
|
||||
|
||||
Reference in New Issue
Block a user