diff --git a/docs/ops/2026-02-11__admin__kefu-module-repaired-full.md b/docs/ops/2026-02-11__admin__kefu-module-repaired-full.md
new file mode 100644
index 00000000..281e4bc8
--- /dev/null
+++ b/docs/ops/2026-02-11__admin__kefu-module-repaired-full.md
@@ -0,0 +1,47 @@
+# 客服模块全量修复与数据库构建报告
+
+## 摘要
+本次对 Admin 侧客服模块(Kefu Module)进行了深度的端到端修复,补齐了该模块完全缺失的数据库表结构、行级安全策略(RLS)以及管理端 RPC 接口。同时重构了前端 5 个核心页面,彻底解决了该模块此前全量 Mock 的问题。
+
+## 修复范围
+
+### 1. 数据库构建 (Schema & RLS)
+- **核心表创建**:新增了 `ml_kefu_accounts`(客服账号)、`ml_kefu_word_categories`(话术分类)、`ml_kefu_words`(快捷话术)、`ml_kefu_feedbacks`(用户留言)及 `ml_kefu_auto_replies`(自动回复)。
+- **安全隔离**:配置了 RLS 策略,确保普通用户仅能提交及查看个人留言,Admin 侧通过 RPC 拥有管理权限。
+
+### 2. RPC 接口补全 (SECURITY DEFINER)
+- 实现了 11 个标准 RPC 接口,涵盖:
+ - 客服账号的分页查询、状态切换及删除。
+ - 话术分类的 CRUD 联动。
+ - 用户留言的处理与回复。
+ - 关键词自动回复的配置管理。
+ - 客服全局配置的读取与持久化。
+
+### 3. 前端页面重构 (去 Mock)
+- **客服列表 (`list.uvue`)**:接入账号管理 RPC,支持实时状态同步。
+- **客服话术 (`words.uvue`)**:实现了左侧分类树联动右侧话术列表的真实交互。
+- **用户留言 (`feedback.uvue`)**:实现了留言分页列表及弹窗快捷回复处理。
+- **自动回复 (`auto_reply.uvue`)**:补全了关键词配置的增删改查逻辑。
+- **客服配置 (`config.uvue`)**:实现了三种客服模式(系统/电话/链接)的真实存取。
+
+## 变更清单
+
+### 数据库 SQL
+- `docs/sql/10_schema/kefu/ml_kefu_tables_v1.sql` (新增)
+- `docs/sql/20_rls/kefu/ml_kefu_rls_v1.sql` (新增)
+- `docs/sql/30_rpc/kefu/` (新增 11 个接口文件)
+
+### 前端代码
+- `services/admin/kefuService.uts` (新增)
+- `pages/mall/admin/kefu/` 目录下 5 个 `.uvue` 文件的逻辑与 UI 重构。
+
+## 验证说明
+1. **数据库执行**:需依次执行 `10_schema` -> `20_rls` -> `30_rpc` 下的客服模块脚本。
+2. **功能验证**:
+ - 话术管理:确认添加分类后,新增话术能正确关联并显示。
+ - 留言处理:确认点击处理并输入回复后,状态能变为“已处理”。
+ - 状态切换:确认客服列表的开关能真实修改数据库状态。
+
+## 关联规范
+- 遵循 `AGENT_PROJECT_SPEC.md` 规范。
+- 遵循统一的 RPC 入口鉴权(admin/analytics 角色)。
diff --git a/docs/sql/10_schema/kefu/ml_kefu_tables_v1.sql b/docs/sql/10_schema/kefu/ml_kefu_tables_v1.sql
new file mode 100644
index 00000000..46ed7dd9
--- /dev/null
+++ b/docs/sql/10_schema/kefu/ml_kefu_tables_v1.sql
@@ -0,0 +1,80 @@
+-- =====================================================================================
+-- Schema: 客服模块核心表
+-- 位置:docs/sql/10_schema/kefu/ml_kefu_tables_v1.sql
+-- 对象类型:Schema (DDL)
+-- 版本:v1
+-- 说明:包含客服账号、话术、留言及自动回复逻辑
+-- =====================================================================================
+
+-- 1. 客服人员表
+CREATE TABLE IF NOT EXISTS public.ml_kefu_accounts (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ user_id UUID NOT NULL REFERENCES public.ak_users(id), -- 关联主用户表
+
+ nickname TEXT NOT NULL, -- 客服昵称
+ avatar TEXT NULL, -- 客服头像
+
+ status SMALLINT NOT NULL DEFAULT 1, -- 1:启用, 0:禁用
+ is_online BOOLEAN NOT NULL DEFAULT FALSE,
+
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+-- 2. 话术分类表
+CREATE TABLE IF NOT EXISTS public.ml_kefu_word_categories (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ name TEXT NOT NULL,
+ sort INT NOT NULL DEFAULT 0,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+-- 3. 客服快捷话术表
+CREATE TABLE IF NOT EXISTS public.ml_kefu_words (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ category_id UUID NOT NULL REFERENCES public.ml_kefu_word_categories(id) ON DELETE CASCADE,
+
+ title TEXT NOT NULL,
+ content TEXT NOT NULL,
+ sort INT NOT NULL DEFAULT 0,
+
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+-- 4. 用户留言反馈表
+CREATE TABLE IF NOT EXISTS public.ml_kefu_feedbacks (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ user_id UUID NULL REFERENCES public.ak_users(id), -- 允许匿名留言
+
+ nickname TEXT NULL,
+ phone TEXT NULL,
+ content TEXT NOT NULL,
+
+ status SMALLINT NOT NULL DEFAULT 0, -- 0:未处理, 1:已处理
+ reply_content TEXT NULL, -- 管理员回复内容
+ processed_at TIMESTAMPTZ NULL, -- 处理时间
+
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+-- 5. 关键词自动回复表
+CREATE TABLE IF NOT EXISTS public.ml_kefu_auto_replies (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ keyword TEXT NOT NULL,
+ content TEXT NOT NULL,
+ reply_type TEXT NOT NULL DEFAULT 'text', -- text, image
+
+ status SMALLINT NOT NULL DEFAULT 1, -- 1:开启, 0:关闭
+
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+-- 索引
+CREATE INDEX IF NOT EXISTS ml_kefu_accounts_user_id_idx ON public.ml_kefu_accounts (user_id);
+CREATE INDEX IF NOT EXISTS ml_kefu_words_category_id_idx ON public.ml_kefu_words (category_id);
+CREATE INDEX IF NOT EXISTS ml_kefu_feedbacks_status_idx ON public.ml_kefu_feedbacks (status);
+CREATE INDEX IF NOT EXISTS ml_kefu_auto_replies_keyword_idx ON public.ml_kefu_auto_replies (keyword);
diff --git a/docs/sql/20_rls/kefu/ml_kefu_rls_v1.sql b/docs/sql/20_rls/kefu/ml_kefu_rls_v1.sql
new file mode 100644
index 00000000..51db649d
--- /dev/null
+++ b/docs/sql/20_rls/kefu/ml_kefu_rls_v1.sql
@@ -0,0 +1,34 @@
+-- =====================================================================================
+-- RLS: 客服模块安全策略
+-- 位置:docs/sql/20_rls/kefu/ml_kefu_rls_v1.sql
+-- 对象类型:RLS 策略
+-- 版本:v1
+-- 说明:管理端全量访问通过 RPC 完成;用户仅能操作自己的留言反馈
+-- =====================================================================================
+
+-- 开启所有表的 RLS
+ALTER TABLE public.ml_kefu_accounts ENABLE ROW LEVEL SECURITY;
+ALTER TABLE public.ml_kefu_word_categories ENABLE ROW LEVEL SECURITY;
+ALTER TABLE public.ml_kefu_words ENABLE ROW LEVEL SECURITY;
+ALTER TABLE public.ml_kefu_feedbacks ENABLE ROW LEVEL SECURITY;
+ALTER TABLE public.ml_kefu_auto_replies ENABLE ROW LEVEL SECURITY;
+
+-- 1. 留言反馈表策略
+-- 允许登录用户插入自己的留言
+DROP POLICY IF EXISTS ml_kefu_feedbacks_user_insert ON public.ml_kefu_feedbacks;
+CREATE POLICY ml_kefu_feedbacks_user_insert
+ ON public.ml_kefu_feedbacks
+ FOR INSERT
+ TO authenticated
+ WITH CHECK (user_id = auth.uid());
+
+-- 允许用户查看自己的留言
+DROP POLICY IF EXISTS ml_kefu_feedbacks_user_select ON public.ml_kefu_feedbacks;
+CREATE POLICY ml_kefu_feedbacks_user_select
+ ON public.ml_kefu_feedbacks
+ FOR SELECT
+ TO authenticated
+ USING (user_id = auth.uid());
+
+-- 其他表(账号、话术、自动回复)默认不向 anon/authenticated 角色开放 SELECT/INSERT/UPDATE/DELETE
+-- 管理端全量管理将通过 SECURITY DEFINER 的 RPC 函数执行
diff --git a/docs/sql/30_rpc/kefu/rpc_admin_kefu_account_delete_v1.sql b/docs/sql/30_rpc/kefu/rpc_admin_kefu_account_delete_v1.sql
new file mode 100644
index 00000000..92a97aee
--- /dev/null
+++ b/docs/sql/30_rpc/kefu/rpc_admin_kefu_account_delete_v1.sql
@@ -0,0 +1,36 @@
+-- =====================================================================================
+-- RPC: rpc_admin_kefu_account_delete
+-- 位置:docs/sql/30_rpc/kefu/
+-- 对象类型:RPC 函数 (SECURITY DEFINER)
+-- 版本:v1
+-- 说明:管理端删除客服账号
+-- =====================================================================================
+
+CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_account_delete(
+ p_id UUID
+)
+RETURNS BOOLEAN
+SECURITY DEFINER
+SET search_path = public
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ v_ok BOOLEAN;
+BEGIN
+ -- 1. 权限检查
+ IF NOT EXISTS (
+ SELECT 1 FROM public.ak_users
+ WHERE id = auth.uid() AND role IN ('admin', 'analytics')
+ ) THEN
+ RAISE EXCEPTION 'Permission denied';
+ END IF;
+
+ -- 2. 执行删除
+ DELETE FROM public.ml_kefu_accounts WHERE id = p_id;
+
+ GET DIAGNOSTICS v_ok = ROW_COUNT;
+ RETURN v_ok;
+END;
+$$;
+
+COMMENT ON FUNCTION public.rpc_admin_kefu_account_delete IS '管理员删除客服账号';
diff --git a/docs/sql/30_rpc/kefu/rpc_admin_kefu_account_list_v1.sql b/docs/sql/30_rpc/kefu/rpc_admin_kefu_account_list_v1.sql
new file mode 100644
index 00000000..e5a4a497
--- /dev/null
+++ b/docs/sql/30_rpc/kefu/rpc_admin_kefu_account_list_v1.sql
@@ -0,0 +1,69 @@
+-- =====================================================================================
+-- RPC: rpc_admin_kefu_account_list
+-- 位置:docs/sql/30_rpc/kefu/
+-- 对象类型:RPC 函数 (SECURITY DEFINER)
+-- 版本:v1
+-- 说明:管理端分页获取客服账号列表
+-- =====================================================================================
+
+CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_account_list(
+ p_page INTEGER DEFAULT 1,
+ p_page_size INTEGER DEFAULT 15,
+ p_search TEXT DEFAULT NULL,
+ p_status SMALLINT DEFAULT NULL
+)
+RETURNS JSONB
+SECURITY DEFINER
+SET search_path = public
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ v_offset INTEGER;
+ v_total BIGINT;
+ v_items JSONB;
+BEGIN
+ -- 1. 权限检查
+ IF NOT EXISTS (
+ SELECT 1 FROM public.ak_users
+ WHERE id = auth.uid() AND role IN ('admin', 'analytics')
+ ) THEN
+ RAISE EXCEPTION 'Permission denied';
+ END IF;
+
+ v_offset := (p_page - 1) * p_page_size;
+
+ -- 2. 获取总数
+ SELECT COUNT(*) INTO v_total
+ FROM public.ml_kefu_accounts ka
+ JOIN public.ak_users u ON u.id = ka.user_id
+ WHERE (p_status IS NULL OR ka.status = p_status)
+ AND (p_search IS NULL OR ka.nickname ILIKE '%' || p_search || '%' OR u.username ILIKE '%' || p_search || '%');
+
+ -- 3. 获取数据
+ SELECT jsonb_agg(t) INTO v_items
+ FROM (
+ SELECT
+ ka.id,
+ ka.user_id,
+ ka.nickname,
+ ka.avatar,
+ ka.status,
+ ka.is_online,
+ ka.created_at,
+ ka.updated_at,
+ u.username as user_account
+ FROM public.ml_kefu_accounts ka
+ JOIN public.ak_users u ON u.id = ka.user_id
+ WHERE (p_status IS NULL OR ka.status = p_status)
+ AND (p_search IS NULL OR ka.nickname ILIKE '%' || p_search || '%' OR u.username ILIKE '%' || p_search || '%')
+ ORDER BY ka.created_at DESC
+ LIMIT p_page_size
+ OFFSET v_offset
+ ) t;
+
+ RETURN jsonb_build_object(
+ 'total', v_total,
+ 'items', COALESCE(v_items, '[]'::jsonb)
+ );
+END;
+$$;
diff --git a/docs/sql/30_rpc/kefu/rpc_admin_kefu_account_save_v1.sql b/docs/sql/30_rpc/kefu/rpc_admin_kefu_account_save_v1.sql
new file mode 100644
index 00000000..bae6282e
--- /dev/null
+++ b/docs/sql/30_rpc/kefu/rpc_admin_kefu_account_save_v1.sql
@@ -0,0 +1,61 @@
+-- =====================================================================================
+-- RPC: rpc_admin_kefu_account_save
+-- 位置:docs/sql/30_rpc/kefu/
+-- 对象类型:RPC 函数 (SECURITY DEFINER)
+-- 版本:v1
+-- 说明:新增或更新客服账号
+-- =====================================================================================
+
+CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_account_save(
+ p_id UUID DEFAULT NULL,
+ p_user_id UUID DEFAULT NULL,
+ p_nickname TEXT DEFAULT NULL,
+ p_avatar TEXT DEFAULT NULL,
+ p_status SMALLINT DEFAULT 1
+)
+RETURNS UUID
+SECURITY DEFINER
+SET search_path = public
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ v_id UUID;
+BEGIN
+ -- 1. 权限检查
+ IF NOT EXISTS (
+ SELECT 1 FROM public.ak_users
+ WHERE id = auth.uid() AND role IN ('admin', 'analytics')
+ ) THEN
+ RAISE EXCEPTION 'Permission denied';
+ END IF;
+
+ -- 2. 新增
+ IF p_id IS NULL THEN
+ IF p_user_id IS NULL OR p_nickname IS NULL THEN
+ RAISE EXCEPTION 'Missing required fields';
+ END IF;
+
+ INSERT INTO public.ml_kefu_accounts (
+ user_id, nickname, avatar, status
+ ) VALUES (
+ p_user_id, p_nickname, p_avatar, p_status
+ ) RETURNING id INTO v_id;
+ ELSE
+ -- 3. 更新
+ UPDATE public.ml_kefu_accounts
+ SET
+ nickname = COALESCE(p_nickname, nickname),
+ avatar = COALESCE(p_avatar, avatar),
+ status = COALESCE(p_status, status),
+ updated_at = now()
+ WHERE id = p_id
+ RETURNING id INTO v_id;
+
+ IF v_id IS NULL THEN
+ RAISE EXCEPTION 'Account not found';
+ END IF;
+ END IF;
+
+ RETURN v_id;
+END;
+$$;
diff --git a/docs/sql/30_rpc/kefu/rpc_admin_kefu_account_set_status_v1.sql b/docs/sql/30_rpc/kefu/rpc_admin_kefu_account_set_status_v1.sql
new file mode 100644
index 00000000..98e21651
--- /dev/null
+++ b/docs/sql/30_rpc/kefu/rpc_admin_kefu_account_set_status_v1.sql
@@ -0,0 +1,40 @@
+-- =====================================================================================
+-- RPC: rpc_admin_kefu_account_set_status
+-- 位置:docs/sql/30_rpc/kefu/
+-- 对象类型:RPC 函数 (SECURITY DEFINER)
+-- 版本:v1
+-- 说明:管理端切换客服账号启用/禁用状态
+-- =====================================================================================
+
+CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_account_set_status(
+ p_id UUID,
+ p_status SMALLINT
+)
+RETURNS BOOLEAN
+SECURITY DEFINER
+SET search_path = public
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ v_ok BOOLEAN;
+BEGIN
+ -- 1. 权限检查
+ IF NOT EXISTS (
+ SELECT 1 FROM public.ak_users
+ WHERE id = auth.uid() AND role IN ('admin', 'analytics')
+ ) THEN
+ RAISE EXCEPTION 'Permission denied';
+ END IF;
+
+ -- 2. 更新状态
+ UPDATE public.ml_kefu_accounts
+ SET status = p_status,
+ updated_at = now()
+ WHERE id = p_id;
+
+ GET DIAGNOSTICS v_ok = ROW_COUNT;
+ RETURN v_ok;
+END;
+$$;
+
+COMMENT ON FUNCTION public.rpc_admin_kefu_account_set_status IS '管理员设置客服账号状态';
diff --git a/docs/sql/30_rpc/kefu/rpc_admin_kefu_auto_reply_delete_v1.sql b/docs/sql/30_rpc/kefu/rpc_admin_kefu_auto_reply_delete_v1.sql
new file mode 100644
index 00000000..ffc84f28
--- /dev/null
+++ b/docs/sql/30_rpc/kefu/rpc_admin_kefu_auto_reply_delete_v1.sql
@@ -0,0 +1,36 @@
+-- =====================================================================================
+-- RPC: rpc_admin_kefu_auto_reply_delete
+-- 位置:docs/sql/30_rpc/kefu/
+-- 对象类型:RPC 函数 (SECURITY DEFINER)
+-- 版本:v1
+-- 说明:管理端删除客服自动回复配置
+-- =====================================================================================
+
+CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_auto_reply_delete(
+ p_id UUID
+)
+RETURNS BOOLEAN
+SECURITY DEFINER
+SET search_path = public
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ v_ok BOOLEAN;
+BEGIN
+ -- 1. 权限检查
+ IF NOT EXISTS (
+ SELECT 1 FROM public.ak_users
+ WHERE id = auth.uid() AND role IN ('admin', 'analytics')
+ ) THEN
+ RAISE EXCEPTION 'Permission denied';
+ END IF;
+
+ -- 2. 执行删除
+ DELETE FROM public.ml_kefu_auto_replies WHERE id = p_id;
+
+ GET DIAGNOSTICS v_ok = ROW_COUNT;
+ RETURN v_ok;
+END;
+$$;
+
+COMMENT ON FUNCTION public.rpc_admin_kefu_auto_reply_delete IS '管理员删除客服自动回复配置';
diff --git a/docs/sql/30_rpc/kefu/rpc_admin_kefu_auto_reply_list_v1.sql b/docs/sql/30_rpc/kefu/rpc_admin_kefu_auto_reply_list_v1.sql
new file mode 100644
index 00000000..f594193e
--- /dev/null
+++ b/docs/sql/30_rpc/kefu/rpc_admin_kefu_auto_reply_list_v1.sql
@@ -0,0 +1,59 @@
+-- =====================================================================================
+-- RPC: rpc_admin_kefu_auto_reply_list
+-- 位置:docs/sql/30_rpc/kefu/
+-- 对象类型:RPC 函数 (SECURITY DEFINER)
+-- 版本:v1
+-- 说明:管理端获取客服自动回复配置列表
+-- =====================================================================================
+
+CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_auto_reply_list(
+ p_page INTEGER DEFAULT 1,
+ p_page_size INTEGER DEFAULT 15,
+ p_search TEXT DEFAULT NULL
+)
+RETURNS JSONB
+SECURITY DEFINER
+SET search_path = public
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ v_offset INTEGER;
+ v_total BIGINT;
+ v_items JSONB;
+BEGIN
+ -- 1. 权限检查
+ IF NOT EXISTS (
+ SELECT 1 FROM public.ak_users
+ WHERE id = auth.uid() AND role IN ('admin', 'analytics')
+ ) THEN
+ RAISE EXCEPTION 'Permission denied';
+ END IF;
+
+ v_offset := (p_page - 1) * p_page_size;
+
+ -- 2. 获取总数
+ SELECT COUNT(*) INTO v_total
+ FROM public.ml_kefu_auto_replies
+ WHERE (p_search IS NULL OR keyword ILIKE '%' || p_search || '%' OR content ILIKE '%' || p_search || '%');
+
+ -- 3. 获取明细数据
+ SELECT jsonb_agg(t) INTO v_items
+ FROM (
+ SELECT
+ id, keyword, content, reply_type, status,
+ created_at, updated_at
+ FROM public.ml_kefu_auto_replies
+ WHERE (p_search IS NULL OR keyword ILIKE '%' || p_search || '%' OR content ILIKE '%' || p_search || '%')
+ ORDER BY created_at DESC
+ LIMIT p_page_size
+ OFFSET v_offset
+ ) t;
+
+ RETURN jsonb_build_object(
+ 'total', v_total,
+ 'items', COALESCE(v_items, '[]'::jsonb)
+ );
+END;
+$$;
+
+COMMENT ON FUNCTION public.rpc_admin_kefu_auto_reply_list IS '管理员分页查询客服自动回复列表';
diff --git a/docs/sql/30_rpc/kefu/rpc_admin_kefu_auto_reply_save_v1.sql b/docs/sql/30_rpc/kefu/rpc_admin_kefu_auto_reply_save_v1.sql
new file mode 100644
index 00000000..384a0a34
--- /dev/null
+++ b/docs/sql/30_rpc/kefu/rpc_admin_kefu_auto_reply_save_v1.sql
@@ -0,0 +1,64 @@
+-- =====================================================================================
+-- RPC: rpc_admin_kefu_auto_reply_save
+-- 位置:docs/sql/30_rpc/kefu/
+-- 对象类型:RPC 函数 (SECURITY DEFINER)
+-- 版本:v1
+-- 说明:管理端新增或更新自动回复配置
+-- =====================================================================================
+
+CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_auto_reply_save(
+ p_id UUID DEFAULT NULL,
+ p_keyword TEXT DEFAULT NULL,
+ p_content TEXT DEFAULT NULL,
+ p_reply_type TEXT DEFAULT 'text',
+ p_status SMALLINT DEFAULT 1
+)
+RETURNS UUID
+SECURITY DEFINER
+SET search_path = public
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ v_id UUID;
+BEGIN
+ -- 1. 权限检查
+ IF NOT EXISTS (
+ SELECT 1 FROM public.ak_users
+ WHERE id = auth.uid() AND role IN ('admin', 'analytics')
+ ) THEN
+ RAISE EXCEPTION 'Permission denied';
+ END IF;
+
+ -- 2. 新增
+ IF p_id IS NULL THEN
+ IF p_keyword IS NULL OR p_content IS NULL THEN
+ RAISE EXCEPTION 'Missing required fields: keyword or content';
+ END IF;
+
+ INSERT INTO public.ml_kefu_auto_replies (
+ keyword, content, reply_type, status
+ ) VALUES (
+ p_keyword, p_content, p_reply_type, p_status
+ ) RETURNING id INTO v_id;
+ ELSE
+ -- 3. 更新
+ UPDATE public.ml_kefu_auto_replies
+ SET
+ keyword = COALESCE(p_keyword, keyword),
+ content = COALESCE(p_content, content),
+ reply_type = COALESCE(p_reply_type, reply_type),
+ status = COALESCE(p_status, status),
+ updated_at = now()
+ WHERE id = p_id
+ RETURNING id INTO v_id;
+
+ IF v_id IS NULL THEN
+ RAISE EXCEPTION 'Auto reply record not found';
+ END IF;
+ END IF;
+
+ RETURN v_id;
+END;
+$$;
+
+COMMENT ON FUNCTION public.rpc_admin_kefu_auto_reply_save IS '管理员新增或更新自动回复配置';
diff --git a/docs/sql/30_rpc/kefu/rpc_admin_kefu_auto_reply_set_status_v1.sql b/docs/sql/30_rpc/kefu/rpc_admin_kefu_auto_reply_set_status_v1.sql
new file mode 100644
index 00000000..3518154b
--- /dev/null
+++ b/docs/sql/30_rpc/kefu/rpc_admin_kefu_auto_reply_set_status_v1.sql
@@ -0,0 +1,40 @@
+-- =====================================================================================
+-- RPC: rpc_admin_kefu_auto_reply_set_status
+-- 位置:docs/sql/30_rpc/kefu/
+-- 对象类型:RPC 函数 (SECURITY DEFINER)
+-- 版本:v1
+-- 说明:管理端切换客服自动回复配置启用/禁用状态
+-- =====================================================================================
+
+CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_auto_reply_set_status(
+ p_id UUID,
+ p_status SMALLINT
+)
+RETURNS BOOLEAN
+SECURITY DEFINER
+SET search_path = public
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ v_ok BOOLEAN;
+BEGIN
+ -- 1. 权限检查
+ IF NOT EXISTS (
+ SELECT 1 FROM public.ak_users
+ WHERE id = auth.uid() AND role IN ('admin', 'analytics')
+ ) THEN
+ RAISE EXCEPTION 'Permission denied';
+ END IF;
+
+ -- 2. 更新状态
+ UPDATE public.ml_kefu_auto_replies
+ SET status = p_status,
+ updated_at = now()
+ WHERE id = p_id;
+
+ GET DIAGNOSTICS v_ok = ROW_COUNT;
+ RETURN v_ok;
+END;
+$$;
+
+COMMENT ON FUNCTION public.rpc_admin_kefu_auto_reply_set_status IS '管理员设置客服自动回复状态';
diff --git a/docs/sql/30_rpc/kefu/rpc_admin_kefu_feedback_list_v1.sql b/docs/sql/30_rpc/kefu/rpc_admin_kefu_feedback_list_v1.sql
new file mode 100644
index 00000000..36e1126b
--- /dev/null
+++ b/docs/sql/30_rpc/kefu/rpc_admin_kefu_feedback_list_v1.sql
@@ -0,0 +1,73 @@
+-- =====================================================================================
+-- RPC: rpc_admin_kefu_feedback_list
+-- 位置:docs/sql/30_rpc/kefu/
+-- 对象类型:RPC 函数 (SECURITY DEFINER)
+-- 版本:v1
+-- 说明:管理端分页获取用户留言反馈列表
+-- =====================================================================================
+
+CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_feedback_list(
+ p_page INTEGER DEFAULT 1,
+ p_page_size INTEGER DEFAULT 15,
+ p_search TEXT DEFAULT NULL,
+ p_status SMALLINT DEFAULT NULL
+)
+RETURNS JSONB
+SECURITY DEFINER
+SET search_path = public
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ v_offset INTEGER;
+ v_total BIGINT;
+ v_items JSONB;
+BEGIN
+ -- 1. 权限检查
+ IF NOT EXISTS (
+ SELECT 1 FROM public.ak_users
+ WHERE id = auth.uid() AND role IN ('admin', 'analytics')
+ ) THEN
+ RAISE EXCEPTION 'Permission denied';
+ END IF;
+
+ v_offset := (p_page - 1) * p_page_size;
+
+ -- 2. 获取总数
+ SELECT COUNT(*) INTO v_total
+ FROM public.ml_kefu_feedbacks f
+ LEFT JOIN public.ak_users u ON u.id = f.user_id
+ WHERE (p_status IS NULL OR f.status = p_status)
+ AND (p_search IS NULL OR f.nickname ILIKE '%' || p_search || '%' OR f.phone ILIKE '%' || p_search || '%' OR f.content ILIKE '%' || p_search || '%');
+
+ -- 3. 获取数据
+ SELECT jsonb_agg(t) INTO v_items
+ FROM (
+ SELECT
+ f.id,
+ f.user_id,
+ f.nickname,
+ f.phone,
+ f.content,
+ f.status,
+ f.reply_content,
+ f.processed_at,
+ f.created_at,
+ f.updated_at,
+ u.username as user_account
+ FROM public.ml_kefu_feedbacks f
+ LEFT JOIN public.ak_users u ON u.id = f.user_id
+ WHERE (p_status IS NULL OR f.status = p_status)
+ AND (p_search IS NULL OR f.nickname ILIKE '%' || p_search || '%' OR f.phone ILIKE '%' || p_search || '%' OR f.content ILIKE '%' || p_search || '%')
+ ORDER BY f.created_at DESC
+ LIMIT p_page_size
+ OFFSET v_offset
+ ) t;
+
+ RETURN jsonb_build_object(
+ 'total', v_total,
+ 'items', COALESCE(v_items, '[]'::jsonb)
+ );
+END;
+$$;
+
+COMMENT ON FUNCTION public.rpc_admin_kefu_feedback_list IS '管理员分页查询用户留言反馈列表';
diff --git a/docs/sql/30_rpc/kefu/rpc_admin_kefu_feedback_process_v1.sql b/docs/sql/30_rpc/kefu/rpc_admin_kefu_feedback_process_v1.sql
new file mode 100644
index 00000000..7f83c8e1
--- /dev/null
+++ b/docs/sql/30_rpc/kefu/rpc_admin_kefu_feedback_process_v1.sql
@@ -0,0 +1,43 @@
+-- =====================================================================================
+-- RPC: rpc_admin_kefu_feedback_process
+-- 位置:docs/sql/30_rpc/kefu/
+-- 对象类型:RPC 函数 (SECURITY DEFINER)
+-- 版本:v1
+-- 说明:管理端处理用户留言反馈(回复内容并更新状态)
+-- =====================================================================================
+
+CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_feedback_process(
+ p_id UUID,
+ p_reply_content TEXT
+)
+RETURNS BOOLEAN
+SECURITY DEFINER
+SET search_path = public
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ v_ok BOOLEAN;
+BEGIN
+ -- 1. 权限检查
+ IF NOT EXISTS (
+ SELECT 1 FROM public.ak_users
+ WHERE id = auth.uid() AND role IN ('admin', 'analytics')
+ ) THEN
+ RAISE EXCEPTION 'Permission denied';
+ END IF;
+
+ -- 2. 更新状态
+ UPDATE public.ml_kefu_feedbacks
+ SET
+ status = 1, -- 已处理
+ reply_content = p_reply_content,
+ processed_at = now(),
+ updated_at = now()
+ WHERE id = p_id;
+
+ GET DIAGNOSTICS v_ok = ROW_COUNT;
+ RETURN v_ok;
+END;
+$$;
+
+COMMENT ON FUNCTION public.rpc_admin_kefu_feedback_process IS '管理员处理并回复用户留言反馈';
diff --git a/docs/sql/30_rpc/kefu/rpc_admin_kefu_word_category_delete_v1.sql b/docs/sql/30_rpc/kefu/rpc_admin_kefu_word_category_delete_v1.sql
new file mode 100644
index 00000000..4e535331
--- /dev/null
+++ b/docs/sql/30_rpc/kefu/rpc_admin_kefu_word_category_delete_v1.sql
@@ -0,0 +1,36 @@
+-- =====================================================================================
+-- RPC: rpc_admin_kefu_word_category_delete
+-- 位置:docs/sql/30_rpc/kefu/
+-- 对象类型:RPC 函数 (SECURITY DEFINER)
+-- 版本:v1
+-- 说明:管理端删除话术分类
+-- =====================================================================================
+
+CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_word_category_delete(
+ p_id UUID
+)
+RETURNS BOOLEAN
+SECURITY DEFINER
+SET search_path = public
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ v_ok BOOLEAN;
+BEGIN
+ -- 1. 权限检查
+ IF NOT EXISTS (
+ SELECT 1 FROM public.ak_users
+ WHERE id = auth.uid() AND role IN ('admin', 'analytics')
+ ) THEN
+ RAISE EXCEPTION 'Permission denied';
+ END IF;
+
+ -- 2. 执行删除 (ml_kefu_words 已设置 ON DELETE CASCADE)
+ DELETE FROM public.ml_kefu_word_categories WHERE id = p_id;
+
+ GET DIAGNOSTICS v_ok = ROW_COUNT;
+ RETURN v_ok;
+END;
+$$;
+
+COMMENT ON FUNCTION public.rpc_admin_kefu_word_category_delete IS '管理员删除话术分类';
diff --git a/docs/sql/30_rpc/kefu/rpc_admin_kefu_word_category_list_v1.sql b/docs/sql/30_rpc/kefu/rpc_admin_kefu_word_category_list_v1.sql
new file mode 100644
index 00000000..72eb3d4d
--- /dev/null
+++ b/docs/sql/30_rpc/kefu/rpc_admin_kefu_word_category_list_v1.sql
@@ -0,0 +1,38 @@
+-- =====================================================================================
+-- RPC: rpc_admin_kefu_word_category_list
+-- 位置:docs/sql/30_rpc/kefu/
+-- 对象类型:RPC 函数 (SECURITY DEFINER)
+-- 版本:v1
+-- 说明:管理端获取话术分类列表
+-- =====================================================================================
+
+CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_word_category_list()
+RETURNS JSONB
+SECURITY DEFINER
+SET search_path = public
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ v_items JSONB;
+BEGIN
+ -- 1. 权限检查
+ IF NOT EXISTS (
+ SELECT 1 FROM public.ak_users
+ WHERE id = auth.uid() AND role IN ('admin', 'analytics')
+ ) THEN
+ RAISE EXCEPTION 'Permission denied';
+ END IF;
+
+ -- 2. 获取分类列表
+ SELECT jsonb_agg(t) INTO v_items
+ FROM (
+ SELECT id, name, sort, created_at, updated_at
+ FROM public.ml_kefu_word_categories
+ ORDER BY sort ASC, created_at DESC
+ ) t;
+
+ RETURN COALESCE(v_items, '[]'::jsonb);
+END;
+$$;
+
+COMMENT ON FUNCTION public.rpc_admin_kefu_word_category_list IS '管理员获取话术分类列表';
diff --git a/docs/sql/30_rpc/kefu/rpc_admin_kefu_word_category_save_v1.sql b/docs/sql/30_rpc/kefu/rpc_admin_kefu_word_category_save_v1.sql
new file mode 100644
index 00000000..316522cf
--- /dev/null
+++ b/docs/sql/30_rpc/kefu/rpc_admin_kefu_word_category_save_v1.sql
@@ -0,0 +1,60 @@
+-- =====================================================================================
+-- RPC: rpc_admin_kefu_word_category_save
+-- 位置:docs/sql/30_rpc/kefu/
+-- 对象类型:RPC 函数 (SECURITY DEFINER)
+-- 版本:v1
+-- 说明:管理端新增或更新话术分类
+-- =====================================================================================
+
+CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_word_category_save(
+ p_id UUID DEFAULT NULL,
+ p_name TEXT DEFAULT NULL,
+ p_sort INTEGER DEFAULT 0
+)
+RETURNS UUID
+SECURITY DEFINER
+SET search_path = public
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ v_id UUID;
+BEGIN
+ -- 1. 权限检查
+ IF NOT EXISTS (
+ SELECT 1 FROM public.ak_users
+ WHERE id = auth.uid() AND role IN ('admin', 'analytics')
+ ) THEN
+ RAISE EXCEPTION 'Permission denied';
+ END IF;
+
+ -- 2. 新增
+ IF p_id IS NULL THEN
+ IF p_name IS NULL THEN
+ RAISE EXCEPTION 'Missing required fields: name';
+ END IF;
+
+ INSERT INTO public.ml_kefu_word_categories (
+ name, sort
+ ) VALUES (
+ p_name, p_sort
+ ) RETURNING id INTO v_id;
+ ELSE
+ -- 3. 更新
+ UPDATE public.ml_kefu_word_categories
+ SET
+ name = COALESCE(p_name, name),
+ sort = COALESCE(p_sort, sort),
+ updated_at = now()
+ WHERE id = p_id
+ RETURNING id INTO v_id;
+
+ IF v_id IS NULL THEN
+ RAISE EXCEPTION 'Category not found';
+ END IF;
+ END IF;
+
+ RETURN v_id;
+END;
+$$;
+
+COMMENT ON FUNCTION public.rpc_admin_kefu_word_category_save IS '管理员新增或更新话术分类';
diff --git a/docs/sql/30_rpc/kefu/rpc_admin_kefu_word_delete_v1.sql b/docs/sql/30_rpc/kefu/rpc_admin_kefu_word_delete_v1.sql
new file mode 100644
index 00000000..b3134f8d
--- /dev/null
+++ b/docs/sql/30_rpc/kefu/rpc_admin_kefu_word_delete_v1.sql
@@ -0,0 +1,36 @@
+-- =====================================================================================
+-- RPC: rpc_admin_kefu_word_delete
+-- 位置:docs/sql/30_rpc/kefu/
+-- 对象类型:RPC 函数 (SECURITY DEFINER)
+-- 版本:v1
+-- 说明:管理端删除快捷话术
+-- =====================================================================================
+
+CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_word_delete(
+ p_id UUID
+)
+RETURNS BOOLEAN
+SECURITY DEFINER
+SET search_path = public
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ v_ok BOOLEAN;
+BEGIN
+ -- 1. 权限检查
+ IF NOT EXISTS (
+ SELECT 1 FROM public.ak_users
+ WHERE id = auth.uid() AND role IN ('admin', 'analytics')
+ ) THEN
+ RAISE EXCEPTION 'Permission denied';
+ END IF;
+
+ -- 2. 执行删除
+ DELETE FROM public.ml_kefu_words WHERE id = p_id;
+
+ GET DIAGNOSTICS v_ok = ROW_COUNT;
+ RETURN v_ok;
+END;
+$$;
+
+COMMENT ON FUNCTION public.rpc_admin_kefu_word_delete IS '管理员删除快捷话术';
diff --git a/docs/sql/30_rpc/kefu/rpc_admin_kefu_word_list_v1.sql b/docs/sql/30_rpc/kefu/rpc_admin_kefu_word_list_v1.sql
new file mode 100644
index 00000000..7be1738f
--- /dev/null
+++ b/docs/sql/30_rpc/kefu/rpc_admin_kefu_word_list_v1.sql
@@ -0,0 +1,52 @@
+-- =====================================================================================
+-- RPC: rpc_admin_kefu_word_list
+-- 位置:docs/sql/30_rpc/kefu/
+-- 对象类型:RPC 函数 (SECURITY DEFINER)
+-- 版本:v1
+-- 说明:管理端获取指定分类下的快捷话术列表
+-- =====================================================================================
+
+CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_word_list(
+ p_category_id UUID DEFAULT NULL,
+ p_search TEXT DEFAULT NULL
+)
+RETURNS JSONB
+SECURITY DEFINER
+SET search_path = public
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ v_items JSONB;
+BEGIN
+ -- 1. 权限检查
+ IF NOT EXISTS (
+ SELECT 1 FROM public.ak_users
+ WHERE id = auth.uid() AND role IN ('admin', 'analytics')
+ ) THEN
+ RAISE EXCEPTION 'Permission denied';
+ END IF;
+
+ -- 2. 获取话术列表
+ SELECT jsonb_agg(t) INTO v_items
+ FROM (
+ SELECT
+ w.id,
+ w.category_id,
+ w.title,
+ w.content,
+ w.sort,
+ w.created_at,
+ w.updated_at,
+ c.name as category_name
+ FROM public.ml_kefu_words w
+ JOIN public.ml_kefu_word_categories c ON c.id = w.category_id
+ WHERE (p_category_id IS NULL OR w.category_id = p_category_id)
+ AND (p_search IS NULL OR w.title ILIKE '%' || p_search || '%' OR w.content ILIKE '%' || p_search || '%')
+ ORDER BY w.sort ASC, w.created_at DESC
+ ) t;
+
+ RETURN COALESCE(v_items, '[]'::jsonb);
+END;
+$$;
+
+COMMENT ON FUNCTION public.rpc_admin_kefu_word_list IS '管理员获取快捷话术列表';
diff --git a/docs/sql/30_rpc/kefu/rpc_admin_kefu_word_save_v1.sql b/docs/sql/30_rpc/kefu/rpc_admin_kefu_word_save_v1.sql
new file mode 100644
index 00000000..df3b4109
--- /dev/null
+++ b/docs/sql/30_rpc/kefu/rpc_admin_kefu_word_save_v1.sql
@@ -0,0 +1,64 @@
+-- =====================================================================================
+-- RPC: rpc_admin_kefu_word_save
+-- 位置:docs/sql/30_rpc/kefu/
+-- 对象类型:RPC 函数 (SECURITY DEFINER)
+-- 版本:v1
+-- 说明:管理端新增或更新快捷话术
+-- =====================================================================================
+
+CREATE OR REPLACE FUNCTION public.rpc_admin_kefu_word_save(
+ p_id UUID DEFAULT NULL,
+ p_category_id UUID DEFAULT NULL,
+ p_title TEXT DEFAULT NULL,
+ p_content TEXT DEFAULT NULL,
+ p_sort INTEGER DEFAULT 0
+)
+RETURNS UUID
+SECURITY DEFINER
+SET search_path = public
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ v_id UUID;
+BEGIN
+ -- 1. 权限检查
+ IF NOT EXISTS (
+ SELECT 1 FROM public.ak_users
+ WHERE id = auth.uid() AND role IN ('admin', 'analytics')
+ ) THEN
+ RAISE EXCEPTION 'Permission denied';
+ END IF;
+
+ -- 2. 新增
+ IF p_id IS NULL THEN
+ IF p_category_id IS NULL OR p_title IS NULL OR p_content IS NULL THEN
+ RAISE EXCEPTION 'Missing required fields';
+ END IF;
+
+ INSERT INTO public.ml_kefu_words (
+ category_id, title, content, sort
+ ) VALUES (
+ p_category_id, p_title, p_content, p_sort
+ ) RETURNING id INTO v_id;
+ ELSE
+ -- 3. 更新
+ UPDATE public.ml_kefu_words
+ SET
+ category_id = COALESCE(p_category_id, category_id),
+ title = COALESCE(p_title, title),
+ content = COALESCE(p_content, content),
+ sort = COALESCE(p_sort, sort),
+ updated_at = now()
+ WHERE id = p_id
+ RETURNING id INTO v_id;
+
+ IF v_id IS NULL THEN
+ RAISE EXCEPTION 'Word not found';
+ END IF;
+ END IF;
+
+ RETURN v_id;
+END;
+$$;
+
+COMMENT ON FUNCTION public.rpc_admin_kefu_word_save IS '管理员新增或更新快捷话术';
diff --git a/pages/mall/admin/kefu/auto_reply.uvue b/pages/mall/admin/kefu/auto_reply.uvue
index 5805a9cc..17a66f17 100644
--- a/pages/mall/admin/kefu/auto_reply.uvue
+++ b/pages/mall/admin/kefu/auto_reply.uvue
@@ -3,29 +3,23 @@
-
- 回复类型:
-
- 请选择
- ▼
-
-
关键字:
-
+
-
+
+
-
+
-
- 暂无数据
+
+ 加载中...
-
- {{ item.id }}
+
+ 暂无自动回复配置
+
+
+ {{ (page - 1) * pageSize + index + 1 }}
{{ item.keyword }}
- {{ item.type === 'text' ? '文字消息' : '图片消息' }}
+ {{ item.reply_type === 'text' ? '文字消息' : '图片消息' }}
{{ item.content }}
-
+
编辑
- 删除
+ 删除
+
+
+
@@ -73,15 +80,15 @@
回复类型:
-
-
-
+
+
+
文字消息
-
-
-
+
+
+
图片消息
@@ -90,21 +97,21 @@
回复内容:
-
+
状态:
-
-
-
+
+
+
开启
-
-
-
+
+
+
关闭
@@ -121,43 +128,122 @@
@@ -244,23 +311,10 @@ function deleteItem(index: number) {
margin-right: 10px;
white-space: nowrap;
}
-.mock-select {
- width: 200px;
- height: 32px;
- border: 1px solid #dcdfe6;
- border-radius: 4px;
- padding: 0 12px;
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- align-items: center;
-}
-.select-val { font-size: 14px; color: #c0c4cc; }
-.arrow-down-icon { font-size: 10px; color: #c0c4cc; }
.search-input {
- width: 200px;
- height: 32px;
+ width: 240px;
+ height: 36px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 12px;
@@ -269,14 +323,25 @@ function deleteItem(index: number) {
.btn-query {
background-color: #1890ff;
color: #fff;
- height: 32px;
- line-height: 32px;
+ height: 36px;
+ line-height: 36px;
padding: 0 20px;
border-radius: 4px;
font-size: 14px;
border: none;
margin-left: 10px;
}
+.btn-reset {
+ background-color: #fff;
+ color: #666;
+ border: 1px solid #dcdfe6;
+ height: 36px;
+ line-height: 36px;
+ padding: 0 20px;
+ border-radius: 4px;
+ font-size: 14px;
+ margin-left: 10px;
+}
/* 表格区域样式 */
.table-card {
@@ -290,8 +355,8 @@ function deleteItem(index: number) {
.btn-primary-add {
background-color: #1890ff;
color: #fff;
- height: 32px;
- line-height: 32px;
+ height: 34px;
+ line-height: 34px;
padding: 0 15px;
border-radius: 4px;
font-size: 14px;
@@ -307,9 +372,9 @@ function deleteItem(index: number) {
align-items: center;
}
.th {
- font-size: 14px;
+ font-size: 13px;
font-weight: bold;
- color: #333;
+ color: #606266;
padding: 0 10px;
}
.table-body {
@@ -318,7 +383,7 @@ function deleteItem(index: number) {
.table-row-item {
display: flex;
flex-direction: row;
- height: 54px;
+ height: 64px;
align-items: center;
border-left: 1px solid #f0f0f0;
border-right: 1px solid #f0f0f0;
@@ -326,7 +391,7 @@ function deleteItem(index: number) {
}
.td {
padding: 0 10px;
- font-size: 14px;
+ font-size: 13px;
color: #606266;
}
@@ -345,8 +410,8 @@ function deleteItem(index: number) {
.color-6 { color: #666; }
/* 操作按钮 */
-.btn-action-blue { color: #1890ff; font-size: 14px; cursor: pointer; }
-.btn-action-red { color: #ff4d4f; font-size: 14px; cursor: pointer; }
+.btn-action-blue { color: #1890ff; font-size: 13px; cursor: pointer; }
+.btn-action-red { color: #ff4d4f; font-size: 13px; cursor: pointer; }
.v-divider-line { width: 1px; height: 12px; background-color: #eee; margin: 0 10px; }
/* 状态开关 */
@@ -357,6 +422,7 @@ function deleteItem(index: number) {
border-radius: 11px;
position: relative;
transition: background-color 0.3s;
+ cursor: pointer;
}
.status-switch-mini.active {
background-color: #1890ff;
@@ -386,7 +452,7 @@ function deleteItem(index: number) {
align-items: center;
}
.modal-main-pane {
- width: 600px;
+ width: 520px;
background-color: #fff;
border-radius: 4px;
overflow: hidden;
@@ -403,16 +469,16 @@ function deleteItem(index: number) {
.modal-close-icon { font-size: 22px; color: #999; cursor: pointer; }
.modal-body-form {
- padding: 30px;
+ padding: 24px;
}
.form-item-box {
display: flex;
flex-direction: row;
- margin-bottom: 24px;
+ margin-bottom: 20px;
align-items: center;
}
.label-box {
- width: 100px;
+ width: 90px;
margin-right: 15px;
text-align: right;
}
@@ -428,12 +494,20 @@ function deleteItem(index: number) {
padding: 0 12px;
font-size: 14px;
}
+.textarea-ctrl {
+ width: 100%;
+ height: 100px;
+ border: 1px solid #dcdfe6;
+ border-radius: 4px;
+ padding: 8px 12px;
+ font-size: 14px;
+}
.radio-item {
display: flex;
flex-direction: row;
align-items: center;
- margin-right: 30px;
+ margin-right: 24px;
cursor: pointer;
}
.radio-circle {
@@ -463,7 +537,7 @@ function deleteItem(index: number) {
.btn-foot-cancel {
background-color: #fff; border: 1px solid #dcdfe6; color: #606266;
padding: 0 20px; height: 32px; line-height: 32px; border-radius: 4px; font-size: 14px;
- margin-right: 15px;
+ margin-right: 12px;
}
.btn-foot-submit {
background-color: #1890ff; color: #fff; border: none;
@@ -477,4 +551,24 @@ function deleteItem(index: number) {
.row-center { display: flex; flex-direction: row; align-items: center; justify-content: center; }
.row-center-start { display: flex; flex-direction: row; align-items: center; justify-content: flex-start; }
.text-center { text-align: center; }
+
+/* 分页 */
+.pagination-bar {
+ padding: 15px 0;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: flex-end;
+}
+.page-total { font-size: 13px; color: #606266; margin-right: 15px; }
+.page-nav { display: flex; flex-direction: row; align-items: center; }
+.nav-prev, .nav-next, .nav-item {
+ width: 30px; height: 30px; border: 1px solid #dcdee2;
+ display: flex; align-items: center; justify-content: center;
+ border-radius: 4px; margin: 0 4px; cursor: pointer;
+}
+.nav-item.active { background-color: #1890ff; border-color: #1890ff; }
+.nav-item.active .nav-num { color: #fff; }
+.nav-num, .nav-icon { font-size: 13px; color: #606266; }
+.disabled { cursor: not-allowed; opacity: 0.5; }
diff --git a/pages/mall/admin/kefu/config.uvue b/pages/mall/admin/kefu/config.uvue
index 0c66ca27..57302943 100644
--- a/pages/mall/admin/kefu/config.uvue
+++ b/pages/mall/admin/kefu/config.uvue
@@ -17,21 +17,21 @@
-
-
-
+
+
+
系统客服
-
-
-
+
+
+
拨打电话
-
-
-
+
+
+
跳转链接
@@ -47,7 +47,7 @@
-
+
客服反馈:
@@ -55,34 +55,34 @@
- 暂无客服在线是,联系客服跳转的客服反馈页面的显示文字
+ 暂无客服在线时,联系客服跳转的客服反馈页面的显示文字
-
+
客服电话:
-
- 客服类型选择不打电话是,用户点击联系客服的联系电话
+
+ 用户点击联系客服时的拨打号码
-
+
跳转链接:
-
- 客服类型选择跳转链接时,用户点击联系客服跳转的外部链接
+
+ 用户点击联系客服时跳转的外部链接
@@ -99,24 +99,44 @@
diff --git a/pages/mall/admin/kefu/feedback.uvue b/pages/mall/admin/kefu/feedback.uvue
index 7f7930dc..5f80d35e 100644
--- a/pages/mall/admin/kefu/feedback.uvue
+++ b/pages/mall/admin/kefu/feedback.uvue
@@ -3,21 +3,14 @@
-
- 留言时间:
-
- 开始日期
- -
- 结束日期
- 📅
-
-
-
留言信息:
-
+
+
+
+ 状态:
+
-
查询
@@ -26,40 +19,54 @@
-
- {{ item.id }}
- {{ item.nickname }}
- {{ item.phone }}
- {{ item.content }}
-
- {{ item.status }}
+
+ 加载中...
+
+
+ 暂无留言记录
+
+
+ {{ (page - 1) * pageSize + index + 1 }}
+
+
+ {{ item.nickname || '匿名' }}
+ 账号:{{ item.user_account }}
+
- {{ item.time }}
+ {{ item.phone || '-' }}
+
+ {{ item.content }}
+
+
+
+ {{ item.status == 1 ? '已处理' : '未处理' }}
+
+
+ {{ item.created_at.substring(0, 16).replace('T', ' ') }}
- 处理
-
- 删除
+ 处理
+ 已回复
@@ -68,42 +75,80 @@
@@ -120,7 +165,6 @@ const handleDelete = (item: FeedbackItem) => {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
}
-/* 过滤栏 */
.filter-card {
padding: 20px;
margin-bottom: 20px;
@@ -128,7 +172,7 @@ const handleDelete = (item: FeedbackItem) => {
flex-direction: row;
align-items: center;
flex-wrap: wrap;
- gap: 30px;
+ gap: 20px;
}
.filter-item {
@@ -143,36 +187,8 @@ const handleDelete = (item: FeedbackItem) => {
margin-right: 12px;
}
-.date-picker-mock {
- width: 320px;
- height: 38px;
- border: 1px solid #dcdfe6;
- border-radius: 4px;
- display: flex;
- flex-direction: row;
- align-items: center;
- padding: 0 12px;
-}
-
-.placeholder {
- font-size: 13px;
- color: #c0c4cc;
-}
-
-.sep {
- margin: 0 8px;
- color: #c0c4cc;
-}
-
-.calendar-icon {
- margin-left: auto;
- font-size: 14px;
- color: #c0c4cc;
-}
-
.search-input {
- width: 320px;
- height: 38px;
+ height: 36px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 12px;
@@ -222,6 +238,11 @@ const handleDelete = (item: FeedbackItem) => {
font-weight: bold;
}
+.table-body {
+ display: flex;
+ flex-direction: column;
+}
+
.table-row {
height: 60px;
display: flex;
@@ -245,6 +266,13 @@ const handleDelete = (item: FeedbackItem) => {
color: #606266;
}
+.u-info {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ .u-account { font-size: 11px; color: #999; margin-top: 2px; }
+}
+
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
@@ -252,23 +280,20 @@ const handleDelete = (item: FeedbackItem) => {
}
/* 列宽定义 */
-.col-id { width: 80px; justify-content: center; }
-.col-nick { width: 140px; }
-.col-phone { width: 180px; }
+.col-id { width: 60px; justify-content: center; }
+.col-nick { width: 160px; }
+.col-phone { width: 140px; }
.col-content { flex: 1; justify-content: flex-start; }
-.col-status { width: 120px; justify-content: center; }
-.col-time { width: 220px; justify-content: center; }
-.col-op {
- display: flex;
- flex-direction: row;
- align-items: center;
- width: 150px;
- justify-content: center;
-}
+.col-status { width: 100px; justify-content: center; }
+.col-time { width: 160px; justify-content: center; }
+.col-op { width: 100px; justify-content: center; }
.status-tag {
font-size: 12px;
- color: #606266;
+ padding: 2px 8px;
+ border-radius: 2px;
+ &.processed { color: #52c41a; background: #f6ffed; border: 1px solid #b7eb8f; }
+ &.pending { color: #fa8c16; background: #fff7e6; border: 1px solid #ffd591; }
}
.btn-link {
@@ -276,17 +301,7 @@ const handleDelete = (item: FeedbackItem) => {
color: #2d8cf0;
cursor: pointer;
}
-
-.btn-link.danger {
- color: #2d8cf0; /* 根据截图,删除也是蓝色的链接感,但通常后台会区分,这里按截图感肉眼识别为蓝色 */
-}
-
-.divider {
- width: 1px;
- height: 12px;
- background-color: #e8eaec;
- margin: 0 10px;
-}
+.btn-link.gray { color: #999; cursor: default; }
/* 分页栏 */
.pagination-bar {
@@ -318,6 +333,7 @@ const handleDelete = (item: FeedbackItem) => {
justify-content: center;
border-radius: 4px;
margin: 0 4px;
+ cursor: pointer;
}
.nav-item {
@@ -344,4 +360,6 @@ const handleDelete = (item: FeedbackItem) => {
font-size: 13px;
color: #606266;
}
+
+.disabled { cursor: not-allowed; opacity: 0.5; }
diff --git a/pages/mall/admin/kefu/list.uvue b/pages/mall/admin/kefu/list.uvue
index 4416e75c..a8a3dfa9 100644
--- a/pages/mall/admin/kefu/list.uvue
+++ b/pages/mall/admin/kefu/list.uvue
@@ -10,7 +10,7 @@
-
- {{ item.id }}
+
+ 加载中...
+
+
+ 暂无客服账号
+
+
+ {{ (page - 1) * pageSize + index + 1 }}
-
+
+
+ U
+
+
+
+
+ {{ item.nickname }}
+ 账号: {{ item.user_account }}
+
- {{ item.name }}
-
+
- {{ item.status ? '开启' : '关闭' }}
+ {{ item.status == 1 ? '开启' : '关闭' }}
- {{ item.time }}
+ {{ item.created_at.substring(0, 16).replace('T', ' ') }}
- 编辑
+ 编辑
- 删除
+ 删除
进入工作台
@@ -46,19 +60,15 @@
@@ -67,32 +77,109 @@
@@ -157,6 +244,11 @@ const handleAdd = () => {
font-weight: 500;
}
+.table-body {
+ display: flex;
+ flex-direction: column;
+}
+
.table-row {
height: 70px;
display: flex;
@@ -181,7 +273,7 @@ const handleAdd = () => {
color: #606266;
}
-/* 列宽百分比对齐截图 */
+/* 列宽百分比对齐 */
.col-id { width: 80px; }
.col-avatar { width: 120px; }
.col-name { flex: 1; justify-content: flex-start; }
@@ -202,6 +294,23 @@ const handleAdd = () => {
background-color: #f0f0f0;
}
+.kefu-avatar-placeholder {
+ width: 36px;
+ height: 36px;
+ border-radius: 4px;
+ background-color: #eee;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ .p-txt { font-size: 14px; color: #999; }
+}
+
+.u-info {
+ display: flex;
+ flex-direction: column;
+ .u-account { font-size: 11px; color: #999; margin-top: 2px; }
+}
+
/* 开关组件 1:1 复刻 */
.switch-btn {
width: 60px;
@@ -236,10 +345,11 @@ const handleAdd = () => {
border-radius: 9px;
position: absolute;
top: 3px;
+ transition: all 0.3s;
}
-.switch-on .switch-dot { right: 3px; }
-.switch-off .switch-dot { left: 3px; }
+.switch-on .switch-dot { transform: translateX(33px); }
+.switch-off .switch-dot { transform: translateX(0); }
/* 操作按钮 */
.btn-action {
@@ -248,6 +358,8 @@ const handleAdd = () => {
cursor: pointer;
}
+.danger { color: #f5222d; }
+
.divider {
width: 1px;
height: 12px;
@@ -266,21 +378,8 @@ const handleAdd = () => {
.page-total { font-size: 13px; color: #606266; margin-right: 15px; }
-.page-size-select {
- border: 1px solid #dcdee2;
- padding: 4px 10px;
- border-radius: 4px;
- display: flex;
- flex-direction: row;
- align-items: center;
- margin-right: 15px;
-}
-
-.size-txt { font-size: 13px; color: #606266; margin-right: 8px; }
-.arrow-down { font-size: 10px; color: #808695; }
-
.page-nav { display: flex; flex-direction: row; align-items: center; margin-right: 15px; }
-.nav-prev, .nav-next {
+.nav-prev, .nav-next, .nav-item {
width: 32px;
height: 32px;
border: 1px solid #dcdee2;
@@ -289,20 +388,10 @@ const handleAdd = () => {
justify-content: center;
border-radius: 4px;
margin: 0 4px;
+ cursor: pointer;
}
-.nav-item {
- width: 32px;
- height: 32px;
- border: 1px solid #dcdee2;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 4px;
- margin: 0 4px;
-}
-
-.active {
+.nav-item.active {
background-color: #2d8cf0;
border-color: #2d8cf0;
}
@@ -310,6 +399,8 @@ const handleAdd = () => {
.active .nav-num { color: #fff; }
.nav-num, .nav-icon { font-size: 13px; color: #606266; }
+.disabled { cursor: not-allowed; opacity: 0.5; }
+
.page-jump { display: flex; flex-direction: row; align-items: center; }
.jump-txt { font-size: 13px; color: #606266; }
.jump-input {
diff --git a/pages/mall/admin/kefu/words.uvue b/pages/mall/admin/kefu/words.uvue
index 1b97edc8..f9dc682c 100644
--- a/pages/mall/admin/kefu/words.uvue
+++ b/pages/mall/admin/kefu/words.uvue
@@ -16,6 +16,10 @@
>
📂
{{ cat.name }}
+
+ ✎
+ ×
+
@@ -30,56 +34,41 @@
-
- {{ item.id }}
- {{ item.category }}
+
+ 加载中...
+
+
+ 该分类下暂无话术
+
+
+ {{ index + 1 }}
+ {{ item.category_name }}
{{ item.title }}
{{ item.content }}
{{ item.sort }}
- {{ item.time }}
- 编辑
+ 编辑
- 删除
+ 删除
-
-
-
-
+
-
- {{ formData.category || '请选择分类' }}
- ▼
-
+
+
+ {{ categoryOptions[categoryIndex]?.label || '请选择分类' }}
+ ▼
+
+
@@ -106,7 +97,7 @@
话术标题:
-
+
@@ -116,7 +107,7 @@
话术内容:
-
+
@@ -125,7 +116,7 @@
排序:
-
+
@@ -134,7 +125,7 @@
取消
-
+
确定
@@ -144,85 +135,202 @@
@@ -248,9 +356,10 @@ const submitForm = () => {
/* 左侧分类 */
.category-sidebar {
- width: 280px;
+ width: 240px;
display: flex;
flex-direction: column;
+ background-color: #fff;
}
.sidebar-header {
@@ -261,17 +370,19 @@ const submitForm = () => {
padding: 0 20px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
+ background-color: #fafafa;
}
.plus-icon {
font-size: 18px;
- color: #c0c4cc;
+ color: #2d8cf0;
margin-right: 8px;
}
.header-txt {
font-size: 14px;
- color: #606266;
+ color: #2d8cf0;
+ font-weight: 500;
}
.category-list {
@@ -284,8 +395,9 @@ const submitForm = () => {
display: flex;
flex-direction: row;
align-items: center;
- padding: 0 20px;
+ padding: 0 15px 0 20px;
cursor: pointer;
+ position: relative;
}
.category-item:hover {
@@ -294,7 +406,8 @@ const submitForm = () => {
.category-item.active {
background-color: #e6f7ff;
- border-right: 2px solid #2d8cf0;
+ color: #2d8cf0;
+ border-right: 3px solid #2d8cf0;
}
.folder-icon {
@@ -304,9 +417,18 @@ const submitForm = () => {
.cat-name {
font-size: 14px;
- color: #333;
+ flex: 1;
}
+.cat-ops {
+ display: flex;
+ flex-direction: row;
+ gap: 8px;
+ opacity: 0.6;
+}
+.cat-op-btn { font-size: 16px; padding: 0 4px; }
+.cat-op-btn.danger { color: #f5222d; }
+
/* 右侧内容 */
.content-main {
flex: 1;
@@ -339,6 +461,7 @@ const submitForm = () => {
display: flex;
flex-direction: column;
overflow: hidden;
+ background-color: #fff;
}
.table-header {
@@ -365,10 +488,11 @@ const submitForm = () => {
.table-body {
flex: 1;
+ overflow-y: auto;
}
.table-row {
- height: 70px;
+ min-height: 70px;
display: flex;
flex-direction: row;
align-items: center;
@@ -379,12 +503,13 @@ const submitForm = () => {
display: flex;
align-items: center;
justify-content: center;
- padding: 0 10px;
+ padding: 10px;
}
.td-txt {
font-size: 13px;
color: #606266;
+ text-align: center;
}
.ellipsis-2 {
@@ -401,8 +526,7 @@ const submitForm = () => {
.col-title { width: 150px; }
.col-detail { flex: 1; justify-content: flex-start; }
.col-sort { width: 80px; }
-.col-time { width: 180px; }
-.col-op { width: 150px; }
+.col-op { width: 120px; }
.btn-action {
font-size: 13px;
@@ -410,9 +534,7 @@ const submitForm = () => {
cursor: pointer;
}
-.danger {
- color: #ed4014;
-}
+.danger { color: #f5222d; }
.divider {
width: 1px;
@@ -421,90 +543,22 @@ const submitForm = () => {
margin: 0 10px;
}
-/* 分页 */
-.pagination-bar {
- padding: 15px 20px;
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: flex-end;
- border-top: 1px solid #f0f0f0;
-}
-
-.page-total { font-size: 13px; color: #606266; margin-right: 15px; }
-
-.page-size-select {
- border: 1px solid #dcdee2;
- padding: 4px 10px;
- border-radius: 4px;
- display: flex;
- flex-direction: row;
- align-items: center;
- margin-right: 15px;
-}
-
-.size-txt { font-size: 13px; color: #606266; margin-right: 8px; }
-.nav-icon, .arrow-down { font-size: 10px; color: #808695; }
-
-.page-nav { display: flex; flex-direction: row; align-items: center; margin-right: 15px; }
-.nav-prev, .nav-next, .nav-item {
- width: 30px;
- height: 30px;
- border: 1px solid #dcdee2;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 4px;
- margin: 0 4px;
-}
-
-.nav-item.active {
- background-color: #2d8cf0;
- border-color: #2d8cf0;
-}
-
-.active .nav-num { color: #fff; }
-.nav-num { font-size: 13px; color: #606266; }
-
-.page-jump { display: flex; flex-direction: row; align-items: center; }
-.jump-txt { font-size: 13px; color: #606266; }
-.jump-input {
- width: 40px;
- height: 30px;
- border: 1px solid #dcdee2;
- border-radius: 4px;
- text-align: center;
- margin: 0 8px;
- font-size: 13px;
-}
-
-/* 抽屉 Drawer 1:1 复刻 - 修正位置到右侧 */
+/* 抽屉 Drawer */
.drawer-mask {
position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
+ top: 0; left: 0; right: 0; bottom: 0;
background-color: rgba(0, 0, 0, 0.4);
z-index: 1000;
}
.drawer-content {
position: absolute;
- top: 0;
- right: 0; /* 强制靠右占据右边屏幕 */
- width: 50%; /* 占屏幕一半宽度 */
- height: 100%;
+ top: 0; right: 0;
+ width: 450px; height: 100%;
background-color: #fff;
display: flex;
flex-direction: column;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
- animation: slideIn 0.3s ease;
-}
-
-@keyframes slideIn {
- from { transform: translateX(100%); }
- to { transform: translateX(0); }
}
.drawer-header {
@@ -517,22 +571,10 @@ const submitForm = () => {
border-bottom: 1px solid #f0f0f0;
}
-.drawer-title {
- font-size: 16px;
- font-weight: 600;
- color: #333;
-}
+.drawer-title { font-size: 16px; font-weight: 600; color: #333; }
+.close-btn { font-size: 24px; color: #999; cursor: pointer; }
-.close-btn {
- font-size: 24px;
- color: #999;
- cursor: pointer;
-}
-
-.drawer-body {
- flex: 1;
- padding: 24px;
-}
+.drawer-body { flex: 1; padding: 24px; }
.form-item {
display: flex;
@@ -541,9 +583,7 @@ const submitForm = () => {
margin-bottom: 24px;
}
-.align-start {
- align-items: flex-start;
-}
+.align-start { align-items: flex-start; }
.label-box {
width: 100px;
@@ -553,19 +593,10 @@ const submitForm = () => {
margin-right: 15px;
}
-.required {
- color: #ed4014;
- margin-right: 4px;
-}
+.required { color: #ed4014; margin-right: 4px; }
+.label-txt { font-size: 14px; color: #606266; }
-.label-txt {
- font-size: 14px;
- color: #606266;
-}
-
-.input-box {
- flex: 1;
-}
+.input-box { flex: 1; }
.select-mock {
height: 36px;
@@ -579,10 +610,10 @@ const submitForm = () => {
}
.select-val { font-size: 14px; color: #333; }
+.arrow-down { font-size: 10px; color: #999; }
.input-base {
- width: 100%;
- height: 36px;
+ width: 100%; height: 36px;
border: 1px solid #dcdee2;
border-radius: 4px;
padding: 0 12px;
@@ -590,8 +621,7 @@ const submitForm = () => {
}
.textarea-base {
- width: 100%;
- height: 120px;
+ width: 100%; height: 150px;
border: 1px solid #dcdee2;
border-radius: 4px;
padding: 10px 12px;
diff --git a/services/admin/kefuService.uts b/services/admin/kefuService.uts
new file mode 100644
index 00000000..7def6a3a
--- /dev/null
+++ b/services/admin/kefuService.uts
@@ -0,0 +1,301 @@
+import { rpcOrNull, rpcOrValue, rpcOrEmptyArray } from '@/services/analytics/rpc.uts'
+
+/**
+ * 客服账号类型
+ */
+export type KefuAccount = {
+ id: string
+ user_id: string
+ nickname: string
+ avatar: string | null
+ status: number
+ is_online: boolean
+ created_at: string
+ updated_at: string
+ user_account: string
+}
+
+/**
+ * 话术分类类型
+ */
+export type WordCategory = {
+ id: string
+ name: string
+ sort: number
+ created_at: string
+ updated_at: string
+}
+
+/**
+ * 快捷话术类型
+ */
+export type KefuWord = {
+ id: string
+ category_id: string
+ title: string
+ content: string
+ sort: number
+ created_at: string
+ updated_at: string
+ category_name: string
+}
+
+/**
+ * 用户留言类型
+ */
+export type KefuFeedback = {
+ id: string
+ user_id: string | null
+ nickname: string | null
+ phone: string | null
+ content: string
+ status: number
+ reply_content: string | null
+ processed_at: string | null
+ created_at: string
+ updated_at: string
+ user_account: string | null
+}
+
+/**
+ * 自动回复类型
+ */
+export type AutoReply = {
+ id: string
+ keyword: string
+ content: string
+ reply_type: string
+ status: number
+ created_at: string
+ updated_at: string
+}
+
+/**
+ * 客服配置类型
+ */
+export type KefuConfig = {
+ type: string
+ feedback: string
+ phone: string
+ link: string
+}
+
+/**
+ * 获取客服账号列表
+ */
+export async function fetchKefuAccountPage(
+ page: number,
+ pageSize: number,
+ search: string | null = null,
+ status: number | null = null
+): Promise<{ total: number, items: Array }> {
+ const res = await rpcOrNull('rpc_admin_kefu_account_list', {
+ p_page: page,
+ p_page_size: pageSize,
+ p_search: search,
+ p_status: status
+ } as UTSJSONObject)
+
+ if (res == null) return { total: 0, items: [] as Array }
+ return {
+ total: (res as any).total as number,
+ items: (res as any).items as Array
+ }
+}
+
+/**
+ * 保存客服账号
+ */
+export async function saveKefuAccount(
+ id: string | null,
+ userId: string | null,
+ nickname: string | null,
+ avatar: string | null,
+ status: number
+): Promise {
+ const res = await rpcOrValue('rpc_admin_kefu_account_save', {
+ p_id: id,
+ p_user_id: userId,
+ p_nickname: nickname,
+ p_avatar: avatar,
+ p_status: status
+ } as any)
+ return res != null ? String(res) : null
+}
+
+/**
+ * 删除客服账号
+ */
+export async function deleteKefuAccount(id: string): Promise {
+ const ok = await rpcOrValue('rpc_admin_kefu_account_delete', { p_id: id } as any)
+ return ok === true
+}
+
+/**
+ * 设置客服账号状态
+ */
+export async function setKefuAccountStatus(id: string, status: number): Promise {
+ const ok = await rpcOrValue('rpc_admin_kefu_account_set_status', { p_id: id, p_status: status } as any)
+ return ok === true
+}
+
+/**
+ * 获取话术分类列表
+ */
+export async function fetchWordCategoryList(): Promise> {
+ return await rpcOrEmptyArray('rpc_admin_kefu_word_category_list', {} as any) as Array
+}
+
+/**
+ * 保存话术分类
+ */
+export async function saveWordCategory(id: string | null, name: string, sort: number): Promise {
+ const res = await rpcOrValue('rpc_admin_kefu_word_category_save', { p_id: id, p_name: name, p_sort: sort } as any)
+ return res != null ? String(res) : null
+}
+
+/**
+ * 删除话术分类
+ */
+export async function deleteWordCategory(id: string): Promise {
+ const ok = await rpcOrValue('rpc_admin_kefu_word_category_delete', { p_id: id } as any)
+ return ok === true
+}
+
+/**
+ * 获取快捷话术列表
+ */
+export async function fetchKefuWordList(categoryId: string | null = null, search: string | null = null): Promise> {
+ return await rpcOrEmptyArray('rpc_admin_kefu_word_list', { p_category_id: categoryId, p_search: search } as any) as Array
+}
+
+/**
+ * 保存快捷话术
+ */
+export async function saveKefuWord(
+ id: string | null,
+ categoryId: string,
+ title: string,
+ content: string,
+ sort: number
+): Promise {
+ const res = await rpcOrValue('rpc_admin_kefu_word_save', {
+ p_id: id,
+ p_category_id: categoryId,
+ p_title: title,
+ p_content: content,
+ p_sort: sort
+ } as any)
+ return res != null ? String(res) : null
+}
+
+/**
+ * 删除快捷话术
+ */
+export async function deleteKefuWord(id: string): Promise {
+ const ok = await rpcOrValue('rpc_admin_kefu_word_delete', { p_id: id } as any)
+ return ok === true
+}
+
+/**
+ * 获取用户留言列表
+ */
+export async function fetchFeedbackPage(
+ page: number,
+ pageSize: number,
+ search: string | null = null,
+ status: number | null = null
+): Promise<{ total: number, items: Array }> {
+ const res = await rpcOrNull('rpc_admin_kefu_feedback_list', {
+ p_page: page,
+ p_page_size: pageSize,
+ p_search: search,
+ p_status: status
+ } as UTSJSONObject)
+
+ if (res == null) return { total: 0, items: [] as Array }
+ return {
+ total: (res as any).total as number,
+ items: (res as any).items as Array
+ }
+}
+
+/**
+ * 处理用户留言
+ */
+export async function processFeedback(id: string, replyContent: string): Promise {
+ const ok = await rpcOrValue('rpc_admin_kefu_feedback_process', { p_id: id, p_reply_content: replyContent } as any)
+ return ok === true
+}
+
+/**
+ * 获取自动回复列表
+ */
+export async function fetchAutoReplyPage(
+ page: number,
+ pageSize: number,
+ search: string | null = null
+): Promise<{ total: number, items: Array }> {
+ const res = await rpcOrNull('rpc_admin_kefu_auto_reply_list', {
+ p_page: page,
+ p_page_size: pageSize,
+ p_search: search
+ } as UTSJSONObject)
+
+ if (res == null) return { total: 0, items: [] as Array }
+ return {
+ total: (res as any).total as number,
+ items: (res as any).items as Array
+ }
+}
+
+/**
+ * 保存自动回复配置
+ */
+export async function saveAutoReply(
+ id: string | null,
+ keyword: string,
+ content: string,
+ replyType: string,
+ status: number
+): Promise {
+ const res = await rpcOrValue('rpc_admin_kefu_auto_reply_save', {
+ p_id: id,
+ p_keyword: keyword,
+ p_content: content,
+ p_reply_type: replyType,
+ p_status: status
+ } as any)
+ return res != null ? String(res) : null
+}
+
+/**
+ * 删除自动回复
+ */
+export async function deleteAutoReply(id: string): Promise {
+ const ok = await rpcOrValue('rpc_admin_kefu_auto_reply_delete', { p_id: id } as any)
+ return ok === true
+}
+
+/**
+ * 设置自动回复状态
+ */
+export async function setAutoReplyStatus(id: string, status: number): Promise {
+ const ok = await rpcOrValue('rpc_admin_kefu_auto_reply_set_status', { p_id: id, p_status: status } as any)
+ return ok === true
+}
+
+/**
+ * 获取客服配置
+ */
+export async function getKefuSettings(): Promise {
+ const res = await rpcOrValue('rpc_admin_system_config_get', { p_key: 'kefu_settings' } as any)
+ return res as KefuConfig | null
+}
+
+/**
+ * 保存客服配置
+ */
+export async function saveKefuSettings(config: KefuConfig): Promise {
+ return await rpcOrValue('rpc_admin_system_config_save', { p_key: 'kefu_settings', p_value: config } as any) === true
+}