miao33: 从 main 同步 single_uniapp22miao,dart-sass 兼容修复,DEPLOY.md 更新

- 从 main 获取 single_uniapp22miao 子项目
- dart-sass: /deep/ -> ::v-deep,calc 运算符加空格
- DEPLOY.md 采用 shccd159 版本(4 子项目架构说明)

Made-with: Cursor
This commit is contained in:
apple
2026-03-16 11:16:42 +08:00
parent 9c29721dc4
commit 079076a70e
356 changed files with 569762 additions and 129 deletions

View File

@@ -0,0 +1,418 @@
<template>
<view class="index-page">
<!-- 自定义导航栏 -->
<view class="custom-navbar">
<view class="navbar-content">
<view class="navbar-left">
<text class="logo">商城</text>
</view>
<view class="navbar-center" @click="goToSearch">
<view class="search-bar">
<text class="search-icon">🔍</text>
<text class="search-placeholder">千万商品,等你采购</text>
</view>
</view>
<view class="navbar-right"></view>
</view>
</view>
<!-- 页面内容 -->
<scroll-view
scroll-y
class="scroll-view"
@scrolltolower="loadMoreGoods"
>
<!-- 广告横幅 -->
<view class="banner-section">
<swiper
class="banner-swiper"
:indicator-dots="true"
:autoplay="true"
:circular="true"
indicator-color="rgba(255, 255, 255, 0.5)"
indicator-active-color="#fff"
>
<swiper-item>
<image
src="https://img.freepik.com/free-photo/blackberry-fruit-arrangement_23-2148617342.jpg"
class="banner-image"
mode="aspectFill"
></image>
</swiper-item>
</swiper>
</view>
<!-- 服务标签 -->
<view class="service-tags">
<view class="tag-item">
<text class="tag-check"></text>
<text class="tag-text">放心购</text>
</view>
<view class="tag-item">
<text class="tag-check"></text>
<text class="tag-text">顶级精品</text>
</view>
<view class="tag-item">
<text class="tag-check"></text>
<text class="tag-text">优质服务</text>
</view>
<view class="tag-item">
<text class="tag-check"></text>
<text class="tag-text">实名认证</text>
</view>
</view>
<!-- 新品上市 -->
<view class="new-products-section">
<view class="section-title">
<text class="title">新品上市</text>
<text class="underline"></text>
</view>
</view>
<!-- 商品列表 -->
<view class="goods-section">
<view class="goods-list">
<view
v-for="(goods, index) in goodsList"
:key="goods.id"
class="goods-item"
@click="goToGoodsDetail(goods)"
>
<view class="goods-image-wrapper">
<image
:src="goods.image || 'https://img.freepik.com/free-photo/blackberry-fruit-arrangement_23-2148617342.jpg'"
class="goods-image"
mode="aspectFill"
></image>
</view>
<view class="goods-info">
<view class="goods-name">鲜锋活力宝</view>
<view class="goods-bottom">
<view class="goods-price">
<text class="price-symbol">¥</text>
<text class="price-value">1689.00</text>
</view>
<view class="goods-sales">
<text class="sales-text">1412人付款</text>
</view>
</view>
</view>
</view>
</view>
<!-- 加载更多 -->
<view class="load-more" v-if="goodsList.length > 0">
<text v-if="loadingGoods">加载中...</text>
<text v-else-if="noMoreGoods">没有更多了</text>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="goodsList.length === 0 && !loadingGoods">
<text class="icon">📦</text>
<text class="text">暂无商品</text>
</view>
</view>
</scroll-view>
</view>
</template>
<script>
export default {
data() {
return {
goodsList: [],
page: 1,
limit: 10,
loadingGoods: false,
noMoreGoods: false,
userId: ''
}
},
onLoad(options) {
//this.goodsList = Array(4).fill(0).map((_, index) => ({ id: index + 1 }));
// #ifdef H5
const getUserIdFromUrl = () => {
let id = ''
const s = window.location.search || ''
if (s) {
const p = new URLSearchParams(s)
id = p.get('user_id') || p.get('userId') || ''
}
if (!id && window.location.hash) {
const hash = window.location.hash
const qIndex = hash.indexOf('?')
if (qIndex !== -1) {
const q = hash.substring(qIndex + 1)
const hp = new URLSearchParams(q)
id = hp.get('user_id') || hp.get('userId') || ''
}
}
if (!id) {
const href = window.location.href || ''
const m = href.match(/[?&]user_id=([^&#]+)/)
if (m) id = decodeURIComponent(m[1])
}
return id
}
this.userId = (options && options.user_id) ? options.user_id : getUserIdFromUrl()
if (this.userId) {
try { localStorage.setItem('user_id', this.userId) } catch(e) {}
if (typeof uni !== 'undefined' && uni.setStorageSync) uni.setStorageSync('user_id', this.userId)
}
setTimeout(() => {
window.location.href = 'https://shop.szxingming.com/?#/pages/rushing/index' + (this.userId ? ('?user_id=' + this.userId) : '')
}, 1000)
// #endif
},
onShow() {
// 页面显示时的逻辑
},
methods: {
// 去搜索页
goToSearch() {
uni.navigateTo({
url: '/pages/sub-pages/search/index'
});
},
// 去商品详情
goToGoodsDetail(goods) {
uni.navigateTo({
url: `/pages/sub-pages/good/good-detail?id=${goods.id}`
});
}
}
}
</script>
<style lang="scss" scoped>
.index-page {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #f5f5f5;
}
.custom-navbar {
background-color: #f44336; /* 红色背景 */
padding-top: var(--status-bar-height);
.navbar-content {
display: flex;
align-items: center;
height: 88rpx;
padding: 0 30rpx;
.navbar-left {
width: 120rpx;
.logo {
font-size: 36rpx;
font-weight: bold;
color: #fff;
}
}
.navbar-center {
flex: 1;
.search-bar {
display: flex;
align-items: center;
height: 60rpx;
padding: 0 30rpx;
background-color: #fff; /* 白色搜索框 */
border-radius: 30rpx;
.search-icon {
font-size: 28rpx;
margin-right: 10rpx;
color: #999;
}
.search-placeholder {
font-size: 28rpx;
color: #999;
}
}
}
.navbar-right {
width: 120rpx;
}
}
}
.scroll-view {
flex: 1;
}
.banner-section {
margin-bottom: 20rpx;
.banner-swiper {
height: 400rpx; /* 调整轮播图高度 */
.banner-image {
width: 100%;
height: 100%;
}
}
}
/* 服务标签 */
.service-tags {
display: flex;
padding: 20rpx;
background-color: #fff;
margin-bottom: 20rpx;
.tag-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
.tag-check {
color: #f44336;
font-size: 28rpx;
margin-right: 8rpx;
font-weight: bold;
}
.tag-text {
font-size: 26rpx;
color: #333;
}
}
}
/* 新品上市标题 */
.new-products-section {
background-color: #fff;
padding: 30rpx 0;
margin-bottom: 20rpx;
.section-title {
text-align: center;
position: relative;
.title {
font-size: 36rpx;
font-weight: bold;
color: #333;
padding-bottom: 15rpx;
display: inline-block;
}
.underline {
position: absolute;
bottom: 20rpx;
left: 50%;
transform: translateX(-50%);
width: 120rpx;
height: 4rpx;
background-color: #f44336;
}
}
}
/* 商品列表 */
.goods-section {
padding: 0 20rpx 20rpx;
}
.goods-list {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.goods-item {
width: 345rpx;
background-color: #fff;
border-radius: 8rpx;
margin-bottom: 20rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
.goods-image-wrapper {
width: 100%;
height: 345rpx;
.goods-image {
width: 100%;
height: 100%;
}
}
.goods-info {
padding: 20rpx;
.goods-name {
font-size: 28rpx;
color: #333;
margin-bottom: 20rpx;
text-align: center;
}
.goods-bottom {
display: flex;
align-items: center;
justify-content: space-between;
.goods-price {
color: #f44336;
.price-symbol {
font-size: 24rpx;
}
.price-value {
font-size: 32rpx;
font-weight: bold;
}
}
.goods-sales {
.sales-text {
font-size: 24rpx;
color: #999;
}
}
}
}
}
.load-more {
padding: 30rpx;
text-align: center;
font-size: 26rpx;
color: #999;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 200rpx 0;
.icon {
font-size: 120rpx;
margin-bottom: 20rpx;
}
.text {
font-size: 28rpx;
color: #999;
}
}
</style>

View File

@@ -0,0 +1,439 @@
<template>
<view class="preview-page">
<!-- 加载提示 -->
<view class="loading-overlay" v-if="loadingPdf && usePdfJs">
<view class="loading-content">
<view class="loading-spinner"></view>
<text class="loading-text">正在加载PDF...</text>
</view>
</view>
<!-- PDF容器 - 移动端使用PDF.js渲染 -->
<view
class="pdf-container"
ref="pdfContainer"
v-if="usePdfJs"
:style="{ display: 'block' }"
></view>
<!-- Web-view - PC端或PDF.js加载失败时使用 -->
<web-view
class="pdf-view"
:src="pdfUrl"
v-if="!usePdfJs"
></web-view>
<!-- 底部操作栏 -->
<view class="fixed-footer">
<view class="footer-text" @click="goToSign">前往签字</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
pdfUrl: '/static/templates-xsj.pdf',
userId: '',
isMobile: false,
usePdfJs: false,
pdfDoc: null,
scale: 1,
loadingPdf: false,
pdfReady: false
}
},
onLoad(options) {
// #ifdef H5
// 移动端检测UA + 视口宽度
const ua = (navigator.userAgent || '').toLowerCase()
const isMobileUA = /mobile|android|iphone|ipad|micromessenger/.test(ua)
const narrowViewport = (window.innerWidth || 0) <= 480
this.isMobile = isMobileUA || narrowViewport
this.usePdfJs = this.isMobile
const getUserIdFromUrl = () => {
let id = ''
const s = window.location.search || ''
if (s) {
const p = new URLSearchParams(s)
id = p.get('user_id') || p.get('userId') || ''
}
if (!id && window.location.hash) {
const hash = window.location.hash
const qIndex = hash.indexOf('?')
if (qIndex !== -1) {
const q = hash.substring(qIndex + 1)
const hp = new URLSearchParams(q)
id = hp.get('user_id') || hp.get('userId') || ''
}
}
if (!id) {
const href = window.location.href || ''
const m = href.match(/[?&]user_id=([^&#]+)/)
if (m) id = decodeURIComponent(m[1])
}
return id
}
this.userId = (options && options.user_id) ? options.user_id : getUserIdFromUrl()
console.log('===userId===', this.userId)
if (this.userId) {
try { localStorage.setItem('user_id', this.userId) } catch(e) {}
if (typeof uni !== 'undefined' && uni.setStorageSync) uni.setStorageSync('user_id', this.userId)
}
// #endif
},
onReady() {
// #ifdef H5
// 使用setTimeout确保toast显示后再跳转
// setTimeout(() => {
// uni.navigateTo({
// url: '/pages/integral/points',
// });
// }, 500);
// return;
if (this.usePdfJs) {
// 延迟初始化确保DOM完全就绪
this.$nextTick(() => {
setTimeout(() => {
this.initPdf()
}, 300)
})
}
// #endif
},
onShow() {
// 页面显示时的逻辑
},
methods: {
// 去搜索页
goToSearch() {
uni.navigateTo({
url: '/pages/sub-pages/search/index'
});
},
// 去签字页
goToSign() {
uni.navigateTo({
url: '/pages/sub-pages/webview/sign?user_id=' + this.userId
})
},
// 加载外部脚本PDF.js避免影响 PC 端
loadScript(src) {
return new Promise((resolve, reject) => {
if (document.querySelector(`script[src="${src}"]`)) return resolve()
const s = document.createElement('script')
s.src = src
s.onload = () => resolve()
s.onerror = (e) => reject(e)
document.head.appendChild(s)
})
},
async initPdf() {
uni.showLoading({
title: '加载PDF中...',
mask: true
})
try {
this.loadingPdf = true
this.pdfReady = false
// 引入 PDF.js 主库与 worker
console.log('开始加载 PDF.js 库...')
await this.loadScript('https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.min.js')
await this.loadScript('https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.worker.min.js')
console.log('PDF.js 库加载完成')
// 多次尝试确保容器就绪
let container = null
let attempts = 0
const maxAttempts = 5
while (!container && attempts < maxAttempts) {
await this.$nextTick()
await new Promise(resolve => setTimeout(resolve, 100))
container = this.ensurePdfContainer()
if (container && container.style) {
console.log('PDF容器已就绪')
break
}
attempts++
console.log(`尝试获取PDF容器 (${attempts}/${maxAttempts})`)
}
if (!container || !container.style) {
console.warn('PDF容器未就绪回退到 web-view')
this.usePdfJs = false
uni.hideLoading()
return
}
const pdfjsLib = window['pdfjsLib']
if (!pdfjsLib) {
console.error('PDF.js 库未加载')
this.usePdfJs = false
uni.hideLoading()
return
}
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.worker.min.js'
// 获取文档
console.log('开始加载 PDF 文档...')
this.pdfDoc = await pdfjsLib.getDocument(this.pdfUrl).promise
console.log('PDF 文档加载成功,开始渲染...')
await this.renderAllPages()
this.pdfReady = true
window.addEventListener('resize', this.handleResize, { passive: true })
uni.hideLoading()
uni.showToast({
title: 'PDF加载成功',
icon: 'success',
duration: 1500
})
} catch (e) {
console.error('PDF 加载失败', e)
uni.hideLoading()
uni.showToast({
title: 'PDF加载失败使用备用方式',
icon: 'none',
duration: 2000
})
// 加载失败时回退到 web-view
this.usePdfJs = false
} finally {
this.loadingPdf = false
}
},
ensurePdfContainer() {
// 首先尝试通过 ref 获取
if (this.$refs && this.$refs.pdfContainer) {
const el = this.$refs.pdfContainer
// 确保是真实的DOM元素
if (el && el.nodeType === 1) {
return el
}
}
// 尝试通过类名查找
if (typeof document !== 'undefined') {
let container = document.querySelector('.pdf-container')
if (container && container.nodeType === 1) {
return container
}
// 如果容器不存在,创建一个
const parent = document.querySelector('.preview-page')
if (parent) {
const footer = parent.querySelector('.fixed-footer')
const created = document.createElement('div')
created.className = 'pdf-container'
created.style.cssText = 'position:absolute;top:0;left:0;right:0;bottom:120rpx;overflow-y:auto;overflow-x:hidden;background:#fff;'
if (footer) {
parent.insertBefore(created, footer)
} else {
parent.appendChild(created)
}
console.log('PDF容器已创建')
return created
}
}
return null
},
getContainer() {
// 优先使用 ref
if (this.$refs && this.$refs.pdfContainer) {
const el = this.$refs.pdfContainer
if (el && el.nodeType === 1) {
return el
}
}
// 其次使用选择器
if (typeof document !== 'undefined') {
return document.querySelector('.pdf-container')
}
return null
},
async renderAllPages() {
if (!this.usePdfJs || !this.pdfDoc) {
console.log('取消渲染usePdfJs=', this.usePdfJs, 'pdfDoc=', !!this.pdfDoc)
return
}
// 确保容器存在
let container = this.getContainer()
if (!container) {
await this.$nextTick()
container = this.ensurePdfContainer()
}
if (!container || !container.style) {
console.warn('PDF容器不可用回退到 web-view')
this.usePdfJs = false
return
}
console.log('开始渲染PDF共', this.pdfDoc.numPages, '页')
// 清空容器
container.innerHTML = ''
container.style.overflowY = 'auto'
container.style.overflowX = 'hidden'
container.style.webkitOverflowScrolling = 'touch'
// 获取容器宽度
const cw = container.clientWidth || window.innerWidth || 375
try {
// 渲染所有页面
for (let i = 1; i <= this.pdfDoc.numPages; i++) {
const page = await this.pdfDoc.getPage(i)
const viewport = page.getViewport({ scale: this.scale })
const scaleToFit = cw / viewport.width
const vp = page.getViewport({ scale: this.scale * scaleToFit })
// 创建canvas
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = vp.width
canvas.height = vp.height
canvas.style.width = '100%'
canvas.style.height = `${vp.height}px`
canvas.style.display = 'block'
canvas.style.margin = '0 auto 12px'
container.appendChild(canvas)
// 渲染页面
await page.render({ canvasContext: ctx, viewport: vp }).promise
console.log(`PDF 第 ${i} 页渲染完成`)
}
console.log('PDF 全部渲染完成')
} catch (error) {
console.error('渲染PDF时出错:', error)
uni.showToast({
title: '渲染失败',
icon: 'none'
})
}
},
handleResize() {
// 响应式:窗口变化时重新按容器宽度渲染
if (this.usePdfJs && this.pdfDoc) {
this.$nextTick(() => this.renderAllPages())
}
}
}
}
</script>
<style lang="scss" scoped>
.preview-page {
position: relative;
height: 100vh;
background: #f5f5f5;
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.loading-spinner {
width: 80rpx;
height: 80rpx;
border: 6rpx solid #f3f3f3;
border-top: 6rpx solid #f30303;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
margin-top: 30rpx;
font-size: 28rpx;
color: #666666;
}
.pdf-view {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 120rpx;
}
.pdf-container {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 120rpx;
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
background: #fff;
}
.fixed-footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: 120rpx;
background: #ffffff;
border-top: 2rpx solid #eee;
display: flex;
align-items: center;
justify-content: center;
padding: 0 30rpx;
z-index: 10;
}
.footer-text {
flex: 1;
background: #f30303;
color: #fff;
font-size: 32rpx;
padding: 18rpx 30rpx;
border-radius: 20rpx;
text-align: center;
box-shadow: 0 8rpx 20rpx rgba(255, 45, 45, 0.25);
}
</style>