| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259 |
- 'use client';
- import { useState, useEffect } from 'react';
- import api from '@/lib/api';
- import { Loader2, Plus, Save, X, FileJson, List, Edit3 } from 'lucide-react';
- import JsonEditor from '@/components/common/JsonEditor';
- interface SchemaManagerProps {
- isOpen: boolean;
- onClose: () => void;
- }
- interface SchemaItem {
- id: number;
- name: string;
- schema_json: string;
- description?: string;
- }
- export default function SchemaManager({ isOpen, onClose }: SchemaManagerProps) {
- const [schemas, setSchemas] = useState<SchemaItem[]>([]);
- const [loading, setLoading] = useState(false);
-
- // 移动端 Tab 状态: 'list' | 'editor'
- const [activeTab, setActiveTab] = useState<'list' | 'editor'>('list');
- // 编辑模式状态
- const [editingId, setEditingId] = useState<number | null>(null);
- const [formData, setFormData] = useState({
- name: '',
- description: '',
- schema_json: '{\n "type": "object",\n "properties": {\n "field_name": { "type": "string", "title": "示例字段" }\n }\n}'
- });
- useEffect(() => {
- if (isOpen) {
- fetchSchemas();
- setActiveTab('list'); // 打开时默认显示列表
- }
- }, [isOpen]);
- const fetchSchemas = async () => {
- setLoading(true);
- try {
- const res = await api.get('/api/vas/schema/list');
- setSchemas(Array.isArray(res.data.data) ? res.data.data : []);
- } catch (e) {
- console.warn("API Error, using mock schema data");
- setSchemas([]);
- } finally {
- setLoading(false);
- }
- };
- const handleSave = async () => {
- if (!formData.name) return alert("名称必填");
- try {
- JSON.parse(formData.schema_json); // 校验 JSON
- if (editingId) {
- await api.post(`/api/vas/schema/update`, formData, {params: {"id": editingId}});
- } else {
- await api.post('/api/vas/schema/create', formData);
- }
-
- alert("保存成功");
- fetchSchemas();
-
- // 保存后逻辑:
- // 如果是新建,保存后清空表单并留再编辑页,或者切回列表
- // 这里选择保留在当前页,但如果是移动端可以考虑切回列表
- if (!editingId) {
- setFormData({ name: '', description: '', schema_json: '{}' });
- }
- } catch (e: any) {
- alert("保存失败: " + (e.message || "JSON 格式错误"));
- }
- };
- const handleCreateNew = () => {
- setEditingId(null);
- setFormData({ name: '', description: '', schema_json: '{}' });
- setActiveTab('editor'); // 移动端自动切换到编辑器 tab
- };
- const handleEdit = (item: SchemaItem) => {
- setEditingId(item.id);
- setFormData({
- name: item.name,
- description: item.description || '',
- schema_json: typeof item.schema_json === 'string' ? item.schema_json : JSON.stringify(item.schema_json, null, 2)
- });
- setActiveTab('editor'); // 移动端自动切换到编辑器 tab
- };
- const handleDelete = async (id: number) => {
- if(!confirm("删除 Schema 可能导致关联商品失效,确定删除吗?")) return;
- try {
- await api.delete('/api/vas/schema/delete', {params: {"id": id}});
- fetchSchemas();
- if (editingId === id) {
- setEditingId(null);
- setFormData({ name: '', description: '', schema_json: '{}' });
- setActiveTab('list'); // 删除后切回列表
- }
- } catch (e) { alert("删除失败"); }
- };
- if (!isOpen) return null;
- return (
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-0 md:p-4">
- {/*
- 容器:
- - 移动端:全屏 (w-full h-full),无圆角
- - 桌面端:固定大小 (max-w-4xl h-[85vh]),有圆角
- */}
- <div className="bg-white w-full h-full md:w-full md:max-w-4xl md:h-[85vh] md:rounded-xl shadow-2xl flex flex-col overflow-hidden animate-in zoom-in duration-200">
-
- {/* Header */}
- <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">
- <h3 className="font-bold text-gray-900 text-lg flex items-center gap-2">
- <FileJson className="text-purple-600" /> 表单 Schema 管理
- </h3>
- <button onClick={onClose} className="text-gray-400 hover:text-gray-600 p-1">
- <X size={24} />
- </button>
- </div>
- {/* Mobile Tabs (仅移动端显示) */}
- <div className="flex md:hidden border-b border-gray-200 flex-shrink-0">
- <button
- onClick={() => setActiveTab('list')}
- className={`flex-1 py-3 text-sm font-medium flex items-center justify-center gap-2 ${
- activeTab === 'list' ? 'text-blue-600 border-b-2 border-blue-600 bg-blue-50/50' : 'text-gray-500'
- }`}
- >
- <List size={16} /> 列表
- </button>
- <button
- onClick={() => setActiveTab('editor')}
- className={`flex-1 py-3 text-sm font-medium flex items-center justify-center gap-2 ${
- activeTab === 'editor' ? 'text-blue-600 border-b-2 border-blue-600 bg-blue-50/50' : 'text-gray-500'
- }`}
- >
- <Edit3 size={16} /> {editingId ? '编辑' : '新建'}
- </button>
- </div>
- {/* Content Body */}
- <div className="flex flex-1 overflow-hidden relative">
-
- {/*
- 左侧:列表
- - 移动端:根据 activeTab === 'list' 显示/隐藏
- - 桌面端:md:flex md:w-1/3
- */}
- <div className={`
- flex-col bg-slate-50 border-r border-slate-200 overflow-y-auto p-4
- ${activeTab === 'list' ? 'flex w-full' : 'hidden'}
- md:flex md:w-1/3
- `}>
- <button
- onClick={handleCreateNew}
- className="w-full mb-4 py-2.5 border-2 border-dashed border-blue-300 text-blue-600 rounded-lg hover:bg-blue-50 flex items-center justify-center gap-2 font-bold text-sm transition"
- >
- <Plus size={16} /> 新建 Schema
- </button>
- <div className="space-y-2">
- {loading && <div className="text-center py-4 text-gray-400"><Loader2 className="animate-spin inline mr-2"/> 加载中...</div>}
- {!loading && schemas.length === 0 && <div className="text-center py-10 text-gray-400 text-xs">暂无数据</div>}
-
- {schemas.map(s => (
- <div
- key={s.id}
- onClick={() => handleEdit(s)}
- className={`p-3 rounded-lg cursor-pointer border transition ${
- editingId === s.id
- ? 'bg-white border-blue-500 shadow-md ring-1 ring-blue-500'
- : 'bg-white border-slate-200 hover:border-blue-300'
- }`}
- >
- <div className="font-bold text-slate-800 text-sm">{s.name}</div>
- <div className="text-xs text-slate-500 truncate mt-1">ID: {s.id}</div>
- </div>
- ))}
- </div>
- </div>
- {/*
- 右侧:编辑区
- - 移动端:根据 activeTab === 'editor' 显示/隐藏
- - 桌面端:md:flex md:w-2/3
- */}
- <div className={`
- flex-col p-4 md:p-6 overflow-y-auto w-full
- ${activeTab === 'editor' ? 'flex' : 'hidden'}
- md:flex md:w-2/3
- `}>
- <h4 className="text-lg font-bold mb-4 border-b pb-2 flex justify-between items-center">
- <span>{editingId ? `编辑 Schema #${editingId}` : '创建新 Schema'}</span>
- {editingId && (
- <button onClick={() => handleDelete(editingId)} className="text-red-500 hover:text-red-700 text-xs flex items-center gap-1 md:hidden">
- <Trash2 size={14} /> 删除
- </button>
- )}
- </h4>
-
- <div className="space-y-4">
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
- <div>
- <label className="block text-sm font-medium mb-1 text-slate-700">名称 (Name) <span className="text-red-500">*</span></label>
- <input
- type="text"
- className="w-full border border-slate-300 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
- value={formData.name}
- onChange={e => setFormData({...formData, name: e.target.value})}
- placeholder="e.g. Japan Visa Form"
- />
- </div>
- <div>
- <label className="block text-sm font-medium mb-1 text-slate-700">描述 (Desc)</label>
- <input
- type="text"
- className="w-full border border-slate-300 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
- value={formData.description}
- onChange={e => setFormData({...formData, description: e.target.value})}
- placeholder="简短描述用途"
- />
- </div>
- </div>
- {/* JSON Editor */}
- <div className="flex-1 min-h-[300px]">
- <JsonEditor
- label="Schema JSON (定义动态表单结构)"
- value={formData.schema_json}
- onChange={(val) => setFormData({...formData, schema_json: val})}
- height="h-[400px] md:h-80" // 移动端稍微高一点
- placeholder='{"type": "object", ...}'
- />
- </div>
- <div className="flex justify-end gap-3 pt-4 border-t mt-auto">
- {editingId && (
- <button onClick={() => handleDelete(editingId)} className="text-red-600 px-4 text-sm hover:underline hidden md:block">删除</button>
- )}
- <button onClick={handleSave} className="bg-blue-600 text-white px-6 py-2.5 rounded-lg font-bold shadow hover:bg-blue-700 flex items-center gap-2 transition w-full md:w-auto justify-center">
- <Save size={18} /> 保存配置
- </button>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- );
- }
|