北斗启航科技教育平台

This commit is contained in:
2026-03-27 17:43:37 +08:00
commit fd5f6e03c9
65 changed files with 135661 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 @@
# open-map-assistant
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
```

49
index.html Normal file
View File

@@ -0,0 +1,49 @@
<!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>
<style>
/* 当屏幕宽度大于或等于3000px时 */
@media (min-width: 3000px) {
html {
font-size: 28px;
}
}
/* 当屏幕宽度在1920px到2999px之间时 */
@media (min-width: 1920px) and (max-width: 2999px) {
html {
font-size: 20px;
}
}
/* 当屏幕宽度在750px到1919px之间时 */
@media (min-width: 750px) and (max-width: 1919px) {
html {
font-size: 14px;
}
}
/* 当屏幕宽度在600px到749px之间时 */
@media (min-width: 600px) and (max-width: 749px) {
html {
font-size: 14px;
}
}
/* 当屏幕宽度小于600px时 */
@media (max-width: 599px) {
html {
font-size: 12px;
}
}
</style>
</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"]
}

42
package.json Normal file
View File

@@ -0,0 +1,42 @@
{
"name": "service-platform",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --port=7017",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.6.8",
"cesium": "^1.117.0",
"core-js": "^3.37.0",
"crypto-js": "^4.2.0",
"echarts": "^5.5.0",
"element-plus": "^2.7.2",
"esbuild": "^0.21.5",
"file-saver": "^2.0.5",
"fingerprintjs2": "^2.1.4",
"ol": "^9.1.0",
"pinia": "^2.1.7",
"pinia-plugin-persist": "^1.0.0",
"vue": "^3.4.21",
"vue-cesium": "^v3.2.7",
"vue-echarts": "^6.7.2",
"vue-i18n": "^9.13.1",
"vue-router": "^4.3.0",
"vue3-openlayers": "^8.1.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: {}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

BIN
public/banner/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

BIN
public/banner/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

BIN
public/banner/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 760 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

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>

14
src/api/banner/banner.js Normal file
View File

@@ -0,0 +1,14 @@
export default {
img1: {
url: '/banner/1.png',
link: 'http://www.hwasmart.com/'
},
img2: {
url: '/banner/2.png',
link: 'http://www.hwasmart.com/'
},
img3: {
url: '/banner/3.png',
link: 'http://www.hwasmart.com/'
},
}

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);
},
}

View File

@@ -0,0 +1,112 @@
export default {
productsAndCourses: {
label: "产品与课程",
serviceBlock: {
block1: {
thumbnail: "/productsAndCourses/BDCreateAndPlayAndAdvancedIntelligence.jpg",
name: "“北斗启航”航天北斗科技课程",
description: "科普北斗卫星导航系统基础知识,讲解北斗卫星导航系统在日常生活中的应用。利用三类北斗应用场景,实现课程的智能化进阶。",
link: "https://mp.weixin.qq.com/s?__biz=MzAxMzIzNTMyMg==&mid=503598042&idx=1&sn=ab0f3fe82ddc816bd8f1bcd9dd9e6064&chksm=00554ecf3722c7d927c580e9b6fe064705dc80df2859d79195c56303ee9a9c11692dbd5f24e4#rd"
},
block2: {
thumbnail: "/productsAndCourses/intelligentLifeAndCreativeProgramming.jpg",
name: "智慧生活与创意编程",
description: "让学生体验生活中各类智慧应用,通过图形化编程软件,拓展无限的创意。特有的北斗图形化编程模块,实现心中的“航天北斗梦”。",
link: "https://mp.weixin.qq.com/s?__biz=MzAxMzIzNTMyMg==&mid=503598048&idx=1&sn=563bdf936678527dff086a997e76f952&chksm=00554ef53722c7e335abd8107423e97d348b53cec959176eff8a9dc5319ead0e35b4a65fc03e#rd"
},
block3: {
thumbnail: "/productsAndCourses/creativeTraining.jpg",
name: "北斗启航科创空间",
description: "创客空间的设计遵循STEAM教育和创客教育的理念,让学生通过项目式、场景化学习的方式,训练学生的技术意识、工程思维、图样表达、创新设计、物化能力等方面。",
link: "https://mp.weixin.qq.com/s/sIqyYE1GQq1PL68poY1TRw"
},
// block4: {
// thumbnail: "/productsAndCourses/interferenceSimulation.jpg",
// name: "北斗干扰仿真软件",
// description: "北斗干扰仿真软件在卫星导航领域具有重要的应用价值,能够为北斗系统的研发、测试和应用提供有力的技术支持。",
// link: "http://test.bdrd.hwasmart.com/product/interferenceSimulation.html"
// }
}
},
cloudService: {
label: "云服务",
serviceBlock: {
block1: {
thumbnail: "/cloudService/nmea0183-thumbnail.jpg",
name: "NMEA-0183协议解析",
description: "NMEA-0183是美国国家海洋电子协会为海用电子设备制定的标准格式,使用北斗定位模块接受到的数据即符合该格式,使用NMEA-0183协议解析工具既能够读取北斗定位模块接受的卫星数据",
link: "http://edu.hwasmart.com/nmea0183"
},
block2: {
thumbnail: "/cloudService/tdt-thumbnail.jpg",
name: "天地图全球定位",
description: "“天地图”是国家测绘地理信息局建设的地理信息综合服务网站,通过天地图提供的地图服务,用户输入自己的经纬度坐标就可以看到自己在地球上的位置",
link: "http://edu.hwasmart.com/cesium"
},
block3: {
thumbnail: "/cloudService/baiduThreeDimensionalMap-thumbnail.jpg",
name: "百度城市地图",
description: "基于百度城市地图,实现经纬度定位功能,更直观的了解城市样貌。",
link: "http://edu.hwasmart.com/baidumap"
},
block4: {
thumbnail: "/cloudService/planisphere-thumbnail.jpg",
name: "星座图工具",
description: "输入卫星编号、载噪比、仰角、方位角等数据,在极坐标系统中绘制卫星的位置,能够更直观的了解当前头顶上方卫星分布的情况,同时在柱状图中显示每个卫星的信号强度",
link: "http://edu.hwasmart.com/planisphere"
},
block5: {
thumbnail: "/cloudService/MapTrajectoryDraw.jpg",
name: "地图轨迹绘制工具",
description: "输入一组经纬度数据,在地图上绘制相应经纬度所呈现的轨迹和图案,能够更直观的展现使用者规划的路径和范围",
link: "http://edu.hwasmart.com/MapTrajectoryDraw"
},
block6: {
thumbnail: "/cloudService/ephemeris_search.jpg",
name: "卫星导航系统历史分布数据查询",
description: "提供北斗全球可见卫星数分布、北斗星下点轨迹、GNSS星下点位置三种维度的历史数据查询。",
link: "http://edu.hwasmart.com/satellite_history_data_search"
},
block7: {
thumbnail: "/cloudService/trajectory.jpg",
name: "卫星轨迹图生成工具",
description: "在同一位置,记录同一颗卫星在不同时间点的经度、纬度、日期、时间、仰角、方位角以及信号强度,在卫星星座图上自动生成其运动轨迹。",
link: "http://edu.hwasmart.com/trajectory"
},
block8: {
thumbnail: "/cloudService/mapTheCampus.jpg",
name: "校园地图绘制工具",
description: "根据同学们在校园中不同的地点测量的卫星星座分布以及信号强度,绘制校园地图,帮助同学们分析卫星信号变化规律。",
link: "http://edu.hwasmart.com/map"
}
}
},
activityAndContests: {
label: "科普活动和创新竞赛",
serviceBlock: {
block1: {
thumbnail: "/activityAndContests/bdlh_new.jpg",
name: "“北斗领航梦想”青少年科学调查体验活动",
description: "2023 年起,青少年科学调查体验活动设置了“北斗领航梦想”主题活动,主要围绕“了解北斗系统、激发探索兴趣、树立北斗精神、展望航天梦想”展开调查体验。",
link: "https://cyscc.org/dctyhd/#/home"
},
block2: {
thumbnail: "/activityAndContests/BD_CASTIC.jpg",
name: "“北斗杯”全国青少年空天科技体验与创新大赛",
description: "“北斗杯”全国青少年空天科技体验与创新大赛是中国青少年卫星导航领域高层次、高水平、大规模的科技盛会,于2010年开始举办,在社会各界大力支持下,已顺利开展了十四届。",
link: "http://www.beidoucup.com"
}
}
},
activityAndContests: {
label: "在线服务",
serviceBlock: {
block1: {
thumbnail: "/bdShortMessagePlatform/bg.jpg",
name: "北斗短报文应用服务系统",
description: "北斗短报文应用服务系统测试",
link: "http://bdrd.hwasmart.com/bdHome"
}
}
},
}

23
src/api/tools/tools.js Normal file
View File

@@ -0,0 +1,23 @@
export default {
file1: {
label: "Mixly图形编程工具(Hwasmart Blockly)",
fileName: "Mixly图形编程工具(Hwasmart Blockly).rar",
loadUrl: "/attachFile/c0c3-a4dc-4ac9-8923-1e95.zip"
},
file2: {
label: "北斗启航MIX驱动",
fileName: "北斗启航MIX驱动.rar",
loadUrl: "/attachFile/7a68-acf7-4ed1-b5cc-190f.rar"
},
file3: {
label:
"Mixly for mix2",
fileName: "Mixly for mix2.rar",
loadUrl: "/attachFile/33e2-5c2d-408f-9447-ee25.zip"
},
// file4: {
// label: "北斗干扰仿真软件",
// fileName: "北斗干扰仿真软件.rar",
// loadUrl: "/attachFile/beidouInterferenceSimulation.rar"
// },
}

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

@@ -0,0 +1,16 @@
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",
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

BIN
src/assets/fav.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

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

BIN
src/assets/北斗启航.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
src/assets/横版LOGO.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

46
src/components/banner.vue Normal file
View File

@@ -0,0 +1,46 @@
<template>
<div class="block text-center mx-auto" style="width: 75%">
<el-carousel :height="imgHeight" class="mx-auto" style="width: 100%">
<el-carousel-item v-for="item in banner" :key="item">
<div class="flex justify-center items-center mx-auto" style="height: 100%">
<a :href="item.link">
<img ref="myImage" :src="item.url" alt="" />
</a>
</div>
</el-carousel-item>
</el-carousel>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from "vue";
import banner from "../api/banner/banner";
const windowWidth = ref(window.innerWidth);
const myImage = ref(null);
const imgHeight = ref("0px");
// 处理窗口大小变化的函数
const handleWindowResize = () => {
windowWidth.value = window.innerWidth;
imgHeight.value = myImage.value[0].offsetHeight + "px";
};
onMounted(() => {
const roll = setInterval(() => {
if (myImage.value) {
if (myImage.value[0].complete) {
imgHeight.value = myImage.value[0].offsetHeight + "px";
clearInterval(roll);
}
}
}, 100);
window.addEventListener("resize", handleWindowResize);
});
onUnmounted(() => {
window.removeEventListener("resize", handleWindowResize);
});
</script>
<style scoped></style>

36
src/components/block.vue Normal file
View File

@@ -0,0 +1,36 @@
<template>
<el-card style="width: 75%" class="mx-auto" shadow="always">
<template #header>
<div class="card-header">
<span>{{ service.label }}</span>
</div>
</template>
<div class="flex flex-wrap">
<el-card
style="width: 20rem"
v-for="block in service.serviceBlock"
:key="block"
class="my-4 mx-auto text-center"
shadow="always"
>
<template #header>
<div class="card-header">
<el-link class="mb-2" :href="block.link">
<img class="w-full" :src="block.thumbnail" alt="" />
</el-link>
</div>
</template>
<el-link class="mb-2" style="font-size: 1.15rem" type="primary" :href="block.link">{{ block.name }}</el-link>
<div class="text-justify indent-8">
{{ block.description }}
</div>
</el-card>
</div>
</el-card>
</template>
<script setup>
const { service } = defineProps(["service"]);
</script>
<style scoped></style>

View File

@@ -0,0 +1,171 @@
<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,12 @@
<template>
<div v-for="service in services" :key="service">
<Block :service="service" />
</div>
</template>
<script setup>
import services from "../api/services/services";
import Block from "./block.vue";
</script>
<style scoped></style>

17
src/components/footer.vue Normal file
View File

@@ -0,0 +1,17 @@
<template>
<div
class="h-full p-5 flex flex-wrap justify-center items-center"
style="background-color: #222d2b"
>
<div class="mx-5">
<a class="text-sky-400 underline" href="http://www.hwasmart.com/">
北京华云智联科技有限公司
</a>
</div>
<div>400-166-5286</div>
</div>
</template>
<script setup></script>
<style scoped></style>

185
src/components/header.vue Normal file
View File

@@ -0,0 +1,185 @@
<template>
<div
class="h-full p-5 flex justify-between items-center"
style="background-color: #232d3b"
>
<a href="http://www.hwasmart.com/">
<img style="width: 12rem;" :src="logo" alt="" />
</a>
<div style="font-size: 2rem;" class="flex-auto text-center font-bold text-gray-800 text-white">北斗启航科技教育平台</div>
<div style="width: 150px;" class="flex justify-center">
<el-button style="font-weight: bold;" v-if="!loginMark" type="primary" @click="toLogin"> 登录 </el-button>
</div>
<el-dropdown style="width: 150px;" class="felx justify-end items-center" 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>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { ElMessage } from "element-plus";
import { ArrowDown } from '@element-plus/icons-vue';
import $http from "../api/index";
import $url from "../api/url";
import { encrypt } from "../utils/aes";
import logo from "../assets/logo.png";
import UserManager from "./userManager.vue";
import PasswordManager from "./changePassword.vue";
const loginMark = ref(false);
const userManagerRef = ref(null);
const userMsg = ref({});
const passwordManagerRef = ref(null);
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();
}
});
} 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>

View File

@@ -0,0 +1,36 @@
<template>
<el-card style="width: 75%" class="mx-auto" shadow="always">
<template #header>
<div class="card-header">
<span>工具下载</span>
</div>
</template>
<div class="flex flex-wrap">
<div v-for="toolFile in tools" :key="toolFile" class="toolBox flex my-4 mr-16 leading-8 text-center bg-graywhite hover:bg-grayblue hover:underline hover:text-white">
<a class="pl-4" :href="toolFile.loadUrl" :download="toolFile.fileName">{{ toolFile.label }}</a>
</div>
</div>
</el-card>
</template>
<script setup>
import tools from "../api/tools/tools"
</script>
<style scoped>
.toolBox::after {
content:"";
margin-right: -1.25rem;
border-top: 1rem solid transparent;
border-bottom: 1rem solid transparent;
border-left: 1.25rem solid #f5f5f5;
}
.toolBox:hover::after {
content:"";
margin-right: -1.25rem;
border-top: 1rem solid transparent;
border-bottom: 1rem solid transparent;
border-left: 1.25rem solid #0da3e2;
}
</style>

View File

@@ -0,0 +1,282 @@
<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>

17
src/main.js Normal file
View File

@@ -0,0 +1,17 @@
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
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')

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

@@ -0,0 +1,26 @@
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/service',
name: 'service',
component: () => import('../views/Service.vue')
}
]
})
router.beforeEach((to, from, next) => {
if (to.matched.length === 0) {
if (to.path == "/") {
router.push({ path: '/service' });
} else {
router.push({ path: '/service' });
}
} 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);
}
}

27
src/views/Service.vue Normal file
View File

@@ -0,0 +1,27 @@
<template>
<div class="common-layout w-h-full">
<el-container class="w-h-full">
<el-header style="padding: 0;" height="6rem">
<Header />
</el-header>
<el-main style="padding: 0;">
<Banner />
<District />
<ToolDownload />
</el-main>
<el-footer style="padding: 0;" height="4rem">
<Footer />
</el-footer>
</el-container>
</div>
</template>
<script setup>
import Header from "../components/header.vue";
import Banner from "../components/banner.vue";
import District from "../components/district.vue";
import ToolDownload from "../components/toolDownload.vue";
import Footer from "../components/footer.vue";
</script>
<style scoped></style>

18
tailwind.config.js Normal file
View File

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

38
vite.config.js Normal file
View File

@@ -0,0 +1,38 @@
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: 7007,
proxy: {
'/authentication': {
target: 'http://127.0.0.1:8011',//目标服务器地址
changeOrigin: true,
rewrite: (path) => path.replace(/^\/authentication/, '')
},
}
}
})