feat(buyer): 新增PC端店铺列表页面并优化搜索与商品缩略图

This commit is contained in:
pikachu1995@126.com
2026-10-29 17:00:03 +08:00
parent 5c7b109be6
commit d113f5c553
5 changed files with 1007 additions and 10 deletions

View File

@@ -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}`,

View File

@@ -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;

View File

@@ -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 {

View 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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
// 店铺名关键词高亮(先转义,避免 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>

View File

@@ -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",