feat(web): 添加 sm-crypto 库支持 SM3 加密及导出功能

- 在登录模块中增加 SM3 加密调用逻辑
- 新增导出按钮和相关处理逻辑,支持统计分析数据导出
- 调整多个页面样式布局,优化组件结构与代码可读性
- 更新设备类型配置,新增“冷机”和“网关”类型
- 添加 download 工具函数,支持通过 URL 下载文件
- 配置 webpack 代理,支持文件下载路径转发
```
This commit is contained in:
zhoumengru
2025-09-23 14:19:36 +08:00
parent ee98556eec
commit 65d1ad93ef
15 changed files with 192 additions and 93 deletions

16
web/package-lock.json generated
View File

@@ -14,6 +14,7 @@
"echarts": "^6.0.0", "echarts": "^6.0.0",
"moment": "^2.30.1", "moment": "^2.30.1",
"qs": "^6.12.3", "qs": "^6.12.3",
"sm-crypto": "^0.3.13",
"vue": "^3.2.13", "vue": "^3.2.13",
"vue-router": "^4.0.3", "vue-router": "^4.0.3",
"vuex": "^4.0.0" "vuex": "^4.0.0"
@@ -9791,6 +9792,12 @@
"js-yaml": "bin/js-yaml.js" "js-yaml": "bin/js-yaml.js"
} }
}, },
"node_modules/jsbn": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/jsbn/-/jsbn-1.1.0.tgz",
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
"license": "MIT"
},
"node_modules/jsesc": { "node_modules/jsesc": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz",
@@ -13633,6 +13640,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/sm-crypto": {
"version": "0.3.13",
"resolved": "https://registry.npmmirror.com/sm-crypto/-/sm-crypto-0.3.13.tgz",
"integrity": "sha512-ztNF+pZq6viCPMA1A6KKu3bgpkmYti5avykRHbcFIdSipFdkVmfUw2CnpM2kBJyppIalqvczLNM3wR8OQ0pT5w==",
"license": "MIT",
"dependencies": {
"jsbn": "^1.1.0"
}
},
"node_modules/sockjs": { "node_modules/sockjs": {
"version": "0.3.24", "version": "0.3.24",
"resolved": "https://registry.npmmirror.com/sockjs/-/sockjs-0.3.24.tgz", "resolved": "https://registry.npmmirror.com/sockjs/-/sockjs-0.3.24.tgz",

View File

@@ -15,6 +15,7 @@
"echarts": "^6.0.0", "echarts": "^6.0.0",
"moment": "^2.30.1", "moment": "^2.30.1",
"qs": "^6.12.3", "qs": "^6.12.3",
"sm-crypto": "^0.3.13",
"vue": "^3.2.13", "vue": "^3.2.13",
"vue-router": "^4.0.3", "vue-router": "^4.0.3",
"vuex": "^4.0.0" "vuex": "^4.0.0"

View File

@@ -192,11 +192,12 @@ input:-internal-autofill-selected {
color: var(--theme-text-default) !important; color: var(--theme-text-default) !important;
} }
.search { .search {
height:70px; // height:70px;
color: #fff; color: #fff;
display: flex; display: flex;
flex-direction: column; // flex-direction: column;
justify-content: space-between; justify-content: space-between;
margin-bottom: 20px;
.page-title { .page-title {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -321,17 +322,17 @@ input:-internal-autofill-selected {
.item { .item {
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 15px; // margin-bottom: 15px;
margin-right: 20px; margin-right: 20px;
} }
} }
} }
.bottom { .bottom {
display: flex; // display: flex;
margin-top: 20px; // margin-top: 20px;
margin-bottom: 20px; // margin-bottom: 20px;
justify-content: space-between; // justify-content: space-between;
align-items: center; // align-items: center;
.button { .button {
// margin-left: 10px; // margin-left: 10px;
} }

View File

@@ -3,7 +3,8 @@ const btnList = [
{ label: '新增', type: 'add', disFlag: 'is_add', icon: 'icon-add' }, { label: '新增', type: 'add', disFlag: 'is_add', icon: 'icon-add' },
{ label: '查看', type: 'read', disFlag: 'is_view' }, { label: '查看', type: 'read', disFlag: 'is_view' },
{ label: '修改', type: 'edit', disFlag: 'is_edit' }, { label: '修改', type: 'edit', disFlag: 'is_edit' },
{ label: '删除', type: 'del', disFlag: 'is_del', icon: 'icon-del' } { label: '删除', type: 'del', disFlag: 'is_del', icon: 'icon-del' },
{ label: '导出', type: 'output', disFlag: 'is_edit', icon: 'icon-add' }
] ]
function findNodeByRoute(tree, targetRoute) { function findNodeByRoute(tree, targetRoute) {
for (const node of tree) { for (const node of tree) {

View File

@@ -80,6 +80,16 @@ export const deviceTypeList = [
label: '视频监控', label: '视频监控',
iconfont: 'icon-shipinjiankong' iconfont: 'icon-shipinjiankong'
}, },
{
value: '14',
label: '冷机',
iconfont: 'icon-lengjitubiao'
},
{
value: '15',
label: '网关',
iconfont: 'icon-wangguan'
},
{ {
value: '100', value: '100',
label: '储能预制舱', label: '储能预制舱',

16
web/src/utils/download.js Normal file
View File

@@ -0,0 +1,16 @@
export function downloadByUrl(file) {
fetch(file.url)
.then((response) => response.blob())
.then((blob) => {
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.style.display = 'none'
a.href = url
// 设置下载的文件名
a.download = file.name
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
})
.catch((error) => console.error('Error downloading the file:', error))
}

View File

@@ -38,8 +38,10 @@
</a-config-provider> </a-config-provider>
</template> </template>
<script> <script>
const {sm3} = require('sm-crypto')
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue' import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
import { postReq,getReq } from '@/request/api.js' import { postReq,getReq } from '@/request/api.js'
export default { export default {
name: 'LoginView', name: 'LoginView',
components: { components: {
@@ -69,11 +71,20 @@ export default {
loading: false loading: false
} }
}, },
mounted() {
// const hashData = sm3('123456');
// console.log(hashData); // 输出SM3哈希值
},
methods: { methods: {
async login() { async login() {
try { try {
const values = await this.$refs.ruleForm.validateFields() const values = await this.$refs.ruleForm.validateFields()
const res = await getReq('/login',this.form ) const newForm={
...this.form,
// passwd:sm3(this.form.passwd)
}
const res = await getReq('/login',newForm )
this.loading = false this.loading = false

View File

@@ -1,22 +1,25 @@
<template> <template>
<div class="statisicalAn"> <div class="statisicalAn">
<div style="display: flex; justify-content: space-between; height: 50px"> <div style="display: flex; justify-content: space-between">
<div class="tab-header"> <div class="header">
<div v-for="item in tabList" :key="item.key" class="tab"> <div
<span v-for="item in tabList"
:key="item.key"
class="tab-item"
:class="[activeKey == item.key ? 'actived' : 'uactived']" :class="[activeKey == item.key ? 'actived' : 'uactived']"
@click="activeKey = item.key" @click="activeKey = item.key"
>{{ item.name }}</span
> >
<span>{{ item.name }}</span>
</div> </div>
</div> </div>
<searchBox <searchBox
class="searchBox" class="searchBox"
ref="searchBox" ref="searchBox"
:btn-option-list="[]" :btn-option-list="btnOptionList"
:search-options="searchOptions" :search-options="searchOptions"
:title-option="{ title: '', info: '' }" :title-option="{ title: '', info: '' }"
@onSearch="onSearch" @onSearch="onSearch"
@operateForm="operateForm"
> >
<template #stationSelect=""> <template #stationSelect="">
<a-select <a-select
@@ -34,6 +37,7 @@
{{ option.label }} {{ option.label }}
</a-select-option> </a-select-option>
</a-select> </a-select>
<!-- <a-range-picker v-model:value="formData[item.key]" value-format="YYYY-MM-DD" @change="$emit('onSearch', formData)" /> -->
</template> </template>
</searchBox> </searchBox>
</div> </div>
@@ -49,9 +53,11 @@
:table-info="tableList[activeKey].tableInfo" :table-info="tableList[activeKey].tableInfo"
:table-data="tableList[activeKey].tableData" :table-data="tableList[activeKey].tableData"
@pagesizeChange="pagesizeChange" @pagesizeChange="pagesizeChange"
></energyEchart> >
</energyEchart>
</a-spin> </a-spin>
</div> </div>
</div> </div>
</template> </template>
@@ -59,15 +65,14 @@
import energyEchart from '@/components/statisticalAnalysis/energyEchart.vue' import energyEchart from '@/components/statisticalAnalysis/energyEchart.vue'
import searchBox from '@/components/SearchBox.vue' import searchBox from '@/components/SearchBox.vue'
import { postReq, getReq } from '@/request/api' import { postReq, getReq } from '@/request/api'
import { getRunDays, getDateDaysAgo } from '@/utils/dealWithData' import {downloadByUrl} from '@/utils/download'
import { contentQuotesLinter } from 'ant-design-vue/es/_util/cssinjs/linters'
export default { export default {
name: 'StatisicalAnView', name: 'StatisicalAnView',
components: { energyEchart, searchBox }, components: { energyEchart, searchBox },
data() { data() {
return { return {
btnOptionList: [],
loading: { loading: {
chart: false, chart: false,
table: false table: false
@@ -493,6 +498,7 @@ export default {
}, },
async mounted() { async mounted() {
// 优先加载第一个页面(activeKey=1)所需的数据 // 优先加载第一个页面(activeKey=1)所需的数据
this.btnOptionList = this.$getBtns(['导出'])
await Promise.all([ await Promise.all([
this.getStationList(), this.getStationList(),
this.getEchartsListForActiveKey(), this.getEchartsListForActiveKey(),
@@ -506,6 +512,34 @@ export default {
clearInterval(this.interval) // 组件销毁时清除定时器 clearInterval(this.interval) // 组件销毁时清除定时器
}, },
methods: { methods: {
operateForm(type, record = {}) {
if (type == 'output') {
this.loading.chart=true
this.handleOutput()
}
},
async handleOutput() {
const { formData:{time=[]} } = this.$refs.searchBox
const params = {
station_id: this.stationId,
category: this.activeKey,
start_date: time[0] || '',
end_date: time[1] || ''
}
try {
const res = await getReq('/exportStatReport', params)
if(res.errcode==0){
window.open('/download/'+res.data,'_parent');
this.loading.chart=false
}
} catch (error) {
console.log(error)
}
},
forceRerender() { forceRerender() {
this.renderKey += 1 this.renderKey += 1
}, },
@@ -524,7 +558,8 @@ export default {
this.tableList[key].pageOption = { this.tableList[key].pageOption = {
page: 1, page: 1,
pageSize: 10, pageSize: 10,
count: 1 } count: 1
}
} }
}) })
}, },
@@ -667,21 +702,21 @@ export default {
padding: 20px; padding: 20px;
background: $bg1-color; background: $bg1-color;
border-radius: 15px; border-radius: 15px;
.tab-header { .header {
display: flex; display: flex;
.tab { .tab-item {
& > span { height: 38px;
line-height: 38px;
font-size: 14px; font-size: 14px;
margin-right: 15px; margin-right: 15px;
display: inline-block; display: inline-block;
padding: 10px 50px; padding: 0 50px;
cursor: pointer; cursor: pointer;
border: 1px solid $tab-border; border: 1px solid $tab-border;
border-radius: 4px; border-radius: 4px;
} }
} }
}
.actived { .actived {
color: #ffffff; color: #ffffff;
@@ -695,7 +730,7 @@ export default {
} }
.main_content { .main_content {
overflow: scroll; overflow: scroll;
height: calc(100% - 30px); height: calc(100% - 40px);
// margin-top: 10px; // margin-top: 10px;
} }
</style> </style>

View File

@@ -51,7 +51,7 @@ import { ExclamationCircleOutlined } from '@ant-design/icons-vue'
import { createVNode } from 'vue' import { createVNode } from 'vue'
import searchBox from '@/components/SearchBox.vue' import searchBox from '@/components/SearchBox.vue'
import { deviceTypeList } from '@/utils/config' import { deviceTypeList } from '@/utils/config.js'
export default { export default {
name: '', name: '',
components: { components: {
@@ -112,8 +112,9 @@ export default {
}, },
//获取设备类型 //获取设备类型
getType(type) { getType(type) {
const deviceType = this.deviceTypeList.find((item) => item.value == type).label || '' const device = this.deviceTypeList.find((item) => item.value == type) || null
return deviceType if (device) return device.label
return type
}, },
async getList() { async getList() {
this.$refs.comTable.loading = true this.$refs.comTable.loading = true
@@ -223,11 +224,11 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.device { .device {
height: 100%; height: 100%;
padding: 0 20px; padding: 20px;
.content-table { .content-table {
width: 100%; width: 100%;
height: calc(100% - 90px); height: calc(100% - 52px);
} }
} }
</style> </style>

View File

@@ -146,7 +146,6 @@ export default {
console.log(data) console.log(data)
}, },
operateForm(type, record = {}) { operateForm(type, record = {}) {
console.log(type, record)
this.formStatus = type this.formStatus = type
switch (type) { switch (type) {
@@ -232,10 +231,10 @@ export default {
.policy { .policy {
// width: 100%; // width: 100%;
height: 100%; height: 100%;
padding: 0 20px; padding: 20px;
.content-table { .content-table {
width: 100%; width: 100%;
height: calc(100% - 90px); height: calc(100% - 52px);
} }
} }
</style> </style>

View File

@@ -375,9 +375,9 @@ export default {
.role { .role {
height: 100%; height: 100%;
padding: 0 20px; padding: 20px;
.content-table { .content-table {
height: calc(100% - 92px); height: calc(100% - 52px);
} }
} }
</style> </style>

View File

@@ -243,9 +243,9 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.service { .service {
height: 100%; height: 100%;
padding: 0 20px; padding: 20px;
.content-table { .content-table {
height: calc(100% - 92px); height: calc(100% - 52px);
} }
} }
</style> </style>

View File

@@ -277,9 +277,9 @@ export default {
.station { .station {
height: 100%; height: 100%;
padding: 0 20px; padding: 20px;
.content-table { .content-table {
height: calc(100% - 92px); height: calc(100% - 52px);
} }
} }
</style> </style>

View File

@@ -211,9 +211,9 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.user { .user {
height: 100%; height: 100%;
padding: 0 20px; padding: 20px;
.content-table { .content-table {
height: calc(100% - 92px); height: calc(100% - 52px);
} }
} }
</style> </style>

View File

@@ -19,6 +19,14 @@ module.exports = defineConfig({
pathRewrite: { pathRewrite: {
'^/api': '' // 重写路径,去掉 '/api' 前缀 '^/api': '' // 重写路径,去掉 '/api' 前缀
} }
},
'/download': {
// 代理前缀,可以自定义(如 '/api'
target: 'http://192.168.0.187:19600', // 目标服务器地址
changeOrigin: true, // 是否改变请求源(跨域必备)
pathRewrite: {
'^/download': '/download' // 重写路径,去掉 '/api' 前缀
}
} }
} }
}, },