index.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  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="u-bold">{{ name }}</div>
  27. <div class="u-relative">
  28. <i class="c-profile__icon el-icon-user" />
  29. {{ roleTip }}
  30. </div>
  31. </div>
  32. <div class="l-flex__fill u-overflow-y--auto">
  33. <el-result
  34. v-if="error"
  35. icon="warning"
  36. >
  37. <template #extra>
  38. <el-link
  39. class="u-pointer"
  40. type="warning"
  41. @click="getUserInfo"
  42. >
  43. 出错了,点击重试
  44. </el-link>
  45. </template>
  46. </el-result>
  47. <div
  48. v-else
  49. class="c-form"
  50. >
  51. <div class="c-form__section">
  52. <span class="c-form__label">手机:</span>
  53. <el-input
  54. v-model="phone"
  55. class="c-form__item"
  56. @change="onPhoneChange"
  57. @keydown.enter="$event.target.blur()"
  58. />
  59. </div>
  60. <div class="c-form__section has-bottom-padding">
  61. <span class="c-form__label">邮箱:</span>
  62. <el-input
  63. v-model="email"
  64. class="c-form__item"
  65. @change="onEmailChange"
  66. @keydown.enter="$event.target.blur()"
  67. />
  68. </div>
  69. <div class="c-form__section">
  70. <i class="o-wechat" />
  71. <div
  72. class="has-padding u-pointer"
  73. :class="color"
  74. @click="onClickWechat"
  75. >
  76. {{ tip }}
  77. </div>
  78. </div>
  79. </div>
  80. </div>
  81. <el-dialog
  82. :visible.sync="showQr"
  83. custom-class="c-preview"
  84. width="0"
  85. @close="onCloseQr"
  86. >
  87. <div class="c-wechat has-padding">
  88. <div class="l-flex--row center has-bottom-padding">
  89. <i class="c-sibling-item o-wechat" />
  90. <span class="c-sibling-item u-color--black u-bold">请使用微信扫一扫</span>
  91. </div>
  92. <div
  93. v-loading="loading"
  94. class="c-wechat__wrapper"
  95. :class="{ retry }"
  96. @click="getQr"
  97. >
  98. <img
  99. class="c-wechat__qr"
  100. :src="qr"
  101. >
  102. </div>
  103. </div>
  104. </el-dialog>
  105. </wrapper>
  106. </template>
  107. <script>
  108. import { mapState } from 'vuex'
  109. import {
  110. userinfo,
  111. updateUser,
  112. getTicket
  113. } from '@/api/user'
  114. import { Role } from '@/constant'
  115. export default {
  116. name: 'Profile',
  117. data () {
  118. return {
  119. error: false,
  120. user: null,
  121. avatar: '',
  122. phone: '',
  123. email: '',
  124. wechat: '',
  125. showQr: false,
  126. qr: null,
  127. loading: false,
  128. retry: false
  129. }
  130. },
  131. computed: {
  132. ...mapState('user', [
  133. 'name',
  134. 'roles'
  135. ]),
  136. avatarStyle () {
  137. const avatar = this.avatar
  138. return avatar ? {
  139. backgroundImage: `url("${avatar}")`
  140. } : null
  141. },
  142. roleTip () {
  143. if (this.roles.has(Role.ADMIN)) {
  144. return '超级管理员'
  145. }
  146. if (this.roles.has(Role.SUPERVISOR)) {
  147. return '主管'
  148. }
  149. if (this.roles.has(Role.STAFF)) {
  150. return '员工'
  151. }
  152. return '游客'
  153. },
  154. tip () {
  155. return this.wechat ? '解除绑定' : '绑定微信'
  156. },
  157. color () {
  158. return this.wechat ? 'u-color--error' : 'u-color--primary'
  159. }
  160. },
  161. created () {
  162. this.$timer = null
  163. this.$checkTimer = null
  164. this.getUserInfo()
  165. },
  166. beforeDestroy () {
  167. this.onCloseQr()
  168. clearInterval(this.$checkTimer)
  169. },
  170. methods: {
  171. onAvatarClick () {
  172. this.$refs.upload.click()
  173. },
  174. onUpload (e) {
  175. const file = e.target.files[0]
  176. e.target.value = null
  177. if (file.size >= 1024 * 1024) {
  178. this.$message({
  179. type: 'warning',
  180. message: '请选择1M以下的图片'
  181. })
  182. return
  183. }
  184. const reader = new FileReader()
  185. reader.onload = () => {
  186. this.avatar = reader.result
  187. }
  188. reader.readAsDataURL(file)
  189. },
  190. getUserInfo () {
  191. this.error = false
  192. userinfo().then(
  193. this.setUserInfo,
  194. () => {
  195. this.error = true
  196. }
  197. )
  198. },
  199. setUserInfo (data) {
  200. this.avatar = this.getOrCreateAttribute(data, 'avatar', '')
  201. this.phone = this.getOrCreateAttribute(data, 'phone', '')
  202. this.email = data.email ?? ''
  203. this.wechat = this.getOrCreateAttribute(data, 'wechat', '')
  204. this.user = data
  205. },
  206. getOrCreateAttribute (user, key, defaults = '') {
  207. if (!user.attributes[key]) {
  208. user.attributes[key] = [defaults]
  209. }
  210. return user.attributes[key][0]
  211. },
  212. updateUser (data, meesage) {
  213. return updateUser(this.user.id, data, meesage)
  214. },
  215. changeAttribute (key, value, meesage) {
  216. if (this.user.attributes[key][0] !== value) {
  217. this.updateUser({
  218. attributes: {
  219. ...this.user.attributes,
  220. [key]: [value]
  221. }
  222. }, meesage).then(
  223. () => {
  224. this.user.attributes[key][0] = value
  225. },
  226. () => {
  227. this.resetAttribute(key)
  228. }
  229. )
  230. }
  231. },
  232. resetAttribute (key) {
  233. this[key] = this.user.attributes[key][0]
  234. },
  235. resetProp (key) {
  236. this[key] = this.user[key] ?? ''
  237. },
  238. onPhoneChange () {
  239. if (!this.phone) {
  240. this.$message({
  241. type: 'warning',
  242. message: '手机号不能为空'
  243. })
  244. this.resetAttribute('phone')
  245. return
  246. }
  247. 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))) {
  248. this.$message({
  249. type: 'warning',
  250. message: '手机号格式错误'
  251. })
  252. this.resetAttribute('phone')
  253. return
  254. }
  255. this.changeAttribute('phone', this.phone, '更新手机号')
  256. },
  257. onEmailChange () {
  258. if (!this.email) {
  259. this.$message({
  260. type: 'warning',
  261. message: '邮箱不能为空'
  262. })
  263. this.resetProp('email')
  264. return
  265. }
  266. 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))) {
  267. this.$message({
  268. type: 'warning',
  269. message: '邮箱格式错误'
  270. })
  271. this.resetProp('email')
  272. return
  273. }
  274. this.updateUser({ email: this.email }, '更新邮箱').then(
  275. () => {
  276. this.user.email = this.email
  277. },
  278. () => {
  279. this.resetProp('email')
  280. }
  281. )
  282. },
  283. onClickWechat () {
  284. if (this.wechat) {
  285. this.$confirm(
  286. '解除绑定后将无法再收到消息推送',
  287. '确定解绑微信?',
  288. { type: 'warning' }
  289. ).then(
  290. () => {
  291. this.changeAttribute('wechat', this.wechat = '', '解绑微信')
  292. }
  293. )
  294. } else {
  295. this.showQr = true
  296. this.getQr()
  297. }
  298. },
  299. onCloseQr () {
  300. this.showQr = false
  301. clearTimeout(this.$timer)
  302. },
  303. getQr () {
  304. if (this.loading || this.qr && !this.retry) {
  305. return
  306. }
  307. this.fetchQr()
  308. },
  309. resetQr () {
  310. clearInterval(this.$checkTimer)
  311. this.$checkTimer = null
  312. this.qr = ''
  313. this.retry = false
  314. },
  315. fetchQr () {
  316. if (!this.showQr) {
  317. this.qr = ''
  318. return
  319. }
  320. if (!this.qr || this.retry) {
  321. this.loading = true
  322. }
  323. this.retry = false
  324. clearTimeout(this.$timer)
  325. getTicket(this.user.id).then(
  326. ({ data }) => {
  327. if (!this.wechat) {
  328. try {
  329. const { expire_seconds, ticket } = JSON.parse(data)
  330. this.qr = `https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=${ticket}`
  331. // 提前10秒重新获取
  332. this.$timer = setTimeout(this.fetchQr, (expire_seconds > 10 ? expire_seconds - 10 : expire_seconds) * 1000)
  333. this.checkBind()
  334. } catch (e) {
  335. console.warn(e)
  336. this.retry = true
  337. }
  338. }
  339. },
  340. () => {
  341. if (!this.wechat) {
  342. this.retry = true
  343. }
  344. }
  345. ).finally(() => {
  346. this.loading = false
  347. })
  348. },
  349. checkBind () {
  350. if (!this.wechat && !this.$checkTimer) {
  351. this.$checkTimer = setInterval(this.check, 2000)
  352. }
  353. },
  354. check () {
  355. userinfo(true).then(data => {
  356. const wechat = data.attributes.wechat?.[0]
  357. if (wechat) {
  358. this.$message({
  359. type: 'success',
  360. message: '绑定微信成功'
  361. })
  362. this.setUserInfo(data)
  363. this.onCloseQr()
  364. this.resetQr()
  365. }
  366. })
  367. }
  368. }
  369. }
  370. </script>
  371. <style lang="scss" scoped>
  372. .c-profile {
  373. overflow: hidden;
  374. &__header {
  375. justify-content: space-between;
  376. height: 176px;
  377. color: #fff;
  378. line-height: 1;
  379. background-color: $blue;
  380. }
  381. &__icon {
  382. position: absolute;
  383. top: 50%;
  384. transform: translate(calc(-100% - 16px), -50%);
  385. }
  386. }
  387. .o-avatar {
  388. display: inline-block;
  389. position: relative;
  390. width: 80px;
  391. height: 80px;
  392. padding: 2px;
  393. border-radius: 50%;
  394. background-color: #fff;
  395. &__img {
  396. display: inline-block;
  397. width: 100%;
  398. height: 100%;
  399. border-radius: 50%;
  400. background: url("~@/assets/icon_avatar.png") 0 0 / 100% 100% no-repeat;
  401. }
  402. &__upload {
  403. display: inline-flex;
  404. justify-content: center;
  405. align-items: center;
  406. position: absolute;
  407. right: 0;
  408. bottom: 0;
  409. width: 26px;
  410. height: 26px;
  411. color: $blue;
  412. border-radius: 50%;
  413. background-color: #fff;
  414. }
  415. }
  416. .o-wechat {
  417. display: inline-block;
  418. width: 32px;
  419. height: 32px;
  420. background: url("~@/assets/icon_wechat.png") 0 0 / 100% 100% no-repeat;
  421. }
  422. .c-wechat {
  423. border-radius: $radius;
  424. background-color: #fff;
  425. &__wrapper {
  426. position: relative;
  427. width: 240px;
  428. height: 240px;
  429. &.retry::after {
  430. content: "点击重试";
  431. display: inline-flex;
  432. justify-content: center;
  433. align-items: center;
  434. position: absolute;
  435. top: 0;
  436. left: 0;
  437. width: 100%;
  438. height: 100%;
  439. color: $black;
  440. font-weight: bold;
  441. background-color: rgba(#fff, 0.8);
  442. cursor: pointer;
  443. }
  444. }
  445. &__qr {
  446. display: inline-block;
  447. width: 100%;
  448. height: 100%;
  449. }
  450. }
  451. </style>