注册全局组件SVG,集成pinia,封装axios

This commit is contained in:
2024-08-08 19:08:15 +08:00
parent f0cc1b54c2
commit 74bd16dc51
31 changed files with 4753 additions and 122 deletions

View File

@@ -0,0 +1,3 @@
NODE_ENV='development'
VITE_TITLE=""
VITE_URL="http://localhost:8081"

3
frontend/.env.production Normal file
View File

@@ -0,0 +1,3 @@
NODE_ENV='production'
VITE_TITLE=""
VITE_URL="http://www.test.com"

3
frontend/.eslintignore Normal file
View File

@@ -0,0 +1,3 @@
// .eslintignore 配置, 防止校验打包的产物
dist
node_modules

View File

@@ -9,14 +9,15 @@
"build-staging": "vite build --mode staging", "build-staging": "vite build --mode staging",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build --force", "type-check": "vue-tsc --build --force",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
"axios": "^1.7.3",
"element-plus": "^2.7.8", "element-plus": "^2.7.8",
"pinia": "^2.1.7", "pinia": "^2.2.1",
"pinia-plugin-persistedstate": "^3.2.1",
"vue": "^3.4.29", "vue": "^3.4.29",
"vue-router": "^4.3.3" "vue-router": "^4.3.3"
}, },
@@ -36,6 +37,7 @@
"typescript": "~5.4.0", "typescript": "~5.4.0",
"vite": "^5.3.1", "vite": "^5.3.1",
"vite-plugin-node-polyfills": "^0.22.0", "vite-plugin-node-polyfills": "^0.22.0",
"vite-plugin-svg-icons": "^2.0.1",
"vue-tsc": "^2.0.21" "vue-tsc": "^2.0.21"
} }
} }

View File

