250 lines
10 KiB
TypeScript
250 lines
10 KiB
TypeScript
import toast from '@/utils/toast'
|
||
import { useState, useEffect } from 'react'
|
||
import {
|
||
Card,
|
||
CardContent,
|
||
CardDescription,
|
||
CardHeader,
|
||
CardTitle,
|
||
} from '@/components/ui/card'
|
||
import { Label } from '@/components/ui/label'
|
||
import { Input } from '@/components/ui/input'
|
||
import { Textarea } from '@/components/ui/textarea'
|
||
import { Button } from '@/components/ui/button'
|
||
import { QrCode, Upload, Link, ExternalLink, Copy, Check, HelpCircle } from 'lucide-react'
|
||
import { get, post } from '@/api/client'
|
||
|
||
export function QRCodesPage() {
|
||
const [liveQRUrls, setLiveQRUrls] = useState('')
|
||
const [wechatGroupUrl, setWechatGroupUrl] = useState('')
|
||
const [copied, setCopied] = useState('')
|
||
const [config, setConfig] = useState<{
|
||
paymentMethods?: { wechat?: { groupQrCode?: string } }
|
||
liveQRCodes?: { id: string; name: string; urls: string[]; clickCount: number }[]
|
||
}>({})
|
||
|
||
const loadConfig = async () => {
|
||
try {
|
||
const data = await get<{
|
||
paymentMethods?: { wechat?: { groupQrCode?: string } }
|
||
liveQRCodes?: { id: string; name: string; urls: string[]; clickCount: number }[]
|
||
}>('/api/config')
|
||
const urls = data?.liveQRCodes?.[0]?.urls
|
||
if (Array.isArray(urls)) setLiveQRUrls(urls.join('\n'))
|
||
const group = data?.paymentMethods?.wechat?.groupQrCode
|
||
if (group) setWechatGroupUrl(group)
|
||
setConfig({
|
||
paymentMethods: data?.paymentMethods,
|
||
liveQRCodes: data?.liveQRCodes,
|
||
})
|
||
} catch (e) {
|
||
console.error(e)
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
loadConfig()
|
||
}, [])
|
||
|
||
const handleCopy = (text: string, field: string) => {
|
||
navigator.clipboard.writeText(text)
|
||
setCopied(field)
|
||
setTimeout(() => setCopied(''), 2000)
|
||
}
|
||
|
||
const handleSaveLiveQR = async () => {
|
||
try {
|
||
const urls = liveQRUrls
|
||
.split('\n')
|
||
.map((u) => u.trim())
|
||
.filter(Boolean)
|
||
const updatedLiveQRCodes = [...(config.liveQRCodes || [])]
|
||
if (updatedLiveQRCodes[0]) {
|
||
updatedLiveQRCodes[0].urls = urls
|
||
} else {
|
||
updatedLiveQRCodes.push({ id: 'live-1', name: '微信群活码', urls, clickCount: 0 })
|
||
}
|
||
await post('/api/db/config', {
|
||
key: 'live_qr_codes',
|
||
value: updatedLiveQRCodes,
|
||
description: '群活码配置',
|
||
})
|
||
toast.success('群活码配置已保存!')
|
||
await loadConfig()
|
||
} catch (e) {
|
||
console.error(e)
|
||
toast.error('保存失败: ' + (e instanceof Error ? e.message : String(e)))
|
||
}
|
||
}
|
||
|
||
const handleSaveWechatGroup = async () => {
|
||
try {
|
||
await post('/api/db/config', {
|
||
key: 'payment_methods',
|
||
value: {
|
||
...(config.paymentMethods || {}),
|
||
wechat: {
|
||
...(config.paymentMethods?.wechat || {}),
|
||
groupQrCode: wechatGroupUrl,
|
||
},
|
||
},
|
||
description: '支付方式配置',
|
||
})
|
||
toast.success('微信群链接已保存!用户支付成功后将自动跳转')
|
||
await loadConfig()
|
||
} catch (e) {
|
||
console.error(e)
|
||
toast.error('保存失败: ' + (e instanceof Error ? e.message : String(e)))
|
||
}
|
||
}
|
||
|
||
const handleTestJump = () => {
|
||
if (wechatGroupUrl) window.open(wechatGroupUrl, '_blank')
|
||
else toast.error('请先配置微信群链接')
|
||
}
|
||
|
||
return (
|
||
<div className="p-8 w-full">
|
||
<div className="mb-8">
|
||
<h2 className="text-2xl font-bold text-white">微信群活码管理</h2>
|
||
<p className="text-gray-400 mt-1">配置微信群跳转链接,用户支付后自动跳转加群</p>
|
||
</div>
|
||
|
||
<div className="mb-6 bg-[#07C160]/10 border border-[#07C160]/30 rounded-xl p-4">
|
||
<div className="flex items-start gap-3">
|
||
<HelpCircle className="w-5 h-5 text-[#07C160] flex-shrink-0 mt-0.5" />
|
||
<div className="text-sm">
|
||
<p className="font-medium mb-2 text-[#07C160]">微信群活码配置指南</p>
|
||
<div className="text-[#07C160]/80 space-y-2">
|
||
<p className="font-medium">方法一:使用草料活码(推荐)</p>
|
||
<ol className="list-decimal list-inside space-y-1 pl-2">
|
||
<li>访问草料二维码创建活码</li>
|
||
<li>上传微信群二维码图片,生成永久链接</li>
|
||
<li>复制生成的短链接填入下方配置</li>
|
||
<li>群满后可直接在草料后台更换新群码,链接不变</li>
|
||
</ol>
|
||
<p className="font-medium mt-3">方法二:直接使用微信群链接</p>
|
||
<ol className="list-decimal list-inside space-y-1 pl-2">
|
||
<li>微信打开目标群 → 右上角"..." → 群二维码</li>
|
||
<li>长按二维码 → 识别二维码 → 复制链接</li>
|
||
</ol>
|
||
<p className="text-[#07C160]/60 mt-2">注意:微信原生群二维码7天后失效,建议使用草料活码</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid gap-6 md:grid-cols-2">
|
||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl md:col-span-2">
|
||
<CardHeader>
|
||
<CardTitle className="text-[#07C160] flex items-center gap-2">
|
||
<QrCode className="w-5 h-5" />
|
||
支付成功跳转链接(核心配置)
|
||
</CardTitle>
|
||
<CardDescription className="text-gray-400">
|
||
用户支付完成后自动跳转到此链接,进入指定微信群
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
<div className="space-y-2">
|
||
<Label className="text-gray-300 flex items-center gap-2">
|
||
<Link className="w-4 h-4" />
|
||
微信群链接 / 活码链接
|
||
</Label>
|
||
<div className="flex gap-2">
|
||
<Input
|
||
placeholder="https://cli.im/xxxxx 或 https://weixin.qq.com/g/..."
|
||
className="bg-[#0a1628] border-gray-700 text-white placeholder:text-gray-500 flex-1"
|
||
value={wechatGroupUrl}
|
||
onChange={(e) => setWechatGroupUrl(e.target.value)}
|
||
/>
|
||
<Button
|
||
variant="outline"
|
||
size="icon"
|
||
className="border-gray-700 bg-transparent hover:bg-gray-700/50"
|
||
onClick={() => handleCopy(wechatGroupUrl, 'group')}
|
||
>
|
||
{copied === 'group' ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4 text-gray-400" />}
|
||
</Button>
|
||
</div>
|
||
<p className="text-xs text-gray-500 flex items-center gap-1">
|
||
<ExternalLink className="w-3 h-3" />
|
||
支持格式:草料短链、微信群链接(https://weixin.qq.com/g/...)、企业微信链接等
|
||
</p>
|
||
</div>
|
||
<div className="flex gap-3">
|
||
<Button onClick={handleSaveWechatGroup} className="flex-1 bg-[#07C160] hover:bg-[#06AD51] text-white">
|
||
<Upload className="w-4 h-4 mr-2" />
|
||
保存配置
|
||
</Button>
|
||
<Button
|
||
onClick={handleTestJump}
|
||
variant="outline"
|
||
className="border-[#07C160] text-[#07C160] hover:bg-[#07C160]/10 bg-transparent"
|
||
>
|
||
<ExternalLink className="w-4 h-4 mr-2" />
|
||
测试跳转
|
||
</Button>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl md:col-span-2">
|
||
<CardHeader>
|
||
<CardTitle className="text-white flex items-center gap-2">
|
||
<QrCode className="w-5 h-5 text-[#38bdac]" />
|
||
多群轮换(高级配置)
|
||
</CardTitle>
|
||
<CardDescription className="text-gray-400">
|
||
配置多个群链接,系统自动轮换分配,避免单群满员
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
<div className="space-y-2">
|
||
<Label className="text-gray-300 flex items-center gap-2">
|
||
<Link className="w-4 h-4" />
|
||
多个群链接(每行一个)
|
||
</Label>
|
||
<Textarea
|
||
placeholder="https://cli.im/group1\nhttps://cli.im/group2"
|
||
className="bg-[#0a1628] border-gray-700 text-white placeholder:text-gray-500 min-h-[120px] font-mono text-sm"
|
||
value={liveQRUrls}
|
||
onChange={(e) => setLiveQRUrls(e.target.value)}
|
||
/>
|
||
<p className="text-xs text-gray-500">每行填写一个群链接,系统将按顺序或随机分配</p>
|
||
</div>
|
||
<div className="flex items-center justify-between p-3 bg-[#0a1628] rounded-lg border border-gray-700/50">
|
||
<span className="text-sm text-gray-400">已配置群数量</span>
|
||
<span className="font-bold text-[#38bdac]">
|
||
{liveQRUrls.split('\n').filter(Boolean).length} 个
|
||
</span>
|
||
</div>
|
||
<Button onClick={handleSaveLiveQR} className="w-full bg-[#38bdac] hover:bg-[#2da396] text-white">
|
||
<Upload className="w-4 h-4 mr-2" />
|
||
保存多群配置
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
<div className="mt-6 bg-[#0f2137] rounded-xl p-4 border border-gray-700/50">
|
||
<h4 className="text-white font-medium mb-3">常见问题</h4>
|
||
<div className="space-y-3 text-sm">
|
||
<div>
|
||
<p className="text-[#38bdac]">Q: 为什么推荐使用草料活码?</p>
|
||
<p className="text-gray-400">
|
||
A: 草料活码是永久链接,群满后可直接在后台更换新群码,无需修改网站配置。微信原生群码7天失效。
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-[#38bdac]">Q: 支付后没有跳转怎么办?</p>
|
||
<p className="text-gray-400">
|
||
A: 1) 检查链接是否正确填写 2) 部分浏览器可能拦截弹窗,用户需手动允许 3) 建议使用https开头的链接
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|