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. export default {
  150. name: 'Profile',
  151. components: {
  152. UserInfoItem
  153. },
  154. data () {
  155. return {
  156. loading: true,
  157. error: false,
  158. user: {
  159. email: '',
  160. attributes: {
  161. phone: ['']
  162. }
  163. },
  164. avatar: '',
  165. phone: '',
  166. email: '',
  167. wechat: '',
  168. applet: '',
  169. showQr: false,
  170. qrType: 'wechatOptions',
  171. wechatOptions: {
  172. loading: false,
  173. retry: false,
  174. qr: null
  175. },
  176. appletOptions: {
  177. loading: false,
  178. retry: false,
  179. qr: null
  180. }
  181. }
  182. },
  183. computed: {
  184. ...mapGetters([
  185. 'name',
  186. 'isSuperAdmin',
  187. 'isTenantAdmin',
  188. 'isGroupAdmin'
  189. ]),
  190. avatarStyle () {
  191. const avatar = this.avatar
  192. return avatar ? { backgroundImage: `url("${avatar}")` } : null
  193. },
  194. qrOptions () {
  195. return this[this.qrType]
  196. },
  197. role () {
  198. if (this.isSuperAdmin) {
  199. return '超级管理员'
  200. }
  201. if (this.isTenantAdmin) {
  202. return '管理员'
  203. }
  204. if (this.isGroupAdmin) {
  205. return '主管'
  206. }
  207. return '员工'
  208. }
  209. },
  210. created () {
  211. this.$timer = null
  212. this.$checkTimer = null
  213. this.getUserInfo()
  214. },
  215. beforeDestroy () {
  216. clearTimeout(this.$timer)
  217. clearInterval(this.$checkTimer)
  218. },
  219. methods: {
  220. onAvatarClick () {
  221. this.$refs.upload.click()
  222. },
  223. onUpload (e) {
  224. const file = e.target.files[0]
  225. e.target.value = null
  226. if (file.size >= 1024 * 1024) {
  227. this.$message({
  228. type: 'warning',
  229. message: '请选择1M以下的图片'
  230. })
  231. return
  232. }
  233. const reader = new FileReader()
  234. reader.onload = () => {
  235. this.avatar = reader.result
  236. }
  237. reader.readAsDataURL(file)
  238. },
  239. getUserInfo () {
  240. this.loading = true
  241. this.error = false
  242. userinfo().then(
  243. this.setUserInfo,
  244. () => {
  245. this.error = true
  246. }
  247. ).finally(() => {
  248. this.loading = false
  249. })
  250. },
  251. setUserInfo (data) {
  252. if (!data.attributes) {
  253. data.attributes = {}
  254. }
  255. this.avatar = this.getOrCreateAttribute(data, 'avatar', '')
  256. this.phone = this.getOrCreateAttribute(data, 'phone', '')
  257. this.email = data.email ?? ''
  258. this.wechat = this.getOrCreateAttribute(data, 'wechat', '')
  259. this.user = data
  260. this.applet = this.getOrCreateAttribute(data, 'wechat-applet-openid', '')
  261. },
  262. getOrCreateAttribute (user, key, defaults = '') {
  263. if (!user.attributes[key]) {
  264. user.attributes[key] = [defaults]
  265. }
  266. return user.attributes[key][0]
  267. },
  268. updateUser (data, meesage) {
  269. return updateUser(this.user.id, data, meesage).then(() => {
  270. this.user = { ...this.user, ...data }
  271. })
  272. },
  273. changeAttribute (key, value, meesage) {
  274. if (this.user.attributes[key][0] !== value) {
  275. this.updateUser({
  276. attributes: {
  277. ...this.user.attributes,
  278. [key]: [value]
  279. }
  280. }, meesage).then(
  281. () => {
  282. this.user.attributes[key][0] = value
  283. },
  284. () => {
  285. this.resetAttribute(key)
  286. }
  287. )
  288. }
  289. },
  290. resetAttribute (key) {
  291. this[key] = this.user.attributes[key][0]
  292. },
  293. resetProp (key) {
  294. this[key] = this.user[key] ?? ''
  295. },
  296. onPhoneChange () {
  297. if (!this.phone) {
  298. this.$message({
  299. type: 'warning',
  300. message: '手机号不能为空'
  301. })
  302. this.resetAttribute('phone')
  303. return
  304. }
  305. if (!validPhone(this.phone)) {
  306. this.$message({
  307. type: 'warning',
  308. message: '手机号格式错误'
  309. })
  310. this.resetAttribute('phone')
  311. return
  312. }
  313. this.changeAttribute('phone', this.phone, '更新手机号')
  314. },
  315. onEmailChange () {
  316. if (!this.email) {
  317. this.$message({
  318. type: 'warning',
  319. message: '邮箱不能为空'
  320. })
  321. this.resetProp('email')
  322. return
  323. }
  324. if (!validEmail(this.email)) {
  325. this.$message({
  326. type: 'warning',
  327. message: '邮箱格式错误'
  328. })
  329. this.resetProp('email')
  330. return
  331. }
  332. this.updateUser({ email: this.email }, '更新邮箱').then(
  333. () => {
  334. this.user.email = this.email
  335. },
  336. () => {
  337. this.resetProp('email')
  338. }
  339. )
  340. },
  341. onClickWechat () {
  342. if (this.wechat) {
  343. this.$confirm(
  344. '解除绑定后将无法再收到消息推送',
  345. '解绑微信',
  346. { type: 'warning' }
  347. ).then(() => {
  348. this.changeAttribute('wechat', this.wechat = '', '解绑公众号')
  349. })
  350. } else {
  351. this.qrType = 'wechatOptions'
  352. this.showQr = true
  353. this.getQr()
  354. }
  355. },
  356. onClickApplet () {
  357. if (this.applet) {
  358. this.$confirm(
  359. '解除绑定后将无法使用小程序的功能',
  360. '解绑小程序',
  361. { type: 'warning' }
  362. ).then(
  363. () => {
  364. this.changeAttribute('wechat-applet-openid', this.applet = '', '解绑小程序')
  365. }
  366. )
  367. } else {
  368. this.qrType = 'appletOptions'
  369. this.showQr = true
  370. this.getQr()
  371. }
  372. },
  373. onCloseQr () {
  374. this.showQr = false
  375. },
  376. getQr () {
  377. const options = this.qrOptions
  378. if (options.loading || options.qr && !options.retry) {
  379. return
  380. }
  381. switch (this.qrType) {
  382. case 'wechatOptions':
  383. this.getWechatQr()
  384. break
  385. case 'appletOptions':
  386. this.getAppletQr()
  387. break
  388. default:
  389. break
  390. }
  391. },
  392. getWechatQr () {
  393. const options = this.wechatOptions
  394. if (!this.showQr || this.qrType !== 'wechatOptions') {
  395. options.qr = null
  396. return
  397. }
  398. options.loading = true
  399. options.retry = false
  400. clearTimeout(this.$timer)
  401. getTicket(this.user.id).then(
  402. ({ data }) => {
  403. if (!this.wechat) {
  404. try {
  405. const { expire_seconds, ticket } = JSON.parse(data)
  406. options.checking = true
  407. options.qr = `https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=${ticket}`
  408. // 提前10秒重新获取
  409. this.$timer = setTimeout(this.getWechatQr, (expire_seconds > 10 ? expire_seconds - 10 : expire_seconds) * 1000)
  410. this.checkBind()
  411. } catch (e) {
  412. console.warn(e)
  413. options.retry = true
  414. }
  415. }
  416. },
  417. () => {
  418. if (!this.wechat) {
  419. options.retry = true
  420. }
  421. }
  422. ).finally(() => {
  423. options.loading = false
  424. })
  425. },
  426. getAppletQr () {
  427. const options = this.appletOptions
  428. options.loading = true
  429. options.retry = false
  430. getQrcode({ page: 'pages/otp/otp' }).then(
  431. ({ data }) => {
  432. options.checking = true
  433. options.qr = `data:image/png;base64,${data}`
  434. this.checkBind()
  435. },
  436. () => {
  437. options.retry = true
  438. }
  439. ).finally(() => {
  440. options.loading = false
  441. })
  442. },
  443. checkBind () {
  444. if (!this.$checkTimer) {
  445. this.$checkTimer = setInterval(this.check, 2000)
  446. }
  447. },
  448. check () {
  449. userinfo(true).then(data => {
  450. let bound = false
  451. let needCheck = false
  452. if (this.wechatOptions.checking) {
  453. const wechat = data.attributes.wechat?.[0]
  454. if (wechat) {
  455. bound = true
  456. this.wechatOptions.checking = false
  457. this.$message({
  458. type: 'success',
  459. message: '绑定公众号成功'
  460. })
  461. clearTimeout(this.$timer)
  462. this.wechatOptions.qr = null
  463. if (this.qrType === 'wechatOptions') {
  464. this.onCloseQr()
  465. }
  466. } else {
  467. needCheck = true
  468. }
  469. }
  470. if (this.appletOptions.checking) {
  471. const applet = data.attributes['wechat-applet-openid']?.[0]
  472. if (applet) {
  473. bound = true
  474. this.appletOptions.checking = false
  475. this.$message({
  476. type: 'success',
  477. message: '绑定小程序成功'
  478. })
  479. this.appletOptions.qr = null
  480. if (this.qrType === 'appletOptions') {
  481. this.onCloseQr()
  482. }
  483. } else {
  484. needCheck = true
  485. }
  486. }
  487. if (bound) {
  488. this.setUserInfo(data)
  489. }
  490. if (!needCheck) {
  491. clearInterval(this.$checkTimer)
  492. this.$checkTimer = null
  493. }
  494. })
  495. }
  496. }
  497. }
  498. </script>
  499. <style lang="scss" scoped>
  500. .c-profile {
  501. overflow: hidden;
  502. &__header {
  503. color: #fff;
  504. line-height: 1;
  505. background-color: $blue;
  506. }
  507. &__name {
  508. font-size: 20px;
  509. }
  510. }
  511. .o-avatar {
  512. display: inline-block;
  513. position: relative;
  514. width: 80px;
  515. height: 80px;
  516. padding: 2px;
  517. border-radius: 50%;
  518. background-color: #fff;
  519. &__img {
  520. display: inline-block;
  521. width: 100%;
  522. height: 100%;
  523. border-radius: 50%;
  524. background: url("~@/assets/icon_avatar.png") 0 0 / 100% 100% no-repeat;
  525. }
  526. &__upload {
  527. display: inline-flex;
  528. justify-content: center;
  529. align-items: center;
  530. position: absolute;
  531. right: 0;
  532. bottom: 0;
  533. width: 26px;
  534. height: 26px;
  535. color: $blue;
  536. border-radius: 50%;
  537. background-color: #fff;
  538. }
  539. }
  540. .el-icon-user {
  541. position: absolute;
  542. left: -32px;
  543. }
  544. .o-app {
  545. display: inline-flex;
  546. flex-direction: column;
  547. justify-content: center;
  548. align-items: center;
  549. width: 128px;
  550. color: $blue;
  551. font-size: 16px;
  552. .o-icon {
  553. margin-bottom: $spacing;
  554. }
  555. }
  556. .o-icon {
  557. &.wechat {
  558. background-image: url("~@/assets/icon_wechat.png");
  559. }
  560. &.applet {
  561. background-image: url("~@/assets/icon_applet.png");
  562. }
  563. }
  564. .c-wechat {
  565. border-radius: $radius;
  566. background-color: #fff;
  567. &__wrapper {
  568. position: relative;
  569. width: 240px;
  570. height: 240px;
  571. &.retry::after {
  572. content: "点击重试";
  573. display: inline-flex;
  574. justify-content: center;
  575. align-items: center;
  576. position: absolute;
  577. top: 0;
  578. left: 0;
  579. width: 100%;
  580. height: 100%;
  581. color: $black;
  582. font-weight: bold;
  583. background-color: rgba(#fff, 0.8);
  584. cursor: pointer;
  585. }
  586. }
  587. &__qr {
  588. display: inline-block;
  589. width: 100%;
  590. height: 100%;
  591. }
  592. }
  593. </style>