Browse Source

feat: project created

master
wally 4 years ago
commit
f8c235487a
  1. 8
      .editorconfig
  2. 1
      .env.development
  3. 1
      .env.production
  4. 1
      .env.test
  5. 9
      .eslintignore
  6. 56
      .eslintrc.js
  7. 8
      .gitignore
  8. 4
      .husky/pre-commit
  9. 1
      .npmrc
  10. 13
      .prettierrc
  11. 7
      README.md
  12. 18
      alias.config.js
  13. 1
      commitlint.config.js
  14. 13
      index.html
  15. 16189
      package-lock.json
  16. 74
      package.json
  17. BIN
      public/favicon.ico
  18. 44
      src/App.vue
  19. 15
      src/apis/index.js
  20. BIN
      src/assets/logo.png
  21. BIN
      src/assets/u1.jpg
  22. 56
      src/components/navbar.vue
  23. 192
      src/config/chart.js
  24. 164
      src/config/config.js
  25. 110
      src/config/log.js
  26. 19
      src/hooks/useDeviceCreate.js
  27. 11
      src/main.js
  28. 52
      src/routers/index.js
  29. 14
      src/store/index.js
  30. 61
      src/store/user.js
  31. 51
      src/utils/axios.js
  32. 128
      src/utils/overview.js
  33. 181
      src/utils/statistical.js
  34. 16
      src/utils/time.js
  35. 3
      src/views/Index.vue
  36. 114
      src/views/user/pw-change.vue
  37. 106
      src/views/user/sign-in.vue
  38. 39
      vite.config.js
  39. 5119
      yarn.lock

8
.editorconfig

@ -0,0 +1,8 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 140
root = true

1
.env.development

@ -0,0 +1 @@
VITE_API_URL=http://localhost:4001

1
.env.production

@ -0,0 +1 @@
VITE_API_URL=http://139.196.27.233:29001

1
.env.test

@ -0,0 +1 @@
VITE_API_URL=https://test.tall.wiki

9
.eslintignore

@ -0,0 +1,9 @@
node_modules
dist/
test
build/
babel.config.js
package.json
postcss.config.js
.eslintrc.js
vite.config.js

56
.eslintrc.js

@ -0,0 +1,56 @@
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: ['plugin:vue/essential', 'airbnb-base', 'plugin:prettier/recommended'],
parserOptions: {
ecmaVersion: 12,
sourceType: 'module',
},
plugins: ['vue'],
rules: {
'import/no-unresolved': 0,
'import/extensions': 0,
'import/no-extraneous-dependencies': 0,
'no-plusplus': 0,
'no-use-before-define': [
'error',
{
functions: false,
classes: true,
variables: true,
},
],
'consistent-return': 0,
'vue/html-self-closing': 'off',
'no-unused-expressions': 'off',
'vue/no-mutating-props': 'off',
'vue/no-multiple-template-root': 'off',
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-param-reassign': 'off',
'max-len': [
'error',
{
code: 140,
tabWidth: 2,
},
],
'object-curly-newline': ['error', { multiline: true }],
'arrow-parens': ['error', 'as-needed'],
'linebreak-style': 'off',
'vue/attributes-order': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/max-attributes-per-line': 'off',
'vue/multiline-html-element-content-newline': 'off',
'vue/html-indent': 'off',
'vue/html-closing-bracket-newline': [
'error',
{
singleline: 'never',
multiline: 'always',
},
],
},
};

8
.gitignore

@ -0,0 +1,8 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
.eslintcache
.idea
.vscode

4
.husky/pre-commit

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged

1
.npmrc

@ -0,0 +1 @@
registry=https://registry.npm.taobao.org

13
.prettierrc

@ -0,0 +1,13 @@
{
"printWidth": 140,
"singleQuote": true,
"semi": true,
"trailingComma": "all",
"arrowParens": "avoid",
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"jsxBracketSameLine": false,
"proseWrap": "always",
"endOfLine": "lf"
}

7
README.md

@ -0,0 +1,7 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar)

18
alias.config.js

@ -0,0 +1,18 @@
const path = require('path');
const resolve = dir => path.join(__dirname, dir);
module.exports = {
resolve: {
alias: {
'~': __dirname,
'@': resolve('src'),
views: resolve('src/views'),
components: resolve('src/components'),
assets: resolve('src/assets'),
utils: resolve('src/utils'),
store: resolve('src/store'),
apis: resolve('src/apis'),
},
},
};

1
commitlint.config.js

@ -0,0 +1 @@
module.exports = {extends: ['@commitlint/config-angular']};

13
index.html

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>时物链条插件商城</title></title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

16189
package-lock.json

File diff suppressed because it is too large

74
package.json

