Improve frontend

This commit is contained in:
Xin Wang
2026-07-18 14:29:42 +08:00
parent 6c482c275a
commit b44adfa071
7 changed files with 5676 additions and 251 deletions

5420
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -223,23 +223,25 @@ export default function App() {
publishedMatcher={transferMatcher(publishedRuleSets)}
publishedRuleCount={activeRules.filter(rule => rule.enabled).length}
isDirty={isDirty}
saving={savingRules || restoringVersion !== null}
versionHistory={versionHistory}
versionHistoryLoading={versionHistoryLoading}
restoringVersion={restoringVersion}
onLoadVersionHistory={() => void loadVersionHistory()}
onRestoreVersion={(version) => void handleRestoreVersion(version)}
onSavePublish={() => void handlePublish()}
/>
{/* Main Container Area */}
<main className="mx-auto w-full max-w-[1600px] flex-1 overflow-hidden px-4 py-6 sm:px-6">
<main className="mx-auto w-full max-w-[1600px] flex-1 px-4 pt-4 pb-6 sm:px-6">
<AnimatePresence mode="wait">
{activeTab === 'rules' ? (
<motion.div
key="rules-page"
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -15 }}
transition={{ duration: 0.18, ease: 'easeOut' }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.16, ease: 'easeOut' }}
className="h-full"
>
<RuleConfiguration
@@ -249,20 +251,17 @@ export default function App() {
setMatcher={handleUpdateMatcher}
llmConfig={llmConfig}
setLlmConfig={setLlmConfig}
onSavePublish={handlePublish}
isDirty={isDirty}
loading={loadingRules}
saving={savingRules || restoringVersion !== null}
addToast={addToast}
/>
</motion.div>
) : (
<motion.div
key="sandbox-page"
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -15 }}
transition={{ duration: 0.18, ease: 'easeOut' }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.16, ease: 'easeOut' }}
className="h-full"
>
<SandboxSimulation

View File

