feat: add data editing functionality with JSON support; integrate CodeMirror for JSON input

This commit is contained in:
2026-02-07 02:36:06 +08:00
parent aaaeb873ac
commit e06b269374
4 changed files with 310 additions and 3 deletions

View File

@@ -12,6 +12,7 @@
"dependencies": {
"@ant-design/icons": "^6.1.0",
"@base-ui/react": "^1.1.0",
"@codemirror/lang-json": "^6.0.2",
"@kevisual/api": "^0.0.44",
"@kevisual/cache": "^0.0.5",
"@kevisual/query": "^0.0.39",
@@ -28,6 +29,7 @@
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-table": "^8.21.3",
"@uiw/react-codemirror": "^4.25.4",
"antd": "^6.2.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",

201
pnpm-lock.yaml generated
View File

@@ -14,6 +14,9 @@ importers:
'@base-ui/react':
specifier: ^1.1.0
version: 1.1.0(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@codemirror/lang-json':
specifier: ^6.0.2
version: 6.0.2
'@kevisual/api':
specifier: ^0.0.44
version: 0.0.44
@@ -62,6 +65,9 @@ importers:
'@tanstack/react-table':
specifier: ^8.21.3
version: 8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@uiw/react-codemirror':
specifier: ^4.25.4
version: 4.25.4(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.20.0)(@codemirror/language@6.12.1)(@codemirror/lint@6.9.3)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.39.12)(codemirror@6.0.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
antd:
specifier: ^6.2.3
version: 6.2.3(date-fns@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -236,6 +242,33 @@ packages:
'@types/react':
optional: true
'@codemirror/autocomplete@6.20.0':
resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==}
'@codemirror/commands@6.10.2':
resolution: {integrity: sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==}
'@codemirror/lang-json@6.0.2':
resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==}
'@codemirror/language@6.12.1':
resolution: {integrity: sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==}
'@codemirror/lint@6.9.3':
resolution: {integrity: sha512-y3YkYhdnhjDBAe0VIA0c4wVoFOvnp8CnAvfLqi0TqotIv92wIlAAP7HELOpLBsKwjAX6W92rSflA6an/2zBvXw==}
'@codemirror/search@6.6.0':
resolution: {integrity: sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==}
'@codemirror/state@6.5.4':
resolution: {integrity: sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==}
'@codemirror/theme-one-dark@6.1.3':
resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==}
'@codemirror/view@6.39.12':
resolution: {integrity: sha512-f+/VsHVn/kOA9lltk/GFzuYwVVAKmOnNjxbrhkk3tPHntFqjWeI2TbIXx006YkBkqC10wZ4NsnWXCQiFPeAISQ==}
'@date-fns/tz@1.4.1':
resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==}
@@ -464,6 +497,21 @@ packages:
peerDependencies:
dotenv: ^17
'@lezer/common@1.5.1':
resolution: {integrity: sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==}
'@lezer/highlight@1.2.3':
resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==}
'@lezer/json@1.0.3':
resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==}
'@lezer/lr@1.4.8':
resolution: {integrity: sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==}
'@marijn/find-cluster-break@1.0.2':
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
'@next/env@16.1.6':
resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==}
@@ -1650,6 +1698,28 @@ packages:
'@types/react@19.2.7':
resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==}
'@uiw/codemirror-extensions-basic-setup@4.25.4':
resolution: {integrity: sha512-YzNwkm0AbPv1EXhCHYR5v0nqfemG2jEB0Z3Att4rBYqKrlG7AA9Rhjc3IyBaOzsBu18wtrp9/+uhTyu7TXSRng==}
peerDependencies:
'@codemirror/autocomplete': '>=6.0.0'
'@codemirror/commands': '>=6.0.0'
'@codemirror/language': '>=6.0.0'
'@codemirror/lint': '>=6.0.0'
'@codemirror/search': '>=6.0.0'
'@codemirror/state': '>=6.0.0'
'@codemirror/view': '>=6.0.0'
'@uiw/react-codemirror@4.25.4':
resolution: {integrity: sha512-ipO067oyfUw+DVaXhQCxkB0ZD9b7RnY+ByrprSYSKCHaULvJ3sqWYC/Zen6zVQ8/XC4o5EPBfatGiX20kC7XGA==}
peerDependencies:
'@babel/runtime': '>=7.11.0'
'@codemirror/state': '>=6.0.0'
'@codemirror/theme-one-dark': '>=6.0.0'
'@codemirror/view': '>=6.0.0'
codemirror: '>=6.0.0'
react: '>=17.0.0'
react-dom: '>=17.0.0'
antd@6.2.3:
resolution: {integrity: sha512-q92r7/hcQAR2iv6CCysdz7c2Pdl/3nhslc3azF9e6AEl4knO6v+nlaeor1oF2jBanZ/tiw2m3NprOVUgPDvyhg==}
peerDependencies:
@@ -1683,12 +1753,18 @@ packages:
react: ^18 || ^19 || ^19.0.0-rc
react-dom: ^18 || ^19 || ^19.0.0-rc
codemirror@6.0.2:
resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==}
compute-scroll-into-view@3.1.1:
resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==}
copy-to-clipboard@3.3.3:
resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==}
crelt@1.0.6:
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
@@ -1986,6 +2062,9 @@ packages:
string-convert@0.2.1:
resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==}
style-mod@4.1.3:
resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==}
styled-jsx@5.1.6:
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
engines: {node: '>= 12.0.0'}
@@ -2079,6 +2158,9 @@ packages:
react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
w3c-keyname@2.2.8:
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
zustand@5.0.11:
resolution: {integrity: sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==}
engines: {node: '>=12.20.0'}
@@ -2176,6 +2258,64 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.7
'@codemirror/autocomplete@6.20.0':
dependencies:
'@codemirror/language': 6.12.1
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.12
'@lezer/common': 1.5.1
'@codemirror/commands@6.10.2':
dependencies:
'@codemirror/language': 6.12.1
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.12
'@lezer/common': 1.5.1
'@codemirror/lang-json@6.0.2':
dependencies:
'@codemirror/language': 6.12.1
'@lezer/json': 1.0.3
'@codemirror/language@6.12.1':
dependencies:
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.12
'@lezer/common': 1.5.1
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
style-mod: 4.1.3
'@codemirror/lint@6.9.3':
dependencies:
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.12
crelt: 1.0.6
'@codemirror/search@6.6.0':
dependencies:
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.12
crelt: 1.0.6
'@codemirror/state@6.5.4':
dependencies:
'@marijn/find-cluster-break': 1.0.2
'@codemirror/theme-one-dark@6.1.3':
dependencies:
'@codemirror/language': 6.12.1
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.12
'@lezer/highlight': 1.2.3
'@codemirror/view@6.39.12':
dependencies:
'@codemirror/state': 6.5.4
crelt: 1.0.6
style-mod: 4.1.3
w3c-keyname: 2.2.8
'@date-fns/tz@1.4.1': {}
'@emnapi/runtime@1.8.1':
@@ -2362,6 +2502,24 @@ snapshots:
'@kevisual/load': 0.0.6
dotenv: 17.2.3
'@lezer/common@1.5.1': {}
'@lezer/highlight@1.2.3':
dependencies:
'@lezer/common': 1.5.1
'@lezer/json@1.0.3':
dependencies:
'@lezer/common': 1.5.1
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@lezer/lr@1.4.8':
dependencies:
'@lezer/common': 1.5.1
'@marijn/find-cluster-break@1.0.2': {}
'@next/env@16.1.6': {}
'@next/swc-darwin-arm64@16.1.6':
@@ -3607,6 +3765,33 @@ snapshots:
dependencies:
csstype: 3.2.3
'@uiw/codemirror-extensions-basic-setup@4.25.4(@codemirror/autocomplete@6.20.0)(@codemirror/commands@6.10.2)(@codemirror/language@6.12.1)(@codemirror/lint@6.9.3)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/view@6.39.12)':
dependencies:
'@codemirror/autocomplete': 6.20.0
'@codemirror/commands': 6.10.2
'@codemirror/language': 6.12.1
'@codemirror/lint': 6.9.3
'@codemirror/search': 6.6.0
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.12
'@uiw/react-codemirror@4.25.4(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.20.0)(@codemirror/language@6.12.1)(@codemirror/lint@6.9.3)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.39.12)(codemirror@6.0.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@babel/runtime': 7.28.6
'@codemirror/commands': 6.10.2
'@codemirror/state': 6.5.4
'@codemirror/theme-one-dark': 6.1.3
'@codemirror/view': 6.39.12
'@uiw/codemirror-extensions-basic-setup': 4.25.4(@codemirror/autocomplete@6.20.0)(@codemirror/commands@6.10.2)(@codemirror/language@6.12.1)(@codemirror/lint@6.9.3)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/view@6.39.12)
codemirror: 6.0.2
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
transitivePeerDependencies:
- '@codemirror/autocomplete'
- '@codemirror/language'
- '@codemirror/lint'
- '@codemirror/search'
antd@6.2.3(date-fns@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
'@ant-design/colors': 8.0.1
@@ -3692,12 +3877,24 @@ snapshots:
- '@types/react'
- '@types/react-dom'
codemirror@6.0.2:
dependencies:
'@codemirror/autocomplete': 6.20.0
'@codemirror/commands': 6.10.2
'@codemirror/language': 6.12.1
'@codemirror/lint': 6.9.3
'@codemirror/search': 6.6.0
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.12
compute-scroll-into-view@3.1.1: {}
copy-to-clipboard@3.3.3:
dependencies:
toggle-selection: 1.0.6
crelt@1.0.6: {}
csstype@3.2.3: {}
date-fns-jalali@4.1.0-0: {}
@@ -4008,6 +4205,8 @@ snapshots:
string-convert@0.2.1: {}
style-mod@4.1.3: {}
styled-jsx@5.1.6(react@19.2.4):
dependencies:
client-only: 0.0.1
@@ -4070,6 +4269,8 @@ snapshots:
- '@types/react'
- '@types/react-dom'
w3c-keyname@2.2.8: {}
zustand@5.0.11(@types/react@19.2.7)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)):
optionalDependencies:
'@types/react': 19.2.7

