vue_zhongyou 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,455 @@
1
+ <template>
2
+ <div class="filter-container">
3
+ <!-- 已选标签区 -->
4
+ <div class="selected-tags" v-if="hasSelected">
5
+ <div class="tag" v-for="(value, key) in selected" :key="key">
6
+ <span class="tag-text">
7
+ {{ getFilterLabel(key) }}: {{ formatValue(key, value) }}
8
+ </span>
9
+ <van-icon name="close" class="tag-close" @click.stop="handleDelete(key)" />
10
+ </div>
11
+ <button class="clear-all" @click="clearAll">清除全部</button>
12
+ </div>
13
+
14
+ <!-- 筛选栏 -->
15
+ <div class="filter-bar">
16
+ <div
17
+ v-for="item in filterConfig"
18
+ :key="item.key"
19
+ class="filter-item"
20
+ :class="{ active: activeKey === item.key }"
21
+ @click="togglePanel(item.key)"
22
+ >
23
+ <span>{{ item.label }}</span>
24
+ <van-icon name="arrow-down" class="filter-arrow" :rotate="activeKey === item.key ? 180 : 0" />
25
+ </div>
26
+ </div>
27
+
28
+ <!-- 面板遮罩 -->
29
+ <div
30
+ class="panel-mask"
31
+ v-if="activeKey"
32
+ @click="closePanel"
33
+ ></div>
34
+
35
+ <!-- 筛选面板 -->
36
+ <div
37
+ class="filter-panel"
38
+ :class="{ show: activeKey }"
39
+ >
40
+ <!-- 面板标题 -->
41
+ <div class="panel-header">
42
+ <h3>{{ getFilterLabel(activeKey) }}</h3>
43
+ <button class="panel-close" @click="closePanel">完成</button>
44
+ </div>
45
+
46
+ <!-- 选项列表(根据类型渲染) -->
47
+ <div class="panel-content">
48
+ <template v-if="currentFilter">
49
+ <!-- 单选类型(如距离、评分) -->
50
+ <div v-if="currentFilter.type === 'radio'">
51
+ <div
52
+ class="option-item"
53
+ v-for="opt in currentFilter.options"
54
+ :key="opt.value"
55
+ :class="{ selected: selected[activeKey] === opt.value }"
56
+ @click="handleSelect(activeKey, opt.value)"
57
+ >
58
+ {{ opt.label }}
59
+ <van-icon
60
+ name="check"
61
+ class="option-check"
62
+ v-if="selected[activeKey] === opt.value"
63
+ />
64
+ </div>
65
+ </div>
66
+
67
+ <!-- 多选类型(如分类) -->
68
+ <div v-if="currentFilter.type === 'checkbox'">
69
+ <div
70
+ class="option-item"
71
+ v-for="opt in currentFilter.options"
72
+ :key="opt.value"
73
+ :class="{ selected: selected[activeKey]?.includes(opt.value) }"
74
+ @click="handleMultiSelect(activeKey, opt.value)"
75
+ >
76
+ {{ opt.label }}
77
+ <van-icon
78
+ name="check"
79
+ class="option-check"
80
+ v-if="selected[activeKey]?.includes(opt.value)"
81
+ />
82
+ </div>
83
+ </div>
84
+
85
+ <!-- 价格区间类型 -->
86
+ <div v-if="currentFilter.type === 'price'">
87
+ <div class="price-inputs">
88
+ <input
89
+ type="number"
90
+ v-model="priceRange.min"
91
+ placeholder="最低"
92
+ @input="handlePriceChange"
93
+ >
94
+ <span>-</span>
95
+ <input
96
+ type="number"
97
+ v-model="priceRange.max"
98
+ placeholder="最高"
99
+ @input="handlePriceChange"
100
+ >
101
+ <span class="price-unit">元</span>
102
+ </div>
103
+ <div class="price-options">
104
+ <div
105
+ class="option-item"
106
+ v-for="opt in currentFilter.options"
107
+ :key="opt.value"
108
+ :class="{ selected: selected[activeKey] === opt.value }"
109
+ @click="handleSelect(activeKey, opt.value)"
110
+ >
111
+ {{ opt.label }}
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </template>
116
+ </div>
117
+ </div>
118
+ </div>
119
+ </template>
120
+
121
+ <script setup>
122
+ import { ref, reactive, computed, watch } from 'vue';
123
+ import { Icon } from 'vant';
124
+
125
+ // 接收父组件传入的筛选配置
126
+ const props = defineProps({
127
+ filterConfig: {
128
+ type: Array,
129
+ required: true,
130
+ // 配置项格式示例:
131
+ // [
132
+ // { key: 'category', label: '分类', type: 'checkbox', options: [{ value: '1', label: '美食' }, ...] },
133
+ // { key: 'distance', label: '距离', type: 'radio', options: [{ value: '1000', label: '1km内' }, ...] },
134
+ // ]
135
+ }
136
+ });
137
+
138
+ // 状态管理
139
+ const activeKey = ref(''); // 当前展开的筛选项key
140
+ const selected = reactive({}); // 选中的值:{ category: ['1', '2'], distance: '1000' }
141
+ const priceRange = reactive({ min: '', max: '' }); // 价格区间临时值
142
+
143
+ // 计算当前激活的筛选配置
144
+ const currentFilter = computed(() => {
145
+ return props.filterConfig.find(item => item.key === activeKey.value);
146
+ });
147
+
148
+ // 检查是否有选中项
149
+ const hasSelected = computed(() => {
150
+ return Object.keys(selected).some(key => {
151
+ const value = selected[key];
152
+ return (Array.isArray(value) && value.length > 0) || (value !== undefined && value !== '');
153
+ });
154
+ });
155
+
156
+ // 切换面板显示/隐藏
157
+ const togglePanel = (key) => {
158
+ if (activeKey.value === key) {
159
+ closePanel();
160
+ } else {
161
+ activeKey.value = key;
162
+ // 初始化价格区间(如果是价格类型)
163
+ if (key === 'price' && selected.price && !selected.price.includes('-')) {
164
+ const [min, max] = selected.price.split('-').map(Number);
165
+ priceRange.min = min || '';
166
+ priceRange.max = max || '';
167
+ }
168
+ }
169
+ };
170
+
171
+ // 关闭面板
172
+ const closePanel = () => {
173
+ activeKey.value = '';
174
+ // 触发父组件筛选事件
175
+ emit('filterChange', { ...selected });
176
+ };
177
+
178
+ // 单选/区间选择处理
179
+ const handleSelect = (key, value) => {
180
+ // 取消选择(再次点击已选项)
181
+ if (selected[key] === value) {
182
+ delete selected[key];
183
+ } else {
184
+ selected[key] = value;
185
+ }
186
+ };
187
+
188
+ // 多选处理
189
+ const handleMultiSelect = (key, value) => {
190
+ if (!selected[key]) {
191
+ selected[key] = [];
192
+ }
193
+ const index = selected[key].indexOf(value);
194
+ if (index > -1) {
195
+ // 移除选中
196
+ selected[key].splice(index, 1);
197
+ // 为空时删除键
198
+ if (selected[key].length === 0) {
199
+ delete selected[key];
200
+ }
201
+ } else {
202
+ // 添加选中
203
+ selected[key].push(value);
204
+ }
205
+ };
206
+
207
+ // 价格区间输入处理
208
+ const handlePriceChange = () => {
209
+ const { min, max } = priceRange;
210
+ if (min || max) {
211
+ selected.price = `${min || ''}-${max || ''}`;
212
+ } else {
213
+ delete selected[activeKey.value];
214
+ }
215
+ };
216
+
217
+ // 删除单个选中项
218
+ const handleDelete = (key) => {
219
+ delete selected[key];
220
+ // 通知父组件更新
221
+ emit('filterChange', { ...selected });
222
+ };
223
+
224
+ // 清除所有选中项
225
+ const clearAll = () => {
226
+ Object.keys(selected).forEach(key => {
227
+ delete selected[key];
228
+ });
229
+ priceRange.min = '';
230
+ priceRange.max = '';
231
+ emit('filterChange', { ...selected });
232
+ };
233
+
234
+ // 获取筛选项的标签文本
235
+ const getFilterLabel = (key) => {
236
+ const item = props.filterConfig.find(i => i.key === key);
237
+ return item?.label || '';
238
+ };
239
+
240
+ // 格式化选中值的显示文本
241
+ const formatValue = (key, value) => {
242
+ const item = props.filterConfig.find(i => i.key === key);
243
+ if (!item) return value;
244
+
245
+ // 多选类型(拼接标签)
246
+ if (item.type === 'checkbox' && Array.isArray(value)) {
247
+ const labels = value.map(v => {
248
+ const opt = item.options.find(o => o.value === v);
249
+ return opt?.label || v;
250
+ });
251
+ return labels.join('、');
252
+ }
253
+
254
+ // 单选/价格类型
255
+ if (item.type === 'radio' || item.type === 'price') {
256
+ const opt = item.options.find(o => o.value === value);
257
+ return opt?.label || value;
258
+ }
259
+
260
+ return value;
261
+ };
262
+
263
+ // 向父组件发射筛选变化事件
264
+ const emit = defineEmits(['filterChange']);
265
+
266
+ // 监听价格面板关闭时重置临时输入
267
+ watch(activeKey, (newVal) => {
268
+ if (newVal !== 'price') {
269
+ priceRange.min = '';
270
+ priceRange.max = '';
271
+ }
272
+ });
273
+ </script>
274
+
275
+ <style scoped>
276
+ .filter-container {
277
+ background-color: #fff;
278
+ }
279
+
280
+ /* 已选标签区 */
281
+ .selected-tags {
282
+ padding: 8px 16px;
283
+ display: flex;
284
+ align-items: center;
285
+ flex-wrap: wrap;
286
+ gap: 8px;
287
+ border-bottom: 1px solid #f5f5f5;
288
+ }
289
+
290
+ .tag {
291
+ display: flex;
292
+ align-items: center;
293
+ padding: 4px 8px;
294
+ background-color: #f5f5f5;
295
+ border-radius: 16px;
296
+ font-size: 12px;
297
+ }
298
+
299
+ .tag-text {
300
+ margin-right: 4px;
301
+ }
302
+
303
+ .tag-close {
304
+ width: 14px;
305
+ height: 14px;
306
+ color: #999;
307
+ }
308
+
309
+ .clear-all {
310
+ margin-left: auto;
311
+ font-size: 12px;
312
+ color: #666;
313
+ background: none;
314
+ border: none;
315
+ cursor: pointer;
316
+ }
317
+
318
+ /* 筛选栏 */
319
+ .filter-bar {
320
+ display: flex;
321
+ overflow-x: auto;
322
+ padding: 0 16px;
323
+ height: 44px;
324
+ border-bottom: 1px solid #f5f5f5;
325
+ }
326
+
327
+ .filter-item {
328
+ display: flex;
329
+ align-items: center;
330
+ padding: 0 12px;
331
+ height: 100%;
332
+ white-space: nowrap;
333
+ font-size: 14px;
334
+ color: #333;
335
+ cursor: pointer;
336
+ }
337
+
338
+ .filter-item.active {
339
+ color: #ff7d00; /* 大众点评橙色调 */
340
+ }
341
+
342
+ .filter-arrow {
343
+ width: 16px;
344
+ height: 16px;
345
+ margin-left: 4px;
346
+ transition: transform 0.3s;
347
+ }
348
+
349
+ /* 面板遮罩 */
350
+ .panel-mask {
351
+ position: fixed;
352
+ top: 0;
353
+ left: 0;
354
+ right: 0;
355
+ bottom: 0;
356
+ background-color: rgba(0, 0, 0, 0.3);
357
+ z-index: 100;
358
+ }
359
+
360
+ /* 筛选面板 */
361
+ .filter-panel {
362
+ position: fixed;
363
+ left: 0;
364
+ right: 0;
365
+ bottom: 0;
366
+ background-color: #fff;
367
+ border-radius: 16px 16px 0 0;
368
+ z-index: 101;
369
+ transform: translateY(100%);
370
+ transition: transform 0.3s;
371
+ max-height: 80vh;
372
+ }
373
+
374
+ .filter-panel.show {
375
+ transform: translateY(0);
376
+ }
377
+
378
+ .panel-header {
379
+ display: flex;
380
+ align-items: center;
381
+ justify-content: center;
382
+ padding: 12px 16px;
383
+ border-bottom: 1px solid #f5f5f5;
384
+ position: relative;
385
+ }
386
+
387
+ .panel-header h3 {
388
+ font-size: 16px;
389
+ font-weight: 500;
390
+ }
391
+
392
+ .panel-close {
393
+ position: absolute;
394
+ right: 16px;
395
+ font-size: 14px;
396
+ color: #ff7d00;
397
+ background: none;
398
+ border: none;
399
+ cursor: pointer;
400
+ }
401
+
402
+ .panel-content {
403
+ padding: 16px;
404
+ overflow-y: auto;
405
+ max-height: calc(80vh - 50px);
406
+ }
407
+
408
+ /* 选项样式 */
409
+ .option-item {
410
+ display: flex;
411
+ align-items: center;
412
+ justify-content: space-between;
413
+ padding: 12px 0;
414
+ border-bottom: 1px solid #f5f5f5;
415
+ font-size: 14px;
416
+ cursor: pointer;
417
+ }
418
+
419
+ .option-item.selected {
420
+ color: #ff7d00;
421
+ }
422
+
423
+ .option-check {
424
+ width: 18px;
425
+ height: 18px;
426
+ color: #ff7d00;
427
+ }
428
+
429
+ /* 价格区间样式 */
430
+ .price-inputs {
431
+ display: flex;
432
+ align-items: center;
433
+ gap: 8px;
434
+ padding: 16px 0;
435
+ border-bottom: 1px solid #f5f5f5;
436
+ }
437
+
438
+ .price-inputs input {
439
+ flex: 1;
440
+ height: 36px;
441
+ padding: 0 8px;
442
+ border: 1px solid #eee;
443
+ border-radius: 4px;
444
+ font-size: 14px;
445
+ }
446
+
447
+ .price-unit {
448
+ color: #666;
449
+ font-size: 14px;
450
+ }
451
+
452
+ .price-options {
453
+ margin-top: 16px;
454
+ }
455
+ </style>
@@ -0,0 +1,110 @@
1
+ <template>
2
+ <div>
3
+ <FilterPanel
4
+ :filter-config="filterConfig"
5
+ @filterChange="handleFilterChange"
6
+ />
7
+ <!-- 列表内容 -->
8
+ <div class="list-container">
9
+ <div v-for="item in filteredList" :key="item.id" class="list-item">
10
+ {{ item.name }}
11
+ </div>
12
+ </div>
13
+ </div>
14
+ </template>
15
+
16
+ <script setup>
17
+ import { ref, computed } from 'vue';
18
+ import FilterPanel from './FilterPanel.vue';
19
+
20
+ // 筛选配置(根据业务需求定义)
21
+ const filterConfig = [
22
+ {
23
+ key: 'category',
24
+ label: '分类',
25
+ type: 'checkbox',
26
+ options: [
27
+ { value: '1', label: '美食' },
28
+ { value: '2', label: '电影' },
29
+ { value: '3', label: '酒店' },
30
+ { value: '4', label: '休闲娱乐' },
31
+ { value: '5', label: '丽人' }
32
+ ]
33
+ },
34
+ {
35
+ key: 'distance',
36
+ label: '距离',
37
+ type: 'radio',
38
+ options: [
39
+ { value: '1000', label: '1km内' },
40
+ { value: '3000', label: '3km内' },
41
+ { value: '5000', label: '5km内' },
42
+ { value: '10000', label: '10km内' }
43
+ ]
44
+ },
45
+ {
46
+ key: 'price',
47
+ label: '价格',
48
+ type: 'price',
49
+ options: [
50
+ { value: '0-50', label: '50元以下' },
51
+ { value: '50-100', label: '50-100元' },
52
+ { value: '100-200', label: '100-200元' },
53
+ { value: '200-500', label: '200-500元' },
54
+ { value: '500-', label: '500元以上' }
55
+ ]
56
+ },
57
+ {
58
+ key: 'rating',
59
+ label: '评分',
60
+ type: 'radio',
61
+ options: [
62
+ { value: '4.5', label: '4.5分以上' },
63
+ { value: '4.0', label: '4.0分以上' },
64
+ { value: '3.5', label: '3.5分以上' }
65
+ ]
66
+ }
67
+ ];
68
+
69
+ // 原始列表数据
70
+ const rawList = ref([
71
+ { id: 1, name: '肯德基', category: '1', distance: '800', price: '30-50', rating: '4.2' },
72
+ { id: 2, name: '万达影城', category: '2', distance: '2500', price: '50-100', rating: '4.5' },
73
+ { id: 3, name: '希尔顿酒店', category: '3', distance: '6000', price: '500-', rating: '4.7' }
74
+ ]);
75
+
76
+ // 筛选后的列表
77
+ const filteredList = ref([...rawList.value]);
78
+
79
+ // 处理筛选变化
80
+ const handleFilterChange = (filters) => {
81
+ // 根据筛选条件过滤列表(实际项目中可能是请求接口)
82
+ let result = [...rawList.value];
83
+
84
+ // 分类筛选(多选)
85
+ if (filters.category?.length) {
86
+ result = result.filter(item => filters.category.includes(item.category));
87
+ }
88
+
89
+ // 距离筛选
90
+ if (filters.distance) {
91
+ result = result.filter(item => Number(item.distance) <= Number(filters.distance));
92
+ }
93
+
94
+ // 价格筛选
95
+ if (filters.price) {
96
+ const [min, max] = filters.price.split('-').map(Number);
97
+ result = result.filter(item => {
98
+ const [itemMin, itemMax] = item.price.split('-').map(Number);
99
+ return (min === '' || itemMin >= min) && (max === '' || itemMax <= max);
100
+ });
101
+ }
102
+
103
+ // 评分筛选
104
+ if (filters.rating) {
105
+ result = result.filter(item => Number(item.rating) >= Number(filters.rating));
106
+ }
107
+
108
+ filteredList.value = result;
109
+ };
110
+ </script>