设备码授权页:展示设备码、过期进度、设备信息、风险提示,并支持确认授权 / 复制代码 / 拒绝。
<script setup lang="ts">
import { computed, ref } from 'vue';
import { CfBanner, CfButton, CfCard, CfProgress, CfTag } from '@chufix-design/vue';
const code = 'K9QF-4M2D';
const copied = ref(false);
const approved = ref(false);
const remainingSeconds = 12 * 60 + 18;
const progress = computed(() => Math.round((remainingSeconds / (15 * 60)) * 100));
function copyCode() {
copied.value = true;
window.setTimeout(() => {
copied.value = false;
}, 1200);
}
function approve() {
approved.value = true;
}
</script>
<template>
<div class="device-auth">
<section class="device-auth__shell">
<div class="device-auth__visual" aria-hidden="true">
<div class="device-auth__halo" />
<div class="device-auth__device device-auth__device--tv">
<span class="device-auth__dot" />
<span class="device-auth__dot" />
<span class="device-auth__dot" />
<strong>{{ code }}</strong>
<small>waiting for browser approval</small>
</div>
<div class="device-auth__link">
<span />
</div>
<div class="device-auth__device device-auth__device--phone">
<span class="device-auth__phone-bar" />
<span class="device-auth__phone-check">✓</span>
<small>trusted session</small>
</div>
</div>
<CfCard class="device-auth__card">
<div class="device-auth__eyebrow">
<CfTag tone="info" variant="soft">Device login</CfTag>
<span>12:18 后过期</span>
</div>
<h2>授权新设备</h2>
<p class="device-auth__desc">
你正在为 ChuFix CLI 授权。确认设备屏幕上的代码一致后继续。
</p>
<div class="device-auth__code" aria-label="设备授权码">
<span v-for="part in code.split('-')" :key="part">{{ part }}</span>
</div>
<div class="device-auth__meter">
<CfProgress :value="progress" size="sm" tone="primary" />
<span>15 分钟有效期</span>
</div>
<CfBanner tone="neutral" :icon="false">
设备:ChuFix CLI · macOS · 上海附近 · 2026-05-10 11:42
</CfBanner>
<ol class="device-auth__steps">
<li>确认设备屏幕显示同一组代码。</li>
<li>授权后设备只会获得当前工作区的只读令牌。</li>
<li>如果你没有发起登录,请拒绝并重置当前账号会话。</li>
</ol>
<div class="device-auth__actions">
<CfButton variant="primary" @click="approve">{{ approved ? '已授权' : '确认授权' }}</CfButton>
<CfButton variant="secondary" @click="copyCode">{{ copied ? '已复制' : '复制代码' }}</CfButton>
<CfButton variant="tertiary">拒绝</CfButton>
</div>
</CfCard>
</section>
</div>
</template>
<style scoped>
.device-auth {
min-height: 640px;
padding: 32px;
font-family: var(--font-sans);
color: var(--fg-1);
}
.device-auth__shell {
display: grid;
grid-template-columns: minmax(320px, 1fr) minmax(360px, 460px);
align-items: center;
gap: 32px;
width: min(980px, 100%);
min-height: 560px;
margin: 0 auto;
}
.device-auth__visual {
position: relative;
min-height: 460px;
border: 1px solid var(--line-1);
border-radius: 24px;
overflow: hidden;
background:
radial-gradient(circle at 22% 18%, var(--accent-soft), transparent 30%),
linear-gradient(135deg, var(--bg-0), var(--bg-2));
}
.device-auth__halo {
position: absolute;
inset: 76px 56px;
border: 2px dashed color-mix(in srgb, var(--accent-1) 62%, transparent);
border-radius: 999px;
transform: rotate(-8deg);
}
.device-auth__device {
position: absolute;
display: grid;
gap: 8px;
border: 1px solid var(--line-2);
border-radius: 20px;
background: var(--bg-0);
box-shadow: 0 18px 40px color-mix(in srgb, var(--shadow-color) 16%, transparent);
}
.device-auth__device--tv {
left: 56px;
top: 92px;
width: 250px;
padding: 24px;
}
.device-auth__device--phone {
right: 58px;
bottom: 86px;
width: 150px;
padding: 22px 18px;
text-align: center;
}
.device-auth__dot {
display: inline-block;
width: 8px;
height: 8px;
margin-right: 6px;
border-radius: 999px;
background: var(--fg-3);
}
.device-auth__device strong {
margin-top: 18px;
font-family: var(--font-mono);
font-size: 34px;
letter-spacing: .04em;
color: var(--accent-1);
}
.device-auth__device small,
.device-auth__eyebrow span,
.device-auth__meter span {
font-size: var(--t-12);
color: var(--fg-3);
}
.device-auth__link {
position: absolute;
left: 306px;
top: 194px;
width: 128px;
height: 52px;
border-top: 5px solid color-mix(in srgb, var(--accent-1) 70%, transparent);
border-radius: 60% 60% 0 0;
}
.device-auth__link span {
position: absolute;
right: -5px;
top: -10px;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--status-success);
}
.device-auth__phone-bar {
width: 64px;
height: 8px;
margin: 0 auto 16px;
border-radius: 999px;
background: var(--line-3);
}
.device-auth__phone-check {
display: grid;
place-items: center;
width: 54px;
height: 54px;
margin: 0 auto 12px;
border-radius: 50%;
background: var(--status-success-soft, var(--accent-soft));
color: var(--status-success);
font-size: 30px;
font-weight: var(--w-semibold);
}
.device-auth__card {
padding: 28px;
}
.device-auth__eyebrow,
.device-auth__meter,
.device-auth__actions {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.device-auth__eyebrow {
justify-content: space-between;
}
.device-auth__card h2 {
margin: 18px 0 8px;
font-size: var(--t-28);
font-weight: var(--w-semibold);
}
.device-auth__desc {
margin: 0 0 20px;
color: var(--fg-2);
line-height: 1.6;
}
.device-auth__code {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
margin-bottom: 16px;
}
.device-auth__code span {
display: grid;
place-items: center;
min-height: 76px;
border: 1px solid var(--line-2);
border-radius: 16px;
background: var(--bg-1);
font-family: var(--font-mono);
font-size: 34px;
font-weight: var(--w-semibold);
color: var(--fg-1);
}
.device-auth__meter {
margin-bottom: 16px;
}
.device-auth__meter :deep(.cf-progress) {
flex: 1 1 180px;
}
.device-auth__steps {
display: grid;
gap: 10px;
margin: 18px 0 22px;
padding-left: 18px;
color: var(--fg-2);
font-size: var(--t-13);
line-height: 1.55;
}
@media (max-width: 820px) {
.device-auth {
padding: 20px;
}
.device-auth__shell {
grid-template-columns: 1fr;
}
.device-auth__visual {
min-height: 340px;
}
.device-auth__device--tv {
left: 28px;
top: 64px;
width: 220px;
}
.device-auth__device--phone {
right: 28px;
bottom: 52px;
}
}
</style>
import { useMemo, useState } from 'react';
import { CfBanner, CfButton, CfCard, CfProgress, CfTag } from '@chufix-design/react';
export function DeviceAuthorization() {
const code = 'K9QF-4M2D';
const [copied, setCopied] = useState(false);
const [approved, setApproved] = useState(false);
const progress = useMemo(() => Math.round(((12 * 60 + 18) / (15 * 60)) * 100), []);
function copyCode() {
navigator.clipboard?.writeText(code).catch(() => undefined);
setCopied(true);
window.setTimeout(() => setCopied(false), 1200);
}
return (
<div className="device-auth">
<section className="device-auth__shell">
<div className="device-auth__visual" aria-hidden="true">
<div className="device-auth__halo" />
<div className="device-auth__device device-auth__device--tv">
<span className="device-auth__dot" />
<span className="device-auth__dot" />
<span className="device-auth__dot" />
<strong>{code}</strong>
<small>waiting for browser approval</small>
</div>
<div className="device-auth__link"><span /></div>
<div className="device-auth__device device-auth__device--phone">
<span className="device-auth__phone-bar" />
<span className="device-auth__phone-check">✓</span>
<small>trusted session</small>
</div>
</div>
<CfCard className="device-auth__card">
<div className="device-auth__eyebrow">
<CfTag tone="info" variant="soft">Device login</CfTag>
<span>12:18 后过期</span>
</div>
<h2>授权新设备</h2>
<p className="device-auth__desc">
你正在为 ChuFix CLI 授权。确认设备屏幕上的代码一致后继续。
</p>
<div className="device-auth__code" aria-label="设备授权码">
{code.split('-').map((part) => <span key={part}>{part}</span>)}
</div>
<div className="device-auth__meter">
<CfProgress value={progress} size="sm" tone="primary" />
<span>15 分钟有效期</span>
</div>
<CfBanner tone="neutral" icon={false}>
设备:ChuFix CLI · macOS · 上海附近 · 2026-05-10 11:42
</CfBanner>
<ol className="device-auth__steps">
<li>确认设备屏幕显示同一组代码。</li>
<li>授权后设备只会获得当前工作区的只读令牌。</li>
<li>如果你没有发起登录,请拒绝并重置当前账号会话。</li>
</ol>
<div className="device-auth__actions">
<CfButton variant="primary" onClick={() => setApproved(true)}>{approved ? '已授权' : '确认授权'}</CfButton>
<CfButton variant="secondary" onClick={copyCode}>{copied ? '已复制' : '复制代码'}</CfButton>
<CfButton variant="tertiary">拒绝</CfButton>
</div>
</CfCard>
</section>
</div>
);
}