register.page.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. <script setup lang="ts">
  2. import { Notify, Toast } from '@/platform';
  3. import {
  4. type FieldKey,
  5. getCaptchaMethod,
  6. processMethod,
  7. registerAccountMethod,
  8. registerFieldsMethod,
  9. scanAccountMethod, searchAccountMethod,
  10. } from '@/request/api';
  11. import { useVisitor } from '@/stores';
  12. import { useRouteQuery } from '@vueuse/router';
  13. import { useCaptcha, useForm, useRequest, useWatcher } from 'alova/client';
  14. import type { FieldRule, FormInstance, NumberKeyboardProps, PasswordInputProps } from 'vant';
  15. import { RadioGroup as vanRadioGroup } from 'vant';
  16. import NavHomeSelect from '@/assets/images/nav-home.select.png?url';
  17. interface Field {
  18. control: {
  19. label: string; placeholder?: string;
  20. type?: string; min?: number; max?: number; minlength?: number; maxlength?: number;
  21. clearable?: boolean; border?: boolean; readonly?:boolean;
  22. };
  23. component?: |
  24. { name: 'radio', options: { label: string; value: string; }[] } |
  25. { name: 'code', props?: Partial<PasswordInputProps> };
  26. keyboard?: { show: boolean; } & Partial<NumberKeyboardProps>;
  27. suffix?: string;
  28. rules?: FieldRule[];
  29. }
  30. const Fields: Record<FieldKey, Field> = {
  31. height: {
  32. control: {
  33. label: '身高', placeholder: '请输入身高',
  34. type: 'number', min: 1, max: 300, clearable: true, readonly: true,
  35. maxlength: 5,
  36. },
  37. keyboard: { show: false, title: '身高', extraKey:'.', closeButtonText: '完成' },
  38. suffix: 'cm',
  39. },
  40. weight: {
  41. control: {
  42. label: '体重', placeholder: '请输入体重',
  43. type: 'number', min: 1, max: 300, clearable: true, readonly: true,
  44. maxlength: 5,
  45. },
  46. keyboard: { show: false, title: '体重', extraKey:'.', closeButtonText: '完成' },
  47. suffix: 'kg',
  48. },
  49. age: {
  50. control: {
  51. label: '年龄', placeholder: '请输入年龄',
  52. type: 'digit', min: 0, max: 300, clearable: true, readonly: true,
  53. maxlength: 3,
  54. },
  55. keyboard: { show: false, title: '年龄', closeButtonText: '完成' },
  56. suffix: '岁',
  57. },
  58. sex: {
  59. control: { label: '性别', border: false },
  60. component: {
  61. name: 'radio' as const,
  62. options: [
  63. { label: '男', value: '0' },
  64. { label: '女', value: '1' },
  65. ],
  66. },
  67. },
  68. isEasyAllergy: {
  69. control: { label: '容易过敏', border: false },
  70. component: {
  71. name: 'radio' as const,
  72. options: [
  73. { label: '是', value: 'Y' },
  74. { label: '否', value: 'N' },
  75. ],
  76. },
  77. },
  78. name: {
  79. control: {
  80. label: '姓名', placeholder: '请输入姓名',
  81. type: 'text', maxlength: 10, clearable: true,
  82. },
  83. },
  84. cardno: {
  85. control: {
  86. label: '身份证号', placeholder: '请输入身份证号',
  87. type: 'text', maxlength: 18, minlength: 18, clearable: true, readonly: true,
  88. },
  89. keyboard: { show: false, title: '身份证号', extraKey: 'X', closeButtonText: '完成' },
  90. rules: [
  91. { required: true, message: '请输入身份证号' },
  92. {
  93. validator: (value: string) => value && value.length === 18,
  94. message: '请输入正确的身份证',
  95. trigger: 'onBlur',
  96. },
  97. ],
  98. },
  99. phone: {
  100. control: {
  101. label: '手机号码', placeholder: '请输入手机号码',
  102. type: 'tel', maxlength: 11, minlength: 11, clearable: true, readonly: true,
  103. },
  104. keyboard: { show: false, title: '手机号码', closeButtonText: '完成' },
  105. rules: [
  106. { required: true, message: '请输入手机号码' },
  107. {
  108. validator: (value: string) => value && value.length === 11,
  109. message: '请输入正确的手机号码',
  110. trigger: 'onBlur',
  111. },
  112. ],
  113. },
  114. code: {
  115. control: {
  116. label: '验证码', placeholder: '请输入验证码',
  117. type: 'digit', maxlength: 6, minlength: 6, clearable: true,
  118. border: false,
  119. },
  120. component: {
  121. name: 'code' as const,
  122. props: { mask: false },
  123. },
  124. keyboard: { show: false, title: '验证码', closeButtonText: '完成' },
  125. rules: [
  126. { required: true, message: '请输入验证码' },
  127. {
  128. validator: (value: string) => value && value.length === 6,
  129. message: '请输入验证码',
  130. trigger: [ 'onChange', 'onBlur' ],
  131. },
  132. ],
  133. },
  134. };
  135. const fields = ref<( Field & { name: FieldKey } )[]>([]);
  136. const { loading } = useRequest(registerFieldsMethod).onSuccess(({ data }) => {
  137. fields.value = data.map(name => {return { ...Fields[ name ], name };});
  138. });
  139. const Visitor = useVisitor();
  140. const formRef = useTemplateRef<FormInstance>('register-form');
  141. const { form: modelRef, loading: submitting, send: submit } = useForm(data => registerAccountMethod(data), {
  142. initialForm: { code: '' } as Record<string, any>,
  143. }).onSuccess(async ({ data }) => {
  144. Visitor.patientId = data;
  145. Toast.success(`操作成功`);
  146. try {
  147. submitting.value = true;
  148. await handle();
  149. } finally {
  150. submitting.value = false;
  151. }
  152. });
  153. const { loading: searching, send: search } = useRequest((data) => searchAccountMethod(data), {
  154. immediate: false,
  155. }).onSuccess(({ data }) => {
  156. modelRef.value = { ...modelRef.value, ...data };
  157. });
  158. let captchaLoaded = false;
  159. const { loading: captchaLoading, countdown, send: getCaptcha } = useCaptcha(
  160. () => getCaptchaMethod(modelRef.value.phone!),
  161. { initialCountdown: 60 },
  162. ).onSuccess(({ data }) => {
  163. captchaLoaded = true;
  164. Toast.success(data ?? '获取成功')
  165. });
  166. const getCaptchaHandle = async () => {
  167. try {
  168. await formRef.value?.validate('phone');
  169. await getCaptcha();
  170. const field = fields.value.find(field => field.name === 'code');
  171. if ( field?.keyboard ) { field.keyboard.show = true; }
  172. } catch ( e: any ) {
  173. Toast.warning(e?.message);
  174. }
  175. };
  176. const searchHandle = async (key: 'cardno' | 'code') => {
  177. try {
  178. await formRef.value?.validate(key);
  179. await search(modelRef.value).catch();
  180. } catch ( e: any ) {
  181. Toast.warning(e?.message);
  182. }
  183. };
  184. function onKeyboardBlur(field: Field & { name: FieldKey }) {
  185. if ( field?.name === 'phone' && !captchaLoaded ) { getCaptchaHandle(); }
  186. if ( field?.name === 'cardno' ) { searchHandle('cardno'); }
  187. if ( field?.name === 'code' ) { searchHandle('code'); }
  188. }
  189. function onSubmitHandle() {
  190. submit(toValue(modelRef));
  191. }
  192. const router = useRouter();
  193. const { send: handle } = useRequest(
  194. () => processMethod('/register'),
  195. { immediate: false },
  196. ).onSuccess(
  197. ({ data }) => {
  198. if ( data ) {
  199. router.replace(data);
  200. } else {
  201. Notify.warning(`[路由] 配置异常无法解析正确路径,请联系管理员`);
  202. }
  203. });
  204. function fix(key: FieldKey) {
  205. for ( const field of fields.value ) {
  206. if (field.keyboard?.show && field.name !== key ) field.keyboard.show = false;
  207. }
  208. }
  209. const scan = useRouteQuery<string>('scan');
  210. useWatcher(
  211. () => scanAccountMethod(sessionStorage.getItem(`scan_${ scan.value }`)!),
  212. [ scan ],
  213. {
  214. immediate: true,
  215. async middleware(_, next) {
  216. if ( scan.value ) {
  217. let scanToastRef: any;
  218. try {
  219. scanToastRef = Toast.loading(100, { message: '加载中' });
  220. await next();
  221. } catch ( error ) {} finally {
  222. scanToastRef?.close?.();
  223. }
  224. }
  225. },
  226. },
  227. ).onSuccess(({ data }) => {
  228. Toast.success('扫码成功');
  229. modelRef.value = { ...modelRef.value, ...data };
  230. });
  231. onBeforeUnmount(() => {
  232. for ( let i = 0; i < sessionStorage.length; i++ ) {
  233. const key = sessionStorage.key(i);
  234. if ( key?.startsWith('scan_') ) sessionStorage.removeItem(key);
  235. }
  236. });
  237. </script>
  238. <template>
  239. <div>
  240. <div class="page-header flex py-4 px-4">
  241. <div class="grow shrink-0 h-full min-w-16"></div>
  242. <div class="grow-[3] shrink mx-2 flex flex-col justify-center overflow-hidden">
  243. <div class="font-bold text-3xl text-nowrap text-center tracking-wide overflow-ellipsis overflow-hidden">
  244. 建档
  245. </div>
  246. </div>
  247. <div class="grow shrink-0 h-full min-w-16 flex items-center justify-end overflow-hidden">
  248. <router-link :to="{ path: '/screen' }" replace>
  249. <img class="size-8 object-scale-down" :src="NavHomeSelect" alt="返回首页">
  250. </router-link>
  251. </div>
  252. </div>
  253. <div class="page-content p-6">
  254. <van-form class="register-form" ref="register-form" colon required="auto"
  255. scroll-to-error scroll-to-error-position="center"
  256. @submit="onSubmitHandle()"
  257. >
  258. <van-cell-group :border="false">
  259. <template v-for="field in fields" :key="field.name">
  260. <van-field v-model="modelRef[field.name]" :name="field.name" :id="field.name"
  261. :rules="field.rules" v-bind="field.control"
  262. :class="{'no-border': field.control?.border === false}"
  263. :focused="field.keyboard?.show" @focus="field.keyboard && (field.keyboard.show = true)"
  264. @blur="field.keyboard && (field.keyboard.show = false)"
  265. :readonly="field.control.readonly" @click="field.keyboard && (field.keyboard.show = true)"
  266. >
  267. <template #input v-if="field.component?.name === 'radio'">
  268. <van-radio-group v-model="modelRef[field.name]" direction="horizontal" shape="dot">
  269. <van-radio v-for="option in field.component?.options" :key="option.value" :name="option.value">
  270. {{ option.label }}
  271. </van-radio>
  272. </van-radio-group>
  273. </template>
  274. <template #input v-else-if="field.component?.name === 'code'">
  275. <van-password-input
  276. style="width: 100%;"
  277. v-model:value="modelRef[field.name]" v-bind="(field.component as any)!.props"
  278. :focused="field.keyboard?.show" @focus="field.keyboard && (field.keyboard.show = true);fix('code')"
  279. />
  280. </template>
  281. <template #button>
  282. <div class="text-primary cursor-pointer">
  283. <template v-if="field.component?.name === 'code'">
  284. <div class="text-primary cursor-pointer" @click="getCaptchaHandle()">
  285. {{ captchaLoading ? '发送中...' : countdown > 0 ? `${ countdown }后可重发` : '获取验证码' }}
  286. </div>
  287. </template>
  288. <template v-else>{{ field.suffix }}</template>
  289. </div>
  290. </template>
  291. </van-field>
  292. <van-number-keyboard
  293. v-if="field.keyboard"
  294. v-model="modelRef[field.name]"
  295. v-bind="field.keyboard" :maxlength="field.control.maxlength"
  296. @blur="field.keyboard.show = false; onKeyboardBlur(field)"
  297. />
  298. </template>
  299. </van-cell-group>
  300. </van-form>
  301. <div class="m-4">
  302. <div class="m-auto size-16 cursor-pointer">
  303. <van-loading v-if="submitting" type="spinner" size="64" color="#38ff6e" />
  304. <img v-else class="size-full"
  305. src="@/assets/images/next-step.svg" alt="提交" @click="formRef?.submit()"
  306. >
  307. </div>
  308. </div>
  309. </div>
  310. </div>
  311. </template>
  312. <style scoped lang="scss">
  313. .register-form {
  314. .van-field {
  315. margin: 0;
  316. padding: 0;
  317. }
  318. .van-field.no-border {
  319. :deep(.van-field__control) {
  320. padding: 0;
  321. border: none;
  322. border-radius: 8px;
  323. text-align: center;
  324. }
  325. }
  326. :deep(.van-field__label) {
  327. margin-bottom: 24px;
  328. padding: 8px 0;
  329. min-width: 100px;
  330. font-size: 18px;
  331. }
  332. :deep(.van-field__control) {
  333. margin-bottom: 24px;
  334. padding: 8px;
  335. border: 1px solid #38ff6e;
  336. border-radius: 8px;
  337. text-align: center;
  338. }
  339. :deep(.van-field__clear) {
  340. align-self: flex-start;
  341. display: flex;
  342. align-items: center;
  343. height: 42px;
  344. }
  345. :deep(.van-field__button) {
  346. margin-bottom: 24px;
  347. padding: 8px var(--van-padding-xs);
  348. min-width: 100px;
  349. font-size: 18px;
  350. text-align: left;
  351. }
  352. :deep(.van-field__error-message) {
  353. position: absolute;
  354. top: 40px + 2px;
  355. }
  356. :deep(.van-password-input) {
  357. margin: 0;
  358. }
  359. :deep(.van-password-input__security) {
  360. justify-content: space-between;
  361. align-items: center;
  362. text-align: center;
  363. $size: 40px;
  364. height: $size + 2px;
  365. &::after {
  366. display: none;
  367. }
  368. li {
  369. height: $size;
  370. width: $size;
  371. flex: none;
  372. border: 1px solid #38ff6e;
  373. border-radius: 8px;
  374. }
  375. }
  376. }
  377. .van-radio-group {
  378. height: 40px + 2px;
  379. }
  380. </style>