mirror of
https://github.com/Cccc-owo/CheckInApp.git
synced 2026-06-17 05:56:29 +00:00
feat: add Token refresh button and optimize Navbar
This commit is contained in:
@@ -64,4 +64,22 @@ body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* 修复按钮图标与文本的垂直对齐 */
|
||||
.ant-btn {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
.ant-btn .anticon {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
line-height: 1 !important;
|
||||
}
|
||||
|
||||
.ant-btn > span {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
<template>
|
||||
<div class="sticky top-0 z-50 md3-surface elevation-2 border-b border-outline-variant">
|
||||
<div
|
||||
class="navbar-wrapper sticky top-0 z-50"
|
||||
:style="{
|
||||
backgroundColor: isDark ? '#1c1b1f' : '#ffffff',
|
||||
boxShadow: isDark
|
||||
? '0 2px 8px rgba(0, 0, 0, 0.6), 0 4px 16px rgba(0, 0, 0, 0.4)'
|
||||
: '0 2px 8px rgba(0, 0, 0, 0.15), 0 4px 16px rgba(0, 0, 0, 0.1)',
|
||||
borderBottom: isDark ? '1px solid rgba(255, 255, 255, 0.1)' : '1px solid rgba(0, 0, 0, 0.08)',
|
||||
}"
|
||||
>
|
||||
<nav class="max-w-7xl mx-auto px-6 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<!-- Logo and Brand -->
|
||||
@@ -18,10 +27,10 @@
|
||||
<router-link v-slot="{ isActive }" to="/dashboard" custom>
|
||||
<a
|
||||
:class="[
|
||||
'px-4 py-2 rounded-full font-medium transition-all cursor-pointer',
|
||||
'nav-button px-4 py-2 rounded-full font-medium transition-all cursor-pointer',
|
||||
isActive
|
||||
? 'bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400'
|
||||
: 'text-on-surface hover:bg-surface-container',
|
||||
: 'text-on-surface',
|
||||
]"
|
||||
@click="router.push('/dashboard')"
|
||||
>
|
||||
@@ -35,10 +44,10 @@
|
||||
<router-link v-slot="{ isActive }" to="/tasks" custom>
|
||||
<a
|
||||
:class="[
|
||||
'px-4 py-2 rounded-full font-medium transition-all cursor-pointer',
|
||||
'nav-button px-4 py-2 rounded-full font-medium transition-all cursor-pointer',
|
||||
isActive
|
||||
? 'bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400'
|
||||
: 'text-on-surface hover:bg-surface-container',
|
||||
: 'text-on-surface',
|
||||
]"
|
||||
@click="router.push('/tasks')"
|
||||
>
|
||||
@@ -52,10 +61,10 @@
|
||||
<router-link v-slot="{ isActive }" to="/records" custom>
|
||||
<a
|
||||
:class="[
|
||||
'px-4 py-2 rounded-full font-medium transition-all cursor-pointer',
|
||||
'nav-button px-4 py-2 rounded-full font-medium transition-all cursor-pointer',
|
||||
isActive
|
||||
? 'bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400'
|
||||
: 'text-on-surface hover:bg-surface-container',
|
||||
: 'text-on-surface',
|
||||
]"
|
||||
@click="router.push('/records')"
|
||||
>
|
||||
@@ -70,10 +79,10 @@
|
||||
<a-dropdown v-if="authStore.isAdmin" :trigger="['hover']">
|
||||
<a
|
||||
:class="[
|
||||
'px-4 py-2 rounded-full font-medium transition-all flex items-center space-x-2 cursor-pointer',
|
||||
'admin-nav-button px-4 py-2 rounded-full font-medium transition-all flex items-center space-x-2 cursor-pointer',
|
||||
isAdminPath
|
||||
? 'bg-secondary-100 dark:bg-secondary-900/30 text-secondary-700 dark:text-secondary-400'
|
||||
: 'text-on-surface hover:bg-surface-container',
|
||||
: 'text-on-surface',
|
||||
]"
|
||||
>
|
||||
<SettingOutlined />
|
||||
@@ -113,7 +122,7 @@
|
||||
<!-- Token Status Indicator (Desktop & Mobile) -->
|
||||
<a-tooltip v-if="showTokenStatus" :title="tokenStatusTooltip">
|
||||
<div
|
||||
class="px-2 md:px-3 py-1.5 rounded-full cursor-pointer transition-all hover:bg-surface-container flex items-center space-x-1 md:space-x-2"
|
||||
class="navbar-item px-2 md:px-3 py-1.5 rounded-full cursor-pointer transition-all flex items-center space-x-1 md:space-x-2"
|
||||
@click="handleTokenStatusClick"
|
||||
>
|
||||
<a-badge :status="tokenBadgeStatus" />
|
||||
@@ -137,7 +146,7 @@
|
||||
<a-tooltip :title="isDark ? '切换到亮色模式' : '切换到暗色模式'" placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
class="w-10 h-10 rounded-full flex items-center justify-center hover:bg-surface-container transition-all"
|
||||
class="navbar-item w-10 h-10 rounded-full flex items-center justify-center transition-all"
|
||||
@click="toggleTheme"
|
||||
>
|
||||
<BulbFilled v-if="isDark" class="text-xl text-yellow-400" />
|
||||
@@ -148,7 +157,7 @@
|
||||
<!-- Desktop User Menu -->
|
||||
<a-dropdown v-if="!isMobile" :trigger="['hover']">
|
||||
<a
|
||||
class="flex items-center space-x-3 px-4 py-2 rounded-full hover:bg-surface-container transition-all cursor-pointer"
|
||||
class="navbar-item flex items-center space-x-3 px-4 py-2 rounded-full transition-all cursor-pointer"
|
||||
>
|
||||
<a-avatar :style="{ backgroundColor: '#f56a00' }">
|
||||
{{ userInitial }}
|
||||
@@ -185,7 +194,7 @@
|
||||
<button
|
||||
v-if="isMobile"
|
||||
type="button"
|
||||
class="w-10 h-10 rounded-full flex items-center justify-center hover:bg-surface-container transition-all"
|
||||
class="navbar-item w-10 h-10 rounded-full flex items-center justify-center transition-all"
|
||||
@click="drawerVisible = true"
|
||||
>
|
||||
<MenuOutlined class="text-xl text-on-surface" />
|
||||
@@ -458,7 +467,3 @@ const handleQRCodeError = error => {
|
||||
message.error({ content: error?.message || 'Token 刷新失败', duration: 4 });
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Additional component-specific styles if needed */
|
||||
</style>
|
||||
|
||||
+61
-1
@@ -532,6 +532,35 @@
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* === Navbar === */
|
||||
.navbar-wrapper {
|
||||
box-shadow:
|
||||
0px 1px 2px 0px rgba(0, 0, 0, 0.12),
|
||||
0px 2px 6px 2px rgba(0, 0, 0, 0.08) !important;
|
||||
transition: box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.navbar-wrapper .text-on-surface {
|
||||
color: #1c1b1f;
|
||||
}
|
||||
|
||||
/* 导航按钮悬浮效果 */
|
||||
/* 普通导航按钮(绿色) */
|
||||
.navbar-wrapper .nav-button:not(.bg-primary-100):hover {
|
||||
background-color: rgba(76, 175, 80, 0.08) !important;
|
||||
}
|
||||
|
||||
/* 管理后台按钮(蓝色) */
|
||||
.navbar-wrapper .admin-nav-button:not(.bg-secondary-100):hover {
|
||||
background-color: rgba(33, 150, 243, 0.08) !important;
|
||||
}
|
||||
|
||||
/* 其他 navbar 组件的通用悬浮效果(绿色) */
|
||||
.navbar-wrapper .navbar-item:hover {
|
||||
background-color: rgba(76, 175, 80, 0.08) !important;
|
||||
}
|
||||
|
||||
/* === Button === */
|
||||
.ant-btn {
|
||||
border-radius: 20px;
|
||||
@@ -810,6 +839,31 @@
|
||||
/* ============================================
|
||||
暗色模式特定样式
|
||||
============================================ */
|
||||
/* Navbar 暗色模式 */
|
||||
.dark .navbar-wrapper {
|
||||
box-shadow:
|
||||
0px 2px 4px 0px rgba(0, 0, 0, 0.4),
|
||||
0px 4px 8px 4px rgba(0, 0, 0, 0.3) !important;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.dark .navbar-wrapper .text-on-surface {
|
||||
color: #e6e1e5;
|
||||
}
|
||||
|
||||
.dark .navbar-wrapper .nav-button:not(.bg-primary-100):hover {
|
||||
background-color: rgba(129, 199, 132, 0.12) !important;
|
||||
}
|
||||
|
||||
.dark .navbar-wrapper .admin-nav-button:not(.bg-secondary-100):hover {
|
||||
background-color: rgba(100, 181, 246, 0.12) !important;
|
||||
}
|
||||
|
||||
.dark .navbar-wrapper .navbar-item:hover {
|
||||
background-color: rgba(129, 199, 132, 0.12) !important;
|
||||
}
|
||||
|
||||
/* Ant Design 组件暗色模式 */
|
||||
.dark .ant-card {
|
||||
background-color: var(--md-sys-color-surface-container-low);
|
||||
border-color: var(--md-sys-color-outline-variant);
|
||||
@@ -1003,7 +1057,13 @@
|
||||
.elevation-2 {
|
||||
box-shadow:
|
||||
0px 1px 2px 0px rgba(0, 0, 0, 0.12),
|
||||
0px 2px 6px 2px rgba(0, 0, 0, 0.08);
|
||||
0px 2px 6px 2px rgba(0, 0, 0, 0.08) !important;
|
||||
}
|
||||
|
||||
.dark .elevation-2 {
|
||||
box-shadow:
|
||||
0px 1px 2px 0px rgba(0, 0, 0, 0.3),
|
||||
0px 2px 6px 2px rgba(0, 0, 0, 0.2) !important;
|
||||
}
|
||||
|
||||
.elevation-3 {
|
||||
|
||||
@@ -46,6 +46,23 @@
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- 刷新 Token 按钮 -->
|
||||
<div style="margin-top: 24px; text-align: center">
|
||||
<!-- Token 未过期时:禁用按钮并显示提示 -->
|
||||
<a-tooltip v-if="tokenStatus.is_valid" title="Token 过期后才可以扫码刷新 Token">
|
||||
<a-button type="primary" size="large" :disabled="true">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新 Token
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
|
||||
<!-- Token 已过期时:启用按钮且无提示 -->
|
||||
<a-button v-else type="primary" size="large" @click="qrcodeModalVisible = true">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新 Token
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-alert
|
||||
v-if="tokenStatus.expiring_soon"
|
||||
message="Token 即将过期"
|
||||
@@ -169,14 +186,22 @@
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- QR Code Modal for Token Refresh -->
|
||||
<QRCodeModal
|
||||
v-model:visible="qrcodeModalVisible"
|
||||
@success="handleQRCodeSuccess"
|
||||
@error="handleQRCodeError"
|
||||
/>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { CalendarOutlined, KeyOutlined, UserOutlined } from '@ant-design/icons-vue';
|
||||
import { CalendarOutlined, KeyOutlined, UserOutlined, ReloadOutlined } from '@ant-design/icons-vue';
|
||||
import Layout from '@/components/Layout.vue';
|
||||
import QRCodeModal from '@/components/QRCodeModal.vue';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { useTaskStore } from '@/stores/task';
|
||||
@@ -199,6 +224,7 @@ const { startPolling } = usePollStatus({
|
||||
const tokenStatusLoading = ref(false);
|
||||
const checkInLoading = ref(false);
|
||||
const selectedTaskId = ref(null);
|
||||
const qrcodeModalVisible = ref(false);
|
||||
|
||||
const tokenStatus = computed(() => userStore.tokenStatus);
|
||||
const lastCheckIn = computed(() => {
|
||||
@@ -310,6 +336,24 @@ const handleCheckIn = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 处理扫码成功(Token 刷新)
|
||||
const handleQRCodeSuccess = async () => {
|
||||
try {
|
||||
// 获取最新的用户信息和 Token 状态
|
||||
await authStore.fetchCurrentUser();
|
||||
await fetchTokenStatus();
|
||||
message.success({ content: 'Token 刷新成功!', duration: 3 });
|
||||
} catch (error) {
|
||||
console.error('刷新用户信息失败:', error);
|
||||
message.error({ content: '获取最新信息失败,请刷新页面', duration: 3 });
|
||||
}
|
||||
};
|
||||
|
||||
// 处理扫码失败
|
||||
const handleQRCodeError = errorMsg => {
|
||||
message.error({ content: errorMsg || '扫码刷新 Token 失败', duration: 3 });
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
fetchTokenStatus();
|
||||
checkInStore.fetchMyRecords({ limit: 1 });
|
||||
|
||||
Reference in New Issue
Block a user