From 3447646fc5b99047062746b77f3fecb8e213d666 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AC=94=E8=AE=B0=E6=9C=AC=E9=87=8C=E7=9A=84=E6=B0=B8?= =?UTF-8?q?=E5=B9=B3?= Date: Mon, 21 Jul 2025 12:22:22 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=9C=AC=E6=AC=A1=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E5=A6=82=E4=B8=8B?= =?UTF-8?q?=20=E8=AE=A1=E5=88=92=E5=88=97=E8=A1=A8=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/src/pages/home/index.tsx | 34 +- .../scenarios/plan/list/index.module.scss | 88 +-- .../src/pages/scenarios/plan/list/index.tsx | 126 +--- .../scenarios/plan/list/planApi.module.scss | 601 ++++++++++++++++++ .../src/pages/scenarios/plan/list/planApi.tsx | 423 ++++++++++++ 5 files changed, 1075 insertions(+), 197 deletions(-) create mode 100644 nkebao/src/pages/scenarios/plan/list/planApi.module.scss create mode 100644 nkebao/src/pages/scenarios/plan/list/planApi.tsx diff --git a/nkebao/src/pages/home/index.tsx b/nkebao/src/pages/home/index.tsx index 3ac3b164..09ef4b78 100644 --- a/nkebao/src/pages/home/index.tsx +++ b/nkebao/src/pages/home/index.tsx @@ -1,18 +1,20 @@ import React, { useEffect, useState } from "react"; import { NavBar } from "antd-mobile"; import { - AppOutline, - UserOutline, ClockCircleOutline, SendOutline, StarOutline, - Bell, - Smartphone, - Users, - Activity, - MessageSquare, - TrendingUp, } from "antd-mobile-icons"; + +import { + BellOutlined, + MobileOutlined, + TeamOutlined, + LineChartOutlined, + MessageOutlined, + RiseOutlined, +} from "@ant-design/icons"; + import MeauMobile from "@/components/MeauMobile/MeauMoible"; import Layout from "@/components/Layout/Layout"; import style from "./index.module.scss"; @@ -73,28 +75,28 @@ const Home: React.FC = () => { { title: "朋友圈同步", value: "12", - icon: , + icon: , color: "text-purple-600", path: "/workspace/moments-sync", }, { title: "群发任务", value: "8", - icon: , + icon: , color: "text-orange-600", path: "/workspace/group-push", }, { title: "获客转化", value: "85%", - icon: , + icon: , color: "text-green-600", path: "/scenarios", }, { title: "系统活跃度", value: "98%", - icon: , + icon: , color: "text-blue-600", path: "/workspace", }, @@ -240,7 +242,7 @@ const Home: React.FC = () => { )} @@ -260,7 +262,7 @@ const Home: React.FC = () => { {dashboard.deviceNum || 42} - +
@@ -274,7 +276,7 @@ const Home: React.FC = () => { {dashboard.wechatNum || 42} - +
@@ -287,7 +289,7 @@ const Home: React.FC = () => { {dashboard.aliveWechatNum || 35} - +
{ const { scenarioId, scenarioName } = useParams<{ @@ -217,20 +219,12 @@ const ScenarioList: React.FC = () => { } } catch (error) { Toast.show({ - content: "获取API设置失败", + content: "获取计划接口失败", position: "top", }); } }; - const handleCopyApiUrl = (url: string) => { - navigator.clipboard.writeText(url); - Toast.show({ - content: "API地址已复制到剪贴板", - position: "top", - }); - }; - const handleCreateNewPlan = () => { navigate(`/scenarios/new/${scenarioId}`); }; @@ -294,31 +288,31 @@ const ScenarioList: React.FC = () => { const getActionMenu = (task: Task) => [ { key: "edit", - text: "编辑", + text: "编辑计划", icon: , onClick: () => { setShowActionMenu(null); navigate(`/scenarios/edit/${task.id}`); }, }, - { - key: "settings", - text: "API设置", - icon: , - onClick: () => { - setShowActionMenu(null); - handleOpenApiSettings(task.id); - }, - }, { key: "copy", - text: "复制", + text: "复制计划", icon: , onClick: () => { setShowActionMenu(null); handleCopyPlan(task.id); }, }, + { + key: "settings", + text: "计划接口", + icon: , + onClick: () => { + setShowActionMenu(null); + handleOpenApiSettings(task.id); + }, + }, { key: "qrcode", text: "二维码", @@ -330,7 +324,7 @@ const ScenarioList: React.FC = () => { }, { key: "delete", - text: "删除", + text: "删除计划", icon: , onClick: () => { setShowActionMenu(null); @@ -355,7 +349,14 @@ const ScenarioList: React.FC = () => { {scenarioName}
} + left={ +
+ + navigate(-1)} fontSize={24} /> + + {scenarioName} +
+ } right={
- {/* API设置弹窗 */} - setShowApiDialog(false)} - position="bottom" - bodyStyle={{ height: "70vh" }} - > -
-
-

API设置

- -
-
-
- -
- - -
-
- 安全提示: - 请妥善保管API密钥,不要在客户端代码中暴露。建议在服务器端使用该密钥。 -
-
-
- -
- - -
-
-
-

必要参数

-
-
- name - 客户姓名 -
-
- phone - 手机号码 -
-
-
-
-

可选参数

-
-
- source - 来源标识 -
-
- remark - 备注信息 -
-
- tags - 客户标签 -
-
-
-
-
-
-
-
+ onClose={() => setShowApiDialog(false)} + apiKey={currentApiSettings.apiKey} + webhookUrl={currentApiSettings.webhookUrl} + taskId={currentApiSettings.taskId} + /> {/* 操作菜单弹窗 */} setShowApiDialog(false)} + * apiKey={apiSettings.apiKey} + * webhookUrl={apiSettings.webhookUrl} + * taskId={apiSettings.taskId} + * /> + * ``` + * + * 特性: + * - 移动端使用 Popup,PC端使用 Modal + * - 支持四个标签页:接口配置、快速测试、开发文档、代码示例 + * - 支持多种编程语言的代码示例 + * - 响应式设计,自适应不同屏幕尺寸 + * - 支持暗色主题 + */ + +interface PlanApiProps { + visible: boolean; + onClose: () => void; + apiKey: string; + webhookUrl: string; + taskId: string; +} + +interface ApiSettings { + apiKey: string; + webhookUrl: string; + taskId: string; +} + +const PlanApi: React.FC = ({ + visible, + onClose, + apiKey, + webhookUrl, + taskId, +}) => { + const [activeTab, setActiveTab] = useState("config"); + const [activeLanguage, setActiveLanguage] = useState("javascript"); + const [testUrl, setTestUrl] = useState( + `${webhookUrl}?name=测试客户&phone=13800138000&source=API测试` + ); + + // 检测是否为移动端 + const isMobile = window.innerWidth <= 768; + + const handleCopy = (text: string, type: string) => { + navigator.clipboard.writeText(text); + Toast.show({ + content: `${type}已复制到剪贴板`, + position: "top", + }); + }; + + const handleTestInBrowser = () => { + window.open(testUrl, "_blank"); + }; + + const renderConfigTab = () => ( +
+ {/* API密钥配置 */} +
+
+
+ + API密钥 +
+ 安全认证 +
+
+ + +
+
+ 安全提示: + 请妥善保管API密钥,不要在客户端代码中暴露。建议在服务器端使用该密钥。 +
+
+ + {/* 接口地址配置 */} +
+
+
+ + 接口地址 +
+ POST请求 +
+
+ + +
+ + {/* 参数说明 */} +
+
+

必要参数

+
+
+ name - 客户姓名 +
+
+ phone - 手机号码 +
+
+
+
+

可选参数

+
+
+ source - 来源标识 +
+
+ remark - 备注信息 +
+
+ tags - 客户标签 +
+
+
+
+
+
+ ); + + const renderQuickTestTab = () => ( +
+
+

快速测试URL

+
+ +
+
+ + +
+
+
+ ); + + const renderDocsTab = () => ( +
+
+ +
+ +
+

完整API文档

+

详细的接口说明和参数文档

+
+ +
+ +
+

集成指南

+

第三方平台集成教程

+
+
+
+ ); + + const renderCodeTab = () => { + const codeExamples = { + javascript: `fetch('${webhookUrl}', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ${apiKey}' + }, + body: JSON.stringify({ + name: '张三', + phone: '13800138000', + source: '官网表单', + }) +})`, + python: `import requests + +url = '${webhookUrl}' +headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ${apiKey}' +} +data = { + 'name': '张三', + 'phone': '13800138000', + 'source': '官网表单' +} + +response = requests.post(url, json=data, headers=headers)`, + php: ` '张三', + 'phone' => '13800138000', + 'source' => '官网表单' +); + +$options = array( + 'http' => array( + 'header' => "Content-type: application/json\\r\\nAuthorization: Bearer ${apiKey}\\r\\n", + 'method' => 'POST', + 'content' => json_encode($data) + ) +); + +$context = stream_context_create($options); +$result = file_get_contents($url, false, $context);`, + java: `import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.URI; + +HttpClient client = HttpClient.newHttpClient(); +String json = "{\\"name\\":\\"张三\\",\\"phone\\":\\"13800138000\\",\\"source\\":\\"官网表单\\"}"; + +HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("${webhookUrl}")) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer ${apiKey}") + .POST(HttpRequest.BodyPublishers.ofString(json)) + .build(); + +HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());`, + }; + + return ( +
+
+ {Object.keys(codeExamples).map((lang) => ( + + ))} +
+
+
+            
+              {codeExamples[activeLanguage as keyof typeof codeExamples]}
+            
+          
+ +
+
+ ); + }; + + const renderContent = () => ( +
+ {/* 头部 */} +
+
+ +
+

计划接口配置

+

+ 通过API接口直接导入客资到该获客计划,支持多种编程语言和第三方平台集成 +

+
+
+ +
+ + {/* 导航标签 */} +
+ + + + +
+ + {/* 内容区域 */} +
+ {activeTab === "config" && renderConfigTab()} + {activeTab === "test" && renderQuickTestTab()} + {activeTab === "docs" && renderDocsTab()} + {activeTab === "code" && renderCodeTab()} +
+ + {/* 底部 */} +
+
+ + 所有数据传输均采用HTTPS加密 +
+ +
+
+ ); + + // 移动端使用Popup + if (isMobile) { + return ( + + {renderContent()} + + ); + } + + // PC端使用Modal + return ( + + {renderContent()} + + ); +}; + +export default PlanApi; From 27c401c518f2e71be0e6f4910a04380cb1498275 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AC=94=E8=AE=B0=E6=9C=AC=E9=87=8C=E7=9A=84=E6=B0=B8?= =?UTF-8?q?=E5=B9=B3?= Date: Mon, 21 Jul 2025 14:07:06 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E6=9C=AC=E6=AC=A1=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E6=9B=B4=E6=96=B0=E5=86=85=E5=AE=B9=E5=A6=82=E4=B8=8B?= =?UTF-8?q?=20=E5=AD=98=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scenarios/plan/list/index.module.scss | 69 ++++++++++++++++++ .../src/pages/scenarios/plan/list/index.tsx | 51 +++++++++++-- .../src/pages/scenarios/plan/list/planApi.tsx | 34 ++++++--- nkebao/src/utils/apiUrl.ts | 73 +++++++++++++++++++ 4 files changed, 209 insertions(+), 18 deletions(-) create mode 100644 nkebao/src/utils/apiUrl.ts diff --git a/nkebao/src/pages/scenarios/plan/list/index.module.scss b/nkebao/src/pages/scenarios/plan/list/index.module.scss index b9c3b93e..f8b8ddb0 100644 --- a/nkebao/src/pages/scenarios/plan/list/index.module.scss +++ b/nkebao/src/pages/scenarios/plan/list/index.module.scss @@ -304,6 +304,7 @@ background: white; border-radius: 16px; padding: 20px; + width: 100%; } .qr-loading { @@ -329,4 +330,72 @@ color: #ff4d4f; font-size: 14px; padding: 40px 20px; +} + +.qr-link-section { + margin-top: 20px; + width: 100%; + padding: 0 10px; +} + +.link-label { + font-size: 14px; + font-weight: 500; + color: #333; + margin-bottom: 8px; + text-align: left; +} + +.link-input-wrapper { + display: flex; + gap: 8px; + align-items: center; + width: 100%; + + @media (max-width: 480px) { + flex-direction: column; + gap: 12px; + } +} + +.link-input { + flex: 1; + + .ant-input { + border-radius: 8px; + font-size: 12px; + color: #666; + background-color: #f8f9fa; + border: 1px solid #e9ecef; + + &:focus { + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + } + } + + @media (max-width: 480px) { + width: 100%; + } +} + +.copy-button { + height: 32px; + padding: 0 12px; + border-radius: 8px; + font-size: 12px; + display: flex; + align-items: center; + gap: 4px; + white-space: nowrap; + flex-shrink: 0; + + .anticon { + font-size: 12px; + } + + @media (max-width: 480px) { + width: 100%; + justify-content: center; + } } \ No newline at end of file diff --git a/nkebao/src/pages/scenarios/plan/list/index.tsx b/nkebao/src/pages/scenarios/plan/list/index.tsx index 7f13eefa..ebeb8f46 100644 --- a/nkebao/src/pages/scenarios/plan/list/index.tsx +++ b/nkebao/src/pages/scenarios/plan/list/index.tsx @@ -37,6 +37,7 @@ import { import style from "./index.module.scss"; import { Task, ApiSettings, PlanDetail } from "./data"; import PlanApi from "./planApi"; +import { buildApiUrl } from "@/utils/apiUrl"; const ScenarioList: React.FC = () => { const { scenarioId, scenarioName } = useParams<{ @@ -57,6 +58,7 @@ const ScenarioList: React.FC = () => { const [showQrDialog, setShowQrDialog] = useState(false); const [qrLoading, setQrLoading] = useState(false); const [qrImg, setQrImg] = useState(""); + const [currentTaskId, setCurrentTaskId] = useState(""); const [showActionMenu, setShowActionMenu] = useState(null); // 分页相关状态 @@ -208,11 +210,14 @@ const ScenarioList: React.FC = () => { try { const response: PlanDetail = await getPlanDetail(taskId); if (response) { + // 处理webhook URL,使用工具函数构建完整地址 + const webhookUrl = buildApiUrl( + response.textUrl?.fullUrl || `webhook/${taskId}` + ); + setCurrentApiSettings({ apiKey: response.apiKey || "demo-api-key-123456", - webhookUrl: - response.textUrl?.fullUrl || - `https://api.example.com/webhook/${taskId}`, + webhookUrl: webhookUrl, taskId: taskId, }); setShowApiDialog(true); @@ -233,6 +238,7 @@ const ScenarioList: React.FC = () => { setQrLoading(true); setShowQrDialog(true); setQrImg(""); + setCurrentTaskId(taskId); // 设置当前任务ID try { const response = await getWxMinAppCode(taskId); @@ -566,11 +572,40 @@ const ScenarioList: React.FC = () => {
生成二维码中...
) : qrImg ? ( - 小程序二维码 + <> + 小程序二维码 + {/* 链接复制区域 */} +
+
小程序链接
+
+ + +
+
+ ) : (
二维码生成失败
)} diff --git a/nkebao/src/pages/scenarios/plan/list/planApi.tsx b/nkebao/src/pages/scenarios/plan/list/planApi.tsx index 954037fd..7fc94b1a 100644 --- a/nkebao/src/pages/scenarios/plan/list/planApi.tsx +++ b/nkebao/src/pages/scenarios/plan/list/planApi.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useMemo } from "react"; import { Popup, Button, Toast, SpinLoading } from "antd-mobile"; import { Modal, Input, Tabs, Card, Tag, Space } from "antd"; import { @@ -12,6 +12,7 @@ import { CheckCircleOutlined, } from "@ant-design/icons"; import style from "./planApi.module.scss"; +import { buildApiUrl } from "@/utils/apiUrl"; /** * 计划接口配置弹窗组件 @@ -40,6 +41,7 @@ import style from "./planApi.module.scss"; * - 支持多种编程语言的代码示例 * - 响应式设计,自适应不同屏幕尺寸 * - 支持暗色主题 + * - 自动拼接API地址前缀 */ interface PlanApiProps { @@ -65,9 +67,17 @@ const PlanApi: React.FC = ({ }) => { const [activeTab, setActiveTab] = useState("config"); const [activeLanguage, setActiveLanguage] = useState("javascript"); - const [testUrl, setTestUrl] = useState( - `${webhookUrl}?name=测试客户&phone=13800138000&source=API测试` - ); + + // 处理webhook URL,确保包含完整的API地址 + const fullWebhookUrl = useMemo(() => { + return buildApiUrl(webhookUrl); + }, [webhookUrl]); + + // 生成测试URL + const testUrl = useMemo(() => { + if (!fullWebhookUrl) return ""; + return `${fullWebhookUrl}?name=测试客户&phone=13800138000&source=API测试`; + }, [fullWebhookUrl]); // 检测是否为移动端 const isMobile = window.innerWidth <= 768; @@ -122,10 +132,14 @@ const PlanApi: React.FC = ({ POST请求
- +