index.vue 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. <template>
  2. <view class="login-page">
  3. <view class="login-brand">
  4. <image
  5. class="login-brand__logo"
  6. src="/static/logo.png"
  7. mode="widthFix"
  8. />
  9. <view class="login-brand__sub">员工工作台登录</view>
  10. </view>
  11. <view class="login-card">
  12. <view class="login-field">
  13. <text class="login-field__label">账号</text>
  14. <view
  15. class="login-field__box"
  16. :class="{ 'is-focus': focusUser, 'is-error': !!errors.username }"
  17. >
  18. <input
  19. class="login-field__input"
  20. type="text"
  21. :value="username"
  22. placeholder="请输入用户名 / 工号"
  23. placeholder-class="login-ph"
  24. @input="onUserInput"
  25. @focus="focusUser = true"
  26. @blur="focusUser = false"
  27. />
  28. </view>
  29. <view v-if="errors.username" class="login-field__error">{{ errors.username }}</view>
  30. </view>
  31. <view class="login-field">
  32. <text class="login-field__label">密码</text>
  33. <view
  34. class="login-field__box"
  35. :class="{ 'is-focus': focusPwd, 'is-error': !!errors.password }"
  36. >
  37. <input
  38. class="login-field__input"
  39. :password="!showPwd"
  40. :value="password"
  41. placeholder="请输入密码"
  42. placeholder-class="login-ph"
  43. @input="onPwdInput"
  44. @focus="focusPwd = true"
  45. @blur="focusPwd = false"
  46. />
  47. <text class="login-field__action" @click="showPwd = !showPwd">
  48. {{ showPwd ? '隐藏' : '显示' }}
  49. </text>
  50. </view>
  51. <view v-if="errors.password" class="login-field__error">{{ errors.password }}</view>
  52. </view>
  53. <view
  54. class="login-submit oa-btn-loading"
  55. :class="{ 'is-loading': loading }"
  56. @click="onPasswordLogin"
  57. >
  58. <text class="oa-btn-loading__label">登录</text>
  59. <view v-if="loading" class="oa-btn-loading__spin" aria-hidden="true" />
  60. </view>
  61. <view class="login-agree" @click="toggleAgree">
  62. <view class="login-agree__box" :class="{ 'is-on': agreed }">
  63. <text v-if="agreed" class="login-agree__check">✓</text>
  64. </view>
  65. <view class="login-agree__text">
  66. 已阅读并同意
  67. <text class="login-agree__link" @click.stop="openProtocol('user')">《用户协议》</text>
  68. 和
  69. <text class="login-agree__link" @click.stop="openProtocol('privacy')">《隐私政策》</text>
  70. </view>
  71. </view>
  72. </view>
  73. <view class="login-divider">或</view>
  74. <button
  75. v-if="agreed"
  76. class="login-wechat oa-btn-loading"
  77. :class="{ 'is-loading': loading }"
  78. open-type="getPhoneNumber"
  79. :disabled="loading"
  80. @getphonenumber="onGetPhoneNumber"
  81. >
  82. <view class="oa-btn-loading__label login-wechat__inner">
  83. <text>手机号快捷登录</text>
  84. </view>
  85. <view v-if="loading" class="oa-btn-loading__spin" aria-hidden="true" />
  86. </button>
  87. <view
  88. v-else
  89. class="login-wechat oa-btn-loading"
  90. :class="{ 'is-loading': loading }"
  91. @click="onWechatGate"
  92. >
  93. <view class="oa-btn-loading__label login-wechat__inner">
  94. <text>手机号快捷登录</text>
  95. </view>
  96. <view v-if="loading" class="oa-btn-loading__spin" aria-hidden="true" />
  97. </view>
  98. <ConfirmDialog
  99. :show="agreeDialogVisible"
  100. title="请先同意协议"
  101. cancel-text="取消"
  102. confirm-text="同意并登录"
  103. :confirm-open-type="agreeDialogForWechat ? 'getPhoneNumber' : ''"
  104. @update:show="agreeDialogVisible = $event"
  105. @confirm="onAgreeConfirm"
  106. @cancel="onAgreeCancel"
  107. @getphonenumber="onAgreeGetPhoneNumber"
  108. >
  109. <text>登录前需阅读并同意</text>
  110. <text class="confirm-dialog__link" @click.stop="openProtocol('user')">
  111. 《用户协议》
  112. </text>
  113. <text>和</text>
  114. <text class="confirm-dialog__link" @click.stop="openProtocol('privacy')">
  115. 《隐私政策》
  116. </text>
  117. <text>。是否同意并继续登录?</text>
  118. </ConfirmDialog>
  119. <view
  120. v-if="envBanner.show"
  121. class="env-banner"
  122. :class="'env-banner--' + envBanner.tone"
  123. >
  124. <text class="env-banner__text">{{ envBanner.label }}</text>
  125. </view>
  126. </view>
  127. </template>
  128. <script>
  129. import { ENV_BANNER } from '../../utils/config.js'
  130. import {
  131. isLoggedIn,
  132. passwordLogin,
  133. wechatLogin,
  134. clearLegacyMockSessionIfNeeded,
  135. takeReturnUrl
  136. } from '../../utils/auth.js'
  137. import { validateCredentials } from '../../utils/validate-credentials.js'
  138. import ConfirmDialog from '../../components/ConfirmDialog.vue'
  139. export default {
  140. components: {
  141. ConfirmDialog
  142. },
  143. data() {
  144. return {
  145. envBanner: ENV_BANNER,
  146. username: '',
  147. password: '',
  148. showPwd: false,
  149. agreed: false,
  150. agreeDialogVisible: false,
  151. /** 微信登录入口打开的协议弹窗:确认键需带 getPhoneNumber */
  152. agreeDialogForWechat: false,
  153. loading: false,
  154. focusUser: false,
  155. focusPwd: false,
  156. errors: {
  157. username: '',
  158. password: ''
  159. }
  160. }
  161. },
  162. onShow() {
  163. clearLegacyMockSessionIfNeeded()
  164. if (isLoggedIn()) {
  165. this.goAfterLogin()
  166. }
  167. },
  168. methods: {
  169. onUserInput(e) {
  170. this.username = e.detail.value
  171. this.errors.username = ''
  172. },
  173. onPwdInput(e) {
  174. this.password = e.detail.value
  175. this.errors.password = ''
  176. },
  177. toggleAgree() {
  178. this.agreed = !this.agreed
  179. },
  180. ensureAgree() {
  181. if (this.agreed) return Promise.resolve(true)
  182. this.agreeDialogForWechat = false
  183. this.agreeDialogVisible = true
  184. return new Promise((resolve) => {
  185. this._agreeResolver = resolve
  186. })
  187. },
  188. onAgreeConfirm() {
  189. this.agreed = true
  190. this.agreeDialogVisible = false
  191. if (this._agreeResolver) {
  192. this._agreeResolver(true)
  193. this._agreeResolver = null
  194. }
  195. },
  196. onAgreeCancel() {
  197. this.agreeDialogVisible = false
  198. this.agreeDialogForWechat = false
  199. if (this._agreeResolver) {
  200. this._agreeResolver(false)
  201. this._agreeResolver = null
  202. }
  203. },
  204. /** 协议弹窗确认键直接拉起手机号授权(与「同意」同一次点击) */
  205. onAgreeGetPhoneNumber(e) {
  206. this.agreed = true
  207. this.agreeDialogVisible = false
  208. this.agreeDialogForWechat = false
  209. this.onGetPhoneNumber(e)
  210. },
  211. openProtocol(type) {
  212. uni.navigateTo({ url: `/pages/protocol/index?type=${type}` })
  213. },
  214. goAfterLogin() {
  215. const back = takeReturnUrl()
  216. if (back) {
  217. const url = back.startsWith('/') ? back : '/' + back
  218. uni.reLaunch({ url })
  219. return
  220. }
  221. uni.reLaunch({ url: '/pages/index/index' })
  222. },
  223. goHome() {
  224. this.goAfterLogin()
  225. },
  226. async onPasswordLogin() {
  227. if (this.loading) return
  228. this.errors = { username: '', password: '' }
  229. const v = validateCredentials(this.username, this.password)
  230. if (!v.ok) {
  231. if (v.code === 'EMPTY_USERNAME') this.errors.username = v.message
  232. if (v.code === 'EMPTY_PASSWORD') this.errors.password = v.message
  233. return
  234. }
  235. if (!(await this.ensureAgree())) return
  236. this.loading = true
  237. try {
  238. await passwordLogin({
  239. username: this.username,
  240. password: this.password
  241. })
  242. uni.showToast({ title: '登录成功', icon: 'success' })
  243. setTimeout(() => this.goAfterLogin(), 400)
  244. } catch (e) {
  245. uni.showToast({ title: e.message || '登录失败', icon: 'none' })
  246. } finally {
  247. this.loading = false
  248. }
  249. },
  250. onWechatGate() {
  251. if (this.loading) return
  252. this.agreeDialogForWechat = true
  253. this.agreeDialogVisible = true
  254. },
  255. async onGetPhoneNumber(e) {
  256. if (this.loading) return
  257. if (!this.agreed) {
  258. // 理论上微信按钮仅在已同意时渲染;防御
  259. this.agreeDialogForWechat = true
  260. this.agreeDialogVisible = true
  261. return
  262. }
  263. const detail = (e && e.detail) || {}
  264. const errMsg = detail.errMsg || ''
  265. if (errMsg && errMsg.indexOf('ok') === -1) {
  266. uni.showToast({
  267. title: '需要授权手机号,或请使用账号密码登录',
  268. icon: 'none'
  269. })
  270. return
  271. }
  272. const phoneCode = detail.code
  273. if (!phoneCode) {
  274. uni.showToast({
  275. title: '需要授权手机号,或请使用账号密码登录',
  276. icon: 'none'
  277. })
  278. return
  279. }
  280. this.loading = true
  281. try {
  282. const loginRes = await new Promise((resolve, reject) => {
  283. uni.login({ provider: 'weixin', success: resolve, fail: reject })
  284. })
  285. await wechatLogin({
  286. loginCode: loginRes.code,
  287. phoneCode
  288. })
  289. uni.showToast({ title: '登录成功', icon: 'success' })
  290. setTimeout(() => this.goAfterLogin(), 400)
  291. } catch (err) {
  292. uni.showToast({
  293. title: (err && err.message) || '登录失败',
  294. icon: 'none'
  295. })
  296. } finally {
  297. this.loading = false
  298. }
  299. }
  300. }
  301. }
  302. </script>
  303. <style scoped>
  304. .login-ph {
  305. color: #9aaba3;
  306. }
  307. </style>