@ -0,0 +1,74 @@
{
"name": "vue3-vite-template",
"version": "0.0.0",
"scripts": {
"dev": "vite --mode test",
"dev:prod": "vite --mode production",
"dev-test": "vite --mode development",
"build:test": "vite build --mode test",
"build": "vite build --mode production",
"serve": "vite preview",
"cz": "git add . && git cz",
"format": "prettier --write ./src",
"lint": "eslint ./src --ext .vue,.js,.ts",
"fix": "eslint --fix ./src --ext .vue,.js,.ts",
"prepare": "husky install"
},
"dependencies": {
"@vitejs/plugin-vue": "^1.9.3",
"axios": "^0.23.0",
"dayjs": "^1.10.7",
"echarts": "^5.2.2",
"element-plus": "^1.1.0-beta.24",
"lodash": "^4.17.21",
"vite": "^2.6.4",
"vite-plugin-compression": "^0.3.5",
"vite-plugin-windicss": "^1.4.11",
"vue": "^3.2.16",
"vue-router": "^4.0.12",
"vuex": "^4.0.2",
"windicss": "^3.1.9"
},
"devDependencies": {
"@commitlint/cli": "^13.2.1",
"@commitlint/config-angular": "^13.2.0",
"commitizen": "^4.2.4",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^7.32.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-html": "^6.2.0",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^7.19.1",
"husky": "^7.0.2",
"lint-staged": "^11.2.3",
"prettier": "^2.4.1",
"unplugin-vue-components": "^0.15.6",
"vite-plugin-linter": "^1.0.1",
"vite-plugin-pwa": "^0.11.6"
},
"browserslist": [
"Android >= 4",
"ios >= 8"
],
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
},
"lint-staged": {
"src/**/*.{js,ts,jsx,json,css,vue}": [
"eslint --fix",
"git add"
],
"*.js": "eslint --cache --fix"
},
"husky": {
"hooks": {
"prepare-commit-msg": "exec < /dev/tty && git cz --hook || true",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
"pre-commit": "lint-staged"
}
}
}

BIN
public/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

44
src/App.vue

@ -0,0 +1,44 @@
<template>
<el-config-provider :locale="local">
<el-container>
<router-view></router-view>
</el-container>
</el-config-provider>
</template>
<script setup>
import { useStore } from 'vuex';
import { useRouter } from 'vue-router';
const store = useStore();
const router = useRouter();
const userString = sessionStorage.getItem('user');
if (userString) {
const user = JSON.parse(userString);
store.commit('user/setUser', user);
} else {
router.push({ name: 'signin' });
}
</script>
<style>
html,
body,
#app,
#app > section {
height: 100%;
}
.el-menu {
min-height: 100%;
}
.el-form--label-top .el-form-item__label {
padding-bottom: 0 !important;
}
.el-form-item {
margin-bottom: 10px !important;
}
</style>

15
src/apis/index.js

@ -0,0 +1,15 @@
// noinspection SpellCheckingInspection
import http from 'utils/axios';
const apiUrl = import.meta.env.VITE_API_URL;
const users = `${apiUrl}/gateway/tall3/v3.0/users`;
// 根据userId 获取token
export const getToken = userId => http.get(`${users}/userId`, { params: { userId } });
// 登录
export const signIn = params => http.post(`${users}/signin`, params);
// 修改密码
export const changePassword = params => http.post(`${users}/password/account`, params);

BIN
src/assets/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
src/assets/u1.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

56
src/components/navbar.vue

@ -0,0 +1,56 @@
<template>
<div class="flex items-center justify-between pr-5 shadow-sm">
<h1 class="text-lg font-medium py-3 px-6">
<i
v-if="menu.show"
:class="{ 'text-gray-800': !menu.collapse, 'text-gray-400': menu.collapse }"
class="el-icon-guide mr-2"
@click="toggleCollapse"
></i>
智能大气腐蚀监测平台{{ $route.meta.title }}
</h1>
<el-dropdown>
<span class="flex items-center">
<el-avatar class="mr-2" size="small" src="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png"></el-avatar>
<div class="mr-3">{{ account }}</div>
<el-icon class="el-icon--right">
<ArrowDown />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="openChangePassword">修改密码</el-dropdown-item>
<el-dropdown-item @click="signOut">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useStore } from 'vuex';
import { useRouter } from 'vue-router';
import { ArrowDown } from '@element-plus/icons';
const store = useStore();
const router = useRouter();
const toggleCollapse = () => {
console.log('Toggle Collapse');
store.commit('toggleCollapse');
};
const menu = computed(() => store.state.menu);
const account = computed(() => store.getters['user/account']);
//
function openChangePassword() {
router.push({ name: 'pw-change' });
}
// 退
function signOut() {
store.commit('user/setUser', null);
router.push({ name: 'signin' });
}
</script>

192
src/config/chart.js