@@ -12,7 +12,7 @@ defineOptions({
import { ElConfigProvider } from 'element-plus' import { ElConfigProvider } from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn' import zhCn from 'element-plus/es/locale/lang/zh-cn'
document.getElementById('loadingPage').remove() document.getElementById('loadingPage')?.remove()
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -1,4 +1,3 @@
/** /**
* 主进程与渲染进程通信频道定义 * 主进程与渲染进程通信频道定义
* Definition of communication channels between main process and rendering process * Definition of communication channels between main process and rendering process
@@ -7,7 +6,4 @@ const ipcApiRoute = {
test: 'controller.example.test', test: 'controller.example.test',
} }
export { export { ipcApiRoute }
ipcApiRoute
}

View File

@@ -0,0 +1,57 @@
import request from '@/utils/http'
import { useUserInfoStore } from '@/stores/user'
import { sm3Digest } from '@/assets/commjs/sm3.js'
import { sm2, encrypt } from '@/assets/commjs/sm2.js'
// 获取公钥
export const publicKey = (params?: any) => {
if (!params) {
const userInfo = useUserInfoStore()
params = {
loginName: encrypt(userInfo.loginName),
}
}
return request({
url: '/user/generateSm2Key',
method: 'get',
params,
})
}
export const pwdSm3 = async (pwd: any, loginName?: string) => {
let publicKeyStr = await publicKey(
loginName ? { loginName: encrypt(loginName) } : false,
)
let sm3Pwd = sm3Digest(pwd) //SM3加密
return sm2(sm3Pwd + '|' + pwd, publicKeyStr.data, 0)
}
//登录获取token
export const login = async (params: any) => {
params.password = await pwdSm3(params.password, params.username)
params.username = encrypt(params.username)
return request({
url: '/pqs-auth/oauth/token',
method: 'post',
params,
})
}
//获取用户信息
export const getUserById = () => {
const userInfo = useUserInfoStore()
return request({
url: '/user-boot/user/getUserById?id=' + userInfo.userIndex,
method: 'get',
})
}
// 刷新token
export function refreshToken(): Promise<any> {
const userInfo = useUserInfoStore()
return login({
grant_type: 'refresh_token',
refresh_token: userInfo.refresh_token,
username: userInfo.loginName,
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,257 @@
/**
* 国密摘要算法SM3
* @param str:raw string
* @return the 256-bit hex string produced by SM3 from a raw string
*/
function sm3Digest(str) {
//1. 转换为二进制数组
var binArr = str2bin(str2rstr_utf8(str));
//2. 填充
var groupNum = alignSM3(binArr, str.length);
//3. 迭代压缩
var v = new Array(8);//初始值
v[0] = 0x7380166f;
v[1] = 0x4914b2b9;
v[2] = 0x172442d7;
v[3] = 0xda8a0600;
v[4] = 0xa96f30bc;
v[5] = 0x163138aa;
v[6] = 0xe38dee4d;
v[7] = 0xb0fb0e4e;
//按 512bit 分组进行压缩
for (var i = 0; i < groupNum; i++) {
v = compress(v, binArr, i);
}
return word2str(v, '');
}
/**
* 将数组转换为字符串。数组长度不定,每个元素为 32bit 的数字。
* @param words:数组,每个元素为 32bit 的数字
* @param seperator:在每个数组元素转换得到的字符串之间的分隔符
*/
function word2str(words, seperator) {
var prefix = Array(8).join('0');
for (var i = 0; i < words.length; i++) {
//若 hex 不足 8 位,则高位补 0
words[i] = (prefix + (words[i] >>> 0).toString(16)).slice(-8);
}
return words.join(seperator);
}
/**
* 将字符串转换为二进制数组,默认字符串编码为 UTF-8且范围在 0x00~0xFF 内。
* 若某些字符的编码超过此范围,则会只保留最低字节。加密可正常进行,但加密结果有误。
* 每个数组元素包含 4 个字符,即 32 bit。
* @param 字符串
* @return 数组,长度为(字符串长度 / 4每个元素为 32bit 的数字
*/
function str2bin(str) {
var binary = new Array(str.length >> 2);
for (var i = 0; i < str.length * 8; i += 8) {
binary[i >> 5] |= (str.charCodeAt(i / 8) & 0xFF) << (24 - i % 32);
}
return binary;
}
/**
* 对明文的二进制串进行填充
* <pre>
* | 满足 mod 512 = 448 | 固定 64 位 |
* | 明文二进制 |填充部分|明文二进制串的长度的二进制表示|
* xxxxxxxxxxxx 10.....0 0...........................xx
* </pre>
* @param arr:数组,每个元素为 32bit 的数字
* @param strLen明文字符串长度
* @return 数组,每个元素为 32bit 的数字,数组长度为 16 的倍数(包括 16
*/
function alignSM3(arr, strLen) {
//在明文二进制串后面拼接 1000 0000
arr[strLen >> 2] |= 0x80 << (24 - strLen % 4 * 8);
var groupNum = ((strLen + 8) >> 6) + 1;//以 512bit 为一组,总的组数
var wordNum = groupNum * 16;//一个 word 32bit总的 word 数
for (var i = (strLen >> 2) + 1; i < wordNum; i++) {
arr[i] = 0;
}
arr[wordNum - 1] = strLen * 8;//在末尾填上明文的二进制长度
return groupNum;
}
/**
* 压缩函数中的置换函数
*/
function p0(x) {
return x ^ bitRol(x, 9) ^ bitRol(x, 17);
}
/**
* 压缩函数中的置换函数
*/
function p1(x) {
return x ^ bitRol(x, 15) ^ bitRol(x, 23);
}
/**
* 循环左移
*/
function bitRol(input, n) {
return (input << n) | (input >>> (32 - n));
}
/**
* 压缩函数
*/
function compress(v, binArr, i) {
//将消息分组扩展成 132 个字
var w1 = new Array(68);
var w2 = new Array(64);
for (var j = 0; j < 68; j++) {
if (j < 16) {
w1[j] = binArr[i * 16 + j];
} else {
w1[j] = p1(w1[j-16] ^ w1[j-9] ^ bitRol(w1[j-3], 15)) ^ bitRol(w1[j-13], 7) ^ w1[j-6];
}
}
for (var j = 0; j < 64; j++) {
w2[j] = w1[j] ^ w1[j+4];
}
//压缩
var a = v[0];
var b = v[1];
var c = v[2];
var d = v[3];
var e = v[4];
var f = v[5];
var g = v[6];
var h = v[7];
var ss1;
var ss2;
var tt1;
var tt2;
for (var j = 0; j < 64; j++) {
ss1 = bitRol(addAll(bitRol(a, 12) , e , bitRol(t(j), j)), 7);
ss2 = ss1 ^ bitRol(a, 12);
tt1 = addAll(ff(a, b, c, j) , d , ss2 , w2[j]);
tt2 = addAll(gg(e, f, g, j) , h , ss1 , w1[j]);
d = c;
c = bitRol(b, 9);
b = a;
a = tt1;
h = g;
g = bitRol(f, 19);
f = e;
e = p0(tt2);
}
v[0] ^= a;
v[1] ^= b;
v[2] ^= c;
v[3] ^= d;
v[4] ^= e;
v[5] ^= f;
v[6] ^= g;
v[7] ^= h;
return v;
}
/**
* 常量 T 随 j 的不同而不同
*/
function t(j) {
if (0 <= j && j < 16) {
return 0x79CC4519;
} else if (j < 64) {
return 0x7A879D8A;
}
}
/**
* 布尔函数,随 j 的变化取不同的表达式
*/
function ff(x, y, z, j) {
if (0 <= j && j < 16) {
return x ^ y ^ z;
} else if (j < 64) {
return (x & y) | (x & z) | (y & z);
}
}
/**
* 布尔函数,随 j 的变化取不同的表达式
*/
function gg(x, y, z, j) {
if (0 <= j && j < 16) {
return x ^ y ^ z;
} else if (j < 64) {
return (x & y) | (~x & z);
}
}
/**
* 两数相加
* 避免某些 js 引擎的 32 位加法的 bug
*/
function safe_add(x, y) {
var lsw = ( x & 0xFFFF ) + (y & 0xFFFF);
var msw = ( x >> 16 ) + (y >> 16) + (lsw >> 16);
return (msw << 16) | ( lsw & 0xFFFF );
}
/**
* 将所有参数相加
*/
function addAll() {
var sum = 0;
for (var i = 0; i < arguments.length; i++) {
sum = safe_add(sum, arguments[i]);
}
return sum;
}
/**
* UTF-16 --> UTF-8
*/
function str2rstr_utf8(input) {
var output = "" ;
var i = -1 ;
var x, y ;
while(++ i < input.length) {
//按 UTF-16 解码
x = input.charCodeAt(i);
y = i + 1 < input.length ? input .charCodeAt (i + 1) : 0 ;
if( 0xD800 <= x && x <= 0xDBFF && 0xDC00 <= y && y <= 0xDFFF ) {
x = 0x10000 + ((x & 0x03FF) << 10 ) + (y & 0x03FF);
i++;
}
//按 UTF-8 编码
if( x <= 0x7F ) {
output += String.fromCharCode(x);
}
else if(x <= 0x7FF) {
output += String.fromCharCode(
0xC0 | ((x >>> 6 ) & 0x1F),
0x80 | ( x & 0x3F ));
} else if(x <= 0xFFFF) {
output += String.fromCharCode(
0xE0 | ((x >>> 12) & 0x0F ),
0x80 | ((x >>> 6 ) & 0x3F),
0x80 | ( x & 0x3F ));
} else if(x <= 0x1FFFFF) {
output += String.fromCharCode(
0xF0 | ((x >>> 18) & 0x07 ),
0x80 | ((x >>> 12) & 0x3F),
0x80 | ((x >>> 6 ) & 0x3F),
0x80 | ( x & 0x3F ));
}
}
return output;
}
export {
sm3Digest
}

View File

@@ -0,0 +1 @@
<svg t="1723085514271" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1125" width="200" height="200"><path d="M765.22 393.6H184.15c-8.28 0-15-6.72-15-15s6.72-15 15-15h581.07c37.38 0 67.8-30.41 67.8-67.8s-30.41-67.8-67.8-67.8-67.8 30.41-67.8 67.8c0 8.28-6.72 15-15 15s-15-6.72-15-15c0-53.93 43.87-97.8 97.8-97.8s97.8 43.87 97.8 97.8-43.87 97.8-97.8 97.8zM783.99 809.79c-43.58 0-79.03-35.45-79.03-79.03 0-8.28 6.72-15 15-15s15 6.72 15 15c0 27.04 22 49.03 49.03 49.03s49.03-22 49.03-49.03-22-49.03-49.03-49.03H297.67c-8.28 0-15-6.72-15-15s6.72-15 15-15h486.32c43.58 0 79.03 35.45 79.03 79.03s-35.45 79.03-79.03 79.03z" fill="#333333" p-id="1126"></path><path d="M481.66 306.63H312.79c-8.28 0-15-6.72-15-15s6.72-15 15-15h168.87c8.28 0 15 6.72 15 15s-6.72 15-15 15zM227.46 681.49h-43c-8.28 0-15-6.72-15-15s6.72-15 15-15h43c8.28 0 15 6.72 15 15s-6.72 15-15 15zM777.81 598.8h-43c-8.28 0-15-6.72-15-15s6.72-15 15-15h43c8.28 0 15 6.72 15 15s-6.72 15-15 15zM383.78 761H241.87c-8.28 0-15-6.72-15-15s6.72-15 15-15h141.91c8.28 0 15 6.72 15 15s-6.72 15-15 15zM848.02 452.99H613.98c-8.28 0-15-6.72-15-15s6.72-15 15-15h234.04c8.28 0 15 6.72 15 15s-6.72 15-15 15z" fill="#333333" p-id="1127"></path><path d="M492 535.33m-243.33 0a243.33 243.33 0 1 0 486.66 0 243.33 243.33 0 1 0-486.66 0Z" fill="#FFFFFF" p-id="1128"></path><path d="M492 793.67c-69 0-133.88-26.87-182.67-75.66-48.79-48.79-75.66-113.67-75.66-182.67s26.87-133.88 75.66-182.67C358.12 303.88 423 277.01 492 277.01s133.88 26.87 182.67 75.66c48.79 48.79 75.66 113.67 75.66 182.67s-26.87 133.88-75.66 182.67C625.88 766.8 561 793.67 492 793.67zM492 307c-60.99 0-118.33 23.75-161.46 66.88-43.13 43.13-66.88 100.47-66.88 161.46s23.75 118.33 66.88 161.46c43.13 43.13 100.47 66.88 161.46 66.88s118.33-23.75 161.46-66.88 66.88-100.47 66.88-161.46-23.75-118.33-66.88-161.46S552.99 307 492 307z" fill="#333333" p-id="1129"></path><path d="M583.85 517.56l-58.52-11.99 24.71-97.1c5.39-21.17-20.94-35.62-35.9-19.71L393.3 517.17c-11.35 12.06-5.06 31.89 11.16 35.22l58.52 11.99-24.71 97.1c-5.39 21.17 20.94 35.62 35.9 19.71l120.84-128.41c11.35-12.06 5.06-31.89-11.16-35.22z" fill="#52E217" p-id="1130"></path><path d="M458.92 702.95c-5.94 0-11.96-1.5-17.55-4.57-14.68-8.06-21.77-24.38-17.64-40.6l20.83-81.86-43.12-8.83c-13.02-2.67-23.19-11.82-27.21-24.49s-0.97-26.02 8.13-35.7L503.2 378.49c11.48-12.2 29.05-14.97 43.73-6.92 14.68 8.06 21.77 24.38 17.64 40.6l-20.83 81.86 43.12 8.83c13.02 2.67 23.19 11.82 27.21 24.49s0.97 26.02-8.13 35.7L485.1 691.46c-7.1 7.54-16.53 11.48-26.17 11.49z m70.43-305.96c-1.33 0-2.84 0.5-4.3 2.04L404.21 527.44c-2.15 2.29-1.79 4.79-1.38 6.07 0.41 1.29 1.55 3.54 4.63 4.17l58.52 11.99c4.02 0.82 7.52 3.25 9.7 6.73 2.18 3.48 2.84 7.69 1.83 11.66l-24.71 97.1c-1.02 4.02 1.44 6.05 3 6.91 1.56 0.86 4.6 1.84 7.44-1.18l120.84-128.41c2.15-2.29 1.79-4.79 1.38-6.07-0.41-1.29-1.55-3.54-4.63-4.17l-58.52-11.99c-4.02-0.82-7.52-3.25-9.7-6.73a14.993 14.993 0 0 1-1.83-11.66l24.71-97.1c1.02-4.02-1.44-6.05-3-6.91-0.76-0.42-1.87-0.87-3.14-0.87z" fill="#333333" p-id="1131"></path></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,19 @@
import type { App, Component } from 'vue'
// 当组件很多的时候,可以使用
import { SvgIcon } from './staticExtend/SvgIcon'
// 这个地方将合并到对象中
const Components: {
[propName: string]: Component
} = { SvgIcon }
// 批量注册全局组件
export default {
install: (app: App) => {
Object.keys(Components).forEach((key) => {
app.component(key, Components[key])
})
},
}

View File

@@ -0,0 +1,2 @@
import SvgIcon from './src/SvgIcon.vue'
export { SvgIcon }

View File

@@ -0,0 +1,89 @@
/**
* @name SvgIcon
* @description svg图标组件
* 支持定义名称、颜色、大小、旋转
* @example <SvgIcon name="icon-name" color="#fff" size="20" spin />
*/
<template>
<svg
aria-hidden="true"
:class="['svg-icon', spin && 'svg-icon-spin']"
:style="getStyle"
>
<use :xlink:href="symbolId" :fill="color" />
</svg>
</template>
<script setup lang='ts'>
defineOptions({
name: 'SvgIcon',
})
import { computed } from 'vue'
import type { CSSProperties } from 'vue'
// 定义组件对外暴露的props
const props = defineProps({
prefix: {
type: String,
default: 'icon',
},
name: {
type: String,
required: true,
},
color: {
type: String,
default: '',
},
size: {
type: [Number, String],
default: 20,
},
spin: {
type: Boolean,
default: false,
},
})
//计算属性获取symbolId
const symbolId = computed(() => {
return `#${props.prefix}-${props.name}`
})
//计算属性获取svg样式
const getStyle = computed((): CSSProperties => {
const { size } = props
let s = `${size}`
// 确保size为px单位
s = `${s.replace('px', '')}px`
return {
width: s,
height: s,
}
})
</script>
<style scoped>
.svg-icon {
display: inline-block;
overflow: hidden;
vertical-align: -0.15em;
fill: currentColor;
}
.svg-icon-spin {
animation: loadingCircle 1.2s infinite linear;
}
/* 旋转动画 */
@keyframes loadingCircle {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
</style>

View File

@@ -1,2 +1 @@
/*文件说明本文件用来定义一些本地缓存的key*/ /*文件说明本文件用来定义一些本地缓存的key*/

View File

@@ -1,37 +1,38 @@
import { createApp } from 'vue'
import {createApp} from 'vue'
// element-plus // element-plus
import ElementPlus from 'element-plus' import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css' import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
// 使用pinia // 使用pinia
import { createPinia } from 'pinia' import pinia from '@/stores'
import App from './App.vue' import App from './App.vue'
import Router from './router/index'; import Router from './router/index'
// 引入tailwindcss // 引入tailwindcss
import '@/assets/styles/tailMain.css' import '@/assets/styles/tailMain.css'
// 导入全局注册的组件
import 'virtual:svg-icons-register'
import registerGlobComp from '@/components'
//创建实例 //创建实例
const app = createApp(App) const app = createApp(App)
const setupAll = async () => { const setupAll = async () => {
app app
.use(Router) // 使用路由 .use(Router) // 使用路由
.use(ElementPlus) // 使用ele-plus组件 .use(ElementPlus) // 使用ele-plus组件
.use(createPinia()) // 使用pinia .use(pinia) // 使用pinia
.use(registerGlobComp) // 使用全局自定义组件
// 自动引入图标
Object.keys(ElementPlusIconsVue).forEach((key) => {
app.component(key, ElementPlusIconsVue[key]);
});
//待路由初始化完毕后挂载app //待路由初始化完毕后挂载app
await Router.isReady() await Router.isReady()
} }
//挂载app //挂载app
setupAll().then(() => { setupAll().then(() => {
app.mount('#app') app.mount('#app')
}) })

View File

@@ -6,15 +6,11 @@ const Router = createRouter({
routes: routerMap, routes: routerMap,
}) })
Router.beforeEach((to, from, next) => { Router.beforeEach((to, from, next) => {
next() next()
}) })
// 路由加载后 // 路由加载后
Router.afterEach(() => { Router.afterEach(() => {})
})
export default Router export default Router

View File

@@ -8,8 +8,8 @@ const constantRouterMap = [
{ {
path: '/', path: '/',
name: 'login', name: 'login',
component: Login component: Login,
}, },
] ]
export default constantRouterMap export default constantRouterMap

