添加 cl-filter-bar 过滤栏组件

This commit is contained in:
icssoa
2025-08-22 00:20:57 +08:00
parent b434516f38
commit 8a8ea1d6c2
7 changed files with 672 additions and 1 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "cool-unix",
"version": "8.0.14",
"version": "8.0.15",
"license": "MIT",
"scripts": {
"build-ui": "node ./uni_modules/cool-ui/scripts/generate-types.js",

View File

@@ -310,6 +310,12 @@
"navigationBarTitleText": "Draggable 拖拽"
}
},
{
"path": "data/filter-bar",
"style": {
"navigationBarTitleText": "FilterBar 筛选栏"
}
},
{
"path": "status/badge",
"style": {

View File

@@ -0,0 +1,465 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('基础用法')">
<cl-filter-bar>
<!-- 下拉框 -->
<cl-filter-item
label="综合排序"
type="select"
:value="1"
:options="coreOptions"
:pt="{
className: 'w-[220rpx] !flex-none'
}"
@change="onOptionsChange"
></cl-filter-item>
<!-- 排序 -->
<cl-filter-item
label="销量"
type="sort"
value="desc"
@change="onSortChange"
></cl-filter-item>
<!-- 开关 -->
<cl-filter-item
label="国补"
type="switch"
:value="false"
@change="onSwitchChange"
></cl-filter-item>
<!-- 自定义 -->
<view
class="flex flex-row items-center justify-center flex-1"
@tap="openFilter"
>
<cl-text>筛选</cl-text>
<cl-icon name="filter-line"></cl-icon>
</view>
</cl-filter-bar>
</demo-item>
<demo-item>
<cl-text pre-wrap :pt="{ className: '!text-sm p-2' }">{{
JSON.stringify(filterForm, null, 4)
}}</cl-text>
</demo-item>
<demo-item>
<cl-text pre-wrap :pt="{ className: '!text-sm p-2' }">{{
JSON.stringify(searchForm, null, 4)
}}</cl-text>
</demo-item>
</view>
<!-- 自定义筛选 -->
<cl-popup
v-model="filterVisible"
:title="t('筛选')"
direction="right"
size="80%"
:show-header="false"
>
<view class="flex flex-col h-full">
<scroll-view class="flex-1">
<cl-form :pt="{ className: 'p-3' }">
<cl-form-item label="服务/折扣">
<cl-row :gutter="20">
<cl-col :span="8" v-for="(item, index) in disOptions" :key="index">
<cl-checkbox
v-model="searchForm.dis"
:label="item.label"
:value="item.value"
:show-icon="false"
:pt="{
className: parseClass([
'mb-3 p-2 rounded-lg justify-center border border-solid border-transparent',
[isDark, 'bg-surface-800', 'bg-surface-100'],
[
searchForm.dis.includes(item.value),
`${isDark ? '!bg-surface-700' : '!bg-white'} !border-primary-500`
]
]),
label: {
className: '!text-sm'
}
}"
></cl-checkbox>
</cl-col>
</cl-row>
</cl-form-item>
<cl-form-item label="价格区间">
<view class="flex flex-row items-center">
<cl-input
v-model="searchForm.minPrice"
type="digit"
placeholder="最低价"
:pt="{
className: 'flex-1',
inner: {
className: 'text-center'
}
}"
></cl-input>
<cl-text
:pt="{
className: 'px-2'
}"
>~</cl-text
>
<cl-input
v-model="searchForm.maxPrice"
type="digit"
placeholder="最高价"
:pt="{
className: 'flex-1',
inner: {
className: 'text-center'
}
}"
></cl-input>
</view>
</cl-form-item>
<cl-form-item label="品牌">
<cl-row :gutter="20">
<cl-col
:span="8"
v-for="(item, index) in brandOptions"
:key="index"
>
<cl-checkbox
v-model="searchForm.brand"
:label="item.label"
:value="item.value"
:show-icon="false"
:pt="{
className: parseClass([
'mb-3 p-2 rounded-lg justify-center border border-solid border-transparent',
[isDark, 'bg-surface-800', 'bg-surface-100'],
[
searchForm.brand.includes(item.value),
`${isDark ? '!bg-surface-700' : '!bg-white'} !border-primary-500`
]
]),
label: {
className: '!text-sm'
}
}"
></cl-checkbox>
</cl-col>
</cl-row>
</cl-form-item>
<cl-form-item label="内存">
<cl-row :gutter="20">
<cl-col
:span="8"
v-for="(item, index) in memoryOptions"
:key="index"
>
<cl-radio
v-model="searchForm.memory"
:label="item.label"
:value="item.value"
:show-icon="false"
:pt="{
className: parseClass([
'mb-3 p-2 rounded-lg justify-center border border-solid border-transparent',
[isDark, 'bg-surface-800', 'bg-surface-100'],
[
searchForm.memory == item.value,
`${isDark ? '!bg-surface-700' : '!bg-white'} !border-primary-500`
]
]),
label: {
className: '!text-sm'
}
}"
></cl-radio>
</cl-col>
</cl-row>
</cl-form-item>
<cl-form-item label="颜色">
<cl-row :gutter="20">
<cl-col
:span="8"
v-for="(item, index) in colorOptions"
:key="index"
>
<cl-radio
v-model="searchForm.color"
:label="item.label"
:value="item.value"
:show-icon="false"
:pt="{
className: parseClass([
'mb-3 p-2 rounded-lg justify-center border border-solid border-transparent',
[isDark, 'bg-surface-800', 'bg-surface-100'],
[
searchForm.color == item.value,
`${isDark ? '!bg-surface-700' : '!bg-white'} !border-primary-500`
]
]),
label: {
className: '!text-sm'
}
}"
></cl-radio>
</cl-col>
</cl-row>
</cl-form-item>
</cl-form>
</scroll-view>
<view class="flex flex-row p-3">
<cl-button
type="info"
text
border
:pt="{
className: 'flex-1'
}"
@tap="closeFilter"
>{{ t("取消") }}</cl-button
>
<cl-button
:pt="{
className: 'flex-1'
}"
@tap="submit"
>{{ t("确定") }}</cl-button
>
</view>
</view>
</cl-popup>
</cl-page>
</template>
<script lang="ts" setup>
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import { reactive, ref } from "vue";
import { useUi, type ClSelectOption } from "@/uni_modules/cool-ui";
import { isDark, parseClass } from "@/cool";
const ui = useUi();
const filterVisible = ref(false);
function openFilter() {
filterVisible.value = true;
}
function closeFilter() {
filterVisible.value = false;
}
function submit() {
closeFilter();
ui.showLoading();
setTimeout(() => {
ui.hideLoading();
}, 1000);
}
const coreOptions = ref<ClSelectOption[]>([
{
label: "综合排序",
value: 1
},
{
label: "价格从高到底",
value: 2
},
{
label: "价格从低到高",
value: 3
}
]);
type Option = {
label: string;
value: string;
};
const disOptions = ref<Option[]>([
{
label: "百亿补贴",
value: "billion_subsidy"
},
{
label: "以旧换新",
value: "trade_in"
},
{
label: "分期免息",
value: "installment"
},
{
label: "包邮",
value: "free_shipping"
},
{
label: "促销",
value: "promotion"
},
{
label: "价保",
value: "price_protection"
},
{
label: "仅看有货",
value: "in_stock"
},
{
label: "货到付款",
value: "cod"
}
]);
const brandOptions = ref<Option[]>([
{
label: "华为",
value: "huawei"
},
{
label: "苹果",
value: "apple"
},
{
label: "小米",
value: "xiaomi"
},
{
label: "三星",
value: "samsung"
},
{
label: "OPPO",
value: "oppo"
},
{
label: "vivo",
value: "vivo"
},
{
label: "荣耀",
value: "honor"
}
]);
const colorOptions = ref<Option[]>([
{
label: "红色",
value: "red"
},
{
label: "蓝色",
value: "blue"
},
{
label: "黑色",
value: "black"
},
{
label: "白色",
value: "white"
},
{
label: "金色",
value: "gold"
},
{
label: "银色",
value: "silver"
},
{
label: "绿色",
value: "green"
},
{
label: "紫色",
value: "purple"
},
{
label: "灰色",
value: "gray"
},
{
label: "粉色",
value: "pink"
}
]);
const memoryOptions = ref<Option[]>([
{
label: "128GB",
value: "128"
},
{
label: "256GB",
value: "256"
},
{
label: "512GB",
value: "512"
},
{
label: "1TB",
value: "1024"
}
]);
type SearchForm = {
dis: string[];
minPrice: string;
maxPrice: string;
brand: string[];
memory: string;
color: string;
};
const searchForm = ref<SearchForm>({
dis: [],
minPrice: "50",
maxPrice: "300",
brand: [],
memory: "",
color: ""
});
type FilterForm = {
core: number;
sort: string;
switch: boolean;
};
const filterForm = reactive<FilterForm>({
core: 0,
sort: "none",
switch: false
});
function onOptionsChange(val: number) {
console.log(val);
filterForm.core = val;
}
function onSortChange(val: string) {
console.log(val);
filterForm.sort = val;
}
function onSwitchChange(val: boolean) {
console.log(val);
filterForm.switch = val;
}
</script>

