You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

422 line
9.5 KiB

  1. <template>
  2. <view class="page">
  3. <view class="logo-area">
  4. <image class="logo" src="https://cdn.csybhelp.com/images/cytx/logo.png" mode="aspectFit" />
  5. <text class="title">肠愈同行</text>
  6. <text class="subtitle">患者关爱</text>
  7. </view>
  8. <view class="btn-area">
  9. <!-- #ifdef MP-WEIXIN -->
  10. <view v-if="mpLoginMode === 'wx'">
  11. <button class="login-btn" @tap="handleWxLogin" :loading="loading">
  12. 微信一键登录
  13. </button>
  14. </view>
  15. <view v-else class="phone-form">
  16. <view class="form-item">
  17. <input class="form-input" type="number" v-model="phone" placeholder="请输入手机号" maxlength="11" />
  18. </view>
  19. <view class="form-item code-row">
  20. <input class="form-input code-input" type="number" v-model="smsCode" placeholder="请输入验证码" maxlength="6" />
  21. <button class="sms-btn" :disabled="countdown > 0" @tap="sendSms">
  22. {{ countdown > 0 ? countdown + 's' : '获取验证码' }}
  23. </button>
  24. </view>
  25. <button class="login-btn" @tap="handlePhoneLogin" :loading="loading">
  26. 登录
  27. </button>
  28. </view>
  29. <button class="mode-switch-btn" @tap="toggleMpLoginMode">
  30. <u-icon
  31. :name="mpLoginMode === 'wx' ? 'phone-fill' : 'weixin-fill'"
  32. size="30"
  33. :color="mpLoginMode === 'wx' ? '#0F78E9' : '#07C160'"
  34. />
  35. </button>
  36. <!-- #endif -->
  37. <!-- #ifdef H5 -->
  38. <!-- 二维码模式 -->
  39. <view v-if="h5Mode === 'qrcode'" class="qrcode-area">
  40. <text class="qrcode-tip">肠愈同行小程序已上线</text>
  41. <text class="qrcode-tip">请长按识别二维码进入小程序</text>
  42. <image class="qrcode-img" src="https://cdn.csybhelp.com/images/cytx/cytx_qrcode.jpg" mode="aspectFit" show-menu-by-longpress />
  43. <text class="switch-phone-btn" @tap="h5Mode = 'phone'">继续使用手机号登录</text>
  44. </view>
  45. <!-- 手机号模式 -->
  46. <view v-else class="phone-form">
  47. <text class="switch-qrcode-btn" @tap="h5Mode = 'qrcode'">← 使用小程序</text>
  48. <view class="form-item">
  49. <input class="form-input" type="number" v-model="phone" placeholder="请输入手机号" maxlength="11" />
  50. </view>
  51. <view class="form-item code-row">
  52. <input class="form-input code-input" type="number" v-model="smsCode" placeholder="请输入验证码" maxlength="6" />
  53. <button class="sms-btn" :disabled="countdown > 0" @tap="sendSms">
  54. {{ countdown > 0 ? countdown + 's' : '获取验证码' }}
  55. </button>
  56. </view>
  57. <button class="login-btn" @tap="handlePhoneLogin" :loading="loading">
  58. 登录
  59. </button>
  60. </view>
  61. <!-- #endif -->
  62. <!-- #ifdef MP-WEIXIN -->
  63. <view class="agree-row">
  64. <u-checkbox-group v-model="agreementValues" @change="handleAgreementChange">
  65. <u-checkbox name="agree" shape="circle" activeColor="#0F78E9" size="18" />
  66. </u-checkbox-group>
  67. <text class="agree-text" @tap="toggleAgreement">请阅读并同意</text>
  68. <text class="link" @tap.stop="goService">《用户服务协议》</text>
  69. <text class="agree-text" @tap="toggleAgreement">和</text>
  70. <text class="link" @tap.stop="goPrivacy">《隐私政策》</text>
  71. </view>
  72. <!-- #endif -->
  73. <!-- #ifdef H5 -->
  74. <view v-if="h5Mode === 'phone'" class="tip">登录即表示同意
  75. <text class="link" @tap="goPrivacy">《隐私政策》</text>
  76. </view>
  77. <!-- #endif -->
  78. </view>
  79. </view>
  80. </template>
  81. <script setup>
  82. import { ref, onUnmounted } from 'vue'
  83. import { post } from '@/utils/request.js'
  84. import { setToken, setUserInfo } from '@/utils/cache.js'
  85. const loading = ref(false)
  86. const phone = ref('')
  87. const smsCode = ref('')
  88. const countdown = ref(0)
  89. const agreementValues = ref([])
  90. const h5Mode = ref('qrcode')
  91. const mpLoginMode = ref('wx')
  92. let timer = null
  93. onUnmounted(() => {
  94. if (timer) { clearInterval(timer); timer = null }
  95. })
  96. const loginSuccess = (res) => {
  97. setToken(res.data.token)
  98. setUserInfo(res.data.userInfo)
  99. uni.showToast({ title: '登录成功', icon: 'success' })
  100. setTimeout(() => {
  101. const pages = getCurrentPages()
  102. if (pages.length > 1) {
  103. uni.navigateBack()
  104. } else {
  105. uni.switchTab({ url: '/pages/profile/profile' })
  106. }
  107. }, 500)
  108. }
  109. const checkAgreement = () => {
  110. // #ifdef MP-WEIXIN
  111. if (!agreementValues.value.includes('agree')) {
  112. uni.showToast({ title: '请先阅读并同意相关协议', icon: 'none' })
  113. return false
  114. }
  115. // #endif
  116. return true
  117. }
  118. // #ifdef MP-WEIXIN
  119. const toggleAgreement = () => {
  120. agreementValues.value = agreementValues.value.includes('agree') ? [] : ['agree']
  121. }
  122. const handleAgreementChange = (values) => {
  123. agreementValues.value = values || []
  124. }
  125. const toggleMpLoginMode = () => {
  126. mpLoginMode.value = mpLoginMode.value === 'wx' ? 'phone' : 'wx'
  127. }
  128. const handleWxLogin = async () => {
  129. if (loading.value) return
  130. if (!checkAgreement()) return
  131. loading.value = true
  132. try {
  133. const code = await new Promise((resolve, reject) => {
  134. uni.login({
  135. provider: 'weixin',
  136. success: (res) => resolve(res.code),
  137. fail: () => reject({ msg: '微信登录失败' })
  138. })
  139. })
  140. const res = await post('/api/mp/login', { code })
  141. loginSuccess(res)
  142. } catch (e) {
  143. if (e && e.msg) uni.showToast({ title: e.msg, icon: 'none' })
  144. } finally {
  145. loading.value = false
  146. }
  147. }
  148. // #endif
  149. const sendSms = async () => {
  150. if (countdown.value > 0) return
  151. if (!checkAgreement()) return
  152. if (!phone.value || phone.value.length !== 11) {
  153. return uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
  154. }
  155. try {
  156. const res = await post('/api/mp/sendSmsCode', { mobile: phone.value, bizType: 'login' })
  157. if (res.data && res.data.code) {
  158. uni.showToast({ title: `验证码: ${res.data.code}`, icon: 'none', duration: 3000 })
  159. } else {
  160. uni.showToast({ title: '验证码已发送', icon: 'none' })
  161. }
  162. countdown.value = 60
  163. timer = setInterval(() => {
  164. countdown.value--
  165. if (countdown.value <= 0) { clearInterval(timer); timer = null }
  166. }, 1000)
  167. } catch (e) {
  168. if (e && e.msg) uni.showToast({ title: e.msg, icon: 'none' })
  169. }
  170. }
  171. const handlePhoneLogin = async () => {
  172. if (loading.value) return
  173. if (!checkAgreement()) return
  174. if (!phone.value || phone.value.length !== 11) {
  175. return uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
  176. }
  177. if (!smsCode.value || smsCode.value.length !== 6) {
  178. return uni.showToast({ title: '请输入6位验证码', icon: 'none' })
  179. }
  180. loading.value = true
  181. try {
  182. const res = await post('/api/mp/phoneLogin', { mobile: phone.value, code: smsCode.value })
  183. loginSuccess(res)
  184. } catch (e) {
  185. if (e && e.msg) uni.showToast({ title: e.msg, icon: 'none' })
  186. } finally {
  187. loading.value = false
  188. }
  189. }
  190. const goPrivacy = () => {
  191. // #ifdef H5
  192. uni.navigateTo({ url: '/pages/content/content?key=privacy_policy_h5' })
  193. // #endif
  194. // #ifdef MP-WEIXIN
  195. uni.navigateTo({ url: '/pages/content/content?key=privacy_policy' })
  196. // #endif
  197. }
  198. const goService = () => {
  199. uni.navigateTo({ url: '/pages/content/content?key=service_policy' })
  200. }
  201. </script>
  202. <style lang="scss" scoped>
  203. .page {
  204. min-height: 100vh;
  205. background: #fff;
  206. display: flex;
  207. flex-direction: column;
  208. align-items: center;
  209. padding-top: 10vh;
  210. }
  211. .logo-area {
  212. display: flex;
  213. flex-direction: column;
  214. align-items: center;
  215. margin-bottom: 80rpx;
  216. .logo {
  217. width: 180rpx;
  218. height: 180rpx;
  219. border-radius: 24rpx;
  220. margin-bottom: 32rpx;
  221. }
  222. .title {
  223. font-size: 44rpx;
  224. font-weight: 600;
  225. color: #303133;
  226. margin-bottom: 8rpx;
  227. }
  228. .subtitle {
  229. font-size: 26rpx;
  230. color: #b0b3b8;
  231. letter-spacing: 2rpx;
  232. }
  233. }
  234. .btn-area {
  235. width: 100%;
  236. padding: 0 64rpx;
  237. box-sizing: border-box;
  238. .login-btn {
  239. width: 100%;
  240. height: 88rpx;
  241. line-height: 88rpx;
  242. background: #0F78E9;
  243. color: #fff;
  244. font-size: 32rpx;
  245. border-radius: 44rpx;
  246. border: none;
  247. &::after {
  248. border: none;
  249. }
  250. &:active {
  251. opacity: 0.85;
  252. }
  253. }
  254. .tip {
  255. text-align: center;
  256. font-size: 24rpx;
  257. color: #909399;
  258. margin-top: 32rpx;
  259. .link {
  260. color: #0F78E9;
  261. }
  262. }
  263. .agree-row {
  264. display: flex;
  265. align-items: center;
  266. justify-content: center;
  267. flex-wrap: wrap;
  268. margin-top: 32rpx;
  269. .agree-text {
  270. font-size: 24rpx;
  271. color: #909399;
  272. margin-left: 8rpx;
  273. }
  274. .link {
  275. font-size: 24rpx;
  276. color: #0F78E9;
  277. }
  278. }
  279. .mode-switch-btn {
  280. width: 96rpx;
  281. height: 96rpx;
  282. line-height: 1;
  283. border-radius: 50%;
  284. margin: 40rpx auto 0;
  285. padding: 0;
  286. background: #fff;
  287. border: 1rpx solid #e5e6eb;
  288. box-shadow: 0 8rpx 24rpx rgba(15, 120, 233, 0.12);
  289. display: flex;
  290. align-items: center;
  291. justify-content: center;
  292. &::after {
  293. border: none;
  294. }
  295. &:active {
  296. transform: scale(0.96);
  297. opacity: 0.9;
  298. }
  299. }
  300. }
  301. .phone-form {
  302. width: 100%;
  303. .form-item {
  304. margin-bottom: 24rpx;
  305. }
  306. .form-input {
  307. width: 100%;
  308. height: 88rpx;
  309. border: 1rpx solid #dcdfe6;
  310. border-radius: 44rpx;
  311. padding: 0 36rpx;
  312. font-size: 30rpx;
  313. box-sizing: border-box;
  314. background: #f8f9fa;
  315. }
  316. .code-row {
  317. display: flex;
  318. gap: 20rpx;
  319. .code-input {
  320. flex: 2;
  321. }
  322. }
  323. .sms-btn {
  324. flex: 1;
  325. height: 88rpx;
  326. line-height: 88rpx;
  327. padding: 0;
  328. background: #0F78E9;
  329. color: #fff;
  330. border: none;
  331. border-radius: 44rpx;
  332. font-size: 26rpx;
  333. white-space: nowrap;
  334. text-align: center;
  335. &::after {
  336. border: none;
  337. }
  338. &[disabled] {
  339. background: #a0cfff;
  340. color: #fff;
  341. }
  342. }
  343. .login-btn {
  344. margin-top: 16rpx;
  345. }
  346. }
  347. .qrcode-area {
  348. display: flex;
  349. flex-direction: column;
  350. align-items: center;
  351. .qrcode-tip {
  352. font-size: 28rpx;
  353. color: #606266;
  354. line-height: 1.6;
  355. }
  356. .qrcode-img {
  357. width: 400rpx;
  358. height: 400rpx;
  359. margin: 40rpx 0;
  360. }
  361. }
  362. .switch-phone-btn {
  363. font-size: 24rpx;
  364. color: #c0c4cc;
  365. text-align: center;
  366. }
  367. .switch-qrcode-btn {
  368. display: block;
  369. font-size: 28rpx;
  370. color: #0F78E9;
  371. margin-bottom: 32rpx;
  372. font-weight: 500;
  373. }
  374. </style>