index.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  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', ['name', 'roles']),
  133. avatarStyle () {
  134. const avatar = this.avatar
  135. return avatar ? {
  136. backgroundImage: `url("${avatar}")`
  137. } : null
  138. },
  139. roleTip () {
  140. if (this.roles.includes(Role.ADMIN)) {
  141. return '超级管理员'
  142. }
  143. if (this.roles.includes(Role.SUPERVISOR)) {
  144. return '主管'
  145. }
  146. if (this.roles.includes(Role.STAFF)) {
  147. return '员工'
  148. }
  149. return '游客'
  150. },
  151. tip () {
  152. return this.wechat ? '解除绑定' : '绑定微信'
  153. },
  154. color () {
  155. return this.wechat ? 'u-color--error' : 'u-color--primary'
  156. }
  157. },
  158. created () {
  159. this.$timer = null
  160. this.$checkTimer = null
  161. this.getUserInfo()
  162. },
  163. beforeDestroy () {
  164. this.onCloseQr()
  165. clearInterval(this.$checkTimer)
  166. },
  167. methods: {
  168. onAvatarClick () {
  169. this.$refs.upload.click()
  170. },
  171. onUpload (e) {
  172. const file = e.target.files[0]
  173. e.target.value = null
  174. if (file.size >= 1024 * 1024) {
  175. this.$message({
  176. type: 'warning',
  177. message: '请选择1M以下的图片'
  178. })
  179. return
  180. }
  181. const reader = new FileReader()
  182. reader.onload = () => {
  183. this.avatar = reader.result
  184. }
  185. reader.readAsDataURL(file)
  186. },
  187. getUserInfo () {
  188. this.error = false
  189. userinfo().then(data => {
  190. this.avatar = this.getOrCreateAttribute(data, 'avatar', '')
  191. this.phone = this.getOrCreateAttribute(data, 'phone', '')
  192. this.email = data.email ?? ''
  193. this.wechat = this.getOrCreateAttribute(data, 'wechat', '')
  194. this.user = data
  195. }, () => {
  196. this.error = true
  197. })
  198. },
  199. getOrCreateAttribute (user, key, defaults = '') {
  200. if (!user.attributes[key]) {
  201. user.attributes[key] = [defaults]
  202. }
  203. return user.attributes[key][0]
  204. },
  205. updateUser (data, meesage) {
  206. return updateUser(this.user.id, data, meesage)
  207. },
  208. changeAttribute (key, value, meesage) {
  209. if (this.user.attributes[key][0] !== value) {
  210. this.updateUser({
  211. attributes: {
  212. ...this.user.attributes,
  213. [key]: [value]
  214. }
  215. }, meesage).then(
  216. () => {
  217. this.user.attributes[key][0] = value
  218. },
  219. () => {
  220. this.resetAttribute(key)
  221. }
  222. )
  223. }
  224. },
  225. resetAttribute (key) {
  226. this[key] = this.user.attributes[key][0]
  227. },
  228. resetProp (key) {
  229. this[key] = this.user[key] ?? ''
  230. },
  231. onPhoneChange () {
  232. if (!this.phone) {
  233. this.$message({
  234. type: 'warning',
  235. message: '手机号不能为空'
  236. })
  237. this.resetAttribute('phone')
  238. return
  239. }
  240. 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))) {
  241. this.$message({
  242. type: 'warning',
  243. message: '手机号格式错误'
  244. })
  245. this.resetAttribute('phone')
  246. return
  247. }
  248. this.changeAttribute('phone', this.phone, '更新手机号')
  249. },
  250. onEmailChange () {
  251. if (!this.email) {
  252. this.$message({
  253. type: 'warning',
  254. message: '邮箱不能为空'
  255. })
  256. this.resetProp('email')
  257. return
  258. }
  259. 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))) {
  260. this.$message({
  261. type: 'warning',
  262. message: '邮箱格式错误'
  263. })
  264. this.resetProp('email')
  265. return
  266. }
  267. this.updateUser({ email: this.email }, '更新邮箱').then(
  268. () => {
  269. this.user.email = this.email
  270. },
  271. () => {
  272. this.resetProp('email')
  273. }
  274. )
  275. },
  276. onClickWechat () {
  277. if (this.wechat) {
  278. this.$confirm('解除绑定后将无法再收到消息推送', '确定解绑微信?', { type: 'warning' }).then(
  279. () => {
  280. this.changeAttribute('wechat', this.wechat = '', '解绑微信')
  281. }
  282. )
  283. } else {
  284. this.showQr = true
  285. this.getQr()
  286. }
  287. },
  288. onCloseQr () {
  289. this.showQr = false
  290. clearTimeout(this.$timer)
  291. },
  292. getQr () {
  293. if (this.loading || this.qr && !this.retry) {
  294. return
  295. }
  296. this.fetchQr()
  297. },
  298. resetQr () {
  299. clearInterval(this.$checkTimer)
  300. this.$checkTimer = null
  301. this.qr = ''
  302. this.retry = false
  303. },
  304. fetchQr () {
  305. if (!this.showQr) {
  306. this.qr = ''
  307. return
  308. }
  309. if (!this.qr || this.retry) {
  310. this.loading = true
  311. }
  312. this.retry = false
  313. clearTimeout(this.$timer)
  314. getTicket(this.user.id).then(
  315. ({ data }) => {
  316. if (!this.wechat) {
  317. try {
  318. const { expire_seconds, ticket } = JSON.parse(data)
  319. this.qr = `https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=${ticket}`
  320. // 提前10秒重新获取
  321. this.$timer = setTimeout(this.fetchQr, (expire_seconds - 10) * 1000)
  322. this.checkBind()
  323. } catch (e) {
  324. console.warn(e)
  325. this.retry = true
  326. }
  327. }
  328. },
  329. () => {
  330. if (!this.wechat) {
  331. this.retry = true
  332. }
  333. }
  334. ).finally(() => {
  335. this.loading = false
  336. })
  337. },
  338. checkBind () {
  339. if (!this.wechat && !this.$checkTimer) {
  340. this.$checkTimer = setInterval(this.check, 2000)
  341. }
  342. },
  343. check () {
  344. userinfo(true).then(data => {
  345. const wechat = data.attributes.wechat?.[0]
  346. if (wechat) {
  347. this.$message({
  348. type: 'success',
  349. message: '绑定微信成功'
  350. })
  351. this.wechat = wechat
  352. this.user.attributes.wechat[0] = wechat
  353. this.onCloseQr()
  354. this.resetQr()
  355. }
  356. })
  357. }
  358. }
  359. }
  360. </script>
  361. <style lang="scss" scoped>
  362. .c-profile {
  363. overflow: hidden;
  364. &__header {
  365. justify-content: space-between;
  366. height: 176px;
  367. color: #fff;
  368. line-height: 1;
  369. background-color: $blue;
  370. }
  371. &__icon {
  372. position: absolute;
  373. top: 50%;
  374. transform: translate(calc(-100% - 16px), -50%);
  375. }
  376. }
  377. .o-avatar {
  378. display: inline-block;
  379. position: relative;
  380. width: 80px;
  381. height: 80px;
  382. padding: 2px;
  383. border-radius: 50%;
  384. background-color: #fff;
  385. &__img {
  386. display: inline-block;
  387. width: 100%;
  388. height: 100%;
  389. border-radius: 50%;
  390. background: url("~@/assets/icon_avatar.png") 0 0 / 100% 100% no-repeat;
  391. }
  392. &__upload {
  393. display: inline-flex;
  394. justify-content: center;
  395. align-items: center;
  396. position: absolute;
  397. right: 0;
  398. bottom: 0;
  399. width: 26px;
  400. height: 26px;
  401. color: $blue;
  402. border-radius: 50%;
  403. background-color: #fff;
  404. }
  405. }
  406. .o-wechat {
  407. display: inline-block;
  408. width: 32px;
  409. height: 32px;
  410. background: url("~@/assets/icon_wechat.png") 0 0 / 100% 100% no-repeat;
  411. }
  412. .c-wechat {
  413. border-radius: $radius;
  414. background-color: #fff;
  415. &__wrapper {
  416. position: relative;
  417. width: 240px;
  418. height: 240px;
  419. &.retry::after {
  420. content: "点击重试";
  421. display: inline-flex;
  422. justify-content: center;
  423. align-items: center;
  424. position: absolute;
  425. top: 0;
  426. left: 0;
  427. width: 100%;
  428. height: 100%;
  429. color: $black;
  430. font-weight: bold;
  431. background-color: rgba(#fff, 0.8);
  432. cursor: pointer;
  433. }
  434. }
  435. &__qr {
  436. display: inline-block;
  437. width: 100%;
  438. height: 100%;
  439. }
  440. }
  441. </style>