index.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. <template>
  2. <wrapper
  3. class="c-profile"
  4. fill
  5. margin
  6. background
  7. >
  8. <div class="l-flex__none l-flex--col center c-profile__header has-padding">
  9. <div
  10. class="o-avatar u-pointer"
  11. @click="onAvatarClick"
  12. >
  13. <i
  14. class="o-avatar__img"
  15. :style="avatarStyle"
  16. />
  17. <i class="o-avatar__upload el-icon-camera" />
  18. <input
  19. ref="upload"
  20. type="file"
  21. accept="image/*"
  22. style="display: none;"
  23. @change="onUpload"
  24. >
  25. </div>
  26. <div class="c-profile__name has-top-padding">{{ name }}</div>
  27. <div class="u-relative has-top-padding">
  28. <i class="el-icon-user" />
  29. {{ role }}
  30. </div>
  31. </div>
  32. <div
  33. v-loading="loading"
  34. class="l-flex__fill l-flex--col center"
  35. >
  36. <warning
  37. v-if="error"
  38. @click="getUserInfo"
  39. />
  40. <div
  41. v-else
  42. class="has-padding l-flex--col"
  43. >
  44. <div class="l-flex--row">
  45. <UserInfoItem
  46. v-if="user"
  47. key="phone"
  48. v-model="phone"
  49. type="phone"
  50. :initial="user.attributes.phone[0]"
  51. @update="changeAttribute('phone', phone, '更新手机号')"
  52. />
  53. </div>
  54. <div class="l-flex--row has-padding--v">
  55. <UserInfoItem
  56. v-if="user"
  57. key="email"
  58. v-model="email"
  59. type="email"
  60. :initial="user.email"
  61. @update="updateUser({email}, '更新邮箱')"
  62. />
  63. </div>
  64. <div class="l-flex--row center has-top-padding">
  65. <div class="o-app">
  66. <i class="o-icon wechat has-bg" />
  67. <template v-if="wechat">
  68. <div class="l-flex--row">
  69. <div class="c-sibling-item u-color--info light">已绑定</div>
  70. <div
  71. class="c-sibling-item u-pointer"
  72. @click="onClickWechat"
  73. >
  74. 解绑
  75. </div>
  76. </div>
  77. </template>
  78. <div
  79. v-else
  80. class="u-pointer"
  81. @click="onClickWechat"
  82. >
  83. 绑定微信
  84. </div>
  85. </div>
  86. <div class="o-app">
  87. <i class="o-icon applet has-bg" />
  88. <template v-if="applet">
  89. <div class="l-flex--row">
  90. <div class="c-sibling-item u-color--info light">已绑定</div>
  91. <div
  92. class="c-sibling-item u-pointer"
  93. @click="onClickApplet"
  94. >
  95. 解绑
  96. </div>
  97. </div>
  98. </template>
  99. <div
  100. v-else
  101. class="u-pointer"
  102. @click="onClickApplet"
  103. >
  104. 绑定小程序
  105. </div>
  106. </div>
  107. </div>
  108. </div>
  109. </div>
  110. <el-dialog
  111. :visible.sync="showQr"
  112. custom-class="c-preview"
  113. width="0"
  114. @close="onCloseQr"
  115. >
  116. <div class="c-wechat has-padding">
  117. <div class="l-flex--row center has-bottom-padding">
  118. <i class="c-sibling-item o-icon medium wechat has-bg" />
  119. <span class="c-sibling-item u-color--black u-bold">请使用微信扫一扫</span>
  120. </div>
  121. <div
  122. v-loading="qrOptions.loading"
  123. class="c-wechat__wrapper"
  124. :class="{ retry: qrOptions.retry }"
  125. @click="getQr"
  126. >
  127. <img
  128. class="c-wechat__qr"
  129. :src="qrOptions.qr"
  130. >
  131. </div>
  132. </div>
  133. </el-dialog>
  134. </wrapper>
  135. </template>
  136. <script>
  137. import { mapGetters } from 'vuex'
  138. import UserInfoItem from './components/UserInfoItem'
  139. import {
  140. userinfo,
  141. updateUser,
  142. getTicket,
  143. getQrcode
  144. } from '@/api/user'
  145. import {
  146. validPhone,
  147. validEmail
  148. } from '@/utils/validate'
  149. import { GATEWAY } from '@/constant'
  150. export default {
  151. name: 'Profile',
  152. components: {
  153. UserInfoItem
  154. },
  155. data () {
  156. return {
  157. loading: true,
  158. error: false,
  159. user: {
  160. email: '',
  161. attributes: {
  162. phone: ['']
  163. }
  164. },
  165. avatar: '',
  166. phone: '',
  167. email: '',
  168. wechat: '',
  169. applet: '',
  170. showQr: false,
  171. qrType: 'wechatOptions',
  172. wechatOptions: {
  173. loading: false,
  174. retry: false,
  175. qr: null
  176. },
  177. appletOptions: {
  178. loading: false,
  179. retry: false,
  180. qr: null
  181. }
  182. }
  183. },
  184. computed: {
  185. ...mapGetters([
  186. 'name',
  187. 'isSuperAdmin',
  188. 'isTenantAdmin',
  189. 'isGroupAdmin'
  190. ]),
  191. avatarStyle () {
  192. const avatar = this.avatar
  193. return avatar ? { backgroundImage: `url("${avatar}")` } : null
  194. },
  195. qrOptions () {
  196. return this[this.qrType]
  197. },
  198. role () {
  199. if (this.isSuperAdmin) {
  200. return '超级管理员'
  201. }
  202. if (this.isTenantAdmin) {
  203. return '管理员'
  204. }
  205. if (this.isGroupAdmin) {
  206. return '主管'
  207. }
  208. return '员工'
  209. }
  210. },
  211. created () {
  212. this.$timer = null
  213. this.$checkTimer = null
  214. this.getUserInfo()
  215. },
  216. beforeDestroy () {
  217. clearTimeout(this.$timer)
  218. clearInterval(this.$checkTimer)
  219. },
  220. methods: {
  221. onAvatarClick () {
  222. this.$refs.upload.click()
  223. },
  224. onUpload (e) {
  225. const file = e.target.files[0]
  226. e.target.value = null
  227. if (file.size >= 1024 * 1024) {
  228. this.$message({
  229. type: 'warning',
  230. message: '请选择1M以下的图片'
  231. })
  232. return
  233. }
  234. const reader = new FileReader()
  235. reader.onload = () => {
  236. this.avatar = reader.result
  237. }
  238. reader.readAsDataURL(file)
  239. },
  240. getUserInfo () {
  241. this.loading = true
  242. this.error = false
  243. userinfo().then(
  244. this.setUserInfo,
  245. () => {
  246. this.error = true
  247. }
  248. ).finally(() => {
  249. this.loading = false
  250. })
  251. },
  252. setUserInfo (data) {
  253. if (!data.attributes) {
  254. data.attributes = {}
  255. }
  256. this.avatar = this.getOrCreateAttribute(data, 'avatar', '')
  257. this.phone = this.getOrCreateAttribute(data, 'phone', '')
  258. this.email = data.email ?? ''
  259. this.wechat = this.getOrCreateAttribute(data, 'wechat', '')
  260. this.user = data
  261. this.applet = this.getOrCreateAttribute(data, 'wechat-applet-openid', '')
  262. },
  263. getOrCreateAttribute (user, key, defaults = '') {
  264. if (!user.attributes[key]) {
  265. user.attributes[key] = [defaults]
  266. }
  267. return user.attributes[key][0]
  268. },
  269. updateUser (data, meesage) {
  270. return updateUser(this.user.id, data, meesage).then(() => {
  271. this.user = { ...this.user, ...data }
  272. })
  273. },
  274. changeAttribute (key, value, meesage) {
  275. if (this.user.attributes[key][0] !== value) {
  276. this.updateUser({
  277. attributes: {
  278. ...this.user.attributes,
  279. [key]: [value]
  280. }
  281. }, meesage).then(
  282. () => {
  283. this.user.attributes[key][0] = value
  284. },
  285. () => {
  286. this.resetAttribute(key)
  287. }
  288. )
  289. }
  290. },
  291. resetAttribute (key) {
  292. this[key] = this.user.attributes[key][0]
  293. },
  294. resetProp (key) {
  295. this[key] = this.user[key] ?? ''
  296. },
  297. onPhoneChange () {
  298. if (!this.phone) {
  299. this.$message({
  300. type: 'warning',
  301. message: '手机号不能为空'
  302. })
  303. this.resetAttribute('phone')
  304. return
  305. }
  306. if (!validPhone(this.phone)) {
  307. this.$message({
  308. type: 'warning',
  309. message: '手机号格式错误'
  310. })
  311. this.resetAttribute('phone')
  312. return
  313. }
  314. this.changeAttribute('phone', this.phone, '更新手机号')
  315. },
  316. onEmailChange () {
  317. if (!this.email) {
  318. this.$message({
  319. type: 'warning',
  320. message: '邮箱不能为空'
  321. })
  322. this.resetProp('email')
  323. return
  324. }
  325. if (!validEmail(this.email)) {
  326. this.$message({
  327. type: 'warning',
  328. message: '邮箱格式错误'
  329. })
  330. this.resetProp('email')
  331. return
  332. }
  333. this.updateUser({ email: this.email }, '更新邮箱').then(
  334. () => {
  335. this.user.email = this.email
  336. },
  337. () => {
  338. this.resetProp('email')
  339. }
  340. )
  341. },
  342. onClickWechat () {
  343. if (this.wechat) {
  344. this.$confirm(
  345. '解除绑定后将无法再收到消息推送',
  346. '解绑微信',
  347. { type: 'warning' }
  348. ).then(() => {
  349. this.changeAttribute('wechat', this.wechat = '', '解绑公众号')
  350. })
  351. } else {
  352. this.qrType = 'wechatOptions'
  353. this.showQr = true
  354. this.getQr()
  355. }
  356. },
  357. onClickApplet () {
  358. if (this.applet) {
  359. this.$confirm(
  360. '解除绑定后将无法使用小程序的功能',
  361. '解绑小程序',
  362. { type: 'warning' }
  363. ).then(
  364. () => {
  365. this.changeAttribute('wechat-applet-openid', this.applet = '', '解绑小程序')
  366. }
  367. )
  368. } else {
  369. this.qrType = 'appletOptions'
  370. this.showQr = true
  371. this.getQr()
  372. }
  373. },
  374. onCloseQr () {
  375. this.showQr = false
  376. },
  377. getQr () {
  378. const options = this.qrOptions
  379. if (options.loading || options.qr && !options.retry) {
  380. return
  381. }
  382. switch (this.qrType) {
  383. case 'wechatOptions':
  384. this.getWechatQr()
  385. break
  386. case 'appletOptions':
  387. this.getAppletQr()
  388. break
  389. default:
  390. break
  391. }
  392. },
  393. getWechatQr () {
  394. const options = this.wechatOptions
  395. if (!this.showQr || this.qrType !== 'wechatOptions') {
  396. options.qr = null
  397. return
  398. }
  399. options.loading = true
  400. options.retry = false
  401. clearTimeout(this.$timer)
  402. getTicket(this.user.id).then(
  403. ({ data }) => {
  404. if (!this.wechat) {
  405. try {
  406. const { expire_seconds, ticket } = JSON.parse(data)
  407. options.checking = true
  408. options.qr = `https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=${ticket}`
  409. // 提前10秒重新获取
  410. this.$timer = setTimeout(this.getWechatQr, (expire_seconds > 10 ? expire_seconds - 10 : expire_seconds) * 1000)
  411. this.checkBind()
  412. } catch (e) {
  413. console.warn(e)
  414. options.retry = true
  415. }
  416. }
  417. },
  418. () => {
  419. if (!this.wechat) {
  420. options.retry = true
  421. }
  422. }
  423. ).finally(() => {
  424. options.loading = false
  425. })
  426. },
  427. getAppletQr () {
  428. const options = this.appletOptions
  429. options.loading = true
  430. options.retry = false
  431. getQrcode({ page: 'pages/device/device', sourceUrl: GATEWAY }).then(
  432. ({ data }) => {
  433. options.checking = true
  434. options.qr = `data:image/png;base64,${data}`
  435. this.checkBind()
  436. },
  437. () => {
  438. options.retry = true
  439. }
  440. ).finally(() => {
  441. options.loading = false
  442. })
  443. },
  444. checkBind () {
  445. if (!this.$checkTimer) {
  446. this.$checkTimer = setInterval(this.check, 2000)
  447. }
  448. },
  449. check () {
  450. userinfo(true).then(data => {
  451. let bound = false
  452. let needCheck = false
  453. if (this.wechatOptions.checking) {
  454. const wechat = data.attributes.wechat?.[0]
  455. if (wechat) {
  456. bound = true
  457. this.wechatOptions.checking = false
  458. this.$message({
  459. type: 'success',
  460. message: '绑定公众号成功'
  461. })
  462. clearTimeout(this.$timer)
  463. this.wechatOptions.qr = null
  464. if (this.qrType === 'wechatOptions') {
  465. this.onCloseQr()
  466. }
  467. } else {
  468. needCheck = true
  469. }
  470. }
  471. if (this.appletOptions.checking) {
  472. const applet = data.attributes['wechat-applet-openid']?.[0]
  473. if (applet) {
  474. bound = true
  475. this.appletOptions.checking = false
  476. this.$message({
  477. type: 'success',
  478. message: '绑定小程序成功'
  479. })
  480. this.appletOptions.qr = null
  481. if (this.qrType === 'appletOptions') {
  482. this.onCloseQr()
  483. }
  484. } else {
  485. needCheck = true
  486. }
  487. }
  488. if (bound) {
  489. this.setUserInfo(data)
  490. }
  491. if (!needCheck) {
  492. clearInterval(this.$checkTimer)
  493. this.$checkTimer = null
  494. }
  495. })
  496. }
  497. }
  498. }
  499. </script>
  500. <style lang="scss" scoped>
  501. .c-profile {
  502. overflow: hidden;
  503. &__header {
  504. color: #fff;
  505. line-height: 1;
  506. background-color: $blue;
  507. }
  508. &__name {
  509. font-size: 20px;
  510. }
  511. }
  512. .o-avatar {
  513. display: inline-block;
  514. position: relative;
  515. width: 80px;
  516. height: 80px;
  517. padding: 2px;
  518. border-radius: 50%;
  519. background-color: #fff;
  520. &__img {
  521. display: inline-block;
  522. width: 100%;
  523. height: 100%;
  524. border-radius: 50%;
  525. background: url("~@/assets/icon_avatar.png") 0 0 / 100% 100% no-repeat;
  526. }
  527. &__upload {
  528. display: inline-flex;
  529. justify-content: center;
  530. align-items: center;
  531. position: absolute;
  532. right: 0;
  533. bottom: 0;
  534. width: 26px;
  535. height: 26px;
  536. color: $blue;
  537. border-radius: 50%;
  538. background-color: #fff;
  539. }
  540. }
  541. .el-icon-user {
  542. position: absolute;
  543. left: -32px;
  544. }
  545. .o-app {
  546. display: inline-flex;
  547. flex-direction: column;
  548. justify-content: center;
  549. align-items: center;
  550. width: 128px;
  551. color: $blue;
  552. font-size: 16px;
  553. .o-icon {
  554. margin-bottom: $spacing;
  555. }
  556. }
  557. .o-icon {
  558. &.wechat {
  559. background-image: url("~@/assets/icon_wechat.png");
  560. }
  561. &.applet {
  562. background-image: url("~@/assets/icon_applet.png");
  563. }
  564. }
  565. .c-wechat {
  566. border-radius: $radius;
  567. background-color: #fff;
  568. &__wrapper {
  569. position: relative;
  570. width: 240px;
  571. height: 240px;
  572. &.retry::after {
  573. content: "点击重试";
  574. display: inline-flex;
  575. justify-content: center;
  576. align-items: center;
  577. position: absolute;
  578. top: 0;
  579. left: 0;
  580. width: 100%;
  581. height: 100%;
  582. color: $black;
  583. font-weight: bold;
  584. background-color: rgba(#fff, 0.8);
  585. cursor: pointer;
  586. }
  587. }
  588. &__qr {
  589. display: inline-block;
  590. width: 100%;
  591. height: 100%;
  592. }
  593. }
  594. </style>