短报文教学

This commit is contained in:
2026-03-27 17:44:32 +08:00
commit 2d2b099313
27 changed files with 9039 additions and 0 deletions

30
.gitignore vendored Normal file
View File

@@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

29
README.md Normal file
View File

@@ -0,0 +1,29 @@
# gis-server
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en" class="w-h-full">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>北斗启航科技教育平台</title>
</head>
<body class="w-h-full">
<div id="app" class="w-h-full text-white"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

8
jsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

7509
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

36
package.json Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "teaching-api",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --port=7019",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.6.8",
"core-js": "^3.37.0",
"crypto-js": "^4.2.0",
"element-plus": "^2.7.2",
"fingerprintjs2": "^2.1.4",
"moment": "^2.30.1",
"pinia": "^2.1.7",
"pinia-plugin-persist": "^1.0.0",
"teaching-api": "file:",
"vue": "^3.4.21",
"vue-i18n": "^9.13.1",
"vue-router": "^4.3.0",
"zipson": "^0.2.12"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
"unplugin-auto-import": "^0.17.5",
"unplugin-vue-components": "^0.27.0",
"vite": "^5.2.8"
}
}

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

11
src/App.vue Normal file
View File

@@ -0,0 +1,11 @@
<script setup>
import { RouterView } from 'vue-router'
</script>
<template>
<RouterView />
</template>
<style scoped>
</style>

113
src/api/index.js Normal file
View File

@@ -0,0 +1,113 @@
import axios from "axios";
import $url from "./url";
import router from "../router";
import { encrypt } from "../utils/aes";
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
if (config.headers) {
config.headers.uid = localStorage.data;
if (localStorage.access_token && localStorage.access_token !== "" && localStorage.access_token !== null && localStorage.access_token !== undefined && localStorage.access_token !== NaN) {
config.headers.authorization = localStorage.access_token;
}
}
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 转到登录页面
const toLogin = () => {
localStorage.clear();
const data = { origin_url: window.location.href };
axios.post($url.toLogin, data).then((res) => {
if (res.data.code === 200) {
window.location.href = res.data.redirect;
} else {
router.push({ path: '/404page' });
}
});
};
// 添加响应拦截器
let result = true;
axios.interceptors.response.use(function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
if (response.data.code === 200) {
if ("access_token" in response.data) {
localStorage.setItem("access_token", response.data.access_token);
}
if ("refresh_token" in response.data) {
localStorage.setItem("refresh_token", response.data.refresh_token);
}
if ("data" in response.data) {
localStorage.setItem("data", response.data.data);
}
}
if(response.data.code === 401 && result) {
result = false;
setTimeout(() => {
result = true;
}, 1000);
let getCode = window.location.href.indexOf('?');
if (getCode != '-1') {
let code = window.location.href.substring(getCode + 1);
const data = {
code: code,
type: encrypt('code')
};
axios.post($url.getToken, data).then((res) => {
if (res.data.code === 200) {
localStorage.setItem("data", res.data.data);
localStorage.setItem("access_token", res.data.access_token);
localStorage.setItem("refresh_token", res.data.refresh_token);
window.location.href = res.data.redirect;
} else {
toLogin();
}
});
} else if (localStorage.refresh_token && localStorage.refresh_token !== "" && localStorage.refresh_token !== null && localStorage.refresh_token !== undefined && localStorage.refresh_token !== NaN) {
const data = {
code: localStorage.refresh_token,
type: localStorage.data
};
axios.post($url.getToken, data).then((res) => {
if (res.data.code === 200) {
localStorage.setItem("data", res.data.data);
localStorage.setItem("access_token", res.data.access_token);
localStorage.setItem("refresh_token", res.data.refresh_token);
window.location.reload();
} else {
toLogin();
}
});
} else {
toLogin();
}
}
return response;
}, function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error);
});
export default {
get(url, query = {}) {
return axios.get(url, {
params: query
});
},
post(url, body) {
return axios.post(url, body);
},
put(url, body) {
return axios.put(url, body);
},
del(url) {
return axios.delete(url);
},
}

27
src/api/url.js Normal file
View File

@@ -0,0 +1,27 @@
export default {
// 用于确认哪个网页在进行登录
toLogin: "/authentication/toLogin",
// 退出登录
unLogin: "/authentication/unLogin",
// 用于获取token
getToken: "/authentication/getToken",
// 用于验证token
verify: "/authentication/verify",
// 获取用户基本信息
getUser: "/authentication/getUser",
// 修改用户基本信息
editUser: "/authentication/editUser",
// 修改密码
changePassword: "/authentication/changePassword",
// token状态查询接口
tokenStatus: "/teachingApi/tokenStatus",
// 用户获取短期token的接口
getUserToken: "/teachingApi/getUserToken",
// 用户延续短期token有效时间的接口
lifeHold: "/teachingApi/lifeHold",
// 教学接口
// 获取最近发送的一条信息
getMsg: "/teachingApi/getMsg",
// 获取若干条历史消息
getHistory: "/teachingApi/getHistory",
}

