国家标注,价格筛选

This commit is contained in:
2026-08-31 14:26:05 +08:00
parent 3bd58e05c5
commit 502d8def61
3 changed files with 174 additions and 8 deletions
+115 -5
View File
@@ -31,9 +31,23 @@
<view class="filter-group"> <view class="filter-group">
<text class="filter-group-title">价格区间</text> <text class="filter-group-title">价格区间</text>
<view class="filter-price-range"> <view class="filter-price-range">
<input class="price-input" placeholder="最低价" type="number" /> <input
class="price-input"
:class="{ 'input-error': minError }"
placeholder="最低价"
type="number"
v-model="minPrice"
@blur="onMinBlur"
/>
<text class="price-divider">-</text> <text class="price-divider">-</text>
<input class="price-input" placeholder="最高价" type="number" /> <input
class="price-input"
:class="{ 'input-error': maxError }"
placeholder="最高价"
type="number"
v-model="maxPrice"
@blur="onMaxBlur"
/>
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
@@ -58,7 +72,83 @@ export default {
default: () => [] default: () => []
} }
}, },
data() {
return {
minPrice: '',
maxPrice: '',
minError: false,
maxError: false
}
},
methods: { methods: {
/**
* 清洗价格输入:只保留数字和小数点(仅一个)
*/
sanitizePrice(v) {
if (v === '' || v === null || v === undefined) return ''
var s = String(v).replace(/[^\d.]/g, '')
var i = s.indexOf('.')
if (i > -1) {
s = s.slice(0, i + 1) + s.slice(i + 1).replace(/\./g, '')
}
return s
},
/**
* 校验单个价格:非空时必须是 ≥0 的数字,否则提示并清空,返回 false
*/
validateSingle(key) {
var raw = this.sanitizePrice(this[key])
this[key] = raw
if (raw === '') return true
var num = parseFloat(raw)
if (isNaN(num) || num < 0) {
uni.showToast({ title: '价格不能小于0', icon: 'none' })
this[key] = ''
return false
}
return true
},
flashError(key) {
this[key] = true
setTimeout(() => { this[key] = false }, 1500)
},
onMinBlur() {
if (!this.validateSingle('minPrice')) return
// 最低价 > 已填的最高价 → 最低价钳制到最高价(反向同样不允许越界)
var min = parseFloat(this.minPrice)
var max = parseFloat(this.maxPrice)
if (this.minPrice !== '' && this.maxPrice !== '' && min > max) {
this.minPrice = this.maxPrice
this.flashError('minError')
uni.showToast({ title: '最低价不能大于最高价,已自动调整', icon: 'none' })
}
},
onMaxBlur() {
if (!this.validateSingle('maxPrice')) return
// 最高价 < 已填的最低价 → 冒红并改成最低价
var min = parseFloat(this.minPrice)
var max = parseFloat(this.maxPrice)
if (this.maxPrice !== '' && this.minPrice !== '' && max < min) {
this.maxPrice = this.minPrice
this.flashError('maxError')
uni.showToast({ title: '最高价不能小于最低价,已自动调整', icon: 'none' })
}
},
/**
* 点击确定前的价格校验,校验不过则阻止请求
*/
validatePrices() {
if (!this.validateSingle('minPrice')) return false
if (!this.validateSingle('maxPrice')) return false
var min = parseFloat(this.minPrice)
var max = parseFloat(this.maxPrice)
if (this.minPrice !== '' && this.maxPrice !== '' && max < min) {
this.flashError('maxError')
uni.showToast({ title: '最高价不能小于最低价', icon: 'none' })
return false
}
return true
},
isSelected(group, tagId) { isSelected(group, tagId) {
return (group.selected || []).indexOf(tagId) > -1 return (group.selected || []).indexOf(tagId) > -1
}, },
@@ -66,10 +156,18 @@ export default {
this.$emit('toggle-tag', { groupIndex: gIdx, tagId }) this.$emit('toggle-tag', { groupIndex: gIdx, tagId })
}, },
onReset() { onReset() {
this.minPrice = ''
this.maxPrice = ''
this.minError = false
this.maxError = false
this.$emit('reset') this.$emit('reset')
}, },
onConfirm() { onConfirm() {
this.$emit('confirm') if (!this.validatePrices()) return
this.$emit('confirm', {
minPrice: this.minPrice,
maxPrice: this.maxPrice
})
}, },
onClose() { onClose() {
this.$emit('close') this.$emit('close')
@@ -128,6 +226,10 @@ export default {
} }
.filter-drawer-body { .filter-drawer-body {
flex: 1; flex: 1;
/* 框架默认 width:100% + padding 时 content-box 会超宽 64rpx
导致右侧"最高价"输入框被挤出屏幕,必须 border-box */
width: 100%;
box-sizing: border-box;
padding: 0 32rpx; padding: 0 32rpx;
} }
.filter-group { .filter-group {
@@ -162,18 +264,26 @@ export default {
.filter-price-range { .filter-price-range {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 20rpx; justify-content: space-between;
} }
.price-input { .price-input {
flex: 1; /* 固定宽度,不依赖 flex 收缩(uni-input 自定义元素收缩行为不可靠) */
width: 230rpx;
box-sizing: border-box;
height: 72rpx; height: 72rpx;
padding: 0 20rpx; padding: 0 20rpx;
background: #f5f5f5; background: #f5f5f5;
border-radius: 12rpx; border-radius: 12rpx;
border: 2rpx solid transparent;
font-size: 26rpx; font-size: 26rpx;
color: #333333; color: #333333;
text-align: center; text-align: center;
} }
/* 校验失败冒红 */
.price-input.input-error {
border-color: #ff0000;
background: #fff0f0;
}
.price-divider { .price-divider {
font-size: 28rpx; font-size: 28rpx;
color: #999999; color: #999999;
+40 -1
View File
@@ -42,10 +42,19 @@
<!-- 商品信息 --> <!-- 商品信息 -->
<view class="product-info"> <view class="product-info">
<text class="product-name">{{ item.name }}</text> <text class="product-name">{{ item.name }}</text>
<!-- 价格 + "起" --> <!-- 价格 + "起" + 国家标签(未选国家时显示商品所属国家) -->
<view class="price-row"> <view class="price-row">
<text class="product-price">{{ item.price }}</text> <text class="product-price">{{ item.price }}</text>
<text class="price-unit"></text> <text class="price-unit"></text>
<view class="country-tag" v-if="showCountryTag && item.countryName">
<image
class="country-tag-icon"
:src="item.countryIcon"
mode="aspectFill"
v-if="item.countryIcon"
></image>
<text class="country-tag-name">{{ item.countryName }}</text>
</view>
</view> </view>
</view> </view>
</view> </view>
@@ -75,6 +84,8 @@ export default {
hasMore: { type: Boolean, default: true }, hasMore: { type: Boolean, default: true },
loading: { type: Boolean, default: false }, loading: { type: Boolean, default: false },
scrollTopVal: { type: Number, default: 0 }, scrollTopVal: { type: Number, default: 0 },
// 未选择国家时显示商品所属国家标签
showCountryTag: { type: Boolean, default: false },
// 商品滚动区固定高度(px),由父组件动态计算传入 // 商品滚动区固定高度(px),由父组件动态计算传入
scrollHeight: { type: Number, default: 400 } scrollHeight: { type: Number, default: 400 }
}, },
@@ -216,6 +227,34 @@ export default {
line-height: 1.2; line-height: 1.2;
margin-left: 4rpx; margin-left: 4rpx;
} }
/* 国家标签 - 价格右侧,国旗图标 + 国家名 */
.country-tag {
display: inline-flex;
align-items: center;
gap: 6rpx;
margin-left: 12rpx;
padding: 4rpx 12rpx;
background: #f7f7f7;
border-radius: 20rpx;
max-width: 50%;
box-sizing: border-box;
overflow: hidden;
}
.country-tag-icon {
width: 28rpx;
height: 20rpx;
border-radius: 4rpx;
flex-shrink: 0;
}
.country-tag-name {
font-size: 20rpx;
font-weight: 500;
color: #666666;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 底部加载提示 */ /* 底部加载提示 */
.load-tip { .load-tip {
+19 -2
View File
@@ -46,6 +46,7 @@
:loading="loadingMore" :loading="loadingMore"
:scroll-top-val="scrollTopVal" :scroll-top-val="scrollTopVal"
:scroll-height="productScrollHeight" :scroll-height="productScrollHeight"
:show-country-tag="!selectedCountry"
@sub-select="selectSubCategory" @sub-select="selectSubCategory"
@product-click="goDetail" @product-click="goDetail"
@scroll-to-lower="onScrollToLower" @scroll-to-lower="onScrollToLower"
@@ -114,6 +115,8 @@ export default {
activeFilters: [], activeFilters: [],
showFilter: false, showFilter: false,
filterGroups: [], filterGroups: [],
minPrice: '',
maxPrice: '',
pagination: { pagination: {
total: 0, total: 0,
current: 1, current: 1,
@@ -255,7 +258,9 @@ export default {
id: item.goodId, id: item.goodId,
name: truncateProductTitle(item.goodName), name: truncateProductTitle(item.goodName),
price: item.price, price: item.price,
image: item.image image: item.image,
countryName: (item.country && item.country.countryName) || '',
countryIcon: (item.country && item.country.countryIcon) ? buildUrl(item.country.countryIcon) : ''
})) }))
const total = data.total || 0 const total = data.total || 0
@@ -316,6 +321,13 @@ export default {
if (kw) { if (kw) {
params.keyword = kw params.keyword = kw
} }
// 价格区间:两个可单独传,也可同时传
if (this.minPrice !== '' && this.minPrice !== null && this.minPrice !== undefined) {
params.minPrice = this.minPrice
}
if (this.maxPrice !== '' && this.maxPrice !== null && this.maxPrice !== undefined) {
params.maxPrice = this.maxPrice
}
const tagsList = (this.filterGroups || []) const tagsList = (this.filterGroups || [])
.filter((g) => g.selected && g.selected.length > 0) .filter((g) => g.selected && g.selected.length > 0)
.map((g) => ({ .map((g) => ({
@@ -514,6 +526,8 @@ export default {
async clearAllFilters() { async clearAllFilters() {
const hasCountry = this.activeFilters.some((f) => f.key === 'country') const hasCountry = this.activeFilters.some((f) => f.key === 'country')
this.activeFilters = [] this.activeFilters = []
this.minPrice = ''
this.maxPrice = ''
if (hasCountry || this.selectedCountry) { if (hasCountry || this.selectedCountry) {
this.selectedCountry = '' this.selectedCountry = ''
await this.loadCategoryData('') await this.loadCategoryData('')
@@ -532,7 +546,10 @@ export default {
this.filterGroups = groups this.filterGroups = groups
}, },
async confirmFilters() { async confirmFilters(payload) {
payload = payload || {}
this.minPrice = payload.minPrice || ''
this.maxPrice = payload.maxPrice || ''
var filters = [] var filters = []
var countryFilter = this.activeFilters.find(function (f) { return f.key === 'country' }) var countryFilter = this.activeFilters.find(function (f) { return f.key === 'country' })
if (countryFilter) filters.push(countryFilter) if (countryFilter) filters.push(countryFilter)