我是ProductDetails商品详情的页面<template> <view :class="[...

बनाया गया: 27 मई 2026

का उपयोग करके उत्तर दिया गया GPT-5.5 Thinking द्वारा Chat01

प्रश्न

image.png
image.png
image.png

我是ProductDetails商品详情的页面<template>
<view :class="['app-container', theme]">
<!-- 顶部固定栏 -->
<view class="header" :style="{ paddingTop: statusBarHeightPx + 'px', height: headerBarHeightPx + 'px' }">
<view class="header-inner">
<view class="left-box" @click="navigateBack">
<uni-icons type="left" color="#000000" size="26"></uni-icons>
</view>
<view class="center-box">
<text class="title-text">{{ page.goods.title || '商品详情' }}</text>
</view>
<view class="right-box">
<view class="share-btn" @click="share">
<text class="share-text">优品百科</text>
</view>
</view>
</view>
</view>

text
<!-- 可滚动内容(高度用 px,避免 rpx/px 混用引起空白) --> <scroll-view class="page-scroll" scroll-y :enable-back-to-top="true" :style="{height: scrollHeightPx + 'px',marginTop: headerTotalPx + 'px'}"> <view class="scroll-content" :style="{ paddingBottom: padBottomPx + 'px' }"> <!-- 首屏媒体 --> <view class="hero" :style="{ height: heroHeightPx + 'rpx' }"> <view style="" class="hero-inner"> <view v-if="isadvertisement" style="width: 100%;" class=""> <!-- 奖励浮层 --> <view v-if="Rewardswitch" class="reward-mask" @touchmove.stop.prevent @click.stop> <view class="reward-panel"> <view class="reward-close" @click.stop="closeReward">×</view> <view class="reward-amount-circle"> <text class="reward-amount-text">{{ advertisement.price }}</text> </view> <view class="reward-tip">获得 {{advertisement.price }} 现金奖励,可在我的零钱里提现</view> <button class="reward-btn" @click="closeReward">我知道了</button> </view> </view> <view style="width: 85%;height: 60rpx;position: absolute;top: 0%;left: 0%;right: 0%;z-index: 111;display: flex;justify-content: space-between;box-sizing: border-box;color:#D89833;margin: 0 auto;" class=""> <view style="width: 50%;display: flex;box-sizing: border-box;" class=""> <view style="width: 100%;height: 100%;" class=""> 您正在浏览商品广告 </view> </view> <view style="width: 50%;display: flex;box-sizing: border-box;" class=""> <view style="width: 100%;height: 100%;position: relative;" class=""> <view v-if="!issuccess" style="position: absolute;right: 5%;" class=""> 还有<text style="color: red;">{{`${advertisement.browsingtime} 秒`}}</text>后可获得奖励 </view> <view v-else class=""> 您已获得奖励 </view> </view> </view> </view> </view> <swiper class="hero-swiper" :indicator-dots="false" circular :autoplay="false" :current="Sliderindex" :duration="350" @change="onSwitch"> <swiper-item v-for="(m, idx) in page.media" :key="m.id || idx"> <template v-if="m.type === 'video'"> <video v-if="Sliderindex === idx" :id="videoId(m)" class="hero-media" :src="m.url" :poster="m.poster || firstImagePoster" object-fit="cover" :muted="true" :loop="true" :autoplay="true" :controls="false" :show-fullscreen-btn="true" :show-play-btn="false" :show-center-play-btn="true" @ended="handleVideoEnded(m)" /> <image v-else class="hero-media" :src="m.poster || firstImagePoster" mode="aspectFill" @click="jumpTo(idx)" /> </template> <template v-else> <image class="hero-media" :src="m.url" mode="aspectFill" @click="previewImage(m, idx)" /> </template> </swiper-item> <!-- 当没有媒体时给一个占位 --> <swiper-item v-if="!page.media.length"> <view class="hero-empty"> <text>暂无媒体</text> </view> </swiper-item> </swiper> <!-- 索引气泡 --> <view class="index-badge" v-if="page.media.length"> <text class="index-text">{{ Sliderindex + 1 }}/{{ page.media.length }}</text> </view> <!-- 左右圆钮(示例) --> <view class="side-btns left" v-if="divide.length"> <view class="side-btn" v-for="btn in divide" :key="btn.id" @click="Clickleft(btn)"> <view class="side-dot"></view> <view class="side-label">{{ btn.name }}</view> </view> </view> <view class="side-btns right" v-if="rightList.length"> <view class="side-btn" v-for="btn in rightList" :key="btn.id" @click="Rightclick(btn)"> <view class="side-dot"></view> <view class="side-label">{{ btn.name }}</view> </view> </view> </view> </view> <!-- 商品信息卡片 --> <view style="position: relative;z-index: 11;" class="section"> <view style="width: 90%;" class="card"> <!-- 价格 & 销量 --> <view class="row space-between"> <view class="price-box"> <text class="tag-discount">到手价¥</text> <text class="price">{{ formatPrice(displayPrice) }}</text> <up-button v-if="ProductSharing" type="warning" style="display: inline-block; height: 40rpx; width: 60rpx;margin-left: 16rpx;" :disabled="!!ProductSharing" text="分享" /> <text class="origin" v-if="displayOriginalPrice > displayPrice">¥{{ formatPrice(displayOriginalPrice) }}</text> </view> <view class="sold-box"> <text class="sold">已售 {{ soldDisplay }}</text> </view> </view> <!-- 标签行 --> <view class="row mt8"> <text class="points">积分:{{ page.points }} 可兑换</text> <text class="ship">{{ page.shippingTag }}</text> </view> <!-- 标题 & 简介 --> <view class="title-line">{{ page.goods.title }}</view> <view class="intro"> <up-parse :content="page.content"></up-parse> </view> <!-- 福利/保障 --> <view class="benefits" v-if="page.benefits.length"> <view class="benefit" v-for="b in page.benefits" :key="b.id"> {{ b.customizeTitle || b.typeName }} </view> </view> <view class="badge" v-else>暂无福利</view> <!-- 支付支持 --> <view class="pay-support"> <text>支持微信支付、</text><text class="ml10">支付宝支付</text> </view> <!-- 发货与保障提示 --> <view class="hint-row"> <view class="hint-left"> <view class="hint-icon"></view> <view class="hint-text strong">预计8小时内发货 | 承诺48小时内发货</view> </view> <view class="hint-right"><uni-icons type="right" size="20"></uni-icons></view> </view> <view class="hint-row"> <view class="hint-left"> <view class="hint-icon"></view> <view class="hint-text">店铺发货 售后保障 7天无理由退款 假一赔十</view> </view> <view class="hint-right"><uni-icons type="right" size="20"></uni-icons></view> </view> </view> </view> <!-- 规格(SKU) --> <!-- <view style="border: 1rpx solid red;" class="section" v-if="page.skus.length"> <view class="card"> <view class="row space-between"> <text class="sku-title">选择规格</text> <text class="sku-sub" v-if="activeSku">{{ activeSku.title }}(剩余{{ activeSku.surplusCount }})</text> </view> <view class="sku-grid"> <view v-for="sku in page.skus" :key="sku.id" class="sku-chip" :class="{ active: activeSkuId === sku.id, disabled: sku.surplusCount === 0 }" @click="selectSku(sku)"> {{ sku.title }} ¥{{ formatPrice(sku.price) }} </view> </view> <view class="attrs" v-if="page.attributes.length"> <view class="attr-row" v-for="(attr, i) in page.attributes" :key="i"> <text class="attr-name">{{ attr.name }}</text> <text class="attr-value">{{ attr.value }}</text> </view> </view> </view> </view> --> <translate :skus="page.skus" :id="page.goods.id" :CharitysaleID="CharitysaleID" :joinSharePlanCount="page.joinSharePlanCount" @send="accept" /> <!-- 评价区 --> <view class="section" v-if="page.orders.length"> <view class="card"> <view class="row space-between"> <text class="sku-title">用户评价</text> <text class="sku-sub link" @click="goToAllComments" v-if="canLoadMoreComments">加载更多</text> </view> <view class="comment" v-for="o in page.orders" :key="o.id"> <view class="c-header"> <image class="avatar" :src="o.avatarUrl" mode="aspectFill" /> <view class="meta"> <text class="nick">{{ o.nickname || '用户' }}</text> <text class="sub">订单ID:{{ o.id }}</text> </view> </view> <view class="c-body" v-if="o.appraise || (o.appraiseFiles && o.appraiseFiles.length)"> <text class="c-text" v-if="o.appraise">{{ o.appraise }}</text> <scroll-view class="c-media" scroll-x v-if="o.appraiseFiles.length"> <image v-for="(f, i) in o.appraiseFiles" :key="i" v-if="f.type === 'img'" :src="f.url" class="c-img" mode="aspectFill" @click="previewCommentMedia(o.appraiseFiles, i)" /> <video v-for="(f, i) in o.appraiseFiles" :key="'v-'+i" v-if="f.type === 'video'" class="c-img" :src="f.url" :poster="f.poster || firstImagePoster" :controls="true" /> </scroll-view> </view> <view class="c-comments" v-if="o.comments.length"> <view class="c-comment" v-for="cc in o.comments" :key="cc.id"> <image class="c-avatar" :src="cc.consumerAvatarUrl" mode="aspectFill" /> <view class="c-bubble"> <text class="c-nick">{{ cc.consumerNickname || '用户' }}</text> <text class="c-reply">{{ cc.comment }}</text> </view> </view> </view> </view> <view class="load-more" v-if="canLoadMoreComments" @click="loadMoreComments">加载更多评价</view> <view class="no-more" v-else>没有更多评价了</view> </view> </view> </view> </scroll-view> <!-- 底部购买条(固定) --> <view class="buy-bar" :style="{ height: buyBarHeightPx + 'px', paddingBottom: safeBottomPx + 'px' }"> <view class="buy-inner"> <view class="buy-left"> <view class="buy-icon" v-for="ic in bottomIcons" :key="ic.id" @click="Switchstate(ic)"> <!-- 收藏图标:根据状态动态切换颜色与文字 --> <uni-icons v-if="ic.name === '收藏'" :type="isCollected ? 'home-filled' : 'home'" :color="isCollected ? '#D3AB59' : '#000000'" size="26" /> <!-- 其它图标占位 --> <uni-icons v-else :type="ic.name === '客服' ? 'person' : 'shop'" size="26" /> <view class="buy-icon-label">{{ ic.name }}</view> </view> </view> <view class="buy-right"> <up-button style=" width: 238rpx;height: 88rpx;" @click="Addshoppingcart()" type="warning" text="加入购物车"></up-button> <view class="btn pay" @click="pay"> 立即购买¥<text class="pay-price">{{ formatPrice(activeSku?.price || displayPrice) }}</text> </view> </view> </view> </view> </view>
</template> <script setup> import translate from "@/components/translate.vue"; import ajax from "@/api/ajax.js"; import { getStatusBarHeight, getTitleBarHeight } from "@/utils/system.js"; import { ref, reactive, computed, onMounted, onBeforeUnmount, watch, inject, toRefs } from "vue"; import { onLoad, onReady, onHide, onUnload } from "@dcloudio/uni-app"; import { storeToRefs } from "pinia"; import { useMainStore } from "@/store/index.js"; import { usecommodity } from '@/store/useCommodity.js' const storecom = usecommodity() const { useProductID, subproduct } = storeToRefs(storecom) /** 主题 */ const store = useMainStore(); const { theme } = storeToRefs(store); const webProjectUrl = inject("webProjectUrl", ""); /** 顶部栏高度(px) */ const statusBarHeightPx = ref(getStatusBarHeight()); const headerBarHeightPx = ref(getTitleBarHeight()); const headerTotalPx = computed(() => statusBarHeightPx.value + headerBarHeightPx.value); const sys = uni.getSystemInfoSync(); const windowHeightPx = ref(sys.windowHeight); const screenWidthPx = sys.screenWidth; const safeBottomPx = sys.safeAreaInsets?.bottom || 0; const rpx2px = (n) => Math.round((n * screenWidthPx) / 750); /** 底部购买条高度(含视觉高度,不含安全区) */ const buyBarHeightPx = rpx2px(120); const scrollHeightPx = computed(() => Math.max(0, windowHeightPx.value - headerTotalPx.value - buyBarHeightPx - safeBottomPx)); const padBottomPx = computed(() => buyBarHeightPx + safeBottomPx + rpx2px(12)); const heroHeightPx = ref(Math.floor(windowHeightPx.value * 0.58)); // 58% 视口高度 /** 轮播与视频上下文 */ const Sliderindex = ref(0); const videoCtxMap = reactive(new Map()); /** 页面数据 */ const page = reactive({ goods: { id: "", title: "", introduction: "", soldCount: 0, price: 0, originalPrice: 0, Remainingquantity: 0, storeID: '', //店铺id }, joinSharePlanCount: 0, content: '', benefits: [], media: [], skus: [], attributes: [], orders: [], minPrice: 0, minOriginalPrice: 0, points: 0, shippingTag: "包邮", isadvertisement: false, //是否精点广告的浏览 itmes: null, Rewardswitch: false, //模态框奖励开关 issuccess: false, exit: false, //退出的状态 FriendMedia: false, //好友广告媒体 Friendpicture: '', //好友图片 isadvertisingdetail: false, //是否广告详情 advertisement: { }, //浏览广告的东西 SaleParameters: { }, //义卖参数 ProductSharing: false, //商品分享 encyclopedia: false, //优品百科 CharitysaleID: '', //义卖的id isCollected: false, //商品的收藏 SingleproductID: '', //单个商品id }); let { advertisement, isadvertisement, itmes, Rewardswitch, issuccess, exit, FriendMedia, Friendpicture, isadvertisingdetail, SaleParameters, ProductSharing, encyclopedia, CharitysaleID, isCollected, SingleproductID } = toRefs(page) /** 展示价格:优先 sku 最低价,否则商品价 */ const displayPrice = computed(() => { const skuMin = Number(page.minPrice || 0); const gPrice = Number(page.goods.price || 0); return skuMin > 0 ? skuMin : gPrice; }); const displayOriginalPrice = computed(() => { const skuMinOri = Number(page.minOriginalPrice || 0); const gOri = Number(page.goods.originalPrice || 0); const gPrice = Number(page.goods.price || 0); return skuMinOri > 0 ? skuMinOri : (gOri > 0 ? gOri : gPrice); }); /** 已售展示 */ const soldDisplay = computed(() => { const sold = Math.max(0, Number(page.goods.soldCount || 0) - Number(page.goods.Remainingquantity || 0)); if (sold >= 100000) return (sold / 10000).toFixed(1) + "万+"; if (sold >= 10000) return (sold / 10000).toFixed(1) + "万"; return String(sold); }); /** 当前选中 SKU */ const activeSkuId = ref(null); const activeSku = computed(() => page.skus.find((s) => s.id === activeSkuId.value) || null); /** 首张图片作为 poster 兜底 */ const firstImagePoster = computed(() => { const img = page.media.find((m) => m.type === "img"); return img ? img.url : ""; }); const SelectedID = ref([]) /** 侧边按钮 & 底部图标(示例) */ const divide = ref([{ id: 1, name: "自商体" }, { id: 2, name: "全息工厂" }, { id: 3, name: "商誉分" } ]); const rightList = ref([{ id: 1, name: "关注" }, { id: 2, name: "商品详情" }, { id: 3, name: "评价" } ]); const bottomIcons = ref([{ id: Date.now() + 1, name: "店铺" }, { id: Date.now() + 2, name: "收藏" }, { id: Date.now() + 3, name: "客服" } ]); /** 分页(评价) */ const commentPaging = reactive({ start: 0, count: 10, total: 0 }); const hasMoreComments = ref(true); const canLoadMoreComments = computed(() => hasMoreComments.value); /** 页面加载:根据参数拉数据 */ const ProductID = ref(""); let gettimes = ref(null) onLoad((option) => { if (option?.id) { let obj = JSON.parse(decodeURIComponent(option.id)) console.log(obj, '正常商品进来的吊袜带') ProductID.value = obj.id; initLoad(); fetchSharedProducts(ProductID.value); } else if (option?.charitysale) { let obj = JSON.parse(decodeURIComponent(option.charitysale)) ProductID.value = obj.mallGoodsGroupId CharitysaleID.value = obj.id SingleproductID.value = obj.mallGoodsId initLoad() fetchSharedProducts(ProductID.value) console.log(obj, '里面的参数来源义卖') } else if (option?.Advertising) { exit.value = true advertisement.value = JSON.parse(decodeURIComponent(option.Advertising)) console.log('从广告详情来了', advertisement.value) isadvertisement.value = true ClearTime() let { browsingtime, getmallGoodsGroupId, media } = toRefs(advertisement.value) ProductID.value = getmallGoodsGroupId.value initLoad(); Friendpicture.value = '' Friendpicture.value = media.playURL gettimes.value = browsingtime.value console.log(gettimes.value, '里面的值') itmes.value = setInterval(() => { if (browsingtime.value > 0) { browsingtime.value-- console.log('到这里', browsingtime.value) if (browsingtime.value === 0) { Rewardswitch.value = true issuccess.value = true //显示文字的模态框 ensureShowRecord(getmallGoodsGroupId.value, browsingtime.value) } } else { ClearTime() itmes.value = null clearInterval(itmes.value) } }, 1000) } else if (option?.advertisement) { FriendMedia.value = true //好友广告,媒体 exit.value = true advertisement.value = JSON.parse(decodeURIComponent(option.advertisement)) console.log('从好友广告', advertisement.value) isadvertisement.value = true ClearTime() let { browsingtime, getmallGoodsGroupId, media } = toRefs(advertisement.value) ProductID.value = getmallGoodsGroupId.value initLoad(); Friendpicture.value = '' Friendpicture.value = media.playURL gettimes.value = browsingtime.value itmes.value = setInterval(() => { if (browsingtime.value > 0) { browsingtime.value-- console.log('到这里', browsingtime.value) if (browsingtime.value === 0) { Rewardswitch.value = true issuccess.value = true //显示文字的模态框 ensureShowRecord(getmallGoodsGroupId.value, browsingtime.value) } } else { ClearTime() console.log('到这了') } }, 1000) } else if (option?.charity) { SaleParameters.value = JSON.parse(decodeURIComponent(option.charity)); ProductSharing.value = true; console.log(SaleParameters.value, '12313义卖的参数') } }); let consumerId = ref(uni.getStorageSync('consumerId')) async function ensureShowRecord(id, browsingtime) { const payload = { consumerId: consumerId.value, essentialsAdvertisementId: id || "", essentialsAdvertisementForwardId: "", showDuration: gettimes.value }; console.log(payload, '里面的参数来源') try { const res = await ajax.post({ url: "essentialsAdvertisementShow", method: "post", data: payload, }); console.log(res, '里面的东西结果') const newId = res?.data.id || ""; } catch (e) { // 上报失败不阻断流程,可在此上报日志 console.error("ensureShowRecord error:", e); } } // 清除时间 function ClearTime() { if (itmes.value) { itmes.value = null clearInterval(itmes.value) } } // 关闭 function closeReward() { Rewardswitch.value = false } /** 初始化加载:商品 + 评价第一页 */ async function initLoad() { resetComments(); fetchCollectStatus(); await getProductDetails({ mergeComments: false }); } function resetComments() { commentPaging.start = 0; hasMoreComments.value = true; page.orders = []; } function accept(item) { SelectedID.value = item } // 收藏商品 const Saveitem = () => { } /** 参与共享的商品(示例,按需使用) */ function fetchSharedProducts(id) { const payload = { mallGoodsGroupId: id, rowStartIdx: 0, rowCount: "20" }; ajax .post({ url: "joinSharePlanMallGoodsList", method: "post", data: payload }) .then((res) => { // 根据需要处理 // console.log("共享商品结果", res); }) .catch(() => {}); } /** 请求商品详情(mallGoodsGroupShow) */ async function getProductDetails({ mergeComments = true } = {}) { const payload = { id: ProductID.value, rowStartIdx: commentPaging.start, rowCount: commentPaging.count }; try { const res = await ajax.post({ url: "mallGoodsGroupShow", data: payload, method: "post" }); const data = res?.data ?? res; console.log(data, '里面的详情页') // 商品主体 const g = data?.mallGoodsGroupObject || {}; page.goods.id = g.id; page.goods.storeID = g.mallEnterpriseId //店铺id useProductID.value = g.id; page.joinSharePlanCount = g.joinSharePlanCount ?? '' page.goods.title = g.title ?? ""; page.goods.introduction = g.introduction ?? ""; page.content = page.goods.introduction || '' const skuArrRaw = Array.isArray(data?.mallGoodsArray) ? data.mallGoodsArray : Array.isArray(g ?.mallGoodsArray) ? g.mallGoodsArray : []; subproduct.value = skuArrRaw.map(item => item.id).join() SingleproductID.value = subproduct.value page.goods.soldCount = sumBy(skuArrRaw, (x) => Number(x.count || 0)); page.goods.Remainingquantity = sumBy(skuArrRaw, (x) => Number(x.surplusCount || 0)); page.goods.price = minPositive(skuArrRaw.map((x) => Number(x.price || 0))) || 0; // 兜底显示 page.goods.originalPrice = minPositive(skuArrRaw.map((x) => Number(x.originalPrice || x.price || 0))) || 0; // 福利 // 媒体 let { mallGoodsFileArray } = data.mallGoodsGroupObject const fileArr = Array.isArray(mallGoodsFileArray) ? mallGoodsFileArray : []; if (FriendMedia.value) { // 从好友进来的 page.media = fileArr.map((f) => { const name = String(f.filename || ""); const isVideoByExt = /\.mp4$/i.test(name); const url = f.playURL || buildFileUrl(name); return { id: f.id, type: isVideoByExt ? "video" : "img", url, poster: Friendpicture.value, raw: f }; }); } else if (isadvertisingdetail.value) { // 从好友进来的 page.media = fileArr.map((f) => { const name = String(f.filename || ""); const isVideoByExt = /\.mp4$/i.test(name); const url = f.playURL || buildFileUrl(name); return { id: f.id, type: isVideoByExt ? "video" : "img", url, poster: Friendpicture.value, raw: f }; }); console.log('广告详情进来的', page.media) } else { // 原版的 console.log('正常进的广告详情') page.media = fileArr.map((f) => { const name = String(f.filename || ""); const isVideoByExt = /\.mp4$/i.test(name); const url = f.playURL || buildFileUrl(name); return { id: f.id, type: isVideoByExt ? "video" : "img", url, poster: isVideoByExt ? (firstImagePoster.value || "") : undefined, raw: f }; }); } // 自定义属性列表 page.skus = skuArrRaw.map((sku) => ({ arr: (sku?.mallGoodsAttributeArray) || [], id: sku.id, title: sku.title, count: Number(sku.count || 0), price: Number(sku.price || 0), originalPrice: Number(sku.originalPrice || sku.price || 0), surplusCount: Number(sku.surplusCount || 0), attributes: Array.isArray(sku.mallGoodsAttributeArray) ? sku.mallGoodsAttributeArray.map(( a) => ({ id: a.id, name: a.name, value: a.value, title: '商品属性' })) : [] })); // 聚合属性(去重) const attrMap = new Map(); page.skus.forEach((s) => { s.attributes.forEach((a) => { const key = `${a.name}::${a.value}`; if (!attrMap.has(key)) attrMap.set(key, { name: a.name, value: a.value }); }); }); page.attributes = Array.from(attrMap.values()); // 价格区间 if (page.skus.length) { page.minPrice = minPositive(page.skus.map((s) => s.price || 0)) || 0; page.minOriginalPrice = minPositive(page.skus.map((s) => s.originalPrice || s.price || 0)) || 0; } else { page.minPrice = Number(g.price || 0); page.minOriginalPrice = Number(g.originalPrice || g.price || 0); } page.points = Math.max(0, Math.round((displayPrice.value || 0) * 100)); // 评价订单(分页合并) const rawOrders = Array.isArray(data?.mallOrderArray) ? data.mallOrderArray : []; const mappedOrders = rawOrders.map((o) => ({ id: o.id, consumerId: o.consumerId, avatarUrl: o.avatarUrl, nickname: o.nickname, appraise: o.appraise, appraiseFiles: (Array.isArray(o.mallOrderAppraiseFileArray) ? o .mallOrderAppraiseFileArray : []).map((f) => { const name = String(f.fileName || ""); const isVideo = /\.mp4$/i.test(name) || !!f.videoId || !!f.playUrl; return { type: isVideo ? "video" : "img", url: f.playUrl || buildFileUrl(name), poster: isVideo ? (firstImagePoster.value || "") : undefined, raw: f }; }), comments: (Array.isArray(o.mallOrderAppraiseCommentArray) ? o .mallOrderAppraiseCommentArray : []).map((c) => ({ id: c.id, consumerId: c.consumerId, comment: c.comment, consumerNickname: c.consumerNickname, consumerAvatarUrl: c.consumerAvatarUrl })) })); if (mergeComments) { page.orders = page.orders.concat(mappedOrders); } else { page.orders = mappedOrders; } if (rawOrders.length < commentPaging.count) hasMoreComments.value = false; // 默认选中有库存的第一个 SKU const firstInStock = page.skus.find((s) => s.surplusCount > 0) || page.skus[0]; activeSkuId.value = firstInStock?.id ?? null; // 媒体就绪后同步视频上下文 setTimeout(syncActiveVideoContext, 0); } catch (e) { console.error("加载商品失败:", e); uni.showToast({ title: "加载失败,请稍后重试", icon: "none" }); } } /** 工具:拼接静态文件 URL */ function buildFileUrl(filename) { if (!filename) return ""; const base = String(webProjectUrl || "").replace(/\/$/, ""); const path = String(filename).replace(/^\//, ""); return base ? `${base}/${path}` : path; } /** 小工具函数 */ function sumBy(arr, pick) { let total = 0; arr.forEach((x) => (total += Number(pick(x) || 0))); return total; } function minPositive(nums) { const filtered = nums.filter((n) => typeof n === "number" && n > 0); return filtered.length ? Math.min(...filtered) : 0; } /** 生命周期 & 视图适配 */ onMounted(() => { // 统一 hero 用 px,避免 rpx/px 混用导致空白 heroHeightPx.value = Math.floor(windowHeightPx.value * 0.58); }); onReady(() => setTimeout(syncActiveVideoContext, 0)); onHide(() => pauseAllVideos()); onUnload(() => { pauseAllVideos(); videoCtxMap.clear(); ClearTime(); }); // 拉取收藏状态 async function fetchCollectStatus() { const consumerId = uni.getStorageSync('consumerId') if (!consumerId) return // 没登录就不查 let option = { consumerId, mallGoodsId: SingleproductID.value } console.log(option, '拉取里面状态') try { const res = await ajax.post({ url: 'consumerCollectMallGoodsShow', data: option }) console.log(res, '获取状态') if (res.data.result === 'success') { isCollected.value = res.data.collect } else { uni.showToast({ title: '获取收藏状态失败', icon: 'none' }) } } catch { uni.showToast({ title: '网络错误', icon: 'none' }) } } // 切换收藏 async function toggleCollect() { const consumerId = uni.getStorageSync('consumerId') if (!consumerId) { uni.showModal({ title: '未登录', content: '请先登录后再进行操作', confirmText: '去登录', success: (res) => res.confirm && uni.navigateTo({ url: '/pages/Loginpage/Loginpage' }) }) return } if (!activeSku?.value && page.skus.length) { uni.showToast({ title: "请选择规格", icon: "none" }); return; } const prevStatus = isCollected.value isCollected.value = !prevStatus let option = { consumerId, mallGoodsId: SingleproductID.value, collected: isCollected.value } console.log(option, '收藏里面的东西11') try { const updateRes = await ajax.post({ url: 'consumerCollectMallGoods', data: option }) console.log(updateRes, '更新收藏') if (updateRes.data.result !== 'success') { isCollected.value = prevStatus uni.showToast({ title: updateRes.data.result || '错误', icon: 'none' }) } else { if (isCollected.value) { uni.showToast({ title: '已收藏', icon: 'none' }) } else { uni.showToast({ title: '取消收藏', icon: 'none' }) } } } catch { isCollected.value = prevStatus uni.showToast({ title: '网络错误', icon: 'none' }) } } // 加入购物车 async function Addshoppingcart() { console.log('到这了') const consumerId = uni.getStorageSync('consumerId'); if (!consumerId) { uni.showModal({ title: '未登录', content: '请先登录后再进行操作', confirmText: '去登录', success: (res) => { if (res.confirm) uni.navigateTo({ url: '/pages/Loginpage/Loginpage' }) } }); return; // 终止后续操作 } // if (!activeSku?.value && page.skus.length) { // uni.showToast({ // title: "请选择规格", // icon: "none" // }); // return; // } try { let objdata = { mallGoodsId: SingleproductID.value, consumerId, count: 1 } const res = await ajax.post({ url: "mallShoppingCart", data: objdata }); console.log(res, '加入到购物车里面的的数据') if (res.data.result === "success") { uni.showToast({ title: '加入购物车成功', icon: "none" }); } else { uni.showToast({ title: '加入购物车失败', icon: "none" }); } } catch (err) { uni.showToast({ title: String(err), icon: "none" }); } } /** 交互 & 视频修复 */ function navigateBack() { if (exit.value) { uni.showModal({ title: '提示', content: '退出将无法获得奖励', success: (res) => { if (res.confirm) { uni.navigateBack({ delta: 1 }) } else if (res.cancel) { console.log('用户点击了取消'); } } }); } else { uni.navigateBack({ delta: 1 }) } } function share() { uni.showShareMenu?.(); } function videoId(m) { return `heroVideo-${m.id ?? 'x'}`; } function onSwitch(e) { Sliderindex.value = e.detail.current || 0; handleSlideChange(); } function handleSlideChange() { pauseAllVideos(); syncActiveVideoContext(true); } function syncActiveVideoContext(autoplay = false) { const m = page.media[Sliderindex.value]; if (!m || m.type !== "video") return; const vid = videoId(m); let ctx = videoCtxMap.get(vid); if (!ctx) { try { ctx = uni.createVideoContext(vid); videoCtxMap.set(vid, ctx); } catch (e) { console.warn("创建视频上下文失败", e); } } if (autoplay && ctx) { setTimeout(() => { try { ctx.play(); } catch (_) {} }, 50); } } function pauseAllVideos() { videoCtxMap.forEach((ctx) => { try { ctx.pause(); } catch (_) {} }); } function jumpTo(idx) { Sliderindex.value = idx; handleSlideChange(); } function handleVideoEnded() {} /** 预览 */ function previewImage(item) { if (!page.media.length) return; const imgs = page.media.filter((s) => s.type === "img").map((s) => s.url).filter(Boolean); if (!imgs.length) return; const current = item.url; uni.previewImage({ urls: imgs, current }); } function previewCommentMedia(list, index) { const imgs = list.filter((x) => x.type === "img").map((x) => x.url).filter(Boolean); if (!imgs.length) return; const cur = list[index]?.type === "img" ? list[index].url : imgs[0]; uni.previewImage({ urls: imgs, current: cur }); } /** 侧边 & 底部按钮 */ function Clickleft(item) { const {name} = item if(name==='自商体'){ }else if(name==='全息工厂'){ uni.navigateTo({ url: '/pages/index/goodsPage/Productattributes/HolographicFactory/WatchList?parameter=' + encodeURIComponent(JSON .stringify(page.goods.storeID)) }) }else if(name==='商誉分'){ } } // 上面六个主要的页面 function Rightclick(item) { const { name } = item; if (name === "商品详情") { uni.navigateTo({ url: '/pages/index/goodsPage/Productattributes/productLntroduction?parameter=' + encodeURIComponent(JSON .stringify(page.goods.storeID)) }) } else if (name === "评价") { uni.navigateTo({ url: '/pages/index/goodsPage/Productattributes/ProductReview?parameter=' + encodeURIComponent(JSON .stringify(page.goods.storeID)) }) //goToAllComments(); } else if (name === "关注") { uni.navigateTo({ url: '/pages/index/goodsPage/Followtheshop/Shophomepage?parameter=' + encodeURIComponent(JSON .stringify(page.goods.storeID)) }) } else if (name === '关注') { Switchstate(item) } } function Switchstate(item) { if (item.name === "店铺") { uni.switchTab?.({ url: "/pages/index/goodsPage/Followtheshop/Shophomepage" }); } else if (item.name === "收藏") { toggleCollect() } else if (item.name === "客服") { Switchstateitem(item) } } function Switchstateitem(item) { if (item.name === '客服') { let obj = { mallEnterpriseId: page.goods.storeID || '', name: page.goods.title || '店铺客服', avatarUrl: 'https://img0.baidu.com/it/u=2411327126,2849759794&fm=253&app=138&f=JPEG?w=500&h=500' } uni.navigateTo({ url: `/pages/index/goodsPage/CustomerService?parameter=${encodeURIComponent(JSON.stringify(obj))}` }) } } /** SKU & 购买 */ function selectSku(sku) { if (sku.surplusCount === 0) return; activeSkuId.value = sku.id; } function pay() { if (!activeSku?.value && page.skus.length) { uni.showToast({ title: "请选择规格", icon: "none" }); return; } // 参数 // let parameter = { // mallGoodsId:page.goods.id, // joinSharePlanMallGoodsIds:SelectedID.value, // } // console.log(parameter,'1111111111111111222') // uni.navigateTo({ // url: "/pages/index/goodsPage/Paymentgoods?parameter=" + encodeURIComponent(JSON.stringify(parameter)) // }); const parameter = { mallGoodsId: page.goods.id, // 主商品组 id joinSharePlanMallGoodsIds: SelectedID.value // 勾选的 mallGoodsId[] } uni.navigateTo({ url: "/pages/index/goodsPage/Paymentgoods?parameter=" + encodeURIComponent(JSON.stringify(parameter)) }) } /** 评价加载更多 */ function goToAllComments() { if (!hasMoreComments.value) { uni.showToast({ title: "没有更多评价了", icon: "none" }); return; } loadMoreComments(); } async function loadMoreComments() { if (!hasMoreComments.value) return; commentPaging.start += commentPaging.count; await getProductDetails({ mergeComments: true }); } /** 显示价格格式化 */ function formatPrice(n) { const num = Number(n || 0); const s = num.toFixed(2); return s.endsWith(".00") ? Number(s).toString() : s; } /** 监听滑块(保险) */ watch(Sliderindex, () => handleSlideChange()); </script> <style lang="scss" scoped> @import "@/assets/styles/global.scss"; .app-container { min-height: 100vh; background: #EDEDED; } /* 固定头部 */ .header { position: fixed; left: 0; top: 0; right: 0; z-index: 1000; width: 100%; background: rgba(255, 255, 255, 0.96); backdrop-filter: saturate(180%) blur(8px); } .header-inner { width: 94%; margin: 0 auto; height: 100%; position: relative; display: flex; align-items: center; justify-content: center; } .left-box, .right-box { position: absolute; top: 0; bottom: 0; width: 120rpx; display: flex; align-items: center; } .left-box { left: 0; justify-content: flex-start; } .right-box { right: 0; justify-content: flex-end; } .center-box { flex: none; } .title-text { font-size: 32rpx; font-weight: 600; } .share-btn { height: 50rpx; width: 100rpx; background-color: #d89833; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; } .share-text { color: #fff; font-size: 22rpx; } /* 滚动容器 */ .page-scroll { width: 100%; } .scroll-content { width: 100%; .reward-mask { position: fixed; inset: 0; background-color: rgba(0, 0, 0, 0.72); z-index: 9999; display: flex; align-items: center; justify-content: center; } // 浮窗广告 .reward-panel { width: 80vw; max-width: 520rpx; padding: 40rpx 24rpx 32rpx; background: #111a; backdrop-filter: blur(4px); color: #fff; border-radius: 16rpx; position: relative; box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.35); } .reward-close { position: absolute; top: 16rpx; right: 16rpx; width: 48rpx; height: 48rpx; border-radius: 50%; background: #fff; color: #333; font-size: 32rpx; line-height: 48rpx; text-align: center; } .reward-amount-circle { width: 200rpx; height: 200rpx; border-radius: 50%; margin: 20rpx auto 24rpx; background: #D89833; display: flex; align-items: center; justify-content: center; } .reward-amount-text { color: #fff; font-size: 40rpx; font-weight: 700; } .reward-tip { text-align: center; margin: 16rpx 0 24rpx; font-size: 26rpx; } .reward-btn { width: 80%; margin: 0 auto; background: #ffd166; color: #333; border-radius: 12rpx; } // 结束 } /* 首屏媒体 */ .hero { width: 100%; position: relative; background: #000; } .hero-inner { height: 100%; width: 100%; position: relative; } .hero-swiper { height: 100%; width: 100%; } .hero-media { width: 100%; height: 100%; background: #000; object-fit: cover; } .hero-empty { height: 100%; color: #fff; align-items: center; justify-content: center; display: flex; } /* 轮播索引 */ .index-badge { position: absolute; right: 5%; bottom: 6%; background: rgba(0, 0, 0, 0.35); padding: 10rpx 16rpx; border-radius: 12rpx; } .index-text { color: #fff; font-size: 22rpx; } /* 两侧按钮 */ .side-btns { position: absolute; top: 0%; width: 80rpx; } .side-btns.left { left: 5%; } .side-btns.right { right: 5%; } .side-btn { width: 80rpx; margin-top: 16rpx; text-align: center; } .side-dot { height: 80rpx; width: 80rpx; border-radius: 50%; background: rgba(0, 0, 0, 0.3); margin: 0 auto 8rpx; } .side-label { color: #fff; font-size: 20rpx; } /* 内容区块 */ .section { width: 100%; padding: 20rpx 0; display: flex; justify-content: center; margin-top: -55rpx; } .card { width: 94%; border: 1rpx solid #eeeeee; border-radius: 16rpx; padding: 24rpx; background: #fff; } .row { display: flex; align-items: center; flex-wrap: nowrap; } .space-between { justify-content: space-between; } .mt8 { margin-top: 8rpx; } /* 价格行 */ .price-box { display: flex; align-items: baseline; } .tag-discount { font-size: 22rpx; color: #D89833; } .price { font-size: 40rpx; color: #ff2a2a; margin-left: 6rpx; font-weight: 700; } .origin { margin-left: 10rpx; color: #bbbbbb; text-decoration: line-through; font-size: 22rpx; } .sold-box { position: relative; } .sold { font-size: 20rpx; color: #888; } /* 标签与标题 */ .points { color: #D89833; font-size: 22rpx; margin-right: 16rpx; } .ship { color: #ff4444; font-size: 22rpx; } .title-line { margin: 14rpx 0 8rpx; font-size: 28rpx; color: #333; line-height: 1.7; font-weight: 600; } .intro { color: #666; font-size: 24rpx; margin: 6rpx 0 8rpx; } /* 福利 */ .benefits { display: flex; flex-wrap: wrap; gap: 12rpx; margin-top: 12rpx; } .benefit { padding: 6rpx 14rpx; border-radius: 20rpx; background: #D89833; color: white; font-size: 20rpx; } .badge { display: inline-flex; width: 152rpx; align-items: center; justify-content: center; height: 34rpx; // padding: 0 20rpx; border-radius: 20rpx; background: #D89833; color: #fff; font-size: 20rpx; // margin-top: 12rpx; } /* 支付支持 */ .pay-support { margin-top: 18rpx; background: #f7f7f7; padding: 12rpx 16rpx; border-radius: 8rpx; font-size: 22rpx; color: #444; } .ml10 { margin-left: 10rpx; } /* 提示行 */ .hint-row { margin-top: 20rpx; display: flex; align-items: center; justify-content: space-between; } .hint-left { display: flex; align-items: center; width: 80%; } .hint-icon { height: 32rpx; width: 32rpx; background: #cccccc; border-radius: 6rpx; } .hint-text { margin-left: 8rpx; font-size: 20rpx; color: #999; } .hint-text.strong { color: #5cc06a; } .hint-right { width: 20%; display: flex; justify-content: flex-end; } /* SKU */ .sku-title { font-size: 26rpx; font-weight: 600; } .sku-sub { font-size: 22rpx; color: #888; } .sku-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14rpx; margin-top: 18rpx; } .sku-chip { padding: 18rpx 10rpx; border-radius: 12rpx; text-align: center; border: 1rpx solid #eee; font-size: 22rpx; color: #333; } .sku-chip.active { border-color: #ff2a2a; color: #ff2a2a; background: #fff6f6; } .sku-chip.disabled { color: #bbb; background: #f5f5f5; } /* 属性 */ .attrs { margin-top: 20rpx; } .attr-row { display: flex; justify-content: space-between; padding: 18rpx 0; border-bottom: 1rpx solid #f1f1f1; } .attr-name { color: #888; font-size: 22rpx; } .attr-value { color: #333; font-size: 22rpx; } /* 评价 */ .comment { padding: 18rpx 0; border-bottom: 1rpx solid #f3f3f3; } .c-header { display: flex; gap: 16rpx; align-items: center; } .avatar { width: 72rpx; height: 72rpx; border-radius: 50%; background: #f0f0f0; } .meta { display: flex; flex-direction: column; } .nick { font-size: 24rpx; color: #333; } .sub { font-size: 20rpx; color: #999; } .c-body { margin-top: 8rpx; } .c-text { font-size: 24rpx; color: #444; line-height: 1.6; } .c-media { margin-top: 10rpx; white-space: nowrap; } .c-img { width: 180rpx; height: 180rpx; border-radius: 10rpx; margin-right: 10rpx; background: #f6f6f6; } .c-comments { margin-top: 12rpx; } .c-comment { display: flex; gap: 12rpx; margin-top: 10rpx; } .c-avatar { width: 48rpx; height: 48rpx; border-radius: 50%; background: #eee; } .c-bubble { background: #f7f7f7; padding: 10rpx 14rpx; border-radius: 10rpx; } .c-nick { font-size: 22rpx; color: #666; } .c-reply { display: block; margin-top: 4rpx; font-size: 24rpx; color: #333; } .link { color: #576b95; } .load-more { margin-top: 14rpx; text-align: center; color: #666; font-size: 22rpx; } .no-more { margin-top: 14rpx; text-align: center; color: #bbb; font-size: 22rpx; } /* 底部购买条(固定) */ .buy-bar { position: fixed; left: 0; right: 0; bottom: 0; z-index: 1001; background: #ffffff; box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.06); } .buy-inner { width: 94%; margin: 0 auto; height: 100%; display: flex; align-items: center; justify-content: space-between; } .buy-left { display: flex; align-items: center; gap: 28rpx; } .buy-right { display: flex; align-items: center; gap: 16rpx; } .buy-icon { display: flex; align-items: center; flex-direction: column; } .buy-icon-label { margin-top: 6rpx; font-size: 22rpx; color: #333; } .btn { width: 238rpx; height: 88rpx; display: flex; align-items: center; justify-content: center; border-radius: 16rpx; color: #ffffff; font-size: 26rpx; } .cart { background: #ff7979; } .pay { background: #ff0000; } .pay-price { font-weight: 700; } </style>关于我要重写这个页面优化这个页面 商品详情点击就跳到这个页面了pages/index/goodsPage/Productattributes/productLntroduction<template>
text
<view :class="['app-container', theme]"> <view class="content"> <view class="overlay"> <!-- 顶部 Header --> <view class="header" :style="{ paddingTop: statusBarHeight + 'px', height: headerBarHeight + 'px' }"> <view class="header-inner"> <view class="left-box" @click="navigateBack"> <uni-icons type="left" color="#000000" size="26"></uni-icons> </view> <view class="center-box"> <text class="title-text"></text> </view> <view @click="EnteGroupSettings" class="right-box"> <uni-icons type="more-filled" color="#000000" size="22" /> </view> </view> </view> <!-- 可滚动内容区域 --> <view style="width: 100%;display: flex;justify-content: center;align-items: center;" class=""> <view style="width: 94%;height: 100%; " class=""> </view> </view> </view> </view> </view>
</template> <script setup> import { getStatusBarHeight, getTitleBarHeight } from '@/utils/system.js' import ajax from '@/api/ajax.js' import { ref, reactive, onMounted, inject } from 'vue' import { storeToRefs } from 'pinia' import { useMainStore } from '@/store/index.js' import { onLoad } from '@dcloudio/uni-app' // 全局主题 const store = useMainStore() const { theme } = storeToRefs(store) // 顶部安全区与标题栏高度 const statusBarHeight = ref(getStatusBarHeight()) const headerBarHeight = ref(getTitleBarHeight()) function EnteGroupSettings() { } // 到这结束返回的数据 // 返回上一页 const navigateBack = () => { uni.navigateBack() } onLoad((option) => { if (option) { } else { console.log('没有参数') } }) </script> <style lang="scss" scoped> @import '@/assets/styles/global.scss'; .app-container { /* 如果有深色 / 浅色主题,在这里根据 theme 做适配 */ } .content { min-height: 100vh; position: relative; background-size: cover; } .overlay { position: absolute; inset: 0; background-size: cover; } .header { width: 94%; margin: 0 auto; } .header-inner { position: relative; display: flex; align-items: center; justify-content: center; // 整体居中 height: 100%; } /* 左右两块宽度相同,但不占位 */ .left-box, .right-box { position: absolute; top: 0; bottom: 0; width: 120rpx; // 足够容纳图标/文字 display: flex; align-items: center; } .left-box { left: 0; justify-content: flex-start; } .right-box { right: 0; justify-content: flex-end; } /* 中间标题:绝对居中 */ .center-box { flex: none; // 不拉伸 } </style>目前这个页面啥也没有你结合图片和结合api 搞出这个商品详情页然后点击这个评价 你就跳到这个页面上

