Merge remote-tracking branch 'origin/ts' into ts

This commit is contained in:
ahao 2023-12-13 11:41:50 +08:00
commit c61afdfefc
21 changed files with 100 additions and 48 deletions

View File

@ -20,6 +20,8 @@ VITE_APP_PORT = 80
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换 # 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
VITE_APP_RSA_PUBLIC_KEY = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==' VITE_APP_RSA_PUBLIC_KEY = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
VITE_APP_RSA_PRIVATE_KEY = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE='
# 客户端id # 客户端id
VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e' VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e'

View File

@ -23,6 +23,8 @@ VITE_APP_PORT = 80
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换 # 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
VITE_APP_RSA_PUBLIC_KEY = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==' VITE_APP_RSA_PUBLIC_KEY = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
VITE_APP_RSA_PRIVATE_KEY = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE='
# 客户端id # 客户端id
VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e' VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e'

View File

@ -17,7 +17,9 @@ npm install --registry=https://registry.npmmirror.com
# 启动服务 # 启动服务
npm run dev npm run dev
# 构建生产环境 yarn build:prod # 构建生产环境
npm run build:prod
# 前端访问地址 http://localhost:80 # 前端访问地址 http://localhost:80
``` ```

View File

@ -2,20 +2,16 @@
<div> <div>
<template v-for="(item, index) in options"> <template v-for="(item, index) in options">
<template v-if="values.includes(item.value)"> <template v-if="values.includes(item.value)">
<span <span v-if="(item.elTagType === 'default' || item.elTagType === '') && (item.elTagClass === '' || item.elTagClass == null)"
v-if="(item.elTagType == 'default' || item.elTagType == '') && (item.elTagClass == '' || item.elTagClass == null)" :key="item.value" :index="index" :class="item.elTagClass">
:key="item.value" {{ item.label + " " }}
:index="index"
:class="item.elTagClass"
>
{{ item.label + ' ' }}
</span> </span>
<el-tag <el-tag
v-else v-else
:key="item.value + ''" :key="item.value + ''"
:disable-transitions="true" :disable-transitions="true"
:index="index" :index="index"
:type="item.elTagType === 'primary' ? '' : item.elTagType" :type="(item.elTagType === 'primary' || item.elTagType === 'default')? '' : item.elTagType"
:class="item.elTagClass" :class="item.elTagClass"
> >
{{ item.label + ' ' }} {{ item.label + ' ' }}

View File

@ -2,7 +2,7 @@
<div class="relative" :style="{ width: width }"> <div class="relative" :style="{ width: width }">
<el-input v-model="modelValue" readonly placeholder="点击选择图标" @click="visible = !visible"> <el-input v-model="modelValue" readonly placeholder="点击选择图标" @click="visible = !visible">
<template #prepend> <template #prepend>
<svg-icon :icon-class="modelValue as string" /> <svg-icon :icon-class="modelValue" />
</template> </template>
</el-input> </el-input>

View File

@ -18,8 +18,8 @@
</template> </template>
<sidebar-item <sidebar-item
v-for="child in item.children" v-for="(child, index) in item.children"
:key="child.path" :key="child.path + index"
:is-nest="true" :is-nest="true"
:item="child" :item="child"
:base-path="resolvePath(child.path)" :base-path="resolvePath(child.path)"

View File