View File

@@ -0,0 +1,14 @@
/**
* 本地缓存Key
*/
// 用户信息
export const USER_INFO = 'userInfo'
// WEB端布局配置
export const STORE_CONFIG = 'storeConfig'
// 后台标签页
export const STORE_TAB_VIEW_CONFIG = 'storeTabViewConfig'
// 字典
export const DICT_DATA = 'dictData'

View File

@@ -0,0 +1,3 @@
import { createPinia } from 'pinia'
const pinia = createPinia()
export default pinia

View File

@@ -0,0 +1,95 @@
// 变量名对应含义请在 /stores/* 里边找
import type { RouteRecordRaw, RouteLocationNormalized } from 'vue-router'
export interface Layout {
showDrawer: boolean
shrink: boolean
layoutMode: string
mainAnimation: string
isDark: boolean
menuWidth: number
menuDefaultIcon: string
menuCollapse: boolean
menuUniqueOpened: boolean
menuShowTopBar: boolean
elementUiPrimary: string[]
tableHeaderBackground: string[]
tableHeaderColor: string[]
tableCurrent: string[]
menuBackground: string[]
menuColor: string[]
menuActiveBackground: string[]
menuActiveColor: string[]
menuTopBarBackground: string[]
headerBarTabColor: string[]
headerBarBackground: string[]
headerBarHoverBackground: string[]
headerBarTabActiveBackground: string[]
headerBarTabActiveColor: string[]
}
export interface NavTabs {
activeIndex: number
activeRoute: RouteLocationNormalized | null
tabsView: RouteLocationNormalized[]
tabFullScreen: boolean
tabsViewRoutes: RouteRecordRaw[]
authNode: Map<string, string[]>
}
// 用户信息
export interface UserInfo {
access_token: string
token_type: string
refresh_token: string
expires_in: number
scope: string
nickname: string
userType: number
deptIndex: string
userIndex: string
client_id: string
headSculpture: any
jti: string
name: string
deptId: string
phone: string
email: string
limitIpStart: string
limitIpEnd: string
limitTime: string
casualUser: number
type: number
smsNotice: number
emailNotice: number
role: string[]
devCode: any
id: string
loginName: string
state: number
registerTime: string
loginTime: string
deptName: string
areaId: string
areaName: string
deptLevel: number
roleList: string[]
roleCode: string[]
}
// 字典数据
export interface DictData {
basic: BasicDictData[]
area: BasicDictData[]
areaTree: BasicDictData[]
userList: string[]
}
export interface BasicDictData {
name: string
id: string
code: string
value: null
sort: number | null
children?: BasicDictData[]
}