View File

@@ -1,9 +1,11 @@
'use client';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useConfigStore } from './store/config';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import CodeMirror from '@uiw/react-codemirror';
import { json } from '@codemirror/lang-json';
import {
Table,
TableBody,
@@ -23,11 +25,11 @@ import {
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { Plus, Pencil, Trash2 } from 'lucide-react';
import { Plus, Pencil, Trash2, Code } from 'lucide-react';
import { LayoutMain } from '@/modules/layout';
const TableList = () => {
const { list, setShowEdit, setFormData, deleteConfig } = useConfigStore();
const { list, setShowEdit, setFormData, deleteConfig, setShowDataEdit, setDataFormData } = useConfigStore();
interface ConfigItem {
id?: string;
@@ -48,6 +50,11 @@ const TableList = () => {
}
};
const handleEditData = (config: ConfigItem) => {
setShowDataEdit(true);
setDataFormData(config);
};
return (
<div className="rounded-md border">
<Table>
@@ -75,6 +82,12 @@ const TableList = () => {
<Pencil className="w-4 h-4 mr-1" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleEditData(config)}>
</Button>
<Popover>
<PopoverTrigger asChild>
<Button
@@ -180,6 +193,88 @@ const FormModal = () => {
);
};
const DataEditModal = () => {
const { showDataEdit, setShowDataEdit, dataFormData, setDataFormData, updateData } = useConfigStore();
const [jsonValue, setJsonValue] = useState('');
const [error, setError] = useState('');
useEffect(() => {
if (!showDataEdit) return;
if (dataFormData?.data) {
try {
const formatted = JSON.stringify(dataFormData.data, null, 2);
setJsonValue(formatted);
setError('');
} catch (e) {
setJsonValue('');
setError('数据格式错误');
}
} else {
setJsonValue('');
}
}, [dataFormData, showDataEdit]);
const handleSave = async () => {
try {
const parsedData = JSON.parse(jsonValue);
const res = await updateData({
...dataFormData,
data: parsedData,
});
if (res.code === 200) {
setShowDataEdit(false);
setDataFormData({});
}
} catch (e) {
setError('JSON 格式错误,请检查后重试');
}
};
return (
<Dialog open={showDataEdit} onOpenChange={(open) => {
setShowDataEdit(open);
if (!open) {
setDataFormData({});
setError('');
}
}}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle> - {dataFormData?.key}</DialogTitle>
</DialogHeader>
<div className="p-4">
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<label className="text-sm font-medium"> (JSON格式)</label>
<div className="border rounded-md overflow-hidden">
<CodeMirror
value={jsonValue}
height="400px"
extensions={[json()]}
onChange={(value) => {
setJsonValue(value);
setError('');
}}
theme="light"
/>
</div>
{error && <span className="text-xs text-red-500">{error}</span>}
</div>
<div className="flex gap-2 justify-end">
<Button type="button" variant="outline" onClick={() => setShowDataEdit(false)}>
</Button>
<Button type="button" onClick={handleSave}>
</Button>
</div>
</div>
</div>
</DialogContent>
</Dialog>
);
};
export const List = () => {
const { getConfigList, setShowEdit, setFormData } = useConfigStore();
@@ -201,6 +296,7 @@ export const List = () => {
</div>
<TableList />
<FormModal />
<DataEditModal />
</div>
);
};

View File

@@ -20,6 +20,10 @@ interface ConfigStore {
updateEnv: (data: Config) => Promise<void>;
envData: Config;
setEnvData: (envData: Config) => void;
showDataEdit: boolean;
setShowDataEdit: (showDataEdit: boolean) => void;
dataFormData: any;
setDataFormData: (dataFormData: any) => void;
}
export const useConfigStore = create<ConfigStore>((set, get) => ({
@@ -101,4 +105,8 @@ export const useConfigStore = create<ConfigStore>((set, get) => ({
},
envData: {},
setEnvData: (envData: any) => set({ envData }),
showDataEdit: false,
setShowDataEdit: (showDataEdit: boolean) => set({ showDataEdit }),
dataFormData: {},
setDataFormData: (dataFormData: any) => set({ dataFormData }),
}));