🐛 调整两个版本邮件格式

This commit is contained in:
sugarBecomer
2026-03-24 22:13:01 +08:00
parent ddcbe65856
commit 4033153037
3 changed files with 401 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
<script setup lang="ts">
import { useLoadingBar } from '@/composables/useLoadingBar'
const { progress, visible, status } = useLoadingBar()
</script>
<template>
<Transition name="loading-bar">
<div
v-if="visible"
class="fixed top-0 left-0 right-0 z-[9999] h-[2px] pointer-events-none"
>
<div
class="h-full transition-all duration-200 ease-out"
:class="[
status === 'error' ? 'bg-destructive' : 'bg-primary',
]"
:style="{ width: progress + '%' }"
>
<div
v-if="status === 'loading'"
class="absolute right-0 top-0 h-full w-24 opacity-60 blur-sm bg-primary"
/>
</div>
</div>
</Transition>
</template>
<style scoped>
.loading-bar-leave-active {
transition: opacity 0.3s ease;
}
.loading-bar-leave-to {
opacity: 0;
}
</style>
+53
View File
@@ -0,0 +1,53 @@
import { ref } from 'vue'
const progress = ref(0)
const visible = ref(false)
const status = ref<'loading' | 'done' | 'error'>('loading')
let timer: ReturnType<typeof setInterval> | null = null
function clearTimer() {
if (timer) {
clearInterval(timer)
timer = null
}
}
function start() {
clearTimer()
progress.value = 0
status.value = 'loading'
visible.value = true
// 模拟进度:快速到 80%,然后减速
timer = setInterval(() => {
if (progress.value < 80) {
progress.value += (80 - progress.value) * 0.1
} else if (progress.value < 95) {
progress.value += 0.5
}
}, 100)
}
function finish() {
clearTimer()
status.value = 'done'
progress.value = 100
setTimeout(() => {
visible.value = false
progress.value = 0
}, 300)
}
function error() {
clearTimer()
status.value = 'error'
progress.value = 100
setTimeout(() => {
visible.value = false
progress.value = 0
}, 800)
}
export function useLoadingBar() {
return { progress, visible, status, start, finish, error }
}
+312
View File
@@ -0,0 +1,312 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { cpUserApi } from '@/api/cpdaily'
const route = useRoute()
const uuid = route.params.uuid as string
type PageState = 'loading' | 'invalid' | 'not-cpdaily' | 'fetching' | 'success' | 'error'
const state = ref<PageState>('loading')
const deviceCode = ref('')
const errorMsg = ref('')
const copied = ref(false)
function isCpdaily(): boolean {
const ua = navigator.userAgent.toLowerCase()
return ua.includes('cpdaily') || ua.includes('wisedu') || ua.includes('campushoy')
}
async function copyCode() {
if (!deviceCode.value) return
try {
await navigator.clipboard.writeText(deviceCode.value)
} catch {
const el = document.createElement('textarea')
el.value = deviceCode.value
el.style.cssText = 'position:fixed;opacity:0'
document.body.appendChild(el)
el.select()
document.execCommand('copy')
document.body.removeChild(el)
}
copied.value = true
setTimeout(() => { copied.value = false }, 2000)
}
function loadCordova(): Promise<void> {
return new Promise((resolve, reject) => {
const isAndroid = /Android|Linux/.test(navigator.userAgent)
const src = 'https://static.campushoy.com/innerjs/'
+ (isAndroid ? 'cordova_android.js' : 'cordova_ios.js')
const s = document.createElement('script')
s.src = src
s.onload = () => resolve()
s.onerror = () => reject(new Error('脚本加载失败'))
document.head.appendChild(s)
if (!(window as any).mamp) (window as any).mamp = {}
;(window as any).mamp.initEnv = (cb: () => void) =>
document.addEventListener('deviceready', cb, false)
})
}
function waitCordova(): Promise<void> {
return new Promise((resolve, reject) => {
let n = 0
const t = setInterval(() => {
if ((window as any).cordova?.exec) {
clearInterval(t)
resolve()
} else if (++n > 600) {
clearInterval(t)
reject(new Error('Cordova 加载超时'))
}
}, 100)
})
}
function getDeviceUuid(): Promise<string> {
return new Promise((resolve, reject) => {
;(window as any).cordova.exec(
(res: any) => resolve(res.uuid),
(err: any) => reject(new Error(err?.message || String(err))),
'MAMPDevice', 'getDeviceInfo', []
)
})
}
onMounted(async () => {
// 1. 验证UUID有效性
try {
await cpUserApi.checkQr(uuid)
} catch {
state.value = 'invalid'
return
}
// 2. 检测是否在今日校园App内
if (!isCpdaily()) {
state.value = 'not-cpdaily'
return
}
// 3. 加载cordova获取设备码
state.value = 'fetching'
try {
await loadCordova()
await waitCordova()
deviceCode.value = await getDeviceUuid()
state.value = 'success'
} catch (e: any) {
errorMsg.value = e.message || '未知错误'
state.value = 'error'
}
})
</script>
<template>
<div class="authorize-page">
<div class="aurora-orb aurora-orb-1" />
<div class="aurora-orb aurora-orb-2" />
<div class="container">
<div class="glass-card">
<!-- 加载中 -->
<template v-if="state === 'loading'">
<div class="icon-wrap icon-loading">📱</div>
<h1>正在验证...</h1>
<p class="desc">请稍候</p>
<div class="spinner" />
</template>
<!-- 二维码无效 -->
<template v-else-if="state === 'invalid'">
<div class="icon-wrap icon-error">✕</div>
<h1>二维码无效</h1>
<p class="err-text">二维码无效或已过期,请重新获取</p>
</template>
<!-- 非今日校园App -->
<template v-else-if="state === 'not-cpdaily'">
<div class="icon-wrap icon-warn">📵</div>
<h1>请用今日校园扫码</h1>
<p class="desc">此页面需要在<strong>今日校园 App</strong>内打开才能获取设备码</p>
<div class="steps">
<div class="step">
<div class="step-num">1</div>
<div class="step-text">打开今日校园 App<span>在手机上启动今日校园</span></div>
</div>
<div class="step">
<div class="step-num">2</div>
<div class="step-text">点击右上角扫一扫<span>使用 App 内置扫码功能</span></div>
</div>
<div class="step">
<div class="step-num">3</div>
<div class="step-text">扫描电脑上的二维码<span>扫码后设备码将自动显示</span></div>
</div>
</div>
</template>
<!-- 正在获取设备码 -->
<template v-else-if="state === 'fetching'">
<div class="icon-wrap icon-loading">📱</div>
<h1>正在获取设备码</h1>
<p class="desc">请稍候,正在读取设备信息...</p>
<div class="spinner" />
</template>
<!-- 获取成功 -->
<template v-else-if="state === 'success'">
<div class="icon-wrap icon-success">✓</div>
<h1>获取成功</h1>
<p class="desc">已成功获取设备码,请复制后转发给系统管理员</p>
<div class="result-section">
<div class="result-label">设备码</div>
<div class="code-box">{{ deviceCode }}</div>
<button class="btn-primary" @click="copyCode">
<span>📋</span>
<span>{{ copied ? '已复制 ✓' : '复制设备码' }}</span>
</button>
</div>
<p class="footer-note">请及时复制,关闭页面后需重新扫码获取</p>
</template>
<!-- 获取失败 -->
<template v-else-if="state === 'error'">
<div class="icon-wrap icon-error">✕</div>
<h1>获取失败</h1>
<p class="err-text">{{ errorMsg }}</p>
</template>
</div>
</div>
</div>
</template>
<style scoped>
.authorize-page {
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "PingFang SC", "Helvetica Neue", Arial, sans-serif;
background: #F5F5F7;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow-x: hidden;
position: relative;
}
.aurora-orb {
position: fixed;
border-radius: 50%;
filter: blur(100px);
opacity: 0.6;
pointer-events: none;
z-index: 0;
animation: float 20s ease-in-out infinite;
}
.aurora-orb-1 {
width: 60vw; height: 60vw;
background: radial-gradient(circle, rgba(162,210,255,0.4), transparent);
top: -20%; left: -10%;
}
.aurora-orb-2 {
width: 55vw; height: 55vw;
background: radial-gradient(circle, rgba(200,180,255,0.3), transparent);
bottom: -20%; right: -10%;
animation-delay: -10s;
}
@keyframes float {
0%,100% { transform: translate(0,0) scale(1); }
50% { transform: translate(30px,-30px) scale(1.1); }
}
.container {
position: relative;
z-index: 1;
width: 100%;
max-width: 380px;
padding: 20px;
}
.glass-card {
background: rgba(255,255,255,0.82);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border: 1px solid rgba(255,255,255,0.6);
border-radius: 24px;
box-shadow: 0 8px 32px rgba(0,0,0,0.08);
padding: 36px 28px 32px;
text-align: center;
animation: fadeInUp 0.6s cubic-bezier(0.25,1,0.5,1) 0.05s backwards;
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(24px); }
to { opacity: 1; transform: translateY(0); }
}
.icon-wrap {
width: 72px; height: 72px;
border-radius: 20px;
display: flex; align-items: center; justify-content: center;
font-size: 36px;
margin: 0 auto 20px;
}
.icon-loading { background: linear-gradient(135deg,#0071e3,#40a9ff); box-shadow: 0 4px 16px rgba(0,113,227,0.3); }
.icon-warn { background: linear-gradient(135deg,#ff9f0a,#ffcc02); box-shadow: 0 4px 16px rgba(255,159,10,0.3); }
.icon-success { background: linear-gradient(135deg,#34c759,#30d158); box-shadow: 0 4px 16px rgba(52,199,89,0.3); }
.icon-error { background: linear-gradient(135deg,#ff3b30,#ff6961); box-shadow: 0 4px 16px rgba(255,59,48,0.3); }
h1 { font-size: 22px; font-weight: 700; letter-spacing: -0.02em; color: #1d1d1f; margin-bottom: 10px; }
.desc { font-size: 15px; color: #86868b; line-height: 1.6; }
.desc strong { color: #1d1d1f; font-weight: 600; }
.spinner {
width: 36px; height: 36px;
border: 3px solid rgba(0,113,227,0.15);
border-top-color: #0071e3;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 20px auto 0;
}
@keyframes spin { to { transform: rotate(360deg); } }
.steps { margin: 20px 0 0; display: flex; flex-direction: column; gap: 10px; text-align: left; }
.step {
display: flex; align-items: flex-start; gap: 12px;
background: rgba(0,0,0,0.03); border-radius: 12px;
padding: 12px 14px; border: 1px solid rgba(0,0,0,0.05);
}
.step-num {
width: 22px; height: 22px; border-radius: 50%;
background: #0071e3; color: white;
font-size: 12px; font-weight: 700;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0; margin-top: 1px;
}
.step-text { font-size: 14px; color: #1d1d1f; line-height: 1.5; }
.step-text span { color: #86868b; font-size: 13px; display: block; margin-top: 2px; }
.err-text { font-size: 15px; color: #d70015; margin-top: 16px; line-height: 1.5; }
.result-section {
margin-top: 20px;
display: flex; flex-direction: column; gap: 14px;
}
.result-label {
font-size: 11px; font-weight: 600;
letter-spacing: 0.07em; text-transform: uppercase;
color: #86868b; text-align: left;
}
.code-box {
background: #F2F2F7; border-radius: 12px;
padding: 14px 16px;
font-family: "SF Mono", "Fira Code", monospace;
font-size: 13px; color: #1d1d1f;
word-break: break-all; line-height: 1.7;
border: 1px solid rgba(0,0,0,0.06);
user-select: all; text-align: left;
}
.btn-primary {
background: #0071e3; color: white;
padding: 13px 24px; border-radius: 980px; border: none;
font-size: 15px; font-weight: 500; cursor: pointer;
transition: all 0.3s cubic-bezier(0.25,1,0.5,1);
width: 100%;
display: flex; align-items: center; justify-content: center; gap: 8px;
}
.btn-primary:hover { transform: scale(1.02); box-shadow: 0 8px 24px rgba(0,113,227,0.3); filter: brightness(1.08); }
.btn-primary:active { transform: scale(0.98); }
.footer-note {
text-align: center; font-size: 13px; color: #86868b;
margin-top: 16px; line-height: 1.5;
}
</style>