BIN
src/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

1
src/assets/logo.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

9
src/assets/main.css Normal file
View File

@@ -0,0 +1,9 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.w-h-full {
@apply w-full h-full m-0 p-0 overflow-hidden;
}
}

BIN
src/assets/qrcode.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@@ -0,0 +1,173 @@
<template>
<el-dialog v-model="dialogVisible" title="密码管理" width="600" center align-center>
<el-form
ref="ruleFormRef"
style="max-width: 600px"
:model="ruleForm"
:rules="rules"
label-width="auto"
size="default"
status-icon
>
<el-form-item label="原密码" prop="oldPassword">
<el-input
clearable
placeholder="请输入原密码"
type="password"
show-password
maxlength="16"
autocomplete="off"
v-model="ruleForm.oldPassword"
@input="filter(ruleForm.oldPassword, 'oldPassword')"
oncut="return false"
onpaste="return false"
oncopy="return false"
/>
</el-form-item>
<el-form-item label="新密码" prop="newPassword">
<el-text class="mx-1" size="small" type="info">
密码必须包含数字、大小写字母以及特殊字符(!@#$%^&*),且长度在8-16位之间
</el-text>
<el-input
clearable
placeholder="请输入新密码"
type="password"
show-password
maxlength="16"
autocomplete="off"
v-model="ruleForm.newPassword"
@input="filter(ruleForm.newPassword, 'newPassword')"
oncut="return false"
onpaste="return false"
oncopy="return false"
/>
</el-form-item>
<el-form-item label="确认新密码" prop="checkPass">
<el-input
clearable
placeholder="请再次输入新密码"
type="checkPass"
show-password
maxlength="16"
autocomplete="off"
v-model="ruleForm.checkPass"
@input="filter(ruleForm.checkPass, 'checkPass')"
oncut="return false"
onpaste="return false"
oncopy="return false"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="resetForm(ruleFormRef)">取消</el-button>
<el-button type="primary" @click="changePassword(ruleFormRef)">
提交修改
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive } from "vue";
import { ElMessage } from "element-plus";
import { encrypt } from "../utils/aes";
import $http from "../api/index";
import $url from "../api/url";
const dialogVisible = ref(false);
const ruleFormRef = ref();
const ruleForm = reactive({
oldPassword: "",
newPassword: "",
checkPass: "",
});
const filter = (value, label) => {
if (/[=\s"'+-]/.test(value)) {
if (ruleForm[label]) {
ruleForm[label] = value.replace(/[=\s"'+-]/g, "");
}
}
};
const passwordRegex = /^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*]).{8,16}$/;
const validatePass = (rule, value, callback) => {
if (value === "") {
callback(new Error("请输入密码"));
} else if (!passwordRegex.test(value)) {
callback(
new Error(
"密码必须包含数字、大小写字母以及特殊字符(!@#$%^&*),且长度在8-16位之间"
)
);
} else {
if (ruleForm.checkPass !== "") {
if (!ruleFormRef.value) return;
ruleFormRef.value.validateField("checkPass");
}
callback();
}
};
const validateCheck = (rule, value, callback) => {
if (value === "") {
callback(new Error("请再次输入密码"));
} else if (value !== ruleForm.newPassword) {
callback(new Error("两次输入的密码不一致"));
} else {
callback();
}
};
const rules = reactive({
oldPassword: [{ required: true, message: "请输入原密码", trigger: "blur" }],
newPassword: [{ required: true, validator: validatePass, trigger: "blur" }],
checkPass: [{ required: true, validator: validateCheck, trigger: "blur" }],
});
const resetForm = (formEl) => {
dialogVisible.value = false;
if (!formEl) return;
formEl.resetFields();
};
const changePassword = async (formEl) => {
if (!formEl) return;
await formEl.validate((valid) => {
if (valid) {
dialogVisible.value = false;
let encryptForm = {
oldPassword: encrypt(ruleForm.oldPassword),
newPassword: encrypt(ruleForm.newPassword),
};
formEl.resetFields();
const data = {
passwordForm: encryptForm,
uid: localStorage.data
};
$http
.post($url.changePassword, data)
.then((res) => {
if (res.data.code === 200) {
localStorage.clear();
window.location.reload();
} else {
ElMessage.error("密码修改失败!");
}
});
} else {
ElMessage.error("提交失败!");
}
});
};
defineExpose({ dialogVisible });
</script>
<style scoped></style>

View File

@@ -0,0 +1,284 @@
<template>
<el-dialog v-model="dialogVisible" title="账户管理" width="600" center align-center>
<el-form
ref="ruleFormRef"
style="max-width: 600px"
:model="ruleForm"
:rules="rules"
label-width="auto"
size="default"
status-icon
>
<el-form-item label="用户名" prop="account_name">
<el-input
:disabled="acabled"
minlength="1"
maxlength="20"
clearable
placeholder="请输入用户名"
v-model="ruleForm.account_name"
@input="filter(ruleForm.account_name, 'account_name')"
/>
</el-form-item>
<el-form-item label="姓名" prop="user_name">
<el-input
maxlength="20"
clearable
placeholder="请输入姓名"
v-model="ruleForm.user_name"
@input="filter(ruleForm.user_name, 'user_name')"
/>
</el-form-item>
<el-form-item label="电话号码" prop="phone">
<el-input
minlength="11"
maxlength="11"
clearable
placeholder="请输入电话号码"
v-model="ruleForm.phone"
@input="filter(ruleForm.phone, 'phone')"
/>
</el-form-item>
<el-form-item v-if="sideMark == 'C'" label="性别" prop="gender">
<el-radio-group v-model="ruleForm.gender">
<el-radio value="1">男</el-radio>
<el-radio value="0">女</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="sideMark == 'C'" label="年龄" prop="age">
<el-input
maxlength="2"
clearable
placeholder="请输入年龄"
v-model="ruleForm.age"
@input="filter(ruleForm.age, 'age')"
/>
</el-form-item>
<el-form-item v-if="sideMark == 'C'" label="邮箱号" prop="email">
<el-input
maxlength="254"
clearable
placeholder="请输入邮箱号"
v-model="ruleForm.email"
@input="filter(ruleForm.email, 'email')"
/>
</el-form-item>
<el-form-item v-if="sideMark == 'C'" label="通信地址" prop="address">
<el-input
maxlength="300"
clearable
placeholder="请输入通信地址"
v-model="ruleForm.address"
@input="filter(ruleForm.address, 'address')"
/>
</el-form-item>
<el-form-item v-if="sideMark == 'C'" label="邮政编码" prop="post_code">
<el-input
maxlength="6"
clearable
placeholder="请输入邮政编码"
v-model="ruleForm.post_code"
@input="filter(ruleForm.post_code, 'post_code')"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="resetForm(ruleFormRef)">取消</el-button>
<el-button type="primary" @click="editUser(ruleFormRef)"> 提交修改 </el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, watch, onMounted } from "vue";
import { ElMessage } from "element-plus";
import userManagerActiveStore from "../stores/userManager";
import { encrypt, decrypt } from "../utils/aes";
import $http from "../api/index";
import $url from "../api/url";
const phoneRegex = /^(?:13\d|14[579]|15\d|16[67]|17[0135678]|18\d|19[89])\d{8}$/;
const emailRegex = /^([a-zA-Z0-9]+[|-|.]?)*[a-zA-Z0-9]+@([a-zA-Z0-9]+[|-|.]?)*[a-zA-Z0-9]+(.[a-zA-Z]{2,3})+$/;
const postCodeRegex = /^(0[1234567]|1[012356]|2[01234567]|3[0123456]|4[01234567]|5[1234567]|6[1234567]|7[012345]|8[013456])\d{4}$/;
const userManagerActive = userManagerActiveStore();
const dialogVisible = ref(false);
const props = defineProps({
msg: Object,
});
let sideMark;
const acabled = ref(true);
const ruleFormRef = ref();
const ruleForm = reactive({
account_name: "",
user_name: "",
phone: "",
});
const filter = (value, label) => {
let filterValue;
if (/[=\s"'+-]/.test(value)) {
if (ruleForm[label]) {
filterValue = value.replace(/[=\s"'+-]/g, "");
}
} else {
filterValue = value;
}
if (label == "phone" || label == "age" || label == "post_code") {
ruleForm[label] = filterValue.replace(/\D/g, "");
} else {
ruleForm[label] = filterValue;
}
};
const validatePhone = (rule, value, callback) => {
if (value && !phoneRegex.test(value)) {
callback(new Error("请输入正确的电话号码"));
} else {
callback();
}
};
const validateEmail = (rule, value, callback) => {
if (value && !emailRegex.test(value)) {
callback(new Error("请输入正确的邮箱地址"));
} else {
callback();
}
};
const validateCode = (rule, value, callback) => {
if (value && !postCodeRegex.test(value)) {
callback(new Error("请输入正确的邮政编码"));
} else {
callback();
}
};
const rules = reactive({
account_name: [{ required: false, message: "请输入用户名", trigger: "blur" }],
user_name: [],
phone: [
{ required: false, message: "请输入手机号", trigger: "blur" },
{ min: 11, max: 11, message: "输入长度应当为 11 位", trigger: "change" },
{ validator: validatePhone },
],
});
const resetForm = (formEl) => {
dialogVisible.value = false;
if (!formEl) return;
formEl.resetFields();
};
const emit = defineEmits(["resetData"]);
const reset = () => {
ruleForm.account_name = props.msg.account_name;
ruleForm.user_name = props.msg.user_name;
ruleForm.phone = props.msg.phone;
if (sideMark == "C") {
ruleForm.gender = props.msg.gender ? "1" : "0";
ruleForm.age = props.msg.age;
ruleForm.email = props.msg.email;
ruleForm.address = props.msg.address;
ruleForm.post_code = props.msg.post_code;
}
};
const editUser = async (formEl) => {
if (!formEl) return;
await formEl.validate((valid) => {
if (valid) {
dialogVisible.value = false;
const data = {
userMsg: ruleForm,
uid: localStorage.data
};
$http
.post($url.editUser, data)
.then((res) => {
if (res.data.code === 200) {
emit("resetData");
} else {
ElMessage.error("编辑用户信息失败!");
}
});
} else {
ElMessage.error("提交失败!");
}
});
};
defineExpose({ dialogVisible, reset });
watch(ruleForm, (value) => {
if (dialogVisible.value) {
userManagerActive.dialogVisible = dialogVisible.value;
userManagerActive.ruleForm.account_name = encrypt(value.account_name);
userManagerActive.ruleForm.user_name = encrypt(value.user_name);
userManagerActive.ruleForm.phone = encrypt(value.phone);
if (sideMark == "C") {
userManagerActive.ruleForm.gender = encrypt(value.gender);
userManagerActive.ruleForm.age = encrypt(value.age.toString());
userManagerActive.ruleForm.email = encrypt(value.email);
userManagerActive.ruleForm.address = encrypt(value.address);
userManagerActive.ruleForm.post_code = encrypt(value.post_code);
}
}
});
watch(dialogVisible, (value) => {
if (!value) {
userManagerActive.dialogVisible = value;
userManagerActive.ruleForm = {};
}
});
onMounted(() => {
dialogVisible.value = userManagerActive.dialogVisible;
if (localStorage.data) {
let decryptData = decrypt(localStorage.data);
sideMark = decryptData[decryptData.length - 1];
}
if (sideMark == "C") {
acabled.value = false;
ruleForm.gender = "";
ruleForm.age = "";
ruleForm.email = "";
ruleForm.address = "";
ruleForm.post_code = "";
rules.account_name[0].required = true;
rules.phone[0].required = true;
rules.age = [];
rules.email = [{ validator: validateEmail }];
rules.address = [];
rules.post_code = [
{ min: 6, max: 6, message: "输入长度应当为 6 位", trigger: "change" },
{ validator: validateCode },
];
}
if (userManagerActive.dialogVisible) {
ruleForm.account_name = decrypt(userManagerActive.ruleForm.account_name);
ruleForm.user_name = decrypt(userManagerActive.ruleForm.user_name);
ruleForm.phone = decrypt(userManagerActive.ruleForm.phone);
if (sideMark == "C") {
ruleForm.gender = decrypt(userManagerActive.ruleForm.gender);
ruleForm.age = decrypt(userManagerActive.ruleForm.age);
ruleForm.email = decrypt(userManagerActive.ruleForm.email);
ruleForm.address = decrypt(userManagerActive.ruleForm.address);
ruleForm.post_code = decrypt(userManagerActive.ruleForm.post_code);
}
}
});
</script>
<style scoped></style>

16
src/main.js Normal file
View File

@@ -0,0 +1,16 @@
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(ElementPlus)
app.use(router)
app.mount('#app')

31
src/router/index.js Normal file
View File

@@ -0,0 +1,31 @@
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/main',
name: 'main',
component: () => import('../views/Main.vue')
},
{
path: '/404page',
name: '404',
component: () => import('../views/404page.vue')
}
]
});
router.beforeEach((to, from, next) => {
if (to.matched.length === 0) {
if (to.path == "/") {
router.push({ path: '/main' });
} else {
router.push({ path: '/404page' });
}
} else {
next();
}
});
export default router

