reset-secret.vue 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. <template>
  2. <view class="acct-pwd-page">
  3. <view class="acct-pwd-nav" :style="{ paddingTop: statusBar + 'px' }">
  4. <view class="acct-pwd-nav-row">
  5. <view class="acct-pwd-back" @tap="goBack">
  6. <text class="acct-pwd-back-icon">←</text>
  7. </view>
  8. <text class="acct-pwd-nav-title">{{ copy.title }}</text>
  9. <view class="acct-pwd-nav-placeholder" />
  10. </view>
  11. </view>
  12. <scroll-view
  13. scroll-y
  14. class="acct-pwd-scroll"
  15. :style="{ height: scrollHeight + 'px' }"
  16. >
  17. <view class="acct-pwd-body" :style="{ paddingBottom: safeBottom + 'px' }">
  18. <view class="acct-pwd-hero">
  19. <view class="acct-pwd-hero-icon-wrap">
  20. <text class="acct-pwd-hero-icon">🔑</text>
  21. </view>
  22. <text class="acct-pwd-hero-title">{{ copy.heroTitle }}</text>
  23. <text class="acct-pwd-hero-desc">{{ copy.heroDesc }}</text>
  24. <text class="acct-reset-handset">+{{ region }} {{ maskedHandset }}</text>
  25. </view>
  26. <view class="acct-pwd-card">
  27. <text class="acct-pwd-label">{{ copy.secretLabel }}</text>
  28. <view class="acct-reset-input-wrap">
  29. <input
  30. v-model="secret"
  31. class="acct-reset-input"
  32. :password="!showSecret"
  33. :placeholder="copy.secretPlaceholder"
  34. placeholder-class="acct-reset-input__placeholder"
  35. maxlength="20"
  36. />
  37. <text class="acct-reset-eye" @tap="showSecret = !showSecret">
  38. {{ showSecret ? copy.hide : copy.show }}
  39. </text>
  40. </view>
  41. <view v-if="secret.length" class="acct-reset-meter">
  42. <view
  43. v-for="bar in 4"
  44. :key="bar"
  45. class="acct-reset-meter__bar"
  46. :class="meterClass(bar)"
  47. />
  48. <text class="acct-reset-meter__txt">{{ meterLabel }}</text>
  49. </view>
  50. </view>
  51. <button
  52. class="acct-pwd-submit"
  53. :disabled="!canSubmit || submitting"
  54. :loading="submitting"
  55. :class="{ 'acct-pwd-submit--off': !canSubmit, 'acct-pwd-submit--busy': submitting }"
  56. @tap="onSubmit"
  57. >
  58. <text class="acct-pwd-submit__text">{{ submitting ? copy.submitting : copy.submit }}</text>
  59. </button>
  60. </view>
  61. </scroll-view>
  62. </view>
  63. </template>
  64. <script>
  65. import { resetSecretBySms } from '@/features/account/account-security-gateway'
  66. import { passcodeShape, secretInRange, trimHandset } from '@/features/passport/form-rules'
  67. export default {
  68. data() {
  69. return {
  70. statusBar: 0,
  71. scrollHeight: 600,
  72. safeBottom: 0,
  73. handset: '',
  74. region: '852',
  75. passcode: '',
  76. ticketId: '',
  77. secret: '',
  78. showSecret: false,
  79. submitting: false,
  80. redirect: ''
  81. }
  82. },
  83. computed: {
  84. copy() {
  85. const t = this.$t.bind(this)
  86. return {
  87. title: t('passport.title-reset-secret'),
  88. heroTitle: t('user.resetSecretHeroTitle'),
  89. heroDesc: t('passport.subtitle-reset-secret'),
  90. secretLabel: t('passport.label-new-secret'),
  91. secretPlaceholder: t('passport.ph-new-secret'),
  92. show: t('passport.btn-show'),
  93. hide: t('passport.btn-hide'),
  94. submit: t('passport.btn-reset-secret'),
  95. submitting: t('passport.btn-probing'),
  96. ok: t('passport.tip-reset-secret-ok')
  97. }
  98. },
  99. maskedHandset() {
  100. const raw = this.handset
  101. if (raw.length <= 4) return raw
  102. return `${raw.slice(0, 3)}****${raw.slice(-4)}`
  103. },
  104. secretStrength() {
  105. const v = this.secret
  106. if (!secretInRange(v)) return 0
  107. let score = 1
  108. if (/[A-Z]/.test(v) && /[a-z]/.test(v)) score += 1
  109. if (/\d/.test(v)) score += 1
  110. if (/[^A-Za-z0-9]/.test(v)) score += 1
  111. return score
  112. },
  113. meterLabel() {
  114. const t = this.$t.bind(this)
  115. return [
  116. '',
  117. t('passport.meter-weak'),
  118. t('passport.meter-mid'),
  119. t('passport.meter-good'),
  120. t('passport.meter-great')
  121. ][this.secretStrength]
  122. },
  123. canSubmit() {
  124. return secretInRange(this.secret) && passcodeShape(this.passcode)
  125. }
  126. },
  127. onLoad(query) {
  128. const sys = uni.getSystemInfoSync()
  129. this.statusBar = sys.statusBarHeight || 0
  130. this.safeBottom = sys.safeAreaInsets ? sys.safeAreaInsets.bottom : 0
  131. const navH = this.statusBar + uni.upx2px(88)
  132. this.scrollHeight = (sys.windowHeight || 667) - navH
  133. if (query && query.handset) {
  134. try {
  135. this.handset = trimHandset(decodeURIComponent(query.handset))
  136. } catch (_) {
  137. this.handset = trimHandset(query.handset)
  138. }
  139. }
  140. if (query && query.region) {
  141. try {
  142. this.region = String(decodeURIComponent(query.region)).replace(/^\+/, '')
  143. } catch (_) {
  144. this.region = String(query.region).replace(/^\+/, '')
  145. }
  146. }
  147. if (query && query.passcode) {
  148. try {
  149. this.passcode = decodeURIComponent(query.passcode)
  150. } catch (_) {
  151. this.passcode = query.passcode
  152. }
  153. }
  154. if (query && query.ticketId) {
  155. try {
  156. this.ticketId = decodeURIComponent(query.ticketId)
  157. } catch (_) {
  158. this.ticketId = query.ticketId || ''
  159. }
  160. }
  161. if (query && query.redirect) {
  162. try {
  163. this.redirect = decodeURIComponent(query.redirect)
  164. } catch (_) {
  165. this.redirect = query.redirect || ''
  166. }
  167. }
  168. },
  169. onShow() {
  170. this.resetSubmitState()
  171. },
  172. methods: {
  173. goBack() {
  174. if (getCurrentPages().length > 1) {
  175. uni.navigateBack({ delta: 1 })
  176. } else {
  177. uni.redirectTo({ url: '/pages/account/sms-verify' })
  178. }
  179. },
  180. toast(msg) {
  181. if (this.$appKit && this.$appKit.msg) this.$appKit.msg(msg)
  182. else uni.showToast({ title: msg, icon: 'none' })
  183. },
  184. resetSubmitState() {
  185. this.submitting = false
  186. try {
  187. uni.hideLoading()
  188. } catch (_) {}
  189. },
  190. meterClass(bar) {
  191. if (bar <= this.secretStrength) {
  192. if (this.secretStrength === 1) return 'acct-reset-meter__bar--weak'
  193. if (this.secretStrength === 2) return 'acct-reset-meter__bar--ok'
  194. return 'acct-reset-meter__bar--great'
  195. }
  196. return ''
  197. },
  198. async onSubmit() {
  199. if (this.submitting) return
  200. if (!this.canSubmit) {
  201. this.toast(this.$t('passport.err-secret'))
  202. return
  203. }
  204. this.submitting = true
  205. uni.showLoading({ title: this.copy.submitting, mask: true })
  206. try {
  207. await resetSecretBySms({
  208. handset: this.handset,
  209. region: this.region,
  210. passcode: this.passcode,
  211. secret: this.secret,
  212. ticketId: this.ticketId
  213. })
  214. this.toast(this.copy.ok)
  215. const target = this.redirect || '/pages/passport/signin?mode=secret'
  216. setTimeout(() => {
  217. uni.reLaunch({
  218. url: target,
  219. fail: () => uni.redirectTo({ url: target })
  220. })
  221. }, 400)
  222. } catch (err) {
  223. const bizHandled = err && (err.kind === 'BUSINESS' || err.kind === 'SILENT')
  224. if (!bizHandled) {
  225. this.toast((err && err.message) || this.$t('passport.tip-reset-secret-failed'))
  226. }
  227. } finally {
  228. this.resetSubmitState()
  229. }
  230. }
  231. }
  232. }
  233. </script>
  234. <style scoped>
  235. @import './account-flow.css';
  236. .acct-reset-handset {
  237. font-size: 28rpx;
  238. color: #8a8d9e;
  239. margin-top: 8rpx;
  240. }
  241. .acct-reset-input-wrap {
  242. display: flex;
  243. flex-direction: row;
  244. align-items: center;
  245. height: 96rpx;
  246. padding: 0 24rpx;
  247. background-color: #f7f9fc;
  248. border: 1rpx solid #e5e9f2;
  249. border-radius: 20rpx;
  250. box-sizing: border-box;
  251. }
  252. .acct-reset-input {
  253. flex: 1;
  254. height: 96rpx;
  255. font-size: 30rpx;
  256. color: #1a1d26;
  257. }
  258. .acct-reset-input__placeholder {
  259. color: #b8bcc8;
  260. }
  261. .acct-reset-eye {
  262. font-size: 26rpx;
  263. color: #4a8cff;
  264. padding-left: 16rpx;
  265. flex-shrink: 0;
  266. }
  267. .acct-reset-meter {
  268. display: flex;
  269. flex-direction: row;
  270. align-items: center;
  271. margin-top: 20rpx;
  272. }
  273. .acct-reset-meter__bar {
  274. width: 48rpx;
  275. height: 8rpx;
  276. margin-right: 12rpx;
  277. border-radius: 4rpx;
  278. background-color: #e5e9f2;
  279. }
  280. .acct-reset-meter__bar--weak {
  281. background-color: #f59e0b;
  282. }
  283. .acct-reset-meter__bar--ok {
  284. background-color: #4a8cff;
  285. }
  286. .acct-reset-meter__bar--great {
  287. background-color: #34d399;
  288. }
  289. .acct-reset-meter__txt {
  290. font-size: 24rpx;
  291. color: #8a8d9e;
  292. }
  293. </style>