diff --git a/Cunkebao/dist/.vite/manifest.json b/Cunkebao/dist/.vite/manifest.json index 4f0c258e..70d5d6b2 100644 --- a/Cunkebao/dist/.vite/manifest.json +++ b/Cunkebao/dist/.vite/manifest.json @@ -1,9 +1,9 @@ { - "_charts-TuAbbBZ5.js": { - "file": "assets/charts-TuAbbBZ5.js", + "_charts-D0fT04H8.js": { + "file": "assets/charts-D0fT04H8.js", "name": "charts", "imports": [ - "_ui-D1w-jetn.js", + "_ui-qLeQLv1F.js", "_vendor-2vc8h_ct.js" ] }, @@ -11,8 +11,8 @@ "file": "assets/ui-D0C0OGrH.css", "src": "_ui-D0C0OGrH.css" }, - "_ui-D1w-jetn.js": { - "file": "assets/ui-D1w-jetn.js", + "_ui-qLeQLv1F.js": { + "file": "assets/ui-qLeQLv1F.js", "name": "ui", "imports": [ "_vendor-2vc8h_ct.js" @@ -33,18 +33,18 @@ "name": "vendor" }, "index.html": { - "file": "assets/index-D3HSx5Yt.js", + "file": "assets/index-Cp05akVy.js", "name": "index", "src": "index.html", "isEntry": true, "imports": [ "_vendor-2vc8h_ct.js", - "_ui-D1w-jetn.js", + "_ui-qLeQLv1F.js", "_utils-6WF66_dS.js", - "_charts-TuAbbBZ5.js" + "_charts-D0fT04H8.js" ], "css": [ - "assets/index-B0SB167P.css" + "assets/index-Eg_DAu9e.css" ] } } \ No newline at end of file diff --git a/Cunkebao/dist/index.html b/Cunkebao/dist/index.html index f79f7013..6261e31e 100644 --- a/Cunkebao/dist/index.html +++ b/Cunkebao/dist/index.html @@ -10,14 +10,14 @@ } - - + + - + - + - +
diff --git a/Cunkebao/index.html b/Cunkebao/index.html index 92ab92a7..16de6c67 100644 --- a/Cunkebao/index.html +++ b/Cunkebao/index.html @@ -10,7 +10,7 @@ } - +
diff --git a/Cunkebao/src/components/DeviceSelection/data.ts b/Cunkebao/src/components/DeviceSelection/data.ts index abc9a214..e3e189e6 100644 --- a/Cunkebao/src/components/DeviceSelection/data.ts +++ b/Cunkebao/src/components/DeviceSelection/data.ts @@ -8,6 +8,8 @@ export interface DeviceSelectionItem { wxid?: string; nickname?: string; usedInPlans?: number; + avatar?: string; + totalFriend?: number; } // 组件属性接口 diff --git a/Cunkebao/src/components/DeviceSelection/index.module.scss b/Cunkebao/src/components/DeviceSelection/index.module.scss index ea776f81..8d004a48 100644 --- a/Cunkebao/src/components/DeviceSelection/index.module.scss +++ b/Cunkebao/src/components/DeviceSelection/index.module.scss @@ -67,60 +67,152 @@ } .deviceItem { display: flex; - align-items: flex-start; - gap: 12px; - padding: 16px; - border-radius: 12px; - border: 1px solid #f0f0f0; + flex-direction: column; + padding: 12px; background: #fff; - cursor: pointer; - transition: background 0.2s; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + transition: all 0.2s ease; + border: 1px solid #f5f5f5; + &:hover { - background: #f5f6fa; + transform: translateY(-1px); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); + } +} + +.headerRow { + display: flex; + align-items: center; + gap: 8px; +} + +.checkboxContainer { + flex-shrink: 0; +} + +.imeiText { + font-size: 13px; + color: #666; + font-family: monospace; + flex: 1; +} + +.mainContent { + display: flex; + align-items: center; + gap: 12px; + cursor: pointer; + padding: 8px; + border-radius: 8px; + transition: background-color 0.2s ease; + + &:hover { + background-color: #f8f9fa; } } .deviceCheckbox { - margin-top: 4px; + flex-shrink: 0; } .deviceInfo { flex: 1; + min-width: 0; + display: flex; + align-items: center; + gap: 12px; } +.deviceAvatar { + width: 64px; + height: 64px; + border-radius: 6px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + box-shadow: 0 2px 8px rgba(102, 126, 234, 0.25); + flex-shrink: 0; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } + + .avatarText { + font-size: 18px; + color: #fff; + font-weight: 700; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + } +} + +.deviceContent { + flex: 1; + min-width: 0; +} + .deviceInfoRow { display: flex; align-items: center; - justify-content: space-between; + gap: 6px; + margin-bottom: 6px; } .deviceName { - font-weight: 500; font-size: 16px; - color: #222; + font-weight: 600; + color: #1a1a1a; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .statusOnline { - width: 56px; - height: 24px; - border-radius: 12px; - background: #52c41a; - color: #fff; - font-size: 13px; - display: flex; - align-items: center; - justify-content: center; + font-size: 11px; + padding: 1px 6px; + border-radius: 8px; + color: #52c41a; + background: #f6ffed; + border: 1px solid #b7eb8f; + font-weight: 500; } .statusOffline { - width: 56px; - height: 24px; - border-radius: 12px; - background: #e5e6eb; - color: #888; - font-size: 13px; - display: flex; - align-items: center; - justify-content: center; + font-size: 11px; + padding: 1px 6px; + border-radius: 8px; + color: #ff4d4f; + background: #fff2f0; + border: 1px solid #ffccc7; + font-weight: 500; } .deviceInfoDetail { + display: flex; + flex-direction: column; + gap: 4px; +} + +.infoItem { + display: flex; + align-items: center; + gap: 8px; +} + +.infoLabel { font-size: 13px; - color: #888; - margin-top: 4px; + color: #666; + min-width: 50px; +} + +.infoValue { + font-size: 13px; + color: #333; + + &.imei { + font-family: monospace; + } + + &.friendCount { + font-weight: 500; + } } .loadingBox { display: flex; diff --git a/Cunkebao/src/components/DeviceSelection/index.tsx b/Cunkebao/src/components/DeviceSelection/index.tsx index 9d31e268..ba6952cd 100644 --- a/Cunkebao/src/components/DeviceSelection/index.tsx +++ b/Cunkebao/src/components/DeviceSelection/index.tsx @@ -46,6 +46,12 @@ const DeviceSelection: React.FC = ({ onSelect(selectedOptions.filter(v => v.id !== id)); }; + // 清除所有已选设备 + const handleClearAll = () => { + if (readonly) return; + onSelect([]); + }; + return ( <> {/* mode=input 显示输入框,mode=dialog不显示 */} @@ -57,6 +63,7 @@ const DeviceSelection: React.FC = ({ onClick={openPopup} prefix={} allowClear={!readonly} + onClear={handleClearAll} size="large" readOnly={readonly} disabled={readonly} @@ -86,11 +93,52 @@ const DeviceSelection: React.FC = ({ style={{ display: "flex", alignItems: "center", - padding: "4px 8px", + padding: "8px 12px", borderBottom: "1px solid #f0f0f0", fontSize: 14, }} > + {/* 头像 */} +
+ {device.avatar ? ( + 头像 + ) : ( + + {(device.memo || device.wechatId || "设")[0]} + + )} +
+
= ({ textOverflow: "ellipsis", }} > - 【 {device.memo}】 - {device.wechatId} + {device.memo} - {device.wechatId}
{!readonly && ( - - - -); + onConfirm, +}) => { + const [selectedDevices, setSelectedDevices] = useState( + [], + ); + const [packageId, setPackageId] = useState(""); + const [scenarioId, setScenarioId] = useState(""); + const [userValue, setUserValue] = useState(0); + const [userStatus, setUserStatus] = useState(0); + const [scenarioOptions, setScenarioOptions] = useState([]); + const [packageOptions, setPackageOptions] = useState([]); + + useEffect(() => { + if (visible) { + fetchScenarioOptions().then(res => { + setScenarioOptions(res); + }); + fetchPackageOptions().then(res => { + setPackageOptions(res); + }); + } + }, [visible]); + + const handleApply = () => { + const params = { + deviceIds: selectedDevices.map(d => d.id.toString()), + packageId, + scenarioId, + userValue, + userStatus, + }; + console.log(params); + + onConfirm(params); + onClose(); + }; + + const handleReset = () => { + setSelectedDevices([]); + setPackageId(""); + setScenarioId(""); + setUserValue(0); + setUserStatus(0); + }; + + return ( + +
+ 筛选选项 +
+
+
设备
+ +
+
+
流量池
+ ({ label: s.name, value: s.id })), + ]} + /> +
+
+
用户价值
+ setUserStatus(v as UserStatus)} + options={statusOptions} + /> +
+
+ + +
+
+ ); +}; export default FilterModal; diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/list/api.ts b/Cunkebao/src/pages/mobile/mine/traffic-pool/list/api.ts index 0a6162a8..a5b757ef 100644 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/list/api.ts +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/list/api.ts @@ -9,11 +9,10 @@ export function fetchTrafficPoolList(params: { return request("/v1/traffic/pool", params, "GET"); } -// 获取分组列表(如无真实接口可用mock) -export async function fetchPackageOptions(): Promise { - // TODO: 替换为真实接口 - return [ - { id: "pkg-1", name: "高价值客户池" }, - { id: "pkg-2", name: "测试流量池" }, - ]; +export async function fetchScenarioOptions(): Promise { + return request("/v1/plan/scenes", {}, "GET"); +} + +export async function fetchPackageOptions(): Promise { + return request("/v1/traffic/pool/getPackage", {}, "GET"); } diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/list/data.ts b/Cunkebao/src/pages/mobile/mine/traffic-pool/list/data.ts index f437b129..65ad7f55 100644 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/list/data.ts +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/list/data.ts @@ -43,3 +43,9 @@ export type ValueLevel = "all" | "high" | "medium" | "low"; // 状态类型 export type UserStatus = "all" | "added" | "pending" | "failed" | "duplicate"; + +// 获客场景类型 +export interface ScenarioOption { + id: string; + name: string; +} diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/list/dataAnyx.tsx b/Cunkebao/src/pages/mobile/mine/traffic-pool/list/dataAnyx.tsx index 7a84040a..6894ce1b 100644 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/list/dataAnyx.tsx +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/list/dataAnyx.tsx @@ -1,11 +1,16 @@ import { useState, useEffect, useMemo } from "react"; -import { fetchTrafficPoolList, fetchPackageOptions } from "./api"; +import { + fetchTrafficPoolList, + fetchPackageOptions, + fetchScenarioOptions, +} from "./api"; import type { TrafficPoolUser, DeviceOption, PackageOption, ValueLevel, UserStatus, + ScenarioOption, } from "./data"; import { Toast } from "antd-mobile"; @@ -19,12 +24,13 @@ export function useTrafficPoolListLogic() { // 筛选相关 const [showFilter, setShowFilter] = useState(false); - const [deviceOptions, setDeviceOptions] = useState([]); const [packageOptions, setPackageOptions] = useState([]); - const [deviceId, setDeviceId] = useState("all"); - const [packageId, setPackageId] = useState("all"); - const [valueLevel, setValueLevel] = useState("all"); - const [userStatus, setUserStatus] = useState("all"); + const [scenarioOptions, setScenarioOptions] = useState([]); + const [selectedDevices, setSelectedDevices] = useState([]); + const [packageId, setPackageId] = useState(0); + const [scenarioId, setScenarioId] = useState(0); + const [userValue, setUserValue] = useState(0); + const [userStatus, setUserStatus] = useState(0); // 批量相关 const [selectedIds, setSelectedIds] = useState([]); @@ -47,15 +53,22 @@ export function useTrafficPoolListLogic() { const getList = async () => { setLoading(true); try { - const res = await fetchTrafficPoolList({ + const params: any = { page, pageSize, keyword: search, - // deviceId, - // packageId, - // valueLevel, - // userStatus, - }); + packageId, + taskId: scenarioId, + userValue, + addStatus: userStatus, + }; + + // 添加筛选参数 + if (selectedDevices.length > 0) { + params.deviceId = selectedDevices.map(d => d.id).join(","); + } + + const res = await fetchTrafficPoolList(params); setList(res.list || []); setTotal(res.total || 0); } finally { @@ -66,13 +79,22 @@ export function useTrafficPoolListLogic() { // 获取筛选项 useEffect(() => { fetchPackageOptions().then(setPackageOptions); + fetchScenarioOptions().then(setScenarioOptions); }, []); // 筛选条件变化时刷新列表 useEffect(() => { getList(); // eslint-disable-next-line - }, [page, search /*, deviceId, packageId, valueLevel, userStatus*/]); + }, [ + page, + search, + selectedDevices, + packageId, + scenarioId, + userValue, + userStatus, + ]); // 全选/反选 const handleSelectAll = (checked: boolean) => { @@ -108,10 +130,11 @@ export function useTrafficPoolListLogic() { // 筛选重置 const resetFilter = () => { - setDeviceId("all"); - setPackageId("all"); - setValueLevel("all"); - setUserStatus("all"); + setSelectedDevices([]); + setPackageId(0); + setScenarioId(0); + setUserValue(0); + setUserStatus(0); }; return { @@ -125,14 +148,16 @@ export function useTrafficPoolListLogic() { setSearch, showFilter, setShowFilter, - deviceOptions, packageOptions, - deviceId, - setDeviceId, + scenarioOptions, + selectedDevices, + setSelectedDevices, packageId, setPackageId, - valueLevel, - setValueLevel, + scenarioId, + setScenarioId, + userValue, + setUserValue, userStatus, setUserStatus, selectedIds, diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx b/Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx index 7e000524..a2a2edec 100644 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx @@ -5,9 +5,9 @@ import { ReloadOutlined, BarChartOutlined, } from "@ant-design/icons"; -import { Input, Button, Checkbox } from "antd"; +import { Input, Button, Checkbox, Pagination } from "antd"; import styles from "./index.module.scss"; -import { List, Empty, Avatar, Modal, Selector, Toast, Card } from "antd-mobile"; +import { Empty, Avatar } from "antd-mobile"; import { useNavigate } from "react-router-dom"; import NavCommon from "@/components/NavCommon"; import { useTrafficPoolListLogic } from "./dataAnyx"; @@ -18,20 +18,6 @@ import BatchAddModal from "./BatchAddModal"; const defaultAvatar = "https://cdn.jsdelivr.net/gh/maokaka/static/avatar-default.png"; -const valueLevelOptions = [ - { label: "全部", value: "all" }, - { label: "高价值", value: "high" }, - { label: "中价值", value: "medium" }, - { label: "低价值", value: "low" }, -]; -const statusOptions = [ - { label: "全部", value: "all" }, - { label: "已添加", value: "added" }, - { label: "待添加", value: "pending" }, - { label: "添加失败", value: "failed" }, - { label: "重复", value: "duplicate" }, -]; - const TrafficPoolList: React.FC = () => { const navigate = useNavigate(); const { @@ -45,15 +31,12 @@ const TrafficPoolList: React.FC = () => { setSearch, showFilter, setShowFilter, - deviceOptions, packageOptions, - deviceId, - setDeviceId, - packageId, + scenarioOptions, + setSelectedDevices, setPackageId, - valueLevel, - setValueLevel, - userStatus, + setScenarioId, + setUserValue, setUserStatus, selectedIds, handleSelectAll, @@ -67,7 +50,6 @@ const TrafficPoolList: React.FC = () => { setShowStats, stats, getList, - resetFilter, } = useTrafficPoolListLogic(); return ( @@ -154,6 +136,17 @@ const TrafficPoolList: React.FC = () => { } + footer={ +
+ +
+ } > {/* 批量加入分组弹窗 */} { setShowFilter(false)} - deviceOptions={deviceOptions} - packageOptions={packageOptions} - deviceId={deviceId} - setDeviceId={setDeviceId} - packageId={packageId} - setPackageId={setPackageId} - valueLevel={valueLevel} - setValueLevel={setValueLevel} - userStatus={userStatus} - setUserStatus={setUserStatus} - onReset={resetFilter} + onConfirm={filters => { + // 更新筛选条件 + setSelectedDevices( + filters.deviceIds.map(id => ({ + id: parseInt(id), + memo: "", + imei: "", + wechatId: "", + status: "offline" as const, + })), + ); + setPackageId(filters.packageId); + setScenarioId(filters.scenarioId); + setUserValue(filters.userValue); + setUserStatus(filters.userStatus); + // 重新获取列表 + getList(); + }} + scenarioOptions={scenarioOptions} />
{list.length === 0 && !loading ? ( @@ -192,7 +193,9 @@ const TrafficPoolList: React.FC = () => { className={styles.card} style={{ cursor: "pointer" }} onClick={() => - navigate(`/traffic-pool/detail/${item.sourceId}/${item.id}`) + navigate( + `/mine/traffic-pool/detail/${item.sourceId}/${item.id}`, + ) } >
@@ -235,23 +238,6 @@ const TrafficPoolList: React.FC = () => {
)}
- {/* 分页 */} - {total > pageSize && ( -
- - - {page} / {Math.ceil(total / pageSize)} - - -
- )} ); }; diff --git a/Cunkebao/src/router/module/mine.tsx b/Cunkebao/src/router/module/mine.tsx index e41f455d..e5c33623 100644 --- a/Cunkebao/src/router/module/mine.tsx +++ b/Cunkebao/src/router/module/mine.tsx @@ -36,12 +36,12 @@ const routes = [ auth: true, }, { - path: "/traffic-pool", + path: "/mine/traffic-pool", element: , auth: true, }, { - path: "/traffic-pool/detail/:wxid/:userId", + path: "/mine/traffic-pool/detail/:wxid/:userId", element: , auth: true, }, diff --git a/Cunkebao/src/types/device.ts b/Cunkebao/src/types/device.ts index b48295ba..09425d39 100644 --- a/Cunkebao/src/types/device.ts +++ b/Cunkebao/src/types/device.ts @@ -11,6 +11,7 @@ export interface Device { nickname?: string; battery?: number; lastActive?: string; + avatar?: string; features?: { autoAddFriend?: boolean; autoReply?: boolean;