Files
ClassHomeworkBoard/src/components/InitServiceChooser.vue
T
Henry_Du be2e979252
Azure Static Web Apps CI/CD / Close Pull Request Job (push) Skipped
Azure Static Web Apps CI/CD / Build and Deploy Job (push) Failing after 18m55s
Build and Deploy / build (push) Failing after 18m56s
style(ui): 苹果风格二次打磨,统一圆角与材质并移除残留位移动效
- 去除卡片内联硬编码圆角(6px/8px)与固定色值,统一走主题变量与 12px 圆角

- 笔记卡片取消上浮投影,改用按压与填充反馈;版权角标、滚动条、拖拽条改为随主题取色

- 全局补充:遮罩轻微虚化、输入框聚焦光晕、卡片标题排版、选中文本强调色

- 设置卡片的悬停投影改为描边反馈;修正 settings.scss 中无效的 rgb(a, x) 写法

- 服务选择入口去掉 2px 撞色描边与悬停投影,改为细描边与极淡渐变
2026-10-24 08:11:32 +08:00

465 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div
v-if="visible"
class="init-overlay"
>
<div class="init-container">
<div class="init-header">
<div class="title">
欢迎使用 Classworks
</div>
<div class="subtitle">
请选择你的使用方式
</div>
</div>
<!-- 主要选择卡片 -->
<div class="main-card-row">
<!-- 初次使用 -->
<v-card
class="main-service-card gradient-new clickable"
elevation="4"
@click="showGuideDialog = true"
>
<v-card-item>
<div class="card-horizontal-layout">
<div class="card-icon-wrapper">
<v-icon
color="primary"
size="48"
>
mdi-new-box
</v-icon>
</div>
<div class="card-content">
<div class="text-h6 font-weight-bold">
初次使用
</div>
<div class="text-body-2 text-medium-emphasis mt-1">
了解 Classworks KV 并开始使用
</div>
</div>
</div>
</v-card-item>
</v-card>
<!-- 已注册设备 -->
<v-card
class="main-service-card gradient-registered clickable"
elevation="4"
@click="showDeviceAuthDialog = true"
>
<v-card-item>
<div class="card-horizontal-layout">
<div class="card-icon-wrapper">
<v-icon
color="success"
size="48"
>
mdi-account-check
</v-icon>
</div>
<div class="card-content">
<div class="text-h6 font-weight-bold">
已注册
</div>
<div class="text-body-2 text-medium-emphasis mt-1">
使用设备 Namespace 登录
</div>
</div>
</div>
</v-card-item>
</v-card>
<!-- Classworks KV 控制台 -->
<v-card
class="main-service-card clickable"
elevation="4"
@click="openClassworksKV"
>
<v-card-item>
<div class="card-horizontal-layout">
<div class="card-icon-wrapper">
<v-icon
color="info"
size="48"
>
mdi-database-cog
</v-icon>
</div>
<div class="card-content">
<div class="text-h6 font-weight-bold">
Classworks KV
</div>
<div class="text-body-2 text-medium-emphasis mt-1">
打开云端控制台管理数据
</div>
</div>
</div>
</v-card-item>
</v-card>
</div>
<div class="options-buttons">
<v-btn
prepend-icon="mdi-laptop"
size="small"
variant="tonal"
@click="useLocalMode"
>
使用本地模式
</v-btn>
<v-btn
prepend-icon="mdi-flash"
size="small"
variant="tonal"
@click="handleAutoAuthorize"
>
授权码式授权(弃用)
</v-btn>
<v-btn
prepend-icon="mdi-key"
size="small"
variant="tonal"
@click="showTokenDialog = true"
>
输入 Token
</v-btn>
<v-btn
prepend-icon="mdi-code-tags"
size="small"
variant="tonal"
@click="showAlternativeCodeDialog = true"
>
输入替代代码
</v-btn>
</div>
<div class="footer-hint">
完成授权后可使用作业同步、考试看板等在线功能。
</div>
</div>
<!-- 对话框 -->
<v-dialog
v-model="showGuideDialog"
max-width="600"
>
<FirstTimeGuide
@close="showGuideDialog = false"
@success="handleGuideSuccess"
/>
</v-dialog>
<v-dialog
v-model="showDeviceAuthDialog"
max-width="500"
>
<DeviceAuthDialog
ref="deviceAuthDialog"
:preconfig="deviceAuthPreconfig"
:show-cancel="true"
@cancel="showDeviceAuthDialog = false"
@success="handleAuthSuccess"
/>
</v-dialog>
<v-dialog
v-model="showTokenDialog"
max-width="500"
>
<TokenInputDialog
:show-cancel="true"
@cancel="showTokenDialog = false"
@success="handleTokenSuccess"
/>
</v-dialog>
<v-dialog
v-model="showAlternativeCodeDialog"
max-width="500"
>
<AlternativeCodeDialog
:show-cancel="true"
@cancel="showAlternativeCodeDialog = false"
@submit="handleAlternativeCodeSubmit"
/>
</v-dialog>
</div>
</template>
<script setup>
import {ref, computed, onMounted, watch} from 'vue'
import {getSetting, setSetting} from '@/utils/settings'
import DeviceAuthDialog from './auth/DeviceAuthDialog.vue'
import TokenInputDialog from './auth/TokenInputDialog.vue'
import AlternativeCodeDialog from './auth/AlternativeCodeDialog.vue'
import FirstTimeGuide from './auth/FirstTimeGuide.vue'
const props = defineProps({
preconfig: {
type: Object,
default: () => ({
namespace: null,
authCode: null,
autoOpen: false,
autoExecute: false
})
}
})
const emit = defineEmits(['done'])
// 控制显示:仅首页且无 kvToken(且 provider 不是 kv-local)显示
const visible = ref(false)
// 对话框控制
const showGuideDialog = ref(false)
const showDeviceAuthDialog = ref(false)
const showTokenDialog = ref(false)
const showAlternativeCodeDialog = ref(false)
// 设备认证对话框引用
const deviceAuthDialog = ref(null)
const provider = computed(() => getSetting('server.provider'))
const isKvProvider = computed(() => provider.value === 'kv-server' || provider.value === 'classworkscloud')
const kvToken = computed(() => getSetting('server.kvToken'))
// 设备认证预配置数据
const deviceAuthPreconfig = computed(() => {
if (props.preconfig?.namespace) {
return {
namespace: props.preconfig.namespace,
password: props.preconfig.authCode || '',
autoExecute: props.preconfig.autoExecute || false
}
}
return null
})
const evaluateVisibility = () => {
const path = window.location.pathname
const onHome = path === '/' || path === '/index' || path === '/index.html'
const need = isKvProvider.value && (!kvToken.value || kvToken.value === '')
visible.value = onHome && need
}
// 监听预配数据变化,自动打开设备认证对话框
watch(
() => props.preconfig,
(newPreconfig) => {
if (newPreconfig?.autoOpen && newPreconfig?.namespace && visible.value) {
console.log('检测到预配数据,自动打开设备认证对话框')
// 延迟一下确保组件已完全挂载
setTimeout(() => {
showDeviceAuthDialog.value = true
}, 500)
}
},
{immediate: true, deep: true}
)
onMounted(() => {
evaluateVisibility()
})
const handleAutoAuthorize = () => {
const authDomain = getSetting('server.authDomain')
const appId = 'd158067f53627d2b98babe8bffd2fd7d'
const currentDomain = window.location.origin
const callbackUrl = encodeURIComponent(`${currentDomain}/authorizecallback`)
const uuid = getSetting('device.uuid') || '00000000-0000-4000-8000-000000000000'
let url = `${authDomain}/authorize?app_id=${appId}&mode=callback&callback_url=${callbackUrl}&remark=Classworks 自动授权 来自${window.location.hostname} ${new Date().toLocaleString()}`
if (uuid !== '00000000-0000-4000-8000-000000000000') {
url += `&uuid=${encodeURIComponent(uuid)}`
}
window.location.href = url
}
const handleGuideSuccess = (tokenData) => {
showGuideDialog.value = false
console.log('渐进式注册成功:', tokenData)
evaluateVisibility()
emit('done')
}
const handleAuthSuccess = (tokenData) => {
showDeviceAuthDialog.value = false
console.log('认证成功:', tokenData)
// 如果是通过预配数据成功的,显示成功消息
if (props.preconfig?.namespace) {
// 可以在这里添加成功提示
console.log(`预配数据认证成功: ${props.preconfig.namespace}`)
}
evaluateVisibility()
emit('done')
}
const handleTokenSuccess = () => {
showTokenDialog.value = false
evaluateVisibility()
emit('done')
}
const handleAlternativeCodeSubmit = (code) => {
console.log('替代代码:', code)
// TODO: 实现替代代码逻辑
showAlternativeCodeDialog.value = false
}
const useLocalMode = () => {
setSetting('server.provider', 'kv-local')
visible.value = false
// 轻量刷新以让首页数据源切换
window.location.reload()
emit('done')
}
const openClassworksKV = () => {
window.open(getSetting('server.authDomain'), '_blank')
}
</script>
<style scoped>
.init-overlay {
position: relative;
}
.init-container {
max-width: 900px;
margin: 24px auto;
padding: 8px 16px;
}
.init-header .title {
font-size: 28px;
font-weight: 700;
text-align: left;
margin-bottom: 8px;
}
.init-header .subtitle {
font-size: 14px;
opacity: .75;
text-align: left;
}
/* 主要卡片 */
.main-card-row {
display: flex;
flex-direction: column;
gap: 16px;
margin-top: 32px;
}
.main-service-card {
min-height: 100px;
cursor: pointer;
transition: background-color 0.22s var(--apple-ease-out),
border-color 0.22s var(--apple-ease-out),
transform 0.14s var(--apple-ease-out);
}
.main-service-card:hover {
border-color: var(--apple-hairline-strong) !important;
}
.main-service-card:active {
transform: scale(0.995);
transition-duration: 0.08s;
}
.main-service-card .v-card-item {
padding: 20px 24px;
}
.card-horizontal-layout {
display: flex;
align-items: center;
gap: 20px;
}
.card-icon-wrapper {
flex-shrink: 0;
}
.card-content {
flex: 1;
text-align: left;
}
/* 三档入口:取自主题强调色,只做极淡的底色渐变与一道细描边 */
.gradient-new {
background-image: linear-gradient(135deg, rgba(var(--v-theme-primary), 0.12), transparent 70%);
border: 1px solid rgba(var(--v-theme-primary), 0.26);
}
.gradient-registered {
background-image: linear-gradient(135deg, rgba(var(--v-theme-success), 0.12), transparent 70%);
border: 1px solid rgba(var(--v-theme-success), 0.26);
}
.gradient-kv {
background-image: linear-gradient(135deg, rgba(var(--v-theme-info), 0.12), transparent 70%);
border: 1px solid rgba(var(--v-theme-info), 0.26);
}
/* 其他选项 */
.alternative-options {
margin-top: 40px;
padding: 20px;
background: rgba(var(--v-theme-surface-variant), 0.3);
border-radius: 12px;
}
.options-title {
font-size: 14px;
font-weight: 600;
opacity: 0.8;
margin-bottom: 12px;
text-align: left;
}
.options-buttons {
margin-top: 24px;
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: flex-start;
}
.clickable {
cursor: pointer;
}
.footer-hint {
margin-top: 24px;
font-size: 13px;
opacity: .7;
text-align: left;
}
@media (max-width: 768px) {
.card-horizontal-layout {
gap: 16px;
}
.card-icon-wrapper .v-icon {
font-size: 40px !important;
}
.options-buttons {
flex-direction: column;
}
.options-buttons .v-btn {
width: 100%;
}
}
</style>