结合图片重写吧 然后背景还有用之前的背景就是图片又多出了几张然后这个是pages/mall/Equi...

생성일: 2026년 5월 25일

답변에 사용된 모델 GPT-5.5 Thinking by Chat01

질문

image.png
image.png
image.png
image.png
image.png

结合图片重写吧 然后背景还有用之前的背景就是图片又多出了几张然后这个是pages/mall/Equitysharing/CouponPage 根目录的页面
根据图片实现优惠卷这种然后根据图片实现实现全部图片的效果 这个优惠卷的形状不一定是根图片一样 你可以参考比如uview的样式的优惠券
实现多个完整的页面功能页面一定要美化啊这个优惠卷借鉴下 uview的优惠卷 或者做的更好一点学学淘宝的优惠卷之类的大概有三四个页面吧 用这个页面<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>作为基础然后这个页面参考背景颜色<template>
text
<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> <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>大概有三四个页面吧 你参考下都写完整的页面功能用zip格式返回完整的页面内容

Python

python
from pathlib import Path import zipfile, textwrap, json, os, shutil, re, math base = Path('/mnt/data/coupon-pages') if base.exists(): shutil.rmtree(base) page_dir = base / 'pages' / 'mall' / 'Equitysharing' page_dir.mkdir(parents=True, exist_ok=True) coupon_mock = r'''export const couponSeed = [ { id: 'cash-2', tab: 'cash', title: '新人立减券', valueText: '立减2元', shortText: '立减2', subValueText: '¥2', tagText: '立减', conditionText: '购买部分品类活动商品可用', ruleText: '领取后 30 天内有效,支付时自动抵扣,不与其他优惠叠加。', validText: '2025.9.30 23:59:59前有效', shopName: '惠商城旗舰店', shopDesc: '严选好物 · 售后保障', logoText: '惠', received: false, stock: 1240, salePrice: 0.2, couponAmount: 2, discountRate: 1, accent: '#ff3b30', products: [ { id: 'p-101', name: '云柔抽纸组合装', intro: '原生木浆 · 家庭囤货', price: 25.99, originPrice: 39.9, category: '生活日用', cover: 'https://images.unsplash.com/photo-1588776814546-1ffcf47267a5?auto=format&fit=crop&w=480&q=80' }, { id: 'p-102', name: '便携保温杯', intro: '316不锈钢 · 轻量随行', price: 25.99, originPrice: 49.9, category: '日用百货', cover: 'https://images.unsplash.com/photo-1602143407151-7111542de6e8?auto=format&fit=crop&w=480&q=80' }, { id: 'p-103', name: '无香洗衣凝珠', intro: '低泡易漂 · 持久洁净', price: 32.8, originPrice: 59.9, category: '家清个护', cover: 'https://images.unsplash.com/photo-1585421514284-efb74c2b69ba?auto=format&fit=crop&w=480&q=80' } ] }, { id: 'discount-80', tab: 'discount', title: '会员折扣券', valueText: '8折', shortText: '八折', subValueText: '8折', tagText: '折扣', conditionText: '购买指定活动商品可用', ruleText: '下单指定商品享 8 折优惠,优惠金额以下单页展示为准。', validText: '2025.9.30 23:59:59前有效', shopName: '优选生活馆', shopDesc: '品质严选 · 正品保障', logoText: '优', received: true, stock: 650, salePrice: 2, couponAmount: 0, discountRate: 0.8, accent: '#ff3b30', products: [ { id: 'p-201', name: '高街休闲卫衣', intro: '舒适面料 · 春秋百搭', price: 100, originPrice: 129, category: '服饰鞋包', cover: 'https://images.unsplash.com/photo-1523398002811-999ca8dec234?auto=format&fit=crop&w=480&q=80' }, { id: 'p-202', name: '轻奢通勤双肩包', intro: '大容量 · 防泼水', price: 100, originPrice: 159, category: '服饰鞋包', cover: 'https://images.unsplash.com/photo-1553062407-98eeb64c6a62?auto=format&fit=crop&w=480&q=80' }, { id: 'p-203', name: '运动训练短袖', intro: '速干透气 · 弹力亲肤', price: 100, originPrice: 139, category: '运动户外', cover: 'https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?auto=format&fit=crop&w=480&q=80' }, { id: 'p-204', name: '云感家居拖鞋', intro: '厚底缓震 · 防滑耐磨', price: 100, originPrice: 119, category: '家居日用', cover: 'https://images.unsplash.com/photo-1560343090-f0409e92791a?auto=format&fit=crop&w=480&q=80' } ] }, { id: 'full-20', tab: 'full', title: '满减神券', valueText: '满80减20', shortText: '满减', subValueText: '¥20', tagText: '满减', conditionText: '订单满 80 元可用', ruleText: '部分商品满 80 元减 20 元,退款时优惠按平台规则退回。', validText: '2025.9.30 23:59:59前有效', shopName: '经典财运小店', shopDesc: '店铺发货 · 售后保障', logoText: '财', received: false, stock: 880, salePrice: 1, couponAmount: 20, discountRate: 1, accent: '#f15a24', products: [ { id: 'p-301', name: '家用香薰机', intro: '静音加湿 · 柔和夜灯', price: 89, originPrice: 119, category: '家居生活', cover: 'https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?auto=format&fit=crop&w=480&q=80' }, { id: 'p-302', name: '桌面收纳套装', intro: '多格分类 · 办公整理', price: 96, originPrice: 139, category: '办公文具', cover: 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?auto=format&fit=crop&w=480&q=80' } ] }, { id: 'cash-5', tab: 'cash', title: '店铺回馈券', valueText: '立减5元', shortText: '立减5', subValueText: '¥5', tagText: '立减', conditionText: '精选百货商品可用', ruleText: '无门槛立减 5 元,领取后可在详情页选择适用商品。', validText: '2025.9.30 23:59:59前有效', shopName: '财运甄选', shopDesc: '快速发货 · 无忧售后', logoText: '甄', received: false, stock: 430, salePrice: 0.5, couponAmount: 5, discountRate: 1, accent: '#ff3b30', products: [ { id: 'p-401', name: '办公室靠垫', intro: '慢回弹 · 久坐舒适', price: 58, originPrice: 79, category: '家居生活', cover: 'https://images.unsplash.com/photo-1586023492125-27b2c045efd7?auto=format&fit=crop&w=480&q=80' }, { id: 'p-402', name: '便携蓝牙音箱', intro: '迷你机身 · 立体声', price: 79, originPrice: 109, category: '数码配件', cover: 'https://images.unsplash.com/photo-1608043152269-423dbba4e7e1?auto=format&fit=crop&w=480&q=80' } ] } ] export function clone(value) { return JSON.parse(JSON.stringify(value)) } export function getCouponList() { return clone(couponSeed) } export function getCouponById(id) { return clone(couponSeed.find((item) => item.id === id) || couponSeed[0]) } export function getProductById(couponId, productId) { const coupon = getCouponById(couponId) return clone(coupon.products.find((item) => item.id === productId) || coupon.products[0]) } export function formatMoney(value) { const numberValue = Number(value || 0) if (Number.isInteger(numberValue)) return numberValue.toFixed(0) return numberValue.toFixed(2) } export function calcOrderAmount(product, coupon) { const price = Number(product?.price || 0) let goodsAfter = price let discountMoney = 0 if (coupon?.tab === 'discount') { goodsAfter = price * Number(coupon.discountRate || 1) discountMoney = price - goodsAfter } if (coupon?.tab === 'cash') { discountMoney = Math.min(Number(coupon.couponAmount || 0), price) goodsAfter = price - discountMoney } if (coupon?.tab === 'full') { const amount = price >= 80 ? Number(coupon.couponAmount || 0) : 0 discountMoney = amount goodsAfter = price - amount } const couponFee = Number(coupon?.salePrice || 0) const total = Math.max(goodsAfter, 0) + couponFee return { origin: formatMoney(price), goodsAfter: formatMoney(Math.max(goodsAfter, 0)), discountMoney: formatMoney(discountMoney), couponFee: formatMoney(couponFee), total: formatMoney(total) } } ''' coupon_page = r'''<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"> <view class="left-box" @click="navigateBack"> <uni-icons type="left" color="#111111" size="26" /> </view> <view class="center-box"> <text class="title-text">惠商城</text> </view> <view @click="EnteGroupSettings" class="right-box"> <uni-icons type="more-filled" color="#111111" size="22" /> </view> </view> </view> <scroll-view scroll-y class="page-scroll" :show-scrollbar="false"> <view class="page-wrap"> <view class="search-card"> <view class="search-box"> <uni-icons type="search" color="#999999" size="18" /> <input v-model="keyword" class="search-input" placeholder="搜索店铺 / 商品 / 优惠券" placeholder-class="search-placeholder" confirm-type="search" /> </view> </view> <view class="hero-card"> <view class="hero-left"> <text class="hero-title">今日优惠券</text> <text class="hero-desc">立减、满减、折扣券都在这里</text> </view> <view class="hero-badge" @click="goMyCoupon"> <text class="hero-badge-num">{{ receivedCount }}</text> <text class="hero-badge-text">我的券</text> </view> </view> <view class="tabs-wrap"> <view v-for="item in tabs" :key="item.key" class="tab-item" :class="{ active: activeTab === item.key }" @click="changeTab(item.key)"> <text>{{ item.name }}</text> </view> </view> <view v-if="filteredCoupons.length" class="coupon-list"> <view v-for="coupon in filteredCoupons" :key="coupon.id" class="coupon-card" :class="{ received: coupon.received }"> <view class="shop-row"> <view class="shop-avatar"> <text>{{ coupon.logoText }}</text> </view> <view class="shop-info" @click="goCouponDetail(coupon)"> <text class="shop-name">{{ coupon.shopName }}</text> <text class="shop-tip">{{ coupon.conditionText }}</text> </view> <view class="coupon-value" @click="goCouponDetail(coupon)"> <text class="coupon-main">{{ coupon.valueText }}</text> <text class="coupon-sub">剩余 {{ coupon.stock }}</text> </view> </view> <view class="coupon-body"> <view class="goods-preview"> <view v-for="product in coupon.products.slice(0, 3)" :key="product.id" class="goods-mini" @click="goBuy(coupon, product)"> <image class="goods-mini-img" :src="product.cover" mode="aspectFill" /> <text class="goods-price">¥{{ formatMoney(product.price) }}</text> </view> <view class="more-entry" @click="goCouponDetail(coupon)"> <text>更多</text> <uni-icons type="right" color="#f24b40" size="12" /> </view> </view> <view class="coupon-action"> <view class="follow-btn" :class="{ followed: coupon.followed }" @click="toggleFollow(coupon)"> <text>{{ coupon.followed ? '已关注' : '关注' }}</text> </view> <view class="receive-btn" :class="{ disabled: coupon.received }" @click="handleCouponAction(coupon)"> <text>{{ coupon.received ? '去使用' : '立即领取' }}</text> </view> </view> </view> <view class="coupon-footer"> <text>{{ coupon.validText }}</text> </view> </view> </view> <view v-else class="empty-card"> <text class="empty-title">暂无可用优惠券</text> <text class="empty-desc">换个关键词试试,或切换其他分类。</text> </view> <view class="bottom-space"></view> </view> </scroll-view> <view class="fake-tabbar"> <view class="tabbar-item active"> <view class="tabbar-dot"></view> <text>惠商城</text> </view> <view class="tabbar-item"> <view class="tabbar-circle"></view> <text>经典财运</text> </view> <view class="tabbar-item"> <view class="tabbar-circle"></view> <text>闻信</text> </view> <view class="tabbar-item" @click="goMyCoupon"> <view class="tabbar-circle"></view> <text>我的</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 { getStatusBarHeight, getTitleBarHeight } from '@/utils/system.js' import { getCouponList, formatMoney } from './couponMock.js' const store = useMainStore() const { theme } = storeToRefs(store) const statusBarHeight = ref(getStatusBarHeight()) const headerBarHeight = ref(getTitleBarHeight()) const keyword = ref('') const activeTab = ref('all') const couponList = ref([]) const tabs = ref([ { key: 'all', name: '综合' }, { key: 'discount', name: '折扣' }, { key: 'cash', name: '立减' }, { key: 'full', name: '满减' } ]) const receivedCount = computed(() => couponList.value.filter((item) => item.received).length) const filteredCoupons = computed(() => { const key = keyword.value.trim() return couponList.value.filter((item) => { const tabMatched = activeTab.value === 'all' || item.tab === activeTab.value const keywordMatched = !key || item.title.includes(key) || item.shopName.includes(key) || item.valueText.includes(key) || item.products.some((product) => product.name.includes(key) || product.category.includes(key)) return tabMatched && keywordMatched }) }) function readReceivedMap() { return uni.getStorageSync('couponReceivedMap') || {} } function saveReceivedMap(map) { uni.setStorageSync('couponReceivedMap', map) } function syncCouponState() { const receivedMap = readReceivedMap() const followMap = uni.getStorageSync('couponFollowMap') || {} couponList.value = getCouponList().map((item) => ({ ...item, received: item.received || !!receivedMap[item.id], followed: !!followMap[item.id] })) } function changeTab(key) { activeTab.value = key } function toggleFollow(coupon) { coupon.followed = !coupon.followed const followMap = uni.getStorageSync('couponFollowMap') || {} followMap[coupon.id] = coupon.followed uni.setStorageSync('couponFollowMap', followMap) uni.showToast({ title: coupon.followed ? '已关注店铺' : '已取消关注', icon: 'none' }) } function receiveCoupon(coupon) { if (coupon.received) return coupon.received = true const receivedMap = readReceivedMap() receivedMap[coupon.id] = true saveReceivedMap(receivedMap) uni.showToast({ title: '领取成功', icon: 'success' }) } function handleCouponAction(coupon) { if (!coupon.received) { receiveCoupon(coupon) return } goCouponDetail(coupon) } function goCouponDetail(coupon) { uni.navigateTo({ url: `/pages/mall/Equitysharing/CouponDetail?id=${coupon.id}` }) } function goBuy(coupon, product) { if (!coupon.received) { receiveCoupon(coupon) } uni.navigateTo({ url: `/pages/mall/Equitysharing/CouponOrder?couponId=${coupon.id}&productId=${product.id}` }) } function goMyCoupon() { uni.navigateTo({ url: '/pages/mall/Equitysharing/MyCoupon' }) } function EnteGroupSettings() { uni.showActionSheet({ itemList: ['我的优惠券', '刷新列表', '清空领取状态'], success: ({ tapIndex }) => { if (tapIndex === 0) goMyCoupon() if (tapIndex === 1) { syncCouponState() uni.showToast({ title: '已刷新', icon: 'none' }) } if (tapIndex === 2) { uni.removeStorageSync('couponReceivedMap') uni.removeStorageSync('couponUseStatusMap') syncCouponState() uni.showToast({ title: '已清空演示状态', icon: 'none' }) } } }) } const navigateBack = () => { uni.navigateBack() } onLoad(() => { syncCouponState() }) onShow(() => { syncCouponState() }) </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%); background-size: cover; } .overlay { position: absolute; inset: 0; display: flex; flex-direction: column; background-size: cover; } .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: 700; color: #111111; } .page-scroll { flex: 1; height: 0; } .page-wrap { width: 94%; margin: 0 auto; padding: 12rpx 0 0; box-sizing: border-box; } .search-card { padding: 8rpx 0 16rpx; } .search-box { height: 72rpx; padding: 0 24rpx; border: 2rpx solid rgba(20, 20, 20, 0.68); border-radius: 999rpx; display: flex; align-items: center; gap: 12rpx; background: rgba(255, 255, 255, 0.72); backdrop-filter: blur(12rpx); } .search-input { flex: 1; height: 72rpx; font-size: 26rpx; color: #222222; } .search-placeholder { color: #999999; } .hero-card { margin-top: 6rpx; padding: 28rpx; border-radius: 30rpx; background: radial-gradient(circle at 88% 18%, rgba(255, 255, 255, 0.38), transparent 24%), linear-gradient(135deg, #24170d 0%, #d39a48 54%, #f0d89a 100%); display: flex; align-items: center; justify-content: space-between; box-shadow: 0 18rpx 38rpx rgba(180, 122, 42, 0.18); overflow: hidden; } .hero-left { display: flex; flex-direction: column; } .hero-title { font-size: 36rpx; color: #fff8e8; font-weight: 800; letter-spacing: 2rpx; } .hero-desc { margin-top: 10rpx; font-size: 24rpx; color: rgba(255, 248, 232, 0.78); } .hero-badge { width: 132rpx; height: 132rpx; border-radius: 50%; background: rgba(255, 255, 255, 0.2); border: 2rpx solid rgba(255, 255, 255, 0.38); display: flex; flex-direction: column; align-items: center; justify-content: center; } .hero-badge-num { font-size: 38rpx; color: #ffffff; font-weight: 800; line-height: 1; } .hero-badge-text { margin-top: 8rpx; font-size: 22rpx; color: rgba(255, 255, 255, 0.85); } .tabs-wrap { margin: 20rpx 0 4rpx; display: flex; align-items: center; gap: 34rpx; padding-left: 4rpx; } .tab-item { position: relative; padding: 12rpx 0 18rpx; font-size: 28rpx; color: #6b7280; } .tab-item.active { color: #ff2d20; font-weight: 700; } .tab-item.active::after { content: ''; position: absolute; left: 50%; bottom: 4rpx; width: 36rpx; height: 6rpx; border-radius: 999rpx; background: #ff2d20; transform: translateX(-50%); } .coupon-list { margin-top: 4rpx; } .coupon-card { position: relative; margin-top: 18rpx; padding: 20rpx 20rpx 18rpx; border-radius: 24rpx; background: rgba(255, 255, 255, 0.96); box-shadow: 0 10rpx 26rpx rgba(31, 35, 41, 0.06); border: 2rpx solid rgba(255, 255, 255, 0.88); overflow: hidden; } .coupon-card::before, .coupon-card::after { content: ''; position: absolute; right: 168rpx; width: 26rpx; height: 26rpx; border-radius: 50%; background: #f7f8fc; z-index: 2; } .coupon-card::before { top: -13rpx; } .coupon-card::after { bottom: -13rpx; } .coupon-card.received { background: rgba(255, 255, 255, 0.92); } .shop-row { display: flex; align-items: center; } .shop-avatar { width: 64rpx; height: 64rpx; border-radius: 50%; background: linear-gradient(135deg, #d6a656 0%, #f0d494 100%); display: flex; align-items: center; justify-content: center; color: #ffffff; font-size: 26rpx; font-weight: 800; flex-shrink: 0; } .shop-info { flex: 1; min-width: 0; margin-left: 18rpx; display: flex; flex-direction: column; } .shop-name { font-size: 27rpx; font-weight: 700; color: #232832; } .shop-tip { margin-top: 8rpx; font-size: 22rpx; color: #8a8f99; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .coupon-value { width: 160rpx; padding-left: 18rpx; border-left: 2rpx dashed #eee2d4; display: flex; flex-direction: column; align-items: flex-end; flex-shrink: 0; } .coupon-main { font-size: 34rpx; font-weight: 800; color: #ff2d20; line-height: 1.1; } .coupon-sub { margin-top: 8rpx; font-size: 20rpx; color: #b0b4bd; } .coupon-body { margin-top: 18rpx; display: flex; justify-content: space-between; gap: 16rpx; } .goods-preview { flex: 1; min-width: 0; display: flex; align-items: center; gap: 12rpx; } .goods-mini { width: 86rpx; flex-shrink: 0; } .goods-mini-img { width: 86rpx; height: 86rpx; border-radius: 12rpx; background: #e5e7eb; display: block; } .goods-price { display: block; margin-top: 8rpx; font-size: 20rpx; color: #ff2d20; text-align: center; line-height: 1; } .more-entry { display: flex; align-items: center; gap: 2rpx; color: #f24b40; font-size: 22rpx; white-space: nowrap; } .coupon-action { width: 150rpx; display: flex; flex-direction: column; align-items: flex-end; gap: 12rpx; flex-shrink: 0; } .follow-btn, .receive-btn { height: 50rpx; border-radius: 999rpx; display: flex; align-items: center; justify-content: center; font-size: 22rpx; } .follow-btn { width: 82rpx; color: #ffffff; background: #a6a6a6; } .follow-btn.followed { background: #d7a657; } .receive-btn { width: 146rpx; color: #ffffff; background: linear-gradient(135deg, #ff3026 0%, #ff6b35 100%); box-shadow: 0 10rpx 18rpx rgba(255, 48, 38, 0.18); } .receive-btn.disabled { background: #999999; box-shadow: none; } .coupon-footer { margin-top: 12rpx; color: #c4c8d0; font-size: 22rpx; } .empty-card { margin-top: 36rpx; padding: 60rpx 30rpx; border-radius: 28rpx; background: rgba(255, 255, 255, 0.86); display: flex; flex-direction: column; align-items: center; } .empty-title { font-size: 30rpx; font-weight: 700; color: #333333; } .empty-desc { margin-top: 12rpx; font-size: 24rpx; color: #8a8f99; } .bottom-space { height: 170rpx; } .fake-tabbar { position: fixed; left: 24rpx; right: 24rpx; bottom: calc(env(safe-area-inset-bottom) + 18rpx); height: 100rpx; border-radius: 30rpx; background: #1d1d1f; display: flex; align-items: center; justify-content: space-around; box-shadow: 0 14rpx 34rpx rgba(0, 0, 0, 0.22); } .tabbar-item { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 20rpx; color: #cfd0d4; gap: 8rpx; } .tabbar-item.active { color: #d9aa58; } .tabbar-dot { width: 30rpx; height: 30rpx; border-radius: 8rpx; background: linear-gradient(135deg, #d9aa58 0%, #f3d28a 100%); } .tabbar-circle { width: 30rpx; height: 30rpx; border-radius: 50%; border: 4rpx solid #cfd0d4; box-sizing: border-box; } .dark.app-container { background: #121212; } .dark .content { background: linear-gradient(180deg, #2a2118 0%, #151515 48%, #151515 100%); } .dark .title-text, .dark .shop-name { color: #f5f5f5; } .dark .coupon-card, .dark .empty-card { background: rgba(34, 34, 36, 0.94); border-color: rgba(255, 255, 255, 0.05); } .dark .search-box { background: rgba(34, 34, 36, 0.8); border-color: rgba(255, 255, 255, 0.2); } .dark .search-input { color: #ffffff; } </style> ''' coupon_detail = r'''<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> <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" :show-scrollbar="false"> <view class="page-wrap"> <view class="ticket-card"> <view class="ticket-main"> <view class="ticket-image"> <text>{{ coupon.logoText }}</text> </view> <view class="ticket-info"> <text class="ticket-title">{{ coupon.title }}</text> <text class="ticket-value">{{ coupon.valueText }}</text> <text class="ticket-date">{{ coupon.validText }}</text> </view> </view> <text class="ticket-rule">{{ coupon.conditionText }}</text> <view v-if="useStatus === 'using'" class="using-tag">使用中</view> </view> <view class="rule-card"> <view class="rule-title-row"> <text class="rule-title">使用说明</text> <text class="rule-mini">下单自动匹配</text> </view> <text class="rule-text">{{ coupon.ruleText }}</text> </view> <view class="section-title-row"> <text class="section-title">可用商品</text> <text class="section-count">共 {{ coupon.products.length }} 件</text> </view> <view class="goods-grid"> <view v-for="product in coupon.products" :key="product.id" class="goods-card" @click="goOrder(product)"> <image class="goods-img" :src="product.cover" mode="aspectFill" /> <view class="goods-info"> <text class="goods-name">{{ product.name }}</text> <text class="goods-intro">{{ product.intro }}</text> <view class="goods-price-row"> <view class="price-box"> <text class="price-now">¥{{ formatMoney(product.price) }}</text> <text class="price-old">¥{{ formatMoney(product.originPrice) }}</text> </view> <view class="buy-mini-btn"> <text>立即购买</text> </view> </view> <text class="coupon-after">券后约 ¥{{ getAfterPrice(product) }}</text> </view> </view> </view> <view class="bottom-space"></view> </view> </scroll-view> <view class="footer"> <view class="footer-left" @click="goMyCoupon"> <text class="footer-main">{{ coupon.received ? '已领取' : '未领取' }}</text> <text class="footer-sub">查看我的优惠券</text> </view> <view class="footer-btn" @click="primaryAction"> <text>{{ coupon.received ? '去使用' : '立即领取' }}</text> </view> </view> </view> </view> </view> </template> <script setup> import { ref } from 'vue' import { onLoad, onShow } from '@dcloudio/uni-app' import { storeToRefs } from 'pinia' import { useMainStore } from '@/store/index.js' import { getStatusBarHeight, getTitleBarHeight } from '@/utils/system.js' import { getCouponById, formatMoney, calcOrderAmount } from './couponMock.js' const store = useMainStore() const { theme } = storeToRefs(store) const statusBarHeight = ref(getStatusBarHeight()) const headerBarHeight = ref(getTitleBarHeight()) const couponId = ref('') const coupon = ref({ products: [] }) const useStatus = ref('unused') function readReceivedMap() { return uni.getStorageSync('couponReceivedMap') || {} } function readUseStatusMap() { return uni.getStorageSync('couponUseStatusMap') || {} } function loadCoupon() { const current = getCouponById(couponId.value) const receivedMap = readReceivedMap() const useStatusMap = readUseStatusMap() current.received = current.received || !!receivedMap[current.id] coupon.value = current useStatus.value = useStatusMap[current.id] || 'unused' } function receiveCoupon() { if (coupon.value.received) return const receivedMap = readReceivedMap() receivedMap[coupon.value.id] = true uni.setStorageSync('couponReceivedMap', receivedMap) coupon.value.received = true uni.showToast({ title: '领取成功', icon: 'success' }) } function getAfterPrice(product) { return calcOrderAmount(product, coupon.value).goodsAfter } function goOrder(product) { if (!coupon.value.received) receiveCoupon() uni.navigateTo({ url: `/pages/mall/Equitysharing/CouponOrder?couponId=${coupon.value.id}&productId=${product.id}` }) } function primaryAction() { if (!coupon.value.received) { receiveCoupon() return } goOrder(coupon.value.products[0]) } function goMyCoupon() { uni.navigateTo({ url: '/pages/mall/Equitysharing/MyCoupon' }) } function openMenu() { uni.showActionSheet({ itemList: ['我的优惠券', '复制优惠券名称', '返回商城'], success: ({ tapIndex }) => { if (tapIndex === 0) goMyCoupon() if (tapIndex === 1) { uni.setClipboardData({ data: coupon.value.title }) } if (tapIndex === 2) navigateBack() } }) } function navigateBack() { uni.navigateBack() } onLoad((option) => { couponId.value = option?.id || 'cash-2' loadCoupon() }) onShow(() => { if (couponId.value) loadCoupon() }) </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: 700; color: #111111; } .page-scroll { flex: 1; height: 0; } .page-wrap { width: 94%; margin: 0 auto; padding: 12rpx 0 0; box-sizing: border-box; } .ticket-card { position: relative; padding: 24rpx; border-radius: 28rpx; background: #ffffff; box-shadow: 0 12rpx 30rpx rgba(31, 35, 41, 0.06); overflow: hidden; } .ticket-card::before, .ticket-card::after { content: ''; position: absolute; top: 140rpx; width: 34rpx; height: 34rpx; border-radius: 50%; background: #f7f8fc; z-index: 2; } .ticket-card::before { left: -17rpx; } .ticket-card::after { right: -17rpx; } .ticket-main { position: relative; padding-bottom: 24rpx; border-bottom: 2rpx dashed #eee2d4; display: flex; align-items: center; gap: 22rpx; } .ticket-image { width: 148rpx; height: 148rpx; border-radius: 18rpx; background: linear-gradient(135deg, #999999 0%, #c2c2c2 100%); display: flex; align-items: center; justify-content: center; flex-shrink: 0; } .ticket-image text { color: #ffffff; font-size: 48rpx; font-weight: 900; } .ticket-info { flex: 1; min-width: 0; display: flex; flex-direction: column; } .ticket-title { font-size: 30rpx; color: #222222; font-weight: 700; } .ticket-value { margin-top: 10rpx; font-size: 44rpx; color: #ff2d20; font-weight: 900; line-height: 1; } .ticket-date { margin-top: 18rpx; font-size: 22rpx; color: #c1c5cc; } .ticket-rule { display: block; margin-top: 22rpx; text-align: center; font-size: 24rpx; color: #7b7f88; } .using-tag { position: absolute; right: 0; top: 60rpx; padding: 12rpx 10rpx; writing-mode: vertical-rl; font-size: 26rpx; color: #222222; letter-spacing: 4rpx; } .rule-card { margin-top: 18rpx; padding: 24rpx; border-radius: 24rpx; background: rgba(255, 255, 255, 0.94); box-shadow: 0 10rpx 24rpx rgba(31, 35, 41, 0.04); } .rule-title-row, .section-title-row { display: flex; align-items: center; justify-content: space-between; } .rule-title, .section-title { font-size: 30rpx; font-weight: 800; color: #20242b; } .rule-mini, .section-count { font-size: 22rpx; color: #999999; } .rule-text { display: block; margin-top: 16rpx; line-height: 1.65; font-size: 24rpx; color: #7b7f88; } .section-title-row { margin-top: 28rpx; margin-bottom: 16rpx; } .goods-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14rpx; } .goods-card { border-radius: 20rpx; overflow: hidden; background: #ffffff; box-shadow: 0 10rpx 24rpx rgba(31, 35, 41, 0.05); } .goods-img { width: 100%; height: 230rpx; background: #d8d8d8; display: block; } .goods-info { padding: 16rpx; } .goods-name { display: block; font-size: 26rpx; font-weight: 700; color: #222222; line-height: 1.25; } .goods-intro { display: block; margin-top: 8rpx; font-size: 22rpx; color: #999999; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .goods-price-row { margin-top: 14rpx; display: flex; align-items: flex-end; justify-content: space-between; gap: 10rpx; } .price-box { min-width: 0; display: flex; flex-direction: column; } .price-now { font-size: 30rpx; font-weight: 800; color: #222222; line-height: 1; } .price-old { margin-top: 6rpx; font-size: 20rpx; color: #b8bbc2; text-decoration: line-through; } .buy-mini-btn { height: 46rpx; padding: 0 16rpx; border-radius: 999rpx; display: flex; align-items: center; justify-content: center; background: #999999; color: #ffffff; font-size: 20rpx; flex-shrink: 0; } .goods-card:nth-child(2) .buy-mini-btn { background: linear-gradient(135deg, #ff3026 0%, #ff6b35 100%); } .coupon-after { display: block; margin-top: 12rpx; font-size: 22rpx; color: #ff2d20; font-weight: 700; } .bottom-space { height: 152rpx; } .footer { position: fixed; left: 0; right: 0; bottom: 0; padding: 18rpx 30rpx calc(env(safe-area-inset-bottom) + 18rpx); background: linear-gradient(180deg, rgba(247, 248, 252, 0) 0%, #f7f8fc 30%); display: flex; align-items: center; gap: 20rpx; } .footer-left { flex: 1; height: 88rpx; border-radius: 20rpx; background: #ffffff; padding: 0 24rpx; display: flex; flex-direction: column; justify-content: center; box-shadow: 0 8rpx 18rpx rgba(31, 35, 41, 0.04); } .footer-main { font-size: 28rpx; font-weight: 800; color: #20242b; } .footer-sub { margin-top: 6rpx; font-size: 22rpx; color: #999999; } .footer-btn { width: 230rpx; height: 88rpx; border-radius: 44rpx; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #ff3026 0%, #ff6b35 100%); box-shadow: 0 10rpx 24rpx rgba(255, 48, 38, 0.22); color: #ffffff; font-size: 30rpx; font-weight: 800; } .dark.app-container { background: #121212; } .dark .content { background: linear-gradient(180deg, #2a2118 0%, #151515 48%, #151515 100%); } .dark .title-text, .dark .ticket-title, .dark .rule-title, .dark .section-title, .dark .goods-name, .dark .price-now, .dark .footer-main { color: #f5f5f5; } .dark .ticket-card, .dark .rule-card, .dark .goods-card, .dark .footer-left { background: rgba(34, 34, 36, 0.94); } </style> ''' coupon_order = r'''<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> <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" :show-scrollbar="false"> <view class="page-wrap"> <view class="shop-line"> <view class="shop-logo"> <text>{{ coupon.logoText }}</text> </view> <text class="shop-name">{{ coupon.shopName }}</text> </view> <view class="product-card"> <image class="product-img" :src="product.cover" mode="aspectFill" /> <view class="product-info"> <text class="product-name">{{ product.name }}</text> <text class="product-intro">{{ product.intro }}</text> <text class="product-after">券后 ¥{{ amount.goodsAfter }}</text> </view> </view> <view class="address-card" @click="chooseAddress"> <view class="address-title">选择收货地址</view> <view class="address-row"> <view class="address-check">✓</view> <view class="address-content"> <text class="address-name">咖宠 · XXXXXXXXXXX</text> <text class="address-detail">北京市 北京市 通州区 西马庄园18号楼11单元</text> </view> <uni-icons type="right" color="#c5c8cf" size="18" /> </view> </view> <view class="coupon-select-card" @click="showCouponSelect = !showCouponSelect"> <view class="coupon-select-left"> <text class="coupon-select-title">一张({{ coupon.shortText }}券)</text> <text class="coupon-select-desc">{{ coupon.conditionText }}</text> </view> <view class="coupon-select-arrow">></view> </view> <view v-if="showCouponSelect" class="coupon-dropdown"> <view class="mini-ticket active"> <view> <text class="mini-ticket-name">{{ coupon.title }}</text> <text class="mini-ticket-rule">{{ coupon.ruleText }}</text> </view> <text class="mini-ticket-value">{{ coupon.valueText }}</text> </view> </view> <view class="pay-card"> <text class="pay-card-title">支付方式</text> <view class="pay-methods"> <view v-for="item in payMethods" :key="item.key" class="pay-method" :class="{ active: selectedPay === item.key }" @click="selectedPay = item.key"> <view class="pay-icon"></view> <text>{{ item.name }}</text> <view v-if="selectedPay === item.key" class="pay-corner">✓</view> </view> </view> </view> <view class="agreement-row" @click="agree = !agree"> <view class="radio" :class="{ checked: agree }"> <text v-if="agree">✓</text> </view> <text class="agreement-text">我已阅读并同意</text> <text class="agreement-link">《用户购买协议》</text> </view> <view class="amount-card"> <view class="amount-row"> <text>商品原价</text> <text>¥ {{ amount.origin }}</text> </view> <view class="amount-row"> <text>商品优惠后</text> <text>¥ {{ amount.goodsAfter }}</text> </view> <view class="amount-row"> <text>{{ coupon.shortText }}券</text> <text>¥ {{ amount.couponFee }}</text> </view> </view> <view class="bottom-space"></view> </view> </scroll-view> <view class="footer"> <view class="footer-price"> <text class="footer-label">支付共</text> <text class="footer-money">¥{{ amount.total }}</text> </view> <view class="pay-btn" @click="openPayPanel"> <text>立即支付</text> </view> </view> <view v-if="showPricePanel" class="panel-mask" @click="showPricePanel = false"> <view class="price-panel" @click.stop> <view class="panel-handle"></view> <view class="panel-title-row"> <text class="panel-title">确认支付</text> <text class="panel-close" @click="showPricePanel = false">×</text> </view> <view class="panel-row"> <text>商品原价</text> <text>¥ {{ amount.origin }}</text> </view> <view class="panel-row"> <text>商品优惠后</text> <text>¥ {{ amount.goodsAfter }}</text> </view> <view class="panel-row"> <text>{{ coupon.shortText }}券</text> <text>¥ {{ amount.couponFee }}</text> </view> <view class="panel-total"> <text>支付共</text> <text>¥ {{ amount.total }}</text> </view> <view class="confirm-pay-btn" @click="confirmPay"> <text>确定支付</text> </view> </view> </view> </view> </view> </view> </template> <script setup> import { ref, computed } 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 { getCouponById, getProductById, calcOrderAmount } from './couponMock.js' const store = useMainStore() const { theme } = storeToRefs(store) const statusBarHeight = ref(getStatusBarHeight()) const headerBarHeight = ref(getTitleBarHeight()) const couponId = ref('discount-80') const productId = ref('p-201') const coupon = ref({ products: [] }) const product = ref({}) const selectedPay = ref('wechat') const agree = ref(false) const showCouponSelect = ref(false) const showPricePanel = ref(false) const payMethods = ref([ { key: 'wechat', name: '微信支付' }, { key: 'alipay', name: '支付宝支付' } ]) const amount = computed(() => calcOrderAmount(product.value, coupon.value)) function loadOrder() { coupon.value = getCouponById(couponId.value) product.value = getProductById(couponId.value, productId.value) } function chooseAddress() { uni.showToast({ title: '这里接入地址选择页', icon: 'none' }) } function openPayPanel() { if (!agree.value) { uni.showToast({ title: '请先同意购买协议', icon: 'none' }) return } showPricePanel.value = true } function confirmPay() { const useStatusMap = uni.getStorageSync('couponUseStatusMap') || {} useStatusMap[coupon.value.id] = 'using' uni.setStorageSync('couponUseStatusMap', useStatusMap) const orderMap = uni.getStorageSync('couponOrderMap') || {} orderMap[coupon.value.id] = { couponId: coupon.value.id, productId: product.value.id, productName: product.value.name, total: amount.value.total, payMethod: selectedPay.value, createdAt: Date.now() } uni.setStorageSync('couponOrderMap', orderMap) uni.showToast({ title: '支付成功', icon: 'success' }) setTimeout(() => { showPricePanel.value = false uni.navigateTo({ url: `/pages/mall/Equitysharing/MyCoupon?status=using&id=${coupon.value.id}` }) }, 500) } function openMenu() { uni.showActionSheet({ itemList: ['查看优惠券', '我的优惠券'], success: ({ tapIndex }) => { if (tapIndex === 0) { uni.navigateTo({ url: `/pages/mall/Equitysharing/CouponDetail?id=${coupon.value.id}` }) } if (tapIndex === 1) { uni.navigateTo({ url: '/pages/mall/Equitysharing/MyCoupon' }) } } }) } function navigateBack() { uni.navigateBack() } onLoad((option) => { couponId.value = option?.couponId || 'discount-80' productId.value = option?.productId || 'p-201' loadOrder() }) </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: 700; color: #111111; } .page-scroll { flex: 1; height: 0; } .page-wrap { width: 94%; margin: 0 auto; padding: 12rpx 0 0; box-sizing: border-box; } .shop-line { display: flex; align-items: center; gap: 14rpx; margin-bottom: 12rpx; padding: 0 8rpx; } .shop-logo { width: 42rpx; height: 42rpx; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #d6a656 0%, #f0d494 100%); color: #ffffff; font-size: 20rpx; font-weight: 800; } .shop-name { font-size: 27rpx; color: #20242b; font-weight: 700; } .product-card, .address-card, .coupon-select-card, .coupon-dropdown, .pay-card, .amount-card { margin-top: 16rpx; border-radius: 22rpx; background: #ffffff; box-shadow: 0 10rpx 24rpx rgba(31, 35, 41, 0.05); } .product-card { padding: 18rpx; display: flex; gap: 18rpx; } .product-img { width: 152rpx; height: 152rpx; border-radius: 16rpx; background: #d8d8d8; flex-shrink: 0; } .product-info { flex: 1; min-width: 0; display: flex; flex-direction: column; justify-content: center; } .product-name { font-size: 30rpx; font-weight: 800; color: #222222; } .product-intro { margin-top: 8rpx; font-size: 24rpx; color: #8a8f99; } .product-after { margin-top: 12rpx; font-size: 24rpx; font-weight: 800; color: #ff2d20; } .address-card { padding: 22rpx; } .address-title { font-size: 28rpx; font-weight: 700; color: #20242b; margin-bottom: 16rpx; } .address-row { display: flex; align-items: center; gap: 14rpx; } .address-check { width: 32rpx; height: 32rpx; border-radius: 50%; background: #b42632; color: #ffffff; display: flex; align-items: center; justify-content: center; font-size: 20rpx; flex-shrink: 0; } .address-content { flex: 1; min-width: 0; display: flex; flex-direction: column; } .address-name { font-size: 24rpx; color: #20242b; } .address-detail { margin-top: 6rpx; font-size: 23rpx; color: #4f5662; line-height: 1.35; } .coupon-select-card { padding: 0 22rpx; height: 88rpx; display: flex; align-items: center; justify-content: space-between; } .coupon-select-left { display: flex; flex-direction: column; min-width: 0; } .coupon-select-title { font-size: 28rpx; color: #8a8f99; } .coupon-select-desc { margin-top: 4rpx; font-size: 20rpx; color: #c1c5cc; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .coupon-select-arrow { font-size: 46rpx; color: #ff2d20; line-height: 1; } .coupon-dropdown { padding: 18rpx; } .mini-ticket { padding: 18rpx; border-radius: 18rpx; background: linear-gradient(135deg, #fff3ec 0%, #fff9f1 100%); border: 2rpx solid rgba(255, 48, 38, 0.16); display: flex; align-items: center; justify-content: space-between; } .mini-ticket-name { display: block; font-size: 26rpx; font-weight: 800; color: #20242b; } .mini-ticket-rule { display: block; margin-top: 8rpx; font-size: 22rpx; color: #8a8f99; line-height: 1.35; } .mini-ticket-value { margin-left: 18rpx; font-size: 34rpx; font-weight: 900; color: #ff2d20; flex-shrink: 0; } .pay-card { position: relative; padding: 26rpx 22rpx 24rpx; margin-top: 22rpx; } .pay-card-title { position: absolute; left: 50%; top: -16rpx; padding: 0 18rpx; background: #ffffff; transform: translateX(-50%); font-size: 26rpx; color: #999999; } .pay-methods { display: grid; grid-template-columns: repeat(2, 1fr); gap: 24rpx; } .pay-method { position: relative; height: 78rpx; border: 2rpx solid #edf0f4; border-radius: 8rpx; display: flex; align-items: center; justify-content: center; gap: 12rpx; color: #8a8f99; font-size: 26rpx; overflow: hidden; } .pay-method.active { border-color: #ffb3ad; color: #20242b; } .pay-icon { width: 38rpx; height: 38rpx; background: #d8d8d8; border-radius: 4rpx; } .pay-corner { position: absolute; right: -2rpx; top: -2rpx; width: 36rpx; height: 28rpx; display: flex; align-items: center; justify-content: center; background: #ff6b6b; color: #ffffff; font-size: 18rpx; border-bottom-left-radius: 12rpx; } .agreement-row { margin-top: 22rpx; padding-left: 6rpx; display: flex; align-items: center; font-size: 22rpx; color: #4f5662; } .radio { width: 28rpx; height: 28rpx; border-radius: 50%; border: 2rpx solid #c8ccd3; margin-right: 12rpx; display: flex; align-items: center; justify-content: center; color: #ffffff; font-size: 18rpx; } .radio.checked { background: #b42632; border-color: #b42632; } .agreement-link { color: #ff5a4f; } .amount-card { margin-top: 20rpx; padding: 4rpx 22rpx; } .amount-row { height: 72rpx; display: flex; align-items: center; justify-content: space-between; font-size: 26rpx; color: #7b7f88; border-bottom: 2rpx solid #f2f3f5; } .amount-row:last-child { border-bottom: none; } .bottom-space { height: 160rpx; } .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%); display: flex; align-items: center; justify-content: space-between; gap: 20rpx; } .footer-price { flex: 1; height: 88rpx; padding: 0 24rpx; border-radius: 10rpx; background: #ffffff; display: flex; align-items: center; box-shadow: 0 8rpx 18rpx rgba(31, 35, 41, 0.04); } .footer-label { font-size: 26rpx; color: #222222; } .footer-money { margin-left: 8rpx; font-size: 30rpx; font-weight: 900; color: #222222; } .pay-btn { width: 188rpx; height: 70rpx; border-radius: 10rpx; background: #70d5f5; display: flex; align-items: center; justify-content: center; color: #ffffff; font-size: 28rpx; font-weight: 700; } .panel-mask { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.28); display: flex; align-items: flex-end; z-index: 20; } .price-panel { width: 100%; padding: 18rpx 30rpx calc(env(safe-area-inset-bottom) + 30rpx); border-radius: 32rpx 32rpx 0 0; background: #ffffff; box-sizing: border-box; } .panel-handle { width: 72rpx; height: 8rpx; border-radius: 999rpx; background: #e5e7eb; margin: 0 auto 16rpx; } .panel-title-row { height: 64rpx; display: flex; align-items: center; justify-content: center; position: relative; } .panel-title { font-size: 32rpx; font-weight: 800; color: #20242b; } .panel-close { position: absolute; right: 4rpx; top: 0; font-size: 48rpx; line-height: 64rpx; color: #999999; } .panel-row { height: 72rpx; margin-top: 12rpx; padding: 0 22rpx; border-radius: 999rpx; border: 2rpx solid #ebedf0; display: flex; align-items: center; justify-content: space-between; font-size: 27rpx; color: #8a8f99; } .panel-total { height: 88rpx; margin-top: 26rpx; padding: 0 24rpx; border-radius: 999rpx; border: 2rpx solid #ebedf0; display: flex; align-items: center; justify-content: space-between; font-size: 28rpx; font-weight: 900; color: #20242b; } .confirm-pay-btn { margin-top: 22rpx; height: 86rpx; border-radius: 999rpx; background: linear-gradient(135deg, #ff3026 0%, #ff6b35 100%); box-shadow: 0 10rpx 24rpx rgba(255, 48, 38, 0.22); display: flex; align-items: center; justify-content: center; color: #ffffff; font-size: 30rpx; font-weight: 800; } .dark.app-container { background: #121212; } .dark .content { background: linear-gradient(180deg, #2a2118 0%, #151515 48%, #151515 100%); } .dark .title-text, .dark .shop-name, .dark .product-name, .dark .address-title, .dark .address-name, .dark .mini-ticket-name, .dark .footer-label, .dark .footer-money, .dark .panel-title, .dark .panel-total { color: #f5f5f5; } .dark .product-card, .dark .address-card, .dark .coupon-select-card, .dark .coupon-dropdown, .dark .pay-card, .dark .amount-card, .dark .footer-price, .dark .price-panel { background: rgba(34, 34, 36, 0.96); } .dark .pay-card-title { background: #222224; } </style> ''' my_coupon = r'''<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> <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" :show-scrollbar="false"> <view class="page-wrap"> <view class="summary-card"> <view> <text class="summary-title">优惠券包</text> <text class="summary-desc">已领取、使用中、已使用都会同步到这里</text> </view> <view class="summary-num-box"> <text class="summary-num">{{ ownedCoupons.length }}</text> <text class="summary-num-label">张</text> </view> </view> <view class="tabs-wrap"> <view v-for="item in tabs" :key="item.key" class="tab-item" :class="{ active: activeStatus === item.key }" @click="activeStatus = item.key"> <text>{{ item.name }}</text> </view> </view> <view v-if="filteredCoupons.length" class="coupon-list"> <view v-for="coupon in filteredCoupons" :key="coupon.id" class="wallet-card" @click="goDetail(coupon)"> <view class="ticket-left"> <view class="ticket-img"> <text>{{ coupon.logoText }}</text> </view> <view class="ticket-info"> <text class="ticket-title">{{ coupon.title }}</text> <text class="ticket-value">{{ coupon.valueText }}</text> <text class="ticket-date">{{ coupon.validText }}</text> </view> </view> <view class="ticket-status" :class="coupon.status"> <text>{{ getStatusText(coupon.status) }}</text> </view> <text class="ticket-rule">{{ coupon.conditionText }}</text> <view v-if="coupon.status === 'using' && orderMap[coupon.id]" class="order-card"> <view class="order-dot"></view> <view class="order-info"> <text class="order-name">{{ orderMap[coupon.id].productName }}</text> <text class="order-desc">支付金额 ¥{{ orderMap[coupon.id].total }} · {{ payName(orderMap[coupon.id].payMethod) }}</text> </view> <view class="order-action" @click.stop="finishUse(coupon)"> <text>确认收货</text> </view> </view> <view class="wallet-actions"> <view class="light-btn" @click.stop="goDetail(coupon)"> <text>查看详情</text> </view> <view v-if="coupon.status !== 'used'" class="red-btn" @click.stop="goUse(coupon)"> <text>{{ coupon.status === 'using' ? '继续购买' : '立即使用' }}</text> </view> </view> </view> </view> <view v-else class="empty-card"> <text class="empty-title">暂无优惠券</text> <text class="empty-desc">去商城领取几张优惠券吧</text> <view class="empty-btn" @click="goMall"> <text>去领取</text> </view> </view> <view class="bottom-space"></view> </view> </scroll-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 { getStatusBarHeight, getTitleBarHeight } from '@/utils/system.js' import { getCouponList } from './couponMock.js' const store = useMainStore() const { theme } = storeToRefs(store) const statusBarHeight = ref(getStatusBarHeight()) const headerBarHeight = ref(getTitleBarHeight()) const activeStatus = ref('unused') const ownedCoupons = ref([]) const orderMap = ref({}) const tabs = ref([ { key: 'unused', name: '未使用' }, { key: 'using', name: '使用中' }, { key: 'used', name: '已使用' } ]) const filteredCoupons = computed(() => { return ownedCoupons.value.filter((item) => item.status === activeStatus.value) }) function getStatusText(status) { const map = { unused: '未使用', using: '使用中', used: '已使用' } return map[status] || '未使用' } function payName(payMethod) { return payMethod === 'alipay' ? '支付宝支付' : '微信支付' } function loadCoupons() { const receivedMap = uni.getStorageSync('couponReceivedMap') || {} const useStatusMap = uni.getStorageSync('couponUseStatusMap') || {} orderMap.value = uni.getStorageSync('couponOrderMap') || {} ownedCoupons.value = getCouponList() .filter((item) => item.received || receivedMap[item.id] || useStatusMap[item.id]) .map((item) => ({ ...item, status: useStatusMap[item.id] || 'unused' })) } function goDetail(coupon) { uni.navigateTo({ url: `/pages/mall/Equitysharing/CouponDetail?id=${coupon.id}` }) } function goUse(coupon) { const product = coupon.products[0] uni.navigateTo({ url: `/pages/mall/Equitysharing/CouponOrder?couponId=${coupon.id}&productId=${product.id}` }) } function finishUse(coupon) { const useStatusMap = uni.getStorageSync('couponUseStatusMap') || {} useStatusMap[coupon.id] = 'used' uni.setStorageSync('couponUseStatusMap', useStatusMap) uni.showToast({ title: '已完成使用', icon: 'success' }) loadCoupons() activeStatus.value = 'used' } function goMall() { uni.navigateTo({ url: '/pages/mall/Equitysharing/CouponPage' }) } function openMenu() { uni.showActionSheet({ itemList: ['去领券中心', '清空使用状态'], success: ({ tapIndex }) => { if (tapIndex === 0) goMall() if (tapIndex === 1) { uni.removeStorageSync('couponUseStatusMap') uni.removeStorageSync('couponOrderMap') loadCoupons() uni.showToast({ title: '已清空', icon: 'none' }) } } }) } function navigateBack() { uni.navigateBack() } onLoad((option) => { if (option?.status) activeStatus.value = option.status loadCoupons() }) onShow(() => { loadCoupons() }) </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: 700; color: #111111; } .page-scroll { flex: 1; height: 0; } .page-wrap { width: 94%; margin: 0 auto; padding: 12rpx 0 0; box-sizing: border-box; } .summary-card { padding: 28rpx; border-radius: 30rpx; background: radial-gradient(circle at 84% 18%, rgba(255, 255, 255, 0.38), transparent 24%), linear-gradient(135deg, #24170d 0%, #d39a48 54%, #f0d89a 100%); display: flex; align-items: center; justify-content: space-between; box-shadow: 0 18rpx 38rpx rgba(180, 122, 42, 0.18); } .summary-title { display: block; font-size: 36rpx; font-weight: 900; color: #fff8e8; } .summary-desc { display: block; margin-top: 10rpx; font-size: 24rpx; color: rgba(255, 248, 232, 0.78); } .summary-num-box { width: 128rpx; height: 128rpx; border-radius: 50%; background: rgba(255, 255, 255, 0.2); border: 2rpx solid rgba(255, 255, 255, 0.38); display: flex; align-items: baseline; justify-content: center; padding-top: 34rpx; box-sizing: border-box; } .summary-num { font-size: 44rpx; font-weight: 900; color: #ffffff; line-height: 1; } .summary-num-label { margin-left: 4rpx; font-size: 22rpx; color: rgba(255, 255, 255, 0.86); } .tabs-wrap { margin: 20rpx 0 4rpx; display: flex; align-items: center; gap: 34rpx; padding-left: 4rpx; } .tab-item { position: relative; padding: 12rpx 0 18rpx; font-size: 28rpx; color: #6b7280; } .tab-item.active { color: #ff2d20; font-weight: 700; } .tab-item.active::after { content: ''; position: absolute; left: 50%; bottom: 4rpx; width: 36rpx; height: 6rpx; border-radius: 999rpx; background: #ff2d20; transform: translateX(-50%); } .wallet-card { position: relative; margin-top: 18rpx; padding: 22rpx; border-radius: 28rpx; background: #ffffff; box-shadow: 0 10rpx 26rpx rgba(31, 35, 41, 0.06); overflow: hidden; } .wallet-card::before, .wallet-card::after { content: ''; position: absolute; top: 104rpx; width: 34rpx; height: 34rpx; border-radius: 50%; background: #f7f8fc; z-index: 2; } .wallet-card::before { left: -17rpx; } .wallet-card::after { right: -17rpx; } .ticket-left { padding-bottom: 20rpx; border-bottom: 2rpx dashed #eee2d4; display: flex; align-items: center; gap: 20rpx; } .ticket-img { width: 132rpx; height: 132rpx; border-radius: 18rpx; background: linear-gradient(135deg, #999999 0%, #c2c2c2 100%); display: flex; align-items: center; justify-content: center; flex-shrink: 0; } .ticket-img text { color: #ffffff; font-size: 42rpx; font-weight: 900; } .ticket-info { flex: 1; min-width: 0; display: flex; flex-direction: column; } .ticket-title { font-size: 28rpx; font-weight: 800; color: #20242b; } .ticket-value { margin-top: 10rpx; font-size: 40rpx; font-weight: 900; color: #ff2d20; line-height: 1; } .ticket-date { margin-top: 16rpx; font-size: 22rpx; color: #c1c5cc; } .ticket-status { position: absolute; right: 0; top: 64rpx; padding: 12rpx 8rpx; writing-mode: vertical-rl; font-size: 24rpx; letter-spacing: 4rpx; color: #20242b; } .ticket-status.used { color: #b0b4bd; } .ticket-status.using { color: #20242b; font-weight: 800; } .ticket-rule { display: block; margin-top: 18rpx; text-align: center; font-size: 23rpx; color: #7b7f88; } .order-card { margin-top: 20rpx; padding: 18rpx; border-radius: 18rpx; background: #f7f8fc; display: flex; align-items: center; gap: 14rpx; } .order-dot { width: 18rpx; height: 18rpx; border-radius: 50%; background: #b42632; flex-shrink: 0; } .order-info { flex: 1; min-width: 0; display: flex; flex-direction: column; } .order-name { font-size: 25rpx; font-weight: 800; color: #20242b; } .order-desc { margin-top: 8rpx; font-size: 22rpx; color: #8a8f99; } .order-action { height: 52rpx; padding: 0 18rpx; border-radius: 999rpx; background: #999999; color: #ffffff; font-size: 22rpx; display: flex; align-items: center; justify-content: center; flex-shrink: 0; } .wallet-actions { margin-top: 18rpx; display: flex; align-items: center; justify-content: flex-end; gap: 14rpx; } .light-btn, .red-btn { height: 58rpx; padding: 0 24rpx; border-radius: 999rpx; display: flex; align-items: center; justify-content: center; font-size: 24rpx; } .light-btn { background: #f5f6f8; color: #606775; } .red-btn { background: linear-gradient(135deg, #ff3026 0%, #ff6b35 100%); color: #ffffff; box-shadow: 0 10rpx 18rpx rgba(255, 48, 38, 0.16); } .empty-card { margin-top: 40rpx; padding: 68rpx 30rpx; border-radius: 28rpx; background: rgba(255, 255, 255, 0.9); display: flex; flex-direction: column; align-items: center; box-shadow: 0 10rpx 24rpx rgba(31, 35, 41, 0.04); } .empty-title { font-size: 30rpx; font-weight: 800; color: #20242b; } .empty-desc { margin-top: 12rpx; font-size: 24rpx; color: #8a8f99; } .empty-btn { margin-top: 26rpx; height: 66rpx; padding: 0 34rpx; border-radius: 999rpx; background: linear-gradient(135deg, #ff3026 0%, #ff6b35 100%); color: #ffffff; display: flex; align-items: center; justify-content: center; font-size: 26rpx; font-weight: 800; } .bottom-space { height: 80rpx; } .dark.app-container { background: #121212; } .dark .content { background: linear-gradient(180deg, #2a2118 0%, #151515 48%, #151515 100%); } .dark .title-text, .dark .ticket-title, .dark .ticket-status, .dark .order-name, .dark .empty-title { color: #f5f5f5; } .dark .wallet-card, .dark .empty-card { background: rgba(34, 34, 36, 0.94); } .dark .order-card, .dark .light-btn { background: rgba(255, 255, 255, 0.08); } </style> ''' pages_json = r'''{ "pages": [ { "path": "pages/mall/Equitysharing/CouponPage", "style": { "navigationStyle": "custom" } }, { "path": "pages/mall/Equitysharing/CouponDetail", "style": { "navigationStyle": "custom" } }, { "path": "pages/mall/Equitysharing/CouponOrder", "style": { "navigationStyle": "custom" } }, { "path": "pages/mall/Equitysharing/MyCoupon", "style": { "navigationStyle": "custom" } } ] } ''' readme = r'''# 优惠券商城页面包 已按照你给的截图和基础页面结构重写,页面根目录为: `pages/mall/Equitysharing/` 包含 4 个完整页面和 1 个模拟数据文件: 1. `CouponPage.vue`:优惠券列表 / 搜索 / 分类 / 领取 / 去使用 2. `CouponDetail.vue`:优惠券详情 / 可用商品 / 商品网格 / 去下单 3. `CouponOrder.vue`:确认订单 / 选择优惠券 / 支付方式 / 支付明细弹层 4. `MyCoupon.vue`:我的优惠券 / 未使用 / 使用中 / 已使用 5. `couponMock.js`:演示数据、价格计算、格式化工具 ## 使用方式 把 `pages/mall/Equitysharing` 文件夹复制到项目对应目录,然后把 `pages-json-snippet.json` 里的页面配置合并到项目根目录 `pages.json`。 当前代码沿用你项目里的: - `@/utils/system.js` - `@/store/index.js` - `@/assets/styles/global.scss` - `uni-icons` 演示逻辑使用 `uni.setStorageSync` 保存领取、支付、使用中状态。后续接后端时,把 `couponMock.js` 和页面里的 `uni.getStorageSync / uni.setStorageSync` 替换成接口即可。 ''' files = { page_dir/'couponMock.js': coupon_mock, page_dir/'CouponPage.vue': coupon_page, page_dir/'CouponDetail.vue': coupon_detail, page_dir/'CouponOrder.vue': coupon_order, page_dir/'MyCoupon.vue': my_coupon, base/'pages-json-snippet.json': pages_json, base/'README.md': readme, } for path, content in files.items(): path.parent.mkdir(parents=True, exist_ok=True) path.write_text(content, encoding='utf-8') zip_path = Path('/mnt/data/coupon_pages_complete.zip') if zip_path.exists(): zip_path.unlink() with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as z: for path in base.rglob('*'): if path.is_file(): z.write(path, path.relative_to(base.parent)) zip_path, list(base.rglob('*'))[:10] (PosixPath('/mnt/data/coupon_pages_complete.zip'), [PosixPath('/mnt/data/coupon-pages/pages'), PosixPath('/mnt/data/coupon-pages/pages-json-snippet.json'), PosixPath('/mnt/data/coupon-pages/README.md'), PosixPath('/mnt/data/coupon-pages/pages/mall'), PosixPath('/mnt/data/coupon-pages/pages/mall/Equitysharing'), PosixPath('/mnt/data/coupon-pages/pages/mall/Equitysharing/couponMock.js'), PosixPath('/mnt/data/coupon-pages/pages/mall/Equitysharing/CouponPage.vue'), PosixPath('/mnt/data/coupon-pages/pages/mall/Equitysharing/CouponDetail.vue'), PosixPath('/mnt/data/coupon-pages/pages/mall/Equitysharing/CouponOrder.vue'), PosixPath('/mnt/data/coupon-pages/pages/mall/Equitysharing/MyCoupon.vue')])

사고

Validating path and adjusting layout issues

I’m thinking about validating the file structure and checking for issues like code syntax or paths with Python. I also need to ensure the @click="selectedPay = item.key" syntax works well with Vue's script setup. For the CSS, it’s safer to use flex-wrap instead of grid to ensure compatibility, especially across platforms like uni-app and WeChat.

Thought for 4m 14s

답변

已按截图重写并打包好了,包含 4 个完整页面:

  • CouponPage.vue:优惠券商城首页、搜索、分类、领取、去使用
  • CouponDetail.vue:优惠券详情、可用商品列表
  • CouponOrder.vue:确认订单、优惠券选择、支付方式、支付明细弹层
  • MyCoupon.vue:我的优惠券、未使用 / 使用中 / 已使用
  • couponMock.js:模拟优惠券、商品、价格计算数据

下载 ZIP: coupon_pages_complete.zip

解压后把里面的 pages/mall/Equitysharing 放到你的项目对应目录,再把 pages-json-snippet.json 里的配置合并到项目 pages.json 即可。

이 Q&A 공유하기