fix(ui): StaffDetail分页查找+SmsList日期筛选+SyncStatus自动刷新

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
浅念
2026-08-12 14:09:43 +08:00
parent 9a953553ec
commit 9e4a5ee536
3 changed files with 90 additions and 15 deletions
+22
View File
@@ -13,6 +13,9 @@ const pageSize = ref(20)
const loading = ref(false)
const errorMsg = ref<string | null>(null)
/** [start, end] as Date[] | null. null = no date filter (all SMS). */
const dateRange = ref<[Date, Date] | null>(null)
// All-fields drawer state.
const drawerOpen = ref(false)
const drawerSms = ref<Sms | null>(null)
@@ -24,6 +27,8 @@ async function load(): Promise<void> {
const res = await getSms({
page: page.value,
page_size: pageSize.value,
start: dateRange.value?.[0]?.getTime(),
end: dateRange.value?.[1]?.getTime(),
})
list.value = res.items
total.value = res.total
@@ -46,6 +51,12 @@ function handlePageSizeChange(s: number): void {
load()
}
/** New date range → reset to page 1 and reload. */
function onDateChange(): void {
page.value = 1
load()
}
/**
* Read a raw_json camelCase key off an SMS, preferring the structured
* snake_case column when it is populated. The first argument is the row so the
@@ -96,6 +107,17 @@ onMounted(load)
<h2 style="margin: 0 0 4px; font-size: 16px">短信记录</h2>
<span class="yk-sms__sub">共 {{ total }} 条</span>
</div>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
size="small"
clearable
style="width: 260px"
@change="onDateChange"
/>
</div>
<el-alert
+28 -12
View File
@@ -11,11 +11,10 @@
* difference is drawer-vs-page; reusing buildAccountSections keeps the DATA
* identical to the drawer (single source of truth for fields/groups/labels).
*
* Data is fetched client-side: getAccounts returns the union of both channels,
* and we match by `user_id` first (the structured column) falling back to
* `account_id`. The dataset is small (~50 rows), so a single page_size=999
* fetch is fine; a per-userId backend endpoint would be cleaner but is out of
* scope for this UX swap.
* Data is fetched client-side: getAccounts is queried per channel page by
* page until the user is found (matches `user_id` first, then `account_id`).
* Paginating avoids the previous page_size=999 hard cap that would break past
* 999 staff; we stop as soon as we have a match.
*/
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
@@ -99,14 +98,8 @@ async function load(): Promise<void> {
loading.value = true
notFound.value = false
try {
// getAccounts drops the channel param when undefined → union of both tabs.
// page_size=999 is plenty; the staff-account dataset is small (~50 rows).
const res = await getAccounts({ page: 1, page_size: 999 })
const id = props.userId
const match =
res.items.find((a) => a.user_id === id) ??
res.items.find((a) => a.account_id === id) ??
null
const match = await findAccountById(id)
if (!match) {
notFound.value = true
} else {
@@ -123,6 +116,29 @@ async function load(): Promise<void> {
}
}
/**
* Walk both channel tables page by page until we hit the requested id. Stops
* early on a match so the common case is a single round-trip; falls through
* to the next channel on exhaustion. Returns null if neither channel has it.
*/
async function findAccountById(id: string): Promise<Account | null> {
const pageSize = 100
for (const ch of ['wechat', 'whatsapp'] as const) {
let page = 1
while (true) {
const res = await getAccounts({ channel: ch, page, page_size: pageSize })
const found =
res.items.find((a: Account) => a.user_id === id) ??
res.items.find((a: Account) => a.account_id === id) ??
null
if (found) return found
if (res.items.length < pageSize) break // last page
page++
}
}
return null
}
function goBack(): void {
// router.back() lands on the originating list if it exists; fall back to
// the staff list for direct URL entry / refresh on the detail page.
+40 -3
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Refresh } from '@element-plus/icons-vue'
import { getSyncStatus } from '@/api'
@@ -11,6 +11,26 @@ const recentRuns = ref<SyncRun[]>([])
const loading = ref(false)
const errorMsg = ref<string | null>(null)
/** Auto-refresh toggle; ON by default, polls every 10s. */
const autoRefresh = ref(true)
let refreshTimer: ReturnType<typeof setInterval> | null = null
function startPolling(): void {
stopPolling()
if (autoRefresh.value) {
refreshTimer = setInterval(() => load(), 10000)
}
}
function stopPolling(): void {
if (refreshTimer) {
clearInterval(refreshTimer)
refreshTimer = null
}
}
watch(autoRefresh, (on) => (on ? startPolling() : stopPolling()))
async function load(): Promise<void> {
loading.value = true
errorMsg.value = null
@@ -47,7 +67,11 @@ function isoToTime(iso?: string | null): string {
return Number.isNaN(t) ? '--' : formatTime(t)
}
onMounted(load)
onMounted(() => {
load()
startPolling()
})
onUnmounted(stopPolling)
</script>
<template>
@@ -59,7 +83,14 @@ onMounted(load)
游标 {{ states.length }} 条 · 最近执行 {{ recentRuns.length }} 条
</span>
</div>
<el-button :icon="Refresh" size="small" @click="load">刷新</el-button>
<div class="yk-sync__actions">
<el-switch
v-model="autoRefresh"
active-text="自动刷新"
size="small"
/>
<el-button :icon="Refresh" size="small" @click="load">刷新</el-button>
</div>
</div>
<el-alert
@@ -157,6 +188,12 @@ onMounted(load)
font-size: 13px;
}
.yk-sync__actions {
display: flex;
align-items: center;
gap: 12px;
}
.yk-sync__section {
font-size: 14px;
font-weight: 600;