View File

@@ -0,0 +1,75 @@
import { defineStore } from 'pinia'
import { USER_INFO } from '@/stores/constant/cacheKey'
import type { UserInfo } from '@/stores/interface'
export const useUserInfoStore = defineStore('userInfo', {
// 行为
actions: {
dataFill(state: UserInfo) {
this.$state = { ...this.$state, ...state }
},
removeToken() {
this.access_token = ''
this.refresh_token = ''
},
setToken(token: string, type: 'auth' | 'refresh') {
const field = type == 'auth' ? 'token' : 'refresh_token'
this[field] = token
},
getToken(type: 'auth' | 'refresh' = 'auth') {
if (type === 'auth') {
if (this.token_type && this.access_token) {
return this.token_type + ' ' + this.access_token
} else {
return ''
}
} else {
return this.refresh_token
}
},
},
// 状态
state: (): UserInfo => {
return {
access_token: '',
token_type: '',
refresh_token: '',
expires_in: 0,
scope: '',
nickname: '',
userType: 0,
deptIndex: '',
userIndex: '',
client_id: '',
headSculpture: '',
jti: '',
name: '',
deptId: '',
phone: '',
email: '',
limitIpStart: '',
limitIpEnd: '',
limitTime: '',
casualUser: 0,
type: 0,
smsNotice: 0,
emailNotice: 0,
role: [],
devCode: '',
id: '',
loginName: '',
state: 0,
registerTime: '',
loginTime: '',
deptName: '',
areaId: '',
areaName: '',
deptLevel: 0,
roleList: [],
roleCode: [],
}
},
persist: {
key: USER_INFO,
},
})

