| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207 |
- 'use client';
- import { useState, useEffect, useMemo } from 'react';
- import api from '@/lib/api';
- import {
- Loader2, Save, RefreshCw, Settings2, Plus, Trash2, AlertCircle
- } from 'lucide-react';
- import { toast } from 'react-hot-toast'; // 假设你项目中使用了 react-hot-toast,如果没有可以用 alert 替代
- export default function TroovBookLimitManager() {
- const [limits, setLimits] = useState<Record<string, number>>({});
- const [loading, setLoading] = useState(false);
- const [saving, setSaving] = useState(false);
-
- // 新增时间点的表单状态
- const [newTime, setNewTime] = useState('');
- const [newValue, setNewValue] = useState(1);
- useEffect(() => {
- fetchConfig();
- }, []);
- // 获取配置
- const fetchConfig = async () => {
- setLoading(true);
- try {
- const res = await api.get('/api/dynamic-configurations/key/troov.book.limit');
- let val = res.data?.data?.config_value;
-
- // 兼容后端可能返回 JSON 字符串或直接返回对象的情况
- if (typeof val === 'string') {
- try { val = JSON.parse(val); } catch (e) { val = {}; }
- }
- setLimits(val || {});
- } catch (error) {
- console.error('获取配置失败:', error);
- toast.error('加载名额配置失败');
- } finally {
- setLoading(false);
- }
- };
- // 保存配置
- const handleSave = async () => {
- setSaving(true);
- try {
- // 按照你提供的 curl 示例,PUT 接口需要将 config_value 作为序列化后的 JSON 字符串传入
- await api.put('/api/dynamic-configurations/key/troov.book.limit', {
- config_value: JSON.stringify(limits)
- });
- toast.success('配置保存成功!');
- fetchConfig();
- } catch (error) {
- console.error('保存配置失败:', error);
- toast.error('保存配置失败,请重试');
- } finally {
- setSaving(false);
- }
- };
- // 更新某个时间点的值
- const handleUpdateLimit = (time: string, value: string) => {
- const num = parseInt(value, 10);
- setLimits(prev => ({
- ...prev,
- [time]: isNaN(num) ? 0 : Math.max(0, num) // 保证不小于0
- }));
- };
- // 删除某个时间点
- const handleDeleteTime = (time: string) => {
- if (!confirm(`确定要删除 ${time} 的配置吗?`)) return;
- const newLimits = { ...limits };
- delete newLimits[time];
- setLimits(newLimits);
- };
- // 新增时间点
- const handleAddTime = (e: React.FormEvent) => {
- e.preventDefault();
- if (!newTime) return;
-
- // 简单校验格式 HH:mm
- if (!/^\d{2}:\d{2}$/.test(newTime)) {
- return toast.error('时间格式需为 HH:mm,如 09:30');
- }
- if (limits[newTime] !== undefined) {
- return toast.error('该时间点已存在!');
- }
- setLimits(prev => ({ ...prev, [newTime]: newValue }));
- setNewTime('');
- setNewValue(1);
- };
- // 对时间键进行排序 (09:00, 09:15, 10:00...) 方便展示
- const sortedTimes = useMemo(() => {
- return Object.keys(limits).sort((a, b) => {
- const [hA, mA] = a.split(':').map(Number);
- const [hB, mB] = b.split(':').map(Number);
- return (hA * 60 + mA) - (hB * 60 + mB);
- });
- }, [limits]);
- return (
- <div className="bg-white border border-slate-200 rounded-xl shadow-sm overflow-hidden flex flex-col">
- {/* 头部 */}
- <div className="p-4 border-b border-slate-200 bg-slate-50 flex flex-wrap gap-4 items-center justify-between">
- <h2 className="font-bold text-slate-800 flex items-center gap-2">
- <Settings2 size={18} className="text-teal-600" />
- Troov 预约名额限制配置
- </h2>
-
- <div className="flex items-center gap-3">
- <button
- onClick={fetchConfig}
- disabled={loading}
- className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-slate-600 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 transition disabled:opacity-50"
- >
- <RefreshCw size={14} className={loading ? 'animate-spin' : ''} /> 刷新
- </button>
-
- <button
- onClick={handleSave}
- disabled={saving}
- className="flex items-center gap-1.5 px-4 py-1.5 text-sm font-bold text-white bg-teal-600 rounded-lg shadow-sm hover:bg-teal-700 transition disabled:opacity-50"
- >
- {saving ? <Loader2 size={14} className="animate-spin" /> : <Save size={14} />}
- 保存配置
- </button>
- </div>
- </div>
- {/* 主体内容 */}
- <div className="p-4 md:p-6 bg-slate-50/30">
-
- {loading && Object.keys(limits).length === 0 ? (
- <div className="py-12 flex justify-center text-slate-400">
- <Loader2 className="animate-spin" size={24} />
- </div>
- ) : (
- <div className="space-y-6">
-
- {/* 警告提示 */}
- <div className="flex items-start gap-2 bg-amber-50 text-amber-800 p-3 rounded-lg border border-amber-200 text-sm">
- <AlertCircle size={16} className="mt-0.5 shrink-0" />
- <p>修改此配置将实时影响 Troov 法签机器人对应时间点的<strong>单日最大预约数</strong>。点击右上角“保存配置”后生效。</p>
- </div>
- {/* 配置网格 */}
- <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
- {sortedTimes.map((time) => (
- <div key={time} className="flex items-center justify-between bg-white border border-slate-200 p-2 rounded-lg shadow-sm group">
- <span className="font-mono font-bold text-slate-700 text-sm pl-1">{time}</span>
- <div className="flex items-center gap-1">
- <input
- type="number"
- min="0"
- className="w-14 text-center text-sm font-bold border border-slate-200 rounded p-1 outline-none focus:border-teal-500 focus:ring-1 focus:ring-teal-500"
- value={limits[time]}
- onChange={(e) => handleUpdateLimit(time, e.target.value)}
- />
- <button
- onClick={() => handleDeleteTime(time)}
- className="p-1 text-slate-300 hover:text-red-500 hover:bg-red-50 rounded transition opacity-0 group-hover:opacity-100"
- title="删除时间点"
- >
- <Trash2 size={14} />
- </button>
- </div>
- </div>
- ))}
- </div>
- {/* 快速添加时间点 */}
- <form onSubmit={handleAddTime} className="flex items-center gap-2 pt-4 border-t border-slate-100">
- <span className="text-sm font-bold text-slate-500">新增时段:</span>
- <input
- type="time"
- required
- className="border border-slate-300 rounded-md px-2 py-1.5 text-sm font-mono outline-none focus:border-teal-500"
- value={newTime}
- onChange={(e) => setNewTime(e.target.value)}
- />
- <span className="text-sm font-bold text-slate-500">数量:</span>
- <input
- type="number"
- min="1"
- required
- className="w-16 border border-slate-300 rounded-md px-2 py-1.5 text-sm outline-none focus:border-teal-500 text-center"
- value={newValue}
- onChange={(e) => setNewValue(parseInt(e.target.value) || 1)}
- />
- <button
- type="submit"
- className="flex items-center gap-1 px-3 py-1.5 bg-slate-100 hover:bg-slate-200 text-slate-700 text-sm font-medium rounded-md transition"
- >
- <Plus size={14} /> 添加
- </button>
- </form>
-
- </div>
- )}
- </div>
- </div>
- );
- }
|