diff --git a/nkebao/src/pages/scenarios/plan/list/index.module.scss b/nkebao/src/pages/scenarios/plan/list/index.module.scss index 02a35d4d..f8b8ddb0 100644 --- a/nkebao/src/pages/scenarios/plan/list/index.module.scss +++ b/nkebao/src/pages/scenarios/plan/list/index.module.scss @@ -275,15 +275,6 @@ font-weight: 500; } -.api-dialog { - background: white; - border-radius: 16px 16px 0 0; - padding: 20px; - height: 100%; - display: flex; - flex-direction: column; -} - .dialog-header { display: flex; justify-content: space-between; @@ -302,90 +293,18 @@ .dialog-content { flex: 1; - overflow-y: auto; -} - -.api-item { - margin-bottom: 20px; - - label { - display: block; - font-size: 14px; - color: #333; - margin-bottom: 8px; - font-weight: 500; - } - - .ant-input { - border-radius: 8px; - } -} - -.input-with-button { + text-align: center; display: flex; - gap: 8px; + flex-direction: column; align-items: center; - - .ant-input { - flex: 1; - } -} - -// API设置相关样式 -.api-tip { - margin-top: 12px; - padding: 12px; - background-color: #fff7e6; - border: 1px solid #ffd591; - border-radius: 8px; - font-size: 12px; - color: #d46b08; - line-height: 1.5; -} - -.api-params { - margin-top: 16px; - display: grid; - grid-template-columns: 1fr 1fr; - gap: 12px; -} - -.param-section { - background-color: #f8f9fa; - border-radius: 8px; - padding: 12px; - border: 1px solid #e9ecef; - - h4 { - margin: 0 0 8px 0; - font-size: 14px; - font-weight: 600; - color: #333; - } -} - -.param-list { - font-size: 12px; - color: #666; - line-height: 1.6; - - div { - margin-bottom: 4px; - } - - code { - background-color: #e9ecef; - padding: 2px 4px; - border-radius: 4px; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 11px; - } + justify-content: center; } .qr-dialog { background: white; border-radius: 16px; padding: 20px; + width: 100%; } .qr-loading { @@ -411,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 37e02fb6..ebeb8f46 100644 --- a/nkebao/src/pages/scenarios/plan/list/index.tsx +++ b/nkebao/src/pages/scenarios/plan/list/index.tsx @@ -24,6 +24,7 @@ import { ClockCircleOutlined, DownOutlined, } from "@ant-design/icons"; +import { LeftOutline } from "antd-mobile-icons"; import Layout from "@/components/Layout/Layout"; import { @@ -35,6 +36,8 @@ import { } from "./api"; 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<{ @@ -55,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); // 分页相关状态 @@ -206,31 +210,26 @@ 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); } } 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}`); }; @@ -239,6 +238,7 @@ const ScenarioList: React.FC = () => { setQrLoading(true); setShowQrDialog(true); setQrImg(""); + setCurrentTaskId(taskId); // 设置当前任务ID try { const response = await getWxMinAppCode(taskId); @@ -294,31 +294,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 +330,7 @@ const ScenarioList: React.FC = () => { }, { key: "delete", - text: "删除", + text: "删除计划", icon: , onClick: () => { setShowActionMenu(null); @@ -355,7 +355,14 @@ const ScenarioList: React.FC = () => { {scenarioName}} + left={ +
+ + navigate(-1)} fontSize={24} /> + + {scenarioName} +
+ } right={ - -
-
- -
- - -
-
- 安全提示: - 请妥善保管API密钥,不要在客户端代码中暴露。建议在服务器端使用该密钥。 -
-
-
- -
- - -
-
-
-

必要参数

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

可选参数

-
-
- source - 来源标识 -
-
- remark - 备注信息 -
-
- tags - 客户标签 -
-
-
-
-
-
- - + onClose={() => setShowApiDialog(false)} + apiKey={currentApiSettings.apiKey} + webhookUrl={currentApiSettings.webhookUrl} + taskId={currentApiSettings.taskId} + /> {/* 操作菜单弹窗 */} {
生成二维码中...
) : qrImg ? ( - 小程序二维码 + <> + 小程序二维码 + {/* 链接复制区域 */} +
+
小程序链接
+
+ + +
+
+ ) : (
二维码生成失败
)} diff --git a/nkebao/src/pages/scenarios/plan/list/planApi.module.scss b/nkebao/src/pages/scenarios/plan/list/planApi.module.scss new file mode 100644 index 00000000..da57e0d6 --- /dev/null +++ b/nkebao/src/pages/scenarios/plan/list/planApi.module.scss @@ -0,0 +1,601 @@ +// 移动端样式 +.plan-api-dialog { + background: white; + border-radius: 16px 16px 0 0; + height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.dialog-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 20px; + border-bottom: 1px solid #f0f0f0; + background: #fafafa; + + .header-left { + display: flex; + align-items: flex-start; + gap: 12px; + flex: 1; + } + + .header-icon { + font-size: 24px; + color: #1890ff; + margin-top: 4px; + } + + .header-content { + flex: 1; + + h3 { + margin: 0 0 8px 0; + font-size: 18px; + font-weight: 600; + color: #333; + } + + p { + margin: 0; + font-size: 14px; + color: #666; + line-height: 1.5; + } + } + + .close-btn { + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + color: #999; + background: transparent; + border: none; + cursor: pointer; + + &:hover { + background: #f5f5f5; + } + } +} + +.nav-tabs { + display: flex; + background: white; + border-bottom: 1px solid #f0f0f0; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + + .nav-tab { + flex: 1; + min-width: 80px; + padding: 12px 8px; + border: none; + background: transparent; + color: #666; + font-size: 14px; + cursor: pointer; + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + transition: all 0.2s ease; + white-space: nowrap; + + svg { + font-size: 16px; + } + + &:hover { + color: #1890ff; + } + + &.active { + color: #1890ff; + border-bottom: 2px solid #1890ff; + } + } +} + +.dialog-content { + flex: 1; + overflow-y: auto; + padding: 20px; +} + +.dialog-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + border-top: 1px solid #f0f0f0; + background: #fafafa; + + .security-note { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: #666; + + svg { + color: #52c41a; + } + } + + .complete-btn { + height: 36px; + padding: 0 24px; + border-radius: 18px; + } +} + +// 配置内容样式 +.config-content { + .config-section { + margin-bottom: 24px; + + &:last-child { + margin-bottom: 0; + } + } + + .section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + + .section-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 16px; + font-weight: 600; + color: #333; + + .section-icon { + color: #1890ff; + } + } + } + + .input-group { + display: flex; + gap: 8px; + margin-bottom: 12px; + + .api-input { + flex: 1; + border-radius: 8px; + } + + .copy-btn { + height: 40px; + padding: 0 16px; + border-radius: 8px; + display: flex; + align-items: center; + gap: 4px; + } + } + + .security-tip { + padding: 12px; + background: #fff7e6; + border: 1px solid #ffd591; + border-radius: 8px; + font-size: 12px; + color: #d46b08; + line-height: 1.5; + } + + .params-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + margin-top: 16px; + } + + .param-section { + background: #f8f9fa; + border-radius: 8px; + padding: 12px; + border: 1px solid #e9ecef; + + h4 { + margin: 0 0 8px 0; + font-size: 14px; + font-weight: 600; + color: #333; + } + + .param-list { + font-size: 12px; + color: #666; + line-height: 1.6; + + div { + margin-bottom: 4px; + } + + code { + background: #e9ecef; + padding: 2px 4px; + border-radius: 4px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 11px; + } + } + } +} + +// 测试内容样式 +.test-content { + .test-section { + h3 { + margin: 0 0 16px 0; + font-size: 16px; + font-weight: 600; + color: #333; + } + + .test-input { + margin-bottom: 16px; + border-radius: 8px; + } + + .test-buttons { + display: flex; + gap: 12px; + + .test-btn { + flex: 1; + height: 40px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + } + } + } +} + +// 文档内容样式 +.docs-content { + .docs-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + } + + .doc-card { + text-align: center; + padding: 24px 16px; + border-radius: 12px; + border: 1px solid #f0f0f0; + transition: all 0.2s ease; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + } + + .doc-icon { + width: 48px; + height: 48px; + border-radius: 50%; + background: #f0f8ff; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 16px; + font-size: 24px; + color: #1890ff; + } + + h4 { + margin: 0 0 8px 0; + font-size: 16px; + font-weight: 600; + color: #333; + } + + p { + margin: 0; + font-size: 14px; + color: #666; + line-height: 1.5; + } + } +} + +// 代码内容样式 +.code-content { + .language-tabs { + display: flex; + gap: 8px; + margin-bottom: 16px; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + + .lang-tab { + padding: 8px 16px; + border: 1px solid #d9d9d9; + background: white; + border-radius: 6px; + font-size: 14px; + color: #666; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; + + &:hover { + border-color: #1890ff; + color: #1890ff; + } + + &.active { + background: #1890ff; + border-color: #1890ff; + color: white; + } + } + } + + .code-block { + position: relative; + background: #f6f8fa; + border-radius: 8px; + border: 1px solid #e1e4e8; + overflow: hidden; + + .code { + margin: 0; + padding: 16px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 13px; + line-height: 1.5; + color: #24292e; + overflow-x: auto; + white-space: pre-wrap; + word-break: break-all; + } + + .copy-code-btn { + position: absolute; + top: 8px; + right: 8px; + height: 32px; + padding: 0 12px; + border-radius: 6px; + display: flex; + align-items: center; + gap: 4px; + font-size: 12px; + } + } +} + +// PC端样式覆盖 +.plan-api-modal { + .plan-api-dialog { + border-radius: 12px; + height: auto; + max-height: 80vh; + } + + .nav-tabs { + .nav-tab { + min-width: 100px; + padding: 16px 12px; + font-size: 15px; + + svg { + font-size: 18px; + } + } + } + + .dialog-content { + padding: 24px; + } + + .params-grid { + grid-template-columns: 1fr 1fr; + } + + .docs-grid { + grid-template-columns: 1fr 1fr; + } + + .test-buttons { + flex-direction: row; + } +} + +// 响应式设计 +@media (max-width: 768px) { + .plan-api-dialog { + .header-content { + h3 { + font-size: 16px; + } + + p { + font-size: 13px; + } + } + } + + .nav-tabs { + .nav-tab { + font-size: 13px; + padding: 10px 6px; + + svg { + font-size: 14px; + } + } + } + + .dialog-content { + padding: 16px; + } + + .config-content { + .params-grid { + grid-template-columns: 1fr; + gap: 8px; + } + } + + .docs-content { + .docs-grid { + grid-template-columns: 1fr; + gap: 12px; + } + } + + .test-content { + .test-buttons { + flex-direction: column; + gap: 8px; + } + } + + .code-content { + .language-tabs { + .lang-tab { + padding: 6px 12px; + font-size: 13px; + } + } + + .code-block { + .code { + font-size: 12px; + padding: 12px; + } + } + } +} + +// 暗色主题支持 +@media (prefers-color-scheme: dark) { + .plan-api-dialog { + background: #1f1f1f; + color: #fff; + + .dialog-header { + background: #262626; + border-bottom-color: #434343; + + .header-content { + h3 { + color: #fff; + } + + p { + color: #a6a6a6; + } + } + } + + .nav-tabs { + background: #262626; + border-bottom-color: #434343; + + .nav-tab { + color: #a6a6a6; + + &:hover { + color: #1890ff; + } + + &.active { + color: #1890ff; + border-bottom-color: #1890ff; + } + } + } + + .dialog-footer { + background: #262626; + border-top-color: #434343; + + .security-note { + color: #a6a6a6; + } + } + } + + .config-content { + .section-title { + color: #fff; + } + + .security-tip { + background: #2a1f00; + border-color: #d48806; + color: #ffc53d; + } + + .param-section { + background: #262626; + border-color: #434343; + + h4 { + color: #fff; + } + + .param-list { + color: #a6a6a6; + + code { + background: #434343; + } + } + } + } + + .test-content { + h3 { + color: #fff; + } + } + + .docs-content { + .doc-card { + background: #262626; + border-color: #434343; + + h4 { + color: #fff; + } + + p { + color: #a6a6a6; + } + } + } + + .code-content { + .code-block { + background: #0d1117; + border-color: #30363d; + + .code { + color: #c9d1d9; + } + } + } +} \ No newline at end of file diff --git a/nkebao/src/pages/scenarios/plan/list/planApi.tsx b/nkebao/src/pages/scenarios/plan/list/planApi.tsx new file mode 100644 index 00000000..7fc94b1a --- /dev/null +++ b/nkebao/src/pages/scenarios/plan/list/planApi.tsx @@ -0,0 +1,437 @@ +import React, { useState, useMemo } from "react"; +import { Popup, Button, Toast, SpinLoading } from "antd-mobile"; +import { Modal, Input, Tabs, Card, Tag, Space } from "antd"; +import { + CopyOutlined, + CodeOutlined, + BookOutlined, + ThunderboltOutlined, + SettingOutlined, + LinkOutlined, + SafetyOutlined, + CheckCircleOutlined, +} from "@ant-design/icons"; +import style from "./planApi.module.scss"; +import { buildApiUrl } from "@/utils/apiUrl"; + +/** + * 计划接口配置弹窗组件 + * + * 使用示例: + * ```tsx + * const [showApiDialog, setShowApiDialog] = useState(false); + * const [apiSettings, setApiSettings] = useState({ + * apiKey: "your-api-key", + * webhookUrl: "https://api.example.com/webhook", + * taskId: "task-123" + * }); + * + * setShowApiDialog(false)} + * apiKey={apiSettings.apiKey} + * webhookUrl={apiSettings.webhookUrl} + * taskId={apiSettings.taskId} + * /> + * ``` + * + * 特性: + * - 移动端使用 Popup,PC端使用 Modal + * - 支持四个标签页:接口配置、快速测试、开发文档、代码示例 + * - 支持多种编程语言的代码示例 + * - 响应式设计,自适应不同屏幕尺寸 + * - 支持暗色主题 + * - 自动拼接API地址前缀 + */ + +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"); + + // 处理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; + + 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('${fullWebhookUrl}', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ${apiKey}' + }, + body: JSON.stringify({ + name: '张三', + phone: '13800138000', + source: '官网表单', + }) +})`, + python: `import requests + +url = '${fullWebhookUrl}' +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("${fullWebhookUrl}")) + .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; diff --git a/nkebao/src/utils/apiUrl.ts b/nkebao/src/utils/apiUrl.ts new file mode 100644 index 00000000..32d6fc6c --- /dev/null +++ b/nkebao/src/utils/apiUrl.ts @@ -0,0 +1,73 @@ +/** + * API URL工具函数 + * 用于统一处理API地址的拼接逻辑 + * + * URL结构: {VITE_API_BASE_URL}/v1/api/scenarios/{path} + * + * 示例: + * - 开发环境: http://localhost:3000/api/v1/api/scenarios/webhook/123 + * - 生产环境: https://api.example.com/v1/api/scenarios/webhook/123 + */ + +/** + * 获取完整的API基础路径 + * @returns 完整的API基础路径,包含 /v1/api/scenarios + * + * 示例: + * - 开发环境: http://localhost:3000/api/v1/api/scenarios + * - 生产环境: https://api.example.com/v1/api/scenarios + */ +export const getFullApiPath = (): string => { + const apiBaseUrl = (import.meta as any).env?.VITE_API_BASE_URL || "/api"; + return `${apiBaseUrl}/v1/api/scenarios`; +}; + +/** + * 构建完整的API URL + * @param path 相对路径或完整URL + * @returns 完整的API URL + * + * 示例: + * - buildApiUrl('/webhook/123') → 'http://localhost:3000/api/v1/api/scenarios/webhook/123' + * - buildApiUrl('webhook/123') → 'http://localhost:3000/api/v1/api/scenarios/webhook/123' + * - buildApiUrl('https://api.example.com/webhook/123') → 'https://api.example.com/webhook/123' + */ +export const buildApiUrl = (path: string): string => { + if (!path) return ""; + + // 如果已经是完整的URL(包含http或https),直接返回 + if (path.startsWith("http://") || path.startsWith("https://")) { + return path; + } + + const fullApiPath = getFullApiPath(); + + // 如果是相对路径,拼接完整API路径 + if (path.startsWith("/")) { + return `${fullApiPath}${path}`; + } + + // 其他情况,拼接完整API路径和路径 + return `${fullApiPath}?${path}`; +}; + +/** + * 构建webhook URL + * @param taskId 任务ID + * @param path 可选的相对路径 + * @returns 完整的webhook URL + * + * 示例: + * - buildWebhookUrl('123') → 'http://localhost:3000/api/v1/api/scenarios/webhook/123' + * - buildWebhookUrl('123', '/custom/path') → 'http://localhost:3000/api/v1/api/scenarios/custom/path' + */ +export const buildWebhookUrl = (taskId: string, path?: string): string => { + const fullApiPath = getFullApiPath(); + const webhookPath = path || `/webhook/${taskId}`; + + if (webhookPath.startsWith("/")) { + return `${fullApiPath}${webhookPath}`; + } + + return `${fullApiPath}/${webhookPath}`; +};