Explorar el Código

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

icssoa hace 7 meses
padre
commit
8a8ea1d6c2

+ 1 - 1
package.json

@@ -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",

+ 6 - 0
pages.json

@@ -311,6 +311,12 @@
 					}
 				},
 				{
+					"path": "data/filter-bar",
+					"style": {
+						"navigationBarTitleText": "FilterBar 筛选栏"
+					}
+				},
+				{
 					"path": "status/badge",
 					"style": {
 						"navigationBarTitleText": "Badge 角标"

+ 465 - 0
pages/demo/data/filter-bar.uvue

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

+ 5 - 0
pages/index/home.uvue

@@ -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"
 				}
 			]
 		},

+ 17 - 0
uni_modules/cool-ui/components/cl-filter-bar/cl-filter-bar.uvue

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

+ 169 - 0
uni_modules/cool-ui/components/cl-filter-item/cl-filter-item.uvue

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

+ 9 - 0
uni_modules/cool-ui/types/index.ts

@@ -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[];
+};