feat(web): 实现能源站监控与运行管理系统的登录功能

- 新增登录页面组件 LoginView.vue
- 添加全局样式和布局调整
- 实现用户登录逻辑,包括表单验证、验证码校验和 token 存储
- 集成 ant-design-vue 组件库
- 添加请求拦截器和错误处理
This commit is contained in:
zhoumengru
2025-08-29 12:28:33 +08:00
parent 5e559f8d36
commit 390ea73d7d
11 changed files with 629 additions and 168 deletions

View File

@@ -1,8 +1,4 @@
<template>
<nav>
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</nav>
<router-view />
</template>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@@ -1,130 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br />
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener"
>vue-cli documentation</a
>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel"
target="_blank"
rel="noopener"
>babel</a
>
</li>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router"
target="_blank"
rel="noopener"
>router</a
>
</li>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex"
target="_blank"
rel="noopener"
>vuex</a
>
</li>
<li>
<a
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint"
target="_blank"
rel="noopener"
>eslint</a
>
</li>
</ul>
<h3>Essential Links</h3>
<ul>
<li>
<a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a>
</li>
<li>
<a href="https://forum.vuejs.org" target="_blank" rel="noopener"
>Forum</a
>
</li>
<li>
<a href="https://chat.vuejs.org" target="_blank" rel="noopener"
>Community Chat</a
>
</li>
<li>
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener"
>Twitter</a
>
</li>
<li>
<a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a>
</li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li>
<a href="https://router.vuejs.org" target="_blank" rel="noopener"
>vue-router</a
>
</li>
<li>
<a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a>
</li>
<li>
<a
href="https://github.com/vuejs/vue-devtools#vue-devtools"
target="_blank"
rel="noopener"
>vue-devtools</a
>
</li>
<li>
<a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener"
>vue-loader</a
>
</li>
<li>
<a
href="https://github.com/vuejs/awesome-vue"
target="_blank"
rel="noopener"
>awesome-vue</a
>
</li>
</ul>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
msg: String,
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

16
web/src/request/api.js Normal file
View File

@@ -0,0 +1,16 @@
import request from "@/request/index.js";
export function postReq(data, url) {
return request({
method: "post",
url,
data,
});
}
export function getReq(data, url) {
// const query = qs.stringify(data, { indices: false })
return request({
method: "get",
url: url + "?" + data,
});
}

61
web/src/request/index.js Normal file
View File

@@ -0,0 +1,61 @@
import axios from "axios";
// import openNotification from "../utils/notification";
// let { config } = window;
// let { baseUrl } = config;
const service = axios.create({
// baseURL: baseUrl,
baseURL: "",
timeout: 120000,
});
service.interceptors.request.use((config) => {
const webConfig = config;
// if (!["/user/login", "/config/getConfig"].includes(config.url)) {
// if (localStorage.getItem("token")) {
// webConfig.headers = {
// Authorization: localStorage.getItem("token"),
// };
// }
// }
return webConfig;
});
service.interceptors.response.use(
(response) => {
// 排除以下接口的错误提示
const { url } = response.config;
const urls = ["/light/", "/serve/delete", "/user/checkRandom"];
const urlFlag = urls.map((item) => {
return url.includes(item);
});
const res = response.data;
if (res.code !== 200) {
if (res.code == 401 || res.tip == "校验token过期") {
setTimeout(() => {
window.$wujie?.props.jump({ path: "/login" });
}, 1000);
} else if (urlFlag.every((item) => item === false)) {
// openNotification({
// status: "error",
// desc: res.tip,
// });
}
}
return res;
},
(error) => {
// console.log(error, 'error 此处添加监控超时处理')
if (
error.name === "AxiosError" &&
error.message === "timeout of 120000ms exceeded" &&
error.code === "ECONNABORTED"
) {
return error;
}
}
);
export default service;

View File

@@ -21,7 +21,7 @@ const routes = [
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
history: createWebHistory(""),
routes,
});

View File

