Browse Source

基本框架

master
deartibers 2 years ago
commit
6eeb67fe2a
  1. 141
      .gitignore
  2. 2
      README.md
  3. 87
      api/.vscode/launch.json
  4. 36
      api/Dockerfile
  5. 3
      api/app/index.js
  6. 119
      api/app/lib/controllers/auth/index.js
  7. 79
      api/app/lib/index.js
  8. 83
      api/app/lib/middlewares/api-log.js
  9. 162
      api/app/lib/middlewares/authenticator.js
  10. 14
      api/app/lib/middlewares/duration-calc.js
  11. 115
      api/app/lib/models/user.js
  12. 11
      api/app/lib/routes/auth/index.js
  13. 17
      api/app/lib/routes/index.js
  14. 36
      api/app/lib/schedule/index.js
  15. 36
      api/app/lib/service/clickHouseClient.js
  16. 20
      api/app/lib/service/kafka.js
  17. 23
      api/app/lib/service/mqttServer.js
  18. 67
      api/app/lib/service/paasRequest.js
  19. 41
      api/app/lib/service/redis.js
  20. 40
      api/app/lib/service/socket.js
  21. 173
      api/app/lib/utils/dataRange.js
  22. 17
      api/app/lib/utils/index.js
  23. 21
      api/app/lib/utils/kafkaSend.js
  24. 30
      api/app/lib/utils/vcmpAuth.js
  25. 82
      api/app/lib/utils/xlsxDownload.js
  26. 298
      api/config.js
  27. 43
      api/package.json
  28. 35
      api/sequelize-automate.config.js
  29. 12
      api/server.js
  30. 19
      jenkinsfile_poms_api
  31. 19
      jenkinsfile_poms_web
  32. 17
      web/.babelrc
  33. 5
      web/.vscode/extensions.json
  34. 18
      web/.vscode/launch.json
  35. 4
      web/.vscode/settings.json
  36. 40
      web/Dockerfile
  37. BIN
      web/client/assets/fonts/DINExp.ttf
  38. BIN
      web/client/assets/fonts/YouSheBiaoTiHei-2.ttf
  39. BIN
      web/client/assets/images/background/console.png
  40. BIN
      web/client/assets/images/background/header.png
  41. BIN
      web/client/assets/images/background/loginBackground.gif
  42. BIN
      web/client/assets/images/background/loginbg.png
  43. BIN
      web/client/assets/images/background/logo.png
  44. BIN
      web/client/assets/images/background/notice.png
  45. BIN
      web/client/assets/images/background/topchoose.png
  46. BIN
      web/client/assets/images/background/user_login.png
  47. BIN
      web/client/assets/images/background/username.png
  48. BIN
      web/client/assets/images/background/videoPlayBg.png
  49. BIN
      web/client/assets/images/background/xiangqi.png
  50. BIN
      web/client/assets/images/console/suo.png
  51. BIN
      web/client/assets/images/console/yonghu.png
  52. BIN
      web/client/assets/images/favicon.ico
  53. 1
      web/client/assets/js/jessibuca/decoder.js
  54. BIN
      web/client/assets/js/jessibuca/decoder.wasm
  55. 1
      web/client/assets/js/jessibuca/jessibuca.js
  56. BIN
      web/client/assets/video/cross_loading.mp4
  57. BIN
      web/client/assets/video/login_bg.mp4
  58. 21
      web/client/index.ejs
  59. 54
      web/client/index.html
  60. 20
      web/client/index.js
  61. 39
      web/client/src/app.jsx
  62. 14
      web/client/src/components/index.js
  63. 40
      web/client/src/components/outHidden.jsx
  64. 50
      web/client/src/components/reminderBox.jsx
  65. 124
      web/client/src/components/setup.jsx
  66. 40
      web/client/src/components/simpleFileDownButton.jsx
  67. 18
      web/client/src/components/skeletonScreen.jsx
  68. 80
      web/client/src/components/textScroll.jsx
  69. 19
      web/client/src/components/textScroll.less
  70. 9
      web/client/src/index.jsx
  71. 78
      web/client/src/index.less
  72. 46
      web/client/src/layout/actions/global.js
  73. 9
      web/client/src/layout/actions/index.js
  74. 41
      web/client/src/layout/actions/webSocket.js
  75. 15
      web/client/src/layout/components/footer/index.jsx
  76. 165
      web/client/src/layout/components/header/index.jsx
  77. 18
      web/client/src/layout/components/header/index.less
  78. 86
      web/client/src/layout/components/sider/index.jsx
  79. 40
      web/client/src/layout/components/sider/index.less
  80. 6
      web/client/src/layout/containers/index.js
  81. 321
      web/client/src/layout/containers/layout/index.jsx
  82. 18
      web/client/src/layout/containers/no-match/index.jsx
  83. 188
      web/client/src/layout/index.jsx
  84. 28
      web/client/src/layout/reducers/ajaxResponse.js
  85. 41
      web/client/src/layout/reducers/global.js
  86. 17
      web/client/src/layout/reducers/index.js
  87. 21
      web/client/src/layout/reducers/webSocket.js
  88. 13
      web/client/src/layout/store/index.js
  89. 30
      web/client/src/layout/store/store.dev.js
  90. 20
      web/client/src/layout/store/store.prod.js
  91. 7
      web/client/src/public-path.js
  92. 83
      web/client/src/sections/auth/actions/auth.js
  93. 7
      web/client/src/sections/auth/actions/index.js
  94. 4
      web/client/src/sections/auth/containers/index.js
  95. 143
      web/client/src/sections/auth/containers/login.jsx
  96. 12
      web/client/src/sections/auth/index.js
  97. 40
      web/client/src/sections/auth/reducers/auth.js
  98. 6
      web/client/src/sections/auth/reducers/index.js
  99. 12
      web/client/src/sections/auth/routes.js
  100. 7
      web/client/src/sections/auth/style.less

141
.gitignore

@ -0,0 +1,141 @@
# ---> Go
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# ---> Node
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
.env.production
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
*yarn.lock
*package-lock.json
*log/
*downloadFiles/

2
README.md

@ -0,0 +1,2 @@
# FS-IOT

87
api/.vscode/launch.json

@ -0,0 +1,87 @@
{
// 使 IntelliSense
//
// 访: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "启动 API",
"program": "${workspaceRoot}/server.js",
"env": {
"NODE_ENV": "development"
},
"args": [
"-p 4600",
"-f http://localhost:4600",
//
// "-g postgres://postgres:123@10.8.30.32:5432/orational_service",
//
"-g postgres://FashionAdmin:123456@10.8.30.156:5432/POMS",
"-k node35:6667,node36:6667,node37:6667",
"--iotaProxy http://10.8.30.157:17007",
"--redisHost 10.8.30.112",
"--redisPort 6379",
"--axyApiUrl http://127.0.0.1:4100",
// "--apiEmisUrl http://10.8.30.112:14000",
//
"--apiEmisUrl http://10.8.30.161:1111",
"--apiVcmpUrl http://localhost:4000",
"--apiIotAuth http://localhost:4200",
"--godUrl https://restapi.amap.com/v3",
"--godKey 21c2d970e1646bb9a795900dd00093ce",
"--mqttVideoServer mqtt://10.8.30.71:30883",
"--qnak XuDgkao6cL0HidoMAPnA5OB10Mc_Ew08mpIfRJK5",
"--qnsk yewcieZLzKZuDfig0wLZ9if9jKp2P_1jd3CMJPSa",
"--qnbkt dev-operational-service",
// "--qndmn http://resources.anxinyun.cn",
"--qndmn http://rhvqdivo5.hn-bkt.clouddn.com",
// click
// "--clickHouseUrl http://10.8.30.71",
// click
"--clickHouseUrl http://10.8.30.161",
// "--clickHouseUrl https://clickhouse01.anxinyun.cn/play",
"--clickHousePort 30123",
// * 2
// "--clickHouseUser ",
// "--clickHousePassword ",
//
// "--clickHouseAnxincloud anxinyun",
// "--clickHousePepEmis pepca",
// "--clickHouseProjectManage peppm",
// "--clickHouseVcmp video_accrss1",
// "--clickHouseDataAlarm default",
//
"--clickHouseAnxincloud Anxinyun13",
"--clickHousePepEmis pepca8",
"--clickHouseProjectManage peppm8",
"--clickHouseVcmp video_access_dev",
"--clickHouseDataAlarm default",
"--confirmAlarmAnxinUserId 1",
"--vcmpAppId 5048b08d-c449-4d7f-b1ec-f741012aefe8",
"--vcmpAppSecret 5ba8c0ab-9fbd-4f07-9817-c48017c3cbad",
]
},
{
"type": "node",
"request": "launch",
"name": "run mocha",
"program": "${workspaceRoot}/node_modules/mocha/bin/_mocha",
"stopOnEntry": false,
"args": [
"app/test/*.test.js",
"--no-timeouts"
],
"cwd": "${workspaceRoot}",
"runtimeExecutable": null,
"env": {
"NODE_ENV": "development"
}
}
]
}

36
api/Dockerfile

@ -0,0 +1,36 @@
# FROM repository.anxinyun.cn/devops/node:12-dev as builder
# COPY . /var/app
# WORKDIR /var/app
# EXPOSE 8080
# RUN npm config set registry=http://10.8.30.22:7000
# RUN echo "{\"time\":\"$BUILD_TIMESTAMP\",\"build\": \"$BUILD_NUMBER\",\"revision\": \"$SVN_REVISION_1\",\"URL\":\"$SVN_URL_1\"}" > version.json
# RUN npm cache clean -f
# RUN rm -rf package-lock.json
# RUN npm install --registry http://10.8.30.22:7000
# FROM registry.cn-hangzhou.aliyuncs.com/fs-devops/node:12
# COPY --from=builder --chown=node /var/app /home/node/app
# WORKDIR /home/node/app
# CMD ["node", "server.js"]
# 旧版本构建方式
FROM repository.anxinyun.cn/base-images/nodejs12:20.10.12.2
COPY . /var/app
WORKDIR /var/app
EXPOSE 8080
CMD ["-u", "http://localhost:8088"]
ENTRYPOINT [ "node", "server.js" ]

3
api/app/index.js

@ -0,0 +1,3 @@
'use strict';
module.exports = require('./lib');

119
api/app/lib/controllers/auth/index.js

@ -0,0 +1,119 @@
'use strict';
const Hex = require('crypto-js/enc-hex');
const MD5 = require('crypto-js/md5');
const moment = require('moment');
const uuid = require('uuid');
async function login (ctx, next) {
// const transaction = await ctx.fs.dc.orm.transaction();
try {
const models = ctx.fs.dc.models;
const params = ctx.request.body;
const emisLoginRes = await ctx.app.fs.emisRequest.post('login', {
data: params
})
if (!emisLoginRes) {
throw "无此用户,请使用正确的登录信息"
} else {
const pomsRegisterRes = await models.User.findOne({
where: {
pepUserId: emisLoginRes.id,
$or: {
deleted: false,
role: { $contains: ['admin'] }
}
}
})
if (!pomsRegisterRes) {
throw '你还不是飞尚运维中台成员,请联系管理员添加权限'
} else if (
pomsRegisterRes.disabled && !pomsRegisterRes.role.includes('admin')
) {
throw '当前账号已禁用'
}
emisLoginRes.authorized = true
emisLoginRes.expired = moment().add(1, 'day')
emisLoginRes.pomsUserInfo = pomsRegisterRes.dataValues
let userUpdateData = {
lastInTime: moment().format(),
inTimes: pomsRegisterRes.inTimes + 1,
lastInAddress: ''
}
try {
// 获取ip转为地点并记录
let ip =
// '117.90.39.49' ||
ctx.ip
console.log(`当前登录用户IP:${ip}`);
if (ip && /^(1\d{2}|2[0-4]\d|25[0-5]|[1-9]\d|[1-9])\.(1\d{2}|2[0-4]\d|25[0-5]|[1-9]\d|\d)\.(1\d{2}|2[0-4]\d|25[0-5]|[1-9]\d|\d)\.(1\d{2}|2[0-4]\d|25[0-5]|[1-9]\d|\d)$/.test(ip)) {
const ipLocationRes = await ctx.app.fs.godRequest.post('ip', {
query: {
ip,
}
})
if (ipLocationRes) {
userUpdateData.lastInAddress = ipLocationRes.province + ipLocationRes.city
}
}
} catch (error) {
ctx.fs.logger.error(`IP GET, error: ${error}`);
}
await models.User.update(userUpdateData, {
where: {
id: emisLoginRes.id
}
})
await ctx.redis.hmset(emisLoginRes.token, {
expired: moment().add(1, 'day'),
userInfo:JSON.stringify(emisLoginRes)
});
ctx.status = 200;
ctx.body = emisLoginRes;
}
// await transaction.commit();
} catch (error) {
// await transaction.rollback();
ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`);
ctx.status = 400;
let message = typeof error == 'string' ? error
: error.response.body.message || "登录失败"
if (message == '账号或密码错误') {
message = '无此用户,请使用正确的登录信息'
}
ctx.body = {
message: message
}
}
}
async function logout (ctx) {
try {
const models = ctx.fs.dc.models;
const params = ctx.request.body;
await ctx.app.fs.emisRequest.put('logout', {
data: params
})
await ctx.redisTools.hdelall(token);
ctx.status = 204;
} catch (error) {
ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`);
ctx.status = 400;
ctx.body = {
}
}
}
module.exports = {
login,
logout,
};

79
api/app/lib/index.js

