add(main):添加登录页,主窗口组件

This commit is contained in:
西街长安 2025-11-03 15:07:36 +08:00
parent 76619c7785
commit c8082e6fab
7 changed files with 1182 additions and 595 deletions

View File

@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>

View File

@ -11,7 +11,7 @@
</template>
<script setup>
import WindowLayout from './components/Layout.vue'
import WindowLayout from './components/Window.vue'
</script>
<style scoped></style>

View File

@ -1,262 +0,0 @@
<template>
<div class="window-container">
<div class="window">
<!-- Windows 风格标题栏 -->
<div class="window-header">
<div class="window-title-area">
<div class="window-icon">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.5 1.5H1.5V6.5H6.5V1.5Z" fill="currentColor"/>
<path d="M14.5 1.5H9.5V6.5H14.5V1.5Z" fill="currentColor"/>
<path d="M6.5 9.5H1.5V14.5H6.5V9.5Z" fill="currentColor"/>
<path d="M14.5 9.5H9.5V14.5H14.5V9.5Z" fill="currentColor"/>
</svg>
</div>
<div class="window-title">我的窗口</div>
</div>
<div class="window-controls">
<button class="control-btn minimize" @click="minimize" title="最小化">
<span class="control-icon"></span>
</button>
<button class="control-btn maximize" @click="maximize" title="最大化">
<span class="control-icon"></span>
</button>
<button class="control-btn close" @click="close" title="关闭">
<span class="control-icon"></span>
</button>
</div>
</div>
<!-- 窗口内容 -->
<div class="window-content">
<slot></slot>
</div>
</div>
</div>
</template>
<script setup>
function minimize() {
console.log('最小化')
}
function maximize() {
console.log('最大化/还原')
}
function close() {
console.log('关闭窗口')
}
</script>
<style scoped>
/* 外层容器,居中 - 保持不变 */
.window-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #f0f0f0;
padding: 20px;
box-sizing: border-box;
}
/* 窗口主体 - 保持不变 */
.window {
width: 800px;
height: 550px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
overflow: hidden;
display: flex;
flex-direction: column;
}
/* 窗口顶部栏 - 优化按钮样式 */
.window-header {
height: 42px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 12px;
background: linear-gradient(135deg, #1a73e8 0%, #4285f4 100%);
color: #fff;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-weight: 600;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
user-select: none;
position: relative;
overflow: hidden;
}
/* 添加顶栏的微光效果 */
.window-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg,
transparent 0%,
rgba(255, 255, 255, 0.4) 20%,
rgba(255, 255, 255, 0.7) 50%,
rgba(255, 255, 255, 0.4) 80%,
transparent 100%);
}
/* 标题区域 */
.window-title-area {
display: flex;
align-items: center;
gap: 8px;
}
/* 窗口图标 */
.window-icon {
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.9);
}
/* 窗口标题 */
.window-title {
font-size: 14px;
letter-spacing: 0.3px;
color: rgba(255, 255, 255, 0.95);
font-weight: 500;
}
/* 标题栏按钮容器 - 增加间距 */
.window-controls {
display: flex;
gap: 8px;
}
/* 控制按钮通用样式 - 优化尺寸和交互 */
.control-btn {
width: 32px;
height: 32px;
border: none;
background: transparent;
color: rgba(255, 255, 255, 0.85);
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.control-btn::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0);
border-radius: 6px;
transition: background 0.2s ease;
}
.control-btn:hover::before {
background: rgba(255, 255, 255, 0.15);
}
.control-btn:active {
transform: scale(0.95);
}
/* 控制按钮图标 - 使用CSS绘制精致图标 */
.control-icon {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
}
/* 最小化按钮图标 */
.minimize .control-icon {
width: 10px;
height: 10px;
position: relative;
}
.minimize .control-icon::before {
content: '';
position: absolute;
width: 10px;
height: 2px;
background: currentColor;
border-radius: 1px;
bottom: 2px;
}
/* 最大化按钮图标 */
.maximize .control-icon {
width: 10px;
height: 10px;
position: relative;
}
.maximize .control-icon::before {
content: '';
position: absolute;
width: 8px;
height: 8px;
border: 1.5px solid currentColor;
border-radius: 1px;
}
/* 关闭按钮图标 */
.close .control-icon {
width: 10px;
height: 10px;
position: relative;
}
.close .control-icon::before,
.close .control-icon::after {
content: '';
position: absolute;
width: 12px;
height: 1.5px;
background: currentColor;
border-radius: 1px;
top: 4px;
left: -1px;
}
.close .control-icon::before {
transform: rotate(45deg);
}
.close .control-icon::after {
transform: rotate(-45deg);
}
/* 关闭按钮特殊样式 */
.close:hover {
color: #fff;
}
.close:hover::before {
background: rgba(232, 17, 35, 0.9);
}
/* 窗口内容 - 保持不变 */
.window-content {
flex: 1;
overflow-y: hidden;
background-color: #fdfdfd;
}
</style>

