add(refreshTokenService):完善刷新令牌服务

This commit is contained in:
2025-11-16 10:55:26 +08:00
12 changed files with 634 additions and 160 deletions
-2
View File
@@ -3,10 +3,8 @@
<template>
<div id="app">
<WindowLayout>
<RouterView></RouterView>
</WindowLayout>
</div>
</template>
+24 -5
View File
@@ -23,12 +23,25 @@
</button>
<button class="control-btn maximize" @click="maximize" title="最大化">
<svg width="12" height="12" viewBox="0 0 12 12">
<rect x="2" y="2" width="8" height="8" stroke="currentColor" fill="none" stroke-width="1.2" />
<rect
x="2"
y="2"
width="8"
height="8"
stroke="currentColor"
fill="none"
stroke-width="1.2"
/>
</svg>
</button>
<button class="control-btn close" @click="close" title="关闭">
<svg width="12" height="12" viewBox="0 0 12 12">
<path d="M2 2l8 8M10 2L2 10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
<path
d="M2 2l8 8M10 2L2 10"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
/>
</svg>
</button>
</div>
@@ -42,9 +55,15 @@
</template>
<script setup>
function minimize() { console.log('最小化') }
function maximize() { console.log('最化') }
function close() { console.log('关闭') }
function minimize() {
console.log('最化')
}
function maximize() {
console.log('最大化')
}
function close() {
console.log('关闭')
}
</script>
<style scoped>
+83 -65
View File
@@ -29,26 +29,21 @@
<div class="content-header">
<h2>{{ getCurrentTabTitle() }}</h2>
</div>
<div class="search-box">
<i class="fas fa-search search-icon"></i>
<input
type="text"
placeholder="搜索..."
v-model="searchText"
@input="handleSearch"
>
<input type="text" placeholder="搜索..." v-model="searchText" @input="handleSearch" />
</div>
<div class="tab-content">
<!-- 消息 -->
<div v-if="currentTab === 'chat'" class="chat-list">
<ul>
<li
v-for="chat in filteredChats"
:key="chat.id"
@click="selectChat(chat)"
:class="{active: currentChat?.id === chat.id}"
<li
v-for="chat in filteredChats"
:key="chat.id"
@click="selectChat(chat)"
:class="{ active: currentChat?.id === chat.id }"
>
<div class="chat-avatar">{{ getInitials(chat.name) }}</div>
<div class="info">
@@ -125,12 +120,14 @@
</header>
<div class="chat-body" ref="chatBody">
<div
v-for="msg in messages"
:key="msg.id"
<div
v-for="msg in messages"
:key="msg.id"
:class="['msg', msg.from === 'me' ? 'me' : 'them']"
>
<div class="msg-avatar">{{ getInitials(msg.from === 'me' ? '我' : currentChat.name) }}</div>
<div class="msg-avatar">
{{ getInitials(msg.from === 'me' ? '我' : currentChat.name) }}
</div>
<div class="msg-content">
<div class="bubble">{{ msg.text }}</div>
<div class="msg-time">{{ msg.time }}</div>
@@ -140,10 +137,10 @@
<footer class="chat-input">
<div class="input-area">
<textarea
v-model="input"
placeholder="输入消息..."
@keydown.enter.prevent="send"
<textarea
v-model="input"
placeholder="输入消息..."
@keydown.enter.prevent="send"
rows="1"
ref="messageInput"
></textarea>
@@ -153,10 +150,13 @@
</div>
</footer>
</aside>
<!-- 空聊天状态 -->
<aside class="chat-area" v-else>
<div class="empty-state" style="height: 100%; display: flex; flex-direction: column; justify-content: center;">
<div
class="empty-state"
style="height: 100%; display: flex; flex-direction: column; justify-content: center"
>
<i class="fas fa-comments icon"></i>
<h3>选择一个对话开始聊天</h3>
<p>在左侧列表中选择联系人开始对话</p>
@@ -172,18 +172,32 @@ import { ref, computed, nextTick, onMounted, onUnmounted } from 'vue'
const menus = ref([
{ key: 'chat', label: '消息', icon: 'fas fa-comment-dots', notification: 3 },
{ key: 'friends', label: '联系人', icon: 'fas fa-user-friends' },
{ key: 'groups', label: '群聊', icon: 'fas fa-users' }
{ key: 'groups', label: '群聊', icon: 'fas fa-users' },
])
const currentTab = ref('chat')
const searchText = ref('')
const chats = ref([
{ id: 1, name: '张三', lastMsg: '今晚一起吃饭吗?', lastTime: '10:30', unread: 2, status: 'online' },
{
id: 1,
name: '张三',
lastMsg: '今晚一起吃饭吗?',
lastTime: '10:30',
unread: 2,
status: 'online',
},
{ id: 2, name: '李四', lastMsg: '收到文件了吗?', lastTime: '昨天', unread: 0, status: 'online' },
{ id: 3, name: '王五', lastMsg: '项目进展如何?', lastTime: '周三', unread: 1, status: 'offline' },
{
id: 3,
name: '王五',
lastMsg: '项目进展如何?',
lastTime: '周三',
unread: 1,
status: 'offline',
},
{ id: 4, name: '赵六', lastMsg: '周末有空吗?', lastTime: '周一', unread: 0, status: 'online' },
{ id: 5, name: '钱七', lastMsg: '会议改期了', lastTime: '3月15日', unread: 0, status: 'online' }
{ id: 5, name: '钱七', lastMsg: '会议改期了', lastTime: '3月15日', unread: 0, status: 'online' },
])
const friends = ref([
@@ -193,7 +207,7 @@ const friends = ref([
{ id: 4, name: '赵六', status: 'online' },
{ id: 5, name: '钱七', status: 'online' },
{ id: 6, name: '孙八', status: 'online' },
{ id: 7, name: '周九', status: 'offline' }
{ id: 7, name: '周九', status: 'offline' },
])
const currentChat = ref(null)
@@ -205,22 +219,23 @@ const messageInput = ref(null)
// 计算属性
const filteredChats = computed(() => {
if (!searchText.value) return chats.value
return chats.value.filter(chat =>
chat.name.toLowerCase().includes(searchText.value.toLowerCase()) ||
chat.lastMsg.toLowerCase().includes(searchText.value.toLowerCase())
return chats.value.filter(
(chat) =>
chat.name.toLowerCase().includes(searchText.value.toLowerCase()) ||
chat.lastMsg.toLowerCase().includes(searchText.value.toLowerCase()),
)
})
const filteredFriends = computed(() => {
if (!searchText.value) return friends.value
return friends.value.filter(friend =>
friend.name.toLowerCase().includes(searchText.value.toLowerCase())
return friends.value.filter((friend) =>
friend.name.toLowerCase().includes(searchText.value.toLowerCase()),
)
})
// 方法
function getCurrentTabTitle() {
const menu = menus.value.find(m => m.key === currentTab.value)
const menu = menus.value.find((m) => m.key === currentTab.value)
return menu ? menu.label : '消息'
}
@@ -236,25 +251,23 @@ function formatTime(date) {
// 模拟回复
function simulateReply() {
if (!currentChat.value) return
const replies = [
'好的,我明白了',
'听起来不错',
'让我考虑一下',
'没问题,就这么办'
]
const replies = ['好的,我明白了', '听起来不错', '让我考虑一下', '没问题,就这么办']
const randomReply = replies[Math.floor(Math.random() * replies.length)]
setTimeout(() => {
messages.value.push({
id: Date.now(),
from: 'them',
text: randomReply,
time: formatTime(new Date())
})
nextTick(scrollBottom)
}, 1000 + Math.random() * 2000)
setTimeout(
() => {
messages.value.push({
id: Date.now(),
from: 'them',
text: randomReply,
time: formatTime(new Date()),
})
nextTick(scrollBottom)
},
1000 + Math.random() * 2000,
)
}
function selectChat(chat) {
@@ -263,13 +276,13 @@ function selectChat(chat) {
{ id: 1, from: 'them', text: `你好,我是 ${chat.name},很高兴认识你!`, time: '09:15' },
{ id: 2, from: 'me', text: '你好呀,也很高兴认识你~', time: '09:16' },
{ id: 3, from: 'them', text: '最近在忙什么呢?', time: '09:20' },
{ id: 4, from: 'me', text: '在做一个新项目,挺有意思的。', time: '09:22' }
{ id: 4, from: 'me', text: '在做一个新项目,挺有意思的。', time: '09:22' },
]
nextTick(() => {
scrollBottom()
focusInput()
})
// 清除未读消息
if (chat.unread) {
chat.unread = 0
@@ -279,16 +292,16 @@ function selectChat(chat) {
function send() {
if (!input.value.trim()) return
messages.value.push({
id: Date.now(),
from: 'me',
text: input.value,
time: formatTime(new Date())
time: formatTime(new Date()),
})
input.value = ''
nextTick(scrollBottom)
// 模拟回复
simulateReply()
}
@@ -320,7 +333,7 @@ onMounted(() => {
if (chats.value.length > 0) {
selectChat(chats.value[0])
}
// 监听窗口大小变化
window.addEventListener('resize', handleResize)
})
@@ -483,13 +496,15 @@ onUnmounted(() => {
padding: 0 20px 20px;
}
.chat-list ul, .friend-list ul {
.chat-list ul,
.friend-list ul {
list-style: none;
padding: 0;
margin: 0;
}
.chat-list li, .friend-list li {
.chat-list li,
.friend-list li {
display: flex;
align-items: center;
gap: 12px;
@@ -500,7 +515,8 @@ onUnmounted(() => {
margin-bottom: 4px;
}
.chat-list li:hover, .friend-list li:hover {
.chat-list li:hover,
.friend-list li:hover {
background: #f7fafc;
}
@@ -529,7 +545,9 @@ onUnmounted(() => {
gap: 4px;
}
.name-row, .last-row, .status-row {
.name-row,
.last-row,
.status-row {
display: flex;
justify-content: space-between;
align-items: center;
@@ -809,7 +827,7 @@ onUnmounted(() => {
.im-container {
min-width: 700px;
}
.chat-area {
width: 350px;
min-width: 300px;
@@ -820,10 +838,10 @@ onUnmounted(() => {
.im-container {
min-width: 600px;
}
.chat-area {
width: 300px;
min-width: 250px;
}
}
</style>
</style>
+274 -84
View File
@@ -6,10 +6,22 @@
<div class="brand">
<div class="brand-logo" aria-hidden="true">
<!-- 圆形头像徽标 -->
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect x="2" y="2" width="20" height="20" rx="5" fill="currentColor" />
<path d="M8 14c0-1.657 1.343-3 3-3s3 1.343 3 3" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="11" cy="9" r="1.3" fill="white"/>
<path
d="M8 14c0-1.657 1.343-3 3-3s3 1.343 3 3"
stroke="white"
stroke-width="1.2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<circle cx="11" cy="9" r="1.3" fill="white" />
</svg>
</div>
<div class="brand-name">即时通讯</div>
@@ -34,19 +46,55 @@
<form class="login-form" @submit.prevent="handleLogin" autocomplete="on" novalidate>
<div class="input-container" :class="{ focused: usernameFocused || username }">
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
<path d="M20 21V19C20 16.7909 18.2091 15 16 15H8C5.79086 15 4 16.7909 4 19V21" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
<circle cx="12" cy="7" r="4" fill="none" stroke="currentColor" stroke-width="1.6"/>
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
<path
d="M20 21V19C20 16.7909 18.2091 15 16 15H8C5.79086 15 4 16.7909 4 19V21"
fill="none"
stroke="currentColor"
stroke-width="1.6"
stroke-linecap="round"
/>
<circle cx="12" cy="7" r="4" fill="none" stroke="currentColor" stroke-width="1.6" />
</svg>
<input id="username" type="text" v-model="username" @focus="usernameFocused = true" @blur="usernameFocused = false" placeholder="用户名 / 邮箱" required />
<input
id="username"
type="text"
v-model="username"
@focus="usernameFocused = true"
@blur="usernameFocused = false"
placeholder="用户名 / 邮箱"
required
/>
</div>
<div class="input-container" :class="{ focused: passwordFocused || password }">
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
<rect x="3" y="11" width="18" height="11" rx="2" fill="none" stroke="currentColor" stroke-width="1.6"/>
<path d="M7 11V7C7 4.23858 9.23858 2 12 2C14.7614 2 17 4.23858 17 7V11" fill="none" stroke="currentColor" stroke-width="1.6"/>
<rect
x="3"
y="11"
width="18"
height="11"
rx="2"
fill="none"
stroke="currentColor"
stroke-width="1.6"
/>
<path
d="M7 11V7C7 4.23858 9.23858 2 12 2C14.7614 2 17 4.23858 17 7V11"
fill="none"
stroke="currentColor"
stroke-width="1.6"
/>
</svg>
<input id="password" type="password" v-model="password" @focus="passwordFocused = true" @blur="passwordFocused = false" placeholder="密码" required />
<input
id="password"
type="password"
v-model="password"
@focus="passwordFocused = true"
@blur="passwordFocused = false"
placeholder="密码"
required
/>
</div>
<div class="row-between">
@@ -126,9 +174,9 @@ const handleLogin = () => {
display: flex;
border-radius: 12px;
overflow: hidden;
background: linear-gradient(180deg, rgba(255,255,255,0.98), rgba(248,250,252,0.98));
box-shadow: 0 18px 40px rgba(16,24,40,0.08);
border: 1px solid rgba(15,23,42,0.04);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.98));
box-shadow: 0 18px 40px rgba(16, 24, 40, 0.08);
border: 1px solid rgba(15, 23, 42, 0.04);
}
/* 左侧品牌区域 */
@@ -136,129 +184,271 @@ const handleLogin = () => {
width: 44%;
min-width: 280px;
padding: 28px 22px;
background: linear-gradient(160deg, rgba(59,130,246,0.06), rgba(99,102,241,0.04));
background: linear-gradient(160deg, rgba(59, 130, 246, 0.06), rgba(99, 102, 241, 0.04));
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
}
.brand { display:flex; align-items:center; gap:12px; margin-bottom:12px; }
.brand-logo { width:56px; height:56px; display:flex; align-items:center; justify-content:center; color: #3b82f6; background: rgba(59,130,246,0.08); border-radius:12px; }
.brand-name { font-size:18px; font-weight:700; color:#0f172a; }
.brand {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.brand-logo {
width: 56px;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
color: #3b82f6;
background: rgba(59, 130, 246, 0.08);
border-radius: 12px;
}
.brand-name {
font-size: 18px;
font-weight: 700;
color: #0f172a;
}
.side-desc { color:#475569; font-size:13px; line-height:1.5; max-width:210px; margin-bottom:18px; }
.side-desc {
color: #475569;
font-size: 13px;
line-height: 1.5;
max-width: 210px;
margin-bottom: 18px;
}
/* 简单的装饰圆块 */
.side-illu { position:absolute; right:-30px; bottom:-20px; }
.bubble { border-radius:50%; opacity:0.12; }
.b1 { width:120px; height:120px; background: linear-gradient(135deg,#60a5fa,#7c3aed); transform: rotate(10deg); margin:8px; }
.b2 { width:72px; height:72px; background: linear-gradient(135deg,#a78bfa,#60a5fa); margin:8px; }
.b3 { width:40px; height:40px; background: linear-gradient(135deg,#93c5fd,#a78bfa); margin:8px; }
.side-illu {
position: absolute;
right: -30px;
bottom: -20px;
}
.bubble {
border-radius: 50%;
opacity: 0.12;
}
.b1 {
width: 120px;
height: 120px;
background: linear-gradient(135deg, #60a5fa, #7c3aed);
transform: rotate(10deg);
margin: 8px;
}
.b2 {
width: 72px;
height: 72px;
background: linear-gradient(135deg, #a78bfa, #60a5fa);
margin: 8px;
}
.b3 {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #93c5fd, #a78bfa);
margin: 8px;
}
/* 右侧表单 */
.login-body {
width: 56%;
padding: 30px 34px;
display:flex;
flex-direction:column;
justify-content:space-between;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.login-header { text-align:left; margin-bottom:6px; }
.login-title { font-size:20px; margin:0 0 6px; color:#0f172a; font-weight:700; }
.login-subtitle { margin:0; font-size:13px; color:#64748b; font-weight:500; }
.login-header {
text-align: left;
margin-bottom: 6px;
}
.login-title {
font-size: 20px;
margin: 0 0 6px;
color: #0f172a;
font-weight: 700;
}
.login-subtitle {
margin: 0;
font-size: 13px;
color: #64748b;
font-weight: 500;
}
/* 表单 */
.login-form { margin-top:10px; display:flex; flex-direction:column; gap:14px; }
.login-form {
margin-top: 10px;
display: flex;
flex-direction: column;
gap: 14px;
}
/* 输入容器 */
.input-container {
position:relative;
display:flex;
align-items:center;
position: relative;
display: flex;
align-items: center;
background: #ffffff;
border-radius:10px;
padding:12px 12px 12px 44px;
border:1px solid rgba(15,23,42,0.06);
box-shadow: 0 6px 18px rgba(15,23,42,0.02);
transition: all .22s ease;
border-radius: 10px;
padding: 12px 12px 12px 44px;
border: 1px solid rgba(15, 23, 42, 0.06);
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.02);
transition: all 0.22s ease;
}
.input-container .icon {
position:absolute;
left:12px;
width:18px; height:18px;
color:#94a3b8;
opacity:0.95;
position: absolute;
left: 12px;
width: 18px;
height: 18px;
color: #94a3b8;
opacity: 0.95;
}
.input-container input {
width:100%;
border:none;
outline:none;
font-size:14px;
color:#0f172a;
width: 100%;
border: none;
outline: none;
font-size: 14px;
color: #0f172a;
background: transparent;
padding:0;
padding: 0;
}
/* 聚焦态 */
.input-container.focused {
border-color: rgba(99,102,241,0.9);
box-shadow: 0 8px 20px rgba(99,102,241,0.08);
border-color: rgba(99, 102, 241, 0.9);
box-shadow: 0 8px 20px rgba(99, 102, 241, 0.08);
transform: translateY(-2px);
}
.input-container.focused .icon { color: rgba(99,102,241,0.95); }
.input-container.focused .icon {
color: rgba(99, 102, 241, 0.95);
}
/* 行内布局 */
.row-between { display:flex; justify-content:space-between; align-items:center; font-size:13px; margin-top:2px; }
.row-between {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
margin-top: 2px;
}
/* 记住我 checkbox */
.remember { display:flex; align-items:center; gap:8px; color:#475569; cursor:pointer; user-select:none; }
.remember input { position:absolute; opacity:0; pointer-events:none; }
.remember .box { width:18px; height:18px; display:inline-block; border-radius:6px; border:1.5px solid rgba(15,23,42,0.08); background:white; box-shadow:inset 0 -1px 0 rgba(0,0,0,0.03); transition:all .14s ease; }
.remember input:checked + .box { background: linear-gradient(90deg,#6366f1,#3b82f6); border-color:transparent; box-shadow:none; }
.remember input:checked + .box::after { content:""; display:block; width:6px; height:10px; border:2px solid white; border-left:0; border-top:0; transform: translate(5px,2px) rotate(45deg); }
.remember {
display: flex;
align-items: center;
gap: 8px;
color: #475569;
cursor: pointer;
user-select: none;
}
.remember input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.remember .box {
width: 18px;
height: 18px;
display: inline-block;
border-radius: 6px;
border: 1.5px solid rgba(15, 23, 42, 0.08);
background: white;
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.03);
transition: all 0.14s ease;
}
.remember input:checked + .box {
background: linear-gradient(90deg, #6366f1, #3b82f6);
border-color: transparent;
box-shadow: none;
}
.remember input:checked + .box::after {
content: '';
display: block;
width: 6px;
height: 10px;
border: 2px solid white;
border-left: 0;
border-top: 0;
transform: translate(5px, 2px) rotate(45deg);
}
/* 链接样式 */
.link { color:#3b82f6; text-decoration:none; font-weight:600; }
.link:hover { text-decoration:underline; color:#2563eb; }
.link {
color: #3b82f6;
text-decoration: none;
font-weight: 600;
}
.link:hover {
text-decoration: underline;
color: #2563eb;
}
/* 主操作按钮 */
.btn-primary {
width:100%;
padding:12px 14px;
border-radius:10px;
border:none;
background: linear-gradient(90deg,#6366f1,#3b82f6);
color:white;
font-weight:700;
font-size:15px;
cursor:pointer;
box-shadow: 0 8px 20px rgba(59,130,246,0.18);
transition: transform .12s ease, box-shadow .12s ease, opacity .12s;
width: 100%;
padding: 12px 14px;
border-radius: 10px;
border: none;
background: linear-gradient(90deg, #6366f1, #3b82f6);
color: white;
font-weight: 700;
font-size: 15px;
cursor: pointer;
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.18);
transition:
transform 0.12s ease,
box-shadow 0.12s ease,
opacity 0.12s;
}
.btn-primary:active {
transform: translateY(0);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: default;
box-shadow: none;
}
.btn-primary:active { transform: translateY(0); }
.btn-primary:disabled { opacity:0.6; cursor:default; box-shadow:none; }
/* 错误提示 */
.error {
color:#b91c1c;
color: #b91c1c;
background: #fff5f5;
border-left:4px solid #f87171;
padding:10px 12px;
border-radius:8px;
font-weight:600;
font-size:13px;
margin-top:6px;
border-left: 4px solid #f87171;
padding: 10px 12px;
border-radius: 8px;
font-weight: 600;
font-size: 13px;
margin-top: 6px;
}
/* 页脚注册 */
.login-footer { display:flex; gap:8px; align-items:center; justify-content:flex-end; font-size:13px; color:#475569; }
.login-footer {
display: flex;
gap: 8px;
align-items: center;
justify-content: flex-end;
font-size: 13px;
color: #475569;
}
/* 响应式 */
@media (max-width: 820px) {
.login-card { flex-direction:column; height:auto; width:100%; }
.login-side { width:100%; min-height:140px; order:1; }
.login-body { width:100%; order:2; padding:22px; }
.login-card {
flex-direction: column;
height: auto;
width: 100%;
}
.login-side {
width: 100%;
min-height: 140px;
order: 1;
}
.login-body {
width: 100%;
order: 2;
padding: 22px;
}
}
</style>