33
frontend/src/types/global.d.ts vendored Normal file
View File

@@ -0,0 +1,33 @@
interface Window {
XEUtils: Record<string, any>
existLoading: boolean
lazy: number
unique: number
tokenRefreshing: boolean
requests: Function[]
eventSource: EventSource
loadLangHandle: Record<string, any>
}
interface anyObj {
[key: string]: any
}
interface treeData {
id?: string
value?: string
name?: string
label?: string
children: treeData[]
[key: string]: any
}
interface ApiResponse<T = any> {
code: number
data: T
msg: string
time: number
}
type ApiPromise<T = any> = Promise<ApiResponse<T>>

View File

@@ -0,0 +1,229 @@
import type { AxiosRequestConfig, Method } from 'axios'
import axios from 'axios'
import { ElLoading, ElNotification, type LoadingOptions } from 'element-plus'
import { refreshToken } from '@/api/user'
import router from '@/router/index'
import { useUserInfoStore } from '@/stores/user'
window.requests = []
window.tokenRefreshing = false
const pendingMap = new Map()
const loadingInstance: LoadingInstance = {
target: null,
count: 0,
}
/**
* 创建`Axios`
* 默认开启`reductDataFormat(简洁响应)`,返回类型为`ApiPromise`
* 关闭`reductDataFormat`,返回类型则为`AxiosPromise`
*/
function createAxios<Data = any, T = ApiPromise<Data>>(
axiosConfig: AxiosRequestConfig,
options: Options = {},
loading: LoadingOptions = {},
): T {
const userInfo = useUserInfoStore()
const Axios = axios.create({
baseURL: import.meta.env.VITE_URL,
timeout: 1000 * 60 * 5,
headers: {},
responseType: 'json',
})
options = Object.assign(
{
CancelDuplicateRequest: true, // 是否开启取消重复请求, 默认为 true
loading: false, // 是否开启loading层效果, 默认为false
reductDataFormat: true, // 是否开启简洁的数据结构响应, 默认为true
showErrorMessage: true, // 是否开启接口错误信息展示,默认为true
showCodeMessage: true, // 是否开启code不为1时的信息提示, 默认为true
showSuccessMessage: false, // 是否开启code为1时的信息提示, 默认为false
anotherToken: '', // 当前请求使用另外的用户token
},
options,
)
// 请求拦截
Axios.interceptors.request.use(
config => {
removePending(config)
options.CancelDuplicateRequest && addPending(config)
// 创建loading实例
if (options.loading) {
loadingInstance.count++
if (loadingInstance.count === 1) {
loadingInstance.target = ElLoading.service(loading)
}
}
// 自动携带token
if (config.headers) {
const token = userInfo.getToken()
if (token) {
;(config.headers as any).Authorization = token
} else {
config.headers.Authorization = 'Basic bmpjbnRlc3Q6bmpjbnBxcw=='
}
}
return config
},
error => {
return Promise.reject(error)
},
)
// 响应拦截
Axios.interceptors.response.use(
response => {
removePending(response.config)
options.loading && closeLoading(options) // 关闭loading
if (
response.data.code === 'A0000' ||
response.data.type === 'application/json'
) {
return options.reductDataFormat ? response.data : response
} else if (response.data.code == 'A0202') {
if (!window.tokenRefreshing) {
window.tokenRefreshing = true
return refreshToken()
.then(res => {
userInfo.setToken(res.data.token, 'auth')
response.headers.Authorization = `${res.data.token}`
window.requests.forEach(cb => cb(res.data.token))
window.requests = []
return Axios(response.config)
})
.catch(err => {
userInfo.removeToken()
router.push({ name: 'login' })
return Promise.reject(err)
})
.finally(() => {
window.tokenRefreshing = false
})
} else {
return new Promise(resolve => {
// 用函数形式将 resolve 存入,等待刷新后再执行
window.requests.push((token: string) => {
response.headers.Authorization = `${token}`
resolve(Axios(response.config))
})
})
}
} else if (response.data.code == 'A0024') {
userInfo.removeToken()
router.push({ name: 'login' })
return Promise.reject(response.data)
} else {
if (options.showCodeMessage) {
ElNotification({
type: 'error',
message: response.data.message || '未知错误',
})
}
return Promise.reject(response.data)
}
},
error => {
error.config && removePending(error.config)
options.loading && closeLoading(options) // 关闭loading
return Promise.reject(error) // 错误继续返回给到具体页面
},
)
return Axios(axiosConfig) as T
}
export default createAxios
/**
* 关闭Loading层实例
*/
function closeLoading(options: Options) {
if (options.loading && loadingInstance.count > 0) loadingInstance.count--
if (loadingInstance.count === 0) {
loadingInstance.target.close()
loadingInstance.target = null
}
}
/**
* 储存每个请求的唯一cancel回调, 以此为标识
*/
function addPending(config: AxiosRequestConfig) {
const pendingKey = getPendingKey(config)
config.cancelToken =
config.cancelToken ||
new axios.CancelToken(cancel => {
if (!pendingMap.has(pendingKey)) {
pendingMap.set(pendingKey, cancel)
}
})
}
/**
* 删除重复的请求
*/
function removePending(config: AxiosRequestConfig) {
const pendingKey = getPendingKey(config)
if (pendingMap.has(pendingKey)) {
const cancelToken = pendingMap.get(pendingKey)
cancelToken(pendingKey)
pendingMap.delete(pendingKey)
}
}
/**
* 生成每个请求的唯一key
*/
function getPendingKey(config: AxiosRequestConfig) {
let { data } = config
const { url, method, params, headers } = config
if (typeof data === 'string') data = JSON.parse(data) // response里面返回的config.data是个字符串对象
return [
url,
method,
headers && (headers as anyObj).Authorization ? (headers as anyObj).Authorization : '',
headers && (headers as anyObj)['ba-user-token'] ? (headers as anyObj)['ba-user-token'] : '',
JSON.stringify(params),
JSON.stringify(data),
].join('&')
}
/**
* 根据请求方法组装请求数据/参数
*/
export function requestPayload(method: Method, data: anyObj) {
if (method == 'GET') {
return {
params: data,
}
} else if (method == 'POST') {
return {
data: data,
}
}
}
interface LoadingInstance {
target: any
count: number
}
interface Options {
// 是否开启取消重复请求, 默认为 true
CancelDuplicateRequest?: boolean
// 是否开启loading层效果, 默认为false
loading?: boolean
// 是否开启简洁的数据结构响应, 默认为true
reductDataFormat?: boolean
// 是否开启code不为A0000时的信息提示, 默认为true
showCodeMessage?: boolean
// 是否开启code为0时的信息提示, 默认为false
showSuccessMessage?: boolean
// 当前请求使用另外的用户token
anotherToken?: string
}