@ -0,0 +1,79 @@
'use strict';
const fs = require('fs');
const path = require('path');
const utils = require('./utils')
const routes = require('./routes');
const redisConnect = require('./service/redis')
const socketConect = require('./service/socket')
const mqttVideoServer = require('./service/mqttServer')
const paasRequest = require('./service/paasRequest');
const authenticator = require('./middlewares/authenticator');
const clickHouseClient = require('./service/clickHouseClient')
const kafka = require('./service/kafka')
const schedule = require('./schedule')
// const apiLog = require('./middlewares/api-log');
module.exports.entry = function (app, router, opts) {
app.fs.logger.log('info', '[FS-AUTH]', 'Inject auth and api mv into router.');
app.fs.api = app.fs.api || {};
app.fs.utils = app.fs.utils || {};
app.fs.api.authAttr = app.fs.api.authAttr || {};
app.fs.api.logAttr = app.fs.api.logAttr || {};
// 顺序固定 ↓
redisConnect(app, opts)
socketConect(app, opts)
mqttVideoServer(app, opts)
// 实例其他平台请求方法
paasRequest(app, opts)
kafka(app, opts)
// clickHouse 数据库 client
clickHouseClient(app, opts)
// 工具类函数
utils(app, opts)
// 定时任务
schedule(app, opts)
// 鉴权中间件
router.use(authenticator(app, opts));
// 日志记录
// router.use(apiLog(app, opts));
router = routes(app, router, opts);
};
module.exports.models = function (dc) { // dc = { orm: Sequelize对象, ORM: Sequelize, models: {} }
// 模型关系摘出来 初始化之后再定义关系才行
fs.readdirSync(path.join(__dirname, '/models')).forEach((filename) => {
require(`./models/${filename}`)(dc)
});
// const {
// AppInspection, ProjectApp, ProjectCorrelation, AppAlarm, App
// } = dc.models;
// AppInspection.belongsTo(App, { foreignKey: 'projectAppId', targetKey: 'id' });
// App.hasMany(AppInspection, { foreignKey: 'projectAppId', sourceKey: 'id' });
// ProjectApp.belongsTo(ProjectCorrelation, { foreignKey: 'projectId', targetKey: 'id' });
// ProjectCorrelation.hasMany(ProjectApp, { foreignKey: 'projectId', sourceKey: 'id' });
// ProjectApp.belongsTo(App, { foreignKey: 'appId', targetKey: 'id' });
// App.hasMany(ProjectApp, { foreignKey: 'appId', sourceKey: 'id' });
// ProjectCorrelation.belongsToMany(App, { through: ProjectApp, foreignKey: 'projectId', otherKey: 'appId' });
// App.belongsToMany(ProjectCorrelation, { through: ProjectApp, foreignKey: 'appId', otherKey: 'projectId' });
// AppAlarm.belongsTo(App, { foreignKey: 'projectAppId', targetKey: 'id' });
// App.hasMany(AppAlarm, { foreignKey: 'projectAppId', sourceKey: 'id' });
};

83
api/app/lib/middlewares/api-log.js

@ -0,0 +1,83 @@
/**
* Created by PengPeng on 2017/4/26.
*/
'use strict';
const moment = require('moment');
const pathToRegexp = require('path-to-regexp');
function factory(app, opts) {
async function sendToEsAsync(producer, payloads) {
return new Promise((resolve, reject) => {
producer.send(payloads, function (err) {
if (err) {
reject(err);
} else {
resolve();
}
});
})
}
async function logger(ctx, next) {
const { path, method } = ctx;
const start = Date.now();
// 等待路由处理
await next();
try {
let logAttr = null;
for (let prop in app.fs.api.logAttr) {
let keys = [];
let re = pathToRegexp(prop.replace(/\:[A-Za-z_\-]+\b/g, '(\\d+)'), keys);
if (re.test(`${method}${path}`)) {
logAttr = app.fs.api.logAttr[prop];
break;
}
}
let parameter = null, parameterShow = null, user_id, _token, app_key;
if (ctx.fs.api) {
const { actionParameter, actionParameterShow, userId, token, appKey } = ctx.fs.api;
parameter = actionParameter;
parameterShow = actionParameterShow;
user_id = userId;
_token = token;
app_key = appKey;
}
const producer = ctx.fs.kafka.producer;
const message = {
log_time: moment().toISOString(),
method: method,
content: logAttr ? logAttr.content : '',
parameter: JSON.stringify(parameter) || JSON.stringify(ctx.request.body),
parameter_show: parameterShow,
visible: logAttr ? logAttr.visible : true,
cost: Date.now() - start,
status_code: ctx.status,
url: ctx.request.url,
user_agent: ctx.request.headers["user-agent"],
user_id: user_id,
session: _token,
app_key: app_key,
header: JSON.stringify(ctx.request.headers),
ip: ctx.request.headers["x-real-ip"] || ctx.ip
};
const payloads = [{
topic: `${opts.kafka.topicPrefix}`,
messages: [JSON.stringify(message)],
partition: 0
}];
// await sendToEsAsync(producer, payloads);
} catch (e) {
ctx.fs.logger.error(`日志记录失败: ${e}`);
}
}
return logger;
}
module.exports = factory;

162
api/app/lib/middlewares/authenticator.js

@ -0,0 +1,162 @@
/**
* Created by PengLing on 2017/3/27.
*/
'use strict';
const pathToRegexp = require('path-to-regexp');
const util = require('util');
const moment = require('moment');
class ExcludesUrls {
constructor(opts) {
this.allUrls = undefined;
this.reload(opts);
}
sanitizePath (path) {
if (!path) return '/';
const p = '/' + path.replace(/^\/+/i, '').replace(/\/+$/, '').replace(/\/{2,}/, '/');
return p;
}
reload (opts) {
// load all url
if (!this.allUrls) {
this.allUrls = opts;
let that = this;
this.allUrls.forEach(function (url, i, arr) {
if (typeof url === "string") {
url = { p: url, o: '*' };
arr[i] = url;
}
const keys = [];
let eachPath = url.p;
url.p = (!eachPath || eachPath === '(.*)' || util.isRegExp(eachPath)) ? eachPath : that.sanitizePath(eachPath);
url.pregexp = pathToRegexp(eachPath, keys);
});
}
}
isExcluded (path, method) {
return this.allUrls.some(function (url) {
return !url.auth
&& url.pregexp.test(path)
&& (url.o === '*' || url.o.indexOf(method) !== -1);
});
}
}
/**
* 判断Url是否不鉴权
* @param {*} opts {exclude: [*] or []}'*'['*']:跳过所有路由; []:所有路由都要验证
* @param {*} path 当前request的path
* @param {*} method 当前request的method
*/
let isPathExcluded = function (opts, path, method) {
let excludeAll = Boolean(opts.exclude && opts.exclude.length && opts.exclude[0] == '*');
let excludes = null;
if (!excludeAll) {
let excludeOpts = opts.exclude || [];
excludeOpts.push({ p: '/login', o: 'POST' });
excludeOpts.push({ p: '/logout', o: 'PUT' });
excludes = new ExcludesUrls(excludeOpts);
}
let excluded = excludeAll || excludes.isExcluded(path, method);
return excluded;
};
let authorizeToken = async function (ctx, token) {
let startTime = moment()
let rslt = null;
const tokenFormatRegexp = /^(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$/g;
if (token && tokenFormatRegexp.test(token)) {
try {
const expired = await ctx.redis.hget(token, 'expired');
// const authorizeRes = await ctx.app.fs.emisRequest.get('authorize', {
// query: { token }
// })
// const { userInfo, expired } = authorizeRes;
// TODO 从项企 clickhouse 数据库中查 token 并更新
if (expired && moment().valueOf() <= moment(expired).valueOf()) {
const userInfo = JSON.parse(await ctx.redis.hmget(token, 'userInfo'));
const { pomsUserInfo: pomsUser } = userInfo
// const pomsUser = await ctx.app.fs.dc.models.User.findOne({
// where: {
// pepUserId: userInfo.id
// }
// }) || {}
rslt = {
'authorized': userInfo.authorized,
'resources': (userInfo || {}).resources || [],
};
ctx.fs.api.userId = pomsUser.id;
ctx.fs.api.userInfo = pomsUser;
ctx.fs.api.pepUserId = userInfo.id;
ctx.fs.api.pepUserInfo = userInfo;
ctx.fs.api.token = token;
}
} catch (err) {
const { error } = err.response || {};
ctx.fs.logger.log('[anxinyun]', '[AUTH] failed', (error || {}).message || `cannot GET /users/${token}`);
}
}
return rslt;
};
let isResourceAvailable = function (resources, options) {
let authCode = null;
// authorize user by authorization attribute
const { authAttr, method, path } = options;
for (let prop in authAttr) {
let keys = [];
let re = pathToRegexp(prop.replace(/\:[A-Za-z_\-]+\b/g, '(\\d+)'), keys);
if (re.test(`${method}${path}`)) {
authCode = authAttr[prop];
break;
}
}
return !authCode || (resources || []).some(code => code === authCode);
};
function factory (app, opts) {
return async function auth (ctx, next) {
const { path, method, header, query } = ctx;
ctx.fs.logger.log('[AUTH] start', path, method);
ctx.fs.api = ctx.fs.api || {};
ctx.fs.port = opts.port;
ctx.redis = app.redis;
ctx.redisTools = app.redisTools;
let error = null;
if (path) {
if (!isPathExcluded(opts, path, method)) {
const user = await authorizeToken(ctx, header.token || query.token);
if (user && user.authorized) {
// if (!isResourceAvailable(user.resources, { authAttr: app.fs.auth.authAttr, path, method })) {
// error = { status: 403, name: 'Forbidden' }
// } else {
// error = { status: 401, name: 'Unauthorized' }
// }
} else {
error = { status: 401, name: 'Unauthorized' }
}
}
} else {
error = { status: 401, name: 'Unauthorized' };
}
if (error) {
ctx.fs.logger.log('[AUTH] failed', path, method);
ctx.status = error.status;
ctx.body = error.name;
} else {
ctx.fs.logger.log('[AUTH] passed', path, method);
await next();
}
}
}
module.exports = factory;

14
api/app/lib/middlewares/duration-calc.js

@ -0,0 +1,14 @@
'use strict';
const moment = require('moment');
async function factory (ctx, next) {
try {
const start = moment()
await next()
ctx.fs.logger.log(`DurationCalc: ${ctx.path} 用时 ${moment().diff(start, 'milliseconds')}`);
} catch (error) {
ctx.fs.logger.error(`DurationCalc, error: ${error}`);
}
}
module.exports = factory;

115
api/app/lib/models/user.js

@ -0,0 +1,115 @@
/* eslint-disable*/
'use strict';
module.exports = dc => {
const DataTypes = dc.ORM;
const sequelize = dc.orm;
const User = sequelize.define("user", {
id: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: null,
comment: null,
primaryKey: true,
field: "id",
autoIncrement: true,
unique: "user_id_uindex"
},
pepUserId: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: null,
comment: "项企对应用户id",
primaryKey: false,
field: "pep_user_id",
autoIncrement: false
},
role: {
type: DataTypes.ARRAY(DataTypes.STRING),
allowNull: true,
defaultValue: null,
comment: "角色 也对应权限 admin 管理员 / all 全部角色 / data_analyst 数据分析 / after_sale 售后运维 / resource_manage 资源管理 / customer_service 客户服务",
primaryKey: false,
field: "role",
autoIncrement: false
},
correlationProject: {
type: DataTypes.ARRAY(DataTypes.INTEGER),
allowNull: true,
defaultValue: null,
comment: "关联的poms的项目id",
primaryKey: false,
field: "correlation_project",
autoIncrement: false
},
lastInTime: {
type: DataTypes.DATE,
allowNull: true,
defaultValue: null,
comment: null,
primaryKey: false,
field: "last_in_time",
autoIncrement: false
},
inTimes: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: "0",
comment: null,
primaryKey: false,
field: "in_times",
autoIncrement: false
},
onlineDuration: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: "0",
comment: "在线时长 单位 s",
primaryKey: false,
field: "online_duration",
autoIncrement: false
},
lastInAddress: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "上次登录地点",
primaryKey: false,
field: "last_in_address",
autoIncrement: false
},
disabled: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
comment: null,
primaryKey: false,
field: "disabled",
autoIncrement: false
},
deleted: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
comment: null,
primaryKey: false,
field: "deleted",
autoIncrement: false
},
updateTime: {
type: DataTypes.DATE,
allowNull: true,
defaultValue: sequelize.fn('now'),
comment: null,
primaryKey: false,
field: "update_time",
autoIncrement: false
}
}, {
tableName: "user",
comment: "",
indexes: []
});
dc.models.User = User;
return User;
};

11
api/app/lib/routes/auth/index.js

@ -0,0 +1,11 @@
'use strict';
const auth = require('../../controllers/auth');
module.exports = function (app, router, opts) {
app.fs.api.logAttr['POST/login'] = { content: '登录', visible: true };
router.post('/login', auth.login);
app.fs.api.logAttr['PUT/logout'] = { content: '登出', visible: false };
router.put('/logout', auth.logout);
};

17
api/app/lib/routes/index.js

@ -0,0 +1,17 @@
'use strict';
const path = require('path');
const fs = require('fs');
module.exports = function (app, router, opts) {
fs.readdirSync(__dirname).forEach((filename) => {
if (filename.indexOf('.') !== 0 && fs.lstatSync(path.join(__dirname, filename)).isDirectory()) {
fs.readdirSync(path.join(__dirname, filename)).forEach((api) => {
if (api.indexOf('.') == 0 || api.indexOf('.js') == -1) return;
require(`./${filename}/${api}`)(app, router, opts);
});
}
});
return router;
};

36
api/app/lib/schedule/index.js

@ -0,0 +1,36 @@
'use strict';
const fs = require('fs');
const nodeSchedule = require('node-schedule');
// 将定时任务汇集未来可根据需要选取操作
module.exports = async function (app, opts) {
const scheduleInit = ({
interval, immediate, proRun,
}, callback) => {
if (proRun && opts.dev) {
return;
}
const j = nodeSchedule.scheduleJob(interval, callback);
if (immediate && (!proRun || (proRun && !opts.dev))) {
setTimeout(callback, 0)
}
return j;
}
app.fs.scheduleInit = scheduleInit
fs.readdirSync(__dirname).forEach((filename) => {
if (!['index.js'].some(f => filename == f)) {
const scheduleList = require(`./${filename}`)(app, opts)
for (let k of Object.keys(scheduleList)) {
console.info(`定时任务 ${k} 启动`);
}
app.fs.schedule = {
...app.fs.schedule,
...scheduleList,
}
}
});
};

36
api/app/lib/service/clickHouseClient.js

@ -0,0 +1,36 @@
'use strict';
const { ClickHouse } = require('clickhouse');
function factory (app, opts) {
if (opts.clickHouse) {
try {
app.fs.clickHouse = {}
const { url, port, user, password, db = [] } = opts.clickHouse
for (let d of db) {
if (d.name && d.db) {
app.fs.clickHouse[d.name] = new ClickHouse({
url: url,
port: port,
debug: true || opts.dev,
format: "json",
basicAuth: user && password ? {
username: user,
password: password,
} : null,
config: {
database: d.db,
},
})
console.info(`ClickHouse ${d.name} 初始化完成`);
} else {
throw 'opts.clickHouse 参数错误!'
}
}
} catch (error) {
console.error(error)
process.exit(-1);
}
}
}
module.exports = factory;

20
api/app/lib/service/kafka.js

@ -0,0 +1,20 @@
'use strict';
const Kafka = require('kafka-node');
module.exports = async function factory (app, opts) {
const client = new Kafka.KafkaClient({ kafkaHost: opts.kafka.rootURL });
const producer = new Kafka.HighLevelProducer(client);
producer.on('error', function (err) {
app.fs.logger.log('error', "[FS-KAFKA]", err);
});
const kafka = {
producer: producer,
configUpdateMessage: opts.configUpdateMessage || {}
};
app.fs.kafka = kafka;
app.fs.logger.log('debug', "[FS-KAFKA]", "Init.Success");
}

23
api/app/lib/service/mqttServer.js

