登录优化

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_API_BASE='https://demo.leapy.cn'
VITE_WS_URL='wss://demo.leapy.cn/ws' 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_API_BASE='https://demo.leapy.cn'
VITE_WS_URL='wss://demo.leapy.cn/ws' 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", "element-plus": "2.13.0",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"image-conversion": "^2.1.1", "image-conversion": "^2.1.1",
"jsencrypt": "^3.5.4",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1", "pinia-plugin-persistedstate": "^4.7.1",
@ -37,6 +38,7 @@
"xgplayer-hls": "^3.0.23" "xgplayer-hls": "^3.0.23"
}, },
"devDependencies": { "devDependencies": {
"@types/crypto-js": "^4.2.2",
"@types/node": "^25.0.3", "@types/node": "^25.0.3",
"@vitejs/plugin-vue": "^6.0.3", "@vitejs/plugin-vue": "^6.0.3",
"eslint": "^9.39.2", "eslint": "^9.39.2",

View File

@ -22,4 +22,6 @@ export default {
//布局 分栏column | 通栏header | 经典menu | 功能坞dock //布局 分栏column | 通栏header | 经典menu | 功能坞dock
//dock将关闭标签和面包屑栏 //dock将关闭标签和面包屑栏
APP_LAYOUT: 'column', 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 api from "@/api"
import sRouter from './system' import sRouter from './system'
import {treeFilter, filterAsyncRouter, flatAsyncRoutes} from '@/utils/route' import {treeFilter, filterAsyncRouter, flatAsyncRoutes} from '@/utils/route'
import {beforeEach, afterEach} from '@/utils/route' import {beforeEach, afterEach, makeMenu} from '@/utils/route'
import i18n from "@/locales" import i18n from "@/locales"
//系统路由 //系统路由
@ -59,7 +59,7 @@ router.beforeEach(async (to, from, next) => {
/* @ts-ignore */ /* @ts-ignore */
const res = await api.auth.menu() const res = await api.auth.menu()
/* @ts-ignore */ /* @ts-ignore */
tools.data.set("MENU", tools.makeMenu(res.data.menus, 0)) tools.data.set("MENU", makeMenu(res.data.menus, 0))
/* @ts-ignore */ /* @ts-ignore */
tools.data.set("PERMISSIONS", res.data.buttons) tools.data.set("PERMISSIONS", res.data.buttons)
/* @ts-ignore */ /* @ts-ignore */
@ -72,7 +72,7 @@ router.beforeEach(async (to, from, next) => {
}) })
let menu = [...userMenu, ...apiMenu] let menu = [...userMenu, ...apiMenu]
var menuRouter = filterAsyncRouter(menu) let menuRouter = filterAsyncRouter(menu);
menuRouter = flatAsyncRoutes(menuRouter) menuRouter = flatAsyncRoutes(menuRouter)
menuRouter.forEach(item => { menuRouter.forEach(item => {
router.addRoute("layout", item) router.addRoute("layout", item)

View File

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

View File

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

1
vite-env.d.ts vendored
View File

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