diff --git a/pages/products/components/FilterDrawer.vue b/pages/products/components/FilterDrawer.vue index 7bc78be..0560db0 100644 --- a/pages/products/components/FilterDrawer.vue +++ b/pages/products/components/FilterDrawer.vue @@ -31,9 +31,23 @@ 价格区间 - + - - + @@ -58,7 +72,83 @@ export default { default: () => [] } }, + data() { + return { + minPrice: '', + maxPrice: '', + minError: false, + maxError: false + } + }, 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) { return (group.selected || []).indexOf(tagId) > -1 }, @@ -66,10 +156,18 @@ export default { this.$emit('toggle-tag', { groupIndex: gIdx, tagId }) }, onReset() { + this.minPrice = '' + this.maxPrice = '' + this.minError = false + this.maxError = false this.$emit('reset') }, onConfirm() { - this.$emit('confirm') + if (!this.validatePrices()) return + this.$emit('confirm', { + minPrice: this.minPrice, + maxPrice: this.maxPrice + }) }, onClose() { this.$emit('close') @@ -128,6 +226,10 @@ export default { } .filter-drawer-body { flex: 1; + /* 框架默认 width:100% + padding 时 content-box 会超宽 64rpx, + 导致右侧"最高价"输入框被挤出屏幕,必须 border-box */ + width: 100%; + box-sizing: border-box; padding: 0 32rpx; } .filter-group { @@ -162,18 +264,26 @@ export default { .filter-price-range { display: flex; align-items: center; - gap: 20rpx; + justify-content: space-between; } .price-input { - flex: 1; + /* 固定宽度,不依赖 flex 收缩(uni-input 自定义元素收缩行为不可靠) */ + width: 230rpx; + box-sizing: border-box; height: 72rpx; padding: 0 20rpx; background: #f5f5f5; border-radius: 12rpx; + border: 2rpx solid transparent; font-size: 26rpx; color: #333333; text-align: center; } +/* 校验失败冒红 */ +.price-input.input-error { + border-color: #ff0000; + background: #fff0f0; +} .price-divider { font-size: 28rpx; color: #999999; diff --git a/pages/products/components/ProductGrid.vue b/pages/products/components/ProductGrid.vue index 81809b7..525aa2e 100644 --- a/pages/products/components/ProductGrid.vue +++ b/pages/products/components/ProductGrid.vue @@ -42,10 +42,19 @@ {{ item.name }} - + ¥{{ item.price }} + + + {{ item.countryName }} + @@ -75,6 +84,8 @@ export default { hasMore: { type: Boolean, default: true }, loading: { type: Boolean, default: false }, scrollTopVal: { type: Number, default: 0 }, + // 未选择国家时显示商品所属国家标签 + showCountryTag: { type: Boolean, default: false }, // 商品滚动区固定高度(px),由父组件动态计算传入 scrollHeight: { type: Number, default: 400 } }, @@ -216,6 +227,34 @@ export default { line-height: 1.2; 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 { diff --git a/pages/products/products.vue b/pages/products/products.vue index 8c82bde..07d8c2c 100644 --- a/pages/products/products.vue +++ b/pages/products/products.vue @@ -46,6 +46,7 @@ :loading="loadingMore" :scroll-top-val="scrollTopVal" :scroll-height="productScrollHeight" + :show-country-tag="!selectedCountry" @sub-select="selectSubCategory" @product-click="goDetail" @scroll-to-lower="onScrollToLower" @@ -114,6 +115,8 @@ export default { activeFilters: [], showFilter: false, filterGroups: [], + minPrice: '', + maxPrice: '', pagination: { total: 0, current: 1, @@ -255,7 +258,9 @@ export default { id: item.goodId, name: truncateProductTitle(item.goodName), 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 @@ -316,6 +321,13 @@ export default { if (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 || []) .filter((g) => g.selected && g.selected.length > 0) .map((g) => ({ @@ -514,6 +526,8 @@ export default { async clearAllFilters() { const hasCountry = this.activeFilters.some((f) => f.key === 'country') this.activeFilters = [] + this.minPrice = '' + this.maxPrice = '' if (hasCountry || this.selectedCountry) { this.selectedCountry = '' await this.loadCategoryData('') @@ -532,7 +546,10 @@ export default { this.filterGroups = groups }, - async confirmFilters() { + async confirmFilters(payload) { + payload = payload || {} + this.minPrice = payload.minPrice || '' + this.maxPrice = payload.maxPrice || '' var filters = [] var countryFilter = this.activeFilters.find(function (f) { return f.key === 'country' }) if (countryFilter) filters.push(countryFilter)