图书馆小程序
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.

523 lines
13 KiB

4 weeks ago
3 weeks ago
4 weeks ago
3 weeks ago
4 weeks ago
3 weeks ago
4 weeks ago
4 weeks ago
3 weeks ago
4 weeks ago
3 weeks ago
4 weeks ago
4 weeks ago
  1. <template>
  2. <view class="page-container">
  3. <!-- 顶部背景 + 标题 -->
  4. <view class="top-bar">
  5. <image class="top-bar-bg" src="@/static/images/mingqi-beij@2x.png" mode="aspectFill"></image>
  6. <view class="library-info">
  7. <image class="avatar" :src="libraryLogo" mode="aspectFill"></image>
  8. <view class="library-name">葛店经济技术开发区图书馆</view>
  9. <view class="sub-title">读者证办理</view>
  10. </view>
  11. </view>
  12. <!-- 表单卡片 -->
  13. <view class="form-card">
  14. <!-- 姓名 -->
  15. <view class="form-item">
  16. <uni-icons class="form-icon" custom-prefix="iconfont" type="icon-xingming" size="22"></uni-icons>
  17. <input
  18. class="input"
  19. placeholder="请输入真实姓名"
  20. v-model="formData.name"
  21. maxlength="20"
  22. />
  23. </view>
  24. <!-- 身份证号 -->
  25. <view class="form-item">
  26. <uni-icons class="form-icon" custom-prefix="iconfont" type="icon-duzhezheng" size="24"></uni-icons>
  27. <input
  28. class="input"
  29. placeholder="请输入18位身份证号"
  30. v-model="formData.idCard"
  31. maxlength="18"
  32. />
  33. </view>
  34. <!-- 手机号 -->
  35. <view class="form-item">
  36. <uni-icons class="form-icon" type="phone" size="24"></uni-icons>
  37. <input
  38. class="input"
  39. placeholder="请输入手机号"
  40. v-model="formData.phone"
  41. type="number"
  42. maxlength="11"
  43. />
  44. <button class="get-phone-btn" open-type="getPhoneNumber" @getphonenumber="getPhoneNumber">
  45. 一键获取
  46. </button>
  47. </view>
  48. <!-- 密码 -->
  49. <view class="form-item">
  50. <uni-icons class="form-icon" type="locked" size="24"></uni-icons>
  51. <input
  52. class="input"
  53. placeholder="请设置6-20位密码"
  54. :password="!showPwd"
  55. v-model="formData.password"
  56. maxlength="20"
  57. />
  58. <uni-icons
  59. class="pwd-icon"
  60. :type="showPwd ? 'eye' : 'eye-slash'"
  61. size="20"
  62. @click="togglePwd"
  63. ></uni-icons>
  64. </view>
  65. <!-- 确认密码 -->
  66. <view class="form-item">
  67. <uni-icons class="form-icon" type="locked" size="24"></uni-icons>
  68. <input
  69. class="input"
  70. placeholder="请再次确认密码"
  71. :password="!showConfirmPwd"
  72. v-model="formData.confirmPassword"
  73. maxlength="20"
  74. />
  75. <uni-icons
  76. class="pwd-icon"
  77. :type="showConfirmPwd ? 'eye' : 'eye-slash'"
  78. size="20"
  79. @click="toggleConfirmPwd"
  80. ></uni-icons>
  81. </view>
  82. <!-- 提交按钮 -->
  83. <button class="submit-btn" type="primary" @click="submit" :loading="loading">
  84. {{ loading ? '提交中...' : '确认办理' }}
  85. </button>
  86. <!-- 同意协议 -->
  87. <view class="agreement-item">
  88. <view class="radio-box" :class="{ checked: agreed }" @click="agreed = !agreed">
  89. <view class="radio-inner" v-if="agreed"></view>
  90. </view>
  91. <text class="agreement-text">
  92. 我已阅读并同意遵守
  93. <text class="agreement-link" @click="openAgreement">爱图智服服务协议</text>
  94. </text>
  95. </view>
  96. </view>
  97. <!-- 温馨提示 -->
  98. <view class="tips-card">
  99. <view class="tips-title">温馨提示</view>
  100. <view class="tips-text">
  101. 1. 请填写真实身份信息用于办理读者证<br>
  102. 2. 密码请牢记用于登录和借阅查询<br>
  103. <!-- 3. 提交后工作人员将尽快审核 -->
  104. </view>
  105. </view>
  106. </view>
  107. </template>
  108. <script>
  109. import config from '@/utils/config';
  110. import { getOpenId } from '@/utils/storage';
  111. import {FetchInitScreenSetting, FetchSessionKey, FetchDecryptPhone } from '@/api/user.js';
  112. export default {
  113. data() {
  114. return {
  115. sessionKey: '',
  116. openid: '',
  117. formData: {
  118. name: '',
  119. idCard: '',
  120. phone: '',
  121. password: '',
  122. confirmPassword: ''
  123. },
  124. showPwd: false,
  125. showConfirmPwd: false,
  126. loading: false,
  127. agreed: false
  128. };
  129. },
  130. onLoad() {
  131. this.getScreenSetting();
  132. this.getSessionKey();
  133. },
  134. methods: {
  135. async getSessionKey() {
  136. try {
  137. uni.login({
  138. success: async (loginRes) => {
  139. console.log('loginRes',loginRes)
  140. if (loginRes.code) {
  141. const res = await FetchSessionKey({
  142. libcode: getApp().globalData.libcode,
  143. code: loginRes.code
  144. });
  145. if (res.code === 200) {
  146. this.sessionKey = res.data.session_key;
  147. this.openid = res.data.openid;
  148. }
  149. }
  150. }
  151. });
  152. } catch (error) {
  153. console.error('获取sessionKey失败:', error);
  154. }
  155. },
  156. async getScreenSetting() {
  157. const res = await FetchInitScreenSetting({ libcode: getApp().globalData.libcode });
  158. if(res.data.library_logo && res.data.library_logo.context){
  159. this.libraryLogo = config.baseUrl + '/files/' + res.data.library_logo.context;
  160. }else{
  161. this.libraryLogo = null;
  162. }
  163. },
  164. getPhoneNumber(e) {
  165. if (e.detail.errMsg !== 'getPhoneNumber:ok') {
  166. uni.showToast({ title: '取消授权', icon: 'none' });
  167. return;
  168. }
  169. if (!this.sessionKey) {
  170. uni.showToast({ title: '请稍后再试', icon: 'none' });
  171. return;
  172. }
  173. FetchDecryptPhone({
  174. sessionKey: this.sessionKey,
  175. encryptedData: e.detail.encryptedData,
  176. iv: e.detail.iv
  177. }).then(res => {
  178. if (res.code === 200) {
  179. this.formData.phone = res.data.phoneNumber || '';
  180. uni.showToast({ title: '获取成功', icon: 'success' });
  181. } else {
  182. uni.showToast({ title: res.message || '获取失败', icon: 'none' });
  183. }
  184. }).catch(() => {
  185. uni.showToast({ title: '获取手机号失败', icon: 'none' });
  186. });
  187. },
  188. togglePwd() {
  189. this.showPwd = !this.showPwd;
  190. },
  191. toggleConfirmPwd() {
  192. this.showConfirmPwd = !this.showConfirmPwd;
  193. },
  194. // 身份证验证(支持15位和18位,自动去除空格和连字符)
  195. validateIdCard(idCard) {
  196. if (!idCard) return false;
  197. // 去除空格和连字符
  198. idCard = idCard.replace(/[\s-]/g, '');
  199. // 15位身份证格式验证
  200. const idCard15Reg = /^[1-9]\d{5}\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}$/;
  201. // 18位身份证格式验证
  202. const idCard18Reg = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/;
  203. // 15位身份证直接验证格式
  204. if (idCard.length === 15) {
  205. return idCard15Reg.test(idCard);
  206. }
  207. // 18位身份证需要验证格式、日期和校验码
  208. if (idCard.length !== 18 || !idCard18Reg.test(idCard)) {
  209. return false;
  210. }
  211. // 验证日期有效性(宽松模式:跳过校验码验证)
  212. const year = +idCard.substring(6, 10);
  213. const month = +idCard.substring(10, 12);
  214. const day = +idCard.substring(12, 14);
  215. const date = new Date(year, month - 1, day);
  216. return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day;
  217. },
  218. // 手机号验证
  219. validatePhone(phone) {
  220. return /^1[3-9]\d{9}$/.test(phone);
  221. },
  222. // 表单验证(更严谨)
  223. validateForm() {
  224. const { name, idCard, phone, password, confirmPassword } = this.formData;
  225. // 1. 姓名:必须非空且trim后有内容
  226. if (!name || name.trim() === '') {
  227. uni.showToast({ title: '请输入姓名', icon: 'none' });
  228. return false;
  229. }
  230. // 2. 身份证:非空 + 格式正确
  231. if (!idCard || idCard.trim() === '') {
  232. uni.showToast({ title: '请输入身份证号', icon: 'none' });
  233. return false;
  234. }
  235. if (!this.validateIdCard(idCard.trim())) {
  236. uni.showToast({ title: '身份证号格式不正确', icon: 'none' });
  237. return false;
  238. }
  239. // 3. 手机号:非空 + 格式正确
  240. if (!phone || phone.trim() === '') {
  241. uni.showToast({ title: '请输入手机号', icon: 'none' });
  242. return false;
  243. }
  244. if (!this.validatePhone(phone.trim())) {
  245. uni.showToast({ title: '手机号格式不正确', icon: 'none' });
  246. return false;
  247. }
  248. // 4. 密码:至少6位
  249. if (!password || password.length < 6) {
  250. uni.showToast({ title: '密码至少6位', icon: 'none' });
  251. return false;
  252. }
  253. // 5. 确认密码:必须一致
  254. if (password !== confirmPassword) {
  255. uni.showToast({ title: '两次密码不一致', icon: 'none' });
  256. return false;
  257. }
  258. // 所有校验通过
  259. return true;
  260. },
  261. // 显示协议弹窗
  262. showAgreementModal() {
  263. uni.showModal({
  264. title: '提示',
  265. content: '请先阅读并同意《爱图智服服务协议》',
  266. confirmText: '同意',
  267. cancelText: '不同意',
  268. success: (res) => {
  269. if (res.confirm) {
  270. // 用户同意,选中radio并继续提交
  271. this.agreed = true;
  272. this.submitForm();
  273. }
  274. }
  275. });
  276. },
  277. // 跳转到协议详情页面
  278. openAgreement() {
  279. uni.navigateTo({
  280. url: '/subpkg/pages/agreement/agreement'
  281. });
  282. },
  283. async submit() {
  284. // 防止重复点击
  285. if (this.loading) return;
  286. // 小程序关键:等 input 数据同步到 data
  287. await this.$nextTick();
  288. // 校验不通过直接 return
  289. if (!this.validateForm()) {
  290. return;
  291. }
  292. // 如果未同意协议,显示协议弹窗
  293. if (!this.agreed) {
  294. this.showAgreementModal();
  295. return;
  296. }
  297. // 已同意协议,直接提交
  298. await this.submitForm();
  299. },
  300. // 实际提交表单
  301. async submitForm() {
  302. this.loading = true;
  303. try {
  304. const openId = await getOpenId();
  305. if (!openId) {
  306. uni.showToast({ title: '获取用户信息失败', icon: 'none' });
  307. return;
  308. }
  309. uni.showToast({ title: '后续流程正在研发中,请稍后试用~', icon: 'none' });
  310. setTimeout(() => uni.navigateBack(), 1500);
  311. // 提交(trim 后再发)
  312. // const res = await uni.request({
  313. // url: config.baseUrl + '/api/reader/register',
  314. // method: 'POST',
  315. // data: {
  316. // name: this.formData.name.trim(),
  317. // idCard: this.formData.idCard.trim(),
  318. // phone: this.formData.phone.trim(),
  319. // password: this.formData.password,
  320. // openId,
  321. // libcode: getApp().globalData.libcode
  322. // }
  323. // });
  324. // if (res.data?.code === 200) {
  325. // uni.showToast({ title: '办理成功', icon: 'success' });
  326. // setTimeout(() => uni.navigateBack(), 1500);
  327. // } else {
  328. // uni.showToast({ title: res.data?.message || '提交失败', icon: 'none' });
  329. // }
  330. } catch (err) {
  331. console.error('提交异常', err);
  332. uni.showToast({ title: '网络异常,请重试', icon: 'none' });
  333. } finally {
  334. this.loading = false;
  335. }
  336. }
  337. }
  338. };
  339. </script>
  340. <style lang="scss" scoped>
  341. page {
  342. background-color: #f5f7fa;
  343. }
  344. .page-container {
  345. min-height: 100vh;
  346. background-color: #f5f7fa;
  347. padding-bottom: 20px;
  348. }
  349. /* 顶部 */
  350. .top-bar {
  351. height: 210px;
  352. .library-info {
  353. position: relative;
  354. z-index: 2;
  355. color: #fff;
  356. }
  357. }
  358. /* 表单卡片 */
  359. .form-card {
  360. margin: -30px 12px 12px;
  361. background: #fff;
  362. border-radius: 12px;
  363. padding: 20px 16px;
  364. box-shadow: 0 4px 15px rgba(0,0,0,0.08);
  365. position: relative;
  366. z-index: 3;
  367. }
  368. .form-item {
  369. display: flex;
  370. align-items: center;
  371. height: 44px;
  372. border-bottom: 1rpx solid #f2f3f5;
  373. margin-bottom: 4px;
  374. .form-icon {
  375. font-size: 14px;
  376. color: #01a4fe;
  377. margin-right: 10px;
  378. }
  379. .input {
  380. flex: 1;
  381. font-size: 14px;
  382. color: #333;
  383. }
  384. .pwd-icon {
  385. color: #999;
  386. padding: 5px;
  387. }
  388. .get-phone-btn {
  389. font-size: 12px;
  390. color: #fff;
  391. background-color: #01a4fe;
  392. border-radius: 20px;
  393. padding: 6px 10px;
  394. border: none;
  395. line-height: 1.2;
  396. &::after {
  397. border: none;
  398. }
  399. }
  400. }
  401. /* 提交按钮 */
  402. .submit-btn {
  403. width: 100%;
  404. height: 44px;
  405. background-color: #01a4fe;
  406. color: #fff;
  407. border-radius: 22px;
  408. font-size: 15px;
  409. margin-top: 20px;
  410. border: none;
  411. &::after {
  412. border: none;
  413. }
  414. }
  415. /* 同意协议 */
  416. .agreement-item {
  417. display: flex;
  418. align-items: center;
  419. margin-top: 16px;
  420. .radio-box {
  421. width: 22rpx;
  422. height: 22rpx;
  423. border: 2rpx solid #ccc;
  424. border-radius: 50%;
  425. margin-right: 10rpx;
  426. display: flex;
  427. align-items: center;
  428. justify-content: center;
  429. transition: all 0.2s;
  430. &.checked {
  431. border-color: #01a4fe;
  432. background-color: #01a4fe;
  433. }
  434. .radio-inner {
  435. width: 12rpx;
  436. height: 12rpx;
  437. background-color: #fff;
  438. border-radius: 50%;
  439. }
  440. }
  441. .agreement-text {
  442. font-size: 12px;
  443. color: #666;
  444. flex: 1;
  445. }
  446. .agreement-link {
  447. color: #01a4fe;
  448. }
  449. }
  450. /* 温馨提示 */
  451. .tips-card {
  452. margin: 0 12px;
  453. background: #fff;
  454. border-radius: 10px;
  455. padding: 15px;
  456. box-shadow: 0 2px 10px rgba(0,0,0,0.05);
  457. .tips-title {
  458. font-size: 13px;
  459. color: #333;
  460. font-weight: 500;
  461. margin-bottom: 8px;
  462. }
  463. .tips-text {
  464. font-size: 12px;
  465. color: #666;
  466. line-height: 1.6;
  467. }
  468. }
  469. </style>