@ -0,0 +1,192 @@
/* eslint-disable max-len */
export const colors = ['#EAB308', '#F97316', '#EC4899', '#F43F5E', '#D946EF', '#06B6D4', '#B45309', '#1E40AF', '#166534'];
export const itemColor = {
'钢腐蚀电流(nA)': colors[5],
'铜腐蚀电流(nA)': colors[6],
'铝腐蚀电流(nA)': colors[7],
'锌腐蚀电流(nA)': colors[8],
'SO2(ppb)': colors[0],
'盐分阻抗(Ω)': colors[1],
'盐分温度(℃)': colors[2],
'环境温度(℃)': colors[3],
'环境湿度(RH%)': colors[4],
};
export const legendData = [
{
name: 'SO2(ppb)',
itemStyle: { color: colors[0] },
listStyle: { color: colors[0] },
},
{
name: '盐分阻抗(Ω)',
itemStyle: { color: colors[1] },
listStyle: { color: colors[1] },
},
{
name: '盐分温度(℃)',
itemStyle: { color: colors[2] },
listStyle: { color: colors[2] },
},
{
name: '环境温度(℃)',
itemStyle: { color: colors[3] },
listStyle: { color: colors[3] },
},
{
name: '环境湿度(RH%)',
itemStyle: { color: colors[4] },
listStyle: { color: colors[4] },
},
{
name: '锌腐蚀电流(nA)',
itemStyle: { color: colors[5] },
listStyle: { color: colors[5] },
},
{
name: '铜腐蚀电流(nA)',
itemStyle: { color: colors[6] },
listStyle: { color: colors[6] },
},
{
name: '铝腐蚀电流(nA)',
itemStyle: { color: colors[7] },
listStyle: { color: colors[7] },
},
{
name: '钢腐蚀电流(nA)',
itemStyle: { color: colors[8] },
listStyle: { color: colors[8] },
},
];
// y轴定义
export const yAxisData = [
{
type: 'value',
name: '腐蚀电流(nA)',
offset: 0,
position: 'left',
axisLine: {
show: true,
lineStyle: { color: colors[7] },
},
axisLabel: { formatter: '{value}' },
axisPointer: { show: false },
},
{
type: 'value',
name: '温度(℃)',
offset: 0,
position: 'right',
axisLine: {
show: true,
lineStyle: { color: colors[3] },
},
axisLabel: { formatter: '{value}' },
axisPointer: { show: false },
},
{
type: 'value',
name: '湿度(RH%)',
offset: 70,
position: 'right',
axisLine: {
show: true,
lineStyle: { color: colors[4] },
},
axisLabel: { formatter: '{value}' },
axisPointer: { show: false },
},
{
type: 'value',
name: 'SO2(ppb)',
position: 'right',
show: false,
offset: 150,
axisLine: {
show: true,
lineStyle: { color: colors[0] },
},
axisLabel: { formatter: '{value}' },
axisPointer: { show: false },
},
{
type: 'value',
name: '盐分阻抗(Ω)',
show: false,
position: 'right',
offset: 220,
axisLine: {
show: true,
lineStyle: { color: colors[1] },
},
axisLabel: { formatter: '{value}' },
axisPointer: { show: false },
},
];
/**
* 生成默认数据
* @param {Object} data
* @returns {[{data: (number|[]|string|*), name: string, type: string},{data: (number|[]|BufferSource|string|*), name: string, type: string, yAxisIndex: number},{data: ([]|string|*), name: string, type: string, yAxisIndex: number},{data: ([]|string|*), name: string, type: string, yAxisIndex: number},{data: [], name: string, type: string, yAxisIndex: number},null,null,null]}
*/
export function generateDefaultSeries(data) {
return [
{
name: '锌腐蚀电流(nA)',
type: 'line',
yAxisIndex: 0,
data: data.corrosionXIN,
},
{
name: '铜腐蚀电流(nA)',
type: 'line',
yAxisIndex: 0,
data: data.corrosionTONG,
},
{
name: '铝腐蚀电流(nA)',
type: 'line',
yAxisIndex: 0,
data: data.corrosionLV,
},
{
name: '钢腐蚀电流(nA)',
type: 'line',
yAxisIndex: 0,
data: data.corrosionGANG,
},
{
name: '环境温度(℃)',
type: 'line',
yAxisIndex: 1,
data: data.environmentTemperature,
},
{
name: '盐分温度(℃)',
type: 'line',
yAxisIndex: 1,
data: data.saltT,
},
{
name: '环境湿度(RH%)',
type: 'line',
yAxisIndex: 2,
data: data.environmentHumidity,
},
{
name: 'SO2(ppb)',
type: 'line',
yAxisIndex: 3,
data: data.so2,
},
{
name: '盐分阻抗(Ω)',
type: 'line',
yAxisIndex: 4,
data: data.saltR,
},
];
}

164
src/config/config.js

