搜索结果列表页面开发
This commit is contained in:
4
src/components/Form/index.ts
Normal file
4
src/components/Form/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import CustomForm from './index.vue';
|
||||
export type * from './types';
|
||||
/* 基础类型暂时只支持input, 复合类型请使用render传递组件 */
|
||||
export default CustomForm;
|
||||
298
src/components/Form/index.vue
Normal file
298
src/components/Form/index.vue
Normal file
@@ -0,0 +1,298 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, toRaw, computed, watchEffect, onBeforeMount } from 'vue';
|
||||
import { Form, FormItem } from 'vue-devui/form';
|
||||
import { Input } from 'vue-devui/input';
|
||||
import InputButton from './inputButton.vue';
|
||||
import InputSelect from './inputSelect.vue';
|
||||
|
||||
import type { FormListProps, GFormProps } from './types';
|
||||
|
||||
const props = withDefaults(defineProps<GFormProps>(), {
|
||||
MessageType: 'text',
|
||||
disabled: false,
|
||||
showFeedback: false,
|
||||
layout: 'vertical',
|
||||
showLabel: false,
|
||||
size: 'lg',
|
||||
labelSize: 'sm',
|
||||
labelAlign: 'start'
|
||||
});
|
||||
|
||||
const Data = reactive<any>({});
|
||||
const GroupRules = reactive<any>({});
|
||||
const FormRef = ref(null);
|
||||
let timer:any = null;
|
||||
|
||||
const emits = defineEmits(['change', 'countDown', 'complete']);
|
||||
|
||||
const setRequiredRule = (target: FormListProps) => {
|
||||
return {
|
||||
required: true,
|
||||
trigger: 'change',
|
||||
message: target.label ? `请填写${target.label}` : '请填写必要信息'
|
||||
};
|
||||
};
|
||||
|
||||
const initData = () => {
|
||||
for (let i = 0; i < props.DataList.length; i++) {
|
||||
const target = props.DataList[i];
|
||||
if (target.defaultValue) {
|
||||
Data[target.key] = target.defaultValue;
|
||||
}
|
||||
GroupRules[target.key] = [];
|
||||
if (props?.required) {
|
||||
if (target?.rules && target.rules?.length) {
|
||||
const isRequired = target.rules.find((rule: any) => rule.required);
|
||||
if (!isRequired) {
|
||||
GroupRules[target.key].unshift(setRequiredRule(target));
|
||||
} else {
|
||||
GroupRules[target.key].push(...target.rules);
|
||||
}
|
||||
} else {
|
||||
GroupRules[target.key].push(setRequiredRule(target));
|
||||
}
|
||||
} else {
|
||||
if (target?.rules) {
|
||||
const isRequired = target.rules.find((rule: any) => rule.required);
|
||||
if (target.required && !isRequired) {
|
||||
GroupRules[target.key] = [setRequiredRule(target), ...target.rules];
|
||||
} else {
|
||||
GroupRules[target.key] = target.rules;
|
||||
}
|
||||
} else if (target.required) {
|
||||
GroupRules[target.key].unshift(setRequiredRule(target));
|
||||
}
|
||||
}
|
||||
};
|
||||
/* form rule */
|
||||
if (props?.rules) {
|
||||
for (const key in props.rules) {
|
||||
if (!GroupRules[key].length && props.rules[key]) {
|
||||
GroupRules[key] = props.rules[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toValidator = async(instance: any, keys?: string[]): Promise<Record<keyof typeof Data, any[]>> => {
|
||||
try {
|
||||
keys?.length ? await instance.validateFields(keys) : await instance.validate();
|
||||
return {};
|
||||
} catch (e) {
|
||||
return e as unknown as Record<keyof typeof Data, any[]>;
|
||||
}
|
||||
};
|
||||
|
||||
const ValidateForm = async(): Promise<{ forms: Record<string, any> | null, errors: Record<string, any> | null, type: 'success' | 'fail' }> => {
|
||||
const res = await toValidator(FormRef.value);
|
||||
const keys = Object.keys(res);
|
||||
if (keys?.length) {
|
||||
return {
|
||||
type: 'fail',
|
||||
errors: res,
|
||||
forms: null
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'success',
|
||||
errors: null,
|
||||
forms: toRaw(Data)
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const ValidateFormKeys = async(keys: string[]): Promise<{ errors: Record<string, any> | null, type: 'success' | 'fail' }> => {
|
||||
const res = await toValidator(FormRef.value, keys);
|
||||
const keyList = Object.keys(res);
|
||||
if (keyList?.length) {
|
||||
return {
|
||||
type: 'fail',
|
||||
errors: res
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'success',
|
||||
errors: null
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getItemWidth = computed(() => {
|
||||
return function(FormItem: FormListProps) {
|
||||
if (FormItem.width === 'auto') {
|
||||
return 'auto';
|
||||
} else if (typeof FormItem.width === 'number') {
|
||||
return `${FormItem.width}px`;
|
||||
} else if (typeof FormItem.width === 'string' && (FormItem.width.includes('px') || FormItem.width.includes('%'))) {
|
||||
return FormItem.width;
|
||||
} else {
|
||||
return 'auto';
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
initData();
|
||||
});
|
||||
|
||||
const handleChange = async(key: string) => {
|
||||
const res = await ValidateFormKeys([key]);
|
||||
emits('change', {
|
||||
key,
|
||||
errors: res.type === 'success' ? null : res.errors,
|
||||
source: toRaw(Data)
|
||||
});
|
||||
};
|
||||
|
||||
const handleClick = () => {};
|
||||
|
||||
const handleOperate = (config: FormListProps) => {
|
||||
const key = config?.props?.aliasKey || config.key;
|
||||
emits('countDown', {
|
||||
key,
|
||||
sourceKey: config.key,
|
||||
value: Data[key]
|
||||
});
|
||||
};
|
||||
|
||||
const handleFormInput = () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
timer = setTimeout(() => {
|
||||
const status = props.DataList.some(item => {
|
||||
return item.required && !Data[item.key];
|
||||
});
|
||||
emits('complete', status);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const ClearForm = () => {
|
||||
if (FormRef.value) {
|
||||
(FormRef.value as typeof Form)?.resetFields();
|
||||
}
|
||||
};
|
||||
|
||||
const ClearFormFields = (keys: string[]) => {
|
||||
if (FormRef.value) {
|
||||
(FormRef.value as typeof Form)?.clearValidate(keys);
|
||||
}
|
||||
};
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
ClearForm,
|
||||
ClearFormFields,
|
||||
ValidateForm,
|
||||
ValidateFormKeys,
|
||||
Data
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form
|
||||
:data="Data"
|
||||
:rules="GroupRules"
|
||||
ref="FormRef"
|
||||
:show-feedback="showFeedback"
|
||||
:disabled="disabled"
|
||||
:message-type="MessageType"
|
||||
:layout="layout"
|
||||
:size="size"
|
||||
:label-size="labelSize"
|
||||
:label-align="labelAlign"
|
||||
class="GForm"
|
||||
@input="handleFormInput"
|
||||
>
|
||||
<FormItem
|
||||
v-for="formItem of DataList"
|
||||
:key="formItem.key"
|
||||
:field="formItem.key"
|
||||
:label="showLabel ? formItem.label : ''"
|
||||
:help-tips="formItem.help"
|
||||
:extra-info="formItem.extra"
|
||||
:style="{ width: getItemWidth(formItem) }"
|
||||
>
|
||||
<template v-if="formItem.type === 'input'">
|
||||
<Input
|
||||
v-model="Data[formItem.key]"
|
||||
:placeholder="`请输入${formItem.label}`"
|
||||
@change="() => handleChange(formItem.key)"
|
||||
v-bind="formItem.props"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="formItem.type === 'inputButton'">
|
||||
<InputButton
|
||||
v-model="Data[formItem.key]"
|
||||
@change="handleChange(formItem.key)"
|
||||
@click="handleOperate(formItem)"
|
||||
:placeholder="formItem.label"
|
||||
v-bind="formItem.props"
|
||||
:hasMobile="!!Data[formItem.props?.aliasKey]"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="formItem.type === 'inputSelect'">
|
||||
<InputSelect
|
||||
v-model="Data[formItem.key]"
|
||||
@change="handleChange(formItem.key)"
|
||||
:placeholder="formItem.label"
|
||||
v-bind="formItem.props"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<component
|
||||
:is="formItem.render"
|
||||
:key="formItem.key"
|
||||
v-model="Data[formItem.key]"
|
||||
@change="handleChange(formItem.key)"
|
||||
@click="handleClick"
|
||||
:placeholder="formItem.label"
|
||||
v-bind="formItem.props"
|
||||
/>
|
||||
</template>
|
||||
</FormItem>
|
||||
<slot name="submit"></slot>
|
||||
<slot name="info"></slot>
|
||||
</Form>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.GForm {
|
||||
.devui-form__label--required:before {
|
||||
margin-left: 0;
|
||||
}
|
||||
.devui-input__wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
.devui-input__inner {
|
||||
padding: 0 8px;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
.devui-input-slot__suffix {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.devui-form__item--vertical {
|
||||
.devui-form__control-extra {
|
||||
padding-top: 2px;
|
||||
text-indent: 1px;
|
||||
}
|
||||
}
|
||||
.error-message{
|
||||
margin: 4px auto;
|
||||
text-indent: 2px;
|
||||
}
|
||||
|
||||
input[type="password"] {
|
||||
letter-spacing: 0;
|
||||
&::placeholder {
|
||||
letter-spacing: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
152
src/components/Form/inputButton.vue
Normal file
152
src/components/Form/inputButton.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInputButtonProps } from './types';
|
||||
import { useModel } from '@/utils/hooks/useModel';
|
||||
import { Input } from 'vue-devui/input';
|
||||
import { computed, ref, onBeforeUnmount, watchEffect } from 'vue';
|
||||
|
||||
const props = withDefaults(defineProps<FormInputButtonProps>(), {
|
||||
text: '获取验证码',
|
||||
placeholder: '',
|
||||
countdown: false,
|
||||
second: 59,
|
||||
hasMobile: false
|
||||
});
|
||||
|
||||
const emits = defineEmits(['change', 'update:modelValue', 'click']);
|
||||
|
||||
const { vModels } = useModel(props, emits);
|
||||
|
||||
const CountDown = ref(props.second);
|
||||
const status = props.second !== 59;
|
||||
const DisabledBtn = ref<boolean>(status);
|
||||
|
||||
let timer: any = null;
|
||||
|
||||
const toCountDown = () => {
|
||||
if (!timer) {
|
||||
if (CountDown.value === 0) {
|
||||
CountDown.value = 59;
|
||||
}
|
||||
timer = setInterval(() => {
|
||||
if (CountDown.value >= 1) {
|
||||
CountDown.value -= 1;
|
||||
} else {
|
||||
CountDown.value = 59;
|
||||
DisabledBtn.value = false;
|
||||
timer && clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
if (status) {
|
||||
toCountDown();
|
||||
}
|
||||
|
||||
const BtnText = computed(() => {
|
||||
if (DisabledBtn.value) {
|
||||
return `已发送 ${CountDown.value}`;
|
||||
} else {
|
||||
return props.text;
|
||||
}
|
||||
});
|
||||
|
||||
const handleClick = () => {
|
||||
if (DisabledBtn.value) {
|
||||
return;
|
||||
}
|
||||
emits('click');
|
||||
};
|
||||
|
||||
const handleChange = () => {
|
||||
emits('change');
|
||||
};
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.countdown) {
|
||||
DisabledBtn.value = true;
|
||||
toCountDown();
|
||||
} else {
|
||||
DisabledBtn.value = false;
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-1 g-input-button">
|
||||
<Input v-model="vModels" :placeholder="`请输入${placeholder}`" v-bind="prop" @change="handleChange">
|
||||
<template #append>
|
||||
<div
|
||||
@click.stop="handleClick"
|
||||
:class="['g-input-button-append', DisabledBtn || !hasMobile ? 'g-input-button-append-inactive' : 'g-input-button-append-active']"
|
||||
>
|
||||
<span class="text-[13px]">{{ BtnText }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</Input>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
$active-color: #2865E0;
|
||||
$inactive-color: var(--color-G500);
|
||||
.g-input-button {
|
||||
border: 1px solid rgba(230, 230, 232, 1);
|
||||
border-radius: var(--devui-border-radius, 2px);
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
&:hover {
|
||||
border-color: var(--devui-form-control-line-active, #5e7ce0);
|
||||
}
|
||||
.devui-input-slot__append {
|
||||
padding: 0;
|
||||
border-width: 0;
|
||||
}
|
||||
&-append {
|
||||
padding: 5px 12px;
|
||||
box-sizing: border-box;
|
||||
min-width: 88px;
|
||||
max-width: 88px;
|
||||
border-left: none;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
line-height: 20px;
|
||||
&::before {
|
||||
position: absolute;
|
||||
left: -1px;
|
||||
top: 9px;
|
||||
content: '';
|
||||
border-left: 1px solid var(--color-G300);
|
||||
width: 1px;
|
||||
height: 13px;
|
||||
}
|
||||
&-active {
|
||||
color: $active-color;
|
||||
cursor: pointer;
|
||||
}
|
||||
&-inactive {
|
||||
color: $inactive-color;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
.devui-input {
|
||||
&--md {
|
||||
height: 30px;
|
||||
}
|
||||
&__wrapper {
|
||||
border-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
42
src/components/Form/inputSelect.vue
Normal file
42
src/components/Form/inputSelect.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormItemSelectProps } from './types';
|
||||
import { useModel } from '@/utils/hooks/useModel';
|
||||
import { Input } from 'vue-devui/input';
|
||||
import { Select } from 'vue-devui/select';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const props = defineProps<FormItemSelectProps>();
|
||||
const emits = defineEmits(['change', 'update:modelValue']);
|
||||
|
||||
const { vModels } = useModel(props, emits);
|
||||
const region = ref('86');
|
||||
const options = [
|
||||
{
|
||||
name: '+ 86',
|
||||
value: '86'
|
||||
}
|
||||
];
|
||||
|
||||
const handleChange = () => {
|
||||
emits('change');
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-1 g-input-select">
|
||||
<Input v-model="vModels" :placeholder="`请输入${placeholder}`" v-bind="prop" @change="handleChange">
|
||||
<template #prepend>
|
||||
<Select v-model="region" :options="options" />
|
||||
</template>
|
||||
</Input>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.g-input-select {
|
||||
.devui-input-slot__prepend {
|
||||
width: 78px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
55
src/components/Form/types.ts
Normal file
55
src/components/Form/types.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { Component } from 'vue';
|
||||
|
||||
export type FormListType = {
|
||||
type: 'input' | 'inputButton' | 'inputSelect'
|
||||
help?: string
|
||||
extra?: string
|
||||
} | {
|
||||
type: 'render'
|
||||
render: Component
|
||||
}
|
||||
|
||||
export type FormListProps = FormListType & {
|
||||
key: string
|
||||
label: string
|
||||
defaultValue?: any
|
||||
rules?: any[]
|
||||
width?: number | string
|
||||
props?: Record<string, any>,
|
||||
required?: boolean
|
||||
[x: string]: any
|
||||
}
|
||||
|
||||
export interface GFormProps {
|
||||
DataList: FormListProps[],
|
||||
rules?: Record<string, any[]>,
|
||||
required?: boolean,
|
||||
disabled?: boolean,
|
||||
showFeedback?:boolean,
|
||||
MessageType?: 'popover' | 'text' | 'none'
|
||||
layout?: 'horizontal' | 'vertical'
|
||||
showLabel?: boolean
|
||||
size?: 'lg' | 'md' | 'sm'
|
||||
labelSize?: 'lg' | 'md' | 'sm'
|
||||
labelAlign?: 'start' | 'center' | 'end'
|
||||
}
|
||||
|
||||
export interface FormInputButtonProps {
|
||||
modelValue?: any
|
||||
'onUpdate:modelValue'?: Function
|
||||
placeholder: string
|
||||
prop?: any
|
||||
text?: string
|
||||
countdown?: boolean
|
||||
second?: number
|
||||
hasMobile?: false
|
||||
[x: string]: any
|
||||
}
|
||||
|
||||
export interface FormItemSelectProps {
|
||||
modelValue?: any
|
||||
'onUpdate:modelValue'?: Function
|
||||
placeholder: string
|
||||
prop?: any
|
||||
[x: string]: any
|
||||
}
|
||||
Reference in New Issue
Block a user