@ -21,6 +21,8 @@ router.beforeEach(async (to, from, next) => {
if (to.path === '/login') { if (to.path === '/login') {
next({ path: '/' }); next({ path: '/' });
NProgress.done(); NProgress.done();
} else if (whiteList.indexOf(to.path) !== -1) {
next()
} else { } else {
if (useUserStore().roles.length === 0) { if (useUserStore().roles.length === 0) {
isRelogin.show = true; isRelogin.show = true;

View File

@ -139,7 +139,7 @@ export const dynamicRoutes: RouteRecordRaw[] = [
path: '/system/oss-config', path: '/system/oss-config',
component: Layout, component: Layout,
hidden: true, hidden: true,
permissions: ['system:oss:list'], permissions: ['system:ossConfig:list'],
children: [ children: [
{ {
path: 'index', path: 'index',

View File

@ -1,4 +1,4 @@
import { TagView, RouteLocationNormalized } from 'vue-router'; import { TagView, RouteRecordNormalized } from 'vue-router';
export const useTagsViewStore = defineStore('tagsView', () => { export const useTagsViewStore = defineStore('tagsView', () => {
const visitedViews = ref<TagView[]>([]); const visitedViews = ref<TagView[]>([]);
@ -35,7 +35,9 @@ export const useTagsViewStore = defineStore('tagsView', () => {
const delView = (view: TagView): Promise<{ visitedViews: TagView[]; cachedViews: string[] }> => { const delView = (view: TagView): Promise<{ visitedViews: TagView[]; cachedViews: string[] }> => {
return new Promise((resolve) => { return new Promise((resolve) => {
delVisitedView(view); delVisitedView(view);
if (!isDynamicRoute(view)) {
delCachedView(view); delCachedView(view);
}
resolve({ resolve({
visitedViews: [...visitedViews.value], visitedViews: [...visitedViews.value],
cachedViews: [...cachedViews.value] cachedViews: [...cachedViews.value]
@ -177,6 +179,11 @@ export const useTagsViewStore = defineStore('tagsView', () => {
} }
}; };
const isDynamicRoute = (view: any): boolean => {
// 检查匹配的路由记录中是否有动态段
return view.matched.some((m: RouteRecordNormalized) => m.path.includes(':'));
};
return { return {
visitedViews, visitedViews,
cachedViews, cachedViews,

View File

@ -63,6 +63,10 @@ export const useUserStore = defineStore('user', () => {
removeToken(); removeToken();
}; };
const setAvatar = (value: string) => {
avatar.value = value;
};
return { return {
userId, userId,
token, token,
@ -72,7 +76,8 @@ export const useUserStore = defineStore('user', () => {
permissions, permissions,
login, login,
getInfo, getInfo,
logout logout,
setAvatar
}; };
}); });

1
src/types/env.d.ts vendored
View File

@ -69,6 +69,7 @@ interface ImportMetaEnv {
VITE_APP_POWERJOB_ADMIN: string; VITE_APP_POWERJOB_ADMIN: string;
VITE_APP_ENV: string; VITE_APP_ENV: string;
VITE_APP_RSA_PUBLIC_KEY: string; VITE_APP_RSA_PUBLIC_KEY: string;
VITE_APP_RSA_PRIVATE_KEY: string;
VITE_APP_CLIENT_ID: string; VITE_APP_CLIENT_ID: string;
VITE_APP_WEBSOCKET: string; VITE_APP_WEBSOCKET: string;
} }

View File

@ -30,6 +30,13 @@ export const encryptBase64 = (str: CryptoJS.lib.WordArray) => {
return CryptoJS.enc.Base64.stringify(str); return CryptoJS.enc.Base64.stringify(str);
}; };
/**
* base64
*/
export const decryptBase64 = (str: string) => {
return CryptoJS.enc.Base64.parse(str);
};
/** /**
* 使 * 使
* @param message * @param message
@ -43,3 +50,17 @@ export const encryptWithAes = (message: string, aesKey: CryptoJS.lib.WordArray)
}); });
return encrypted.toString(); return encrypted.toString();
}; };
/**
* 使
* @param message
* @param aesKey
* @returns {string}
*/
export const decryptWithAes = (message: string, aesKey: CryptoJS.lib.WordArray) => {
const decrypted = CryptoJS.AES.decrypt(message, aesKey, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
return decrypted.toString(CryptoJS.enc.Utf8);
};

View File

@ -4,7 +4,7 @@ import JSEncrypt from 'jsencrypt';
const publicKey = import.meta.env.VITE_APP_RSA_PUBLIC_KEY; const publicKey = import.meta.env.VITE_APP_RSA_PUBLIC_KEY;
// 前端不建议存放私钥 不建议解密数据 因为都是透明的意义不大 // 前端不建议存放私钥 不建议解密数据 因为都是透明的意义不大
const privateKey = '**********'; const privateKey = import.meta.env.VITE_APP_RSA_PRIVATE_KEY;
// 加密 // 加密
export const encrypt = (txt: string) => { export const encrypt = (txt: string) => {

View File

@ -8,9 +8,10 @@ import { errorCode } from '@/utils/errorCode';
import { LoadingInstance } from 'element-plus/es/components/loading/src/loading'; import { LoadingInstance } from 'element-plus/es/components/loading/src/loading';
import FileSaver from 'file-saver'; import FileSaver from 'file-saver';
import { getLanguage } from '@/lang'; import { getLanguage } from '@/lang';
import { encryptBase64, encryptWithAes, generateAesKey } from '@/utils/crypto'; import { encryptBase64, encryptWithAes, generateAesKey, decryptWithAes, decryptBase64 } from '@/utils/crypto';
import { encrypt } from '@/utils/jsencrypt'; import { encrypt, decrypt } from '@/utils/jsencrypt';
const encryptHeader = 'encrypt-key';
let downloadLoadingInstance: LoadingInstance; let downloadLoadingInstance: LoadingInstance;
// 是否显示重新登录 // 是否显示重新登录
export const isRelogin = { show: false }; export const isRelogin = { show: false };
@ -78,7 +79,7 @@ service.interceptors.request.use(
if (isEncrypt && (config.method === 'post' || config.method === 'put')) { if (isEncrypt && (config.method === 'post' || config.method === 'put')) {
// 生成一个 AES 密钥 // 生成一个 AES 密钥
const aesKey = generateAesKey(); const aesKey = generateAesKey();
config.headers['encrypt-key'] = encrypt(encryptBase64(aesKey)); config.headers[encryptHeader] = encrypt(encryptBase64(aesKey));
config.data = typeof config.data === 'object' ? encryptWithAes(JSON.stringify(config.data), aesKey) : encryptWithAes(config.data, aesKey); config.data = typeof config.data === 'object' ? encryptWithAes(JSON.stringify(config.data), aesKey) : encryptWithAes(config.data, aesKey);
} }
// FormData数据去请求头Content-Type // FormData数据去请求头Content-Type
@ -95,6 +96,20 @@ service.interceptors.request.use(
// 响应拦截器 // 响应拦截器
service.interceptors.response.use( service.interceptors.response.use(
(res: AxiosResponse) => { (res: AxiosResponse) => {
// 加密后的 AES 秘钥
const keyStr = res.headers[encryptHeader];
// 加密
if (keyStr != null && keyStr != '') {
const data = res.data;
// 请求体 AES 解密
const base64Str = decrypt(keyStr);
// base64 解码 得到请求头的 AES 秘钥
const aesKey = decryptBase64(base64Str.toString());
// aesKey 解码 data
const decryptData = decryptWithAes(data, aesKey);
// 将结果 (得到的是 JSON 字符串) 转为 JSON
res.data = JSON.parse(decryptData);
}
// 未设置状态码则默认成功状态 // 未设置状态码则默认成功状态
const code = res.data.code || HttpStatus.SUCCESS; const code = res.data.code || HttpStatus.SUCCESS;
// 获取错误信息 // 获取错误信息

View File

@ -61,8 +61,10 @@
</template> </template>
<script setup name="Online" lang="ts"> <script setup name="Online" lang="ts">
import { forceLogout, list as initData } from '@/api/monitor/online'; import { forceLogout, list as initData } from "@/api/monitor/online";
import { OnlineQuery, OnlineVO } from '@/api/monitor/online/types'; import { OnlineQuery, OnlineVO } from "@/api/monitor/online/types";
import api from "@/api/system/user";
import {to} from "await-to-js";
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_device_type } = toRefs<any>(proxy?.useDict('sys_device_type')); const { sys_device_type } = toRefs<any>(proxy?.useDict('sys_device_type'));
@ -100,11 +102,13 @@ const resetQuery = () => {
}; };
/** 强退按钮操作 */ /** 强退按钮操作 */
const handleForceLogout = async (row: OnlineVO) => { const handleForceLogout = async (row: OnlineVO) => {
await proxy?.$modal.confirm('是否确认强退名称为"' + row.userName + '"的用户?'); const [err] = await to(proxy?.$modal.confirm('是否确认强退名称为"' + row.userName + '"的用户?') as any);
if (!err) {
await forceLogout(row.tokenId); await forceLogout(row.tokenId);
await getList(); await getList();
proxy?.$modal.msgSuccess('删除成功'); proxy?.$modal.msgSuccess("删除成功");
}; }
}
onMounted(() => { onMounted(() => {
getList(); getList();

View File

@ -49,13 +49,8 @@
<el-table-column v-if="false" label="字典编码" align="center" prop="dictCode" /> <el-table-column v-if="false" label="字典编码" align="center" prop="dictCode" />
<el-table-column label="字典标签" align="center" prop="dictLabel"> <el-table-column label="字典标签" align="center" prop="dictLabel">
<template #default="scope"> <template #default="scope">
<span <span v-if="(scope.row.listClass === '' || scope.row.listClass === 'default') && (scope.row.cssClass === '' || scope.row.cssClass == null)">{{ scope.row.dictLabel }}</span>
v-if="(scope.row.listClass == '' || scope.row.listClass == 'default') && (scope.row.cssClass == '' || scope.row.cssClass == null)" <el-tag v-else :type="(scope.row.listClass === 'primary' || scope.row.listClass === 'default') ? '' : scope.row.listClass" :class="scope.row.cssClass">{{ scope.row.dictLabel }}</el-tag>
>{{ scope.row.dictLabel }}</span
>
<el-tag v-else :type="scope.row.listClass == 'primary' ? '' : scope.row.listClass" :class="scope.row.cssClass">{{
scope.row.dictLabel
}}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="字典键值" align="center" prop="dictValue" /> <el-table-column label="字典键值" align="center" prop="dictValue" />

View File

@ -169,7 +169,7 @@
<el-input v-model="form.perms" placeholder="请输入权限标识" maxlength="100" /> <el-input v-model="form.perms" placeholder="请输入权限标识" maxlength="100" />
<template #label> <template #label>
<span> <span>
<el-tooltip content="控制器中定义的权限字符,如:@PreAuthorize(`@ss.hasPermi('system:user:list')`)" placement="top"> <el-tooltip content="控制器中定义的权限字符,如:@SaCheckPermission('system:user:list')" placement="top">
<el-icon> <el-icon>
<question-filled /> <question-filled />
</el-icon> </el-icon>

View File

@ -29,13 +29,13 @@
<template #header> <template #header>
<el-row :gutter="10" class="mb8"> <el-row :gutter="10" class="mb8">
<el-col :span="1.5"> <el-col :span="1.5">
<el-button v-hasPermi="['system:oss:add']" type="primary" plain icon="Plus" @click="handleAdd">新增</el-button> <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:ossConfig:add']">新增</el-button>
</el-col> </el-col>
<el-col :span="1.5"> <el-col :span="1.5">
<el-button v-hasPermi="['system:oss:edit']" type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()">修改</el-button> <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:ossConfig:edit']">修改</el-button>
</el-col> </el-col>
<el-col :span="1.5"> <el-col :span="1.5">
<el-button v-hasPermi="['system:oss:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()"> <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:ossConfig:remove']">
删除 删除
</el-button> </el-button>
</el-col> </el-col>
@ -67,10 +67,10 @@
<el-table-column label="操作" fixed="right" align="center" width="150" class-name="small-padding"> <el-table-column label="操作" fixed="right" align="center" width="150" class-name="small-padding">
<template #default="scope"> <template #default="scope">
<el-tooltip content="修改" placement="top"> <el-tooltip content="修改" placement="top">
<el-button v-hasPermi="['system:oss:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button> <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:ossConfig:edit']"></el-button>
</el-tooltip> </el-tooltip>
<el-tooltip content="删除" placement="top"> <el-tooltip content="删除" placement="top">
<el-button v-hasPermi="['system:oss:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button> <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:ossConfig:remove']"></el-button>
</el-tooltip> </el-tooltip>
</template> </template>
</el-table-column> </el-table-column>

View File

@ -107,7 +107,7 @@
<el-form-item prop="roleKey"> <el-form-item prop="roleKey">
<template #label> <template #label>
<span> <span>
<el-tooltip content="控制器中定义的权限字符,如:@PreAuthorize(`@ss.hasRole('admin')`)" placement="top"> <el-tooltip content="控制器中定义的权限字符,如:@SaCheckRole('admin')" placement="top">
<el-icon><question-filled /></el-icon> <el-icon><question-filled /></el-icon>
</el-tooltip> </el-tooltip>
权限字符 权限字符

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="user-info-head" @click="editCropper()"> <div class="user-info-head" @click="editCropper()">
<img :src="options.img as string" title="点击上传头像" class="img-circle img-lg" /> <img :src="options.img" title="点击上传头像" class="img-circle img-lg" />
<el-dialog v-model="open" :title="title" width="800px" append-to-body @opened="modalOpened" @close="closeDialog"> <el-dialog :title="title" v-model="open" width="800px" append-to-body @opened="modalOpened" @close="closeDialog">
<el-row> <el-row>
<el-col :xs="24" :md="12" :style="{ height: '350px' }"> <el-col :xs="24" :md="12" :style="{ height: '350px' }">
<vue-cropper <vue-cropper
@ -62,7 +62,7 @@ import { uploadAvatar } from '@/api/system/user';
import useUserStore from '@/store/modules/user'; import useUserStore from '@/store/modules/user';
interface Options { interface Options {
img: string | ArrayBuffer | null; // img: string | any; //
autoCrop: boolean; // autoCrop: boolean; //
autoCropWidth: number; // autoCropWidth: number; //
autoCropHeight: number; // autoCropHeight: number; //
@ -138,8 +138,8 @@ const uploadImg = async () => {
const res = await uploadAvatar(formData); const res = await uploadAvatar(formData);
open.value = false; open.value = false;
options.img = res.data.imgUrl; options.img = res.data.imgUrl;
userStore.avatar = options.img as string; userStore.setAvatar(options.img as string)
proxy?.$modal.msgSuccess('修改成功'); proxy?.$modal.msgSuccess("修改成功");
visible.value = false; visible.value = false;
}); });
}; };

View File

@ -97,9 +97,9 @@
<el-tabs v-model="preview.activeName"> <el-tabs v-model="preview.activeName">
<el-tab-pane <el-tab-pane
v-for="(value, key) in preview.data" v-for="(value, key) in preview.data"
:label="key.substring(key.lastIndexOf('/') + 1, key.indexOf('.vm'))"
:name="key.substring(key.lastIndexOf('/') + 1, key.indexOf('.vm'))"
:key="value" :key="value"
:label="(key as any).substring((key as any).lastIndexOf('/') + 1, (key as any).indexOf('.vm'))"
:name="(key as any).substring((key as any).lastIndexOf('/') + 1, (key as any).indexOf('.vm'))"
> >
<el-link v-copyText="value" v-copyText:callback="copyTextSuccess" :underline="false" icon="DocumentCopy" style="float: right"> <el-link v-copyText="value" v-copyText:callback="copyTextSuccess" :underline="false" icon="DocumentCopy" style="float: right">
&nbsp;复制 &nbsp;复制