Browse Source

initialization

tzjzzhang 1 year ago
parent
commit
33d4e39327
100 changed files with 5854 additions and 0 deletions
  1. 3 0
      .browserslistrc
  2. 14 0
      .editorconfig
  3. 80 0
      .env
  4. 14 0
      .env.alpha
  5. 31 0
      .env.development
  6. 19 0
      .env.fujian
  7. 15 0
      .env.inspur
  8. 18 0
      .env.rc
  9. 10 0
      .env.sc
  10. 6 0
      .eslintignore
  11. 277 0
      .eslintrc.js
  12. 25 0
      .gitignore
  13. 1 0
      .nvmrc
  14. 8 0
      README.md
  15. 5 0
      babel.config.js
  16. 5 0
      commitlint.config.js
  17. 37 0
      feature.js
  18. 130 0
      keycloak-theme/login/error.ftl
  19. 137 0
      keycloak-theme/login/info.ftl
  20. 230 0
      keycloak-theme/login/login-config-totp.ftl
  21. 192 0
      keycloak-theme/login/login-otp.ftl
  22. 14 0
      keycloak-theme/login/login-page-expired.ftl
  23. 276 0
      keycloak-theme/login/login-update-password.ftl
  24. 262 0
      keycloak-theme/login/login.ftl
  25. 32 0
      keycloak-theme/login/messages/messages_zh_CN.properties
  26. BIN
      keycloak-theme/login/resources/img/applet.png
  27. BIN
      keycloak-theme/login/resources/img/icon_lock.png
  28. BIN
      keycloak-theme/login/resources/img/icon_user.png
  29. BIN
      keycloak-theme/login/resources/img/illustration.png
  30. BIN
      keycloak-theme/login/resources/img/logo/fujian.png
  31. BIN
      keycloak-theme/login/resources/img/logo/inspur.png
  32. 0 0
      keycloak-theme/login/resources/js/md5.js
  33. 1 0
      keycloak-theme/login/theme.properties
  34. 78 0
      mock/mock-server.js
  35. 46 0
      mock/proxy.js
  36. 79 0
      package.json
  37. BIN
      platform/fav/fujian.ico
  38. BIN
      platform/fav/inspur.ico
  39. BIN
      platform/logo/fujian.png
  40. BIN
      platform/logo/inspur.png
  41. 19 0
      public/hash.js
  42. 96 0
      public/index.html
  43. 26 0
      public/mediainfo.js
  44. 13 0
      src/App.vue
  45. 461 0
      src/api/asset.js
  46. 150 0
      src/api/base.js
  47. 163 0
      src/api/calendar.js
  48. 54 0
      src/api/camera.js
  49. 638 0
      src/api/device.js
  50. 505 0
      src/api/external.js
  51. 136 0
      src/api/merchant.js
  52. 242 0
      src/api/mesh.js
  53. 273 0
      src/api/platform.js
  54. 133 0
      src/api/program.js
  55. 72 0
      src/api/statistics.js
  56. 55 0
      src/api/unified.js
  57. 487 0
      src/api/user.js
  58. 78 0
      src/api/workflow.js
  59. 109 0
      src/app.js
  60. BIN
      src/assets/bg_big.png
  61. BIN
      src/assets/bg_top_s.png
  62. BIN
      src/assets/dot.png
  63. BIN
      src/assets/icon_applet.png
  64. BIN
      src/assets/icon_avatar.png
  65. 15 0
      src/assets/icon_camera.svg
  66. 15 0
      src/assets/icon_camera_white.svg
  67. BIN
      src/assets/icon_card_abnormal.png
  68. BIN
      src/assets/icon_card_normal.png
  69. BIN
      src/assets/icon_card_unknown.png
  70. BIN
      src/assets/icon_condition.png
  71. BIN
      src/assets/icon_date.png
  72. BIN
      src/assets/icon_delete.png
  73. BIN
      src/assets/icon_device_reboot.png
  74. BIN
      src/assets/icon_device_restore.png
  75. BIN
      src/assets/icon_device_switch.png
  76. BIN
      src/assets/icon_device_time.png
  77. BIN
      src/assets/icon_download.png
  78. BIN
      src/assets/icon_edit.png
  79. BIN
      src/assets/icon_enlarge.png
  80. BIN
      src/assets/icon_flooding.png
  81. BIN
      src/assets/icon_image.png
  82. BIN
      src/assets/icon_info.png
  83. BIN
      src/assets/icon_inform.png
  84. BIN
      src/assets/icon_light.png
  85. BIN
      src/assets/icon_log.png
  86. BIN
      src/assets/icon_narrow.png
  87. 16 0
      src/assets/icon_off.svg
  88. 16 0
      src/assets/icon_on.svg
  89. 21 0
      src/assets/icon_on_2.svg
  90. BIN
      src/assets/icon_online.png
  91. BIN
      src/assets/icon_performance.png
  92. BIN
      src/assets/icon_refresh.png
  93. BIN
      src/assets/icon_safety.png
  94. BIN
      src/assets/icon_screen_light.png
  95. BIN
      src/assets/icon_screen_network.png
  96. BIN
      src/assets/icon_screen_switch.png
  97. BIN
      src/assets/icon_screen_volume.png
  98. BIN
      src/assets/icon_screenshot.png
  99. 16 0
      src/assets/icon_screenshot.svg
  100. BIN
      src/assets/icon_setting.png

+ 3 - 0
.browserslistrc

@@ -0,0 +1,3 @@
+> 1%
+last 2 versions
+not dead

+ 14 - 0
.editorconfig

@@ -0,0 +1,14 @@
+# https://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+insert_final_newline = false
+trim_trailing_whitespace = false

+ 80 - 0
.env

@@ -0,0 +1,80 @@
+ENV = 'stable'
+
+PLATFORM = 'inspur'
+
+LOGGER = 'disabled'
+
+# 预览功能
+__FEATURE__ = 'disabled'
+# 备份设备
+__SUB_DEVICE__ = 'disabled'
+# 设备接管
+__TAKEOVER__ = 'disabled'
+# 短信
+__ALARM_SMS__ = 'disabled'
+# 邮件
+__ALARM_EMAIL__ = 'disabled'
+# 微信公众号
+__ALARM_WECHAT_OFFICIAL__ = 'disabled'
+# 微信小程序
+__ALARM_WECHAT_APPLAT__ = 'disabled'
+VUE_APP_AD_ORDER_QR = 'https://msr.inspur.com'
+VUE_APP_APPLET_PAGE = 'pages/device/device'
+# 跳审
+__JUMP_REVIEW__ = 'enabled'
+# 应急广播平台
+__EMERGENCY_PLATFORM__ = 'disabled'
+
+VUE_APP_NAME = '浪潮屏媒安播云平台'
+
+# gateway
+VUE_APP_GATEWAY = ''
+
+# base api
+VUE_APP_BASE_API = '/prod-api'
+
+# minio
+VUE_APP_MINIO = '/oss-api'
+
+# thumbnail
+VUE_APP_THUMBNAIL = '/api/imageproxy'
+VUE_APP_THUMBNAIL_ORIGIN = ''
+
+# keycloak
+# 默认需要realm-manager下的view-user,用于查看用户信息
+# 管理员需要realm-manager下的view-realm,用于查看租户、角色、凭证等信息
+VUE_APP_KEYCLOAK_OPTIONS_URL = '/auth'
+VUE_APP_KEYCLOAK_OPTIONS_REALM = 'smsb'
+VUE_APP_KEYCLOAK_OPTIONS_CLIENTID = 'frontend-api'
+VUE_APP_KEYCLOAK_OPTIONS_ONLOAD = 'login-required'
+
+# mqtt
+# {protocol}://{gateway | host}{proxy}
+VUE_APP_MQTT_PROXY = '/mqtt'
+VUE_APP_MQTT_USER_NAME = 'frontend'
+VUE_APP_MQTT_PASSWORD = 'inspur-frontend'
+
+# camera
+# {protocol}://{gateway | host}{proxy}
+VUE_APP_CAMERA_PROXY = '/prod-api/websocket'
+VUE_APP_CAMERA_EVS_PROXY = '/prod-api/dahuaEVSWebsocket'
+VUE_APP_CAMERA_RECORD_PROXY = '/prod-api/playBackWebSocket'
+
+# user default password
+# MD5('123456a?' + MD5('123456a?'))
+# VUE_APP_USER_PASSWORD = '1fc3fdb8d13bd4e2d1336f6dd377ee4f'
+#MD5('LCaby@2024?' + MD5('123456a?'))
+VUE_APP_USER_PASSWORD = '503167abe652d1476b888762e95b5782'
+# MD5('123456a?')
+VUE_APP_SALT = '42857cfddb33f3fddb27fff9773683f3'
+
+# gaode
+# VUE_APP_GAODE_MAP_KEY = '9c499e7000d066c05de9af8556a890f7'
+# VUE_APP_GAODE_MAP_JSCODE = 'e7b3c29a5112657edcc688a3c589bd15'
+VUE_APP_GAODE_MAP_KEY = 'd450b58ff3a620e5eaf477e29c7ea94c'
+VUE_APP_GAODE_MAP_JSCODE = '92a333ddc89042aee3e3459162f0abe6'
+VUE_APP_GAODE_MAP_RANGE_LEVEL = 'country'
+VUE_APP_GAODE_MAP_RANGE = '中国'
+# 离线地图访问地址,地址下需存放各图片
+# roadmap 街道图 satellite 卫星图 overlay 混合图
+VUE_APP_OFFLINE_MAP = './tiles'

+ 14 - 0
.env.alpha

@@ -0,0 +1,14 @@
+NODE_ENV = 'production'
+
+ENV = 'alpha'
+
+LOGGER = 'enabled'
+
+# 短信
+__ALARM_SMS__ = 'enabled'
+# 邮件
+__ALARM_EMAIL__ = 'enabled'
+# 微信公众号
+__ALARM_WECHAT_OFFICIAL__ = 'enabled'
+# 微信小程序
+__ALARM_WECHAT_APPLAT__ = 'enabled'

+ 31 - 0
.env.development

@@ -0,0 +1,31 @@
+ENV = 'alpha'
+
+VUE_APP_BASE_API = '/dev-api'
+
+VUE_APP_MINIO = '/dev-api/oss-api'
+
+VUE_APP_THUMBNAIL = '/dev-api/api/imageproxy'
+
+# 离线地图配置
+VUE_APP_OFFLINE_MAP = '/dev-api/offline-map'
+VUE_APP_OFFLINE_MAP_GATEWAY = '127.0.0.1'
+VUE_APP_OFFLINE_MAP_PROXY = '/tiles'
+
+# 84服务器 10.180.88.84:8093
+# VUE_APP_GATEWAY = 'isoc.artaplay.com:8443'
+# VUE_APP_KEYCLOAK_OPTIONS_URL = 'https://isoc.artaplay.com:8443/auth'
+# 71服务器 10.180.88.71:6443
+# VUE_APP_GATEWAY = 'isoc.artaplay.com:7443'
+# VUE_APP_KEYCLOAK_OPTIONS_URL = 'https://isoc.artaplay.com:8443/auth'
+# 110.5服务器
+# VUE_APP_GATEWAY = '10.58.110.5'
+# VUE_APP_KEYCLOAK_OPTIONS_REALM = 'smsb-test'
+# 浪潮云
+#VUE_APP_GATEWAY = 'msr.inspur.com:8084'
+# VUE_APP_KEYCLOAK_OPTIONS_URL = 'https://msr.inspur.com:8084/auth'
+# 深圳办公环境 测试
+# VUE_APP_GATEWAY = '10.180.88.84:8093'
+# VUE_APP_KEYCLOAK_OPTIONS_URL = 'http://10.180.88.84:8093/auth'
+# 屏管家
+ VUE_APP_GATEWAY = '172.31.4.238'
+ VUE_APP_KEYCLOAK_OPTIONS_URL = 'http://172.31.4.238/auth'

+ 19 - 0
.env.fujian

@@ -0,0 +1,19 @@
+NODE_ENV = 'production'
+
+ENV = 'fujian'
+
+PLATFORM = 'fujian'
+
+# 短信
+__ALARM_SMS__ = 'enabled'
+
+# 下列配置块仅开启一个
+# 万福千屏
+# VUE_APP_NAME = '万福千屏户外安播云平台'
+# VUE_APP_THUMBNAIL_ORIGIN = 'http://192.168.10.10'
+# 五个一百
+VUE_APP_NAME = '安全宣教联播联控系统'
+__EMERGENCY_PLATFORM__ = 'enabled'
+
+VUE_APP_GAODE_MAP_RANGE_LEVEL = 'province'
+VUE_APP_GAODE_MAP_RANGE = '福建省'

+ 15 - 0
.env.inspur

@@ -0,0 +1,15 @@
+NODE_ENV = 'production'
+
+ENV = 'inspur'
+
+# 短信
+__ALARM_SMS__ = 'enabled'
+# 邮件
+__ALARM_EMAIL__ = 'enabled'
+# 微信公众号
+__ALARM_WECHAT_OFFICIAL__ = 'enabled'
+# 微信小程序
+__ALARM_WECHAT_APPLAT__ = 'enabled'
+
+VUE_APP_GAODE_MAP_RANGE_LEVEL = 'city'
+VUE_APP_GAODE_MAP_RANGE = '济南市'

+ 18 - 0
.env.rc

@@ -0,0 +1,18 @@
+NODE_ENV = 'production'
+
+ENV = 'rc'
+
+LOGGER = 'enabled'
+
+# 短信
+__ALARM_SMS__ = 'enabled'
+# 邮件
+__ALARM_EMAIL__ = 'enabled'
+# 微信公众号
+__ALARM_WECHAT_OFFICIAL__ = 'enabled'
+# 微信小程序
+__ALARM_WECHAT_APPLAT__ = 'enabled'
+
+# keycloak
+VUE_APP_KEYCLOAK_OPTIONS_URL = 'https://isoc.artaplay.com:8443/auth'
+VUE_APP_KEYCLOAK_OPTIONS_REALM = 'smsb-test'

+ 10 - 0
.env.sc

@@ -0,0 +1,10 @@
+NODE_ENV = 'production'
+
+ENV = 'sc'
+
+# 短信
+__ALARM_SMS__ = 'enabled'
+# 微信小程序
+__ALARM_WECHAT_APPLAT__ = 'enabled'
+VUE_APP_AD_ORDER_QR = 'https://led.inspur.com'
+VUE_APP_APPLET_PAGE = 'pages/device/device'

+ 6 - 0
.eslintignore

