Kaynağa Gözat

Merge branch 'development' of github.com:visuddhinanda/mint into development

visuddhinanda 4 gün önce
ebeveyn
işleme
66293692b1

+ 13 - 10
api-v13/app/Console/Commands/UpgradeAITranslation.php

@@ -278,16 +278,19 @@ class UpgradeAITranslation extends Command
             ->get();
         $result = [];
         foreach ($sentences as $key => $sentence) {
-            $id = "{$sentence->book_id}-{$sentence->paragraph}-{$sentence->word_start}-{$sentence->word_end}";
-            $aiNissaya = $this->nissayaTranslateService
-                ->setModel($this->model)
-                ->translate($sentence->content, false);
-            Log::debug('ai response ', ['content' => $aiNissaya['data']]);
-            $result[] = [
-                'id' => $id,
-                'content' => json_encode($aiNissaya['data'] ?? [], JSON_UNESCAPED_UNICODE),
-                'content_type' => 'json',
-            ];
+            if (!empty($sentence->content)) {
+                $id = "{$sentence->book_id}-{$sentence->paragraph}-{$sentence->word_start}-{$sentence->word_end}";
+
+                $aiNissaya = $this->nissayaTranslateService
+                    ->setModel($this->model)
+                    ->translate($sentence->content, false);
+                Log::debug('ai response ', ['content' => $aiNissaya['data']]);
+                $result[] = [
+                    'id' => $id,
+                    'content' => json_encode($aiNissaya['data'] ?? [], JSON_UNESCAPED_UNICODE),
+                    'content_type' => 'json',
+                ];
+            }
         }
 
         return $result;

+ 12 - 7
api-v13/app/Http/Controllers/NissayaCardController.php

@@ -2,14 +2,15 @@
 
 namespace App\Http\Controllers;
 
-use App\Models\NissayaEnding;
+use Illuminate\Support\Facades\Log;
 use Illuminate\Http\Request;
-use mustache\mustache;
+use Illuminate\Support\Facades\App;
+
 use App\Http\Api\ChannelApi;
 use App\Http\Api\MdRender;
-use Illuminate\Support\Facades\App;
 use App\Models\DhammaTerm;
 use App\Models\Relation;
+use App\Models\NissayaEnding;
 
 class NissayaCardController extends Controller
 {
@@ -50,13 +51,15 @@ class NissayaCardController extends Controller
             "_System_Grammar_Term_" . strtolower($request->input('lang')) . "_",
             "_System_Grammar_Term_en_"
         );
+
         if (!$localTerm) {
             return $this->error('no term channel');
         }
-        $termTable = DhammaTerm::where('channal', $localTerm);
 
         $cardData['ending']['word'] = $nissayaEnding;