@ -0,0 +1,23 @@
'use strict';
const mqtt = require('mqtt');
module.exports = async function factory (app, opts) {
// console.info(`mqtt connecting ${opts.mqtt.mqttVideoServer}`);
// const client = mqtt.connect(opts.mqtt.mqttVideoServer);
// client.on('connect', function () {
// console.info(`mqtt connect success ${opts.mqtt.mqttVideoServer}`);
// client.subscribe('topic/test', { qos: 0 });//订阅主题为test的消息
// })
// client.on('error', function (e) {
// console.error(`mqtt connect failed ${opts.mqtt.mqttVideoServer}`);
// app.fs.logger.error('info', '[FS-AUTH-MQTT]', `mqtt connect failed ${opts.mqtt.mqttVideoServer}`);
// })
// client.on('message', async (top, message) => {
// });
// app.mqttVideoServer = client
}

67
api/app/lib/service/paasRequest.js

@ -0,0 +1,67 @@
'use strict';
const request = require('superagent')
class paasRequest {
constructor(root, { query = {} } = {}, option) {
this.root = root;
this.query = query
this.option = option
}
#buildUrl = (url) => {
return `${this.root}/${url}`;
}
#resultHandler = (resolve, reject) => {
return (err, res) => {
if (err) {
reject(err);
} else {
resolve(res[this.option.dataWord]);
}
};
}
get = (url, { query = {}, header = {} } = {}) => {
return new Promise((resolve, reject) => {
request.get(this.#buildUrl(url)).set(header).query(Object.assign(query, this.query)).end(this.#resultHandler(resolve, reject));
})
}
post = (url, { data = {}, query = {}, header = {} } = {}) => {
return new Promise((resolve, reject) => {
request.post(this.#buildUrl(url)).set(header).query(Object.assign(query, this.query)).send(data).end(this.#resultHandler(resolve, reject));
})
}
put = (url, { data = {}, header = {}, query = {}, } = {}) => {
return new Promise((resolve, reject) => {
request.put(this.#buildUrl(url)).set(header).query(Object.assign(query, this.query)).send(data).end(this.#resultHandler(resolve, reject));
})
}
delete = (url, { header = {}, query = {} } = {}) => {
return new Promise((resolve, reject) => {
request.delete(this.#buildUrl(url)).set(header).query(Object.assign(query, this.query)).end(this.#resultHandler(resolve, reject));
})
}
}
function factory (app, opts) {
if (opts.pssaRequest) {
try {
for (let r of opts.pssaRequest) {
if (r.name && r.root) {
app.fs[r.name] = new paasRequest(r.root, { ...(r.params || {}) }, { dataWord: r.dataWord || 'body' })
} else {
throw 'opts.pssaRequest 参数错误!'
}
}
} catch (error) {
console.error(error)
process.exit(-1);
}
}
}
module.exports = factory;

41
api/app/lib/service/redis.js

@ -0,0 +1,41 @@
'use strict';
// https://github.com/luin/ioredis
const redis = require("ioredis")
module.exports = async function factory (app, opts) {
let client = opts.redis.pwd ?
new redis.Cluster([
{
host: opts.redis.host,
port: opts.redis.port
}
], {
redisOptions: {
password: opts.redis.pwd,
},
})
: new redis(opts.redis.port, opts.redis.host, {
password: opts.redis.pwd,
});
client.on("error", function (err) {
app.fs.logger.error('info', '[FS-AUTH-REDIS]', `redis connect error. ${opts.redis.host + ':' + opts.redis.port}`);
// console.error("Error :", err);
// process.exit(-1);
});
client.on('connect', function () {
console.info(`redis connect success ${opts.redis.host + ':' + opts.redis.port}`);
})
// 自定义方法
async function hdelall (key) {
const obj = await client.hgetall(key);
await client.hdel(key, Object.keys(obj))
}
app.redis = client
app.redisTools = {
hdelall,
}
}

40
api/app/lib/service/socket.js

@ -0,0 +1,40 @@
'use strict';
const moment = require('moment')
module.exports = async function factory (app, opts) {
app.socket.on('connection', async (socket) => {
console.info('WEB_SOCKET token:' + socket.handshake.query.token + ' 已连接:id ' + socket.id + ' 时间:' + moment(socket.handshake.time).format());
socket.on('disconnecting', async (reason) => {
const connectSeconds = moment().diff(moment(socket.handshake.time), 'seconds')
console.info('WEB_SOCKET token:' + socket.handshake.query.token + ' 已断开连接:' + reason + ' 连接时长:' + connectSeconds + 's');
const { models } = app.fs.dc
await models.User.increment({
onlineDuration: connectSeconds
}, {
where: {
id: socket.handshake.query.pomsUserId
}
})
})
})
// 使用测试 保持链接
// setInterval(async () => {
// const { connected } = app.socket.sockets
// const roomId = 'ROOM_' + Math.random()
// // if (connected) {
// // for (let c in connected) {
// // connected[c].join(roomId)
// // }
// // app.socket.to(roomId).emit('TEST', { someProperty: `【星域 ROOM:${roomId}】呼叫自然选择号!!!`, })
// // }
// app.socket.emit('TEST', { someProperty: '【广播】呼叫青铜时代号!!!', })
// }, 3000)
}

173
api/app/lib/utils/dataRange.js

@ -0,0 +1,173 @@
'use strict';
const fs = require('fs');
const moment = require('moment')
module.exports = function (app, opts) {
function judgeSuper (ctx) {
try {
const { userInfo = {} } = ctx.fs.api || {};
const { role = [] } = userInfo
return role.some(r => r == 'SuperAdmin' || r == 'admin')
} catch (error) {
ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`);
}
}
async function anxinStrucIdRange ({ ctx, pepProjectId, keywordTarget, keyword }) {
const { models } = ctx.fs.dc;
const { userInfo = {} } = ctx.fs.api || {};
const { clickHouse } = ctx.app.fs
const { correlationProject = [] } = userInfo
const isSuper = judgeSuper(ctx)
let findOption = {
where: {
del: false
}
}
if (pepProjectId) {
// 有 特定的项目id 就按此查询
findOption.where.pepProjectId = pepProjectId
} else if (!isSuper) {
// 还不是超管或管理员就按关联的项目id的数据范围查
findOption.where.id = { $in: correlationProject }
}
let pepProjectWhereOptions = []
if (keywordTarget == 'pepProject' && keyword) {
pepProjectWhereOptions.push(`t_pim_project.project_name LIKE '%${keyword}%'`)
findOption.where.name = {
$or: [{
$eq: null
}, {
$like: `%${keyword}%`
}]
}
}
// TODO 这儿也许需要判断传进来的 pepProjectId 在不在当前用户的关注范围内
// 根据 poms 的项目绑定关系查相关联的项企项目、安心云项目id信息
const bindRes = await models.ProjectCorrelation.findAll(findOption)
// 获取不重复的 项企项目id
let pepProjectIds = []
for (let b of bindRes) {
if (b.pepProjectId) {
pepProjectIds.push(b.pepProjectId)
}
}
// 查询项企项目的信息
const pepProjectRes = pepProjectIds.length ?
await clickHouse.projectManage.query(
`
SELECT
t_pim_project.id AS id,
t_pim_project.project_name AS projectName,
t_pim_project.isdelete AS isdelete,
t_pim_project_construction.construction_status_id AS constructionStatusId,
t_pim_project_state.construction_status AS constructionStatus
FROM
t_pim_project
LEFT JOIN t_pim_project_construction
ON t_pim_project.id = t_pim_project_construction.project_id
LEFT JOIN t_pim_project_state
ON t_pim_project_construction.construction_status_id = t_pim_project_state.id
WHERE
id IN (${pepProjectIds.join(',')})
${pepProjectWhereOptions.length ? `AND ${pepProjectWhereOptions.join(' AND ')}`
: ''}
`
).toPromise() :
[]
// 获取不重复的 安心云项目 id
const anxinProjectIds = [
...(
keywordTarget == 'pepProject' && keyword ?
bindRes.filter(b => b.name || pepProjectRes.some(pp => pp.id == b.pepProjectId)) :
bindRes
).reduce(
(arr, b) => {
for (let sid of b.anxinProjectId) {
arr.add(sid);
}
return arr;
},
new Set()
)
]
// 查询安心云项目及结构物信息
let undelStrucWhereOptions = []
if (keywordTarget && keyword) {
if (keywordTarget == 'struc') {
undelStrucWhereOptions.push(`t_structure.name LIKE '%${keyword}%'`)
}
}
const undelStrucRes = anxinProjectIds.length ?
await clickHouse.anxinyun.query(
`
SELECT
t_project.id AS projectId,
t_structure.id AS strucId,
t_structure.name AS strucName,
project_state
FROM
t_project
LEFT JOIN
t_project_structure
ON t_project_structure.project = t_project.id
LEFT JOIN
t_project_structuregroup
ON t_project_structuregroup.project = t_project.id
LEFT JOIN
t_structuregroup_structure
ON t_structuregroup_structure.structuregroup = t_project_structuregroup.structuregroup
RIGHT JOIN
t_structure
ON t_structure.id = t_project_structure.structure
OR t_structure.id = t_structuregroup_structure.structure
WHERE
project_state != -1
AND
t_project.id IN (${anxinProjectIds.join(',')})
${undelStrucWhereOptions.length ? `AND ${undelStrucWhereOptions.join(' AND ')}` : ''}
`
).toPromise() :
[]
// 构建安心云结构物和项企项目的关系
// 并保存信息至数据
let undelStruc = []
for (let s of undelStrucRes) {
if (!undelStruc.some(us => us.strucId == s.strucId)) {
let pomsProject = []
for (let { dataValues: br } of bindRes) {
if (br.anxinProjectId.some(braId => braId == s.projectId)) {
let corPepProject = pepProjectRes.find(pp => pp.id == br.pepProjectId)
pomsProject.push({
...br,
pepProject: corPepProject
})
}
}
undelStruc.push({
strucId: s.strucId,
strucName: s.strucName,
// projectId: s.projectId,
pomsProject: pomsProject
})
}
}
return undelStruc
}
return {
judgeSuper,
anxinStrucIdRange
}
}

17
api/app/lib/utils/index.js

@ -0,0 +1,17 @@
'use strict';
const path = require('path');
const fs = require('fs');
module.exports = async function (app, opts) {
fs.readdirSync(__dirname).forEach((filename) => {
if (!['index.js'].some(f => filename == f)) {
const utils = require(`./${filename}`)(app, opts)
console.log(`载入 ${filename} 工具集成功`);
app.fs.utils = {
...app.fs.utils,
...utils,
}
}
});
};

21
api/app/lib/utils/kafkaSend.js

@ -0,0 +1,21 @@
'use strict';
module.exports = function (app, opts) {
async function kfkSendAsync (payloads) {
const { producer } = app.fs.kafka
return new Promise((resolve, reject) => {
producer.send(payloads, function (err) {
if (err) {
reject(err);
} else {
resolve();
}
});
})
}
return {
kfkSendAsync
}
}

30
api/app/lib/utils/vcmpAuth.js

@ -0,0 +1,30 @@
'use strict';
const moment = require('moment')
module.exports = function (app, opts) {
async function vcmpAuth () {
const { vcmp: { app: vcApp } } = opts
const vcmpAuth = await app.redis.hgetall('vcmpAuth');
if (vcmpAuth.token && moment().isBefore(moment(vcmpAuth.expires))) {
return vcmpAuth.token
} else {
let res = await app.fs.iotAuthRequest.post('oauth2/token', {
data: {
grant_type: 'client_credentials'
},
header: {
Authorization: `Basic ${Buffer.from(`${encodeURIComponent(vcApp.id)}:${encodeURIComponent(vcApp.secret)}`).toString('base64')}`
}
})
await app.redis.hmset('vcmpAuth', {
...res
});
return res.token
}
}
return {
vcmpAuth
}
}

82
api/app/lib/utils/xlsxDownload.js

@ -0,0 +1,82 @@
'use strict';
const fs = require('fs');
const xlsx = require('better-xlsx');
const path = require('path')
const moment = require('moment')
module.exports = function (app, opts) {
//递归创建目录 同步方法
async function makeDir (dir) {
if (!fs.existsSync(dir)) {
makeDir(path.dirname(dir))
fs.mkdirSync(dir, function (err) {
if (err) {
throw err
}
});
}
}
async function simpleExcelDown ({ data = [], header = [], fileName = moment().format('YYYY-MM-DD HH:mm:ss') } = {}) {
const fileDirPath = path.join(__dirname, `../../downloadFiles`)
makeDir(fileDirPath)
const file = new xlsx.File();
const sheet_1 = file.addSheet('sheet_1');
// header
const headerStyle = new xlsx.Style();
headerStyle.align.h = 'center';
headerStyle.align.v = 'center';
headerStyle.border.right = 'thin';
headerStyle.border.rightColor = '#000000';
headerStyle.border.bottom = 'thin';
headerStyle.border.bottomColor = '#000000';
const headerRow = sheet_1.addRow();
const indexCell = headerRow.addCell();
indexCell.value = '序号'
indexCell.style = headerStyle
for (let h of header) {
const cell = headerRow.addCell();
cell.value = h.title;
cell.style = headerStyle
}
// data
const style = new xlsx.Style();
style.align.h = 'left';
style.align.v = 'center';
style.border.right = 'thin';
style.border.rightColor = '#000000';
style.border.bottom = 'thin';
style.border.bottomColor = '#000000';
for (let i = 0; i < data.length; i++) {
const row = sheet_1.addRow();
const indexCell = row.addCell();
indexCell.value = i + 1
indexCell.style = headerStyle
for (let h of header) {
const cell = row.addCell();
cell.value = data[i][h.key];
cell.style = style
}
}
const savePath = path.join(fileDirPath, fileName)
await new Promise(function (resolve, reject) {
file.saveAs()
.pipe(fs.createWriteStream(savePath))
.on('finish', () => {
resolve()
});
})
return savePath
}
return {
simpleExcelDown,
makeDir
}
}

298
api/config.js

@ -0,0 +1,298 @@
'use strict';
/*jslint node:true*/
const path = require('path');
const os = require('os');
const moment = require('moment');
const args = require('args');
const dev = process.env.NODE_ENV == 'development';
// 启动参数
args.option(['p', 'port'], '启动端口');
args.option(['g', 'pg'], 'postgre 服务 URL');
args.option(['f', 'fileHost'], '文件中心本地化存储: WebApi 服务器地址(必填), 该服务器提供文件上传Web服务');
args.option(['k', 'kafka'], 'kafka 服务 URL');
args.option('iotaProxy', '以太代理')
args.option('redisHost', 'redisHost');
args.option('redisPort', 'redisPort');
args.option('redisPswd', 'redisPassword');
args.option('axyApiUrl', '安心云 api');
args.option('apiEmisUrl', '企业管理 api');
args.option('apiVcmpUrl', '视频平台 api');
args.option('apiIotAuth', 'IOT 鉴权平台')
args.option('godUrl', '高德地图API请求地址');
args.option('godKey', '高德地图API key');
args.option('mqttVideoServer', '视频后台 mqtt 服务 URL');
// 七牛云存储参数
args.option('qnak', 'qiniuAccessKey');
args.option('qnsk', 'qiniuSecretKey');
args.option('qnbkt', 'qiniuBucket');
args.option('qndmn', 'qiniuDomain');
// clickHouse
args.option('clickHouseUrl', 'clickHouse Url');
args.option('clickHousePort', 'clickHouse Port');
args.option('clickHouseAnxincloud', 'clickHouse 安心云数据库名称');
args.option('clickHousePepEmis', 'clickHouse 项企数据库名称');
args.option('clickHouseProjectManage', 'clickHouse 项目管理数据库名称');
args.option('clickHouseVcmp', 'clickHouse 视频平台数据库名称');
args.option('clickHouseDataAlarm', 'clickHouse 视频平台数据告警库名称');
args.option('confirmAlarmAnxinUserId', '确认告警时保存到 ES 的安心云的用户的 id');
// 视频应用秘钥
args.option('vcmpAppId', '视频平台 应用 id')
args.option('vcmpAppSecret', '视频平台 应用秘钥')
const flags = args.parse(process.argv);
const POMS_DB = process.env.POMS_DB || flags.pg;
const LOCAL_SVR_ORIGIN = process.env.LOCAL_SVR_ORIGIN || flags.fileHost;
// kafka
const ANXINCLOUD_KAFKA_BROKERS = process.env.ANXINCLOUD_KAFKA_BROKERS || flags.kafka;
// 以太代理
const IOT_PROXY = process.env.IOT_PROXY || flags.iotaProxy;
// Redis 参数
const IOTA_REDIS_SERVER_HOST = process.env.IOTA_REDIS_SERVER_HOST || flags.redisHost || "localhost";//redis IP
const IOTA_REDIS_SERVER_PORT = process.env.IOTA_REDIS_SERVER_PORT || flags.redisPort || "6379";//redis 端口
const IOTA_REDIS_SERVER_PWD = process.env.IOTA_REDIS_SERVER_PWD || flags.redisPswd || "";//redis 密码
// 安心云api
const API_ANXINYUN_URL = process.env.API_ANXINYUN_URL || flags.axyApiUrl;
// 企业管理 api
const API_EMIS_URL = process.env.API_EMIS_URL || flags.apiEmisUrl;
// 视频平台 api
const API_VCMP_URL = process.env.API_VCMP_URL || flags.apiVcmpUrl;
// iot鉴权平台 api
const API_IOT_AUTH = process.env.API_IOT_AUTH || flags.apiIotAuth;
// 高德地图的参数
const GOD_URL = process.env.GOD_URL || flags.godUrl || 'https://restapi.amap.com/v3';
const GOD_KEY = process.env.GOD_KEY || flags.godKey;
// 视频后台 mqtt 信息推送地址
const MQTT_VIDEO_SERVER = process.env.MQTT_VIDEO_SERVER || flags.mqttVideoServer;
// 七牛云存储参数
const QINIU_DOMAIN_QNDMN_RESOURCE = process.env.ANXINCLOUD_QINIU_DOMAIN_QNDMN_RESOURCE || flags.qndmn;
const QINIU_BUCKET_RESOURCE = process.env.ANXINCLOUD_QINIU_BUCKET_RESOURCE || flags.qnbkt;
const QINIU_AK = process.env.ANXINCLOUD_QINIU_ACCESSKEY || flags.qnak;
const QINIU_SK = process.env.ANXINCLOUD_QINIU_SECRETKEY || flags.qnsk;
// clickHouse
const CLICKHOUST_URL = process.env.CLICKHOUST_URL || flags.clickHouseUrl
const CLICKHOUST_PORT = process.env.CLICKHOUST_PORT || flags.clickHousePort
const CLICKHOUST_USER = process.env.CLICKHOUST_USER || flags.clickHouseUser
const CLICKHOUST_PASSWORD = process.env.CLICKHOUST_PASSWORD || flags.clickHousePassword
const CLICKHOUST_ANXINCLOUD = process.env.CLICKHOUST_ANXINCLOUD || flags.clickHouseAnxincloud
const CLICKHOUST_PEP_EMIS = process.env.CLICKHOUST_PEP_EMIS || flags.clickHousePepEmis
const CLICKHOUST_PROJECT_MANAGE = process.env.CLICKHOUST_PROJECT_MANAGE || flags.clickHouseProjectManage
const CLICKHOUST_VCMP = process.env.CLICKHOUST_VCMP || flags.clickHouseVcmp
const CLICKHOUST_DATA_ALARM = process.env.CLICKHOUST_DATA_ALARM || flags.clickHouseDataAlarm
const CONFIRM_ALARM_ANXIN_USER_ID = process.env.CONFIRM_ALARM_ANXIN_USER_ID || flags.confirmAlarmAnxinUserId
const PLATFORM_NAME = process.env.PLATFORM_NAME || flags.platformName || 'anxinyun';
// 视频平台应用秘钥
const VCMP_APP_ID = process.env.VCMP_APP_ID || flags.vcmpAppId
const VCMP_APP_SECRET = process.env.VCMP_APP_SECRET || flags.vcmpAppSecret
if (
!POMS_DB
|| !IOTA_REDIS_SERVER_HOST || !IOTA_REDIS_SERVER_PORT
|| !ANXINCLOUD_KAFKA_BROKERS
|| !GOD_KEY
|| !API_ANXINYUN_URL
|| !API_EMIS_URL
|| !API_VCMP_URL
|| !API_IOT_AUTH
|| !QINIU_DOMAIN_QNDMN_RESOURCE || !QINIU_BUCKET_RESOURCE || !QINIU_AK || !QINIU_SK
|| !CLICKHOUST_URL || !CLICKHOUST_PORT
|| !CLICKHOUST_ANXINCLOUD || !CLICKHOUST_PEP_EMIS || !CLICKHOUST_PROJECT_MANAGE || !CLICKHOUST_VCMP || !CLICKHOUST_DATA_ALARM
|| !CONFIRM_ALARM_ANXIN_USER_ID
|| !VCMP_APP_ID || !VCMP_APP_SECRET
) {
console.log('缺少启动参数,异常退出');
args.showHelp();
process.exit(-1);
}
const product = {
port: flags.port || 8080,
staticDirs: ['static'],
mws: [
{
entry: require('@fs/attachment').entry,
opts: {
qiniu: {
domain: QINIU_DOMAIN_QNDMN_RESOURCE,
bucket: QINIU_BUCKET_RESOURCE,
accessKey: QINIU_AK,
secretKey: QINIU_SK
},
maxSize: 104857600, // 100M
}
}, {
entry: require('./app').entry,
opts: {
dev,
exclude: [
{ p: '/attachments/:p', o: 'POST' },
{ p: '/alarm/application/inspection', o: 'POST' },
{ p: '/project/app_list', o: 'GET' },
{ p: '/alarm/application/api', o: 'POST' }
], // 不做认证的路由,也可以使用 exclude: ["*"] 跳过所有路由
anxinCloud: {
confirmAlarmAnxinUserId: CONFIRM_ALARM_ANXIN_USER_ID
},
vcmp: {
app: {
id: VCMP_APP_ID,
secret: VCMP_APP_SECRET
}
},
kafka: {
rootURL: ANXINCLOUD_KAFKA_BROKERS,
topicPrefix: PLATFORM_NAME
},
redis: {
host: IOTA_REDIS_SERVER_HOST,
port: IOTA_REDIS_SERVER_PORT,
pwd: IOTA_REDIS_SERVER_PWD
},
mqtt: {
mqttVideoServer: MQTT_VIDEO_SERVER,
},
sms: {
///阿里云-安心云
accessKey: 'LTAI5tAFdjz7j38aNF2C9Qe8',
accessSecret: '1trYkmiqfBtvZL6BxkNH2uQcQQPs0S'
},
email: {
enabled: true,
host: 'smtp.exmail.qq.com',
port: 465,
sender: {
name: '运维服务',
address: 'fsiot@free-sun.com.cn',
password: 'Fs2689'
}
},
pssaRequest: [{// name 会作为一个 request 出现在 ctx.app.fs
name: 'axyRequest',
root: API_ANXINYUN_URL
}, {
name: 'emisRequest',
root: API_EMIS_URL
}, {
name: 'vcmpRequest',
root: API_VCMP_URL
}, {
name: 'iotAuthRequest',
root: API_IOT_AUTH
}, {
name: 'iotRequest',
root: IOT_PROXY + '/_iota_api'
}, {
name: 'godRequest',
root: GOD_URL,
params: {
query: {
key: GOD_KEY
}
}
},],
clickHouse: {
url: CLICKHOUST_URL,
port: CLICKHOUST_PORT,
user: CLICKHOUST_USER,
password: CLICKHOUST_PASSWORD,
db: [
{
name: 'anxinyun',
db: CLICKHOUST_ANXINCLOUD
}, {
name: 'pepEmis',
db: CLICKHOUST_PEP_EMIS
}, {
name: 'projectManage',
db: CLICKHOUST_PROJECT_MANAGE
}, {
name: 'vcmp',
db: CLICKHOUST_VCMP
}, {
name: 'dataAlarm',
db: CLICKHOUST_DATA_ALARM
}
]
}
}
}
],
dc: {
url: POMS_DB,
opts: {
pool: {
max: 80,
min: 10,
idle: 10000
},
define: {
freezeTableName: true, // 固定表名
timestamps: false // 不含列 "createAt"/"updateAt"/"DeleteAt"
},
timezone: '+08:00',
logging: false
},
models: [require('./app').models]
},
logger: {
level: 'info',
json: false,
filename: path.join(__dirname, 'log', 'runtime.log'),
colorize: false,
maxsize: 1024 * 1024 * 5,
rotationFormat: false,
zippedArchive: true,
maxFiles: 10,
prettyPrint: true,
label: '',
timestamp: () => moment().format('YYYY-MM-DD HH:mm:ss.SSS'),
eol: os.EOL,
tailable: true,
depth: null,
showLevel: true,
maxRetries: 1
}
};
const development = {
port: product.port,
staticDirs: product.staticDirs,
mws: product.mws,
dc: product.dc,
logger: product.logger
};
if (dev) {
// mws
for (let mw of development.mws) {
// if (mw.opts.exclude) mw.opts.exclude = ['*']; // 使用 ['*'] 跳过所有路由
}
// logger
development.logger.filename = path.join(__dirname, 'log', 'development.log');
development.logger.level = 'debug';
development.dc.opts.logging = console.log;
}
module.exports = dev ? development : product;

43
api/package.json

@ -0,0 +1,43 @@
{
"name": "smart-emergency",
"version": "1.0.0",
"description": "fs smart emergency api",
"main": "server.js",
"scripts": {
"test": "set DEBUG=true&&\"node_modules/.bin/mocha\" --harmony --reporter spec app/test/*.test.js",
"start": "set NODE_ENV=development&&node server -p 4000 -g postgres://postgres:123@10.8.30.32:5432/video_access -f http://localhost:4000",
"start:linux": "export NODE_ENV=development&&node server -p 4000 -g postgres://FashionAdmin:123456@10.8.30.39:5432/pm1",
"automate": "sequelize-automate -c sequelize-automate.config.js"
},
"author": "",
"license": "MIT",
"repository": {},
"dependencies": {
"@alicloud/pop-core": "^1.7.12",
"@fs/attachment": "^1.0.0",
"args": "^3.0.7",
"better-xlsx": "^0.7.6",
"clickhouse": "^2.6.0",
"crypto-js": "^4.0.0",
"file-saver": "^2.0.2",
"fs-web-server-scaffold": "^2.0.2",
"ioredis": "^5.0.4",
"kafka-node": "^2.2.3",
"koa-convert": "^1.2.0",
"koa-proxy": "^0.9.0",
"moment": "^2.24.0",
"mqtt": "^4.3.7",
"node-schedule": "^2.1.0",
"nodemailer": "^6.7.7",
"path": "^0.12.7",
"path-to-regexp": "^3.0.0",
"pg": "^7.9.0",
"redis": "^3.1.2",
"request": "^2.88.2",
"superagent": "^3.5.2",
"uuid": "^3.3.2"
},
"devDependencies": {
"mocha": "^6.0.2"
}
}

35
api/sequelize-automate.config.js

@ -0,0 +1,35 @@
module.exports = {
// 数据库配置 与 sequelize 相同
dbOptions: {
database: 'orational_service',
username: 'postgres',
password: '123',
dialect: 'postgres',
host: '10.8.30.32',
port: 5432,
define: {
underscored: false,
freezeTableName: false,
charset: 'utf8mb4',
timezone: '+00: 00',
dialectOptions: {
collate: 'utf8_general_ci',
},
timestamps: false,
},
},
options: {
type: 'freesun', // 指定 models 代码风格
camelCase: true, // Models 文件中代码是否使用驼峰命名
modalNameSuffix: false, // 模型名称是否带 ‘Model’ 后缀
fileNameCamelCase: false, // Model 文件名是否使用驼峰法命名,默认文件名会使用表名,如 `user_post.js`;如果为 true,则文件名为 `userPost.js`
dir: './app/lib/models', // 指定输出 models 文件的目录
typesDir: 'models', // 指定输出 TypeScript 类型定义的文件目录,只有 TypeScript / Midway 等会有类型定义
emptyDir: false, // !!! 谨慎操作 生成 models 之前是否清空 `dir` 以及 `typesDir`
tables: ['quick_link'], // 指定生成哪些表的 models,如 ['user', 'user_post'];如果为 null,则忽略改属性
skipTables: [], // 指定跳过哪些表的 models,如 ['user'];如果为 null,则忽略改属性
tsNoCheck: false, // 是否添加 `@ts-nocheck` 注释到 models 文件中
ignorePrefix: [], // 生成的模型名称忽略的前缀,因为 项目中有以下表名是以 t_ 开头的,在实际模型中不需要, 可以添加多个 [ 't_data_', 't_',] ,长度较长的 前缀放前面
attrLength: false, // 在生成模型的字段中 是否生成 如 var(128)这种格式,公司一般使用 String ,则配置为 false
},
}

12
api/server.js

@ -0,0 +1,12 @@
/**
* Created by rain on 2016/1/25.
*/
'use strict';
/*jslint node:true*/
//from koa
const scaffold = require('fs-web-server-scaffold');
const config = require('./config');
module.exports = scaffold(config);

19
jenkinsfile_poms_api

@ -0,0 +1,19 @@
pipeline {
agent {
node{
label 'jnlp-slave'
}
}
stages {
stage('Testing poms ......') {
steps {
sh 'switch-auth.sh anxinyun'
buildName "#${BUILD_NUMBER} ~/fs-cloud/${JOB_NAME}:${IMAGE_VERSION}"
buildDescription "registry.cn-hangzhou.aliyuncs.com/${CLOUD}/${JOB_NAME}:${IMAGE_VERSION}"
sh 'docker build -t registry.cn-hangzhou.aliyuncs.com/${CLOUD}/${JOB_NAME}:${IMAGE_VERSION} ./api'
sh 'docker push registry.cn-hangzhou.aliyuncs.com/${CLOUD}/${JOB_NAME}:${IMAGE_VERSION}'
}
}
}
}

19
jenkinsfile_poms_web

@ -0,0 +1,19 @@
pipeline {
agent {
node{
label 'jnlp-slave'
}
}
stages {
stage('Testing poms ......') {
steps {
sh 'switch-auth.sh anxinyun'
buildName "#${BUILD_NUMBER} ~/fs-cloud/${JOB_NAME}:${IMAGE_VERSION}"
buildDescription "registry.cn-hangzhou.aliyuncs.com/${CLOUD}/${JOB_NAME}:${IMAGE_VERSION}"
sh 'docker build -t registry.cn-hangzhou.aliyuncs.com/${CLOUD}/${JOB_NAME}:${IMAGE_VERSION} ./web'
sh 'docker push registry.cn-hangzhou.aliyuncs.com/${CLOUD}/${JOB_NAME}:${IMAGE_VERSION}'
}
}
}
}

17
web/.babelrc

@ -0,0 +1,17 @@
{
"presets": [
"@babel/preset-react",
"@babel/preset-env"
],
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-object-rest-spread",
// ["import", {
// // "libraryName": "antd",
// "libraryDirectory": "es"
// }]
],
"env": {
"development": {}
}
}

5
web/.vscode/extensions.json

@ -0,0 +1,5 @@
{
"recommendations": [
"formulahendry.code-runner"
]
}

18
web/.vscode/launch.json

@ -0,0 +1,18 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Server",
"type": "node",
"request": "launch",
"program": "${workspaceRoot}/server.js",
"args": [
"-u http://127.0.0.1:4000"
],
"outputCapture": "std",
"env": {
"NODE_ENV": "development"
}
}
]
}

4
web/.vscode/settings.json

@ -0,0 +1,4 @@
//
{
"editor.fontSize": 16,
}

40
web/Dockerfile

@ -0,0 +1,40 @@
# FROM repository.anxinyun.cn/devops/node:12-dev as builder
# COPY . /var/app
# WORKDIR /var/app
# EXPOSE 8080
# RUN npm config set registry=http://10.8.30.22:7000
# RUN echo "{\"time\":\"$BUILD_TIMESTAMP\",\"build\": \"$BUILD_NUMBER\",\"revision\": \"$SVN_REVISION_1\",\"URL\":\"$SVN_URL_1\"}" > version.json
# RUN npm cache clean -f
# RUN npm install --registry http://10.8.30.22:7000
# RUN npm run build
# RUN rm -rf client/src
# RUN rm -rf node_modules
# RUN npm install --production --registry http://10.8.30.22:7000
# FROM registry.cn-hangzhou.aliyuncs.com/fs-devops/node-16:7.22-06-20
# COPY --from=builder --chown=node /var/app /home/node/app
# WORKDIR /home/node/app
# CMD ["node", "server.js"]
# 旧版本构建方式
FROM repository.anxinyun.cn/base-images/nodejs12:20.10.12.2
COPY . /var/app
WORKDIR /var/app
EXPOSE 8080
CMD ["-u", "http://localhost:8088"]
ENTRYPOINT [ "node", "server.js" ]

BIN
web/client/assets/fonts/DINExp.ttf

Binary file not shown.

BIN
web/client/assets/fonts/YouSheBiaoTiHei-2.ttf

Binary file not shown.

BIN
web/client/assets/images/background/console.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

BIN
web/client/assets/images/background/header.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

BIN
web/client/assets/images/background/loginBackground.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 791 KiB

BIN
web/client/assets/images/background/loginbg.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
web/client/assets/images/background/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
web/client/assets/images/background/notice.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 B

BIN
web/client/assets/images/background/topchoose.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
web/client/assets/images/background/user_login.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
web/client/assets/images/background/username.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 B

BIN
web/client/assets/images/background/videoPlayBg.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1012 B

BIN
web/client/assets/images/background/xiangqi.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
web/client/assets/images/console/suo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 B

BIN
web/client/assets/images/console/yonghu.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 924 B

BIN
web/client/assets/images/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

1
web/client/assets/js/jessibuca/decoder.js

File diff suppressed because one or more lines are too long

BIN
web/client/assets/js/jessibuca/decoder.wasm

Binary file not shown.

1
web/client/assets/js/jessibuca/jessibuca.js

File diff suppressed because one or more lines are too long

BIN
web/client/assets/video/cross_loading.mp4

Binary file not shown.

BIN
web/client/assets/video/login_bg.mp4

Binary file not shown.

21
web/client/index.ejs

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
<!-- <meta content="upgrade-insecure-requests" http-equiv="Content-Security-Policy"> -->
<link rel="shortcut icon" href="/assets/images/favicon.ico">
<script charset="UTF-8" id="LA_COLLECT" src="//sdk.51.la/js-sdk-pro.min.js"></script>
<script>LA.init({ id: "Jo4eTlZVqgx3uwqm", ck: "Jo4eTlZVqgx3uwqm" })</script>
<script src="https://lf1-cdn-tos.bytegoofy.com/obj/iconpark/icons_19077_10.1efd80a22a5e53e48737fd5ab150ffd2.es5.js"></script>
</head>
<body>
<div id='PomsApp' style="height: 100%;"></div>
</body>
</html>

54
web/client/index.html

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
<link rel="shortcut icon" href="/assets/images/favicon.ico">
<script charset="UTF-8" id="LA_COLLECT" src="//sdk.51.la/js-sdk-pro.min.js"></script>
<script>LA.init({ id: "Jo4eTlZVqgx3uwqm", ck: "Jo4eTlZVqgx3uwqm" })</script>
<script
src="https://lf1-cdn-tos.bytegoofy.com/obj/iconpark/icons_19077_11.27aacefc59cea1cbc86236576463a6d2.es5.js"></script>
</head >
<body>
<div id='PomsApp' style="height: 100%;"></div>
<!-- Webpack -->
<script type="text/javascript" src="http://localhost:5601/client/build/app.js"></script>
<!-- Vite -->
<!-- <script type="module">
import RefreshRuntime from "http://localhost:5602/@react-refresh"
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => { }
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
const global = window
</script>
<script type="module" src="http://localhost:5002/src/index.jsx"></script> -->
<!-- Vite End -->
<script>
//过滤掉一些无用的警告、没有价值的报错
//代理console.warn方法
// const _consoleWarn = (...rest) => {
// console.error(...rest)
// };
// console.error = (...rest) => {
// if (
// ![
// 'Each child in a list should have a unique "key" prop',
// ].some((item) => rest[0].indexOf(item) !== -1)
// ) {
// _consoleWarn(...rest);
// }
// };
</script>
</body>
</html>

20
web/client/index.js

@ -0,0 +1,20 @@
/**
* User: liuxinyi/liu.xinyi@free-sun.com.cn
* Date: 2016/2/22
* Time: 15:29
*
*/
'use strict';
const views = require('koa-view');
const path = require('path');
module.exports = {
entry: function (app, router, opt) {
app.use(views(__dirname));
router.get('(.*)', async function (ctx) {
await ctx.render(path.join(__dirname, './index'));
});
}
};

39
web/client/src/app.jsx

@ -0,0 +1,39 @@
'use strict';
import React, { useEffect } from 'react';
import Layout from './layout';
import Auth from './sections/auth';
import Example from './sections/example';
import NoMatch from './sections/noMatch';
const App = props => {
const { projectName } = props
useEffect(() => {
document.title = projectName;
console.log(`
_ _
         
      |  _  _ l
      \` ミ_xノ
     /      |
    /    
      | | |
 |   | | |
 | (ヽ__ヽ_)__)
 二つ
`);
}, [])
return (
<Layout
title={projectName}
sections={[
Example,Auth, NoMatch,
]}
/>
)
}
export default App;

14
web/client/src/components/index.js

@ -0,0 +1,14 @@
'use strict';
import SimpleFileDownButton from './simpleFileDownButton'
import ReminderBox from './reminderBox'
import Setup from './setup'
import { SkeletonScreen } from './skeletonScreen'
import OutHidden from './outHidden'
export {
SimpleFileDownButton,
ReminderBox,
Setup,
SkeletonScreen,
OutHidden,
};

40
web/client/src/components/outHidden.jsx

@ -0,0 +1,40 @@
import React, { useState, useEffect } from "react";
import { Tooltip } from "@douyinfe/semi-ui";
function OutHidden ({ name, width, height, color, background, number }) {
return <>
{number ?
<div style={{ display: 'inline-block' }}>
<Tooltip content={name}>
<div style={{
width: width,
height: height,
color: color,
background: background,
}}>
{name.length > number ? `${name.substr(0, number)}...` : name}
</div>
</Tooltip>
</div>
: <div style={{ display: 'inline-block' }}>
<Tooltip content={name}>
<div style={{
width: width,
height: height,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
color: color,
background: background,
}}>
{name}
</div>
</Tooltip>
</div>}
</>
}
export default OutHidden;

50
web/client/src/components/reminderBox.jsx

@ -0,0 +1,50 @@
import { Button, Checkbox, Modal } from "@douyinfe/semi-ui";
import React from "react";
const ReminderBox = ({ title, wait, toadd, visible, onOk, close, USER}) => {
return (
<Modal
title={
<div style={{ fontSize: 14, lineHeight: "35px", marginLeft: 16 }}>
{title}
</div>
}
onCancel={() => {
close()
}}
icon={
<img
src="../../assets/images/logo/figure.png"
style={{ width: 40, height: 40 }}
/>
}
footer={
<div
style={{
width: "100%",
display: "flex",
justifyContent: "space-between",
}}
>
<Checkbox
onChange={(checked) =>{
localStorage.setItem( USER, JSON.stringify(checked.target.checked)
)
}}
aria-label="Checkbox 示例"
style={{ width: 100 }}
>
不再提醒
</Checkbox>
<div>
<Button onClick={() => close()}>{wait}</Button>
<Button theme="solid" onClick={() => onOk()}>{toadd}</Button>
</div>
</div>
}
visible={visible}
></Modal>
);
};
export default ReminderBox;

124
web/client/src/components/setup.jsx

@ -0,0 +1,124 @@
import React, { useState, useEffect } from "react";
import {
Modal,
CheckboxGroup,
Checkbox,
} from "@douyinfe/semi-ui";
function Setup(props) {
const {
close,
tableType,
tableList
} = props;
console.log(tableType,
tableList);
const [check, setCheck] = useState([]);
const checkboxcss = { width: "25%", height: 16, margin: "0 0 20px 0" };
useEffect(() => {
//
const checkItem = localStorage.getItem(tableType);
setCheck(checkItem?JSON.parse(checkItem) : [])
ischeck();
}, []);
function ischeck(value) {
if (check.length >= 8) {
if (check.includes(value)) {
return false;
} else {
return true;
}
}
}
return (
<Modal
title={
<div>
表格属性设置
<span
style={{
width: 50,
lineHeight: "19px",
display: "inline-block",
color: "white",
textAlign: "center",
marginLeft: 6,
background:
check.length == 8
? "rgba(40, 123, 255, 1)"
: "rgba(176, 176, 176, 1)",
}}
>
{check.length}/8
</span>
</div>
}
visible={true}
style={{ width: 600 }}
onOk={() => {
localStorage.setItem(tableType, JSON.stringify(check));
close();
}}
onCancel={() => {
close();
}}
>
<CheckboxGroup
style={{ width: "100%", fontSize: 14 }}
key="primary1"
direction="horizontal"
defaultValue={check}
aria-label="表格属性设置"
onChange={(check) => {
setCheck(check);
ischeck();
}}
>
{tableList.map((item,index)=>{
return(
<div
key={index}
style={{
width: 550,
border: "1px solid #EAEAEA",
padding: "0px 5px",
borderRadius: 4,
marginBottom: "20px",
}}
>
<div
style={{
borderBottom: "1px solid #EAEAEA",
marginLeft: "10px",
padding: "8px 0px",
}}
>
{item.title}
</div>
<div style={{ padding: "15px 12px", width: 530 }}>
{item.list?.map((itm) => {
return (
<Checkbox
key={itm.value}
value={itm.value}
style={checkboxcss}
disabled={ischeck(itm.value)}
>
{itm.name}
</Checkbox>
);
})}
</div>
</div>
)})}
</CheckboxGroup>
</Modal>
);
}
export default Setup;

40
web/client/src/components/simpleFileDownButton.jsx

@ -0,0 +1,40 @@
import React, { useState, useEffect, useRef } from "react";
import { connect } from "react-redux";
import moment from 'moment'
import { Button, } from "@douyinfe/semi-ui";
const SimpleFileDownButton = (props) => {
const { src, user } = props
const [downloadUrl, setDownloadUrl] = useState('')
return (
<>
<Button
style={{
width: 65,
height: 32,
background: "#FFFFFF",
borderRadius: 3,
border: "1px solid #1859C1",
}}
onClick={() => {
setDownloadUrl(`${src}?token=${user.token}&timestamp=${moment().valueOf()}`)
}}
>
导出
</Button>
{
downloadUrl ? <iframe src={`/_api/${downloadUrl}`} style={{ display: 'none' }} /> : ''
}
</>
)
}
function mapStateToProps (state) {
const { auth } = state;
return {
user: auth.user,
};
}
export default connect(mapStateToProps)(SimpleFileDownButton);

18
web/client/src/components/skeletonScreen.jsx

@ -0,0 +1,18 @@
import React, { useState, useEffect } from "react";
import { Skeleton } from "@douyinfe/semi-ui";
export function SkeletonScreen () {
return <>
<Skeleton.Title style={{ width: "95%", height: 24, margin: "8px 20px" }} />
<Skeleton.Title style={{ width: "80%", height: 28, margin: "16px 20px" }} />
<Skeleton.Title style={{ width: "50%", height: 28, margin: "16px 20px" }} />
<Skeleton.Title style={{ width: "60%", height: 28, margin: "16px 20px" }} />
<Skeleton.Title style={{ width: "90%", height: 28, margin: "16px 20px" }} />
<Skeleton.Title style={{ width: "70%", height: 28, margin: "16px 20px" }} />
<Skeleton.Title style={{ width: "50%", height: 28, margin: "16px 20px" }} />
<Skeleton.Title style={{ width: "40%", height: 28, margin: "16px 20px" }} />
<Skeleton.Title style={{ width: "60%", height: 28, margin: "16px 20px" }} />
<Skeleton.Title style={{ width: "40%", height: 28, margin: "16px 20px" }} />
</>
}

80
web/client/src/components/textScroll.jsx

@ -0,0 +1,80 @@
import React, { useRef, useEffect, useState } from 'react'
import moment from 'moment'
import './textScroll.less'
function TextScroll (props) {
const { content, duration, roll, videoObj = {} } = props
const [showContent, setShowContent] = useState('')
const showIndex = useRef(0)
const initialization = useRef(false)
const cancel = useRef(false)
let videoObjId = videoObj.id || 0
useEffect(() => {
if (content.length) {
if (roll) {
let contentParent = document.getElementById("marquee_box" + videoObjId)
document.getElementById('contentPMakeUp' + videoObjId).style.width = contentParent.clientWidth + 'px'
const contentP = document.getElementById('contentP' + videoObjId)
contentP.style.visibility = 'visible'
setShowContent(content[0])
window.cancelAnimationFrame(cancel.current)
contentParent.scrollLeft = 0
initialization.current = false
showIndex.current = 0
}
else {
let repeatTime = moment()
let refreshTime = moment()
const scroll = () => {
let contentParent = document.getElementById("marquee_box" + videoObjId)
document.getElementById('contentPMakeUp' + videoObjId).style.width = contentParent.clientWidth + 'px'
//
// if(!showContent&&!initialization.current){
if (!initialization.current) {
const contentP = document.getElementById('contentP' + videoObjId)
contentParent.scrollLeft = 0
setShowContent(content[showIndex.current])
showIndex.current = (showIndex.current + 1) % content.length
contentP.style.visibility = 'visible'
initialization.current = true
}
//
if (moment().diff(refreshTime) > 1000 / 60) {
const contentP = document.getElementById('contentP' + videoObjId)
//
if (moment().diff(repeatTime) > 1000 * 1.5) {
contentP.style.visibility = 'visible'
}
//
if (moment().diff(repeatTime) > 1000 * 3) {
contentParent.scrollLeft = contentParent.scrollLeft + 1
}
//
if (contentParent.scrollLeft >= contentP.clientWidth + 24) {
contentParent.scrollLeft = 0
repeatTime = moment()
setShowContent(content[showIndex.current])
showIndex.current = (showIndex.current + 1) % content.length
contentP.style.visibility = 'hidden'
}
refreshTime = moment()
}
let text = null
text = window.requestAnimationFrame(scroll)
cancel.current = text
}
window.requestAnimationFrame(scroll)
}
}
}, [content, roll])
return (
<div className="marquee_box" id={"marquee_box" + videoObjId} style={{ overflow: 'hidden', color: '#F9F9F9' }} >
<div style={{ position: 'relative', left: 24 }}>
<div id={'contentP' + videoObjId} style={{ display: 'inline-block', visibility: 'hidden' }}>{showContent}</div>
<div id={'contentPMakeUp' + videoObjId} style={{ width: 0, display: 'inline-block' }}></div>
</div>
</div>
)
}
export default React.memo(TextScroll)

19
web/client/src/components/textScroll.less

@ -0,0 +1,19 @@
.marquee_box {
width: 100%;
height: 100%;
word-break: keep-all;
white-space: nowrap;
// display: flex;
// align-items: center;
}
.marquee_box p {
// display: inline-block;
padding: 0;
margin: 0;
}
.marquee_box:hover p {
animation-play-state: paused;
cursor: default;
}

9
web/client/src/index.jsx

@ -0,0 +1,9 @@
'use strict';
import './public-path'
import React from 'react';
import { render } from 'react-dom';
import App from './app';
import './index.less';
render((<App projectName="运维服务" />), document.getElementById('PomsApp'));

78
web/client/src/index.less

@ -0,0 +1,78 @@
// webpack (vite 用 alias 兼容了)
// @import '~@douyinfe/semi-ui/dist/css/semi.min.css';
@import '~perfect-scrollbar/css/perfect-scrollbar.css';
@import '~nprogress/nprogress.css';
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
margin: 0;
height: 100%;
width: 100%;
overflow: hidden;
#App {
height: 100%;
}
.semi-timepicker-panel {
//时间选择器不显示滚动栏
::-webkit-scrollbar {
display: none;
/* Chrome Safari */
}
scrollbar-width: none;
/* firefox */
-ms-overflow-style: none;
/* IE 10+ */
overflow-x: hidden;
overflow-y: auto;
}
a:link {
text-decoration: none;
color: unset
}
a:visited {
text-decoration: none;
color: unset
}
a:hover {
text-decoration: none;
color: unset
}
a:active {
text-decoration: none;
color: unset
}
}
// SEMI 全局样式
.semi-popover-content {
min-width: 300px
}
.semi-portal-inner {
position: fixed;
}
@font-face {
font-family: 'YouSheBiaoTiHei'; //这个可以任意取,但是应与后面相对应eg:yxingguang
src: url('/assets/fonts/YouSheBiaoTiHei-2.ttf');
}
@font-face {
font-family: 'DINExp'; //这个可以任意取,但是应与后面相对应eg:yxingguang
src: url('/assets/fonts/DINExp.ttf');
}

