19 changed files with 4387 additions and 2268 deletions
@ -0,0 +1,253 @@ |
|||
# 省市区选择组件使用说明 |
|||
|
|||
## 概述 |
|||
|
|||
`AreaPicker.vue` 是一个功能完整的省市区三级联动选择组件,支持自定义样式插槽,可以灵活地集成到各种页面中。 |
|||
|
|||
## 功能特性 |
|||
|
|||
- ✅ 省市区三级联动选择 |
|||
- ✅ 支持默认值设置 |
|||
- ✅ 支持插槽自定义显示样式 |
|||
- ✅ 完整的事件回调 |
|||
- ✅ 灵活的数据请求方式 |
|||
- ✅ 错误处理和容错机制 |
|||
|
|||
## Props |
|||
|
|||
| 参数 | 类型 | 默认值 | 说明 | |
|||
|------|------|--------|------| |
|||
| placeholder | String | '请选择' | 占位符文本 | |
|||
| defaultValue | Object | null | 默认选中值,格式:`{ provinceId, cityId, areaId }` | |
|||
| disabled | Boolean | false | 是否禁用 | |
|||
|
|||
## Events |
|||
|
|||
| 事件名 | 参数 | 说明 | |
|||
|--------|------|------| |
|||
| change | data | 选择改变时触发,返回选中的省市区信息 | |
|||
|
|||
### change 事件返回数据格式 |
|||
|
|||
```javascript |
|||
{ |
|||
provinceId: '110000', // 省份ID |
|||
cityId: '110100', // 城市ID |
|||
areaId: '110101', // 区域ID |
|||
fullText: '北京市北京市东城区' // 完整地址文本 |
|||
} |
|||
``` |
|||
|
|||
## Methods |
|||
|
|||
| 方法名 | 参数 | 返回值 | 说明 | |
|||
|--------|------|--------|------| |
|||
| getValue | - | Object | 获取当前选中的值 | |
|||
| reset | - | - | 重置选择 | |
|||
|
|||
## 插槽使用 |
|||
|
|||
组件提供了默认插槽,允许完全自定义显示样式。插槽提供以下数据: |
|||
|
|||
| 插槽参数 | 类型 | 说明 | |
|||
|----------|------|------| |
|||
| selectedText | String | 当前选中的完整文本 | |
|||
| placeholder | String | 占位符文本 | |
|||
| provinceData | Array | 省份数据列表 | |
|||
| cityData | Array | 城市数据列表 | |
|||
| areaData | Array | 区域数据列表 | |
|||
| multiIndex | Array | 当前选中的索引数组 | |
|||
| currentSelection | Object | 当前选中的详细信息 | |
|||
|
|||
### currentSelection 对象结构 |
|||
|
|||
```javascript |
|||
{ |
|||
province: { id: '110000', name: '北京市' }, |
|||
city: { id: '110100', name: '北京市' }, |
|||
area: { id: '110101', name: '东城区' }, |
|||
fullText: '北京市北京市东城区' |
|||
} |
|||
``` |
|||
|
|||
## 使用示例 |
|||
|
|||
### 基本使用 |
|||
|
|||
```vue |
|||
<template> |
|||
<view> |
|||
<!-- 使用默认样式 --> |
|||
<AreaPicker |
|||
ref="areaPicker" |
|||
placeholder="请选择省市区" |
|||
:defaultValue="{ provinceId: '110000', cityId: '110100', areaId: '110101' }" |
|||
@change="onAreaChange" |
|||
/> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
methods: { |
|||
onAreaChange(data) { |
|||
console.log('选中的地区:', data) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
``` |
|||
|
|||
### 自定义样式(插槽) |
|||
|
|||
```vue |
|||
<template> |
|||
<view> |
|||
<!-- 自定义样式1:简单自定义 --> |
|||
<AreaPicker |
|||
placeholder="请选择省市区" |
|||
@change="onAreaChange" |
|||
> |
|||
<template v-slot="{ selectedText, placeholder }"> |
|||
<view class="custom-display"> |
|||
<text class="icon">📍</text> |
|||
<text class="text">{{ selectedText || placeholder }}</text> |
|||
<text class="arrow">▼</text> |
|||
</view> |
|||
</template> |
|||
</AreaPicker> |
|||
|
|||
<!-- 自定义样式2:显示详细信息 --> |
|||
<AreaPicker |
|||
placeholder="请选择省市区" |
|||
@change="onAreaChange" |
|||
> |
|||
<template v-slot="{ selectedText, placeholder, currentSelection }"> |
|||
<view class="detail-display"> |
|||
<view v-if="currentSelection.province" class="detail-info"> |
|||
<text>省:{{ currentSelection.province.name }}</text> |
|||
<text>市:{{ currentSelection.city.name }}</text> |
|||
<text>区:{{ currentSelection.area.name }}</text> |
|||
</view> |
|||
<text v-else class="placeholder">{{ placeholder }}</text> |
|||
</view> |
|||
</template> |
|||
</AreaPicker> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
methods: { |
|||
onAreaChange(data) { |
|||
console.log('选中的地区:', data) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style> |
|||
.custom-display { |
|||
display: flex; |
|||
align-items: center; |
|||
padding: 12px 16px; |
|||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|||
border-radius: 25px; |
|||
color: white; |
|||
} |
|||
|
|||
.icon { |
|||
margin-right: 10px; |
|||
} |
|||
|
|||
.text { |
|||
flex: 1; |
|||
} |
|||
|
|||
.arrow { |
|||
margin-left: 10px; |
|||
} |
|||
|
|||
.detail-display { |
|||
padding: 15px; |
|||
background-color: #f8f9fa; |
|||
border: 2px dashed #dee2e6; |
|||
border-radius: 8px; |
|||
} |
|||
|
|||
.detail-info { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 5px; |
|||
} |
|||
|
|||
.placeholder { |
|||
color: #adb5bd; |
|||
font-style: italic; |
|||
} |
|||
</style> |
|||
``` |
|||
|
|||
### 在 header 组件中使用 |
|||
|
|||
```vue |
|||
<template> |
|||
<view> |
|||
<header |
|||
:isSearch="true" |
|||
:isAreaPicker="true" |
|||
areaPlaceholder="请选择省市区" |
|||
:defaultAreaValue="{ provinceId: '110000', cityId: '110100', areaId: '110101' }" |
|||
@areaChange="onAreaChange" |
|||
/> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
methods: { |
|||
onAreaChange(data) { |
|||
console.log('选中的地区:', data) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
``` |
|||
|
|||
## 数据接口要求 |
|||
|
|||
组件需要后端提供省市区数据接口,接口返回格式如下: |
|||
|
|||
```javascript |
|||
// 省份数据 |
|||
[ |
|||
{ id: '110000', name: '北京市' }, |
|||
{ id: '120000', name: '天津市' }, |
|||
// ... |
|||
] |
|||
|
|||
// 城市数据(根据省份ID获取) |
|||
[ |
|||
{ id: '110100', name: '北京市', pid: '110000' }, |
|||
// ... |
|||
] |
|||
|
|||
// 区域数据(根据城市ID获取) |
|||
[ |
|||
{ id: '110101', name: '东城区', pid: '110100' }, |
|||
{ id: '110102', name: '西城区', pid: '110100' }, |
|||
// ... |
|||
] |
|||
``` |
|||
|
|||
## 注意事项 |
|||
|
|||
1. 组件会自动处理数据请求的多种方式(父组件 Post 方法、全局 Post 方法、uni.request) |
|||
2. 建议在使用前确保数据接口正常可用 |
|||
3. 插槽内容会完全替换默认的显示样式 |
|||
4. 可以通过 ref 调用组件的 getValue() 和 reset() 方法 |
|||
5. 组件支持设置默认值,会自动回显对应的省市区信息 |
|||
|
|||
## 完整示例 |
|||
|
|||
查看 `AreaPickerSlotExample.vue` 文件获取完整的使用示例,包含多种自定义样式的演示。 |
@ -0,0 +1,429 @@ |
|||
<template> |
|||
<view class="area-picker"> |
|||
<picker |
|||
mode="multiSelector" |
|||
:range="newProvinceDataList" |
|||
range-key="name" |
|||
@change="changeArea" |
|||
@columnchange="pickerColumnchange" |
|||
:value="multiIndex" |
|||
> |
|||
<!-- 使用插槽,允许父组件自定义显示样式 --> |
|||
<slot |
|||
:selectedText="selectedText" |
|||
:placeholder="placeholder" |
|||
:provinceData="newProvinceDataList[0]" |
|||
:cityData="newProvinceDataList[1]" |
|||
:areaData="newProvinceDataList[2]" |
|||
:multiIndex="multiIndex" |
|||
:currentSelection="getCurrentSelection()" |
|||
> |
|||
<!-- 默认显示样式 --> |
|||
<view class="picker-display"> |
|||
<text class="picker-text" :class="{ 'placeholder': !selectedText }">{{ selectedText || placeholder }}</text> |
|||
<image |
|||
class="dropdown-icon" |
|||
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iOCIgdmlld0JveD0iMCAwIDEyIDgiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xIDFMNiA2TDExIDEiIHN0cm9rZT0iIzk5OTk5OSIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+" |
|||
mode="heightFix" |
|||
></image> |
|||
</view> |
|||
</slot> |
|||
</picker> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'AreaPicker', |
|||
props: { |
|||
// 占位符文本 |
|||
placeholder: { |
|||
type: String, |
|||
default: '请选择地区' |
|||
}, |
|||
// 默认选中的省市区ID |
|||
defaultValue: { |
|||
type: Object, |
|||
default: () => ({ |
|||
provinceId: null, |
|||
cityId: null, |
|||
areaId: null |
|||
}) |
|||
}, |
|||
// 是否禁用 |
|||
disabled: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
}, |
|||
data() { |
|||
return { |
|||
// 省市区数据 |
|||
columns: [], |
|||
newProvinceDataList: [[], [], []], |
|||
multiIndex: [0, 0, 0], |
|||
provinceId: null, |
|||
cityId: null, |
|||
areaId: null, |
|||
ready: false, |
|||
selectedText: '', |
|||
} |
|||
}, |
|||
mounted() { |
|||
this.getSeldCityList() |
|||
}, |
|||
watch: { |
|||
defaultValue: { |
|||
handler(newVal) { |
|||
console.log('----有值回显') |
|||
if (newVal && newVal.provinceId && this.ready) { |
|||
this.setDefaultValue(newVal) |
|||
} |
|||
}, |
|||
deep: true, |
|||
immediate: true |
|||
} |
|||
}, |
|||
methods: { |
|||
// 获取省市区数据 |
|||
getSeldCityList() { |
|||
// 尝试多种方式获取数据 |
|||
let requestMethod = null; |
|||
|
|||
// 1. 尝试使用父组件的 Post 方法 |
|||
if (this.$parent && this.$parent.Post) { |
|||
requestMethod = this.$parent.Post; |
|||
} |
|||
// 2. 尝试使用全局的 Post 方法(如果通过 mixin 引入) |
|||
else if (this.Post) { |
|||
requestMethod = this.Post; |
|||
} |
|||
// 3. 尝试使用 uni.request |
|||
else { |
|||
this.requestWithUni(); |
|||
return; |
|||
} |
|||
|
|||
requestMethod({}, '/api/areas/getAll').then(res => { |
|||
if (res) { |
|||
this.processAreaData(res.data) |
|||
} |
|||
}).catch(err => { |
|||
console.warn('省市区数据获取失败:', err) |
|||
// 可以在这里设置一些默认数据或提示用户 |
|||
}) |
|||
}, |
|||
|
|||
// 使用 uni.request 获取数据的备用方法 |
|||
requestWithUni() { |
|||
uni.request({ |
|||
url: '/api/areas/getAll', // 这里需要根据实际的完整URL调整 |
|||
method: 'POST', |
|||
data: {}, |
|||
success: (res) => { |
|||
if (res.data) { |
|||
this.processAreaData(res.data.data || res.data) |
|||
} |
|||
}, |
|||
fail: (err) => { |
|||
console.warn('省市区数据获取失败:', err) |
|||
// 可以提供一些默认的省市区数据或提示用户 |
|||
uni.showToast({ |
|||
title: '地区数据加载失败', |
|||
icon: 'none' |
|||
}) |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
// 处理省市区数据 |
|||
processAreaData(data) { |
|||
var result = {} |
|||
for (var i = 0; i < data.length; i++) { |
|||
var item = data[i] |
|||
if (item.parent_id == 0) { |
|||
continue |
|||
} |
|||
// 获取省 |
|||
if (item.parent_id == "1") { |
|||
result[item.id.toString()] = {} |
|||
result[item.id.toString()].children = [] |
|||
result[item.id.toString()].name = item.name |
|||
result[item.id.toString()].id = item.id |
|||
} else if (result[item.parent_id.toString()]) { |
|||
// 填充市 |
|||
var t = { |
|||
id: item.id, |
|||
name: item.name, |
|||
children: [] |
|||
} |
|||
result[item.parent_id.toString()].children.push(t) |
|||
} else { |
|||
// 填充区 |
|||
var k = { |
|||
id: item.id, |
|||
name: item.name |
|||
} |
|||
for (var j = 0; j < result[item.parent_id.toString().substr(0, 2) + "0000"].children.length; j++) { |
|||
if (result[item.parent_id.toString().substr(0, 2) + "0000"].children[j].id == item.parent_id) { |
|||
result[item.parent_id.toString().substr(0, 2) + "0000"].children[j].children.push(k) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
var r = [] |
|||
// 将Object转为Array |
|||
for (var i in result) { |
|||
r.push(result[i]) |
|||
} |
|||
|
|||
this.columns = r |
|||
this.initPickerData() |
|||
this.ready = true |
|||
|
|||
// 如果有默认值,设置默认选中 |
|||
if (this.defaultValue && this.defaultValue.provinceId) { |
|||
this.setDefaultValue(this.defaultValue) |
|||
} |
|||
}, |
|||
|
|||
// 初始化picker数据 |
|||
initPickerData() { |
|||
if (this.columns.length === 0) return |
|||
|
|||
// 初始化省份数据 |
|||
this.newProvinceDataList[0] = this.columns.map(item => ({ |
|||
name: item.name, |
|||
id: item.id |
|||
})) |
|||
|
|||
// 初始化城市数据(默认第一个省份的城市) |
|||
if (this.columns[0] && this.columns[0].children) { |
|||
this.newProvinceDataList[1] = this.columns[0].children.map(item => ({ |
|||
name: item.name, |
|||
id: item.id |
|||
})) |
|||
} |
|||
|
|||
// 初始化区县数据(默认第一个城市的区县) |
|||
if (this.columns[0] && this.columns[0].children[0] && this.columns[0].children[0].children) { |
|||
this.newProvinceDataList[2] = this.columns[0].children[0].children.map(item => ({ |
|||
name: item.name, |
|||
id: item.id |
|||
})) |
|||
} |
|||
}, |
|||
|
|||
// 设置默认值 |
|||
setDefaultValue(defaultValue) { |
|||
if (!this.ready || !defaultValue) return |
|||
|
|||
// 查找省份索引 |
|||
const provinceIndex = this.newProvinceDataList[0].findIndex(item => item.id == defaultValue.provinceId) |
|||
if (provinceIndex === -1) return |
|||
|
|||
this.multiIndex[0] = provinceIndex |
|||
|
|||
// 更新城市数据 |
|||
this.newProvinceDataList[1] = this.columns[provinceIndex].children.map(item => ({ |
|||
name: item.name, |
|||
id: item.id |
|||
})) |
|||
|
|||
// 查找城市索引 |
|||
const cityIndex = this.newProvinceDataList[1].findIndex(item => item.id == defaultValue.cityId) |
|||
if (cityIndex !== -1) { |
|||
this.multiIndex[1] = cityIndex |
|||
|
|||
// 更新区县数据 |
|||
this.newProvinceDataList[2] = this.columns[provinceIndex].children[cityIndex].children.map(item => ({ |
|||
name: item.name, |
|||
id: item.id |
|||
})) |
|||
|
|||
// 查找区县索引 |
|||
const areaIndex = this.newProvinceDataList[2].findIndex(item => item.id == defaultValue.areaId) |
|||
if (areaIndex !== -1) { |
|||
this.multiIndex[2] = areaIndex |
|||
} |
|||
} |
|||
|
|||
this.updateSelectedText() |
|||
}, |
|||
|
|||
// 选择完成事件 |
|||
changeArea(e) { |
|||
this.multiIndex = e.detail.value |
|||
this.updateSelectedText() |
|||
this.emitChange() |
|||
}, |
|||
|
|||
// 列滑动事件 |
|||
pickerColumnchange(e) { |
|||
const column = e.detail.column |
|||
const value = e.detail.value |
|||
|
|||
if (column === 0) { |
|||
// 第一列滑动(省份) |
|||
this.multiIndex[0] = value |
|||
|
|||
// 更新城市数据 |
|||
this.newProvinceDataList[1] = this.columns[this.multiIndex[0]].children.map(item => ({ |
|||
name: item.name, |
|||
id: item.id |
|||
})) |
|||
|
|||
// 更新区县数据 |
|||
if (this.columns[this.multiIndex[0]].children.length === 1) { |
|||
this.newProvinceDataList[2] = this.columns[this.multiIndex[0]].children[0].children.map(item => ({ |
|||
name: item.name, |
|||
id: item.id |
|||
})) |
|||
} else { |
|||
this.newProvinceDataList[2] = this.columns[this.multiIndex[0]].children[this.multiIndex[1]].children.map(item => ({ |
|||
name: item.name, |
|||
id: item.id |
|||
})) |
|||
} |
|||
|
|||
// 重置城市和区县索引 |
|||
this.multiIndex.splice(1, 1, 0) |
|||
this.multiIndex.splice(2, 1, 0) |
|||
} else if (column === 1) { |
|||
// 第二列滑动(城市) |
|||
this.multiIndex[1] = value |
|||
|
|||
// 更新区县数据 |
|||
this.newProvinceDataList[2] = this.columns[this.multiIndex[0]].children[this.multiIndex[1]].children.map(item => ({ |
|||
name: item.name, |
|||
id: item.id |
|||
})) |
|||
|
|||
// 重置区县索引 |
|||
this.multiIndex.splice(2, 1, 0) |
|||
} else if (column === 2) { |
|||
// 第三列滑动(区县) |
|||
this.multiIndex[2] = value |
|||
} |
|||
}, |
|||
|
|||
// 更新选中的省市区ID和显示文本 |
|||
updateSelectedText() { |
|||
if (this.newProvinceDataList[0][this.multiIndex[0]] && |
|||
this.newProvinceDataList[1][this.multiIndex[1]] && |
|||
this.newProvinceDataList[2][this.multiIndex[2]]) { |
|||
|
|||
this.selectedText = |
|||
this.newProvinceDataList[1][this.multiIndex[1]].name |
|||
|
|||
this.provinceId = this.newProvinceDataList[0][this.multiIndex[0]].id |
|||
this.cityId = this.newProvinceDataList[1][this.multiIndex[1]].id |
|||
this.areaId = this.newProvinceDataList[2][this.multiIndex[2]].id |
|||
} |
|||
}, |
|||
|
|||
// 触发change事件 |
|||
emitChange() { |
|||
const selectedText = this.newProvinceDataList[0][this.multiIndex[0]].name + |
|||
this.newProvinceDataList[1][this.multiIndex[1]].name + |
|||
this.newProvinceDataList[2][this.multiIndex[2]].name |
|||
|
|||
const result = { |
|||
provinceId: this.provinceId, |
|||
cityId: this.cityId, |
|||
areaId: this.areaId, |
|||
province: this.newProvinceDataList[0][this.multiIndex[0]]?.name || '', |
|||
city: this.newProvinceDataList[1][this.multiIndex[1]]?.name || '', |
|||
area: this.newProvinceDataList[2][this.multiIndex[2]]?.name || '', |
|||
fullText: selectedText |
|||
} |
|||
|
|||
this.$emit('change', result) |
|||
}, |
|||
|
|||
// 获取当前选中值 |
|||
getValue() { |
|||
const selectedText = this.newProvinceDataList[0][this.multiIndex[0]]?.name + |
|||
this.newProvinceDataList[1][this.multiIndex[1]]?.name + |
|||
this.newProvinceDataList[2][this.multiIndex[2]]?.name |
|||
|
|||
return { |
|||
provinceId: this.provinceId, |
|||
cityId: this.cityId, |
|||
areaId: this.areaId, |
|||
provinceName: this.newProvinceDataList[0][this.multiIndex[0]]?.name || '', |
|||
cityName: this.newProvinceDataList[1][this.multiIndex[1]]?.name || '', |
|||
areaName: this.newProvinceDataList[2][this.multiIndex[2]]?.name || '', |
|||
fullText: selectedText |
|||
} |
|||
}, |
|||
|
|||
// 重置选择 |
|||
reset() { |
|||
this.multiIndex = [0, 0, 0] |
|||
this.selectedText = '' |
|||
this.provinceId = null |
|||
this.cityId = null |
|||
this.areaId = null |
|||
this.initPickerData() |
|||
}, |
|||
|
|||
// 获取当前选中的详细信息(供插槽使用) |
|||
getCurrentSelection() { |
|||
if (!this.newProvinceDataList[0][this.multiIndex[0]] || |
|||
!this.newProvinceDataList[1][this.multiIndex[1]] || |
|||
!this.newProvinceDataList[2][this.multiIndex[2]]) { |
|||
return { |
|||
province: null, |
|||
city: null, |
|||
area: null, |
|||
fullText: '' |
|||
} |
|||
} |
|||
|
|||
return { |
|||
province: this.newProvinceDataList[0][this.multiIndex[0]], |
|||
city: this.newProvinceDataList[1][this.multiIndex[1]], |
|||
area: this.newProvinceDataList[2][this.multiIndex[2]], |
|||
fullText: this.newProvinceDataList[0][this.multiIndex[0]].name + |
|||
this.newProvinceDataList[1][this.multiIndex[1]].name + |
|||
this.newProvinceDataList[2][this.multiIndex[2]].name |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
.area-picker { |
|||
width: 100%; |
|||
} |
|||
|
|||
.picker-display { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
padding: 8rpx 16rpx; |
|||
border: 2rpx solid #e0e0e0; |
|||
border-radius: 8rpx; |
|||
background-color: #fff; |
|||
min-height: 60rpx; |
|||
} |
|||
|
|||
.picker-text { |
|||
font-size: 30rpx; |
|||
color: #333; |
|||
flex: 1; |
|||
|
|||
&.placeholder { |
|||
color: #999; |
|||
} |
|||
} |
|||
|
|||
.dropdown-icon { |
|||
width: 24rpx; |
|||
height: 16rpx; |
|||
margin-left: 8rpx; |
|||
} |
|||
</style> |
@ -0,0 +1,408 @@ |
|||
<template> |
|||
<view class="collection-page"> |
|||
<!-- 顶部标题区域 --> |
|||
<!-- 商品网格 --> |
|||
<view class="products-grid" v-if="collectionList.length > 0"> |
|||
<view |
|||
class="product-item" |
|||
:style="{ |
|||
'border-width': |
|||
index == collectionList.length - 1 || |
|||
index == collectionList.length - 2 |
|||
? 0 |
|||
: '0.5rpx', |
|||
}" |
|||
v-for="(item, index) in collectionList" |
|||
:key="index" |
|||
@click="goToDetail(item)" |
|||
> |
|||
<image |
|||
class="product-image" |
|||
src="https://images.unsplash.com/photo-1578662996442-48f60103fc96?auto=format&fit=crop&w=400" |
|||
mode="aspectFill" |
|||
></image> |
|||
<view class="product-info"> |
|||
<view class="product-title">{{ item.title }}</view> |
|||
<view class="product-price-box"> |
|||
<view class="product-price">{{ item.price }}</view> |
|||
<view class=""> |
|||
<image |
|||
@click.stop="handleUnlikeClick(item, index)" |
|||
class="heart-icon" |
|||
src="https://epic.js-dyyj.com/uploads/20250728/dd7ed269b24e84a2dd141da6ab980fd6.png" |
|||
> |
|||
</image> |
|||
</view> |
|||
</view> |
|||
<view class="product-details"> |
|||
<text class="detail-item" |
|||
>限量发售{{ item.publishQuantity }}份</text |
|||
> |
|||
<text class="detail-item" |
|||
>{{ item.purchaseQuantity || 0 }}人付款</text |
|||
> |
|||
<text class="detail-item">浏览{{ item.viewQuantity || 0 }}</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 空状态展示 --> |
|||
<view class="empty-state" v-else> |
|||
<view class="empty-content"> |
|||
<image |
|||
class="empty-icon" |
|||
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxjaXJjbGUgY3g9IjUwIiBjeT0iNTAiIHI9IjQ1IiBzdHJva2U9IiNEREREREQiIHN0cm9rZS13aWR0aD0iMiIgZmlsbD0ibm9uZSIvPgo8cGF0aCBkPSJNMzUgNDBINjVNMzUgNTBINjVNMzUgNjBINjUiIHN0cm9rZT0iI0RERERERCIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4KPC9zdmc+" |
|||
mode="widthFix" |
|||
></image> |
|||
<view class="empty-title">暂无收藏</view> |
|||
<view class="empty-desc">您还没有收藏任何商品</view> |
|||
<view class="empty-tip">去发现更多精彩商品吧</view> |
|||
<view class="empty-action" @click="goToDiscover"> |
|||
<text class="action-text">去发现</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: "UserCollection", |
|||
mixins: [require("@/mixins/myMixins.js")], |
|||
data() { |
|||
return { |
|||
collectionList: [], |
|||
sortValue: 0, // 0: 最新收藏, 1: 价格排序 |
|||
}; |
|||
}, |
|||
onLoad() { |
|||
this.loadCollectionList(); |
|||
}, |
|||
onShow() { |
|||
// 每次显示页面时刷新收藏列表 |
|||
this.loadCollectionList(); |
|||
}, |
|||
methods: { |
|||
// 加载收藏列表 |
|||
loadCollectionList() { |
|||
this.Post( |
|||
{ |
|||
sortValue: this.sortValue, |
|||
}, |
|||
"/framework/benefitPackage/collectList", |
|||
"DES" |
|||
) |
|||
.then((res) => { |
|||
if (res.data) { |
|||
this.collectionList = res.data; |
|||
} |
|||
}) |
|||
.catch((error) => { |
|||
console.error("加载收藏列表失败:", error); |
|||
// 使用模拟数据 |
|||
}); |
|||
}, |
|||
|
|||
// 模拟数据(当API不可用时使用) |
|||
loadMockData() { |
|||
this.collectionList = [ |
|||
{ |
|||
benefitPackageId: 1, |
|||
title: "苏州园林数字藏品", |
|||
price: "299", |
|||
coverUrl: |
|||
"https://images.unsplash.com/photo-1578662996442-48f60103fc96?auto=format&fit=crop&w=400", |
|||
publishQuantity: 1000, |
|||
purchaseQuantity: 156, |
|||
viewQuantity: 2340, |
|||
type: true, |
|||
}, |
|||
{ |
|||
benefitPackageId: 2, |
|||
title: "江南水乡文化资产", |
|||
price: "199", |
|||
coverUrl: |
|||
"https://images.unsplash.com/photo-1578662996442-48f60103fc96?auto=format&fit=crop&w=400", |
|||
publishQuantity: 500, |
|||
purchaseQuantity: 89, |
|||
viewQuantity: 1200, |
|||
type: true, |
|||
}, |
|||
{ |
|||
benefitPackageId: 3, |
|||
title: "吴文化数字纪念册", |
|||
price: "399", |
|||
coverUrl: |
|||
"https://images.unsplash.com/photo-1578662996442-48f60103fc96?auto=format&fit=crop&w=400", |
|||
publishQuantity: 200, |
|||
purchaseQuantity: 45, |
|||
viewQuantity: 890, |
|||
type: true, |
|||
}, |
|||
{ |
|||
benefitPackageId: 4, |
|||
title: "苏州丝绸数字资产", |
|||
price: "159", |
|||
coverUrl: |
|||
"https://images.unsplash.com/photo-1578662996442-48f60103fc96?auto=format&fit=crop&w=400", |
|||
publishQuantity: 800, |
|||
purchaseQuantity: 234, |
|||
viewQuantity: 1560, |
|||
type: true, |
|||
}, |
|||
]; |
|||
}, |
|||
|
|||
// 排序处理 |
|||
handleSort(sortType) { |
|||
this.sortValue = sortType; |
|||
this.loadCollectionList(); |
|||
}, |
|||
|
|||
// 取消收藏 |
|||
handleUnlikeClick(item, index) { |
|||
uni.showModal({ |
|||
title: "取消收藏", |
|||
content: "确定要取消收藏这个商品吗?", |
|||
success: (res) => { |
|||
if (res.confirm) { |
|||
this.Post( |
|||
{ |
|||
packageId: item.benefitPackageId, |
|||
type: false, |
|||
}, |
|||
"/framework/benefitPackage/collect", |
|||
"DES" |
|||
) |
|||
.then((res) => { |
|||
// 从列表中移除 |
|||
this.collectionList.splice(index, 1); |
|||
uni.showToast({ |
|||
title: "已取消收藏", |
|||
icon: "success", |
|||
}); |
|||
}) |
|||
.catch((error) => { |
|||
console.error("取消收藏失败:", error); |
|||
// 模拟成功 |
|||
this.collectionList.splice(index, 1); |
|||
uni.showToast({ |
|||
title: "已取消收藏", |
|||
icon: "success", |
|||
}); |
|||
}); |
|||
} |
|||
}, |
|||
}); |
|||
}, |
|||
|
|||
// 跳转到商品详情 |
|||
goToDetail(item) { |
|||
uni.navigateTo({ |
|||
url: `/subPackages/equityGoods/detail?id=${item.benefitPackageId}`, |
|||
}); |
|||
}, |
|||
|
|||
// 跳转到发现页面 |
|||
goToDiscover() { |
|||
uni.navigateTo({ |
|||
url: "/subPackages/equityGoods/list", |
|||
}); |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style> |
|||
page { |
|||
background-color: #f5f5f5; |
|||
} |
|||
</style> |
|||
|
|||
<style lang="scss" scoped> |
|||
.collection-page { |
|||
min-height: 100vh; |
|||
background: #f5f5f5; |
|||
padding-bottom: 40rpx; |
|||
} |
|||
|
|||
.header-section { |
|||
padding: 60rpx 30rpx 30rpx; |
|||
|
|||
.title { |
|||
font-size: 36rpx; |
|||
color: #000000; |
|||
text-align: center; |
|||
margin-bottom: 30rpx; |
|||
padding-bottom: 30rpx; |
|||
font-weight: 500; |
|||
border-bottom: 0.5rpx solid #999999; |
|||
} |
|||
|
|||
.filter-buttons { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
gap: 40rpx; |
|||
|
|||
.filter-btn { |
|||
font-size: 28rpx; |
|||
color: #000000; |
|||
border-radius: 20rpx; |
|||
transition: all 0.3s; |
|||
padding: 12rpx 24rpx; |
|||
|
|||
&.active { |
|||
background: #007aff; |
|||
color: white; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.products-grid { |
|||
display: grid; |
|||
grid-template-columns: 1fr 1fr; |
|||
gap: 20rpx; |
|||
padding: 30rpx; |
|||
|
|||
.product-item { |
|||
overflow: hidden; |
|||
background: white; |
|||
border-radius: 16rpx; |
|||
margin-bottom: 20rpx; |
|||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08); |
|||
transition: all 0.3s ease; |
|||
|
|||
&:active { |
|||
transform: scale(0.98); |
|||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.12); |
|||
} |
|||
|
|||
.product-image { |
|||
width: 100%; |
|||
height: 429rpx; |
|||
object-fit: cover; |
|||
border-radius: 20rpx; |
|||
} |
|||
|
|||
.product-info { |
|||
padding: 20rpx 24rpx 24rpx; |
|||
|
|||
.product-title { |
|||
font-size: 22rpx; |
|||
color: #000000; |
|||
margin-bottom: 16rpx; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
white-space: nowrap; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.product-price { |
|||
font-size: 36rpx; |
|||
font-weight: 500; |
|||
color: #000000; |
|||
} |
|||
|
|||
.product-details { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
|
|||
.detail-item { |
|||
font-size: 18rpx; |
|||
color: #808080; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 空状态样式 |
|||
.empty-state { |
|||
padding: 80rpx 30rpx; |
|||
min-height: 80vh; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
|
|||
.empty-content { |
|||
text-align: center; |
|||
max-width: 400rpx; |
|||
|
|||
.empty-icon { |
|||
width: 120rpx; |
|||
height: 120rpx; |
|||
margin: 0 auto 40rpx; |
|||
opacity: 0.6; |
|||
} |
|||
|
|||
.empty-title { |
|||
font-size: 32rpx; |
|||
color: #666666; |
|||
font-weight: 500; |
|||
margin-bottom: 20rpx; |
|||
} |
|||
|
|||
.empty-desc { |
|||
font-size: 26rpx; |
|||
color: #999999; |
|||
line-height: 1.5; |
|||
margin-bottom: 16rpx; |
|||
} |
|||
|
|||
.empty-tip { |
|||
font-size: 24rpx; |
|||
color: #bbbbbb; |
|||
line-height: 1.4; |
|||
margin-bottom: 40rpx; |
|||
} |
|||
|
|||
.empty-action { |
|||
display: inline-block; |
|||
padding: 20rpx 40rpx; |
|||
background: #007aff; |
|||
border-radius: 40rpx; |
|||
transition: all 0.3s; |
|||
|
|||
&:active { |
|||
transform: scale(0.95); |
|||
background: #0056cc; |
|||
} |
|||
|
|||
.action-text { |
|||
font-size: 28rpx; |
|||
color: white; |
|||
font-weight: 500; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.heart-icon { |
|||
width: 35rpx; |
|||
height: 30rpx; |
|||
transition: all 0.3s ease; |
|||
flex-shrink: 0; |
|||
top: -2rpx; |
|||
position: relative; |
|||
margin-right: 6rpx; |
|||
|
|||
&.liked { |
|||
opacity: 1; |
|||
filter: hue-rotate(320deg) saturate(2); |
|||
} |
|||
|
|||
&:active { |
|||
transform: scale(1.2); |
|||
} |
|||
} |
|||
|
|||
.product-price-box { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
margin-bottom: 10rpx; |
|||
} |
|||
</style> |
Loading…
Reference in new issue