<!--
|
多条件查询弹层(schema 驱动)
|
|
<FilterPanel v-model:show="showFilter" :model-value="query" :fields="filterFields"
|
@search="onSearch" @reset="onReset" />
|
|
注意:query 用 reactive 定义时不要写 v-model="query"。
|
组件回吐的是整个筛选对象,reactive 常量不可整体重新赋值
|
(会编译成 `query = $event`,运行时抛 Assignment to constant variable)。
|
用 :model-value 传入 + 在 @search/@reset 里 Object.assign(query, values) 写回:
|
|
const query = reactive({ projectName: '', bidResult: '' });
|
const onSearch = values => { Object.assign(query, values); getList(); };
|
|
页面若另有搜索输入框绑到 query 的某个字段,两边会天然同步(同一个对象)。
|
|
fields 支持的类型:
|
{ prop: 'projectName', label: '项目名称', type: 'input' }
|
{ prop: 'bidResult', label: '招投标结果', type: 'select', options: [...] }
|
{ prop: 'filingDate', label: '立案时间', type: 'date' }
|
{ prop: 'bidDateRange', label: '投标日期', type: 'daterange' }
|
|
daterange 类型的值是 [开始, 结束] 两元素数组,页面在提交前自行拆成后端要的参数名
|
(各模块的区间参数名不统一,如 bidDateStart/bidDateEnd、beginShippingDate/endShippingDate)。
|
-->
|
<template>
|
<up-popup :show="show"
|
mode="bottom"
|
:round="16"
|
@close="close">
|
<view class="filter-panel">
|
<view class="filter-panel__header">
|
<text class="filter-panel__title">{{ title }}</text>
|
<u-icon name="close"
|
size="20"
|
color="#909399"
|
@click="close"></u-icon>
|
</view>
|
|
<scroll-view scroll-y
|
class="filter-panel__body">
|
<view v-for="field in fields"
|
:key="field.prop"
|
class="filter-panel__field">
|
<text class="filter-panel__label">{{ field.label }}</text>
|
<view class="filter-panel__control">
|
<u-input v-if="field.type === 'input'"
|
v-model="local[field.prop]"
|
:placeholder="field.placeholder || '请输入'"
|
clearable />
|
<FormPicker v-else-if="field.type === 'select'"
|
v-model="local[field.prop]"
|
type="select"
|
:options="field.options || []"
|
:title="`选择${field.label}`"
|
:placeholder="field.placeholder || '请选择'" />
|
<FormPicker v-else-if="field.type === 'date'"
|
v-model="local[field.prop]"
|
type="date"
|
:placeholder="field.placeholder || '请选择'" />
|
<view v-else-if="field.type === 'daterange'"
|
class="filter-panel__range">
|
<FormPicker v-model="local[field.prop][0]"
|
type="date"
|
placeholder="开始日期" />
|
<text class="filter-panel__range-sep">至</text>
|
<FormPicker v-model="local[field.prop][1]"
|
type="date"
|
placeholder="结束日期" />
|
</view>
|
</view>
|
</view>
|
</scroll-view>
|
|
<view class="filter-panel__footer">
|
<u-button class="filter-panel__btn"
|
@click="reset">重置</u-button>
|
<u-button class="filter-panel__btn"
|
type="primary"
|
@click="search">查询</u-button>
|
</view>
|
</view>
|
</up-popup>
|
</template>
|
|
<script setup>
|
import { ref, watch } from "vue";
|
import FormPicker from "@/components/FormPicker.vue";
|
|
const props = defineProps({
|
show: {
|
type: Boolean,
|
default: false,
|
},
|
modelValue: {
|
type: Object,
|
default: () => ({}),
|
},
|
fields: {
|
type: Array,
|
default: () => [],
|
},
|
title: {
|
type: String,
|
default: "筛选",
|
},
|
});
|
|
const emit = defineEmits(["update:show", "update:modelValue", "search", "reset"]);
|
|
const buildEmpty = () => {
|
const empty = {};
|
props.fields.forEach(field => {
|
empty[field.prop] = field.type === "daterange" ? [] : "";
|
});
|
return empty;
|
};
|
|
// 必须先用 buildEmpty() 铺满字段:弹层即使隐藏时也会渲染子节点,
|
// daterange 的 v-model 会对 local[prop][0] 取值,local[prop] 为 undefined 会直接报错
|
const local = ref(buildEmpty());
|
|
const syncFromParent = () => {
|
const source = props.modelValue || {};
|
const next = buildEmpty();
|
props.fields.forEach(field => {
|
const value = source[field.prop];
|
if (field.type === "daterange") {
|
next[field.prop] = Array.isArray(value) ? [...value] : [];
|
} else if (value !== undefined && value !== null) {
|
next[field.prop] = value;
|
}
|
});
|
local.value = next;
|
};
|
|
watch(
|
() => props.show,
|
show => {
|
if (show) syncFromParent();
|
},
|
{ immediate: true }
|
);
|
|
// fields 是异步拿到的场景:补一次铺底,避免字段缺失
|
watch(
|
() => props.fields,
|
() => {
|
if (props.show) {
|
syncFromParent();
|
} else {
|
local.value = buildEmpty();
|
}
|
},
|
{ deep: true }
|
);
|
|
const close = () => {
|
emit("update:show", false);
|
};
|
|
const search = () => {
|
emit("update:modelValue", { ...local.value });
|
emit("search", { ...local.value });
|
close();
|
};
|
|
const reset = () => {
|
local.value = buildEmpty();
|
emit("update:modelValue", { ...local.value });
|
emit("reset", { ...local.value });
|
};
|
</script>
|
|
<style scoped lang="scss">
|
.filter-panel {
|
background: #ffffff;
|
padding: 16px;
|
padding-bottom: calc(16px + env(safe-area-inset-bottom));
|
}
|
|
.filter-panel__header {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
padding-bottom: 12px;
|
border-bottom: 1px solid #f0f0f0;
|
}
|
|
.filter-panel__title {
|
font-size: 16px;
|
font-weight: 600;
|
color: #303133;
|
}
|
|
.filter-panel__body {
|
max-height: 55vh;
|
padding: 8px 0;
|
}
|
|
.filter-panel__field {
|
display: flex;
|
align-items: center;
|
padding: 10px 0;
|
border-bottom: 1px solid #f8f8f8;
|
}
|
|
.filter-panel__label {
|
width: 90px;
|
flex-shrink: 0;
|
font-size: 14px;
|
color: #606266;
|
}
|
|
.filter-panel__control {
|
flex: 1;
|
min-width: 0;
|
}
|
|
.filter-panel__range {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
}
|
|
.filter-panel__range-sep {
|
flex-shrink: 0;
|
font-size: 13px;
|
color: #909399;
|
}
|
|
.filter-panel__footer {
|
display: flex;
|
gap: 12px;
|
padding-top: 16px;
|
}
|
|
.filter-panel__btn {
|
flex: 1;
|
}
|
</style>
|