u
This commit is contained in:
1 parent
35951777b0
commit
63f1d4d1dc
39 files changed
+880
-133
No files matched your search
@@ -1,18 +1,15 @@
|
||||
<script setup>
|
||||
import { onBeforeUnmount, onMounted } from 'vue'
|
||||
import { onBeforeUnmount } from 'vue'
|
||||
import Layout from '@/layout/index.vue'
|
||||
import { useWebSocket } from './stores/websocketStore'
|
||||
|
||||
const websocket = useWebSocket()
|
||||
|
||||
onMounted(() => {
|
||||
websocket.connect('ws://127.0.0.1:8090/ws')
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
websocket.disconnect()
|
||||
// websocket.disconnect()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterView />
|
||||
<Layout />
|
||||
</template>
|
||||
@@ -0,0 +1,97 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '../stores/userStore'
|
||||
import { useWebSocket } from '../stores/websocketStore'
|
||||
|
||||
const websocket = useWebSocket()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const activeIndex = ref('/home')
|
||||
|
||||
onMounted(() => {
|
||||
router.afterEach((to) => {
|
||||
activeIndex.value = to.path
|
||||
})
|
||||
})
|
||||
|
||||
function toPath(url) {
|
||||
if (url === '/login') {
|
||||
console.log(userStore.isLogined())
|
||||
|
||||
if (userStore.isLogined()) {
|
||||
websocket.disconnect()
|
||||
userStore.resetUserInfo()
|
||||
}
|
||||
router.push(url)
|
||||
}
|
||||
else {
|
||||
router.push(url)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout">
|
||||
<el-menu
|
||||
v-model="activeIndex"
|
||||
:default-active="activeIndex"
|
||||
mode="horizontal"
|
||||
active-text-color="#1553f1"
|
||||
>
|
||||
<el-menu-item index="/home" @click="toPath('/home')">
|
||||
首页
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/im" @click="toPath('/im')">
|
||||
IM即时通信
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/login" @click="toPath('/login')">
|
||||
登录
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
<div class="layout-main">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.layout {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
& *::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
& *::-webkit-scrollbar-track {
|
||||
background-color: #fff;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
& *::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
& *::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.el-menu--horizontal {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.layout-main {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
scrollbar-gutter: stable both-edges;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,14 +1,30 @@
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
import Login from '@/views/auth/index.vue'
|
||||
import Home from '@/views/home/index.vue'
|
||||
import Im from '@/views/im/index.vue'
|
||||
|
||||
import { useWebSocket } from '../stores/websocketStore'
|
||||
// 新增登录页面
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/im',
|
||||
redirect: '/home',
|
||||
},
|
||||
{
|
||||
path: '/home',
|
||||
component: Home,
|
||||
meta: { title: '首页' },
|
||||
},
|
||||
{
|
||||
path: '/im',
|
||||
component: Im,
|
||||
meta: { title: 'IM聊天' },
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
component: Login,
|
||||
meta: { title: '登录' },
|
||||
},
|
||||
]
|
||||
|
||||
@@ -17,16 +33,36 @@ const router = createRouter({
|
||||
routes,
|
||||
})
|
||||
|
||||
// 全局前置守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
// 设置页面标题
|
||||
document.title = to.meta.title ? `${to.meta.title} - 系统名称` : '系统名称'
|
||||
|
||||
// 通过检查,继续跳转
|
||||
const userStore = useUserStore()
|
||||
const websocket = useWebSocket()
|
||||
|
||||
// 登录页直接放行,不主动断开
|
||||
if (to.path === '/login') {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
if (!userStore.isLogined()) {
|
||||
ElMessage.warning('请先登录')
|
||||
|
||||
userStore.resetUserInfo()
|
||||
next({ path: '/login', query: { redirect: to.fullPath } })
|
||||
return
|
||||
}
|
||||
|
||||
// 登录状态下,确保连接
|
||||
if (!websocket.isConnected.value) {
|
||||
websocket.connect()
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
router.afterEach(() => {
|
||||
router.afterEach((to, from) => {
|
||||
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -4,18 +4,19 @@ import { ref } from 'vue'
|
||||
export const useUserStore = defineStore(
|
||||
'userInfo',
|
||||
() => {
|
||||
|
||||
const userInfo = ref({
|
||||
orgid: '',
|
||||
orgid: 'g3hd',
|
||||
userid: '',
|
||||
token: '',
|
||||
username: '',
|
||||
})
|
||||
|
||||
// 设置用户信息
|
||||
function setUserInfo(orgid, userid, token) {
|
||||
function setUserInfo(orgid, userid, username, token) {
|
||||
userInfo.value.orgid = orgid
|
||||
userInfo.value.userid = userid
|
||||
userInfo.value.token = token
|
||||
userInfo.value.username = username
|
||||
}
|
||||
|
||||
// 重置用户信息
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
// stores/websocket.js
|
||||
import { ElNotification } from 'element-plus'
|
||||
import { onScopeDispose, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { useUserStore } from './userStore'
|
||||
|
||||
let instance = null // 单例缓存
|
||||
|
||||
export function useWebSocket() {
|
||||
if (instance)
|
||||
return instance // 已存在则直接返回
|
||||
|
||||
const userStore = useUserStore()
|
||||
const socket = ref(null)
|
||||
const isConnected = ref(false)
|
||||
const listeners = ref(new Set())
|
||||
@@ -9,25 +17,19 @@ export function useWebSocket() {
|
||||
let reconnectAttempts = 0
|
||||
let pingTimer = null
|
||||
let pongTimeout = null
|
||||
const PING_INTERVAL = 10000 // 收到PONG后间隔再发送下一次PING
|
||||
const PONG_TIMEOUT = 15000 // PING发送后15秒未收到PONG就重连
|
||||
|
||||
const PING_INTERVAL = 10000
|
||||
const PONG_TIMEOUT = 15000
|
||||
const currentUrl = 'ws://127.0.0.1:8090/ws'
|
||||
let reconnectNotification = null
|
||||
let currentUrl = null
|
||||
let isReconnecting = false
|
||||
let manualClose = false // ✅ 标记是否为手动关闭
|
||||
|
||||
// --------------------
|
||||
// 私有方法
|
||||
// --------------------
|
||||
// ---------------- 清理 ----------------
|
||||
function cleanup() {
|
||||
if (pingTimer) {
|
||||
clearTimeout(pingTimer)
|
||||
pingTimer = null
|
||||
}
|
||||
if (pongTimeout) {
|
||||
clearTimeout(pongTimeout)
|
||||
pongTimeout = null
|
||||
}
|
||||
clearTimeout(pingTimer)
|
||||
clearTimeout(pongTimeout)
|
||||
pingTimer = null
|
||||
pongTimeout = null
|
||||
}
|
||||
|
||||
function handleDisconnect() {
|
||||
@@ -39,23 +41,14 @@ export function useWebSocket() {
|
||||
listeners.value.forEach(cb => cb(message))
|
||||
}
|
||||
|
||||
// --------------------
|
||||
// 心跳逻辑(链式)
|
||||
// --------------------
|
||||
// ---------------- 心跳 ----------------
|
||||
function sendPing() {
|
||||
if (!socket.value || socket.value.readyState !== WebSocket.OPEN)
|
||||
return
|
||||
|
||||
try {
|
||||
sendMessage('PING')
|
||||
}
|
||||
catch (err) {
|
||||
console.error('发送 PING 失败', err)
|
||||
}
|
||||
sendMessage({ type: 'ping' })
|
||||
|
||||
// 设置 PONG 超时
|
||||
if (pongTimeout)
|
||||
clearTimeout(pongTimeout)
|
||||
clearTimeout(pongTimeout)
|
||||
pongTimeout = setTimeout(() => {
|
||||
console.warn('未收到PONG,触发重连')
|
||||
handleReconnect()
|
||||
@@ -63,15 +56,8 @@ export function useWebSocket() {
|
||||
}
|
||||
|
||||
function handlePong() {
|
||||
// 收到 PONG,清理超时
|
||||
if (pongTimeout) {
|
||||
clearTimeout(pongTimeout)
|
||||
pongTimeout = null
|
||||
}
|
||||
|
||||
// 等 PING_INTERVAL 后再发送下一次 PING
|
||||
if (pingTimer)
|
||||
clearTimeout(pingTimer)
|
||||
clearTimeout(pongTimeout)
|
||||
clearTimeout(pingTimer)
|
||||
pingTimer = setTimeout(sendPing, PING_INTERVAL)
|
||||
}
|
||||
|
||||
@@ -79,62 +65,57 @@ export function useWebSocket() {
|
||||
sendPing()
|
||||
}
|
||||
|
||||
// --------------------
|
||||
// 重连逻辑
|
||||
// --------------------
|
||||
// ---------------- 重连 ----------------
|
||||
function handleReconnect() {
|
||||
if (isReconnecting)
|
||||
return
|
||||
if (isReconnecting || manualClose)
|
||||
return // ✅ 如果是手动断开则不重连
|
||||
|
||||
isReconnecting = true
|
||||
cleanup()
|
||||
isConnected.value = false
|
||||
socket.value?.close()
|
||||
|
||||
reconnectAttempts++
|
||||
if (reconnectNotification)
|
||||
reconnectNotification.close()
|
||||
reconnectNotification?.close()
|
||||
reconnectNotification = ElNotification({
|
||||
title: '提示',
|
||||
message: `WebSocket 连接失败,正在第 ${reconnectAttempts} 次重连...`,
|
||||
type: 'warning',
|
||||
duration: 0,
|
||||
key: 'websocket-reconnect',
|
||||
})
|
||||
|
||||
const delay = Math.min(3000 * 2 ** (reconnectAttempts - 1), 30000)
|
||||
setTimeout(() => {
|
||||
isReconnecting = false
|
||||
connect(currentUrl)
|
||||
connect()
|
||||
}, delay)
|
||||
}
|
||||
|
||||
function clearReconnectNotification() {
|
||||
if (reconnectNotification) {
|
||||
reconnectNotification.close()
|
||||
reconnectNotification = null
|
||||
}
|
||||
reconnectNotification?.close()
|
||||
reconnectNotification = null
|
||||
}
|
||||
|
||||
// --------------------
|
||||
// 公共方法
|
||||
// --------------------
|
||||
function connect(url) {
|
||||
currentUrl = url
|
||||
// ---------------- 公共方法 ----------------
|
||||
function connect() {
|
||||
if (isConnected.value)
|
||||
return
|
||||
if (socket.value)
|
||||
disconnect()
|
||||
|
||||
const newSocket = new WebSocket(url)
|
||||
manualClose = false // ✅ 每次连接前重置为 false
|
||||
|
||||
const newSocket = new WebSocket(currentUrl)
|
||||
|
||||
newSocket.onopen = () => {
|
||||
isConnected.value = true
|
||||
isReconnecting = false
|
||||
clearReconnectNotification()
|
||||
|
||||
if (userStore.isLogined)
|
||||
sendLogin(userStore.userInfo.userid)
|
||||
|
||||
ElNotification({
|
||||
title: '提示',
|
||||
message: 'WebSocket 连接成功',
|
||||
message: '上线成功',
|
||||
type: 'success',
|
||||
duration: 2000,
|
||||
})
|
||||
@@ -144,26 +125,42 @@ export function useWebSocket() {
|
||||
}
|
||||
|
||||
newSocket.onmessage = (event) => {
|
||||
let message
|
||||
let resData
|
||||
try {
|
||||
message = JSON.parse(event.data)
|
||||
console.log(event.data)
|
||||
|
||||
if (message.type === 'PONG') {
|
||||
handlePong()
|
||||
return
|
||||
}
|
||||
|
||||
notifyListeners(message)
|
||||
resData = JSON.parse(event.data)
|
||||
}
|
||||
catch (err) {
|
||||
console.error('消息解析失败:', err)
|
||||
notifyListeners({ type: 'ERROR', data: '消息解析失败' })
|
||||
return
|
||||
}
|
||||
|
||||
if (resData.code !== 200) {
|
||||
ElNotification({
|
||||
title: '错误',
|
||||
message: resData.message || '发生未知错误',
|
||||
type: 'error',
|
||||
showClose: true,
|
||||
duration: 3000,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (resData.type === 'pong') {
|
||||
handlePong()
|
||||
}
|
||||
else {
|
||||
notifyListeners(resData)
|
||||
}
|
||||
}
|
||||
|
||||
newSocket.onclose = () => {
|
||||
handleDisconnect()
|
||||
if (!isReconnecting)
|
||||
|
||||
// ✅ 非手动断开才重连
|
||||
if (!manualClose && !isReconnecting)
|
||||
handleReconnect()
|
||||
}
|
||||
|
||||
@@ -175,14 +172,25 @@ export function useWebSocket() {
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
if (!socket.value)
|
||||
return
|
||||
|
||||
manualClose = true
|
||||
socket.value?.close()
|
||||
cleanup()
|
||||
clearReconnectNotification()
|
||||
|
||||
ElNotification({
|
||||
type: 'warning',
|
||||
title: '提示',
|
||||
message: '已下线',
|
||||
})
|
||||
}
|
||||
|
||||
function sendMessage(type, data) {
|
||||
function sendMessage(data) {
|
||||
if (isConnected.value && socket.value) {
|
||||
const message = { type, data }
|
||||
socket.value.send(JSON.stringify(message))
|
||||
data.orgId = userStore.userInfo.orgid || ''
|
||||
socket.value.send(JSON.stringify(data))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,13 +199,23 @@ export function useWebSocket() {
|
||||
return () => listeners.value.delete(callback)
|
||||
}
|
||||
|
||||
onScopeDispose(() => disconnect())
|
||||
function sendLogin(userId) {
|
||||
if (!isConnected.value)
|
||||
return
|
||||
sendMessage({
|
||||
type: 'login',
|
||||
from: userId,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
instance = {
|
||||
isConnected,
|
||||
connect,
|
||||
disconnect,
|
||||
sendMessage,
|
||||
onMessage,
|
||||
sendLogin,
|
||||
}
|
||||
|
||||
return instance
|
||||
}
|
||||
@@ -1,34 +1,51 @@
|
||||
// src/utils/axios.js
|
||||
import axios from 'axios'
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
|
||||
const instance = axios.create({
|
||||
baseURL: 'http://localhost:8089/api', // 后端接口地址
|
||||
timeout: 5000
|
||||
})
|
||||
timeout: 5000,
|
||||
})
|
||||
|
||||
// 请求拦截器:自动添加 token
|
||||
instance.interceptors.request.use(
|
||||
config => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
(config) => {
|
||||
const userStore = useUserStore()
|
||||
|
||||
if (userStore.userInfo.token) {
|
||||
config.headers.Authorization = `Bearer ${userStore.userInfo.token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => Promise.reject(error)
|
||||
error => Promise.reject(error),
|
||||
)
|
||||
|
||||
// 响应拦截器:统一处理错误或过期 token
|
||||
instance.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
(error) => {
|
||||
const userStore = useUserStore()
|
||||
if (error.response && error.response.status === 401) {
|
||||
alert('登录失效,请重新登录')
|
||||
window.localStorage.removeItem('token')
|
||||
userStore.resetUserInfo()
|
||||
window.location.href = '/login'
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export function axiosPost(url, data = {}, config = {}) {
|
||||
return instance.post(url, data, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
...config,
|
||||
}).then(res => res.data)
|
||||
}
|
||||
|
||||
export function axiosGet(url, params = {}, config = {}) {
|
||||
return instance.get(url, {
|
||||
params,
|
||||
...config,
|
||||
}).then(res => res.data)
|
||||
}
|
||||
|
||||
export default instance
|
||||
@@ -0,0 +1,127 @@
|
||||
<script setup>
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
import { axiosPost } from '@/utils/axios'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const router = useRouter()
|
||||
const form = ref({
|
||||
orgid: userStore.userInfo.orgid,
|
||||
userid: userStore.userInfo.userid,
|
||||
password: '',
|
||||
})
|
||||
const loginLoading = ref(false)
|
||||
|
||||
async function onLogin() {
|
||||
loginLoading.value = true
|
||||
try {
|
||||
const resData = await axiosPost(
|
||||
'/login',
|
||||
{
|
||||
orgid: form.value.orgid,
|
||||
userid: form.value.userid,
|
||||
password: form.value.password,
|
||||
},
|
||||
)
|
||||
if (resData.token) {
|
||||
userStore.setUserInfo(form.value.orgid, form.value.userid, resData.userInfo.username, resData.token)
|
||||
ElMessage.success(resData.message || '登录成功')
|
||||
router.push('/home')
|
||||
}
|
||||
else {
|
||||
ElMessage.error(resData.message || '登录失败')
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
ElMessage.error(`请求错误:${err.response?.data?.message || err.message}`)
|
||||
}
|
||||
finally {
|
||||
loginLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app">
|
||||
<div class="login">
|
||||
<div class="login-left">
|
||||
<img src="/vite.svg" alt="Logo">
|
||||
</div>
|
||||
<div class="login-right">
|
||||
<div class="login-right-title">
|
||||
Online
|
||||
</div>
|
||||
<el-form :model="form" label-width="auto" label-position="top" size="large">
|
||||
<el-form-item label="用户名" required>
|
||||
<el-input v-model="form.userid" style="height: 36px !important" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码">
|
||||
<el-input
|
||||
v-model="form.password" type="password" show-password style="height: 36px !important"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary" :loading="loginLoading" style="width: 100%; margin-top: 16px"
|
||||
@click="onLogin"
|
||||
>
|
||||
登录
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="css">
|
||||
.app {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.login {
|
||||
display: flex;
|
||||
width: 1000px;
|
||||
height: 500px;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0px 6px 24px -8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.login-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 60%;
|
||||
padding: 32px;
|
||||
border-right: 1px solid #e9e9e9;
|
||||
}
|
||||
|
||||
.login-left img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.login-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
justify-content: center;
|
||||
width: 50%;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.login-right-title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,67 @@
|
||||
<script setup>
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
import { axiosGet, axiosPost } from '@/utils/axios'
|
||||
import { useWebSocket } from '../../stores/websocketStore'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const websocket = useWebSocket()
|
||||
|
||||
const tableData = ref([])
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await axiosGet('/loadCompanyList')
|
||||
tableData.value = res.data
|
||||
}
|
||||
catch (err) {
|
||||
ElMessage.error(`请求错误:${err.response?.data?.message || err.message}`)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="home-container">
|
||||
<div class="welcome">
|
||||
{{ userStore.userInfo.username }},欢迎登录
|
||||
</div>
|
||||
<div class="home-header">
|
||||
IM 聊天列表(支持多公司,单公司聊天)
|
||||
</div>
|
||||
<el-table :data="tableData" border stripe style="width: 50%;">
|
||||
<el-table-column prop="b_name" label="用户名称" />
|
||||
<el-table-column prop="_online" label="在线状态" />
|
||||
|
||||
<el-table-column fixed="right" label="操作">
|
||||
<template #default="scope">
|
||||
<el-button v-if="userStore.userInfo.userid !== scope.row.b_id" link type="primary" size="small">
|
||||
聊天
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.home-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 40px 20px;
|
||||
}
|
||||
|
||||
.welcome {
|
||||
font-size: 16px;
|
||||
color: #555;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.home-header {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 24px;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user