搜索结果列表页面开发

This commit is contained in:
付民康
2025-03-12 18:41:20 +08:00
commit 7cebe8fc00
739 changed files with 88149 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
import CustomForm from './index.vue';
export type * from './types';
/* 基础类型暂时只支持input, 复合类型请使用render传递组件 */
export default CustomForm;

View 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>

View 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>

View 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>

View 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
}