View File

@@ -310,6 +310,11 @@ const data = computed<Item[]>(() => {
label: t("拖拽"),
icon: "drag-move-line",
path: "/pages/demo/data/draggable"
},
{
label: t("筛选栏"),
icon: "filter-line",
path: "/pages/demo/data/filter-bar"
}
]
},

View File

@@ -0,0 +1,17 @@
<template>
<view class="cl-filter-bar">
<slot></slot>
</view>
</template>
<script lang="ts" setup>
defineOptions({
name: "cl-filter-bar"
});
</script>
<style lang="scss" scoped>
.cl-filter-bar {
@apply flex flex-row;
}
</style>

View File

@@ -0,0 +1,169 @@
<template>
<view class="cl-filter-item" :class="[pt.className]" @tap="onTap">
<slot>
<cl-text
:pt="{
className: parseClass([
[isActive, '!text-primary-500'],
'text-center',
pt.label?.className
])
}"
>{{ text }}</cl-text
>
<!-- 排序 -->
<cl-icon
v-if="type == 'sort' && sort != 'none'"
:name="`sort-${sort}`"
:pt="{
className: 'ml-1'
}"
></cl-icon>
<!-- 下拉框 -->
<cl-icon
v-if="type == 'select'"
name="arrow-down-s-line"
:pt="{
className: 'ml-1'
}"
></cl-icon>
</slot>
</view>
<cl-select
v-model="selectValue"
ref="selectRef"
:show-trigger="false"
:options="options"
></cl-select>
</template>
<script lang="ts" setup>
import { parsePt, parseClass } from "@/cool";
import { computed, onMounted, ref, watch, type PropType } from "vue";
import type { PassThroughProps, ClFilterItemType, ClSelectOption } from "../../types";
defineOptions({
name: "cl-filter-item"
});
const props = defineProps({
pt: {
type: Object,
default: () => ({})
},
label: {
type: String,
default: ""
},
value: {
type: [String, Number, Boolean, Array] as PropType<any>,
required: true
},
type: {
type: String as PropType<ClFilterItemType>,
default: "switch"
},
options: {
type: Array as PropType<ClSelectOption[]>,
default: () => []
}
});
const emit = defineEmits(["change"]);
// 透传样式类型
type PassThrough = {
className?: string;
label?: PassThroughProps;
};
// 解析透传样式
const pt = computed(() => parsePt<PassThrough>(props.pt));
// select组件的ref引用用于调用select的方法
const selectRef = ref<ClSelectComponentPublicInstance | null>(null);
// switch类型的激活状态
const isActive = ref(false);
// sort类型的排序状态可为"asc"、"desc"、"none"
const sort = ref("none");
// select类型的当前选中值
const selectValue = ref<any | null>(null);
// 根据类型动态计算显示文本
const text = computed(() => {
// 如果是select类型显示选中项的label
if (props.type == "select") {
return props.options.find((e) => e.value == selectValue.value)?.label ?? "";
} else {
// 其他类型直接显示label
return props.label;
}
});
// 点击事件,根据不同类型处理
function onTap() {
// 排序类型,切换排序状态
if (props.type == "sort") {
if (sort.value == "asc") {
sort.value = "desc";
} else if (sort.value == "desc") {
sort.value = "none";
} else {
sort.value = "asc";
}
emit("change", sort.value);
}
// 开关类型,切换激活状态
if (props.type == "switch") {
isActive.value = !isActive.value;
emit("change", isActive.value);
}
// 选择类型打开select组件
if (props.type == "select") {
// 打开select弹窗选择后回调
selectRef.value!.open((val) => {
emit("change", val);
});
}
}
// 组件挂载时监听props.value变化并同步到本地状态
onMounted(() => {
watch(
computed(() => props.value!),
(val: any) => {
switch (props.type) {
case "select":
// select类型同步选中值
selectValue.value = val as any;
break;
case "switch":
// switch类型同步激活状态
isActive.value = val as boolean;
break;
case "sort":
// sort类型同步排序状态
sort.value = val as string;
break;
}
},
{
immediate: true
}
);
});
</script>
<style lang="scss" scoped>
.cl-filter-item {
@apply flex flex-row flex-1 justify-center items-center h-[72rpx];
}
</style>

View File

@@ -172,3 +172,12 @@ export type ClFormValidateResult = {
};
export type ClFormLabelPosition = "left" | "top" | "right";
export type ClFilterItemType = "switch" | "sort" | "select";
export type ClFilterItem = {
label: string;
value: any;
type: ClFilterItemType;
options?: ClSelectOption[];
};