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

@@ -1,35 +1,271 @@
<template>
<div>
<div class="userBox">
<div class="box">
<!-- 我的订单组件 -->
<myOrderPage :homePage="true" />
</div>
<div class="box">
<!-- 近期收藏 -->
<myFavorites :homePage="true" />
</div>
<div class="box">
<div class="member-home">
<div class="home-head">
<div>
<div class="home-hello">你好,{{ displayName }}</div>
<div class="home-sub">查看订单进度、优惠券和账户余额</div>
</div>
<button type="button" class="msg-entry" @click="go('MsgList')">
<el-icon :size="18"><Bell /></el-icon>
<span>我的消息</span>
<i v-if="unreadMsg > 0" class="msg-badge">{{ unreadLabel }}</i>
</button>
</div>
<div class="status-grid">
<button
v-for="item in orderStats"
:key="item.key"
type="button"
class="stat-card"
@click="item.action()"
>
<span class="stat-count">{{ item.count }}</span>
<span class="stat-label">{{ item.label }}</span>
</button>
</div>
<div class="asset-grid">
<button type="button" class="asset-card" @click="go('Coupons')">
<div>
<div class="asset-label">优惠券</div>
<div class="asset-value">{{ couponCount }}<span>张可用</span></div>
</div>
<span class="asset-link">查看</span>
</button>
<button type="button" class="asset-card" @click="go('MoneyManagement')">
<div>
<div class="asset-label">账户余额</div>
<div class="asset-value">¥{{ $filters.unitPrice(balance) }}</div>
</div>
<span class="asset-link">查看</span>
</button>
</div>
</div>
</template>
<script>
import myOrderPage from '@/pages/home/orderCenter/MyOrder'
import myFavorites from '@/pages/home/memberCenter/Favorites'
import { Bell } from "@element-plus/icons-vue";
import storage from "@/plugins/storage";
import { getOrderList } from "@/api/order";
import { afterSaleList, getMembersWallet, memberCouponList } from "@/api/member";
import { refreshUnreadMessage } from "@/utils/unreadMessage";
function totalOf(res) {
return res && res.success ? Number(res.result && res.result.total) || 0 : 0;
}
export default {
name: 'main',
components: {
myOrderPage,
myFavorites
}
}
name: "UserCenterHome",
components: { Bell },
data() {
return {
displayName: "会员",
waitPay: 0,
waitShip: 0,
waitReceive: 0,
afterSale: 0,
couponCount: 0,
balance: 0,
};
},
computed: {
unreadMsg() {
return Number(this.$store.state.unreadMsg) || 0;
},
unreadLabel() {
return this.unreadMsg > 99 ? "99+" : String(this.unreadMsg);
},
orderStats() {
return [
{ key: "pay", label: "待付款", count: this.waitPay, action: () => this.goOrder("WAIT_PAY") },
{ key: "ship", label: "待发货", count: this.waitShip, action: () => this.goOrder("WAIT_SHIP") },
{ key: "receive", label: "待收货", count: this.waitReceive, action: () => this.goOrder("WAIT_USE") },
{ key: "after", label: "售后", count: this.afterSale, action: () => this.go("AfterSale") },
];
},
},
mounted() {
this.readUser();
this.loadSummary();
},
methods: {
readUser() {
try {
const raw = storage.getItem("userInfo");
const user = raw ? JSON.parse(raw) : {};
this.displayName = user.nickName || user.username || "会员";
} catch {
this.displayName = "会员";
}
},
loadSummary() {
const page = { pageNumber: 1, pageSize: 1 };
Promise.all([
getOrderList({ ...page, tag: "WAIT_PAY" }).catch(() => null),
getOrderList({ ...page, tag: "WAIT_SHIP" }).catch(() => null),
getOrderList({ ...page, tag: "WAIT_USE" }).catch(() => null),
afterSaleList({ ...page, sort: "createTime", order: "desc" }).catch(() => null),
memberCouponList({ ...page, memberCouponStatus: "NEW" }).catch(() => null),
getMembersWallet().catch(() => null),
refreshUnreadMessage(),
]).then(([pay, ship, receive, afterSale, coupon, wallet]) => {
this.waitPay = totalOf(pay);
this.waitShip = totalOf(ship);
this.waitReceive = totalOf(receive);
this.afterSale = totalOf(afterSale);
this.couponCount = totalOf(coupon);
this.balance = wallet && wallet.success ? wallet.result.memberWallet || 0 : 0;
});
},
go(name) {
this.$router.push({ name });
},
goOrder(tag) {
this.$router.push({ name: "MyOrder", query: { tag } });
},
},
};
</script>
<style scoped lang="scss">
.userBox {
padding: 0 0 20px 0;
.member-home {
display: flex;
flex-direction: column;
gap: 16px;
}
.home-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 4px 4px 8px;
}
.home-hello {
color: #333;
font-size: 18px;
font-weight: 600;
line-height: 26px;
}
.home-sub {
margin-top: 4px;
color: #999;
font-size: 14px;
line-height: 22px;
}
.msg-entry {
display: inline-flex;
align-items: center;
gap: 6px;
height: 36px;
padding: 0 14px;
border: 1px solid #eee;
border-radius: 18px;
background: #fff;
color: #333;
font-size: 14px;
cursor: pointer;
&:hover {
color: $theme_color;
border-color: $theme_color;
}
}
.msg-badge {
min-width: 16px;
height: 16px;
padding: 0 4px;
border-radius: 8px;
background: $theme_color;
color: #fff;
font-size: 12px;
font-style: normal;
line-height: 16px;
text-align: center;
}
.status-grid,
.asset-grid {
display: grid;
gap: 12px;
}
.status-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.asset-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.stat-card,
.asset-card {
border: 1px solid #eee;
border-radius: 8px;
background: #fff;
cursor: pointer;
text-align: left;
&:hover {
border-color: $theme_color;
}
}
.stat-card {
display: flex;
flex-direction: column;
gap: 6px;
padding: 18px 16px;
}
.stat-count {
color: $theme_color;
font-size: 24px;
font-weight: 600;
line-height: 32px;
}
.stat-label {
color: #666;
font-size: 14px;
line-height: 22px;
}
.asset-card {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 20px;
}
.asset-label {
color: #999;
font-size: 14px;
line-height: 22px;
}
.asset-value {
margin-top: 4px;
color: #333;
font-size: 20px;
font-weight: 600;
line-height: 28px;
span {
margin-left: 4px;
color: #999;
font-size: 14px;
font-weight: 400;
}
}
.asset-link {
color: $theme_color;
font-size: 14px;
}
</style>