View File

@@ -1,9 +1,10 @@
const Renderer = (window.require && window.require('electron')) || window.electron || {}; const Renderer =
(window.require && window.require('electron')) || window.electron || {}
/** /**
* ipc * ipc
* 官方api说明https://www.electronjs.org/zh/docs/latest/api/ipc-renderer * 官方api说明https://www.electronjs.org/zh/docs/latest/api/ipc-renderer
* *
* 属性/方法 * 属性/方法
* ipc.invoke(channel, param) - 发送异步消息invoke/handle 模型) * ipc.invoke(channel, param) - 发送异步消息invoke/handle 模型)
* ipc.sendSync(channel, param) - 发送同步消息send/on 模型) * ipc.sendSync(channel, param) - 发送同步消息send/on 模型)
@@ -20,14 +21,11 @@ const Renderer = (window.require && window.require('electron')) || window.electr
/** /**
* ipc * ipc
*/ */
const ipc = Renderer.ipcRenderer || undefined; const ipc = Renderer.ipcRenderer || undefined
/** /**
* 是否为EE环境 * 是否为EE环境
*/ */
const isEE = ipc ? true : false; const isEE = ipc ? true : false
export {
Renderer, ipc, isEE
};
export { Renderer, ipc, isEE }

View File

@@ -1,15 +0,0 @@
<template>
<div></div>
</template>
<script setup lang='ts'>
defineOptions({
name: "componentName"
})
import {ref, reactive} from 'vue'
let select = ref(1)
defineExpose({ select })
</script>
<style scoped>
</style>

