/**
|
* @author 韩天尊
|
* @time 2024-01-15
|
* @version 1.0.0
|
* @description 管理端积分核销页面,用于扫码核销用户积分
|
*/
|
import React, { useState, useEffect } from 'react';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useAppContext } from '../context/AppContext';
|
import PageHeader from '../components/PageHeader';
|
import { adminAPI } from '../services/api';
|
|
const AdminPointsRedemptionPage: React.FC = () => {
|
const navigate = useNavigate();
|
const location = useLocation();
|
const { state } = useAppContext();
|
const [redemptionPoints, setRedemptionPoints] = useState('');
|
const [redemptionNote, setRedemptionNote] = useState('');
|
const [exchangeProduct, setExchangeProduct] = useState('');
|
const [exchangeQuantity, setExchangeQuantity] = useState('');
|
const [isScanning, setIsScanning] = useState(false);
|
const [scanResult, setScanResult] = useState<string | null>(null);
|
|
// 接收从积分核销页面传递的参数
|
const [scanData, setScanData] = useState<{
|
communityCode: string;
|
volunteerId: string;
|
volunteerName: string;
|
points: number;
|
} | null>(null);
|
|
// 用户信息状态
|
const [userInfo, setUserInfo] = useState<{
|
communityCode: string;
|
communityName: string;
|
volunteerName: string;
|
volunteerId: string;
|
points: number;
|
} | null>(null);
|
const [loadingUserInfo, setLoadingUserInfo] = useState(false);
|
|
// URL参数检测状态
|
const [hasUrlParams, setHasUrlParams] = useState(false);
|
|
// 弹窗状态
|
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
const [confirmData, setConfirmData] = useState<{
|
points: string;
|
userName: string;
|
communityName: string;
|
productName: string;
|
quantity: string;
|
note: string;
|
} | null>(null);
|
const [successData, setSuccessData] = useState<{
|
userName: string;
|
remainingPoints: number;
|
} | null>(null);
|
const [processingRedemption, setProcessingRedemption] = useState(false);
|
|
// 获取用户信息的函数
|
const fetchUserInfo = async (communityCode: string, volunteerId: string) => {
|
setLoadingUserInfo(true);
|
try {
|
const response = await adminAPI.getQRCodeUserInfo({
|
communityCode,
|
userId: volunteerId
|
});
|
|
if (response.code === 0) {
|
const userData = response.data;
|
setUserInfo(userData);
|
setScanResult(volunteerId);
|
setIsScanning(false);
|
} else {
|
console.error('获取用户信息失败:', response.msg);
|
alert('获取用户信息失败:' + response.msg);
|
}
|
} catch (error) {
|
console.error('获取用户信息失败:', error);
|
alert('获取用户信息失败,请重试');
|
} finally {
|
setLoadingUserInfo(false);
|
}
|
};
|
|
// 解析URL参数并获取用户信息
|
useEffect(() => {
|
const urlParams = new URLSearchParams(location.search);
|
const communityCode = urlParams.get('communityCode');
|
const volunteerId = urlParams.get('volunteerId');
|
|
if (communityCode && volunteerId) {
|
// 从URL参数获取用户信息
|
setHasUrlParams(true);
|
fetchUserInfo(communityCode, volunteerId);
|
} else if (location.state) {
|
// 兼容原有的state传递方式
|
setHasUrlParams(true);
|
const { communityCode, volunteerId, volunteerName, points } = location.state as {
|
communityCode: string;
|
volunteerId: string;
|
volunteerName: string;
|
points: number;
|
};
|
|
setScanData({
|
communityCode,
|
volunteerId,
|
volunteerName,
|
points
|
});
|
|
// 自动设置扫描结果
|
setScanResult(volunteerId);
|
setIsScanning(false);
|
} else {
|
// 没有URL参数和state,显示提示语
|
setHasUrlParams(false);
|
}
|
}, [location.search, location.state]);
|
|
// 扫描动画效果
|
useEffect(() => {
|
const scanLine = document.querySelector('.scan-line');
|
if (scanLine && isScanning) {
|
scanLine.classList.add('scanning');
|
} else if (scanLine) {
|
scanLine.classList.remove('scanning');
|
}
|
}, [isScanning]);
|
|
// 处理积分核销
|
const handleProcessRedemption = async () => {
|
if (!redemptionPoints || redemptionPoints.trim() === '' || isNaN(Number(redemptionPoints)) || Number(redemptionPoints) <= 0) {
|
alert('请输入有效的核减积分');
|
return;
|
}
|
|
if (!exchangeProduct.trim()) {
|
alert('请输入兑换产品名称');
|
return;
|
}
|
|
if (!exchangeQuantity || exchangeQuantity.trim() === '' || isNaN(Number(exchangeQuantity)) || Number(exchangeQuantity) <= 0) {
|
alert('请输入有效的兑换数量');
|
return;
|
}
|
|
if (!scanResult) {
|
alert('请先扫描用户二维码');
|
return;
|
}
|
|
// 显示确认弹窗
|
setConfirmData({
|
points: redemptionPoints,
|
userName: userInfo?.volunteerName || scanData?.volunteerName || '未知',
|
communityName: userInfo?.communityName || scanData?.communityCode || '未知',
|
productName: exchangeProduct,
|
quantity: exchangeQuantity,
|
note: redemptionNote
|
});
|
setShowConfirmModal(true);
|
};
|
|
// 确认核销
|
const confirmRedemption = async () => {
|
if (!confirmData || !scanResult) return;
|
|
setProcessingRedemption(true);
|
setShowConfirmModal(false);
|
|
try {
|
const response = await adminAPI.redeemPoints({
|
qrCode: scanResult,
|
communityCode: userInfo?.communityCode || scanData?.communityCode || '',
|
volunteerId: userInfo?.volunteerId || scanData?.volunteerId || '',
|
points: Number(confirmData.points),
|
productNum: Number(confirmData.quantity),
|
productName: confirmData.productName,
|
description: confirmData.note || `兑换${confirmData.productName}`
|
});
|
|
if (response.code === 0) {
|
setSuccessData({
|
userName: response.data.userName,
|
remainingPoints: response.data.remainingPoints
|
});
|
setShowSuccessModal(true);
|
|
// 重置表单,但保留用户信息
|
setRedemptionPoints('');
|
setRedemptionNote('');
|
setExchangeProduct('');
|
setExchangeQuantity('');
|
// 不重置 scanResult, scanData, userInfo,保持用户信息显示
|
|
// 延迟关闭成功弹窗并刷新用户信息
|
setTimeout(() => {
|
setShowSuccessModal(false);
|
// 刷新用户信息以更新积分
|
if (userInfo?.communityCode && userInfo?.volunteerId) {
|
fetchUserInfo(userInfo.communityCode, userInfo.volunteerId);
|
}
|
}, 3000);
|
} else {
|
alert('积分核销失败:' + response.msg);
|
}
|
} catch (error) {
|
console.error('积分核销失败:', error);
|
alert('积分核销失败,请重试');
|
} finally {
|
setProcessingRedemption(false);
|
}
|
};
|
|
// 模拟扫码功能
|
const handleStartScan = () => {
|
setIsScanning(true);
|
|
// 模拟扫码过程
|
setTimeout(() => {
|
const mockUserId = 'USER_' + Math.random().toString(36).substr(2, 9);
|
setScanResult(mockUserId);
|
setIsScanning(false);
|
alert(`扫码成功!用户ID: ${mockUserId}`);
|
}, 2000);
|
};
|
|
// 重置扫码
|
const handleResetScan = () => {
|
setScanResult(null);
|
setIsScanning(false);
|
setScanData(null);
|
setUserInfo(null);
|
};
|
|
// 处理返回按钮点击
|
const handleBack = () => {
|
// 跳转到管理端首页
|
navigate('/admin');
|
};
|
|
return (
|
<div className="page admin-points-redemption-page">
|
<PageHeader title="积分核销" showBack={true} onBack={handleBack} />
|
|
<div className="admin-redemption-container">
|
{/* 用户信息区域 */}
|
{userInfo ? (
|
<div className="user-info-area">
|
<div className="user-info-card">
|
<div className="user-info-header">
|
<div className="header-icon">
|
<i className="fas fa-user-circle"></i>
|
</div>
|
<div className="header-content">
|
<h3>用户信息</h3>
|
<p>核销用户详细信息</p>
|
</div>
|
<div className="header-status">
|
<i className="fas fa-check-circle"></i>
|
<span>已验证</span>
|
</div>
|
</div>
|
|
<div className="user-info-content">
|
<div className="info-section">
|
<div className="info-item">
|
<div className="item-icon">
|
<i className="fas fa-building"></i>
|
</div>
|
<div className="item-content">
|
<span className="info-label">社区名称</span>
|
<span className="info-value">{userInfo.communityName}</span>
|
</div>
|
</div>
|
|
<div className="info-item">
|
<div className="item-icon">
|
<i className="fas fa-user"></i>
|
</div>
|
<div className="item-content">
|
<span className="info-label">用户姓名</span>
|
<span className="info-value">{userInfo.volunteerName}</span>
|
</div>
|
</div>
|
|
<div className="info-item points-item">
|
<div className="item-icon">
|
<i className="fas fa-coins"></i>
|
</div>
|
<div className="item-content">
|
<span className="info-label">可用积分</span>
|
<span className="info-value points">{userInfo.points}</span>
|
</div>
|
<div className="points-badge">
|
<span>积分</span>
|
</div>
|
</div>
|
</div>
|
|
<div className="info-footer">
|
<div className="footer-item">
|
<i className="fas fa-clock"></i>
|
<span>信息更新时间:{new Date().toLocaleString()}</span>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
) : hasUrlParams ? (
|
<div className="scan-area">
|
<div className={`scan-frame ${isScanning ? 'scanning' : ''} ${scanResult ? 'scanned' : ''}`}>
|
<div className="scan-corner top-left"></div>
|
<div className="scan-corner top-right"></div>
|
<div className="scan-corner bottom-left"></div>
|
<div className="scan-corner bottom-right"></div>
|
<div className="scan-line"></div>
|
|
{scanResult && (
|
<div className="scan-success">
|
<i className="fas fa-check-circle"></i>
|
<span>扫码成功</span>
|
</div>
|
)}
|
</div>
|
|
<div className="scan-tip">
|
<i className="fas fa-qrcode"></i>
|
{loadingUserInfo ? (
|
<p>正在获取用户信息...</p>
|
) : isScanning ? (
|
<p>正在扫描中...</p>
|
) : scanResult ? (
|
<div>
|
<p>已识别用户:{scanData?.volunteerName || scanResult}</p>
|
{scanData && (
|
<div className="user-info">
|
<p>社区编码:{scanData.communityCode}</p>
|
<p>用户ID:{scanData.volunteerId}</p>
|
<p>可用积分:{scanData.points}</p>
|
</div>
|
)}
|
</div>
|
) : (
|
<p>扫描用户二维码进行核销</p>
|
)}
|
</div>
|
|
<div className="scan-buttons">
|
{!scanResult && !isScanning && (
|
<button
|
className="scan-btn start"
|
onClick={handleStartScan}
|
>
|
<i className="fas fa-camera"></i>
|
开始扫码
|
</button>
|
)}
|
|
{scanResult && (
|
<button
|
className="scan-btn reset"
|
onClick={handleResetScan}
|
>
|
<i className="fas fa-redo"></i>
|
重新扫码
|
</button>
|
)}
|
</div>
|
</div>
|
) : (
|
<div className="wechat-scan-tip">
|
<div className="tip-icon">
|
<i className="fab fa-weixin"></i>
|
</div>
|
<div className="tip-content">
|
<h3>请用微信扫码进行积分核销</h3>
|
<p>使用微信扫描用户二维码,自动获取用户信息并完成积分核销</p>
|
</div>
|
<div className="tip-steps">
|
<div className="step-item">
|
<div className="step-number">1</div>
|
<div className="step-text">打开微信扫一扫</div>
|
</div>
|
<div className="step-item">
|
<div className="step-number">2</div>
|
<div className="step-text">扫描用户二维码</div>
|
</div>
|
<div className="step-item">
|
<div className="step-number">3</div>
|
<div className="step-text">自动跳转核销页面</div>
|
</div>
|
</div>
|
</div>
|
)}
|
|
{/* 核销设置 */}
|
<div className="redemption-settings">
|
<div className="setting-item">
|
<label>核减积分 <span className="required">*</span></label>
|
<input
|
type="number"
|
value={redemptionPoints}
|
onChange={(e) => setRedemptionPoints(e.target.value)}
|
placeholder="请输入核减积分"
|
min="1"
|
max="1000"
|
required
|
/>
|
</div>
|
|
<div className="setting-item">
|
<label>兑换产品 <span className="required">*</span></label>
|
<input
|
type="text"
|
value={exchangeProduct}
|
onChange={(e) => setExchangeProduct(e.target.value)}
|
placeholder="请输入兑换产品名称"
|
maxLength={50}
|
required
|
/>
|
</div>
|
|
<div className="setting-item">
|
<label>兑换数量 <span className="required">*</span></label>
|
<input
|
type="number"
|
value={exchangeQuantity}
|
onChange={(e) => setExchangeQuantity(e.target.value)}
|
placeholder="请输入兑换数量"
|
min="1"
|
max="100"
|
required
|
/>
|
</div>
|
|
<div className="setting-item">
|
<label>核销备注</label>
|
<input
|
type="text"
|
value={redemptionNote}
|
onChange={(e) => setRedemptionNote(e.target.value)}
|
placeholder="请输入核销备注(可选)"
|
maxLength={50}
|
/>
|
</div>
|
</div>
|
|
{/* 核销操作 */}
|
<div className="redemption-actions">
|
<button
|
className={`redemption-btn ${!scanResult || processingRedemption ? 'disabled' : ''}`}
|
onClick={handleProcessRedemption}
|
disabled={!scanResult || processingRedemption}
|
>
|
{processingRedemption ? (
|
<>
|
<i className="fas fa-spinner fa-spin"></i>
|
<span>核销中...</span>
|
</>
|
) : (
|
<>
|
<i className="fas fa-check"></i>
|
<span>确认核销</span>
|
</>
|
)}
|
</button>
|
</div>
|
</div>
|
|
{/* 确认核销弹窗 */}
|
{showConfirmModal && confirmData && (
|
<div className="modal-overlay">
|
<div className="confirm-modal">
|
<div className="modal-header">
|
<div className="modal-icon">
|
<i className="fas fa-exclamation-triangle"></i>
|
</div>
|
<h3 className="modal-title">确认积分核销</h3>
|
</div>
|
|
<div className="modal-content">
|
<div className="confirm-info">
|
<div className="info-row">
|
<span className="info-label">核销积分:</span>
|
<span className="info-value highlight">{confirmData.points} 分</span>
|
</div>
|
<div className="info-row">
|
<span className="info-label">用户姓名:</span>
|
<span className="info-value">{confirmData.userName}</span>
|
</div>
|
<div className="info-row">
|
<span className="info-label">所属社区:</span>
|
<span className="info-value">{confirmData.communityName}</span>
|
</div>
|
<div className="info-row">
|
<span className="info-label">兑换产品:</span>
|
<span className="info-value">{confirmData.productName}</span>
|
</div>
|
<div className="info-row">
|
<span className="info-label">兑换数量:</span>
|
<span className="info-value">{confirmData.quantity}</span>
|
</div>
|
{confirmData.note && (
|
<div className="info-row">
|
<span className="info-label">核销备注:</span>
|
<span className="info-value">{confirmData.note}</span>
|
</div>
|
)}
|
</div>
|
|
<div className="confirm-warning">
|
<i className="fas fa-info-circle"></i>
|
<span>请确认以上信息无误,核销后将无法撤销</span>
|
</div>
|
</div>
|
|
<div className="modal-actions">
|
<button
|
className="modal-btn cancel"
|
onClick={() => setShowConfirmModal(false)}
|
>
|
取消
|
</button>
|
<button
|
className="modal-btn confirm"
|
onClick={confirmRedemption}
|
>
|
<i className="fas fa-check"></i>
|
确认核销
|
</button>
|
</div>
|
</div>
|
</div>
|
)}
|
|
{/* 成功弹窗 */}
|
{showSuccessModal && successData && (
|
<div className="modal-overlay success-overlay">
|
<div className="success-modal">
|
<div className="success-icon">
|
<i className="fas fa-check-circle"></i>
|
</div>
|
<div className="success-content">
|
<h3 className="success-title">积分核销成功!</h3>
|
<div className="success-info">
|
<div className="success-item">
|
<span className="success-label">用户:</span>
|
<span className="success-value">{successData.userName}</span>
|
</div>
|
<div className="success-item">
|
<span className="success-label">剩余积分:</span>
|
<span className="success-value highlight">{successData.remainingPoints} 分</span>
|
</div>
|
</div>
|
<p className="success-subtitle">核销记录已保存,页面即将自动刷新...</p>
|
</div>
|
<div className="success-animation">
|
<div className="loading-dots">
|
<span></span>
|
<span></span>
|
<span></span>
|
</div>
|
</div>
|
</div>
|
</div>
|
)}
|
</div>
|
);
|
};
|
|
export default AdminPointsRedemptionPage;
|