View File

@@ -1,44 +1,36 @@
<template>
<div class="wrapper">
<UserCenterLayout>
<div class="safeList">
<!-- 密码 -->
<div class="safeItem">
<div class="safeItem-icon">
<el-icon :size="40"><Key /></el-icon>
<div class="safe-list">
<div class="safe-card">
<div class="safe-icon">
<el-icon :size="22"><Key /></el-icon>
</div>
<div class="safeItem-content">
<div class="setDivItem">登录密码</div>
<div class="setDivItem theme">互联网账号存在被盗风险,建议您定期更改密码以保护账户安全。</div>
</div>
<div class="safeItem-action">
<el-button @click="modifyPwd">修改密码</el-button>
<div class="safe-body">
<p class="safe-title">登录密码</p>
<p class="safe-desc">互联网账号存在被盗风险,建议您定期更改密码以保护账户安全。</p>
</div>
<el-button @click="modifyPwd">修改密码</el-button>
</div>
<div class="safeItem safeItem-connect">
<div class="safeItem-main">
<div class="safeItem-icon">
<el-icon :size="40"><Link /></el-icon>
<div class="safe-card safe-card-connect">
<div class="safe-card-main">
<div class="safe-icon">
<el-icon :size="22"><Link /></el-icon>
</div>
<div class="safeItem-content">
<div class="setDivItem">第三方账户绑定</div>
<div class="setDivItem theme">绑定后可使用第三方账号快捷登录。</div>
</div>
<div class="safeItem-action">
<el-button v-if="!isBound(wechatConnect.type)" type="primary" @click="openBindModal(wechatConnect)">绑定</el-button>
<el-button v-else @click="confirmUnbind(wechatConnect)">解绑</el-button>
<div class="safe-body">
<p class="safe-title">第三方账户绑定</p>
<p class="safe-desc">绑定后可使用第三方账号快捷登录。</p>
</div>
<el-button v-if="!isBound(wechatConnect.type)" type="primary" @click="openBindModal(wechatConnect)">绑定</el-button>
<el-button v-else @click="confirmUnbind(wechatConnect)">解绑</el-button>
</div>
<div class="connectList">
<div class="connectRow">
<div class="connectRowLeft">
<span class="connectName">{{ wechatConnect.label }}</span>
<el-tag v-if="isBound(wechatConnect.type)" color="success">已绑定</el-tag>
<el-tag v-else>未绑定</el-tag>
</div>
</div>
<el-skeleton size="large" fix v-if="connectLoading"></el-skeleton>
<div class="connect-row">
<span class="connect-name">{{ wechatConnect.label }}</span>
<span class="connect-status" :class="{ 'is-bound': isBound(wechatConnect.type) }">
{{ isBound(wechatConnect.type) ? "已绑定" : "未绑定" }}
</span>
<el-skeleton v-if="connectLoading" animated class="connect-skeleton" />
</div>
</div>
</div>
@@ -273,73 +265,101 @@ export default {
</script>
<style scoped lang="scss">
.theme {
color: $theme_color;
.safe-list {
display: flex;
flex-direction: column;
gap: 12px;
padding-top: 4px;
}
.setDivItem {
line-height: 1.75;
}
.safeItem {
.safe-card,
.safe-card-main {
display: flex;
align-items: center;
gap: 16px;
border-bottom: 1px solid $border_color;
padding: 16px 0;
}
.safeItem-connect {
.safe-card {
padding: 20px;
border: 1px solid #f0f0f0;
border-radius: 8px;
}
.safe-card-connect {
display: block;
padding: 0;
}
.safeItem-main {
display: flex;
align-items: center;
gap: 16px;
.safe-card-main {
padding: 20px 20px 16px;
}
.safeItem-icon {
.safe-icon {
flex-shrink: 0;
width: 56px;
width: 48px;
height: 48px;
border-radius: 8px;
background: #fff5f4;
color: $theme_color;
display: flex;
align-items: center;
justify-content: center;
color: $theme_color;
}
.safeItem-content {
.safe-body {
flex: 1;
min-width: 0;
}
.safeItem-action {
flex-shrink: 0;
display: flex;
align-items: center;
.safe-title {
margin: 0;
color: #171A26;
font-size: 16px;
font-weight: 600;
line-height: 24px;
}
.connectList {
.safe-desc {
margin: 6px 0 0;
color: #999;
font-size: 13px;
line-height: 20px;
}
.connect-row {
position: relative;
margin-top: 8px;
margin-left: 72px;
}
.connectRow {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 0;
}
.connectRowLeft {
display: flex;
align-items: center;
gap: 10px;
margin: 0 20px 16px 84px;
padding-top: 12px;
border-top: 1px solid #f5f5f5;
}
.connectName {
color: $title_color;
.connect-name {
color: #171A26;
font-size: 14px;
line-height: 22px;
}
.connect-status {
height: 22px;
padding: 0 8px;
border-radius: 4px;
background: #f5f5f5;
color: #999;
font-size: 12px;
line-height: 22px;
}
.connect-status.is-bound {
background: #f0faf3;
color: #1f9d55;
}
.connect-skeleton {
width: 120px;
height: 22px;
}
.wechatQrBox {

View File

@@ -280,3 +280,9 @@ export default {
}
}
</style>

View File

@@ -200,11 +200,10 @@ export default {
},
goShop(id) { // 跳转店铺页面
let url = this.$router.resolve({
this.$router.push({
path: '/merchant',
query: { id }
})
window.open(url.href, '_blank')
}
},
mounted() {

View File

@@ -25,6 +25,7 @@
<script>
import { Message, Modal } from "@/utils/message";
import { delMemberMsg, getMemberMessage, readMemberMsg } from '@/api/member.js';
import { refreshUnreadMessage } from '@/utils/unreadMessage';
const CACHE_KEY = 'buyer-member-message';
@@ -71,7 +72,10 @@ export default {
markRead () {
if (!this.detail.id || this.detail.status !== 'UN_READY') return
readMemberMsg(this.detail.id).then(res => {
if (res && res.success) this.detail.status = 'ALREADY_READY'
if (res && res.success) {
this.detail.status = 'ALREADY_READY'
refreshUnreadMessage()
}
}).catch(() => {})
},
goBack () {
@@ -86,6 +90,7 @@ export default {
delMemberMsg(this.detail.id).then((res) => {
if (res.success) {
Message.success('消息已成功放入回收站')
refreshUnreadMessage()
this.goBack()
}
})

View File

@@ -94,6 +94,9 @@ export default {
memberMsgList(this.params).then(res => {
if (res.success) {
this.messageData = res.result;
if (this.params.status === 'UN_READY') {
this.$store.commit('SET_UNREAD_MSG', Number(res.result.total) || 0);
}
}
})
},

View File

@@ -212,11 +212,10 @@ export default {
},
// 跳转店铺首页
shopPage(id) {
let routeUrl = this.$router.resolve({
this.$router.push({
path: '/merchant',
query: {id: id}
});
window.open(routeUrl.href, '_blank');
},
change(index) {
this.activeTab = index

View File

@@ -211,11 +211,10 @@ export default {
},
shopPage (id) {
if (!id) return
const routeUrl = this.$router.resolve({
this.$router.push({
path: '/merchant',
query: { id }
})
window.open(routeUrl.href, '_blank')
},
filterOrderStatus (status) { // 获取订单状态中文
const ob = this.afterSaleStatusList.filter(e => { return e.status === status });

View File

@@ -223,7 +223,7 @@ export default {
// 状态数组
orderStatusList,
afterSaleStatusList,
changeWay: ['全部', '待支付', '待发货', '待收货/使用'],
changeWay: ['全部', '待付款', '待发货', '待收货/使用'],
activeOrderTab: 0,
total: 0, // 数据总数
spinShow: false, // 加载状态
@@ -235,8 +235,18 @@ export default {
},
mounted () {
if (this.homePage) this.params.pageSize = 5;
if (!this.homePage) this.applyRouteTag();
this.getList();
},
watch: {
'$route.query.tag' () {
if (this.homePage || this.$route.name !== 'MyOrder') return;
const next = this.$route.query.tag || 'ALL';
if (this.params.tag === next) return;
this.applyRouteTag();
this.getList();
}
},
methods: {
// 退款状态枚举
refundPriceList(status) {
@@ -261,22 +271,39 @@ export default {
});
window.open(routeUrl.href, '_blank');
},
orderTags () {
return ['ALL', 'WAIT_PAY', 'WAIT_SHIP', 'WAIT_USE'];
},
applyRouteTag () {
const tags = this.orderTags();
const tag = this.$route.query.tag;
const index = tags.indexOf(tag);
this.activeOrderTab = index > -1 ? index : 0;
this.params.tag = tags[this.activeOrderTab];
this.params.pageNumber = 1;
},
// 切换订单状态
change (index) {
this.activeOrderTab = index;
this.params.pageNumber = 1;
delete this.params.commentStatus;
const tags = ['ALL', 'WAIT_PAY', 'WAIT_SHIP', 'WAIT_USE'];
const tags = this.orderTags();
this.params.tag = tags[index] || 'ALL';
const query = { ...this.$route.query };
if (index === 0) delete query.tag;
else query.tag = this.params.tag;
const current = this.$route.query.tag || 'ALL';
if (current !== this.params.tag) {
this.$router.replace({ name: 'MyOrder', query });
}
this.getList()
},
// 跳转店铺首页
shopPage (id) {
let routeUrl = this.$router.resolve({
this.$router.push({
path: '/merchant',
query: { id: id }
});
window.open(routeUrl.href, '_blank');
},
orderDetail (sn) {
this.$router.push({ name: 'OrderDetail', query: {sn} });

View File

@@ -589,11 +589,10 @@ export default {
},
// 跳转店铺首页
shopPage(id) {
let routeUrl = this.$router.resolve({
this.$router.push({
path: "/merchant",
query: { id: id },
});
window.open(routeUrl.href, "_blank");
},
async getDetail() {
// 获取订单详情

View File

@@ -72,11 +72,10 @@ export default {
},
// 跳转店铺首页
shopPage(id) {
let routeUrl = this.$router.resolve({
this.$router.push({
path: "/merchant",
query: { id: id },
});
window.open(routeUrl.href, "_blank");
},
clearAll() {
// 清除全部足迹