12
src/stores/counter.js Normal file
View File

@@ -0,0 +1,12 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})

35
src/stores/userManager.js Normal file
View File

@@ -0,0 +1,35 @@
import { defineStore } from 'pinia';
import { parse, stringify } from 'zipson';
const userManagerActiveStore = defineStore('userManager', {
state: () => ({
// 持久化数据
dialogVisible: false,
ruleForm: {
account_name: "",
user_name: "",
phone: "",
gender: "",
age: "",
email: "",
address: "",
post_code: "",
},
}),
persist: {
enabled: true, // Store 中数据持久化生效
strategies: [
{
key: 'userManagerActive', // 存储的 key
storage: localStorage, // 存储位置
paths: ["dialogVisible", "ruleForm"], // 需要持久化的字段
serializer: {
deserialize: parse,
serialize: stringify,
},
}
]
},
})
export default userManagerActiveStore

21
src/utils/aes.js Normal file
View File

@@ -0,0 +1,21 @@
import CryptoJS from 'crypto-js';
const KEY = CryptoJS.enc.Utf8.parse('BDQH');
export const encrypt = (plaintext) => {
if(plaintext !== null && plaintext !== undefined && plaintext !== "") {
return CryptoJS.AES.encrypt(plaintext, KEY, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}).toString();
}
};
export const decrypt = (plaintext) => {
if(plaintext !== null && plaintext !== undefined && !plaintext == "") {
return CryptoJS.AES.decrypt(plaintext, KEY, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}).toString(CryptoJS.enc.Utf8);
}
};