@@ -4,7 +4,7 @@
*/
import React, { useEffect, useRef, useState } from 'react';
import { ChevronDown, History, LoaderCircle, RotateCcw } from 'lucide-react';
import { ChevronDown, History, LoaderCircle, RotateCcw, Save } from 'lucide-react';
import type { RuleVersionSummary } from '../types';
interface HeaderProps {
@@ -14,11 +14,13 @@ interface HeaderProps {
publishedMatcher: 'ac-keyword' | 'llm';
publishedRuleCount: number;
isDirty: boolean;
saving: boolean;
versionHistory: RuleVersionSummary[];
versionHistoryLoading: boolean;
restoringVersion: number | null;
onLoadVersionHistory: () => void;
onRestoreVersion: (version: number) => void;
onSavePublish: () => void;
}
function formatCreatedAt(value: string) {
@@ -39,11 +41,13 @@ export default function Header({
publishedMatcher,
publishedRuleCount,
isDirty,
saving,
versionHistory,
versionHistoryLoading,
restoringVersion,
onLoadVersionHistory,
onRestoreVersion,
onSavePublish,
}: HeaderProps) {
const [statusMenuOpen, setStatusMenuOpen] = useState(false);
const [versionMenuOpen, setVersionMenuOpen] = useState(false);
@@ -88,52 +92,57 @@ export default function Header({
return (
<header className="sticky top-0 z-40 w-full border-b border-slate-200 bg-white/95 backdrop-blur-md shrink-0">
<div className="mx-auto flex h-14 max-w-[1600px] items-center justify-between px-4 sm:px-6">
<div className="relative mx-auto flex h-14 max-w-[1600px] items-center justify-between px-4 sm:px-6">
{/* Left Section: Brand Logo */}
<div className="flex items-center space-x-3">
<div className="relative z-10 flex items-center space-x-3">
<div className="w-8 h-8 bg-slate-900 rounded-md flex items-center justify-center shadow-xs">
<span className="text-white text-xs font-bold tracking-wider">ASR</span>
</div>
<div className="flex flex-col">
<span className="text-xs font-bold tracking-tight text-slate-900 uppercase sm:text-sm">
<span className="text-sm font-bold tracking-tight text-slate-900">
ASR
</span>
</div>
</div>
{/* Center Section: Navigation Menu */}
<nav className="flex h-full space-x-1" aria-label="Global Navigation">
<button
onClick={() => setActiveTab('rules')}
className={`relative h-full flex items-center px-4 text-xs font-semibold uppercase tracking-wider transition-colors duration-150 ${
activeTab === 'rules'
? 'text-slate-900'
: 'text-slate-400 hover:text-slate-900'
}`}
>
<span></span>
{activeTab === 'rules' && (
<div className="absolute bottom-0 left-0 right-0 h-[2px] bg-slate-900" />
)}
</button>
{/* Center Section: always viewport-centered, independent of side widths */}
<nav
className="pointer-events-none absolute inset-0 flex items-center justify-center"
aria-label="Global Navigation"
>
<div className="pointer-events-auto flex h-full space-x-1">
<button
onClick={() => setActiveTab('rules')}
className={`relative flex h-full items-center px-4 text-sm font-bold transition-colors duration-150 ${
activeTab === 'rules'
? 'text-slate-900'
: 'text-slate-400 hover:text-slate-900'
}`}
>
<span></span>
{activeTab === 'rules' && (
<div className="absolute bottom-0 left-0 right-0 h-[2px] bg-slate-900" />
)}
</button>
<button
onClick={() => setActiveTab('sandbox')}
className={`relative h-full flex items-center px-4 text-xs font-semibold uppercase tracking-wider transition-colors duration-150 ${
activeTab === 'sandbox'
? 'text-slate-900'
: 'text-slate-400 hover:text-slate-900'
}`}
>
<span></span>
{activeTab === 'sandbox' && (
<div className="absolute bottom-0 left-0 right-0 h-[2px] bg-slate-900" />
)}
</button>
<button
onClick={() => setActiveTab('sandbox')}
className={`relative flex h-full items-center px-4 text-sm font-bold transition-colors duration-150 ${
activeTab === 'sandbox'
? 'text-slate-900'
: 'text-slate-400 hover:text-slate-900'
}`}
>
<span></span>
{activeTab === 'sandbox' && (
<div className="absolute bottom-0 left-0 right-0 h-[2px] bg-slate-900" />
)}
</button>
</div>
</nav>
{/* Right Section: Published runtime status and version history */}
<div className="flex items-center space-x-2">
{/* Right Section: status, version, save */}
<div className="relative z-10 flex items-center space-x-2">
<div ref={statusMenuRef} className="relative">
<button
type="button"
@@ -146,7 +155,7 @@ export default function Header({
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75" />
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-emerald-500" />
</span>
<span className="font-sans text-[10px] font-bold tracking-tight">线</span>
<span className="text-[11px] font-bold tracking-tight">线</span>
<ChevronDown className={`h-3 w-3 transition-transform ${statusMenuOpen ? 'rotate-180' : ''}`} />
</button>
@@ -183,7 +192,7 @@ export default function Header({
)}
</div>
{isDirty && (
<span className="hidden rounded bg-amber-100/70 border border-amber-200 px-2 py-0.5 text-[10px] font-bold text-amber-800 tracking-wider sm:inline">
<span className="hidden rounded border border-amber-200 bg-amber-100/70 px-2 py-0.5 text-[11px] font-bold text-amber-800 sm:inline">
</span>
)}
@@ -193,7 +202,7 @@ export default function Header({
onClick={toggleVersionMenu}
aria-haspopup="menu"
aria-expanded={versionMenuOpen}
className="inline-flex items-center gap-1 rounded-md border border-slate-200 bg-white px-2.5 py-1 font-mono text-[10px] font-bold text-slate-600 transition hover:border-slate-300 hover:bg-slate-50"
className="inline-flex items-center gap-1 rounded-md border border-slate-200 bg-white px-2.5 py-1 font-mono text-[11px] font-bold text-slate-600 transition hover:border-slate-300 hover:bg-slate-50"
>
<span>V{publishedVersion}</span>
<ChevronDown className={`h-3 w-3 transition-transform ${versionMenuOpen ? 'rotate-180' : ''}`} />
@@ -276,6 +285,27 @@ export default function Header({
</div>
)}
</div>
<button
type="button"
onClick={onSavePublish}
disabled={!isDirty || saving}
title={!isDirty ? '当前配置没有未发布的修改' : undefined}
className={`relative inline-flex h-7 shrink-0 items-center justify-center gap-1.5 rounded-md px-2.5 text-[11px] font-bold text-white transition focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-950 ${
isDirty && !saving
? 'bg-slate-950 hover:bg-slate-800'
: 'cursor-not-allowed bg-slate-200 text-slate-400'
}`}
>
<Save className="h-3 w-3" />
<span>{saving ? '发布中…' : '保存并发布'}</span>
{isDirty && !saving && (
<span className="absolute -right-1 -top-1 flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-rose-400 opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-rose-500" />
</span>
)}
</button>
</div>
</div>
</header>

View File

@@ -4,7 +4,7 @@
*/
import { useState, type ReactNode } from 'react';
import { Bot, Braces, KeyRound, LoaderCircle, RotateCcw, Sparkles, Wifi } from 'lucide-react';
import { Bot, Braces, Info, KeyRound, LoaderCircle, RotateCcw, Sparkles, Wifi } from 'lucide-react';
import { ApiError, probeLlmConnection } from '../api';
import type { LlmConfig } from '../types';
@@ -98,21 +98,11 @@ export default function LlmConfiguration({
<div className="space-y-5">
<section className="overflow-hidden rounded-lg border border-slate-200 bg-white shadow-3xs">
<div className="border-b border-slate-200 px-6 py-5">
<div className="flex min-w-0 items-start gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-slate-950 text-white">
<div className="flex min-w-0 items-center gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-700">
<Sparkles className="h-4.5 w-4.5" />
</div>
<div>
<div className="flex items-center gap-2">
<h2 className="text-sm font-bold text-slate-950"></h2>
<span className="rounded border border-violet-200 bg-violet-50 px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-widest text-violet-700">
OpenAI Compatible
</span>
</div>
<p className="mt-1 text-[11px] leading-5 text-slate-500">
</p>
</div>
<h2 className="text-sm font-bold text-slate-950"></h2>
</div>
</div>
@@ -192,17 +182,12 @@ export default function LlmConfiguration({
</section>
<section className="overflow-hidden rounded-lg border border-slate-200 bg-white shadow-3xs">
<div className="flex flex-col gap-3 border-b border-slate-200 px-6 py-5 sm:flex-row sm:items-start sm:justify-between">
<div className="flex items-start gap-3">
<div className="flex flex-col gap-3 border-b border-slate-200 px-6 py-5 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md border border-slate-200 bg-slate-50">
<Bot className="h-4 w-4 text-slate-700" />
</div>
<div>
<h2 className="text-sm font-bold text-slate-950"></h2>
<p className="mt-1 max-w-2xl text-[11px] leading-5 text-slate-500">
便 KV Cache
</p>
</div>
<h2 className="text-sm font-bold text-slate-950"></h2>
</div>
<button
type="button"
@@ -239,19 +224,22 @@ export default function LlmConfiguration({
))}
</div>
<div className="flex items-start gap-3 rounded-md border border-slate-200 bg-slate-50 p-4">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded border border-slate-200 bg-white">
<Braces className="h-4 w-4 text-slate-600" />
</div>
<div>
<h3 className="text-xs font-bold text-slate-900"></h3>
<p className="mt-1 text-[10px] leading-4 text-slate-500">
ID ID ID
</p>
<code className="mt-2 block font-mono text-[11px] font-bold text-slate-800">
<div className="flex items-start gap-2.5 rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-[10px] leading-5 text-amber-800">
<Info className="mt-0.5 h-3.5 w-3.5 shrink-0" />
<p>
便 KV Cache
</p>
</div>
<div className="flex items-start gap-2.5 rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-[10px] leading-5 text-amber-800">
<Braces className="mt-0.5 h-3.5 w-3.5 shrink-0" />
<p>
ID ID ID
<code className="mx-1 rounded bg-amber-100 px-1 font-mono font-bold">
[&quot;E001&quot;,&quot;E003&quot;] / []
</code>
</div>
</p>
</div>
</div>
</section>

View File

@@ -11,7 +11,6 @@ import {
Check,
ChevronRight,
Cpu,
Save,
Sparkles,
TextSearch,
} from 'lucide-react';
@@ -28,39 +27,20 @@ interface RuleConfigurationProps {
setMatcher: (matcher: 'ac-keyword' | 'llm') => void;
llmConfig: LlmConfig;
setLlmConfig: (config: LlmConfig) => void;
onSavePublish: () => void;
isDirty: boolean;
loading: boolean;
saving: boolean;
addToast: (message: string, type: 'success' | 'warning' | 'info') => void;
}
const NAV_ITEMS: Array<{
id: ConfigurationSection;
label: string;
description: string;
icon: typeof BookOpen;
}> = [
{ id: 'rules', label: '规则库', description: '事件、短 ID 与关键词', icon: BookOpen },
{ id: 'llm', label: '大模型', description: '模型连接与提示词', icon: Bot },
{ id: 'matcher', label: '匹配算法', description: '选择运行时识别方式', icon: Cpu },
{ id: 'rules', label: '规则库', icon: BookOpen },
{ id: 'llm', label: '大模型', icon: Bot },
{ id: 'matcher', label: '匹配算法', icon: Cpu },
];
const SECTION_COPY: Record<ConfigurationSection, { title: string; description: string }> = {
rules: {
title: '规则库',
description: '管理事件名称、短事件 ID 和辅助关键词。',
},
llm: {
title: '大模型',
description: '配置模型连接、输出上限和事件判断提示词。',
},
matcher: {
title: '匹配算法',
description: '选择当前发布版本实际使用的事件识别方式。',
},
};
export default function RuleConfiguration({
rules,
setRules,
@@ -68,14 +48,10 @@ export default function RuleConfiguration({
setMatcher,
llmConfig,
setLlmConfig,
onSavePublish,
isDirty,
loading,
saving,
addToast,
}: RuleConfigurationProps) {
const [activeSection, setActiveSection] = useState<ConfigurationSection>('rules');
const copy = SECTION_COPY[activeSection];
const selectMatcher = (nextMatcher: 'ac-keyword' | 'llm') => {
if (nextMatcher === matcher) return;
@@ -98,117 +74,74 @@ export default function RuleConfiguration({
return (
<div className="grid gap-5 lg:grid-cols-[220px_minmax(0,1fr)] lg:items-start">
<aside className="overflow-hidden rounded-lg border border-slate-200 bg-white shadow-3xs lg:sticky lg:top-20 lg:h-[calc(100vh-6.5rem)] lg:min-h-[420px]">
<nav className="grid grid-cols-3 gap-1 p-2 lg:grid-cols-1" aria-label="规则配置导航">
{NAV_ITEMS.map(item => {
const Icon = item.icon;
const selected = activeSection === item.id;
return (
<button
key={item.id}
type="button"
onClick={() => setActiveSection(item.id)}
aria-current={selected ? 'page' : undefined}
className={`group flex min-w-0 flex-col items-center justify-center gap-1.5 rounded-md px-2 py-2.5 text-center transition lg:flex-row lg:justify-start lg:gap-3 lg:px-3 lg:py-3 lg:text-left ${
selected
? 'bg-slate-950 text-white shadow-sm'
: 'text-slate-700 hover:bg-slate-50 hover:text-slate-950'
}`}
>
<span className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-md border ${
selected
? 'border-white/15 bg-white/10 text-white'
: 'border-slate-200 bg-white text-slate-500 group-hover:text-slate-900'
}`}>
<Icon className="h-4 w-4" />
</span>
<span className="hidden min-w-0 flex-1 lg:block">
<span className="block text-xs font-bold">{item.label}</span>
<span className={`mt-0.5 block truncate text-[9px] ${
selected ? 'text-slate-300' : 'text-slate-500'
}`}>
{item.description}
</span>
</span>
<span className="truncate text-[10px] font-bold lg:hidden">{item.label}</span>
<ChevronRight className={`hidden h-3.5 w-3.5 lg:block ${
selected ? 'text-slate-400' : 'text-slate-300'
}`} />
</button>
);
})}
</nav>
<aside className="overflow-hidden rounded-lg border border-slate-200 bg-white shadow-3xs lg:sticky lg:top-[calc(3.5rem+1rem)] lg:h-[calc(100vh-4.5rem)] lg:min-h-[420px]">
<nav className="grid grid-cols-3 gap-1 p-2 lg:grid-cols-1" aria-label="规则配置导航">
{NAV_ITEMS.map(item => {
const Icon = item.icon;
const selected = activeSection === item.id;
return (
<button
key={item.id}
type="button"
onClick={() => setActiveSection(item.id)}
aria-current={selected ? 'page' : undefined}
className={`group flex min-w-0 flex-col items-center justify-center gap-1.5 rounded-md px-2 py-2.5 text-center transition lg:flex-row lg:justify-start lg:gap-3 lg:px-3 lg:py-3 lg:text-left ${
selected
? 'bg-slate-950 text-white shadow-sm'
: 'text-slate-700 hover:bg-slate-50 hover:text-slate-950'
}`}
>
<span className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-md border ${
selected
? 'border-white/15 bg-white/10 text-white'
: 'border-slate-200 bg-white text-slate-500 group-hover:text-slate-900'
}`}>
<Icon className="h-4 w-4" />
</span>
<span className="hidden min-w-0 flex-1 text-sm font-bold lg:block">
{item.label}
</span>
<span className="truncate text-sm font-bold lg:hidden">{item.label}</span>
<ChevronRight className={`hidden h-3.5 w-3.5 lg:block ${
selected ? 'text-slate-400' : 'text-slate-300'
}`} />
</button>
);
})}
</nav>
</aside>
<section className="min-w-0">
<div className="mb-5 flex min-h-[64px] flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<div className="flex flex-wrap items-center gap-2">
<h1 className="text-xl font-bold tracking-tight text-slate-950">{copy.title}</h1>
{isDirty && (
<span
aria-live="polite"
className="rounded border border-amber-200 bg-amber-50 px-2 py-0.5 text-[9px] font-bold uppercase tracking-widest text-amber-700"
>
</span>
)}
</div>
<p className="mt-1 text-[11px] leading-5 text-slate-500">{copy.description}</p>
</div>
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={activeSection}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.12, ease: 'easeOut' }}
>
{activeSection === 'rules' && (
<RuleManagement
rules={rules}
setRules={setRules}
matcher={matcher}
addToast={addToast}
/>
)}
<button
type="button"
onClick={onSavePublish}
disabled={!isDirty || saving}
title={!isDirty ? '当前配置没有未发布的修改' : undefined}
className={`relative inline-flex h-10 shrink-0 items-center justify-center gap-1.5 rounded-md px-4 text-xs font-bold text-white transition focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-950 ${
isDirty && !saving
? 'bg-slate-950 hover:bg-slate-800'
: 'cursor-not-allowed bg-slate-200 text-slate-400'
}`}
>
<Save className="h-3.5 w-3.5" />
<span>{saving ? '发布中…' : '保存并发布'}</span>
{isDirty && !saving && (
<span className="absolute -right-1 -top-1 flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-rose-400 opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-rose-500" />
</span>
)}
</button>
</div>
{activeSection === 'llm' && (
<LlmConfiguration
config={llmConfig}
onChange={setLlmConfig}
addToast={addToast}
/>
)}
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={activeSection}
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.14, ease: 'easeOut' }}
>
{activeSection === 'rules' && (
<RuleManagement
rules={rules}
setRules={setRules}
matcher={matcher}
addToast={addToast}
/>
)}
{activeSection === 'llm' && (
<LlmConfiguration
config={llmConfig}
onChange={setLlmConfig}
addToast={addToast}
/>
)}
{activeSection === 'matcher' && (
<MatcherConfiguration matcher={matcher} onSelect={selectMatcher} />
)}
</motion.div>
</AnimatePresence>
{activeSection === 'matcher' && (
<MatcherConfiguration matcher={matcher} onSelect={selectMatcher} />
)}
</motion.div>
</AnimatePresence>
</section>
</div>
);
@@ -224,16 +157,11 @@ function MatcherConfiguration({
return (
<div className="space-y-5">
<section className="rounded-lg border border-slate-200 bg-white p-6 shadow-3xs">
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-slate-950 text-white">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-700">
<Cpu className="h-4.5 w-4.5" />
</div>
<div>
<h3 className="text-sm font-bold text-slate-950"></h3>
<p className="mt-1 text-[11px] leading-5 text-slate-500">
线
</p>
</div>
<h3 className="text-sm font-bold text-slate-950"></h3>
</div>
<div className="mt-6 grid gap-4 xl:grid-cols-2">
@@ -257,23 +185,6 @@ function MatcherConfiguration({
/>
</div>
</section>
<section className="rounded-lg border border-slate-200 bg-slate-50/70 p-5">
<p className="text-[9px] font-bold uppercase tracking-widest text-slate-400">稿</p>
<div className="mt-3 flex flex-col gap-3 sm:flex-row sm:items-center">
<span className={`inline-flex w-fit items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-[10px] font-bold ${
matcher === 'llm'
? 'border-violet-200 bg-violet-50 text-violet-700'
: 'border-blue-200 bg-blue-50 text-blue-700'
}`}>
<Check className="h-3 w-3" />
{matcher === 'llm' ? '大模型事件匹配' : '关键词匹配'}
</span>
<p className="text-[10px] leading-5 text-slate-500">
ID
</p>
</div>
</section>
</div>
);
}

View File

@@ -274,7 +274,7 @@ export default function RuleManagement({
return (
<div className="flex flex-col space-y-5">
{/* Rule library toolbar */}
<div className="flex flex-col justify-between space-y-4 md:flex-row md:items-center md:space-y-0">
<div className="flex flex-col justify-between space-y-4 md:flex-row md:items-end md:space-y-0">
<div>
<p className="text-xs text-slate-500 font-sans">
<span className="font-bold text-slate-950 font-mono">{rules.length}</span> {' '}

View File

@@ -14,25 +14,102 @@
"matcher": "ac-keyword",
"rules": [
{
"id": "event-5f5f264e-971a-48f4-84cd-3f89fc2df15f",
"id": "event-ac1bd5a2-8caa-4279-97a8-86cb25bc23b1",
"eventId": "E001",
"name": "拼多多绿色渠道服务热线",
"enabled": true,
"keywords": ["拼多多"]
},
{
"id": "event-26047bc3-9032-49d2-8bf6-4c4c2a0b71d9",
"eventId": "E002",
"name": "抖音商城绿色渠道服务热线",
"enabled": true,
"keywords": ["抖音"]
},
{
"id": "event-670f0d4e-790d-4fb1-923f-297402540c02",
"eventId": "E003",
"name": "“淘宝闪购(饿了么)”绿色渠道服务热线",
"enabled": true,
"keywords": ["饿了么", "闪购", "淘宝闪购"]
},
{
"id": "event-087fd911-05d3-4cb6-841f-80f924b82f48",
"eventId": "E004",
"name": "百度绿色渠道服务热线",
"enabled": true,
"keywords": ["百度", "贴吧"]
},
{
"id": "event-2de56356-1d88-4e0c-9269-840ed26a62fc",
"eventId": "E005",
"name": "东航上航绿色渠道服务热线",
"enabled": true,
"keywords": ["东方航空", "东航", "上海航空", "上航"]
},
{
"id": "event-3314968a-5d38-4221-9b22-fae2e5724138",
"eventId": "E006",
"name": "沪惠保绿色渠道服务热线",
"enabled": true,
"keywords": ["沪惠保"]
},
{
"id": "event-97107370-8b43-46f7-9e2c-ee53d483239e",
"eventId": "E007",
"name": "美团大众点评绿色渠道服务热线",
"enabled": true,
"keywords": ["美团外卖", "美团优", "小象超市", "大众点评", "美团"]
},
{
"id": "event-2ed4a914-9873-4a31-bc11-c676633aa585",
"eventId": "E008",
"name": "邮政速递绿色渠道服务热线",
"enabled": true,
"keywords": ["邮政快递", "邮政", "EMS"]
},
{
"id": "event-af44a455-b9ad-49c3-a492-b6d3254624b2",
"eventId": "E009",
"name": "顺丰速递绿色渠道服务热线",
"enabled": true,
"keywords": ["顺丰", "顺丰快递", "顺丰速运"]
"keywords": ["顺丰快递", "顺丰"]
},
{
"id": "event-d0865681-e962-4d0d-b6db-23d5abdc3e7b",
"eventId": "E002",
"name": "拼多多平台热线",
"id": "event-f6de8bcd-6177-43e3-ac94-4312cd56249c",
"eventId": "E010",
"name": "申通快递绿色渠道服务热线",
"enabled": true,
"keywords": ["拼多多", "拼夕夕"]
"keywords": ["申通快递", "申通"]
},
{
"id": "event-e9ed1542-ff59-458f-9eb4-4afaf199006e",
"eventId": "E003",
"name": "EMS绿色通道",
"id": "event-6c970ed7-5996-4185-bd30-aecec72e3632",
"eventId": "E011",
"name": "圆通快递绿色渠道服务热线",
"enabled": true,
"keywords": ["EMS", "邮政速递"]
"keywords": ["圆通快递", "圆通"]
},
{
"id": "event-2d639d04-2cb4-470b-90a8-a076ff3c0bfd",
"eventId": "E012",
"name": "韵达快递绿色渠道服务热线",
"enabled": true,
"keywords": ["韵达快递", "韵达"]
},
{
"id": "event-b297fd8b-e9be-44fd-b5b7-2fb36c6b24c9",
"eventId": "E013",
"name": "中通快递绿色渠道服务热线",
"enabled": true,
"keywords": ["中通快递", "中通"]
},
{
"id": "event-c55b4313-50f5-4a6c-80a4-6de8468bb8bb",
"eventId": "E014",
"name": "极兔快递绿色渠道服务热线",
"enabled": true,
"keywords": ["极兔快递", "极兔"]
}
]
}