diff --git a/Cunkebao/src/components/ContentSelection/api.ts b/Cunkebao/src/components/ContentSelection/api.ts index a4d4bf3e..f7919df0 100644 --- a/Cunkebao/src/components/ContentSelection/api.ts +++ b/Cunkebao/src/components/ContentSelection/api.ts @@ -1,5 +1,5 @@ import request from "@/api/request"; export function getContentLibraryList(params: any) { - return request("/v1/content/library/list", params, "GET"); + return request("/v1/content/library/list", { ...params, formType: 0 }, "GET"); } diff --git a/Cunkebao/src/components/DeviceSelection/data.ts b/Cunkebao/src/components/DeviceSelection/data.ts index d002905c..9f966a46 100644 --- a/Cunkebao/src/components/DeviceSelection/data.ts +++ b/Cunkebao/src/components/DeviceSelection/data.ts @@ -26,4 +26,5 @@ export interface DeviceSelectionProps { showSelectedList?: boolean; // 新增 readonly?: boolean; // 新增 deviceGroups?: any[]; // 传递设备组数据 + singleSelect?: boolean; // 新增,是否单选模式 } diff --git a/Cunkebao/src/components/DeviceSelection/index.tsx b/Cunkebao/src/components/DeviceSelection/index.tsx index ba6952cd..80d27c49 100644 --- a/Cunkebao/src/components/DeviceSelection/index.tsx +++ b/Cunkebao/src/components/DeviceSelection/index.tsx @@ -18,6 +18,7 @@ const DeviceSelection: React.FC = ({ showInput = true, showSelectedList = true, readonly = false, + singleSelect = false, }) => { // 弹窗控制 const [popupVisible, setPopupVisible] = useState(false); @@ -37,6 +38,9 @@ const DeviceSelection: React.FC = ({ // 获取显示文本 const getDisplayText = () => { if (selectedOptions.length === 0) return ""; + if (singleSelect && selectedOptions.length > 0) { + return selectedOptions[0].memo || selectedOptions[0].wechatId || "已选择设备"; + } return `已选择 ${selectedOptions.length} 个设备`; }; @@ -179,6 +183,7 @@ const DeviceSelection: React.FC = ({ onClose={() => setRealVisible(false)} selectedOptions={selectedOptions} onSelect={onSelect} + singleSelect={singleSelect} /> ); diff --git a/Cunkebao/src/components/DeviceSelection/selectionPopup.tsx b/Cunkebao/src/components/DeviceSelection/selectionPopup.tsx index 7a839f70..0485f58d 100644 --- a/Cunkebao/src/components/DeviceSelection/selectionPopup.tsx +++ b/Cunkebao/src/components/DeviceSelection/selectionPopup.tsx @@ -12,6 +12,7 @@ interface SelectionPopupProps { onClose: () => void; selectedOptions: DeviceSelectionItem[]; onSelect: (devices: DeviceSelectionItem[]) => void; + singleSelect?: boolean; } const PAGE_SIZE = 20; @@ -21,6 +22,7 @@ const SelectionPopup: React.FC = ({ onClose, selectedOptions, onSelect, + singleSelect = false, }) => { // 设备数据 const [devices, setDevices] = useState([]); @@ -110,6 +112,15 @@ const SelectionPopup: React.FC = ({ // 处理设备选择 const handleDeviceToggle = (device: DeviceSelectionItem) => { + if (singleSelect) { + // 单选模式:如果已选中,则取消选择;否则替换为当前设备 + if (tempSelectedOptions.some(v => v.id === device.id)) { + setTempSelectedOptions([]); + } else { + setTempSelectedOptions([device]); + } + } else { + // 多选模式:原有的逻辑 if (tempSelectedOptions.some(v => v.id === device.id)) { setTempSelectedOptions( tempSelectedOptions.filter(v => v.id !== device.id), @@ -117,6 +128,7 @@ const SelectionPopup: React.FC = ({ } else { const newSelectedOptions = [...tempSelectedOptions, device]; setTempSelectedOptions(newSelectedOptions); + } } }; @@ -179,6 +191,7 @@ const SelectionPopup: React.FC = ({ totalPages={totalPages} loading={loading} selectedCount={tempSelectedOptions.length} + singleSelect={singleSelect} onPageChange={setCurrentPage} onCancel={onClose} onConfirm={() => { @@ -187,7 +200,7 @@ const SelectionPopup: React.FC = ({ onClose(); }} isAllSelected={isCurrentPageAllSelected} - onSelectAll={handleSelectAllCurrentPage} + onSelectAll={singleSelect ? undefined : handleSelectAllCurrentPage} /> } > diff --git a/Cunkebao/src/components/FriendSelection/index.tsx b/Cunkebao/src/components/FriendSelection/index.tsx index 63c04a45..a3745eff 100644 --- a/Cunkebao/src/components/FriendSelection/index.tsx +++ b/Cunkebao/src/components/FriendSelection/index.tsx @@ -95,9 +95,9 @@ export default function FriendSelection({ {(selectedOptions || []).map(friend => (
- +
-
{friend.nickname}
+
{friend.nickname || friend.friendName}
{friend.wechatId}
{!readonly && ( diff --git a/Cunkebao/src/components/PopuLayout/footer.tsx b/Cunkebao/src/components/PopuLayout/footer.tsx index 60e562be..3dbf8a52 100644 --- a/Cunkebao/src/components/PopuLayout/footer.tsx +++ b/Cunkebao/src/components/PopuLayout/footer.tsx @@ -14,6 +14,7 @@ interface PopupFooterProps { // 全选功能相关 isAllSelected?: boolean; onSelectAll?: (checked: boolean) => void; + singleSelect?: boolean; } const PopupFooter: React.FC = ({ @@ -26,11 +27,13 @@ const PopupFooter: React.FC = ({ onConfirm, isAllSelected = false, onSelectAll, + singleSelect = false, }) => { return ( <> {/* 分页栏 */}
+ {onSelectAll && (
= ({ 全选当前页
+ )}
-
已选择 {selectedCount} 条记录
+
+ {singleSelect + ? selectedCount > 0 + ? "已选择设备" + : "未选择设备" + : `已选择 ${selectedCount} 条记录`} +
))}
-
是否启用AI
+
- 启用AI后,该内容库下的所有内容都会通过AI生成 + 启用后,该内容库下的内容会通过AI生成
{useAI && ( @@ -305,19 +432,24 @@ export default function ContentForm() { />
)} +
-
时间限制
-
- -
+
+
+ 时间限制 +
+
+
+ setShowStartPicker(true)} />
- -
+
+ setShowEndPicker(true)} /> +
-
- 是否启用 +
+
+ 是否启用 +
diff --git a/Cunkebao/src/pages/mobile/mine/content/list/api.ts b/Cunkebao/src/pages/mobile/mine/content/list/api.ts index 51821b6f..f1ca83db 100644 --- a/Cunkebao/src/pages/mobile/mine/content/list/api.ts +++ b/Cunkebao/src/pages/mobile/mine/content/list/api.ts @@ -12,7 +12,7 @@ export function getContentLibraryList(params: { keyword?: string; sourceType?: number; }): Promise { - return request("/v1/content/library/list", params, "GET"); + return request("/v1/content/library/list", { ...params, formType: 0 }, "GET"); } // 获取内容库详情 @@ -24,7 +24,7 @@ export function getContentLibraryDetail(id: string): Promise { export function createContentLibrary( params: CreateContentLibraryParams, ): Promise { - return request("/v1/content/library/create", params, "POST"); + return request("/v1/content/library/create", { ...params, formType: 0 }, "POST"); } // 更新内容库 diff --git a/Cunkebao/src/pages/mobile/mine/content/materials/list/api.ts b/Cunkebao/src/pages/mobile/mine/content/materials/list/api.ts index e1d6edea..037a90e6 100644 --- a/Cunkebao/src/pages/mobile/mine/content/materials/list/api.ts +++ b/Cunkebao/src/pages/mobile/mine/content/materials/list/api.ts @@ -47,3 +47,11 @@ export function aiRewriteContent(params: AIRewriteParams) { export function replaceContent(params: ReplaceContentParams) { return request("/v1/content/library/aiEditContent", params, "POST"); } + +// 导入Excel素材 +export function importMaterialsFromExcel(params: { + id: string; + fileUrl: string; +}) { + return request("/v1/content/library/import-excel", params, "POST"); +} diff --git a/Cunkebao/src/pages/mobile/mine/content/materials/list/index.module.scss b/Cunkebao/src/pages/mobile/mine/content/materials/list/index.module.scss index 29c91883..834b4e26 100644 --- a/Cunkebao/src/pages/mobile/mine/content/materials/list/index.module.scss +++ b/Cunkebao/src/pages/mobile/mine/content/materials/list/index.module.scss @@ -776,3 +776,87 @@ } } } + +// 导入弹窗样式 +.import-popup-content { + padding: 20px; + max-height: 90vh; + overflow-y: auto; + background: #ffffff; + + .import-popup-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; + padding-bottom: 12px; + border-bottom: 1px solid #e8f4ff; + + h3 { + font-size: 18px; + font-weight: 600; + color: #1677ff; + margin: 0; + display: flex; + align-items: center; + + &::before { + content: ''; + display: inline-block; + width: 4px; + height: 18px; + background: #1677ff; + margin-right: 8px; + border-radius: 2px; + } + } + } + + .import-form { + .import-form-item { + margin-bottom: 20px; + + .import-form-label { + font-size: 15px; + font-weight: 500; + color: #333; + margin-bottom: 8px; + display: flex; + align-items: center; + + &::before { + content: ''; + display: inline-block; + width: 3px; + height: 14px; + background: #1677ff; + margin-right: 6px; + border-radius: 2px; + } + } + + .import-form-control { + .import-tip { + font-size: 12px; + color: #999; + margin-top: 8px; + line-height: 1.5; + } + } + } + + .import-actions { + margin-top: 24px; + + button { + height: 44px; + font-size: 16px; + border-radius: 8px; + + &:first-child { + box-shadow: 0 2px 6px rgba(22, 119, 255, 0.2); + } + } + } + } +} diff --git a/Cunkebao/src/pages/mobile/mine/content/materials/list/index.tsx b/Cunkebao/src/pages/mobile/mine/content/materials/list/index.tsx index aef8c2a8..77d0a864 100644 --- a/Cunkebao/src/pages/mobile/mine/content/materials/list/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/content/materials/list/index.tsx @@ -15,10 +15,12 @@ import { VideoCameraOutlined, FileTextOutlined, AppstoreOutlined, + UploadOutlined, } from "@ant-design/icons"; import Layout from "@/components/Layout/Layout"; import NavCommon from "@/components/NavCommon"; -import { getContentItemList, deleteContentItem, aiRewriteContent, replaceContent } from "./api"; +import { getContentItemList, deleteContentItem, aiRewriteContent, replaceContent, importMaterialsFromExcel } from "./api"; +import FileUpload from "@/components/Upload/FileUpload"; import { ContentItem } from "./data"; import style from "./index.module.scss"; @@ -50,6 +52,11 @@ const MaterialsList: React.FC = () => { const [aiLoading, setAiLoading] = useState(false); const [replaceLoading, setReplaceLoading] = useState(false); + // 导入相关状态 + const [showImportPopup, setShowImportPopup] = useState(false); + const [importFileUrl, setImportFileUrl] = useState(""); + const [importLoading, setImportLoading] = useState(false); + // 获取素材列表 const fetchMaterials = useCallback(async () => { if (!id) return; @@ -187,6 +194,64 @@ const MaterialsList: React.FC = () => { fetchMaterials(); }; + // 处理导入文件上传 + const handleImportFileChange = (fileInfo: { fileName: string; fileUrl: string }) => { + setImportFileUrl(fileInfo.fileUrl); + }; + + // 执行导入 + const handleImport = async () => { + if (!id) { + Toast.show({ + content: "内容库ID不存在", + position: "top", + }); + return; + } + + if (!importFileUrl) { + Toast.show({ + content: "请先上传Excel文件", + position: "top", + }); + return; + } + + try { + setImportLoading(true); + await importMaterialsFromExcel({ + id: id, + fileUrl: importFileUrl, + }); + + Toast.show({ + content: "导入成功", + position: "top", + }); + + // 关闭弹窗并重置状态 + setShowImportPopup(false); + setImportFileUrl(""); + + // 刷新素材列表 + fetchMaterials(); + } catch (error: unknown) { + console.error("导入失败:", error); + Toast.show({ + content: error instanceof Error ? error.message : "导入失败,请重试", + position: "top", + }); + } finally { + setImportLoading(false); + } + }; + + // 关闭导入弹窗 + const closeImportPopup = () => { + setShowImportPopup(false); + setImportFileUrl(""); + }; + const handlePageChange = (page: number) => { setCurrentPage(page); }; @@ -354,9 +419,18 @@ const MaterialsList: React.FC = () => { title="素材管理" backFn={() => navigate("/mine/content")} right={ - + <> + + + } /> {/* 搜索栏 */} @@ -586,6 +660,71 @@ const MaterialsList: React.FC = () => {
+ + {/* 导入弹窗 */} + +
+
+

导入素材

+ +
+ +
+
+
选择Excel文件
+
+ { + const fileUrl = Array.isArray(url) ? url[0] : url; + setImportFileUrl(fileUrl || ""); + }} + acceptTypes={["excel"]} + maxSize={50} + maxCount={1} + showPreview={false} + /> +
+ 请上传Excel格式的文件,文件大小不超过50MB +
+
+
+ +
+ + +
+
+
+
); }; diff --git a/Cunkebao/src/pages/mobile/mine/devices/api.ts b/Cunkebao/src/pages/mobile/mine/devices/api.ts index c8e91198..fd8a26ab 100644 --- a/Cunkebao/src/pages/mobile/mine/devices/api.ts +++ b/Cunkebao/src/pages/mobile/mine/devices/api.ts @@ -42,3 +42,7 @@ export const fetchDeviceQRCode = (accountId: string) => // 通过IMEI添加设备 export const addDeviceByImei = (imei: string, name: string) => request("/v1/api/device/add-by-imei", { imei, name }, "POST"); + +// 获取设备添加结果(用于轮询检查) +export const fetchAddResults = (params: { accountId?: string }) => + request("/v1/devices/add-results", params, "GET"); diff --git a/Cunkebao/src/pages/mobile/mine/devices/index.tsx b/Cunkebao/src/pages/mobile/mine/devices/index.tsx index f84b30c7..802821f5 100644 --- a/Cunkebao/src/pages/mobile/mine/devices/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/devices/index.tsx @@ -15,6 +15,7 @@ import { fetchDeviceQRCode, addDeviceByImei, deleteDevice, + fetchAddResults, } from "./api"; import type { Device } from "@/types/device"; import { comfirm } from "@/utils/common"; @@ -44,12 +45,18 @@ const Devices: React.FC = () => { const [name, setName] = useState(""); const [addLoading, setAddLoading] = useState(false); + // 轮询监听相关 + const [isPolling, setIsPolling] = useState(false); + const pollingRef = useRef(null); + const loadDevicesRef = useRef<((reset?: boolean) => Promise) | null>(null); + // 删除弹窗 const [delVisible, setDelVisible] = useState(false); const [delLoading, setDelLoading] = useState(false); const navigate = useNavigate(); const { user } = useUserStore(); + // 加载设备列表 const loadDevices = useCallback( async (reset = false) => { @@ -74,6 +81,11 @@ const Devices: React.FC = () => { [loading, search, page], ); + // 更新 loadDevices 的 ref + useEffect(() => { + loadDevicesRef.current = loadDevices; + }, [loadDevices]); + // 首次加载和搜索 useEffect(() => { loadDevices(true); @@ -110,6 +122,56 @@ const Devices: React.FC = () => { return true; }); + // 开始轮询监听设备状态 + const startPolling = useCallback(() => { + if (isPolling) return; + + setIsPolling(true); + + const pollDeviceStatus = async () => { + try { + const res = await fetchAddResults({ accountId: user?.s2_accountId }); + if (res.added) { + Toast.show({ content: "设备添加成功!", position: "top" }); + setAddVisible(false); + setIsPolling(false); + if (pollingRef.current) { + clearInterval(pollingRef.current); + pollingRef.current = null; + } + // 刷新设备列表 + if (loadDevicesRef.current) { + await loadDevicesRef.current(true); + } + return; + } + } catch (error) { + console.error("轮询检查设备状态失败:", error); + } + }; + + // 每3秒检查一次设备状态 + pollingRef.current = setInterval(pollDeviceStatus, 3000); + }, [isPolling, user?.s2_accountId]); + + // 停止轮询 + const stopPolling = useCallback(() => { + setIsPolling(false); + if (pollingRef.current) { + clearInterval(pollingRef.current); + pollingRef.current = null; + } + }, []); + + // 组件卸载时清理轮询 + useEffect(() => { + return () => { + if (pollingRef.current) { + clearInterval(pollingRef.current); + } + }; + }, []); + // 获取二维码 const handleGetQr = async () => { setQrLoading(true); @@ -119,6 +181,8 @@ const Devices: React.FC = () => { if (!accountId) throw new Error("未获取到用户信息"); const res = await fetchDeviceQRCode(accountId); setQrCode(res.qrCode); + // 获取二维码后开始轮询监听 + startPolling(); } catch (e: any) { Toast.show({ content: e.message || "获取二维码失败", position: "top" }); } finally { @@ -362,7 +426,11 @@ const Devices: React.FC = () => { {/* 添加设备弹窗 */} setAddVisible(false)} + onMaskClick={() => { + setAddVisible(false); + stopPolling(); + setQrCode(null); + }} bodyStyle={{ borderTopLeftRadius: 16, borderTopRightRadius: 16, @@ -403,6 +471,13 @@ const Devices: React.FC = () => {
请用手机扫码添加设备
+ {isPolling && ( +
+ 正在监听设备添加状态... +
+ )}
)}
diff --git a/Cunkebao/src/pages/mobile/mine/recharge/buy-power/api.ts b/Cunkebao/src/pages/mobile/mine/recharge/buy-power/api.ts index 261f38dc..144ee1da 100644 --- a/Cunkebao/src/pages/mobile/mine/recharge/buy-power/api.ts +++ b/Cunkebao/src/pages/mobile/mine/recharge/buy-power/api.ts @@ -4,7 +4,7 @@ import request from "@/api/request"; export interface PowerPackage { id: number; name: string; - tokens: number; // 算力点数 + tokens: number | string; // 算力点数(可能是字符串,如"2,800") price: number; // 价格(分) originalPrice: number; // 原价(分) unitPrice: number; // 单价 @@ -13,7 +13,7 @@ export interface PowerPackage { isRecommend: number; // 是否推荐 isHot: number; // 是否热门 isVip: number; // 是否VIP - features: string[]; // 功能特性 + features?: string[]; // 功能特性(可选) description: string[]; // 描述关键词 status: number; createTime: string; diff --git a/Cunkebao/src/pages/mobile/mine/recharge/index/api.ts b/Cunkebao/src/pages/mobile/mine/recharge/index/api.ts index de13a1e0..9297d518 100644 --- a/Cunkebao/src/pages/mobile/mine/recharge/index/api.ts +++ b/Cunkebao/src/pages/mobile/mine/recharge/index/api.ts @@ -6,6 +6,9 @@ export interface Statistics { monthUsed: number; // 本月使用 remainingTokens: number; // 剩余算力 totalConsumed: number; // 总消耗 + yesterdayUsed?: number; // 昨日消耗 + historyConsumed?: number; // 历史消耗 + estimatedDays?: number; // 预计可用天数 } // 算力统计接口 export function getStatistics(): Promise { @@ -143,3 +146,56 @@ export function buyPackage(params: { id: number; price: number }) { export function buyCustomPower(params: { amount: number }) { return request("/v1/power/buy-custom", params, "POST"); } + +// 查询订单状态 +export interface QueryOrderResponse { + id: number; + mchId: number; + companyId: number; + userId: number; + orderType: number; + status: number; // 0: 待支付, 1: 已支付 + goodsId: number; + goodsName: string; + goodsSpecs: string; + money: number; + orderNo: string; + payType: number | null; + payTime: number | null; + payInfo: any; + createTime: number; +} + +export function queryOrder(orderNo: string): Promise { + return request("/v1/tokens/queryOrder", { orderNo }, "GET"); +} + +// 账号信息 +export interface Account { + id: number; + uid?: number; // 用户ID(用于分配算力) + userId?: number; // 用户ID(别名) + userName: string; + realName: string; + nickname: string; + departmentId: number; + departmentName: string; + avatar: string; +} + +// 获取账号列表 +export function getAccountList(): Promise<{ list: Account[]; total: number }> { + return request("/v1/kefu/accounts/list", undefined, "GET"); +} + +// 分配算力接口参数 +export interface AllocateTokensParams { + targetUserId: number; // 目标用户ID + tokens: number; // 分配的算力数量 + remarks?: string; // 备注 +} + +// 分配算力 +export function allocateTokens(params: AllocateTokensParams): Promise { + return request("/v1/tokens/allocate", params, "POST"); +} diff --git a/Cunkebao/src/pages/mobile/mine/recharge/index/index.module.scss b/Cunkebao/src/pages/mobile/mine/recharge/index/index.module.scss index 99d1355e..efa18cf9 100644 --- a/Cunkebao/src/pages/mobile/mine/recharge/index/index.module.scss +++ b/Cunkebao/src/pages/mobile/mine/recharge/index/index.module.scss @@ -1,359 +1,1339 @@ -// 算力管理页面样式 .powerPage { + min-height: 100vh; + background-color: #f5f5f5; + padding-bottom: 20px; +} + +.overviewSection { padding: 16px; + background-color: #f5f5f5; } -.powerTabs { - :global { - .adm-tabs-header { - background: #fff; - border-bottom: 1px solid #f0f0f0; - } - } +.overviewCard { + background: linear-gradient(135deg, #1677ff 0%, #4096ff 100%); + margin-bottom: 8px; + border-radius: 12px; + padding: 12px 20px; + position: relative; + overflow: hidden; + box-shadow: none; } -.refreshBtn { - font-size: 18px; +.overviewContent { + position: relative; + z-index: 1; +} + +.remainingPower { + margin-bottom: 6px; + display: flex; + align-items: baseline; + gap: 8px; +} + +.remainingLabel { + font-size: 12px; + color: rgba(255, 255, 255, 0.9); +} + +.remainingValue { + font-size: 28px; + font-weight: bold; + color: #ffffff; + line-height: 1.2; +} + +.totalInfo { + font-size: 12px; + color: rgba(255, 255, 255, 0.9); + margin-bottom: 8px; +} + +.progressBar { + width: 100%; + height: 6px; + background-color: rgba(255, 255, 255, 0.3); + border-radius: 3px; + overflow: hidden; + margin-bottom: 6px; +} + +.progressFill { + height: 100%; + background-color: #ffffff; + border-radius: 4px; + transition: width 0.3s ease; +} + +.usageRate { + font-size: 12px; + color: rgba(255, 255, 255, 0.9); +} + +.lightningIcon { + position: absolute; + right: 16px; + top: 50%; + transform: translateY(-50%); + font-size: 36px; + color: rgba(255, 255, 255, 0.2); +} + +.navTabs { + display: flex; + background-color: #ffffff; + border-bottom: 1px solid #f0f0f0; + padding: 0 16px; + margin-bottom: 16px; + position: sticky; + top: 0; + z-index: 100; + transition: box-shadow 0.3s ease; +} + +.navTab { + flex: 1; + text-align: center; + padding: 12px 0; + font-size: 14px; color: #666; cursor: pointer; - padding: 4px; - - &:active { - opacity: 0.6; - } + position: relative; + transition: all 0.3s; } -// ==================== 概览Tab ==================== -.overviewContent { +.navTab:not(:last-child)::after { + content: ""; + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); + width: 1px; + height: 20px; + background-color: #f0f0f0; } -.accountCards { +.navTabActive { + color: #1677ff; + font-weight: 600; + background-color: #ffffff; +} + +.navTabActive::before { + content: ""; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 2px; + background-color: #1677ff; +} + +.statsGrid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} + +.statCard { + border-radius: 12px; + padding: 10px 12px; + position: relative; + overflow: hidden; + border: none; + box-shadow: none; +} + +.statCardGreen { + background: #52c41a; + color: #ffffff; +} + +.statCardGrey { + background: #262626; + color: #ffffff; +} + +.statCardPurple { + background: #722ed1; + color: #ffffff; +} + +.statCardOrange { + background: #fa8c16; + color: #ffffff; +} + +.statIcon { + font-size: 18px; + opacity: 0.9; + margin-right: 6px; +} + +.statTitle { + font-size: 11px; + opacity: 0.9; +} + +.statTitleRow { + display: flex; + align-items: center; + margin-bottom: 6px; +} + +.statValue { + font-size: 20px; + font-weight: bold; + line-height: 1.2; +} + +.filters { display: flex; gap: 12px; + padding: 0 16px; margin-bottom: 16px; } -.balanceCard, -.powerCard { +.filterButton { flex: 1; - border-radius: 10px; - border: 1px solid #f0f0f0; + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + padding: 8px 12px; + background-color: #ffffff; + border-radius: 8px; + font-size: 14px; + color: #333; + cursor: pointer; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); } -.balanceCard { - border: 1px solid #cbdbea; - background: #edfcff; +.filterIcon { + font-size: 12px; + color: #999; } -.powerCard { - border: 1px solid #e3dbf0; - background: #f9f0ff; -} -.powerCard .iconWrapper { - background: #e9d9ff; +.recordList { + padding: 0 16px; } -.cardContent { +.recordItem { + margin-bottom: 12px; + border-radius: 12px; + padding: 16px; + background-color: #ffffff; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + cursor: pointer; + transition: all 0.2s ease; +} + +.recordItem:active { + transform: scale(0.98); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +/* 左侧区域 */ +.recordLeft { display: flex; align-items: center; gap: 12px; + flex: 1; + min-width: 0; } -.iconWrapper { - width: 48px; - height: 48px; - border-radius: 8px; +.recordIconWrapper { + width: 44px; + height: 44px; + border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; } -.balanceCard .iconWrapper { - background: #d6edff; +.recordIcon { + font-size: 22px; } -.cardIcon { - font-size: 24px; -} - -.balanceCard .cardIcon { - color: #1890ff; -} - -.powerCard .cardIcon { - color: #722ed1; -} - -.textWrapper { +.recordInfo { flex: 1; + min-width: 0; display: flex; flex-direction: column; - justify-content: center; + gap: 6px; } -.cardTitle { - font-size: 12px; - color: #666; - margin-bottom: 4px; -} - -.cardValue { - font-size: 24px; - font-weight: 600; - color: #333; -} - -.balanceCard .cardValue { - color: #1890ff; -} - -.powerCard .cardValue { - color: #722ed1; -} - -// 使用情况卡片 -.usageCard { - padding: 20px 16px; - border-radius: 12px; - margin-bottom: 16px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); -} - -.usageTitle { +.recordTitle { font-size: 16px; - font-weight: 600; - color: #222; - margin-bottom: 16px; -} - -.usageStats { - display: flex; - justify-content: space-between; -} - -.usageItem { - flex: 1; - text-align: center; -} - -.usageValue { - font-size: 20px; - font-weight: 600; - margin-bottom: 8px; -} - -.valueGreen { - color: #52c41a; -} - -.valueBlue { - color: #1890ff; -} - -.valuePurple { - color: #722ed1; -} - -.usageLabel { - font-size: 13px; - color: #888; -} - -// 快速操作卡片 -.actionCard { - padding: 20px 16px; - border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); -} - -.actionTitle { - font-size: 16px; - font-weight: 600; - color: #222; - margin-bottom: 16px; -} - -.actionButtons { - display: flex; - gap: 12px; -} - -.buyButton { - flex: 1; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - border: none; - color: #fff; - font-size: 15px; font-weight: 500; - padding: 12px; - border-radius: 8px; - display: flex; - align-items: center; - justify-content: center; - gap: 6px; - - &:active { - opacity: 0.8; - } -} - -.buttonIcon { - font-size: 16px; -} - -.recordButton { - flex: 1; - background: #fff; - border: 1px solid #e5e5e5; - color: #333; - font-size: 15px; - padding: 12px; - border-radius: 8px; - display: flex; - align-items: center; - justify-content: center; - gap: 6px; - - &:active { - background: #f5f5f5; - } -} - -// ==================== 消费记录Tab ==================== -.recordsContent { -} - -.filters { - display: flex; - gap: 12px; - margin-bottom: 16px; -} - -.filterButton { - display: flex; - align-items: center; - gap: 4px; - padding: 8px 12px; - background: #fff; - border: 1px solid #e5e5e5; - border-radius: 8px; - font-size: 14px; - color: #333; - cursor: pointer; - - span { - font-size: 12px; - } -} - -.recordList { - display: flex; - flex-direction: column; - gap: 12px; -} - -.recordItem { - padding: 16px; - border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); - border: 1px solid #f0f0f0; - cursor: pointer; - transition: - transform 0.2s ease, - box-shadow 0.2s ease; -} - -.recordItem:active { - transform: scale(0.98); -} - -.recordItem:hover { - box-shadow: 0 6px 18px rgba(22, 119, 255, 0.08); -} - -.recordHeader { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 8px; -} - -.recordLeft { - display: flex; - align-items: center; - gap: 8px; -} - -.recordType { - font-size: 16px; - font-weight: 600; - color: #222; -} - -.recordStatus { - font-size: 12px; - padding: 2px 8px; - border-radius: 10px; -} - -.recordRight { - text-align: right; -} - -.recordAmount { - font-size: 16px; - font-weight: 600; - color: #ff4d4f; - margin-bottom: 4px; -} - -.recordPower { - font-size: 12px; - color: #666; -} - -.recordDesc { - font-size: 13px; - color: #666; - margin-bottom: 6px; + line-height: 1.4; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .recordTime { font-size: 12px; color: #999; + line-height: 1.4; } -.emptyRecords { - text-align: center; - padding: 60px 20px; +/* 右侧区域 */ +.recordRight { + display: flex; + flex-direction: column; + align-items: flex-end; + justify-content: center; + flex-shrink: 0; } -.emptyIcon { - font-size: 64px; - margin-bottom: 16px; +.recordPower { + font-size: 20px; + font-weight: 700; + line-height: 1.2; + margin-bottom: 4px; } -.emptyText { - font-size: 14px; +.recordPowerUnit { + font-size: 12px; color: #999; + line-height: 1.2; } .loadingContainer { - text-align: center; - padding: 40px 20px; + display: flex; + justify-content: center; + align-items: center; + padding: 40px 0; } .loadingText { font-size: 14px; color: #999; +} + +.emptyRecords { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 20px; +} + +.emptyText { + font-size: 14px; + color: #999; margin-top: 12px; } -.paginationWrap { - padding: 15px; - background: #fff; +// 购买算力页面样式 +.buyPowerContent { + padding: 16px; +} + +.packageCard { + margin-bottom: 16px; + border-radius: 12px; + padding: 20px; + border: none; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); + background-color: #ffffff; +} + +.packageHeader { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 16px; +} + +.packageNameRow { + display: flex; + align-items: center; + gap: 8px; + flex: 1; +} + +.packageName { + font-size: 18px; + font-weight: 600; + color: #333; +} + +.discountTag { + background-color: #ff4d4f; + color: #ffffff; + font-size: 12px; + padding: 2px 8px; + border-radius: 12px; + font-weight: 500; +} + +.packageTokensHeader { + display: flex; + align-items: center; + gap: 8px; +} + +.tokensIcon { + font-size: 24px; + color: #ffd700; +} + +.tokensValue { + display: flex; + flex-direction: column; + align-items: flex-end; +} + +.tokensNumber { + font-size: 20px; + font-weight: 600; + color: #333; + line-height: 1.2; +} + +.tokensUnit { + font-size: 13px; + color: #999; + margin-top: 2px; +} + +.packagePriceSection { + margin-bottom: 20px; +} + +.priceRow { + display: flex; + align-items: baseline; + gap: 12px; + margin-bottom: 8px; +} + +.currentPrice { + font-size: 24px; + font-weight: 600; + color: #ff4d4f; +} + +.originalPrice { + font-size: 14px; + color: #999; + text-decoration: line-through; +} + +.savings { + font-size: 14px; + color: #52c41a; + margin-bottom: 8px; +} + +.unitPrice { + font-size: 13px; + color: #999; +} + +.packageFeatures { + margin-bottom: 20px; + padding-top: 16px; + border-top: 1px solid #f0f0f0; +} + +.featureItem { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; + font-size: 14px; + color: #666; + + &:last-child { + margin-bottom: 0; + } +} + +.featureIcon { + font-size: 16px; + color: #52c41a; + flex-shrink: 0; +} + +.buyButton { + margin-top: 8px; + height: 48px; + border-radius: 8px; + font-size: 16px; + font-weight: 500; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +.buyButtonIcon { + font-size: 18px; +} + +// 自定义算力包样式 +.customPackageCard { + margin-bottom: 16px; + border-radius: 12px; + padding: 24px 20px; + border: none; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); + background-color: #ffffff; +} + +.customPackageHeader { + text-align: center; + margin-bottom: 24px; +} + +.customPackageIcon { + width: 60px; + height: 60px; + border-radius: 50%; + background-color: #ffffff; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + + svg { + font-size: 32px; + color: #1677ff; + } +} + +.customPackageTitle { + font-size: 20px; + font-weight: 600; + color: #333; + margin-bottom: 8px; +} + +.customPackageDesc { + font-size: 14px; + color: #999; +} + +.quickSelectButtons { + display: flex; + gap: 12px; + margin-bottom: 16px; +} + +.quickSelectBtn { + flex: 1; + height: 44px; + border: 1px solid #e0e0e0; + border-radius: 8px; + background-color: #ffffff; + font-size: 16px; + color: #333; + cursor: pointer; + transition: all 0.3s; + + &:active { + transform: scale(0.98); + } +} + +.quickSelectBtnActive { + border-color: #1677ff; + background-color: #e6f7ff; + color: #1677ff; +} + +.customInputWrapper { + margin-bottom: 20px; +} + +.customInput { + height: 44px; + border: 1px solid #e0e0e0; + border-radius: 8px; + font-size: 16px; +} + +.customBuyButton { + height: 48px; + border-radius: 8px; + font-size: 16px; + font-weight: 500; + background: linear-gradient(135deg, #722ed1 0%, #1677ff 100%); + border: none; + color: #ffffff; + + &:disabled { + background: #d9d9d9; + color: #ffffff; + } +} + +// 安全保障承诺样式 +.securityCard { + margin-bottom: 16px; + border-radius: 12px; + padding: 20px; + border: none; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); + background-color: #ffffff; +} + +.securityHeader { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 16px; +} + +.securityIcon { + font-size: 18px; + color: #52c41a; +} + +.securityTitle { + font-size: 16px; + font-weight: 600; + color: #333; +} + +.securityList { + display: flex; + flex-direction: column; + gap: 12px; +} + +.securityItem { + display: flex; + align-items: flex-start; + gap: 8px; + font-size: 14px; + color: #666; + line-height: 1.5; +} + +.securityItemIcon { + font-size: 16px; + color: #52c41a; + flex-shrink: 0; + margin-top: 2px; +} + +// 购买记录页面样式 +.ordersContent { + padding: 16px; +} + +.searchBar { + display: flex; + align-items: center; + gap: 8px; + padding: 12px; + background-color: #ffffff; + border-radius: 8px; + margin-bottom: 12px; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); +} + +.searchIcon { + font-size: 18px; + color: #999; + flex-shrink: 0; +} + +.searchInput { + flex: 1; + border: none; + background: transparent; + font-size: 14px; +} + +.orderFilters { + display: flex; + gap: 8px; + margin-bottom: 16px; +} + +.orderList { + display: flex; + flex-direction: column; + gap: 12px; +} + +.orderCard { + border-radius: 12px; + padding: 16px; + border: none; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); + background-color: #ffffff; +} + +.orderHeader { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 12px; +} + +.orderTitle { + font-size: 16px; + font-weight: 600; + color: #333; + flex: 1; +} + +.orderAmount { + font-size: 18px; + font-weight: 600; + color: #1677ff; +} + +.orderInfo { + display: flex; + justify-content: space-between; + align-items: flex-end; +} + +.orderLeft { + flex: 1; +} + +.orderNumber { + font-size: 13px; + color: #666; + margin-bottom: 8px; +} + +.orderPayment { + display: flex; + align-items: center; + gap: 4px; + font-size: 13px; + color: #666; +} + +.paymentIcon { + font-size: 14px; +} + +.orderRight { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 8px; +} + +.orderStatus { + font-size: 12px; + padding: 4px 12px; + border-radius: 12px; + font-weight: 500; +} + +.orderTime { + font-size: 13px; + color: #999; +} + +// 算力分配页面样式 +.allocationContent { + padding: 16px; +} + +.allocationFormCard { + margin-bottom: 16px; + border-radius: 12px; + padding: 20px; + border: none; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); + background-color: #ffffff; +} + +.allocationFormHeader { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 20px; +} + +.allocationFormIcon { + font-size: 20px; + color: #1677ff; +} + +.allocationFormTitle { + font-size: 18px; + font-weight: 600; + color: #333; +} + +.allocationFormBody { + display: flex; + flex-direction: column; + gap: 16px; +} + +.formItem { + display: flex; + flex-direction: column; + gap: 8px; +} + +.formLabel { + font-size: 14px; + color: #666; + font-weight: 500; +} + +.formInput { + height: 44px; + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 0 12px; + font-size: 14px; + display: flex; + align-items: center; + justify-content: space-between; + background-color: #ffffff; + cursor: pointer; +} + +.formInputIcon { + font-size: 12px; + color: #999; +} + +.allocationConfirmButton { + margin-top: 8px; + height: 48px; + border-radius: 8px; + font-size: 16px; + font-weight: 500; +} + +.allocationRecordsCard { + border-radius: 12px; + padding: 20px; + border: none; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); + background-color: #ffffff; +} + +.allocationRecordsHeader { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 16px; + flex-wrap: wrap; +} + +.allocationRecordsIcon { + font-size: 18px; + color: #ff7a00; +} + +.allocationRecordsTitle { + font-size: 16px; + font-weight: 600; + color: #333; + flex: 1; +} + +.allocationRecordsFilters { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.allocationRecordsList { + display: flex; + flex-direction: column; + gap: 12px; +} + +.allocationRecordItem { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 0; + border-bottom: 1px solid #f0f0f0; + + &:last-child { + border-bottom: none; + } +} + +.allocationRecordLeft { + display: flex; + align-items: center; + gap: 12px; + flex: 1; +} + +.allocationRecordIcon { + font-size: 20px; + color: #1677ff; + background-color: #e6f7ff; + width: 36px; + height: 36px; + border-radius: 50%; display: flex; align-items: center; justify-content: center; } + +.allocationRecordInfo { + flex: 1; +} + +.allocationRecordName { + font-size: 15px; + font-weight: 500; + color: #333; + margin-bottom: 4px; +} + +.allocationRecordTime { + font-size: 13px; + color: #999; +} + +.allocationRecordAmount { + font-size: 16px; + font-weight: 600; + color: #333; +} + +// 支付弹框样式 +.paymentDialog { + padding: 20px; + background-color: #ffffff; +} + +.paymentHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding-bottom: 16px; + border-bottom: 1px solid #f0f0f0; +} + +.paymentTitle { + font-size: 18px; + font-weight: 600; + color: #333; +} + +.paymentClose { + font-size: 20px; + color: #999; + cursor: pointer; +} + +.paymentProduct { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 16px; + background-color: #f8f9fa; + border-radius: 8px; + margin-bottom: 20px; +} + +.paymentProductLeft { + display: flex; + align-items: flex-start; + gap: 12px; + flex: 1; +} + +.paymentProductIcon { + font-size: 24px; + color: #1677ff; + margin-top: 4px; +} + +.paymentProductInfo { + flex: 1; +} + +.paymentProductName { + font-size: 16px; + font-weight: 600; + color: #333; + margin-bottom: 8px; +} + +.paymentProductTokens { + font-size: 14px; + color: #1677ff; + margin-bottom: 4px; +} + +.paymentProductValid { + font-size: 13px; + color: #666; +} + +.paymentProductRight { + text-align: right; +} + +.paymentPrice { + font-size: 20px; + font-weight: 600; + color: #ff4d4f; + margin-bottom: 4px; +} + +.paymentDiscount { + font-size: 12px; + color: #ff4d4f; + background-color: #fff1f0; + padding: 2px 8px; + border-radius: 4px; + display: inline-block; +} + +.paymentMethods { + display: flex; + gap: 12px; + margin-bottom: 20px; +} + +.paymentMethod { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 12px; + border: 1px solid #e0e0e0; + border-radius: 8px; + background-color: #ffffff; + cursor: pointer; + transition: all 0.3s; +} + +.paymentMethodActive { + border-color: #1677ff; + background-color: #e6f7ff; +} + +.paymentMethodIcon { + font-size: 20px; +} + +.paymentQrCode { + text-align: center; + margin-bottom: 20px; +} + +.qrCodeImage { + width: 250px; + height: 250px; + margin: 0 auto 16px; + display: block; + border-radius: 8px; +} + +.qrCodeHint { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + font-size: 14px; + color: #666; +} + +.qrCodeHintIcon { + font-size: 16px; + color: #52c41a; +} + +.paymentInfo { + margin-bottom: 20px; + padding: 16px; + background-color: #f8f9fa; + border-radius: 8px; +} + +.paymentInfoItem { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; + font-size: 14px; + + &:last-child { + margin-bottom: 0; + } +} + +.paymentInfoIcon { + font-size: 16px; + color: #1677ff; +} + +.paymentInfoLabel { + color: #666; + flex: 1; +} + +.paymentInfoValue { + color: #1677ff; + font-weight: 500; +} + +.paymentOrderNo { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + justify-content: flex-end; +} + +.orderNoText { + color: #333; + font-family: monospace; + font-size: 13px; +} + +.copyIcon { + font-size: 16px; + color: #1677ff; + cursor: pointer; +} + +.completePaymentButton { + margin-bottom: 20px; + height: 48px; + border-radius: 8px; + font-size: 16px; + font-weight: 500; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +.completePaymentIcon { + font-size: 18px; +} + +.paymentInstructions { + padding-top: 16px; + border-top: 1px solid #f0f0f0; +} + +.paymentInstructionsTitle { + display: flex; + align-items: center; + gap: 8px; + font-size: 15px; + font-weight: 600; + color: #333; + margin-bottom: 12px; +} + +.instructionsIcon { + font-size: 16px; + color: #faad14; +} + +.paymentInstructionsList { + display: flex; + flex-direction: column; + gap: 10px; +} + +.instructionItem { + display: flex; + align-items: flex-start; + gap: 8px; + font-size: 13px; + color: #666; + line-height: 1.5; +} + +.instructionIcon { + font-size: 14px; + color: #52c41a; + flex-shrink: 0; + margin-top: 2px; +} + +// 支付成功弹框样式 +.paymentSuccessDialog { + padding: 20px; + background-color: #ffffff; +} + +.paymentSuccessHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; + padding-bottom: 16px; + border-bottom: 1px solid #f0f0f0; +} + +.paymentSuccessTitle { + font-size: 18px; + font-weight: 600; + color: #333; +} + +.paymentSuccessClose { + font-size: 20px; + color: #999; + cursor: pointer; +} + +.paymentSuccessIcon { + width: 80px; + height: 80px; + border-radius: 50%; + background-color: #f6ffed; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 16px; + + svg { + font-size: 48px; + color: #52c41a; + } +} + +.paymentSuccessText { + font-size: 20px; + font-weight: 600; + color: #333; + text-align: center; + margin-bottom: 8px; +} + +.paymentSuccessDesc { + font-size: 14px; + color: #666; + text-align: center; + margin-bottom: 24px; +} + +.paymentSuccessProduct { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 16px; + background-color: #f8f9fa; + border-radius: 8px; + margin-bottom: 20px; +} + +.paymentSuccessProductLeft { + display: flex; + align-items: flex-start; + gap: 12px; + flex: 1; +} + +.paymentSuccessProductIcon { + font-size: 24px; + color: #1677ff; + margin-top: 4px; +} + +.paymentSuccessProductInfo { + flex: 1; +} + +.paymentSuccessProductName { + font-size: 16px; + font-weight: 600; + color: #333; + margin-bottom: 8px; +} + +.paymentSuccessProductTokens { + font-size: 14px; + color: #1677ff; +} + +.paymentSuccessProductRight { + text-align: right; +} + +.paymentSuccessPrice { + font-size: 18px; + font-weight: 600; + color: #ff4d4f; + margin-bottom: 8px; +} + +.paymentSuccessPayType { + font-size: 13px; + color: #666; +} + +.paymentSuccessInfo { + margin-bottom: 24px; + padding: 16px; + background-color: #f8f9fa; + border-radius: 8px; +} + +.paymentSuccessInfoItem { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + font-size: 14px; + + &:last-child { + margin-bottom: 0; + } +} + +.paymentSuccessInfoLabel { + color: #666; +} + +.paymentSuccessInfoValue { + color: #333; + font-weight: 500; +} + +.paymentSuccessOrderNo { + display: flex; + align-items: center; + gap: 8px; +} + +.successOrderNoText { + color: #333; + font-family: monospace; + font-size: 13px; +} + +.successCopyIcon { + font-size: 16px; + color: #1677ff; + cursor: pointer; +} + +.paymentSuccessButton { + height: 48px; + border-radius: 8px; + font-size: 16px; + font-weight: 500; +} diff --git a/Cunkebao/src/pages/mobile/mine/recharge/index/index.tsx b/Cunkebao/src/pages/mobile/mine/recharge/index/index.tsx index 3f52a632..02f65703 100644 --- a/Cunkebao/src/pages/mobile/mine/recharge/index/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/recharge/index/index.tsx @@ -1,136 +1,262 @@ import React, { useState, useEffect } from "react"; -import { useNavigate } from "react-router-dom"; -import { Card, Button, Toast, Tabs, Tag, Picker } from "antd-mobile"; +import { Card, Toast, Picker, InfiniteScroll } from "antd-mobile"; import style from "./index.module.scss"; import { - SyncOutlined, - ShoppingCartOutlined, - HistoryOutlined, + ThunderboltOutlined, LineChartOutlined, + BarChartOutlined, + HistoryOutlined, + CalendarOutlined, DownOutlined, + CheckCircleOutlined, + SafetyOutlined, + AimOutlined, + SearchOutlined, + WechatOutlined, + AlipayCircleOutlined, + DesktopOutlined, + UserOutlined, + CloseOutlined, + ClockCircleOutlined, + CopyOutlined, + CheckOutlined, + ExclamationCircleOutlined, } from "@ant-design/icons"; +import { Button, Dialog, Input, Popup } from "antd-mobile"; import NavCommon from "@/components/NavCommon"; import Layout from "@/components/Layout/Layout"; -import { getStatistics, getOrderList } from "./api"; +import { useUserStore } from "@/store"; +import { getStatistics, getOrderList, queryOrder, getAccountList, allocateTokens } from "./api"; +import type { QueryOrderResponse, Account } from "./api"; +import { getTokensUseRecord } from "../usage-records/api"; +import { getTaocanList, buyPackage } from "../buy-power/api"; import type { Statistics, OrderList } from "./api"; -import { Pagination } from "antd"; +import type { TokensUseRecordItem } from "../usage-records/api"; +import type { PowerPackage } from "../buy-power/api"; -type TagColor = NonNullable["color"]>; - -type GoodsSpecs = - | { - id: number; - name: string; - price: number; - tokens: number; - } - | undefined; - -const parseGoodsSpecs = (value: OrderList["goodsSpecs"]): GoodsSpecs => { - if (!value) return undefined; - if (typeof value === "string") { - try { - return JSON.parse(value); - } catch (error) { - console.warn("解析 goodsSpecs 失败:", error, value); - return undefined; - } - } - return value; -}; - -const formatTimestamp = (value?: number | string | null) => { +// 格式化日期时间:2025/2/22 17:02 +const formatDateTime = (value?: string | number | null) => { if (value === undefined || value === null) return ""; if (typeof value === "string" && value.trim() === "") return ""; - const numericValue = - typeof value === "number" ? value : Number.parseFloat(value); - - if (Number.isNaN(numericValue)) { - return String(value); + let date: Date; + if (typeof value === "string") { + date = new Date(value); + } else { + const numericValue = Number(value); + if (Number.isNaN(numericValue)) { + return String(value); + } + const timestamp = + numericValue > 1e12 + ? numericValue + : numericValue > 1e10 + ? numericValue + : numericValue * 1000; + date = new Date(timestamp); } - const timestamp = - numericValue > 1e12 - ? numericValue - : numericValue > 1e10 - ? numericValue - : numericValue * 1000; - - const date = new Date(timestamp); if (Number.isNaN(date.getTime())) { return String(value); } - return date.toLocaleString("zh-CN", { - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }); + + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const day = date.getDate(); + const hour = String(date.getHours()).padStart(2, "0"); + const minute = String(date.getMinutes()).padStart(2, "0"); + + return `${year}/${month}/${day} ${hour}:${minute}`; }; -const centsToYuan = (value?: number | string | null) => { - if (value === undefined || value === null) return 0; - if (typeof value === "string" && value.trim() === "") return 0; +// 格式化数值,超过1000用K,超过10000用W,保留1位小数 +const formatNumber = (value: number | undefined): string => { + if (value === undefined || value === null) return "0"; const num = Number(value); - if (!Number.isFinite(num)) return 0; - if (Number.isInteger(num)) { - return num / 100; + if (isNaN(num)) return "0"; + + if (num >= 10000) { + const w = num / 10000; + return w % 1 === 0 ? `${w}w` : `${w.toFixed(1)}w`; + } else if (num >= 1000) { + const k = num / 1000; + return k % 1 === 0 ? `${k}k` : `${k.toFixed(1)}k`; } - return num; + return num.toFixed(1); +}; + +// 类型映射:form字段到显示名称(数据库字段映射) +const formLabelMap: Record = { + 0: "未知", + 1: "点赞", + 2: "朋友圈同步", + 3: "朋友圈发布", + 4: "群发微信", + 5: "群发群消息", + 6: "群发群公告", + 7: "海报获客", + 8: "订单获客", + 9: "电话获客", + 10: "微信群获客", + 11: "API获客", + 12: "AI改写", + 13: "AI客服", + 14: "生成群公告", + 1001: "商家", + 1002: "充值", + 1003: "系统", +}; + +// 根据form字段获取类型名称,优先显示remarks +const getRecordTitle = (item: TokensUseRecordItem): string => { + // 优先显示remarks,如果没有则显示类型名称 + if (item.remarks && item.remarks.trim()) { + return item.remarks; + } + return formLabelMap[item.form] || "使用记录"; }; const PowerManagement: React.FC = () => { - const navigate = useNavigate(); - const [activeTab, setActiveTab] = useState("overview"); + const user = useUserStore(state => state.user); + const isAdmin = user?.isAdmin === 1; // 判断是否为管理员 + const [activeTab, setActiveTab] = useState("details"); // details, buy, orders, allocation const [loading, setLoading] = useState(false); const [stats, setStats] = useState(null); - const [records, setRecords] = useState([]); + const [records, setRecords] = useState([]); + const [recordPage, setRecordPage] = useState(1); + const [recordTotal, setRecordTotal] = useState(0); + const [recordHasMore, setRecordHasMore] = useState(true); + const [recordLoadingMore, setRecordLoadingMore] = useState(false); + const [orderPage, setOrderPage] = useState(1); + const [orderTotal, setOrderTotal] = useState(0); + const [orderHasMore, setOrderHasMore] = useState(true); + const [orderLoadingMore, setOrderLoadingMore] = useState(false); + const [packages, setPackages] = useState([]); + const [buyLoading, setBuyLoading] = useState(false); + const [customAmount, setCustomAmount] = useState(""); + const [selectedQuickAmount, setSelectedQuickAmount] = useState(null); + const [orders, setOrders] = useState([]); + const [orderKeyword, setOrderKeyword] = useState(""); + const [orderStatus, setOrderStatus] = useState("all"); + const [orderTime, setOrderTime] = useState("7days"); + const [orderPayType, setOrderPayType] = useState("all"); + const [orderStatusVisible, setOrderStatusVisible] = useState(false); + const [orderTimeVisible, setOrderTimeVisible] = useState(false); + const [orderPayTypeVisible, setOrderPayTypeVisible] = useState(false); + const [allocationAccount, setAllocationAccount] = useState(null); + const [allocationAmount, setAllocationAmount] = useState(""); + const [allocationAccountVisible, setAllocationAccountVisible] = useState(false); + const [allocationRecords, setAllocationRecords] = useState([]); + const [allocationPage, setAllocationPage] = useState(1); + const [allocationTotal, setAllocationTotal] = useState(0); + const [allocationHasMore, setAllocationHasMore] = useState(true); + const [allocationLoadingMore, setAllocationLoadingMore] = useState(false); + const [allocationAccountFilter, setAllocationAccountFilter] = useState("all"); + const [allocationTimeFilter, setAllocationTimeFilter] = useState("7days"); + const [allocationAccountFilterVisible, setAllocationAccountFilterVisible] = useState(false); + const [allocationTimeFilterVisible, setAllocationTimeFilterVisible] = useState(false); + const [accounts, setAccounts] = useState([]); + const [paymentDialogVisible, setPaymentDialogVisible] = useState(false); + const [paymentData, setPaymentData] = useState<{ + packageName: string; + tokens: string; + price: number; + originalPrice?: number; + discount?: number; + codeUrl: string; + orderNo: string; + payType: number; // 1: 微信, 2: 支付宝 + } | null>(null); + const [countdown, setCountdown] = useState(300); // 5分钟倒计时(秒) + const [expireTime, setExpireTime] = useState(null); + const [paymentSuccessVisible, setPaymentSuccessVisible] = useState(false); + const [paymentSuccessData, setPaymentSuccessData] = useState(null); + const [pollingTimer, setPollingTimer] = useState(null); + + // 筛选器状态 const [filterType, setFilterType] = useState("all"); - const [filterStatus, setFilterStatus] = useState("all"); + const [filterAction, setFilterAction] = useState("all"); + const [filterTime, setFilterTime] = useState("all"); const [filterTypeVisible, setFilterTypeVisible] = useState(false); - const [filterStatusVisible, setFilterStatusVisible] = useState(false); - const [page, setPage] = useState(1); - const [pageSize] = useState(10); - const [total, setTotal] = useState(0); + const [filterActionVisible, setFilterActionVisible] = useState(false); + const [filterTimeVisible, setFilterTimeVisible] = useState(false); const typeOptions = [ { label: "全部类型", value: "all" }, - { label: "AI分析", value: "ai_analysis" }, - { label: "内容生成", value: "content_gen" }, - { label: "数据训练", value: "data_train" }, - { label: "智能推荐", value: "smart_rec" }, - { label: "语音识别", value: "voice_rec" }, + { label: "点赞", value: "1" }, + { label: "朋友圈同步", value: "2" }, + { label: "朋友圈发布", value: "3" }, + { label: "群发微信", value: "4" }, + { label: "群发群消息", value: "5" }, + { label: "群发群公告", value: "6" }, + { label: "海报获客", value: "7" }, + { label: "订单获客", value: "8" }, + { label: "电话获客", value: "9" }, + { label: "微信群获客", value: "10" }, + { label: "API获客", value: "11" }, + { label: "AI改写", value: "12" }, + { label: "AI客服", value: "13" }, + { label: "生成群公告", value: "14" }, + { label: "商家", value: "1001" }, + { label: "充值", value: "1002" }, + { label: "系统", value: "1003" }, ]; - const statusOptions = [ - { label: "全部状态", value: "all" }, - { label: "待支付", value: "pending", requestValue: "0" }, - { label: "已支付", value: "paid", requestValue: "1" }, - { label: "已取消", value: "cancelled", requestValue: "2" }, - { label: "已退款", value: "refunded", requestValue: "3" }, + const actionOptions = [ + { label: "全部行为", value: "all" }, + { label: "消耗", value: "consume" }, + { label: "充值", value: "recharge" }, ]; - const statusMeta: Record = { - 0: { label: "待支付", color: "warning" }, - 1: { label: "已支付", color: "success" }, - 2: { label: "已取消", color: "default" }, - 3: { label: "已退款", color: "primary" }, - }; + const timeOptions = [ + { label: "最近7天", value: "7days" }, + { label: "最近30天", value: "30days" }, + { label: "最近90天", value: "90days" }, + { label: "全部时间", value: "all" }, + ]; + + // 导航标签数据(根据管理员权限过滤) + const navTabs = [ + { key: "details", label: "算力明细" }, + { key: "buy", label: "购买算力" }, + { key: "orders", label: "购买记录" }, + // 只有管理员才能看到算力分配 + ...(isAdmin ? [{ key: "allocation", label: "算力分配" }] : []), + ]; useEffect(() => { fetchStats(); }, []); + useEffect(() => { - if (activeTab === "records") { - setPage(1); - fetchRecords(1); + // 如果非管理员尝试访问分配页面,自动跳转到明细页面 + if (activeTab === "allocation" && !isAdmin) { + setActiveTab("details"); + return; + } + + if (activeTab === "details") { + // 筛选条件变化时重置 + setRecordPage(1); + setRecordHasMore(true); + fetchRecords(1, false); + } else if (activeTab === "buy") { + fetchPackages(); + } else if (activeTab === "orders") { + // 筛选条件变化时重置 + setOrderPage(1); + setOrderHasMore(true); + fetchOrders(1, false); + } else if (activeTab === "allocation" && isAdmin) { + fetchAccounts(); + // 重置分配记录 + setAllocationPage(1); + setAllocationHasMore(true); + fetchAllocationRecords(1, false); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [activeTab, filterType, filterStatus]); + }, [activeTab, filterType, filterAction, filterTime, orderStatus, orderTime, orderPayType, orderKeyword, isAdmin]); const fetchStats = async () => { try { @@ -142,303 +268,1663 @@ const PowerManagement: React.FC = () => { } }; - const fetchRecords = async (customPage?: number) => { + const fetchRecords = async (page: number = 1, append: boolean = false) => { + if (append) { + setRecordLoadingMore(true); + } else { + setLoading(true); + } + try { + // 根据筛选条件构建参数 + const params: any = { + page: String(page), + limit: "10", + type: filterAction === "consume" ? "0" : filterAction === "recharge" ? "1" : undefined, + }; + + // 类型筛选:根据form字段筛选 + if (filterType !== "all") { + params.form = filterType; + } + + // 时间筛选(转换为 startTime 和 endTime,只保留日期) + if (filterTime !== "all") { + const now = new Date(); + const daysMap: Record = { + "7days": 7, + "30days": 30, + "90days": 90, + }; + const days = daysMap[filterTime] || 7; + const startTime = new Date(now.getTime() - days * 24 * 60 * 60 * 1000); + params.startTime = startTime.toISOString().slice(0, 10); // YYYY-MM-DD + params.endTime = now.toISOString().slice(0, 10); // YYYY-MM-DD + } + + const res = await getTokensUseRecord(params); + console.log("接口返回数据:", res); + + // 处理返回数据:request拦截器会返回 res.data.data,所以直接使用 res.list + let list = Array.isArray(res?.list) ? res.list : []; + const total = res?.total || 0; + + console.log("处理后的列表:", list, "列表长度:", list.length, "总数:", total); + + if (append) { + // 追加数据 + setRecords(prev => [...prev, ...list]); + } else { + // 替换数据 + setRecords(list); + } + + setRecordTotal(total); + // 判断是否还有更多数据 + const hasMore = records.length + list.length < total; + setRecordHasMore(hasMore); + } catch (error) { + console.error("获取使用记录失败:", error); + Toast.show({ content: "获取使用记录失败", position: "top" }); + } finally { + if (append) { + setRecordLoadingMore(false); + } else { + setLoading(false); + } + } + }; + + const fetchPackages = async () => { setLoading(true); try { - const reqPage = customPage !== undefined ? customPage : page; - const statusRequestValue = statusOptions.find( - opt => opt.value === filterStatus, - )?.requestValue; - const res = await getOrderList({ - page: String(reqPage), - limit: String(pageSize), - orderType: "1", - status: statusRequestValue, - }); - setRecords(res.list || []); - setTotal(Number(res.total || 0)); + const res = await getTaocanList(); + setPackages(res.list || []); } catch (error) { - console.error("获取订单记录失败:", error); - Toast.show({ content: "获取订单记录失败", position: "top" }); + console.error("获取套餐列表失败:", error); + Toast.show({ content: "获取套餐列表失败", position: "top" }); } finally { setLoading(false); } }; - const handleRefresh = () => { - if (loading) return; - fetchStats(); - if (activeTab === "records") { - fetchRecords(); + const fetchOrders = async (page: number = 1, append: boolean = false) => { + if (append) { + setOrderLoadingMore(true); + } else { + setLoading(true); + } + try { + const params: any = { + page: String(page), + limit: "10", + orderType: "1", // 算力充值 + }; + + // 关键词搜索(订单号) + if (orderKeyword) { + params.keyword = orderKeyword; + } + + // 订单状态筛选 + if (orderStatus !== "all") { + params.status = orderStatus; + } + + // 支付方式筛选 + if (orderPayType !== "all") { + params.payType = orderPayType; + } + + // 时间筛选(转换为 startTime 和 endTime,只保留日期) + if (orderTime !== "all") { + const now = new Date(); + const daysMap: Record = { + "7days": 7, + "30days": 30, + "90days": 90, + }; + const days = daysMap[orderTime] || 7; + const startTime = new Date(now.getTime() - days * 24 * 60 * 60 * 1000); + params.startTime = startTime.toISOString().slice(0, 10); // YYYY-MM-DD + params.endTime = now.toISOString().slice(0, 10); // YYYY-MM-DD + } + + const res = await getOrderList(params); + const list = res.list || []; + const total = res.total || 0; + + if (append) { + // 追加数据 + setOrders(prev => [...prev, ...list]); + } else { + // 替换数据 + setOrders(list); + } + + setOrderTotal(total); + // 判断是否还有更多数据 + if (append) { + const hasMore = orders.length + list.length < total; + setOrderHasMore(hasMore); + } else { + const hasMore = list.length < total; + setOrderHasMore(hasMore); + } + } catch (error) { + console.error("获取订单列表失败:", error); + Toast.show({ content: "获取订单列表失败", position: "top" }); + } finally { + if (append) { + setOrderLoadingMore(false); + } else { + setLoading(false); + } } }; - const handleBuyPower = () => { - navigate("/recharge/buy-power"); - }; + const handleBuyPackage = async (pkg: PowerPackage) => { + setBuyLoading(true); + try { + const res = await buyPackage({ + id: pkg.id, + price: pkg.price, + }); - const handleViewRecords = () => { - navigate("/recharge/usage-records"); - }; + if (res?.code_url) { + // 显示新版支付弹框 + const tokensValue = formatTokens(pkg.tokens); + const savings = getSavings(pkg); + const discount = savings > 0 && pkg.originalPrice + ? Math.round((savings / pkg.originalPrice) * 100) + : pkg.discount || 0; - const getTypeLabel = () => { - return ( - typeOptions.find(opt => opt.value === filterType)?.label || "全部类型" - ); - }; - - const getStatusLabel = () => { - return ( - statusOptions.find(opt => opt.value === filterStatus)?.label || "全部状态" - ); - }; - - // 格式化数值:超过1000用k,超过10000用w,保留1位小数 - const formatNumber = (value: number | undefined): string => { - if (value === undefined || value === null) return "0"; - const num = Number(value); - if (isNaN(num)) return "0"; - - if (num >= 10000) { - const w = num / 10000; - return w % 1 === 0 ? `${w}w` : `${w.toFixed(1)}w`; - } else if (num >= 1000) { - const k = num / 1000; - return k % 1 === 0 ? `${k}k` : `${k.toFixed(1)}k`; + setPaymentData({ + packageName: pkg.name, + tokens: tokensValue, + price: pkg.price / 100, + originalPrice: pkg.originalPrice ? pkg.originalPrice / 100 : undefined, + discount: discount, + codeUrl: res.code_url, + orderNo: res.orderNo || res.order_no || `ORDER${Date.now()}`, + payType: 1, // 默认微信支付 + }); + setCountdown(300); // 5分钟倒计时 + const expire = new Date(); + expire.setMinutes(expire.getMinutes() + 5); + setExpireTime(expire); + setPaymentDialogVisible(true); + } + } catch (error) { + console.error("购买失败:", error); + Toast.show({ content: "购买失败,请重试", position: "top" }); + } finally { + setBuyLoading(false); } - return String(num); }; - // 渲染概览Tab - const renderOverview = () => ( -
- {/* 账户信息卡片 */} -
- -
-
- -
-
-
总算力
-
- {formatNumber(stats?.totalTokens)} -
-
-
-
-
+ // 格式化算力值 + const formatTokens = (tokens: number | string): string => { + if (typeof tokens === "string") { + return tokens; + } + return tokens.toLocaleString(); + }; - {/* 使用情况卡片 */} - -
使用情况
-
-
-
- {formatNumber(stats?.todayUsed)} -
-
今日使用
-
-
-
- {formatNumber(stats?.monthUsed)} -
-
本月使用
-
-
-
- {formatNumber(stats?.remainingTokens)} -
-
剩余算力
-
-
-
+ // 计算节省金额 + const getSavings = (pkg: PowerPackage): number => { + if (pkg.originalPrice && pkg.originalPrice > pkg.price) { + return pkg.originalPrice - pkg.price; + } + return 0; + }; - {/* 快速操作 */} - -
快速操作
-
- - -
-
-
- ); + // 处理快速选择金额 + const handleQuickSelect = (amount: number) => { + setSelectedQuickAmount(amount); + setCustomAmount(amount.toString()); + }; - // 渲染订单记录Tab - const renderRecords = () => ( -
- {/* 筛选器 */} -
- setFilterTypeVisible(false)} - value={[filterType]} - onConfirm={value => { - setFilterType(value[0] as string); - setFilterTypeVisible(false); - }} - > - {() => ( -
setFilterTypeVisible(true)} - > - - {getTypeLabel()} -
- )} -
+ // 处理自定义金额输入(只允许整数) + const handleCustomAmountChange = (value: string) => { + // 只允许输入数字,自动去除小数点 + const intValue = value.replace(/[^\d]/g, ''); + setCustomAmount(intValue); + setSelectedQuickAmount(null); + }; - setFilterStatusVisible(false)} - value={[filterStatus]} - onConfirm={value => { - setFilterStatus(value[0] as string); - setFilterStatusVisible(false); - }} - > - {() => ( -
setFilterStatusVisible(true)} - > - - {getStatusLabel()} -
- )} -
-
+ // 购买自定义金额 + const handleBuyCustom = async () => { + if (!customAmount || !customAmount.trim()) { + Toast.show({ content: "请输入购买金额", position: "top" }); + return; + } - {/* 订单记录列表 */} -
- {loading && records.length === 0 ? ( -
-
加载中...
-
- ) : records.length > 0 ? ( - records.map(record => { - const statusCode = - record.status !== undefined ? Number(record.status) : undefined; - const tagColor = - statusCode !== undefined - ? statusMeta[statusCode]?.color || "default" - : "default"; - const tagLabel = - record.statusText || - (statusCode !== undefined - ? statusMeta[statusCode]?.label || "未知状态" - : "未知状态"); - const goodsSpecs = parseGoodsSpecs(record.goodsSpecs); - const amount = centsToYuan(record.money); - const powerValue = Number(goodsSpecs?.tokens ?? record.tokens ?? 0); - const power = Number.isNaN(powerValue) ? 0 : powerValue; - const description = - record.orderTypeText || - goodsSpecs?.name || - record.goodsName || - ""; - const createTime = formatTimestamp(record.createTime); + const amount = parseInt(customAmount); + if (isNaN(amount) || amount < 1) { + Toast.show({ content: "请输入有效的金额1元", position: "top" }); + return; + } - return ( - - record.orderNo && - navigate(`/recharge/order/${record.orderNo}`) - } - > -
-
-
- {record.goodsName || "算力充值"} -
- - {tagLabel} - -
-
-
- -¥{amount.toFixed(2)} -
-
- {formatNumber(power)} 算力 -
-
-
-
{description}
-
{createTime}
-
- ); - }) - ) : ( -
-
📋
-
暂无订单记录
-
- )} -
-
- ); + setBuyLoading(true); + try { + // 使用自定义购买接口,传入金额(元) + const res = await buyPackage({ + price: amount, // 金额传元,不是分 + }); + + if (res?.code_url) { + // 显示新版支付弹框 + setPaymentData({ + packageName: "自定义购买算力", + tokens: "待计算", // 自定义购买时算力点数由后端返回 + price: amount, + codeUrl: res.code_url, + orderNo: res.orderNo || res.order_no || `CUSTOM${Date.now()}`, + payType: 1, // 默认微信支付 + }); + setCountdown(300); // 5分钟倒计时 + const expire = new Date(); + expire.setMinutes(expire.getMinutes() + 5); + setExpireTime(expire); + setPaymentDialogVisible(true); + } + } catch (error) { + console.error("购买失败:", error); + Toast.show({ content: "购买失败,请重试", position: "top" }); + } finally { + setBuyLoading(false); + } + }; + + // 计算使用率 + const getUsageRate = (): number => { + if (!stats || !stats.totalTokens || stats.totalTokens === 0) return 0; + const used = stats.totalTokens - stats.remainingTokens; + return Math.round((used / stats.totalTokens) * 100); + }; + + // 获取预计可用天数(直接使用接口返回的字段) + const getEstimatedDays = (): number => { + return stats?.estimatedDays || 0; + }; + + const getTypeLabel = () => + typeOptions.find(opt => opt.value === filterType)?.label || "全部"; + const getActionLabel = () => + actionOptions.find(opt => opt.value === filterAction)?.label || "全部"; + const getTimeLabel = () => + timeOptions.find(opt => opt.value === filterTime)?.label || "最近7天"; + + // 订单筛选选项 + const orderStatusOptions = [ + { label: "全部状态", value: "all" }, + { label: "待支付", value: "0" }, + { label: "已支付", value: "1" }, + { label: "已取消", value: "2" }, + { label: "已退款", value: "3" }, + ]; + + const orderTimeOptions = [ + { label: "最近7天", value: "7days" }, + { label: "最近30天", value: "30days" }, + { label: "最近90天", value: "90days" }, + { label: "全部", value: "all" }, + ]; + + const orderPayTypeOptions = [ + { label: "全部方式", value: "all" }, + { label: "微信支付", value: "1" }, + { label: "支付宝", value: "2" }, + ]; + + const getOrderStatusLabel = () => + orderStatusOptions.find(opt => opt.value === orderStatus)?.label || "全部状态"; + const getOrderTimeLabel = () => + orderTimeOptions.find(opt => opt.value === orderTime)?.label || "最近7天"; + const getOrderPayTypeLabel = () => + orderPayTypeOptions.find(opt => opt.value === orderPayType)?.label || "全部方式"; + + // 获取支付方式文本 + const getPayTypeText = (payType?: number): string => { + switch (payType) { + case 1: + return "微信支付"; + case 2: + return "支付宝支付"; + default: + return "未知"; + } + }; + + // 获取状态文本和样式 + const getOrderStatusInfo = (status?: number) => { + switch (status) { + case 0: + return { text: "待支付", color: "#faad14", bgColor: "#fffbe6" }; + case 1: + return { text: "已支付", color: "#52c41a", bgColor: "#f6ffed" }; + case 2: + return { text: "已取消", color: "#999999", bgColor: "#f5f5f5" }; + case 3: + return { text: "已退款", color: "#1890ff", bgColor: "#e6f7ff" }; + default: + return { text: "未知", color: "#666", bgColor: "#f5f5f5" }; + } + }; + + // 获取账号列表 + const fetchAccounts = async () => { + try { + const res = await getAccountList(); + setAccounts(res.list || []); + } catch (error) { + console.error("获取账号列表失败:", error); + Toast.show({ content: "获取账号列表失败", position: "top" }); + } + }; + + // 获取账号显示名称 + const getAccountDisplayName = (account: Account) => { + const parts: string[] = []; + if (account.realName) { + parts.push(account.realName); + } + if (account.userName) { + parts.push(account.userName); + } + if (parts.length > 0) { + return parts.join(" - "); + } + return account.nickname || `账号${account.id}`; + }; + + // 获取分配记录(form=1001的算力明细记录) + const fetchAllocationRecords = async (page: number = 1, append: boolean = false) => { + if (append) { + setAllocationLoadingMore(true); + } else { + setLoading(true); + } + try { + const params: any = { + page: String(page), + limit: "10", + form: "1001", // 商家类型 + }; + + // 账号筛选 + if (allocationAccountFilter !== "all") { + // 这里需要根据账号ID筛选,可能需要后端支持 + // 暂时先不处理,等后端接口支持 + } + + // 时间筛选 + if (allocationTimeFilter !== "all") { + const now = new Date(); + const daysMap: Record = { + "7days": 7, + "30days": 30, + "90days": 90, + }; + const days = daysMap[allocationTimeFilter] || 7; + const startTime = new Date(now.getTime() - days * 24 * 60 * 60 * 1000); + params.startTime = startTime.toISOString().slice(0, 10); + params.endTime = now.toISOString().slice(0, 10); + } + + const res = await getTokensUseRecord(params); + let list = Array.isArray(res?.list) ? res.list : []; + const total = res?.total || 0; + + if (append) { + setAllocationRecords(prev => [...prev, ...list]); + } else { + setAllocationRecords(list); + } + + setAllocationTotal(total); + if (append) { + const hasMore = allocationRecords.length + list.length < total; + setAllocationHasMore(hasMore); + } else { + const hasMore = list.length < total; + setAllocationHasMore(hasMore); + } + } catch (error) { + console.error("获取分配记录失败:", error); + Toast.show({ content: "获取分配记录失败", position: "top" }); + } finally { + if (append) { + setAllocationLoadingMore(false); + } else { + setLoading(false); + } + } + }; + + // 处理确认分配 + const handleConfirmAllocation = async () => { + if (!allocationAccount) { + Toast.show({ content: "请选择账号", position: "top" }); + return; + } + if (!allocationAmount || !allocationAmount.trim()) { + Toast.show({ content: "请输入分配算力数量", position: "top" }); + return; + } + const amount = parseInt(allocationAmount); + if (isNaN(amount) || amount <= 0) { + Toast.show({ content: "请输入有效的算力数量", position: "top" }); + return; + } + + setLoading(true); + try { + // 调用分配接口 + await allocateTokens({ + targetUserId: allocationAccount.uid || allocationAccount.userId || allocationAccount.id, + tokens: amount, + remarks: `分配给${getAccountDisplayName(allocationAccount)}`, + }); + + Toast.show({ content: "分配成功", position: "top" }); + setAllocationAccount(null); + setAllocationAmount(""); + + // 刷新统计数据 + fetchStats(); + + // 重新加载记录 + setAllocationPage(1); + setAllocationHasMore(true); + fetchAllocationRecords(1, false); + } catch (error) { + console.error("分配失败:", error); + Toast.show({ content: "分配失败,请重试", position: "top" }); + } finally { + setLoading(false); + } + }; + + // 分配记录筛选选项 + const allocationAccountOptions = [ + { label: "全部账号", value: "all" }, + ...accounts.map(acc => ({ label: getAccountDisplayName(acc), value: (acc.uid || acc.userId || acc.id).toString() })), + ]; + + const allocationTimeOptions = [ + { label: "最近7天", value: "7days" }, + { label: "最近30天", value: "30days" }, + { label: "最近90天", value: "90days" }, + { label: "全部", value: "all" }, + ]; + + // 倒计时效果 + useEffect(() => { + if (paymentDialogVisible && countdown > 0) { + const timer = setInterval(() => { + setCountdown(prev => { + if (prev <= 1) { + clearInterval(timer); + return 0; + } + return prev - 1; + }); + }, 1000); + return () => clearInterval(timer); + } + }, [paymentDialogVisible, countdown]); + + // 格式化倒计时时间 + const formatCountdown = (seconds: number): string => { + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`; + }; + + // 格式化过期时间 + const formatExpireTime = (date: Date | null): string => { + if (!date) return ""; + const month = date.getMonth() + 1; + const day = date.getDate(); + const hour = String(date.getHours()).padStart(2, "0"); + const minute = String(date.getMinutes()).padStart(2, "0"); + return `${month}/${day} ${hour}:${minute}`; + }; + + // 复制订单号 + const handleCopyOrderNo = async () => { + if (!paymentData?.orderNo) return; + try { + await navigator.clipboard.writeText(paymentData.orderNo); + Toast.show({ content: "订单号已复制", position: "top" }); + } catch (error) { + // 降级方案 + const textarea = document.createElement("textarea"); + textarea.value = paymentData.orderNo; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand("copy"); + document.body.removeChild(textarea); + Toast.show({ content: "订单号已复制", position: "top" }); + } + }; + + // 切换支付方式 + const handleChangePayType = (payType: number) => { + if (paymentData) { + setPaymentData({ ...paymentData, payType }); + // TODO: 根据支付方式重新获取二维码 + } + }; + + // 查询订单状态(轮询) + const pollOrderStatus = async (orderNo: string) => { + try { + const res = await queryOrder(orderNo); + if (res.status === 1) { + // 订单已支付,显示支付成功弹框 + setPaymentSuccessData(res); + setPaymentSuccessVisible(true); + setPaymentDialogVisible(false); + // 清除轮询 + if (pollingTimer) { + clearInterval(pollingTimer); + setPollingTimer(null); + } + // 刷新统计数据 + fetchStats(); + // 刷新订单列表 + if (activeTab === "orders") { + fetchOrders(); + } + } + } catch (error) { + console.error("查询订单状态失败:", error); + } + }; + + // 开始轮询订单状态 + useEffect(() => { + if (paymentDialogVisible && paymentData?.orderNo) { + // 立即查询一次 + pollOrderStatus(paymentData.orderNo); + // 每3秒轮询一次 + const timer = setInterval(() => { + pollOrderStatus(paymentData.orderNo); + }, 3000); + setPollingTimer(timer); + return () => { + clearInterval(timer); + setPollingTimer(null); + }; + } else { + // 关闭支付弹框时清除轮询 + if (pollingTimer) { + clearInterval(pollingTimer); + setPollingTimer(null); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [paymentDialogVisible, paymentData?.orderNo]); + + // 完成支付 + const handleCompletePayment = () => { + // 手动触发一次查询 + if (paymentData?.orderNo) { + pollOrderStatus(paymentData.orderNo); + } + }; + + // 关闭支付成功弹框 + const handleClosePaymentSuccess = () => { + setPaymentSuccessVisible(false); + setPaymentSuccessData(null); + }; + + // 格式化支付时间 + const formatPayTime = (timestamp: number | null): string => { + if (!timestamp) return ""; + const date = new Date(timestamp * 1000); + const month = date.getMonth() + 1; + const day = date.getDate(); + const hour = String(date.getHours()).padStart(2, "0"); + const minute = String(date.getMinutes()).padStart(2, "0"); + return `${month}/${day} ${hour}:${minute}`; + }; + + // 解析商品规格 + const parseGoodsSpecs = (specs: string) => { + try { + const parsed = typeof specs === "string" ? JSON.parse(specs) : specs; + return parsed; + } catch { + return null; + } + }; + + // 复制支付成功弹框的订单号 + const handleCopySuccessOrderNo = async () => { + if (!paymentSuccessData?.orderNo) return; + try { + await navigator.clipboard.writeText(paymentSuccessData.orderNo); + Toast.show({ content: "订单号已复制", position: "top" }); + } catch (error) { + const textarea = document.createElement("textarea"); + textarea.value = paymentSuccessData.orderNo; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand("copy"); + document.body.removeChild(textarea); + Toast.show({ content: "订单号已复制", position: "top" }); + } + }; + + const handleTabChange = (tab: string) => { + setActiveTab(tab); + // 移除导航逻辑,只切换tab + }; + + // 触底加载更多 - 算力明细 + const loadMoreRecords = async () => { + if (recordLoadingMore || !recordHasMore) return; + const nextPage = recordPage + 1; + setRecordPage(nextPage); + await fetchRecords(nextPage, true); + }; + + // 触底加载更多 - 订单 + const loadMoreOrders = async () => { + if (orderLoadingMore || !orderHasMore) return; + const nextPage = orderPage + 1; + setOrderPage(nextPage); + await fetchOrders(nextPage, true); + }; + + // 触底加载更多 - 分配记录 + const loadMoreAllocationRecords = async () => { + if (allocationLoadingMore || !allocationHasMore) return; + const nextPage = allocationPage + 1; + setAllocationPage(nextPage); + await fetchAllocationRecords(nextPage, true); + }; return ( - - - - } - /> - - - - - - } - footer={ - activeTab === "records" && records.length > 0 ? ( -
- { - setPage(p); - fetchRecords(p); - }} - /> -
- ) : null - } + loading={loading && !stats} + header={} >
- {activeTab === "overview" && renderOverview()} - {activeTab === "records" && renderRecords()} + {/* 算力概览区域:蓝色卡片 + 4个统计卡片 */} +
+ {/* 蓝色算力概览卡片 */} +
+
+
+
剩余算力
+
+ {formatNumber(stats?.remainingTokens)} +
+
+
+ 总计 {stats?.totalTokens || 0} +
+
+
+
+
+ 使用率 {getUsageRate()}% +
+ +
+
+ + {/* 4个统计卡片(2x2网格) */} +
+ +
+ +
今日消耗
+
+
+ {formatNumber(stats?.todayUsed)} +
+
+ +
+ +
昨日消耗
+
+
+ {formatNumber(stats?.yesterdayUsed)} +
+
+ +
+ +
历史消耗
+
+
+ {formatNumber(stats?.historyConsumed || stats?.totalConsumed)} +
+
+ +
+ +
预计可用(天)
+
+
+ {getEstimatedDays()} +
+
+
+
+ + {/* 导航标签 */} +
+ {navTabs.map(tab => ( +
handleTabChange(tab.key)} + > + {tab.label} +
+ ))} +
+ + + {/* 算力明细Tab内容 */} + {activeTab === "details" && ( + <> + {/* 筛选器 */} +
+ setFilterTypeVisible(false)} + value={[filterType]} + onConfirm={value => { + setFilterType(value[0] as string); + setFilterTypeVisible(false); + }} + > + {() => ( +
setFilterTypeVisible(true)} + > + + {getTypeLabel()} +
+ )} +
+ + setFilterActionVisible(false)} + value={[filterAction]} + onConfirm={value => { + setFilterAction(value[0] as string); + setFilterActionVisible(false); + }} + > + {() => ( +
setFilterActionVisible(true)} + > + + {getActionLabel()} +
+ )} +
+ + setFilterTimeVisible(false)} + value={[filterTime]} + onConfirm={value => { + setFilterTime(value[0] as string); + setFilterTimeVisible(false); + }} + > + {() => ( +
setFilterTimeVisible(true)} + > + + {getTimeLabel()} +
+ )} +
+
+ + {/* 算力消耗记录列表 */} +
+ {loading && records.length === 0 ? ( +
+
加载中...
+
+ ) : records.length > 0 ? ( + <> + {records.map(record => { + const isConsume = record.type === 0; + const powerColor = isConsume ? '#ff7a00' : '#52c41a'; + const iconBgColor = isConsume ? '#fff4e6' : '#f6ffed'; + + return ( +
+ {/* 左侧:图标 + 文字信息 */} +
+
+ +
+
+
+ {getRecordTitle(record)} +
+
+ {formatDateTime(record.createTime)} +
+
+
+ {/* 右侧:算力数值 */} +
+
+ {isConsume ? '-' : '+'}{formatNumber(Math.abs(record.tokens))} +
+
算力点
+
+
+ ); + })} + + + ) : ( +
+
暂无消耗记录
+
+ )} +
+ + )} + + {/* 购买算力Tab内容 */} + {activeTab === "buy" && ( +
+ {loading && packages.length === 0 ? ( +
+
加载中...
+
+ ) : ( + packages.map(pkg => { + const savings = getSavings(pkg); + const tokensValue = formatTokens(pkg.tokens); + + return ( + + {/* 套餐头部 */} +
+
+ {pkg.name} + {pkg.discount > 0 && ( + + {pkg.discount}% OFF + + )} +
+
+ +
+
{tokensValue}
+
算力
+
+
+
+ + {/* 价格信息 */} +
+
+ + ¥{(pkg.price / 100).toFixed(2)} + + {pkg.originalPrice && pkg.originalPrice > pkg.price && ( + + ¥{(pkg.originalPrice / 100).toFixed(2)} + + )} +
+ {savings > 0 && ( +
+ 节省 ¥{(savings / 100).toFixed(2)} +
+ )} +
+ ¥{pkg.unitPrice.toFixed(3)}/算力 +
+
+ + {/* 功能列表 */} + {pkg.description && pkg.description.length > 0 && ( +
+ {pkg.description.map((desc, index) => ( +
+ + {desc} +
+ ))} +
+ )} + + {/* 立即购买按钮 */} + +
+ ); + }) + )} + + {/* 自定义算力包 */} + +
+
+ +
+
自定义算力包
+
+ 根据您的需求定制,灵活购买算力 +
+
+ + {/* 快速选择 */} +
+ {[10, 50, 100].map(amount => ( + + ))} +
+ + {/* 自定义输入 */} +
+ +
+ + {/* 立即购买按钮 */} + +
+ + {/* 安全保障承诺 */} + +
+ + 安全保障承诺 +
+
+
+ + 算力永不过期,购买后永久有效 +
+
+ + 透明计费,每次AI服务消耗明细可查 +
+
+ + 7×24小时技术支持 +
+
+ + 企业级数据安全全认证 +
+
+
+
+ )} + + {/* 购买记录Tab内容 */} + {activeTab === "orders" && ( +
+ {/* 搜索栏 */} +
+ + { + setOrderKeyword(value); + }} + onEnterPress={() => { + setOrderPage(1); + setOrderHasMore(true); + fetchOrders(1, false); + }} + className={style.searchInput} + /> +
+ + {/* 筛选器 */} +
+ setOrderStatusVisible(false)} + value={[orderStatus]} + onConfirm={value => { + setOrderStatus(value[0] as string); + setOrderStatusVisible(false); + // useEffect 会自动触发 fetchOrders + }} + > + {() => ( +
setOrderStatusVisible(true)} + > + + {getOrderStatusLabel()} +
+ )} +
+ + setOrderTimeVisible(false)} + value={[orderTime]} + onConfirm={value => { + setOrderTime(value[0] as string); + setOrderTimeVisible(false); + // useEffect 会自动触发 fetchOrders + }} + > + {() => ( +
setOrderTimeVisible(true)} + > + + {getOrderTimeLabel()} +
+ )} +
+ + setOrderPayTypeVisible(false)} + value={[orderPayType]} + onConfirm={value => { + setOrderPayType(value[0] as string); + setOrderPayTypeVisible(false); + // useEffect 会自动触发 fetchOrders + }} + > + {() => ( +
setOrderPayTypeVisible(true)} + > + + {getOrderPayTypeLabel()} +
+ )} +
+
+ + {/* 订单列表 */} +
+ {loading && orders.length === 0 ? ( +
+
加载中...
+
+ ) : orders.length > 0 ? ( + <> + {orders.map(order => { + const statusInfo = getOrderStatusInfo(order.status); + // tokens 已经是格式化好的字符串,如 "280,000" + const tokens = order.tokens || "0"; + + return ( + +
+
+ {order.goodsName || "算力充值"} +
+
+ +{tokens} +
+
+
+
+
+ 订单号: {order.orderNo || "-"} +
+
+ {order.payType === 1 ? ( + + ) : order.payType === 2 ? ( + + ) : null} + {order.payType ? getPayTypeText(order.payType) : order.payTypeText || "未支付"} +
+
+
+
+ {order.statusText || statusInfo.text} +
+
+ {formatDateTime(order.createTime)} +
+
+
+
+ ); + })} + + + ) : ( +
+
暂无订单记录
+
+ )} +
+
+ )} + + {/* 算力分配Tab内容 */} + {activeTab === "allocation" && ( +
+ {/* 算力分配表单 */} + +
+ + 算力分配 +
+ +
+ {/* 选择账号 */} +
+ + ({ label: getAccountDisplayName(acc), value: acc.uid || acc.userId || acc.id }))]} + visible={allocationAccountVisible} + onClose={() => setAllocationAccountVisible(false)} + value={allocationAccount ? [allocationAccount.uid || allocationAccount.userId || allocationAccount.id] : []} + onConfirm={value => { + const selectedAccount = accounts.find(acc => (acc.uid || acc.userId || acc.id) === value[0]); + setAllocationAccount(selectedAccount || null); + setAllocationAccountVisible(false); + }} + > + {() => ( +
setAllocationAccountVisible(true)} + > + {allocationAccount ? getAccountDisplayName(allocationAccount) : "请选择账号"} + +
+ )} +
+
+ + {/* 填写数量 */} +
+ + +
+ + {/* 确认分配按钮 */} + +
+
+ + {/* 分配记录 */} + +
+ + 分配记录 +
+ setAllocationAccountFilterVisible(false)} + value={[allocationAccountFilter]} + onConfirm={value => { + setAllocationAccountFilter(value[0] as string); + setAllocationAccountFilterVisible(false); + // 重新加载记录 + setAllocationPage(1); + setAllocationHasMore(true); + fetchAllocationRecords(1, false); + }} + > + {() => ( +
setAllocationAccountFilterVisible(true)} + > + {allocationAccountOptions.find(opt => opt.value === allocationAccountFilter)?.label || "全部账号"} + +
+ )} +
+ + setAllocationTimeFilterVisible(false)} + value={[allocationTimeFilter]} + onConfirm={value => { + setAllocationTimeFilter(value[0] as string); + setAllocationTimeFilterVisible(false); + // 重新加载记录 + setAllocationPage(1); + setAllocationHasMore(true); + fetchAllocationRecords(1, false); + }} + > + {() => ( +
setAllocationTimeFilterVisible(true)} + > + {allocationTimeOptions.find(opt => opt.value === allocationTimeFilter)?.label || "最近7天"} + +
+ )} +
+
+
+ + {/* 记录列表 */} +
+ {loading && allocationRecords.length === 0 ? ( +
+
加载中...
+
+ ) : allocationRecords.length > 0 ? ( + <> + {allocationRecords.map(record => { + // 从remarks中提取账号信息,格式可能是 "账号名 - 其他信息" 或直接是账号名 + const accountName = record.remarks || `账号${record.wechatAccountId || record.id}`; + + return ( +
+
+ +
+
+ {accountName} +
+
+ {formatDateTime(record.createTime)} +
+
+
+
+ +{formatNumber(Math.abs(record.tokens))} +
+
+ ); + })} + + + ) : ( +
+
暂无分配记录
+
+ )} +
+
+
+ )}
+ + {/* 支付弹框 */} + setPaymentDialogVisible(false)} + bodyStyle={{ + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + maxHeight: "90vh", + overflowY: "auto", + }} + > + {paymentData && ( +
+ {/* 头部 */} +
+
购买算力包
+ setPaymentDialogVisible(false)} + /> +
+ + {/* 产品详情 */} +
+
+ +
+
+ {paymentData.packageName} +
+
+ 算力点数: {paymentData.tokens} +
+
+ 有效期: 永久 +
+
+
+
+
¥{paymentData.price.toFixed(2)}
+ {paymentData.discount && paymentData.discount > 0 && ( +
+ 省{paymentData.discount}% +
+ )} +
+
+ + {/* 支付方式选择 */} +
+
handleChangePayType(1)} + > + + 微信支付 +
+
handleChangePayType(2)} + > + + 支付宝 +
+
+ + {/* 二维码 */} +
+ 支付二维码 +
+ + 请使用{paymentData.payType === 1 ? "微信" : "支付宝"}扫码支付 +
+
+ + {/* 支付信息 */} +
+
+ + 支付剩余时间 + + {formatCountdown(countdown)} + +
+
+ 二维码有效期至 + + {formatExpireTime(expireTime)} + +
+
+ 订单号 +
+ {paymentData.orderNo} + +
+
+
+ + {/* 完成支付按钮 */} + + + {/* 支付说明 */} +
+
+ + 支付说明 +
+
+
+ + + 请在5分钟内完成支付,超时后需重新生成二维码 + +
+
+ + + 支付成功后算力将立即到账,可在账户中查看 + +
+
+ + + 支持自动检测支付状态,无需手动刷新 + +
+
+ + + 如遇问题请联系客服:400-123-4567 + +
+
+ + + 支持7天无理由退款(未使用部分按比例退款) + +
+
+
+
+ )} +
+ + {/* 支付成功弹框 */} + + {paymentSuccessData && ( +
+ {/* 头部 */} +
+
支付成功
+ +
+ + {/* 成功图标和文字 */} +
+ +
+
支付成功
+
+ 您购买的算力包已到账 +
+ + {/* 套餐信息 */} +
+
+ +
+
+ {paymentSuccessData.goodsName} +
+ {(() => { + const specs = parseGoodsSpecs(paymentSuccessData.goodsSpecs); + const tokens = specs?.tokens || 0; + return ( +
+ 算力点数: {tokens.toLocaleString()} +
+ ); + })()} +
+
+
+
+ ¥{(paymentSuccessData.money / 100).toFixed(2)} +
+
+ 支付方式: {paymentSuccessData.payType === 1 ? "微信支付" : paymentSuccessData.payType === 2 ? "支付宝" : "未知"} +
+
+
+ + {/* 订单信息 */} +
+
+ 支付时间 + + {formatPayTime(paymentSuccessData.payTime)} + +
+
+ 订单号 +
+ + {paymentSuccessData.orderNo} + + +
+
+
+ + {/* 完成按钮 */} + +
+ )} +
); }; diff --git a/Cunkebao/src/pages/mobile/mine/recharge/usage-records/api.ts b/Cunkebao/src/pages/mobile/mine/recharge/usage-records/api.ts index 5d3aff23..e9a318d5 100644 --- a/Cunkebao/src/pages/mobile/mine/recharge/usage-records/api.ts +++ b/Cunkebao/src/pages/mobile/mine/recharge/usage-records/api.ts @@ -36,6 +36,7 @@ export interface TokensUseRecordItem { export interface TokensUseRecordList { list: TokensUseRecordItem[]; + total?: number; } //算力使用明细 diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts index 7a9a1ce3..8c14f3e9 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/api.ts @@ -49,6 +49,8 @@ export function transferWechatFriends(params: { wechatId: string; devices: number[]; inherit: boolean; + greeting?: string; + firstMessage?: string; }) { return request("/v1/wechats/transfer-friends", params, "POST"); } @@ -92,6 +94,20 @@ export async function exportWechatMoments(params: { } ); + // 检查响应类型,如果是JSON错误响应,需要解析错误信息 + const contentType = response.headers["content-type"] || ""; + if (contentType.includes("application/json")) { + // 如果是JSON响应,说明可能是错误信息 + const text = await response.data.text(); + const errorData = JSON.parse(text); + throw new Error(errorData.message || errorData.msg || "导出失败"); + } + + // 检查响应状态 + if (response.status !== 200) { + throw new Error(`导出失败,状态码: ${response.status}`); + } + // 创建下载链接 const blob = new Blob([response.data]); const url = window.URL.createObjectURL(blob); @@ -114,6 +130,32 @@ export async function exportWechatMoments(params: { document.body.removeChild(link); window.URL.revokeObjectURL(url); } catch (error: any) { - throw new Error(error.response?.data?.message || error.message || "导出失败"); + // 如果是我们抛出的错误,直接抛出 + if (error.message && error.message !== "导出失败") { + throw error; + } + + // 处理axios错误响应 + if (error.response) { + // 如果响应是blob类型,尝试读取为文本 + if (error.response.data instanceof Blob) { + try { + const text = await error.response.data.text(); + const errorData = JSON.parse(text); + throw new Error(errorData.message || errorData.msg || "导出失败"); + } catch (parseError) { + throw new Error("导出失败,请重试"); + } + } else { + throw new Error( + error.response.data?.message || + error.response.data?.msg || + error.message || + "导出失败" + ); + } + } else { + throw new Error(error.message || "导出失败"); + } } } diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss index 954921fc..95c33d8a 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/detail.module.scss @@ -339,8 +339,8 @@ height: 5px; border-radius: 10px 10px 0 0; background: #1677ff; - } } + } .stat-icon-chat { width: 20px; @@ -373,6 +373,12 @@ font-weight: 600; color: #52c41a; } + + .stat-value-negative { + font-size: 20px; + font-weight: 600; + color: #ff4d4f; + } } } @@ -700,7 +706,7 @@ .adm-avatar { width: 48px; height: 48px; - border-radius: 50%; + border-radius: 50%; } } @@ -710,13 +716,13 @@ } .friend-name-row { - display: flex; - align-items: center; + display: flex; + align-items: center; gap: 8px; - margin-bottom: 4px; + margin-bottom: 4px; } - .friend-name { + .friend-name { font-size: 15px; font-weight: 600; color: #111; @@ -727,7 +733,7 @@ display: flex; flex-wrap: wrap; gap: 4px; - } + } .friend-tag { font-size: 11px; @@ -735,17 +741,17 @@ border-radius: 999px; background: #f5f5f5; color: #666; - } + } .friend-id-row { - font-size: 12px; + font-size: 12px; color: #999; margin-bottom: 6px; - } + } .friend-status-row { - display: flex; - flex-wrap: wrap; + display: flex; + flex-wrap: wrap; gap: 6px; } @@ -764,7 +770,7 @@ font-size: 11px; color: #999; margin-bottom: 4px; - } + } .value-amount { font-size: 14px; @@ -868,9 +874,10 @@ .type-selector { display: flex; gap: 8px; - flex-wrap: wrap; + width: 100%; .type-option { + flex: 1; padding: 8px 16px; border: 1px solid #e0e0e0; border-radius: 8px; @@ -879,6 +886,7 @@ cursor: pointer; transition: all 0.2s; background: white; + text-align: center; &:hover { border-color: #1677ff; @@ -1331,82 +1339,54 @@ } } +.moments-action-bar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: white; + border-bottom: 1px solid #f0f0f0; + + .action-button, .action-button-dark { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: 8px; + background: #1677ff; + border: none; + cursor: pointer; + transition: all 0.2s; + color: white; + + &:active { + background: #0958d9; + transform: scale(0.95); + } + + svg { + font-size: 20px; + color: white; + } + } + + .action-button-dark { + background: #1677ff; + color: white; + + &:active { + background: #0958d9; + } + } +} + .moments-content { padding: 16px 0; height: 500px; overflow-y: auto; background: #f5f5f5; - .moments-action-bar { - display: flex; - justify-content: space-between; - padding: 0 16px 16px; - - .action-button, .action-button-dark { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - width: 70px; - height: 40px; - border-radius: 8px; - background: #1677ff; - - .action-icon-text, .action-icon-image, .action-icon-video, .action-icon-export { - width: 20px; - height: 20px; - background: rgba(255, 255, 255, 0.2); - border-radius: 4px; - margin-bottom: 2px; - position: relative; - - &::before { - content: ''; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 12px; - height: 2px; - background: white; - } - } - - .action-icon-image::after { - content: ''; - position: absolute; - top: 6px; - left: 6px; - width: 8px; - height: 8px; - border-radius: 2px; - background: white; - } - - .action-icon-video::before { - content: ''; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 0; - height: 0; - border-style: solid; - border-width: 5px 0 5px 8px; - border-color: transparent transparent transparent white; - } - - .action-text, .action-text-light { - font-size: 12px; - color: white; - } - } - - .action-button-dark { - background: #333; - } - } - .moments-list { padding: 0 16px; @@ -1459,7 +1439,7 @@ .image-grid { display: grid; - gap: 8px; + gap: 4px; width: 100%; // 1张图片:宽度拉伸,高度自适应 @@ -1469,56 +1449,57 @@ img { width: 100%; height: auto; + max-height: 400px; object-fit: cover; - border-radius: 8px; + border-radius: 4px; } } - // 2张图片:左右并列 + // 2张图片:左右并列,1:1比例 &.double { grid-template-columns: 1fr 1fr; img { width: 100%; - height: 120px; + aspect-ratio: 1 / 1; object-fit: cover; - border-radius: 8px; + border-radius: 4px; } } - // 3张图片:三张并列 + // 3张图片:三张并列,1:1比例 &.triple { grid-template-columns: 1fr 1fr 1fr; img { width: 100%; - height: 100px; + aspect-ratio: 1 / 1; object-fit: cover; - border-radius: 8px; + border-radius: 4px; } } - // 4张图片:2x2网格布局 + // 4张图片:2x2网格布局,1:1比例 &.quad { grid-template-columns: repeat(2, 1fr); img { width: 100%; - height: 140px; + aspect-ratio: 1 / 1; object-fit: cover; - border-radius: 8px; + border-radius: 4px; } } - // 5张及以上:网格布局(9宫格) + // 5张及以上:网格布局(9宫格),1:1比例 &.grid { grid-template-columns: repeat(3, 1fr); img { width: 100%; - height: 100px; + aspect-ratio: 1 / 1; object-fit: cover; - border-radius: 8px; + border-radius: 4px; } .image-more { @@ -1526,11 +1507,11 @@ align-items: center; justify-content: center; background: rgba(0, 0, 0, 0.5); - border-radius: 8px; + border-radius: 4px; color: white; font-size: 12px; font-weight: 500; - height: 100px; + aspect-ratio: 1 / 1; } } } @@ -1547,6 +1528,34 @@ } } } + + .moments-loading { + display: flex; + align-items: center; + justify-content: center; + padding: 16px 0; + } + + .moments-no-more { + display: flex; + align-items: center; + justify-content: center; + padding: 16px 0; + } + } + + .friends-loading { + display: flex; + align-items: center; + justify-content: center; + padding: 16px 0; + } + + .friends-no-more { + display: flex; + align-items: center; + justify-content: center; + padding: 16px 0; } } diff --git a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx index 333eb353..ae694b3a 100644 --- a/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx @@ -12,13 +12,18 @@ import { Tag, Switch, DatePicker, + InfiniteScroll, } from "antd-mobile"; -import { Input, Pagination } from "antd"; +import { Input } from "antd"; import NavCommon from "@/components/NavCommon"; import { SearchOutlined, ReloadOutlined, UserOutlined, + FileTextOutlined, + PictureOutlined, + VideoCameraOutlined, + DownloadOutlined, } from "@ant-design/icons"; import Layout from "@/components/Layout/Layout"; import style from "./detail.module.scss"; @@ -32,6 +37,7 @@ import { } from "./api"; import DeviceSelection from "@/components/DeviceSelection"; import { DeviceSelectionItem } from "@/components/DeviceSelection/data"; +import dayjs from "dayjs"; import { WechatAccountSummary, Friend, MomentItem } from "./data"; @@ -47,6 +53,8 @@ const WechatAccountDetail: React.FC = () => { const [showTransferConfirm, setShowTransferConfirm] = useState(false); const [selectedDevices, setSelectedDevices] = useState([]); const [inheritInfo, setInheritInfo] = useState(true); + const [greeting, setGreeting] = useState(""); + const [firstMessage, setFirstMessage] = useState(""); const [transferLoading, setTransferLoading] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const [activeTab, setActiveTab] = useState("overview"); @@ -105,6 +113,84 @@ const WechatAccountDetail: React.FC = () => { } }, [id]); + // 计算账号价值 + // 规则: + // 1. 1个好友3块 + // 2. 1个群1块 + // 3. 修改过微信号10块 + const calculateAccountValue = useCallback(() => { + // 获取好友数量(优先使用概览数据,其次使用好友列表总数,最后使用账号信息) + const friendsCount = overviewData?.totalFriends || friendsTotal || accountInfo?.friendShip?.totalFriend || 0; + + // 获取群数量(优先使用概览数据,其次使用账号信息) + const groupsCount = overviewData?.highValueChatrooms || accountInfo?.friendShip?.groupNumber || 0; + + // 判断是否修改过微信号 + // 注意:需要根据实际API返回的字段来判断,可能的字段名: + // - isWechatIdModified (布尔值) + // - wechatIdModified (布尔值) + // - hasModifiedWechatId (布尔值) + // - wechatIdChangeCount (数字,大于0表示修改过) + // 如果API没有返回该字段,需要后端添加或根据其他逻辑判断 + const isWechatIdModified = + accountInfo?.isWechatIdModified || + accountInfo?.wechatIdModified || + accountInfo?.hasModifiedWechatId || + (accountInfo?.wechatIdChangeCount && accountInfo.wechatIdChangeCount > 0) || + false; + + // 计算各部分价值 + const friendsValue = friendsCount * 3; // 好友数 * 3 + const groupsValue = groupsCount * 1; // 群数 * 1 + const wechatIdModifiedValue = isWechatIdModified ? 10 : 0; // 修改过微信号 ? 10 : 0 + + // 计算总价值 + const totalValue = friendsValue + groupsValue + wechatIdModifiedValue; + + return { + value: totalValue, + formatted: `¥${totalValue.toLocaleString()}`, + breakdown: { + friends: friendsValue, + groups: groupsValue, + wechatIdModified: wechatIdModifiedValue, + friendsCount, + groupsCount, + isWechatIdModified, + }, + }; + }, [overviewData, friendsTotal, accountInfo]); + + // 计算今日价值变化 + // 规则: + // 1. 今日新增好友 * 3块 + // 2. 今日新增群 * 1块 + const calculateTodayValueChange = useCallback(() => { + // 获取今日新增好友数 + const todayNewFriends = overviewData?.todayNewFriends || accountSummary?.statistics?.todayAdded || 0; + + // 获取今日新增群数 + const todayNewChatrooms = overviewData?.todayNewChatrooms || 0; + + // 计算今日价值变化 + const friendsValueChange = todayNewFriends * 3; // 今日新增好友数 * 3 + const groupsValueChange = todayNewChatrooms * 1; // 今日新增群数 * 1 + + const totalChange = friendsValueChange + groupsValueChange; + + return { + change: totalChange, + formatted: totalChange >= 0 ? `+${totalChange.toLocaleString()}` : `${totalChange.toLocaleString()}`, + isPositive: totalChange >= 0, + breakdown: { + friends: friendsValueChange, + groups: groupsValueChange, + todayNewFriends, + todayNewChatrooms, + }, + }; + }, [overviewData, accountSummary]); + // 获取概览数据 const fetchOverviewData = useCallback(async () => { if (!id) return; @@ -120,7 +206,7 @@ const WechatAccountDetail: React.FC = () => { // 获取好友列表 - 封装为独立函数 const fetchFriendsList = useCallback( - async (page: number = 1, keyword: string = "") => { + async (page: number = 1, keyword: string = "", append: boolean = false) => { if (!id) return; setIsFetchingFriends(true); @@ -130,7 +216,7 @@ const WechatAccountDetail: React.FC = () => { const response = await getWechatFriends({ wechatAccount: id, page: page, - limit: 5, + limit: 20, keyword: keyword, }); @@ -173,15 +259,17 @@ const WechatAccountDetail: React.FC = () => { }; }); - setFriends(newFriends); + setFriends(prev => (append ? [...prev, ...newFriends] : newFriends)); setFriendsTotal(response.total); setFriendsPage(page); - setIsFriendsEmpty(newFriends.length === 0); + setIsFriendsEmpty(newFriends.length === 0 && !append); } catch (error) { console.error("获取好友列表失败:", error); setHasFriendLoadError(true); - setFriends([]); - setIsFriendsEmpty(true); + if (!append) { + setFriends([]); + setIsFriendsEmpty(true); + } Toast.show({ content: "获取好友列表失败,请检查网络连接", position: "top", @@ -236,22 +324,20 @@ const WechatAccountDetail: React.FC = () => { // 搜索好友 const handleSearch = useCallback(() => { setFriendsPage(1); - fetchFriendsList(1, searchQuery); + fetchFriendsList(1, searchQuery, false); }, [searchQuery, fetchFriendsList]); // 刷新好友列表 const handleRefreshFriends = useCallback(() => { - fetchFriendsList(friendsPage, searchQuery); + fetchFriendsList(friendsPage, searchQuery, false); }, [friendsPage, searchQuery, fetchFriendsList]); - // 分页切换 - const handlePageChange = useCallback( - (page: number) => { - setFriendsPage(page); - fetchFriendsList(page, searchQuery); - }, - [searchQuery, fetchFriendsList], - ); + // 加载更多好友 + const handleLoadMoreFriends = async () => { + if (isFetchingFriends) return; + if (friends.length >= friendsTotal) return; + await fetchFriendsList(friendsPage + 1, searchQuery, true); + }; // 初始化数据 useEffect(() => { @@ -266,7 +352,7 @@ const WechatAccountDetail: React.FC = () => { if (activeTab === "friends" && id) { setIsFriendsEmpty(false); setHasFriendLoadError(false); - fetchFriendsList(1, searchQuery); + fetchFriendsList(1, searchQuery, false); } }, [activeTab, id, fetchFriendsList, searchQuery]); @@ -294,6 +380,10 @@ const WechatAccountDetail: React.FC = () => { const handleTransferFriends = () => { setSelectedDevices([]); setInheritInfo(true); + // 设置默认打招呼内容,使用当前微信账号昵称 + const nickname = accountInfo?.nickname || "未知"; + setGreeting(`我是${nickname}的新号,请通过`); + setFirstMessage("这个是我的新号,重新加你一下,以后业务就用这个号!"); setShowTransferConfirm(true); }; @@ -321,7 +411,9 @@ const WechatAccountDetail: React.FC = () => { await transferWechatFriends({ wechatId: id, devices: selectedDevices.map(device => device.id), - inherit: inheritInfo + inherit: inheritInfo, + greeting: greeting.trim(), + firstMessage: firstMessage.trim() }); Toast.show({ @@ -330,6 +422,7 @@ const WechatAccountDetail: React.FC = () => { }); setShowTransferConfirm(false); setSelectedDevices([]); + setFirstMessage(""); navigate("/scenarios"); } catch (error) { console.error("好友转移失败:", error); @@ -376,10 +469,10 @@ const WechatAccountDetail: React.FC = () => { navigate(`/mine/traffic-pool/detail/${friend.wechatId}/${friend.id}`); }; - const handleLoadMoreMoments = () => { + const handleLoadMoreMoments = async () => { if (isFetchingMoments) return; if (moments.length >= momentsTotal) return; - fetchMomentsList(momentsPage + 1, true); + await fetchMomentsList(momentsPage + 1, true); }; // 处理朋友圈导出 @@ -389,6 +482,18 @@ const WechatAccountDetail: React.FC = () => { return; } + // 验证时间范围不超过1个月 + if (exportStartTime && exportEndTime) { + const maxDate = dayjs(exportStartTime).add(1, "month").toDate(); + if (exportEndTime > maxDate) { + Toast.show({ + content: "日期范围不能超过1个月", + position: "top", + }); + return; + } + } + setExportLoading(true); try { // 格式化时间 @@ -409,18 +514,27 @@ const WechatAccountDetail: React.FC = () => { }); Toast.show({ content: "导出成功", position: "top" }); - setShowExportPopup(false); - // 重置筛选条件 + // 重置筛选条件(先重置,再关闭弹窗) setExportKeyword(""); setExportType(undefined); setExportStartTime(null); setExportEndTime(null); + setShowStartTimePicker(false); + setShowEndTimePicker(false); + // 延迟关闭弹窗,确保Toast显示 + setTimeout(() => { + setShowExportPopup(false); + }, 500); } catch (error: any) { console.error("导出失败:", error); + const errorMessage = error?.message || "导出失败,请重试"; Toast.show({ - content: error.message || "导出失败,请重试", + content: errorMessage, position: "top", + duration: 2000, }); + // 确保loading状态被重置 + setExportLoading(false); } finally { setExportLoading(false); } @@ -539,7 +653,7 @@ const WechatAccountDetail: React.FC = () => {
- {overviewData?.accountValue?.formatted || `¥${overviewData?.accountValue?.value || "29,800"}`} + {calculateAccountValue().formatted}
@@ -549,8 +663,8 @@ const WechatAccountDetail: React.FC = () => {
今日价值变化
-
- {overviewData?.todayValueChange?.formatted || `+${overviewData?.todayValueChange?.change || "500"}`} +
+ {calculateTodayValueChange().formatted}
@@ -752,7 +866,7 @@ const WechatAccountDetail: React.FC = () => {
好友总估值
- {overviewData?.accountValue?.formatted || "¥1,500,000"} + {calculateAccountValue().formatted}
@@ -771,7 +885,7 @@ const WechatAccountDetail: React.FC = () => { - - )} + + {isFetchingMoments && moments.length > 0 && ( +
+ + + 加载中... + +
+ )} + {moments.length >= momentsTotal && moments.length > 0 && ( +
+ 没有更多了 +
+ )} +
@@ -1049,6 +1181,38 @@ const WechatAccountDetail: React.FC = () => { + + {/* 打招呼 */} +
+
打招呼
+
+ setGreeting(e.target.value)} + rows={3} + maxLength={200} + showCount + style={{ resize: "none" }} + /> +
+
+ + {/* 通过后首次消息 */} +
+
好友通过后的首次消息
+
+ setFirstMessage(e.target.value)} + rows={3} + maxLength={200} + showCount + style={{ resize: "none" }} + /> +
+
@@ -1068,6 +1232,10 @@ const WechatAccountDetail: React.FC = () => { onClick={() => { setShowTransferConfirm(false); setSelectedDevices([]); + // 重置为默认打招呼内容 + const nickname = accountInfo?.nickname || "未知"; + setGreeting(`这个是${nickname}的新号,之前那个号没用了,重新加一下您`); + setFirstMessage(""); }} > 取消 @@ -1079,7 +1247,11 @@ const WechatAccountDetail: React.FC = () => { {/* 朋友圈导出弹窗 */} setShowExportPopup(false)} + onMaskClick={() => { + setShowExportPopup(false); + setShowStartTimePicker(false); + setShowEndTimePicker(false); + }} bodyStyle={{ borderRadius: "16px 16px 0 0" }} >
@@ -1088,7 +1260,11 @@ const WechatAccountDetail: React.FC = () => { @@ -1162,11 +1338,13 @@ const WechatAccountDetail: React.FC = () => { visible={showStartTimePicker} title="开始时间" value={exportStartTime} + max={exportEndTime || new Date()} onClose={() => setShowStartTimePicker(false)} onConfirm={val => { setExportStartTime(val); setShowStartTimePicker(false); }} + onCancel={() => setShowStartTimePicker(false)} />
@@ -1185,6 +1363,8 @@ const WechatAccountDetail: React.FC = () => { visible={showEndTimePicker} title="结束时间" value={exportEndTime} + min={exportStartTime || undefined} + max={new Date()} onClose={() => setShowEndTimePicker(false)} onConfirm={val => { setExportEndTime(val); @@ -1214,6 +1394,8 @@ const WechatAccountDetail: React.FC = () => { setExportType(undefined); setExportStartTime(null); setExportEndTime(null); + setShowStartTimePicker(false); + setShowEndTimePicker(false); }} style={{ marginTop: 12 }} > diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx b/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx index 59613c07..903b86fb 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx @@ -370,13 +370,13 @@ const ScenarioList: React.FC = () => { title={scenarioName || ""} right={ scenarioId !== "10" ? ( - + ) : null } /> @@ -427,13 +427,13 @@ const ScenarioList: React.FC = () => { {searchTerm ? "没有找到匹配的计划" : "暂无计划"}
{scenarioId !== "10" && ( - + )} ) : ( diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/list/planApi.tsx b/Cunkebao/src/pages/mobile/scenarios/plan/list/planApi.tsx index ee67b59c..f2f84642 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/list/planApi.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/list/planApi.tsx @@ -70,24 +70,72 @@ const PlanApi: React.FC = ({ // 处理webhook URL,确保包含完整的API地址 const fullWebhookUrl = useMemo(() => { - return buildApiUrl(webhookUrl); + return buildApiUrl(''); }, [webhookUrl]); - // 生成测试URL + // 快速测试使用的 GET 地址(携带示例查询参数,方便在浏览器中直接访问) const testUrl = useMemo(() => { - if (!fullWebhookUrl) return ""; - return `${fullWebhookUrl}?name=测试客户&phone=13800138000&source=API测试`; - }, [fullWebhookUrl]); + return buildApiUrl(webhookUrl); + }, [webhookUrl]); // 检测是否为移动端 const isMobile = window.innerWidth <= 768; const handleCopy = (text: string, type: string) => { - navigator.clipboard.writeText(text); - Toast.show({ - content: `${type}已复制到剪贴板`, - position: "top", - }); + // 先尝试使用 Clipboard API + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard + .writeText(text) + .then(() => { + Toast.show({ + content: `${type}已复制到剪贴板`, + position: "top", + }); + }) + .catch(() => { + // 回退到传统的 textarea 复制方式 + const textarea = document.createElement("textarea"); + textarea.value = text; + textarea.style.position = "fixed"; + textarea.style.left = "-9999px"; + document.body.appendChild(textarea); + textarea.select(); + try { + document.execCommand("copy"); + Toast.show({ + content: `${type}已复制到剪贴板`, + position: "top", + }); + } catch { + Toast.show({ + content: `${type}复制失败,请手动复制`, + position: "top", + }); + } + document.body.removeChild(textarea); + }); + } else { + // 不支持 Clipboard API 时直接使用回退方案 + const textarea = document.createElement("textarea"); + textarea.value = text; + textarea.style.position = "fixed"; + textarea.style.left = "-9999px"; + document.body.appendChild(textarea); + textarea.select(); + try { + document.execCommand("copy"); + Toast.show({ + content: `${type}已复制到剪贴板`, + position: "top", + }); + } catch { + Toast.show({ + content: `${type}复制失败,请手动复制`, + position: "top", + }); + } + document.body.removeChild(textarea); + } }; const handleTestInBrowser = () => { @@ -96,7 +144,7 @@ const PlanApi: React.FC = ({ const renderConfigTab = () => (
- {/* API密钥配置 */} + {/* 鉴权参数配置 */}
@@ -122,7 +170,7 @@ const PlanApi: React.FC = ({
- {/* 接口地址配置 */} + {/* 接口地址与参数说明 */}
@@ -150,27 +198,42 @@ const PlanApi: React.FC = ({ {/* 参数说明 */}
-

必要参数

+

鉴权参数(必填)

- name - 客户姓名 + apiKey - 分配给第三方的接口密钥(每个任务唯一)
- phone - 手机号码 + sign - 签名值,按文档的签名规则生成 +
+
+ timestamp - 秒级时间戳(与服务器时间差不超过 5 分钟)
-

可选参数

+

业务参数

- source - 来源标识 + wechatId - 微信号,存在时优先作为主标识 +
+
+ phone - 手机号,当 wechatId 为空时用作主标识 +
+
+ name - 客户姓名 +
+
+ source - 线索来源描述,如“百度推广”、“抖音直播间”
remark - 备注信息
- tags - 客户标签 + tags - 微信标签,逗号分隔,如 "高意向,电商,女装" +
+
+ siteTags - 站内标签,逗号分隔,用于站内进一步细分
@@ -179,93 +242,131 @@ const PlanApi: React.FC = ({
); - const renderQuickTestTab = () => ( -
-
-

快速测试URL

-
- -
-
- - + const renderQuickTestTab = () => { + return ( +
+
+

快速测试 URL(GET 示例)

+
+ +
+
+ + +
-
- ); + ); + }; - const renderDocsTab = () => ( -
-
- -
- -
-

完整API文档

-

详细的接口说明和参数文档

-
- -
- -
-

集成指南

-

第三方平台集成教程

-
+ const renderDocsTab = () => { + const docUrl = `${import.meta.env.VITE_API_BASE_URL}/doc/api_v1.md`; + + return ( +
+
+ +
+ +
+

对外获客线索上报接口文档(V1)

+

点击下方按钮可直接在浏览器中打开最新的远程文档。

+
+ + +
+
+
-
- ); + ); + }; const renderCodeTab = () => { const codeExamples = { - javascript: `fetch('${fullWebhookUrl}', { + javascript: `// 参考 api_v1 文档示例,使用 JSON 方式 POST +fetch('${fullWebhookUrl}', { method: 'POST', headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ${apiKey}' + 'Content-Type': 'application/json' }, body: JSON.stringify({ - name: '张三', + apiKey: '${apiKey}', + timestamp: 1710000000, // 秒级时间戳 phone: '13800138000', + name: '张三', source: '官网表单', + remark: '通过H5表单提交', + tags: '高意向,电商', + siteTags: '新客,女装', + // sign 需要根据签名规则生成 + sign: '根据签名规则生成的MD5字符串' }) })`, python: `import requests url = '${fullWebhookUrl}' headers = { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ${apiKey}' + 'Content-Type': 'application/json' } data = { - 'name': '张三', - 'phone': '13800138000', - 'source': '官网表单' + "apiKey": "${apiKey}", + "timestamp": 1710000000, + "phone": "13800138000", + "name": "张三", + "source": "官网表单", + "remark": "通过H5表单提交", + "tags": "高意向,电商", + "siteTags": "新客,女装", + # sign 需要根据签名规则生成 + "sign": "根据签名规则生成的MD5字符串" } response = requests.post(url, json=data, headers=headers)`, php: ` '张三', + 'apiKey' => '${apiKey}', + 'timestamp' => 1710000000, 'phone' => '13800138000', - 'source' => '官网表单' + 'name' => '张三', + 'source' => '官网表单', + 'remark' => '通过H5表单提交', + 'tags' => '高意向,电商', + 'siteTags' => '新客,女装', + // sign 需要根据签名规则生成 + 'sign' => '根据签名规则生成的MD5字符串' ); $options = array( 'http' => array( - 'header' => "Content-type: application/json\\r\\nAuthorization: Bearer ${apiKey}\\r\\n", + 'header' => "Content-type: application/json\\r\\n", 'method' => 'POST', 'content' => json_encode($data) ) @@ -279,12 +380,11 @@ import java.net.http.HttpResponse; import java.net.URI; HttpClient client = HttpClient.newHttpClient(); -String json = "{\\"name\\":\\"张三\\",\\"phone\\":\\"13800138000\\",\\"source\\":\\"官网表单\\"}"; +String json = "{\\"apiKey\\":\\"${apiKey}\\",\\"timestamp\\":1710000000,\\"phone\\":\\"13800138000\\",\\"name\\":\\"张三\\",\\"source\\":\\"官网表单\\",\\"remark\\":\\"通过H5表单提交\\",\\"tags\\":\\"高意向,电商\\",\\"siteTags\\":\\"新客,女装\\",\\"sign\\":\\"根据签名规则生成的MD5字符串\\"}"; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("${fullWebhookUrl}")) .header("Content-Type", "application/json") - .header("Authorization", "Bearer ${apiKey}") .POST(HttpRequest.BodyPublishers.ofString(json)) .build(); @@ -394,11 +494,7 @@ HttpResponse response = client.send(request, HttpResponse.BodyHandlers.o 所有数据传输均采用HTTPS加密
-
diff --git a/Cunkebao/src/pages/mobile/workspace/auto-group/form/components/OwnerAdminSelector.tsx b/Cunkebao/src/pages/mobile/workspace/auto-group/form/components/OwnerAdminSelector.tsx new file mode 100644 index 00000000..b96c73db --- /dev/null +++ b/Cunkebao/src/pages/mobile/workspace/auto-group/form/components/OwnerAdminSelector.tsx @@ -0,0 +1,235 @@ +import React, { useImperativeHandle, forwardRef } from "react"; +import { Form, Card, Tabs } from "antd"; +import DeviceSelection from "@/components/DeviceSelection"; +import FriendSelection from "@/components/FriendSelection"; +import { DeviceSelectionItem } from "@/components/DeviceSelection/data"; +import { FriendSelectionItem } from "@/components/FriendSelection/data"; + +interface OwnerAdminSelectorProps { + selectedOwners: DeviceSelectionItem[]; + selectedAdmins: FriendSelectionItem[]; + onNext: (data: { + devices: string[]; + devicesOptions: DeviceSelectionItem[]; + admins: string[]; + adminsOptions: FriendSelectionItem[]; + }) => void; +} + +export interface OwnerAdminSelectorRef { + validate: () => Promise; + getValues: () => any; +} + +const OwnerAdminSelector = forwardRef< + OwnerAdminSelectorRef, + OwnerAdminSelectorProps +>(({ selectedOwners, selectedAdmins, onNext }, ref) => { + const [form] = Form.useForm(); + const [owners, setOwners] = React.useState( + selectedOwners || [] + ); + const [admins, setAdmins] = React.useState( + selectedAdmins || [] + ); + + // 当外部传入的 selectedOwners 或 selectedAdmins 变化时,同步内部状态 + React.useEffect(() => { + setOwners(selectedOwners || []); + }, [selectedOwners]); + + React.useEffect(() => { + setAdmins(selectedAdmins || []); + }, [selectedAdmins]); + + // 暴露方法给父组件 + useImperativeHandle(ref, () => ({ + validate: async () => { + // 验证群主和管理员 + if (owners.length === 0) { + form.setFields([ + { + name: "devices", + errors: ["请选择一个群主"], + }, + ]); + return false; + } + if (owners.length > 1) { + form.setFields([ + { + name: "devices", + errors: ["群主只能选择一个设备"], + }, + ]); + return false; + } + if (admins.length === 0) { + form.setFields([ + { + name: "admins", + errors: ["请至少选择一个管理员"], + }, + ]); + return false; + } + // 清除错误 + form.setFields([ + { + name: "devices", + errors: [], + }, + { + name: "admins", + errors: [], + }, + ]); + return true; + }, + getValues: () => { + return { + devices: owners.map(o => o.id.toString()), + admins: admins.map(a => a.id.toString()), + devicesOptions: owners, + adminsOptions: admins, + }; + }, + })); + + // 群主选择(设备选择) + const handleOwnersSelect = (selectedDevices: DeviceSelectionItem[]) => { + const previousOwnerId = owners.length > 0 ? owners[0]?.id : null; + const newOwnerId = selectedDevices.length > 0 ? selectedDevices[0]?.id : null; + + // 当群主改变时,清空已选的管理员(因为筛选条件变了) + const shouldClearAdmins = previousOwnerId !== newOwnerId; + + setOwners(selectedDevices); + const ownerIds = selectedDevices.map(d => d.id.toString()); + form.setFieldValue("devices", ownerIds); + + if (shouldClearAdmins) { + setAdmins([]); + form.setFieldValue("admins", []); + } + + // 通知父组件数据变化 + onNext({ + devices: ownerIds, + devicesOptions: selectedDevices, + admins: shouldClearAdmins ? [] : admins.map(a => a.id.toString()), + adminsOptions: shouldClearAdmins ? [] : admins, + }); + }; + + // 管理员选择 + const handleAdminsSelect = (selectedFriends: FriendSelectionItem[]) => { + setAdmins(selectedFriends); + const adminIds = selectedFriends.map(f => f.id.toString()); + form.setFieldValue("admins", adminIds); + // 通知父组件数据变化 + onNext({ + devices: owners.map(o => o.id.toString()), + devicesOptions: owners, + admins: adminIds, + adminsOptions: selectedFriends, + }); + }; + + const tabItems = [ + { + key: "devices", + label: `群主 (${owners.length})`, + children: ( +
+
+

+ 请选择一个群主(设备),该设备将作为新建群聊的群主 +

+
+ 1 ? "error" : ""} + help={ + owners.length === 0 + ? "请选择一个群主(设备)" + : owners.length > 1 + ? "群主只能选择一个设备" + : "" + } + > + + +
+ ), + }, + { + key: "admins", + label: `管理员 (${admins.length})`, + children: ( +
+
+

+ {owners.length === 0 + ? "请先选择群主(设备),然后选择该设备下的好友作为管理员" + : "请选择管理员,管理员将协助管理新建的群聊(仅显示所选设备下的好友)"} +

+
+ + 0 ? owners.map(d => d.id) : []} + enableDeviceFilter={true} + readonly={owners.length === 0} + /> + +
+ ), + }, + ]; + + return ( + +
item.id.toString()), + admins: (selectedAdmins || []).map(item => item.id.toString()), + }} + > +
+

+ 选择群主和管理员 +

+

+ 请选择一个群主(设备)和管理员(好友),他们将负责管理新建的群聊 +

+
+ + + +
+ ); +}); + +OwnerAdminSelector.displayName = "OwnerAdminSelector"; + +export default OwnerAdminSelector; diff --git a/Cunkebao/src/pages/mobile/workspace/auto-group/form/index.tsx b/Cunkebao/src/pages/mobile/workspace/auto-group/form/index.tsx index bd38b751..db12abc0 100644 --- a/Cunkebao/src/pages/mobile/workspace/auto-group/form/index.tsx +++ b/Cunkebao/src/pages/mobile/workspace/auto-group/form/index.tsx @@ -7,24 +7,29 @@ import { createAutoGroup, updateAutoGroup, getAutoGroupDetail } from "./api"; import { AutoGroupFormData, StepItem } from "./types"; import StepIndicator from "@/components/StepIndicator"; import BasicSettings, { BasicSettingsRef } from "./components/BasicSettings"; -import DeviceSelector, { DeviceSelectorRef } from "./components/DeviceSelector"; +import OwnerAdminSelector, { + OwnerAdminSelectorRef, +} from "./components/OwnerAdminSelector"; import PoolSelector, { PoolSelectorRef } from "./components/PoolSelector"; import NavCommon from "@/components/NavCommon/index"; import dayjs from "dayjs"; import { DeviceSelectionItem } from "@/components/DeviceSelection/data"; +import { FriendSelectionItem } from "@/components/FriendSelection/data"; import { PoolSelectionItem } from "@/components/PoolSelection/data"; const steps: StepItem[] = [ { id: 1, title: "步骤 1", subtitle: "基础设置" }, - { id: 2, title: "步骤 2", subtitle: "选择设备" }, + { id: 2, title: "步骤 2", subtitle: "选择群主和管理员" }, { id: 3, title: "步骤 3", subtitle: "选择流量池包" }, ]; const defaultForm: AutoGroupFormData = { name: "", type: 4, - deviceGroups: [], // 设备组 - deviceGroupsOptions: [], // 设备组选项 + devices: [], // 群主ID列表 + devicesOptions: [], // 群主选项 + admins: [], // 管理员ID列表 + adminsOptions: [], // 管理员选项 poolGroups: [], // 内容库 poolGroupsOptions: [], // 内容库选项 startTime: dayjs().format("HH:mm"), // 开始时间 (HH:mm) @@ -45,16 +50,15 @@ const AutoGroupForm: React.FC = () => { const [loading, setLoading] = useState(false); const [dataLoaded, setDataLoaded] = useState(!isEdit); // 非编辑模式直接标记为已加载 const [formData, setFormData] = useState(defaultForm); - const [deviceGroupsOptions, setDeviceGroupsOptions] = useState< - DeviceSelectionItem[] - >([]); + const [devicesOptions, setDevicesOptions] = useState([]); + const [adminsOptions, setAdminsOptions] = useState([]); const [poolGroupsOptions, setpoolGroupsOptions] = useState< PoolSelectionItem[] >([]); // 创建子组件的ref const basicSettingsRef = useRef(null); - const deviceSelectorRef = useRef(null); + const ownerAdminSelectorRef = useRef(null); const poolSelectorRef = useRef(null); useEffect(() => { @@ -64,8 +68,10 @@ const AutoGroupForm: React.FC = () => { const updatedForm = { ...defaultForm, name: res.name, - deviceGroups: res.config.deviceGroups || [], - deviceGroupsOptions: res.config.deviceGroupsOptions || [], + devices: res.config.deviceGroups || res.config.devices || [], // 兼容deviceGroups和devices + devicesOptions: res.config.deviceGroupsOptions || res.config.devicesOptions || [], // 兼容deviceGroupsOptions和devicesOptions + admins: res.config.admins || [], + adminsOptions: res.config.adminsOptions || [], poolGroups: res.config.poolGroups || [], poolGroupsOptions: res.config.poolGroupsOptions || [], startTime: res.config.startTime, @@ -80,7 +86,8 @@ const AutoGroupForm: React.FC = () => { id: res.id, }; setFormData(updatedForm); - setDeviceGroupsOptions(res.config.deviceGroupsOptions || []); + setDevicesOptions(res.config.deviceGroupsOptions || res.config.devicesOptions || []); // 兼容deviceGroupsOptions和devicesOptions + setAdminsOptions(res.config.adminsOptions || []); setpoolGroupsOptions(res.config.poolGroupsOptions || []); setDataLoaded(true); // 标记数据已加载 }); @@ -90,16 +97,20 @@ const AutoGroupForm: React.FC = () => { setFormData(prev => ({ ...prev, ...values })); }; - // 设备组选择 - const handleDevicesChange = (data: { - deviceGroups: string[]; - deviceGroupsOptions: DeviceSelectionItem[]; + // 群主和管理员选择 + const handleOwnerAdminChange = (data: { + devices: string[]; + devicesOptions: DeviceSelectionItem[]; + admins: string[]; + adminsOptions: FriendSelectionItem[]; }) => { setFormData(prev => ({ ...prev, - deviceGroups: data.deviceGroups, + devices: data.devices, + admins: data.admins, })); - setDeviceGroupsOptions(data.deviceGroupsOptions); + setDevicesOptions(data.devicesOptions); + setAdminsOptions(data.adminsOptions); }; // 流量池包选择 @@ -116,8 +127,16 @@ const AutoGroupForm: React.FC = () => { Toast.show({ content: "请输入任务名称" }); return; } - if (formData.deviceGroups.length === 0) { - Toast.show({ content: "请选择至少一个设备组" }); + if (formData.devices.length === 0) { + Toast.show({ content: "请选择一个群主" }); + return; + } + if (formData.devices.length > 1) { + Toast.show({ content: "群主只能选择一个设备" }); + return; + } + if (formData.admins.length === 0) { + Toast.show({ content: "请至少选择一个管理员" }); return; } if (formData.poolGroups.length === 0) { @@ -127,9 +146,13 @@ const AutoGroupForm: React.FC = () => { setLoading(true); try { + // 构建提交数据,将devices映射为deviceGroups + const { devices, devicesOptions, ...restFormData } = formData; const submitData = { - ...formData, - deviceGroupsOptions: deviceGroupsOptions, + ...restFormData, + deviceGroups: devices, // 设备ID数组,传输字段名为deviceGroups + deviceGroupsOptions: devicesOptions, // 设备完整信息,传输字段名为deviceGroupsOptions + adminsOptions: adminsOptions, poolGroupsOptions: poolGroupsOptions, }; @@ -173,8 +196,9 @@ const AutoGroupForm: React.FC = () => { break; case 2: - // 调用 DeviceSelector 的表单校验 - isValid = (await deviceSelectorRef.current?.validate()) || false; + // 调用 OwnerAdminSelector 的表单校验 + isValid = + (await ownerAdminSelectorRef.current?.validate()) || false; if (isValid) { setCurrentStep(3); } @@ -217,10 +241,11 @@ const AutoGroupForm: React.FC = () => { ); case 2: return ( - ); case 3: diff --git a/Cunkebao/src/pages/mobile/workspace/auto-group/form/types.ts b/Cunkebao/src/pages/mobile/workspace/auto-group/form/types.ts index ba9cbf8a..87c18911 100644 --- a/Cunkebao/src/pages/mobile/workspace/auto-group/form/types.ts +++ b/Cunkebao/src/pages/mobile/workspace/auto-group/form/types.ts @@ -1,13 +1,16 @@ -import { DeviceSelectionItem } from "@/components/DeviceSelection/data"; import { PoolSelectionItem } from "@/components/PoolSelection/data"; +import { DeviceSelectionItem } from "@/components/DeviceSelection/data"; +import { FriendSelectionItem } from "@/components/FriendSelection/data"; // 自动建群表单数据类型定义 export interface AutoGroupFormData { id?: string; // 任务ID type: number; // 任务类型 name: string; // 任务名称 - deviceGroups: string[]; // 设备组 - deviceGroupsOptions: DeviceSelectionItem[]; // 设备组选项 + devices: string[]; // 群主ID列表(设备ID) + devicesOptions: DeviceSelectionItem[]; // 群主选项(设备) + admins: string[]; // 管理员ID列表(好友ID) + adminsOptions: FriendSelectionItem[]; // 管理员选项(好友) poolGroups: string[]; // 流量池 poolGroupsOptions: PoolSelectionItem[]; // 流量池选项 startTime: string; // 开始时间 (YYYY-MM-DD HH:mm:ss) @@ -34,9 +37,13 @@ export const formValidationRules = { { required: true, message: "请输入任务名称" }, { min: 2, max: 50, message: "任务名称长度应在2-50个字符之间" }, ], - deviceGroups: [ - { required: true, message: "请选择设备组" }, - { type: "array", min: 1, message: "至少选择一个设备组" }, + devices: [ + { required: true, message: "请选择群主" }, + { type: "array", min: 1, max: 1, message: "群主只能选择一个设备" }, + ], + admins: [ + { required: true, message: "请选择管理员" }, + { type: "array", min: 1, message: "至少选择一个管理员" }, ], poolGroups: [ { required: true, message: "请选择内容库" }, diff --git a/Cunkebao/src/pages/mobile/workspace/auto-like/list/index.tsx b/Cunkebao/src/pages/mobile/workspace/auto-like/list/index.tsx index a967f725..22ff25dc 100644 --- a/Cunkebao/src/pages/mobile/workspace/auto-like/list/index.tsx +++ b/Cunkebao/src/pages/mobile/workspace/auto-like/list/index.tsx @@ -116,20 +116,38 @@ const AutoLike: React.FC = () => { setLoading(true); try { const Res: any = await fetchAutoLikeTasks(); - // 直接就是任务数组,无需再解包 - const mappedTasks = Res?.list?.map((task: any) => ({ - ...task, - status: task.status || 2, // 默认为关闭状态 - deviceCount: task.deviceCount || 0, - targetGroup: task.targetGroup || "全部好友", - likeInterval: task.likeInterval || 60, - maxLikesPerDay: task.maxLikesPerDay || 100, - lastLikeTime: task.lastLikeTime || "暂无", - createTime: task.createTime || "", - updateTime: task.updateTime || "", - todayLikeCount: task.todayLikeCount || 0, - totalLikeCount: task.totalLikeCount || 0, - })); + // 数据在 data.list 中 + const taskList = Res?.data?.list || Res?.list || []; + const mappedTasks = taskList.map((task: any) => { + const config = task.config || {}; + const friends = config.friends || []; + const devices = config.devices || []; + + // 判断目标人群:如果 friends 为空或未设置,表示选择全部好友 + let targetGroup = "全部好友"; + if (friends.length > 0) { + targetGroup = `${friends.length} 个好友`; + } + + return { + id: task.id?.toString() || "", + name: task.name || "", + status: task.status === 1 ? 1 : 2, // 1: 开启, 2: 关闭 + deviceCount: devices.length, + targetGroup: targetGroup, + likeInterval: config.interval || 60, + maxLikesPerDay: config.maxLikes || 100, + lastLikeTime: task.lastLikeTime || "暂无", + createTime: task.createTime || "", + updateTime: task.updateTime || "", + todayLikeCount: config.todayLikeCount || 0, + totalLikeCount: config.totalLikeCount || 0, + // 保留原始数据 + config: config, + devices: devices, + friends: friends, + }; + }); setTasks(mappedTasks); } catch (error) { console.error("获取自动点赞任务失败:", error); @@ -355,7 +373,7 @@ const AutoLike: React.FC = () => { /> 今日点赞: - {task.lastLikeTime} + {task.todayLikeCount || 0}
diff --git a/Cunkebao/src/pages/mobile/workspace/auto-like/new/index.tsx b/Cunkebao/src/pages/mobile/workspace/auto-like/new/index.tsx index 779aecea..b02e5e61 100644 --- a/Cunkebao/src/pages/mobile/workspace/auto-like/new/index.tsx +++ b/Cunkebao/src/pages/mobile/workspace/auto-like/new/index.tsx @@ -36,6 +36,7 @@ const NewAutoLike: React.FC = () => { const [isSubmitting, setIsSubmitting] = useState(false); const [isLoading, setIsLoading] = useState(isEditMode); const [autoEnabled, setAutoEnabled] = useState(false); + const [selectAllFriends, setSelectAllFriends] = useState(false); const [formData, setFormData] = useState({ name: "", interval: 5, @@ -45,8 +46,8 @@ const NewAutoLike: React.FC = () => { contentTypes: ["text", "image", "video"], deviceGroups: [], deviceGroupsOptions: [], - friendsGroups: [], - friendsGroupsOptions: [], + wechatFriends: [], + wechatFriendsOptions: [], targetTags: [], friendMaxLikes: 10, enableFriendTags: false, @@ -74,8 +75,8 @@ const NewAutoLike: React.FC = () => { contentTypes: config.contentTypes || ["text", "image", "video"], deviceGroups: config.deviceGroups || [], deviceGroupsOptions: config.deviceGroupsOptions || [], - friendsGroups: config.friendsgroups || [], - friendsGroupsOptions: config.friendsGroupsOptions || [], + wechatFriends: config.wechatFriends || [], + wechatFriendsOptions: config.wechatFriendsOptions || [], targetTags: config.targetTags || [], friendMaxLikes: config.friendMaxLikes || 10, enableFriendTags: config.enableFriendTags || false, @@ -85,6 +86,10 @@ const NewAutoLike: React.FC = () => { (taskDetail as any).status === 1 || (taskDetail as any).status === "running", ); + // 如果 wechatFriends 为空或未设置,可能表示选择了全部好友 + setSelectAllFriends( + !config.wechatFriends || config.wechatFriends.length === 0 + ); } } catch (error) { message.error("获取任务详情失败"); @@ -127,11 +132,19 @@ const NewAutoLike: React.FC = () => { } setIsSubmitting(true); try { + // 如果选择了全部好友,提交时传空数组或特殊标识 + const submitData = { + ...formData, + wechatFriends: selectAllFriends ? [] : formData.wechatFriends, + wechatFriendsOptions: selectAllFriends ? [] : formData.wechatFriendsOptions, + selectAllFriends: selectAllFriends, // 添加标识字段 + }; + if (isEditMode) { - await updateAutoLikeTask({ ...formData, id }); + await updateAutoLikeTask({ ...submitData, id }); message.success("更新成功"); } else { - await createAutoLikeTask(formData); + await createAutoLikeTask(submitData); message.success("创建成功"); } navigate("/workspace/auto-like"); @@ -142,6 +155,28 @@ const NewAutoLike: React.FC = () => { } }; + // 选择全部好友(仅设置标识) + const handleSelectAllFriends = () => { + if (!formData.deviceGroups || formData.deviceGroups.length === 0) { + message.warning("请先选择执行设备"); + return; + } + + if (selectAllFriends) { + // 取消全选标识 + setSelectAllFriends(false); + // 清空已选好友 + handleUpdateFormData({ + wechatFriends: [], + wechatFriendsOptions: [], + }); + } else { + // 设置全选标识 + setSelectAllFriends(true); + message.success("已标记为选择全部好友"); + } + }; + // 步骤器 const renderStepIndicator = () => ( @@ -364,16 +399,39 @@ const NewAutoLike: React.FC = () => { const renderFriendSettings = () => (
- - handleUpdateFormData({ - friendsGroups: friends.map(f => f.id), - friendsGroupsOptions: friends, - }) - } - deviceIds={formData.deviceGroups} - /> +
+
选择好友
+ +
+ {selectAllFriends ? ( +
+ + 已标记为选择全部好友 +
+ ) : ( + { + handleUpdateFormData({ + wechatFriends: friends.map(f => f.id), + wechatFriendsOptions: friends, + }); + // 如果手动选择了好友,取消全选标识 + if (selectAllFriends) { + setSelectAllFriends(false); + } + }} + deviceIds={formData.deviceGroups} + /> + )}