21
src/views/404page.vue Normal file
View File

@@ -0,0 +1,21 @@
<template>
<div class="common-layout w-h-full">
<el-text class="mx-1"> 404 not found </el-text>
<br />
<el-text class="mx-1"> 您搜索的页面不存在 </el-text>
<br />
<el-button style="font-weight: bold;" type="primary" @click="backHome">返回首页</el-button>
</div>
</template>
<script setup>
import { useRouter } from "vue-router";
const router = useRouter();
const backHome = () => {
router.push("/main");
};
</script>
<style scoped></style>

598
src/views/Main.vue Normal file
View File

@@ -0,0 +1,598 @@
<template>
<div>
<el-button type="primary" @click="tokenStatus">tokenStatus</el-button>
<el-button type="primary" @click="getUserToken">getUserToken</el-button>
<el-button type="primary" @click="lifeHold">lifeHold</el-button>
<el-button type="primary" @click="getMsg">getMsg</el-button>
<el-button type="primary" @click="getHistory">getHistory</el-button>
<div style="font-size: 2rem" class="text-center flex-auto font-bold text-gray-800">
北斗短报文应用教学
</div>
<div style="width: 100%" class="flex justify-end">
<el-button
style="font-weight: bold"
v-if="!loginMark"
type="primary"
@click="toLogin"
>
登录
</el-button>
</div>
<el-dropdown
style="width: 100%"
class="text-center felx justify-end items-center px-6"
v-if="loginMark"
>
<el-button style="font-weight: bold" type="success" size="small">
账户名: {{ userMsg.account_name }}
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
style="background-color: rgba(56, 188, 248, 0.1)"
@click="editUser"
>
<el-link style="font-weight: bold" :underline="false" type="primary">
账户管理
</el-link>
</el-dropdown-item>
<el-dropdown-item
style="background-color: rgba(56, 188, 248, 0.1)"
@click="changePassword"
>
<el-link style="font-weight: bold" :underline="false" type="primary">
密码管理
</el-link>
</el-dropdown-item>
<el-dropdown-item
style="background-color: rgba(240, 70, 70, 0.1)"
@click="unLogin"
>
<el-link style="font-weight: bold" :underline="false" type="danger">
退出登录
</el-link>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<UserManager @resetData="resetData" ref="userManagerRef" :msg="userMsg" />
<PasswordManager ref="passwordManagerRef" />
</div>
<div class="w-h-full bg-sky-500">
<div class="px-6">
<el-text
v-if="!loginMark"
style="font-size: 1.2rem"
class="font-bold text-gray-800 text-white"
>
您还未登录,无法使用本系统!请先进行登录!
</el-text>
<br />
<el-button
v-if="tokenType == 'none' ? loginMark : false"
type="primary"
@click="getUserToken"
>
获取临时 token
</el-button>
<br />
<el-button
v-if="tokenType == 'short' ? true : false"
type="primary"
@click="lifeHold"
>
延长临时 token 可用时间(仅一次)
</el-button>
<br />
<el-text
v-if="tokenType == 'none' ? false : true"
style="font-size: 1.2rem"
class="font-bold text-gray-800 text-white"
>
您的 token 是: {{ token }}
</el-text>
<br />
<el-text
v-if="tokenType == 'none' ? false : tokenType == 'long' ? false : true"
style="font-size: 1.2rem"
class="font-bold text-gray-800 text-white"
>
token 剩余有效时间: {{ formatTimeInSeconds(remainder) }}
</el-text>
<br />
<el-text
v-if="tokenType == 'none' ? false : tokenType == 'long' ? false : true"
style="font-size: 1.2rem"
class="font-bold text-gray-800 text-white"
>
token 预计过期时间: {{ overdueAt }}
</el-text>
<br />
<el-text
v-if="tokenType == 'none' ? false : true"
style="font-size: 1.2rem"
class="font-bold text-gray-800 text-white"
>
黑名单状态: {{ attacksMark ? "token 已被临时封禁" : "token 可以正常使用" }}
</el-text>
<br />
<el-text
v-if="tokenType == 'none' ? false : attacksMark"
style="font-size: 1.2rem"
class="font-bold text-gray-800 text-white"
>
token 剩余封禁时间: {{ formatTimeInSeconds(banAt / 1000) }}
</el-text>
<br />
<el-text
v-if="tokenType == 'none' ? false : attacksMark"
style="font-size: 1.2rem"
class="font-bold text-gray-800 text-white"
>
token 预计解封时间: {{ UnblockingAt }}
</el-text>
</div>
<div class="w-h-full bg-sky-200 px-6">
<el-text
v-if="tokenType == 'none' ? attacksMark : loginMark"
style="font-size: 1.2rem"
class="font-bold text-gray-800 text-white"
>
教学接口使用示例:
</el-text>
<br />
<el-text
v-if="tokenType == 'none' ? attacksMark : loginMark"
style="font-size: 1.2rem"
class="font-bold text-gray-800 text-white"
>
获取最近发送的一条信息:
</el-text>
<el-text style="font-size: 1.2rem" class="font-bold text-gray-800 text-white"
>http://xxxx/xxxx/getMsg?token=</el-text
>
<el-text style="font-size: 1.2rem" class="font-bold text-gray-800 text-white">{{
msgRuleForm.token
}}</el-text>
<el-form
ref="msgRuleFormRef"
style="max-width: 600px"
:model="msgRuleForm"
:rules="msgRules"
label-width="auto"
>
<el-form-item label="token" prop="token">
<el-input
v-model.number="msgRuleForm.token"
@input="filterAlphanumericChars('msg')"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getMsg(msgRuleFormRef)"> 提交请求 </el-button>
<el-button @click="resetForm(msgRuleFormRef)"> 清除 </el-button>
</el-form-item>
</el-form>
<el-text style="font-size: 1.2rem" class="font-bold text-gray-800 text-white"
>请求结果:
</el-text>
<el-text style="font-size: 1.2rem" class="font-bold text-gray-800 text-white">{{
msgResult
}}</el-text>
<br />
<el-text
v-if="tokenType == 'none' ? attacksMark : loginMark"
style="font-size: 1.2rem"
class="font-bold text-gray-800 text-white"
>
获取若干条历史消息的接口:
</el-text>
<el-text style="font-size: 1.2rem" class="font-bold text-gray-800 text-white"
>http://xxxx/xxxx/getHistory?token=</el-text
>
<el-text style="font-size: 1.2rem" class="font-bold text-gray-800 text-white">{{
historyRuleForm.token
}}</el-text>
<el-text style="font-size: 1.2rem" class="font-bold text-gray-800 text-white"
>&limit=</el-text
>
<el-text style="font-size: 1.2rem" class="font-bold text-gray-800 text-white">{{
historyRuleForm.limit
}}</el-text>
<el-text style="font-size: 1.2rem" class="font-bold text-gray-800 text-white"
>&offset=</el-text
>
<el-text style="font-size: 1.2rem" class="font-bold text-gray-800 text-white">{{
historyRuleForm.offset
}}</el-text>
<el-form
ref="historyRuleFormRef"
style="max-width: 600px"
:model="historyRuleForm"
:rules="historyRules"
label-width="auto"
>
<el-form-item label="token" prop="token">
<el-input
v-model.number="historyRuleForm.token"
@input="filterAlphanumericChars('history')"
/>
</el-form-item>
<el-form-item label="limit" prop="limit">
<el-input v-model.number="historyRuleForm.limit" />
</el-form-item>
<el-form-item label="offset" prop="offset">
<el-input v-model.number="historyRuleForm.offset" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getHistory(historyRuleFormRef)">
提交请求
</el-button>
<el-button @click="resetForm(historyRuleFormRef)"> 清除 </el-button>
</el-form-item>
</el-form>
<el-text style="font-size: 1.2rem" class="font-bold text-gray-800 text-white"
>请求结果:
</el-text>
<el-text style="font-size: 1.2rem" class="font-bold text-gray-800 text-white">{{
historyResult
}}</el-text>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import { ElMessage } from "element-plus";
import { ArrowDown } from "@element-plus/icons-vue";
import moment from "moment";
import $http from "../api/index";
import $url from "../api/url";
import { encrypt } from "../utils/aes";
import UserManager from "../components/userManager.vue";
import PasswordManager from "../components/changePassword.vue";
const loginMark = ref(false);
const userManagerRef = ref(null);
const userMsg = ref({});
const passwordManagerRef = ref(null);
const tokenType = ref("none");
const token = ref("none");
const remainder = ref("none");
const overdueAt = ref(0);
const attacksMark = ref(false);
const banAt = ref(0);
const UnblockingAt = ref(0);
const msgRuleFormRef = ref(null);
const historyRuleFormRef = ref(null);
const msgRuleForm = reactive({
token: "",
});
const historyRuleForm = reactive({
token: "",
limit: 10,
offset: 1,
});
const msgResult = ref();
const historyResult = ref();
const filterAlphanumericChars = (str) => {
if (str == "msg") {
msgRuleForm.token = msgRuleForm.token.replace(/[^a-zA-Z0-9]/g, "");
} else if (str == "history") {
historyRuleForm.token = historyRuleForm.token.replace(/[^a-zA-Z0-9]/g, "");
}
};
const checkToken = (rule, value, callback) => {
if (value === "") {
callback(new Error("请输入已获取到的 token"));
} else {
callback();
}
};
const checkLimit = (rule, value, callback) => {
if (value === "") {
callback(new Error("请输入一次请求要获取的数据量 limit"));
} else {
callback();
}
};
const checkOffset = (rule, value, callback) => {
if (value === "") {
callback(new Error("请输入需要跳过的页数 offset"));
} else {
callback();
}
};
const msgRules = reactive({
token: [{ validator: checkToken, trigger: "blur" }],
});
const historyRules = reactive({
token: [{ validator: checkToken, trigger: "blur" }],
limit: [{ validator: checkLimit, trigger: "blur" }],
offset: [{ validator: checkOffset, trigger: "blur" }],
});
const resetForm = (formEl) => {
if (!formEl) return;
formEl.resetFields();
};
let timer = null,
banTimer = null;
const formatTimeInSeconds = (seconds) => {
const day = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
const pad = (num) => (num < 10 ? "0" + num : num);
return `${pad(day)}天${pad(hours)}小时${pad(minutes)}分钟${pad(secs)}秒`;
};
const tokenStatus = () => {
const data = {
uid: localStorage.data,
};
$http.post($url.tokenStatus, data).then((res) => {
if (res.data.code === 200) {
console.log(res.data);
tokenType.value = res.data.type;
if (res.data.type !== "none") {
token.value = res.data.token;
attacksMark.value = res.data.attacksMark;
if (res.data.attacksMark) {
banAt.value = res.data.banAt + 3600000 - new Date().getTime();
clearInterval(banTimer);
banTimer = setInterval(() => {
banAt.value = banAt.value - 1000;
}, 1000);
UnblockingAt.value = moment
.utc(new Date(res.data.banAt + 3600000))
.add(8, "hours")
.format("YYYY年MM月DD日HH:mm:ss");
}
if (res.data.type !== "long") {
remainder.value = res.data.remainder;
clearInterval(timer);
timer = setInterval(() => {
remainder.value = remainder.value - 1;
}, 1000);
overdueAt.value = moment
.utc(new Date(new Date().getTime() + res.data.remainder * 1000))
.add(8, "hours")
.format("YYYY年MM月DD日HH:mm:ss");
}
}
} else {
console.log(404);
}
});
};
const getUserToken = () => {
const data = {
uid: localStorage.data,
};
$http.post($url.getUserToken, data).then((res) => {
if (res.data.code === 200) {
console.log(res.data);
tokenStatus();
} else {
console.log(404);
}
});
};
const lifeHold = () => {
const data = {
uid: localStorage.data,
};
$http.post($url.lifeHold, data).then((res) => {
if (res.data.code === 200) {
console.log(res.data);
tokenStatus();
} else {
console.log(404);
}
});
};
const getMsg = (formEl) => {
if (!formEl) return;
formEl.validate((valid) => {
if (valid) {
const data = {
token: msgRuleForm.token,
};
$http.get($url.getMsg, data).then((res) => {
console.log(res.data);
msgResult.value = res.data;
const code = res.data.code;
if (code === 200) {
console.log(200);
} else if (code == "t401") {
console.log(401);
} else if (code == 403) {
console.log(403);
} else if (code == 500) {
console.log(500);
} else {
console.log(404);
}
});
} else {
console.log("请求提交失败");
}
});
};
const getHistory = (formEl) => {
if (!formEl) return;
formEl.validate((valid) => {
if (valid) {
const data = {
token: historyRuleForm.token,
limit: historyRuleForm.limit,
offset: historyRuleForm.offset,
};
$http.get($url.getHistory, data).then((res) => {
console.log(res.data);
historyResult.value = res.data;
const code = res.data.code;
if (code === 200) {
console.log(200);
} else if (code == "t401") {
console.log(401);
} else if (code == 403) {
console.log(403);
} else if (code == 500) {
console.log(500);
} else {
console.log(404);
}
});
} else {
console.log("请求提交失败");
}
});
};
const toLogin = () => {
localStorage.clear();
const data = {
origin_url: window.location.href,
};
$http.post($url.toLogin, data).then((res) => {
if (res.data.code === 200) {
window.location.href = res.data.redirect;
} else {
router.push({ path: "/404page" });
}
});
};
const editUser = () => {
if (userManagerRef.value) {
userManagerRef.value.dialogVisible = true;
userManagerRef.value.reset();
}
};
const changePassword = () => {
if (passwordManagerRef.value) {
passwordManagerRef.value.dialogVisible = true;
}
};
const unLogin = () => {
const data = {
uid: localStorage.data,
access_token: localStorage.access_token,
refresh_token: localStorage.refresh_token,
};
$http.post($url.unLogin, data).then((res) => {
localStorage.clear();
window.location.reload();
});
};
const resetData = () => {
const data = {
uid: localStorage.data,
};
$http.post($url.getUser, data).then((res) => {
if (res.data.code === 200) {
userMsg.value = res.data.userData;
} else {
ElMessage.error("获取用户信息失败!");
}
});
};
onMounted(() => {
let getCode = window.location.href.indexOf("?");
if (getCode != "-1") {
let code = window.location.href.substring(getCode + 1);
const data = {
code: code,
type: encrypt("code"),
};
$http.post($url.getToken, data).then((res) => {
if (res.data.code === 200) {
localStorage.setItem("data", res.data.data);
localStorage.setItem("access_token", res.data.access_token);
localStorage.setItem("refresh_token", res.data.refresh_token);
window.location.href = res.data.redirect;
}
});
} else if (
localStorage.data &&
localStorage.data !== "" &&
localStorage.data !== null &&
localStorage.data !== undefined &&
localStorage.data !== NaN &&
localStorage.access_token &&
localStorage.access_token !== "" &&
localStorage.access_token !== null &&
localStorage.access_token !== undefined &&
localStorage.access_token !== NaN
) {
const data = {
access_token: localStorage.access_token,
data: localStorage.data,
};
$http.post($url.verify, data).then((res) => {
if (res.data.code === 200) {
localStorage.setItem("data", res.data.data);
loginMark.value = true;
resetData();
tokenStatus();
}
});
} else if (
localStorage.data &&
localStorage.data !== "" &&
localStorage.data !== null &&
localStorage.data !== undefined &&
localStorage.data !== NaN &&
localStorage.refresh_token &&
localStorage.refresh_token !== "" &&
localStorage.refresh_token !== null &&
localStorage.refresh_token !== undefined &&
localStorage.refresh_token !== NaN
) {
const data = {
code: localStorage.refresh_token,
type: localStorage.data,
};
$http.post($url.getToken, data).then((res) => {
if (res.data.code === 200) {
localStorage.setItem("data", res.data.data);
localStorage.setItem("access_token", res.data.access_token);
localStorage.setItem("refresh_token", res.data.refresh_token);
window.location.reload();
}
});
}
});
</script>
<style scoped></style>

13
tailwind.config.js Normal file
View File

@@ -0,0 +1,13 @@
/** @type {import('tailwindcss').Config} */
export default {
purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
darkMode: false, // or 'media' or 'class'
content: [],
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}

43
vite.config.js Normal file
View File

@@ -0,0 +1,43 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vitejs.dev/config/
export default defineConfig({
publicPath: './',
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
//用来配置跨域
host: '127.0.0.1',
port: 7009,
proxy: {
'/authentication': {
target: 'http://127.0.0.1:8011',//鉴权服务器地址
changeOrigin: true,
rewrite: (path) => path.replace(/^\/authentication/, '')
},
'/teachingApi': {
target: 'http://127.0.0.1:8017',//资源服务器地址
changeOrigin: true,
rewrite: (path) => path.replace(/^\/teachingApi/, '')
},
}
}
})