Merge branch 'yongpxu-dev' into yongpxu-dev2
# Conflicts: # nkebao/src/pages/home/index.tsx resolved by yongpxu-dev2 version
This commit is contained in:
@@ -275,15 +275,6 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.api-dialog {
|
|
||||||
background: white;
|
|
||||||
border-radius: 16px 16px 0 0;
|
|
||||||
padding: 20px;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-header {
|
.dialog-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -302,90 +293,18 @@
|
|||||||
|
|
||||||
.dialog-content {
|
.dialog-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
text-align: center;
|
||||||
}
|
|
||||||
|
|
||||||
.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 {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.qr-dialog {
|
.qr-dialog {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qr-loading {
|
.qr-loading {
|
||||||
@@ -411,4 +330,72 @@
|
|||||||
color: #ff4d4f;
|
color: #ff4d4f;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding: 40px 20px;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
ClockCircleOutlined,
|
ClockCircleOutlined,
|
||||||
DownOutlined,
|
DownOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
|
import { LeftOutline } from "antd-mobile-icons";
|
||||||
|
|
||||||
import Layout from "@/components/Layout/Layout";
|
import Layout from "@/components/Layout/Layout";
|
||||||
import {
|
import {
|
||||||
@@ -35,6 +36,8 @@ import {
|
|||||||
} from "./api";
|
} from "./api";
|
||||||
import style from "./index.module.scss";
|
import style from "./index.module.scss";
|
||||||
import { Task, ApiSettings, PlanDetail } from "./data";
|
import { Task, ApiSettings, PlanDetail } from "./data";
|
||||||
|
import PlanApi from "./planApi";
|
||||||
|
import { buildApiUrl } from "@/utils/apiUrl";
|
||||||
|
|
||||||
const ScenarioList: React.FC = () => {
|
const ScenarioList: React.FC = () => {
|
||||||
const { scenarioId, scenarioName } = useParams<{
|
const { scenarioId, scenarioName } = useParams<{
|
||||||
@@ -55,6 +58,7 @@ const ScenarioList: React.FC = () => {
|
|||||||
const [showQrDialog, setShowQrDialog] = useState(false);
|
const [showQrDialog, setShowQrDialog] = useState(false);
|
||||||
const [qrLoading, setQrLoading] = useState(false);
|
const [qrLoading, setQrLoading] = useState(false);
|
||||||
const [qrImg, setQrImg] = useState<any>("");
|
const [qrImg, setQrImg] = useState<any>("");
|
||||||
|
const [currentTaskId, setCurrentTaskId] = useState<string>("");
|
||||||
const [showActionMenu, setShowActionMenu] = useState<string | null>(null);
|
const [showActionMenu, setShowActionMenu] = useState<string | null>(null);
|
||||||
|
|
||||||
// 分页相关状态
|
// 分页相关状态
|
||||||
@@ -206,31 +210,26 @@ const ScenarioList: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const response: PlanDetail = await getPlanDetail(taskId);
|
const response: PlanDetail = await getPlanDetail(taskId);
|
||||||
if (response) {
|
if (response) {
|
||||||
|
// 处理webhook URL,使用工具函数构建完整地址
|
||||||
|
const webhookUrl = buildApiUrl(
|
||||||
|
response.textUrl?.fullUrl || `webhook/${taskId}`
|
||||||
|
);
|
||||||
|
|
||||||
setCurrentApiSettings({
|
setCurrentApiSettings({
|
||||||
apiKey: response.apiKey || "demo-api-key-123456",
|
apiKey: response.apiKey || "demo-api-key-123456",
|
||||||
webhookUrl:
|
webhookUrl: webhookUrl,
|
||||||
response.textUrl?.fullUrl ||
|
|
||||||
`https://api.example.com/webhook/${taskId}`,
|
|
||||||
taskId: taskId,
|
taskId: taskId,
|
||||||
});
|
});
|
||||||
setShowApiDialog(true);
|
setShowApiDialog(true);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
content: "获取API设置失败",
|
content: "获取计划接口失败",
|
||||||
position: "top",
|
position: "top",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyApiUrl = (url: string) => {
|
|
||||||
navigator.clipboard.writeText(url);
|
|
||||||
Toast.show({
|
|
||||||
content: "API地址已复制到剪贴板",
|
|
||||||
position: "top",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateNewPlan = () => {
|
const handleCreateNewPlan = () => {
|
||||||
navigate(`/scenarios/new/${scenarioId}`);
|
navigate(`/scenarios/new/${scenarioId}`);
|
||||||
};
|
};
|
||||||
@@ -239,6 +238,7 @@ const ScenarioList: React.FC = () => {
|
|||||||
setQrLoading(true);
|
setQrLoading(true);
|
||||||
setShowQrDialog(true);
|
setShowQrDialog(true);
|
||||||
setQrImg("");
|
setQrImg("");
|
||||||
|
setCurrentTaskId(taskId); // 设置当前任务ID
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await getWxMinAppCode(taskId);
|
const response = await getWxMinAppCode(taskId);
|
||||||
@@ -294,31 +294,31 @@ const ScenarioList: React.FC = () => {
|
|||||||
const getActionMenu = (task: Task) => [
|
const getActionMenu = (task: Task) => [
|
||||||
{
|
{
|
||||||
key: "edit",
|
key: "edit",
|
||||||
text: "编辑",
|
text: "编辑计划",
|
||||||
icon: <EditOutlined />,
|
icon: <EditOutlined />,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setShowActionMenu(null);
|
setShowActionMenu(null);
|
||||||
navigate(`/scenarios/edit/${task.id}`);
|
navigate(`/scenarios/edit/${task.id}`);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: "settings",
|
|
||||||
text: "API设置",
|
|
||||||
icon: <SettingOutlined />,
|
|
||||||
onClick: () => {
|
|
||||||
setShowActionMenu(null);
|
|
||||||
handleOpenApiSettings(task.id);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: "copy",
|
key: "copy",
|
||||||
text: "复制",
|
text: "复制计划",
|
||||||
icon: <CopyOutlined />,
|
icon: <CopyOutlined />,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setShowActionMenu(null);
|
setShowActionMenu(null);
|
||||||
handleCopyPlan(task.id);
|
handleCopyPlan(task.id);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "settings",
|
||||||
|
text: "计划接口",
|
||||||
|
icon: <SettingOutlined />,
|
||||||
|
onClick: () => {
|
||||||
|
setShowActionMenu(null);
|
||||||
|
handleOpenApiSettings(task.id);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "qrcode",
|
key: "qrcode",
|
||||||
text: "二维码",
|
text: "二维码",
|
||||||
@@ -330,7 +330,7 @@ const ScenarioList: React.FC = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "delete",
|
key: "delete",
|
||||||
text: "删除",
|
text: "删除计划",
|
||||||
icon: <DeleteOutlined />,
|
icon: <DeleteOutlined />,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setShowActionMenu(null);
|
setShowActionMenu(null);
|
||||||
@@ -355,7 +355,14 @@ const ScenarioList: React.FC = () => {
|
|||||||
<NavBar
|
<NavBar
|
||||||
back={null}
|
back={null}
|
||||||
style={{ background: "#fff" }}
|
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={
|
right={
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
@@ -504,81 +511,14 @@ const ScenarioList: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* API设置弹窗 */}
|
{/* 计划接口弹窗 */}
|
||||||
<Popup
|
<PlanApi
|
||||||
visible={showApiDialog}
|
visible={showApiDialog}
|
||||||
onMaskClick={() => setShowApiDialog(false)}
|
onClose={() => setShowApiDialog(false)}
|
||||||
position="bottom"
|
apiKey={currentApiSettings.apiKey}
|
||||||
bodyStyle={{ height: "70vh" }}
|
webhookUrl={currentApiSettings.webhookUrl}
|
||||||
>
|
taskId={currentApiSettings.taskId}
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* 操作菜单弹窗 */}
|
{/* 操作菜单弹窗 */}
|
||||||
<Popup
|
<Popup
|
||||||
@@ -632,11 +572,40 @@ const ScenarioList: React.FC = () => {
|
|||||||
<div>生成二维码中...</div>
|
<div>生成二维码中...</div>
|
||||||
</div>
|
</div>
|
||||||
) : qrImg ? (
|
) : qrImg ? (
|
||||||
<img
|
<>
|
||||||
src={qrImg}
|
<img
|
||||||
alt="小程序二维码"
|
src={qrImg}
|
||||||
className={style["qr-image"]}
|
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>
|
<div className={style["qr-error"]}>二维码生成失败</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
601
nkebao/src/pages/scenarios/plan/list/planApi.module.scss
Normal file
601
nkebao/src/pages/scenarios/plan/list/planApi.module.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
437
nkebao/src/pages/scenarios/plan/list/planApi.tsx
Normal file
437
nkebao/src/pages/scenarios/plan/list/planApi.tsx
Normal 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}
|
||||||
|
* />
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* 特性:
|
||||||
|
* - 移动端使用 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<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;
|
||||||
73
nkebao/src/utils/apiUrl.ts
Normal file
73
nkebao/src/utils/apiUrl.ts
Normal 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}`;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user