登录优化

This commit is contained in:
zhang zhuo 2026-01-10 16:15:47 +08:00
parent 7bcdc963b9
commit 3b6d02e21e
11 changed files with 104 additions and 153 deletions

View File

@ -7,3 +7,14 @@ VITE_APP_ENV='development'
# 开发环境
VITE_API_BASE='https://demo.leapy.cn'
VITE_WS_URL='wss://demo.leapy.cn/ws'
# SM2公钥
VITE_RSA_PUBLIC_KEY='-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgSGuTW9sIHkDCOcXe0Gk
euU82C9vGDaL569ChxdG3Ab4BCu1LqtGARMBeTW99amHbRbJE4/dzafUsGEh2rOq
/yb/qwKixtk1PWfrcqTuHJ8vmuj9MCQ0EIirKzc7lvGDUoIQuGAyQMQOTx2/iiDW
Kk50n1wtA4j8CITNuvZIXcfyKcPNtrgvBnhIuBVuZ7+X8oEjiO4nknVN2HrgeDK7
aQ4B43MR9rraqaupOv2l8Ua1nwMI3BtBdhQgXkjzMruHehL5+Bq4EHY01mCccUWv
7YoL2R9Idu8KKxvMxypO1SffMGj3ViE4TvAQHU+eRrnXWDv2c7WCXSzSZUPfXalO
EwIDAQAB
-----END PUBLIC KEY-----'

View File

@ -7,3 +7,14 @@ VITE_APP_ENV='production'
# 生产环境
VITE_API_BASE='https://demo.leapy.cn'
VITE_WS_URL='wss://demo.leapy.cn/ws'
# SM2公钥
VITE_RSA_PUBLIC_KEY='-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgSGuTW9sIHkDCOcXe0Gk
euU82C9vGDaL569ChxdG3Ab4BCu1LqtGARMBeTW99amHbRbJE4/dzafUsGEh2rOq
/yb/qwKixtk1PWfrcqTuHJ8vmuj9MCQ0EIirKzc7lvGDUoIQuGAyQMQOTx2/iiDW
Kk50n1wtA4j8CITNuvZIXcfyKcPNtrgvBnhIuBVuZ7+X8oEjiO4nknVN2HrgeDK7
aQ4B43MR9rraqaupOv2l8Ua1nwMI3BtBdhQgXkjzMruHehL5+Bq4EHY01mCccUWv
7YoL2R9Idu8KKxvMxypO1SffMGj3ViE4TvAQHU+eRrnXWDv2c7WCXSzSZUPfXalO
EwIDAQAB
-----END PUBLIC KEY-----'

View File