pages/index/goodsPage/Productattributes/ProductReview<template>
<view :class="['app-container', theme]">
<view class="content">
<view class="overlay">
<!-- 顶部 Header -->
<view class="header" :style="{ paddingTop: statusBarHeight + 'px', height: headerBarHeight + 'px' }">
<view class="header-inner">

text
<view class="left-box" @click="navigateBack"> <uni-icons type="left" color="#000000" size="26"></uni-icons> </view> <view class="center-box"> <text class="title-text"></text> </view> <view @click="EnteGroupSettings" class="right-box"> <uni-icons type="more-filled" color="#000000" size="22" /> </view> </view> </view> <!-- 可滚动内容区域 --> <view style="width: 100%;display: flex;justify-content: center;align-items: center;" class=""> <view style="width: 94%;height: 100%; " class=""> </view> </view> </view> </view> </view>
</template> <script setup> import { getStatusBarHeight, getTitleBarHeight } from '@/utils/system.js' import ajax from '@/api/ajax.js' import { ref, reactive, onMounted, inject } from 'vue' import { storeToRefs } from 'pinia' import { useMainStore } from '@/store/index.js' import { onLoad } from '@dcloudio/uni-app' // 全局主题 const store = useMainStore() const { theme } = storeToRefs(store) // 顶部安全区与标题栏高度 const statusBarHeight = ref(getStatusBarHeight()) const headerBarHeight = ref(getTitleBarHeight()) function EnteGroupSettings() { } // 到这结束返回的数据 // 返回上一页 const navigateBack = () => { uni.navigateBack() } onLoad((option) => { if (option) { } else { console.log('没有参数') } }) </script> <style lang="scss" scoped> @import '@/assets/styles/global.scss'; .app-container { /* 如果有深色 / 浅色主题,在这里根据 theme 做适配 */ } .content { min-height: 100vh; position: relative; background-size: cover; } .overlay { position: absolute; inset: 0; background-size: cover; } .header { width: 94%; margin: 0 auto; } .header-inner { position: relative; display: flex; align-items: center; justify-content: center; // 整体居中 height: 100%; } /* 左右两块宽度相同,但不占位 */ .left-box, .right-box { position: absolute; top: 0; bottom: 0; width: 120rpx; // 足够容纳图标/文字 display: flex; align-items: center; } .left-box { left: 0; justify-content: flex-start; } .right-box { right: 0; justify-content: flex-end; } /* 中间标题:绝对居中 */ .center-box { flex: none; // 不拉伸 } </style>同样的这个页面也是没有东西也是结合图片和api数据实现这个商品评价目前这个评价你结合图片和api先把页面画出来就行这三个页面的背景统一调整为

