index.vue 9.7 KB

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