feat(frontend): show QR refresh in dialog

This commit is contained in:
2026-05-06 21:09:54 +08:00
parent 6afc5817a7
commit a17a913618
2 changed files with 69 additions and 53 deletions
+39 -32
View File
@@ -27,6 +27,7 @@ import { useRouter } from '@/app/router'
import StateBlock from '@/components/StateBlock.vue' import StateBlock from '@/components/StateBlock.vue'
import { alertClass, cardClass, inputClass, sectionHeaderClass, toneClass } from '@/components/ui' import { alertClass, cardClass, inputClass, sectionHeaderClass, toneClass } from '@/components/ui'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { import {
cronLabel, cronLabel,
@@ -58,6 +59,7 @@ const qrRefreshError = ref('')
const qrRefreshImage = ref('') const qrRefreshImage = ref('')
const qrRefreshSessionId = ref('') const qrRefreshSessionId = ref('')
const qrRefreshSucceeded = ref(false) const qrRefreshSucceeded = ref(false)
const qrRefreshDialogOpen = ref(false)
let pollTimer: number | undefined let pollTimer: number | undefined
let qrRefreshPollTimer: number | undefined let qrRefreshPollTimer: number | undefined
@@ -179,9 +181,24 @@ async function cancelQrRefresh(clearFeedback = true) {
if (sessionId) await authApi.cancelQRCodeSession(sessionId).catch(() => undefined) if (sessionId) await authApi.cancelQRCodeSession(sessionId).catch(() => undefined)
} }
async function closeQrRefreshDialog() {
qrRefreshDialogOpen.value = false
await cancelQrRefresh()
}
async function handleQrRefreshDialogOpenChange(open: boolean) {
if (open) {
qrRefreshDialogOpen.value = true
return
}
await closeQrRefreshDialog()
}
async function requestQrRefresh() { async function requestQrRefresh() {
if (!canRefreshToken.value || qrRefreshLoading.value) return if (!canRefreshToken.value || qrRefreshLoading.value) return
qrRefreshDialogOpen.value = true
const alias = auth.state.user?.alias?.trim() const alias = auth.state.user?.alias?.trim()
if (!alias) { if (!alias) {
qrRefreshError.value = '当前用户缺少用户名,无法创建扫码刷新会话。' qrRefreshError.value = '当前用户缺少用户名,无法创建扫码刷新会话。'
@@ -443,40 +460,35 @@ onBeforeUnmount(() => {
<QrCode class="size-4" /> <QrCode class="size-4" />
{{ qrRefreshLoading ? '创建中' : '扫码刷新' }} {{ qrRefreshLoading ? '创建中' : '扫码刷新' }}
</Button> </Button>
<div </div>
v-if="qrRefreshImage || qrRefreshInfo || qrRefreshError" </div>
class="grid gap-3 rounded-lg border border-border bg-muted p-3" </section>
>
<div class="flex items-start justify-between gap-3"> <Dialog :open="qrRefreshDialogOpen" @update:open="handleQrRefreshDialogOpenChange">
<div> <DialogContent class="gap-0 overflow-hidden p-0 sm:max-w-[420px]">
<div class="font-medium text-foreground">扫码刷新授权</div> <DialogHeader class="border-b border-border bg-muted/55 px-5 py-4">
<div v-if="qrRefreshInfo" class="mt-1 text-muted-foreground"> <DialogTitle class="flex items-center gap-2">
<QrCode class="size-5" />
扫码刷新授权
</DialogTitle>
</DialogHeader>
<div class="grid gap-4 p-5 text-sm">
<div v-if="qrRefreshInfo" class="text-muted-foreground">
{{ qrRefreshInfo }} {{ qrRefreshInfo }}
</div> </div>
</div>
<Button
v-if="qrRefreshSessionId || qrRefreshImage"
variant="ghost"
size="icon"
type="button"
aria-label="关闭扫码刷新"
@click="cancelQrRefresh"
>
<X class="size-4" />
</Button>
</div>
<div v-if="qrRefreshError" :class="alertClass.danger"> <div v-if="qrRefreshError" :class="alertClass.danger">
{{ qrRefreshError }} {{ qrRefreshError }}
</div> </div>
<div v-if="qrRefreshSucceeded" :class="alertClass.success">授权刷新成功</div> <div v-if="qrRefreshSucceeded" :class="alertClass.success">授权刷新成功</div>
<div v-if="qrRefreshImage" class="rounded-lg border border-border bg-background p-3"> <div v-if="qrRefreshImage" class="rounded-lg border border-border bg-background p-4">
<img <img
:src="qrRefreshImageSrc" :src="qrRefreshImageSrc"
alt="QQ 授权刷新二维码" alt="QQ 授权刷新二维码"
class="mx-auto size-44 rounded-md bg-background object-contain" class="mx-auto size-56 max-w-full rounded-md bg-background object-contain"
/> />
</div> </div>
<div class="flex flex-wrap gap-2"> <StateBlock v-else-if="qrRefreshLoading" title="正在创建二维码" type="loading" />
<div class="flex flex-wrap justify-end gap-2">
<Button <Button
variant="outline" variant="outline"
type="button" type="button"
@@ -486,19 +498,14 @@ onBeforeUnmount(() => {
<RotateCw class="size-4" /> <RotateCw class="size-4" />
重新获取 重新获取
</Button> </Button>
<Button <Button variant="ghost" type="button" @click="closeQrRefreshDialog">
v-if="qrRefreshSessionId || qrRefreshImage" <X class="size-4" />
variant="ghost"
type="button"
@click="cancelQrRefresh"
>
取消 取消
</Button> </Button>
</div> </div>
</div> </div>
</div> </DialogContent>
</div> </Dialog>
</section>
<section :class="[cardClass, 'overflow-hidden']"> <section :class="[cardClass, 'overflow-hidden']">
<div :class="sectionHeaderClass"> <div :class="sectionHeaderClass">
+9
View File
@@ -101,3 +101,12 @@ def test_dashboard_refresh_uses_qr_api_instead_of_login_redirect() -> None:
assert "authApi.getQRCodeStatus" in dashboard assert "authApi.getQRCodeStatus" in dashboard
assert "authApi.cancelQRCodeSession" in dashboard assert "authApi.cancelQRCodeSession" in dashboard
assert "router.navigate('/login')" not in dashboard assert "router.navigate('/login')" not in dashboard
def test_dashboard_qr_refresh_uses_dialog() -> None:
dashboard = (SRC_ROOT / "views" / "DashboardView.vue").read_text(encoding="utf-8")
assert "@/components/ui/dialog" in dashboard
assert "qrRefreshDialogOpen" in dashboard
assert "<Dialog" in dashboard
assert "<DialogContent" in dashboard