mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-11-06 17:09:08 +08:00
feat(buyer): 新增PC端店铺列表页面并优化搜索与商品缩略图
This commit is contained in:
@@ -6,7 +6,8 @@ import request, {
|
||||
export function shopList (params) {
|
||||
return request({
|
||||
url: '/buyer/store/store',
|
||||
needToken: true,
|
||||
needToken: false,
|
||||
optionalToken: true,
|
||||
method: Method.GET,
|
||||
params
|
||||
})
|
||||
@@ -52,6 +53,16 @@ export function getStoreById (id) {
|
||||
}
|
||||
|
||||
// 店铺详情
|
||||
// 获取店铺工商/营业执照信息
|
||||
export function getLicencePhoto (id) {
|
||||
return request({
|
||||
url: `/buyer/store/store/get/licencePhoto/${id}`,
|
||||
needToken: false,
|
||||
optionalToken: true,
|
||||
method: Method.GET
|
||||
})
|
||||
}
|
||||
|
||||
export function getDetailById (id) {
|
||||
return request({
|
||||
url: `/buyer/store/store/get/detail/${id}`,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
v-model="searchData"
|
||||
size="large"
|
||||
class="search-input"
|
||||
placeholder="输入你想查找的商品"
|
||||
:placeholder="placeholderText"
|
||||
@focus="openPanel"
|
||||
@input="queueSuggest"
|
||||
@keyup.enter="search"
|
||||
@@ -37,12 +37,22 @@
|
||||
@click="$router.push('/')"
|
||||
/>
|
||||
<div class="search-box">
|
||||
<!-- 搜索类型切换:商品 / 店铺(店铺内搜索时不展示) -->
|
||||
<div v-if="showTypeSwitch" class="search-type">
|
||||
<span
|
||||
v-for="item in searchTypes"
|
||||
:key="item.value"
|
||||
class="search-type-item"
|
||||
:class="{ active: searchType === item.value }"
|
||||
@click="switchType(item.value)"
|
||||
>{{ item.label }}</span>
|
||||
</div>
|
||||
<div class="search">
|
||||
<el-input
|
||||
v-model="searchData"
|
||||
size="large"
|
||||
class="search-input"
|
||||
placeholder="输入你想查找的商品"
|
||||
:placeholder="placeholderText"
|
||||
@focus="openPanel"
|
||||
@input="queueSuggest"
|
||||
@keyup.enter="search"
|
||||
@@ -165,7 +175,27 @@ const promotionTags = computed(() => {
|
||||
return parseHotWords(appStore.hotWordsList)
|
||||
})
|
||||
|
||||
// 搜索类型:goods 商品 / store 店铺
|
||||
const searchTypes = [
|
||||
{ label: '商品', value: 'goods' },
|
||||
{ label: '店铺', value: 'store' },
|
||||
]
|
||||
const searchType = ref(route.path === '/storeList' ? 'store' : 'goods')
|
||||
// 店铺内搜索(传了 storeId)只能搜本店商品,不展示切换
|
||||
const showTypeSwitch = computed(() => !props.storeId && props.useClass !== 'fixed-show')
|
||||
const isStoreMode = computed(() => searchType.value === 'store' && !props.storeId)
|
||||
const placeholderText = computed(() => (isStoreMode.value ? '输入你想查找的店铺' : '输入你想查找的商品'))
|
||||
|
||||
function switchType(type) {
|
||||
if (searchType.value === type) return
|
||||
searchType.value = type
|
||||
suggestions.value = []
|
||||
suggesting.value = false
|
||||
}
|
||||
|
||||
const showPanel = computed(() => {
|
||||
// 店铺搜索暂无联想/历史,不展示下拉面板
|
||||
if (isStoreMode.value) return false
|
||||
if (!panelVisible.value) return false
|
||||
if (trimmedKeyword.value) return true
|
||||
return history.value.length > 0 || promotionTags.value.length > 0
|
||||
@@ -179,6 +209,8 @@ watch(
|
||||
)
|
||||
|
||||
function selectTags(item) {
|
||||
// 热词/历史均为商品关键词,点击时按商品搜索
|
||||
searchType.value = 'goods'
|
||||
searchData.value = item
|
||||
search()
|
||||
}
|
||||
@@ -208,6 +240,7 @@ function onDocumentClick(event) {
|
||||
}
|
||||
|
||||
function queueSuggest() {
|
||||
if (isStoreMode.value) return
|
||||
panelVisible.value = true
|
||||
clearTimeout(suggestTimer)
|
||||
const keyword = trimmedKeyword.value
|
||||
@@ -254,6 +287,14 @@ function search() {
|
||||
const keyword = trimmedKeyword.value
|
||||
searchData.value = keyword
|
||||
closePanel()
|
||||
if (isStoreMode.value) {
|
||||
if (route.path === '/storeList') {
|
||||
emit('search', keyword)
|
||||
} else {
|
||||
router.push({ path: '/storeList', query: keyword ? { keyword } : {} })
|
||||
}
|
||||
return
|
||||
}
|
||||
if (keyword) remember(keyword)
|
||||
const url = route.path
|
||||
if (url == '/goodsList') {
|
||||
@@ -261,7 +302,7 @@ function search() {
|
||||
} else {
|
||||
const pushData = {
|
||||
path: '/goodsList',
|
||||
query: { keyword },
|
||||
query: keyword ? { keyword } : {},
|
||||
}
|
||||
if (props.storeId) pushData.query.storeId = props.storeId
|
||||
|
||||
@@ -346,9 +387,8 @@ onUnmounted(() => {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
border: 1.4px solid $theme_color;
|
||||
box-sizing: border-box;
|
||||
border-radius: 19.6px;
|
||||
border: 2px solid #409EFF;
|
||||
border-radius: 4px;
|
||||
padding-left: 26px;
|
||||
padding-right: calc(#{$btn-gap * 2} + #{$btn-width});
|
||||
font-size: 14px;
|
||||
@@ -365,15 +405,20 @@ onUnmounted(() => {
|
||||
top: $btn-gap;
|
||||
bottom: $btn-gap;
|
||||
width: $btn-width;
|
||||
border-radius: calc((#{$input-height} - #{$btn-gap * 2}) / 2);
|
||||
border-radius: 0 3px 3px 0;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
background-color: $theme_color;
|
||||
background-color: #409EFF;
|
||||
color: #ffffff;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: #66b1ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,6 +429,38 @@ onUnmounted(() => {
|
||||
.search-box{
|
||||
margin-left: 28px;
|
||||
}
|
||||
|
||||
.search-type {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
height: 22px;
|
||||
padding-left: 18px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.search-type-item {
|
||||
padding: 2px 10px;
|
||||
border-radius: 4px 4px 0 0;
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
color: $theme_color;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #fff;
|
||||
background: $theme_color;
|
||||
}
|
||||
}
|
||||
|
||||
.search-type + .search {
|
||||
margin-top: 0;
|
||||
}
|
||||
.logo-img {
|
||||
max-width: 150px;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<template>
|
||||
<div class="goods-thumb" ref="rootEl" :class="{ 'is-loaded': loaded, 'is-error': error }">
|
||||
<img v-if="currentSrc" :src="currentSrc" :alt="alt" @load="loaded = true" @error="onError" />
|
||||
<img v-else-if="error" :src="defaultImg" :alt="alt || '默认商品图片'" class="default-img" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import defaultImg from "@/assets/images/default.png";
|
||||
|
||||
defineOptions({ name: "GoodsThumb" });
|
||||
|
||||
@@ -26,6 +28,21 @@ const loaded = ref(false);
|
||||
const error = ref(false);
|
||||
let observer = null;
|
||||
|
||||
function isValidUrl(url) {
|
||||
if (!url || typeof url !== "string") return false;
|
||||
const s = url.trim().toLowerCase();
|
||||
if (
|
||||
!s ||
|
||||
s === "null" ||
|
||||
s === "undefined" ||
|
||||
s.startsWith("null?") ||
|
||||
s.startsWith("undefined?")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.src,
|
||||
() => {
|
||||
@@ -45,7 +62,7 @@ function observe() {
|
||||
currentSrc.value = "";
|
||||
loaded.value = false;
|
||||
error.value = false;
|
||||
if (!props.src) {
|
||||
if (!isValidUrl(props.src)) {
|
||||
error.value = true;
|
||||
return;
|
||||
}
|
||||
@@ -109,6 +126,13 @@ onBeforeUnmount(() => {
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
|
||||
&.default-img {
|
||||
object-fit: contain;
|
||||
padding: 14px;
|
||||
box-sizing: border-box;
|
||||
opacity: 0.55;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes goods-thumb-loading {
|
||||
|
||||
878
buyer/src/pages/StoreList.vue
Normal file
878
buyer/src/pages/StoreList.vue
Normal file
@@ -0,0 +1,878 @@
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<TopBar></TopBar>
|
||||
<SearchHeader @search="handleSearch"></SearchHeader>
|
||||
|
||||
<div class="container">
|
||||
<!-- 工具栏:排序 + 筛选 -->
|
||||
<div class="store-tool">
|
||||
<div class="store-sort">
|
||||
<span
|
||||
v-for="(item, index) in sortTools"
|
||||
:key="item.value"
|
||||
class="store-sort-item"
|
||||
:class="{ active: index === sortIndex }"
|
||||
@click="changeSort(index)"
|
||||
>
|
||||
{{ item.title }}
|
||||
<el-icon v-if="index === sortIndex && item.value"><ArrowDown /></el-icon>
|
||||
</span>
|
||||
</div>
|
||||
<div class="store-tool-right">
|
||||
<span class="store-total-text">
|
||||
<template v-if="params.storeName">
|
||||
“<span class="keyword">{{ params.storeName }}</span>”
|
||||
</template>
|
||||
共找到 <span class="num">{{ total }}</span> 家店铺
|
||||
</span>
|
||||
<el-checkbox v-model="selfOnly" @change="changeSelfOnly">只看自营</el-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 店铺列表 -->
|
||||
<div class="store-list" v-loading="loading">
|
||||
<div v-if="searched && !loading && storeList.length === 0" class="store-empty">
|
||||
<empty :_Title="emptyTitle" />
|
||||
</div>
|
||||
|
||||
<div v-for="store in storeList" :key="store.id" class="store-item">
|
||||
<!-- 店铺信息 -->
|
||||
<div class="store-info">
|
||||
<div class="store-logo" @click="goShopPage(store.id)">
|
||||
<img
|
||||
v-if="store.storeLogo"
|
||||
:src="store.storeLogo"
|
||||
:alt="store.storeName"
|
||||
loading="lazy"
|
||||
@error="$event.target.style.visibility = 'hidden'"
|
||||
/>
|
||||
<span v-else class="store-logo-text">{{ (store.storeName || '店').slice(0, 1) }}</span>
|
||||
</div>
|
||||
<div class="store-meta">
|
||||
<div class="store-name" @click="goShopPage(store.id)">
|
||||
<span class="store-name-text" :title="store.storeName" v-html="highlight(store.storeName)"></span>
|
||||
<span v-if="store.selfOperated" class="store-tag-self">自营</span>
|
||||
</div>
|
||||
|
||||
<!-- 结构化字段:商家名称 / 所在地区 / 工商信息 -->
|
||||
<div class="store-fields">
|
||||
<div class="field-row">
|
||||
<span class="field-label">商家名称</span>
|
||||
<span class="field-value" :title="store.storeName">{{ store.storeName || '-' }}</span>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<span class="field-label">所在地区</span>
|
||||
<span class="field-value" :title="formatRegion(store.storeAddressPath)">
|
||||
{{ formatRegion(store.storeAddressPath) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<span class="field-label">工商信息</span>
|
||||
<span class="field-value">
|
||||
<span class="licence-btn" @click.stop="openLicence(store)">[查看详情]</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="store-ops">
|
||||
<button class="btn-enter" @click="goShopPage(store.id)">
|
||||
进入店铺<el-icon><ArrowRight /></el-icon>
|
||||
</button>
|
||||
<button
|
||||
class="btn-collect"
|
||||
:class="{ 'is-collected': collectedMap[store.id] }"
|
||||
@click="toggleCollect(store)"
|
||||
>
|
||||
{{ collectedMap[store.id] ? '已收藏' : '+ 收藏' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 店铺热销商品 -->
|
||||
<div class="store-goods">
|
||||
<template v-if="goodsMap[store.id] && goodsMap[store.id].length">
|
||||
<div
|
||||
v-for="goods in goodsMap[store.id]"
|
||||
:key="goods.id"
|
||||
class="store-goods-item"
|
||||
@click="goGoodsDetail(goods.id, goods.goodsId)"
|
||||
>
|
||||
<div class="store-goods-img">
|
||||
<GoodsThumb :src="goods.thumbnail || goods.small" :alt="goods.goodsName" />
|
||||
</div>
|
||||
<div class="store-goods-name" :title="goods.goodsName">{{ goods.goodsName }}</div>
|
||||
<div class="store-goods-price">{{ $filters.unitPrice(goods.price, '¥') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else-if="goodsMap[store.id]" class="store-goods-empty">
|
||||
店铺暂无在售商品,<span @click="goShopPage(store.id)">去店铺逛逛 ›</span>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div v-for="n in PREVIEW_GOODS_SIZE" :key="n" class="store-goods-item is-placeholder">
|
||||
<div class="store-goods-img"><GoodsThumb src="" /></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="store-page" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="params.pageNumber"
|
||||
v-model:page-size="params.pageSize"
|
||||
:page-sizes="[10, 20, 30]"
|
||||
:total="total"
|
||||
layout="total, prev, pager, next, sizes"
|
||||
@current-change="getStoreList"
|
||||
@size-change="changePageSize"
|
||||
></el-pagination>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工商资质信息弹窗 -->
|
||||
<el-dialog
|
||||
v-model="licenceDialogVisible"
|
||||
title="工商资质信息公示"
|
||||
width="520px"
|
||||
align-center
|
||||
destroy-on-close
|
||||
>
|
||||
<div v-loading="licenceLoading" class="licence-modal">
|
||||
<template v-if="licenceData">
|
||||
<div class="licence-row">
|
||||
<span class="l-label">店铺名称</span>
|
||||
<span class="l-val">{{ currentStore?.storeName || '-' }}</span>
|
||||
</div>
|
||||
<div class="licence-row" v-if="licenceData.companyName">
|
||||
<span class="l-label">企业名称</span>
|
||||
<span class="l-val">{{ licenceData.companyName }}</span>
|
||||
</div>
|
||||
<div class="licence-row" v-if="licenceData.companyAddressPath || licenceData.companyAddress">
|
||||
<span class="l-label">企业住所</span>
|
||||
<span class="l-val">
|
||||
{{ formatRegion(licenceData.companyAddressPath) }} {{ licenceData.companyAddress || '' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="licence-row" v-if="licenceData.scope">
|
||||
<span class="l-label">经营范围</span>
|
||||
<span class="l-val">{{ licenceData.scope }}</span>
|
||||
</div>
|
||||
<div class="licence-photo-box">
|
||||
<div class="photo-title">营业执照电子版</div>
|
||||
<div v-if="licenceData.licencePhoto" class="photo-wrap">
|
||||
<el-image
|
||||
:src="licenceData.licencePhoto"
|
||||
:preview-src-list="[licenceData.licencePhoto]"
|
||||
fit="contain"
|
||||
class="licence-img"
|
||||
/>
|
||||
<div class="photo-hint">点击图片可放大查看</div>
|
||||
</div>
|
||||
<div v-else class="photo-empty">暂未上传营业执照电子版</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else-if="!licenceLoading" class="licence-none">
|
||||
暂未查询到该店铺的工商资质信息
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<BaseFooter></BaseFooter>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "StoreList",
|
||||
beforeRouteEnter(to, from, next) {
|
||||
window.scrollTo(0, 0);
|
||||
next();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { ArrowDown, ArrowRight } from "@element-plus/icons-vue";
|
||||
import { Message } from "@/utils/message";
|
||||
import storage from "@/plugins/storage";
|
||||
import GoodsThumb from "@/components/goods/GoodsThumb.vue";
|
||||
import TopBar from '@/components/indexDecorate/components/TopBar.vue';
|
||||
import SearchHeader from '@/components/indexDecorate/components/SearchHeader.vue';
|
||||
import { shopList, getLicencePhoto } from "@/api/shopentry";
|
||||
import { goodsList as goodsListApi } from "@/api/goods";
|
||||
import { cancelStoreCollect, collectStore, isStoreCollection } from "@/api/member";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
// 每个店铺预览的商品数量
|
||||
const PREVIEW_GOODS_SIZE = 5;
|
||||
|
||||
// 排序项(字段由后端 camel2Underline 转为列名)
|
||||
const sortTools = [
|
||||
{ title: "综合", value: "" },
|
||||
{ title: "商品数", value: "goodsNum" },
|
||||
{ title: "收藏数", value: "collectionNum" },
|
||||
{ title: "服务评分", value: "serviceScore" },
|
||||
];
|
||||
const sortIndex = ref(0);
|
||||
const selfOnly = ref(false);
|
||||
|
||||
const params = reactive({
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
storeName: route.query.keyword || "",
|
||||
// 买家端只展示营业中的店铺
|
||||
storeDisable: "OPEN",
|
||||
sort: "",
|
||||
order: "",
|
||||
});
|
||||
|
||||
const storeList = ref([]);
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
const searched = ref(false);
|
||||
// 店铺预览商品:storeId -> goods[]
|
||||
const goodsMap = reactive({});
|
||||
// 收藏状态:storeId -> boolean
|
||||
const collectedMap = reactive({});
|
||||
let requestSeq = 0;
|
||||
|
||||
// 工商资质弹窗状态
|
||||
const licenceDialogVisible = ref(false);
|
||||
const licenceLoading = ref(false);
|
||||
const currentStore = ref(null);
|
||||
const licenceData = ref(null);
|
||||
|
||||
const emptyTitle = computed(() =>
|
||||
params.storeName ? `没有找到与“${params.storeName}”相关的店铺` : "暂无店铺"
|
||||
);
|
||||
|
||||
watch(
|
||||
() => route.query.keyword,
|
||||
(keyword) => {
|
||||
if (route.path !== "/storeList") return;
|
||||
params.storeName = keyword || "";
|
||||
params.pageNumber = 1;
|
||||
getStoreList();
|
||||
}
|
||||
);
|
||||
|
||||
// 格式化地区展示(如 "北京市,东城区" -> "北京市东城区")
|
||||
function formatRegion(path) {
|
||||
if (!path) return "暂未填写";
|
||||
const parts = String(path).split(/[,,\s]+/).filter(Boolean);
|
||||
if (!parts.length) return "暂未填写";
|
||||
const deduped = [];
|
||||
parts.forEach((p) => {
|
||||
if (!deduped.length || deduped[deduped.length - 1] !== p) {
|
||||
deduped.push(p);
|
||||
}
|
||||
});
|
||||
return deduped.join("") || "暂未填写";
|
||||
}
|
||||
|
||||
// 打开工商资质信息弹窗
|
||||
function openLicence(store) {
|
||||
currentStore.value = store;
|
||||
licenceData.value = null;
|
||||
licenceDialogVisible.value = true;
|
||||
licenceLoading.value = true;
|
||||
getLicencePhoto(store.id)
|
||||
.then((res) => {
|
||||
if (res && res.success && res.result) {
|
||||
licenceData.value = res.result;
|
||||
} else {
|
||||
licenceData.value = null;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
licenceData.value = null;
|
||||
})
|
||||
.finally(() => {
|
||||
licenceLoading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
// 搜索框(店铺模式)回调:写入 URL,便于刷新 / 回退
|
||||
function handleSearch(keyword) {
|
||||
const value = keyword || "";
|
||||
if (value === (route.query.keyword || "")) {
|
||||
params.storeName = value;
|
||||
params.pageNumber = 1;
|
||||
getStoreList();
|
||||
return;
|
||||
}
|
||||
router.push({ path: "/storeList", query: value ? { keyword: value } : {} });
|
||||
}
|
||||
|
||||
function changeSort(index) {
|
||||
sortIndex.value = index;
|
||||
const { value } = sortTools[index];
|
||||
params.sort = value;
|
||||
params.order = value ? "desc" : "";
|
||||
params.pageNumber = 1;
|
||||
getStoreList();
|
||||
}
|
||||
|
||||
function changeSelfOnly() {
|
||||
params.pageNumber = 1;
|
||||
getStoreList();
|
||||
}
|
||||
|
||||
function changePageSize(size) {
|
||||
params.pageSize = size;
|
||||
params.pageNumber = 1;
|
||||
getStoreList();
|
||||
}
|
||||
|
||||
function getStoreList() {
|
||||
const seq = ++requestSeq;
|
||||
loading.value = true;
|
||||
const query = { ...params };
|
||||
if (!query.sort) {
|
||||
delete query.sort;
|
||||
delete query.order;
|
||||
}
|
||||
if (!query.storeName) delete query.storeName;
|
||||
if (selfOnly.value) query.selfOperated = true;
|
||||
shopList(query)
|
||||
.then((res) => {
|
||||
if (seq !== requestSeq) return;
|
||||
if (res.success) {
|
||||
storeList.value = res.result.records || [];
|
||||
total.value = res.result.total || 0;
|
||||
loadStoreExtras(storeList.value);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (seq !== requestSeq) return;
|
||||
storeList.value = [];
|
||||
total.value = 0;
|
||||
})
|
||||
.finally(() => {
|
||||
if (seq !== requestSeq) return;
|
||||
loading.value = false;
|
||||
searched.value = true;
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
}
|
||||
|
||||
// 加载每个店铺的预览商品与收藏状态(失败不影响列表展示)
|
||||
function loadStoreExtras(list) {
|
||||
const logged = !!storage.getItem("userInfo");
|
||||
list.forEach((store) => {
|
||||
const storeId = store.id;
|
||||
if (!storeId) return;
|
||||
if (!goodsMap[storeId]) {
|
||||
goodsListApi({
|
||||
storeId,
|
||||
pageNumber: 1,
|
||||
pageSize: PREVIEW_GOODS_SIZE,
|
||||
sort: "buyCount",
|
||||
order: "desc",
|
||||
})
|
||||
.then((res) => {
|
||||
goodsMap[storeId] = res && res.success ? res.result.records || [] : [];
|
||||
})
|
||||
.catch(() => {
|
||||
goodsMap[storeId] = [];
|
||||
});
|
||||
}
|
||||
if (logged && collectedMap[storeId] === undefined) {
|
||||
isStoreCollection("STORE", storeId)
|
||||
.then((res) => {
|
||||
collectedMap[storeId] = !!(res && res.success && res.result);
|
||||
})
|
||||
.catch(() => {
|
||||
// 收藏状态查询失败静默处理
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function toggleCollect(store) {
|
||||
const storeId = store.id;
|
||||
if (!storage.getItem("userInfo")) {
|
||||
Message.warning("请先登录");
|
||||
router.push("/login");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (collectedMap[storeId]) {
|
||||
const res = await cancelStoreCollect("STORE", storeId);
|
||||
if (res.success) {
|
||||
collectedMap[storeId] = false;
|
||||
store.collectionNum = Math.max((store.collectionNum || 1) - 1, 0);
|
||||
Message.success("已取消收藏");
|
||||
}
|
||||
} else {
|
||||
const res = await collectStore("STORE", storeId);
|
||||
if (res.success || res.code === 200) {
|
||||
collectedMap[storeId] = true;
|
||||
store.collectionNum = (store.collectionNum || 0) + 1;
|
||||
Message.success("收藏店铺成功,可前往我的收藏查看");
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
const msg = err?.message || err?.data?.message || "";
|
||||
if (msg.includes("重复收藏")) {
|
||||
collectedMap[storeId] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
// 店铺名关键词高亮(先转义,避免 XSS)
|
||||
function highlight(name) {
|
||||
const text = escapeHtml(name || "");
|
||||
const keyword = (params.storeName || "").trim();
|
||||
if (!keyword) return text;
|
||||
const escapedKeyword = escapeHtml(keyword).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
return text.replace(new RegExp(escapedKeyword, "gi"), (m) => `<em class="hl">${m}</em>`);
|
||||
}
|
||||
|
||||
function goShopPage(id) {
|
||||
if (!id) {
|
||||
Message.warning("店铺信息不存在");
|
||||
return;
|
||||
}
|
||||
router.push({ path: "/merchant", query: { id } });
|
||||
}
|
||||
|
||||
function goGoodsDetail(skuId, goodsId) {
|
||||
const location = { path: "/goodsDetail", query: { skuId, goodsId } };
|
||||
window.open(router.resolve(location).href, "_blank");
|
||||
}
|
||||
|
||||
getStoreList();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$theme_light: color-mix(in srgb, #{$theme_color} 8%, #fff);
|
||||
|
||||
.container {
|
||||
margin: 20px auto 32px;
|
||||
width: 1200px;
|
||||
min-width: 1000px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* ---------------工具栏------------------- */
|
||||
.store-tool {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 56px;
|
||||
padding: 0 20px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.store-tool-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.store-total-text {
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
|
||||
.keyword,
|
||||
.num {
|
||||
color: $theme_color;
|
||||
}
|
||||
}
|
||||
|
||||
.store-sort {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.store-sort-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
height: 30px;
|
||||
padding: 0 16px;
|
||||
border-radius: 15px;
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
color: $theme_color;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: $theme_color;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------店铺列表------------------- */
|
||||
.store-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-height: 240px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.store-empty {
|
||||
padding: 40px 0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.store-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 28px;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
transition: box-shadow 0.2s, transform 0.2s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.store-info {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 16px;
|
||||
width: 350px;
|
||||
padding-right: 28px;
|
||||
border-right: 1px dashed #e8e8e8;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.store-logo {
|
||||
flex-shrink: 0;
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
border: 2px solid #f2f2f2;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.store-logo-text {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
background: $theme_color;
|
||||
}
|
||||
|
||||
.store-meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.store-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
|
||||
.store-name-text {
|
||||
overflow: hidden;
|
||||
color: #171a26;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:hover .store-name-text {
|
||||
color: $theme_color;
|
||||
}
|
||||
|
||||
:deep(.hl) {
|
||||
color: $theme_color;
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.store-tag-self {
|
||||
flex-shrink: 0;
|
||||
padding: 0 5px;
|
||||
border-radius: 3px;
|
||||
background: $theme_color;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
/* 结构化属性 */
|
||||
.store-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.field-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
|
||||
.field-label {
|
||||
flex-shrink: 0;
|
||||
width: 56px;
|
||||
color: #999;
|
||||
text-align: justify;
|
||||
text-align-last: justify;
|
||||
}
|
||||
|
||||
.field-value {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.licence-btn {
|
||||
color: #2d8cf0;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
color: $theme_color;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.store-ops {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
|
||||
button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
height: 28px;
|
||||
padding: 0 14px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s, background 0.2s;
|
||||
}
|
||||
|
||||
.btn-enter {
|
||||
border: none;
|
||||
background: $theme_color;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.88;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-collect {
|
||||
border: 1px solid $theme_color;
|
||||
background: #fff;
|
||||
color: $theme_color;
|
||||
|
||||
&:hover {
|
||||
background: $theme_light;
|
||||
}
|
||||
|
||||
&.is-collected {
|
||||
border-color: #ddd;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------热销商品------------------- */
|
||||
.store-goods {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.store-goods-item {
|
||||
min-width: 0;
|
||||
cursor: pointer;
|
||||
|
||||
.store-goods-img {
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
&:hover .store-goods-img {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
&:hover .store-goods-name {
|
||||
color: $theme_color;
|
||||
}
|
||||
|
||||
&.is-placeholder {
|
||||
cursor: default;
|
||||
|
||||
.store-goods-img {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.store-goods-name {
|
||||
margin-top: 8px;
|
||||
overflow: hidden;
|
||||
color: #555;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.store-goods-price {
|
||||
margin-top: 2px;
|
||||
color: $theme_color;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.store-goods-empty {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 120px;
|
||||
border-radius: 6px;
|
||||
background: #fafafa;
|
||||
color: #bbb;
|
||||
font-size: 13px;
|
||||
|
||||
span {
|
||||
color: $theme_color;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.store-page {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* ---------------工商资质弹窗------------------- */
|
||||
.licence-modal {
|
||||
min-height: 160px;
|
||||
padding: 8px 12px 16px;
|
||||
|
||||
.licence-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
|
||||
.l-label {
|
||||
flex-shrink: 0;
|
||||
width: 70px;
|
||||
color: #999;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.l-val {
|
||||
flex: 1;
|
||||
color: #333;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.licence-photo-box {
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px dashed #eee;
|
||||
|
||||
.photo-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.photo-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.licence-img {
|
||||
max-width: 100%;
|
||||
max-height: 260px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
background: #fafafa;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.photo-hint {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.photo-empty {
|
||||
padding: 20px 0;
|
||||
text-align: center;
|
||||
color: #bbb;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.licence-none {
|
||||
padding: 40px 0;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -53,6 +53,7 @@ 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 StoreList = () => import("@/pages/StoreList");
|
||||
const NotFound = () => import("@/pages/NotFound");
|
||||
const topic = () => import("@/pages/Topic");
|
||||
const ShopEntry = () => import("@/pages/shopEntry/ShopEntry");
|
||||
@@ -338,6 +339,12 @@ export const router = createRouter({
|
||||
component: Merchant,
|
||||
meta: { title: "店铺" },
|
||||
},
|
||||
{
|
||||
path: "/storeList",
|
||||
name: "StoreList",
|
||||
component: StoreList,
|
||||
meta: { title: "店铺列表" },
|
||||
},
|
||||
{
|
||||
path: "/:pathMatch(.*)*",
|
||||
name: "NotFound",
|
||||
|
||||
Reference in New Issue
Block a user