46
web/client/src/layout/actions/global.js

@ -0,0 +1,46 @@
'use strict';
// import { RouteRequest } from '@peace/utils';
import { RouteTable, RouteRequest } from '$utils'
export const INIT_LAYOUT = 'INIT_LAYOUT';
export function initLayout (title, copyright, sections, actions) {
return {
type: INIT_LAYOUT,
payload: {
title,
copyright,
sections,
actions
}
};
}
export const RESIZE = 'RESIZE';
export function resize (clientHeight, clientWidth) {
const headerHeight = 50
const footerHeight = 0
return {
type: RESIZE,
payload: {
clientHeight: clientHeight - headerHeight - footerHeight,
clientWidth: clientWidth
}
}
}
export const INIT_API_ROOT = 'INIT_API_ROOT';
export function initApiRoot () {
return dispatch => {
return RouteRequest.get(RouteTable.apiRoot).then(res => {
localStorage.setItem('apiRoot', JSON.stringify(res));
return dispatch({
type: INIT_API_ROOT,
payload: {
apiRoot: res.root,
iotVcmpWeb:res.iotVcmpWeb,
}
})
});
}
}

9
web/client/src/layout/actions/index.js

@ -0,0 +1,9 @@
'use strict';
import * as global from './global'
import * as socket from './webSocket';
export default {
...global,
...socket,
};