@ -0,0 +1,164 @@
// 网络参数设置
export const networkConfig = {
ip1: '',
port1: '',
ip2: '',
port2: '',
ip3: '',
port3: '',
ipBackup: '',
portBackup: '',
account: '',
password: '',
apn: '',
status: '',
};
// 功能参数设置
export const functionConfig = {
frequency: {
so2: 0, // SO2采样频率
metal: 0, // 金属腐蚀采样频率
th: 0, // 温湿度 采样频率
salt: 0, // 盐雾 采样频率
}, // 采样频率
count: 0, // 采集个数
time: Date.now(), // 设置时间
batteryLow: 0, // 电池电压低阈值
batteryHigh: 0, // 电池电压高阈值
sunHigh: 0, // 太阳能电压高阈值
humidityHigh: 0, // 湿度高阈值
temperatureLow: 0, // 温度低阈值
temperatureHigh: 0, // 温度高阈值
securityMode: 'OPEN', // 安全模式 OPEN->不加密 ENCRYPTION->加密
report: {
type: 'CYCLE', // 上报周期类型 0->时间点 1->周期
timePoints: [''], // 设置时间点
cycle: 240, // 上报周期分钟数
},
status: '',
};
// 金属腐蚀类型
export const corrosiveTypes = [
{
value: 'XIN',
type: '锌',
},
{
value: 'LV',
type: '铝',
},
{
value: 'TONG',
type: '铜',
},
{
value: 'GANG',
type: '钢',
},
];
// 添加设备
export const deviceData = {
deviceId: '', // 设备id
deviceFullId: '', // 设备完整id
deviceDirection: '', // 设备朝向
area: '', // 地区
address: '', // 站点名称
contact: '', // 联系人
phone: '', // 联系人电话
lon: '', // 经度
lat: '', // 纬度
head: '', // 负责人
installLocation: '', // 安装位置
installTime: '', // 安装时间
runTime: '', // 正式运行时间
linkAddress: '', // 链路地址
probNo: '', // 探头编号
simple: '', // 试样
sim1: '', // sim卡1
protocolVersion: '', // 协议版本
joint: '', // 主站后台联调情况
operationRecord: '', // 维修记录
remark: '', // 备注
type: 'IACD', // 产品类型
};
// 设备添加编辑 规则
export const deviceRules = {
deviceId: [
{
required: true,
message: '请输入设备ID号',
trigger: 'blur',
},
{
len: 6,
message: '请输入6位设备ID号',
trigger: 'blur',
},
],
address: [
{
required: true,
message: '请输入站点名称',
trigger: 'blur',
},
],
};
// 下发类型
export const PEND_TYPE = {
PENDING: {
type: 'primary',
text: '待下发',
},
FAIL: {
type: 'danger',
text: '配置失败',
},
SUCCESS: {
type: 'success',
text: '配置成功',
},
};
// 上报类型
export const REPORT_TYPE = {
CYCLE: '周期上报',
POINT: '定时上报',
};
// 下发类型
export const PENDING_TYPE = {
DATA: { text: '业务上报' },
EVENT: { text: '事件上报' },
};
// 实时查询数据的轮询时间 1分钟
export const REALTIME_DATA_INTERVAL = 60000 * 5;
// 设备状态 情景
export const STATUS_COLOR = {
BROKEN: {
text: '故障',
color: '#FCA5A5',
},
WARNING: {
text: '报警',
color: '#FCD34D',
},
NORMAL: {
text: '正常',
color: '#BEF264',
},
OFFLINE: {
text: '离线',
color: '#CBD5E1',
},
ONLINE: {
text: '在线',
color: '#6EE7B7',
},
};

110
src/config/log.js

@ -0,0 +1,110 @@
// 功能码
export const CODE = {
BUSINESS_REPORT: {
text: '业务上报',
code: 0,
},
EVENT_REPORT: {
text: '事件上报',
code: 1,
},
NET_PARAM_SETTING_OR_READING: {
code: 2,
text: '联网参数设置或读取',
},
FUNC_PARAM_SETTING_OR_READING: {
code: 3,
text: '功能参数设置或读取',
},
READ_HISTORY_DATA: {
code: 4,
text: '读取历史日志记录',
},
READ_HISTORY_EVENT: {
code: 5,
text: '读取历史事件记录',
},
UPGRADE: {
code: 6,
text: '升级启动、传输、结束',
},
ACK: {
code: 7,
text: '应答帧(固定帧)',
},
OVER: {
code: 8,
text: '结束帧',
},
RESERVED: {
code: 9,
text: '保留',
},
};
// 传输方向
export const DIRECTION = {
IACD2SERVER: '上行',
SERVER2IACD: '下行',
};
// 传输启动
export const PRM = {
ACK: '传输结束',
REQ: '传输启动',
};
// 流控
export const DFC = {
CONTINUE: '有后续包',
OVER: '无后续包',
};
// 安全方式
export const SER = {
OPEN: '不加密',
ENCRYPTION: '加密',
};
// 类型标识
export const ASDU_TYPE = {
IACD: {
code: 1,
text: '大气腐蚀',
},
OTHER: {
code: 2,
text: '其他',
},
};
// 信息体序
export const INFO_ORDER = {
NoOrder: '无序',
HasOrder: '有序',
None: '无',
};
// 传输原因
export const REASON = {
None: {
code: 0,
text: '无',
},
ReportData: {
code: 1,
text: '业务主动上报',
},
ReportEvent: {
code: 2,
text: '事件主动上报',
},
History: {
code: 3,
text: '历史数据记录或程序数据',
},
HumanOperator: {
code: 4,
text: '人工上报',
},
};

19
src/hooks/useDeviceCreate.js

@ -0,0 +1,19 @@
import { ref } from 'vue';
export default function useDeviceCreate() {
const display = ref(false);
function hide() {
display.value = false;
}
function show() {
display.value = true;
}
return {
display,
hide,
show,
};
}

11
src/main.js

