| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380 |
- <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 { CascaderOption, Fields, Option, RegisterModel } from '@/request/model';
- import { useRouteQuery } from '@vueuse/router';
- import { useCaptcha, useRequest, useSerialRequest } from 'alova/client';
- 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 } from '@/stores';
- const formRef = useTemplateRef<FormInstance>('register-form');
- const modelRef = ref<Partial<RegisterModel>>({ code: '' });
- const modelValueRef = ref<Partial<RegisterModel>>({});
- const model = computed(() => ({ ...modelRef.value, ...modelValueRef.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');
- modelRef.value.sex = unknown?.value;
- }
- });
- const flow = useFlowStore();
- const { loading: submitting, send: submit } = useRequest(registerAccountMethod, { immediate: false }).onSuccess(({ data }) => {
- flow.router.push();
- });
- 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');
- const modelLabel = {} as Record<string, any>;
- const modelValue = {} as Record<string, any>;
- const forbidden = {} as Record<string, boolean>;
- for (const [key, value] of Object.entries(data)) {
- const field = fields.value?.find((field) => field.name === key);
- if (field) forbidden[key] = !!value && !['phone', 'cardno'].includes(key);
- if (typeof value === 'string' && field?.component?.name === 'picker') {
- const result = value.split(',').map((value) => {
- const [v, l] = value.split(':');
- return { value, label: l ?? (field.component as { options: Option[] })?.options?.find((option) => option.value === v)?.label ?? v };
- });
- modelValue[key] = result.map((t) => t.value).join(',');
- modelLabel[key] = result.map((t) => t.label).join(',');
- } else if (typeof value === 'object' && field?.component?.name === 'cascader') {
- modelLabel[key] = value.map((option) => option.label).join(' / ');
- modelValue[key] = value;
- } else {
- modelLabel[key] = value;
- }
- }
- forbiddenFields.value = forbidden;
- modelRef.value = { ...modelRef.value, ...modelLabel };
- modelValueRef.value = { ...modelValueRef.value, ...modelValue };
- });
- let captchaLoaded = false;
- const { loading: captchaLoading, countdown, send: getCaptcha } = useCaptcha(
- () => getCaptchaMethod(modelRef.value.phone!),
- { initialCountdown: 60 },
- ).onSuccess(({ data }) => {
- captchaLoaded = true;
- Toast.success(data ?? '获取成功');
- });
- const getCaptchaHandle = async () => {
- try {
- await formRef.value?.validate('phone');
- if ( !modelRef.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 searchHandle = async (key: 'cardno' | 'code') => {
- const forbidden = { cardno: 'phone', code: 'cardno' }[key];
- try {
- await formRef.value?.validate(key);
- forbiddenFields.value = {};
- const { cardno, phone, code } = modelRef.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' && modelRef.value.cardno ) { searchHandle('cardno'); }
- if ( field?.name === 'code' && modelRef.value.phone ) { searchHandle('code'); }
- }
- function onSubmitHandle() {
- submit(model.value);
- }
- function fix(key: string) {
- for ( const field of fields.value ) {
- if (field.keyboard?.show && field.name !== key ) field.keyboard.show = false;
- }
- }
- const scan = useRouteQuery<string>('scan');
- watch(scan, key => {
- if ( key ) {
- try {
- const { model } = JSON.parse(sessionStorage.getItem(`scan_${ key }`) ?? '');
- modelRef.value = { ...modelRef.value, ...model };
- } catch ( e: any ) {}
- }
- }, { immediate: true });
- onBeforeUnmount(() => {
- for ( let i = 0; i < sessionStorage.length; i++ ) {
- const key = sessionStorage.key(i);
- if ( key?.startsWith('scan_') ) sessionStorage.removeItem(key);
- }
- });
- const keyboardProps = reactive({
- key: '',
- props: {},
- show: false,
- });
- const pickerProps = reactive({
- key: '',
- props: { options: [], selected: [] },
- show: false,
- handle(options: Option[]) {
- const key = (this ?? pickerProps).key;
- (modelRef.value as Record<string, any>)[key] = options.map(option => option.label).join(',');
- (modelValueRef.value as Record<string, any>)[key] = options.map(option => option.value).join(',');
- }
- });
- const cascaderProps = reactive({
- key: '',
- props: { options: [], loading: false, },
- show: false,
- handle(options: CascaderOption[]) {
- const key = (this ?? cascaderProps).key;
- (modelRef.value as Record<string, any>)[key] = options.map((option) => option.label).join(' / ');
- (modelValueRef.value as Record<string, any>)[key] = options;
- },
- });
- 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: (modelValueRef.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: (modelValueRef.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;
- }
- </script>
- <template>
- <div>
- <div class="page-header flex py-4 px-4 overflow-hidden">
- <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>
- </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="返回首页">
- </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 v-model="modelRef[field.name]" :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 v-model="modelRef[field.name]" direction="horizontal" shape="dot" :disabled="forbiddenFields[field.name]">
- <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="modelRef[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>
- <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()">
- </div>
- </div>
- <van-number-keyboard v-bind="keyboardProps.props" :show="keyboardProps.show" v-model="modelRef[keyboardProps.key]"></van-number-keyboard>
- <PickerDialog v-bind="pickerProps.props" v-model:show="pickerProps.show" @selected="pickerProps.handle($event)"></PickerDialog>
- <CascaderDialog v-bind="cascaderProps.props" v-model:show="cascaderProps.show" @selected="cascaderProps.handle($event)"></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>
|