refactor: 四端完成 Options API → script setup + vuex → pinia 迁移

- buyer(95) / seller(169) / im(53) / manager(280) 共 597 个组件全部从 Options API 转换为 <script setup>
- 四端 vuex 全部移除,状态管理统一迁移至 pinia 4(stores/ 目录)
- util.js 等基础设施去除 vm 首参依赖;全局属性($Message/$filters 等)在 script 内改为具名导入
- 被外部 ref 访问的公共组件均已补齐 defineExpose(清单见各端 MIGRATION-VUE3.md)
- 各端 eslint 启用 vue/component-api-style 防止新代码回退 Options API
- 四端 yarn build 全部通过;迁移约定与注意事项已更新至各端 MIGRATION-VUE3.md
This commit is contained in:
lifenlong
2026-10-13 07:06:59 +08:00
parent 870bacdd8c
commit 88fc1b0b2a
674 changed files with 71220 additions and 69943 deletions

View File

@@ -18,60 +18,57 @@
</span>
</div>
</template>
<script>
<script setup>
import { onMounted, ref } from 'vue';
import { AlarmClock } from '@element-plus/icons-vue';
export default {
components: { AlarmClock },
props: {
time: { // 传入的初始时间
default: 1718977559428
},
type: { // 区分是在详情还是购物车调用
default: 'goodsDetail', // 设置两个值,goodsDetail和cart,样式不同
type: String
}
const props = defineProps({
time: { // 传入的初始时间
default: 1718977559428
},
data () {
return {
end: '', // 结束状态
hours: '', // 小时
minutes: '', // 分钟
seconds: '', // 秒
interval: '' // 定时器
};
},
mounted () {
this.init()
},
methods: {
countDown (val) { // 倒计时方法
function addZero (i) {
return i < 10 ? '0' + i : i + '';
}
var nowtime = new Date();
var endtime = new Date(val);
var lefttime = parseInt((endtime.getTime() - nowtime.getTime()) / 1000);
var h = parseInt((lefttime / (60 * 60)) % 24);
var m = parseInt((lefttime / 60) % 60);
var s = parseInt(lefttime % 60);
h = addZero(h);
m = addZero(m);
s = addZero(s);
this.hours = h;
this.minutes = m
this.seconds = s;
if (lefttime <= 0) {
this.end = `活动已结束`;
clearInterval(this.interval)
}
},
init () { // 初始化
this.interval = setInterval(() => {
this.countDown(this.time);
}, 1000);
}
type: { // 区分是在详情还是购物车调用
default: 'goodsDetail', // 设置两个值,goodsDetail和cart,样式不同
type: String
}
};
});
const end = ref(''); // 结束状态
const hours = ref(''); // 小时
const minutes = ref(''); // 分钟
const seconds = ref(''); // 秒
let interval = null; // 定时器
function countDown (val) { // 倒计时方法
function addZero (i) {
return i < 10 ? '0' + i : i + '';
}
var nowtime = new Date();
var endtime = new Date(val);
var lefttime = parseInt((endtime.getTime() - nowtime.getTime()) / 1000);
var h = parseInt((lefttime / (60 * 60)) % 24);
var m = parseInt((lefttime / 60) % 60);
var s = parseInt(lefttime % 60);
h = addZero(h);
m = addZero(m);
s = addZero(s);
hours.value = h;
minutes.value = m
seconds.value = s;
if (lefttime <= 0) {
end.value = `活动已结束`;
clearInterval(interval)
}
}
function init () { // 初始化
interval = setInterval(() => {
countDown(props.time);
}, 1000);
}
onMounted(() => {
init()
});
</script>
<style scoped lang="scss">
.cart-promotion{

View File

@@ -108,7 +108,7 @@
class="item-coupon"
v-for="(item, index) in promotionMap['COUPON'].slice(0, 6)"
:key="index"
@click="receiveCoupon(item.id)"
@click="handleReceiveCoupon(item.id)"
>
<span v-if="item.couponType == 'PRICE'"
>满{{ item.consumeThreshold }}减{{ item.price }}</span>
@@ -125,7 +125,7 @@
class="item-coupon"
v-for="(item, index) in promotionMap['COUPON'].slice(6, promotionMap['COUPON'].length)"
:key="index"
@click="receiveCoupon(item.id)"
@click="handleReceiveCoupon(item.id)"
>
<span v-if="item.couponType == 'PRICE'"
>满{{ item.consumeThreshold }}减{{ item.price }}</span>
@@ -318,11 +318,14 @@
</div>
</template>
<script>
<script setup>
import { ref, computed, watch, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import { Message } from "@/utils/message";
import { StarFilled } from '@element-plus/icons-vue';
import Promotion from "./Promotion.vue";
import DPlayer from "dplayer";
import storage from "@/plugins/storage.js";
import {
collectGoods,
isCollection,
@@ -344,375 +347,389 @@ import {
E_COUPON_CART_POINTS,
} from "@/constants/goodsType";
export default {
name: "ShowGoods",
props: {
// 商品数据
detail: {
type: Object,
default: null,
},
defineOptions({ name: "ShowGoods" });
const props = defineProps({
// 商品数据
detail: {
type: Object,
default: null,
},
watch: {
detail: {
handler(val) {
this.skuDetail = val.data;
this.wholesaleList = val.wholesaleList;
this.syncCountWithQuantity();
this.promotion();
this.swiperGoodsImg();
},
deep: true,
immediate: true,
},
showGoodsVideo(val){
if(val){
this.initVideo();
});
const emit = defineEmits(["handleClickSku"]);
const route = useRoute();
const router = useRouter();
const showGoodsVideo = ref(false);
const goodsVideo = ref("");
const wholesaleList = ref([]);
const count = ref(1); // 商品数量
const imgIndex = ref(0); // 展示图片下标
const currentSelceted = ref([]); // 当前商品sku
const imgList = ref([]); // 商品图片列表
const skuDetail = ref({
specList: [],
}); // sku详情
const goodsSpecList = ref(props.detail.specs); // 商品spec
const promotionMap = ref(createEmptyPromotionMap());
const formatList = ref([]); // 选择商品品类的数组
const loading = ref(false); // 立即购买loading
const loading1 = ref(false); // 加入购物车loading
const loadingPintuan = ref(false);
const isCollected = ref(false); // 是否收藏
const skuList = ref([]); // formatSku 中赋值(原 data 未声明)
// startCheckout 通过字符串 key 操作对应 loading
const loadings = { loading, loading1, loadingPintuan };
const currentImg = computed(() => {
const item = imgList.value[imgIndex.value];
return item ? item.url || item : "";
});
const wholesalePrice = computed(() => {
return wholesaleList.value.length
? wholesaleList.value.map((item) => {
return item.price;
})
: [];
});
const wholesaleNum = computed(() => {
return wholesaleList.value.length
? wholesaleList.value.map((item) => {
return item.num;
})
: [];
});
/** 卡密商品:禁加购,仅立即购买;库存读 quantity(M-02 起支持促销) */
const isECouponGoods = computed(() => {
return isECoupon(skuDetail.value.goodsType);
});
const displayStock = computed(() => {
if (isECouponGoods.value) {
return getECouponStock(skuDetail.value);
}
return Number(skuDetail.value.quantity) || 0;
});
const quantityMax = computed(() => {
if (isECouponGoods.value) {
return getECouponMaxBuyNum(skuDetail.value);
}
const qty = Number(skuDetail.value.quantity) || 0;
return qty > 0 ? qty : 1;
});
const isOutOfStock = computed(() => {
if (isECouponGoods.value) {
return getECouponStock(skuDetail.value) <= 0;
}
return (Number(skuDetail.value.quantity) || 0) <= 0;
});
watch(
() => props.detail,
(val) => {
skuDetail.value = val.data;
wholesaleList.value = val.wholesaleList;
syncCountWithQuantity();
promotion();
swiperGoodsImg();
},
{ deep: true, immediate: true }
);
watch(showGoodsVideo, (val) => {
if (val) {
initVideo();
}
});
// 初始化video
function initVideo() {
if (!goodsVideo.value) {
setTimeout(() => {
goodsVideo.value = new DPlayer({
container: document.getElementById('dplayer'),
video: {
url: skuDetail.value.goodsVideo,
},
});
}, 100);
}
}
function syncCountWithQuantity() {
const qty = isECouponGoods.value
? getECouponStock(skuDetail.value)
: Number(skuDetail.value.quantity) || 0;
if (qty <= 0) {
count.value = 1;
return;
}
if (!isECouponGoods.value && wholesaleList.value && wholesaleList.value.length > 0) {
count.value = Math.min(Math.max(wholesaleList.value[0].num, 1), qty);
return;
}
const max = isECouponGoods.value ? getECouponMaxBuyNum(skuDetail.value) : qty;
if (count.value > max) {
count.value = max;
} else if (count.value < 1) {
count.value = 1;
}
}
function changeCount(val) {
if (wholesaleList.value && wholesaleList.value.length > 0) {
if (count.value <= wholesaleList.value[0].num) {
Message.warning("批发商品购买数量不能小于起批数量");
count.value = wholesaleList.value[0].num;
}
}
}
function select(index, value) {
// 选择规格
currentSelceted.value[index] = value;
let selectedSkuId = goodsSpecList.value.find((i) => {
let matched = true;
let specValues = i.specValues.filter((j) => j.specName !== "images");
for (let n = 0; n < specValues.length; n++) {
if (specValues[n].specValue !== currentSelceted.value[n]) {
matched = false;
return;
}
}
},
data() {
return {
playIcon,
showGoodsVideo:false,
goodsVideo:"",
wholesaleList: [],
count: 1, // 商品数量
imgIndex: 0, // 展示图片下标
currentSelceted: [], // 当前商品sku
imgList: [], // 商品图片列表
skuDetail: {
specList: [],
}, // sku详情
goodsSpecList: this.detail.specs, // 商品spec
promotionMap: createEmptyPromotionMap(),
formatList: [], // 选择商品品类的数组
loading: false, // 立即购买loading
loading1: false, // 加入购物车loading
loadingPintuan: false,
isCollected: false, // 是否收藏
};
},
components: { Promotion, StarFilled },
computed: {
currentImg() {
const item = this.imgList[this.imgIndex];
return item ? item.url || item : "";
},
wholesalePrice(key) {
return this.wholesaleList.length
? this.wholesaleList.map((item) => {
return item.price;
})
: [];
},
wholesaleNum(key) {
return this.wholesaleList.length
? this.wholesaleList.map((item) => {
return item.num;
})
: [];
},
/** 卡密商品:禁加购,仅立即购买;库存读 quantity(M-02 起支持促销) */
isECouponGoods() {
return isECoupon(this.skuDetail.goodsType);
},
displayStock() {
if (this.isECouponGoods) {
return getECouponStock(this.skuDetail);
}
return Number(this.skuDetail.quantity) || 0;
},
quantityMax() {
if (this.isECouponGoods) {
return getECouponMaxBuyNum(this.skuDetail);
}
const qty = Number(this.skuDetail.quantity) || 0;
return qty > 0 ? qty : 1;
},
isOutOfStock() {
if (this.isECouponGoods) {
return getECouponStock(this.skuDetail) <= 0;
}
return (Number(this.skuDetail.quantity) || 0) <= 0;
},
},
methods: {
eCouponAfterSaleHint,
// 初始化video
initVideo(){
if(!this.goodsVideo ){
setTimeout(()=>{
this.goodsVideo = new DPlayer({
container: document.getElementById('dplayer'),
video: {
url:this.skuDetail.goodsVideo,
},
});
},100)
}
if (matched) {
return i;
}
});
emit("handleClickSku", {
skuId: selectedSkuId.skuId,
goodsId: skuDetail.value.goodsId,
});
}
},
syncCountWithQuantity() {
const qty = this.isECouponGoods
? getECouponStock(this.skuDetail)
: Number(this.skuDetail.quantity) || 0;
if (qty <= 0) {
this.count = 1;
return;
}
if (!this.isECouponGoods && this.wholesaleList && this.wholesaleList.length > 0) {
this.count = Math.min(Math.max(this.wholesaleList[0].num, 1), qty);
return;
}
const max = this.isECouponGoods ? getECouponMaxBuyNum(this.skuDetail) : qty;
if (this.count > max) {
this.count = max;
} else if (this.count < 1) {
this.count = 1;
}
},
changeCount(val) {
if (this.wholesaleList && this.wholesaleList.length > 0) {
if (this.count <= this.wholesaleList[0].num) {
Message.warning("批发商品购买数量不能小于起批数量");
this.count = this.wholesaleList[0].num;
}
}
},
select(index, value) {
// 选择规格
this.currentSelceted[index] = value;
let selectedSkuId = this.goodsSpecList.find((i) => {
let matched = true;
let specValues = i.specValues.filter((j) => j.specName !== "images");
for (let n = 0; n < specValues.length; n++) {
if (specValues[n].specValue !== this.currentSelceted[n]) {
matched = false;
return;
}
}
if (matched) {
return i;
}
});
this.$emit("handleClickSku", {
skuId: selectedSkuId.skuId,
goodsId: this.skuDetail.goodsId,
});
},
addShoppingCartBtn() {
// FR-B-01:E_COUPON 禁止加购物车(后端亦返回 CARD_KEY_E_COUPON_CART_FORBIDDEN)
if (this.isECouponGoods) {
Message.warning("电子卡券请使用立即购买");
return;
}
// 添加购物车
const params = {
num: this.count,
skuId: this.skuDetail.id,
};
this.loading = true;
addCartGoods(params)
.then((res) => {
this.loading = false;
if (res.success) {
this.$router.push({
path: "/shoppingCart",
query: {
skuId: this.skuDetail.id,
goodsId: this.skuDetail.goodsId,
thumbnail: this.skuDetail.thumbnail,
goodsName: this.skuDetail.goodsName,
count: this.count,
},
});
} else {
Message.warning(res.message);
}
})
.catch(() => {
this.loading = false;
});
},
buyNow() {
this.startCheckout(E_COUPON_CART_BUY_NOW, "loading1");
},
pintuanBuy() {
this.startCheckout(E_COUPON_CART_PINTUAN, "loadingPintuan");
},
pointPay() {
this.startCheckout(E_COUPON_CART_POINTS, "loading");
},
startCheckout(action, loadingKey) {
const params = {
num: this.count,
skuId: this.skuDetail.id,
cartType: "BUY_NOW",
};
if (this.skuDetail.goodsType === "VIRTUAL_GOODS") {
params.cartType = "VIRTUAL";
} else if (this.isECouponGoods) {
params.cartType = resolveECouponCartType({
routeWay: this.$route.query.way,
action,
});
}
this[loadingKey] = true;
addCartGoods(params)
.then((res) => {
this[loadingKey] = false;
if (res.success) {
this.$router.push({
path: "/pay",
query: { way: params.cartType },
});
} else {
Message.warning(res.message);
}
})
.catch(() => {
this[loadingKey] = false;
});
},
async collect() {
if (!this.Cookies.getItem("accessToken") || !this.Cookies.getItem("userInfo")) {
this.$router.push({
path: "/login",
function addShoppingCartBtn() {
// FR-B-01:E_COUPON 禁止加购物车(后端亦返回 CARD_KEY_E_COUPON_CART_FORBIDDEN)
if (isECouponGoods.value) {
Message.warning("电子卡券请使用立即购买");
return;
}
// 添加购物车
const params = {
num: count.value,
skuId: skuDetail.value.id,
};
loading.value = true;
addCartGoods(params)
.then((res) => {
loading.value = false;
if (res.success) {
router.push({
path: "/shoppingCart",
query: {
rePath: this.$route.path,
query: JSON.stringify(this.$route.query || {}),
skuId: skuDetail.value.id,
goodsId: skuDetail.value.goodsId,
thumbnail: skuDetail.value.thumbnail,
goodsName: skuDetail.value.goodsName,
count: count.value,
},
});
return;
}
// 收藏商品
if (this.isCollected) {
let cancel = await cancelCollect("GOODS", this.skuDetail.id);
if (cancel.success) {
Message.success("取消收藏成功");
this.isCollected = false;
}
} else {
let collect = await collectGoods("GOODS", this.skuDetail.id);
if (collect.code === 200) {
this.isCollected = true;
Message.success("收藏商品成功,可以前往个人中心我的收藏查看");
}
Message.warning(res.message);
}
},
// 格式化数据
formatSku(list) {
let arr = [{}];
list.forEach((item, index) => {
item.specValues.forEach((spec, specIndex) => {
let name = spec.specName;
let values = {
value: spec.specValue,
quantity: item.quantity,
};
if (name === "images") {
return;
}
})
.catch(() => {
loading.value = false;
});
}
arr.forEach((arrItem, arrIndex) => {
if (
arrItem.name === name &&
arrItem.values &&
!arrItem.values.find((i) => i.value === values.value)
) {
arrItem.values.push(values);
}
function buyNow() {
startCheckout(E_COUPON_CART_BUY_NOW, "loading1");
}
let keys = arr.map((key) => {
return key.name;
});
if (!keys.includes(name)) {
arr.push({
name: name,
values: [values],
});
}
});
function pintuanBuy() {
startCheckout(E_COUPON_CART_PINTUAN, "loadingPintuan");
}
function pointPay() {
startCheckout(E_COUPON_CART_POINTS, "loading");
}
function startCheckout(action, loadingKey) {
const params = {
num: count.value,
skuId: skuDetail.value.id,
cartType: "BUY_NOW",
};
if (skuDetail.value.goodsType === "VIRTUAL_GOODS") {
params.cartType = "VIRTUAL";
} else if (isECouponGoods.value) {
params.cartType = resolveECouponCartType({
routeWay: route.query.way,
action,
});
}
loadings[loadingKey].value = true;
addCartGoods(params)
.then((res) => {
loadings[loadingKey].value = false;
if (res.success) {
router.push({
path: "/pay",
query: { way: params.cartType },
});
});
arr.shift();
this.formatList = arr;
let cur = list.filter((i) => i.skuId === this.$route.query.skuId)[0];
if (cur) {
cur.specValues
.filter((i) => i.specName !== "images")
.forEach((value, _index) => {
this.currentSelceted[_index] = value.specValue;
});
} else {
Message.warning(res.message);
}
this.skuList = list;
},
receiveCoupon(id) {
// 领取优惠券
receiveCoupon(id).then((res) => {
if (res.success) {
Message.success("优惠券领取成功");
} else {
Message.warning(res.message);
}
});
},
promotion() {
this.promotionMap = createEmptyPromotionMap();
if (!this.detail || !this.detail.promotionMap) return false;
const keysArr = Object.keys(this.detail.promotionMap);
if (keysArr.length === 0) return false;
})
.catch(() => {
loadings[loadingKey].value = false;
});
}
for (let i = 0; i < keysArr.length; i++) {
const key = keysArr[i].split("-")[0];
if (key === "COUPON") {
if (!Array.isArray(this.promotionMap.COUPON)) {
this.promotionMap.COUPON = [];
}
this.promotionMap.COUPON.push(this.detail.promotionMap[keysArr[i]]);
} else if (Object.prototype.hasOwnProperty.call(this.promotionMap, key)) {
this.promotionMap[key] = this.detail.promotionMap[keysArr[i]];
} else {
this.promotionMap[key] = this.detail.promotionMap[keysArr[i]];
}
}
},
// 相册里会混进规格 JSON,只留图片地址;没有相册时用主图
swiperGoodsImg() {
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 (gallery.length) {
this.imgList = gallery;
async function collect() {
if (!storage.getItem("accessToken") || !storage.getItem("userInfo")) {
router.push({
path: "/login",
query: {
rePath: route.path,
query: JSON.stringify(route.query || {}),
},
});
return;
}
// 收藏商品
if (isCollected.value) {
let cancel = await cancelCollect("GOODS", skuDetail.value.id);
if (cancel.success) {
Message.success("取消收藏成功");
isCollected.value = false;
}
} else {
let collect = await collectGoods("GOODS", skuDetail.value.id);
if (collect.code === 200) {
isCollected.value = true;
Message.success("收藏商品成功,可以前往个人中心我的收藏查看");
}
}
}
// 格式化数据
function formatSku(list) {
let arr = [{}];
list.forEach((item, index) => {
item.specValues.forEach((spec, specIndex) => {
let name = spec.specName;
let values = {
value: spec.specValue,
quantity: item.quantity,
};
if (name === "images") {
return;
}
this.imgList = this.skuDetail.original ? [this.skuDetail.original] : [];
},
},
mounted() {
// 用户登录才会判断是否收藏(须同时有 token,避免残留 userInfo 触发 401 跳转登录)
if (this.Cookies.getItem("accessToken") && this.Cookies.getItem("userInfo")) {
isCollection("GOODS", this.skuDetail.id).then((res) => {
if (res.success && res.result) {
this.isCollected = true;
arr.forEach((arrItem, arrIndex) => {
if (
arrItem.name === name &&
arrItem.values &&
!arrItem.values.find((i) => i.value === values.value)
) {
arrItem.values.push(values);
}
let keys = arr.map((key) => {
return key.name;
});
if (!keys.includes(name)) {
arr.push({
name: name,
values: [values],
});
}
});
}
});
});
arr.shift();
formatList.value = arr;
this.formatSku(this.goodsSpecList);
// this.promotion();
document.title = this.skuDetail.goodsName;
},
};
let cur = list.filter((i) => i.skuId === route.query.skuId)[0];
if (cur) {
cur.specValues
.filter((i) => i.specName !== "images")
.forEach((value, _index) => {
currentSelceted.value[_index] = value.specValue;
});
}
skuList.value = list;
}
function handleReceiveCoupon(id) {
// 领取优惠券
receiveCoupon(id).then((res) => {
if (res.success) {
Message.success("优惠券领取成功");
} else {
Message.warning(res.message);
}
});
}
function promotion() {
promotionMap.value = createEmptyPromotionMap();
if (!props.detail || !props.detail.promotionMap) return false;
const keysArr = Object.keys(props.detail.promotionMap);
if (keysArr.length === 0) return false;
for (let i = 0; i < keysArr.length; i++) {
const key = keysArr[i].split("-")[0];
if (key === "COUPON") {
if (!Array.isArray(promotionMap.value.COUPON)) {
promotionMap.value.COUPON = [];
}
promotionMap.value.COUPON.push(props.detail.promotionMap[keysArr[i]]);
} else if (Object.prototype.hasOwnProperty.call(promotionMap.value, key)) {
promotionMap.value[key] = props.detail.promotionMap[keysArr[i]];
} else {
promotionMap.value[key] = props.detail.promotionMap[keysArr[i]];
}
}
}
// 相册里会混进规格 JSON,只留图片地址;没有相册时用主图
function swiperGoodsImg() {
const source = skuDetail.value.goodsGalleryList || [];
const gallery = Array.from(source).filter((item) => {
if (typeof item !== "string") return false;
return item.indexOf('"url":') === -1 && item.indexOf('"status":') === -1;
});
if (gallery.length) {
imgList.value = gallery;
return;
}
imgList.value = skuDetail.value.original ? [skuDetail.value.original] : [];
}
onMounted(() => {
// 用户登录才会判断是否收藏(须同时有 token,避免残留 userInfo 触发 401 跳转登录)
if (storage.getItem("accessToken") && storage.getItem("userInfo")) {
isCollection("GOODS", skuDetail.value.id).then((res) => {
if (res.success && res.result) {
isCollected.value = true;
}
});
}
formatSku(goodsSpecList.value);
// promotion();
document.title = skuDetail.value.goodsName;
});
</script>
<style scoped lang="scss">

View File

@@ -132,160 +132,162 @@
</div>
</template>
<script>
<script setup>
import { computed, onMounted, onBeforeUnmount, reactive, ref } from 'vue';
import { Refresh } from '@element-plus/icons-vue';
import { goodsComment, goodsCommentNum } from '@/api/member.js';
import StoreSide from './StoreSide.vue';
export default {
components: { Refresh, StoreSide },
name: 'ShowGoodsDetail',
props: {
detail: {
type: Object,
default: null
}
},
data () {
return {
detailTabs: [
{ name: 'intro', label: '商品介绍' },
{ name: 'comment', label: '商品评价' },
{ name: 'params', label: '商品参数' }
],
activeDetailTab: 'intro',
anchorStuck: false,
lockScrollSpy: false,
scrollTimer: null,
commentList: [],
commentParams: {
pageNumber: 1,
pageSize: 10,
grade: '',
goodsId: ''
},
commentTypeNum: {},
commentTotal: 0
};
},
computed: {
skuDetail () {
return this.detail.data;
},
goodsParamsList () {
const list = this.detail && Array.isArray(this.detail.goodsParamsDTOList) ? this.detail.goodsParamsDTOList : [];
const flat = [];
defineOptions({ name: 'ShowGoodsDetail' });
list.forEach((item) => {
if (!item) return;
if (Array.isArray(item.goodsParamsItemDTOList)) {
flat.push(...item.goodsParamsItemDTOList);
} else {
flat.push(item);
}
});
const cleaned = flat.filter((p) => p && (p.paramName || p.paramId));
const hasSort = cleaned.some((p) => p && p.sort !== undefined && p.sort !== null);
if (!hasSort) return cleaned;
return cleaned.slice().sort((a, b) => {
const sa = Number(a && a.sort) || 0;
const sb = Number(b && b.sort) || 0;
return sa - sb;
});
}
},
methods: {
getNavOffset () {
const nav = this.$refs.tabNav;
return (nav ? nav.offsetHeight : 52) + 8;
},
scrollToSection (name) {
const el = this.$refs[name];
if (!el) return;
this.activeDetailTab = name;
this.lockScrollSpy = true;
const top = el.getBoundingClientRect().top + window.pageYOffset - this.getNavOffset();
window.scrollTo({ top: Math.max(0, top), behavior: 'smooth' });
const unlock = () => {
this.lockScrollSpy = false;
window.removeEventListener('scrollend', unlock);
};
window.addEventListener('scrollend', unlock);
window.clearTimeout(this.scrollTimer);
this.scrollTimer = window.setTimeout(unlock, 900);
},
handleScroll () {
const nav = this.$refs.tabNav;
if (nav) {
this.anchorStuck = nav.getBoundingClientRect().top <= 1;
}
if (this.lockScrollSpy) return;
const offset = this.getNavOffset() + 16;
let current = '';
this.detailTabs.forEach((tab) => {
const el = this.$refs[tab.name];
if (el && el.getBoundingClientRect().top <= offset) {
current = tab.name;
}
});
if (current) {
this.activeDetailTab = current;
}
},
changePageNum (val) {
this.commentParams.pageNumber = val;
this.getList();
},
changePageSize (val) {
this.commentParams.pageNumber = 1;
this.commentParams.pageSize = val;
this.getList();
},
getList () {
this.commentParams.goodsId = this.skuDetail.goodsId;
goodsComment(this.commentParams).then(res => {
if (res.success) {
this.commentList = res.result.records;
this.commentTotal = res.result.total;
}
});
goodsCommentNum(this.skuDetail.goodsId).then(res => {
if (res.success) {
this.commentTypeNum = res.result;
}
});
},
viewByGrade (grade) {
this.commentParams.grade = grade;
this.commentParams.pageNumber = 1;
this.getList();
},
previewImg (img, item) {
item.previewImg = img;
item.deg = item.deg || 0;
},
hidePreviewImg (item) {
item.previewImg = '';
},
rotatePreviewImg (type, item) {
const current = item.deg || 0;
item.deg = type ? current + 90 : current - 90;
}
},
mounted () {
window.addEventListener('scroll', this.handleScroll, { passive: true });
this.getList();
if (this.skuDetail.grade === null || this.skuDetail.grade === undefined) {
this.skuDetail.grade = 100;
}
},
beforeUnmount () {
window.removeEventListener('scroll', this.handleScroll);
window.clearTimeout(this.scrollTimer);
const props = defineProps({
detail: {
type: Object,
default: null
}
};
});
const tabNav = ref(null);
const intro = ref(null);
const comment = ref(null);
const params = ref(null);
const sectionRefs = { intro, comment, params };
const detailTabs = ref([
{ name: 'intro', label: '商品介绍' },
{ name: 'comment', label: '商品评价' },
{ name: 'params', label: '商品参数' }
]);
const activeDetailTab = ref('intro');
const anchorStuck = ref(false);
const lockScrollSpy = ref(false);
let scrollTimer = null;
const commentList = ref([]);
const commentParams = reactive({
pageNumber: 1,
pageSize: 10,
grade: '',
goodsId: ''
});
const commentTypeNum = ref({});
const commentTotal = ref(0);
const skuDetail = computed(() => {
return props.detail.data;
});
const goodsParamsList = computed(() => {
const list = props.detail && Array.isArray(props.detail.goodsParamsDTOList) ? props.detail.goodsParamsDTOList : [];
const flat = [];
list.forEach((item) => {
if (!item) return;
if (Array.isArray(item.goodsParamsItemDTOList)) {
flat.push(...item.goodsParamsItemDTOList);
} else {
flat.push(item);
}
});
const cleaned = flat.filter((p) => p && (p.paramName || p.paramId));
const hasSort = cleaned.some((p) => p && p.sort !== undefined && p.sort !== null);
if (!hasSort) return cleaned;
return cleaned.slice().sort((a, b) => {
const sa = Number(a && a.sort) || 0;
const sb = Number(b && b.sort) || 0;
return sa - sb;
});
});
function getNavOffset () {
const nav = tabNav.value;
return (nav ? nav.offsetHeight : 52) + 8;
}
function scrollToSection (name) {
const el = sectionRefs[name] && sectionRefs[name].value;
if (!el) return;
activeDetailTab.value = name;
lockScrollSpy.value = true;
const top = el.getBoundingClientRect().top + window.pageYOffset - getNavOffset();
window.scrollTo({ top: Math.max(0, top), behavior: 'smooth' });
const unlock = () => {
lockScrollSpy.value = false;
window.removeEventListener('scrollend', unlock);
};
window.addEventListener('scrollend', unlock);
window.clearTimeout(scrollTimer);
scrollTimer = window.setTimeout(unlock, 900);
}
function handleScroll () {
const nav = tabNav.value;
if (nav) {
anchorStuck.value = nav.getBoundingClientRect().top <= 1;
}
if (lockScrollSpy.value) return;
const offset = getNavOffset() + 16;
let current = '';
detailTabs.value.forEach((tab) => {
const el = sectionRefs[tab.name] && sectionRefs[tab.name].value;
if (el && el.getBoundingClientRect().top <= offset) {
current = tab.name;
}
});
if (current) {
activeDetailTab.value = current;
}
}
function changePageNum (val) {
commentParams.pageNumber = val;
getList();
}
function changePageSize (val) {
commentParams.pageNumber = 1;
commentParams.pageSize = val;
getList();
}
function getList () {
commentParams.goodsId = skuDetail.value.goodsId;
goodsComment(commentParams).then(res => {
if (res.success) {
commentList.value = res.result.records;
commentTotal.value = res.result.total;
}
});
goodsCommentNum(skuDetail.value.goodsId).then(res => {
if (res.success) {
commentTypeNum.value = res.result;
}
});
}
function viewByGrade (grade) {
commentParams.grade = grade;
commentParams.pageNumber = 1;
getList();
}
function previewImg (img, item) {
item.previewImg = img;
item.deg = item.deg || 0;
}
function hidePreviewImg (item) {
item.previewImg = '';
}
function rotatePreviewImg (type, item) {
const current = item.deg || 0;
item.deg = type ? current + 90 : current - 90;
}
onMounted(() => {
window.addEventListener('scroll', handleScroll, { passive: true });
getList();
if (skuDetail.value.grade === null || skuDetail.value.grade === undefined) {
skuDetail.value.grade = 100;
}
});
onBeforeUnmount(() => {
window.removeEventListener('scroll', handleScroll);
window.clearTimeout(scrollTimer);
});
</script>
<style scoped lang="scss">

View File

@@ -65,149 +65,217 @@
</aside>
</template>
<script>
import { Message } from "@/utils/message";
<script setup>
import { computed, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { Message, Modal } 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";
import { getIMDetail } from "@/api/common";
import { getMemberMsg } from "@/api/login";
import storage from "@/plugins/storage.js";
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: "",
},
defineOptions({ name: "StoreSide" });
const props = defineProps({
storeId: {
type: [String, Number],
default: "",
},
data() {
return {
store: {},
logoError: false,
collected: false,
recommends: [],
};
goodsId: {
type: [String, Number],
default: "",
},
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";
},
skuId: {
type: [String, Number],
default: "",
},
watch: {
storeId: {
immediate: true,
handler(id) {
if (!id) return;
this.logoError = false;
this.loadStore(id);
this.loadRecommend(id);
this.loadCollected(id);
storeNameFallback: {
type: String,
default: "",
},
});
const route = useRoute();
const router = useRouter();
const store = ref({});
const logoError = ref(false);
const collected = ref(false);
const recommends = ref([]);
// 原 talkIm mixin 的 IM 链接
const IMLink = ref("");
const storeName = computed(() => {
return store.value.storeName || props.storeNameFallback || "店铺";
});
const storeInitial = computed(() => {
return storeName.value.slice(0, 1);
});
// 自营标记后端可能是 true、1 或 "1"
const isSelfOperated = computed(() => {
const flag = store.value.selfOperated;
return flag === true || flag === 1 || flag === "1";
});
watch(
() => props.storeId,
(id) => {
if (!id) return;
logoError.value = false;
loadStore(id);
loadRecommend(id);
loadCollected(id);
},
{ immediate: true }
);
// 没有评分或小于等于 0 时按满分展示
function formatScore(value) {
const score = Number(value);
if (!Number.isFinite(score) || score <= 0) return "5.00";
return score.toFixed(2);
}
function loadStore(id) {
getStoreById(id).then((res) => {
if (res.success && res.result) store.value = res.result;
}).catch((err) => {
console.warn('获取店铺信息失败', err);
});
}
// 按销量取店内商品,去掉当前商品,最多 4 个
function loadRecommend(id) {
goodsList({
storeId: id,
pageNumber: 1,
pageSize: 8,
sort: "buyCount",
order: "desc",
}).then((res) => {
const records = (res.success && res.result && res.result.records) || [];
recommends.value = records
.filter((item) => String(item.goodsId) !== String(props.goodsId))
.slice(0, 4);
}).catch(() => {
// 推荐商品为可选数据,加载失败时静默
});
}
// 同时有 token 和用户信息才查收藏,避免残留 userInfo 触发登录跳转
function loadCollected(id) {
if (!storage.getItem("accessToken") || !storage.getItem("userInfo")) return;
isStoreCollection("STORE", id).then((res) => {
if (res.success && res.result) collected.value = true;
}).catch(() => {
// 收藏状态为可选数据,失败时静默
});
}
function goStore() {
router.push({ path: "/merchant", query: { id: props.storeId } });
}
// 推荐商品新开详情页,店铺页仍在当前页打开
function goGoods(item) {
const href = router.resolve({
path: "/goodsDetail",
query: { skuId: item.id, goodsId: item.goodsId },
}).href;
window.open(href, "_blank");
}
// 原 talkIm mixin 的登录提示
function promptLogin() {
Modal.confirm({
title: "温馨提示",
content: "请登录后执行此操作",
okText: "立即登录",
cancelText: "取消",
onOk: () => {
router.push({
path: "/login",
query: {
rePath: route.path,
query: JSON.stringify(route.query || {}),
},
});
},
});
}
// 获取im信息(原 talkIm mixin)
async function getIMDetailMethods() {
let res = await getIMDetail();
if (res.success) {
IMLink.value = res.result;
}
}
// 跳转im客服(原 talkIm mixin;本组件无 storeMsg,mixin 的 storeMsg?.storeId 兜底恒为 undefined,等价于直接用传入的 id)
async function IMService(id, goodsId, skuId) {
if (!storage.getItem("accessToken")) {
promptLogin();
return;
}
try {
await getIMDetailMethods();
const userInfo = await getMemberMsg();
if (!userInfo.success) {
promptLogin();
return;
}
const accessToken = storage.getItem("accessToken");
const storeId = id;
const baseUrl = `${IMLink.value}?token=${accessToken}&id=${storeId}`;
if (goodsId && skuId) {
window.open(`${baseUrl}&goodsId=${goodsId}&skuId=${skuId}`);
} else {
window.open(baseUrl);
}
} catch {
// token 过期等场景由 request 拦截器处理;未登录时避免未捕获异常
if (!storage.getItem("accessToken")) {
promptLogin();
}
}
}
function contact() {
IMService(props.storeId, props.goodsId, props.skuId);
}
// 未登录先去登录,并带回当前商品页
async function toggleCollect() {
if (!storage.getItem("accessToken") || !storage.getItem("userInfo")) {
router.push({
path: "/login",
query: {
rePath: route.path,
query: JSON.stringify(route.query || {}),
},
},
},
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((err) => {
console.warn('获取店铺信息失败', err);
});
},
// 按销量取店内商品,去掉当前商品,最多 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("收藏店铺成功,可以前往个人中心我的收藏查看");
}
},
},
};
});
return;
}
if (collected.value) {
const cancel = await cancelStoreCollect("STORE", props.storeId);
if (cancel.success) {
collected.value = false;
Message.success("已取消收藏");
}
return;
}
const collect = await collectStore("STORE", props.storeId);
if (collect.code === 200) {
collected.value = true;
Message.success("收藏店铺成功,可以前往个人中心我的收藏查看");
}
}
</script>
<style scoped lang="scss">