mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-11-07 17:38:55 +08:00
feat(buyer): 增强搜索功能并提升用户体验
- 新增 `getStoreById` 接口方法,用于获取包含评分在内的店铺信息 - `Search.vue` 新增搜索建议面板,展示搜索历史和热词,优化搜索体验 - 新增 `SearchSuggest.vue` 组件,用于展示搜索建议和搜索历史 - `App.vue` 根据路由元信息按条件设置页面标题 - 优化多个组件的布局与样式,让整体风格更统一 - 重构多个组件的跳转逻辑,改用路由跳转(`router.push`)替代新开窗口(`window.open`),提升用户体验
This commit is contained in:
@@ -23,7 +23,9 @@ export default {
|
||||
storage.setItem("siteIcon", data.buyerSideIcon);
|
||||
this.$store.commit("SET_SITENAME", data.siteName);
|
||||
this.$store.commit("SET_LOGOIMG", data.buyerSideLogo);
|
||||
window.document.title = data.siteName;
|
||||
if (this.$route.meta.title === undefined) {
|
||||
window.document.title = data.siteName;
|
||||
}
|
||||
let link =
|
||||
document.querySelector("link[rel*='icon']") ||
|
||||
document.createElement("link");
|
||||
|
||||
@@ -42,6 +42,15 @@ export function applyThird (params) {
|
||||
})
|
||||
}
|
||||
|
||||
// 店铺信息(含评分)
|
||||
export function getStoreById (id) {
|
||||
return request({
|
||||
url: `/buyer/store/store/store/${id}`,
|
||||
needToken: false,
|
||||
method: Method.GET
|
||||
})
|
||||
}
|
||||
|
||||
// 店铺详情
|
||||
export function getDetailById (id) {
|
||||
return request({
|
||||
|
||||
@@ -7,11 +7,24 @@
|
||||
size="large"
|
||||
class="search-input"
|
||||
placeholder="输入你想查找的商品"
|
||||
@focus="openPanel"
|
||||
@input="queueSuggest"
|
||||
@keyup.enter="search"
|
||||
/>
|
||||
<div class="search-btn" @click="search">
|
||||
<el-icon :size="21"><SearchIcon /></el-icon>
|
||||
</div>
|
||||
<div v-if="showPanel" class="suggest-panel" @mousedown.prevent>
|
||||
<SearchSuggest
|
||||
:keyword="trimmedKeyword"
|
||||
:history="history"
|
||||
:hot-words="promotionTags"
|
||||
:suggestions="suggestions"
|
||||
:suggesting="suggesting"
|
||||
@pick="selectTags"
|
||||
@clear="clearHistory"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -30,11 +43,24 @@
|
||||
size="large"
|
||||
class="search-input"
|
||||
placeholder="输入你想查找的商品"
|
||||
@focus="openPanel"
|
||||
@input="queueSuggest"
|
||||
@keyup.enter="search"
|
||||
/>
|
||||
<div class="search-btn" @click="search">
|
||||
<el-icon :size="21"><SearchIcon /></el-icon>
|
||||
</div>
|
||||
<div v-if="showPanel" class="suggest-panel" @mousedown.prevent>
|
||||
<SearchSuggest
|
||||
:keyword="trimmedKeyword"
|
||||
:history="history"
|
||||
:hot-words="promotionTags"
|
||||
:suggestions="suggestions"
|
||||
:suggesting="suggesting"
|
||||
@pick="selectTags"
|
||||
@clear="clearHistory"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="showTag">
|
||||
<div v-if="promotionTags.length === 0"></div>
|
||||
@@ -56,22 +82,49 @@
|
||||
|
||||
<script>
|
||||
import storage from '@/plugins/storage.js'
|
||||
import {hotWords} from '@/api/goods.js'
|
||||
import { goodsList, hotWords } from '@/api/goods.js'
|
||||
import { Search as SearchIcon } from '@element-plus/icons-vue'
|
||||
import SearchSuggest from '@/components/SearchSuggest.vue'
|
||||
|
||||
const HISTORY_KEY = 'buyerSearchHistory'
|
||||
const HISTORY_LIMIT = 10
|
||||
|
||||
function readHistory() {
|
||||
try {
|
||||
const raw = localStorage.getItem(HISTORY_KEY)
|
||||
const list = raw ? JSON.parse(raw) : []
|
||||
return Array.isArray(list) ? list.filter((item) => typeof item === 'string' && item.trim()) : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function parseHotWords(raw) {
|
||||
if (!raw) return []
|
||||
if (Array.isArray(raw)) return raw.filter(Boolean)
|
||||
if (typeof raw !== 'string') return []
|
||||
try {
|
||||
const parsed = JSON.parse(raw)
|
||||
if (Array.isArray(parsed)) return parsed.filter(Boolean)
|
||||
} catch {
|
||||
// 旧数据可能是逗号拼接
|
||||
}
|
||||
return raw.split(',').map((item) => item.trim()).filter(Boolean)
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'search',
|
||||
components: { SearchIcon },
|
||||
components: { SearchIcon, SearchSuggest },
|
||||
props: {
|
||||
showTag: { // 是否展示搜索栏下方热门搜索
|
||||
showTag: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showLogo: { // 是否展示左侧logo
|
||||
showLogo: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
storeId: { // 是否为店铺页面
|
||||
storeId: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
@@ -86,64 +139,148 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchData: '', // 搜索内容
|
||||
searchData: '',
|
||||
history: [],
|
||||
panelVisible: false,
|
||||
suggestions: [],
|
||||
suggesting: false,
|
||||
suggestTimer: null,
|
||||
suggestSeq: 0,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
selectTags(item) { // 选择热门标签
|
||||
selectTags(item) {
|
||||
this.searchData = item;
|
||||
this.search();
|
||||
},
|
||||
search () { // 全平台搜索商品
|
||||
remember(keyword) {
|
||||
const next = [keyword, ...this.history.filter((item) => item !== keyword)].slice(0, HISTORY_LIMIT)
|
||||
this.history = next
|
||||
localStorage.setItem(HISTORY_KEY, JSON.stringify(next))
|
||||
},
|
||||
clearHistory() {
|
||||
this.history = []
|
||||
localStorage.removeItem(HISTORY_KEY)
|
||||
},
|
||||
openPanel() {
|
||||
this.panelVisible = true
|
||||
if (this.trimmedKeyword) this.queueSuggest()
|
||||
},
|
||||
closePanel() {
|
||||
this.panelVisible = false
|
||||
},
|
||||
onDocumentClick(event) {
|
||||
if (this.$el && !this.$el.contains(event.target)) this.closePanel()
|
||||
},
|
||||
queueSuggest() {
|
||||
this.panelVisible = true
|
||||
clearTimeout(this.suggestTimer)
|
||||
const keyword = this.trimmedKeyword
|
||||
if (!keyword) {
|
||||
this.suggestions = []
|
||||
this.suggesting = false
|
||||
return
|
||||
}
|
||||
this.suggesting = true
|
||||
this.suggestTimer = setTimeout(() => this.fetchSuggest(keyword), 300)
|
||||
},
|
||||
fetchSuggest(keyword) {
|
||||
const seq = ++this.suggestSeq
|
||||
const params = { keyword, pageNumber: 1, pageSize: 8 }
|
||||
if (this.storeId) params.storeId = this.storeId
|
||||
goodsList(params).then((res) => {
|
||||
if (seq !== this.suggestSeq || keyword !== this.trimmedKeyword) return
|
||||
const records = res && res.success ? (res.result.records || []) : []
|
||||
const seen = new Set()
|
||||
const names = []
|
||||
this.promotionTags.forEach((word) => {
|
||||
if (word && String(word).toLowerCase().includes(keyword.toLowerCase()) && !seen.has(word)) {
|
||||
seen.add(word)
|
||||
names.push(word)
|
||||
}
|
||||
})
|
||||
records.forEach((item) => {
|
||||
const name = item && item.goodsName
|
||||
if (name && !seen.has(name)) {
|
||||
seen.add(name)
|
||||
names.push(name)
|
||||
}
|
||||
})
|
||||
this.suggestions = names.slice(0, 8)
|
||||
}).catch(() => {
|
||||
if (seq === this.suggestSeq) this.suggestions = []
|
||||
}).finally(() => {
|
||||
if (seq === this.suggestSeq) this.suggesting = false
|
||||
})
|
||||
},
|
||||
search () {
|
||||
const keyword = this.trimmedKeyword
|
||||
this.searchData = keyword
|
||||
this.closePanel()
|
||||
if (keyword) this.remember(keyword)
|
||||
const url = this.$route.path;
|
||||
if(url == '/goodsList'){
|
||||
this.$emit('search', this.searchData)
|
||||
this.$emit('search', keyword)
|
||||
}else{
|
||||
const pushData = {
|
||||
path:'/goodsList',
|
||||
query: { keyword: this.searchData },
|
||||
query: { keyword },
|
||||
}
|
||||
if(this.storeId) pushData.query.storeId = this.storeId
|
||||
|
||||
this.$router.push(pushData);
|
||||
}
|
||||
},
|
||||
searchStore() { // 店铺搜索商品
|
||||
this.$emit('search', this.searchData)
|
||||
searchStore() {
|
||||
this.$emit('search', this.trimmedKeyword)
|
||||
},
|
||||
applyHotWords(result) {
|
||||
const list = parseHotWords(result)
|
||||
this.$store.commit('SET_HOTWORDS', list)
|
||||
storage.setItem('hotWordsList', JSON.stringify(list))
|
||||
},
|
||||
loadHotWords() {
|
||||
hotWords({ count: 5 }).then((res) => {
|
||||
if (res.success && res.result) this.applyHotWords(res.result)
|
||||
}).catch(() => {})
|
||||
storage.setItem('hotWordsReloadTime', new Date().getTime())
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
trimmedKeyword() {
|
||||
return (this.searchData || '').trim()
|
||||
},
|
||||
promotionTags() {
|
||||
const raw = this.$store.state.hotWordsList;
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
return typeof raw === "string" ? JSON.parse(raw) : raw;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
return parseHotWords(this.$store.state.hotWordsList)
|
||||
},
|
||||
showPanel() {
|
||||
if (!this.panelVisible) return false
|
||||
if (this.trimmedKeyword) return true
|
||||
return this.history.length > 0 || this.promotionTags.length > 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route.query.keyword'(value) {
|
||||
this.searchData = value || ''
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.searchData = this.$route.query.keyword
|
||||
this.searchData = this.$route.query.keyword || ''
|
||||
this.history = readHistory()
|
||||
|
||||
if (!this.hover) { // 首页顶部固定搜索栏不调用热词接口
|
||||
// 搜索热词每5分钟请求一次
|
||||
if (!this.hover) {
|
||||
const reloadTime = storage.getItem('hotWordsReloadTime')
|
||||
const time = new Date().getTime() - 5 * 60 * 1000
|
||||
if (!reloadTime) {
|
||||
hotWords({count: 5}).then(res => {
|
||||
if (res.success && res.result) storage.setItem('hotWordsList', res.result)
|
||||
}).catch(() => {})
|
||||
storage.setItem('hotWordsReloadTime', new Date().getTime())
|
||||
} else if (reloadTime && time > reloadTime) {
|
||||
hotWords({count: 5}).then(res => {
|
||||
if (res.success && res.result) storage.setItem('hotWordsList', res.result)
|
||||
}).catch(() => {})
|
||||
storage.setItem('hotWordsReloadTime', new Date().getTime())
|
||||
}
|
||||
if (!reloadTime || time > reloadTime) this.loadHotWords()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.onDocumentClick = this.onDocumentClick.bind(this)
|
||||
document.addEventListener('click', this.onDocumentClick)
|
||||
},
|
||||
beforeUnmount() {
|
||||
clearTimeout(this.suggestTimer)
|
||||
document.removeEventListener('click', this.onDocumentClick)
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -169,7 +306,6 @@ export default {
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -217,6 +353,11 @@ export default {
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.search-embedded.search {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-box{
|
||||
margin-left: 28px;
|
||||
}
|
||||
@@ -225,28 +366,7 @@ export default {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.store-search {
|
||||
width: 55.6px;
|
||||
padding: 0 9px;
|
||||
border-radius: 0;
|
||||
border-radius: 3px;
|
||||
|
||||
&:nth-child(2) {
|
||||
width: 55px;
|
||||
margin-left: -2px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-div {
|
||||
position: relative;
|
||||
height: 0px;
|
||||
top: -38px;
|
||||
left: 352px;
|
||||
}
|
||||
|
||||
.history-list {
|
||||
|
||||
margin-left: 28px;
|
||||
}
|
||||
|
||||
@@ -259,4 +379,18 @@ export default {
|
||||
color: #666666;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.suggest-panel {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: calc(100% + 8px);
|
||||
z-index: 30;
|
||||
padding: 8px 0;
|
||||
background: #fff;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
151
buyer/src/components/SearchSuggest.vue
Normal file
151
buyer/src/components/SearchSuggest.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="keyword" class="suggest-section">
|
||||
<div v-if="suggesting && !suggestions.length" class="suggest-empty">正在查找...</div>
|
||||
<button
|
||||
v-for="item in suggestions"
|
||||
:key="item"
|
||||
type="button"
|
||||
class="suggest-item"
|
||||
@click="$emit('pick', item)"
|
||||
>
|
||||
<span v-for="(part, index) in parts(item)" :key="index" :class="{ hit: part.hit }">{{ part.text }}</span>
|
||||
</button>
|
||||
<div v-if="!suggesting && !suggestions.length" class="suggest-empty">没有匹配的商品,回车查看全部结果</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div v-if="history.length" class="suggest-section">
|
||||
<div class="suggest-head">
|
||||
<span>搜索历史</span>
|
||||
<button type="button" class="suggest-clear" @click="$emit('clear')">清空</button>
|
||||
</div>
|
||||
<div class="suggest-tags">
|
||||
<button
|
||||
v-for="item in history"
|
||||
:key="item"
|
||||
type="button"
|
||||
class="suggest-tag"
|
||||
@click="$emit('pick', item)"
|
||||
>{{ item }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hotWords.length" class="suggest-section">
|
||||
<div class="suggest-head"><span>热门搜索</span></div>
|
||||
<div class="suggest-tags">
|
||||
<button
|
||||
v-for="item in hotWords"
|
||||
:key="item"
|
||||
type="button"
|
||||
class="suggest-tag"
|
||||
@click="$emit('pick', item)"
|
||||
>{{ item }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SearchSuggest",
|
||||
props: {
|
||||
keyword: { type: String, default: "" },
|
||||
history: { type: Array, default: () => [] },
|
||||
hotWords: { type: Array, default: () => [] },
|
||||
suggestions: { type: Array, default: () => [] },
|
||||
suggesting: { type: Boolean, default: false },
|
||||
},
|
||||
emits: ["pick", "clear"],
|
||||
methods: {
|
||||
parts(text) {
|
||||
const value = String(text || "");
|
||||
const key = this.keyword;
|
||||
if (!key) return [{ text: value, hit: false }];
|
||||
const index = value.toLowerCase().indexOf(key.toLowerCase());
|
||||
if (index < 0) return [{ text: value, hit: false }];
|
||||
return [
|
||||
{ text: value.slice(0, index), hit: false },
|
||||
{ text: value.slice(index, index + key.length), hit: true },
|
||||
{ text: value.slice(index + key.length), hit: false },
|
||||
].filter((part) => part.text);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.suggest-section + .suggest-section {
|
||||
margin-top: 4px;
|
||||
padding-top: 4px;
|
||||
border-top: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.suggest-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 16px;
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.suggest-clear {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.suggest-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 0 16px 8px;
|
||||
}
|
||||
|
||||
.suggest-tag {
|
||||
max-width: 180px;
|
||||
padding: 4px 10px;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.suggest-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.hit {
|
||||
color: $theme_color;
|
||||
}
|
||||
}
|
||||
|
||||
.suggest-empty {
|
||||
padding: 10px 16px;
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
@@ -26,7 +26,7 @@
|
||||
v-for="(img,imgIndex) in order.orderItems"
|
||||
:key="imgIndex" width="40" height="40" alt="">
|
||||
</div>
|
||||
<div class="order-handle"><span>{{ $filters.unitPrice(order.flowPrice, "¥") }}</span><span class="hover-color" @click="linkTo(`home/OrderDetail?sn=${order.sn}`)">查看订单</span></div>
|
||||
<div class="order-handle"><span>{{ $filters.unitPrice(order.flowPrice, "¥") }}</span><span class="hover-color" @click="linkTo(`/home/OrderDetail?sn=${order.sn}`)">查看订单</span></div>
|
||||
</li>
|
||||
</ul>
|
||||
<el-button type="primary" @click="linkTo('/home/MyOrder')" style="width:100%">查看全部订单</el-button>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<el-icon :size="20"><component :is="resolveIcon(item.icon)" /></el-icon>
|
||||
<p class="barTitle" v-if="item.titleShow"> {{item.title}}</p>
|
||||
<div class="circle" v-if="item.title === '购物车'">
|
||||
{{cartNum < 100 ? cartNum : 99}}
|
||||
{{ Number(cartNum) > 99 ? '99+' : cartNum }}
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
@@ -79,21 +79,15 @@ export default {
|
||||
this.handleDrawer = false
|
||||
switch (val.title) {
|
||||
case '会员中心':
|
||||
this.openBlank('/home')
|
||||
this.$router.push('/home')
|
||||
break;
|
||||
case '我的资产':
|
||||
this.openBlank('/home/MoneyManagement')
|
||||
this.$router.push('/home/MoneyManagement')
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
openBlank (path) { // 新页面打开地址
|
||||
let routerUrl = this.$router.resolve({
|
||||
path: path
|
||||
})
|
||||
window.open(routerUrl.href, '_blank')
|
||||
},
|
||||
getCartList () { // 获取购物车列表
|
||||
cartCount().then(res => {
|
||||
this.$store.commit('SET_CARTNUM', res.result)
|
||||
@@ -131,11 +125,15 @@ export default {
|
||||
}
|
||||
|
||||
.circle {
|
||||
width: 20px;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
padding: 0 4px;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
background: $theme_color;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
|
||||
@@ -90,10 +90,7 @@ export default {
|
||||
methods: {
|
||||
goArticle() {
|
||||
// 跳转文章页
|
||||
let routeUrl = this.$router.resolve({
|
||||
path: "/article",
|
||||
});
|
||||
window.open(routeUrl.href, "_blank");
|
||||
this.$router.push("/article");
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
118
buyer/src/components/goods/GoodsThumb.vue
Normal file
118
buyer/src/components/goods/GoodsThumb.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div class="goods-thumb" :class="{ 'is-loaded': loaded, 'is-error': error }">
|
||||
<img v-if="currentSrc" :src="currentSrc" :alt="alt" @load="loaded = true" @error="onError" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "GoodsThumb",
|
||||
props: {
|
||||
src: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
alt: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentSrc: "",
|
||||
loaded: false,
|
||||
error: false,
|
||||
observer: null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
src() {
|
||||
this.observe();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.observe();
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.disconnect();
|
||||
},
|
||||
methods: {
|
||||
disconnect() {
|
||||
if (this.observer) {
|
||||
this.observer.disconnect();
|
||||
this.observer = null;
|
||||
}
|
||||
},
|
||||
observe() {
|
||||
this.disconnect();
|
||||
this.currentSrc = "";
|
||||
this.loaded = false;
|
||||
this.error = false;
|
||||
if (!this.src) {
|
||||
this.error = true;
|
||||
return;
|
||||
}
|
||||
if (typeof IntersectionObserver === "undefined") {
|
||||
this.currentSrc = this.src;
|
||||
return;
|
||||
}
|
||||
this.observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (!entries.some((entry) => entry.isIntersecting)) return;
|
||||
this.currentSrc = this.src;
|
||||
this.disconnect();
|
||||
},
|
||||
{ rootMargin: "160px 0px" }
|
||||
);
|
||||
this.observer.observe(this.$el);
|
||||
},
|
||||
onError() {
|
||||
this.error = true;
|
||||
this.loaded = false;
|
||||
this.currentSrc = "";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.goods-thumb {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
overflow: hidden;
|
||||
background: #f3f3f3;
|
||||
}
|
||||
|
||||
.goods-thumb::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(90deg, #f3f3f3 25%, #e8e8e8 37%, #f3f3f3 63%);
|
||||
background-size: 400% 100%;
|
||||
animation: goods-thumb-loading 1.2s ease infinite;
|
||||
}
|
||||
|
||||
.goods-thumb.is-loaded::before,
|
||||
.goods-thumb.is-error::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.goods-thumb img {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes goods-thumb-loading {
|
||||
0% {
|
||||
background-position: 100% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -8,9 +8,8 @@
|
||||
<div class="item-detail-big-img" v-if="imgList[imgIndex]">
|
||||
<el-image
|
||||
:src="currentImg"
|
||||
:preview-src-list="previewList"
|
||||
fit="contain"
|
||||
style="width: 100%; height: 350px"
|
||||
fit="cover"
|
||||
style="width: 100%; height: 100%"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -44,27 +43,6 @@
|
||||
<div class="item-detail-title">
|
||||
<div class="item-detail-title-row">
|
||||
<span class="item-detail-name">{{ skuDetail.goodsName }}</span>
|
||||
<el-tag
|
||||
v-if="isECouponGoods"
|
||||
class="goods-type-tag"
|
||||
size="small"
|
||||
>
|
||||
电子卡券
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-else-if="skuDetail.goodsType !== 'VIRTUAL_GOODS'"
|
||||
class="goods-type-tag"
|
||||
size="small"
|
||||
>
|
||||
实物商品
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-else-if="skuDetail.goodsType == 'VIRTUAL_GOODS'"
|
||||
class="goods-type-tag"
|
||||
size="small"
|
||||
>
|
||||
虚拟商品
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sell-point">
|
||||
@@ -95,7 +73,7 @@
|
||||
<!-- 批发价格 -->
|
||||
<div v-if="!isECouponGoods && wholesaleNum && wholesaleNum.length">
|
||||
<div class="flex">
|
||||
<div class="item-price-title">价格</div>
|
||||
<div class="item-price-title"></div>
|
||||
|
||||
<div
|
||||
v-for="(item, index) in wholesalePrice"
|
||||
@@ -119,7 +97,6 @@
|
||||
|
||||
<!-- 普通价格 -->
|
||||
<div v-else>
|
||||
<span class="item-price-title">价格</span>
|
||||
<span class="item-price">{{ $filters.unitPrice(skuDetail.price, "¥") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -228,7 +205,7 @@
|
||||
</div>
|
||||
<!-- 选择颜色 -->
|
||||
<div
|
||||
class="item-select"
|
||||
class="item-select item-spec"
|
||||
v-for="(sku, index) in formatList"
|
||||
:key="sku.name"
|
||||
>
|
||||
@@ -257,7 +234,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="add-buy-car-box">
|
||||
<div class="item-select">
|
||||
<div class="item-select item-spec">
|
||||
<div class="item-select-title">
|
||||
<p>数量</p>
|
||||
</div>
|
||||
@@ -271,25 +248,10 @@
|
||||
:precision="0"
|
||||
@blur="changeCount"
|
||||
></el-input-number>
|
||||
<span class="inventory"> 库存{{ displayStock }}</span>
|
||||
<span class="inventory">(库存:{{ displayStock }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="item-select"
|
||||
v-if="
|
||||
!isECouponGoods &&
|
||||
skuDetail.goodsType !== 'VIRTUAL_GOODS' &&
|
||||
skuDetail.weight !== 0
|
||||
"
|
||||
>
|
||||
<div class="item-select-title">
|
||||
<p>重量</p>
|
||||
</div>
|
||||
<div class="item-select-column">
|
||||
<span class="inventory">{{ skuDetail.weight }}kg</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="item-select add-buy-car-row"
|
||||
v-if="$route.query.way === 'POINT' && skuDetail.authFlag === 'PASS'"
|
||||
@@ -439,9 +401,6 @@ export default {
|
||||
const item = this.imgList[this.imgIndex];
|
||||
return item ? item.url || item : "";
|
||||
},
|
||||
previewList() {
|
||||
return this.imgList.map((item) => item.url || item);
|
||||
},
|
||||
wholesalePrice(key) {
|
||||
return this.wholesaleList.length
|
||||
? this.wholesaleList.map((item) => {
|
||||
@@ -624,6 +583,16 @@ export default {
|
||||
});
|
||||
},
|
||||
async collect() {
|
||||
if (!this.Cookies.getItem("accessToken") || !this.Cookies.getItem("userInfo")) {
|
||||
this.$router.push({
|
||||
path: "/login",
|
||||
query: {
|
||||
rePath: this.$route.path,
|
||||
query: JSON.stringify(this.$route.query || {}),
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 收藏商品
|
||||
if (this.isCollected) {
|
||||
let cancel = await cancelCollect("GOODS", this.skuDetail.id);
|
||||
@@ -717,16 +686,18 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
// 相册里会混进规格 JSON,只留图片地址;没有相册时用主图
|
||||
swiperGoodsImg() {
|
||||
this.skuDetail.specList.forEach((e) => {
|
||||
if (e.specName === "images") {
|
||||
this.imgList = this.skuDetail.goodsGalleryList.filter(i => i.indexOf("\"url\":") === -1 && i.indexOf("\"status\":") === -1);
|
||||
}
|
||||
const source = this.skuDetail.goodsGalleryList || [];
|
||||
const gallery = Array.from(source).filter((item) => {
|
||||
if (typeof item !== "string") return false;
|
||||
return item.indexOf('"url":') === -1 && item.indexOf('"status":') === -1;
|
||||
});
|
||||
if (!this.imgList) {
|
||||
this.imgList = [this.skuDetail.original];
|
||||
if (gallery.length) {
|
||||
this.imgList = gallery;
|
||||
return;
|
||||
}
|
||||
|
||||
this.imgList = this.skuDetail.original ? [this.skuDetail.original] : [];
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
@@ -784,42 +755,50 @@ export default {
|
||||
}
|
||||
|
||||
.item-detail-left {
|
||||
width: 350px;
|
||||
width: 500px;
|
||||
margin-right: 32px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item-detail-big-img {
|
||||
width: 350px;
|
||||
height: 350px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
margin-bottom: 12px;
|
||||
background: #fff;
|
||||
margin-bottom: 16px;
|
||||
background: #f5f5f5;
|
||||
|
||||
:deep(.el-image),
|
||||
:deep(.el-image__inner) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-image__inner) {
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
#dplayer{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.item-detail-big-img img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.item-detail-img-row {
|
||||
margin-top: 0;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.item-detail-img-small {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
width: 86px;
|
||||
height: 86px;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
background: #f5f5f5;
|
||||
|
||||
&.is-active {
|
||||
border-color: $theme_color;
|
||||
@@ -829,6 +808,7 @@ export default {
|
||||
.item-detail-img-small img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/*商品选购详情*/
|
||||
@@ -848,18 +828,11 @@ export default {
|
||||
|
||||
.item-detail-name {
|
||||
color: #171A26;
|
||||
font-weight: 600;
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.goods-type-tag {
|
||||
flex-shrink: 0;
|
||||
--el-tag-bg-color: #fafafa;
|
||||
--el-tag-border-color: #eee;
|
||||
--el-tag-text-color: #666;
|
||||
}
|
||||
|
||||
.item-detail-express {
|
||||
font-size: 14px;
|
||||
padding: 2px 3px;
|
||||
@@ -1026,6 +999,22 @@ export default {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.item-spec {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
|
||||
.item-select-title {
|
||||
width: auto;
|
||||
margin-right: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.item-select-column {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.item-select-title {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
@@ -1215,8 +1204,9 @@ export default {
|
||||
|
||||
.inventory {
|
||||
padding-left: 8px;
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
}
|
||||
.view-video{
|
||||
cursor: pointer;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<div class="goods-detail-extra">
|
||||
<div class="goods-detail-extra__main">
|
||||
<div class="detail-layout">
|
||||
<div class="detail-main">
|
||||
<div class="detail-anchor" ref="tabNav" :class="{ 'is-stuck': anchorStuck }">
|
||||
<button
|
||||
v-for="tab in detailTabs"
|
||||
@@ -117,15 +120,25 @@
|
||||
<div class="detail-empty" v-else>暂无商品参数</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<StoreSide
|
||||
:store-id="skuDetail.storeId"
|
||||
:goods-id="skuDetail.goodsId"
|
||||
:sku-id="skuDetail.id"
|
||||
:store-name-fallback="skuDetail.storeName"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Refresh } from '@element-plus/icons-vue';
|
||||
import { goodsComment, goodsCommentNum } from '@/api/member.js';
|
||||
import StoreSide from './StoreSide.vue';
|
||||
|
||||
export default {
|
||||
components: { Refresh },
|
||||
components: { Refresh, StoreSide },
|
||||
name: 'ShowGoodsDetail',
|
||||
props: {
|
||||
detail: {
|
||||
@@ -277,8 +290,25 @@ export default {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.goods-detail-extra {
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.goods-detail-extra__main {
|
||||
width: 1200px;
|
||||
margin: 16px auto 24px;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.detail-layout {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.detail-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.detail-anchor {
|
||||
@@ -334,6 +364,7 @@ export default {
|
||||
.detail-panel {
|
||||
background: #fff;
|
||||
border-radius: 0 0 4px 4px;
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
|
||||
.detail-section + .detail-section {
|
||||
@@ -381,7 +412,7 @@ export default {
|
||||
.item-param-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
margin: 16px 24px 24px;
|
||||
margin: 16px 24px 0;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
border-left: 1px solid #f0f0f0;
|
||||
border-radius: 4px;
|
||||
|
||||
419
buyer/src/components/goodsDetail/StoreSide.vue
Normal file
419
buyer/src/components/goodsDetail/StoreSide.vue
Normal file
@@ -0,0 +1,419 @@
|
||||
<template>
|
||||
<aside class="store-side" v-if="storeId">
|
||||
<div class="store-card">
|
||||
<div class="store-head" @click="goStore">
|
||||
<img
|
||||
v-if="store.storeLogo && !logoError"
|
||||
class="store-logo"
|
||||
:src="store.storeLogo"
|
||||
:alt="storeName"
|
||||
@error="logoError = true"
|
||||
/>
|
||||
<span v-else class="store-logo store-logo-fallback">{{ storeInitial }}</span>
|
||||
<div class="store-head-text">
|
||||
<p class="store-name">
|
||||
<span v-if="isSelfOperated" class="store-self">自营</span>
|
||||
<span class="store-name-text">{{ storeName }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="store-scores">
|
||||
<div class="score-item">
|
||||
<span class="score-label">描述</span>
|
||||
<span class="score-value">{{ formatScore(store.descriptionScore) }}</span>
|
||||
</div>
|
||||
<div class="score-item">
|
||||
<span class="score-label">服务</span>
|
||||
<span class="score-value">{{ formatScore(store.serviceScore) }}</span>
|
||||
</div>
|
||||
<div class="score-item">
|
||||
<span class="score-label">物流</span>
|
||||
<span class="score-value">{{ formatScore(store.deliveryScore) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="store-actions">
|
||||
<button type="button" class="store-btn" @click="contact">联系客服</button>
|
||||
<button type="button" class="store-btn" :class="{ 'is-on': collected }" @click="toggleCollect">
|
||||
{{ collected ? "已收藏" : "收藏店铺" }}
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="enter-store" @click="goStore">进店逛逛</button>
|
||||
</div>
|
||||
|
||||
<div class="recommend-card" v-if="recommends.length">
|
||||
<p class="recommend-title">店铺推荐</p>
|
||||
<div
|
||||
class="recommend-item"
|
||||
v-for="item in recommends"
|
||||
:key="item.id"
|
||||
@click="goGoods(item)"
|
||||
>
|
||||
<img class="recommend-img" :src="item.thumbnail || item.small" :alt="item.goodsName" />
|
||||
<div class="recommend-info">
|
||||
<p class="recommend-name">{{ item.goodsName }}</p>
|
||||
<div class="recommend-meta">
|
||||
<p class="recommend-price">{{ $filters.unitPrice(item.price, "¥") }}</p>
|
||||
<span class="recommend-sales">销量 {{ item.buyCount || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Message } from "@/utils/message";
|
||||
import { goodsList } from "@/api/goods";
|
||||
import { getStoreById } from "@/api/shopentry";
|
||||
import { cancelStoreCollect, collectStore, isStoreCollection } from "@/api/member";
|
||||
import imTalk from "@/components/mixes/talkIm";
|
||||
|
||||
export default {
|
||||
name: "StoreSide",
|
||||
mixins: [imTalk],
|
||||
props: {
|
||||
storeId: {
|
||||
type: [String, Number],
|
||||
default: "",
|
||||
},
|
||||
goodsId: {
|
||||
type: [String, Number],
|
||||
default: "",
|
||||
},
|
||||
skuId: {
|
||||
type: [String, Number],
|
||||
default: "",
|
||||
},
|
||||
storeNameFallback: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
store: {},
|
||||
logoError: false,
|
||||
collected: false,
|
||||
recommends: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
storeName() {
|
||||
return this.store.storeName || this.storeNameFallback || "店铺";
|
||||
},
|
||||
storeInitial() {
|
||||
return this.storeName.slice(0, 1);
|
||||
},
|
||||
// 自营标记后端可能是 true、1 或 "1"
|
||||
isSelfOperated() {
|
||||
const flag = this.store.selfOperated;
|
||||
return flag === true || flag === 1 || flag === "1";
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
storeId: {
|
||||
immediate: true,
|
||||
handler(id) {
|
||||
if (!id) return;
|
||||
this.logoError = false;
|
||||
this.loadStore(id);
|
||||
this.loadRecommend(id);
|
||||
this.loadCollected(id);
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// 没有评分或小于等于 0 时按满分展示
|
||||
formatScore(value) {
|
||||
const score = Number(value);
|
||||
if (!Number.isFinite(score) || score <= 0) return "5.00";
|
||||
return score.toFixed(2);
|
||||
},
|
||||
loadStore(id) {
|
||||
getStoreById(id).then((res) => {
|
||||
if (res.success && res.result) this.store = res.result;
|
||||
}).catch(() => {});
|
||||
},
|
||||
// 按销量取店内商品,去掉当前商品,最多 4 个
|
||||
loadRecommend(id) {
|
||||
goodsList({
|
||||
storeId: id,
|
||||
pageNumber: 1,
|
||||
pageSize: 8,
|
||||
sort: "buyCount",
|
||||
order: "desc",
|
||||
}).then((res) => {
|
||||
const records = (res.success && res.result && res.result.records) || [];
|
||||
this.recommends = records
|
||||
.filter((item) => String(item.goodsId) !== String(this.goodsId))
|
||||
.slice(0, 4);
|
||||
}).catch(() => {});
|
||||
},
|
||||
// 同时有 token 和用户信息才查收藏,避免残留 userInfo 触发登录跳转
|
||||
loadCollected(id) {
|
||||
if (!this.Cookies.getItem("accessToken") || !this.Cookies.getItem("userInfo")) return;
|
||||
isStoreCollection("STORE", id).then((res) => {
|
||||
if (res.success && res.result) this.collected = true;
|
||||
}).catch(() => {});
|
||||
},
|
||||
goStore() {
|
||||
this.$router.push({ path: "/merchant", query: { id: this.storeId } });
|
||||
},
|
||||
// 推荐商品新开详情页,店铺页仍在当前页打开
|
||||
goGoods(item) {
|
||||
const href = this.$router.resolve({
|
||||
path: "/goodsDetail",
|
||||
query: { skuId: item.id, goodsId: item.goodsId },
|
||||
}).href;
|
||||
window.open(href, "_blank");
|
||||
},
|
||||
contact() {
|
||||
this.IMService(this.storeId, this.goodsId, this.skuId);
|
||||
},
|
||||
// 未登录先去登录,并带回当前商品页
|
||||
async toggleCollect() {
|
||||
if (!this.Cookies.getItem("accessToken") || !this.Cookies.getItem("userInfo")) {
|
||||
this.$router.push({
|
||||
path: "/login",
|
||||
query: {
|
||||
rePath: this.$route.path,
|
||||
query: JSON.stringify(this.$route.query || {}),
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this.collected) {
|
||||
const cancel = await cancelStoreCollect("STORE", this.storeId);
|
||||
if (cancel.success) {
|
||||
this.collected = false;
|
||||
Message.success("已取消收藏");
|
||||
}
|
||||
return;
|
||||
}
|
||||
const collect = await collectStore("STORE", this.storeId);
|
||||
if (collect.code === 200) {
|
||||
this.collected = true;
|
||||
Message.success("收藏店铺成功,可以前往个人中心我的收藏查看");
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.store-side {
|
||||
width: 260px;
|
||||
flex-shrink: 0;
|
||||
position: sticky;
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
.store-card,
|
||||
.recommend-card {
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.store-card {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.store-head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.store-logo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
border: 1px solid #f0f0f0;
|
||||
flex-shrink: 0;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.store-logo-fallback {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
background: $theme_color;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.store-head-text {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.store-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
margin: 0;
|
||||
color: #171A26;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.store-name-text {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.store-self {
|
||||
flex-shrink: 0;
|
||||
padding: 0 6px;
|
||||
border-radius: 2px;
|
||||
background: $theme_color;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.store-scores {
|
||||
display: flex;
|
||||
margin-top: 14px;
|
||||
padding: 10px 0;
|
||||
border-top: 1px solid #f5f5f5;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.score-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.score-label {
|
||||
display: block;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.score-value {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
color: $theme_color;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.store-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.store-btn,
|
||||
.enter-store {
|
||||
border: 1px solid #eee;
|
||||
background: #fff;
|
||||
color: #171A26;
|
||||
font-size: 13px;
|
||||
line-height: 32px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: $theme_color;
|
||||
border-color: $theme_color;
|
||||
}
|
||||
}
|
||||
|
||||
.store-btn {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.store-btn.is-on {
|
||||
color: $theme_color;
|
||||
border-color: $theme_color;
|
||||
background: #fff5f4;
|
||||
}
|
||||
|
||||
.enter-store {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.recommend-card {
|
||||
margin-top: 12px;
|
||||
padding: 12px 12px 4px;
|
||||
}
|
||||
|
||||
.recommend-title {
|
||||
margin: 0 0 8px;
|
||||
color: #171A26;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.recommend-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 8px 0 12px;
|
||||
cursor: pointer;
|
||||
|
||||
& + & {
|
||||
border-top: 1px solid #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.recommend-img {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.recommend-info {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.recommend-name {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.recommend-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.recommend-price {
|
||||
margin: 0;
|
||||
color: $theme_color;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.recommend-sales {
|
||||
flex-shrink: 0;
|
||||
color: #999;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
}
|
||||
</style>
|
||||
@@ -29,7 +29,7 @@
|
||||
</div>
|
||||
<transition name='fade'>
|
||||
<ul class="drop-items">
|
||||
<li @click="goUserCenter('/home/MyOrder')">我的主页</li>
|
||||
<li @click="goUserCenter('/home')">我的主页</li>
|
||||
<li @click="goUserCenter('/home/Coupons')">优惠券</li>
|
||||
<li @click="goUserCenter('/home/Favorites')">我的收藏</li>
|
||||
<li @click="signOutFun">退出登录</li>
|
||||
@@ -39,7 +39,12 @@
|
||||
</li>
|
||||
<li @click="goUserCenter('/home/MyOrder')"><span class="nav-item">我的订单</span></li>
|
||||
<li @click="goUserCenter('/home/MyTracks')"><span class="nav-item">我的足迹</span></li>
|
||||
<li @click="goUserCenter('/home/MsgList')"><span class="nav-item">我的消息</span></li>
|
||||
<li @click="goUserCenter('/home/MsgList')">
|
||||
<span class="nav-item">
|
||||
我的消息
|
||||
<i v-if="unreadMsg > 0" class="msg-badge">{{ unreadLabel }}</i>
|
||||
</span>
|
||||
</li>
|
||||
<li v-if="$route.name !== 'Cart'" class="cart-nav-item">
|
||||
<el-dropdown
|
||||
class="cart-nav-dropdown"
|
||||
@@ -49,7 +54,7 @@
|
||||
>
|
||||
<span class="nav-item" @click="goToPay">
|
||||
<el-icon :size="18"><ShoppingCart /></el-icon>
|
||||
购物车({{ cartNum < 100 ? cartNum : '99' }})
|
||||
购物车({{ Number(cartNum) > 99 ? '99+' : cartNum }})
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<div class="shopping-cart-null" v-show="shoppingCart.length <= 0">
|
||||
@@ -90,6 +95,7 @@ import storage from '@/plugins/storage.js';
|
||||
import {cartGoodsAll} from '@/api/cart.js';
|
||||
import {logout} from '@/api/account.js';
|
||||
import config from '@/config';
|
||||
import { refreshUnreadMessage } from '@/utils/unreadMessage';
|
||||
|
||||
export default {
|
||||
components: { ShoppingCart },
|
||||
@@ -97,6 +103,7 @@ export default {
|
||||
created() {
|
||||
if (storage.getItem('userInfo')) {
|
||||
this.userInfo = JSON.parse(storage.getItem('userInfo'));
|
||||
refreshUnreadMessage();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -114,6 +121,12 @@ export default {
|
||||
// 购物车商品数量
|
||||
cartNum() {
|
||||
return this.$store.state.cartNum;
|
||||
},
|
||||
unreadMsg() {
|
||||
return Number(this.$store.state.unreadMsg) || 0;
|
||||
},
|
||||
unreadLabel() {
|
||||
return this.unreadMsg > 99 ? '99+' : String(this.unreadMsg);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -136,16 +149,10 @@ export default {
|
||||
});
|
||||
return;
|
||||
}
|
||||
const url = this.$router.resolve({
|
||||
path: '/cart',
|
||||
});
|
||||
window.open(url.href, '_blank');
|
||||
this.$router.push('/cart');
|
||||
},
|
||||
myInfo() { // 跳转会员中心
|
||||
let url = this.$router.resolve({
|
||||
path: '/home/MyOrder'
|
||||
});
|
||||
window.open(url.href, '_blank');
|
||||
this.$router.push('/home');
|
||||
},
|
||||
signOutFun() { // 退出登录
|
||||
logout().then(res => {
|
||||
@@ -154,6 +161,7 @@ export default {
|
||||
storage.removeItem('userInfo');
|
||||
storage.removeItem('cartNum');
|
||||
this.$store.commit('SET_CARTNUM', 0)
|
||||
this.$store.commit('SET_UNREAD_MSG', 0)
|
||||
this.$router.push('/login');
|
||||
});
|
||||
},
|
||||
@@ -181,11 +189,10 @@ export default {
|
||||
},
|
||||
shopEntry() {
|
||||
if (storage.getItem('accessToken')) {
|
||||
const routeUrl = this.$router.resolve({
|
||||
this.$router.push({
|
||||
path: '/shopEntry',
|
||||
query: { id: 1 },
|
||||
});
|
||||
window.open(routeUrl.href, '_blank');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
@@ -289,6 +296,19 @@ export default {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.cart-nav-item {
|
||||
:deep(.cart-nav-dropdown),
|
||||
:deep(.el-dropdown) {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<span @mouseenter="getCartList">
|
||||
<el-icon class="cart-icon" :size="20" @click="goCartList"><ShoppingCart /></el-icon>
|
||||
</span>
|
||||
<i class="cart-badge">{{ cartNum < 100 ? cartNum : "99" }}</i>
|
||||
<i class="cart-badge">{{ Number(cartNum) > 99 ? "99+" : cartNum }}</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,8 +45,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
goCartList() {
|
||||
const routerUrl = this.$router.resolve({ path: '/cart' })
|
||||
window.open(routerUrl.href, '_blank')
|
||||
this.$router.push('/cart')
|
||||
},
|
||||
getCartList() {
|
||||
if (storage.getItem('userInfo')) {
|
||||
@@ -71,7 +70,7 @@ $cart-col-width: 40px;
|
||||
|
||||
.scroll-show {
|
||||
height: 60px;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -125,8 +124,9 @@ $cart-col-width: 40px;
|
||||
background-color: $theme_color;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
width: 17px;
|
||||
min-width: 17px;
|
||||
height: 17px;
|
||||
padding: 0 4px;
|
||||
border-radius: 10px;
|
||||
line-height: 17px;
|
||||
text-align: center;
|
||||
|
||||
@@ -164,7 +164,7 @@ export default {
|
||||
},
|
||||
{
|
||||
value: "0",
|
||||
label: "代付款"
|
||||
label: "待付款"
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -184,7 +184,12 @@ export default {
|
||||
};
|
||||
},
|
||||
isMemberRoute(location) {
|
||||
return location.path.startsWith("/home/") || location.path === "/cart";
|
||||
const path = location.path && location.path.charAt(0) === "/" ? location.path : `/${location.path || ""}`;
|
||||
return path === "/home" || path.startsWith("/home/") || path === "/cart";
|
||||
},
|
||||
normalizeLocation(location) {
|
||||
if (!location.path || location.path.charAt(0) === "/") return location;
|
||||
return { ...location, path: `/${location.path}` };
|
||||
},
|
||||
promptLoginThenNavigate(location) {
|
||||
Modal.confirm({
|
||||
@@ -208,13 +213,12 @@ export default {
|
||||
},
|
||||
// 快捷跳转中心
|
||||
entryControl(val) {
|
||||
const location = this.resolveRoute(val.path);
|
||||
const location = this.normalizeLocation(this.resolveRoute(val.path));
|
||||
if (this.isMemberRoute(location) && !storage.getItem("userInfo")) {
|
||||
this.promptLoginThenNavigate(location);
|
||||
return;
|
||||
}
|
||||
const url = this.$router.resolve(location);
|
||||
window.open(url.href, "_blank");
|
||||
this.$router.push(location);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -87,11 +87,10 @@ export default {
|
||||
})
|
||||
},
|
||||
goArticle (id) { // 跳转文章详情
|
||||
let routeUrl = this.$router.resolve({
|
||||
this.$router.push({
|
||||
path: '/article',
|
||||
query: {id}
|
||||
});
|
||||
window.open(routeUrl.href, '_blank');
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
|
||||
@@ -80,8 +80,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
goToSeckill() {
|
||||
const routeUrl = this.$router.resolve({ path: "/seckill" });
|
||||
window.open(routeUrl.href, "_blank");
|
||||
this.$router.push("/seckill");
|
||||
},
|
||||
countDown(currIndex) {
|
||||
const zeroTime = new Date(new Date().toLocaleDateString()).getTime();
|
||||
|
||||
@@ -198,11 +198,10 @@ export default {
|
||||
if (!arr[0]) {
|
||||
arr.shift();
|
||||
}
|
||||
let routerUrl = this.$router.resolve({
|
||||
this.$router.push({
|
||||
path: "/goodsList",
|
||||
query: { categoryId: arr.toString() },
|
||||
});
|
||||
window.open(routerUrl.href, "_blank");
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -281,11 +281,10 @@ export default {
|
||||
},
|
||||
// 跳转店铺首页
|
||||
goShopPage(id) {
|
||||
let routeUrl = this.$router.resolve({
|
||||
this.$router.push({
|
||||
path: "/merchant",
|
||||
query: { id },
|
||||
});
|
||||
window.open(routeUrl.href, "_blank");
|
||||
},
|
||||
// 同步购物车商品收藏状态
|
||||
async syncCartCollectionStatus() {
|
||||
@@ -959,3 +958,6 @@ export default {
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -6,27 +6,10 @@
|
||||
<div class="shop-nav-container">
|
||||
<el-breadcrumb>
|
||||
<el-breadcrumb-item to="/">首页</el-breadcrumb-item>
|
||||
<el-breadcrumb-item v-for="(item, index) in categoryBar" :to="goGoodsList(index)" target="_blank" :key="index">
|
||||
<el-breadcrumb-item v-for="(item, index) in categoryBar" :to="goGoodsList(index)" :key="index">
|
||||
{{ item.name }}
|
||||
</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
<div class="store-collect" v-if="!takeDownSale && goodsMsg.data">
|
||||
<span class="mr_10" v-if="goodsMsg.data">
|
||||
<router-link
|
||||
class="store-name-link"
|
||||
:to="{ path: '/merchant', query: { id: goodsMsg.data.storeId } }"
|
||||
>{{ goodsMsg.data.storeName }}</router-link>
|
||||
</span>
|
||||
<span
|
||||
class="store-collect-btn"
|
||||
:class="{ 'is-collected': storeCollected }"
|
||||
@click="collect"
|
||||
>
|
||||
<el-icon class="store-collect-icon"><StarFilled /></el-icon>
|
||||
{{ storeCollected? "已收藏店铺": "收藏店铺" }}
|
||||
</span>
|
||||
<span class="ml_10 contact-service-btn" @click="IMService(goodsMsg.data.storeId,goodsMsg.data.goodsId,goodsMsg.data.id)">联系客服</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -48,21 +31,13 @@
|
||||
|
||||
<script>
|
||||
import { Message } from "@/utils/message";
|
||||
import { StarFilled } from '@element-plus/icons-vue';
|
||||
import Search from "@/components/Search";
|
||||
|
||||
import ShowGoods from "@/components/goodsDetail/ShowGoods";
|
||||
import empty from "@/components/empty/Main";
|
||||
import ShowGoodsDetail from "@/components/goodsDetail/ShowGoodsDetail";
|
||||
import { goodsSkuDetail } from "@/api/goods";
|
||||
import {
|
||||
cancelStoreCollect,
|
||||
collectStore,
|
||||
isStoreCollection,
|
||||
getGoodsDistribution,
|
||||
} from "@/api/member";
|
||||
import { getDetailById } from "@/api/shopentry";
|
||||
import imTalk from '@/components/mixes/talkIm'
|
||||
import { getGoodsDistribution } from "@/api/member";
|
||||
export default {
|
||||
name: "GoodsDetail",
|
||||
beforeRouteEnter (to, from, next) {
|
||||
@@ -72,14 +47,11 @@ export default {
|
||||
created () {
|
||||
this.getGoodsDetail();
|
||||
},
|
||||
mixins: [imTalk],
|
||||
data () {
|
||||
return {
|
||||
goodsMsg: {}, // 商品信息
|
||||
isLoading: false, // 加载状态
|
||||
categoryBar: [], // 分类
|
||||
storeCollected: false, // 商品收藏
|
||||
storeMsg: {}, // 店铺信息
|
||||
takeDownSale:false, // 是否下架
|
||||
|
||||
};
|
||||
@@ -140,24 +112,6 @@ export default {
|
||||
if (!this.goodsMsg.data.intro) {
|
||||
this.goodsMsg.data.intro = ''
|
||||
}
|
||||
// 判断是否收藏(须同时有 token,避免未登录/ token 失效时误跳登录页)
|
||||
if (this.Cookies.getItem("accessToken") && this.Cookies.getItem("userInfo")) {
|
||||
isStoreCollection("STORE", this.goodsMsg.data.storeId).then((res) => {
|
||||
if (res.success && res.result) {
|
||||
this.storeCollected = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.storeMsg) {
|
||||
// 获取店铺信息
|
||||
getDetailById(this.goodsMsg.data.storeId).then((res) => {
|
||||
if (res.success) {
|
||||
this.storeMsg = res.result;
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
Message.error(res.message);
|
||||
this.isLoading = false
|
||||
@@ -183,37 +137,12 @@ export default {
|
||||
query: { categoryId: arr.toString() },
|
||||
};
|
||||
},
|
||||
async collect () {
|
||||
// 收藏店铺
|
||||
const storeId = this.goodsMsg.data && this.goodsMsg.data.storeId;
|
||||
if (!storeId) {
|
||||
return;
|
||||
}
|
||||
if (!this.Cookies.getItem("userInfo")) {
|
||||
Message.warning("请先登录");
|
||||
this.$router.push("/login");
|
||||
return;
|
||||
}
|
||||
if (this.storeCollected) {
|
||||
let cancel = await cancelStoreCollect("STORE", storeId);
|
||||
if (cancel.success) {
|
||||
Message.success("已取消收藏");
|
||||
this.storeCollected = false;
|
||||
}
|
||||
} else {
|
||||
let collect = await collectStore("STORE", storeId);
|
||||
if (collect.code === 200) {
|
||||
this.storeCollected = true;
|
||||
Message.success("收藏店铺成功,可以前往个人中心我的收藏查看");
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {},
|
||||
components: { Search,
|
||||
ShowGoods,
|
||||
ShowGoodsDetail,
|
||||
empty, StarFilled },
|
||||
empty },
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@@ -248,52 +177,6 @@ export default {
|
||||
:deep(.el-breadcrumb__item:last-child .el-breadcrumb__inner) {
|
||||
color: #171A26;
|
||||
}
|
||||
|
||||
.store-collect {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
color: #171A26;
|
||||
font-size: 14px;
|
||||
|
||||
.store-collect-btn,
|
||||
.contact-service-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
color: #171A26;
|
||||
|
||||
&:hover {
|
||||
color: $theme_color;
|
||||
}
|
||||
}
|
||||
|
||||
.store-collect-btn {
|
||||
.store-collect-icon {
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&.is-collected,
|
||||
&.is-collected .store-collect-icon {
|
||||
color: $theme_color;
|
||||
}
|
||||
}
|
||||
|
||||
.store-name-link {
|
||||
color: #438cde;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: $theme_color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sale-btn{
|
||||
margin:10px 0
|
||||
|
||||
@@ -21,32 +21,34 @@
|
||||
>
|
||||
<span :class="{ 'goods-list-tool-active': index === sortIndex }">
|
||||
{{ item.title }}
|
||||
<el-icon><ArrowDown /></el-icon>
|
||||
<el-icon v-if="index === sortIndex" class="sort-arrow"><ArrowDown /></el-icon>
|
||||
</span>
|
||||
</li>
|
||||
<li @click="orderBy('price', 5, 'up')" class="price-sort">
|
||||
<span :class="{ 'goods-list-tool-active': 5 === sortIndex }">
|
||||
<li @click="orderBy('price', priceSortIndex)" class="price-sort">
|
||||
<span :class="{ 'goods-list-tool-active': priceSortIndex === sortIndex }">
|
||||
价格
|
||||
<span class="price-arrows">
|
||||
<el-icon :class="{ 'price-color': sortPriceIndex == 'desc' }"><ArrowUp /></el-icon>
|
||||
<el-icon :class="{ 'price-color': sortPriceIndex == 'asc' }"><ArrowDown /></el-icon>
|
||||
<el-icon :class="{ 'is-on': priceSortIndex === sortIndex && sortPriceIndex === 'asc' }"><ArrowUp /></el-icon>
|
||||
<el-icon :class="{ 'is-on': priceSortIndex === sortIndex && sortPriceIndex === 'desc' }"><ArrowDown /></el-icon>
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- 列表 -->
|
||||
<div class="goods-list">
|
||||
<empty v-if="goodsList.length === 0" />
|
||||
<div v-if="searched && !loading && goodsList.length === 0" class="list-empty">
|
||||
<empty :_Title="emptyTitle" />
|
||||
<div v-if="recommendList.length" class="recommend-title">为你推荐</div>
|
||||
</div>
|
||||
<div class="goods-list" v-if="displayGoods.length">
|
||||
<div
|
||||
v-else
|
||||
class="goods-show-info"
|
||||
v-for="(item, index) in goodsList"
|
||||
:key="index"
|
||||
v-for="(item, index) in displayGoods"
|
||||
:key="item.id || index"
|
||||
@click="goGoodsDetail(item.id, item.goodsId)"
|
||||
>
|
||||
<div class="goods-show-img">
|
||||
<img :src="item.thumbnail" />
|
||||
<GoodsThumb :src="item.thumbnail" :alt="item.goodsName" />
|
||||
</div>
|
||||
<div class="goods-show-price">
|
||||
<span>
|
||||
@@ -85,7 +87,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="goods-page">
|
||||
<div class="goods-page" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="params.pageNumber"
|
||||
v-model:page-size="params.pageSize"
|
||||
@@ -105,6 +107,7 @@
|
||||
import { Message } from "@/utils/message";
|
||||
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
|
||||
import GoodsClassNav from "@/components/nav/GoodsClassNav";
|
||||
import GoodsThumb from "@/components/goods/GoodsThumb.vue";
|
||||
import * as apiGoods from "@/api/goods";
|
||||
export default {
|
||||
name: "GoodsList",
|
||||
@@ -115,7 +118,10 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
sortIndex: 0, // 排序状态
|
||||
priceSortIndex: 4,
|
||||
sortPriceIndex: false, // 判断价格升序还是降序
|
||||
searched: false,
|
||||
recommendList: [],
|
||||
goodsTool: [
|
||||
// 排序类型
|
||||
{ title: "综合", en: "" },
|
||||
@@ -154,6 +160,16 @@ export default {
|
||||
this.getGoodsList();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
displayGoods() {
|
||||
if (this.goodsList.length) return this.goodsList;
|
||||
return this.searched && !this.loading ? this.recommendList : [];
|
||||
},
|
||||
emptyTitle() {
|
||||
const keyword = this.params.keyword;
|
||||
return keyword ? `没有找到与“${keyword}”相关的商品` : "暂无商品";
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// 搜索
|
||||
handleSearch(key) {
|
||||
@@ -204,11 +220,10 @@ export default {
|
||||
Message.warning("店铺信息不存在");
|
||||
return;
|
||||
}
|
||||
const routeUrl = this.$router.resolve({
|
||||
this.$router.push({
|
||||
path: "/merchant",
|
||||
query: { id },
|
||||
});
|
||||
window.open(routeUrl.href, "_blank");
|
||||
},
|
||||
// 分页 修改页码
|
||||
changePageNum(val) {
|
||||
@@ -230,14 +245,28 @@ export default {
|
||||
.then((res) => {
|
||||
this.loading = false;
|
||||
if (res.success) {
|
||||
this.goodsList = res.result.records;
|
||||
this.goodsList = res.result.records || [];
|
||||
this.total = res.result.total;
|
||||
if (!this.goodsList.length) this.loadRecommend();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.goodsList = [];
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
this.searched = true;
|
||||
});
|
||||
},
|
||||
loadRecommend() {
|
||||
if (this.recommendList.length) return;
|
||||
apiGoods
|
||||
.goodsList({ pageNumber: 1, pageSize: 10, sort: "buyCount", order: "desc" })
|
||||
.then((res) => {
|
||||
if (res.success) this.recommendList = res.result.records || [];
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
getParams(val) {
|
||||
// 筛选条件回显
|
||||
Object.assign(this.params, val);
|
||||
@@ -256,6 +285,7 @@ export default {
|
||||
},
|
||||
components: {
|
||||
GoodsClassNav,
|
||||
GoodsThumb,
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
},
|
||||
@@ -395,8 +425,12 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.price-color {
|
||||
color: #b3b3b3;
|
||||
.el-icon {
|
||||
color: #c8c8c8;
|
||||
}
|
||||
|
||||
.el-icon.is-on {
|
||||
color: $theme_color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -433,9 +467,30 @@ export default {
|
||||
background-color: transparent !important;
|
||||
font-weight: 600;
|
||||
|
||||
.price-color {
|
||||
color: $theme_color !important;
|
||||
.sort-arrow {
|
||||
color: $theme_color;
|
||||
}
|
||||
|
||||
.price-arrows .el-icon {
|
||||
color: #c8c8c8;
|
||||
}
|
||||
|
||||
.price-arrows .el-icon.is-on {
|
||||
color: $theme_color;
|
||||
}
|
||||
}
|
||||
|
||||
.list-empty {
|
||||
background: #fff;
|
||||
border: 1px solid #eee;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.recommend-title {
|
||||
padding: 4px 16px 12px;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.goods-list {
|
||||
|
||||
@@ -92,9 +92,9 @@
|
||||
</div>
|
||||
<div class="foot">
|
||||
<div type="flex" justify="space-around" class="help">
|
||||
<router-link to="/article" class="item" target="_blank">帮助</router-link>
|
||||
<router-link to="/article?id=1371779927900160000" class="item" target="_blank">隐私</router-link>
|
||||
<router-link to="/article?id=1371992704333905920" class="item" target="_blank">条款</router-link>
|
||||
<router-link to="/article" class="item">帮助</router-link>
|
||||
<router-link to="/article?id=1371779927900160000" class="item">隐私</router-link>
|
||||
<router-link to="/article?id=1371992704333905920" class="item">条款</router-link>
|
||||
</div>
|
||||
<div type="flex" justify="center" class="copyright">
|
||||
Copyright © {{year}} - Present
|
||||
@@ -601,3 +601,5 @@ export default {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
44
buyer/src/pages/NotFound.vue
Normal file
44
buyer/src/pages/NotFound.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div class="not-found-page">
|
||||
<BaseHeader></BaseHeader>
|
||||
<Search :show-tag="false"></Search>
|
||||
<div class="not-found">
|
||||
<h1>页面不存在</h1>
|
||||
<p>你访问的地址没有对应页面</p>
|
||||
<el-button type="primary" @click="$router.push('/')">返回首页</el-button>
|
||||
</div>
|
||||
<BaseFooter></BaseFooter>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "NotFound",
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.not-found {
|
||||
width: 1200px;
|
||||
min-height: 420px;
|
||||
margin: 24px auto 40px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.not-found h1 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.not-found p {
|
||||
margin: 12px 0 24px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -66,7 +66,7 @@
|
||||
<el-form-item>
|
||||
<el-button class="signup-btn" @click="handleRegist">注册</el-button>
|
||||
</el-form-item>
|
||||
<p class="agreement">点击注册,表示您同意《<router-link to="/article?id=1371992704333905920" target="_blank">商城用户协议</router-link>》</p>
|
||||
<p class="agreement">点击注册,表示您同意《<router-link to="/article?id=1371992704333905920">商城用户协议</router-link>》</p>
|
||||
</el-form>
|
||||
<Verify
|
||||
ref="verify"
|
||||
@@ -78,9 +78,9 @@
|
||||
</div>
|
||||
<div class="foot">
|
||||
<div type="flex" justify="space-around" class="help">
|
||||
<router-link to="/article" class="item" target="_blank">帮助</router-link>
|
||||
<router-link to="/article?id=1371779927900160000" class="item" target="_blank">隐私</router-link>
|
||||
<router-link to="/article?id=1371992704333905920" class="item" target="_blank">条款</router-link>
|
||||
<router-link to="/article" class="item">帮助</router-link>
|
||||
<router-link to="/article?id=1371779927900160000" class="item">隐私</router-link>
|
||||
<router-link to="/article?id=1371992704333905920" class="item">条款</router-link>
|
||||
</div>
|
||||
<div type="flex" justify="center" class="copyright">
|
||||
Copyright © {{year}} - Present
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -280,3 +280,9 @@ export default {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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} });
|
||||
|
||||
@@ -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() {
|
||||
// 获取订单详情
|
||||
|
||||
@@ -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() {
|
||||
// 清除全部足迹
|
||||
|
||||
@@ -836,11 +836,10 @@ export default {
|
||||
},
|
||||
// 跳转店铺首页
|
||||
goShopPage(id) {
|
||||
let routeUrl = this.$router.resolve({
|
||||
this.$router.push({
|
||||
path: "/merchant",
|
||||
query: { id: id },
|
||||
});
|
||||
window.open(routeUrl.href, "_blank");
|
||||
},
|
||||
useCoupon(id, used) {
|
||||
// 使用优惠券
|
||||
|
||||
@@ -29,46 +29,32 @@
|
||||
<div v-else class="goods-show-info1" v-for="(item, index) in goodsList" :key="index"
|
||||
@click="goGoodsDetail(item.skuId, item.goodsId)">
|
||||
<div class="goods-show-img">
|
||||
<img width="200" height="200" :src="item.goodsImage" />
|
||||
<img :src="item.goodsImage" :alt="item.goodsName" />
|
||||
</div>
|
||||
<div class="goods-show-price">
|
||||
<span>
|
||||
<span class="seckill-price text-danger">{{ $filters.unitPrice(item.price, "¥") }}</span>
|
||||
<span style="color: #999; text-decoration: line-through">{{ $filters.unitPrice(item.originalPrice, "¥") }}</span>
|
||||
</span>
|
||||
<span class="seckill-price text-danger">{{ $filters.unitPrice(item.price, "¥") }}</span>
|
||||
<span class="origin-price">{{ $filters.unitPrice(item.originalPrice, "¥") }}</span>
|
||||
</div>
|
||||
<div class="goods-show-detail" style="margin-bottom: 20px">
|
||||
<div class="goods-show-detail">
|
||||
<span>{{ item.goodsName }}</span>
|
||||
</div>
|
||||
<div class="goods-seckill-btn" :class="{
|
||||
'goods-seckill-btn-gray': nowHour < list[currIndex].timeLine,
|
||||
}">
|
||||
{{ nowHour >= list[currIndex].timeLine ? "立即抢购" : "即将开始" }}
|
||||
</div>
|
||||
<div v-if="nowHour >= list[currIndex].timeLine && item.quantity <= item.salesNum"
|
||||
class="goods-seckill-btn goods-seckill-btn-gray">
|
||||
已售罄
|
||||
</div>
|
||||
<div v-if="nowHour >= list[currIndex].timeLine" class="goods-show-num1">
|
||||
<span>已售{{
|
||||
(item.quantity && item.quantity > 0
|
||||
? Math.ceil(
|
||||
(item.salesNum / (item.quantity)) * 100
|
||||
)
|
||||
: 100) + "%"
|
||||
}}</span><el-progress
|
||||
<span>已售{{ salePercent(item) }}%</span>
|
||||
<el-progress
|
||||
:show-text="false"
|
||||
color="#df0021"
|
||||
style="width: 110px"
|
||||
class="ml_10"
|
||||
:percentage="item.quantity && item.quantity > 0
|
||||
? Math.ceil((item.salesNum / item.quantity) * 100)
|
||||
: 100"
|
||||
:color="'var(--theme-color, #ff3c2a)'"
|
||||
:percentage="salePercent(item)"
|
||||
/>
|
||||
</div>
|
||||
<div class="goods-show-seller">
|
||||
<span>{{ item.storeName }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="goods-seckill-btn"
|
||||
:class="{ 'is-disabled': !canBuy(item) }"
|
||||
>
|
||||
{{ buyText(item) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -142,6 +128,25 @@ export default {
|
||||
});
|
||||
window.open(routeUrl.href, "_blank");
|
||||
},
|
||||
// 已售占比。没有库存时按售罄展示
|
||||
salePercent (item) {
|
||||
if (!item.quantity || item.quantity <= 0) return 100;
|
||||
return Math.min(100, Math.ceil((item.salesNum / item.quantity) * 100));
|
||||
},
|
||||
// 场次已开始,且销量达到库存
|
||||
isSoldOut (item) {
|
||||
return this.nowHour >= this.list[this.currIndex].timeLine && item.quantity <= item.salesNum;
|
||||
},
|
||||
// 场次已开始且未售罄才可抢
|
||||
canBuy (item) {
|
||||
return this.nowHour >= this.list[this.currIndex].timeLine && !this.isSoldOut(item);
|
||||
},
|
||||
// 按钮文案:未开始 / 已售罄 / 立即抢购
|
||||
buyText (item) {
|
||||
if (this.nowHour < this.list[this.currIndex].timeLine) return "即将开始";
|
||||
if (this.isSoldOut(item)) return "已售罄";
|
||||
return "立即抢购";
|
||||
},
|
||||
countDown (currIndex) {
|
||||
// 倒计时
|
||||
// 0点时间戳
|
||||
@@ -175,73 +180,96 @@ export default {
|
||||
<style lang="scss" scoped>
|
||||
@import "../../assets/styles/goodsList.scss";
|
||||
|
||||
.goods-seckill-btn {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 80px;
|
||||
color: #fff;
|
||||
height: 35px;
|
||||
text-align: center;
|
||||
line-height: 35px;
|
||||
font-size: 14px;
|
||||
background-color: $theme_color;
|
||||
margin-bottom: 1px;
|
||||
margin-right: 1px;
|
||||
.seckill-list {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.goods-seckill-btn-gray {
|
||||
background-color: #666;
|
||||
}
|
||||
.promotion-decorate {
|
||||
width: auto;
|
||||
margin: 28px auto 16px;
|
||||
color: #171A26;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
|
||||
.promotion-decorate::before,
|
||||
.promotion-decorate::after {
|
||||
background-image: url("/src/assets/images/sprite@2x.png");
|
||||
&::before,
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.time-line {
|
||||
width: 1200px;
|
||||
max-width: 1200px;
|
||||
height: 60px;
|
||||
height: auto;
|
||||
margin: 0 auto;
|
||||
background-color: #fff;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
li {
|
||||
padding: 0 30px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
width: 240px;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
width: auto;
|
||||
min-height: 78px;
|
||||
padding: 12px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
gap: 4px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #171A26;
|
||||
box-sizing: border-box;
|
||||
|
||||
& + li {
|
||||
box-shadow: inset 1px 0 0 #f2f2f2;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.not-curr {
|
||||
border: 1px solid #999;
|
||||
border-radius: 20px;
|
||||
padding: 3px 10px;
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.currTimeline {
|
||||
background-color: $theme_color;
|
||||
background: $theme_color;
|
||||
color: #fff;
|
||||
|
||||
>div:nth-child(1) {
|
||||
font-size: 20px;
|
||||
&:hover {
|
||||
background: $theme_color;
|
||||
}
|
||||
|
||||
>div:nth-child(2) {
|
||||
font-size: 14px;
|
||||
margin-left: 10px;
|
||||
> div:nth-child(1) {
|
||||
font-size: 22px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
> div:nth-child(2) {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.not-curr {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -249,55 +277,116 @@ export default {
|
||||
.seckill-goods-wrap {
|
||||
width: 1200px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
margin: 16px auto 40px;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
min-height: 320px;
|
||||
|
||||
.goods-list {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
column-gap: 10px;
|
||||
column-gap: 16px;
|
||||
}
|
||||
|
||||
:deep(.empty) {
|
||||
margin: 48px 0;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-show-info1 {
|
||||
width: calc(25% - 8px);
|
||||
width: calc(25% - 12px);
|
||||
min-width: 0;
|
||||
padding: 6px;
|
||||
margin: 10px 0;
|
||||
padding: 12px;
|
||||
margin: 0 0 16px;
|
||||
position: relative;
|
||||
border: 1px solid #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.goods-show-info1:hover {
|
||||
border: 1px solid #ccc;
|
||||
box-shadow: 0px 0px 15px #ccc;
|
||||
&:hover {
|
||||
border-color: #eee;
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
}
|
||||
|
||||
.goods-show-img {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: #fafafa;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
height: auto;
|
||||
aspect-ratio: 1;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-show-price {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.origin-price {
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.goods-show-detail {
|
||||
height: 40px;
|
||||
margin: 8px 0;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.goods-show-num1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
margin-bottom: 6px;
|
||||
margin-bottom: 8px;
|
||||
color: #666;
|
||||
|
||||
span {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:deep(.el-progress) {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.el-progress-bar__inner {
|
||||
background-color: #df0021;
|
||||
.goods-show-seller {
|
||||
margin-bottom: 10px;
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.goods-seckill-btn {
|
||||
margin-top: auto;
|
||||
height: 34px;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
line-height: 34px;
|
||||
font-size: 14px;
|
||||
background-color: $theme_color;
|
||||
|
||||
&.is-disabled {
|
||||
background: #c8c8c8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -24,17 +24,25 @@
|
||||
<p>{{ userInfo.nickName }}</p>
|
||||
</div>
|
||||
|
||||
<el-menu-item index="UserCenter">
|
||||
<el-icon><House /></el-icon>
|
||||
<span>我的主页</span>
|
||||
</el-menu-item>
|
||||
<template v-for="(menu, index) in menuList" :key="index">
|
||||
<el-sub-menu v-if="menu.display" :index="menu.title">
|
||||
<template #title>
|
||||
<el-icon><Location /></el-icon>
|
||||
<el-icon><component :is="menuIcon(menu.icon)" /></el-icon>
|
||||
<span>{{ menu.title }}</span>
|
||||
</template>
|
||||
<el-menu-item
|
||||
v-for="(chlidren, i) in menu.menus"
|
||||
:key="i"
|
||||
:index="chlidren.path"
|
||||
>{{ chlidren.title }}</el-menu-item>
|
||||
>
|
||||
<el-icon><component :is="menuIcon(chlidren.icon)" /></el-icon>
|
||||
<span>{{ chlidren.title }}</span>
|
||||
<span v-if="chlidren.path === 'MsgList' && unreadMsg > 0" class="menu-badge">{{ unreadLabel }}</span>
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</template>
|
||||
</el-menu>
|
||||
@@ -48,12 +56,51 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Location, User } from '@element-plus/icons-vue';
|
||||
import {
|
||||
Bell,
|
||||
ChatDotRound,
|
||||
Clock,
|
||||
Coin,
|
||||
Goods,
|
||||
House,
|
||||
List,
|
||||
Location,
|
||||
Lock,
|
||||
Present,
|
||||
RefreshLeft,
|
||||
Star,
|
||||
Ticket,
|
||||
Trophy,
|
||||
User,
|
||||
Wallet,
|
||||
Warning,
|
||||
} from '@element-plus/icons-vue';
|
||||
import menuList from "./menu";
|
||||
import Storage from "@/plugins/storage.js";
|
||||
import { refreshUnreadMessage } from "@/utils/unreadMessage";
|
||||
|
||||
const MENU_ICONS = {
|
||||
Bell,
|
||||
ChatDotRound,
|
||||
Clock,
|
||||
Coin,
|
||||
Goods,
|
||||
House,
|
||||
List,
|
||||
Location,
|
||||
Lock,
|
||||
Present,
|
||||
RefreshLeft,
|
||||
Star,
|
||||
Ticket,
|
||||
Trophy,
|
||||
User,
|
||||
Wallet,
|
||||
Warning,
|
||||
};
|
||||
|
||||
export default {
|
||||
components: { Location, User },
|
||||
components: { House, User },
|
||||
name: "Home",
|
||||
data() {
|
||||
return {
|
||||
@@ -67,14 +114,24 @@ export default {
|
||||
}
|
||||
return {};
|
||||
},
|
||||
unreadMsg() {
|
||||
return Number(this.$store.state.unreadMsg) || 0;
|
||||
},
|
||||
unreadLabel() {
|
||||
return this.unreadMsg > 99 ? "99+" : String(this.unreadMsg);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
document.body.classList.add("buyer-user-center");
|
||||
refreshUnreadMessage();
|
||||
},
|
||||
beforeUnmount() {
|
||||
document.body.classList.remove("buyer-user-center");
|
||||
},
|
||||
methods: {
|
||||
menuIcon(name) {
|
||||
return MENU_ICONS[name] || Goods;
|
||||
},
|
||||
onSelect(name) {
|
||||
this.$router.push({ name });
|
||||
},
|
||||
@@ -134,6 +191,38 @@ export default {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.side-menu {
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-sub-menu__title) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.el-menu-item .el-icon),
|
||||
:deep(.el-sub-menu__title .el-icon) {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-badge {
|
||||
margin-left: auto;
|
||||
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;
|
||||
}
|
||||
|
||||
/* 选中项会把文字设成主题红。角标用 span,避免 i 继承成红底红字 */
|
||||
.side-menu :deep(.el-menu-item.is-active) .menu-badge {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.layout-footer-center {
|
||||
padding: 0px 15px;
|
||||
padding-bottom: 15px;
|
||||
|
||||
@@ -6,20 +6,20 @@
|
||||
|
||||
// 订单中心
|
||||
const order = [{
|
||||
icon: '',
|
||||
icon: 'List',
|
||||
title: '订单中心',
|
||||
menus: [{
|
||||
icon: '',
|
||||
icon: 'Goods',
|
||||
title: '我的订单',
|
||||
path: 'MyOrder'
|
||||
},
|
||||
{
|
||||
icon: '',
|
||||
icon: 'Location',
|
||||
title: '收货地址',
|
||||
path: 'MyAddress'
|
||||
},
|
||||
{
|
||||
icon: '',
|
||||
icon: 'RefreshLeft',
|
||||
title: '售后订单',
|
||||
path: 'AfterSale'
|
||||
}
|
||||
@@ -29,66 +29,66 @@ const order = [{
|
||||
|
||||
// 会员中心
|
||||
const member = [{
|
||||
icon: '',
|
||||
icon: 'User',
|
||||
title: '会员中心',
|
||||
menus: [{
|
||||
icon: '',
|
||||
icon: 'User',
|
||||
title: '用户信息',
|
||||
path: 'Profile'
|
||||
},
|
||||
{
|
||||
icon: '',
|
||||
icon: 'Lock',
|
||||
title: '账户安全',
|
||||
path: 'AccountSafe'
|
||||
},
|
||||
{
|
||||
icon: '',
|
||||
icon: 'Bell',
|
||||
title: '我的消息',
|
||||
path: 'MsgList'
|
||||
},
|
||||
{
|
||||
icon: '',
|
||||
icon: 'Clock',
|
||||
title: '我的足迹',
|
||||
path: 'MyTracks'
|
||||
},
|
||||
|
||||
{
|
||||
icon: '',
|
||||
icon: 'Star',
|
||||
title: '我的收藏',
|
||||
path: 'Favorites'
|
||||
},
|
||||
{
|
||||
icon: '',
|
||||
icon: 'ChatDotRound',
|
||||
title: '我的评论',
|
||||
path: 'CommentList'
|
||||
},
|
||||
{
|
||||
icon: '',
|
||||
icon: 'Warning',
|
||||
title: '我的投诉',
|
||||
path: 'ComplainList'
|
||||
},
|
||||
{
|
||||
icon: '',
|
||||
icon: 'Coin',
|
||||
title: '我的积分',
|
||||
path: 'Point'
|
||||
},
|
||||
{
|
||||
icon: '',
|
||||
icon: 'Trophy',
|
||||
title: '我的等级',
|
||||
path: 'MemberGrade'
|
||||
},
|
||||
{
|
||||
icon: '',
|
||||
icon: 'Ticket',
|
||||
title: '我的优惠券',
|
||||
path: 'Coupons'
|
||||
},
|
||||
{
|
||||
icon: '',
|
||||
icon: 'Wallet',
|
||||
title: '我的余额',
|
||||
path: 'MoneyManagement'
|
||||
},
|
||||
{
|
||||
icon: '',
|
||||
icon: 'Present',
|
||||
title: '我的礼品卡',
|
||||
path: 'MyGiftCards'
|
||||
}
|
||||
|
||||
@@ -51,7 +51,9 @@ const MoneyManagement = () => import("@/pages/home/userCenter/MoneyManagement");
|
||||
const MyGiftCards = () => import("@/pages/home/userCenter/MyGiftCards");
|
||||
|
||||
const Home = () => import("@/pages/user/Home");
|
||||
const UserCenter = () => import("@/pages/home/Main");
|
||||
const Merchant = () => import("@/pages/Merchant");
|
||||
const NotFound = () => import("@/pages/NotFound");
|
||||
const topic = () => import("@/pages/Topic");
|
||||
const ShopEntry = () => import("@/pages/shopEntry/ShopEntry");
|
||||
|
||||
@@ -178,8 +180,13 @@ export const router = createRouter({
|
||||
{
|
||||
path: "/home",
|
||||
component: Home,
|
||||
redirect: { name: "MyOrder" },
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "UserCenter",
|
||||
component: UserCenter,
|
||||
meta: { title: "个人中心" },
|
||||
},
|
||||
{
|
||||
path: "MyTracks",
|
||||
name: "MyTracks",
|
||||
@@ -213,19 +220,19 @@ export const router = createRouter({
|
||||
path: "CommentList",
|
||||
name: "CommentList",
|
||||
component: CommentList,
|
||||
mate: { title: "评价列表" },
|
||||
meta: { title: "评价列表" },
|
||||
},
|
||||
{
|
||||
path: "AddEval",
|
||||
name: "AddEval",
|
||||
component: AddEval,
|
||||
mate: { title: "添加评价" },
|
||||
meta: { title: "添加评价" },
|
||||
},
|
||||
{
|
||||
path: "EvalDetail",
|
||||
name: "EvalDetail",
|
||||
component: EvalDetail,
|
||||
mate: { title: "评价详情" },
|
||||
meta: { title: "评价详情" },
|
||||
},
|
||||
{
|
||||
path: "ComplainList",
|
||||
@@ -331,6 +338,12 @@ export const router = createRouter({
|
||||
component: Merchant,
|
||||
meta: { title: "店铺" },
|
||||
},
|
||||
{
|
||||
path: "/:pathMatch(.*)*",
|
||||
name: "NotFound",
|
||||
component: NotFound,
|
||||
meta: { title: "页面不存在" },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -1,16 +1,40 @@
|
||||
/**
|
||||
* 买家端内部链接跳转(首页装修、列表等共用)
|
||||
*
|
||||
* @author Mike
|
||||
* @date 2026-08-05
|
||||
* 商品详情新开标签,其余站内页面在当前页打开。外链仍新开。
|
||||
*/
|
||||
|
||||
/** 内部链接新窗口打开,外链直接打开。浏览商品详情不要求登录。 */
|
||||
const NEW_TAB_PATHS = ["/goodsDetail", "/pointGoodsDetail"];
|
||||
|
||||
function isExternalLink(url) {
|
||||
return /^https?:\/\//i.test(url) || url.startsWith("//");
|
||||
}
|
||||
|
||||
function normalizeInternal(url) {
|
||||
return url.charAt(0) === "/" ? url : `/${url}`;
|
||||
}
|
||||
|
||||
function pathnameOf(url) {
|
||||
return normalizeInternal(url).split("?")[0].split("#")[0];
|
||||
}
|
||||
|
||||
export function shouldOpenNewTab(url) {
|
||||
if (!url) return false;
|
||||
if (isExternalLink(url)) return true;
|
||||
const path = pathnameOf(url);
|
||||
return NEW_TAB_PATHS.indexOf(path) !== -1;
|
||||
}
|
||||
|
||||
export function openLink(url, router) {
|
||||
if (!url) return;
|
||||
if (url.charAt(0) !== "/") {
|
||||
if (isExternalLink(url)) {
|
||||
window.open(url, "_blank");
|
||||
return;
|
||||
}
|
||||
window.open(router.resolve(url).href, "_blank");
|
||||
const internal = normalizeInternal(url);
|
||||
if (shouldOpenNewTab(internal)) {
|
||||
window.open(router.resolve(internal).href, "_blank");
|
||||
return;
|
||||
}
|
||||
return router.push(internal);
|
||||
}
|
||||
|
||||
24
buyer/src/utils/unreadMessage.js
Normal file
24
buyer/src/utils/unreadMessage.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import store from "@/vuex/store";
|
||||
import storage from "@/plugins/storage";
|
||||
import { memberMsgList } from "@/api/member";
|
||||
|
||||
// 用未读消息总数刷新角标,只取 1 条记录
|
||||
export function refreshUnreadMessage() {
|
||||
if (!storage.getItem("userInfo")) {
|
||||
store.commit("SET_UNREAD_MSG", 0);
|
||||
return Promise.resolve(0);
|
||||
}
|
||||
return memberMsgList({
|
||||
pageNumber: 1,
|
||||
pageSize: 1,
|
||||
status: "UN_READY",
|
||||
})
|
||||
.then((res) => {
|
||||
const total = res && res.success ? Number(res.result && res.result.total) || 0 : 0;
|
||||
store.commit("SET_UNREAD_MSG", total);
|
||||
return total;
|
||||
})
|
||||
.catch(() => {
|
||||
return Number(store.state.unreadMsg) || 0;
|
||||
});
|
||||
}
|
||||
@@ -9,6 +9,9 @@ export const SET_NAVLIST = (state, data) => {
|
||||
export const SET_CARTNUM = (state, data) => {
|
||||
state.cartNum = data
|
||||
}
|
||||
export const SET_UNREAD_MSG = (state, data) => {
|
||||
state.unreadMsg = Number(data) || 0
|
||||
}
|
||||
export const SET_HOTWORDS = (state, data) => {
|
||||
state.hotWordsList = data
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ export default createStore({
|
||||
state: {
|
||||
navList: [],
|
||||
cartNum: storage.getItem("cartNum") || 0,
|
||||
unreadMsg: 0,
|
||||
logoImg: storage.getItem("logoImg") || defaultLogo,
|
||||
siteName: storage.getItem("siteName") || "lilishop",
|
||||
hotWordsList: storage.getItem("hotWordsList"),
|
||||
|
||||
Reference in New Issue
Block a user