ソースを参照

feat: profile

Casper Dai 3 年 前
コミット
a1b9512cad

+ 3 - 3
src/api/base.js

@@ -17,7 +17,7 @@ export function send (config) {
 
 export function messageSend (config, message) {
   return send(config).then(data => {
-    Message({
+    message && Message({
       type: 'success',
       message: `${message}成功`
     })
@@ -45,8 +45,8 @@ export function add (config) {
   return messageSend(config, '新增')
 }
 
-export function update (config) {
-  return messageSend(config, '更新')
+export function update (config, message = '更新') {
+  return messageSend(config, message)
 }
 
 export function submit (config, tip) {

+ 39 - 0
src/api/user.js

@@ -0,0 +1,39 @@
+import request from '@/utils/request'
+import {
+  send,
+  update
+} from './base'
+
+let baseUrl = null
+let realm = null
+
+export function setBase (kc) {
+  realm = encodeURIComponent(kc.realm)
+  if (kc.authServerUrl.charAt(kc.authServerUrl.length - 1) === '/') {
+    baseUrl = `${kc.authServerUrl}realms/${realm}`
+  } else {
+    baseUrl = `${kc.authServerUrl}/realms/${realm}`
+  }
+}
+
+export function userinfo (silence) {
+  const config = {
+    url: `${baseUrl}/account`,
+    method: 'GET',
+    unusual: true
+  }
+  if (silence) {
+    config.custom = true
+    return request(config)
+  }
+  return send(config)
+}
+
+export function updateUser (id, data, message) {
+  return update({
+    url: `/auth/admin/realms/${realm}/users/${id}`,
+    method: 'PUT',
+    unusual: true,
+    data
+  }, message)
+}

BIN
src/assets/icon_avatar.png


BIN
src/assets/icon_wechat.png


+ 0 - 3
src/components/Preview/index.vue

@@ -64,6 +64,3 @@ export default {
   }
 }
 </script>
-
-<style lang="scss" scoped>
-</style>

+ 1 - 0
src/components/Wrapper/index.vue

@@ -55,6 +55,7 @@ export default {
     flex: none;
     min-width: 800px;
     min-height: 0;
+    overflow: hidden;
 
     &.fill {
       flex: 1 0 0;

+ 3 - 2
src/layout/components/Navbar/Breadcrumb.vue

@@ -90,14 +90,15 @@ export default {
   position: absolute;
 }
 
-.c-breadcrumb.el-breadcrumb {
+.c-breadcrumb {
   display: inline-block;
   font-size: 14px;
   line-height: 50px;
+  user-select: none;
 
   .last {
     color: #97a8be;
-    cursor: text;
+    cursor: auto;
   }
 
   .placeholder {

+ 8 - 8
src/layout/components/Navbar/index.vue

@@ -22,11 +22,14 @@
         <i class="el-icon-arrow-down" />
       </div>
       <el-dropdown-menu slot="dropdown">
-        <!-- <router-link to="/profile/index">
-          <el-dropdown-item divided>个人中心</el-dropdown-item>
-        </router-link> -->
-        <el-dropdown-item @click.native="logout">
-          登出
+        <router-link to="/profile">
+          <el-dropdown-item>个人设置</el-dropdown-item>
+        </router-link>
+        <el-dropdown-item
+          divided
+          @click.native="logout"
+        >
+          退出登录
         </el-dropdown-item>
       </el-dropdown-menu>
     </el-dropdown>
@@ -52,9 +55,6 @@ export default {
   },
   methods: {
     async logout () {
-      if (this.$route.name !== 'home') {
-        this.$router.replace({ name: 'home' })
-      }
       await this.$store.dispatch('user/logout')
     }
   }

+ 5 - 0
src/main.js

@@ -27,6 +27,8 @@ import {
   closeLoading
 } from './utils/pop'
 
+import { setBase } from '@/api/user'
+
 async function startApp () {
   document.body.setAttribute('version', __VERSION__)
 
@@ -54,6 +56,9 @@ async function startApp () {
   Vue.prototype.__PLACEHOLDER__ = __PLACEHOLDER__
   Vue.prototype.__SENSOR_ELK__ = __SENSOR_ELK__
 
+  console.log(keycloak)
+  setBase(keycloak)
+
   await store.dispatch('user/login', keycloak.token)
 
   new Vue({

+ 8 - 16
src/router/index.js

@@ -50,24 +50,16 @@ export const asyncRoutes = [
     children: [
       {
         path: 'dashboard',
-        name: 'home',
+        name: 'dashboard',
         component: () => import('@/views/dashboard/index'),
         meta: { title: '首页', icon: 'el-icon-s-home' }
-      }
-    ]
-  },
-  {
-    hidden: true,
-    path: '/abnormal',
-    redirect: '/dashboard',
-    component: Layout,
-    meta: { title: '首页' },
-    children: [
+      },
       {
-        path: ':type',
-        name: 'abnormal',
-        component: () => import('@/views/abnormal/index'),
-        meta: { title: '异常', activeMenu: '/dashboard' }
+        name: 'profile',
+        path: 'profile',
+        component: () => import('@/views/profile/index'),
+        meta: { title: '个人设置' },
+        hidden: true
       }
     ]
   },
@@ -157,7 +149,7 @@ export const asyncRoutes = [
         dev: true,
         name: 'remote',
         path: 'remote',
-        component: () => import('@/views/remote/index'),
+        component: () => import('@/views/device/remote/index'),
         meta: { title: '设备操控' }
       }
     ]

+ 0 - 5
src/store/modules/user.js

@@ -44,11 +44,6 @@ const actions = {
         return
       }
 
-      // you can also use the method loadUserProfile() to get user attributes
-      // Vue.prototype.$keycloak.loadUserProfile().then(profile => {
-      //   console.log(profile)
-      // })
-
       const roleArray = Object.values(Role)
       const roles = Vue.prototype.$keycloak.realmAccess?.roles?.filter(role => roleArray.includes(role))
       // roles must be a non-empty array

+ 1 - 1
src/utils/request.js

@@ -59,7 +59,7 @@ service.interceptors.response.use(
   response => {
     const res = response.data || {}
 
-    if (res.success) {
+    if (res.success || response.config?.unusual) {
       return res
     }
 

+ 0 - 3
src/views/abnormal/index.vue

@@ -1,3 +0,0 @@
-<template>
-  <div>abnormal</div>
-</template>

+ 1 - 4
src/views/dashboard/components/Card.vue

@@ -59,10 +59,7 @@ export default {
   },
   methods: {
     go () {
-      // this.$router.push({
-      //   name: 'abnormal',
-      //   params: { type: 'a' }
-      // })
+      // todo
     }
   }
 }

+ 1 - 1
src/views/dashboard/components/Device.vue

@@ -148,7 +148,7 @@ export default {
     },
     styles () {
       return this.isActivated && this.isOnline && this.shot ? {
-        backgroundImage: `url("${this.shot}"`
+        backgroundImage: `url("${this.shot}")`
       } : null
     },
     nextInfo () {

+ 1 - 1
src/views/device/detail/components/ScreenShot.vue

@@ -54,7 +54,7 @@ export default {
     onScreenshotUpdate ({ waiting, base64 }) {
       this.asking = waiting
       this.styles = !waiting && base64 ? {
-        backgroundImage: `url("${base64}"`
+        backgroundImage: `url("${base64}")`
       } : null
     },
     invoke () {

+ 0 - 0
src/views/remote/components/ControlPanel.vue → src/views/device/remote/components/ControlPanel.vue


+ 0 - 0
src/views/remote/index.vue → src/views/device/remote/index.vue


+ 458 - 0
src/views/profile/index.vue

@@ -0,0 +1,458 @@
+<template>
+  <wrapper
+    class="c-profile"
+    fill
+    margin
+    background
+  >
+    <div class="l-flex__none l-flex--col center c-profile__header has-padding">
+      <div
+        class="o-avatar u-pointer"
+        @click="onAvatarClick"
+      >
+        <i
+          class="o-avatar__img"
+          :style="avatarStyle"
+        />
+        <i class="o-avatar__upload el-icon-camera" />
+        <input
+          ref="upload"
+          type="file"
+          accept="image/*"
+          style="display: none;"
+          @change="onUpload"
+        >
+      </div>
+      <div class="u-bold">{{ name }}</div>
+      <div class="u-relative">
+        <i class="c-profile__icon el-icon-user" />
+        {{ roleTip }}
+      </div>
+    </div>
+    <div class="l-flex__fill u-overflow-y--auto">
+      <el-result
+        v-if="error"
+        icon="warning"
+      >
+        <template #extra>
+          <el-link
+            class="u-pointer"
+            type="warning"
+            @click="getUserInfo"
+          >
+            出错了,点击重试
+          </el-link>
+        </template>
+      </el-result>
+      <div
+        v-else
+        class="c-form"
+      >
+        <div class="c-form__section">
+          <span class="c-form__label">手机:</span>
+          <el-input
+            v-model="phone"
+            class="c-form__item"
+            @change="onPhoneChange"
+            @keydown.enter="$event.target.blur()"
+          />
+        </div>
+        <div class="c-form__section has-bottom-padding">
+          <span class="c-form__label">邮箱:</span>
+          <el-input
+            v-model="email"
+            class="c-form__item"
+            @change="onEmailChange"
+            @keydown.enter="$event.target.blur()"
+          />
+        </div>
+        <div class="c-form__section">
+          <i class="o-wechat" />
+          <div
+            class="has-padding u-pointer"
+            :class="color"
+            @click="onClickWechat"
+          >
+            {{ tip }}
+          </div>
+        </div>
+      </div>
+    </div>
+    <el-dialog
+      :visible.sync="showQr"
+      custom-class="c-preview"
+      width="0"
+      @close="onCloseQr"
+    >
+      <div class="c-wechat has-padding">
+        <div class="l-flex--row center has-bottom-padding">
+          <i class="c-sibling-item o-wechat" />
+          <span class="c-sibling-item u-color--black u-bold">请使用微信扫一扫</span>
+        </div>
+        <div
+          v-loading="loading"
+          class="c-wechat__wrapper"
+          :class="{ retry }"
+          @click="getQr"
+        >
+          <img
+            class="c-wechat__qr"
+            :src="qr"
+          >
+        </div>
+      </div>
+    </el-dialog>
+  </wrapper>
+</template>
+
+<script>
+import { mapState } from 'vuex'
+import {
+  userinfo,
+  updateUser
+} from '@/api/user'
+import { Role } from '@/constant'
+
+export default {
+  name: 'Profile',
+  data () {
+    return {
+      error: false,
+      user: null,
+      avatar: '',
+      phone: '',
+      email: '',
+      wechat: '',
+      showQr: false,
+      qr: '',
+      loading: false,
+      retry: false
+    }
+  },
+  computed: {
+    ...mapState('user', ['name', 'roles']),
+    avatarStyle () {
+      const avatar = this.avatar
+      return avatar ? {
+        backgroundImage: `url("${avatar}")`
+      } : null
+    },
+    roleTip () {
+      if (this.roles.includes(Role.ADMIN)) {
+        return '超级管理员'
+      }
+      if (this.roles.includes(Role.SUPERVISOR)) {
+        return '主管'
+      }
+      if (this.roles.includes(Role.STAFF)) {
+        return '员工'
+      }
+      return '游客'
+    },
+    tip () {
+      return this.wechat ? '解除绑定' : '绑定微信'
+    },
+    color () {
+      return this.wechat ? 'u-color--error' : 'u-color--primary'
+    }
+  },
+  created () {
+    this.$timer = null
+    this.$checkTimer = null
+    this.getUserInfo()
+  },
+  beforeDestroy () {
+    this.onCloseQr()
+    clearInterval(this.$checkTimer)
+  },
+  methods: {
+    onAvatarClick () {
+      this.$refs.upload.click()
+    },
+    onUpload (e) {
+      const file = e.target.files[0]
+      e.target.value = null
+      if (file.size >= 1024 * 1024) {
+        this.$message({
+          type: 'warning',
+          message: '请选择1M以下的图片'
+        })
+        return
+      }
+      const reader = new FileReader()
+      reader.onload = () => {
+        this.avatar = reader.result
+      }
+      reader.readAsDataURL(file)
+    },
+    getUserInfo () {
+      this.error = false
+      userinfo().then(data => {
+        // this.avatar = this.getOrCreateAttribute(data, 'avatar', '')
+        this.phone = this.getOrCreateAttribute(data, 'phone', '')
+        this.email = data.email || ''
+        this.wechat = this.getOrCreateAttribute(data, 'wechat', '')
+        this.user = data
+      }, () => {
+        this.error = true
+      })
+    },
+    getOrCreateAttribute (user, key, defaults = '') {
+      if (!user.attributes[key]) {
+        user.attributes[key] = [defaults]
+      }
+      return user.attributes[key][0]
+    },
+    updateUser (data, meesage) {
+      return updateUser(this.user.id, data, meesage)
+    },
+    changeAttribute (key, value, meesage) {
+      if (this.user.attributes[key][0] !== value) {
+        return this.updateUser({
+          attributes: {
+            ...this.user.attributes,
+            [key]: [value]
+          }
+        }, meesage).then(
+          () => {
+            this.user.attributes[key][0] = value
+          },
+          e => {
+            this.resetAttribute(key)
+            return Promise.reject(e)
+          }
+        )
+      }
+      return Promise.resolve()
+    },
+    resetAttribute (key) {
+      this[key] = this.user.attributes[key][0]
+    },
+    onPhoneChange () {
+      if (!this.phone) {
+        this.$message({
+          type: 'warning',
+          message: '手机号不能为空'
+        })
+        this.resetAttribute('phone')
+        return
+      }
+      if (!(/^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/.test(this.phone))) {
+        this.$message({
+          type: 'warning',
+          message: '手机号格式错误'
+        })
+        this.resetAttribute('phone')
+        return
+      }
+      this.changeAttribute('phone', this.phone, '更新手机号')
+    },
+    onEmailChange () {
+      if (!this.email) {
+        this.$message({
+          type: 'warning',
+          message: '邮箱不能为空'
+        })
+        this.resetAttribute('email')
+        return
+      }
+      if (!(/^([a-zA-Z0-9]+[_|.]?)*[a-zA-Z0-9]+@([a-zA-Z0-9]+[_|.]?)*[a-zA-Z0-9]+\.[a-zA-Z]{2,3}$/.test(this.email))) {
+        this.$message({
+          type: 'warning',
+          message: '邮箱格式错误'
+        })
+        this.resetAttribute('email')
+        return
+      }
+      this.updateUser({ email: this.email }, '更新邮箱').then(
+        () => {
+          this.user.email = this.email
+        },
+        () => {
+          this.email = this.user.email
+        }
+      )
+    },
+    onClickWechat () {
+      if (this.wechat) {
+        this.$confirm('解除绑定后将无法再收到消息推送', '确定解绑微信?', { type: 'warning' }).then(
+          () => {
+            this.changeAttribute('wechat', '', '解绑微信').then(() => {
+              this.wechat = ''
+              this.user.attributes.wechat[0] = ''
+            })
+          }
+        )
+      } else {
+        this.showQr = true
+        this.getQr()
+      }
+    },
+    onCloseQr () {
+      this.showQr = false
+      clearTimeout(this.$timer)
+    },
+    getQr () {
+      if (this.loading || this.qr && !this.retry) {
+        return
+      }
+      this.fetchQr()
+    },
+    resetQr () {
+      clearInterval(this.$checkTimer)
+      this.$checkTimer = null
+      this.qr = ''
+      this.retry = false
+    },
+    fetchQr () {
+      if (!this.showQr) {
+        this.qr = ''
+        return
+      }
+      if (!this.qr || this.retry) {
+        this.loading = true
+      }
+      this.retry = false
+      clearTimeout(this.$timer)
+      new Promise((resolve, reject) => {
+        setTimeout(Math.random() + 0.5 | 0 ? resolve : reject, 1000)
+      }).then(
+        () => {
+          if (!this.wechat) {
+            this.qr = Math.random()
+            this.$timer = setTimeout(this.fetchQr, 5000)
+            this.checkBind()
+          }
+        },
+        () => {
+          if (!this.wechat) {
+            this.retry = true
+          }
+        }
+      ).finally(() => {
+        this.loading = false
+      })
+    },
+    checkBind () {
+      if (!this.wechat && !this.$checkTimer) {
+        this.$checkTimer = setInterval(this.check, 2000)
+        setTimeout(() => {
+          this.updateUser({
+            attributes: {
+              ...this.user.attributes,
+              wechat: ['9999']
+            }
+          }, null)
+        }, 5000)
+      }
+    },
+    check () {
+      userinfo(true).then(data => {
+        const wechat = data.attributes.wechat?.[0]
+        if (wechat) {
+          this.$message({
+            type: 'success',
+            message: '绑定微信成功'
+          })
+          this.wechat = wechat
+          this.user.attributes.wechat[0] = wechat
+          this.onCloseQr()
+          this.resetQr()
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.c-profile {
+  overflow: hidden;
+
+  &__header {
+    justify-content: space-between;
+    height: 176px;
+    color: #fff;
+    line-height: 1;
+    background-color: $blue;
+  }
+
+  &__icon {
+    position: absolute;
+    top: 50%;
+    transform: translate(calc(-100% - 16px), -50%);
+  }
+}
+
+.o-avatar {
+  display: inline-block;
+  position: relative;
+  width: 80px;
+  height: 80px;
+  padding: 2px;
+  border-radius: 50%;
+  background-color: #fff;
+
+  &__img {
+    display: inline-block;
+    width: 100%;
+    height: 100%;
+    border-radius: 50%;
+    background: url("~@/assets/icon_avatar.png") 0 0 / 100% 100% no-repeat;
+  }
+
+  &__upload {
+    display: inline-flex;
+    justify-content: center;
+    align-items: center;
+    position: absolute;
+    right: 0;
+    bottom: 0;
+    width: 26px;
+    height: 26px;
+    color: $blue;
+    border-radius: 50%;
+    background-color: #fff;
+  }
+}
+
+.o-wechat {
+  display: inline-block;
+  width: 32px;
+  height: 32px;
+  background: url("~@/assets/icon_wechat.png") 0 0 / 100% 100% no-repeat;
+}
+
+.c-wechat {
+  border-radius: $radius;
+  background-color: #fff;
+
+  &__wrapper {
+    position: relative;
+    width: 240px;
+    height: 240px;
+
+    &.retry::after {
+      content: "点击重试";
+      display: inline-flex;
+      justify-content: center;
+      align-items: center;
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+      color: $black;
+      font-weight: bold;
+      background-color: rgba(#fff, 0.8);
+      cursor: pointer;
+    }
+  }
+
+  &__qr {
+    display: inline-block;
+    width: 100%;
+    height: 100%;
+  }
+}
+</style>