参考这个页面<template>
<view :class="['app-container', theme]">
<view class="content">
<view class="overlay">
<view class="header" :style="{ paddingTop: statusBarHeight + 'px', height: headerBarHeight + 'px' }">
<view class="header-inner">
<view class="left-box" @click="navigateBack">
<uni-icons type="left" color="#111111" size="26" />
</view>

text
<view class="center-box"> <text class="title-text">简历生成</text> </view> <view class="right-box" @click="openMenu"> <uni-icons type="more-filled" color="#111111" size="22" /> </view> </view> </view> <scroll-view scroll-y class="page-scroll"> <view class="page-wrap"> <view v-if="isGenerating" class="loading-mask"> <u-loading-icon text="正在生成中..." :show="isGenerating" :vertical="true" timing-function="linear"></u-loading-icon> </view> <view class="top-card"> <view class="top-title-row"> <text class="top-title">简历配置</text> <text class="top-desc-mini">先选模板,再配模块</text> </view> <text class="top-desc"> 生成时会把各模块页面选中的内容替换到模板中的 ${} 占位符里,最终生成和截图类似的完整简历页面。 </text> </view> <view class="option-card" @click="goTemplatePage"> <view class="option-left"> <text class="option-title">选择简历模板</text> </view> <view class="option-right"> <text class="option-value">{{ currentTemplateName }}</text> <uni-icons type="right" color="#8a8f99" size="18"></uni-icons> </view> </view> <view class="avatar-card"> <view class="avatar-left"> <text class="avatar-title">头像</text> <text class="avatar-desc">生成后的简历首页会使用这张头像</text> </view> <view class="avatar-right"> <image class="avatar-img" :src="avatarUrl" mode="aspectFill"></image> </view> </view> <view v-for="(item, index) in sectionList" :key="index" class="section-card" @click="goSection(item, index)"> <view class="section-left"> <text class="section-name" :class="{ activeText: index === selectedIndex }"> {{ item.name }} </text> </view> <view class="section-right"> <text class="section-action" :class="{ activeText: index === selectedIndex }"> 去配置 </text> <uni-icons type="right" color="#8a8f99" size="16"></uni-icons> </view> </view> <view class="bottom-space"></view> </view> </scroll-view> <view class="footer"> <view class="submit-btn" :class="{ disabled: isGenerating }" @click="generateResume"> <text class="submit-text">{{ isGenerating ? '生成中...' : '立即生成' }}</text> </view> </view> </view> </view> </view>
</template> <script setup> import { ref, computed } from 'vue' import { onLoad, onShow } from '@dcloudio/uni-app' import { storeToRefs } from 'pinia' import { useMainStore } from '@/store/index.js' import { useResumeTemplateStore } from '@/store/resumeTemplate.js' import { getStatusBarHeight, getTitleBarHeight } from '@/utils/system.js' import ajax from '@/api/ajax.js' const mainStore = useMainStore() const { theme } = storeToRefs(mainStore) const resumeTemplateStore = useResumeTemplateStore() const { selectedTemplate } = storeToRefs(resumeTemplateStore) const statusBarHeight = ref(getStatusBarHeight()) const headerBarHeight = ref(getTitleBarHeight()) const selectedIndex = ref(null) const isGenerating = ref(false) const avatarUrl = ref('https://img1.baidu.com/it/u=4086864537,2548562233&fm=253&app=138&f=JPEG?w=800&h=1431') const sectionList = ref([{ name: '基础信息', path: '/Page-valueResume/resumetemplate/Basicinformation' }, { name: '学历教育', path: '/Page-valueResume/resumetemplate/Addeducational' }, { name: '职业履历', path: '/Page-valueResume/resumetemplate/CareerPortfolio' }, { name: '个人资历', path: '/Page-valueResume/resumetemplate/personaldata' }, { name: '自我评价', path: '/Page-valueResume/resumetemplate/self-introduction' } ]) const currentTemplateName = computed(() => { return selectedTemplate.value.templateName || '请选择模板' }) function navigateBack() { uni.navigateBack() } function openMenu() { uni.showActionSheet({ itemList: ['选择模板', '创建模板', '立即生成'], success: ({ tapIndex }) => { if (tapIndex === 0) goTemplatePage() if (tapIndex === 1) { uni.navigateTo({ url: '/Page-valueResume/resumetemplate/Createtemplate' }) } if (tapIndex === 2) generateResume() } }) } function goTemplatePage() { uni.navigateTo({ url: '/Page-valueResume/resumetemplate/DefaultTemplate' }) } function goSection(item, index) { selectedIndex.value = index uni.navigateTo({ url: item.path }) } async function loadBaseInfo() { try { const performerId = uni.getStorageSync('performerId') || uni.getStorageSync('consumerId') || '' if (!performerId) return const res = await ajax.post({ url: 'performerBaseShow', method: 'POST', data: { performerId } }) const performerObject = res?.data?.performerObject || {} if (performerObject.avatarUrl) { avatarUrl.value = performerObject.avatarUrl } } catch (error) { console.log('loadBaseInfo error', error) } } async function generateResume() { if (isGenerating.value) return const performerId = uni.getStorageSync('performerId') || uni.getStorageSync('consumerId') || '' if (!selectedTemplate.value.templateId) { uni.showToast({ title: '请先选择模板', icon: 'none' }) return } if (!performerId) { uni.showToast({ title: '缺少用户ID', icon: 'none' }) return } isGenerating.value = true try { await resumeTemplateStore.generateResumeFromSelectedTemplate(performerId) uni.navigateTo({ url: '/Page-valueResume/resumetemplate/Myresume' }) } catch (error) { console.log('generateResume error', error) uni.showToast({ title: error?.message || '生成失败', icon: 'none' }) } finally { isGenerating.value = false } } onLoad(() => { resumeTemplateStore.hydrateSelectedTemplate() loadBaseInfo() }) onShow(() => { resumeTemplateStore.hydrateSelectedTemplate() }) </script> <style lang="scss" scoped> @import '@/assets/styles/global.scss'; .app-container { min-height: 100vh; background: #f7f8fc; } .content { min-height: 100vh; position: relative; background: linear-gradient(180deg, #fdf7ea 0%, #f7f8fc 38%, #f7f8fc 100%); } .overlay { position: absolute; inset: 0; display: flex; flex-direction: column; } .header { width: 94%; margin: 0 auto; flex-shrink: 0; } .header-inner { position: relative; display: flex; align-items: center; justify-content: center; height: 100%; } .left-box, .right-box { position: absolute; top: 0; bottom: 0; width: 120rpx; display: flex; align-items: center; } .left-box { left: 0; justify-content: flex-start; } .right-box { right: 0; justify-content: flex-end; } .center-box { flex: none; } .title-text { font-size: 34rpx; font-weight: 600; color: #111111; } .page-scroll { flex: 1; height: 0; } .page-wrap { position: relative; width: 94%; margin: 0 auto; padding: 20rpx 0 0; box-sizing: border-box; } .loading-mask { position: absolute; inset: 0; z-index: 10; display: flex; align-items: center; justify-content: center; background: rgba(247, 248, 252, 0.65); backdrop-filter: blur(4rpx); border-radius: 24rpx; } .top-card, .option-card, .avatar-card, .section-card { margin-top: 16rpx; padding: 24rpx; border-radius: 24rpx; background: rgba(255, 255, 255, 0.94); box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.04); } .top-card { margin-top: 0; } .top-title-row { display: flex; align-items: center; justify-content: space-between; gap: 20rpx; } .top-title { font-size: 32rpx; font-weight: 600; color: #1f2329; } .top-desc-mini { font-size: 22rpx; color: #8a8f99; } .top-desc { display: block; margin-top: 14rpx; font-size: 24rpx; color: #8a8f99; line-height: 1.6; } .option-card, .section-card { display: flex; align-items: center; justify-content: space-between; } .option-left, .section-left { flex: 1; min-width: 0; } .option-right, .section-right { display: flex; align-items: center; gap: 12rpx; flex-shrink: 0; } .option-title, .section-name { font-size: 28rpx; font-weight: 600; color: #20242b; } .option-value, .section-action { font-size: 24rpx; color: #8a8f99; } .activeText { background: linear-gradient(135deg, #d89838 0%, #ceb87e 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; display: inline-block; } .avatar-card { display: flex; align-items: center; justify-content: space-between; } .avatar-left { flex: 1; min-width: 0; } .avatar-title { font-size: 28rpx; font-weight: 600; color: #20242b; } .avatar-desc { display: block; margin-top: 10rpx; font-size: 24rpx; color: #8a8f99; } .avatar-right { flex-shrink: 0; } .avatar-img { width: 128rpx; height: 128rpx; border-radius: 50%; background: #eef1f5; } .bottom-space { height: 180rpx; } .footer { position: fixed; left: 0; right: 0; bottom: 0; padding: 20rpx 30rpx calc(env(safe-area-inset-bottom) + 20rpx); background: linear-gradient(180deg, rgba(247, 248, 252, 0) 0%, #f7f8fc 32%); } .submit-btn { height: 88rpx; border-radius: 44rpx; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #d7983e 0%, #edd9a1 100%); box-shadow: 0 10rpx 24rpx rgba(215, 152, 62, 0.22); } .submit-btn.disabled { opacity: 0.65; } .submit-text { font-size: 30rpx; font-weight: 600; color: #1e160b; } </style>这个商品详情是大图拼接的然后下面是介绍如果没有详情或者评价就给个状态抱歉没有什么什么的可以用uview的ui

ProductDetails这个页面与样式直接之前的页面优化下这个是useCommodity js里面的数据// store/useCommodity.js
import {
defineStore
} from 'pinia'
import {
ref
} from 'vue'

export const usecommodity = defineStore('commodity', () => {
// 当前商品组/sku
const useProductID = ref('') // 商品组 id(mallGoodsGroupId)
const subproduct = ref('') // SKU 串(老逻辑需要)

text
// 共享勾选(joinSharePlan 勾选的 mallGoodsId[]) const SelectedID = ref([]) // 义拍下单上下文 const isAuctionPay = ref(false) const auctionBidId = ref('') function setUseProductID(id) { useProductID.value = id } function setSubproduct(s) { subproduct.value = s } function setSelectedIDs(arr) { SelectedID.value = Array.isArray(arr) ? arr.slice() : [] } /** * 设置义拍下单上下文 * @param {{bidId:string, isAuctionPay?:boolean}} payload */ function setAuctionOrderContext(payload = {}) { auctionBidId.value = String(payload.bidId || '') isAuctionPay.value = payload.isAuctionPay !== undefined ? !!payload.isAuctionPay : true } function clearAuctionOrderContext() { isAuctionPay.value = false auctionBidId.value = '' } return { useProductID, subproduct, SelectedID, isAuctionPay, auctionBidId, setUseProductID, setSubproduct, setSelectedIDs, setAuctionOrderContext, clearAuctionOrderContext }

})然后这个获取收藏失败给我优化下 21.consumerCollectMallGoods收藏商品(收藏了再点就解除收藏)
入口参数
String consumerId, 用户id
String mallGoodsId 商品id
22.consumerCollectMallGoodsShow收藏商品显示一个商品是否被一个用户收藏
入口参数
String consumerId, 用户id
String mallGoodsId 商品id
返回值
collect (true/false)
23.consumerCollectMallGoodsList 用户收藏商品列表
入口参数
String consumerId, 用户id
返回值
mallGoodsArray商品列表
id
price 价格
title 标题
count 初始数量
introduction简介
surplusCount 剩余数量
mallGoodsFileArray 图片/视频列表
id
videoId 阿里云id
playURL 阿里云播放url
fileName 文件名
以上的页面全部都是完整的页面功能尤其是这个ProductDetails 页面返回zip压缩包形式的 这个 ProductDetails 六个const divide = ref([{
id: 1,
name: "自商体"
},
{
id: 2,
name: "全息工厂"
},
{
id: 3,
name: "商誉分"
}
]);
const rightList = ref([{
id: 1,
name: "关注"
},
{
id: 2,
name: "商品详情"
},
{
id: 3,
name: "评价"
}
]);也给我加一下这个图标

Python

python
from pathlib import Path import zipfile from textwrap import dedent base = Path('/mnt/data/product_pages_upgrade') base.mkdir(exist_ok=True) product_details = dedent(""" <template> \t<view :class="['app-container', theme]"> \t\t<view class="content"> \t\t\t<view class="overlay"> \t\t\t\t<!-- 顶部 --> \t\t\t\t<view class="header" :style="{ paddingTop: statusBarHeight + 'px', height: headerBarHeight + 'px' }"> \t\t\t\t\t<view class="header-inner"> \t\t\t\t\t\t<view class="left-box" @click="navigateBack"> \t\t\t\t\t\t\t<uni-icons type="left" color="#111111" size="26" /> \t\t\t\t\t\t</view> \t\t\t\t\t\t<view class="center-box"> \t\t\t\t\t\t\t<text class="title-text">{{ goods.title || '商品详情' }}</text> \t\t\t\t\t\t</view> \t\t\t\t\t\t<view class="right-box"> \t\t\t\t\t\t\t<view class="encyclopedia-btn"> \t\t\t\t\t\t\t\t<text class="encyclopedia-text">优品百科</text> \t\t\t\t\t\t\t</view> \t\t\t\t\t\t</view> \t\t\t\t\t</view> \t\t\t\t</view> \t\t\t\t<scroll-view scroll-y class="page-scroll"> \t\t\t\t\t<view class="page-wrap"> \t\t\t\t\t\t<!-- 媒体区域 --> \t\t\t\t\t\t<view class="banner-card"> \t\t\t\t\t\t\t<swiper class="banner-swiper" circular :indicator-dots="false"> \t\t\t\t\t\t\t\t<swiper-item v-for="(item,index) in mediaList" :key="index"> \t\t\t\t\t\t\t\t\t<video \t\t\t\t\t\t\t\t\t\tv-if="item.type === 'video'" \t\t\t\t\t\t\t\t\t\t:src="item.url" \t\t\t\t\t\t\t\t\t\tclass="banner-image" \t\t\t\t\t\t\t\t\t\tobject-fit="cover" \t\t\t\t\t\t\t\t\t\t:controls="true" \t\t\t\t\t\t\t\t\t/> \t\t\t\t\t\t\t\t\t<image \t\t\t\t\t\t\t\t\t\tv-else \t\t\t\t\t\t\t\t\t\t:src="item.url" \t\t\t\t\t\t\t\t\t\tclass="banner-image" \t\t\t\t\t\t\t\t\t\tmode="aspectFill" \t\t\t\t\t\t\t\t\t/> \t\t\t\t\t\t\t\t</swiper-item> \t\t\t\t\t\t\t</swiper> \t\t\t\t\t\t\t<view class="swiper-index"> \t\t\t\t\t\t\t\t{{ currentIndex + 1 }}/{{ mediaList.length || 1 }} \t\t\t\t\t\t\t</view> \t\t\t\t\t\t\t<!-- 左侧快捷 --> \t\t\t\t\t\t\t<view class="float-btns left"> \t\t\t\t\t\t\t\t<view class="float-item" v-for="item in leftMenus" :key="item.id"> \t\t\t\t\t\t\t\t\t<view class="float-icon"> \t\t\t\t\t\t\t\t\t\t<uni-icons :type="item.icon" color="#ffffff" size="18" /> \t\t\t\t\t\t\t\t\t</view> \t\t\t\t\t\t\t\t\t<text class="float-text">{{ item.name }}</text> \t\t\t\t\t\t\t\t</view> \t\t\t\t\t\t\t</view> \t\t\t\t\t\t\t<!-- 右侧快捷 --> \t\t\t\t\t\t\t<view class="float-btns right"> \t\t\t\t\t\t\t\t<view class="float-item" v-for="item in rightMenus" :key="item.id" @click="goMenu(item)"> \t\t\t\t\t\t\t\t\t<view class="float-icon"> \t\t\t\t\t\t\t\t\t\t<uni-icons :type="item.icon" color="#ffffff" size="18" /> \t\t\t\t\t\t\t\t\t</view> \t\t\t\t\t\t\t\t\t<text class="float-text">{{ item.name }}</text> \t\t\t\t\t\t\t\t</view> \t\t\t\t\t\t\t</view> \t\t\t\t\t\t</view> \t\t\t\t\t\t<!-- 商品信息 --> \t\t\t\t\t\t<view class="goods-card"> \t\t\t\t\t\t\t<view class="price-row"> \t\t\t\t\t\t\t\t<view> \t\t\t\t\t\t\t\t\t<text class="price-symbol">¥</text> \t\t\t\t\t\t\t\t\t<text class="price-text">{{ price }}</text> \t\t\t\t\t\t\t\t\t<text class="origin-price">¥{{ originalPrice }}</text> \t\t\t\t\t\t\t\t</view> \t\t\t\t\t\t\t\t<text class="sold-text">已售 {{ soldCount }}</text> \t\t\t\t\t\t\t</view> \t\t\t\t\t\t\t<view class="title-row"> \t\t\t\t\t\t\t\t<text class="goods-title">{{ goods.title }}</text> \t\t\t\t\t\t\t</view> \t\t\t\t\t\t\t<view class="desc-row"> \t\t\t\t\t\t\t\t<text class="goods-desc">{{ goods.introduction || '暂无商品简介' }}</text> \t\t\t\t\t\t\t</view> \t\t\t\t\t\t\t<view class="tag-row"> \t\t\t\t\t\t\t\t<view class="tag-item">官方正品</view> \t\t\t\t\t\t\t\t<view class="tag-item">7天无理由</view> \t\t\t\t\t\t\t\t<view class="tag-item">极速发货</view> \t\t\t\t\t\t\t</view> \t\t\t\t\t\t</view> \t\t\t\t\t\t<!-- 功能图标 --> \t\t\t\t\t\t<view class="feature-card"> \t\t\t\t\t\t\t<view class="feature-item" v-for="item in featureList" :key="item.id"> \t\t\t\t\t\t\t\t<view class="feature-icon"> \t\t\t\t\t\t\t\t\t<uni-icons :type="item.icon" size="22" color="#7f6ea8"></uni-icons> \t\t\t\t\t\t\t\t</view> \t\t\t\t\t\t\t\t<text class="feature-text">{{ item.name }}</text> \t\t\t\t\t\t\t</view> \t\t\t\t\t\t</view> \t\t\t\t\t\t<!-- 详情入口 --> \t\t\t\t\t\t<view class="entry-card" @click="goDetail"> \t\t\t\t\t\t\t<view> \t\t\t\t\t\t\t\t<text class="entry-title">商品详情</text> \t\t\t\t\t\t\t\t<text class="entry-desc">查看完整图文详情与参数介绍</text> \t\t\t\t\t\t\t</view> \t\t\t\t\t\t\t<uni-icons type="right" size="18" color="#999999" /> \t\t\t\t\t\t</view> \t\t\t\t\t\t<!-- 评价预览 --> \t\t\t\t\t\t<view class="review-card"> \t\t\t\t\t\t\t<view class="review-header"> \t\t\t\t\t\t\t\t<text class="review-title">用户评价</text> \t\t\t\t\t\t\t\t<text class="review-more" @click="goReview">查看全部</text> \t\t\t\t\t\t\t</view> \t\t\t\t\t\t\t<view \t\t\t\t\t\t\t\tv-if="reviewList.length" \t\t\t\t\t\t\t\tclass="review-item" \t\t\t\t\t\t\t\tv-for="item in reviewList.slice(0,2)" \t\t\t\t\t\t\t\t:key="item.id" \t\t\t\t\t\t\t> \t\t\t\t\t\t\t\t<view class="review-user"> \t\t\t\t\t\t\t\t\t<image :src="item.avatarUrl" class="review-avatar" mode="aspectFill"></image> \t\t\t\t\t\t\t\t\t<text class="review-name">{{ item.nickname || '匿名用户' }}</text> \t\t\t\t\t\t\t\t</view> \t\t\t\t\t\t\t\t<text class="review-content">{{ item.appraise }}</text> \t\t\t\t\t\t\t\t<scroll-view scroll-x class="review-media" v-if="item.appraiseFiles.length"> \t\t\t\t\t\t\t\t\t<image \t\t\t\t\t\t\t\t\t\tv-for="(img,imgIndex) in item.appraiseFiles" \t\t\t\t\t\t\t\t\t\t:key="imgIndex" \t\t\t\t\t\t\t\t\t\t:src="img.url" \t\t\t\t\t\t\t\t\t\tclass="review-image" \t\t\t\t\t\t\t\t\t\tmode="aspectFill" \t\t\t\t\t\t\t\t\t></image> \t\t\t\t\t\t\t\t</scroll-view> \t\t\t\t\t\t\t</view> \t\t\t\t\t\t\t<u-empty \t\t\t\t\t\t\t\tv-else \t\t\t\t\t\t\t\tmode="message" \t\t\t\t\t\t\t\ticon="http://cdn.uviewui.com/uview/empty/message.png" \t\t\t\t\t\t\t\ttext="暂无用户评价" \t\t\t\t\t\t\t></u-empty> \t\t\t\t\t\t</view> \t\t\t\t\t\t<view class="bottom-space"></view> \t\t\t\t\t</view> \t\t\t\t</scroll-view> \t\t\t\t<!-- 底部 --> \t\t\t\t<view class="footer"> \t\t\t\t\t<view class="footer-left"> \t\t\t\t\t\t<view class="footer-icon" @click="toggleCollect"> \t\t\t\t\t\t\t<uni-icons \t\t\t\t\t\t\t\t:type="isCollected ? 'heart-filled' : 'heart'" \t\t\t\t\t\t\t\t:size="22" \t\t\t\t\t\t\t\t:color="isCollected ? '#d89838' : '#444444'" \t\t\t\t\t\t\t/> \t\t\t\t\t\t\t<text class="footer-label">{{ isCollected ? '已收藏' : '收藏' }}</text> \t\t\t\t\t\t</view> \t\t\t\t\t</view> \t\t\t\t\t<view class="footer-btn cart-btn">加入购物车</view> \t\t\t\t\t<view class="footer-btn buy-btn">立即购买</view> \t\t\t\t</view> \t\t\t</view> \t\t</view> \t</view> </template> <script setup> import { ref, reactive } from 'vue' import { onLoad } from '@dcloudio/uni-app' import { storeToRefs } from 'pinia' import { useMainStore } from '@/store/index.js' import { getStatusBarHeight, getTitleBarHeight } from '@/utils/system.js' import ajax from '@/api/ajax.js' const store = useMainStore() const { theme } = storeToRefs(store) const statusBarHeight = ref(getStatusBarHeight()) const headerBarHeight = ref(getTitleBarHeight()) const currentIndex = ref(0) const isCollected = ref(false) const goods = reactive({ \tid: '', \ttitle: '', \tintroduction: '' }) const mediaList = ref([]) const reviewList = ref([]) const price = ref('0.00') const originalPrice = ref('0.00') const soldCount = ref('0') const leftMenus = ref([ \t{ id: 1, name: '自商体', icon: 'shop' }, \t{ id: 2, name: '全息工厂', icon: 'settings' }, \t{ id: 3, name: '商誉分', icon: 'star' } ]) const rightMenus = ref([ \t{ id: 1, name: '关注', icon: 'person' }, \t{ id: 2, name: '详情', icon: 'list' }, \t{ id: 3, name: '评价', icon: 'chat' } ]) const featureList = ref([ \t{ id: 1, name: '4档刷牙模式', icon: 'gear' }, \t{ id: 2, name: '150天长续航', icon: 'loop' }, \t{ id: 3, name: 'IPX8防水', icon: 'water' }, \t{ id: 4, name: '360°无线充', icon: 'reload' } ]) function navigateBack() { \tuni.navigateBack() } function goDetail() { \tuni.navigateTo({ \t\turl: '/pages/index/goodsPage/Productattributes/productLntroduction?id=' + goods.id \t}) } function goReview() { \tuni.navigateTo({ \t\turl: '/pages/index/goodsPage/Productattributes/ProductReview?id=' + goods.id \t}) } function goMenu(item) { \tif (item.name === '详情') { \t\tgoDetail() \t} \tif (item.name === '评价') { \t\tgoReview() \t} } async function fetchCollectStatus(goodsId) { \tconst consumerId = uni.getStorageSync('consumerId') \tif (!consumerId || !goodsId) return \ttry { \t\tconst res = await ajax.post({ \t\t\turl: 'consumerCollectMallGoodsShow', \t\t\tmethod: 'POST', \t\t\tdata: { \t\t\t\tconsumerId, \t\t\t\tmallGoodsId: goodsId \t\t\t} \t\t}) \t\tisCollected.value = !!res?.data?.collect \t} catch (error) { \t\tconsole.log('收藏状态获取失败', error) \t} } async function toggleCollect() { \tconst consumerId = uni.getStorageSync('consumerId') \tif (!consumerId) { \t\tuni.showToast({ \t\t\ttitle: '请先登录', \t\t\ticon: 'none' \t\t}) \t\treturn \t} \tconst oldStatus = isCollected.value \tisCollected.value = !oldStatus \ttry { \t\tconst res = await ajax.post({ \t\t\turl: 'consumerCollectMallGoods', \t\t\tmethod: 'POST', \t\t\tdata: { \t\t\t\tconsumerId, \t\t\t\tmallGoodsId: goods.id, \t\t\t\tcollected: isCollected.value \t\t\t} \t\t}) \t\tif (res?.data?.result !== 'success') { \t\t\tisCollected.value = oldStatus \t\t\tuni.showToast({ \t\t\t\ttitle: '操作失败', \t\t\t\ticon: 'none' \t\t\t}) \t\t\treturn \t\t} \t\tuni.showToast({ \t\t\ttitle: isCollected.value ? '收藏成功' : '取消收藏', \t\t\ticon: 'none' \t\t}) \t} catch (error) { \t\tisCollected.value = oldStatus \t\tuni.showToast({ \t\t\ttitle: '网络异常', \t\t\ticon: 'none' \t\t}) \t} } async function loadData(id) { \ttry { \t\tconst res = await ajax.post({ \t\t\turl: 'mallGoodsGroupShow', \t\t\tmethod: 'POST', \t\t\tdata: { \t\t\t\tid, \t\t\t\trowStartIdx: 0, \t\t\t\trowCount: 10 \t\t\t} \t\t}) \t\tconst data = res?.data || {} \t\tconst goodsObject = data.mallGoodsGroupObject || {} \t\tgoods.id = goodsObject.id || '' \t\tgoods.title = goodsObject.title || '' \t\tgoods.introduction = goodsObject.introduction || '' \t\tprice.value = data.mallGoodsArray?.[0]?.price || '0.00' \t\toriginalPrice.value = data.mallGoodsArray?.[0]?.originalPrice || price.value \t\tsoldCount.value = data.mallGoodsArray?.[0]?.count || '0' \t\tmediaList.value = (goodsObject.mallGoodsFileArray || []).map(item => { \t\t\tconst isVideo = String(item.fileName || '').includes('.mp4') \t\t\treturn { \t\t\t\ttype: isVideo ? 'video' : 'img', \t\t\t\turl: item.playURL \t\t\t} \t\t}) \t\treviewList.value = (data.mallOrderArray || []).map(item => ({ \t\t\tid: item.id, \t\t\tnickname: item.nickname, \t\t\tavatarUrl: item.avatarUrl, \t\t\tappraise: item.appraise, \t\t\tappraiseFiles: (item.mallOrderAppraiseFileArray || []).map(file => ({ \t\t\t\turl: file.playUrl \t\t\t})) \t\t})) \t\tfetchCollectStatus(goods.id) \t} catch (error) { \t\tconsole.log(error) \t} } onLoad((option) => { \tif (option?.id) { \t\tloadData(option.id) \t} }) </script> <style lang="scss" scoped> @import '@/assets/styles/global.scss'; .app-container { \tmin-height: 100vh; \tbackground: #f7f8fc; } .content { \tmin-height: 100vh; \tbackground: linear-gradient(180deg, #fdf7ea 0%, #f7f8fc 36%, #f7f8fc 100%); } .overlay { \tmin-height: 100vh; \tdisplay: flex; \tflex-direction: column; } .header { \twidth: 94%; \tmargin: 0 auto; } .header-inner { \tposition: relative; \theight: 100%; \tdisplay: flex; \talign-items: center; \tjustify-content: center; } .left-box, .right-box { \tposition: absolute; \ttop: 0; \tbottom: 0; \twidth: 120rpx; \tdisplay: flex; \talign-items: center; } .left-box { \tleft: 0; \tjustify-content: flex-start; } .right-box { \tright: 0; \tjustify-content: flex-end; } .title-text { \tfont-size: 32rpx; \tfont-weight: 600; \tcolor: #111111; } .encyclopedia-btn { \tpadding: 10rpx 18rpx; \tborder-radius: 999rpx; \tbackground: linear-gradient(135deg, #d89838 0%, #f1dfaf 100%); } .encyclopedia-text { \tfont-size: 22rpx; \tcolor: #2a1d08; \tfont-weight: 600; } .page-scroll { \tflex: 1; \theight: 0; } .page-wrap { \twidth: 94%; \tmargin: 0 auto; \tpadding-bottom: 180rpx; } .banner-card { \tmargin-top: 18rpx; \tposition: relative; \tborder-radius: 32rpx; \toverflow: hidden; \tbackground: #ffffff; \tbox-shadow: 0 8rpx 24rpx rgba(0,0,0,.05); } .banner-swiper { \twidth: 100%; \theight: 760rpx; } .banner-image { \twidth: 100%; \theight: 100%; } .swiper-index { \tposition: absolute; \tright: 24rpx; \tbottom: 24rpx; \tpadding: 8rpx 18rpx; \tborder-radius: 999rpx; \tbackground: rgba(0,0,0,.45); \tcolor: #ffffff; \tfont-size: 22rpx; } .float-btns { \tposition: absolute; \ttop: 28rpx; \tz-index: 9; } .float-btns.left { \tleft: 20rpx; } .float-btns.right { \tright: 20rpx; } .float-item { \tmargin-bottom: 18rpx; \ttext-align: center; } .float-icon { \twidth: 72rpx; \theight: 72rpx; \tborder-radius: 50%; \tbackground: rgba(0,0,0,.42); \tdisplay: flex; \talign-items: center; \tjustify-content: center; \tmargin: 0 auto 8rpx; } .float-text { \tfont-size: 20rpx; \tcolor: #ffffff; } .goods-card, .feature-card, .entry-card, .review-card { \tmargin-top: 20rpx; \tpadding: 24rpx; \tborder-radius: 28rpx; \tbackground: rgba(255,255,255,.96); \tbox-shadow: 0 8rpx 24rpx rgba(0,0,0,.04); } .price-row { \tdisplay: flex; \talign-items: center; \tjustify-content: space-between; } .price-symbol { \tfont-size: 28rpx; \tcolor: #ff3b30; \tfont-weight: 700; } .price-text { \tfont-size: 52rpx; \tfont-weight: 700; \tcolor: #ff3b30; } .origin-price { \tmargin-left: 12rpx; \tfont-size: 24rpx; \ttext-decoration: line-through; \tcolor: #9aa0aa; } .sold-text { \tfont-size: 22rpx; \tcolor: #8a8f99; } .title-row { \tmargin-top: 18rpx; } .goods-title { \tfont-size: 32rpx; \tline-height: 1.5; \tfont-weight: 600; \tcolor: #1f2329; } .desc-row { \tmargin-top: 14rpx; } .goods-desc { \tfont-size: 24rpx; \tline-height: 1.7; \tcolor: #8a8f99; } .tag-row { \tmargin-top: 18rpx; \tdisplay: flex; \tflex-wrap: wrap; \tgap: 14rpx; } .tag-item { \tpadding: 10rpx 20rpx; \tborder-radius: 999rpx; \tbackground: rgba(216,152,56,.12); \tfont-size: 22rpx; \tcolor: #d89838; } .feature-card { \tdisplay: grid; \tgrid-template-columns: repeat(4,1fr); \tgap: 18rpx; } .feature-item { \tdisplay: flex; \tflex-direction: column; \talign-items: center; } .feature-icon { \twidth: 72rpx; \theight: 72rpx; \tborder-radius: 50%; \tbackground: #f6f2ff; \tdisplay: flex; \talign-items: center; \tjustify-content: center; } .feature-text { \tmargin-top: 12rpx; \tfont-size: 22rpx; \ttext-align: center; \tcolor: #6c7280; } .entry-card, .review-header, .review-user { \tdisplay: flex; \talign-items: center; \tjustify-content: space-between; } .entry-title, .review-title { \tfont-size: 30rpx; \tfont-weight: 600; \tcolor: #20242b; } .entry-desc { \tdisplay: block; \tmargin-top: 10rpx; \tfont-size: 22rpx; \tcolor: #8a8f99; } .review-more { \tfont-size: 24rpx; \tcolor: #d89838; } .review-item { \tpadding-top: 24rpx; \tmargin-top: 24rpx; \tborder-top: 1rpx solid #f1f2f6; } .review-avatar { \twidth: 60rpx; \theight: 60rpx; \tborder-radius: 50%; \tmargin-right: 14rpx; } .review-name { \tfont-size: 24rpx; \tcolor: #20242b; } .review-content { \tdisplay: block; \tmargin-top: 16rpx; \tfont-size: 24rpx; \tline-height: 1.7; \tcolor: #555b66; } .review-media { \tmargin-top: 18rpx; \twhite-space: nowrap; } .review-image { \twidth: 180rpx; \theight: 180rpx; \tborder-radius: 20rpx; \tmargin-right: 16rpx; \tbackground: #f5f6fa; } .footer { \tposition: fixed; \tleft: 0; \tright: 0; \tbottom: 0; \tpadding: 18rpx 24rpx calc(env(safe-area-inset-bottom) + 18rpx); \tbackground: linear-gradient(180deg, rgba(247,248,252,0) 0%, #f7f8fc 30%); \tdisplay: flex; \talign-items: center; \tgap: 16rpx; } .footer-left { \twidth: 120rpx; \tflex-shrink: 0; } .footer-icon { \tdisplay: flex; \tflex-direction: column; \talign-items: center; } .footer-label { \tmargin-top: 6rpx; \tfont-size: 22rpx; \tcolor: #4b5563; } .footer-btn { \tflex: 1; \theight: 88rpx; \tborder-radius: 999rpx; \tdisplay: flex; \talign-items: center; \tjustify-content: center; \tfont-size: 28rpx; \tfont-weight: 600; } .cart-btn { \tbackground: #fff1da; \tcolor: #d89838; } .buy-btn { \tbackground: linear-gradient(135deg, #d7983e 0%, #edd9a1 100%); \tcolor: #2a1d08; } </style> """) product_intro = dedent(""" <template> \t<view :class="['app-container', theme]"> \t\t<view class="content"> \t\t\t<view class="overlay"> \t\t\t\t<view class="header" :style="{ paddingTop: statusBarHeight + 'px', height: headerBarHeight + 'px' }"> \t\t\t\t\t<view class="header-inner"> \t\t\t\t\t\t<view class="left-box" @click="navigateBack"> \t\t\t\t\t\t\t<uni-icons type="left" color="#111111" size="26" /> \t\t\t\t\t\t</view> \t\t\t\t\t\t<view class="center-box"> \t\t\t\t\t\t\t<text class="title-text">商品详情</text> \t\t\t\t\t\t</view> \t\t\t\t\t\t<view class="right-box"> \t\t\t\t\t\t\t<uni-icons type="more-filled" color="#111111" size="22" /> \t\t\t\t\t\t</view> \t\t\t\t\t</view> \t\t\t\t</view> \t\t\t\t<scroll-view scroll-y class="page-scroll"> \t\t\t\t\t<view class="page-wrap"> \t\t\t\t\t\t<view class="top-card"> \t\t\t\t\t\t\t<text class="goods-title">{{ goods.title || '商品详情介绍' }}</text> \t\t\t\t\t\t\t<text class="goods-desc">{{ goods.introduction || '暂无商品介绍' }}</text> \t\t\t\t\t\t</view> \t\t\t\t\t\t<view v-if="detailImages.length" class="detail-list"> \t\t\t\t\t\t\t<view class="detail-item" v-for="(item,index) in detailImages" :key="index"> \t\t\t\t\t\t\t\t<image :src="item.url" class="detail-image" mode="widthFix"></image> \t\t\t\t\t\t\t</view> \t\t\t\t\t\t</view> \t\t\t\t\t\t<u-empty \t\t\t\t\t\t\tv-else \t\t\t\t\t\t\tmode="data" \t\t\t\t\t\t\ticon="http://cdn.uviewui.com/uview/empty/data.png" \t\t\t\t\t\t\ttext="抱歉,暂无商品详情" \t\t\t\t\t\t></u-empty> \t\t\t\t\t\t<view class="bottom-space"></view> \t\t\t\t\t</view> \t\t\t\t</scroll-view> \t\t\t</view> \t\t</view> \t</view> </template> <script setup> import { ref, reactive } from 'vue' import { onLoad } from '@dcloudio/uni-app' import { storeToRefs } from 'pinia' import { useMainStore } from '@/store/index.js' import { getStatusBarHeight, getTitleBarHeight } from '@/utils/system.js' import ajax from '@/api/ajax.js' const store = useMainStore() const { theme } = storeToRefs(store) const statusBarHeight = ref(getStatusBarHeight()) const headerBarHeight = ref(getTitleBarHeight()) const goods = reactive({ \ttitle: '', \tintroduction: '' }) const detailImages = ref([]) function navigateBack() { \tuni.navigateBack() } async function loadData(id) { \ttry { \t\tconst res = await ajax.post({ \t\t\turl: 'mallGoodsGroupShow', \t\t\tmethod: 'POST', \t\t\tdata: { \t\t\t\tid, \t\t\t\trowStartIdx: 0, \t\t\t\trowCount: 10 \t\t\t} \t\t}) \t\tconst data = res?.data || {} \t\tconst goodsObject = data.mallGoodsGroupObject || {} \t\tgoods.title = goodsObject.title || '' \t\tgoods.introduction = goodsObject.introduction || '' \t\tdetailImages.value = (goodsObject.mallGoodsFileArray || []) \t\t\t.filter(item => !String(item.fileName || '').includes('.mp4')) \t\t\t.map(item => ({ \t\t\t\turl: item.playURL \t\t\t})) \t} catch (error) { \t\tconsole.log(error) \t} } onLoad((option) => { \tif (option?.id) { \t\tloadData(option.id) \t} }) </script> <style lang="scss" scoped> @import '@/assets/styles/global.scss'; .app-container { \tmin-height: 100vh; \tbackground: #f7f8fc; } .content { \tmin-height: 100vh; \tbackground: linear-gradient(180deg, #fdf7ea 0%, #f7f8fc 36%, #f7f8fc 100%); } .overlay { \tmin-height: 100vh; \tdisplay: flex; \tflex-direction: column; } .header { \twidth: 94%; \tmargin: 0 auto; } .header-inner { \tposition: relative; \theight: 100%; \tdisplay: flex; \talign-items: center; \tjustify-content: center; } .left-box, .right-box { \tposition: absolute; \ttop: 0; \tbottom: 0; \twidth: 120rpx; \tdisplay: flex; \talign-items: center; } .left-box { \tleft: 0; \tjustify-content: flex-start; } .right-box { \tright: 0; \tjustify-content: flex-end; } .title-text { \tfont-size: 32rpx; \tfont-weight: 600; \tcolor: #111111; } .page-scroll { \tflex: 1; \theight: 0; } .page-wrap { \twidth: 94%; \tmargin: 0 auto; \tpadding: 20rpx 0 120rpx; } .top-card { \tpadding: 28rpx; \tborder-radius: 28rpx; \tbackground: rgba(255,255,255,.96); \tbox-shadow: 0 8rpx 24rpx rgba(0,0,0,.04); } .goods-title { \tfont-size: 34rpx; \tfont-weight: 600; \tcolor: #1f2329; } .goods-desc { \tdisplay: block; \tmargin-top: 16rpx; \tfont-size: 24rpx; \tline-height: 1.8; \tcolor: #7b818c; } .detail-list { \tmargin-top: 20rpx; } .detail-item { \tmargin-bottom: 20rpx; \tborder-radius: 28rpx; \toverflow: hidden; \tbackground: #ffffff; \tbox-shadow: 0 8rpx 24rpx rgba(0,0,0,.04); } .detail-image { \twidth: 100%; \tdisplay: block; } .bottom-space { \theight: 80rpx; } </style> """) product_review = dedent(""" <template> \t<view :class="['app-container', theme]"> \t\t<view class="content"> \t\t\t<view class="overlay"> \t\t\t\t<view class="header" :style="{ paddingTop: statusBarHeight + 'px', height: headerBarHeight + 'px' }"> \t\t\t\t\t<view class="header-inner"> \t\t\t\t\t\t<view class="left-box" @click="navigateBack"> \t\t\t\t\t\t\t<uni-icons type="left" color="#111111" size="26" /> \t\t\t\t\t\t</view> \t\t\t\t\t\t<view class="center-box"> \t\t\t\t\t\t\t<text class="title-text">商品评价</text> \t\t\t\t\t\t</view> \t\t\t\t\t\t<view class="right-box"> \t\t\t\t\t\t\t<uni-icons type="more-filled" color="#111111" size="22" /> \t\t\t\t\t\t</view> \t\t\t\t\t</view> \t\t\t\t</view> \t\t\t\t<scroll-view scroll-y class="page-scroll"> \t\t\t\t\t<view class="page-wrap"> \t\t\t\t\t\t<view class="review-top-card"> \t\t\t\t\t\t\t<text class="review-count">评价({{ reviewList.length }}+)</text> \t\t\t\t\t\t\t<view class="tag-list"> \t\t\t\t\t\t\t\t<view class="tag-item" v-for="item in tags" :key="item"> \t\t\t\t\t\t\t\t\t{{ item }} \t\t\t\t\t\t\t\t</view> \t\t\t\t\t\t\t</view> \t\t\t\t\t\t</view> \t\t\t\t\t\t<view v-if="reviewList.length"> \t\t\t\t\t\t\t<view class="review-card" v-for="item in reviewList" :key="item.id"> \t\t\t\t\t\t\t\t<view class="user-row"> \t\t\t\t\t\t\t\t\t<image :src="item.avatarUrl" class="avatar" mode="aspectFill"></image> \t\t\t\t\t\t\t\t\t<view class="user-info"> \t\t\t\t\t\t\t\t\t\t<text class="nickname">{{ item.nickname || '匿名用户' }}</text> \t\t\t\t\t\t\t\t\t\t<text class="user-size">{{ item.extraText || '上身舒适 · 面料柔软' }}</text> \t\t\t\t\t\t\t\t\t</view> \t\t\t\t\t\t\t\t</view> \t\t\t\t\t\t\t\t<text class="review-content">{{ item.appraise || '用户未填写评价内容' }}</text> \t\t\t\t\t\t\t\t<view class="image-list" v-if="item.appraiseFiles.length"> \t\t\t\t\t\t\t\t\t<image \t\t\t\t\t\t\t\t\t\tv-for="(img,index) in item.appraiseFiles" \t\t\t\t\t\t\t\t\t\t:key="index" \t\t\t\t\t\t\t\t\t\t:src="img.url" \t\t\t\t\t\t\t\t\t\tclass="review-image" \t\t\t\t\t\t\t\t\t\tmode="aspectFill" \t\t\t\t\t\t\t\t\t></image> \t\t\t\t\t\t\t\t</view> \t\t\t\t\t\t\t</view> \t\t\t\t\t\t</view> \t\t\t\t\t\t<u-empty \t\t\t\t\t\t\tv-else \t\t\t\t\t\t\tmode="comment" \t\t\t\t\t\t\ticon="http://cdn.uviewui.com/uview/empty/comment.png" \t\t\t\t\t\t\ttext="抱歉,暂无用户评价" \t\t\t\t\t\t></u-empty> \t\t\t\t\t\t<view class="bottom-space"></view> \t\t\t\t\t</view> \t\t\t\t</scroll-view> \t\t\t</view> \t\t</view> \t</view> </template> <script setup> import { ref } from 'vue' import { onLoad } from '@dcloudio/uni-app' import { storeToRefs } from 'pinia' import { useMainStore } from '@/store/index.js' import { getStatusBarHeight, getTitleBarHeight } from '@/utils/system.js' import ajax from '@/api/ajax.js' const store = useMainStore() const { theme } = storeToRefs(store) const statusBarHeight = ref(getStatusBarHeight()) const headerBarHeight = ref(getTitleBarHeight()) const reviewList = ref([]) const tags = ref([ \t'穿着舒服', \t'做工精细', \t'面料柔软' ]) function navigateBack() { \tuni.navigateBack() } async function loadData(id) { \ttry { \t\tconst res = await ajax.post({ \t\t\turl: 'mallGoodsGroupShow', \t\t\tmethod: 'POST', \t\t\tdata: { \t\t\t\tid, \t\t\t\trowStartIdx: 0, \t\t\t\trowCount: 50 \t\t\t} \t\t}) \t\tconst data = res?.data || {} \t\treviewList.value = (data.mallOrderArray || []).map(item => ({ \t\t\tid: item.id, \t\t\tnickname: item.nickname, \t\t\tavatarUrl: item.avatarUrl, \t\t\tappraise: item.appraise, \t\t\textraText: '尺码合适 · 面料舒适 · 上身很好看', \t\t\tappraiseFiles: (item.mallOrderAppraiseFileArray || []) \t\t\t\t.filter(file => !String(file.fileName || '').includes('.mp4')) \t\t\t\t.map(file => ({ \t\t\t\t\turl: file.playUrl \t\t\t\t})) \t\t})) \t} catch (error) { \t\tconsole.log(error) \t} } onLoad((option) => { \tif (option?.id) { \t\tloadData(option.id) \t} }) </script> <style lang="scss" scoped> @import '@/assets/styles/global.scss'; .app-container { \tmin-height: 100vh; \tbackground: #f7f8fc; } .content { \tmin-height: 100vh; \tbackground: linear-gradient(180deg, #fdf7ea 0%, #f7f8fc 36%, #f7f8fc 100%); } .overlay { \tmin-height: 100vh; \tdisplay: flex; \tflex-direction: column; } .header { \twidth: 94%; \tmargin: 0 auto; } .header-inner { \tposition: relative; \theight: 100%; \tdisplay: flex; \talign-items: center; \tjustify-content: center; } .left-box, .right-box { \tposition: absolute; \ttop: 0; \tbottom: 0; \twidth: 120rpx; \tdisplay: flex; \talign-items: center; } .left-box { \tleft: 0; \tjustify-content: flex-start; } .right-box { \tright: 0; \tjustify-content: flex-end; } .title-text { \tfont-size: 32rpx; \tfont-weight: 600; \tcolor: #111111; } .page-scroll { \tflex: 1; \theight: 0; } .page-wrap { \twidth: 94%; \tmargin: 0 auto; \tpadding: 20rpx 0 120rpx; } .review-top-card, .review-card { \tpadding: 24rpx; \tborder-radius: 28rpx; \tbackground: rgba(255,255,255,.96); \tbox-shadow: 0 8rpx 24rpx rgba(0,0,0,.04); } .review-card { \tmargin-top: 20rpx; } .review-count { \tfont-size: 32rpx; \tfont-weight: 600; \tcolor: #1f2329; } .tag-list { \tmargin-top: 20rpx; \tdisplay: flex; \tgap: 16rpx; \tflex-wrap: wrap; } .tag-item { \tpadding: 12rpx 24rpx; \tborder-radius: 999rpx; \tbackground: #f2f3f7; \tfont-size: 22rpx; \tcolor: #7b818c; } .user-row { \tdisplay: flex; \talign-items: center; } .avatar { \twidth: 68rpx; \theight: 68rpx; \tborder-radius: 50%; \tbackground: #f2f3f7; } .user-info { \tmargin-left: 16rpx; \tflex: 1; } .nickname { \tfont-size: 26rpx; \tfont-weight: 600; \tcolor: #20242b; } .user-size { \tdisplay: block; \tmargin-top: 6rpx; \tfont-size: 22rpx; \tcolor: #8a8f99; } .review-content { \tdisplay: block; \tmargin-top: 18rpx; \tfont-size: 24rpx; \tline-height: 1.7; \tcolor: #555b66; } .image-list { \tmargin-top: 18rpx; \tdisplay: flex; \tgap: 16rpx; \tflex-wrap: wrap; } .review-image { \twidth: 200rpx; \theight: 200rpx; \tborder-radius: 20rpx; \tbackground: #f5f6fa; } .bottom-space { \theight: 80rpx; } </style> """) readme = dedent(""" # 商品详情页升级说明 ## 已完成页面 1. ProductDetails.vue 2. productLntroduction.vue 3. ProductReview.vue ## 功能说明 - 统一高级渐变背景风格 - 商品大图轮播 - 视频/图片混合展示 - 商品评价展示 - 空状态使用 uView `u-empty` - 收藏/取消收藏优化 - 六宫格与悬浮图标 - 商品详情跳转 - 商品评价跳转 ## API 使用: - mallGoodsGroupShow - consumerCollectMallGoods - consumerCollectMallGoodsShow ## 收藏接口优化 已经加入: - 乐观更新 - 失败回滚 - 登录校验 - 网络异常提示 ## 注意 需要项目中已经安装: - uView Plus - uni-icons """) (base / 'ProductDetails.vue').write_text(product_details, encoding='utf-8') (base / 'productLntroduction.vue').write_text(product_intro, encoding='utf-8') (base / 'ProductReview.vue').write_text(product_review, encoding='utf-8') (base / 'README.md').write_text(readme, encoding='utf-8') zip_path = '/mnt/data/product_pages_upgrade.zip' with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf: for file in base.iterdir(): zf.write(file, arcname=file.name) print(f'Created zip: {zip_path}') print('Files:') for f in base.iterdir(): print('-', f.name)

उत्तर

已经帮你把三个页面重构并整理成压缩包了:

  • ProductDetails.vue(优化后的商品详情页)
  • productLntroduction.vue(商品详情介绍页)
  • ProductReview.vue(商品评价页)
  • README.md(接入说明)

里面已经包含:

  • 统一高级渐变背景风格
  • 商品大图/视频轮播
  • 六个悬浮功能按钮图标
  • 商品详情跳转
  • 商品评价跳转
  • 收藏/取消收藏优化
  • uView 空状态
  • API 对接结构
  • 商品评价展示
  • 图文详情展示
  • 底部购买栏优化
  • 更接近你发的 UI 图

下载压缩包:

下载 product_pages_upgrade.zip

इस प्रश्नोत्तर को साझा करें