41
web/client/src/layout/actions/webSocket.js

@ -0,0 +1,41 @@
'use strict';
import io from 'socket.io-client';
export const INIT_WEB_SOCKET = 'INIT_WEB_SOCKET'
export function initWebSocket ({ ioUrl, token, pomsUserId }) {
if (!ioUrl) {
ioUrl = localStorage.getItem('apiRoot')
ioUrl = JSON.parse(ioUrl).root
}
if (!token) {
let user = sessionStorage.getItem('pomsUser')
if (user) {
user = JSON.parse(user)
token = user.token
pomsUserId = user.pomsUserInfo.id
}
}
if (!ioUrl || !token || !pomsUserId) {
return {
type: '',
}
}
return dispatch => {
const socket = io(
ioUrl
// 'http://10.8.30.7:4000'
, {
query: {
token: token,
pomsUserId: pomsUserId
},
});
dispatch({
type: INIT_WEB_SOCKET,
payload: {
socket: socket
}
})
}
}

15
web/client/src/layout/components/footer/index.jsx

@ -0,0 +1,15 @@
'use strict';
import React from 'react';
import moment from 'moment'
export default class Footer extends React.Component {
render () {
// const { } = this.props;
return (
<div style={{ textAlign: 'center', lineHeight: '32px',fontSize:13,color:'rgb(139, 139, 139)' }}>
Copyright © {moment().year()} All Rights Reserved 版权所有· 江西飞尚科技有限公司
</div>
);
}
};

