Merge branch 'yongpxu-dev' into yongpxu-dev2

# Conflicts:
#	nkebao/src/pages/home/index.tsx   resolved by yongpxu-dev2 version
This commit is contained in:
笔记本里的永平
2025-07-21 14:08:32 +08:00
5 changed files with 1256 additions and 189 deletions

View File

@@ -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;
}
}

View File

@@ -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<any>("");
const [currentTaskId, setCurrentTaskId] = useState<string>("");
const [showActionMenu, setShowActionMenu] = useState<string | null>(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: <EditOutlined />,
onClick: () => {
setShowActionMenu(null);
navigate(`/scenarios/edit/${task.id}`);
},
},
{
key: "settings",
text: "API设置",
icon: <SettingOutlined />,
onClick: () => {
setShowActionMenu(null);
handleOpenApiSettings(task.id);
},
},
{
key: "copy",
text: "复制",
text: "复制计划",
icon: <CopyOutlined />,
onClick: () => {
setShowActionMenu(null);
handleCopyPlan(task.id);
},
},
{
key: "settings",
text: "计划接口",
icon: <SettingOutlined />,
onClick: () => {
setShowActionMenu(null);
handleOpenApiSettings(task.id);
},
},
{
key: "qrcode",
text: "二维码",
@@ -330,7 +330,7 @@ const ScenarioList: React.FC = () => {
},
{
key: "delete",
text: "删除",
text: "删除计划",
icon: <DeleteOutlined />,
onClick: () => {
setShowActionMenu(null);
@@ -355,7 +355,14 @@ const ScenarioList: React.FC = () => {
<NavBar
back={null}
style={{ background: "#fff" }}
left={<div className={style["nav-title"]}>{scenarioName}</div>}
left={
<div className={style["nav-title"]}>
<span style={{ verticalAlign: "middle" }}>
<LeftOutline onClick={() => navigate(-1)} fontSize={24} />
</span>
{scenarioName}
</div>
}
right={
<Button
size="small"
@@ -504,81 +511,14 @@ const ScenarioList: React.FC = () => {
)}
</div>
{/* API设置弹窗 */}
<Popup
{/* 计划接口弹窗 */}
<PlanApi
visible={showApiDialog}
onMaskClick={() => setShowApiDialog(false)}
position="bottom"
bodyStyle={{ height: "70vh" }}
>
<div className={style["api-dialog"]}>
<div className={style["dialog-header"]}>
<h3>API设置</h3>
<Button size="small" onClick={() => setShowApiDialog(false)}>
</Button>
</div>
<div className={style["dialog-content"]}>
<div className={style["api-item"]}>
<label>API Key:</label>
<div className={style["input-with-button"]}>
<Input value={currentApiSettings.apiKey} disabled />
<Button
size="mini"
onClick={() => handleCopyApiUrl(currentApiSettings.apiKey)}
>
</Button>
</div>
<div className={style["api-tip"]}>
<strong></strong>
API密钥使
</div>
</div>
<div className={style["api-item"]}>
<label>Webhook URL:</label>
<div className={style["input-with-button"]}>
<Input value={currentApiSettings.webhookUrl} disabled />
<Button
size="mini"
onClick={() =>
handleCopyApiUrl(currentApiSettings.webhookUrl)
}
>
</Button>
</div>
<div className={style["api-params"]}>
<div className={style["param-section"]}>
<h4></h4>
<div className={style["param-list"]}>
<div>
<code>name</code> -
</div>
<div>
<code>phone</code> -
</div>
</div>
</div>
<div className={style["param-section"]}>
<h4></h4>
<div className={style["param-list"]}>
<div>
<code>source</code> -
</div>
<div>
<code>remark</code> -
</div>
<div>
<code>tags</code> -
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</Popup>
onClose={() => setShowApiDialog(false)}
apiKey={currentApiSettings.apiKey}
webhookUrl={currentApiSettings.webhookUrl}
taskId={currentApiSettings.taskId}
/>
{/* 操作菜单弹窗 */}
<Popup
@@ -632,11 +572,40 @@ const ScenarioList: React.FC = () => {
<div>...</div>
</div>
) : qrImg ? (
<img
src={qrImg}
alt="小程序二维码"
className={style["qr-image"]}
/>
<>
<img
src={qrImg}
alt="小程序二维码"
className={style["qr-image"]}
/>
{/* 链接复制区域 */}
<div className={style["qr-link-section"]}>
<div className={style["link-label"]}></div>
<div className={style["link-input-wrapper"]}>
<Input
value={`https://h5.ckb.quwanzhi.com/#/pages/form/input?id=${currentTaskId}`}
readOnly
className={style["link-input"]}
placeholder="小程序链接"
/>
<Button
size="small"
onClick={() => {
const link = `https://h5.ckb.quwanzhi.com/#/pages/form/input?id=${currentTaskId}`;
navigator.clipboard.writeText(link);
Toast.show({
content: "链接已复制到剪贴板",
position: "top",
});
}}
className={style["copy-button"]}
>
<CopyOutlined />
</Button>
</div>
</div>
</>
) : (
<div className={style["qr-error"]}></div>
)}

View File

@@ -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;
}
}
}
}