-        $endingTerm = $termTable->where('word', $nissayaEnding)->first();
+        $endingTerm = DhammaTerm::where('channal', $localTerm)
+            ->where('word', $nissayaEnding)
+            ->first();
         if ($endingTerm) {
             $cardData['ending']['id'] = $endingTerm->guid;
             $cardData['ending']['tag'] = $endingTerm->tag;
@@ -107,7 +110,9 @@ class NissayaCardController extends Controller
                     $localCase  = [];
                     foreach ($cases as $case) {
                         $localCase[] = [
-                            'label' => $termTable->where('word', $case)->value('meaning'),
+                            'label' => DhammaTerm::where('channal', $localTerm)
+                                ->where('word', $case)
+                                ->value('meaning'),
                             'case' => $case,
                             'link' => config('mint.server.dashboard_base_path') . '/term/list/' . $case
                         ];
@@ -124,7 +129,7 @@ class NissayaCardController extends Controller
                     $localTo  = [];
                     foreach ($linkTos->case as $to) {
                         $localTo[] = [
-                            'label' => $termTable->where('word', $to)->value('meaning'),
+                            'label' => DhammaTerm::where('channal', $localTerm)->where('word', $to)->value('meaning'),
                             'case' => $to,
                             'link' => config('mint.server.dashboard_base_path') . '/term/list/' . $to
                         ];

+ 252 - 201
api-v13/app/Http/Controllers/RelationController.php

@@ -6,297 +6,348 @@ use App\Models\Relation;
 use Illuminate\Http\Request;
 use App\Http\Resources\RelationResource;
 use App\Services\AuthService;
-use Illuminate\Support\Facades\App;
 use PhpOffice\PhpSpreadsheet\Spreadsheet;
 use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
 use Illuminate\Support\Facades\Cache;
+use Illuminate\Http\JsonResponse;
 
 class RelationController extends Controller
 {
+    /**
+     * Vocabulary cache key — shared across requests.
+     */
+    private const VOCABULARY_CACHE_KEY = 'relation-vocabulary';
+
     /**
      * Display a listing of the resource.
      *
-     * @return \Illuminate\Http\Response
+     * Supports optional filters: case, search, name, from, to, match, category.
+     * When the `vocabulary` parameter is present, returns the full unfiltered
+     * list from cache (suitable for populating UI dropdowns / autocomplete).
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\JsonResponse
      */
-    public function index(Request $request)
+    public function index(Request $request): JsonResponse
     {
-        //
-        $key = 'relation-vocabulary';
-        if ($request->has('vocabulary')) {
-            if (Cache::has($key)) {
-                return $this->ok(Cache::get($key));
-            }
-        }
-        $table = Relation::select([
-            'id',
-            'name',
-            'case',
-            'from',
-            'to',
-            'category',
-            'editor_id',
-            'match',
-            'updated_at',
-            'created_at'
-        ]);
-        if (($request->has('case'))) {
-            $table = $table->whereIn('case', explode(",", $request->input('case')));
+        if ($request->boolean('vocabulary')) {
+            return $this->ok(
+                Cache::remember(
+                    self::VOCABULARY_CACHE_KEY,
+                    config('mint.cache.expire'),
+                    fn() => $this->buildVocabularyPayload()
+                )
+            );
         }
-        if (($request->has('search'))) {
-            $table = $table->where('name', 'like', $request->input('search') . "%");
-        }
-        if (($request->has('name'))) {
-            $table = $table->where('name', $request->input('name'));
-        }
-        if (($request->has('from'))) {
-            $table = $table->whereJsonContains('from->case', $request->input('from'));
-        }
-        if (($request->has('to'))) {
-            $table = $table->whereJsonContains('to', $request->input('to'));
-        }
-        if (($request->has('match'))) {
-            $table = $table->whereJsonContains('match', $request->input('match'));
-        }
-        if (($request->has('category'))) {
-            $table = $table->where('category', $request->input('category'));
-        }
-        $table = $table->orderBy($request->input('order', 'updated_at'), $request->input('dir', 'desc'));
-        $count = $table->count();
-
-        $table = $table->skip($request->input("offset", 0))
-            ->take($request->input('limit', 1000));
-        $result = $table->get();
-
-        $output = ["rows" => RelationResource::collection($result), "count" => $count];
 
-        if ($request->has('vocabulary')) {
-            if (!Cache::has($key)) {
-                Cache::put($key, $output, config('mint.cache.expire'));
-            }
-        }
-        return $this->ok($output);
+        return $this->ok($this->buildFilteredPayload($request));
     }
 
-
     /**
      * Store a newly created resource in storage.
      *
+     * Requires authentication. Invalidates the vocabulary cache on success.
+     *
      * @param  \Illuminate\Http\Request  $request
-     * @return \Illuminate\Http\Response
+     * @return \Illuminate\Http\JsonResponse
      */
-    public function store(Request $request)
+    public function store(Request $request): JsonResponse
     {
-        //
         $user = AuthService::current($request);
         if (!$user) {
             return $this->error(__('auth.failed'), [], 401);
         }
-        //TODO 判断权限
+
         $validated = $request->validate([
             'name' => 'required',
         ]);
-        $case = $request->input('case', '');
-        $new = new Relation;
-        $new->name = $validated['name'];
 
-        $new->case = $request->input('case');
-        $new->category = $request->input('category');
+        $relation = new Relation();
+        $relation->name      = $validated['name'];
+        $relation->case      = $request->input('case');
+        $relation->category  = $request->input('category');
+        $relation->from      = $this->encodeJsonField($request, 'from');
+        $relation->to        = $this->encodeJsonField($request, 'to');
+        $relation->match     = $this->encodeJsonField($request, 'match');
+        $relation->editor_id = $user['user_uid'];
+        $relation->save();
 
-        if ($request->has('from')) {
-            $new->from = json_encode($request->input('from'), JSON_UNESCAPED_UNICODE);
-        } else {
-            $new->from = null;
-        }
-        if ($request->has('to')) {
-            $new->to = json_encode($request->input('to'), JSON_UNESCAPED_UNICODE);
-        } else {
-            $new->to = null;
-        }
-        if ($request->has('match')) {
-            $new->match = json_encode($request->input('match'), JSON_UNESCAPED_UNICODE);
-        } else {
-            $new->match = null;
-        }
-        $new->editor_id = $user['user_uid'];
-        $new->save();
-        return $this->ok(new RelationResource($new));
+        Cache::forget(self::VOCABULARY_CACHE_KEY);
+
+        return $this->ok(new RelationResource($relation));
     }
 
     /**
      * Display the specified resource.
      *
      * @param  \App\Models\Relation  $relation
-     * @return \Illuminate\Http\Response
+     * @return \Illuminate\Http\JsonResponse
      */
-    public function show(Relation $relation)
+    public function show(Relation $relation): JsonResponse
     {
-        //
         return $this->ok(new RelationResource($relation));
     }
 
-
     /**
      * Update the specified resource in storage.
      *
+     * Requires authentication. Invalidates the vocabulary cache on success.
+     *
      * @param  \Illuminate\Http\Request  $request
-     * @param  \App\Models\Relation  $relation
-     * @return \Illuminate\Http\Response
+     * @param  \App\Models\Relation      $relation
+     * @return \Illuminate\Http\JsonResponse
      */
-    public function update(Request $request, Relation $relation)
+    public function update(Request $request, Relation $relation): JsonResponse
     {
-        //
         $user = AuthService::current($request);
         if (!$user) {
-            return $this->error(__('auth.failed'));
+            return $this->error(__('auth.failed'), [], 401);
         }
 
-        $relation->name = $request->input('name');
-        $relation->case = $request->input('case');
-        $relation->category = $request->input('category');
-
-        if ($request->has('from')) {
-            $relation->from = json_encode($request->input('from'), JSON_UNESCAPED_UNICODE);
-        } else {
-            $relation->from = null;
-        }
-        if ($request->has('to')) {
-            $relation->to = json_encode($request->input('to'), JSON_UNESCAPED_UNICODE);
-        } else {
-            $relation->to = null;
-        }
-        if ($request->has('match')) {
-            $relation->match = json_encode($request->input('match'), JSON_UNESCAPED_UNICODE);
-        } else {
-            $relation->match = null;
-        }
+        $relation->name      = $request->input('name');
+        $relation->case      = $request->input('case');
+        $relation->category  = $request->input('category');
+        $relation->from      = $this->encodeJsonField($request, 'from');
+        $relation->to        = $this->encodeJsonField($request, 'to');
+        $relation->match     = $this->encodeJsonField($request, 'match');
         $relation->editor_id = $user['user_uid'];
         $relation->save();
+
+        Cache::forget(self::VOCABULARY_CACHE_KEY);
+
         return $this->ok(new RelationResource($relation));
     }
 
     /**
      * Remove the specified resource from storage.
      *
+     * Requires authentication. Invalidates the vocabulary cache on success.
+     *
      * @param  \Illuminate\Http\Request  $request
-     * @param  \App\Models\Relation  $relation
-     * @return \Illuminate\Http\Response
+     * @param  \App\Models\Relation      $relation
+     * @return \Illuminate\Http\JsonResponse
      */
-    public function destroy(Request $request, Relation $relation)
+    public function destroy(Request $request, Relation $relation): JsonResponse
     {
-        //
         $user = AuthService::current($request);
         if (!$user) {
-            return $this->error(__('auth.failed'));
+            return $this->error(__('auth.failed'), [], 401);
         }
-        //TODO 判断当前用户是否有权限
-        $delete = 0;
-        $delete = $relation->delete();
 
-        return $this->ok($delete);
+        $deleted = $relation->delete();
+
+        Cache::forget(self::VOCABULARY_CACHE_KEY);
+
+        return $this->ok($deleted);
     }
 
-    public function export()
+    /**
+     * Export all relations as an XLSX file download.
+     *
+     * Streams the spreadsheet directly to the browser via php://output.
+     * Columns: id, name, from, to, match, category.
+     *
+     * @return void
+     */
+    public function export(): void
     {
-        $spreadsheet = new Spreadsheet();
+        $spreadsheet     = new Spreadsheet();
         $activeWorksheet = $spreadsheet->getActiveSheet();
-        $activeWorksheet->setCellValue('A1', 'id');
-        $activeWorksheet->setCellValue('B1', 'name');
-        $activeWorksheet->setCellValue('C1', 'from');
-        $activeWorksheet->setCellValue('D1', 'to');
-        $activeWorksheet->setCellValue('E1', 'match');
-        $activeWorksheet->setCellValue('F1', 'category');
-
-        $nissaya = Relation::cursor();
+
+        $activeWorksheet->fromArray(['id', 'name', 'from', 'to', 'match', 'category'], null, 'A1');
+
         $currLine = 2;
-        foreach ($nissaya as $key => $row) {
-            # code...
-            $activeWorksheet->setCellValue("A{$currLine}", $row->id);
-            $activeWorksheet->setCellValue("B{$currLine}", $row->name);
-            $activeWorksheet->setCellValue("C{$currLine}", $row->from);
-            $activeWorksheet->setCellValue("D{$currLine}", $row->to);
-            $activeWorksheet->setCellValue("E{$currLine}", $row->match);
-            $activeWorksheet->setCellValue("F{$currLine}", $row->category);
+        foreach (Relation::cursor() as $row) {
+            $activeWorksheet->fromArray(
+                [$row->id, $row->name, $row->from, $row->to, $row->match, $row->category],
+                null,
+                "A{$currLine}"
+            );
             $currLine++;
         }
+
         $writer = new Xlsx($spreadsheet);
         header('Content-Type: application/vnd.ms-excel');
         header('Content-Disposition: attachment; filename="relation.xlsx"');
-        $writer->save("php://output");
+        $writer->save('php://output');
     }
 
-    public function import(Request $request)
+    /**
+     * Import relations from an uploaded XLSX file.
+     *
+     * Requires authentication. Reads the file path from the `filename` request
+     * parameter. Existing records are matched by id and updated in place;
+     * rows without a matching id are inserted as new records.
+     * Invalidates the vocabulary cache on completion.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function import(Request $request): JsonResponse
     {
         $user = AuthService::current($request);
         if (!$user) {
-            return $this->error(__('auth.failed'));
+            return $this->error(__('auth.failed'), [], 401);
         }
 
         $filename = $request->input('filename');
-        $reader = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx();
+        $reader   = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx();
         $reader->setReadDataOnly(true);
-        $spreadsheet = $reader->load($filename);
+        $spreadsheet     = $reader->load($filename);
         $activeWorksheet = $spreadsheet->getActiveSheet();
-        $currLine = 2;
+
+        $currLine  = 2;
         $countFail = 0;
-        $error = "";
-        do {
-            # code...
-            $id = $activeWorksheet->getCell("A{$currLine}")->getValue();
+        $error     = '';
+
+        while (true) {
             $name = $activeWorksheet->getCell("B{$currLine}")->getValue();
-            $from = $activeWorksheet->getCell("C{$currLine}")->getValue();
-            $to = $activeWorksheet->getCell("D{$currLine}")->getValue();
-            $match = $activeWorksheet->getCell("E{$currLine}")->getValue();
-            $category = $activeWorksheet->getCell("F{$currLine}")->getValue();
-            if (!empty($name)) {
-                //查询是否有冲突数据
-                //查询此id是否有旧数据
-                if (!empty($id)) {
-                    $oldRow = Relation::find($id);
-                }
-                //查询是否跟已有数据重复
-                /*
-                $row = Relation::where(['name'=>$name,
-                                        'from'=>json_decode($from,true),
-                                        'to'=>$to,
-                                        'match'=>$match,
-                                        'category'=>$category
-                                        ])->first();
-                */
-                $row = false;
-                if (!$row) {
-                    //不重复
-                    if (isset($oldRow) && $oldRow) {
-                        //有旧的记录-修改旧数据
-                        $row = $oldRow;
-                    } else {
-                        //没找到旧的记录-新建
-                        $row = new Relation();
-                    }
-                } else {
-                    //重复-如果与旧的id不同旧报错
-                    if (isset($oldRow) && $oldRow && $row->id !== $id) {
-                        $error .= "重复的数据:id={$id} -\n";
-                        $currLine++;
-                        $countFail++;
-                        continue;
-                    }
-                }
-                $row->name = $name;
-                if (empty($from)) {
-                    $row->from = null;
-                } else {
-                    $row->from = $from;
-                }
-                $row->to = $to;
-                $row->match = $match;
-                $row->category = $category;
-                $row->editor_id = $user['user_uid'];
-                $row->save();
-            } else {
+            if (empty($name)) {
                 break;
             }
+
+            $id       = $activeWorksheet->getCell("A{$currLine}")->getValue();
+            $from     = $activeWorksheet->getCell("C{$currLine}")->getValue();
+            $to       = $activeWorksheet->getCell("D{$currLine}")->getValue();
+            $match    = $activeWorksheet->getCell("E{$currLine}")->getValue();
+            $category = $activeWorksheet->getCell("F{$currLine}")->getValue();
+
+            $row = (!empty($id) ? Relation::find($id) : null) ?? new Relation();
+
+            $row->name      = $name;
+            $row->from      = empty($from) ? null : $from;
+            $row->to        = $to;
+            $row->match     = $match;
+            $row->category  = $category;
+            $row->editor_id = $user['user_uid'];
+            $row->save();
+
             $currLine++;
-        } while (true);
-        return $this->ok(["success" => $currLine - 2 - $countFail, 'fail' => ($countFail)], $error);
+        }
+
+        Cache::forget(self::VOCABULARY_CACHE_KEY);
+
+        $success = $currLine - 2 - $countFail;
+        return $this->ok(['success' => $success, 'fail' => $countFail], $error);
+    }
+
+    // -------------------------------------------------------------------------
+    // Private helpers
+    // -------------------------------------------------------------------------
+
+    /**
+     * Build the full vocabulary payload for caching.
+     *
+     * ResourceCollection is resolved to a plain PHP array via toArray() before
+     * being stored, preventing __PHP_Incomplete_Class_Name deserialization
+     * errors when using Redis or file-based cache drivers.
+     *
+     * @return array{rows: array<int, array<string, mixed>>, count: int}
+     */
+    private function buildVocabularyPayload(): array
+    {
+        $rows = Relation::select([
+            'id',
+            'name',
+            'case',
+            'from',
+            'to',
+            'category',
+            'editor_id',
+            'match',
+            'updated_at',
+            'created_at',
+        ])->orderBy('updated_at', 'desc')->get();
+
+        return [
+            'rows'  => RelationResource::collection($rows)->toArray(request()),
+            'count' => $rows->count(),
+        ];
+    }
+
+    /**
+     * Build a filtered and paginated payload for standard index requests.
+     *
+     * Supported query parameters:
+     *   - case      (comma-separated)  filter by case values
+     *   - search    (string)           prefix match on name
+     *   - name      (string)           exact match on name
+     *   - from      (string)           JSON contains on from->case
+     *   - to        (string)           JSON contains on to
+     *   - match     (string)           JSON contains on match
+     *   - category  (string)           exact match on category
+     *   - order     (string)           column to sort by  (default: updated_at)
+     *   - dir       (asc|desc)         sort direction     (default: desc)
+     *   - offset    (int)              skip N rows        (default: 0)
+     *   - limit     (int)              max rows returned  (default: 1000)
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return array{rows: \Illuminate\Http\Resources\Json\AnonymousResourceCollection, count: int}
+     */
+    private function buildFilteredPayload(Request $request): array
+    {
+        $query = Relation::select([
+            'id',
+            'name',
+            'case',
+            'from',
+            'to',
+            'category',
+            'editor_id',
+            'match',
+            'updated_at',
+            'created_at',
+        ]);
+
+        if ($request->filled('case')) {
+            $query->whereIn('case', explode(',', $request->input('case')));
+        }
+        if ($request->filled('search')) {
+            $query->where('name', 'like', $request->input('search') . '%');
+        }
+        if ($request->filled('name')) {
+            $query->where('name', $request->input('name'));
+        }
+        if ($request->filled('from')) {
+            $query->whereJsonContains('from->case', $request->input('from'));
+        }
+        if ($request->filled('to')) {
+            $query->whereJsonContains('to', $request->input('to'));
+        }
+        if ($request->filled('match')) {
+            $query->whereJsonContains('match', $request->input('match'));
+        }
+        if ($request->filled('category')) {
+            $query->where('category', $request->input('category'));
+        }
+
+        $query->orderBy($request->input('order', 'updated_at'), $request->input('dir', 'desc'));
+
+        $count = $query->count();
+        $rows  = $query->skip($request->input('offset', 0))
+            ->take($request->input('limit', 1000))
+            ->get();
+
+        return [
+            'rows'  => RelationResource::collection($rows),
+            'count' => $count,
+        ];
+    }
+
+    /**
+     * Encode a request field as a JSON string.
+     *
+     * Returns null when the field is absent from the request, preserving the
+     * semantic distinction between "not provided" and an explicit empty value.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  string                    $field
+     * @return string|null
+     */
+    private function encodeJsonField(Request $request, string $field): ?string
+    {
+        return $request->has($field)
+            ? json_encode($request->input($field), JSON_UNESCAPED_UNICODE)
+            : null;
     }
 }

+ 1 - 2
api-v13/app/Http/Resources/RelationResource.php

@@ -31,9 +31,8 @@ class RelationResource extends JsonResource
 
 
         if (!$request->has('vocabulary')) {
-            //TODO 默认英文
+
             $data["editor"] = UserApi::getByUuid($this->editor_id);
-            $lang = $request->input('ui-lang');
 
             $uiLang = strtolower($request->input('ui-lang', 'zh-Hans'));
             $term_channel = ChannelApi::getSysChannel("_System_Grammar_Term_{$uiLang}_");

+ 1 - 1
api-v13/app/Http/Resources/SentSimResource.php

@@ -23,7 +23,7 @@ class SentSimResource extends JsonResource
         //获取实际句子信息
         $sent = PaliSentence::find($this->sent2);
         $channels = explode(',', $request->input('channels'));
-        $mode = explode(',', $request->input('mode', 'read'));
+        $mode = $request->input('mode', 'read');
         $sentId = $sent->book . '-' . $sent->paragraph . '-' . $sent->word_begin . '-' . $sent->word_end;
         $Sent = new CorpusController();
         $tpl =

+ 1 - 1
api-v13/app/Services/TermService.php

@@ -92,7 +92,7 @@ class TermService
             "_System_Grammar_Term_" . strtolower($lang) . "_",
             "_System_Grammar_Term_en_"
         );
-        $result = DhammaTerm::select(['word', 'tag', 'meaning', 'other_meaning'])
+        $result = DhammaTerm::select(['guid', 'word', 'tag', 'meaning', 'other_meaning'])
             ->where('channal', $localTermChannel)
             ->get();
         return ['items' => $result, 'total' => count($result)];

+ 4 - 2
dashboard-v6/src/features/editor/Chapter.tsx

@@ -50,11 +50,13 @@ export default function ChapterEditor({
 
   useEffect(() => {
     if (!currUser?.id || !chapterId) return;
-
+    const paramObj = search
+      ? Object.fromEntries(new URLSearchParams(search))
+      : undefined;
     save({
       type: "chapter",
       article_id: chapterId,
-      param: search || undefined,
+      param: JSON.stringify(paramObj),
     });
   }, [currUser?.id, chapterId, search, save]);
 

+ 4 - 1
dashboard-v6/src/features/editor/Paragraph.tsx

@@ -51,10 +51,13 @@ export default function ParaEditor({
   useEffect(() => {
     if (!currUser?.id || !chapterId) return;
 
+    const paramObj = search
+      ? Object.fromEntries(new URLSearchParams(search))
+      : undefined;
     save({
       type: "chapter",
       article_id: chapterId,
-      param: search || undefined,
+      param: JSON.stringify(paramObj),
     });
   }, [currUser?.id, chapterId, search, save]);
 

+ 4 - 2
dashboard-v6/src/features/editor/Term.tsx

@@ -45,11 +45,13 @@ export default function TermEditor({
 
   useEffect(() => {
     if (!currUser?.id || !termId) return;
-
+    const paramObj = search
+      ? Object.fromEntries(new URLSearchParams(search))
+      : undefined;
     save({
       type: "term",
       article_id: termId,
-      param: search || undefined,
+      param: JSON.stringify(paramObj),
     });
   }, [currUser?.id, termId, search, save]);