165
web/client/src/layout/components/header/index.jsx

@ -0,0 +1,165 @@
"use strict";
import React from "react";
import { connect } from "react-redux";
import { SplitButtonGroup, Dropdown, Button, Nav, Avatar } from '@douyinfe/semi-ui';
import { IconTreeTriangleDown } from '@douyinfe/semi-icons';
import "./index.less";
const Header = (props) => {
const { dispatch, history, user, actions, socket, headerItems, tochange } = props;
return (
<>
<div id="top-slider">
<Nav
mode={"horizontal"}
onClick={({ itemKey }) => {
if (itemKey == "logout") {
dispatch(actions.auth.logout(user));
if (socket) {
socket.disconnect();
}
history.push(`/signin`);
}
}}
style={{
height: 48,
minWidth: 520,
background: '#1D2343',
backgroundSize: "100% 100%",
color: "white",
}}
header={{
logo: (
<img
src="/assets/images/install/long_logo.png"
style={{ display: "inline-block", width: 200, height: 40, marginLeft: -24 }}
/>
),
text: (
<>
{/* <SplitButtonGroup style={{ marginLeft: 10 }} aria-label="项目操作按钮组"> */}
<Button theme="solid" type="primary" style={{ width: 52, height: 24, background: '#005ABD' }}>全局</Button>
{/* <Dropdown onVisibleChange={(v) => { }} menu={{}} trigger="click" position="bottomRight"> */}
<Button style={{ width: 16, height: 24, background: '#005ABD' }} theme="solid" type="primary" icon={<IconTreeTriangleDown />}></Button>
{/* </Dropdown> */}
{/* </SplitButtonGroup> */}
</>
),
}}
footer={
<>
{headerItems.map((item, index) => {
if (item.hasOwnProperty('items')) {
return (
<Nav.Sub
key={index + 'a'}
itemKey={item.itemKey}
text={item.text}
dropdownStyle={{ color: '#F2F3F5' }}
>
{item.hasOwnProperty('items') && item.items.map((ite, idx) => (
<Nav.Item key={idx + 'd'} itemKey={ite.itemKey} text={ite.text} onClick={() => { tochange(ite); }} />
))}
</Nav.Sub>
)
}
else {
return (
<div key='console' style={{ display: 'inline-flex' }}>
<img src="/assets/images/background/console.png" style={{ width: 24, marginRight: -10 }} />
<Nav.Item key={index + 'a'} itemKey={item.itemKey} text={item.text} onClick={() => { tochange(item) }} />
</div>
)
}
})}
<Nav.Sub
itemKey={"user"}
text={
<div
style={{
marginLeft: -8,
display: "inline-block",
color: "white",
}}
>
<Avatar size="extra-small" color="light-blue" style={{ marginRight: 4 }}>
{user?.name?.substr(0, 1)}
</Avatar>
<div style={{
display: "inline-block", position: "relative",
top: 10,
left: 4,
marginRight: 4,
}}
>
</div>
</div>
}
>
<div style={{ width: 133, }}>
<div style={{ display: "flex", alignItems: 'center', background: '#F0F5FF', padding: '8px 0px 8px 12px' }}>
<Avatar size="extra-small" color="light-blue" style={{ marginRight: 4 }}>
{user?.name?.substr(0, 1)}
</Avatar>
<div style={{ fontSize: 12, color: '#4A4A4A' }}>
{user.name}
<div title={user?.department?.map(v => v.name + '、')} style={{ width: 60, overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis', color: '#969799', fontSize: 12 }}>{user?.department?.map(v => v.name + '、')}</div>
</div>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', height: 30, borderBottom: '1px solid #F2F3F5', cursor: "pointer" }}
onClick={() => {
history.push(`/userCenter`);
}}>
<div style={{ width: 15, height: 15, marginLeft: 20 }}>
<img src="/assets/images/console/yonghu.png" alt="" style={{ width: '100%', height: '100%' }} />
</div>
<div style={{ color: '#646566', fontSize: 12, marginLeft: 9 }}>
用户中心
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', height: 30, borderBottom: '1px solid #F2F3F5', cursor: "pointer" }}>
<div style={{ width: 15, height: 15, marginLeft: 20 }}>
<img src="/assets/images/console/suo.png" alt="" style={{ width: '100%', height: '100%' }} />
</div>
<div style={{ color: '#646566', fontSize: 12, marginLeft: 9 }}>
安全认证
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: 27, color: '#646566', fontSize: 12, cursor: "pointer" }}
onClick={() => {
dispatch(actions.auth.logout(user));
if (socket) {
socket.disconnect();
}
history.push(`/signin`);
}}>
退出
{/* <Nav.Item itemKey={"logout"} text={"退出"} style={{ textAlign: "center" }} /> */}
</div>
{/* collapseButton collapseText */}
</Nav.Sub>
</>
}
/>
</div>
</>
);
};
function mapStateToProps (state) {
const { global, auth, webSocket } = state;
return {
actions: global.actions,
user: auth.user,
socket: webSocket.socket,
};
}
export default connect(mapStateToProps)(Header);

18
web/client/src/layout/components/header/index.less

@ -0,0 +1,18 @@
#top-slider{
.semi-navigation-item-text{
font-size: 13px;
color: #F2F3F5;
}
.semi-navigation-item-icon{
color: #F2F3F5;
}
.semi-navigation-item-selected{
background: none;
}
.semi-navigation-item{
margin: 0px;
}
.semi-navigation-item-text{
overflow: inherit;
}
}

86
web/client/src/layout/components/sider/index.jsx

@ -0,0 +1,86 @@
import React, { useEffect, useState } from 'react';
import PerfectScrollbar from 'perfect-scrollbar';
import { connect } from 'react-redux';
import { Nav } from '@douyinfe/semi-ui';
import { push } from 'react-router-redux';
import { useLocation } from 'react-router';
import "./index.less";
let scrollbar = null
const homePath = '/example/e1/c1'
const Sider = (props) => {
const { collapsed, clientHeight, dispatch, pathname, leftItems, leftChange } = props
const [items, setItems] = useState([])
const [selectedKeys, setSelectedKeys] = useState([])
const [openKeys, setOpenKeys] = useState([])
useEffect(() => {
const { sections, dispatch, user } = props;
let nextItems = leftItems
setItems(nextItems)
scrollbar = new PerfectScrollbar('#page-slider', { suppressScrollX: true });
if (pathname == '/') {
dispatch(push(homePath))
}
}, [leftItems])
let routeSelectedKey = [useLocation().pathname.split('/')[1]]//
let routeSelectedKeys = [useLocation().pathname.split('/')[2]]//
let routeSelectedKeyss = [useLocation().pathname.split('/')[3]]//
useEffect(() => {
if (routeSelectedKeyss[0]) {
setSelectedKeys(routeSelectedKeyss)
}
else if (routeSelectedKeys[0]) {
setSelectedKeys(routeSelectedKeys)
}
else {
setSelectedKeys(routeSelectedKey)
}
const lastOpenKeys = localStorage.getItem('poms_open_sider')
if (lastOpenKeys) {
setOpenKeys(JSON.parse(lastOpenKeys))
}
}, [window.localStorage.poms_open_sider, window.localStorage.poms_selected_sider, leftChange])
useEffect(() => {
if (scrollbar) {
scrollbar.update();
}
})
return (
<div id={'page-slider'} style={{ height: clientHeight, position: 'relative', background: '#101531' }}>
<Nav
style={{ background: '#101531', padding: 0 }}
selectedKeys={selectedKeys}
bodyStyle={{height:clientHeight-64}}
mode = "vertical"
footer={{
collapseButton: true,
}}
openKeys={openKeys}
onSelect={({ selectedItems, selectedKeys, }) => {
const selectItem = selectedItems[0]
if (selectItem.to) {
dispatch(push(selectItem.to))
}
let myfatherKey = selectItem.to
localStorage.setItem('poms_selected_sider', JSON.stringify([myfatherKey.split('/')[1]]))
setSelectedKeys(selectedKeys)
}}
onOpenChange={({ openKeys }) => {
setOpenKeys(openKeys)
localStorage.setItem('poms_open_sider', JSON.stringify(openKeys))
}}
items={items}
/>
</div>
)
}
function mapStateToProps (state) {
const { global } = state;
return {
clientHeight: global.clientHeight,
};
}
export default connect(mapStateToProps)(Sider);