@ -0,0 +1,11 @@
import 'virtual:windi.css';
import 'element-plus/dist/index.css';
import { createApp } from 'vue';
import App from './App.vue';
import router from './routers/index';
import store from './store/index';
const app = createApp(App);
app.use(router).use(store).mount('#app');

52
src/routers/index.js

@ -0,0 +1,52 @@
import { createRouter, createWebHistory } from 'vue-router';
// 还有 createWebHashHistory 和 createMemoryHistory
export const routes = [
// {
// path: '/store/overview',
// name: 'overview',
// meta: {
// title: '设备概览',
// icon: 'el-icon-data-board',
// },
// component: () => import('@/views/overview.vue'),
// },
];
export const user = [
{
path: '/store/user/signin',
name: 'signin',
meta: {},
component: () => import('views/user/sign-in.vue'),
},
{
path: '/store/user/pw-change',
name: 'pw-change',
meta: {},
component: () => import('views/user/pw-change.vue'),
},
];
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
redirect: '/store/user/signin',
},
{
path: '/store',
redirect: '/store/user/signin',
},
{
path: '/store/home',
name: 'home',
component: () => import('views/Index.vue'),
children: routes,
},
...user,
],
});
export default router;

14
src/store/index.js

@ -0,0 +1,14 @@
import { createStore } from 'vuex';
import user from './user';
export default createStore({
modules: { user },
state: { menu: { show: true, collapse: false } },
getters: {},
mutations: {
toggleCollapse(state) {
state.menu.collapse = !state.menu.collapse;
},
},
actions: {},
});

61
src/store/user.js

@ -0,0 +1,61 @@
import { getToken } from 'apis/index';
export default {
namespaced: true,
state: { user: null },
getters: {
token({ user }) {
if (!user) return null;
return user.token;
},
userId({ user }) {
if (!user) return null;
return user.id;
},
account({ user }) {
if (!user) return null;
return user.account;
},
isAdmin({ user }) {
if (!user) return false;
return user.account === 'iacdadmin';
},
},
mutations: {
/**
* 设置state.user
* @param {*} state
* @param {object|null} user 用户信息
*/
setUser(state, user) {
state.user = user;
if (user) {
sessionStorage.setItem('token', user.token);
sessionStorage.setItem('user', JSON.stringify(user));
} else {
sessionStorage.removeItem('token');
sessionStorage.removeItem('user');
}
},
},
actions: {
/**
* 根据userId获取token级user信息
* @param {*} param0
* @param {string} userId 用户id
*/
async getTokenByUserId({ commit }, userId) {
try {
const data = await getToken(userId);
commit('setUser', data || null);
return data;
} catch (error) {
throw new Error(error);
}
},
},
};

51
src/utils/axios.js

@ -0,0 +1,51 @@
import Axios from 'axios';
import { ElMessage } from 'element-plus';
import store from 'store';
const baseUrl = '/gateway';
const instance = Axios.create({
baseUrl,
timeout: 20000,
});
// request
instance.interceptors.request.use(
config => {
const token = store.getters['user/token'] || sessionStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
error => {
return Promise.reject(error);
},
);
// response
instance.interceptors.response.use(
response => {
if (response.status !== 200 || !response.data) {
return Promise.reject(response.statusText);
}
const { code, data, msg } = response.data;
if (code === 200) {
return data;
}
return Promise.reject(msg);
},
error => {
if (error.response && error.response.data) {
const code = error.response.status;
const msg = error.response.data.message;
ElMessage.error(`Code: ${code}, Message: ${msg}`);
console.error(`[Axios Error]`, error.response);
} else {
// ElMessage.error(`${error}`);
}
return Promise.reject(error);
},
);
export default instance;

128
src/utils/overview.js

@ -0,0 +1,128 @@
import { STATUS_COLOR } from '@/config/config';
/**
* 生成设备概览 数量数据
* @param {object} count
* @param {number} count.online 在线数量
* @param {number} count.offline 离线数量
* @param {number} count.fault 故障数量
* @param {number} count.warning 报警数量
* @param {number} count.total 总设备数
* @returns {object} {}
*/
// eslint-disable-next-line import/prefer-default-export
export function generateChartOption(count) {
return {
tooltip: { trigger: 'item' },
title: {
text: '设备总数',
top: '45%',
left: '50%',
textAlign: 'center',
textVerticalAlign: 'middle',
textStyle: { fontSize: 22 },
subtext: count.total,
subtextStyle: { fontSize: 18 },
},
series: [
{
name: '设备统计',
type: 'pie',
radius: ['70%', '95%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2,
},
label: {
show: true,
position: 'inside',
fontSize: 17,
color: '#323232',
// formatter: '{b}\n{c}',
lineHeight: 40,
},
emphasis: {
label: {
show: true,
fontSize: 19,
fontWeight: 'bold',
},
},
labelLine: { show: false },
data: [
{
value: count.online,
name: STATUS_COLOR.ONLINE.text,
itemStyle: { color: STATUS_COLOR.ONLINE.color },
label: { show: count.online },
},
{
value: count.offline,
name: STATUS_COLOR.OFFLINE.text,
itemStyle: { color: STATUS_COLOR.OFFLINE.color },
label: { show: count.offline },
},
],
},
{
name: '设备统计',
type: 'pie',
radius: ['35%', '60%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2,
},
label: {
show: true,
position: 'inside',
fontSize: 14,
color: '#323232',
// formatter: '{b}\n{c}',
lineHeight: 40,
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 'bold',
},
},
labelLine: { show: false },
data: [
{
value: count.fault,
name: STATUS_COLOR.BROKEN.text,
itemStyle: { color: STATUS_COLOR.BROKEN.color },
label: { show: count.fault },
},
{
value: count.warning,
name: STATUS_COLOR.WARNING.text,
itemStyle: { color: STATUS_COLOR.WARNING.color },
label: { show: count.warning },
},
{
value: count.normal,
name: STATUS_COLOR.NORMAL.text,
itemStyle: { color: STATUS_COLOR.NORMAL.color },
label: { show: count.normal },
},
{
value: count.offline,
name: STATUS_COLOR.OFFLINE.text,
itemStyle: {
color: STATUS_COLOR.OFFLINE.color,
opacity: 0,
},
label: { show: count.offline },
},
],
},
],
};
}

