fix(ui): StaffDetail分页查找+SmsList日期筛选+SyncStatus自动刷新
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user