feat(buyer): 增强搜索功能并提升用户体验

- 新增 `getStoreById` 接口方法,用于获取包含评分在内的店铺信息
- `Search.vue` 新增搜索建议面板,展示搜索历史和热词,优化搜索体验
- 新增 `SearchSuggest.vue` 组件,用于展示搜索建议和搜索历史
- `App.vue` 根据路由元信息按条件设置页面标题
- 优化多个组件的布局与样式,让整体风格更统一
- 重构多个组件的跳转逻辑,改用路由跳转(`router.push`)替代新开窗口(`window.open`),提升用户体验
This commit is contained in:
pikachu1995@126.com
2026-10-02 15:26:58 +08:00
parent 4777eb2dd6
commit 8410179885
44 changed files with 2116 additions and 674 deletions

View File

@@ -2,55 +2,43 @@
<div class="coupon-center">
<BaseHeader></BaseHeader>
<div class="content">
<div>
<div class="coupon-title">
<router-link to="/">
<img src="../assets/images/logo.png" width="120" alt="" />
</router-link>
<p>领券中心</p>
<el-input
v-model="keyword"
style="width: 400px"
@keyup.enter="search"
placeholder="搜索优惠券"
/>
</div>
<h2 class="coupon-page-title">领券中心</h2>
<div class="fontsize_18 recommend">推荐好券</div>
<empty v-if="!loading && list.length === 0" />
<ul class="coupon-list" v-else-if="list.length > 0">
<li v-for="(item, index) in list" class="coupon-item" :key="index">
<div class="c-left">
<div>
<span
v-if="item.couponType === 'PRICE'"
class="fontsize_12 global_color"
>¥<span class="price">{{ $filters.unitPrice(item.price) }}</span></span>
<span
v-if="item.couponType === 'DISCOUNT'"
class="fontsize_12 global_color"
><span class="price">{{ item.couponDiscount || item.discount }}</span>折</span>
<span class="describe"
>满{{ item.consumeThreshold }}元可用</span>
</div>
<p>使用范围:{{ useScope(item.scopeType, item.storeName) }}</p>
<p>有效期:{{ item.endTime }}</p>
<empty v-if="!loading && list.length === 0" />
<ul class="coupon-list" v-else-if="list.length > 0">
<li v-for="item in list" class="coupon-item" :key="item.id">
<div class="c-left">
<div class="coupon-amount">
<span v-if="item.couponType === 'PRICE'" class="global_color">
¥<span class="price">{{ $filters.unitPrice(item.price) }}</span>
</span>
<span v-if="item.couponType === 'DISCOUNT'" class="global_color">
<span class="price">{{ item.couponDiscount || item.discount }}</span>折
</span>
<span class="describe">{{ thresholdText(item) }}</span>
</div>
<b></b>
<a class="c-right" @click="receive(item)">立即领取</a>
<i class="circle-top"></i>
<i class="circle-bottom"></i>
</li>
</ul>
<el-pagination v-model:current-page="params.pageNumber"
v-model:page-size="params.pageSize"
:total="total"
@current-change="changePageNum"
class="pageration"
@size-change="changePageSize"
layout="total, sizes, prev, pager, next"></el-pagination>
</div>
<p class="coupon-name">
<span class="scope-tag">{{ scopeTag(item.scopeType, item.storeName) }}</span>
<span>{{ item.couponName }}</span>
</p>
<p class="coupon-time">有效期:{{ item.endTime }}</p>
</div>
<a
class="c-right"
:class="{ 'is-finished': isFinished(item) }"
@click="receive(item)"
>{{ isFinished(item) ? "已领完" : "立即领取" }}</a>
</li>
</ul>
<el-pagination
v-model:current-page="params.pageNumber"
v-model:page-size="params.pageSize"
:total="total"
@current-change="changePageNum"
class="pageration"
@size-change="changePageSize"
layout="total, sizes, prev, pager, next"
></el-pagination>
</div>
<BaseFooter></BaseFooter>
</div>
@@ -65,7 +53,6 @@ export default {
list: [], // 优惠券列表
total: 0, // 优惠券总数
loading: false,
keyword: "",
params: {
// 请求参数
getType: "FREE",
@@ -75,13 +62,6 @@ export default {
};
},
methods: {
// 搜索优惠券
search() {
this.params.couponName = this.keyword.trim();
this.params.pageNumber = 1;
this.getList();
},
// 获取优惠券列表
getList() {
if (this.loading) return;
this.loading = true;
@@ -116,6 +96,7 @@ export default {
},
// 领取优惠券
receive(item) {
if (this.isFinished(item)) return;
const userInfo = storage.getItem("userInfo");
const isLogin = userInfo && JSON.parse(userInfo).username;
if (!isLogin) {
@@ -158,23 +139,22 @@ export default {
})
.catch(() => {});
},
// 优惠券可用范围
useScope(type, storeName) {
let shop = "平台";
let goods = "全部商品";
if (storeName !== "platform") shop = storeName;
switch (type) {
case "ALL":
goods = "全部商品";
break;
case "PORTION_GOODS":
goods = "部分商品";
break;
case "PORTION_GOODS_CATEGORY":
goods = "部分分类商品";
break;
}
return `${shop}${goods}可用`;
// 发行量已领完
isFinished(item) {
return Number(item.publishNum) > 0 && Number(item.receivedNum) >= Number(item.publishNum);
},
// 使用门槛文案
thresholdText(item) {
const threshold = Number(item.consumeThreshold);
if (!threshold) return "无使用门槛";
return `满${item.consumeThreshold}元可用`;
},
// 券范围。店铺名为 platform 时是平台通用券
scopeTag(type, storeName) {
if (type === "PORTION_GOODS") return "商品券";
if (type === "PORTION_GOODS_CATEGORY") return "分类券";
if (storeName && storeName !== "platform") return "店铺券";
return "通用券";
},
},
mounted() {
@@ -184,84 +164,156 @@ export default {
</script>
<style lang="scss" scoped>
@import "../assets/styles/coupon.scss";
.coupon-center {
background: #f5f5f5;
}
.content {
width: 100%;
background-color: #fff;
> div {
margin: 10px auto;
width: 1200px;
}
width: 1200px;
margin: 0 auto 48px;
background: transparent;
}
.coupon-title {
display: flex;
align-items: center;
p {
font-size: 18px;
margin-right: 500px;
}
border-bottom: 2px solid $theme_color;
}
.recommend {
margin: 20px auto;
font-weight: bold;
width: 200px;
.coupon-page-title {
margin: 28px 0 20px;
color: #171A26;
font-size: 28px;
font-weight: 600;
text-align: center;
}
.coupon-item {
overflow: hidden;
$claim-width: 55px;
.c-left {
width: calc(100% - #{$claim-width});
box-sizing: border-box;
.coupon-list {
gap: 16px;
margin: 0;
padding: 0;
list-style: none;
}
.coupon-item {
width: calc((100% - 32px) / 3);
height: auto;
min-height: 132px;
margin: 0;
display: flex;
align-items: stretch;
overflow: hidden;
border: none;
border-radius: 8px;
background: #fff;
b,
i.circle-top,
i.circle-bottom {
display: none;
}
b {
right: $claim-width;
width: 0;
background: none;
border-right: 1px dashed #e0e0e0;
.c-left {
flex: 1;
width: auto;
min-width: 0;
padding: 16px 16px 14px;
box-sizing: border-box;
.coupon-amount {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 10px;
}
.price {
font-size: 28px;
font-weight: 600;
line-height: 1;
}
.describe {
margin-left: 0;
padding: 0;
background: transparent;
font-size: 13px;
line-height: 20px;
}
.coupon-name {
display: flex;
align-items: center;
gap: 6px;
margin: 0 0 8px;
color: #171A26;
font-size: 14px;
font-weight: 400;
line-height: 20px;
}
.scope-tag {
flex-shrink: 0;
padding: 0 6px;
border-radius: 2px;
background: #fff5f4;
color: $theme_color;
font-size: 12px;
line-height: 20px;
}
.coupon-name span:last-child {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.coupon-time {
margin: 0;
color: #999;
font-size: 12px;
font-weight: 400;
line-height: 18px;
}
}
.c-right {
width: $claim-width;
position: relative;
right: auto;
top: auto;
flex-shrink: 0;
box-sizing: border-box;
width: 48px;
height: auto;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
writing-mode: vertical-rl;
text-orientation: mixed;
letter-spacing: 4px;
font-size: 16px;
white-space: nowrap;
font-size: 14px;
font-weight: 600;
line-height: 1;
cursor: pointer;
text-decoration: none;
user-select: none;
&:hover {
opacity: 0.9;
opacity: 0.92;
}
&.is-finished {
background: #c8c8c8;
cursor: default;
&:hover {
opacity: 1;
}
}
}
i.circle-top,
i.circle-bottom {
right: calc(#{$claim-width} - 9px);
width: 18px;
height: 18px;
}
i.circle-top {
top: -9px;
}
i.circle-bottom {
bottom: -9px;
}
}
.pageration {
text-align: right;
padding-bottom: 10px;
display: flex;
justify-content: flex-end;
margin-top: 20px;
padding-bottom: 0;
}
</style>