181
src/utils/statistical.js

@ -0,0 +1,181 @@
/* eslint-disable max-len,object-curly-newline */
import dayjs from 'dayjs';
import max from 'lodash/max';
import isNaN from 'lodash/isNaN';
import { colors, generateDefaultSeries, itemColor, legendData, yAxisData } from '@/config/chart';
/**
* 生成chart所需参数
* @param {Object[]} data 服务端返回数据
* @param {string} data[].time 时间 ms
* @param {string} data[].so2 SO2
* @param {string} data[].saltR 盐阻
* @param {string} data[].saltT 盐温
* @param {string} data[].environmentTemperature 环温
* @param {string} data[].environmentHumidity 环湿
* @param {string} data[].corrosion1
* @param {string} data[].corrosion2
* @param {string} data[].corrosion3
* @param {string} data[].corrosion4
* @returns {{environmentTemperature: *[], corrosionXIN: *[], corrosionGANG: *[], corrosionTONG: *[], so2: *[], corrosionLV: *[], time: *[], saltT: *[], saltR: *[], environmentHumidity: *[]}}
*/
function generateParams(data) {
const result = {
time: [],
so2: [],
saltR: [],
saltT: [],
environmentTemperature: [],
environmentHumidity: [],
corrosionXIN: [],
corrosionTONG: [],
corrosionLV: [],
corrosionGANG: [],
};
data.forEach(item => {
result.time.push(dayjs(new Date(+item.time)).format('YY/MM/DD HH:mm'));
!isNaN(+item.so2) ? result.so2.push(+item.so2) : result.so2.push('');
!isNaN(+item.saltR) ? result.saltR.push(+item.saltR) : result.saltR.push('');
!isNaN(+item.saltT) ? result.saltT.push(+item.saltT) : result.saltT.push('');
!isNaN(+item.environmentTemperature)
? result.environmentTemperature.push(+item.environmentTemperature)
: result.environmentTemperature.push('');
!isNaN(+item.environmentHumidity) ? result.environmentHumidity.push(+item.environmentHumidity) : result.environmentHumidity.push('');
!isNaN(+item.corrosion1) ? result.corrosionXIN.push(+item.corrosion1) : result.corrosionXIN.push('');
!isNaN(+item.corrosion2) ? result.corrosionTONG.push(+item.corrosion2) : result.corrosionTONG.push('');
!isNaN(+item.corrosion3) ? result.corrosionLV.push(+item.corrosion3) : result.corrosionLV.push('');
!isNaN(+item.corrosion4) ? result.corrosionGANG.push(+item.corrosion4) : result.corrosionGANG.push('');
});
return result;
}
/**
* 计算Y轴的显示
* @param {string} yName Y轴的name
* @param {Object} selectedLegend legends
* @returns {boolean}
*/
export function computeYAxisShow(yName, selectedLegend) {
// eslint-disable-next-line guard-for-in,no-restricted-syntax
for (const key in selectedLegend) {
if (key.includes(yName) && selectedLegend[key]) {
return true;
}
}
return false;
}
/**
* 生产y轴内容
* @param {Object} selectedLegend
* @returns {({axisLabel: {formatter: string}, axisLine: {lineStyle: {color: string}, show: boolean}, name: string, position: string, type: string}|{axisLabel: {formatter: string}, offset: number, axisLine: {lineStyle: {color: string}, show: boolean}, name: string, position: string, type: string}|{axisLabel: {formatter: string}, offset: number, axisLine: {lineStyle: {color: string}, show: boolean}, name: string, position: string, type: string}|{axisLabel: {formatter: string}, offset: number, axisLine: {lineStyle: {color: string}, show: boolean}, name: string, position: string, type: string}|{axisLabel: {formatter: string}, axisLine: {lineStyle: {color: string}, show: boolean}, name: string, position: string, type: string})[]}
*/
export function generateYAxis(selectedLegend) {
let leftIndex = 0;
let rightIndex = 0;
yAxisData.forEach(item => {
item.show = computeYAxisShow(item.name, selectedLegend);
if (item.show) {
if (item.position === 'left') {
item.offset = 100 * leftIndex;
leftIndex += 1;
} else {
item.offset = 80 * rightIndex;
rightIndex += 1;
}
}
});
return yAxisData;
}
/**
* 生成series数据
* @param {Object} data
* @param {Object[]} yAxis
* @returns {({data: ([]|number|string|*), name: string, type: string}|{data: ([]|number|BufferSource|string|*), name: string, type: string, yAxisIndex: number}|{data: ([]|string|*), name: string, type: string, yAxisIndex: number}|{data: ([]|string|*), name: string, type: string, yAxisIndex: number}|{data: [], name: string, type: string, yAxisIndex: number})[]|*[]}
*/
function generateSeries(data, yAxis) {
const seriesArr = generateDefaultSeries(data);
const showArr = seriesArr.filter(item => yAxis.find(y => y.name === item.name));
const hideArr = seriesArr.filter(item => !yAxis.find(y => y.name === item.name));
const result = [...showArr, ...hideArr];
result.forEach(item => {
item.itemStyle = { color: itemColor[item.name] };
if (item.name.includes('电流')) {
item.yAxisIndex = 0;
} else if (item.name.includes('温度')) {
item.yAxisIndex = 1;
} else if (item.name.includes('湿度')) {
item.yAxisIndex = 2;
} else if (item.name.includes('SO2')) {
item.yAxisIndex = 3;
} else if (item.name.includes('阻抗')) {
item.yAxisIndex = 4;
}
});
return result || [];
}
/**
* 计算图表grid left right值
* @param {Object[]} yAxis
* @returns {{left: number, right: number}}
*/
function generateGrid(yAxis) {
const left = [];
const right = [];
yAxis.forEach(item => {
if (item.show) {
if (item.position === 'left') {
left.push(item.offset || 0);
} else {
right.push(item.offset || 0);
}
}
});
return {
left: max(left) + 100,
right: max(right) + 80,
};
}
/**
* 生成chart参数
* @param {Object[]} rawData 返回段返回的data数据
* @param {Object} selected 选中的legend
* @returns {{yAxis: ({axisLabel: {formatter: string}, axisLine: {lineStyle: {color: string}, show: boolean}, name: string, position: string, type: string}|{axisLabel: {formatter: string}, offset: number, axisLine: {lineStyle: {color: string}, show: boolean}, name: string, position: string, type: string})[], xAxis: [{data: *[], axisTick: {alignWithLabel: boolean}, type: string}], color: [string,string,string,string,string,string,string,string,string], grid: {left: number, right: number}, legend: {data: [string,string,string,string,string,string,string,string,string], type: string, selected}, series: (({data: ([]|number|string|*), name: string, type: string}|{data: ([]|number|BufferSource|string|*), name: string, type: string, yAxisIndex: number}|{data: ([]|string|*), name: string, type: string, yAxisIndex: number}|{data: [], name: string, type: string, yAxisIndex: number})[]|*[]), tooltip: {axisPointer: {type: string, snap: boolean}, trigger: string}, dataZoom: [{type: string},{type: string}]}}
*/
export function generateChartOption(rawData, selected) {
const data = generateParams(rawData);
const yAxis = generateYAxis(selected);
const series = generateSeries(data, yAxis);
const grid = generateGrid(yAxis);
const option = {
color: colors,
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
snap: true,
},
},
grid,
legend: {
type: 'scroll',
right: 140,
selected,
data: legendData,
},
dataZoom: [{ type: 'inside' }, { type: 'slider' }],
xAxis: [
{
type: 'category',
axisTick: { alignWithLabel: true },
data: data.time,
},
],
yAxis,
series,
};
return option;
}