40
web/client/src/layout/components/sider/index.less

@ -0,0 +1,40 @@
#page-slider{
.semi-navigation-sub .semi-navigation-item-selected{
background: rgba(0, 90, 189, 0.5);
color: #F2F3F5;
}
.semi-navigation-item-text{
color: #F2F3F5;
}
.semi-navigation-item-icon{
color:#F2F3F5
}
.semi-navigation-item-selected{
background: rgba(0, 90, 189, 0.5);
color: #F2F3F5;
}
.semi-navigation-item-inner{
font-family: YouSheBiaoTiHei;
font-size: 16px;
letter-spacing:2px;
font-weight: 400;
}
.semi-navigation-sub-title-selected{
background: #1D2343;
.semi-navigation-item-icon{
color: #0F7EFB !important;
}
.semi-icon-code{
color: #0F7EFB !important;
}
}
.semi-navigation{
width: 180px;
}
.semi-navigation-collapsed{
width: 48px !important;
}
.semi-button-with-icon{
color:#F2F3F5
}
}

6
web/client/src/layout/containers/index.js

@ -0,0 +1,6 @@
'use strict';
import Layout from './layout';
import NoMatch from './no-match';
export { Layout };
export { NoMatch };

321
web/client/src/layout/containers/layout/index.jsx

@ -0,0 +1,321 @@
'use strict';
import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import { Layout, Notification } from '@douyinfe/semi-ui';
import Sider from '../../components/sider';
import Header from '../../components/header';
import Footer from '../../components/footer';
import { resize } from '../../actions/global';
import * as NProgress from 'nprogress';
import PerfectScrollbar from 'perfect-scrollbar';
import { useLocation } from "react-router";
NProgress.configure({
template: `
<div class="bar" style="height:2px" role="bar">
<div class="peg"></div>
</div>
<div class="spinner" role="spinner">
<div class="spinner-icon"></div>
</div>
`
});
let scrollbar
// const location111 = useLocation();
const LayoutContainer = props => {
const {
dispatch, msg, user, copyright, children, sections, clientWidth, clientHeight,
location, match, routes, history, socket,
} = props
const [collapsed, setCollapsed] = useState(false)
NProgress.start();
const resize_ = () => {
dispatch(resize(
document.getElementById('PomsApp').clientHeight,
document.getElementById('PomsApp').clientWidth - (collapsed ? 120 : 240)
));
}
function deepCopy (data) {//
//string,number,bool,null,undefined,symbol
//object,array,date
if (data && typeof data === "object") {
//
if (typeof data === "function") {
let tempFunc = data.bind(null);
tempFunc.prototype = deepCopy(data.prototype);
return tempFunc;
}
switch (Object.prototype.toString.call(data)) {
case "[object String]":
return data.toString();
case "[object Number]":
return Number(data.toString());
case "[object Boolean]":
return new Boolean(data.toString());
case "[object Date]":
return new Date(data.getTime());
case "[object Array]":
var arr = [];
for (let i = 0; i < data.length; i++) {
arr[i] = deepCopy(data[i]);
}
return arr;
//js
case "[object Object]":
var obj = {};
for (let key in data) {
//hasOwnProperty obj.hasOwnProperty(prop)
obj[key] = deepCopy(data[key]);
}
return obj;
}
} else {
//string,number,bool,null,undefined,symbol
return data;
}
}
const [allItems, setAllItems] = useState([])
const [headerItems, setHeaderItems] = useState([])
const [leftItems, setLeftItems] = useState([])
const [leftChange, setLeftChange] = useState(true)
const [leftShow, setLeftShow] = useState(false)
useEffect(() => {
let topItems = []//
const lastSelectedKeys = localStorage.getItem('poms_selected_sider')
let nextItems = []//
for (let c of sections) {
if (typeof c.getNavItem == 'function') {
let item = c.getNavItem(user, dispatch);
if (item) {
nextItems.push.apply(nextItems, item)
if (item.length > 0) {
for (let j = 0; j < item.length; j++) {
let itm = deepCopy(item[j]);
if (itm.hasOwnProperty('items')) {
for (let i = 0; i < itm.items.length; i++) {
itm.items[i].fatherKey = itm.itemKey
delete itm.items[i].items
}
topItems.push(itm)
}
else {
topItems.push.apply(topItems, item)
}
}
}
}
}
}
setAllItems(nextItems)
setHeaderItems(topItems)
if (lastSelectedKeys) {//
for (let i = 0; i < nextItems.length; i++) {
if (JSON.parse(lastSelectedKeys)[0] == nextItems[i].itemKey) {
let openArr = []
for (let j = 0; j < nextItems[i].items.length; j++) {
openArr.push(nextItems[i].items[j].itemKey)
}
localStorage.setItem('poms_open_sider', JSON.stringify(openArr))
setLeftItems(nextItems[i].items)
}
}
setLeftShow(true)
}
else {
setLeftShow(false)
}
window.addEventListener('resize', resize_);
return () => {
window.removeEventListener('resize', resize_);
}
}, [])
useEffect(() => {
let pathnameArr = location.pathname.split('/')
let openArr = []
for (let i = 0; i < allItems.length; i++) {
if (allItems[i].items) {
for (let j = 0; j < allItems[i].items.length; j++) {
if (allItems[i].items[j].items) {
for (let k = 0; k < allItems[i].items[j].items.length; k++) {
if (allItems[i].items[j].items[k].to == location.pathname) {
for (let o = 0; o < allItems[i].items.length; o++) {
openArr.push(allItems[i].items[o].itemKey)
}
localStorage.setItem('poms_selected_sider', JSON.stringify([pathnameArr[1]]))
localStorage.setItem('poms_open_sider', JSON.stringify(openArr))
setLeftItems(allItems[i].items)
setLeftShow(true)
}
}
}
}
}
}
}, [location])
useEffect(() => {
NProgress.done();
if ((!user || !user.authorized)) {
history.push('/signin');
}
if (msg) {
if (msg.done) {
Notification.success({
// title: msg.done,
content: msg.done,
duration: 2,
})
}
if (msg.error) {
Notification.error({
// title: msg.error,
content: msg.error,
duration: 2,
})
}
}
const dom = document.getElementById('page-content');
if (dom) {
if (!scrollbar) {
scrollbar = new PerfectScrollbar('#page-content', { suppressScrollX: true });
scrollbar.update();
} else {
scrollbar.update();
dom.scrollTop = 0;
}
}
})
// websocket 使
useEffect(() => {
// console.log(socket)
if (socket) {
socket.on('CAMERA_ONLINE', function (msg) {
console.info(msg);
if (msg.online == 'ON') {
Notification.success({
title: 'Hi',
content: (<div><div>{msg.name}</div><div style={{ marginTop: 5 }}>已上线</div></div>),
duration: 2,
})
}
if (msg.online == 'OFF') {
Notification.error({
title: 'Hi',
content: (<div><div>{msg.name}</div><div style={{ marginTop: 5 }}>发生离线</div></div>),
duration: 2,
})
}
});
return () => {
socket.off("CAMERA_ONLINE");
}
}
}, [socket])
return (
<Layout id="layout" style={{ height: '100%' }}>
{
<>
<Layout.Header>
<Header
headerItems={headerItems}
user={user}
pathname={location.pathname}
toggleCollapsed={() => {
setCollapsed(!collapsed);
}}
collapsed={collapsed}
history={history}
tochange={(val) => {
setLeftChange(!leftChange)
if (val.fatherKey) {
for (let i = 0; i < allItems.length; i++) {
if (val.fatherKey == allItems[i].itemKey) {
let openArr = []
for (let j = 0; j < allItems[i].items.length; j++) {
openArr.push(allItems[i].items[j].itemKey)
}
localStorage.setItem('poms_selected_sider', JSON.stringify([val.fatherKey]))
localStorage.setItem('poms_open_sider', JSON.stringify(openArr))
setLeftItems(allItems[i].items)
}
}
setLeftShow(true)
}
else {
localStorage.setItem('poms_open_sider', JSON.stringify([]))
localStorage.removeItem('poms_selected_sider')
setLeftShow(false)
}
history.push(val.to);
}}
/>
</Layout.Header>
<Layout style={{ height: 'calc(100% - 50px)' }}>
{leftShow ? (<Layout.Sider>
<Sider
sections={sections}
leftItems={leftItems}
dispatch={dispatch}
user={user}
leftChange={leftChange}
pathname={location.pathname}
collapsed={collapsed}
/>
</Layout.Sider>) : ('')}
<Layout.Content>
<div style={{
background: "#F2F3F5",
}}>
<div id="page-content" style={{
height: clientHeight - 12,
minWidth: 520,
position: 'relative',
}}>
<div style={{
minHeight: clientHeight - 32 - 12,
position: 'relative',
padding: '12px 8px',
}}>
{children}
</div>
<Layout.Footer>
<Footer />
</Layout.Footer>
</div>
</div>
</Layout.Content>
</Layout>
</>
}
</Layout >
)
}
function mapStateToProps (state) {
const { global, auth, ajaxResponse, webSocket } = state;
return {
title: global.title,
copyright: global.copyright,
sections: global.sections,
actions: global.actions,
clientWidth: global.clientWidth,
clientHeight: global.clientHeight,
msg: ajaxResponse.msg,
user: auth.user,
socket: webSocket.socket
};
}
export default connect(mapStateToProps)(LayoutContainer);

18
web/client/src/layout/containers/no-match/index.jsx

@ -0,0 +1,18 @@
'use strict';
import React from 'react';
import moment from 'moment'
const NoMatch = props => {
return (
<div style={{ textAlign: 'center', padding: 120 }}>
<p style={{ fontSize: 80, lineHeight: 1.5 }}>404</p>
<p style={{ fontSize: 32, lineHeight: 2 }}>PAGE NOT FOUND</p>
<p>很遗憾您暂时无法访问该页面</p>
<p>请检查您访问的链接地址是否正确</p>
<p style={{ marginTop: 80 }}>Copyright © {moment().year()} 飞尚</p>
</div>
)
}
export default NoMatch;

188
web/client/src/layout/index.jsx

@ -0,0 +1,188 @@
'use strict';
import React, { useEffect, useState } from 'react';
import moment from 'moment';
import configStore from './store';
import { Provider } from 'react-redux';
import { createBrowserHistory } from 'history';
import { ConnectedRouter } from 'connected-react-router'
import { Layout, NoMatch } from './containers';
import { BrowserRouter, Switch, Route } from "react-router-dom";
import { ConfigProvider } from '@douyinfe/semi-ui';
import layoutActions from './actions';
import zhCN from '@douyinfe/semi-ui/lib/es/locale/source/zh_CN';
import { basicReducer } from '@peace/utils';
import { push } from 'react-router-redux';
import 'moment/locale/zh-cn';
moment.locale('zh-cn');
const { initLayout, initApiRoot, resize, initWebSocket } = layoutActions;
const Root = props => {
const { sections, title, copyright } = props;
const [history, setHistory] = useState(null)
const [store, setStore] = useState(null)
const [outerRoutes, setOuterRoutes] = useState([])
const [combineRoutes, setCombineRoutes] = useState([])
const [innnerRoutes, setInnerRoutes] = useState([])
const flatRoutes = (routes) => {
const combineRoutes = [];
function flat (routes, parentRoute) {
routes.forEach((route, i) => {
let obj = {
path: route.path,
breadcrumb: route.breadcrumb,
component: route.component || null,
authCode: route.authCode || '',
key: route.key
}
if (!route.path.startsWith("/")) {
console.error('路由配置需以 "/" 开始:' + route.path);
}
if (route.path.length > 1 && route.path[route.path.length] == '/') {
console.error('除根路由路由配置不可以以 "/" 结束:' + route.path);
}
if (parentRoute && parentRoute != '/') {
obj.path = parentRoute + route.path;
}
if (route.exact) {
obj.exact = true
}
if (route.hasOwnProperty('childRoutes')) {
combineRoutes.push(obj);
flat(route.childRoutes, obj.path)
} else {
combineRoutes.push(obj)
}
})
}
flat(routes);
return combineRoutes;
}
const initReducer = (reducers, reducerName, action) => {
let reducerParams = {}
const { actionType, initReducer, reducer } = action()()
if (initReducer || reducer) {
if (reducer) {
if (reducer.name) {
reducerName = reducer.name
}
if (reducer.params) {
reducerParams = reducer.params
}
} else {
reducerName = `${reducerName}Rslt`
}
reducers[reducerName] = function (state, action) {
return basicReducer(state, action, Object.assign({ actionType: actionType }, reducerParams));
}
}
}
useEffect(async () => {
let innerRoutes = []
let outerRoutes = []
let reducers = {}
let actions = {
layout: layoutActions
}
for (let s of sections) {
if (!s.key) console.warn('请给你的section添加一个key值,section name:' + s.name);
for (let r of s.routes) {
if (r.type == 'inner' || r.type == 'home') {
innerRoutes.push(r.route)
} else if (r.type == 'outer') {
outerRoutes.push(r.route)
}
}
if (s.reducers) {
reducers = { ...reducers, ...s.reducers }
}
if (s.actions) {
actions = { ...actions, [s.key]: s.actions }
if (s.key != 'auth') {
for (let ak in s.actions) {
let actions = s.actions[ak]
if (actions && typeof actions == 'object') {
for (let actionName in actions) {
initReducer(reducers, actionName, actions[actionName])
}
} else if (typeof actions == 'function') {
initReducer(reducers, ak, actions)
}
}
}
}
}
let history = createBrowserHistory();
let store = configStore(reducers, history);
store.dispatch(initLayout(title, copyright, sections, actions));
// store.dispatch(resize(document.body.clientHeight, document.body.clientWidth));
store.dispatch(resize(document.getElementById('PomsApp').clientHeight, document.getElementById('PomsApp').clientWidth));
store.dispatch(actions.auth.initAuth());
const resourceRoot = await store.dispatch(initApiRoot())
store.dispatch(initWebSocket({}))
const combineRoutes = flatRoutes(innerRoutes);
setInnerRoutes(combineRoutes)
setHistory(history)
setStore(store)
setOuterRoutes(outerRoutes.map(route => (
<Route
key={route.key}
exact
path={route.path}
component={route.component}
/>
)))
setCombineRoutes(combineRoutes.map(route => (
<Route
key={route.key}
exact={route.hasOwnProperty('exact') ? route.exact : true}
path={route.path}
component={route.component}
/>
)))
}, [])
return (
<>
{
store ?
<ConfigProvider locale={zhCN}>
<Provider store={store}>
<BrowserRouter basename={window.__MICRO_APP_BASE_ROUTE__ || '/'}>
<ConnectedRouter history={history}>
<Switch>
{outerRoutes}
<Layout
history={history}
routes={innnerRoutes}
>
{combineRoutes}
</Layout>
<Route
path={'*'}
component={NoMatch}
/>
</Switch>
</ConnectedRouter>
</BrowserRouter>
</Provider>
</ConfigProvider>
: ''
}
</>
)
}
export default Root;