@@ -1,5 +1,441 @@
<template>
<div class="about">
<h1>login</h1>
</div>
<a-config-provider
:theme="{
token: {
colorPrimary: '#065758',
},
}"
>
<div class="login">
<div class="main-title">能源站监控与运行管理系统</div>
<div class="login-content">
<div class="title" style="">账号登录</div>
<a-form ref="ruleForm" :model="form" :rules="rules">
<a-form-item label="" name="user">
<a-input
:bordered="false"
v-model:value="form.user"
placeholder="请输入账号"
autocomplete
>
<template #prefix>
<span class="iconfont icon-a-xingzhuangjiehe1x"></span>
</template>
</a-input>
</a-form-item>
<a-form-item label="" name="passwd">
<a-input-password
:bordered="false"
v-model:value="form.passwd"
placeholder="请输入密码"
autocomplete
@pressEnter="login"
style="background-color: #fff; margin-top: 6px"
>
<template #prefix>
<span class="iconfont icon-a-xingzhuangjiehe1x1"></span>
</template>
</a-input-password>
</a-form-item>
</a-form>
<a-button class="login-btn" @click="login" :loading="loading"
>登录
</a-button>
</div>
</div>
</a-config-provider>
</template>
<script>
import { getReq, postReq } from "@/request/api.js";
// import { sm2Encrypt, sm2Decrypt } from "@c/utils/sm2Utils.js";
// import { getSysConfig } from "@/utils/index.js";
// import { copyRight } from "@c/utils/config.js";
import // EyeInvisibleOutlined,
// EyeTwoTone,
// LockOutlined,
// UserOutlined,
"@ant-design/icons-vue";
// import moment from "moment";
// import { themeColor } from "@c/utils/config.js";
export default {
name: "loginView",
// components: { EyeTwoTone, EyeInvisibleOutlined },
props: {},
data() {
return {
copyRight: "",
loading: false,
companyName: "",
publickey: "",
form: {},
rules: {
user: [{ required: true, message: "请输入用户名", trigger: "blur" }],
passwd: [
{ required: true, message: "请输入登录密码", trigger: "blur" },
],
code: [{ required: true, message: "请输入验证码", trigger: "blur" }],
},
// rules: {
// user: [{ validator: this.checkUser, trigger: 'blur' }],
// passwd: [{ validator: this.checkPasswd, trigger: 'blur' }],
// code: [
// { required: true, message: '请输入验证码', trigger: 'blur' },
// // { validator: this.asynCheckRandom, },
// ]
// },
codeId: Math.random(),
codeString: "", // 验证码字符串
};
},
async mounted() {
// await this.getRandom();
// this.companyName = await getSysConfig("app-name");
// this.copyRight = await getSysConfig("app-version");
// this.publickey = await getSysConfig("secret-pub-key");
document.title = this.companyName;
},
methods: {
// 验证码校验
async asynCheckRandom(code) {
const res = await getReq({ id: this.codeId, code }, "/user/checkRandom");
if (res.code == 200 && res.data) {
return Promise.resolve({ success: true, msg: "" });
} else {
return Promise.resolve({ success: false, msg: "验证码输入错误" });
}
},
// })
// 登录后首次判断主题色
// async changeTheme() {
// const res = await getSysConfig("app-map-center");
// const theme = localStorage.getItem("theme");
// if (!theme) {
// const { sunriseSunset } = JSON.parse(res).weather;
// const sunrise = sunriseSunset.sunrise.trim().split(/\s+/)[1] || "04:00";
// const sunset = sunriseSunset.sunset.trim().split(/\s+/)[1] || "20:00";
// const current = moment().locale("zh-cn").format("HH:mm");
// if (sunrise < current && current < sunset) {
// localStorage.setItem("theme", "light");
// Object.keys(themeColor).forEach((key) => {
// document.documentElement.style.setProperty(
// key,
// themeColor[key].light
// );
// });
// } else {
// localStorage.setItem("theme", "dark");
// Object.keys(themeColor).forEach((key) => {
// document.documentElement.style.setProperty(
// key,
// themeColor[key].dark
// );
// });
// }
// } else {
// Object.keys(themeColor).forEach((key) => {
// document.documentElement.style.setProperty(
// key,
// themeColor[key][theme]
// );
// });
// }
// },
// 填充密码
async fillPassword() {
let userName = localStorage.getItem("userName");
if (userName) {
// this.form.user = localStorage.getItem('userName')
// const key = await getSysConfig('secret-pub-key')
// this.form.passwd = sm2Decrypt(localStorage.getItem('passwd'), key)
this.form.remember = true;
}
},
// 获取验证码
async getRandom() {
const res = await getReq({ id: this.codeId }, "/user/getRandom");
if (res.code == 200) {
this.codeString = res.data;
} else {
this.codeString = "获取失败";
}
},
async login() {
this.$refs.ruleForm
.validate()
.then((res) => {
if (res) {
this.asynCheckRandom(res.code).then((res) => {
if (!res.success) {
this.$openNotification({
name: "",
type: "",
status: "error",
desc: res.msg,
});
this.form.code = "";
} else {
this.submitLoginForm();
}
});
}
})
.catch((err) => {
console.log(err);
});
},
async submitLoginForm() {
// let newPsdSM2 = sm2Encrypt(this.form.passwd, this.publickey);
// if (newPsdSM2) {
let paramsDate = {
user: this.form.user,
// passwd: newPsdSM2,
};
if (this.form.remember) {
localStorage.setItem("userName", this.form.user);
// localStorage.setItem("passwd", newPsdSM2);
}
try {
const res = await postReq(paramsDate, "/user/login");
// 记住密码
if (res.code == 200) {
const { token, user } = res.data;
this.$openNotification({
name: "",
type: "",
status: "success",
desc: "登录成功",
});
localStorage.setItem("token", token);
localStorage.setItem("showNotice", true);
// this.changeTheme();
this.getUserRoute(user);
} else {
this.loading = false;
throw new Error(res);
}
} catch (error) {
this.loading = false;
this.form.code = "";
this.getRandom();
}
// }
},
async getUserRoute(user) {
const { userExtend } = user;
const { role } = userExtend;
localStorage.setItem("user", JSON.stringify(user));
if (role && role.permissionList.length > 0) {
const allMenus = [];
role.permissionList.forEach((item) => {
allMenus.push(...item.menusList);
});
const uniqueArray = [
...new Map(allMenus.map((item) => [item.id, item])).values(),
];
const treeMunes = this.buildTree(uniqueArray);
const pcMunes = treeMunes.filter((item) => item.type == 0);
localStorage.setItem("menuList", JSON.stringify(pcMunes));
}
setTimeout(() => {
this.loading = false;
this.$router.push("/");
}, 1000);
this.loading = false;
},
buildTree(items, parentId = null) {
const result = [];
const itemMap = {}; // 用于存储id和对应节点的映射方便快速查找父节点
items.forEach((item) => {
if (!itemMap[item.id]) {
itemMap[item.id] = { ...item, children: [] };
}
const node = itemMap[item.id];
if (item.parentId === parentId || item.parentId == 0) {
result.push(node);
} else {
if (!itemMap[item.parentId]) {
itemMap[item.parentId] = { children: [] };
}
itemMap[item.parentId].children.push(node);
}
});
result.sort((a, b) => a.seq - b.seq);
result.forEach((item) => {
if (item.children.length == 0) {
delete item.children;
}
});
return result;
},
onChange() {},
forgetPassword() {
this.$openNotification({
name: "",
type: "",
status: "warning",
desc: "请联系平台管理员",
});
},
},
};
</script>
<style lang="scss" scoped>
.login {
position: relative;
display: flex;
justify-content: center;
align-items: center;
min-width: 1440px;
min-height: 900px;
width: 100%;
height: 100%;
background-image: url("@/assets/images/loginBg.png");
background-size: cover;
background-repeat: no-repeat;
background-position: center;
.main-title {
background: linear-gradient(
180deg,
rgba(255, 255, 255, 1) 0%,
rgba(40, 235, 231, 1) 100%
);
color: transparent;
-webkit-background-clip: text;
background-clip: text;
font-size: 50px;
font-weight: 400;
letter-spacing: 5px;
line-height: 53px;
}
.login-content {
width: 390px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 40px;
.title {
margin-bottom: 10px;
color: #fff;
font-size: 20px;
}
:deep(.ant-input-prefix) {
color: #217575;
}
:deep(.ant-form-item) {
margin-bottom: 20px !important;
}
:deep(.ant-form-item-explain-error) {
font-size: 12px;
}
.iconfont {
margin: 0 10px 0 5px;
}
}
.yanzhengma {
width: 60px;
font-family: yanzhengma;
font-size: 23px;
cursor: pointer;
}
.rememberPass {
font-size: 14px;
color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: space-around;
align-items: center;
width: 290px;
.ant-checkbox-wrapper,
span {
color: rgba(0, 0, 0, 0.5);
cursor: pointer;
}
.ant-checkbox-wrapper:hover {
color: #065758;
}
span:hover {
color: #065758;
}
}
.login-btn {
width: 335px;
height: 45px;
border-radius: 4px;
background: #2a82e4;
font-size: 16px;
margin-top: 20px;
.btn-text {
}
}
.copyright {
position: absolute;
bottom: 5px;
left: 50%;
transform: translateX(-50%);
color: #fff;
font-weight: 700;
font-size: 20px;
}
}
:deep(.ant-input) {
&::placeholder {
// font-size: 12px !important;
// line-height: 12px;
// line-height: 100px !important;
// height: 40px !important;
// line-height: 40px !important;
}
}
:deep(span.ant-input-affix-wrapper) {
height: 40px !important;
background: #eceff4 !important;
color: #065758 !important;
border: none !important;
border-radius: 8px !important;
.ant-input {
// height: 40px !important;
border-radius: 0 !important;
background: #eceff4 !important;
}
&:hover {
border-color: #0caf60 !important;
}
&:focus {
border: none !important;
background-color: #eceff4;
}
}
:deep(.ant-input-affix-wrapper) {
padding: 0 10px !important;
}
// 输入框自动填充后的背景改色
input:-internal-autofill-previewed,
input:-internal-autofill-selected {
-webkit-text-fill-color: #000000 !important;
transition: background-color 500s ease-out 0.5s;
}
</style>

View File

@@ -7,12 +7,9 @@
<script>
// @ is an alias to /src
import HelloWorld from "@/components/HelloWorld.vue";
export default {
name: "HomeView",
components: {
HelloWorld,
},
components: {},
};
</script>