16
src/utils/time.js

@ -0,0 +1,16 @@
import dayjs from 'dayjs';
/**
* 格式化时间
* @param {string | number} time 时间戳字符串或数字
* @param {string} formatStyle
* @returns
*/
export function formatMsTime(time, formatStyle = 'YYYY-MM-DD HH:mm:ss') {
return dayjs(new Date(+time)).format(formatStyle);
}
// 图表时间轴的时间格式化
export function formatChartTime(time) {
return formatMsTime(time, 'MM/DD HH:mm');
}

3
src/views/Index.vue

@ -0,0 +1,3 @@
<template>
index.vue
</template>

114
src/views/user/pw-change.vue

@ -0,0 +1,114 @@
<template>
<div class="wrap">
<div class="content border rounded-md">
<h1 class="text-2xl text-center font-bold mb-10 text-gray-700">智能大气腐蚀监测云平台</h1>
<el-form ref="passwordChangeFormRef" :model="passwordChangeForm" :rules="rules" label-width="70px" status-icon>
<el-form-item class="mb-3" label="账号" prop="account" style="margin-bottom: 22px !important">
<el-input v-model="passwordChangeForm.account" type="text"></el-input>
</el-form-item>
<el-form-item label="旧密码" prop="passwordOld" style="margin-bottom: 22px !important">
<el-input v-model="passwordChangeForm.passwordOld" type="password"></el-input>
</el-form-item>
<el-form-item label="新密码" prop="passwordNew" style="margin-bottom: 22px !important">
<el-input v-model="passwordChangeForm.passwordNew" type="password"></el-input>
</el-form-item>
<el-form-item>
<el-button class="float-left" style="display: block; margin-bottom: 22px !important" type="primary" @click="onSubmit">
确认修改
</el-button>
<el-button class="float-right" type="text">
<router-link to="signin">返回登录</router-link>
</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { changePassword } from 'apis';
import { ElMessage } from 'element-plus';
import { useStore } from 'vuex';
import { useRouter } from 'vue-router';
const store = useStore();
const router = useRouter();
const passwordChangeFormRef = ref(null);
const passwordChangeForm = reactive({
account: '',
passwordOld: '',
passwordNew: '',
});
const rules = reactive({
account: [
{
required: true,
message: '请输入账号',
trigger: 'blur',
},
],
passwordOld: [
{
required: true,
message: '请输入旧密码',
trigger: 'blur',
},
],
passwordNew: [
{
required: true,
message: '请输入新密码',
trigger: 'blur',
},
],
});
//
function onSubmit() {
passwordChangeFormRef.value.validate(async valid => {
if (valid) {
try {
const params = { ...passwordChangeForm };
const resData = await changePassword(params);
store.commit('user/setUser', resData);
ElMessage.success('修改成功');
setTimeout(() => {
//
router.push({ name: 'signin' });
}, 1000);
} catch (error) {
ElMessage.error(error);
}
}
});
}
</script>
<style scoped>
.wrap {
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
background: url('@/assets/u1.jpg') no-repeat center center;
background-size: cover;
}
.content {
width: 480px;
padding: 30px;
background: #fff;
}
.content :deep(.el-form-item) {
display: flex;
margin-bottom: 22px !important;
}
</style>

