渠道代码提交

This commit is contained in:
wong
2025-12-18 10:34:50 +08:00
parent d8183909ca
commit 1cb7903e37
5 changed files with 774 additions and 505 deletions

View File

@@ -1,10 +1,11 @@
// 步骤定义 - 只保留三个步骤
// 步骤定义 - 个步骤
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
import { GroupSelectionItem } from "@/components/GroupSelection/data";
export const steps = [
{ id: 1, title: "步骤一", subtitle: "基础设置" },
{ id: 2, title: "步骤二", subtitle: "好友申请设置" },
{ id: 3, title: "步骤三", subtitle: "消息设置" },
{ id: 3, title: "步骤三", subtitle: "渠道设置" },
{ id: 4, title: "步骤四", subtitle: "消息设置" },
];
// 类型定义

View File

@@ -4,6 +4,7 @@ import { message, Button, Space } from "antd";
import NavCommon from "@/components/NavCommon";
import BasicSettings from "./steps/BasicSettings";
import FriendRequestSettings from "./steps/FriendRequestSettings";
import DistributionSettings from "./steps/DistributionSettings";
import MessageSettings from "./steps/MessageSettings";
import Layout from "@/components/Layout/Layout";
import StepIndicator from "@/components/StepIndicator";
@@ -254,6 +255,14 @@ export default function NewPlan() {
<FriendRequestSettings formData={formData} onChange={onChange} />
);
case 3:
return (
<DistributionSettings
formData={formData}
onChange={onChange}
planId={planId}
/>
);
case 4:
return <MessageSettings formData={formData} onChange={onChange} />;
default:
return null;

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef, useCallback } from "react";
import React, { useState, useEffect, useRef } from "react";
import { Input, Button, Tag, Switch, Spin, message, Modal } from "antd";
import {
PlusOutlined,
@@ -8,17 +8,12 @@ import {
SearchOutlined,
DeleteOutlined,
} from "@ant-design/icons";
import { Checkbox, Popup } from "antd-mobile";
import { uploadFile } from "@/api/common";
import styles from "./base.module.scss";
import { fetchChannelList } from "@/pages/mobile/workspace/distribution-management/api";
import { posterTemplates } from "./base.data";
import GroupSelection from "@/components/GroupSelection";
import FileUpload from "@/components/Upload/FileUpload";
import { GroupSelectionItem } from "@/components/GroupSelection/data";
import Layout from "@/components/Layout/Layout";
import PopupHeader from "@/components/PopuLayout/header";
import PopupFooter from "@/components/PopuLayout/footer";
interface BasicSettingsProps {
isEdit: boolean;
@@ -75,25 +70,6 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
questionExtraction: formData.phoneSettings?.questionExtraction ?? true,
});
// 分销相关状态
const [distributionEnabled, setDistributionEnabled] = useState<boolean>(
formData.distributionEnabled ?? false,
);
const [channelModalVisible, setChannelModalVisible] = useState(false);
const [channelLoading, setChannelLoading] = useState(false);
const [channelList, setChannelList] = useState<any[]>([]);
const [tempSelectedChannelIds, setTempSelectedChannelIds] = useState<
Array<string | number>
>(formData.distributionChannelIds || []);
const [channelSearchQuery, setChannelSearchQuery] = useState("");
const [channelCurrentPage, setChannelCurrentPage] = useState(1);
const [channelTotal, setChannelTotal] = useState(0);
const [customerReward, setCustomerReward] = useState<number | undefined>(
formData.distributionCustomerReward
);
const [addReward, setAddReward] = useState<number | undefined>(
formData.distributionAddReward
);
// 新增:自定义海报相关状态
const [customPosters, setCustomPosters] = useState<Material[]>([]);
@@ -124,18 +100,6 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
setTips(formData.tips || "");
}, [formData.tips]);
// 同步分销相关的外部表单数据到本地状态
useEffect(() => {
setDistributionEnabled(formData.distributionEnabled ?? false);
setTempSelectedChannelIds(formData.distributionChannelIds || []);
setCustomerReward(formData.distributionCustomerReward);
setAddReward(formData.distributionAddReward);
}, [
formData.distributionEnabled,
formData.distributionChannelIds,
formData.distributionCustomerReward,
formData.distributionAddReward,
]);
// 选中场景
const handleScenarioSelect = (sceneId: number) => {
@@ -266,179 +230,6 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
});
};
const PAGE_SIZE = 20;
// 加载分销渠道列表支持keyword和分页强制只获取启用的渠道
const loadDistributionChannels = useCallback(
async (keyword: string = "", page: number = 1) => {
setChannelLoading(true);
try {
const res = await fetchChannelList({
page,
limit: PAGE_SIZE,
keyword: keyword.trim() || undefined,
status: "enabled", // 强制只获取启用的渠道
});
setChannelList(res.list || []);
setChannelTotal(res.total || 0);
} catch (error: any) {
} finally {
setChannelLoading(false);
}
},
[]
);
const handleToggleDistribution = (value: boolean) => {
setDistributionEnabled(value);
// 关闭时清空已选渠道和奖励金额
if (!value) {
setTempSelectedChannelIds([]);
setCustomerReward(undefined);
setAddReward(undefined);
onChange({
...formData,
distributionEnabled: false,
distributionChannelIds: [],
distributionChannelsOptions: [],
distributionCustomerReward: undefined,
distributionAddReward: undefined,
});
} else {
onChange({
...formData,
distributionEnabled: true,
});
}
};
// 打开弹窗时获取第一页
useEffect(() => {
if (channelModalVisible) {
setChannelSearchQuery("");
setChannelCurrentPage(1);
// 复制一份已选渠道到临时变量
setTempSelectedChannelIds(formData.distributionChannelIds || []);
loadDistributionChannels("", 1);
}
}, [channelModalVisible, loadDistributionChannels, formData.distributionChannelIds]);
// 搜索防抖
useEffect(() => {
if (!channelModalVisible) return;
const timer = setTimeout(() => {
setChannelCurrentPage(1);
loadDistributionChannels(channelSearchQuery, 1);
}, 500);
return () => clearTimeout(timer);
}, [channelSearchQuery, channelModalVisible, loadDistributionChannels]);
// 翻页时重新请求
useEffect(() => {
if (!channelModalVisible) return;
loadDistributionChannels(channelSearchQuery, channelCurrentPage);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [channelCurrentPage]);
const handleOpenChannelModal = () => {
setChannelModalVisible(true);
};
const handleChannelToggle = (channel: any) => {
const id = channel.id;
setTempSelectedChannelIds(prev =>
prev.includes(id) ? prev.filter(v => v !== id) : [...prev, id],
);
};
// 直接使用从API返回的渠道列表API已过滤为只返回启用的
const filteredChannels = channelList;
const channelTotalPages = Math.max(1, Math.ceil(channelTotal / PAGE_SIZE));
// 全选当前页
const handleSelectAllCurrentPage = (checked: boolean) => {
if (checked) {
// 全选:添加当前页面所有未选中的渠道
const currentPageChannels = filteredChannels.filter(
(channel: any) => !tempSelectedChannelIds.includes(channel.id),
);
setTempSelectedChannelIds(prev => [
...prev,
...currentPageChannels.map((c: any) => c.id),
]);
} else {
// 取消全选:移除当前页面的所有渠道
const currentPageChannelIds = filteredChannels.map((c: any) => c.id);
setTempSelectedChannelIds(prev =>
prev.filter(id => !currentPageChannelIds.includes(id)),
);
}
};
// 检查当前页是否全选
const isCurrentPageAllSelected =
filteredChannels.length > 0 &&
filteredChannels.every((channel: any) =>
tempSelectedChannelIds.includes(channel.id),
);
const handleConfirmChannels = () => {
const selectedOptions =
channelList
.filter(c => tempSelectedChannelIds.includes(c.id))
.map(c => ({
id: c.id,
name: c.name,
})) || [];
onChange({
...formData,
distributionEnabled: true,
distributionChannelIds: tempSelectedChannelIds,
distributionChannelsOptions: selectedOptions,
});
setDistributionEnabled(true);
setChannelModalVisible(false);
};
const handleCancelChannels = () => {
setChannelModalVisible(false);
// 取消时恢复为表单中的已有值
setTempSelectedChannelIds(formData.distributionChannelIds || []);
};
// 获取显示文本(参考设备选择)
const getChannelDisplayText = () => {
const selectedChannels = formData.distributionChannelsOptions || [];
if (selectedChannels.length === 0) return "";
return `已选择 ${selectedChannels.length} 个渠道`;
};
// 删除已选渠道
const handleRemoveChannel = (id: string | number) => {
const newChannelIds = (formData.distributionChannelIds || []).filter(
(cid: string | number) => cid !== id
);
const newChannelOptions = (formData.distributionChannelsOptions || []).filter(
(item: { id: string | number; name: string }) => item.id !== id
);
onChange({
...formData,
distributionChannelIds: newChannelIds,
distributionChannelsOptions: newChannelOptions,
});
};
// 清除所有已选渠道
const handleClearAllChannels = () => {
onChange({
...formData,
distributionChannelIds: [],
distributionChannelsOptions: [],
});
};
return (
<div className={styles["basic-container"]}>
@@ -688,190 +479,6 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
</div>
)}
{/* 分销设置 */}
<div className={styles["basic-distribution"]}>
<div className={styles["basic-distribution-header"]}>
<div>
<div className={styles["basic-distribution-title"]}></div>
<div className={styles["basic-distribution-desc"]}>
</div>
</div>
<Switch
checked={distributionEnabled}
onChange={handleToggleDistribution}
/>
</div>
{distributionEnabled && (
<>
{/* 输入框 - 参考设备选择样式 */}
<div className={styles["distribution-input-wrapper"]}>
<Input
placeholder="选择分销渠道"
value={getChannelDisplayText()}
onClick={handleOpenChannelModal}
prefix={<SearchOutlined />}
allowClear
onClear={handleClearAllChannels}
size="large"
readOnly
style={{ cursor: "pointer" }}
/>
</div>
{/* 已选渠道列表 - 参考设备选择样式 */}
{formData.distributionChannelsOptions &&
formData.distributionChannelsOptions.length > 0 ? (
<div
className={styles["distribution-selected-list"]}
style={{
maxHeight: 300,
overflowY: "auto",
marginTop: 8,
border: "1px solid #e5e6eb",
borderRadius: 8,
background: "#fff",
}}
>
{formData.distributionChannelsOptions.map(
(item: { id: string | number; name: string; code?: string }) => (
<div
key={item.id}
className={styles["distribution-selected-item"]}
style={{
display: "flex",
alignItems: "center",
padding: "8px 12px",
borderBottom: "1px solid #f0f0f0",
fontSize: 14,
}}
>
{/* 渠道图标 */}
<div
style={{
width: 40,
height: 40,
borderRadius: "6px",
background:
"linear-gradient(135deg, #1677ff 0%, #0958d9 100%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
boxShadow: "0 2px 8px rgba(22, 119, 255, 0.25)",
marginRight: "12px",
flexShrink: 0,
}}
>
<span
style={{
fontSize: 16,
color: "#fff",
fontWeight: 700,
textShadow: "0 1px 3px rgba(0,0,0,0.3)",
}}
>
{(item.name || "渠")[0]}
</span>
</div>
<div
style={{
flex: 1,
minWidth: 0,
display: "flex",
flexDirection: "column",
gap: 2,
}}
>
<div
style={{
fontSize: 14,
fontWeight: 500,
color: "#1a1a1a",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{item.name}
</div>
{item.code && (
<div
style={{
fontSize: 12,
color: "#8c8c8c",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
: {item.code}
</div>
)}
</div>
<Button
type="text"
icon={<DeleteOutlined />}
size="small"
style={{
marginLeft: 4,
color: "#ff4d4f",
border: "none",
background: "none",
minWidth: 24,
height: 24,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
onClick={() => handleRemoveChannel(item.id)}
/>
</div>
),
)}
</div>
) : null}
{/* 奖励金额设置 */}
<div className={styles["distribution-rewards"]}>
<div className={styles["basic-label"]}></div>
<Input
type="number"
placeholder="请输入获客奖励金额"
value={customerReward}
onChange={e => {
const value = e.target.value ? Number(e.target.value) : undefined;
setCustomerReward(value);
onChange({
...formData,
distributionCustomerReward: value,
});
}}
min={0}
step={0.01}
style={{ marginBottom: 12 }}
/>
<div className={styles["basic-label"]}></div>
<Input
type="number"
placeholder="请输入添加奖励金额"
value={addReward}
onChange={e => {
const value = e.target.value ? Number(e.target.value) : undefined;
setAddReward(value);
onChange({
...formData,
distributionAddReward: value,
});
}}
min={0}
step={0.01}
/>
</div>
</>
)}
</div>
{/* 订单导入区块 - 使用FileUpload组件 */}
<div className={styles["basic-order-upload"]} style={openOrder}>
<div className={styles["basic-order-upload-label"]}></div>
@@ -959,114 +566,6 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
/>
</div>
{/* 分销渠道选择弹框 - 参考设备选择样式 */}
<Popup
visible={channelModalVisible}
onMaskClick={handleCancelChannels}
position="bottom"
bodyStyle={{ height: "100vh" }}
closeOnMaskClick={false}
>
<Layout
header={
<PopupHeader
title="选择分销渠道"
searchQuery={channelSearchQuery}
setSearchQuery={setChannelSearchQuery}
searchPlaceholder="搜索渠道名称、编码..."
loading={channelLoading}
onRefresh={() => loadDistributionChannels(channelSearchQuery, channelCurrentPage)}
showTabs={false}
/>
}
footer={
<PopupFooter
currentPage={channelCurrentPage}
totalPages={channelTotalPages}
loading={channelLoading}
selectedCount={tempSelectedChannelIds.length}
onPageChange={setChannelCurrentPage}
onCancel={handleCancelChannels}
onConfirm={handleConfirmChannels}
isAllSelected={isCurrentPageAllSelected}
onSelectAll={handleSelectAllCurrentPage}
/>
}
>
<div className={styles["channelList"]}>
{channelLoading && channelList.length === 0 ? (
<div className={styles["loadingBox"]}>
<div className={styles["loadingText"]}>...</div>
</div>
) : filteredChannels.length === 0 ? (
<div className={styles["loadingBox"]}>
<div className={styles["loadingText"]}>
</div>
</div>
) : (
<div className={styles["channelListInner"]}>
{filteredChannels.map((channel: any) => (
<div key={channel.id} className={styles["channelItem"]}>
{/* 顶部行:选择框和编码 */}
<div className={styles["headerRow"]}>
<div className={styles["checkboxContainer"]}>
<Checkbox
checked={tempSelectedChannelIds.includes(channel.id)}
onChange={() => handleChannelToggle(channel)}
className={styles["channelCheckbox"]}
/>
</div>
<span className={styles["codeText"]}>
: {channel.code}
</span>
</div>
{/* 主要内容区域:渠道信息 */}
<div className={styles["mainContent"]}>
{/* 渠道信息 */}
<div className={styles["channelContent"]}>
<div className={styles["channelInfoRow"]}>
<span className={styles["channelName"]}>
{channel.name}
</span>
<div
className={
channel.status === "enabled"
? styles["statusEnabled"]
: styles["statusDisabled"]
}
>
{channel.status === "enabled" ? "启用" : "禁用"}
</div>
</div>
<div className={styles["channelInfoDetail"]}>
{channel.phone && (
<div className={styles["infoItem"]}>
<span className={styles["infoLabel"]}>:</span>
<span className={styles["infoValue"]}>
{channel.phone}
</span>
</div>
)}
{channel.wechatId && (
<div className={styles["infoItem"]}>
<span className={styles["infoLabel"]}>:</span>
<span className={styles["infoValue"]}>
{channel.wechatId}
</span>
</div>
)}
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
</Layout>
</Popup>
</div>
);
};

View File

@@ -0,0 +1,743 @@
import React, { useState, useEffect, useCallback } from "react";
import { Input, Button, Switch } from "antd";
import { Toast, SpinLoading } from "antd-mobile";
import {
SearchOutlined,
DeleteOutlined,
QrcodeOutlined,
} from "@ant-design/icons";
import { Checkbox, Popup } from "antd-mobile";
import styles from "./base.module.scss";
import { fetchChannelList } from "@/pages/mobile/workspace/distribution-management/api";
import Layout from "@/components/Layout/Layout";
import PopupHeader from "@/components/PopuLayout/header";
import PopupFooter from "@/components/PopuLayout/footer";
import request from "@/api/request";
interface DistributionSettingsProps {
formData: any;
onChange: (data: any) => void;
planId?: string; // 计划ID用于生成二维码
}
const DistributionSettings: React.FC<DistributionSettingsProps> = ({
formData,
onChange,
planId,
}) => {
// 分销相关状态
const [distributionEnabled, setDistributionEnabled] = useState<boolean>(
formData.distributionEnabled ?? false,
);
const [channelModalVisible, setChannelModalVisible] = useState(false);
const [channelLoading, setChannelLoading] = useState(false);
const [channelList, setChannelList] = useState<any[]>([]);
const [tempSelectedChannelIds, setTempSelectedChannelIds] = useState<
Array<string | number>
>(formData.distributionChannelIds || []);
const [channelSearchQuery, setChannelSearchQuery] = useState("");
const [channelCurrentPage, setChannelCurrentPage] = useState(1);
const [channelTotal, setChannelTotal] = useState(0);
const [customerReward, setCustomerReward] = useState<number | undefined>(
formData.distributionCustomerReward
);
const [addReward, setAddReward] = useState<number | undefined>(
formData.distributionAddReward
);
// 二维码相关状态
const [qrCodeMap, setQrCodeMap] = useState<Record<string | number, {
qrCode: string;
url: string;
loading: boolean;
}>>({});
const [showQrDialog, setShowQrDialog] = useState(false);
const [currentQrChannel, setCurrentQrChannel] = useState<{
id: string | number;
name: string;
code?: string;
} | null>(null);
const PAGE_SIZE = 20;
// 生成渠道二维码
const generateChannelQRCode = async (channelId: string | number, channelCode?: string) => {
// 如果已经有二维码,直接返回
if (qrCodeMap[channelId]?.qrCode) {
return;
}
// 设置加载状态
setQrCodeMap(prev => ({
...prev,
[channelId]: { ...prev[channelId], loading: true },
}));
try {
// 参考列表生成的参数使用计划ID和渠道ID/code生成二维码
// 如果是在新建状态没有planId使用渠道code如果有planId使用planId和channelId
const params: any = {};
if (planId) {
params.taskId = planId;
params.channelId = channelId;
} else if (channelCode) {
params.channelCode = channelCode;
}
// 调用API生成二维码参考列表中的实现
const response = await request(
`/v1/plan/getWxMinAppCode`,
params,
"GET"
);
if (response && response.qrCode) {
setQrCodeMap(prev => ({
...prev,
[channelId]: {
qrCode: response.qrCode,
url: response.url || "",
loading: false,
},
}));
} else {
throw new Error("二维码生成失败");
}
} catch (error: any) {
Toast.show({
content: error.message || "生成二维码失败",
position: "top",
});
setQrCodeMap(prev => ({
...prev,
[channelId]: { ...prev[channelId], loading: false },
}));
}
};
// 显示二维码弹窗
const handleShowQRCode = async (channel: { id: string | number; name: string; code?: string }) => {
setCurrentQrChannel(channel);
setShowQrDialog(true);
// 如果还没有生成二维码,则生成
if (!qrCodeMap[channel.id]?.qrCode && !qrCodeMap[channel.id]?.loading) {
await generateChannelQRCode(channel.id, channel.code);
}
};
// 同步分销相关的外部表单数据到本地状态
useEffect(() => {
setDistributionEnabled(formData.distributionEnabled ?? false);
setTempSelectedChannelIds(formData.distributionChannelIds || []);
setCustomerReward(formData.distributionCustomerReward);
setAddReward(formData.distributionAddReward);
}, [
formData.distributionEnabled,
formData.distributionChannelIds,
formData.distributionCustomerReward,
formData.distributionAddReward,
]);
// 加载分销渠道列表支持keyword和分页强制只获取启用的渠道
const loadDistributionChannels = useCallback(
async (keyword: string = "", page: number = 1) => {
setChannelLoading(true);
try {
const res = await fetchChannelList({
page,
limit: PAGE_SIZE,
keyword: keyword.trim() || undefined,
status: "enabled", // 强制只获取启用的渠道
});
setChannelList(res.list || []);
setChannelTotal(res.total || 0);
} catch (error: any) {
// 错误处理
} finally {
setChannelLoading(false);
}
},
[]
);
const handleToggleDistribution = (value: boolean) => {
setDistributionEnabled(value);
// 关闭时清空已选渠道和奖励金额
if (!value) {
setTempSelectedChannelIds([]);
setCustomerReward(undefined);
setAddReward(undefined);
onChange({
...formData,
distributionEnabled: false,
distributionChannelIds: [],
distributionChannelsOptions: [],
distributionCustomerReward: undefined,
distributionAddReward: undefined,
});
} else {
onChange({
...formData,
distributionEnabled: true,
});
}
};
// 打开弹窗时获取第一页
useEffect(() => {
if (channelModalVisible) {
setChannelSearchQuery("");
setChannelCurrentPage(1);
// 复制一份已选渠道到临时变量
setTempSelectedChannelIds(formData.distributionChannelIds || []);
loadDistributionChannels("", 1);
}
}, [channelModalVisible, loadDistributionChannels, formData.distributionChannelIds]);
// 搜索防抖
useEffect(() => {
if (!channelModalVisible) return;
const timer = setTimeout(() => {
setChannelCurrentPage(1);
loadDistributionChannels(channelSearchQuery, 1);
}, 500);
return () => clearTimeout(timer);
}, [channelSearchQuery, channelModalVisible, loadDistributionChannels]);
// 翻页时重新请求
useEffect(() => {
if (!channelModalVisible) return;
loadDistributionChannels(channelSearchQuery, channelCurrentPage);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [channelCurrentPage]);
const handleOpenChannelModal = () => {
setChannelModalVisible(true);
};
const handleChannelToggle = (channel: any) => {
const id = channel.id;
setTempSelectedChannelIds(prev =>
prev.includes(id) ? prev.filter(v => v !== id) : [...prev, id],
);
};
// 直接使用从API返回的渠道列表API已过滤为只返回启用的
const filteredChannels = channelList;
const channelTotalPages = Math.max(1, Math.ceil(channelTotal / PAGE_SIZE));
// 全选当前页
const handleSelectAllCurrentPage = (checked: boolean) => {
if (checked) {
// 全选:添加当前页面所有未选中的渠道
const currentPageChannels = filteredChannels.filter(
(channel: any) => !tempSelectedChannelIds.includes(channel.id),
);
setTempSelectedChannelIds(prev => [
...prev,
...currentPageChannels.map((c: any) => c.id),
]);
} else {
// 取消全选:移除当前页面的所有渠道
const currentPageChannelIds = filteredChannels.map((c: any) => c.id);
setTempSelectedChannelIds(prev =>
prev.filter(id => !currentPageChannelIds.includes(id)),
);
}
};
// 检查当前页是否全选
const isCurrentPageAllSelected =
filteredChannels.length > 0 &&
filteredChannels.every((channel: any) =>
tempSelectedChannelIds.includes(channel.id),
);
const handleConfirmChannels = () => {
const selectedOptions =
channelList
.filter(c => tempSelectedChannelIds.includes(c.id))
.map(c => ({
id: c.id,
name: c.name,
code: c.code,
})) || [];
onChange({
...formData,
distributionEnabled: true,
distributionChannelIds: tempSelectedChannelIds,
distributionChannelsOptions: selectedOptions,
});
setDistributionEnabled(true);
setChannelModalVisible(false);
};
const handleCancelChannels = () => {
setChannelModalVisible(false);
// 取消时恢复为表单中的已有值
setTempSelectedChannelIds(formData.distributionChannelIds || []);
};
// 获取显示文本(参考设备选择)
const getChannelDisplayText = () => {
const selectedChannels = formData.distributionChannelsOptions || [];
if (selectedChannels.length === 0) return "";
return `已选择 ${selectedChannels.length} 个渠道`;
};
// 删除已选渠道
const handleRemoveChannel = (id: string | number) => {
const newChannelIds = (formData.distributionChannelIds || []).filter(
(cid: string | number) => cid !== id
);
const newChannelOptions = (formData.distributionChannelsOptions || []).filter(
(item: { id: string | number; name: string }) => item.id !== id
);
onChange({
...formData,
distributionChannelIds: newChannelIds,
distributionChannelsOptions: newChannelOptions,
});
};
// 清除所有已选渠道
const handleClearAllChannels = () => {
onChange({
...formData,
distributionChannelIds: [],
distributionChannelsOptions: [],
});
};
return (
<div className={styles["basic-container"]}>
{/* 分销设置 */}
<div className={styles["basic-distribution"]}>
<div className={styles["basic-distribution-header"]}>
<div>
<div className={styles["basic-distribution-title"]}></div>
<div className={styles["basic-distribution-desc"]}>
</div>
</div>
<Switch
checked={distributionEnabled}
onChange={handleToggleDistribution}
/>
</div>
{distributionEnabled && (
<>
{/* 输入框 - 参考设备选择样式 */}
<div className={styles["distribution-input-wrapper"]}>
<Input
placeholder="选择分销渠道"
value={getChannelDisplayText()}
onClick={handleOpenChannelModal}
prefix={<SearchOutlined />}
allowClear
onClear={handleClearAllChannels}
size="large"
readOnly
style={{ cursor: "pointer" }}
/>
</div>
{/* 已选渠道列表 - 参考设备选择样式 */}
{formData.distributionChannelsOptions &&
formData.distributionChannelsOptions.length > 0 ? (
<div
className={styles["distribution-selected-list"]}
style={{
maxHeight: 300,
overflowY: "auto",
marginTop: 8,
border: "1px solid #e5e6eb",
borderRadius: 8,
background: "#fff",
}}
>
{formData.distributionChannelsOptions.map(
(item: { id: string | number; name: string; code?: string }) => (
<div
key={item.id}
className={styles["distribution-selected-item"]}
style={{
display: "flex",
alignItems: "center",
padding: "8px 12px",
borderBottom: "1px solid #f0f0f0",
fontSize: 14,
}}
>
{/* 渠道图标 */}
<div
style={{
width: 40,
height: 40,
borderRadius: "6px",
background:
"linear-gradient(135deg, #1677ff 0%, #0958d9 100%)",
display: "flex",
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
boxShadow: "0 2px 8px rgba(22, 119, 255, 0.25)",
marginRight: "12px",
flexShrink: 0,
}}
>
<span
style={{
fontSize: 16,
color: "#fff",
fontWeight: 700,
textShadow: "0 1px 3px rgba(0,0,0,0.3)",
}}
>
{(item.name || "渠")[0]}
</span>
</div>
<div
style={{
flex: 1,
minWidth: 0,
display: "flex",
flexDirection: "column",
gap: 2,
}}
>
<div
style={{
fontSize: 14,
fontWeight: 500,
color: "#1a1a1a",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{item.name}
</div>
{item.code && (
<div
style={{
fontSize: 12,
color: "#8c8c8c",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
: {item.code}
</div>
)}
</div>
<div style={{ display: "flex", gap: 4, alignItems: "center" }}>
<Button
type="text"
icon={<QrcodeOutlined />}
size="small"
style={{
color: "#1890ff",
border: "none",
background: "none",
minWidth: 24,
height: 24,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
onClick={() => handleShowQRCode(item)}
/>
<Button
type="text"
icon={<DeleteOutlined />}
size="small"
style={{
color: "#ff4d4f",
border: "none",
background: "none",
minWidth: 24,
height: 24,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
onClick={() => handleRemoveChannel(item.id)}
/>
</div>
</div>
),
)}
</div>
) : null}
{/* 奖励金额设置 */}
<div className={styles["distribution-rewards"]}>
<div className={styles["basic-label"]}></div>
<Input
type="number"
placeholder="请输入获客奖励金额"
value={customerReward}
onChange={e => {
const value = e.target.value ? Number(e.target.value) : undefined;
setCustomerReward(value);
onChange({
...formData,
distributionCustomerReward: value,
});
}}
min={0}
step={0.01}
style={{ marginBottom: 12 }}
/>
<div className={styles["basic-label"]}></div>
<Input
type="number"
placeholder="请输入添加奖励金额"
value={addReward}
onChange={e => {
const value = e.target.value ? Number(e.target.value) : undefined;
setAddReward(value);
onChange({
...formData,
distributionAddReward: value,
});
}}
min={0}
step={0.01}
/>
</div>
</>
)}
</div>
{/* 分销渠道选择弹框 - 参考设备选择样式 */}
<Popup
visible={channelModalVisible}
onMaskClick={handleCancelChannels}
position="bottom"
bodyStyle={{ height: "100vh" }}
closeOnMaskClick={false}
>
<Layout
header={
<PopupHeader
title="选择分销渠道"
searchQuery={channelSearchQuery}
setSearchQuery={setChannelSearchQuery}
searchPlaceholder="搜索渠道名称、编码..."
loading={channelLoading}
onRefresh={() => loadDistributionChannels(channelSearchQuery, channelCurrentPage)}
showTabs={false}
/>
}
footer={
<PopupFooter
currentPage={channelCurrentPage}
totalPages={channelTotalPages}
loading={channelLoading}
selectedCount={tempSelectedChannelIds.length}
onPageChange={setChannelCurrentPage}
onCancel={handleCancelChannels}
onConfirm={handleConfirmChannels}
isAllSelected={isCurrentPageAllSelected}
onSelectAll={handleSelectAllCurrentPage}
/>
}
>
<div className={styles["channelList"]}>
{channelLoading && channelList.length === 0 ? (
<div className={styles["loadingBox"]}>
<div className={styles["loadingText"]}>...</div>
</div>
) : filteredChannels.length === 0 ? (
<div className={styles["loadingBox"]}>
<div className={styles["loadingText"]}>
</div>
</div>
) : (
<div className={styles["channelListInner"]}>
{filteredChannels.map((channel: any) => (
<div key={channel.id} className={styles["channelItem"]}>
{/* 顶部行:选择框和编码 */}
<div className={styles["headerRow"]}>
<div className={styles["checkboxContainer"]}>
<Checkbox
checked={tempSelectedChannelIds.includes(channel.id)}
onChange={() => handleChannelToggle(channel)}
className={styles["channelCheckbox"]}
/>
</div>
<span className={styles["codeText"]}>
: {channel.code}
</span>
</div>
{/* 主要内容区域:渠道信息 */}
<div className={styles["mainContent"]}>
{/* 渠道信息 */}
<div className={styles["channelContent"]}>
<div className={styles["channelInfoRow"]}>
<span className={styles["channelName"]}>
{channel.name}
</span>
<div
className={
channel.status === "enabled"
? styles["statusEnabled"]
: styles["statusDisabled"]
}
>
{channel.status === "enabled" ? "启用" : "禁用"}
</div>
</div>
<div className={styles["channelInfoDetail"]}>
{channel.phone && (
<div className={styles["infoItem"]}>
<span className={styles["infoLabel"]}>:</span>
<span className={styles["infoValue"]}>
{channel.phone}
</span>
</div>
)}
{channel.wechatId && (
<div className={styles["infoItem"]}>
<span className={styles["infoLabel"]}>:</span>
<span className={styles["infoValue"]}>
{channel.wechatId}
</span>
</div>
)}
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
</Layout>
</Popup>
{/* 二维码弹窗 - 参考列表中的实现 */}
<Popup
visible={showQrDialog}
onMaskClick={() => setShowQrDialog(false)}
position="bottom"
>
<div style={{
background: "#fff",
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
padding: "20px",
maxHeight: "80vh",
overflowY: "auto",
}}>
<div style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 20,
}}>
<h3 style={{ margin: 0, fontSize: 18, fontWeight: 600 }}>
{currentQrChannel?.name || "渠道"}
</h3>
<Button
size="small"
onClick={() => setShowQrDialog(false)}
>
</Button>
</div>
<div style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 16,
}}>
{currentQrChannel && (
<>
{qrCodeMap[currentQrChannel.id]?.loading ? (
<div style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 12,
padding: "40px 20px",
}}>
<SpinLoading color="primary" style={{ fontSize: 32 }} />
<div style={{ fontSize: 14, color: "#666" }}>...</div>
</div>
) : qrCodeMap[currentQrChannel.id]?.qrCode ? (
<>
<img
src={qrCodeMap[currentQrChannel.id].qrCode}
alt="渠道二维码"
style={{
width: 200,
height: 200,
border: "1px solid #e5e6eb",
borderRadius: 8,
padding: 8,
background: "#fff",
}}
/>
{qrCodeMap[currentQrChannel.id].url && (
<div style={{
width: "100%",
padding: "12px",
background: "#f5f5f5",
borderRadius: 8,
}}>
<div style={{
fontSize: 12,
color: "#666",
marginBottom: 8,
}}>
</div>
<div style={{
fontSize: 12,
color: "#333",
wordBreak: "break-all",
}}>
{qrCodeMap[currentQrChannel.id].url}
</div>
</div>
)}
</>
) : (
<div style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 12,
padding: "40px 20px",
color: "#999",
}}>
<QrcodeOutlined style={{ fontSize: 48 }} />
<div style={{ fontSize: 14 }}></div>
<Button
size="small"
color="primary"
onClick={() => currentQrChannel && generateChannelQRCode(currentQrChannel.id, currentQrChannel.code)}
>
</Button>
</div>
)}
</>
)}
</div>
</div>
</Popup>
</div>
);
};
export default DistributionSettings;

View File

@@ -441,6 +441,7 @@ class PlanSceneV1Controller extends BaseController
{
$params = $this->request->param();
$taskId = isset($params['taskId']) ? intval($params['taskId']) : 0;
$channelId = isset($params['channelId']) ? intval($params['channelId']) : 0;
if($taskId <= 0) {
return ResponseHelper::error('任务ID或场景ID不能为空', 400);
@@ -451,8 +452,24 @@ class PlanSceneV1Controller extends BaseController
return ResponseHelper::error('任务不存在', 400);
}
// 如果提供了channelId验证渠道是否存在且有效
if ($channelId > 0) {
$channel = Db::name('distribution_channel')
->where([
['id', '=', $channelId],
['companyId', '=', $task['companyId']],
['status', '=', 'enabled'],
['deleteTime', '=', 0]
])
->find();
if (!$channel) {
return ResponseHelper::error('分销渠道不存在或已被禁用', 400);
}
}
$posterWeChatMiniProgram = new PosterWeChatMiniProgram();
$result = $posterWeChatMiniProgram->generateMiniProgramCodeWithScene($taskId);
$result = $posterWeChatMiniProgram->generateMiniProgramCodeWithScene($taskId, $channelId);
$result = json_decode($result, true);
if ($result['code'] == 200){
return ResponseHelper::success($result['data'], '获取小程序码成功');