View File

@@ -1,32 +1,22 @@
<template> <template>
<div ref="loginContent" class="text-xl text-red-600"> <div ref="loginContent" class="text-xl text-red-600">
<!--用户名密码暂时不做图形验证码-->
<h1 ref="loginTitle">123</h1> <h1 ref="loginTitle">123</h1>
<Child ref="child"/> <svg-icon name="wind" color="#fff" size="100" spin > </svg-icon>
</div> </div>
</template> </template>
<script setup lang='ts'> <script setup lang='ts'>
defineOptions({ defineOptions(
name: "Login" {
}) name: "CustomLogin"
import {ref, reactive,onMounted} from 'vue' }
import Child from './Child.vue' )
import {ref, onMounted} from 'vue'
let loginContent = ref() let loginContent = ref()
let loginTitle = ref() let loginTitle = ref()
let child = ref()
function showLog(){
console.log(child.value)
}
showLog()
console.log(loginTitle.value);
console.log(child.value);
onMounted(() => { onMounted(() => {
console.log(loginTitle.value); console.log(loginTitle.value);
console.log(child.value);
}); });

View File

@@ -1,9 +1,8 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
content: ["index.html", "./src/**/*.{html,js,ts,jsx,tsx,vue}"], content: ['index.html', './src/**/*.{html,js,ts,jsx,tsx,vue}'],
theme: { theme: {
extend: {}, extend: {},
}, },
plugins: [], plugins: [],
} }