106
src/views/user/sign-in.vue

@ -0,0 +1,106 @@
<template>
<div class="wrap">
<div class="content border rounded-md">
<h1 class="text-2xl text-center font-bold mb-10 text-gray-700">时物链条插件商城</h1>
<el-form ref="signInFormRef" :model="signInForm" :rules="rules" label-width="60px" status-icon>
<el-form-item class="mb-3" label="账号" prop="username" style="margin-bottom: 22px !important">
<el-input v-model="signInForm.username" type="text"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password" style="margin-bottom: 22px !important">
<el-input v-model="signInForm.password" type="password"></el-input>
</el-form-item>
<el-form-item>
<el-button class="float-left" style="display: block; margin-bottom: 22px !important" type="primary" @click="onSubmit">
立即登录
</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { signIn } from 'apis';
import { ElMessage } from 'element-plus';
import { useStore } from 'vuex';
import { useRouter } from 'vue-router';
const store = useStore();
const router = useRouter();
const signInFormRef = ref(null);
const signInForm = reactive({
username: '',
password: '',
});
const rules = reactive({
username: [
{
required: true,
message: '请输入账号',
trigger: 'blur',
},
],
password: [
{
required: true,
message: '请输入密码',
trigger: 'blur',
},
],
});
//
function onSubmit() {
signInFormRef.value.validate(async valid => {
if (valid) {
try {
const params = {
client: 1,
data: {
identifier: signInForm.username,
credential: signInForm.password,
},
type: 3,
};
const resData = await signIn(params);
store.commit('user/setUser', resData);
ElMessage.success('登录成功, 欢迎回来');
setTimeout(() => {
//
router.push({ name: 'home' });
}, 1000);
} catch (error) {
ElMessage.error(error);
}
}
});
}
</script>
<style scoped>
.wrap {
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
background: url('@/assets/u1.jpg') no-repeat center center;
background-size: cover;
}
.content {
width: 480px;
padding: 30px;
background: #fff;
}
.content :deep(.el-form-item) {
display: flex;
margin-bottom: 22px !important;
}
</style>

39
vite.config.js

@ -0,0 +1,39 @@
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
import { VitePWA } from 'vite-plugin-pwa';
import WindiCSS from 'vite-plugin-windicss';
import { defineConfig } from 'vite';
import path from 'path';
import viteCompression from 'vite-plugin-compression';
import vue from '@vitejs/plugin-vue';
const resolve = dir => path.join(__dirname, dir);
// https://vitejs.dev/config/
export default defineConfig({
base: '/store/',
plugins: [vue(), VitePWA(), WindiCSS(), Components({ resolvers: [ElementPlusResolver()] }), viteCompression()],
resolve: {
alias: {
'~': __dirname,
'@': resolve('src'),
views: resolve('src/views'),
components: resolve('src/components'),
assets: resolve('src/assets'),
utils: resolve('src/utils'),
store: resolve('src/store'),
apis: resolve('src/apis'),
},
},
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
return id.toString().split('node_modules/')[1].split('/')[0].toString();
}
},
},
},
},
});

5119
yarn.lock

File diff suppressed because it is too large
Loading…
Cancel
Save