forked from gxwebsoft/mp-10550
refactor(user/gift): 修复 CSS 兼容性问题并优化礼品卡功能
- 移除了不兼容的 CSS 类名,解决了 WXSS 编译错误 - 优化了礼品卡详细页面,添加了二维码弹窗功能 - 简化了礼品卡统计组件,提高了页面加载速度 - 修复了 SimpleQRCodeModal组件中的样式问题 - 优化了验证页面中的布局结构
This commit is contained in:
@@ -1,10 +1,8 @@
|
||||
import React, { useState } from 'react'
|
||||
import React from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { Button, Tag, Rate } from '@nutui/nutui-react-taro'
|
||||
import { Gift, Clock, Location, Phone, ShoppingCart, Tips, QrCode } from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { Tag, Rate } from '@nutui/nutui-react-taro'
|
||||
import { Gift, Clock, Location } from '@nutui/icons-react-taro'
|
||||
import dayjs from 'dayjs'
|
||||
import GiftCardQRCode from './GiftCardQRCode'
|
||||
import './GiftCard.scss'
|
||||
|
||||
export interface GiftCardProps {
|
||||
@@ -91,13 +89,9 @@ export interface GiftCardProps {
|
||||
}
|
||||
|
||||
const GiftCard: React.FC<GiftCardProps> = ({
|
||||
id,
|
||||
name,
|
||||
goodsName,
|
||||
description,
|
||||
code,
|
||||
goodsImage,
|
||||
goodsImages,
|
||||
faceValue,
|
||||
originalPrice,
|
||||
type = 10,
|
||||
@@ -105,23 +99,16 @@ const GiftCard: React.FC<GiftCardProps> = ({
|
||||
expireTime,
|
||||
useTime,
|
||||
useLocation,
|
||||
contactInfo,
|
||||
goodsInfo,
|
||||
promotionInfo,
|
||||
showCode = false,
|
||||
showUseBtn = false,
|
||||
showDetailBtn = true,
|
||||
showGoodsDetail = true,
|
||||
theme = 'gold',
|
||||
onUse,
|
||||
onDetail,
|
||||
onClick
|
||||
}) => {
|
||||
const [currentImageIndex, setCurrentImageIndex] = useState(0)
|
||||
const [showQRCode, setShowQRCode] = useState(false)
|
||||
|
||||
// 获取显示名称,优先使用商品名称
|
||||
const displayName = goodsName || name
|
||||
// const displayName = goodsName || name
|
||||
// 获取礼品卡类型文本
|
||||
const getTypeText = () => {
|
||||
switch (type) {
|
||||
@@ -197,29 +184,7 @@ const GiftCard: React.FC<GiftCardProps> = ({
|
||||
return code.replace(/(.{4})/g, '$1 ').trim()
|
||||
}
|
||||
|
||||
// 获取商品图片列表
|
||||
const getImageList = () => {
|
||||
if (goodsImages && goodsImages.length > 0) {
|
||||
return goodsImages
|
||||
}
|
||||
if (goodsImage) {
|
||||
return [goodsImage]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
// 计算折扣百分比
|
||||
const getDiscountPercent = () => {
|
||||
if (!originalPrice || !faceValue) return null
|
||||
const original = parseFloat(originalPrice)
|
||||
const current = parseFloat(faceValue)
|
||||
if (original <= current) return null
|
||||
return Math.round(((original - current) / original) * 100)
|
||||
}
|
||||
|
||||
const statusInfo = getStatusInfo()
|
||||
const imageList = getImageList()
|
||||
const discountPercent = getDiscountPercent()
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -272,9 +237,6 @@ const GiftCard: React.FC<GiftCardProps> = ({
|
||||
<View className="rating-info">
|
||||
<Rate
|
||||
value={goodsInfo.rating}
|
||||
readonly
|
||||
size="12"
|
||||
spacing="2"
|
||||
/>
|
||||
<Text className="rating-text">{goodsInfo.rating}</Text>
|
||||
{goodsInfo.reviewCount && (
|
||||
@@ -330,12 +292,11 @@ const GiftCard: React.FC<GiftCardProps> = ({
|
||||
{goodsInfo.applicableStores && goodsInfo.applicableStores.length > 0 && (
|
||||
<View className="instruction-section">
|
||||
<View className="section-header">
|
||||
<ShoppingCart size="14" className="section-icon" />
|
||||
<Text className="section-title">适用门店</Text>
|
||||
</View>
|
||||
<View className="store-list">
|
||||
{goodsInfo.applicableStores.map((store, index) => (
|
||||
<Tag key={index} size="small" plain className="store-tag">
|
||||
<Tag key={index} plain className="store-tag">
|
||||
{store}
|
||||
</Tag>
|
||||
))}
|
||||
@@ -371,33 +332,28 @@ const GiftCard: React.FC<GiftCardProps> = ({
|
||||
</View>
|
||||
|
||||
{/* 卡片底部操作 */}
|
||||
<View className="gift-card-footer">
|
||||
<View className="footer-info">
|
||||
{contactInfo && (
|
||||
<View className="contact-info">
|
||||
<Phone size="12" className="text-gray-400" />
|
||||
<Text className="contact-text">{contactInfo}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{/*<View className="gift-card-footer">*/}
|
||||
{/* <View className="footer-info">*/}
|
||||
{/* {contactInfo && (*/}
|
||||
{/* <View className="contact-info">*/}
|
||||
{/* <Phone size="12" className="text-gray-400" />*/}
|
||||
{/* <Text className="contact-text">{contactInfo}</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/* )}*/}
|
||||
{/* </View>*/}
|
||||
|
||||
<View className="footer-actions">
|
||||
{showUseBtn && status === 0 && (
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<QrCode />}
|
||||
className={`use-btn ${getThemeClass()}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setShowQRCode(true)
|
||||
}}
|
||||
>
|
||||
立即使用
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
{/* <View className="footer-actions">*/}
|
||||
{/* {showUseBtn && status === 0 && (*/}
|
||||
{/* <Button*/}
|
||||
{/* size="small"*/}
|
||||
{/* type="primary"*/}
|
||||
{/* className={`use-btn ${getThemeClass()}`}*/}
|
||||
{/* >*/}
|
||||
{/* 立即使用*/}
|
||||
{/* </Button>*/}
|
||||
{/* )}*/}
|
||||
{/* </View>*/}
|
||||
{/*</View>*/}
|
||||
|
||||
{/* 状态遮罩 */}
|
||||
{status !== 0 && (
|
||||
@@ -408,22 +364,6 @@ const GiftCard: React.FC<GiftCardProps> = ({
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 二维码核销弹窗 */}
|
||||
<GiftCardQRCode
|
||||
visible={showQRCode}
|
||||
onClose={() => setShowQRCode(false)}
|
||||
giftCard={{
|
||||
id,
|
||||
name,
|
||||
goodsName,
|
||||
code,
|
||||
faceValue,
|
||||
type,
|
||||
status,
|
||||
expireTime,
|
||||
contactInfo
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
284
src/components/QRCodeGenerator.tsx
Normal file
284
src/components/QRCodeGenerator.tsx
Normal file
@@ -0,0 +1,284 @@
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import { View, Text, Canvas } from '@tarojs/components'
|
||||
import { Button, Popup } from '@nutui/nutui-react-taro'
|
||||
import { Close, Copy, Download } from '@nutui/icons-react-taro'
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
export interface QRCodeGeneratorProps {
|
||||
/** 是否显示弹窗 */
|
||||
visible: boolean
|
||||
/** 关闭弹窗回调 */
|
||||
onClose: () => void
|
||||
/** 二维码内容 */
|
||||
text: string
|
||||
/** 二维码尺寸 */
|
||||
size?: number
|
||||
/** 礼品卡名称 */
|
||||
title?: string
|
||||
/** 礼品卡面值 */
|
||||
subtitle?: string
|
||||
}
|
||||
|
||||
const QRCodeGenerator: React.FC<QRCodeGeneratorProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
text,
|
||||
size = 200,
|
||||
title,
|
||||
subtitle
|
||||
}) => {
|
||||
const [qrDataURL, setQrDataURL] = useState<string>('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const canvasRef = useRef<string>('qrcode-canvas')
|
||||
|
||||
// 使用Canvas API生成二维码
|
||||
const generateQRCode = async () => {
|
||||
if (!text || !visible) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
// 方案1: 使用在线API生成二维码
|
||||
const qrApiUrl = `https://api.qrserver.com/v1/create-qr-code/?size=${size}x${size}&data=${encodeURIComponent(text)}`
|
||||
setQrDataURL(qrApiUrl)
|
||||
|
||||
// 方案2: 如果需要离线生成,可以使用Canvas绘制
|
||||
// await drawQRCodeOnCanvas()
|
||||
|
||||
} catch (error) {
|
||||
console.error('生成二维码失败:', error)
|
||||
Taro.showToast({
|
||||
title: '生成二维码失败',
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 使用Canvas绘制二维码(简化版本)
|
||||
const drawQRCodeOnCanvas = async () => {
|
||||
const ctx = Taro.createCanvasContext(canvasRef.current)
|
||||
|
||||
// 清空画布
|
||||
ctx.clearRect(0, 0, size, size)
|
||||
|
||||
// 绘制白色背景
|
||||
ctx.setFillStyle('#ffffff')
|
||||
ctx.fillRect(0, 0, size, size)
|
||||
|
||||
// 绘制黑色边框
|
||||
ctx.setStrokeStyle('#000000')
|
||||
ctx.setLineWidth(2)
|
||||
ctx.strokeRect(0, 0, size, size)
|
||||
|
||||
// 绘制定位点
|
||||
const drawFinderPattern = (x: number, y: number) => {
|
||||
ctx.setFillStyle('#000000')
|
||||
ctx.fillRect(x, y, 28, 28)
|
||||
ctx.setFillStyle('#ffffff')
|
||||
ctx.fillRect(x + 4, y + 4, 20, 20)
|
||||
ctx.setFillStyle('#000000')
|
||||
ctx.fillRect(x + 8, y + 8, 12, 12)
|
||||
}
|
||||
|
||||
// 三个角的定位点
|
||||
drawFinderPattern(10, 10) // 左上
|
||||
drawFinderPattern(size - 38, 10) // 右上
|
||||
drawFinderPattern(10, size - 38) // 左下
|
||||
|
||||
// 生成数据点(模拟二维码数据)
|
||||
ctx.setFillStyle('#000000')
|
||||
const moduleSize = 4
|
||||
const modules = Math.floor((size - 80) / moduleSize)
|
||||
|
||||
for (let i = 0; i < modules; i++) {
|
||||
for (let j = 0; j < modules; j++) {
|
||||
// 简单的伪随机算法,基于文本内容生成固定的图案
|
||||
const hash = text.charCodeAt(i % text.length) + text.charCodeAt(j % text.length)
|
||||
if (hash % 3 === 0) {
|
||||
const x = 40 + i * moduleSize
|
||||
const y = 40 + j * moduleSize
|
||||
ctx.fillRect(x, y, moduleSize - 1, moduleSize - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.draw()
|
||||
}
|
||||
|
||||
// 复制文本内容
|
||||
const copyText = () => {
|
||||
if (text) {
|
||||
Taro.setClipboardData({
|
||||
data: text,
|
||||
success: () => {
|
||||
Taro.showToast({
|
||||
title: '内容已复制',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 保存二维码图片
|
||||
const saveQRCode = () => {
|
||||
if (qrDataURL) {
|
||||
Taro.downloadFile({
|
||||
url: qrDataURL,
|
||||
success: (res) => {
|
||||
Taro.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath,
|
||||
success: () => {
|
||||
Taro.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success'
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
Taro.showToast({
|
||||
title: '保存失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 当弹窗打开时生成二维码
|
||||
useEffect(() => {
|
||||
if (visible && text) {
|
||||
generateQRCode()
|
||||
}
|
||||
}, [visible, text])
|
||||
|
||||
return (
|
||||
<Popup
|
||||
visible={visible}
|
||||
position="center"
|
||||
closeable
|
||||
closeIcon={<Close />}
|
||||
onClose={onClose}
|
||||
style={{
|
||||
width: '90%',
|
||||
maxWidth: '400px',
|
||||
borderRadius: '12px'
|
||||
}}
|
||||
>
|
||||
<View className="p-6">
|
||||
{/* 标题 */}
|
||||
<View className="text-center mb-4">
|
||||
<Text className="text-lg font-bold">二维码</Text>
|
||||
{title && (
|
||||
<Text className="text-sm text-gray-600 block mt-1">{title}</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 副标题信息 */}
|
||||
{subtitle && (
|
||||
<View className="bg-gray-50 rounded-lg p-3 mb-4 text-center">
|
||||
<Text className="text-lg font-bold text-red-500">{subtitle}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 二维码显示区域 */}
|
||||
<View className="text-center mb-4">
|
||||
{loading ? (
|
||||
<View
|
||||
className="bg-gray-100 rounded-lg flex items-center justify-center"
|
||||
style={{ width: `${size}px`, height: `${size}px`, margin: '0 auto' }}
|
||||
>
|
||||
<Text className="text-gray-500">生成中...</Text>
|
||||
</View>
|
||||
) : qrDataURL ? (
|
||||
<View className="p-4 bg-white border border-gray-200 rounded-lg">
|
||||
<img
|
||||
src={qrDataURL}
|
||||
alt="二维码"
|
||||
style={{
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
display: 'block',
|
||||
margin: '0 auto'
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<View className="p-4 bg-white border border-gray-200 rounded-lg">
|
||||
<Canvas
|
||||
canvasId={canvasRef.current}
|
||||
style={{
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
border: '1px solid #e5e5e5'
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 文本内容显示 */}
|
||||
<View className="bg-blue-50 rounded-lg p-3 mb-4">
|
||||
<View className="flex justify-between items-center">
|
||||
<View className="flex-1">
|
||||
<Text className="text-sm text-blue-600 mb-1">二维码内容</Text>
|
||||
<Text className="text-sm text-blue-800" style={{ wordBreak: 'break-all' }}>
|
||||
{text}
|
||||
</Text>
|
||||
</View>
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
icon={<Copy />}
|
||||
onClick={copyText}
|
||||
className="ml-2"
|
||||
>
|
||||
复制
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<View className="flex">
|
||||
<Button
|
||||
size="large"
|
||||
fill="outline"
|
||||
icon={<Download />}
|
||||
onClick={saveQRCode}
|
||||
className="flex-1 mr-3"
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
<Button
|
||||
size="large"
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
generateQRCode()
|
||||
Taro.showToast({
|
||||
title: '二维码已刷新',
|
||||
icon: 'success'
|
||||
})
|
||||
}}
|
||||
className="flex-1"
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
{/* 使用说明 */}
|
||||
<View className="mt-4 p-3 bg-yellow-50 rounded-lg">
|
||||
<Text className="text-sm text-yellow-800 font-medium mb-2">使用说明:</Text>
|
||||
<View>
|
||||
<Text className="text-xs text-yellow-700 block mb-1">• 长按二维码可以识别或保存</Text>
|
||||
<Text className="text-xs text-yellow-700 block mb-1">• 点击保存按钮可保存到相册</Text>
|
||||
<Text className="text-xs text-yellow-700 block">• 可以复制二维码内容进行分享</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Popup>
|
||||
)
|
||||
}
|
||||
|
||||
export default QRCodeGenerator
|
||||
63
src/components/SimpleQRCodeModal.tsx
Normal file
63
src/components/SimpleQRCodeModal.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React from 'react'
|
||||
import {View, Text} from '@tarojs/components'
|
||||
import {Popup} from '@nutui/nutui-react-taro'
|
||||
import {Close, QrCode} from '@nutui/icons-react-taro'
|
||||
|
||||
export interface SimpleQRCodeModalProps {
|
||||
/** 是否显示弹窗 */
|
||||
visible: boolean
|
||||
/** 关闭弹窗回调 */
|
||||
onClose: () => void
|
||||
/** 二维码内容(礼品卡code码) */
|
||||
qrContent: string
|
||||
}
|
||||
|
||||
const SimpleQRCodeModal: React.FC<SimpleQRCodeModalProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
qrContent
|
||||
}) => {
|
||||
|
||||
|
||||
return (
|
||||
<Popup
|
||||
visible={visible}
|
||||
position="center"
|
||||
closeable
|
||||
closeIcon={<Close/>}
|
||||
onClose={onClose}
|
||||
style={{
|
||||
width: '85%',
|
||||
maxWidth: '350px',
|
||||
borderRadius: '12px'
|
||||
}}
|
||||
>
|
||||
<View className="p-6">
|
||||
{/* 标题 */}
|
||||
<View className="text-center mb-4">
|
||||
<Text className="text-lg font-bold">礼品卡二维码</Text>
|
||||
<Text className="text-sm text-gray-500 block mt-1">
|
||||
请向商家出示此二维码
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 二维码区域 */}
|
||||
<View className="text-center mb-4">
|
||||
<View className="p-4 bg-white border border-gray-200 rounded-lg">
|
||||
<View className="bg-gray-100 rounded flex items-center justify-center"
|
||||
style={{width: '200px', height: '200px', margin: '0 auto'}}>
|
||||
<View className="text-center">
|
||||
<QrCode size="48" className="text-gray-400 mb-2"/>
|
||||
<Text className="text-gray-500 text-sm">二维码</Text>
|
||||
<Text className="text-xs text-gray-400 mt-1">ID: {qrContent ? qrContent.slice(-6) : '------'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
</Popup>
|
||||
)
|
||||
}
|
||||
|
||||
export default SimpleQRCodeModal
|
||||
Reference in New Issue
Block a user