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