28
web/client/src/layout/reducers/ajaxResponse.js

@ -0,0 +1,28 @@
/**
* Created by liu.xinyi
* on 2016/4/1.
*/
'use strict';
const initState = {
msg: null
};
import Immutable from 'immutable';
/**
* 全局ajax响应处理
* 判断action中是否有done字段如果有则修改store中的msg.done
* 判断action中是否有error字段如果有则修改store中msg.error
* 在layout中根据msg的值呈现提示信息
*/
export default function ajaxResponse (state = initState, action) {
if (action.done) {
return Immutable.fromJS(state).set('msg', { done: action.done }).toJS();
}
if (action.error) {
return Immutable.fromJS(state).set('msg', { error: action.error }).toJS();
}
return { msg: null };
};

41
web/client/src/layout/reducers/global.js

@ -0,0 +1,41 @@
'use strict';
import Immutable from 'immutable';
import { INIT_LAYOUT, RESIZE, INIT_API_ROOT } from '../actions/global';
function global (state = {
title: '',
copyright: '',
sections: [],
actions: {},
plugins: {},
clientHeight: 768,
clientWidth: 1024,
apiRoot: '',
iotVcmpWeb: '',
}, action) {
const payload = action.payload;
switch (action.type) {
case RESIZE:
return Immutable.fromJS(state).merge({
clientHeight: payload.clientHeight,
clientWidth: payload.clientWidth
}).toJS();
case INIT_LAYOUT:
return Immutable.fromJS(state).merge({
title: payload.title,
copyright: payload.copyright,
sections: payload.sections,
actions: payload.actions,
plugins: payload.plugins,
}).toJS();
case INIT_API_ROOT:
return Immutable.fromJS(state).merge({
apiRoot: payload.apiRoot,
iotVcmpWeb: payload.iotVcmpWeb
}).toJS();
default:
return state;
}
}
export default global;

17
web/client/src/layout/reducers/index.js

@ -0,0 +1,17 @@
/**
* User: liuxinyi/liu.xinyi@free-sun.com.cn
* Date: 2016/1/13
* Time: 17:52
*
*/
'use strict';
import global from './global';
import webSocket from './webSocket'
import ajaxResponse from './ajaxResponse';
export default {
global,
webSocket,
ajaxResponse,
};

21
web/client/src/layout/reducers/webSocket.js

@ -0,0 +1,21 @@
'use strict';
import * as actionTypes from '../actions/webSocket';
import Immutable from 'immutable';
const initState = {
socket: null,
};
function webSocket (state = initState, action) {
const payload = action.payload;
switch (action.type) {
case actionTypes.INIT_WEB_SOCKET:
return Immutable.fromJS(state).merge({
socket: payload.socket,
}).toJS();
default:
return state;
}
}
export default webSocket;

13
web/client/src/layout/store/index.js

@ -0,0 +1,13 @@
'use strict';
import storeProd from './store.prod'
import storeDev from './store.dev'
let store = null;
if (process.env.NODE_ENV == 'production') {
store = storeProd;
} else {
store = storeDev;
}
export default store;

30
web/client/src/layout/store/store.dev.js

@ -0,0 +1,30 @@
/**
* Created by liu.xinyi
* on 2016/4/8.
*/
'use strict';
import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
import reduxThunk from 'redux-thunk';
import { connectRouter, routerMiddleware } from 'connected-react-router';
import innerReducers from '../reducers';
function configStore(reducers, history) {
const reducer = Object.assign({}, innerReducers, reducers, {
router: connectRouter(history)
});
const composeEnhancers =
typeof window === 'object' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
// Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
}) : compose;
const enhancers = composeEnhancers(
applyMiddleware(routerMiddleware(history), reduxThunk)
);
return createStore(combineReducers(reducer), {}, enhancers);
}
export default configStore;

20
web/client/src/layout/store/store.prod.js

@ -0,0 +1,20 @@
/**
* Created by liu.xinyi
* on 2016/4/8.
*/
'use strict';
import { createStore, combineReducers, applyMiddleware } from 'redux';
import reduxThunk from 'redux-thunk';
import { connectRouter, routerMiddleware } from 'connected-react-router';
import innerReducers from '../reducers';
function configStore(reducers, history){
const reducer = Object.assign({}, innerReducers, reducers, {
router: connectRouter(history)
});
return createStore(combineReducers(reducer), {}, applyMiddleware(routerMiddleware(history), reduxThunk));
}
export default configStore;

7
web/client/src/public-path.js

@ -0,0 +1,7 @@
// __MICRO_APP_ENVIRONMENT__和__MICRO_APP_PUBLIC_PATH__是由micro-app注入的全局变量
if (window.__MICRO_APP_ENVIRONMENT__) {
// eslint-disable-next-line
__webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__
} else {
__webpack_public_path__ = '/'
}

83
web/client/src/sections/auth/actions/auth.js

@ -0,0 +1,83 @@
'use strict';
import { ApiTable, AxyRequest, EmisRequest } from '$utils'
import { Request } from '@peace/utils';
export const INIT_AUTH = 'INIT_AUTH';
export function initAuth (userData) {
const sessionUser = JSON.parse(sessionStorage.getItem('pomsUser'))
const user = userData || sessionUser || {};
if (user.authorized && !sessionUser) {
sessionStorage.setItem('pomsUser', JSON.stringify(user))
}
return {
type: INIT_AUTH,
payload: {
user: user
}
};
}
export const REQUEST_LOGIN = 'REQUEST_LOGIN';
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
export const LOGIN_ERROR = 'LOGIN_ERROR';
export function login (username, password) {
return dispatch => {
dispatch({ type: REQUEST_LOGIN });
if (!username || !password) {
dispatch({
type: LOGIN_ERROR,
payload: { error: '请输入账号名和密码' }
});
return Promise.resolve();
}
// return dispatch({
// type: LOGIN_SUCCESS,
// payload: {
// user: {
// authorized: true,
// namePresent: 'TEST'
// }
// },
// });
return Request.post(ApiTable.login, { username, password, code: 'POMS' })
.then(user => {
sessionStorage.setItem('pomsUser', JSON.stringify(user));
return dispatch({
type: LOGIN_SUCCESS,
payload: { user: user },
});
}, error => {
let { body } = error.response;
return dispatch({
type: LOGIN_ERROR,
payload: {
error: body && body.message ? body.message : '登录失败'
}
})
});
}
}
export const LOGOUT = 'LOGOUT';
export function logout () {
const user = JSON.parse(sessionStorage.getItem('pomsUser'))
user && user.token ?
Request.put(ApiTable.logout, {
token: user.token,
code: 'POMS'
}) : null;
sessionStorage.removeItem('pomsUser');
return {
type: LOGOUT
};
}
export default {
initAuth,
login,
logout
}

7
web/client/src/sections/auth/actions/index.js

@ -0,0 +1,7 @@
'use strict';
import auth from './auth';
export default {
...auth
};

4
web/client/src/sections/auth/containers/index.js

@ -0,0 +1,4 @@
'use strict';
import Login from './login';
export { Login };

143
web/client/src/sections/auth/containers/login.jsx

@ -0,0 +1,143 @@
'use strict';
import React, { useEffect, useRef } from 'react';
import { connect } from 'react-redux';
import { push } from 'react-router-redux';
import { Form, Button, Toast } from '@douyinfe/semi-ui';
import { login, LOGIN_SUCCESS } from '../actions/auth';
import { IconLock, IconUser } from '@douyinfe/semi-icons';
import '../style.less'
const Login = props => {
const { dispatch, user, error, actions, apiRoot, isRequesting } = props
const form = useRef();
useEffect(() => {
if (error) {
Toast.error(error);
form.current.setValue('password', '')
}
}, [error])
useEffect(() => {
if (user && user.authorized) {
dispatch(push('/console'));
localStorage.setItem('poms_open_sider', JSON.stringify([]))
localStorage.removeItem('poms_selected_sider')
}
}, [user])
return (
<div style={{
width: '100%',
height: '100%',
// backgroundImage: "url('/assets/images/background/loginBackground.gif')",
// backgroundSize: 'cover',
// backgroundRepeat: 'no-repeat',
// position: 'relative',
}}>
<video
autoPlay loop muted
style={{
width: 'calc(100% - 512px)', objectFit: "fill", objectPosition: 'left top', height: '100%'
}}
src="/assets/video/login_bg.mp4"
type="video/mp4"
/>
{/* <img src='/assets/images/background/loginBackground.gif' style={{
width: "100%",
height: 'calc(100vh - 4px)',
objectFit: "cover",
objectPosition: 'left top',
position: 'absolute',
top: 0,
left: 0,
zIndex: "5"
}} /> */}
<div style={{
width: 600,
height: 90,
objectFit: "cover",
objectPosition: 'left top',
position: 'absolute',
top: 100,
left: 41,
zIndex: 5,
fontSize: 14,
color: ' #005ABD',
display:'flex',
flexDirection: 'column',
justifyContent: 'space-between',
}}>
<div><span style={{ fontFamily: 'YouSheBiaoTiHei', }}>POMS运维中台管理系统</span> 为企业数字化转型提供安全稳定高效的数字化运维底座</div>
<div><span style={{ fontFamily: 'YouSheBiaoTiHei', }}>高效</span> 为运维智能化升级提供领先的创新技术和平台管理</div>
<div><span style={{ fontFamily: 'YouSheBiaoTiHei', }}>智能</span>助力数字化转型和智能化升级一步到位</div>
</div>
<div style={{
width: 512,
height: '100%',
padding: '45px 60px',
background: 'rgb(255 255 255 / 50%)',
backdropFilter: "saturate(100%) contrast(100%) blur(17px)",
position: 'absolute',
top: 0,
right: 0,
zIndex: "6",
textAlign: 'center',
}}>
<div style={{
width: 388,
marginTop: "40%"
}}>
<img src="/assets/images/background/user_login.png" alt="" style={{ width: 324, height: 24, marginBottom: 71 }} />
<Form
onSubmit={values => {
dispatch(login(values.username, values.password)).then(res => {
const data = res.payload.user
dispatch(actions.layout.initWebSocket({ ioUrl: apiRoot, token: data.token, pomsUserId: data.pomsUserInfo.id }))
})
}}
getFormApi={formApi => form.current = formApi}
>
<Form.Input
className='inputbgc'
field='username'
noLabel={true}
label='用户名'
placeholder='请输入账号'
prefix={<IconUser style={{ color: '#1859C1', marginRight: 14, marginLeft: 8 }} />}
style={{ background: '#FFFFFF', height: 46, marginBottom: 33, border: '1px solid rgb(185 211 239)', borderRadius: '4px' }}
/>
<Form.Input
field='password'
noLabel={true}
mode="password"
autoComplete=""
placeholder='请输入密码'
label='密码'
prefix={<IconLock style={{ color: '#1859C1', marginRight: 14, marginLeft: 8 }} />}
style={{ background: '#FFFFFF', height: 46, border: '1px solid rgb(185 211 239)', borderRadius: '4px' }}
/>
<img src="/assets/images/background/xiangqi.png" style={{ width: 112, height: 14, margin: "4px 0 0 278px" }} />
<Button htmlType='submit' block theme="solid" loading={isRequesting} style={{ marginTop: 56, height: 46, backgroundColor: '#005ABD', borderRadius: '3px' }}>立即登录</Button>
</Form>
</div>
</div>
</div>
);
}
function mapStateToProps (state) {
const { auth, global } = state;
return {
user: auth.user,
error: auth.error,
actions: global.actions,
apiRoot: global.apiRoot,
isRequesting: auth.isRequesting
}
}
export default connect(mapStateToProps)(Login);

12
web/client/src/sections/auth/index.js

@ -0,0 +1,12 @@
'use strict';
import routes from './routes';
import reducers from './reducers';
import actions from './actions';
export default {
key: 'auth',
reducers: reducers,
routes: routes,
actions: actions
};

40
web/client/src/sections/auth/reducers/auth.js

@ -0,0 +1,40 @@
'use strict';
import * as actionTypes from '../actions/auth';
import Immutable from 'immutable';
const initState = {
user: {},
isRequesting: false,
error: null
};
function auth (state = initState, action) {
const payload = action.payload;
switch (action.type) {
case actionTypes.INIT_AUTH:
return Immutable.fromJS(state).set('user', payload.user).toJS();
case actionTypes.REQUEST_LOGIN:
return Immutable.fromJS(state).merge({
isRequesting: true,
error: null
}).toJS();
case actionTypes.LOGIN_SUCCESS:
return Immutable.fromJS(state).merge({
isRequesting: false,
user: payload.user
}).toJS();
case actionTypes.LOGIN_ERROR:
return Immutable.fromJS(state).merge({
isRequesting: false,
error: payload.error
}).toJS();
case actionTypes.LOGOUT:
return Immutable.fromJS(state).merge({
user: null
}).toJS();
default:
return state;
}
}
export default auth;

6
web/client/src/sections/auth/reducers/index.js

@ -0,0 +1,6 @@
'use strict';
import auth from './auth'
export default {
auth
};

12
web/client/src/sections/auth/routes.js

@ -0,0 +1,12 @@
'use strict';
import { Login } from './containers';
export default [{
type: 'outer',
route: {
key:'signin',
path: "/signin",
component: Login
}
}];

7
web/client/src/sections/auth/style.less

@ -0,0 +1,7 @@
input:-webkit-autofill{
-webkit-text-fill-color:black !important;
-webkit-box-shadow: 0 0 0px 1000px transparent inset !important;
box-shadow: 0 0 0px 1000px transparent inset !important;
background-color:transparent;
transition: background-color 50000s ease-in-out 0s;
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save