View File

@@ -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"
* });
*
* <PlanApi
* visible={showApiDialog}
* onClose={() => setShowApiDialog(false)}
* apiKey={apiSettings.apiKey}
* webhookUrl={apiSettings.webhookUrl}
* taskId={apiSettings.taskId}
* />
* ```
*
* 特性:
* - 移动端使用 PopupPC端使用 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<PlanApiProps> = ({
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 = () => (
<div className={style["config-content"]}>
{/* API密钥配置 */}
<div className={style["config-section"]}>
<div className={style["section-header"]}>
<div className={style["section-title"]}>
<CheckCircleOutlined className={style["section-icon"]} />
API密钥
</div>
<Tag color="green"></Tag>
</div>
<div className={style["input-group"]}>
<Input value={apiKey} disabled className={style["api-input"]} />
<Button
size="small"
onClick={() => handleCopy(apiKey, "API密钥")}
className={style["copy-btn"]}
>
<CopyOutlined />
</Button>
</div>
<div className={style["security-tip"]}>
<strong></strong>
API密钥使
</div>
</div>
{/* 接口地址配置 */}
<div className={style["config-section"]}>
<div className={style["section-header"]}>
<div className={style["section-title"]}>
<LinkOutlined className={style["section-icon"]} />
</div>
<Tag color="blue">POST请求</Tag>
</div>
<div className={style["input-group"]}>
<Input
value={fullWebhookUrl}
disabled
className={style["api-input"]}
/>
<Button
size="small"
onClick={() => handleCopy(fullWebhookUrl, "接口地址")}
className={style["copy-btn"]}
>
<CopyOutlined />
</Button>
</div>
{/* 参数说明 */}
<div className={style["params-grid"]}>
<div className={style["param-section"]}>
<h4></h4>
<div className={style["param-list"]}>
<div>
<code>name</code> -
</div>
<div>
<code>phone</code> -
</div>
</div>
</div>
<div className={style["param-section"]}>
<h4></h4>
<div className={style["param-list"]}>
<div>
<code>source</code> -
</div>
<div>
<code>remark</code> -
</div>
<div>
<code>tags</code> -
</div>
</div>
</div>
</div>
</div>
</div>
);
const renderQuickTestTab = () => (
<div className={style["test-content"]}>
<div className={style["test-section"]}>
<h3>URL</h3>
<div className={style["input-group"]}>
<Input value={testUrl} disabled className={style["test-input"]} />
</div>
<div className={style["test-buttons"]}>
<Button
onClick={() => handleCopy(testUrl, "测试URL")}
className={style["test-btn"]}
>
<CopyOutlined />
URL
</Button>
<Button
type="primary"
onClick={handleTestInBrowser}
className={style["test-btn"]}
>
</Button>
</div>
</div>
</div>
);
const renderDocsTab = () => (
<div className={style["docs-content"]}>
<div className={style["docs-grid"]}>
<Card className={style["doc-card"]}>
<div className={style["doc-icon"]}>
<BookOutlined />
</div>
<h4>API文档</h4>
<p></p>
</Card>
<Card className={style["doc-card"]}>
<div className={style["doc-icon"]}>
<LinkOutlined />
</div>
<h4></h4>
<p></p>
</Card>
</div>
</div>
);
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: `<?php
$url = '${fullWebhookUrl}';
$data = array(
'name' => '张三',
'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<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());`,
};
return (
<div className={style["code-content"]}>
<div className={style["language-tabs"]}>
{Object.keys(codeExamples).map((lang) => (
<button
key={lang}
className={`${style["lang-tab"]} ${
activeLanguage === lang ? style["active"] : ""
}`}
onClick={() => setActiveLanguage(lang)}
>
{lang.charAt(0).toUpperCase() + lang.slice(1)}
</button>
))}
</div>
<div className={style["code-block"]}>
<pre className={style["code"]}>
<code>
{codeExamples[activeLanguage as keyof typeof codeExamples]}
</code>
</pre>
<Button
size="small"
onClick={() =>
handleCopy(
codeExamples[activeLanguage as keyof typeof codeExamples],
"代码"
)
}
className={style["copy-code-btn"]}
>
<CopyOutlined />
</Button>
</div>
</div>
);
};
const renderContent = () => (
<div className={style["plan-api-dialog"]}>
{/* 头部 */}
<div className={style["dialog-header"]}>
<div className={style["header-left"]}>
<CodeOutlined className={style["header-icon"]} />
<div className={style["header-content"]}>
<h3></h3>
<p>
API接口直接导入客资到该获客计划
</p>
</div>
</div>
<Button size="small" onClick={onClose} className={style["close-btn"]}>
×
</Button>
</div>
{/* 导航标签 */}
<div className={style["nav-tabs"]}>
<button
className={`${style["nav-tab"]} ${activeTab === "config" ? style["active"] : ""}`}
onClick={() => setActiveTab("config")}
>
<SettingOutlined />
</button>
<button
className={`${style["nav-tab"]} ${activeTab === "test" ? style["active"] : ""}`}
onClick={() => setActiveTab("test")}
>
<ThunderboltOutlined />
</button>
<button
className={`${style["nav-tab"]} ${activeTab === "docs" ? style["active"] : ""}`}
onClick={() => setActiveTab("docs")}
>
<BookOutlined />
</button>
<button
className={`${style["nav-tab"]} ${activeTab === "code" ? style["active"] : ""}`}
onClick={() => setActiveTab("code")}
>
<CodeOutlined />
</button>
</div>
{/* 内容区域 */}
<div className={style["dialog-content"]}>
{activeTab === "config" && renderConfigTab()}
{activeTab === "test" && renderQuickTestTab()}
{activeTab === "docs" && renderDocsTab()}
{activeTab === "code" && renderCodeTab()}
</div>
{/* 底部 */}
<div className={style["dialog-footer"]}>
<div className={style["security-note"]}>
<SafetyOutlined />
HTTPS加密
</div>
<Button
type="primary"
onClick={onClose}
className={style["complete-btn"]}
>
</Button>
</div>
</div>
);
// 移动端使用Popup
if (isMobile) {
return (
<Popup
visible={visible}
onMaskClick={onClose}
position="bottom"
bodyStyle={{ height: "90vh" }}
>
{renderContent()}
</Popup>
);
}
// PC端使用Modal
return (
<Modal
open={visible}
onCancel={onClose}
footer={null}
width={800}
centered
className={style["plan-api-modal"]}
>
{renderContent()}
</Modal>
);
};
export default PlanApi;

View File

@@ -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}`;
};