|
@@ -3,19 +3,9 @@
|
|
|
import { useState, useEffect, useRef } from 'react';
|
|
import { useState, useEffect, useRef } from 'react';
|
|
|
import api from '@/lib/api';
|
|
import api from '@/lib/api';
|
|
|
import {
|
|
import {
|
|
|
- Loader2,
|
|
|
|
|
- Trash2,
|
|
|
|
|
- Plus,
|
|
|
|
|
- Save,
|
|
|
|
|
- X,
|
|
|
|
|
- QrCode,
|
|
|
|
|
- FileImage,
|
|
|
|
|
- Smartphone,
|
|
|
|
|
- ToggleLeft,
|
|
|
|
|
- ToggleRight,
|
|
|
|
|
- Upload
|
|
|
|
|
|
|
+ Loader2, Trash2, Plus, Save, X, QrCode, FileImage,
|
|
|
|
|
+ Smartphone, ToggleLeft, ToggleRight, Upload
|
|
|
} from 'lucide-react';
|
|
} from 'lucide-react';
|
|
|
-// 1. 引入 LocalTime 组件
|
|
|
|
|
import LocalTime from '@/components/common/LocalTime';
|
|
import LocalTime from '@/components/common/LocalTime';
|
|
|
|
|
|
|
|
interface QrManagerProps {
|
|
interface QrManagerProps {
|
|
@@ -28,7 +18,6 @@ interface QrManagerProps {
|
|
|
interface PaymentQr {
|
|
interface PaymentQr {
|
|
|
id: number;
|
|
id: number;
|
|
|
qr_code: string;
|
|
qr_code: string;
|
|
|
- image_url?: string;
|
|
|
|
|
priority?: number;
|
|
priority?: number;
|
|
|
is_active: boolean | number;
|
|
is_active: boolean | number;
|
|
|
description?: string;
|
|
description?: string;
|
|
@@ -43,7 +32,6 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
|
|
|
const [togglingId, setTogglingId] = useState<number | null>(null);
|
|
const [togglingId, setTogglingId] = useState<number | null>(null);
|
|
|
|
|
|
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
-
|
|
|
|
|
const [form, setForm] = useState({
|
|
const [form, setForm] = useState({
|
|
|
qr_code: '',
|
|
qr_code: '',
|
|
|
description: '',
|
|
description: '',
|
|
@@ -52,9 +40,13 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
|
|
|
is_active: 1
|
|
is_active: 1
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ // 移动端 Tab: 'list' (查看列表) | 'add' (添加)
|
|
|
|
|
+ const [activeTab, setActiveTab] = useState<'list' | 'add'>('list');
|
|
|
|
|
+
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
if (isOpen && providerId) {
|
|
if (isOpen && providerId) {
|
|
|
fetchQrs();
|
|
fetchQrs();
|
|
|
|
|
+ setActiveTab('list'); // 默认显示列表
|
|
|
}
|
|
}
|
|
|
}, [isOpen, providerId]);
|
|
}, [isOpen, providerId]);
|
|
|
|
|
|
|
@@ -67,7 +59,6 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
|
|
|
const list = Array.isArray(res.data.data) ? res.data.data : [];
|
|
const list = Array.isArray(res.data.data) ? res.data.data : [];
|
|
|
setQrs(list);
|
|
setQrs(list);
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
- console.warn("Fetch QR failed");
|
|
|
|
|
setQrs([]);
|
|
setQrs([]);
|
|
|
} finally {
|
|
} finally {
|
|
|
setLoading(false);
|
|
setLoading(false);
|
|
@@ -77,51 +68,33 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
|
|
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
const file = e.target.files?.[0];
|
|
const file = e.target.files?.[0];
|
|
|
if (!file) return;
|
|
if (!file) return;
|
|
|
-
|
|
|
|
|
- if (file.size > 2 * 1024 * 1024) {
|
|
|
|
|
- alert("图片太大,请选择小于 2MB 的图片");
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ if (file.size > 2 * 1024 * 1024) return alert("图片太大,请选择小于 2MB 的图片");
|
|
|
const reader = new FileReader();
|
|
const reader = new FileReader();
|
|
|
- reader.onloadend = () => {
|
|
|
|
|
- const base64String = reader.result as string;
|
|
|
|
|
- setForm(prev => ({ ...prev, qr_code: base64String }));
|
|
|
|
|
- };
|
|
|
|
|
|
|
+ reader.onloadend = () => setForm(prev => ({ ...prev, qr_code: reader.result as string }));
|
|
|
reader.readAsDataURL(file);
|
|
reader.readAsDataURL(file);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const handleClearImage = () => {
|
|
const handleClearImage = () => {
|
|
|
setForm(prev => ({ ...prev, qr_code: '' }));
|
|
setForm(prev => ({ ...prev, qr_code: '' }));
|
|
|
- if (fileInputRef.current) {
|
|
|
|
|
- fileInputRef.current.value = '';
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (fileInputRef.current) fileInputRef.current.value = '';
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const handleAdd = async () => {
|
|
const handleAdd = async () => {
|
|
|
if (!form.qr_code) return alert("请上传二维码图片");
|
|
if (!form.qr_code) return alert("请上传二维码图片");
|
|
|
-
|
|
|
|
|
setSubmitting(true);
|
|
setSubmitting(true);
|
|
|
try {
|
|
try {
|
|
|
- const payload = {
|
|
|
|
|
|
|
+ await api.post('/api/vas/payment_qr/create', {
|
|
|
provider: providerName,
|
|
provider: providerName,
|
|
|
- qr_code: form.qr_code,
|
|
|
|
|
- device: form.device,
|
|
|
|
|
|
|
+ ...form,
|
|
|
is_active: Number(form.is_active),
|
|
is_active: Number(form.is_active),
|
|
|
priority: Number(form.priority),
|
|
priority: Number(form.priority),
|
|
|
- description: form.description
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- await api.post('/api/vas/payment_qr/create', payload);
|
|
|
|
|
-
|
|
|
|
|
|
|
+ });
|
|
|
alert("添加成功");
|
|
alert("添加成功");
|
|
|
fetchQrs();
|
|
fetchQrs();
|
|
|
-
|
|
|
|
|
setForm({ qr_code: '', description: '', device: '', priority: 10, is_active: 1 });
|
|
setForm({ qr_code: '', description: '', device: '', priority: 10, is_active: 1 });
|
|
|
if (fileInputRef.current) fileInputRef.current.value = '';
|
|
if (fileInputRef.current) fileInputRef.current.value = '';
|
|
|
-
|
|
|
|
|
|
|
+ setActiveTab('list'); // 添加成功后切回列表
|
|
|
} catch (e: any) {
|
|
} catch (e: any) {
|
|
|
- console.error(e);
|
|
|
|
|
alert("添加失败: " + (e.response?.data?.message || "未知错误"));
|
|
alert("添加失败: " + (e.response?.data?.message || "未知错误"));
|
|
|
} finally {
|
|
} finally {
|
|
|
setSubmitting(false);
|
|
setSubmitting(false);
|
|
@@ -131,7 +104,7 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
|
|
|
const handleDelete = async (id: number) => {
|
|
const handleDelete = async (id: number) => {
|
|
|
if (!confirm("确定删除此二维码吗?")) return;
|
|
if (!confirm("确定删除此二维码吗?")) return;
|
|
|
try {
|
|
try {
|
|
|
- await api.delete(`/api/vas/payment_qr/${id}`); // 如果API有变动,请调整此处
|
|
|
|
|
|
|
+ await api.delete(`/api/vas/payment_qr/${id}`);
|
|
|
fetchQrs();
|
|
fetchQrs();
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
alert("删除失败");
|
|
alert("删除失败");
|
|
@@ -141,9 +114,7 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
|
|
|
const handleToggleActive = async (qr: PaymentQr) => {
|
|
const handleToggleActive = async (qr: PaymentQr) => {
|
|
|
setTogglingId(qr.id);
|
|
setTogglingId(qr.id);
|
|
|
try {
|
|
try {
|
|
|
- await api.put(`/api/vas/payment_qr/${qr.id}`, { // 假设更新接口
|
|
|
|
|
- is_active: !qr.is_active
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ await api.put(`/api/vas/payment_qr/${qr.id}`, { is_active: !qr.is_active });
|
|
|
fetchQrs();
|
|
fetchQrs();
|
|
|
} catch (e: any) {
|
|
} catch (e: any) {
|
|
|
alert("状态更新失败");
|
|
alert("状态更新失败");
|
|
@@ -152,116 +123,109 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- const isRenderableImage = (content: string) => {
|
|
|
|
|
- return content && (content.startsWith('data:image') || content.startsWith('http'));
|
|
|
|
|
- };
|
|
|
|
|
|
|
+ const isRenderableImage = (content: string) => content && (content.startsWith('data:image') || content.startsWith('http'));
|
|
|
|
|
|
|
|
if (!isOpen) return null;
|
|
if (!isOpen) return null;
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
|
|
|
|
|
- <div className="bg-white rounded-xl shadow-2xl w-full max-w-5xl overflow-hidden flex flex-col h-[85vh] animate-in zoom-in duration-200">
|
|
|
|
|
|
|
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-0 md:p-4">
|
|
|
|
|
+ <div className="bg-white w-full h-full md:w-full md:max-w-5xl md:h-[85vh] md:rounded-xl shadow-2xl flex flex-col overflow-hidden animate-in zoom-in duration-200">
|
|
|
|
|
|
|
|
{/* Header */}
|
|
{/* Header */}
|
|
|
- <div className="px-6 py-4 border-b flex justify-between items-center bg-slate-50">
|
|
|
|
|
|
|
+ <div className="px-4 py-3 md:px-6 md:py-4 border-b flex justify-between items-center bg-slate-50 flex-shrink-0">
|
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex items-center gap-3">
|
|
|
- <div className="bg-green-100 p-2 rounded-lg text-green-600">
|
|
|
|
|
|
|
+ <div className="bg-green-100 p-2 rounded-lg text-green-600 hidden md:block">
|
|
|
<QrCode size={24} />
|
|
<QrCode size={24} />
|
|
|
</div>
|
|
</div>
|
|
|
<div>
|
|
<div>
|
|
|
- <h3 className="font-bold text-gray-900 text-lg">静态收款码管理</h3>
|
|
|
|
|
- <p className="text-sm text-gray-500">
|
|
|
|
|
- 当前服务商代码: <span className="font-mono font-medium text-blue-600 bg-blue-50 px-1 rounded">{providerName}</span>
|
|
|
|
|
|
|
+ <h3 className="font-bold text-gray-900 text-base md:text-lg">静态收款码管理</h3>
|
|
|
|
|
+ <p className="text-xs md:text-sm text-gray-500">
|
|
|
|
|
+ 服务商: <span className="font-mono font-medium text-blue-600 bg-blue-50 px-1 rounded">{providerName}</span>
|
|
|
</p>
|
|
</p>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
- <button onClick={onClose} className="text-gray-400 hover:text-gray-600 transition">
|
|
|
|
|
|
|
+ <button onClick={onClose} className="text-gray-400 hover:text-gray-600 p-1">
|
|
|
<X size={24} />
|
|
<X size={24} />
|
|
|
</button>
|
|
</button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
+ {/* Mobile Tabs */}
|
|
|
|
|
+ <div className="flex md:hidden border-b border-gray-200">
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => setActiveTab('list')}
|
|
|
|
|
+ className={`flex-1 py-3 text-sm font-medium ${activeTab === 'list' ? 'text-blue-600 border-b-2 border-blue-600 bg-blue-50/50' : 'text-gray-500'}`}
|
|
|
|
|
+ >
|
|
|
|
|
+ 二维码列表 ({qrs.length})
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => setActiveTab('add')}
|
|
|
|
|
+ className={`flex-1 py-3 text-sm font-medium ${activeTab === 'add' ? 'text-blue-600 border-b-2 border-blue-600 bg-blue-50/50' : 'text-gray-500'}`}
|
|
|
|
|
+ >
|
|
|
|
|
+ 添加新码
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
{/* Content */}
|
|
{/* Content */}
|
|
|
- <div className="flex flex-1 overflow-hidden">
|
|
|
|
|
|
|
+ <div className="flex flex-1 overflow-hidden relative">
|
|
|
|
|
|
|
|
- {/* 左侧:添加/编辑表单 */}
|
|
|
|
|
- <div className="w-1/3 bg-slate-50 border-r p-5 overflow-y-auto">
|
|
|
|
|
|
|
+ {/* 左侧:添加表单 (Desktop: 30%, Mobile: 根据 Tab 显示) */}
|
|
|
|
|
+ <div className={`
|
|
|
|
|
+ flex-col bg-slate-50 overflow-y-auto border-r border-slate-200 p-5
|
|
|
|
|
+ ${activeTab === 'add' ? 'flex w-full' : 'hidden'}
|
|
|
|
|
+ md:flex md:w-1/3
|
|
|
|
|
+ `}>
|
|
|
<h4 className="text-sm font-bold text-slate-800 mb-4 flex items-center gap-2">
|
|
<h4 className="text-sm font-bold text-slate-800 mb-4 flex items-center gap-2">
|
|
|
<Plus size={16} /> 添加收款码
|
|
<Plus size={16} /> 添加收款码
|
|
|
</h4>
|
|
</h4>
|
|
|
<div className="space-y-4">
|
|
<div className="space-y-4">
|
|
|
|
|
|
|
|
- {/* 图片上传区域 */}
|
|
|
|
|
|
|
+ {/* 图片上传 */}
|
|
|
<div>
|
|
<div>
|
|
|
<label className="text-xs font-bold text-slate-500 mb-2 block">二维码图片 *</label>
|
|
<label className="text-xs font-bold text-slate-500 mb-2 block">二维码图片 *</label>
|
|
|
-
|
|
|
|
|
- <input
|
|
|
|
|
- type="file"
|
|
|
|
|
- accept="image/*"
|
|
|
|
|
- ref={fileInputRef}
|
|
|
|
|
- className="hidden"
|
|
|
|
|
- onChange={handleFileChange}
|
|
|
|
|
- />
|
|
|
|
|
-
|
|
|
|
|
|
|
+ <input type="file" accept="image/*" ref={fileInputRef} className="hidden" onChange={handleFileChange} />
|
|
|
{!form.qr_code ? (
|
|
{!form.qr_code ? (
|
|
|
- <div
|
|
|
|
|
- onClick={() => fileInputRef.current?.click()}
|
|
|
|
|
- className="border-2 border-dashed border-slate-300 rounded-lg h-32 flex flex-col items-center justify-center cursor-pointer hover:border-blue-500 hover:bg-blue-50 transition bg-white"
|
|
|
|
|
- >
|
|
|
|
|
|
|
+ <div onClick={() => fileInputRef.current?.click()} className="border-2 border-dashed border-slate-300 rounded-lg h-32 flex flex-col items-center justify-center cursor-pointer hover:border-blue-500 hover:bg-blue-50 transition bg-white">
|
|
|
<Upload size={24} className="text-slate-400 mb-2" />
|
|
<Upload size={24} className="text-slate-400 mb-2" />
|
|
|
<span className="text-xs text-slate-600">点击选择图片</span>
|
|
<span className="text-xs text-slate-600">点击选择图片</span>
|
|
|
- <span className="text-[10px] text-slate-400 mt-1">支持 PNG, JPG (Max 2MB)</span>
|
|
|
|
|
</div>
|
|
</div>
|
|
|
) : (
|
|
) : (
|
|
|
<div className="relative border rounded-lg bg-white p-2">
|
|
<div className="relative border rounded-lg bg-white p-2">
|
|
|
- <img
|
|
|
|
|
- src={form.qr_code}
|
|
|
|
|
- alt="Preview"
|
|
|
|
|
- className="w-full h-40 object-contain rounded"
|
|
|
|
|
- />
|
|
|
|
|
- <button
|
|
|
|
|
- onClick={handleClearImage}
|
|
|
|
|
- className="absolute top-2 right-2 bg-red-100 text-red-600 p-1.5 rounded-full hover:bg-red-200 transition shadow-sm"
|
|
|
|
|
- title="移除图片"
|
|
|
|
|
- >
|
|
|
|
|
|
|
+ <img src={form.qr_code} alt="Preview" className="w-full h-40 object-contain rounded" />
|
|
|
|
|
+ <button onClick={handleClearImage} className="absolute top-2 right-2 bg-red-100 text-red-600 p-1.5 rounded-full hover:bg-red-200">
|
|
|
<Trash2 size={14} />
|
|
<Trash2 size={14} />
|
|
|
</button>
|
|
</button>
|
|
|
- <div className="text-[10px] text-slate-400 mt-2 text-center truncate px-2">
|
|
|
|
|
- Base64 ({form.qr_code.length} chars)
|
|
|
|
|
- </div>
|
|
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div>
|
|
<div>
|
|
|
- <label className="text-xs font-bold text-slate-500 mb-1 block">备注 (Description)</label>
|
|
|
|
|
|
|
+ <label className="text-xs font-bold text-slate-500 mb-1 block">备注</label>
|
|
|
<input
|
|
<input
|
|
|
- type="text" className="w-full border border-slate-300 rounded p-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
|
|
|
|
|
|
|
+ type="text" className="w-full border border-slate-300 rounded p-2 text-sm"
|
|
|
placeholder="例如:支付宝个人收款码-01"
|
|
placeholder="例如:支付宝个人收款码-01"
|
|
|
- value={form.description}
|
|
|
|
|
- onChange={e => setForm({...form, description: e.target.value})}
|
|
|
|
|
|
|
+ value={form.description} onChange={e => setForm({...form, description: e.target.value})}
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div>
|
|
<div>
|
|
|
- <label className="text-xs font-bold text-slate-500 mb-1 block">设备名称 (Device)</label>
|
|
|
|
|
|
|
+ <label className="text-xs font-bold text-slate-500 mb-1 block">设备名称</label>
|
|
|
<input
|
|
<input
|
|
|
- type="text" className="w-full border border-slate-300 rounded p-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
|
|
|
|
|
|
|
+ type="text" className="w-full border border-slate-300 rounded p-2 text-sm"
|
|
|
placeholder="例如:iPhone 13"
|
|
placeholder="例如:iPhone 13"
|
|
|
- value={form.device}
|
|
|
|
|
- onChange={e => setForm({...form, device: e.target.value})}
|
|
|
|
|
|
|
+ value={form.device} onChange={e => setForm({...form, device: e.target.value})}
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
<div>
|
|
<div>
|
|
|
- <label className="text-xs font-bold text-slate-500 mb-1 block">权重 (Priority)</label>
|
|
|
|
|
- <input type="number" className="w-full border rounded p-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
|
|
|
|
|
|
|
+ <label className="text-xs font-bold text-slate-500 mb-1 block">权重</label>
|
|
|
|
|
+ <input type="number" className="w-full border rounded p-2 text-sm"
|
|
|
value={form.priority} onChange={e => setForm({...form, priority: parseInt(e.target.value) || 0})}
|
|
value={form.priority} onChange={e => setForm({...form, priority: parseInt(e.target.value) || 0})}
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
<div>
|
|
<div>
|
|
|
<label className="text-xs font-bold text-slate-500 mb-1 block">状态</label>
|
|
<label className="text-xs font-bold text-slate-500 mb-1 block">状态</label>
|
|
|
- <select className="w-full border rounded p-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
|
|
|
|
|
|
|
+ <select className="w-full border rounded p-2 text-sm"
|
|
|
value={form.is_active} onChange={e => setForm({...form, is_active: parseInt(e.target.value)})}
|
|
value={form.is_active} onChange={e => setForm({...form, is_active: parseInt(e.target.value)})}
|
|
|
>
|
|
>
|
|
|
<option value={1}>启用</option>
|
|
<option value={1}>启用</option>
|
|
@@ -270,20 +234,20 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <button
|
|
|
|
|
- onClick={handleAdd}
|
|
|
|
|
- disabled={submitting}
|
|
|
|
|
- className="w-full bg-blue-600 text-white py-2.5 rounded-lg text-sm font-bold hover:bg-blue-700 flex justify-center items-center gap-2 mt-4 shadow-sm transition disabled:opacity-50"
|
|
|
|
|
- >
|
|
|
|
|
|
|
+ <button onClick={handleAdd} disabled={submitting} className="w-full bg-blue-600 text-white py-2.5 rounded-lg text-sm font-bold hover:bg-blue-700 flex justify-center items-center gap-2 mt-4 shadow-sm transition disabled:opacity-50">
|
|
|
{submitting ? <Loader2 className="animate-spin" size={16} /> : <Save size={16} />}
|
|
{submitting ? <Loader2 className="animate-spin" size={16} /> : <Save size={16} />}
|
|
|
提交新增
|
|
提交新增
|
|
|
</button>
|
|
</button>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- {/* 右侧:预览列表 */}
|
|
|
|
|
- <div className="w-2/3 p-6 overflow-y-auto bg-white">
|
|
|
|
|
- <div className="flex justify-between items-center mb-4">
|
|
|
|
|
|
|
+ {/* 右侧:预览列表 (Desktop: 70%, Mobile: 根据 Tab 显示) */}
|
|
|
|
|
+ <div className={`
|
|
|
|
|
+ flex-col bg-white p-4 md:p-6 overflow-y-auto
|
|
|
|
|
+ ${activeTab === 'list' ? 'flex w-full' : 'hidden'}
|
|
|
|
|
+ md:flex md:w-2/3
|
|
|
|
|
+ `}>
|
|
|
|
|
+ <div className="hidden md:flex justify-between items-center mb-4">
|
|
|
<h4 className="text-sm font-bold text-slate-800">现有收款码 ({qrs.length})</h4>
|
|
<h4 className="text-sm font-bold text-slate-800">现有收款码 ({qrs.length})</h4>
|
|
|
<span className="text-xs text-slate-400">系统将根据权重轮询使用</span>
|
|
<span className="text-xs text-slate-400">系统将根据权重轮询使用</span>
|
|
|
</div>
|
|
</div>
|
|
@@ -299,95 +263,38 @@ export default function QrManager({ providerId, providerName, isOpen, onClose }:
|
|
|
<div className="grid grid-cols-1 gap-4">
|
|
<div className="grid grid-cols-1 gap-4">
|
|
|
{qrs.map((item) => {
|
|
{qrs.map((item) => {
|
|
|
const isActive = Boolean(item.is_active);
|
|
const isActive = Boolean(item.is_active);
|
|
|
-
|
|
|
|
|
return (
|
|
return (
|
|
|
- <div
|
|
|
|
|
- key={item.id}
|
|
|
|
|
- className={`border rounded-lg p-4 flex gap-5 items-start transition group
|
|
|
|
|
- ${isActive ? 'border-slate-200 hover:border-blue-300 hover:shadow-sm' : 'border-slate-100 bg-slate-50 opacity-75'}
|
|
|
|
|
- `}
|
|
|
|
|
- >
|
|
|
|
|
|
|
+ <div key={item.id} className={`border rounded-lg p-4 flex gap-4 items-start transition ${isActive ? 'border-slate-200' : 'border-slate-100 bg-slate-50 opacity-75'}`}>
|
|
|
|
|
|
|
|
- {/* 图片预览区 */}
|
|
|
|
|
- <div className="w-24 h-24 bg-white rounded-lg flex items-center justify-center flex-shrink-0 border border-slate-100 overflow-hidden relative">
|
|
|
|
|
|
|
+ {/* 缩略图 */}
|
|
|
|
|
+ <div className="w-20 h-20 md:w-24 md:h-24 bg-white rounded-lg flex items-center justify-center flex-shrink-0 border border-slate-100 overflow-hidden relative">
|
|
|
{isRenderableImage(item.qr_code) ? (
|
|
{isRenderableImage(item.qr_code) ? (
|
|
|
- <img
|
|
|
|
|
- src={item.qr_code}
|
|
|
|
|
- alt="QR Preview"
|
|
|
|
|
- className={`w-full h-full object-contain ${!isActive ? 'grayscale' : ''}`}
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ <img src={item.qr_code} alt="QR" className={`w-full h-full object-contain ${!isActive ? 'grayscale' : ''}`} />
|
|
|
) : (
|
|
) : (
|
|
|
- <div className="text-center">
|
|
|
|
|
- <QrCode size={24} className="text-gray-400 mx-auto mb-1" />
|
|
|
|
|
- <span className="text-[10px] text-gray-400">无效图片</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- {!isActive && (
|
|
|
|
|
- <div className="absolute inset-0 bg-gray-900/60 flex items-center justify-center backdrop-blur-[1px]">
|
|
|
|
|
- <span className="text-white text-[10px] font-bold px-2 py-1 bg-black/60 rounded shadow-sm border border-white/20">
|
|
|
|
|
- 已禁用
|
|
|
|
|
- </span>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <QrCode size={24} className="text-gray-400" />
|
|
|
)}
|
|
)}
|
|
|
|
|
+ {!isActive && <div className="absolute inset-0 bg-black/50 flex items-center justify-center"><span className="text-white text-[10px] font-bold px-2 py-1 bg-black/60 rounded">已禁用</span></div>}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- {/* 信息区 */}
|
|
|
|
|
- <div className="flex-1 min-w-0 py-1 space-y-2">
|
|
|
|
|
|
|
+ {/* 信息 */}
|
|
|
|
|
+ <div className="flex-1 min-w-0 space-y-1.5">
|
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
|
- <span className="text-xs font-mono bg-slate-100 px-2 py-0.5 rounded text-slate-600 font-bold">ID: {item.id}</span>
|
|
|
|
|
-
|
|
|
|
|
- {isActive ?
|
|
|
|
|
- <span className="text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded font-medium">Running</span> :
|
|
|
|
|
- <span className="text-xs bg-gray-200 text-gray-600 px-2 py-0.5 rounded font-medium">Stopped</span>
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- <span className="text-xs text-blue-600 bg-blue-50 px-2 py-0.5 rounded font-medium">权重: {item.priority}</span>
|
|
|
|
|
-
|
|
|
|
|
- {item.device && (
|
|
|
|
|
- <span className="text-xs bg-orange-50 text-orange-700 px-2 py-0.5 rounded font-medium flex items-center gap-1 border border-orange-100">
|
|
|
|
|
- <Smartphone size={10} /> {item.device}
|
|
|
|
|
- </span>
|
|
|
|
|
- )}
|
|
|
|
|
|
|
+ <span className="text-xs font-mono bg-slate-100 px-2 py-0.5 rounded text-slate-600 font-bold">#{item.id}</span>
|
|
|
|
|
+ <span className="text-xs text-blue-600 bg-blue-50 px-2 py-0.5 rounded font-medium">权重 {item.priority}</span>
|
|
|
|
|
+ {item.device && <span className="text-xs bg-orange-50 text-orange-700 px-2 py-0.5 rounded font-medium flex items-center gap-1 border border-orange-100"><Smartphone size={10} /> {item.device}</span>}
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
- <p className="text-sm font-bold text-gray-800">
|
|
|
|
|
- {item.description || "未命名收款码"}
|
|
|
|
|
- </p>
|
|
|
|
|
-
|
|
|
|
|
- <div className="bg-slate-50 p-2 rounded text-[10px] font-mono text-slate-500 break-all border h-10 overflow-hidden relative">
|
|
|
|
|
- {item.qr_code ? item.qr_code.substring(0, 100) + '...' : '无数据'}
|
|
|
|
|
- <div className="absolute bottom-0 left-0 right-0 h-4 bg-gradient-to-t from-slate-50 to-transparent"></div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
|
|
+ <p className="text-sm font-bold text-gray-800 line-clamp-1">{item.description || "未命名"}</p>
|
|
|
<div className="text-[10px] text-gray-400">
|
|
<div className="text-[10px] text-gray-400">
|
|
|
- {/* 2. 使用 LocalTime 组件替代 toLocaleString */}
|
|
|
|
|
- 创建时间: {item.created_at ? <LocalTime date={item.created_at} /> : '-'}
|
|
|
|
|
|
|
+ 创建于 <LocalTime date={item.created_at} />
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- {/* 操作区 */}
|
|
|
|
|
- <div className="flex flex-col gap-2 border-l pl-4 border-slate-100">
|
|
|
|
|
- <button
|
|
|
|
|
- onClick={() => handleToggleActive(item)}
|
|
|
|
|
- disabled={togglingId === item.id}
|
|
|
|
|
- className={`transition-colors ${isActive ? 'text-green-600 hover:text-green-700' : 'text-gray-400 hover:text-gray-600'}`}
|
|
|
|
|
- title={isActive ? "点击禁用" : "点击启用"}
|
|
|
|
|
- >
|
|
|
|
|
- {togglingId === item.id ? (
|
|
|
|
|
- <Loader2 size={24} className="animate-spin text-blue-600" />
|
|
|
|
|
- ) : isActive ? (
|
|
|
|
|
- <ToggleRight size={28} />
|
|
|
|
|
- ) : (
|
|
|
|
|
- <ToggleLeft size={28} />
|
|
|
|
|
- )}
|
|
|
|
|
|
|
+ {/* 操作 */}
|
|
|
|
|
+ <div className="flex flex-col gap-2 border-l pl-3 border-slate-100">
|
|
|
|
|
+ <button onClick={() => handleToggleActive(item)} disabled={togglingId === item.id} className={isActive ? 'text-green-600' : 'text-gray-400'}>
|
|
|
|
|
+ {togglingId === item.id ? <Loader2 size={24} className="animate-spin" /> : isActive ? <ToggleRight size={28} /> : <ToggleLeft size={28} />}
|
|
|
</button>
|
|
</button>
|
|
|
-
|
|
|
|
|
- <button
|
|
|
|
|
- onClick={() => handleDelete(item.id)}
|
|
|
|
|
- className="text-slate-300 hover:text-red-500 p-1 rounded hover:bg-red-50 transition self-center mt-auto"
|
|
|
|
|
- title="删除"
|
|
|
|
|
- >
|
|
|
|
|
|
|
+ <button onClick={() => handleDelete(item.id)} className="text-slate-300 hover:text-red-500 p-1 rounded transition mt-auto">
|
|
|
<Trash2 size={18} />
|
|
<Trash2 size={18} />
|
|
|
</button>
|
|
</button>
|
|
|
</div>
|
|
</div>
|