View File

@@ -1,13 +1,22 @@
{ {
"extends": "@vue/tsconfig/tsconfig.dom.json", "extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"], "include": [
"exclude": ["src/**/__tests__/*"], "env.d.ts",
"src/**/*",
"src/**/*.vue"
],
"exclude": [
"src/**/__tests__/*"
],
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"noEmit": false,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": [
"./src/*"
]
} }
} }
} }

View File

@@ -9,8 +9,36 @@
} }
], ],
"compilerOptions": { "compilerOptions": {
"target": "esnext",
"declaration": true,
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "Node",
"strict": true,
"jsx": "preserve",
"allowJs": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["esnext", "DOM"],
"skipLibCheck": true,
"noEmit": false,
"types": [ "types": [
"node" "node",
] "element-plus/global"
} ],
"baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
"paths": { //路径映射相对于baseUrl
"@/*": ["src/*"]
},
"allowSyntheticDefaultImports": true // 允许默认导入
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"vite.config.ts"
],
"exclude": ["node_modules", "dist", "**/*.js"]
} }

View File

@@ -9,9 +9,8 @@
], ],
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"noEmit": true, "noEmit": false,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Bundler", "moduleResolution": "Bundler",
"types": ["node"] "types": ["node"]

View File

@@ -1,44 +1,56 @@
import { defineConfig } from 'vite' import {defineConfig, loadEnv} from 'vite'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import path from 'path' import path from 'path'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig((config) => {
plugins: [ // 根据当前工作目录中的 `mode` 加载 .env 文件
vue(), // 设置第三个参数为 '' 来加载所有环境变量,而不管是否有 `VITE_` 前缀。
], const {command, mode} = config
// 基础配置 const env = loadEnv(mode, process.cwd(), '')
base: './', return {
publicDir: 'public', plugins: [
resolve: { vue(),
alias: { // svg图标配置可以使用svg图标
'@': path.resolve(__dirname, 'src') createSvgIconsPlugin({
} iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
}, symbolId: 'icon-[dir]-[name]',
css: { }),
preprocessorOptions: { ],
less: { // 基础配置
modifyVars: { base: './',
'@border-color-base': '#dce3e8', publicDir: 'public',
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
}, },
javascriptEnabled: true, css: {
}, preprocessorOptions: {
}, less: {
}, modifyVars: {
build: { '@border-color-base': '#dce3e8',
outDir: 'dist', },
assetsDir: 'assets', javascriptEnabled: true,
assetsInlineLimit: 4096, },
cssCodeSplit: true, },
brotliSize: false, },
sourcemap: false, build: {
minify: 'terser', outDir: 'dist',
terserOptions: { assetsDir: 'assets',
compress: { assetsInlineLimit: 4096,
// 生产环境去除console及debug cssCodeSplit: true,
drop_console: false, sourcemap: false,
drop_debugger: true, minify: 'terser',
}, terserOptions: {
}, compress: {
}, // 生产环境去除console及debug
drop_console: false,
drop_debugger: true,
},
},
},
}
}) })