View File

@ -0,0 +1,136 @@
<template>
<div class="window-container">
<div class="window">
<div class="window-header">
<div class="window-left">
<div class="window-icon">
<!-- 应用图标 -->
<svg viewBox="0 0 24 24" width="18" height="18">
<rect x="3" y="3" width="8" height="8" rx="2" fill="currentColor" />
<rect x="13" y="3" width="8" height="8" rx="2" fill="currentColor" opacity="0.8" />
<rect x="3" y="13" width="8" height="8" rx="2" fill="currentColor" opacity="0.6" />
<rect x="13" y="13" width="8" height="8" rx="2" fill="currentColor" opacity="0.4" />
</svg>
</div>
<span class="window-title">即时通讯</span>
</div>
<div class="window-controls">
<button class="control-btn minimize" @click="minimize" title="最小化">
<svg width="12" height="12" viewBox="0 0 12 12">
<rect x="2" y="5.5" width="8" height="1" fill="currentColor" />
</svg>
</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" />
</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" />
</svg>
</button>
</div>
</div>
<div class="window-content">
<slot></slot>
</div>
</div>
</div>
</template>
<script setup>
function minimize() { console.log('最小化') }
function maximize() { console.log('最大化') }
function close() { console.log('关闭') }
</script>
<style scoped>
.window-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(145deg, #eef2ff, #f9fafb);
padding: 20px;
box-sizing: border-box;
}
.window {
width: clamp(640px, 85vw, 1100px);
height: clamp(520px, 80vh, 900px);
display: flex;
flex-direction: column;
background: white;
border-radius: 12px;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.window-header {
height: 46px;
background: linear-gradient(90deg, #4f46e5, #3b82f6);
color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px;
user-select: none;
}
.window-left {
display: flex;
align-items: center;
gap: 8px;
}
.window-icon {
display: flex;
align-items: center;
justify-content: center;
}
.window-title {
font-weight: 500;
letter-spacing: 0.5px;
}
.window-controls {
display: flex;
align-items: center;
gap: 6px;
}
.control-btn {
width: 26px;
height: 26px;
border: none;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
cursor: pointer;
color: #fff;
transition: background 0.15s ease;
}
.control-btn:hover {
background: rgba(255, 255, 255, 0.15);
}
.control-btn.close:hover {
background: rgba(239, 68, 68, 0.85);
}
.window-content {
flex: 1;
display: flex;
flex-direction: column;
padding: 12px;
min-height: 0;
background: #f9fafb;
}
</style>

View File

@ -1,6 +1,11 @@
import { createRouter, createWebHistory } from 'vue-router'
import MainView from '@/views/Main.vue'
const routes = [{ path: '/auth/login', component: () => import('@/views/auth/Login.vue') }]
const routes = [
{ path: '/auth/login', component: () => import('@/views/auth/Login.vue') },
{ path: '/', component: MainView },
{ path: '/index', component: MainView },
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),

View File

@ -0,0 +1,829 @@
<template>
<div class="im-container">
<!-- 左侧导航栏 -->
<nav class="sidebar">
<div class="avatar"></div>
<div class="menu">
<button
v-for="item in menus"
:key="item.key"
@click="currentTab = item.key"
:class="['menu-btn', { active: currentTab === item.key }]"
:title="item.label"
>
<i :class="item.icon" class="menu-icon"></i>
<span v-if="item.notification" class="notification-badge">{{ item.notification }}</span>
</button>
</div>
<div class="bottom-menu">
<button class="menu-btn" title="设置" @click="currentTab = 'settings'">
<i class="fas fa-cog menu-icon"></i>
</button>
</div>
</nav>
<!-- 中间内容区 -->
<main class="content">
<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"
>
</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}"
>
<div class="chat-avatar">{{ getInitials(chat.name) }}</div>
<div class="info">
<div class="name-row">
<span class="name">{{ chat.name }}</span>
<span class="time">{{ chat.lastTime }}</span>
</div>
<div class="last-row">
<span class="last">{{ chat.lastMsg }}</span>
<span v-if="chat.unread" class="unread-count">{{ chat.unread }}</span>
</div>
</div>
</li>
</ul>
</div>
<!-- 联系人 -->
<div v-else-if="currentTab === 'friends'" class="friend-list">
<ul>
<li v-for="f in filteredFriends" :key="f.id">
<div class="chat-avatar">{{ getInitials(f.name) }}</div>
<div class="info">
<div class="name-row">
<span class="name">{{ f.name }}</span>
</div>
<div class="status-row">
<span :class="['status', f.status === 'offline' ? 'offline' : '']">
{{ f.status === 'online' ? '在线' : '离线' }}
</span>
</div>
</div>
</li>
</ul>
</div>
<!-- 群聊 -->
<div v-else-if="currentTab === 'groups'" class="group-list">
<div class="empty-state">
<i class="fas fa-users icon"></i>
<h3>暂无群聊</h3>
<p>创建或加入群组开始群聊</p>
</div>
</div>
<!-- 设置 -->
<div v-else class="settings">
<div class="empty-state">
<i class="fas fa-cog icon"></i>
<h3>设置中心</h3>
<p>可以在这里修改昵称主题通知开关等</p>
</div>
</div>
</div>
</main>
<!-- 右侧聊天区域 -->
<aside class="chat-area" v-if="currentChat">
<header class="chat-header">
<div class="chat-user">
<div class="chat-user-avatar">{{ getInitials(currentChat.name) }}</div>
<div class="chat-user-info">
<div class="name">{{ currentChat.name }}</div>
<div class="status">{{ currentChat.status === 'online' ? '在线' : '离线' }}</div>
</div>
</div>
<div class="chat-actions">
<button title="视频通话">
<i class="fas fa-video"></i>
</button>
<button title="语音通话">
<i class="fas fa-phone-alt"></i>
</button>
</div>
</header>
<div class="chat-body" ref="chatBody">
<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-content">
<div class="bubble">{{ msg.text }}</div>
<div class="msg-time">{{ msg.time }}</div>
</div>
</div>
</div>
<footer class="chat-input">
<div class="input-area">
<textarea
v-model="input"
placeholder="输入消息..."
@keydown.enter.prevent="send"
rows="1"
ref="messageInput"
></textarea>
<button class="send-btn" @click="send">
<i class="fas fa-paper-plane"></i>
</button>
</div>
</footer>
</aside>
<!-- 空聊天状态 -->
<aside class="chat-area" v-else>
<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>
</div>
</aside>
</div>
</template>
<script setup>
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' }
])
const currentTab = ref('chat')
const searchText = ref('')
const chats = ref([
{ 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: 4, name: '赵六', lastMsg: '周末有空吗?', lastTime: '周一', unread: 0, status: 'online' },
{ id: 5, name: '钱七', lastMsg: '会议改期了', lastTime: '3月15日', unread: 0, status: 'online' }
])
const friends = ref([
{ id: 1, name: '张三', status: 'online' },
{ id: 2, name: '李四', status: 'online' },
{ id: 3, name: '王五', status: 'offline' },
{ id: 4, name: '赵六', status: 'online' },
{ id: 5, name: '钱七', status: 'online' },
{ id: 6, name: '孙八', status: 'online' },
{ id: 7, name: '周九', status: 'offline' }
])
const currentChat = ref(null)
const messages = ref([])
const input = ref('')
const chatBody = ref(null)
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())
)
})
const filteredFriends = computed(() => {
if (!searchText.value) return friends.value
return friends.value.filter(friend =>
friend.name.toLowerCase().includes(searchText.value.toLowerCase())
)
})
//
function getCurrentTabTitle() {
const menu = menus.value.find(m => m.key === currentTab.value)
return menu ? menu.label : '消息'
}
function getInitials(name) {
return name.substring(0, 1)
}
//
function formatTime(date) {
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
}
//
function simulateReply() {
if (!currentChat.value) return
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)
}
function selectChat(chat) {
currentChat.value = chat
messages.value = [
{ 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' }
]
nextTick(() => {
scrollBottom()
focusInput()
})
//
if (chat.unread) {
chat.unread = 0
menus.value[0].notification = Math.max(0, menus.value[0].notification - 1)
}
}
function send() {
if (!input.value.trim()) return
messages.value.push({
id: Date.now(),
from: 'me',
text: input.value,
time: formatTime(new Date())
})
input.value = ''
nextTick(scrollBottom)
//
simulateReply()
}
function scrollBottom() {
if (chatBody.value) {
chatBody.value.scrollTop = chatBody.value.scrollHeight
}
}
function focusInput() {
if (messageInput.value) {
messageInput.value.focus()
}
}
function handleSearch() {
//
}
//
function handleResize() {
//
}
//
onMounted(() => {
//
if (chats.value.length > 0) {
selectChat(chats.value[0])
}
//
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
</script>
<style scoped>
.im-container {
width: 100%;
height: 100%;
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
overflow: hidden;
display: flex;
min-width: 800px; /* 最小宽度防止过度压缩 */
}
/* 左侧导航栏 */
.sidebar {
width: 80px;
min-width: 80px;
background: #2d3748;
display: flex;
flex-direction: column;
align-items: center;
padding: 24px 0;
flex-shrink: 0;
}
.avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: #4a5568;
margin-bottom: 40px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
font-size: 18px;
}
.menu {
display: flex;
flex-direction: column;
gap: 16px;
flex-grow: 1;
}
.bottom-menu {
margin-top: auto;
}
.menu-btn {
background: transparent;
border: none;
color: #a0aec0;
cursor: pointer;
padding: 12px;
border-radius: 8px;
transition: all 0.2s;
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.menu-btn:hover {
background: rgba(255, 255, 255, 0.05);
color: white;
}
.menu-btn.active {
background: #4a5568;
color: white;
}
.menu-icon {
font-size: 20px;
}
.notification-badge {
position: absolute;
top: 8px;
right: 8px;
background: #e53e3e;
color: white;
border-radius: 8px;
width: 16px;
height: 16px;
font-size: 10px;
display: flex;
justify-content: center;
align-items: center;
}
/* 中间内容区 */
.content {
flex: 1;
background: white;
border-right: 1px solid #e2e8f0;
display: flex;
flex-direction: column;
min-width: 300px;
overflow: hidden;
}
.content-header {
padding: 20px;
border-bottom: 1px solid #e2e8f0;
flex-shrink: 0;
}
.content-header h2 {
font-size: 20px;
font-weight: 600;
color: #2d3748;
margin: 0;
}
.search-box {
margin: 16px 20px;
position: relative;
flex-shrink: 0;
}
.search-box input {
width: 100%;
padding: 12px 16px 12px 40px;
border-radius: 8px;
border: 1px solid #e2e8f0;
outline: none;
font-size: 14px;
background: #f7fafc;
box-sizing: border-box;
max-width: 100%;
}
.search-box input:focus {
border-color: #4299e1;
background: white;
}
.search-icon {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
color: #a0aec0;
}
.tab-content {
flex: 1;
overflow-y: auto;
padding: 0 20px 20px;
}
.chat-list ul, .friend-list ul {
list-style: none;
padding: 0;
margin: 0;
}
.chat-list li, .friend-list li {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s;
margin-bottom: 4px;
}
.chat-list li:hover, .friend-list li:hover {
background: #f7fafc;
}
.chat-list li.active {
background: #ebf8ff;
}
.chat-avatar {
width: 44px;
height: 44px;
border-radius: 50%;
background: #e2e8f0;
display: flex;
align-items: center;
justify-content: center;
color: #4a5568;
font-weight: 500;
flex-shrink: 0;
}
.info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.name-row, .last-row, .status-row {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.name {
font-weight: 600;
color: #2d3748;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
.time {
font-size: 12px;
color: #a0aec0;
font-weight: normal;
flex-shrink: 0;
margin-left: 8px;
}
.last {
font-size: 14px;
color: #718096;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
.unread-count {
background: #e53e3e;
color: white;
border-radius: 8px;
padding: 2px 6px;
font-size: 11px;
margin-left: 8px;
flex-shrink: 0;
}
.status {
font-size: 12px;
color: #38a169;
flex-shrink: 0;
}
.status.offline {
color: #a0aec0;
}
/* 聊天区 */
.chat-area {
width: 400px;
min-width: 350px;
display: flex;
flex-direction: column;
background: white;
flex-shrink: 0;
}
.chat-header {
padding: 16px 20px;
border-bottom: 1px solid #e2e8f0;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.chat-user {
display: flex;
gap: 12px;
align-items: center;
flex: 1;
min-width: 0;
}
.chat-user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: #e2e8f0;
display: flex;
align-items: center;
justify-content: center;
color: #4a5568;
font-weight: 500;
flex-shrink: 0;
}
.chat-user-info {
flex: 1;
min-width: 0;
}
.chat-user-info .name {
font-size: 16px;
margin-bottom: 2px;
}
.chat-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
.chat-actions button {
background: transparent;
border: none;
color: #718096;
cursor: pointer;
padding: 8px;
border-radius: 6px;
transition: all 0.2s;
flex-shrink: 0;
}
.chat-actions button:hover {
background: #f7fafc;
color: #2d3748;
}
.chat-body {
flex: 1;
overflow-y: auto;
padding: 20px;
background: #f7fafc;
display: flex;
flex-direction: column;
gap: 16px;
}
.msg {
display: flex;
max-width: 80%;
}
.them {
align-self: flex-start;
}
.me {
align-self: flex-end;
flex-direction: row-reverse;
}
.msg-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
align-self: flex-end;
margin: 0 8px;
background: #e2e8f0;
display: flex;
align-items: center;
justify-content: center;
color: #4a5568;
font-size: 12px;
font-weight: 500;
flex-shrink: 0;
}
.msg-content {
flex: 1;
min-width: 0;
}
.bubble {
padding: 12px 16px;
border-radius: 16px;
font-size: 14px;
line-height: 1.4;
word-wrap: break-word;
overflow-wrap: break-word;
}
.them .bubble {
background: white;
color: #2d3748;
border-top-left-radius: 4px;
}
.me .bubble {
background: #4299e1;
color: white;
border-top-right-radius: 4px;
}
.msg-time {
font-size: 11px;
color: #a0aec0;
margin-top: 4px;
}
.them .msg-time {
text-align: left;
}
.chat-input {
display: flex;
border-top: 1px solid #e2e8f0;
background: white;
padding: 16px 20px;
flex-shrink: 0;
}
.input-area {
display: flex;
align-items: flex-end;
gap: 12px;
width: 100%;
}
.input-area textarea {
flex: 1;
resize: none;
border: none;
outline: none;
padding: 12px 16px;
font-size: 14px;
border-radius: 8px;
background: #f7fafc;
height: 44px;
line-height: 1.4;
min-width: 0;
box-sizing: border-box;
}
.input-area textarea:focus {
background: white;
box-shadow: 0 0 0 1px #4299e1;
}
.send-btn {
background: #4299e1;
color: white;
border: none;
padding: 12px 16px;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s;
flex-shrink: 0;
}
.send-btn:hover {
background: #3182ce;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 40px 20px;
color: #a0aec0;
}
.empty-state .icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.empty-state h3 {
font-size: 18px;
margin-bottom: 8px;
color: #718096;
}
.empty-state p {
font-size: 14px;
line-height: 1.5;
}
/* 响应式设计 */
@media (max-width: 1024px) {
.im-container {
min-width: 700px;
}
.chat-area {
width: 350px;
min-width: 300px;
}
}
@media (max-width: 768px) {
.im-container {
min-width: 600px;
}
.chat-area {
width: 300px;
min-width: 250px;
}
}
</style>

View File

@ -1,58 +1,76 @@
<template>
<div class="login-content">
<div class="login-header">
<h2 class="login-title">欢迎登录</h2>
<p class="login-subtitle">请输入您的账号信息</p>
</div>
<form @submit.prevent="handleLogin" class="login-form">
<div class="input-group">
<div class="input-container">
<input id="username" type="text" v-model="username" placeholder=" " required>
<label for="username" class="floating-label">用户名</label>
<div class="input-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 21V19C20 16.7909 18.2091 15 16 15H8C5.79086 15 4 16.7909 4 19V21" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<circle cx="12" cy="7" r="4" stroke="currentColor" stroke-width="2"/>
<div class="login-wrap" @keyup.enter="handleLogin">
<div class="login-card" role="form" aria-label="登录表单">
<!-- 左侧品牌/装饰 -->
<div class="login-side">
<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">
<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"/>
</svg>
</div>
<div class="brand-name">即时通讯</div>
</div>
<p class="side-desc">与朋友同事保持联络 轻量快速私密</p>
<div class="side-illu" aria-hidden="true">
<!-- 简单装饰圆块避免加载外部图片 -->
<div class="bubble b1"></div>
<div class="bubble b2"></div>
<div class="bubble b3"></div>
</div>
</div>
<div class="input-group">
<div class="input-container">
<input id="password" type="password" v-model="password" placeholder=" " required>
<label for="password" class="floating-label">密码</label>
<div class="input-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="11" width="18" height="11" rx="2" stroke="currentColor" stroke-width="2"/>
<path d="M7 11V7C7 4.23858 9.23858 2 12 2C14.7614 2 17 4.23858 17 7V11" stroke="currentColor" stroke-width="2"/>
<!-- 右侧表单 -->
<div class="login-body">
<header class="login-header">
<h2 class="login-title">欢迎回来</h2>
<p class="login-subtitle">使用账号登录以继续</p>
</header>
<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>
<input id="username" type="text" v-model="username" @focus="usernameFocused = true" @blur="usernameFocused = false" placeholder="用户名 / 邮箱" required />
</div>
</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"/>
</svg>
<input id="password" type="password" v-model="password" @focus="passwordFocused = true" @blur="passwordFocused = false" placeholder="密码" required />
</div>
<div class="row-between">
<label class="remember">
<input type="checkbox" v-model="remember" />
<span class="box" aria-hidden="true"></span>
记住我
</label>
<a class="link" href="#" @click.prevent>忘记密码?</a>
</div>
<button type="submit" class="btn-primary" :disabled="loading" aria-disabled="loading">
<span v-if="!loading">登录</span>
<span v-else>登录中...</span>
</button>
<p v-if="errorMsg" class="error">{{ errorMsg }}</p>
</form>
<footer class="login-footer">
<span>还没有账号</span>
<a class="link" href="#" @click.prevent>注册</a>
</footer>
</div>
<div class="options-row">
<label class="remember-me">
<input type="checkbox">
<span class="checkmark"></span>
记住我
</label>
<a href="#" class="forgot-link">忘记密码?</a>
</div>
<button type="submit" class="login-btn">
<span>登录</span>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 12H19M19 12L12 5M19 12L12 19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<p v-if="errorMsg" class="error">{{ errorMsg }}</p>
</form>
<div class="login-footer">
<p>还没有账号? <a href="#" class="register-link">立即注册</a></p>
</div>
</div>
</template>
@ -62,325 +80,185 @@ import { ref } from 'vue'
const username = ref('')
const password = ref('')
const remember = ref(false)
const errorMsg = ref('')
const loading = ref(false)
const usernameFocused = ref(false)
const passwordFocused = ref(false)
const handleLogin = () => {
if (!username.value || !password.value) {
errorMsg.value = ''
if (!username.value.trim() || !password.value) {
errorMsg.value = '用户名或密码不能为空'
return
}
if (username.value === 'admin' && password.value === '123456') {
alert('登录成功!')
errorMsg.value = ''
// TODO: IM
} else {
errorMsg.value = '用户名或密码错误'
}
loading.value = true
// API
setTimeout(() => {
loading.value = false
if (username.value === 'admin' && password.value === '123456') {
errorMsg.value = ''
alert('登录成功!')
// TODO: IM
} else {
errorMsg.value = '用户名或密码错误'
}
}, 700)
}
</script>
<style scoped>
.login-content {
padding: 40px 30px;
/* 容器与居中 */
.login-wrap {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
/* 登录卡片:左右两栏 */
.login-card {
width: 740px;
max-width: calc(100% - 48px);
height: 460px;
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);
}
/* 左侧品牌区域 */
.login-side {
width: 44%;
min-width: 280px;
padding: 28px 22px;
background: linear-gradient(160deg, rgba(59,130,246,0.06), rgba(99,102,241,0.04));
display: flex;
flex-direction: column;
justify-content: center;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border-radius: 0 0 8px 8px;
position: relative;
overflow: hidden;
}
/* 添加背景装饰元素 */
.login-content::before {
content: '';
position: absolute;
top: -50%;
right: -20%;
width: 300px;
height: 300px;
border-radius: 50%;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.05) 0%, rgba(37, 99, 235, 0.1) 100%);
z-index: 0;
.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-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;
}
.login-content::after {
content: '';
position: absolute;
bottom: -30%;
left: -10%;
width: 200px;
height: 200px;
border-radius: 50%;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.05) 0%, rgba(37, 99, 235, 0.08) 100%);
z-index: 0;
}
.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: center;
margin-bottom: 32px;
position: relative;
z-index: 1;
}
.login-title {
font-size: 26px;
font-weight: 700;
color: #1e293b;
margin-bottom: 8px;
letter-spacing: -0.5px;
}
.login-subtitle {
font-size: 14px;
color: #64748b;
font-weight: 500;
}
.login-form {
display: flex;
flex-direction: column;
gap: 20px;
max-width: 320px;
margin: 0 auto;
width: 100%;
position: relative;
z-index: 1;
}
/* 输入组 */
.input-group {
margin-bottom: 8px;
}
/* 表单 */
.login-form { margin-top:10px; display:flex; flex-direction:column; gap:14px; }
/* 输入容器 */
.input-container {
position: relative;
margin-top: 8px;
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;
}
.input-container .icon {
position:absolute;
left:12px;
width:18px; height:18px;
color:#94a3b8;
opacity:0.95;
}
.input-container input {
width: 100%;
padding: 16px 16px 16px 48px;
border: 1.5px solid #e2e8f0;
border-radius: 10px;
font-size: 15px;
outline: none;
transition: all 0.3s ease;
background-color: #ffffff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
position: relative;
z-index: 1;
width:100%;
border:none;
outline:none;
font-size:14px;
color:#0f172a;
background: transparent;
padding:0;
}
.input-container input:focus {
border-color: #3b82f6;
background-color: #ffffff;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1), 0 2px 6px rgba(59, 130, 246, 0.1);
transform: translateY(-1px);
}
.input-container input:focus + .floating-label,
.input-container input:not(:placeholder-shown) + .floating-label {
top: -8px;
left: 48px;
font-size: 12px;
color: #3b82f6;
background: linear-gradient(180deg, #f8fafc 50%, #ffffff 50%);
padding: 0 6px;
font-weight: 600;
}
.floating-label {
position: absolute;
top: 16px;
left: 48px;
font-size: 15px;
color: #64748b;
pointer-events: none;
transition: all 0.3s ease;
z-index: 2;
}
.input-icon {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
color: #94a3b8;
transition: color 0.3s ease;
z-index: 2;
}
.input-container input:focus ~ .input-icon {
color: #3b82f6;
}
/* 选项行 */
.options-row {
display: flex;
justify-content: space-between;
align-items: center;
margin: 10px 0 5px;
}
.remember-me {
display: flex;
align-items: center;
font-size: 14px;
color: #64748b;
cursor: pointer;
font-weight: 500;
}
.remember-me input {
display: none;
}
.checkmark {
width: 18px;
height: 18px;
border: 2px solid #cbd5e1;
border-radius: 4px;
margin-right: 8px;
position: relative;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.remember-me:hover .checkmark {
border-color: #94a3b8;
}
.remember-me input:checked + .checkmark {
background-color: #3b82f6;
border-color: #3b82f6;
}
.remember-me input:checked + .checkmark::after {
content: '';
position: absolute;
left: 5px;
top: 2px;
width: 4px;
height: 8px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.forgot-link {
font-size: 14px;
color: #3b82f6;
text-decoration: none;
transition: color 0.3s ease;
font-weight: 500;
}
.forgot-link:hover {
color: #2563eb;
text-decoration: underline;
}
/* 按钮 */
.login-btn {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
color: #fff;
font-weight: 600;
font-size: 15px;
border: none;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-top: 10px;
box-shadow: 0 4px 6px rgba(59, 130, 246, 0.25);
position: relative;
overflow: hidden;
}
.login-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s;
}
.login-btn:hover {
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
box-shadow: 0 6px 12px rgba(59, 130, 246, 0.35);
/* 聚焦态 */
.input-container.focused {
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); }
.login-btn:hover::before {
left: 100%;
}
/* 行内布局 */
.row-between { display:flex; justify-content:space-between; align-items:center; font-size:13px; margin-top:2px; }
.login-btn:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.4);
/* 记住我 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); }
/* 链接样式 */
.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;
}
.btn-primary:active { transform: translateY(0); }
.btn-primary:disabled { opacity:0.6; cursor:default; box-shadow:none; }
/* 错误提示 */
.error {
color: #ef4444;
margin-top: 16px;
font-size: 14px;
text-align: center;
padding: 12px;
background-color: #fef2f2;
border-radius: 8px;
border-left: 4px solid #ef4444;
box-shadow: 0 2px 4px rgba(239, 68, 68, 0.1);
font-weight: 500;
color:#b91c1c;
background: #fff5f5;
border-left:4px solid #f87171;
padding:10px 12px;
border-radius:8px;
font-weight:600;
font-size:13px;
margin-top:6px;
}
/* 登录页脚 */
.login-footer {
margin-top: 24px;
text-align: center;
font-size: 14px;
color: #64748b;
position: relative;
z-index: 1;
}
/* 页脚注册 */
.login-footer { display:flex; gap:8px; align-items:center; justify-content:flex-end; font-size:13px; color:#475569; }
.register-link {
color: #3b82f6;
text-decoration: none;
font-weight: 600;
transition: color 0.3s ease;
position: relative;
/* 响应式 */
@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; }
}
.register-link::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 2px;
background: #3b82f6;
transition: width 0.3s ease;
}
.register-link:hover {
color: #2563eb;
}
.register-link:hover::after {
width: 100%;
}
</style>
</style>