@ -25,6 +25,7 @@
"element-plus": "2.13.0",
"highlight.js": "^11.11.1",
"image-conversion": "^2.1.1",
"jsencrypt": "^3.5.4",
"nprogress": "^0.2.0",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
@ -37,6 +38,7 @@
"xgplayer-hls": "^3.0.23"
},
"devDependencies": {
"@types/crypto-js": "^4.2.2",
"@types/node": "^25.0.3",
"@vitejs/plugin-vue": "^6.0.3",
"eslint": "^9.39.2",

View File

@ -22,4 +22,6 @@ export default {
//布局 分栏column | 通栏header | 经典menu | 功能坞dock
//dock将关闭标签和面包屑栏
APP_LAYOUT: 'column',
// sm2公钥
RSA_PUBLIC_KEY: import.meta.env.VITE_RSA_PUBLIC_KEY
}

View File

@ -7,7 +7,7 @@ import tools from '@/utils/tools'
import api from "@/api"
import sRouter from './system'
import {treeFilter, filterAsyncRouter, flatAsyncRoutes} from '@/utils/route'
import {beforeEach, afterEach} from '@/utils/route'
import {beforeEach, afterEach, makeMenu} from '@/utils/route'
import i18n from "@/locales"
//系统路由
@ -59,7 +59,7 @@ router.beforeEach(async (to, from, next) => {
/* @ts-ignore */
const res = await api.auth.menu()
/* @ts-ignore */
tools.data.set("MENU", tools.makeMenu(res.data.menus, 0))
tools.data.set("MENU", makeMenu(res.data.menus, 0))
/* @ts-ignore */
tools.data.set("PERMISSIONS", res.data.buttons)
/* @ts-ignore */
@ -72,7 +72,7 @@ router.beforeEach(async (to, from, next) => {
})
let menu = [...userMenu, ...apiMenu]
var menuRouter = filterAsyncRouter(menu)
let menuRouter = filterAsyncRouter(menu);
menuRouter = flatAsyncRoutes(menuRouter)
menuRouter.forEach(item => {
router.addRoute("layout", item)

View File

@ -112,3 +112,29 @@ export function getMenu() {
})
return [...userMenu, ...apiMenu]
}
export function makeMenu(menus, pid = 0) {
const arr = [];
for (let item of menus) {
if (item.pid === pid) {
// 数据格式处理
const tmp = {
name: item['name'],
path: item['type'] == 0 ? '/' + item['path'] : item['path'],
component: item['path'],
meta: {
'title': item['title'],
'icon': item['icon'],
'hidden': item['hidden'],
'type': item['type'] == 0 ? 'menu' : 'link'
}
};
const children = makeMenu(menus, item.menu_id);
if (children.length > 0) {
tmp['children'] = children
}
arr.push(tmp)
}
}
return arr
}

View File

@ -1,4 +1,6 @@
import CryptoJS from 'crypto-js';
import CryptoJS from 'crypto-js'
import JSEncrypt from 'jsencrypt'
import config from "@/config"
const tools = {
data: {
@ -11,7 +13,9 @@ const tools = {
},
get(cacheKey: string) {
try {
const cacheValue = JSON.parse(tools.base64.decrypt(localStorage.getItem(cacheKey)))
const data = localStorage.getItem(cacheKey);
if (!data) return null
const cacheValue = JSON.parse(tools.base64.decrypt(data))
if (cacheValue) {
let nowTime = new Date().getTime()
if (nowTime > cacheValue.expireIn && cacheValue.expireIn !== 0) {
@ -48,36 +52,36 @@ const tools = {
return [null, err]
}
},
crypto: {
//MD5加密
MD5(data: string) {
return CryptoJS.MD5(data).toString()
},
md5: function (data: string) {
return CryptoJS.MD5(data).toString()
},
makeMenu: function (menus, pid = 0) {
const arr = [];
for (let item of menus) {
if (item.pid === pid) {
// 数据格式处理
const tmp = {
name: item['name'],
path: item['type'] == 0 ? '/' + item['path'] : item['path'],
component: item['path'],
meta: {
'title': item['title'],
'icon': item['icon'],
'hidden': item['hidden'],
'type': item['type'] == 0 ? 'menu' : 'link'
}
};
const children = this.makeMenu(menus, item.menu_id);
if (children.length > 0) {
tmp['children'] = children
}
arr.push(tmp)
}
aes: {
encrypt(message: string, secretKey: string): string {
return CryptoJS.AES.encrypt(message, secretKey).toString()
},
decrypt(ciphertext: string, secretKey: string) {
CryptoJS.AES.decrypt(ciphertext, secretKey).toString(CryptoJS.enc.Utf8)
}
},
rsa: {
encrypt(message: string): string {
const encryptor = new JSEncrypt()
encryptor.setPublicKey(config.RSA_PUBLIC_KEY)
const encrypted = encryptor.encrypt(message)
if (!encrypted) {
throw new Error('RSA encrypt failed')
}
return encrypted;
},
decrypt(cipherText: string, privateKey: string): string {
const decryptor = new JSEncrypt()
decryptor.setPrivateKey(privateKey)
const decrypted = decryptor.decrypt(cipherText)
if (!decrypted) {
throw new Error('RSA decrypt failed')
}
return decrypted
}
return arr
},
screen: function (element) {
var isFull = !!(document.webkitIsFullScreen || document.mozFullScreen || document.msFullscreenElement || document.fullscreenElement);
@ -129,16 +133,13 @@ const tools = {
}
return fmt;
},
randomUUIDString: function (len = 16) {
return crypto.randomUUID().replace(/-/g, '').slice(0, len)
},
makeTreeData: function (data, pid = 0, key = "id", parent = "parent_id") {
const arr = [];
for (let item of data) {
if (item[parent] == pid) {
// 数据格式处理
const tmp = item;
const children = tools.makeTreeData(data, item[key], key, parent);
const children = this.makeTreeData(data, item[key], key, parent);
if (children.length > 0) {
tmp['children'] = children
}
@ -147,6 +148,9 @@ const tools = {
}
return arr
},
randomUUIDString: function (len = 16) {
return crypto.randomUUID().replace(/-/g, '').slice(0, len)
},
getBrowser(userAgent: string) {
// 检测浏览器类型和版本
if (/Opera|OPR/.test(userAgent)) {

View File

@ -1,86 +0,0 @@
<template>
<el-main class="pi-page">
<el-page-header @back="onBack" :content="title" class="header">
<template #extra>
<div class="flex items-center">
<el-popconfirm title="确定删除吗?" @confirm="del">
<template #reference>
<el-button type="danger" circle icon="el-icon-delete" :disabled="!info.mess_id"/>
</template>
</el-popconfirm>
</div>
</template>
</el-page-header>
<section v-if="info.mess_id" class="intro">
<el-text>发布人{{ info.nickname }}</el-text>
<el-text v-time="info.create_time"></el-text>
</section>
<section v-if="info.mess_id" v-html="info.content"/>
<section v-if="!info.mess_id" class="empty">
<img src="@/assets/images/404.png">
</section>
</el-main>
</template>
<script setup>
import {getCurrentInstance, onMounted, ref} from "vue";
import api from "@/api"
import useTabs from "@/utils/useTabs"
import {useRoute} from "vue-router"
defineOptions({
name: "messageDetail"
})
const {proxy} = getCurrentInstance()
const route = useRoute()
let info = ref({})
let title = ref("消息不存在")
onMounted(() => {
loadData()
})
async function loadData() {
const res = await api.system.message.detail({id: route.query.id})
if (res.data) {
info.value = res.data
title.value = info.value.title
}
}
function onBack() {
useTabs.close()
}
async function del() {
const res = await api.system.message.remove({id: route.query.id})
proxy.$message.success(res.msg)
onBack()
}
</script>
<style lang="scss" scoped>
.pi-page {
background: var(--el-bg-color);
margin: 20px auto;
padding: 25px;
}
.header {
margin-bottom: 15px;
}
.empty {
text-align: center;
padding: 80px 0;
}
.intro {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
</style>

View File

@ -186,11 +186,11 @@ async function login() {
}
isLogin.value = true
const data = {
username: form.value.username,
password: tools.crypto.MD5(form.value.password),
username: tools.rsa.encrypt(form.value.username),
password: tools.rsa.encrypt(tools.md5(form.value.password)),
uuid: form.value.uuid,
code: form.value.code
};
code: tools.rsa.encrypt(form.value.code)
}
//
const [res, err] = await tools.go(api.auth.login(data))
isLogin.value = false

View File

@ -1,33 +1,13 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": [
"ESNext",
"DOM"
],
"jsx": "preserve",
"strict": true,
"skipLibCheck": true,
"types": [
"node",
"vite/client"
],
"baseUrl": ".",
"moduleResolution": "node",
"types": ["node"],
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"
]
"@/*": ["src/*"]
},
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"ignoreDeprecations": "6.0"
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue"
]
"allowSyntheticDefaultImports": true
}
}

1
vite-env.d.ts vendored
View File

@ -12,6 +12,7 @@ interface ImportMetaEnv {
readonly VITE_APP_TITLE: string
readonly VITE_WS_URL: string,
readonly VITE_APP_ENV: string
readonly VITE_RSA_PUBLIC_KEY: string
}
interface Document {