|
|
@@ -1,232 +1,52 @@
|
|
|
<script setup lang="ts">
|
|
|
import NavHomeSelect from '@/assets/images/nav-home.select.png?url';
|
|
|
-import { Toast } from '@/platform';
|
|
|
|
|
|
-import { getCaptchaMethod, registerAccountMethod, registerFieldsMethod, dictionariesMethod, searchAccountMethod } from '@/request/api';
|
|
|
-import type { Field, Fields, Option, RegisterModel } from '@/request/model';
|
|
|
+import type RegisterForm from '@/components/RegisterForm.vue';
|
|
|
+import type { RegisterModel } from '@/request/model';
|
|
|
+
|
|
|
import { useRouteQuery } from '@vueuse/router';
|
|
|
-import { useCaptcha, useRequest, useSerialRequest } from 'alova/client';
|
|
|
+import { useRequest } from 'alova/client';
|
|
|
+import { getApplicationMethod, registerAccountMethod } from '@/request/api';
|
|
|
|
|
|
-import type { FormInstance } from 'vant';
|
|
|
-import { RadioGroup as vanRadioGroup } from 'vant';
|
|
|
-import PickerDialog from '@/components/PickerDialog.vue';
|
|
|
-import CascaderDialog from '@/components/CascaderDialog.vue';
|
|
|
import { useFlowStore, useVisitor } from '@/stores';
|
|
|
|
|
|
-const formRef = useTemplateRef<FormInstance>('register-form');
|
|
|
-const modelLabel = ref<Partial<RegisterModel>>({ });
|
|
|
-const modelValue = ref<Partial<RegisterModel>>({ code: '' });
|
|
|
-const model = computed(() => ({ ...modelLabel.value, ...modelValue.value }));
|
|
|
|
|
|
-const { data: fields, loading } = useSerialRequest([dictionariesMethod, (dictionaries) => registerFieldsMethod(dictionaries)]).onSuccess(({ data }) => {
|
|
|
- const sex = data.find((field) => field.name === 'sex');
|
|
|
- if (sex) {
|
|
|
- const unknown = (<any>sex).component?.options?.find((option: any) => option.value === '2');
|
|
|
- modelLabel.value.sex = unknown?.label;
|
|
|
- modelValue.value.sex = unknown?.value;
|
|
|
- }
|
|
|
-});
|
|
|
+useRequest(getApplicationMethod, { initialData: { image: {} } });
|
|
|
|
|
|
const flow = useFlowStore();
|
|
|
const visitor = useVisitor();
|
|
|
-const { loading: submitting, send: submit } = useRequest(registerAccountMethod, { immediate: false }).onSuccess(({ data }) => {
|
|
|
- visitor.updatePatient(modelLabel.value, data);
|
|
|
- flow.router.push();
|
|
|
-});
|
|
|
+const { loading: submitting, send: submit } = useRequest(registerAccountMethod, { immediate: false });
|
|
|
|
|
|
-const forbiddenFields = shallowRef<Record<string, boolean>>({});
|
|
|
-const { loading: searching, send: search } = useRequest((data) => searchAccountMethod(data), {
|
|
|
- immediate: false,
|
|
|
-}).onSuccess(({ data }) => {
|
|
|
- if (!fields.value.some((field) => field.name === 'phone')) Reflect.deleteProperty(data, 'phone');
|
|
|
- setValues(data);
|
|
|
-});
|
|
|
+const formRef = ref<InstanceType<typeof RegisterForm> | null>(null);
|
|
|
+async function onSubmitFromForm(payload: { model: RegisterModel; modelLabel: Partial<RegisterModel> }) {
|
|
|
+ const patientId = await submit(payload.model);
|
|
|
+ visitor.updatePatient(payload.modelLabel, patientId);
|
|
|
+ flow.router.push();
|
|
|
+}
|
|
|
|
|
|
-let captchaLoaded = false;
|
|
|
-const { loading: captchaLoading, countdown, send: getCaptcha } = useCaptcha(
|
|
|
- () => getCaptchaMethod(model.value.phone!),
|
|
|
- { initialCountdown: 60 },
|
|
|
-).onSuccess(({ data }) => {
|
|
|
- captchaLoaded = true;
|
|
|
- Toast.success(data ?? '获取成功');
|
|
|
-});
|
|
|
-const getCaptchaHandle = async () => {
|
|
|
- try {
|
|
|
- await formRef.value?.validate('phone');
|
|
|
- if ( !model.value.phone ) throw { message: `请输入手机号码` };
|
|
|
- await getCaptcha();
|
|
|
- const field = fields.value.find(field => field.name === 'code');
|
|
|
- if ( field?.keyboard ) { field.keyboard.show = true; }
|
|
|
- } catch ( e: any ) {
|
|
|
- Toast.warning(e?.message);
|
|
|
- }
|
|
|
-};
|
|
|
+const scan = useRouteQuery<string>('scan');
|
|
|
|
|
|
-const searchHandle = async (key: 'cardno' | 'code') => {
|
|
|
- const forbidden = { cardno: 'phone', code: 'cardno' }[key];
|
|
|
+function applyScanToForm() {
|
|
|
+ const key = scan.value;
|
|
|
+ if (!key) return;
|
|
|
try {
|
|
|
- await formRef.value?.validate(key);
|
|
|
- forbiddenFields.value = {};
|
|
|
- const { cardno, phone, code } = model.value;
|
|
|
- await search({ cardno, phone, code })
|
|
|
- .then((data) => {
|
|
|
- forbiddenFields.value[forbidden] = !!(data as any)[forbidden];
|
|
|
- triggerRef(forbiddenFields);
|
|
|
- })
|
|
|
- .catch();
|
|
|
- } catch (e: any) {
|
|
|
- Toast.warning(e?.message);
|
|
|
- }
|
|
|
-};
|
|
|
-
|
|
|
-function onKeyboardBlur(field: Fields[number]) {
|
|
|
- if ( field?.name === 'phone' && !captchaLoaded ) { getCaptchaHandle(); }
|
|
|
- if ( field?.name === 'cardno' && model.value.cardno ) { searchHandle('cardno'); }
|
|
|
- if ( field?.name === 'code' && model.value.phone ) { searchHandle('code'); }
|
|
|
-}
|
|
|
-
|
|
|
-function onSubmitHandle() {
|
|
|
- submit(model.value);
|
|
|
+ const { model } = JSON.parse(sessionStorage.getItem(`scan_${key}`) ?? '');
|
|
|
+ nextTick(() => formRef.value?.setValues(model));
|
|
|
+ } catch (_e: any) {}
|
|
|
}
|
|
|
|
|
|
-function fix(key: string) {
|
|
|
- for ( const field of fields.value ) {
|
|
|
- if (field.keyboard?.show && field.name !== key ) field.keyboard.show = false;
|
|
|
- }
|
|
|
-}
|
|
|
+watch(scan, applyScanToForm, { immediate: true, flush: 'post' });
|
|
|
|
|
|
-const scan = useRouteQuery<string>('scan');
|
|
|
-watch(scan, key => {
|
|
|
- if ( key ) {
|
|
|
- try {
|
|
|
- const { model } = JSON.parse(sessionStorage.getItem(`scan_${ key }`) ?? '');
|
|
|
- setValues(model);
|
|
|
- } catch ( e: any ) {}
|
|
|
- }
|
|
|
-}, { immediate: true });
|
|
|
+watch(formRef, (inst) => {
|
|
|
+ if (inst && scan.value) applyScanToForm();
|
|
|
+}, { flush: 'post' });
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
- for ( let i = 0; i < sessionStorage.length; i++ ) {
|
|
|
+ for (let i = 0; i < sessionStorage.length; i++) {
|
|
|
const key = sessionStorage.key(i);
|
|
|
- if ( key?.startsWith('scan_') ) sessionStorage.removeItem(key);
|
|
|
+ if (key?.startsWith('scan_')) sessionStorage.removeItem(key);
|
|
|
}
|
|
|
});
|
|
|
-
|
|
|
-const keyboardProps = reactive({
|
|
|
- key: '',
|
|
|
- props: {},
|
|
|
- show: false,
|
|
|
-});
|
|
|
-const pickerProps = reactive({
|
|
|
- key: '',
|
|
|
- props: { options: [], selected: [] },
|
|
|
- show: false,
|
|
|
-});
|
|
|
-const cascaderProps = reactive({
|
|
|
- key: '',
|
|
|
- props: { options: [], loading: false },
|
|
|
- show: false,
|
|
|
-});
|
|
|
-
|
|
|
-const handle = (value: any, field: Field | string) => {
|
|
|
- field = ((key: Field | string): Field => (typeof key === 'string' ? fields.value.find((field) => field.name === key)! : key))(field);
|
|
|
- const key = field.name!;
|
|
|
- if (field.component?.name === 'radio') {
|
|
|
- const option = field?.component?.options?.find((option) => option.value === value);
|
|
|
- (modelLabel.value as any)[key] = option?.label;
|
|
|
- (modelValue.value as any)[key] = option?.value;
|
|
|
- } else if (field.component?.name === 'picker') {
|
|
|
- if (typeof value === 'string') {
|
|
|
- value = value.split(',').map((value) => {
|
|
|
- const [v, l] = value.split(':');
|
|
|
- return { value, label: l ?? (field.component as any).options?.find((option: Option) => option.value === v)?.label ?? v };
|
|
|
- });
|
|
|
- }
|
|
|
- (modelLabel.value as any)[key] = value.map((option: Option) => option.label).join(',');
|
|
|
- (modelValue.value as any)[key] = value.map((option: Option) => option.value).join(',');
|
|
|
- } else if (field.component?.name === 'cascader') {
|
|
|
- (modelLabel.value as any)[key] = value.map((option: Option) => option.label).join('/');
|
|
|
- (modelValue.value as any)[key] = value;
|
|
|
- } else if (field.component?.name === 'code') {
|
|
|
- (modelValue.value as any)[key] = value;
|
|
|
- } else {
|
|
|
- (modelLabel.value as any)[key] = value;
|
|
|
- (modelValue.value as any)[key] = value;
|
|
|
- }
|
|
|
-};
|
|
|
-const setValues = (values: Record<string, any>) => {
|
|
|
- forbiddenFields.value = {};
|
|
|
- if (!values) {
|
|
|
- modelLabel.value = {};
|
|
|
- modelValue.value = {};
|
|
|
- }
|
|
|
- for (const [key, value] of Object.entries(values ?? {})) {
|
|
|
- const field = fields.value?.find((field) => field.name === key);
|
|
|
- if (field) {
|
|
|
- forbiddenFields.value[key] = !!value && !['phone', 'cardno'].includes(key);
|
|
|
- handle(value, field as any)
|
|
|
- } else {
|
|
|
- (modelValue as any).value[key] = value;
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-function onFieldFocus(field: any) {
|
|
|
- if (forbiddenFields.value[field.name]) return;
|
|
|
- if (field.keyboard) {
|
|
|
- keyboardProps.key = field.name;
|
|
|
- keyboardProps.show = true;
|
|
|
- keyboardProps.props = {
|
|
|
- ...field.keyboard,
|
|
|
- maxlength: field.control?.maxlength ?? Number.POSITIVE_INFINITY,
|
|
|
- onBlur() {
|
|
|
- keyboardProps.show = false;
|
|
|
- onKeyboardBlur(field);
|
|
|
- },
|
|
|
- }
|
|
|
- } else if (field.component?.name === 'picker') {
|
|
|
- pickerProps.key = field.name;
|
|
|
- pickerProps.show = true;
|
|
|
- pickerProps.props = {
|
|
|
- ...field.component.props,
|
|
|
- title: field.control.label,
|
|
|
- options: field.component.options,
|
|
|
- selected: (modelValue.value as Record<string, string>)[field.name]?.split(','),
|
|
|
- };
|
|
|
- } else if (field.component?.name === 'cascader') {
|
|
|
- cascaderProps.key = field.name;
|
|
|
- cascaderProps.show = true;
|
|
|
- if (typeof field.component.options === 'function') {
|
|
|
- cascaderProps.props = {
|
|
|
- ...field.component.props,
|
|
|
- title: field.control.label,
|
|
|
- options: [],
|
|
|
- loading: true,
|
|
|
- };
|
|
|
- (async function () {
|
|
|
- field.component.options = await field.component.options();
|
|
|
- cascaderProps.props = {
|
|
|
- ...field.component.props,
|
|
|
- title: field.control.label,
|
|
|
- options: field.component.options,
|
|
|
- loading: false,
|
|
|
- selected: (modelValue.value as Record<string, { value: string }[]>)[field.name]?.map((option) => option.value),
|
|
|
- };
|
|
|
- })();
|
|
|
- } else {
|
|
|
- cascaderProps.props = {
|
|
|
- ...field.component.props,
|
|
|
- title: field.control.label,
|
|
|
- options: field.component.options,
|
|
|
- };
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-function onFieldBlur(field: any) {
|
|
|
- keyboardProps.show = false;
|
|
|
- pickerProps.show = false;
|
|
|
- cascaderProps.show = false;
|
|
|
-}
|
|
|
</script>
|
|
|
<template>
|
|
|
<div>
|
|
|
@@ -234,160 +54,23 @@ function onFieldBlur(field: any) {
|
|
|
<div class="grow shrink-0 h-full min-w-16"></div>
|
|
|
<div class="grow-[3] shrink mx-2 flex flex-col justify-center overflow-hidden">
|
|
|
<div class="flex justify-center font-bold text-3xl text-nowrap text-center tracking-wide overflow-ellipsis overflow-hidden">
|
|
|
- 建档 <van-loading v-if="searching" style="margin-left: 4px; color: #38ff6e;"></van-loading>
|
|
|
+ 建档 <van-loading v-if="formRef?.searching" style="margin-left: 4px; color: #38ff6e"></van-loading>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="grow shrink-0 h-full min-w-16 flex items-center justify-end overflow-hidden">
|
|
|
<router-link :to="{ path: '/screen' }" replace>
|
|
|
- <img class="size-8 object-scale-down" :src="NavHomeSelect" alt="返回首页">
|
|
|
+ <img class="size-8 object-scale-down" :src="NavHomeSelect" alt="返回首页" />
|
|
|
</router-link>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="page-content p-6 overflow-auto">
|
|
|
- <van-form class="register-form" ref="register-form" colon required="auto" scroll-to-error scroll-to-error-position="center" @submit="onSubmitHandle()">
|
|
|
- <van-cell-group :border="false">
|
|
|
- <template v-for="field in fields" :key="field.name">
|
|
|
- <template v-if="!field.control?.hide || (typeof field.control?.hide === 'function' && !field.control.hide(model))">
|
|
|
- <van-field
|
|
|
- :model-value="field.control?.readonly ? modelLabel[field.name] : model[field.name]" @update:model-value="handle($event, field)"
|
|
|
- :name="field.name" :id="field.name"
|
|
|
- :rules="field.rules" v-bind="field.control"
|
|
|
- :class="{'no-border': field.control?.border === false}"
|
|
|
- @focus="onFieldFocus(field)" @blur="onFieldBlur(field)"
|
|
|
- :readonly="field.control?.readonly" @click="onFieldFocus(field)"
|
|
|
- :disabled="forbiddenFields[field.name]"
|
|
|
- >
|
|
|
- <template #input v-if="field.component?.name === 'radio'">
|
|
|
- <van-radio-group direction="horizontal" shape="dot" :disabled="forbiddenFields[field.name]" v-model="modelValue[field.name]" @change="handle($event, field)">
|
|
|
- <van-radio v-for="option in field.component?.options" :key="option.value" :name="option.value">
|
|
|
- {{ option.label }}
|
|
|
- </van-radio>
|
|
|
- </van-radio-group>
|
|
|
- </template>
|
|
|
- <template #input v-else-if="field.component?.name === 'code'">
|
|
|
- <van-password-input
|
|
|
- style="width: 100%;"
|
|
|
- v-model:value="modelValue[field.name]" v-bind="(field.component as any)!.props"
|
|
|
- :focused="field.keyboard?.show" @focus="field.keyboard && (field.keyboard.show = true);fix('code')"
|
|
|
- />
|
|
|
- </template>
|
|
|
- <template #button>
|
|
|
- <div class="text-primary cursor-pointer">
|
|
|
- <template v-if="field.component?.name === 'code'">
|
|
|
- <div class="text-primary cursor-pointer" @click="getCaptchaHandle()">
|
|
|
- {{ captchaLoading ? '发送中...' : countdown > 0 ? `${ countdown }后可重发` : '获取验证码' }}
|
|
|
- </div>
|
|
|
- </template>
|
|
|
- <template v-else>{{ field.suffix }}</template>
|
|
|
- </div>
|
|
|
- </template>
|
|
|
- </van-field>
|
|
|
- </template>
|
|
|
- </template>
|
|
|
- </van-cell-group>
|
|
|
- </van-form>
|
|
|
+ <RegisterForm ref="formRef" :search-forbidden-field="!extra_hack" @submit="onSubmitFromForm" />
|
|
|
<div class="m-4">
|
|
|
<div class="m-auto size-16 cursor-pointer">
|
|
|
- <van-loading v-if="submitting || loading" type="spinner" size="64" color="#38ff6e" />
|
|
|
- <img v-else class="size-full" src="@/assets/images/next-step.svg" alt="提交" @click="formRef?.submit()">
|
|
|
+ <van-loading v-if="submitting || formRef?.loading" type="spinner" size="64" color="#38ff6e" />
|
|
|
+ <img v-else class="size-full" src="@/assets/images/next-step.svg" alt="提交" @click="formRef?.submit()" />
|
|
|
</div>
|
|
|
</div>
|
|
|
- <van-number-keyboard v-bind="keyboardProps.props" :show="keyboardProps.show" v-model="modelValue[keyboardProps.key]" @update:model-value="handle($event, keyboardProps.key)"></van-number-keyboard>
|
|
|
- <PickerDialog v-bind="pickerProps.props" v-model:show="pickerProps.show" @selected="handle($event, pickerProps.key)"></PickerDialog>
|
|
|
- <CascaderDialog v-bind="cascaderProps.props" v-model:show="cascaderProps.show" @selected="handle($event, cascaderProps.key)"></CascaderDialog>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
-<style scoped lang="scss">
|
|
|
-.register-form {
|
|
|
- .van-field {
|
|
|
- margin: 0;
|
|
|
- padding: 0;
|
|
|
- }
|
|
|
-
|
|
|
- .van-field.no-border {
|
|
|
- :deep(.van-field__control) {
|
|
|
- padding: 0;
|
|
|
- border: none;
|
|
|
- border-radius: 8px;
|
|
|
- text-align: center;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- :deep(.van-field--disabled) {
|
|
|
- --tw-text-opacity: 0.5;
|
|
|
- --van-radio-checked-icon-color: rgba(56, 255, 110, var(--tw-text-opacity, 1));
|
|
|
- .text-primary {
|
|
|
- --tw-text-opacity: 0.5;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- :deep(.van-field__label) {
|
|
|
- margin-bottom: 24px;
|
|
|
- padding: 8px 0;
|
|
|
- min-width: 100px;
|
|
|
- font-size: 18px;
|
|
|
- }
|
|
|
-
|
|
|
- :deep(.van-field__control) {
|
|
|
- margin-bottom: 24px;
|
|
|
- padding: 8px;
|
|
|
- border: 1px solid var(--van-radio-checked-icon-color);
|
|
|
- border-radius: 8px;
|
|
|
- text-align: center;
|
|
|
- }
|
|
|
-
|
|
|
- :deep(.van-field__clear) {
|
|
|
- align-self: flex-start;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- height: 42px;
|
|
|
- }
|
|
|
-
|
|
|
- :deep(.van-field__button) {
|
|
|
- margin-bottom: 24px;
|
|
|
- padding: 8px var(--van-padding-xs);
|
|
|
- min-width: 100px;
|
|
|
- font-size: 18px;
|
|
|
- text-align: left;
|
|
|
- }
|
|
|
-
|
|
|
- :deep(.van-field__error-message) {
|
|
|
- position: absolute;
|
|
|
- top: 40px + 2px;
|
|
|
- }
|
|
|
-
|
|
|
- :deep(.van-password-input) {
|
|
|
- margin: 0;
|
|
|
- }
|
|
|
-
|
|
|
- :deep(.van-password-input__security) {
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
- text-align: center;
|
|
|
- $size: 40px;
|
|
|
- height: $size + 2px;
|
|
|
-
|
|
|
- &::after {
|
|
|
- display: none;
|
|
|
- }
|
|
|
-
|
|
|
- li {
|
|
|
- height: $size;
|
|
|
- width: $size;
|
|
|
- flex: none;
|
|
|
- border: 1px solid #38ff6e;
|
|
|
- border-radius: 8px;
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-.van-radio-group {
|
|
|
- height: 40px + 2px;
|
|
|
-}
|
|
|
-
|
|
|
-.sub-option.checked {
|
|
|
- color: #fff;
|
|
|
- background-color: var(--primary-color);
|
|
|
-}
|
|
|
-</style>
|