@@ -0,0 +1,6 @@
+build/*.js
+src/assets
+public
+dist
+mock
+keycloak-theme

+ 277 - 0
.eslintrc.js

@@ -0,0 +1,277 @@
+const { features } = require('./feature')
+
+module.exports = {
+  root: true,
+  env: {
+    browser: true,
+    node: true,
+    es6: true
+  },
+  extends: [
+    'plugin:vue/recommended',
+    'eslint:recommended'
+  ],
+  parserOptions: {
+    parser: '@babel/eslint-parser',
+    sourceType: 'module'
+  },
+  globals: (function (map) {
+    Object.keys(features).forEach(key => {
+      map[key] = true
+    })
+    return map
+  }({})),
+  // add your custom rules here
+  rules: {
+    'vue/multi-word-component-names': 0,
+    'vue/no-mutating-props': 0,
+    'vue/max-attributes-per-line': [2, {
+      'singleline': 1,
+      'multiline': 1
+    }],
+    'vue/singleline-html-element-content-newline': [2, {
+      'ignoreWhenNoAttributes': true,
+      'ignoreWhenEmpty': true,
+      'ignores': ['pre']
+    }],
+    'vue/multiline-html-element-content-newline': [2, {
+      'ignoreWhenEmpty': true,
+      'allowEmptyLines': false,
+      'ignores': ['pre']
+    }],
+    'vue/component-definition-name-casing': [2, 'PascalCase'],
+    'vue/no-v-html': 0,
+    // 8.17.0
+    // Possible Problems
+    'array-callback-return': 2,
+    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
+    'no-constructor-return': 2,
+    'no-duplicate-imports': 2,
+    'no-self-compare': 2,
+    'no-unmodified-loop-condition': 2,
+    'no-unused-vars': [2, {
+      'vars': 'all',
+      'args': 'after-used',
+      'ignoreRestSiblings': true
+    }],
+    'no-use-before-define': [2, 'nofunc'],
+    'no-template-curly-in-string': 2,
+    'no-unreachable-loop': 2,
+    'valid-typeof': [2, { 'requireStringLiterals': true }],
+    // Suggestions
+    'accessor-pairs': 2,
+    'arrow-body-style': [2, 'as-needed', { 'requireReturnForObjectLiteral': true }],
+    'block-scoped-var': 2,
+    'camelcase': [0, {
+      'properties': 'always',
+      'ignoreDestructuring': true
+    }],
+    'complexity': [0, 3],
+    'consistent-return': [2, {
+      'treatUndefinedAsUnspecified': true
+    }],
+    'consistent-this': [2, 'self'],
+    'curly': [2, 'all'],
+    'default-case': 2,
+    'default-case-last': 2,
+    'default-param-last': 2,
+    'eqeqeq': ['error', 'always', { 'null': 'ignore' }],
+    'func-name-matching': 2,
+    'func-names': [2, 'never'],
+    'func-style': [2, 'declaration', { 'allowArrowFunctions': true }],
+    'grouped-accessor-pairs': [2, 'getBeforeSet'],
+    'guard-for-in': 2,
+    'max-depth': [2, 4],
+    'max-nested-callbacks': [2, 4],
+    'max-params': [2, 4],
+    'new-cap': [2, {
+      'newIsCap': true,
+      'capIsNew': false
+    }],
+    'no-alert': 2,
+    'no-array-constructor': 2,
+    'no-caller': 2,
+    'no-console': 0,
+    'no-div-regex': 2,
+    'no-else-return': [2, { 'allowElseIf': true }],
+    'no-empty-function': [2, { 'allow': ['arrowFunctions'] }],
+    'no-eval': 2,
+    'no-extend-native': 2,
+    'no-extra-bind': 2,
+    'no-extra-label': 2,
+    'no-floating-decimal': 2,
+    'no-implied-eval': 2,
+    'no-invalid-this': 2,
+    'no-iterator': 2,
+    'no-label-var': 2,
+    'no-labels': [2, {
+      'allowLoop': false,
+      'allowSwitch': false
+    }],
+    'no-lone-blocks': 2,
+    'no-lonely-if': 2,
+    'no-loop-func': 2,
+    'no-multi-assign': [2, { 'ignoreNonDeclaration': true }],
+    'no-multi-str': 2,
+    'no-negated-condition': 2,
+    'no-new-func': 2,
+    'no-new-object': 2,
+    'no-new-wrappers': 2,
+    'no-octal-escape': 2,
+    'no-proto': 2,
+    'no-return-assign': [2, 'except-parens'],
+    'no-return-await': 2,
+    'no-script-url': 2,
+    'no-sequences': 2,
+    'no-throw-literal': 2,
+    'no-undef-init': 2,
+    'no-undefined': 2,
+    'no-unneeded-ternary': [2, {
+      'defaultAssignment': false
+    }],
+    'no-unused-expressions': [2, {
+      'allowShortCircuit': true,
+      'allowTernary': true
+    }],
+    'no-useless-call': 2,
+    'no-useless-computed-key': 2,
+    'no-useless-concat': 2,
+    'no-useless-constructor': 2,
+    'no-useless-rename': 2,
+    'no-useless-return': 2,
+    'no-var': 2,
+    'object-shorthand': [2, 'always'],
+    'one-var': [2, 'never'],
+    'operator-assignment': 2,
+    'prefer-arrow-callback': [2, {
+      'allowNamedFunctions': false,
+      'allowUnboundThis': false
+    }],
+    'prefer-const': 2,
+    'prefer-exponentiation-operator': 2,
+    'prefer-object-spread': 2,
+    'prefer-regex-literals': 2,
+    'prefer-rest-params': 2,
+    'prefer-spread': 2,
+    'prefer-template': 2,
+    'require-await': 2,
+    'spaced-comment': [2, 'always', {
+      'markers': [
+        'global',
+        'globals',
+        'eslint',
+        'eslint-disable',
+        '*package',
+        '!',
+        ','
+      ]
+    }],
+    'symbol-description': 2,
+    'yoda': [2, 'never'],
+    // Layout & Formatting
+    'array-bracket-spacing': [2, 'never'],
+    'array-element-newline': [2, 'consistent'],
+    'arrow-parens': [2, 'as-needed'],
+    'arrow-spacing': [2, {
+      'before': true,
+      'after': true
+    }],
+    'block-spacing': [2, 'always'],
+    'brace-style': [2, '1tbs', {
+      'allowSingleLine': true
+    }],
+    'comma-dangle': [2, 'never'],
+    'comma-spacing': [2, {
+      'before': false,
+      'after': true
+    }],
+    'comma-style': [2, 'last'],
+    'computed-property-spacing': [2, 'never'],
+    'dot-location': [2, 'property'],
+    'eol-last': [2, 'always'],
+    'func-call-spacing': [2, 'never'],
+    'function-call-argument-newline': [2, 'consistent'],
+    'function-paren-newline': [2, 'multiline-arguments'],
+    'generator-star-spacing': [2, 'before'],
+    'implicit-arrow-linebreak': [2, 'beside'],
+    'indent': [2, 2, {
+      'VariableDeclarator': 'first',
+      'SwitchCase': 1,
+      'MemberExpression': 1,
+      'ArrayExpression': 'first',
+      'ObjectExpression': 'first',
+      'ImportDeclaration': 'first',
+      'flatTernaryExpressions': false,
+      'ignoreComments': false
+    }],
+    'jsx-quotes': [2, 'prefer-single'],
+    'key-spacing': [2, {
+      'beforeColon': false,
+      'afterColon': true
+    }],
+    'keyword-spacing': [2, {
+      'before': true,
+      'after': true
+    }],
+    'max-statements-per-line': [2, { 'max': 2 }],
+    'multiline-ternary': [2, 'always-multiline'],
+    'new-parens': [2, 'always'],
+    'newline-per-chained-call': [2, { 'ignoreChainWithDepth': 3 }],
+    'no-extra-parens': [2, 'functions'],
+    'no-multi-spaces': 2,
+    'no-multiple-empty-lines': [2, { 'max': 1 }],
+    'no-tabs': 2,
+    'no-trailing-spaces': 2,
+    'no-whitespace-before-property': 2,
+    'nonblock-statement-body-position': [2, 'beside'],
+    'object-curly-newline': [2, {
+      'ObjectPattern': {
+        'multiline': true
+      },
+      'ImportDeclaration': {
+        'multiline': true,
+        'minProperties': 2
+      },
+      'ExportDeclaration': 'never'
+    }],
+    'object-curly-spacing': [2, 'always', {
+      'arraysInObjects': true,
+      'objectsInObjects': true
+    }],
+    'operator-linebreak': [2, 'after', {
+      'overrides': {
+        '?': 'before',
+        ':': 'before',
+        '&&': 'before',
+        '||': 'before'
+      }
+    }],
+    'padded-blocks': [2, 'never'],
+    'quotes': [2, 'single', {
+      'avoidEscape': true,
+      'allowTemplateLiterals': true
+    }],
+    'rest-spread-spacing': [2, 'never'],
+    'semi': [2, 'never'],
+    'semi-spacing': [2, {
+      'before': false,
+      'after': true
+    }],
+    'space-before-blocks': [2, 'always'],
+    'space-before-function-paren': [2, 'always'],
+    'space-in-parens': [2, 'never'],
+    'space-infix-ops': 2,
+    'space-unary-ops': [2, {
+      'words': true,
+      'nonwords': false
+    }],
+    'switch-colon-spacing': [2, {
+      'before': false,
+      'after': true
+    }],
+    'template-curly-spacing': [2, 'never'],
+    'template-tag-spacing': [2, 'never'],
+    'wrap-iife': [2, 'any'],
+    'yield-star-spacing': [2, 'before']
+  }
+}

+ 25 - 0
.gitignore

@@ -0,0 +1,25 @@
+.DS_Store
+node_modules
+/dist
+
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# mock
+db.json

+ 1 - 0
.nvmrc

@@ -0,0 +1 @@
+v16.13.1

+ 8 - 0
README.md

@@ -0,0 +1,8 @@
+### 分支说明
+
+- master 稳定版
+
+- develop 开发分支,所有开发基于该分支进行
+
+- rc 预发布版,rc分支push后会自动部署至71测试服务器供测试验证
+

+ 5 - 0
babel.config.js

@@ -0,0 +1,5 @@
+module.exports = {
+  presets: [
+    '@vue/cli-plugin-babel/preset'
+  ]
+}

+ 5 - 0
commitlint.config.js

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

+ 37 - 0
feature.js

@@ -0,0 +1,37 @@
+const isProd = process.env.NODE_ENV !== 'development'
+const isStaging = process.env.ENV === 'alpha' || process.env.ENV === 'rc'
+
+function isEnable (feature) {
+  return !isProd || process.env[feature] === 'enabled'
+}
+
+function createFeature (feature) {
+  return { [feature]: isEnable(feature) }
+}
+
+function getTimestamp () {
+  const now = new Date()
+  return `${now.getFullYear()}${(now.getMonth() + 1).toString().padStart(2, '0')}${now.getDate().toString().padStart(2, '0')}${now.getHours().toString().padStart(2, '0')}${now.getMinutes().toString().padStart(2, '0')}${now.getSeconds().toString().padStart(2, '0')}`
+}
+
+const version = `v${require('./package.json').version}.${getTimestamp()}`
+
+module.exports = {
+  version,
+  isProd,
+  logger: isEnable('LOGGER'),
+  features: {
+    __VERSION__: JSON.stringify(version),
+    __DEV__: !isProd,
+    __STAGING__: isStaging,
+    ...createFeature('__FEATURE__'),
+    ...createFeature('__SUB_DEVICE__'),
+    ...createFeature('__TAKEOVER__'),
+    ...createFeature('__ALARM_SMS__'),
+    ...createFeature('__ALARM_EMAIL__'),
+    ...createFeature('__ALARM_WECHAT_OFFICIAL__'),
+    ...createFeature('__ALARM_WECHAT_APPLAT__'),
+    ...createFeature('__JUMP_REVIEW__'),
+    ...createFeature('__EMERGENCY_PLATFORM__')
+  }
+}

+ 130 - 0
keycloak-theme/login/error.ftl

@@ -0,0 +1,130 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8" />
+  <meta name='viewport' content='width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no' />
+  <title>${realm.displayName}</title>
+  <style>
+    * {
+      margin: 0;
+      padding: 0;
+    }
+
+    html,
+    body {
+      width: 100%;
+      height: 100%;
+    }
+
+    body {
+      box-sizing: border-box;
+      -moz-osx-font-smoothing: grayscale;
+      -webkit-font-smoothing: antialiased;
+      text-rendering: optimizeLegibility;
+      font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
+      background-color: #F4F7FB;
+    }
+
+    *,
+    *::before,
+    *::after {
+      box-sizing: inherit;
+    }
+
+    a {
+      border: none;
+      outline: none;
+      text-decoration: none;
+      -webkit-touch-callout: none;
+      -webkit-tap-highlight-color: transparent;
+    }
+
+    .c-login {
+      display: flex;
+      min-height: 100%;
+      width: 100%;
+    }
+
+    .c-login__img {
+      flex: 1 0 400px;
+      display: inline-block;
+      max-width: 800px;
+      background: url("${url.resourcesPath}/img/illustration.png") 50% 50% / 757px 613px no-repeat;
+    }
+
+    .c-login__main {
+      flex: 1 0 auto;
+    }
+
+    .c-login-form {
+      display: inline-flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      padding: 40px 0 80px;
+      background-color: #FFFFFF;
+      overflow: auto;
+    }
+
+    .c-login-form__header {
+      display: inline-flex;
+      align-items: center;
+    }
+
+    .c-login-form__logo {
+      flex: none;
+    }
+
+    .c-login-form__name {
+      margin-left: 16px;
+      color: #1C5CB0;
+      font-size: 28px;
+      font-weight: bold;
+      line-height: 1;
+    }
+
+    .c-login-form__title {
+      padding: 36px 0 24px;
+      color: #1C5CB0;
+      font-size: 18px;
+      font-weight: bold;
+    }
+
+    .c-login-form__info {
+      width: 400px;
+      color: #333333;
+      font-size: 16px;
+      line-height: 20px;
+    }
+
+    @media screen and (max-width: 900px) {
+      .c-login__img {
+        display: none;
+      }
+    }
+  </style>
+</head>
+<body>
+  <div class="c-login">
+    <div class="c-login__img"></div>
+    <div class="c-login__main c-login-form">
+      <div class="c-login-form__header">
+        <img class="c-login-form__logo" src="${url.resourcesPath}/img/logo/${realm.displayNameHtml!'inspur.png'}">
+        <div class="c-login-form__name">${realm.displayName}</div>
+      </div>
+      <div class="c-login-form__title">很抱歉...</div>
+      <div class="c-login-form__info">
+        <#if message?has_content>
+          <p>${message.summary?no_esc}</p>
+        </#if>
+        <br />
+        <#if client?? && client.baseUrl?has_content>
+          <p>
+            <a href="${client.baseUrl}">回到应用</a>
+          </p>
+        </#if>
+      </div>
+    </div>
+  </div>
+</body>
+</html>

+ 137 - 0
keycloak-theme/login/info.ftl

@@ -0,0 +1,137 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8" />
+  <meta name='viewport' content='width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no' />
+  <title>${realm.displayName}</title>
+  <style>
+    * {
+      margin: 0;
+      padding: 0;
+    }
+
+    html,
+    body {
+      width: 100%;
+      height: 100%;
+    }
+
+    body {
+      box-sizing: border-box;
+      -moz-osx-font-smoothing: grayscale;
+      -webkit-font-smoothing: antialiased;
+      text-rendering: optimizeLegibility;
+      font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
+      background-color: #F4F7FB;
+    }
+
+    *,
+    *::before,
+    *::after {
+      box-sizing: inherit;
+    }
+
+    a {
+      border: none;
+      outline: none;
+      text-decoration: none;
+      -webkit-touch-callout: none;
+      -webkit-tap-highlight-color: transparent;
+    }
+
+    .c-login {
+      display: flex;
+      min-height: 100%;
+      width: 100%;
+    }
+
+    .c-login__img {
+      flex: 1 0 400px;
+      display: inline-block;
+      max-width: 800px;
+      background: url("${url.resourcesPath}/img/illustration.png") 50% 50% / 757px 613px no-repeat;
+    }
+
+    .c-login__main {
+      flex: 1 0 auto;
+    }
+
+    .c-login-form {
+      display: inline-flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      padding: 40px 0 80px;
+      background-color: #FFFFFF;
+      overflow: auto;
+    }
+
+    .c-login-form__header {
+      display: inline-flex;
+      align-items: center;
+    }
+
+    .c-login-form__logo {
+      flex: none;
+    }
+
+    .c-login-form__name {
+      margin-left: 16px;
+      color: #1C5CB0;
+      font-size: 28px;
+      font-weight: bold;
+      line-height: 1;
+    }
+
+    .c-login-form__title {
+      padding: 36px 0 24px;
+      color: #1C5CB0;
+      font-size: 18px;
+      font-weight: bold;
+    }
+
+    .c-login-form__info {
+      width: 400px;
+      color: #333333;
+      font-size: 16px;
+      line-height: 20px;
+    }
+
+    @media screen and (max-width: 900px) {
+      .c-login__img {
+        display: none;
+      }
+    }
+  </style>
+</head>
+<body>
+  <div class="c-login">
+    <div class="c-login__img"></div>
+    <div class="c-login__main c-login-form">
+      <div class="c-login-form__header">
+        <img class="c-login-form__logo" src="${url.resourcesPath}/img/logo/${realm.displayNameHtml!'inspur.png'}">
+        <div class="c-login-form__name">${realm.displayName}</div>
+      </div>
+      <div class="c-login-form__title">${message.summary}</div>
+      <div class="c-login-form__info">
+        <#if skipLink??>
+        <#else>
+          <#if pageRedirectUri?has_content>
+            <p>
+              <a href="${pageRedirectUri}">回到应用</a>
+            </p>
+          <#elseif actionUri?has_content>
+            <p>
+              <a href="${actionUri}">继续</a>
+            </p>
+          <#elseif (client.baseUrl)?has_content>
+            <p>
+              <a href="${client.baseUrl}">回到应用</a>
+            </p>
+          </#if>
+        </#if>
+      </div>
+    </div>
+  </div>
+</body>
+</html>

+ 230 - 0
keycloak-theme/login/login-config-totp.ftl

@@ -0,0 +1,230 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8" />
+  <meta name='viewport' content='width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no' />
+  <title>${realm.displayName}</title>
+  <style>
+    * {
+      margin: 0;
+      padding: 0;
+    }
+
+    html,
+    body {
+      width: 100%;
+      height: 100%;
+    }
+
+    body {
+      box-sizing: border-box;
+      -moz-osx-font-smoothing: grayscale;
+      -webkit-font-smoothing: antialiased;
+      text-rendering: optimizeLegibility;
+      font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
+      background-color: #F4F7FB;
+    }
+
+    *,
+    *::before,
+    *::after {
+      box-sizing: inherit;
+    }
+
+    a {
+      border: none;
+      outline: none;
+      text-decoration: none;
+      -webkit-touch-callout: none;
+      -webkit-tap-highlight-color: transparent;
+    }
+
+    .c-login {
+      display: flex;
+      min-height: 100%;
+      width: 100%;
+    }
+
+    .c-login__img {
+      flex: 1 0 400px;
+      display: inline-block;
+      max-width: 800px;
+      background: url("${url.resourcesPath}/img/illustration.png") 50% 50% / 757px 613px no-repeat;
+    }
+
+    .c-login__main {
+      flex: 1 0 auto;
+    }
+
+    .c-login-form {
+      display: inline-flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      padding: 40px 0 80px;
+      background-color: #FFFFFF;
+      overflow: auto;
+    }
+
+    .c-login-form__header {
+      display: inline-flex;
+      align-items: center;
+    }
+
+    .c-login-form__logo {
+      flex: none;
+    }
+
+    .c-login-form__name {
+      margin-left: 16px;
+      color: #1C5CB0;
+      font-size: 28px;
+      font-weight: bold;
+      line-height: 1;
+    }
+
+    .c-login-form__title {
+      padding: 36px 0 24px;
+      color: #1C5CB0;
+      font-size: 18px;
+      font-weight: bold;
+    }
+
+    .c-login-form__info {
+      width: 400px;
+      color: #333333;
+      font-size: 14px;
+      line-height: 20px;
+    }
+
+    .c-login-form__info ol {
+      line-height: 24px;
+    }
+
+    .c-login-form__qr {
+      width: 144px;
+      height: 144px;
+    }
+
+    .c-login-form__retry {
+      color: #1C5CB0;
+      font-size: 12px;
+    }
+
+    .c-login-form__required {
+      color: #EF1D1D;
+    }
+
+    .c-login-form__required ::before {
+      content: "*";
+      color: #FF0000;
+      font-size: 14px;
+    }
+
+    .c-login-form__input {
+      padding: 0 10px;
+      margin-top: 6px;
+      width: 240px;
+      height: 30px;
+      font-size: 14px;
+      line-height: 30px;
+      border: 1px solid #C4C4C4;
+      outline: none;
+    }
+
+    .c-login-form__error {
+      height: 30px;
+      color: #F56C6C;
+      font-size: 12px;
+      line-height: 24px;
+      white-space: nowrap;
+    }
+
+    .c-login-form__submit {
+      display: inline-flex;
+      justify-content: center;
+      align-items: center;
+      height: 40px;
+      padding: 0 40px;
+      color: #ffffff;
+      font-size: 18px;
+      border: none;
+      background-color: #1C5CB0;
+      border-radius: 8px;
+      cursor: pointer;
+    }
+
+    .c-login-form__submit[disabled] {
+      background-color: #C4C4C4;
+    }
+
+    .l-flex {
+      display: flex;
+      align-items: flex-start;
+      gap: 16px;
+    }
+
+    @media screen and (max-width: 900px) {
+      .c-login__img {
+        display: none;
+      }
+    }
+
+    @media screen and (max-height: 800px) {
+      .c-login-form {
+        padding: 0;
+      }
+
+      .c-login-form__title {
+        padding: 24px 0 16px;
+      }
+    }
+  </style>
+</head>
+<body>
+  <div class="c-login">
+    <div class="c-login__img"></div>
+    <form class="c-login__main c-login-form" action="${url.loginAction}" method="post"
+      onsubmit="login.disabled = true; return true;">
+      <input id="totpSecret" type="hidden" name="totpSecret" value="${totp.totpSecret}" />
+      <#if mode??>
+        <input id="mode" type="hidden" name="mode" value="${mode}" />
+      </#if>
+      <div class="c-login-form__header">
+        <img class="c-login-form__logo" src="${url.resourcesPath}/img/logo/${realm.displayNameHtml!'inspur.png'}" />
+        <div class="c-login-form__name">${realm.displayName}</div>
+      </div>
+      <div class="c-login-form__title">手机验证</div>
+      <div class="c-login-form__info">
+        <ol>
+          <li>请先使用微信搜索小程序“浪潮安播云”进入“我的-登录验证”,或扫描下面右侧小程序码。</li>
+          <li>打开应用后扫描下面左侧二维码。</li>
+          <li>输入应用提供的一次性码,点击提交完成验证。</li>
+        </ol>
+        <br />
+        <div class="l-flex">
+          <div>
+            <img class="c-login-form__qr" src="data:image/png;base64,${totp.totpSecretQrCode}" />
+            <p>
+              <a class="c-login-form__retry" href="${totp.manualUrl}">
+                无法扫码
+              </a>
+            </p>
+          </div>
+          <img class="c-login-form__qr" src="${url.resourcesPath}/img/applet.png" />
+        </div>
+        <br />
+        <p class="c-login-form__required">一次性验证码</p>
+        <input type="text" name="totp" class="c-login-form__input" autocomplete="off" />
+        <div class="c-login-form__error">
+          <#if messagesPerField.existsError('totp')>
+            ${kcSanitize(messagesPerField.get('totp'))?no_esc}
+          </#if>
+        </div>
+        <input type="submit" name="login" class="c-login-form__submit" value="提交">
+      </div>
+    </form>
+  </div>
+</body>
+
+</html>

+ 192 - 0
keycloak-theme/login/login-otp.ftl

@@ -0,0 +1,192 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8" />
+  <meta name='viewport' content='width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no' />
+  <title>${realm.displayName}</title>
+  <link rel="stylesheet" href="${url.resourcesPath}/css/totp.css" />
+  <style>
+    * {
+      margin: 0;
+      padding: 0;
+    }
+
+    html,
+    body {
+      width: 100%;
+      height: 100%;
+    }
+
+    body {
+      box-sizing: border-box;
+      -moz-osx-font-smoothing: grayscale;
+      -webkit-font-smoothing: antialiased;
+      text-rendering: optimizeLegibility;
+      font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
+      background-color: #F4F7FB;
+    }
+
+    *,
+    *::before,
+    *::after {
+      box-sizing: inherit;
+    }
+
+    a {
+      border: none;
+      outline: none;
+      text-decoration: none;
+      -webkit-touch-callout: none;
+      -webkit-tap-highlight-color: transparent;
+    }
+
+    .c-login {
+      display: flex;
+      min-height: 100%;
+      width: 100%;
+    }
+
+    .c-login__img {
+      flex: 1 0 400px;
+      display: inline-block;
+      max-width: 800px;
+      background: url("${url.resourcesPath}/img/illustration.png") 50% 50% / 757px 613px no-repeat;
+    }
+
+    .c-login__main {
+      flex: 1 0 auto;
+    }
+
+    .c-login-form {
+      display: inline-flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      padding: 40px 0 80px;
+      background-color: #FFFFFF;
+      overflow: auto;
+    }
+
+    .c-login-form__header {
+      display: inline-flex;
+      align-items: center;
+    }
+
+    .c-login-form__logo {
+      flex: none;
+    }
+
+    .c-login-form__name {
+      margin-left: 16px;
+      color: #1C5CB0;
+      font-size: 28px;
+      font-weight: bold;
+      line-height: 1;
+    }
+
+    .c-login-form__title {
+      padding: 36px 0 24px;
+      color: #1C5CB0;
+      font-size: 18px;
+      font-weight: bold;
+    }
+
+    .c-login-form__info {
+      width: 400px;
+      color: #333333;
+      font-size: 16px;
+      line-height: 20px;
+    }
+
+    .c-login-form__required {
+      color: #EF1D1D;
+    }
+
+    .c-login-form__required ::before {
+      content: "*";
+      color: #FF0000;
+      font-size: 14px;
+    }
+
+    .c-login-form__input {
+      padding: 0 10px;
+      margin-top: 6px;
+      width: 240px;
+      height: 30px;
+      font-size: 14px;
+      line-height: 30px;
+      border: 1px solid #C4C4C4;
+      outline: none;
+    }
+
+    .c-login-form__error {
+      height: 30px;
+      color: #F56C6C;
+      font-size: 12px;
+      line-height: 24px;
+      white-space: nowrap;
+    }
+
+    .c-login-form__submit {
+      display: inline-flex;
+      justify-content: center;
+      align-items: center;
+      height: 40px;
+      padding: 0 40px;
+      color: #ffffff;
+      font-size: 18px;
+      border: none;
+      background-color: #1C5CB0;
+      border-radius: 8px;
+      cursor: pointer;
+    }
+
+    .c-login-form__submit[disabled] {
+      background-color: #C4C4C4;
+    }
+
+    @media screen and (max-width: 900px) {
+      .c-login__img {
+        display: none;
+      }
+    }
+  </style>
+</head>
+<body>
+  <div class="c-login">
+    <div class="c-login__img"></div>
+    <form
+      class="c-login__main c-login-form"
+      action="${url.loginAction}"
+      method="post"
+      onsubmit="login.disabled = true; return true;"
+    >
+      <div class="c-login-form__header">
+        <img class="c-login-form__logo" src="${url.resourcesPath}/img/logo/${realm.displayNameHtml!'inspur.png'}">
+        <div class="c-login-form__name">${realm.displayName}</div>
+      </div>
+      <div class="c-login-form__title">手机验证</div>
+      <div class="c-login-form__info">
+        <p class="c-login-form__required">一次性验证码</p>
+        <input
+          type="text"
+          name="otp"
+          class="c-login-form__input"
+          autocomplete="off"
+        />
+        <div class="c-login-form__error">
+          <#if messagesPerField.existsError('totp')>
+            ${kcSanitize(messagesPerField.get('totp'))?no_esc}
+          </#if>
+        </div>
+        <input
+          type="submit"
+          name="login"
+          class="c-login-form__submit"
+          value="提交"
+        >
+      </div>
+    </form>
+  </div>
+</body>
+</html>

+ 14 - 0
keycloak-theme/login/login-page-expired.ftl

@@ -0,0 +1,14 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8" />
+  <meta name='viewport' content='width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no' />
+  <title>${realm.displayName}</title>
+</head>
+<body>
+  <script type="text/javascript">
+    console.log('page expired');
+    window.location.replace('${url.loginRestartFlowUrl}');
+  </script>
+</body>
+</html>

+ 276 - 0
keycloak-theme/login/login-update-password.ftl

@@ -0,0 +1,276 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8" />
+  <meta name='viewport' content='width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no' />
+  <title>${realm.displayName}</title>
+  <style>
+    * {
+      margin: 0;
+      padding: 0;
+    }
+
+    html,
+    body {
+      width: 100%;
+      height: 100%;
+    }
+
+    body {
+      box-sizing: border-box;
+      -moz-osx-font-smoothing: grayscale;
+      -webkit-font-smoothing: antialiased;
+      text-rendering: optimizeLegibility;
+      font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
+      background-color: #FFFFFF;
+    }
+
+    *,
+    *::before,
+    *::after {
+      box-sizing: inherit;
+    }
+
+    a {
+      border: none;
+      outline: none;
+      text-decoration: none;
+      -webkit-touch-callout: none;
+      -webkit-tap-highlight-color: transparent;
+    }
+
+    .c-login {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      min-height: 100%;
+      width: 100%;
+    }
+
+    .c-login__main {
+      flex: 1 0 auto;
+    }
+
+    .c-login-form {
+      display: inline-flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      padding: 0 0 40px;
+      overflow: auto;
+    }
+
+    .c-login-form__header {
+      color: #1C5CB0;
+      font-size: 28px;
+      font-weight: bold;
+      text-align: center;
+    }
+
+    .c-login-form__tip {
+      padding: 20px;
+      height: 60px;
+    }
+
+    .c-login-form__tip .success {
+      color: #67C23A;
+    }
+
+    .c-login-form__tip .warning {
+      color: #E6A23C;
+    }
+
+    .c-login-form__tip .error {
+      color: #F56C6C;
+    }
+
+    .c-login-form__tip .info {
+      color: #409EFF;
+    }
+
+    .c-login-form__section {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+    }
+
+    .c-login-form__wrapper {
+      display: flex;
+      align-items: center;
+    }
+
+    .c-login-form__label {
+      width: 150px;
+      margin-right: 20px;
+      color: #333333;
+      font-size: 16px;
+      text-align: right;
+    }
+
+    .c-login-form__label::before {
+      content: "*";
+      color: #FF0000;
+      font-size: 14px;
+    }
+
+    .c-login-form__input {
+      padding: 0 10px;
+      width: 320px;
+      height: 30px;
+      font-size: 14px;
+      line-height: 30px;
+      border: 1px solid #C4C4C4;
+      outline: none;
+    }
+
+    .c-login-form__input.password {
+      -webkit-text-security: disc;
+    }
+
+    .c-login-form__error {
+      display: inline-block;
+      width: 320px;
+      height: 30px;
+      margin-left: 170px;
+      color: #F56C6C;
+      font-size: 12px;
+      line-height: 24px;
+      white-space: nowrap;
+    }
+
+    .c-login-form__submit {
+      display: inline-flex;
+      justify-content: center;
+      align-items: center;
+      height: 40px;
+      padding: 0 40px;
+      margin-top: 60px;
+      color: #ffffff;
+      font-size: 18px;
+      border: none;
+      background-color: #1C5CB0;
+      border-radius: 8px;
+      cursor: pointer;
+    }
+
+    .c-login-form__submit[disabled] {
+      background-color: #C4C4C4;
+    }
+
+  </style>
+</head>
+<body>
+  <div class="c-login">
+    <form
+      id="loginForm"
+      class="c-login__main c-login-form"
+      action="${url.loginAction}"
+      method="post"
+      onsubmit="return false;"
+    >
+      <div class="c-login-form__header">修改密码</div>
+      <div class="c-login-form__tip">
+        <#if !messagesPerField.existsError('password','password-confirm') && message?has_content && !isAppInitiatedAction??>
+          <span class="${message.type}">${kcSanitize(message.summary)?no_esc}</span>
+        </#if>
+      </div>
+      <div class="c-login-form__tip">
+          <span class="warning"> 至少包含数字、字符、小写字母、大写字母 字符范围:+_!@#$%^&amp;*.,?</span>
+      </div>
+      <input id="username" name="username" type="text" value="${username}" autocomplete="username" readonly="readonly" style="display:none;"/>
+      <input id="password" name="password" type="password" autocomplete="current-password" style="display:none;"/>
+      <div class="c-login-form__section">
+        <div class="c-login-form__wrapper">
+          <label
+            for="passwordNewProxy"
+            class="c-login-form__label"
+          >
+            新密码:
+          </label>
+          <input
+            id="passwordNewProxy"
+            type="password"
+            class="c-login-form__input"
+            placeholder="请输入新密码"
+            autocomplete="new-password"
+            aria-autocomplete="none"
+          >
+        </div>
+        <div class="c-login-form__error">
+          <#if messagesPerField.existsError('password')>
+            ${kcSanitize(messagesPerField.get('password'))?no_esc}
+          </#if>
+        </div>
+      </div>
+      <div class="c-login-form__section">
+        <div class="c-login-form__wrapper">
+          <label
+            for="passwordConfirmProxy"
+            class="c-login-form__label"
+          >
+            确认新密码:
+          </label>
+          <input
+            id="passwordConfirmProxy"
+            type="password"
+            class="c-login-form__input"
+            placeholder="请输入新密码"
+            autocomplete="new-password"
+            aria-autocomplete="none"
+          >
+        </div>
+        <div class="c-login-form__error">
+          <#if messagesPerField.existsError('password-confirm')>
+            ${kcSanitize(messagesPerField.get('password-confirm'))?no_esc}
+          </#if>
+        </div>
+      </div>
+      <input id="passwordNew" name="password-new" hidden >
+      <input id="passwordConfirm" name="password-confirm" hidden >
+      <button
+        class="c-login-form__submit"
+        name="login"
+        onclick="onSubmit()"
+      >
+        保存
+      </button>
+    </form>
+  </div>
+  <script src="${url.resourcesPath}/js/md5.js"></script>
+  <script>
+    function onSubmit () {
+      var form = document.getElementById('loginForm')
+      var value =document.getElementById('passwordNewProxy').value
+      var res = checkPassword(value)
+      if(res){
+         window.alert(res)
+         return
+      }
+      document.getElementById('passwordNew').value = MD5Salt(document.getElementById('passwordNewProxy').value)
+      document.getElementById('passwordConfirm').value = MD5Salt(document.getElementById('passwordConfirmProxy').value)
+      form.submit()
+    }
+    function checkPassword (password) {
+      if (password.length < 8) {
+        return '密码最少8位'
+      }
+      if (!/^[\da-zA-Z+_!@#$%^&*.,?]+$/.test(password)) {
+        return '包含非法字符'
+      }
+      if (!/\d/.test(password)) {
+        return '缺少数字'
+      }
+      if (!/[a-z]/.test(password)) {
+        return '缺少小写字母'
+      }
+      if (!/[A-Z]/.test(password)) {
+        return '缺少大写字母'
+      }
+      if (!/[+_!@#$%^&*.,?]/.test(password)) {
+        return '缺少特殊字符'
+      }
+      return ''
+    }
+  </script>
+</body>
+</html>

+ 262 - 0
keycloak-theme/login/login.ftl

@@ -0,0 +1,262 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8" />
+  <meta name='viewport' content='width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no' />
+  <title>${realm.displayName}</title>
+  <style>
+    * {
+      margin: 0;
+      padding: 0;
+    }
+
+    html,
+    body {
+      width: 100%;
+      height: 100%;
+    }
+
+    body {
+      box-sizing: border-box;
+      -moz-osx-font-smoothing: grayscale;
+      -webkit-font-smoothing: antialiased;
+      text-rendering: optimizeLegibility;
+      font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
+      background-color: #F4F7FB;
+    }
+
+    *,
+    *::before,
+    *::after {
+      box-sizing: inherit;
+    }
+
+    a {
+      border: none;
+      outline: none;
+      text-decoration: none;
+      -webkit-touch-callout: none;
+      -webkit-tap-highlight-color: transparent;
+    }
+
+    .c-login {
+      display: flex;
+      min-height: 100%;
+      width: 100%;
+    }
+
+    .c-login__img {
+      flex: 1 0 400px;
+      display: inline-block;
+      max-width: 800px;
+      background: url("${url.resourcesPath}/img/illustration.png") 50% 50% / 757px 613px no-repeat;
+    }
+
+    .c-login__main {
+      flex: 1 0 auto;
+    }
+
+    @media screen and (max-width: 900px) {
+      .c-login__img {
+        display: none;
+      }
+    }
+
+    .c-login-form {
+      display: inline-flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      padding: 40px 0 80px;
+      background-color: #FFFFFF;
+      overflow: auto;
+    }
+
+    .c-login-form__header {
+      display: inline-flex;
+      align-items: center;
+    }
+
+    .c-login-form__logo {
+      flex: none;
+    }
+
+    .c-login-form__name {
+      margin-left: 16px;
+      color: #1C5CB0;
+      font-size: 28px;
+      font-weight: bold;
+      line-height: 1;
+    }
+
+    .c-login-form__tip {
+      padding: 20px;
+      height: 100px;
+    }
+
+    .c-login-form__tip .success {
+      color: #67C23A;
+    }
+
+    .c-login-form__tip .warning {
+      color: #E6A23C;
+    }
+
+    .c-login-form__tip .error {
+      color: #F56C6C;
+    }
+
+    .c-login-form__tip .info {
+      color: #409EFF;
+    }
+
+    .c-login-form__wrapper {
+      display: inline-flex;
+      flex-direction: column;
+      color: #C4C4C4;
+    }
+
+    .c-login-form__error {
+      height: 30px;
+      color: #F56C6C;
+      font-size: 12px;
+      line-height: 24px;
+      white-space: nowrap;
+    }
+
+    .c-login-form__label {
+      font-size: 24px;
+      line-height: 1;
+    }
+
+    .c-login-form__section {
+      display: inline-flex;
+      align-items: center;
+      padding: 8px 0 2px;
+      border-bottom: 1px solid currentColor;
+    }
+
+    .c-login-form__input {
+      width: 320px;
+      height: 30px;
+      font-size: 18px;
+      line-height: 30px;
+      border: none;
+      outline: none;
+    }
+
+    .c-login-form__input.password {
+      -webkit-text-security: disc;
+    }
+
+    .c-login-form__icon {
+      display: inline-block;
+      margin-left: 10px;
+      width: 30px;
+      height: 30px;
+      background-position: 0 0;
+      background-size: 100% 100%;
+      background-repeat: no-repeat;
+    }
+
+    .c-login-form__icon.user {
+      background-image: url("${url.resourcesPath}/img/icon_user.png");
+    }
+
+    .c-login-form__icon.lock {
+      background-image: url("${url.resourcesPath}/img/icon_lock.png");
+    }
+
+    .c-login-form__submit {
+      display: inline-flex;
+      justify-content: center;
+      align-items: center;
+      width: 360px;
+      height: 40px;
+      margin-top: 50px;
+      color: #ffffff;
+      font-size: 18px;
+      border: none;
+      background-color: #1C5CB0;
+      border-radius: 8px;
+      cursor: pointer;
+    }
+
+    .c-login-form__submit[disabled] {
+      background-color: #C4C4C4;
+    }
+  </style>
+</head>
+<body>
+  <div class="c-login">
+    <div class="c-login__img"></div>
+    <form
+      id="loginForm"
+      class="c-login__main c-login-form"
+      action="${url.loginAction}"
+      method="post"
+      onsubmit="login.disabled = true; return false;"
+    >
+      <div class="c-login-form__header">
+        <img class="c-login-form__logo" src="${url.resourcesPath}/img/logo/${realm.displayNameHtml!'inspur.png'}">
+        <div class="c-login-form__name">${realm.displayName}</div>
+      </div>
+      <div class="c-login-form__tip">
+        <#if !messagesPerField.existsError('username','password') && message?has_content && !isAppInitiatedAction??>
+          <span class="${message.type}">${kcSanitize(message.summary)?no_esc}</span>
+        </#if>
+      </div>
+      <div class="c-login-form__wrapper">
+        <label class="c-login-for__label">用户名</label>
+        <div class="c-login-form__section">
+          <input
+            id="usernameProxy"
+            type="text"
+            class="c-login-form__input"
+            value="${(login.username!'')}"
+            autocomplete="off"
+          >
+          <i class="c-login-form__icon user"></i>
+        </div>
+        <div class="c-login-form__error">
+          <#if messagesPerField.existsError('username','password')>
+            ${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc}
+          </#if>
+        </div>
+      </div>
+      <div class="c-login-form__wrapper">
+        <label class="c-login-for__label">密码</label>
+        <div class="c-login-form__section">
+          <input
+            id="passwordProxy"
+            type="password"
+            class="c-login-form__input"
+            autocomplete="off"
+            aria-autocomplete="none"
+          >
+          <i class="c-login-form__icon lock"></i>
+        </div>
+        <div class="c-login-form__error"></div>
+      </div>
+      <input id="username" name="username" hidden >
+      <input id="password" name="password" hidden >
+      <button
+        class="c-login-form__submit"
+        name="login"
+        onclick="onSubmit()"
+      >
+        登 录
+      </button>
+    </form>
+  </div>
+  <script src="${url.resourcesPath}/js/md5.js"></script>
+  <script>
+    function onSubmit () {
+      var form = document.getElementById('loginForm')
+      document.getElementById('username').value = document.getElementById('usernameProxy').value
+      document.getElementById('password').value = MD5Salt(document.getElementById('passwordProxy').value)
+      form.submit()
+    }
+  </script>
+</body>
+</html>

+ 32 - 0
keycloak-theme/login/messages/messages_zh_CN.properties

@@ -0,0 +1,32 @@
+# encoding: utf-8
+invalidUserMessage=无效的用户名或密码
+accountDisabledMessage=账户被禁用,请联系管理员
+accountTemporarilyDisabledMessage=账户被暂时禁用,请稍后再试或联系管理员
+expiredCodeMessage=登录超时,请重新登录
+expiredActionMessage=登录超时,请重新登录
+
+missingUsernameMessage=请输入用户名
+missingPasswordMessage=请输入密码
+missingTotpMessage=请输入验证码
+notMatchPasswordMessage=密码不匹配
+
+invalidPasswordExistingMessage=无效的旧密码
+invalidPasswordConfirmMessage=确认密码不相同
+invalidTotpMessage=无效的验证码
+
+updatePasswordMessage=您需要更新您的密码来激活您的账户
+
+loginTimeout=登录超时,请重新开始登录
+
+alreadyLoggedIn=您已经登录
+
+invalidAccessCodeMessage=无效的验证码
+sessionNotActiveMessage=会话不在活动状态
+invalidCodeMessage=发生错误,请重新通过应用登录
+identityProviderUnexpectedErrorMessage=在与认证提供者认证过程中发生未知错误
+identityProviderNotFoundMessage=无法找到认证提供方
+staleCodeMessage=当前页面已无效,请到登录界面重新登录
+cookieNotFoundMessage="授权码失效"
+
+invalidParameterMessage=无效的参数 \: {0}
+invalidRequestMessage=无效的请求参数

BIN
keycloak-theme/login/resources/img/applet.png


BIN
keycloak-theme/login/resources/img/icon_lock.png


BIN
keycloak-theme/login/resources/img/icon_user.png


BIN
keycloak-theme/login/resources/img/illustration.png


BIN
keycloak-theme/login/resources/img/logo/fujian.png


BIN
keycloak-theme/login/resources/img/logo/inspur.png


File diff suppressed because it is too large
+ 0 - 0
keycloak-theme/login/resources/js/md5.js


+ 1 - 0
keycloak-theme/login/theme.properties

@@ -0,0 +1 @@
+locales=zh-CN

+ 78 - 0
mock/mock-server.js

@@ -0,0 +1,78 @@
+const fs = require('fs')
+const path = require('path')
+const chalk = require('chalk')
+const chokidar = require('chokidar')
+const express = require('express')
+const Mock = require('mockjs')
+const router = express.Router()
+
+const mockDir = path.join(process.cwd(), 'mock')
+
+function responseFake(url, type, respond) {
+  return {
+    url: new RegExp(`${url}`),
+    type: type || 'get',
+    response (req, res) {
+      console.log('request invoke:' + req.path)
+      res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
+    }
+  }
+}
+
+function register (router) {
+  require('./proxy').register(router)
+
+  router.use(express.json())
+  router.use(express.urlencoded({
+    extended: true
+  }))
+
+  const routeDir = path.join(__dirname, 'routes')
+  if (fs.existsSync(routeDir)) {
+    const routes = []
+    fs.readdirSync(routeDir).forEach(file => {
+      const mocks = require(`./routes/${file}`)
+      if (Array.isArray(mocks) && mocks.length) {
+        routes.push(...mocks)
+      }
+    })
+    routes.forEach(route => {
+      const { type, url, response } = responseFake(route.url, route.type, route.response)
+      router[type](url, response)
+    })
+  } else {
+    console.log('no mock routes')
+  }
+}
+
+function unregister () {
+  router.stack = []
+
+  // clear routes cache
+  Object.keys(require.cache).forEach(i => {
+    if (i.includes(mockDir)) {
+      delete require.cache[require.resolve(i)]
+    }
+  })
+}
+
+module.exports = app => {
+  app.use(process.env.VUE_APP_BASE_API, router)
+  register(router)
+
+  // watch files, hot reload mock server
+  chokidar.watch(mockDir, {
+    ignored: /mock-server/,
+    ignoreInitial: true
+  }).on('all', (event, path) => {
+    if (event === 'change' || event === 'add') {
+      try {
+        unregister()
+        register(router)
+        console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`))
+      } catch (error) {
+        console.log(chalk.redBright(error))
+      }
+    }
+  })
+}

+ 46 - 0
mock/proxy.js

@@ -0,0 +1,46 @@
+const proxy = require('express-http-proxy')
+
+const use = true
+
+const gate = process.env.VUE_APP_GATEWAY
+const base_url = process.env.VUE_APP_BASE_API
+const mapKey = process.env.VUE_APP_OFFLINE_MAP.replace(base_url, '')
+const minioKey = process.env.VUE_APP_MINIO.replace(base_url, '')
+const thumbnailKey = process.env.VUE_APP_THUMBNAIL.replace(base_url, '')
+
+const isHttps = url => !/^[0-9.:]+$/.test(url)
+
+module.exports = {
+  register (router) {
+    if (use) {
+      router.use(mapKey, createProxy(process.env.VUE_APP_OFFLINE_MAP_GATEWAY, process.env.VUE_APP_OFFLINE_MAP_PROXY))
+      router.use(minioKey, createProxy(gate, minioKey))
+      router.use(thumbnailKey, createThumbnailProxy(gate))
+      router.use('/', createProxy(gate, '/prod-api'))
+    }
+  }
+}
+
+function createProxy (to, replace) {
+  return proxy(to, {
+    https: isHttps(to),
+    parseReqBody: false,
+    proxyReqPathResolver (req) {
+      const url = replace ? `${replace}${req.url}` : req.url
+      console.log(`proxy ${url} to ${to}`)
+      return url
+    }
+  })
+}
+
+function createThumbnailProxy (to) {
+  return proxy(to, {
+    https: isHttps(to),
+    parseReqBody: false,
+    proxyReqPathResolver (req) {
+      const url = `${thumbnailKey}${req.url.replace(new RegExp(`http.*${minioKey}`), `http${isHttps ? 's' : ''}://${gate}${minioKey}`)}`
+      console.log(`thumbnail ${url} to ${to}`)
+      return url
+    }
+  })
+}

+ 79 - 0
package.json

@@ -0,0 +1,79 @@
+{
+  "private": true,
+  "name": "msr",
+  "version": "1.0.0",
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build",
+    "lint": "eslint . --ext .js,.vue",
+    "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
+    "commit": "cz"
+  },
+  "dependencies": {
+    "@amap/amap-jsapi-loader": "^1.0.1",
+    "@wangeditor/editor": "^5.1.20",
+    "@wangeditor/editor-for-vue": "^1.0.2",
+    "app-info-parser": "^1.1.3",
+    "axios": "^0.24.0",
+    "core-js": "^3.6.5",
+    "crypto-js": "^4.1.1",
+    "dom-to-image": "^2.6.0",
+    "echarts": "^5.4.3",
+    "element-ui": "^2.15.6",
+    "hls.js": "^1.1.2",
+    "keycloak-js": "^15.0.2",
+    "maplibre-gl": "2.4.0",
+    "md5": "^2.3.0",
+    "mediainfo.js": "^0.1.7",
+    "mpegts.js": "^1.6.10",
+    "mqtt": "^4.3.7",
+    "nprogress": "^0.2.0",
+    "path-to-regexp": "^6.2.0",
+    "spark-md5": "^3.0.2",
+    "vue": "^2.6.11",
+    "vue-router": "^3.2.0",
+    "vue-seamless-scroll": "^1.1.23",
+    "vuedraggable": "^2.24.3",
+    "vuex": "^3.4.0"
+  },
+  "devDependencies": {
+    "@babel/core": "^7.18.2",
+    "@babel/eslint-parser": "^7.18.2",
+    "@commitlint/cli": "^16.2.3",
+    "@commitlint/config-conventional": "^16.2.1",
+    "@vue/cli-plugin-babel": "~4.5.0",
+    "@vue/cli-plugin-router": "~4.5.0",
+    "@vue/cli-plugin-vuex": "~4.5.0",
+    "@vue/cli-service": "~4.5.0",
+    "chokidar": "^3.5.2",
+    "commitizen": "^4.2.4",
+    "conventional-changelog-cli": "^2.1.1",
+    "cz-conventional-changelog": "^3.3.0",
+    "eslint": "^8.17.0",
+    "eslint-plugin-vue": "^8.0.3",
+    "express-http-proxy": "^1.6.3",
+    "lint-staged": "^9.5.0",
+    "mockjs": "^1.1.0",
+    "sass": "^1.56.1",
+    "sass-loader": "^8.0.2",
+    "script-ext-html-webpack-plugin": "^2.1.5",
+    "svg-sprite-loader": "^6.0.11",
+    "vue-template-compiler": "^2.6.11",
+    "yorkie": "^2.0.0"
+  },
+  "gitHooks": {
+    "pre-commit": "lint-staged",
+    "commit-msg": "commitlint --env GIT_PARAMS"
+  },
+  "lint-staged": {
+    "*.{js,jsx,vue}": [
+      "eslint --ext .js,.vue --fix",
+      "git add"
+    ]
+  },
+  "config": {
+    "commitizen": {
+      "path": "./node_modules/cz-conventional-changelog"
+    }
+  }
+}

BIN
platform/fav/fujian.ico


BIN
platform/fav/inspur.ico


BIN
platform/logo/fujian.png


BIN
platform/logo/inspur.png


+ 19 - 0
public/hash.js

@@ -0,0 +1,19 @@
+// web-worker
+self.importScripts('spark-md5.min.js')
+
+self.onmessage = async e => {
+  const { chunks } = e.data
+  calculate(chunks)
+}
+
+async function calculate (chunks) {
+  const spark = new self.SparkMD5.ArrayBuffer()
+  const total = chunks.length
+  const appendToSpark = blob => blob.arrayBuffer().then(arrayBuffer => spark.append(arrayBuffer))
+  self.postMessage({ index: 0, total })
+  for (let i = 0; i < total; i++) {
+    await appendToSpark(chunks[i].raw)
+    self.postMessage({ index: i, total })
+  }
+  self.postMessage({ hash: spark.end() })
+}

+ 96 - 0
public/index.html

@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+  <meta charset="utf-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+  <meta name="renderer" content="webkit">
+  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+  <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+  <title>
+    <%= webpackConfig.name %>
+  </title>
+  <style type="text/css">
+    #loading {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      -webkit-transform: translate(-50%, -50%);
+      transform: translate(-50%, -50%);
+      text-align: center;
+      z-index: 999;
+    }
+
+    @-webkit-keyframes ball-beat {
+      50% {
+        opacity: 0.2;
+        -webkit-transform: scale(0.75);
+        transform: scale(0.75);
+      }
+
+      100% {
+        opacity: 1;
+        -webkit-transform: scale(1);
+        transform: scale(1);
+      }
+    }
+
+    @keyframes ball-beat {
+      50% {
+        opacity: 0.2;
+        -webkit-transform: scale(0.75);
+        transform: scale(0.75);
+      }
+
+      100% {
+        opacity: 1;
+        -webkit-transform: scale(1);
+        transform: scale(1);
+      }
+    }
+
+    #loading .ball-beat>div {
+      display: inline-block;
+      background-color: #1c5cb0;
+      width: 15px;
+      height: 15px;
+      border-radius: 100% !important;
+      margin: 2px;
+      -webkit-animation-fill-mode: both;
+      animation-fill-mode: both;
+      -webkit-animation: ball-beat 0.7s 0s infinite linear;
+      animation: ball-beat 0.7s 0s infinite linear;
+    }
+
+    #loading .ball-beat>div:nth-child(2n-1) {
+      -webkit-animation-delay: 0.35s !important;
+      animation-delay: 0.35s !important;
+    }
+
+  </style>
+</head>
+
+<body>
+  <noscript>
+    <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
+        Please enable it to continue.</strong>
+  </noscript>
+  <div id="app">
+    <div id="loading">
+      <div style="display: inline-flex; align-items: center; margin-bottom: 32px; line-height: 1;">
+        <img src="./logo.png" />
+        <div style="margin-left: 16px; color: #1c5cb0; font-size: 28px; font-weight: bold;">
+          <%= webpackConfig.name %>
+        </div>
+      </div>
+      <div class="loader-inner ball-beat">
+        <div></div>
+        <div></div>
+        <div></div>
+      </div>
+    </div>
+  </div>
+  <!-- built files will be auto injected -->
+</body>
+
+</html>

+ 26 - 0
public/mediainfo.js

@@ -0,0 +1,26 @@
+// web-worker
+self.importScripts('mediainfo.min.js')
+
+self.onmessage = async e => {
+  calculate(e.data)
+}
+
+function readChunk (file, chunkSize, offset) {
+  return file.slice(offset, offset + chunkSize).arrayBuffer().then(arrayBuffer => new Uint8Array(arrayBuffer))
+}
+
+async function calculate (obj) {
+  self.MediaInfo().then(mediaInfo => {
+    const { totalSize, file } = obj
+    mediaInfo
+      .analyzeData(() => totalSize, (...args) => readChunk(file, ...args))
+      .then(result => {
+        self.postMessage({ media: result.media })
+      })
+      .catch(e => {
+        self.postMessage({ error: e })
+      })
+  }, e => {
+    self.postMessage({ error: e })
+  })
+}

+ 13 - 0
src/App.vue

@@ -0,0 +1,13 @@
+<template>
+  <div id="app">
+    <router-view />
+  </div>
+</template>
+
+<style lang="scss">
+#app {
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  height: 100%;
+}
+</style>

+ 461 - 0
src/api/asset.js

@@ -0,0 +1,461 @@
+import request, {
+  tenantRequest,
+  downloadRequest
+} from '@/utils/request.js'
+import {
+  State,
+  Dataset
+} from '@/constant.js'
+import {
+  getAssetThumb,
+  getAssetDiff,
+  parseTime
+} from '@/utils'
+import {
+  send,
+  add,
+  del,
+  update,
+  submit,
+  resolve,
+  messageSend,
+  confirmAndSend,
+  addStatus,
+  addTenant
+} from './base.js'
+
+function download ({ data, headers }, fileName) {
+  const blob = new Blob([data], { type: headers['content-type'] })
+  const url = window.URL.createObjectURL(blob)
+  const dom = document.createElement('a')
+  dom.href = url
+  dom.download = decodeURI(fileName)
+  dom.style.display = 'none'
+  document.body.appendChild(dom)
+  dom.click()
+  dom.parentNode.removeChild(dom)
+  window.URL.revokeObjectURL(url)
+}
+
+export function resourceExport (data) {
+  return send({
+    url: '/minio-data/usage/count/export',
+    method: 'POST',
+    data
+  }, downloadRequest).then(response => {
+    download(response, `资源曝光率${parseTime(new Date(), '{y}{m}{d}{h}{i}{s}')}.zip`)
+  })
+}
+
+export function addAsset (data) {
+  return add({
+    url: '/minio-data',
+    method: 'POST',
+    data: addTenant(data)
+  }, tenantRequest)
+}
+
+export function getAsset (keyName) {
+  return send({
+    url: '/minio-data/queryById',
+    method: 'GET',
+    params: { keyName }
+  })
+}
+
+export function getAssetsByQuery (query) {
+  const { pageNum: currentPage, pageSize: pageCount, typeList, type, ...params } = query
+  return request({
+    url: '/minio-data/listByPage',
+    method: 'POST',
+    data: addStatus({
+      currentPage,
+      pageCount,
+      typeList: typeList || (type ? [type] : void 0),
+      ...params
+    })
+  }).then(({ data, totalCount }) => {
+    data.forEach(asset => {
+      if (asset.status === State.DRAFT) {
+        asset.draft = '解析中'
+      } else if (asset.status === State.TRANSCODE_FAILURE) {
+        asset.draft = '转码失败'
+      } else {
+        const tag = asset.tag
+        asset.file = {
+          type: asset.type,
+          url: asset.keyName,
+          thumb: getAssetThumb(asset),
+          files: asset.childrenData?.length
+            ? asset.childrenData.map(({ type, keyName, size, md5, sort }) => {
+              // 仅会为图片
+              return { tag, type, keyName, size, md5, sort }
+            })
+            : null
+        }
+      }
+      asset.diff = getAssetDiff(asset)
+    })
+    return { data, totalCount }
+  })
+}
+
+export function updateAsset (data) {
+  return update({
+    url: '/minio-data/update',
+    method: 'POST',
+    data
+  })
+}
+
+export function deleteAsset ({ keyName, originalName }) {
+  return del({
+    url: '/minio-data/delete',
+    method: 'DELETE',
+    params: { keyName }
+  }, originalName)
+}
+
+export function deleteAssets (keyNames) {
+  return del({
+    url: '/minio-data/batchDelete',
+    method: 'POST',
+    data: keyNames
+  }, '所选资源')
+}
+
+export function getAssetUrl (keyName) {
+  return `${process.env.VUE_APP_MINIO}/${keyName}`
+}
+
+const LIMIT_SIZE = 1024 * 1024
+export function getThumbnailUrl (item, option) {
+  let url
+  if (item && typeof item === 'object') {
+    const { size, keyName } = item
+    if (size <= LIMIT_SIZE) {
+      return getAssetUrl(keyName)
+    }
+    url = getAssetUrl(keyName)
+    option = getImageProxyOption(option, size)
+  } else {
+    url = getAssetUrl(item)
+    option = getImageProxyOption(option)
+  }
+  if (url.charAt(0) === '/') {
+    url = `${process.env.VUE_APP_THUMBNAIL_ORIGIN || location.origin}${url}`
+  }
+  return `${process.env.VUE_APP_THUMBNAIL}/${option}/${url}`
+}
+
+function getImageProxyOption (option, size) {
+  switch (option) {
+    case 'size':
+      return size ? `q${Math.ceil(LIMIT_SIZE * 100 / size)}` : 'q60'
+    default:
+      return option || 'x0.2,q30'
+  }
+}
+
+export function submitAsset ({ keyName, originalName }) {
+  return submit({
+    url: '/minio-data/submit',
+    method: 'GET',
+    params: { keyName }
+  }, originalName)
+}
+
+export function resolveAsset ({ keyName, originalName }) {
+  return resolve({
+    url: '/minio-data/reviewed',
+    method: 'GET',
+    params: { keyName }
+  }, originalName)
+}
+
+export function rejectAsset ({ keyName }, remark) {
+  return messageSend({
+    url: '/minio-data/reject',
+    method: 'POST',
+    data: { keyName, remark }
+  }, '驳回')
+}
+
+export function getAssetSubTags () {
+  return tenantRequest({
+    url: '/minio-data/subtag/queryList',
+    method: 'GET',
+    params: addTenant({})
+  })
+}
+
+export function getAssetSubTagsByTenant (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return tenantRequest({
+    url: '/minio-data/subtag/queryListPage',
+    method: 'GET',
+    params: addTenant({
+      pageIndex, pageSize,
+      ...params
+    })
+  })
+}
+
+export function addAssetSubTag (data) {
+  return add({
+    url: '/minio-data/subtag/add',
+    method: 'POST',
+    data: addTenant(data)
+  }, tenantRequest)
+}
+
+export function updateAssetSubTag (data) {
+  return update({
+    url: '/minio-data/subtag/modify',
+    method: 'PUT',
+    data
+  })
+}
+
+export function deleteAssetSubTag ({ id, name }) {
+  return del({
+    url: '/minio-data/subtag/delBatchByIds',
+    method: 'POST',
+    data: [id]
+  }, name)
+}
+
+export function deleteAssetSubTags (ids) {
+  return del({
+    url: '/minio-data/subtag/delBatchByIds',
+    method: 'POST',
+    data: ids
+  }, '所选标签')
+}
+
+export function getDatasets (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return tenantRequest({
+    url: '/media/dataset/pageQuery',
+    method: 'GET',
+    params: addTenant({
+      pageIndex, pageSize,
+      ...params
+    })
+  })
+}
+
+export function addDataset (data) {
+  return add({
+    url: '/media/dataset',
+    method: 'POST',
+    data: addTenant(data)
+  }, tenantRequest)
+}
+
+export function copyDataset (data) {
+  return messageSend({
+    url: '/media/dataset/copy',
+    method: 'POST',
+    data
+  }, '复制')
+}
+
+export function updateDataset (data) {
+  return update({
+    url: '/media/dataset',
+    method: 'PUT',
+    data
+  })
+}
+
+export function getFillDataset (id) {
+  return request({
+    url: `/media/dataset/${id}`,
+    params: {
+      type: Dataset.FILL,
+      flag: 1
+    }
+  })
+}
+
+export function getCommonDataset (id) {
+  return request({
+    url: '/media/dataset/order',
+    params: {
+      id,
+      flag: 1
+    }
+  })
+}
+
+export function deleteDataset ({ id, name }) {
+  return del({
+    url: `/media/dataset/${id}`,
+    method: 'DELETE'
+  }, name)
+}
+
+export function bindAssetsToDataset (datasetId, assets) {
+  return messageSend({
+    url: '/media/dataset/batchBindAsset',
+    method: 'POST',
+    data: assets.map(item => {
+      return {
+        datasetId,
+        ...item
+      }
+    })
+  }, '添加')
+}
+
+export function updateDatasetAssets (datasetId, assets) {
+  return update({
+    url: '/media/dataset/orderBindAsset',
+    method: 'POST',
+    data: {
+      id: datasetId,
+      relationList: assets
+    }
+  })
+}
+
+export function getDevicesByDataset (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return request({
+    url: '/media/dataset/pageQueryDevice',
+    method: 'GET',
+    params: {
+      pageIndex, pageSize,
+      ...params
+    }
+  })
+}
+
+export function countByDay (query) {
+  return request({
+    url: '/minio-data/usage/countByDay',
+    method: 'GET',
+    params: query
+  })
+}
+
+// 查询媒资执行队列
+export function listActivate (query) {
+  return request({
+    url: '/minio-data/mediaProcessTask/listActivate',
+    method: 'GET',
+    params: query
+  })
+}
+
+export function mediaList (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return tenantRequest({
+    url: '/minio-data/mediaProcessTask/pageQuery',
+    method: 'GET',
+    params: addTenant({
+      pageIndex, pageSize,
+      ...params
+    })
+  })
+}
+
+// 转换旧媒资
+export function secondaryTransList (data) {
+  return request({
+    url: '/minio-data/mediaProcessTask/batchSave',
+    method: 'POST',
+    data
+  })
+}
+
+export function treeLocation (data) {
+  return request({
+    url: '/minio-data/treeLocation',
+    method: 'PUT',
+    data
+  })
+}
+
+export function getDatasetByDevice (id) {
+  return request({
+    url: `/media/dataset/${id}`,
+    params: {
+      type: 1,
+      flag: 1
+    }
+  })
+}
+
+export function bindDatasetToDevice (data) {
+  return messageSend({
+    url: '/media/dataset/bindDevice',
+    method: 'POST',
+    data
+  }, '绑定')
+}
+
+export function unbindDatasetByDevice ({ id, name }) {
+  return confirmAndSend('解绑', name, {
+    url: '/media/dataset/batchUnbindDevice',
+    method: 'POST',
+    data: [id]
+  })
+}
+
+export function unbindDatasetByDevices (ids) {
+  return confirmAndSend('解绑', '所选设备', {
+    url: '/media/dataset/batchUnbindDevice',
+    method: 'POST',
+    data: ids
+  })
+}
+
+export function unbindAssetsFromDataset (datasetId, keyNames) {
+  return messageSend({
+    url: `/media/dataset/batchUnbindAsset/${datasetId}`,
+    method: 'POST',
+    data: keyNames
+  }, '移除')
+}
+
+export function updateDatasetAssetDuration (relationId, adDuration) {
+  return update({
+    url: '/media/dataset/assetChangDuration',
+    method: 'POST',
+    data: {
+      relationId,
+      adDuration
+    }
+  })
+}
+
+export function getVideoBitrate () {
+  return request({
+    url: '/minio-data/videoBitRate',
+    method: 'GET'
+  })
+}
+
+export function setVideoBitrate (videoBitRate) {
+  return update({
+    url: '/minio-data/videoBitRate',
+    method: 'POST',
+    data: { videoBitRate }
+  })
+}
+
+export function cancelProcess (id) {
+  return confirmAndSend('取消', '转码', {
+    url: `/minio-data/mediaProcessTask/task/stop/${id}`,
+    method: 'DELETE'
+  })
+}
+
+export function cancelAllProcess () {
+  return confirmAndSend('取消', '全部转码任务', {
+    url: `/minio-data/mediaProcessTask/task/stop/all`,
+    method: 'DELETE'
+  })
+}

+ 150 - 0
src/api/base.js

@@ -0,0 +1,150 @@
+import store from '@/store'
+import {
+  MessageBox,
+  Message
+} from 'element-ui'
+import { State } from '@/constant'
+import request from '@/utils/request'
+import {
+  showLoading,
+  closeLoading
+} from '@/utils/pop'
+
+export function addTenant (data = {}) {
+  if (data.tenant && store.getters.isSuperAdmin) {
+    return data
+  }
+  return {
+    ...data,
+    tenant: store.getters.tenant
+  }
+}
+
+export function addOrg (data = {}) {
+  return {
+    ...data,
+    org: store.getters.org
+  }
+}
+
+export function addTenantAndOrg (data = {}) {
+  return {
+    ...data,
+    tenant: store.getters.tenant,
+    org: store.getters.org
+  }
+}
+
+export function addTenantOrOrg (data = {}) {
+  return store.getters.isTopGroup
+    ? {
+      ...data,
+      tenant: store.getters.tenant
+    }
+    : {
+      ...data,
+      org: store.getters.org
+    }
+}
+
+export function addUser (data = {}, key = 'user') {
+  return {
+    ...data,
+    [key]: store.getters.userId
+  }
+}
+
+export function addScope (data) {
+  return store.getters.isGroupAdmin
+    ? addTenantOrOrg(data)
+    : addUser(data)
+}
+
+const Status = {
+  [State.READY]: [-1, 0],
+  [State.SUBMITTED]: [1],
+  [State.RESOLVED]: [2],
+  [State.REJECTED]: [3, 4, 5],
+  [State.REVIEW_ASSET]: [0, 1],
+  [State.AVAILABLE]: [0, 1, 2],
+  [State.DRAFT_CONTENT]: [-1, 0, 3, 4, 5]
+}
+export function addStatus ({ status, ...data }) {
+  data.statusList = Status[status]
+  return data
+}
+
+export function addStatusScope ({ status, ...data }) {
+  switch (status) {
+    case State.RESOLVED:
+      return {
+        ...addTenant(data),
+        status
+      }
+    case State.AVAILABLE:
+      return {
+        ...addTenantOrOrg(data),
+        status
+      }
+    default:
+      return {
+        ...addUser(data),
+        status
+      }
+  }
+}
+
+export function send (config, service = request) {
+  const loading = showLoading()
+  return service(config).finally(() => {
+    closeLoading(loading)
+  })
+}
+
+export function messageSend (config, message, service = request) {
+  return (config.onUploadProgress ? service(config) : send(config, service)).then(data => {
+    message && Message({
+      type: 'success',
+      message: `${message}成功`
+    })
+    return data
+  })
+}
+
+export function confirm (message) {
+  return MessageBox.confirm(
+    message,
+    '操作确认',
+    { type: 'warning' }
+  )
+}
+
+export function confirmAndSend (type, tip, config, service = request) {
+  return confirm(
+    tip ? `${type} ${tip} ?` : `${type}?`
+  ).then(() => messageSend(config, type, service))
+}
+
+export function add (config, service = request) {
+  return messageSend(config, '新增', service)
+}
+
+export function update (config, message, service = request) {
+  return messageSend(config, message || '更新', service)
+}
+
+export function submit (config, tip, service = request) {
+  return confirmAndSend('提交', tip, config, service)
+}
+
+export function del (config, tip, service = request) {
+  return confirmAndSend('删除', tip, config, service)
+}
+
+export function resolve (config, tip, service = request) {
+  return confirmAndSend('通过', tip, config, service)
+}
+
+export function reject (config, tip, service = request) {
+  return confirmAndSend('驳回', tip, config, service)
+}

+ 163 - 0
src/api/calendar.js

@@ -0,0 +1,163 @@
+import { Message } from 'element-ui'
+import request, { tenantRequest } from '@/utils/request'
+import {
+  add,
+  del,
+  submit,
+  resolve,
+  send,
+  messageSend,
+  addStatus,
+  addStatusScope,
+  addTenant
+} from './base'
+import { ScheduleType } from '@/constant'
+
+export function getSchedulesByQuery (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return tenantRequest({
+    url: '/content/calendar/page',
+    method: 'POST',
+    data: addStatus({
+      pageIndex, pageSize,
+      ...params
+    })
+  })
+}
+
+export function getSchedules (query) {
+  return getSchedulesByQuery(addStatusScope(query))
+}
+
+export function getSchedule (id, options) {
+  return request({
+    url: `/content/programCalendar/${id}`,
+    method: 'GET',
+    ...options
+  }).then(({ data }) => {
+    if (data) {
+      const { eventDetail, ...schedule } = data
+      return { events: JSON.parse(eventDetail) || [], ...schedule }
+    }
+    return null
+  })
+}
+
+export function addSchedule (data) {
+  return add({
+    url: '/content/calendar',
+    method: 'POST',
+    data: addTenant(data)
+  }, tenantRequest)
+}
+
+export function deleteSchedule ({ id, name }) {
+  return del({
+    url: `/content/calendar/${id}`,
+    method: 'DELETE'
+  }, name)
+}
+
+export function deleteSchedules (ids) {
+  return del({
+    url: '/content/calendar/batchDelete',
+    method: 'POST',
+    data: ids
+  }, '所选内容')
+}
+
+export function saveScheduleEvents (schedule, events) {
+  const { id } = schedule
+  return messageSend({
+    url: `/content/calendar/${id}/eventList`,
+    method: 'POST',
+    data: events
+  }, '保存')
+}
+
+function checkSchedule (type, events) {
+  if (!events.length) {
+    Message({
+      type: 'warning',
+      message: '请先添加节目'
+    })
+    return false
+  }
+  switch (type) {
+    case ScheduleType.RECUR:
+      if (events.length < 2) {
+        Message({
+          type: 'warning',
+          message: '请至少添加两个节目'
+        })
+        return false
+      }
+      break
+    case ScheduleType.COMPLEX:
+      if (type === ScheduleType.COMPLEX) {
+        const now = Date.now()
+        if (!events.some(({ until }) => !until || new Date(until) > now)) {
+          Message({
+            type: 'warning',
+            message: '无有效节目,请添加节目'
+          })
+          return false
+        }
+      }
+      break
+    default:
+      Message({
+        type: 'warning',
+        message: '不支持的类型'
+      })
+      return false
+  }
+  return true
+}
+
+export function submitSchedule ({ id, type, name }, events) {
+  return new Promise((resolve, reject) => {
+    if (events) {
+      resolve(events)
+    } else {
+      send({
+        url: `/content/programCalendar/${id}`,
+        method: 'GET'
+      }).then(({ data }) => {
+        resolve(JSON.parse(data.eventDetail))
+      }, reject)
+    }
+  }).then(events => {
+    if (checkSchedule(type, events)) {
+      return submit({
+        url: `/content/calendar/${id}/submit`,
+        method: 'POST'
+      }, name)
+    }
+    return Promise.reject()
+  })
+}
+
+export function resolveSchedule ({ id, name }) {
+  return resolve({
+    url: `/content/calendar/${id}/approval`,
+    method: 'POST',
+    data: { remark: '' }
+  }, name)
+}
+
+export function rejectSchedule ({ id }, remark) {
+  return messageSend({
+    url: `/content/calendar/${id}/reject`,
+    method: 'POST',
+    data: { remark }
+  }, '驳回')
+}
+
+export function copySchedule (schedule) {
+  return messageSend({
+    url: '/content/calendar/copy',
+    method: 'POST',
+    data: addTenant(schedule)
+  }, '复制', tenantRequest)
+}

+ 54 - 0
src/api/camera.js

@@ -0,0 +1,54 @@
+import request from '@/utils/request'
+import { update } from './base'
+
+// 获取摄像头人流量
+export function getStatistic (params) {
+  return request({
+    url: '/camera/statistic',
+    method: 'GET',
+    params
+  })
+}
+
+// 获取摄像头当前的视频参数
+export function getVideoInfo (deviceId) {
+  return request({
+    url: `/camera/${deviceId}/video/param`,
+    method: 'GET'
+  })
+}
+
+// 获取摄像头可用的视频参数
+export function getAvailableParams () {
+  return request({
+    url: `/camera/video/availableParam`,
+    method: 'GET'
+  })
+}
+
+// 设置摄像头参数
+export function setCameraParams (data) {
+  const { deviceId, ...params } = data
+  return update({
+    url: `/camera/${deviceId}/video/param`,
+    method: 'PUT',
+    data: params
+  })
+}
+
+// 摄像头在线状态
+export function isOnline (identifier, options) {
+  return request({
+    url: `/camera/${identifier}/online`,
+    method: 'GET',
+    ...options
+  })
+}
+
+// 直播流详情
+export function getStreamDetail (stream) {
+  return request({
+    url: `/${stream}/detail`,
+    method: 'GET'
+  })
+}

+ 638 - 0
src/api/device.js

@@ -0,0 +1,638 @@
+import store from '@/store'
+import request, { tenantRequest } from '@/utils/request.js'
+import {
+  add,
+  update,
+  del,
+  confirm,
+  messageSend,
+  send,
+  confirmAndSend,
+  addTenant,
+  addTenantOrOrg,
+  addUser
+} from './base.js'
+import {
+  AssetType,
+  SupportedAlarmStrategies
+} from '@/constant.js'
+
+export function getRatiosWithUser () {
+  return tenantRequest({
+    url: '/device/resolutionRatio',
+    method: 'GET',
+    params: addTenantOrOrg({})
+  }).then(({ data }) => {
+    return {
+      data: Object.keys(data).map(key => {
+        return {
+          value: key,
+          label: `${key} ${data[key].slice(0, 3).map(device => device.name).join(', ')}`
+        }
+      })
+    }
+  })
+}
+
+export function getRatios () {
+  return tenantRequest({
+    url: '/device/resolutionRatio',
+    method: 'GET',
+    params: addTenant({})
+  }).then(({ data }) => {
+    return {
+      data: Object.keys(data).map(key => {
+        return {
+          value: key,
+          label: key
+        }
+      })
+    }
+  })
+}
+
+export function getTimeline (deviceId, options) {
+  return request({
+    url: `/content/deviceCalender/${deviceId}`,
+    method: 'GET',
+    ...options
+  }).then(({ data }) => JSON.parse(data.eventDetail) || [])
+}
+
+export function getTimelineByRange (deviceId, startTime, endTime, options) {
+  return request({
+    url: `/content/deviceCalender/${deviceId}`,
+    method: 'GET',
+    params: {
+      startTime,
+      endTime
+    },
+    ...options
+  }).then(({ data }) => JSON.parse(data.eventDetail) || [])
+}
+
+export function addDevice (data) {
+  return add({
+    url: '/device',
+    method: 'POST',
+    data
+  })
+}
+
+export function updateDevice (data) {
+  return update({
+    url: '/device',
+    method: 'PUT',
+    data
+  })
+}
+
+export function replaceDevice (id, data) {
+  return update({
+    url: `/device/${id}/replace`,
+    method: 'POST',
+    data
+  })
+}
+
+function deleteDeviceById (id) {
+  return messageSend({
+    url: `/device/${id}`,
+    method: 'DELETE'
+  }, '删除')
+}
+
+export function deleteDevice ({ id, name, masterId }) {
+  if (__SUB_DEVICE__) {
+    if (masterId) {
+      return confirm(`删除备份设备【${name}】?`).then(() => deleteDeviceById(id))
+    }
+    return send({
+      url: `/device/${id}/standbyDevice`,
+      method: 'GET',
+      params: { pageNum: 1, pageSize: 1 }
+    }).then(({ data }) => confirm(
+      data.length
+        ? `删除主设备【${name}】后备份设备也将删除`
+        : `删除设备【${name}】?`
+    )).then(() => deleteDeviceById(id))
+  }
+  return confirm(`删除设备【${name}】?`).then(() => deleteDeviceById(id))
+}
+
+// isAttentionFlag 是否带关注标识位,开启值为1
+// attentionValue 过滤值,2为不过滤
+export function getDevices (query, options) {
+  return getDevicesByQuery(addTenantOrOrg(query), options)
+}
+
+export function getDevicesByQuery (query, options) {
+  const { tenant, org, ...params } = query
+  if (tenant || org && org === store.getters.tenant) {
+    return getDevicesByTenant(tenant || org, params, options)
+  }
+  return getDevicesByOrg(org, params, options)
+}
+
+export function getDevicesByTenant (tenant, query, options) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return tenantRequest({
+    url: '/device/tenant/page',
+    method: 'GET',
+    params: {
+      tenant,
+      pageIndex,
+      pageSize,
+      ...params
+    },
+    timeout: 30000,
+    ...options
+  })
+}
+
+export function getDevicesByOrg (org, query, options) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return tenantRequest({
+    url: '/device/relation/page',
+    method: 'GET',
+    params: {
+      org,
+      pageIndex,
+      pageSize,
+      ...params
+    },
+    timeout: 30000,
+    ...options
+  })
+}
+
+export function activateDevice ({ id, name }) {
+  return confirmAndSend('激活', name, {
+    url: '/device/batch/activate',
+    method: 'PUT',
+    data: [id]
+  })
+}
+
+export function deactivateDevice ({ id, name }) {
+  return confirmAndSend('停用', name, {
+    url: '/device/batch/deactivate',
+    method: 'PUT',
+    data: [id]
+  })
+}
+
+export function getDevice (id) {
+  return request({
+    url: `/device/${id}`,
+    method: 'GET'
+  })
+}
+
+export function getSubDevices (query) {
+  const { id, pageNum: pageIndex, pageSize, ...params } = query
+  return request({
+    url: `/device/${id}/standbyDevice`,
+    method: 'GET',
+    params: {
+      pageIndex, pageSize,
+      ...params
+    }
+  })
+}
+
+export function addSubDevice ({ id }, data) {
+  return add({
+    url: `/device/${id}/addStandby`,
+    method: 'POST',
+    data
+  })
+}
+
+export function addProductType (data) {
+  return add({
+    url: '/productType',
+    method: 'POST',
+    data
+  })
+}
+
+export function updateProductType (data) {
+  return update({
+    url: '/productType',
+    method: 'put',
+    data
+  })
+}
+
+export function deleteProductType ({ id, name }) {
+  return del({
+    url: `/productType/${id}`,
+    method: 'DELETE'
+  }, name)
+}
+
+export function getProductTypes (query, options) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return tenantRequest({
+    url: '/productType/list',
+    method: 'GET',
+    params: addTenant({
+      pageIndex, pageSize,
+      ...params
+    }),
+    ...options
+  })
+}
+
+export function addProduct (data) {
+  return add({
+    url: '/product',
+    method: 'POST',
+    data
+  })
+}
+
+export function updateProduct (data) {
+  return update({
+    url: '/product',
+    method: 'put',
+    data
+  })
+}
+
+export function deleteProduct ({ id, name }) {
+  return del({
+    url: `/product/${id}`,
+    method: 'DELETE'
+  }, name)
+}
+
+const productMap = {}
+export function getProduct (id) {
+  if (productMap[id]) {
+    return Promise.resolve({ data: productMap[id] })
+  }
+  return request({
+    url: `/product/${id}`,
+    method: 'GET'
+  }).then(({ data }) => {
+    if (data) {
+      productMap[id] = data
+    }
+    return { data }
+  })
+}
+
+export function getProducts (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return tenantRequest({
+    url: '/product/list',
+    method: 'GET',
+    params: addTenant({
+      pageIndex, pageSize,
+      ...params
+    })
+  })
+}
+
+export function addDeviceGroup (data) {
+  return add({
+    url: '/deviceGroup',
+    method: 'POST',
+    data
+  })
+}
+
+export function updateDeviceGroup (data) {
+  return update({
+    url: '/deviceGroup',
+    method: 'PUT',
+    data
+  })
+}
+
+export function deleteDeviceGroup ({ id, name }) {
+  return del({
+    url: `/deviceGroup/${id}`,
+    method: 'DELETE'
+  }, name)
+}
+
+export function getDeviceGroups (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return tenantRequest({
+    url: '/deviceGroup/list',
+    method: 'GET',
+    params: addUser({
+      pageIndex, pageSize,
+      ...params
+    })
+  })
+}
+
+export function getDevicesByGroup (id) {
+  return tenantRequest({
+    url: `/deviceGroup/${id}/device`,
+    method: 'GET',
+    params: addTenantOrOrg({})
+  })
+}
+
+export function getDeviceTree () {
+  return tenantRequest({
+    url: '/deviceGroup/deviceTree',
+    method: 'GET',
+    params: addUser(addTenantOrOrg({}))
+  })
+}
+
+export function addDevicesToGroup (id, deviceIds) {
+  return add({
+    url: `/deviceGroup/${id}/device`,
+    method: 'POST',
+    data: deviceIds
+  })
+}
+
+export function deleteDeviceFromGroup (id, { id: deviceId, name }) {
+  return confirmAndSend('移除', name, {
+    url: `/deviceGroup/${id}/device`,
+    method: 'DELETE',
+    params: { deviceId }
+  })
+}
+
+export function getDeviceStatistics () {
+  return getDeviceStatisticsByPath(store.getters.org)
+}
+
+export function getDeviceStatisticsByPath (path, options) {
+  return tenantRequest({
+    url: '/device/listDeviceTotal',
+    method: 'GET',
+    params: path === store.getters.tenant
+      ? { tenant: path }
+      : { org: path },
+    ...options
+  })
+}
+
+const handeEnum = ['应用重启', '设备重启', '恢复出厂', '未干预']
+const typeEnum = ['primary', 'success', 'danger']
+const labelEnum = ['处理中', '成功', '失败']
+
+export function getDeviceAlarms (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return request({
+    url: '/deviceException/list',
+    method: 'GET',
+    params: {
+      pageIndex, pageSize,
+      ...params
+    }
+  }).then(({ data, totalCount }) => {
+    return {
+      data: data.map(({ id, messageId, deviceName, level, pic, picUrl, type, handle, status, happenTime, bugMessage, ...item }) => {
+        const alarm = {
+          id, messageId, deviceName, level, happenTime,
+          file: pic && picUrl
+            ? {
+              type: AssetType.IMAGE,
+              url: picUrl,
+              origin: true
+            }
+            : null,
+          type: bugMessage || `E${type.toString().padStart(3, '0')}`,
+          handle: handeEnum[handle] || '-',
+          status: handle <= 2 && status <= 2
+            ? {
+              type: typeEnum[status],
+              label: labelEnum[status]
+            }
+            : { label: '-' }
+        }
+        SupportedAlarmStrategies.forEach(({ key }) => {
+          alarm[key] = getTag(item[key])
+        })
+        return alarm
+      }),
+      totalCount
+    }
+  })
+}
+
+function getTag (status) {
+  switch (status) {
+    case 0:
+      return {
+        type: 'primary',
+        label: '待发送'
+      }
+    case 1:
+      return {
+        type: 'success',
+        label: '已发送'
+      }
+    case 2:
+      return {
+        type: 'warning',
+        label: '发送中'
+      }
+    case 3:
+      return {
+        type: 'danger',
+        label: '失败'
+      }
+    case 4:
+      return {
+        type: 'danger',
+        label: '无目标'
+      }
+    case 5:
+      return {
+        type: 'warning',
+        label: '未开启'
+      }
+    default:
+      return null
+  }
+}
+
+export function getUsersByInform ({ messageId }) {
+  return request({
+    url: '/deviceException/user/list',
+    method: 'GET',
+    params: { messageId }
+  })
+}
+
+export function getTasks (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return request({
+    url: '/device/functionTask',
+    method: 'GET',
+    params: {
+      pageIndex, pageSize,
+      ...params
+    }
+  })
+}
+
+export function addTask (task) {
+  return add({
+    url: '/device/functionTask',
+    method: 'POST',
+    data: task
+  })
+}
+
+export function updateTask (task) {
+  return update({
+    url: '/device/functionTask',
+    method: 'PUT',
+    data: task
+  })
+}
+
+export function deleteTask ({ taskId }) {
+  return del({
+    url: `/device/functionTask/${taskId}`,
+    method: 'DELETE'
+  })
+}
+
+export function activateTask ({ taskId }) {
+  return confirmAndSend('启用', null, {
+    url: '/device/functionTask/resume',
+    method: 'PUT',
+    data: [taskId]
+  })
+}
+
+export function deactivateTask ({ taskId }) {
+  return confirmAndSend('停用', null, {
+    url: '/device/functionTask/pause',
+    method: 'PUT',
+    data: [taskId]
+  })
+}
+
+export function getShadow (deviceId) {
+  return request({
+    url: `/device/shadow/${deviceId}`,
+    method: 'GET'
+  })
+}
+
+export function getRecordConfig (deviceId, config) {
+  return request({
+    url: '/deviceStream/config',
+    method: 'GET',
+    params: { deviceId },
+    ...config
+  })
+}
+
+export function addRecordConfig (data, config) {
+  return request({
+    url: '/deviceStream/config',
+    method: 'POST',
+    data,
+    ...config
+  })
+}
+
+export function updateRecordConfig (data, config) {
+  return request({
+    url: '/deviceStream/config',
+    method: 'PUT',
+    data,
+    ...config
+  })
+}
+
+export function authCode (stream, options) {
+  return request({
+    url: `/deviceStream/${stream}/authCode`,
+    method: 'GET',
+    ...options
+  })
+}
+
+export function addDeviceAttention (deviceId) {
+  return update({
+    url: `/device/attention/add/${deviceId}`,
+    method: 'POST',
+    data: { deviceId }
+  }, '关注')
+}
+
+export function cancelDeviceAttention (deviceId) {
+  return update({
+    url: `/device/attention/cancel/${deviceId}`,
+    method: 'DELETE'
+  }, '取关')
+}
+
+export function getDeviceAttentionList (options) {
+  return request({
+    url: '/device/user/attention/list',
+    method: 'GET',
+    ...options
+  })
+}
+
+export function getMultiCardStatusReport (id, options) {
+  return request({
+    url: '/device/screenPower/latestStatusReport',
+    method: 'POST',
+    data: { deviceIds: id },
+    ...options
+  })
+}
+
+export function getReceivingCardStatusReport (id, options) {
+  return request({
+    url: '/device/receiverCard/cacheData',
+    method: 'GET',
+    params: { deviceId: id },
+    ...options
+  })
+}
+
+export function getDevicesWithPower (params, options) {
+  return request({
+    url: '/device/bond/multiFunction/list',
+    method: 'GET',
+    params: addTenant(params),
+    timeout: 30000,
+    ...options
+  })
+}
+
+export function getDepartmentDeviceTree (options) {
+  return getDepartmentDeviceTreeByGroup(store.getters.org, options)
+}
+
+export function getDepartmentDeviceTreeByGroup (path, options) {
+  const data = addTenant()
+  if (path && path !== data.tenant) {
+    data.org = path
+  }
+  return tenantRequest({
+    url: '/tenant/department/device/list',
+    method: 'GET',
+    params: data,
+    timeout: 30000,
+    ...options
+  })
+}
+
+export function getPlaylist (data) {
+  return request({
+    url: '/orchestration/timeSplit/page',
+    method: 'POST',
+    data
+  })
+}

+ 505 - 0
src/api/external.js

@@ -0,0 +1,505 @@
+import request from '@/utils/request'
+import {
+  add,
+  update,
+  del,
+  messageSend,
+  send,
+  addTenantAndOrg,
+  addTenant
+} from './base'
+
+// 厂商
+export function getManufacturers (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return request({
+    url: '/device/manufacturer',
+    method: 'GET',
+    params: {
+      pageIndex, pageSize,
+      flag: 1,
+      ...params
+    }
+  })
+}
+
+export function getManufacturersByType (type) {
+  return request({
+    url: '/device/manufacturer',
+    method: 'GET',
+    params: { type }
+  })
+}
+
+export function addManufacturer (data) {
+  return add({
+    url: '/device/manufacturer',
+    method: 'POST',
+    data
+  })
+}
+
+export function updateManufacturer (data) {
+  return update({
+    url: '/device/manufacturer',
+    method: 'PUT',
+    data
+  })
+}
+
+export function deleteManufacturer ({ manufacturerKey, manufacturerName }) {
+  return del({
+    url: '/device/manufacturer',
+    method: 'DELETE',
+    params: { manufacturerKey }
+  }, manufacturerName)
+}
+
+// 屏幕
+export function getScreens (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return request({
+    url: '/device/thirdPartyScreen/list',
+    method: 'GET',
+    params: addTenant({
+      pageIndex, pageSize,
+      ...params
+    })
+  })
+}
+
+export function addScreen (data) {
+  return add({
+    url: '/device/thirdPartyScreen',
+    method: 'POST',
+    data: addTenantAndOrg(data)
+  })
+}
+
+export function updateScreen (data) {
+  return update({
+    url: '/device/thirdPartyScreen',
+    method: 'PUT',
+    data
+  })
+}
+
+export function deleteScreen ({ id }) {
+  return del({
+    url: `/device/thirdPartyScreen/${id}`,
+    method: 'DELETE'
+  }, '该屏幕')
+}
+
+// 发送控制设备
+export function getSendingCards (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return request({
+    url: '/device/thirdPartySendingCard/list',
+    method: 'GET',
+    params: addTenant({
+      pageIndex, pageSize,
+      ...params
+    })
+  })
+}
+
+export function addSendingCard (data) {
+  return add({
+    url: '/device/thirdPartySendingCard',
+    method: 'POST',
+    data: addTenantAndOrg(data)
+  })
+}
+
+export function updateSendingCard (data) {
+  return update({
+    url: '/device/thirdPartySendingCard',
+    method: 'PUT',
+    data
+  })
+}
+
+export function deleteSendingCard ({ id }) {
+  return del({
+    url: `/device/thirdPartySendingCard/${id}`,
+    method: 'DELETE'
+  }, '该发送控制设备')
+}
+
+// 内容保护
+export function getContentProtection (deviceId, options) {
+  return request({
+    url: `/device/contentProtect/${deviceId}`,
+    method: 'GET',
+    ...options
+  })
+}
+
+export function updateContentProtection (deviceId, data) {
+  return update({
+    url: '/device/contentProtect',
+    method: 'POST',
+    data: {
+      deviceId,
+      ...data
+    }
+  })
+}
+
+// 多功能卡
+export function getMultifunctionCards (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return request({
+    url: '/device/thirdPartyMultifunctionCard/pageQuery',
+    method: 'GET',
+    params: addTenant({
+      pageIndex, pageSize,
+      ...params
+    })
+  })
+}
+
+export function addMultifunctionCard (data) {
+  return add({
+    url: '/device/thirdPartyMultifunctionCard',
+    method: 'POST',
+    data: addTenantAndOrg(data)
+  })
+}
+
+export function updateMultifunctionCard (data) {
+  return update({
+    url: '/device/thirdPartyMultifunctionCard',
+    method: 'PUT',
+    data
+  })
+}
+
+export function deleteMultifunctionCard ({ id }) {
+  return del({
+    url: '/device/thirdPartyMultifunctionCard',
+    method: 'DELETE',
+    params: { id }
+  }, '该多功能卡')
+}
+
+// 传感器
+export function getSensors (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return request({
+    url: '/device/thirdPartySensor/pageQuery',
+    method: 'GET',
+    params: addTenant({
+      pageIndex, pageSize,
+      ...params
+    })
+  })
+}
+
+export function addSensor (data) {
+  return add({
+    url: '/device/thirdPartySensor',
+    method: 'POST',
+    data: addTenantAndOrg(data)
+  })
+}
+
+export function updateSensor (data) {
+  return update({
+    url: '/device/thirdPartySensor',
+    method: 'PUT',
+    data
+  })
+}
+
+export function deleteSensor ({ id }) {
+  return del({
+    url: '/device/thirdPartySensor',
+    method: 'DELETE',
+    params: { id }
+  }, '该传感器')
+}
+
+export function getSensorRecords (params, options) {
+  return request({
+    url: '/device/sensorList',
+    method: 'GET',
+    params,
+    ...options
+  })
+}
+
+// PLC
+export function getPLCs (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return request({
+    url: '/device/thirdPartyPlc/pageQuery',
+    method: 'GET',
+    params: addTenant({
+      pageIndex, pageSize,
+      ...params
+    })
+  })
+}
+
+export function addPLC (data) {
+  return add({
+    url: '/device/thirdPartyPlc',
+    method: 'POST',
+    data: addTenantAndOrg(data)
+  })
+}
+
+export function updatePLC (data) {
+  return update({
+    url: '/device/thirdPartyPlc',
+    method: 'PUT',
+    data
+  })
+}
+
+export function deletePLC ({ id }) {
+  return del({
+    url: '/device/thirdPartyPlc',
+    method: 'DELETE',
+    params: { id }
+  }, '该PLC')
+}
+
+// 网关
+export function getGateways (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return request({
+    url: '/device/thirdPartyGateway/list',
+    method: 'GET',
+    params: addTenant({
+      pageIndex, pageSize,
+      ...params
+    })
+  })
+}
+
+export function addGateway (data) {
+  return add({
+    url: '/device/thirdPartyGateway',
+    method: 'POST',
+    data: addTenantAndOrg(data)
+  })
+}
+
+export function updateGateway (data) {
+  return update({
+    url: '/device/thirdPartyGateway',
+    method: 'PUT',
+    data
+  })
+}
+
+export function deleteGateway ({ id }) {
+  return del({
+    url: `/device/thirdPartyGateway/${id}`,
+    method: 'DELETE'
+  }, '该网关')
+}
+
+export function plcCommand (deviceId, status) {
+  return messageSend({
+    url: '/device/thirdplc/command',
+    method: 'POST',
+    data: { deviceId, status }
+  }, '执行')
+}
+
+// 摄像头
+export function getCameras (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return request({
+    url: '/camera/list',
+    method: 'GET',
+    params: addTenant({
+      pageIndex, pageSize,
+      ...params
+    })
+  })
+}
+
+// 摄像头单个查询
+export function getCameraById (id) {
+  return request({
+    url: `/camera/${id}`,
+    method: 'GET'
+  })
+}
+
+export function addCamera (data) {
+  return add({
+    url: '/camera',
+    method: 'POST',
+    data: addTenantAndOrg(data)
+  })
+}
+
+export function updateCamera (data) {
+  return update({
+    url: '/camera',
+    method: 'PUT',
+    data
+  })
+}
+
+export function deleteCamera (id) {
+  return del({
+    url: `/camera/${id}`,
+    method: 'DELETE'
+  }, '该摄像头')
+}
+
+// 录像
+// 查询SD卡的录像
+export function getSDRecords ({ identifier, throughEvs, ...params }) {
+  if (throughEvs === 1) {
+    return request({
+      url: `/dahua/evs/${identifier}/queryRecord`,
+      method: 'GET',
+      params: {
+        ...params
+      }
+    })
+  }
+  return request({
+    url: `/camera/${identifier}/queryRecord`,
+    method: 'GET',
+    params: {
+      ...params
+    }
+  })
+}
+
+// 新建下载录像
+export function downloadRecord ({ identifier, ...params }) {
+  return add({
+    url: `/camera/${identifier}/downloadRecord`,
+    method: 'POST',
+    data: params
+  })
+}
+
+// 停止下载录像
+export function stopDownloadRecord (identifier) {
+  return update({
+    url: `/camera/${identifier}/stopDownloadingRecord`,
+    method: 'POST'
+  }, '停止')
+}
+
+// 恢复下载录像
+export function resumeDownloadRecord (id) {
+  return update({
+    url: `/camera/resumeDownload/${id}`,
+    method: 'POST'
+  }, '恢复')
+}
+
+// 录像任务
+export function getRecords (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return request({
+    url: '/camera/downloadRecord/page',
+    method: 'GET',
+    params: {
+      pageIndex, pageSize,
+      ...params
+    }
+  })
+}
+
+// 下载中的录像
+export function getDownloadingRecords (identifier) {
+  return request({
+    url: `/camera/${identifier}/downloadingRecord`,
+    method: 'GET'
+  }).then(({ data }) => {
+    return { data: data ? [data] : null }
+  })
+}
+
+// 删除录像
+export function deleteRecord (id) {
+  return del({
+    url: `/camera/record/${id}`,
+    method: 'DELETE'
+  })
+}
+
+// 接收卡
+export function getReceivingCardManufacturers () {
+  return request({
+    url: '/device/manufacturer/list',
+    method: 'GET'
+  })
+}
+
+export function getReceivingCard (id, options) {
+  return request({
+    url: `/device/information/${id}`,
+    method: 'GET',
+    ...options
+  })
+}
+
+export function getReceivingCards (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return request({
+    url: '/device/thirdPartyReceiverCard/pageQuery',
+    method: 'GET',
+    params: addTenant({
+      pageIndex, pageSize,
+      ...params
+    })
+  })
+}
+
+export function addReceivingCard (data) {
+  return add({
+    url: '/device/thirdPartyReceiverCard',
+    method: 'POST',
+    data: addTenantAndOrg(data)
+  })
+}
+
+export function updateReceivingCard (data) {
+  return update({
+    url: '/device/thirdPartyReceiverCard',
+    method: 'PUT',
+    data
+  })
+}
+
+export function deleteReceivingCard ({ id }) {
+  return del({
+    url: '/device/thirdPartyReceiverCard',
+    method: 'DELETE',
+    params: { id }
+  }, '该接收卡')
+}
+
+export function getReceivingCardTopology (id, options) {
+  return (options?.loading ? send : request)({
+    url: '/device/thirdPartyReceiverCard/getTopology',
+    method: 'GET',
+    params: { id },
+    ...options
+  })
+}
+
+export function updateReceivingCardTopology (id, topology) {
+  return update({
+    url: '/device/thirdPartyReceiverCard/updateTopology',
+    method: 'POST',
+    data: {
+      receiverCardId: id,
+      list: topology
+    }
+  })
+}

+ 136 - 0
src/api/merchant.js

@@ -0,0 +1,136 @@
+import request from '@/utils/request.js'
+import { State } from '@/constant.js'
+import { getAssetThumb } from '@/utils'
+import {
+  send,
+  add,
+  del,
+  update,
+  addTenant
+} from './base.js'
+
+export function addMerchant (data) {
+  return add({
+    url: '/device/siteMerchantManage',
+    method: 'POST',
+    data: addTenant(data)
+  })
+}
+
+export function getMerchantByDeviceId (id) {
+  return send({
+    url: '/device/siteMerchantManage/deviceId',
+    method: 'GET',
+    params: { deviceId: id }
+  })
+}
+
+export function bindMerchantToDevice (data) {
+  return request({
+    url: '/device/siteDeviceRelevancy/bound',
+    method: 'POST',
+    data
+  })
+}
+
+export function getMerchantsByQuery (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return request({
+    url: '/device/siteMerchantManage/pageQuery',
+    method: 'get',
+    params: {
+      pageIndex, pageSize,
+      ...params
+    }
+  })
+}
+
+export function getSignByQuery (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return request({
+    url: '/device/siteAgencyManage/pageQuery',
+    method: 'get',
+    params: {
+      pageIndex, pageSize,
+      ...params
+    }
+  }).then(({ data, totalCount }) => {
+    data.forEach(asset => {
+      if (asset.status === State.DRAFT) {
+        asset.draft = '解析中'
+      } else if (asset.status === State.TRANSCODE_FAILURE) {
+        asset.draft = '转码失败'
+      } else {
+        asset.install = {
+          type: 1,
+          url: asset.installUrl,
+          thumb: getAssetThumb(asset),
+          files: null
+        }
+        asset.nameplate = {
+          type: 1,
+          url: asset.nameplateUrl,
+          thumb: getAssetThumb(asset),
+          files: null
+        }
+      }
+    })
+    return { data, totalCount }
+  })
+}
+
+export function deleteSign ({ id }) {
+  return del({
+    url: '/device/siteAgencyManage',
+    method: 'DELETE',
+    params: { id }
+  })
+}
+
+export function updateMerchant (data) {
+  return update({
+    url: '/device/siteMerchantManage',
+    method: 'PUT',
+    data
+  })
+}
+
+export function getDeviceRelevancy (data) {
+  return update({
+    url: '/device/siteDeviceRelevancy/query',
+    method: 'POST',
+    data
+  })
+}
+
+export function deleteMerchant ({ id }) {
+  return del({
+    url: '/device/siteMerchantManage',
+    method: 'DELETE',
+    params: { id }
+  })
+}
+
+export function uploadMerchant (fromData, onUploadProgress) {
+  const tenant = addTenant()
+  fromData.append('tenant', tenant.tenant || '')
+  return add({
+    url: '/device/siteMerchantManage/import',
+    method: 'POST',
+    data: fromData,
+    timeout: 0,
+    onUploadProgress
+  })
+}
+
+export function getRegion (query) {
+  return request({
+    url: '/device/siteRegionArea/query',
+    method: 'GET',
+    params: {
+      pageIndex: 1,
+      pageSize: 999,
+      ...query
+    }
+  })
+}

+ 242 - 0
src/api/mesh.js

@@ -0,0 +1,242 @@
+import request, { tenantRequest } from '@/utils/request'
+import {
+  add,
+  update,
+  del,
+  addTenant,
+  messageSend
+} from './base'
+
+export function getMeshes (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return tenantRequest({
+    url: '/device/mesh/pageQuery',
+    method: 'GET',
+    params: addTenant({
+      pageIndex, pageSize,
+      ...params
+    })
+  })
+}
+
+export function addMesh (data) {
+  return add({
+    url: '/device/mesh',
+    method: 'POST',
+    data: addTenant(data)
+  }, tenantRequest)
+}
+
+export function deleteMesh ({ meshId, name }) {
+  return del({
+    url: '/device/mesh',
+    method: 'DELETE',
+    params: { meshId }
+  }, name)
+}
+
+export function updateMesh (data) {
+  return update({
+    url: '/device/mesh',
+    method: 'PUT',
+    data
+  })
+}
+
+export function getMesh (meshId) {
+  return request({
+    url: '/device/mesh/getMeshStructure',
+    method: 'GET',
+    params: { meshId }
+  })
+}
+
+export function addNodeToMesh (meshId, node) {
+  return add({
+    url: '/device/mesh/node',
+    method: 'POST',
+    data: {
+      meshId,
+      ...node
+    }
+  })
+}
+
+export function updateNode (node) {
+  return update({
+    url: '/device/mesh/node',
+    method: 'PUT',
+    data: node
+  })
+}
+
+export function deleteNode ({ id }) {
+  return messageSend({
+    url: '/device/mesh/node',
+    method: 'DELETE',
+    params: { nodeId: id }
+  }, '删除')
+}
+
+export function addNodeAfterNode (targetNode, node, port = -1) {
+  return add({
+    url: '/device/bind/insertAfter',
+    method: 'POST',
+    data: {
+      id: targetNode.id,
+      port,
+      node: {
+        meshId: targetNode.meshId,
+        ...node
+      }
+    }
+  })
+}
+
+export function addNodeBeforeNode (targetNode, node, port = -1) {
+  return add({
+    url: '/device/bind/insertAhead',
+    method: 'POST',
+    data: {
+      id: targetNode.id,
+      port,
+      node: {
+        meshId: targetNode.meshId,
+        ...node
+      }
+    }
+  })
+}
+
+export function insertNodeToEdge (edge, node, port = -1) {
+  return add({
+    url: '/device/bind/insertBetween',
+    method: 'POST',
+    data: {
+      id: edge.nodeBondId,
+      port,
+      node: {
+        meshId: edge.meshId,
+        ...node
+      }
+    }
+  })
+}
+
+export function addEdgeToMesh (meshId, sourceNode, targetNode, port = -1) {
+  return messageSend({
+    url: '/device/bind/nodeBond',
+    method: 'POST',
+    data: {
+      meshId,
+      preNodeId: sourceNode.id,
+      preNodeType: sourceNode.nodeType,
+      nextNodeId: targetNode.id,
+      nextNodeType: targetNode.nodeType,
+      port
+    }
+  }, '连接')
+}
+
+export function deleteEdge ({ nodeBondId }) {
+  return messageSend({
+    url: `/device/bind/nodeBond?bondId=${nodeBondId}`,
+    method: 'DELETE'
+  }, '删除连接')
+}
+
+export function updateEdgePort ({ meshId, nodeBondId, preNodeId, preNodeType, nextNodeId, nextNodeType }, port) {
+  const edge = {
+    meshId,
+    nodeBondId,
+    preNodeId,
+    preNodeType,
+    nextNodeId,
+    nextNodeType,
+    port
+  }
+  return messageSend({
+    url: '/device/bind/nodeBond',
+    method: 'PUT',
+    data: edge
+  }, '更新端口').then(() => {
+    return { data: edge }
+  })
+}
+
+export function reverseEdge ({ meshId, nodeBondId, preNodeId, preNodeType, nextNodeId, nextNodeType, port }) {
+  const edge = {
+    meshId,
+    nodeBondId,
+    preNodeId: nextNodeId,
+    preNodeType: nextNodeType,
+    nextNodeId: preNodeId,
+    nextNodeType: preNodeType,
+    port
+  }
+  return messageSend({
+    url: '/device/bind/nodeBond',
+    method: 'PUT',
+    data: edge
+  }, '反转').then(() => {
+    return { data: edge }
+  })
+}
+
+export function bindInstanceToNode ({ meshId, id, nodeType }, instance) {
+  return messageSend({
+    url: '/device/mesh/nodeAndInstance',
+    method: 'POST',
+    data: {
+      meshId, id, nodeType,
+      instanceId: instance.id
+    }
+  }, '绑定')
+}
+
+export function releaseInstanceFormNode ({ id }) {
+  return messageSend({
+    url: `/device/bind/nodeAndInstance?nodeId=${id}`,
+    method: 'DELETE'
+  }, '解绑')
+}
+
+export function getMeshByBox (deviceId) {
+  return request({
+    url: '/device/mesh/getStructureByDeviceId',
+    method: 'GET',
+    params: { deviceId }
+  })
+}
+
+export function getMeshByInstance (instanceId) {
+  return request({
+    url: '/device/mesh/getMeshByInstanceId',
+    method: 'GET',
+    params: { instanceId }
+  })
+}
+
+export function getThirdPartyDevicesByThirdPartyDevice (instanceId, typeList, options) {
+  return request({
+    url: '/device/bind/listAssignedNode',
+    method: 'POST',
+    data: {
+      instanceId,
+      typeList
+    },
+    ...options
+  })
+}
+
+export function getFollowThirdPartyDevicesByThirdPartyDevice (instanceId, typeList, options) {
+  return request({
+    url: '/device/bind/listAssignedBindNode',
+    method: 'POST',
+    data: {
+      instanceId,
+      typeList
+    },
+    ...options
+  })
+}

+ 273 - 0
src/api/platform.js

@@ -0,0 +1,273 @@
+import {
+  PublishType,
+  AssetTagInfo,
+  AssetTypeInfo
+} from '@/constant.js'
+import {
+  parseByte,
+  parseTime,
+  getAssetThumb,
+  getAssetDiff
+} from '@/utils'
+import request, { tenantRequest } from '@/utils/request.js'
+import {
+  add,
+  del,
+  send,
+  messageSend,
+  addUser,
+  addTenant,
+  addTenantAndOrg
+} from './base.js'
+
+// 发布
+export function publish (type, ids, targetList, options) {
+  const requestData = addTenantAndOrg({
+    type,
+    targetList: targetList.map(publishTarget => JSON.stringify(publishTarget)),
+    ...options
+  })
+  switch (type) {
+    case PublishType.PROGRAM_TO_DEVICE:
+    case PublishType.ASSET_TO_DEVICE:
+      requestData.deviceIds = ids
+      break
+    case PublishType.PROGRAM_TO_PRODUCT_TYPE:
+    case PublishType.ASSET_TO_PRODUCT_TYPE:
+      requestData.productTypeIds = ids
+      break
+    default:
+      break
+  }
+  return messageSend({
+    url: '/orchestration/calendarReleaseScheduling',
+    method: 'POST',
+    data: requestData
+  }, '发布', tenantRequest)
+}
+
+export function publishIntercut (data) {
+  return messageSend({
+    url: '/orchestration/deviceCalendarRecordAdd',
+    method: 'POST',
+    data: addTenantAndOrg(data)
+  }, '发布', tenantRequest)
+}
+
+export function savePowerLogger (data) {
+  const { user } = addUser()
+  return tenantRequest({
+    url: '/sysLog/webOperation',
+    method: 'POST',
+    data: addTenant({
+      userId: user,
+      business: 5,
+      ip: '',
+      costTime: 0,
+      status: 1,
+      operTime: parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}'),
+      ...data
+    }),
+    custom: true,
+    background: true
+  })
+}
+
+export function saveBoxLogger (data) {
+  const { user } = addUser()
+  return tenantRequest({
+    url: '/sysLog/webOperation',
+    method: 'POST',
+    data: addTenant({
+      userId: user,
+      business: 99,
+      ip: '',
+      costTime: 0,
+      status: 1,
+      operTime: parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}'),
+      ...data
+    }),
+    custom: true,
+    background: true
+  })
+}
+
+export function getSpacers (data) {
+  return request({
+    url: '/device/spacer/query',
+    method: 'POST',
+    data
+  }).then(({ data: { spacers } }) => {
+    return {
+      data: spacers.map(spacer => {
+        spacer.file = {
+          type: spacer.type,
+          url: spacer.keyName,
+          thumb: getAssetThumb(spacer)
+        }
+        spacer.tagInfo = AssetTagInfo[spacer.tag]
+        spacer.typeInfo = AssetTypeInfo[spacer.type]
+        spacer.sizeInfo = parseByte(spacer.size)
+        spacer.diff = getAssetDiff(spacer)
+        return spacer
+      })
+    }
+  })
+}
+
+export function addSpacers (data) {
+  return add({
+    url: '/device/spacer/set',
+    method: 'POST',
+    data
+  })
+}
+
+export function deleteSpacer ({ id }) {
+  return del({
+    url: `/device/spacer/delete?id=${id}`,
+    method: 'DELETE'
+  })
+}
+
+export function sendDeviceAlarm (data) {
+  return request({
+    url: '/device/error/record',
+    method: 'POST',
+    data: {
+      messageId: `web_${Date.now()}_${Math.random().toString(16).slice(2)}`,
+      handleEnumId: 3,
+      statusEnumId: 0,
+      happenTime: parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}'),
+      ...data
+    }
+  })
+}
+
+// 批量关闭/开启
+export function toggleDevicePower (data) {
+  return messageSend({
+    url: '/device/screenPower/batchOperate',
+    method: 'POST',
+    data
+  }, '添加批量任务')
+}
+
+// 查询上次操作
+export function getOperationResult (operationId) {
+  return (operationId ? request : send)({
+    url: '/device/screenPower/batchOperate',
+    method: 'GET',
+    params: { operationId }
+  })
+}
+
+// 分页查询历史操作结果
+export function getOperationResults (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return request({
+    url: '/device/screenPower/operationResult/pageQuery',
+    method: 'GET',
+    params: {
+      pageIndex, pageSize,
+      ...params
+    }
+  })
+}
+
+export function getPublishConflicts (data) {
+  return send({
+    url: '/orchestration/deviceCalendarConflict',
+    method: 'POST',
+    data
+  })
+}
+
+export function getPublishConfilctPriorities (data) {
+  return request({
+    url: '/orchestration/deviceCalendar/priority',
+    method: 'POST',
+    data,
+    custom: true
+  })
+}
+
+export function getBatchTaskHistory (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return request({
+    url: '/device/powerScheduleHis/list',
+    method: 'GET',
+    params: {
+      pageIndex, pageSize,
+      ...params
+    }
+  })
+}
+
+export function addBatchTaskHistory (value, value1) {
+  return request({
+    url: '/device/powerScheduleHis/add',
+    method: 'POST',
+    data: {
+      value,
+      value1
+    },
+    custom: true,
+    background: true
+  })
+}
+
+export function addTasks (deviceIds, tasks, options) {
+  return send({
+    url: '/device/schedule/task',
+    method: 'POST',
+    data: addTenantAndOrg({
+      deviceIdList: deviceIds,
+      taskList: tasks,
+      ...options
+    })
+  })
+}
+
+export function getTasks (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return request({
+    url: '/device/schedule/task/pageQuery',
+    method: 'POSt',
+    data: addTenant({
+      pageIndex, pageSize,
+      ...params
+    })
+  })
+}
+
+export function updateTasksStatus (data) {
+  return request({
+    url: '/device/schedule/task/status',
+    method: 'PUT',
+    data: addTenantAndOrg(data)
+  })
+}
+
+export function getTaskOperationHistory (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return request({
+    url: '/device/schedule/task/opHis',
+    method: 'POST',
+    data: addTenant({
+      pageIndex, pageSize,
+      ...params
+    })
+  })
+}
+
+export function updateTasks (ids, data) {
+  return request({
+    url: '/device/schedule/task/batchUpt',
+    method: 'PUT',
+    data: {
+      taskIdList: ids,
+      ...data
+    }
+  })
+}

+ 133 - 0
src/api/program.js

@@ -0,0 +1,133 @@
+import request, { tenantRequest } from '@/utils/request'
+import {
+  add,
+  update,
+  del,
+  messageSend,
+  addTenant,
+  addTenantAndOrg,
+  addUser
+} from './base'
+
+export function addProgramDraft (data) {
+  return add({
+    url: '/item/origin/add',
+    method: 'POST',
+    data: addTenantAndOrg(data)
+  }, tenantRequest)
+}
+
+export function getProgramDrafts (query) {
+  const { pageNum: currentPage, pageSize: pageCount, ...params } = query
+  return tenantRequest({
+    url: '/item/origin/listByPage',
+    method: 'POST',
+    data: addUser({
+      currentPage, pageCount,
+      ...params
+    })
+  })
+}
+
+export function deleteProgramDraft ({ id, name }) {
+  return del({
+    url: '/item/origin/delete',
+    method: 'DELETE',
+    params: { id }
+  }, name)
+}
+
+export function updateProgramDraft ({ id, name, duration, itemJsonStr, keyNameList, base64, generate }) {
+  const formData = new FormData()
+  const result = /^data:(.+);base64,(.+)$/.exec(base64)
+  if (result) {
+    const binaryString = atob(result[2])
+    const length = binaryString.length
+    const mine = new Uint8Array(length)
+    for (let i = 0; i < length; i++) {
+      mine[i] = binaryString.charCodeAt(i)
+    }
+    formData.append('file', new Blob([mine], { type: result[1] }))
+  }
+  formData.append('id', id)
+  formData.append('duration', duration)
+  formData.append('itemJsonStr', itemJsonStr)
+  name && formData.append('name', name)
+  generate && formData.append('keyNameList', JSON.stringify(keyNameList))
+  formData.append('generate', generate)
+  return request({
+    url: '/item/origin/update',
+    method: 'POST',
+    data: formData,
+    timeout: 0,
+    custom: true
+  }).then(({ data }) => data)
+}
+
+export function updateProgramDraftName (data) {
+  return update({
+    url: '/item/origin/updateItemName',
+    method: 'POST',
+    data
+  })
+}
+
+export function getProgramDraft (id, options) {
+  return request({
+    url: `/item/origin/getById/${id}`,
+    method: 'GET',
+    ...options
+  })
+}
+
+export function copyProgramDraft (programDraft) {
+  return messageSend({
+    url: '/item/origin/copy',
+    method: 'POST',
+    data: addTenantAndOrg(programDraft)
+  }, '复制', tenantRequest)
+}
+
+export function copyProgram (program) {
+  return messageSend({
+    url: '/item/copy',
+    method: 'POST',
+    data: addTenantAndOrg(program)
+  }, '复制', tenantRequest)
+}
+
+export function getPrograms (query) {
+  const { pageNum: currentPage, pageSize: pageCount, ...params } = query
+  return tenantRequest({
+    url: '/item/listByPage',
+    method: 'POST',
+    data: addTenant({
+      currentPage, pageCount,
+      ...params
+    })
+  })
+}
+
+export function getProgram (id, options) {
+  return request({
+    url: `/item/getById/${id}`,
+    method: 'GET',
+    ...options
+  })
+}
+
+export function deleteProgram ({ id, name }) {
+  return del({
+    url: '/item/delete',
+    method: 'DELETE',
+    params: { id }
+  }, name)
+}
+
+export function updateProgramName (data) {
+  return update({
+    url: '/item/updateItemName',
+    method: 'POST',
+    data
+  })
+}

+ 72 - 0
src/api/statistics.js

@@ -0,0 +1,72 @@
+import request from '@/utils/request'
+
+// yyyy-MM-dd
+export function triggerAdSnap (date) {
+  return request({
+    url: '/ad/statistic/daily/compute/trigger',
+    method: 'GET',
+    params: { date }
+  })
+}
+
+export function getAdReport (query = {}) {
+  if (query.deviceId) {
+    return request({
+      url: '/ad/statistic/daily/device/list',
+      method: 'GET',
+      params: query
+    })
+  }
+  return request({
+    url: '/ad/statistic/daily/tenant/list',
+    method: 'GET',
+    params: query
+  })
+}
+
+export function getAdCollect (query = {}) {
+  if (query.deviceId) {
+    return request({
+      url: '/ad/statistic/aggregate/device',
+      method: 'GET',
+      params: query
+    })
+  }
+  if (query.tenant) {
+    return request({
+      url: '/ad/statistic/aggregate/tenant',
+      method: 'GET',
+      params: query
+    })
+  }
+  return request({
+    url: '/ad/statistic/aggregate/platform',
+    method: 'GET'
+  })
+}
+
+export function triggetOnlineDurationSnap (params) {
+  return request({
+    url: '/deviceOnlineInfoDetail/syncDeviceOnlineSummary',
+    method: 'GET',
+    params
+  })
+}
+
+export function getOnlineDurationByDevice (id) {
+  return request({
+    url: '/deviceOnlineInfoDetail/calOnlineHoursByDeviceId',
+    method: 'POST',
+    data: [id]
+  }).then(({ data }) => {
+    return { data: data[0] }
+  })
+}
+
+export function getOnlineDurationReport (query) {
+  return request({
+    url: '/deviceOnlineSummary/queryByDeviceId',
+    method: 'GET',
+    params: query
+  })
+}

+ 55 - 0
src/api/unified.js

@@ -0,0 +1,55 @@
+import request from '@/utils/request.js'
+import {
+  add,
+  del,
+  update,
+  addTenant
+} from './base.js'
+
+export function addSelf (data) {
+  return add({
+    url: '/minio-data/manage/self/advertising/add',
+    method: 'POST',
+    data: addTenant(data)
+  })
+}
+
+export function getSelfByAdvertisingQuery (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return request({
+    url: '/minio-data/manage/self/advertising/pageQuery',
+    method: 'get',
+    params: {
+      pageIndex, pageSize,
+      ...params
+    }
+  })
+}
+
+export function updateSelf (data) {
+  return update({
+    url: '/minio-data/manage/self/advertising/update',
+    method: 'POST',
+    data
+  })
+}
+
+export function deleteSelf ({ id }) {
+  return del({
+    url: '/minio-data/manage/self/advertising/delete',
+    method: 'POST',
+    data: { ids: [id] }
+  })
+}
+
+export function uploadSelf (fromData, onUploadProgress) {
+  const tenant = addTenant()
+  fromData.append('tenant', tenant.tenant || '')
+  return add({
+    url: '/minio-data/manage/self/advertising/import',
+    method: 'POST',
+    data: fromData,
+    timeout: 0,
+    onUploadProgress
+  })
+}

+ 487 - 0
src/api/user.js

@@ -0,0 +1,487 @@
+import md5 from 'md5'
+import store from '@/store'
+import {
+  Role,
+  AlarmLevelInfo
+} from '@/constant.js'
+import request, {
+  keycloakRequest,
+  tenantRequest
+} from '@/utils/request.js'
+import {
+  send,
+  add,
+  update,
+  messageSend,
+  addTenant as addTenantKey
+} from './base.js'
+
+export function logout (params) {
+  return request({
+    url: '/keycloak/userLogout',
+    method: 'GET',
+    params,
+    custom: true,
+    background: true
+  })
+}
+
+export function getWechatQr (id) {
+  return request({
+    url: `/keycloak/query/ticket/${id}`,
+    method: 'GET'
+  })
+}
+
+export function getAppletQr (params) {
+  return request({
+    url: '/wxapplet/qrcode',
+    method: 'GET',
+    params
+  })
+}
+
+export function userinfo (silence) {
+  const config = {
+    url: `/users/${store.getters.userId}`,
+    method: 'GET'
+  }
+  if (silence) {
+    config.custom = true
+    return keycloakRequest(config)
+  }
+  return send(config, keycloakRequest)
+}
+
+export function addUser (data) {
+  return add({
+    url: '/admin/users',
+    method: 'POST',
+    data: addTenantKey({
+      ...data,
+      credentials: [{ type: 'password', value: process.env.VUE_APP_USER_PASSWORD, temporary: true }]
+    })
+  })
+}
+
+export function updateUser (data, message) {
+  return update({
+    url: '/keycloak/users/attribute',
+    method: 'PUT',
+    data
+  }, message)
+}
+
+export function toggleUser ({ userId, userName }, enabled) {
+  return messageSend({
+    url: '/admin/users/enabled',
+    method: 'PUT',
+    data: addTenantKey({
+      userId,
+      userName,
+      enabled
+    })
+  }, enabled ? '启用' : '停用')
+}
+
+export function deleteUser ({ userId, userName }) {
+  return messageSend({
+    url: `/admin/users/${userId}?userName=${userName}`,
+    method: 'DELETE'
+  }, '注销')
+}
+
+export function getUserCredentials (userId) {
+  return request({
+    url: `/admin/users/${userId}/credentials`,
+    method: 'GET'
+  }).then(({ data }) => data)
+}
+
+export function deleteUserCredentials ({ userId, userName }, credentialId) {
+  return messageSend({
+    url: `/admin/users/${userId}/credentials/${credentialId}?userName=${userName}`,
+    method: 'DELETE'
+  }, '重置')
+}
+
+export function resetPassword ({ userId, userName }) {
+  return messageSend({
+    url: `/admin/users/${userId}/resetPassword`,
+    method: 'PUT',
+    data: {
+      type: 'password',
+      value: process.env.VUE_APP_USER_PASSWORD,
+      temporary: true,
+      userLabel: userName
+    }
+  }, '重置')
+}
+
+export function resetPasswordByUser (newPassword) {
+  return messageSend({
+    url: '/keycloak/changePassword',
+    method: 'PUT',
+    params: { newPassword: md5(`${newPassword}${process.env.VUE_APP_SALT}`) }
+  }, '重置')
+}
+
+export function getTenantsByQuery (query, recursive) {
+  const { pageSize: max, pageNum, name, ...params } = query
+  return keycloakRequest({
+    url: '/groups',
+    method: 'GET',
+    params: {
+      briefRepresentation: false,
+      max,
+      first: pageNum ? (pageNum - 1) * max : void 0,
+      search: name || void 0,
+      ...params
+    }
+  }).then(data => {
+    return { data: normalizeGroups(data, recursive) }
+  })
+}
+
+function normalizeGroups (groups, recursive, parentGroup) {
+  return groups.map(({ id, name, path, attributes, subGroups }) => {
+    const remark = attributes.remark?.[0] || ''
+    const group = {
+      parentGroup,
+      id,
+      path,
+      name,
+      remark,
+      label: remark || name
+    }
+    if (!recursive) {
+      return group
+    }
+    group.subGroups = normalizeGroups(subGroups.sort((a, b) => a.name <= b.name ? -1 : 1), recursive, group)
+    return group
+  })
+}
+
+export function getTenantCount (name) {
+  return keycloakRequest({
+    url: '/groups/count',
+    method: 'GET',
+    params: {
+      top: true,
+      search: name || void 0
+    }
+  })
+}
+
+export async function getTenants (query) {
+  const { count } = await getTenantCount(query.name)
+  const { data } = await getTenantsByQuery(query, false)
+  return {
+    data,
+    totalCount: count
+  }
+}
+
+export async function getPlatformTenants () {
+  const { count } = await getTenantCount()
+  return getTenantsByQuery({ pageSize: count }, false)
+}
+
+export function addTenant ({ name, remark }) {
+  return add({
+    url: '/super/admin/tenant',
+    method: 'POST',
+    data: {
+      name,
+      attributes: {
+        remark: [remark]
+      }
+    }
+  })
+}
+
+export function deleteTenant ({ id }) {
+  return messageSend({
+    url: `/super/admin/tenant/${id}`,
+    method: 'DELETE'
+  }, '删除')
+}
+
+export function updateTenant ({ id, name, remark }) {
+  return update({
+    url: `/admin/group/${id}`,
+    method: 'PUT',
+    data: {
+      name,
+      attributes: {
+        remark: [remark]
+      }
+    }
+  })
+}
+
+export function getUsersByGroup (query) {
+  const { id, pageSize: max, pageNum, ...params } = query
+  return keycloakRequest({
+    url: `/groups/${id}/members`,
+    method: 'GET',
+    params: {
+      briefRepresentation: true,
+      max: max + 1,
+      first: pageNum ? (pageNum - 1) * max : void 0,
+      ...params
+    }
+  }).then(data => {
+    const totalCount = data.length
+    if (totalCount > max) {
+      data = data.slice(0, -1)
+    }
+    return {
+      data,
+      totalCount
+    }
+  })
+}
+
+export function getUserRoleMapping (userId) {
+  return keycloakRequest({
+    url: `/users/${userId}/role-mappings/realm`,
+    method: 'GET'
+  }).then(
+    roles => getRoles().then(
+      available => {
+        return {
+          available,
+          roles
+        }
+      }
+    )
+  )
+}
+
+let roles = null
+function getRoles () {
+  if (roles) {
+    return Promise.resolve(roles)
+  }
+  return keycloakRequest({
+    url: `/roles`,
+    method: 'GET'
+  }).then(data => {
+    const isSuperAdmin = store.getters.isSuperAdmin
+    return (roles = data.filter(({ name }) => name !== Role.SUPER_ADMIN && (isSuperAdmin || name !== Role.ADMIN) && name.startsWith('ROLE_')))
+  })
+}
+
+export function updateUserRoles (userId, available, fromKeys, toKeys) {
+  const delRoles = []
+  const toSet = new Set(toKeys)
+  fromKeys.forEach(id => {
+    if (!toSet.has(id)) {
+      delRoles.push(available.find(role => role.id === id))
+    }
+  })
+  const addRoles = []
+  const fromSet = new Set(fromKeys)
+  toKeys.forEach(id => {
+    if (!fromSet.has(id)) {
+      addRoles.push(available.find(role => role.id === id))
+    }
+  })
+  return request({
+    url: '/admin/users/role/configure',
+    method: 'PUT',
+    data: {
+      userId,
+      addRoleList: addRoles,
+      removeRoleList: delRoles
+    }
+  })
+}
+
+export function sendVerificationCode (data) {
+  return request({
+    url: '/authcode/send',
+    method: 'POST',
+    data
+  })
+}
+
+export function checkVerificationCode (data) {
+  return request({
+    url: '/authcode/check',
+    method: 'POST',
+    data
+  })
+}
+
+export function getDepartments () {
+  return tenantRequest({
+    url: '/admin/department/list',
+    method: 'POST',
+    data: addTenantKey()
+  })
+}
+
+let departmentCache = null
+export function resetDepartmentCache () {
+  departmentCache = null
+}
+
+export function getDepartmentTree () {
+  if (departmentCache) {
+    return Promise.resolve({ data: departmentCache })
+  }
+  if (store.getters.isTenantAdmin) {
+    return getDepartments().then(({ data }) => {
+      departmentCache = [{
+        path: store.getters.tenant,
+        name: store.getters.tenantName || '我的部门',
+        children: data
+      }]
+      return { data: departmentCache }
+    })
+  }
+  return request({
+    url: '/keycloak/oneself/tree',
+    method: 'GET'
+  }).then(({ data }) => {
+    departmentCache = Array.isArray(data)
+      ? [{
+        path: store.getters.org,
+        name: '我的部门',
+        children: data
+      }]
+      : [data]
+    return { data: departmentCache }
+  })
+}
+
+export function addDepartment (data) {
+  return add({
+    url: '/admin/department',
+    method: 'POST',
+    data: addTenantKey(data)
+  }, tenantRequest).finally(resetDepartmentCache)
+}
+
+export function updateDepartment (data) {
+  return update({
+    url: '/admin/department',
+    method: 'PUT',
+    data
+  }).finally(resetDepartmentCache)
+}
+
+export function deleteDepartment ({ id }) {
+  return messageSend({
+    url: `/admin/department/${id}`,
+    method: 'DELETE'
+  }, '删除').finally(resetDepartmentCache)
+}
+
+export function getUsersByDepartment (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return tenantRequest({
+    url: '/admin/department/user/list',
+    method: 'POST',
+    data: addTenantKey({
+      pageIndex, pageSize,
+      ...params
+    })
+  })
+}
+
+export function updateUserDepartment ({ userId, userName }, { id, name }) {
+  return tenantRequest({
+    url: '/admin/users/regrouping',
+    method: 'PUT',
+    data: addTenantKey({
+      userId,
+      userName,
+      ...(id ? { departmentId: id, departmentName: name } : { departmentName: '顶层部门' })
+    })
+  })
+}
+
+export function updateUserName (userId, name) {
+  return update({
+    url: '/admin/users/update',
+    method: 'PUT',
+    data: addTenantKey({
+      userId,
+      name
+    })
+  }, null, tenantRequest)
+}
+
+export function updateUserInformLevel ({ userId, userName }, informLevel, informType = []) {
+  let informName = '未知'
+  switch (true) {
+    case informLevel === 9999:
+      informName = '自定义'
+      break
+    case informLevel >= 0:
+      informName = AlarmLevelInfo[informLevel]
+      break
+    case informLevel === -1:
+      informName = '不预警'
+      break
+    default:
+      break
+  }
+  return update({
+    url: '/admin/users/update/deviceExceptionLevel',
+    method: 'PUT',
+    data: addTenantKey({
+      userId,
+      userName,
+      informLevel,
+      informName,
+      informType
+    })
+  }, null, tenantRequest)
+}
+
+export function getUserInforms ({ userId }) {
+  return send({
+    url: '/admin/user/custom',
+    method: 'GET',
+    params: { userId }
+  })
+}
+
+export function migrateUser (data) {
+  return tenantRequest({
+    url: '/admin/users/temporary/migration',
+    method: 'POST',
+    data: addTenantKey(data)
+  })
+}
+
+export function getTenantTree () {
+  return getGroups().then(data => {
+    const groups = normalizeGroups([data], true)
+    groups[0].label = '根部门'
+    return { data: groups }
+  })
+}
+
+async function getGroups () {
+  if (!store.getters.tenantId) {
+    const groups = await getUserGroups(store.getters.userId)
+    store.commit('user/SET_TENANT_ID', groups[0].id)
+  }
+  return keycloakRequest({
+    url: `/groups/${store.getters.tenantId}`,
+    method: 'GET'
+  })
+}
+
+export function getUserGroups (id) {
+  return keycloakRequest({
+    url: `/users/${id}/groups`,
+    method: 'GET',
+    params: { briefRepresentation: true }
+  }).then(data => data.sort((a, b) => a.path.length - b.path.length))
+}

+ 78 - 0
src/api/workflow.js

@@ -0,0 +1,78 @@
+import store from '@/store'
+import {
+  State,
+  Access,
+  WorkflowState
+} from '@/constant.js'
+import request from '@/utils/request.js'
+import {
+  send,
+  addTenant,
+  addUser
+} from './base.js'
+
+export function getAuditWorkflows (query, options) {
+  const { pageNum: pageIndex, pageSize } = query
+  const condition = { status: State.SUBMITTED }
+  const access = store.getters.access
+  const isFinal = access.has(Access.REVIEW_RELEASE_FINAL)
+  if (!isFinal || !__JUMP_REVIEW__) {
+    const arr = []
+    if (isFinal) {
+      arr.push(WorkflowState.FINAL_LEVEL)
+    }
+    if (access.has(Access.REVIEW_RELEASE_SECOND)) {
+      arr.push(WorkflowState.SECOND_LEVEL)
+    }
+    if (access.has(Access.REVIEW_RELEASE_FIRST)) {
+      arr.push(WorkflowState.FIRST_LEVEL)
+    }
+    condition.currentSeveralReviewedList = arr
+  }
+  return request({
+    url: '/workflow/pageByPropertis',
+    method: 'POST',
+    data: {
+      pageIndex, pageSize,
+      param: addTenant({
+        status: State.SUBMITTED,
+        ...condition
+      })
+    },
+    ...options
+  })
+}
+
+export function getWorkflowsByUser (query, options) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return request({
+    url: '/workflow/pageByPropertis',
+    method: 'POST',
+    data: {
+      pageIndex, pageSize,
+      param: addTenant(addUser(params, 'createBy'))
+    },
+    ...options
+  })
+}
+
+export function getWorkflowsBySuperAdmin (query, options) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return request({
+    url: '/workflow/pageByPropertis',
+    method: 'POST',
+    data: {
+      pageIndex, pageSize,
+      param: addTenant(params)
+    },
+    ...options
+  })
+}
+
+export function getWorkflowDetail (workflowId, options) {
+  return (options?.loading ? send : request)({
+    url: '/workflow/getBussinessData',
+    method: 'POST',
+    params: { workflowId }
+  })
+}

+ 109 - 0
src/app.js

@@ -0,0 +1,109 @@
+import Vue from 'vue'
+
+import 'element-ui/lib/theme-chalk/index.css'
+import './scss/index.scss'
+
+import Element, {
+  MessageBox,
+  InputNumber
+} from 'element-ui'
+
+import App from './App.vue'
+import store from './store'
+import router from './router'
+
+import './icons'
+import './components'
+import './permission'
+
+import {
+  showLoading,
+  closeLoading
+} from '@/utils/pop'
+
+export default async function startApp (keycloak, auth) {
+  console.log('app')
+  console.time('app')
+
+  document.body.setAttribute('version', __VERSION__)
+  if (auth) {
+    refreshKeycloak(keycloak)
+  }
+
+  InputNumber.methods.handleInputChange = function (value) {
+    const newVal = value === '' ? this.min === -Infinity ? void 0 : this.min : Number(value)
+    if (!isNaN(newVal) || value === '') {
+      this.setCurrentValue(newVal)
+    }
+    this.userInput = null
+  }
+  Vue.use(Element)
+
+  Vue.config.productionTip = false
+  Vue.config.errorHandler = err => {
+    closeLoading()
+    throw (err || 'custom reject')
+  }
+
+  Vue.prototype.$keycloak = keycloak
+  Vue.prototype.$showLoading = showLoading
+  Vue.prototype.$closeLoading = closeLoading
+  Vue.prototype.$REQUEST_LIMIT = { limit: 0 }
+  Vue.prototype.$designProgram = function (id) {
+    window.open(this.$router.resolve({
+      name: 'program',
+      params: { id }
+    }).href, '_blank')
+  }
+
+  window._AMapSecurityConfig = {
+    securityJsCode: process.env.VUE_APP_GAODE_MAP_JSCODE
+  }
+
+  await store.dispatch('user/login', keycloak)
+  store.dispatch('permission/generateRoutes')
+  router.addRoutes(store.getters.permissionRoutes)
+
+  new Vue({
+    router,
+    store,
+    render: h => h(App)
+  }).$mount('#app')
+
+  console.timeEnd('app')
+}
+
+function refreshKeycloak (keycloak) {
+  // Token Refresh
+  setInterval(() => {
+    keycloak.updateToken(70).then(refreshed => {
+      if (refreshed) {
+        store.dispatch('user/refresh', keycloak).catch(() => {
+          try {
+            MessageBox.close()
+          } finally {
+            store.dispatch('user/clearToken')
+            MessageBox.confirm(
+              '账号信息发生变化,请重新登录',
+              '系统提示',
+              {
+                type: 'warning',
+                confirmButtonText: '重新登录',
+                center: true,
+                showClose: false,
+                showCancelButton: false,
+                closeOnClickModal: false,
+                closeOnPressEscape: false,
+                closeOnHashChange: false
+              }
+            ).then(() => {
+              store.dispatch('user/logout')
+            })
+          }
+        })
+      } else {
+        console.warn(`Token not refreshed, valid for ${Math.round(keycloak.tokenParsed.exp + keycloak.timeSkew - new Date().getTime() / 1000)} seconds`)
+      }
+    }).catch(e => console.error('Failed to refresh token', e))
+  }, 6000)
+}

BIN
src/assets/bg_big.png


BIN
src/assets/bg_top_s.png


BIN
src/assets/dot.png


BIN
src/assets/icon_applet.png


BIN
src/assets/icon_avatar.png


+ 15 - 0
src/assets/icon_camera.svg

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 24.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
+<g>
+	<path d="M14.6,16.3c-0.3,0-0.7-0.1-1-0.2L2.7,12.1C2,11.9,1.4,11.3,1,10.6C0.7,9.9,0.7,9.1,0.9,8.3L3,2.7C3.3,1.9,3.8,1.3,4.5,1
+		c0.7-0.3,1.5-0.4,2.3-0.1L20,5.7c0.8,0.3,1.3,0.9,1.5,1.6c0.2,0.8,0.1,1.6-0.4,2.2L17,15.1C16.4,15.8,15.5,16.3,14.6,16.3z
+		 M14.2,14.2c0.4,0.2,0.9,0,1.1-0.3l4.1-5.6c0.1-0.2,0.1-0.4,0.1-0.4c0-0.1-0.1-0.3-0.3-0.3L6.1,2.7c-0.3-0.1-0.5-0.1-0.8,0
+		C5.1,2.9,5,3.1,4.9,3.3L2.8,9c-0.1,0.3-0.1,0.5,0,0.8C3,10,3.2,10.2,3.4,10.3L14.2,14.2z"/>
+	<polygon points="13.1,17 11.9,18.7 8.7,17.5 6,20.3 2.5,20.3 2.5,22.3 0.5,22.3 0.5,16.3 2.5,16.3 2.5,18.3 5.1,18.3 6.7,16.8 
+		2.6,15.3 3.3,13.4 	"/>
+	<polygon points="23.5,11.2 19.7,16.2 17.7,15.5 21.5,10.5 	"/>
+	<circle cx="6.4" cy="5" r="1"/>
+</g>
+</svg>

+ 15 - 0
src/assets/icon_camera_white.svg

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 24.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
+<g style='fill: #fff'>
+	<path d="M14.6,16.3c-0.3,0-0.7-0.1-1-0.2L2.7,12.1C2,11.9,1.4,11.3,1,10.6C0.7,9.9,0.7,9.1,0.9,8.3L3,2.7C3.3,1.9,3.8,1.3,4.5,1
+		c0.7-0.3,1.5-0.4,2.3-0.1L20,5.7c0.8,0.3,1.3,0.9,1.5,1.6c0.2,0.8,0.1,1.6-0.4,2.2L17,15.1C16.4,15.8,15.5,16.3,14.6,16.3z
+		 M14.2,14.2c0.4,0.2,0.9,0,1.1-0.3l4.1-5.6c0.1-0.2,0.1-0.4,0.1-0.4c0-0.1-0.1-0.3-0.3-0.3L6.1,2.7c-0.3-0.1-0.5-0.1-0.8,0
+		C5.1,2.9,5,3.1,4.9,3.3L2.8,9c-0.1,0.3-0.1,0.5,0,0.8C3,10,3.2,10.2,3.4,10.3L14.2,14.2z"/>
+	<polygon points="13.1,17 11.9,18.7 8.7,17.5 6,20.3 2.5,20.3 2.5,22.3 0.5,22.3 0.5,16.3 2.5,16.3 2.5,18.3 5.1,18.3 6.7,16.8 
+		2.6,15.3 3.3,13.4 	"/>
+	<polygon points="23.5,11.2 19.7,16.2 17.7,15.5 21.5,10.5 	"/>
+	<circle cx="6.4" cy="5" r="1"/>
+</g>
+</svg>

BIN
src/assets/icon_card_abnormal.png


BIN
src/assets/icon_card_normal.png


BIN
src/assets/icon_card_unknown.png


BIN
src/assets/icon_condition.png


BIN
src/assets/icon_date.png


BIN
src/assets/icon_delete.png


BIN
src/assets/icon_device_reboot.png


BIN
src/assets/icon_device_restore.png


BIN
src/assets/icon_device_switch.png


BIN
src/assets/icon_device_time.png


BIN
src/assets/icon_download.png


BIN
src/assets/icon_edit.png


BIN
src/assets/icon_enlarge.png


BIN
src/assets/icon_flooding.png


BIN
src/assets/icon_image.png


BIN
src/assets/icon_info.png


BIN
src/assets/icon_inform.png


BIN
src/assets/icon_light.png


BIN
src/assets/icon_log.png


BIN
src/assets/icon_narrow.png


+ 16 - 0
src/assets/icon_off.svg

@@ -0,0 +1,16 @@
+<svg id="icon_off" xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28">
+  <defs>
+    <style>
+      .cls-1 {
+        fill: #c0c4cf;
+      }
+
+      .cls-2 {
+        fill: #fff;
+        fill-rule: evenodd;
+      }
+    </style>
+  </defs>
+  <circle id="椭圆_8_拷贝" data-name="椭圆 8 拷贝" class="cls-1" cx="14" cy="14" r="14"/>
+  <path id="矩形_1477" data-name="矩形 1477" class="cls-2" d="M457,591h1v7h-1v-7Zm0.5,15a7.495,7.495,0,0,1-4.362-13.593l0.6,0.8a6.5,6.5,0,1,0,7.532,0l0.6-.8A7.495,7.495,0,0,1,457.5,606Z" transform="translate(-443 -584)"/>
+</svg>

+ 16 - 0
src/assets/icon_on.svg

@@ -0,0 +1,16 @@
+<svg id="icon_on" xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28">
+  <defs>
+    <style>
+      .cls-1 {
+        fill: #1c5cb0;
+      }
+
+      .cls-2 {
+        fill: #fff;
+        fill-rule: evenodd;
+      }
+    </style>
+  </defs>
+  <circle id="椭圆_8_拷贝" data-name="椭圆 8 拷贝" class="cls-1" cx="14" cy="14" r="14"/>
+  <path id="矩形_1477" data-name="矩形 1477" class="cls-2" d="M457,171h1v7h-1v-7Zm0.5,15a7.495,7.495,0,0,1-4.362-13.593l0.6,0.8a6.5,6.5,0,1,0,7.532,0l0.6-.8A7.495,7.495,0,0,1,457.5,186Z" transform="translate(-443 -164)"/>
+</svg>

+ 21 - 0
src/assets/icon_on_2.svg

@@ -0,0 +1,21 @@
+<svg id="icon_on_2" xmlns="http://www.w3.org/2000/svg" width="64" height="28" viewBox="0 0 64 28">
+  <defs>
+    <style>
+      .cls-1 {
+        fill: #e7e9f1;
+      }
+
+      .cls-2 {
+        fill: #1c5cb0;
+      }
+
+      .cls-3 {
+        fill: #fefefe;
+        fill-rule: evenodd;
+      }
+    </style>
+  </defs>
+  <rect id="矩形_1478" data-name="矩形 1478" class="cls-1" width="64" height="28" rx="14" ry="14"/>
+  <path id="矩形_1478_拷贝" data-name="矩形 1478 拷贝" class="cls-2" d="M32,0H50A14,14,0,0,1,64,14v0A14,14,0,0,1,50,28H32a0,0,0,0,1,0,0V0A0,0,0,0,1,32,0Z"/>
+  <path id="半开" class="cls-3" d="M460.829,172.4a14.778,14.778,0,0,1-1.451,2.735l0.78,0.312a26.3,26.3,0,0,0,1.607-2.711Zm-5.277,2.687a12.345,12.345,0,0,0-1.427-2.651l-0.8.324a13.33,13.33,0,0,1,1.38,2.687Zm7.3,3.694h-4.917v-1.919h4.222v-0.876h-4.222v-3.862h-0.912v3.862h-4.053v0.876h4.053v1.919h-4.821v0.875h4.821v3.382h0.912v-3.382h4.917v-0.875Zm5.092-1.631v-3.4h3.334v3.4h-3.334Zm6.908,0h-2.662v-3.4h2.29V172.9h-9.846v0.851h2.411v2.879c0,0.18,0,.336-0.012.516h-2.843v0.863H467a5.508,5.508,0,0,1-2.771,4.45,4.084,4.084,0,0,1,.708.659,6.172,6.172,0,0,0,2.974-5.109h3.37v5.073h0.912v-5.073h2.662V177.15Z" transform="translate(-415 -164)"/>
+</svg>

BIN
src/assets/icon_online.png


BIN
src/assets/icon_performance.png


BIN
src/assets/icon_refresh.png


BIN
src/assets/icon_safety.png


BIN
src/assets/icon_screen_light.png


BIN
src/assets/icon_screen_network.png


BIN
src/assets/icon_screen_switch.png


BIN
src/assets/icon_screen_volume.png


BIN
src/assets/icon_screenshot.png


+ 16 - 0
src/assets/icon_screenshot.svg

@@ -0,0 +1,16 @@
+<svg id="_-s-icon_screenshot" data-name="-s-icon_screenshot" xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28">
+  <defs>
+    <style>
+      .cls-1 {
+        fill: #1c5cb0;
+      }
+
+      .cls-2 {
+        fill: #fff;
+        fill-rule: evenodd;
+      }
+    </style>
+  </defs>
+  <circle id="椭圆_8" data-name="椭圆 8" class="cls-1" cx="14" cy="14" r="14"/>
+  <path id="矩形_1476_拷贝_2" data-name="矩形 1476 拷贝 2" class="cls-2" d="M420,183h-2v2h-1v-2h-9v-9h-2v-1h2v-2h1v2h9v9h2v1Zm-3-9h-8v8h8v-8Z" transform="translate(-399 -164)"/>
+</svg>

BIN
src/assets/icon_setting.png


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