Merge branch 'yongpxu-dev' into yongpxu-dev2
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
# 基础环境变量示例
|
# 基础环境变量示例
|
||||||
VITE_API_BASE_URL=http://www.yishi.com
|
VITE_API_BASE_URL=http://www.yishi.com
|
||||||
# VITE_API_BASE_URL=https://ckbapi.quwanzhi.com
|
|
||||||
VITE_APP_TITLE=Nkebao Base
|
VITE_APP_TITLE=Nkebao Base
|
||||||
|
|
||||||
|
|||||||
26
nkebao/src/components/DeviceSelection/data.ts
Normal file
26
nkebao/src/components/DeviceSelection/data.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// 设备选择项接口
|
||||||
|
export interface DeviceSelectionItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
imei: string;
|
||||||
|
wechatId: string;
|
||||||
|
status: "online" | "offline";
|
||||||
|
wxid?: string;
|
||||||
|
nickname?: string;
|
||||||
|
usedInPlans?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件属性接口
|
||||||
|
export interface DeviceSelectionProps {
|
||||||
|
selectedDevices: string[];
|
||||||
|
onSelect: (devices: string[]) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
mode?: "input" | "dialog"; // 新增,默认input
|
||||||
|
open?: boolean; // 仅mode=dialog时生效
|
||||||
|
onOpenChange?: (open: boolean) => void; // 仅mode=dialog时生效
|
||||||
|
selectedListMaxHeight?: number; // 新增,已选列表最大高度,默认500
|
||||||
|
showInput?: boolean; // 新增
|
||||||
|
showSelectedList?: boolean; // 新增
|
||||||
|
readonly?: boolean; // 新增
|
||||||
|
}
|
||||||
@@ -19,12 +19,7 @@
|
|||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popupContainer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100vh;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
.popupHeader {
|
.popupHeader {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|||||||
@@ -1,39 +1,10 @@
|
|||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, { useState } from "react";
|
||||||
import { SearchOutlined, ReloadOutlined } from "@ant-design/icons";
|
import { SearchOutlined } from "@ant-design/icons";
|
||||||
import { Checkbox, Popup, Toast } from "antd-mobile";
|
|
||||||
import { Input, Button } from "antd";
|
import { Input, Button } from "antd";
|
||||||
import { getDeviceList } from "./api";
|
|
||||||
import style from "./index.module.scss";
|
|
||||||
import { DeleteOutlined } from "@ant-design/icons";
|
import { DeleteOutlined } from "@ant-design/icons";
|
||||||
|
import { DeviceSelectionProps } from "./data";
|
||||||
// 设备选择项接口
|
import SelectionPopup from "./selectionPopup";
|
||||||
interface DeviceSelectionItem {
|
import style from "./index.module.scss";
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
imei: string;
|
|
||||||
wechatId: string;
|
|
||||||
status: "online" | "offline";
|
|
||||||
wxid?: string;
|
|
||||||
nickname?: string;
|
|
||||||
usedInPlans?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件属性接口
|
|
||||||
interface DeviceSelectionProps {
|
|
||||||
selectedDevices: string[];
|
|
||||||
onSelect: (devices: string[]) => void;
|
|
||||||
placeholder?: string;
|
|
||||||
className?: string;
|
|
||||||
mode?: "input" | "dialog"; // 新增,默认input
|
|
||||||
open?: boolean; // 仅mode=dialog时生效
|
|
||||||
onOpenChange?: (open: boolean) => void; // 仅mode=dialog时生效
|
|
||||||
selectedListMaxHeight?: number; // 新增,已选列表最大高度,默认500
|
|
||||||
showInput?: boolean; // 新增
|
|
||||||
showSelectedList?: boolean; // 新增
|
|
||||||
readonly?: boolean; // 新增
|
|
||||||
}
|
|
||||||
|
|
||||||
const PAGE_SIZE = 20;
|
|
||||||
|
|
||||||
const DeviceSelection: React.FC<DeviceSelectionProps> = ({
|
const DeviceSelection: React.FC<DeviceSelectionProps> = ({
|
||||||
selectedDevices,
|
selectedDevices,
|
||||||
@@ -57,92 +28,10 @@ const DeviceSelection: React.FC<DeviceSelectionProps> = ({
|
|||||||
if (!isDialog) setPopupVisible(v);
|
if (!isDialog) setPopupVisible(v);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 设备数据
|
// 打开弹窗
|
||||||
const [devices, setDevices] = useState<DeviceSelectionItem[]>([]);
|
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
|
||||||
const [statusFilter, setStatusFilter] = useState("all");
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
|
||||||
const [total, setTotal] = useState(0);
|
|
||||||
|
|
||||||
// 获取设备列表,支持keyword和分页
|
|
||||||
const fetchDevices = useCallback(
|
|
||||||
async (keyword: string = "", page: number = 1) => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await getDeviceList({
|
|
||||||
page,
|
|
||||||
limit: PAGE_SIZE,
|
|
||||||
keyword: keyword.trim() || undefined,
|
|
||||||
});
|
|
||||||
if (res && Array.isArray(res.list)) {
|
|
||||||
setDevices(
|
|
||||||
res.list.map((d: any) => ({
|
|
||||||
id: d.id?.toString() || "",
|
|
||||||
name: d.memo || d.imei || "",
|
|
||||||
imei: d.imei || "",
|
|
||||||
wechatId: d.wechatId || "",
|
|
||||||
status: d.alive === 1 ? "online" : "offline",
|
|
||||||
wxid: d.wechatId || "",
|
|
||||||
nickname: d.nickname || "",
|
|
||||||
usedInPlans: d.usedInPlans || 0,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
setTotal(res.total || 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("获取设备列表失败:", error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
// 打开弹窗时获取第一页
|
|
||||||
const openPopup = () => {
|
const openPopup = () => {
|
||||||
if (readonly) return;
|
if (readonly) return;
|
||||||
setSearchQuery("");
|
|
||||||
setCurrentPage(1);
|
|
||||||
setRealVisible(true);
|
setRealVisible(true);
|
||||||
fetchDevices("", 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 搜索防抖
|
|
||||||
useEffect(() => {
|
|
||||||
if (!realVisible) return;
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setCurrentPage(1);
|
|
||||||
fetchDevices(searchQuery, 1);
|
|
||||||
}, 500);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [searchQuery, realVisible, fetchDevices]);
|
|
||||||
|
|
||||||
// 翻页时重新请求
|
|
||||||
useEffect(() => {
|
|
||||||
if (!realVisible) return;
|
|
||||||
fetchDevices(searchQuery, currentPage);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [currentPage]);
|
|
||||||
|
|
||||||
// 过滤设备(只保留状态过滤)
|
|
||||||
const filteredDevices = devices.filter((device) => {
|
|
||||||
const matchesStatus =
|
|
||||||
statusFilter === "all" ||
|
|
||||||
(statusFilter === "online" && device.status === "online") ||
|
|
||||||
(statusFilter === "offline" && device.status === "offline");
|
|
||||||
return matchesStatus;
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
|
||||||
|
|
||||||
// 处理设备选择
|
|
||||||
const handleDeviceToggle = (deviceId: string) => {
|
|
||||||
if (selectedDevices.includes(deviceId)) {
|
|
||||||
onSelect(selectedDevices.filter((id) => id !== deviceId));
|
|
||||||
} else {
|
|
||||||
onSelect([...selectedDevices, deviceId]);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取显示文本
|
// 获取显示文本
|
||||||
@@ -151,138 +40,12 @@ const DeviceSelection: React.FC<DeviceSelectionProps> = ({
|
|||||||
return `已选择 ${selectedDevices.length} 个设备`;
|
return `已选择 ${selectedDevices.length} 个设备`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取已选设备详细信息
|
|
||||||
const selectedDeviceObjs = selectedDevices
|
|
||||||
.map((id) => devices.find((d) => d.id === id))
|
|
||||||
.filter(Boolean) as DeviceSelectionItem[];
|
|
||||||
|
|
||||||
// 删除已选设备
|
// 删除已选设备
|
||||||
const handleRemoveDevice = (id: string) => {
|
const handleRemoveDevice = (id: string) => {
|
||||||
if (readonly) return;
|
if (readonly) return;
|
||||||
onSelect(selectedDevices.filter((d) => d !== id));
|
onSelect(selectedDevices.filter((d) => d !== id));
|
||||||
};
|
};
|
||||||
|
|
||||||
// 弹窗内容
|
|
||||||
const popupContent = (
|
|
||||||
<div className={style.popupContainer}>
|
|
||||||
<div className={style.popupHeader}>
|
|
||||||
<div className={style.popupTitle}>选择设备</div>
|
|
||||||
</div>
|
|
||||||
<div className={style.popupSearchRow}>
|
|
||||||
<div className={style.popupSearchInputWrap}>
|
|
||||||
<SearchOutlined className={style.inputIcon} />
|
|
||||||
<Input
|
|
||||||
placeholder="搜索设备IMEI/备注/微信号"
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(val) => setSearchQuery(val)}
|
|
||||||
className={style.popupSearchInput}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<select
|
|
||||||
value={statusFilter}
|
|
||||||
onChange={(e) => setStatusFilter(e.target.value)}
|
|
||||||
className={style.statusSelect}
|
|
||||||
>
|
|
||||||
<option value="all">全部状态</option>
|
|
||||||
<option value="online">在线</option>
|
|
||||||
<option value="offline">离线</option>
|
|
||||||
</select>
|
|
||||||
<Button
|
|
||||||
fill="outline"
|
|
||||||
size="mini"
|
|
||||||
onClick={() => fetchDevices(searchQuery, currentPage)}
|
|
||||||
disabled={loading}
|
|
||||||
className={style.refreshBtn}
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<div className={style.loadingIcon}>⟳</div>
|
|
||||||
) : (
|
|
||||||
<ReloadOutlined />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className={style.deviceList}>
|
|
||||||
{loading ? (
|
|
||||||
<div className={style.loadingBox}>
|
|
||||||
<div className={style.loadingText}>加载中...</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className={style.deviceListInner}>
|
|
||||||
{filteredDevices.map((device) => (
|
|
||||||
<label key={device.id} className={style.deviceItem}>
|
|
||||||
<Checkbox
|
|
||||||
checked={selectedDevices.includes(device.id)}
|
|
||||||
onChange={() => handleDeviceToggle(device.id)}
|
|
||||||
className={style.deviceCheckbox}
|
|
||||||
/>
|
|
||||||
<div className={style.deviceInfo}>
|
|
||||||
<div className={style.deviceInfoRow}>
|
|
||||||
<span className={style.deviceName}>{device.name}</span>
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
device.status === "online"
|
|
||||||
? style.statusOnline
|
|
||||||
: style.statusOffline
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{device.status === "online" ? "在线" : "离线"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={style.deviceInfoDetail}>
|
|
||||||
<div>IMEI: {device.imei}</div>
|
|
||||||
<div>微信号: {device.wechatId}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{/* 分页栏 */}
|
|
||||||
<div className={style.paginationRow}>
|
|
||||||
<div className={style.totalCount}>总计 {total} 个设备</div>
|
|
||||||
<div className={style.paginationControls}>
|
|
||||||
<Button
|
|
||||||
fill="none"
|
|
||||||
size="mini"
|
|
||||||
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
|
|
||||||
disabled={currentPage === 1 || loading}
|
|
||||||
className={style.pageBtn}
|
|
||||||
>
|
|
||||||
<
|
|
||||||
</Button>
|
|
||||||
<span className={style.pageInfo}>
|
|
||||||
{currentPage} / {totalPages}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
fill="none"
|
|
||||||
size="mini"
|
|
||||||
onClick={() =>
|
|
||||||
setCurrentPage(Math.min(totalPages, currentPage + 1))
|
|
||||||
}
|
|
||||||
disabled={currentPage === totalPages || loading}
|
|
||||||
className={style.pageBtn}
|
|
||||||
>
|
|
||||||
>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={style.popupFooter}>
|
|
||||||
<div className={style.selectedCount}>
|
|
||||||
已选择 {selectedDevices.length} 个设备
|
|
||||||
</div>
|
|
||||||
<div className={style.footerBtnGroup}>
|
|
||||||
<Button fill="outline" onClick={() => setRealVisible(false)}>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button color="primary" onClick={() => setRealVisible(false)}>
|
|
||||||
确定
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* mode=input 显示输入框,mode=dialog不显示 */}
|
{/* mode=input 显示输入框,mode=dialog不显示 */}
|
||||||
@@ -304,75 +67,71 @@ const DeviceSelection: React.FC<DeviceSelectionProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* 已选设备列表窗口 */}
|
{/* 已选设备列表窗口 */}
|
||||||
{mode === "input" &&
|
{mode === "input" && showSelectedList && selectedDevices.length > 0 && (
|
||||||
showSelectedList &&
|
<div
|
||||||
selectedDeviceObjs.length > 0 && (
|
className={style.selectedListWindow}
|
||||||
<div
|
style={{
|
||||||
className={style.selectedListWindow}
|
maxHeight: selectedListMaxHeight,
|
||||||
style={{
|
overflowY: "auto",
|
||||||
maxHeight: selectedListMaxHeight,
|
marginTop: 8,
|
||||||
overflowY: "auto",
|
border: "1px solid #e5e6eb",
|
||||||
marginTop: 8,
|
borderRadius: 8,
|
||||||
border: "1px solid #e5e6eb",
|
background: "#fff",
|
||||||
borderRadius: 8,
|
}}
|
||||||
background: "#fff",
|
>
|
||||||
}}
|
{selectedDevices.map((deviceId) => (
|
||||||
>
|
<div
|
||||||
{selectedDeviceObjs.map((device) => (
|
key={deviceId}
|
||||||
|
className={style.selectedListRow}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "4px 8px",
|
||||||
|
borderBottom: "1px solid #f0f0f0",
|
||||||
|
fontSize: 14,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
key={device.id}
|
|
||||||
className={style.selectedListRow}
|
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
flex: 1,
|
||||||
alignItems: "center",
|
minWidth: 0,
|
||||||
padding: "4px 8px",
|
whiteSpace: "nowrap",
|
||||||
borderBottom: "1px solid #f0f0f0",
|
overflow: "hidden",
|
||||||
fontSize: 14,
|
textOverflow: "ellipsis",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
{deviceId}
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 0,
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
overflow: "hidden",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{device.name}
|
|
||||||
</div>
|
|
||||||
{!readonly && (
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={<DeleteOutlined />}
|
|
||||||
size="small"
|
|
||||||
style={{
|
|
||||||
marginLeft: 4,
|
|
||||||
color: "#ff4d4f",
|
|
||||||
border: "none",
|
|
||||||
background: "none",
|
|
||||||
minWidth: 24,
|
|
||||||
height: 24,
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
onClick={() => handleRemoveDevice(device.id)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
{!readonly && (
|
||||||
</div>
|
<Button
|
||||||
)}
|
type="text"
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
size="small"
|
||||||
|
style={{
|
||||||
|
marginLeft: 4,
|
||||||
|
color: "#ff4d4f",
|
||||||
|
border: "none",
|
||||||
|
background: "none",
|
||||||
|
minWidth: 24,
|
||||||
|
height: 24,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
onClick={() => handleRemoveDevice(deviceId)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* 弹窗 */}
|
{/* 弹窗 */}
|
||||||
<Popup
|
<SelectionPopup
|
||||||
visible={realVisible && !readonly}
|
visible={realVisible && !readonly}
|
||||||
onMaskClick={() => setRealVisible(false)}
|
onClose={() => setRealVisible(false)}
|
||||||
position="bottom"
|
selectedDevices={selectedDevices}
|
||||||
bodyStyle={{ height: "100vh" }}
|
onSelect={onSelect}
|
||||||
>
|
/>
|
||||||
{popupContent}
|
|
||||||
</Popup>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import React from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
import { SearchOutlined, ReloadOutlined } from "@ant-design/icons";
|
import { Checkbox, Popup } from "antd-mobile";
|
||||||
import { Input, Button, Checkbox, Popup } from "antd-mobile";
|
import { getDeviceList } from "./api";
|
||||||
import style from "./index.module.scss";
|
import style from "./index.module.scss";
|
||||||
|
import Layout from "@/components/Layout/Layout";
|
||||||
|
import PopupHeader from "@/components/PopuLayout/header";
|
||||||
|
import PopupFooter from "@/components/PopuLayout/footer";
|
||||||
|
|
||||||
interface DeviceSelectionItem {
|
interface DeviceSelectionItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -19,42 +22,95 @@ interface SelectionPopupProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
selectedDevices: string[];
|
selectedDevices: string[];
|
||||||
onSelect: (devices: string[]) => void;
|
onSelect: (devices: string[]) => void;
|
||||||
devices: DeviceSelectionItem[];
|
|
||||||
loading: boolean;
|
|
||||||
searchQuery: string;
|
|
||||||
setSearchQuery: (v: string) => void;
|
|
||||||
statusFilter: string;
|
|
||||||
setStatusFilter: (v: string) => void;
|
|
||||||
onRefresh: () => void;
|
|
||||||
filteredDevices: DeviceSelectionItem[];
|
|
||||||
total: number;
|
|
||||||
currentPage: number;
|
|
||||||
totalPages: number;
|
|
||||||
setCurrentPage: (v: number) => void;
|
|
||||||
onCancel: () => void;
|
|
||||||
onConfirm: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PAGE_SIZE = 20;
|
||||||
|
|
||||||
const SelectionPopup: React.FC<SelectionPopupProps> = ({
|
const SelectionPopup: React.FC<SelectionPopupProps> = ({
|
||||||
visible,
|
visible,
|
||||||
onClose,
|
onClose,
|
||||||
selectedDevices,
|
selectedDevices,
|
||||||
onSelect,
|
onSelect,
|
||||||
devices,
|
|
||||||
loading,
|
|
||||||
searchQuery,
|
|
||||||
setSearchQuery,
|
|
||||||
statusFilter,
|
|
||||||
setStatusFilter,
|
|
||||||
onRefresh,
|
|
||||||
filteredDevices,
|
|
||||||
total,
|
|
||||||
currentPage,
|
|
||||||
totalPages,
|
|
||||||
setCurrentPage,
|
|
||||||
onCancel,
|
|
||||||
onConfirm,
|
|
||||||
}) => {
|
}) => {
|
||||||
|
// 设备数据
|
||||||
|
const [devices, setDevices] = useState<DeviceSelectionItem[]>([]);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [statusFilter, setStatusFilter] = useState("all");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
|
||||||
|
// 获取设备列表,支持keyword和分页
|
||||||
|
const fetchDevices = useCallback(
|
||||||
|
async (keyword: string = "", page: number = 1) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await getDeviceList({
|
||||||
|
page,
|
||||||
|
limit: PAGE_SIZE,
|
||||||
|
keyword: keyword.trim() || undefined,
|
||||||
|
});
|
||||||
|
if (res && Array.isArray(res.list)) {
|
||||||
|
setDevices(
|
||||||
|
res.list.map((d: any) => ({
|
||||||
|
id: d.id?.toString() || "",
|
||||||
|
name: d.memo || d.imei || "",
|
||||||
|
imei: d.imei || "",
|
||||||
|
wechatId: d.wechatId || "",
|
||||||
|
status: d.alive === 1 ? "online" : "offline",
|
||||||
|
wxid: d.wechatId || "",
|
||||||
|
nickname: d.nickname || "",
|
||||||
|
usedInPlans: d.usedInPlans || 0,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
setTotal(res.total || 0);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取设备列表失败:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 打开弹窗时获取第一页
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible) {
|
||||||
|
setSearchQuery("");
|
||||||
|
setCurrentPage(1);
|
||||||
|
fetchDevices("", 1);
|
||||||
|
}
|
||||||
|
}, [visible, fetchDevices]);
|
||||||
|
|
||||||
|
// 搜索防抖
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible) return;
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setCurrentPage(1);
|
||||||
|
fetchDevices(searchQuery, 1);
|
||||||
|
}, 500);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [searchQuery, visible, fetchDevices]);
|
||||||
|
|
||||||
|
// 翻页时重新请求
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible) return;
|
||||||
|
fetchDevices(searchQuery, currentPage);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [currentPage]);
|
||||||
|
|
||||||
|
// 过滤设备(只保留状态过滤)
|
||||||
|
const filteredDevices = devices.filter((device) => {
|
||||||
|
const matchesStatus =
|
||||||
|
statusFilter === "all" ||
|
||||||
|
(statusFilter === "online" && device.status === "online") ||
|
||||||
|
(statusFilter === "offline" && device.status === "offline");
|
||||||
|
return matchesStatus;
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
||||||
|
|
||||||
// 处理设备选择
|
// 处理设备选择
|
||||||
const handleDeviceToggle = (deviceId: string) => {
|
const handleDeviceToggle = (deviceId: string) => {
|
||||||
if (selectedDevices.includes(deviceId)) {
|
if (selectedDevices.includes(deviceId)) {
|
||||||
@@ -67,49 +123,45 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
|
|||||||
return (
|
return (
|
||||||
<Popup
|
<Popup
|
||||||
visible={visible}
|
visible={visible}
|
||||||
// 禁止点击遮罩关闭
|
onMaskClick={onClose}
|
||||||
onMaskClick={() => {}}
|
|
||||||
position="bottom"
|
position="bottom"
|
||||||
bodyStyle={{ height: "100vh" }}
|
bodyStyle={{ height: "100vh" }}
|
||||||
closeOnMaskClick={false}
|
closeOnMaskClick={false}
|
||||||
>
|
>
|
||||||
<div className={style.popupContainer}>
|
<Layout
|
||||||
<div className={style.popupHeader}>
|
header={
|
||||||
<div className={style.popupTitle}>选择设备</div>
|
<PopupHeader
|
||||||
</div>
|
title="选择设备"
|
||||||
<div className={style.popupSearchRow}>
|
searchQuery={searchQuery}
|
||||||
<div className={style.popupSearchInputWrap}>
|
setSearchQuery={setSearchQuery}
|
||||||
<SearchOutlined className={style.inputIcon} />
|
searchPlaceholder="搜索设备IMEI/备注/微信号"
|
||||||
<Input
|
loading={loading}
|
||||||
placeholder="搜索设备IMEI/备注/微信号"
|
onRefresh={() => fetchDevices(searchQuery, currentPage)}
|
||||||
value={searchQuery}
|
showTabs={true}
|
||||||
onChange={setSearchQuery}
|
tabsConfig={{
|
||||||
className={style.popupSearchInput}
|
activeKey: statusFilter,
|
||||||
/>
|
onChange: setStatusFilter,
|
||||||
</div>
|
tabs: [
|
||||||
<select
|
{ title: "全部", key: "all" },
|
||||||
value={statusFilter}
|
{ title: "在线", key: "online" },
|
||||||
onChange={(e) => setStatusFilter(e.target.value)}
|
{ title: "离线", key: "offline" },
|
||||||
className={style.statusSelect}
|
],
|
||||||
>
|
}}
|
||||||
<option value="all">全部状态</option>
|
/>
|
||||||
<option value="online">在线</option>
|
}
|
||||||
<option value="offline">离线</option>
|
footer={
|
||||||
</select>
|
<PopupFooter
|
||||||
<Button
|
total={total}
|
||||||
fill="outline"
|
currentPage={currentPage}
|
||||||
size="mini"
|
totalPages={totalPages}
|
||||||
onClick={onRefresh}
|
loading={loading}
|
||||||
disabled={loading}
|
selectedCount={selectedDevices.length}
|
||||||
className={style.refreshBtn}
|
onPageChange={setCurrentPage}
|
||||||
>
|
onCancel={onClose}
|
||||||
{loading ? (
|
onConfirm={onClose}
|
||||||
<div className={style.loadingIcon}>⟳</div>
|
/>
|
||||||
) : (
|
}
|
||||||
<ReloadOutlined />
|
>
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className={style.deviceList}>
|
<div className={style.deviceList}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className={style.loadingBox}>
|
<div className={style.loadingBox}>
|
||||||
@@ -147,49 +199,7 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* 分页栏 */}
|
</Layout>
|
||||||
<div className={style.paginationRow}>
|
|
||||||
<div className={style.totalCount}>总计 {total} 个设备</div>
|
|
||||||
<div className={style.paginationControls}>
|
|
||||||
<Button
|
|
||||||
fill="none"
|
|
||||||
size="mini"
|
|
||||||
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
|
|
||||||
disabled={currentPage === 1 || loading}
|
|
||||||
className={style.pageBtn}
|
|
||||||
>
|
|
||||||
<
|
|
||||||
</Button>
|
|
||||||
<span className={style.pageInfo}>
|
|
||||||
{currentPage} / {totalPages}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
fill="none"
|
|
||||||
size="mini"
|
|
||||||
onClick={() =>
|
|
||||||
setCurrentPage(Math.min(totalPages, currentPage + 1))
|
|
||||||
}
|
|
||||||
disabled={currentPage === totalPages || loading}
|
|
||||||
className={style.pageBtn}
|
|
||||||
>
|
|
||||||
>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={style.popupFooter}>
|
|
||||||
<div className={style.selectedCount}>
|
|
||||||
已选择 {selectedDevices.length} 个设备
|
|
||||||
</div>
|
|
||||||
<div className={style.footerBtnGroup}>
|
|
||||||
<Button fill="outline" onClick={onCancel}>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button color="primary" onClick={onConfirm}>
|
|
||||||
确定
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Popup>
|
</Popup>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { SearchOutlined, DeleteOutlined } from "@ant-design/icons";
|
import { SearchOutlined, DeleteOutlined } from "@ant-design/icons";
|
||||||
import { Popup, Toast } from "antd-mobile";
|
import { Popup } from "antd-mobile";
|
||||||
import { Button, Input } from "antd";
|
import { Button, Input } from "antd";
|
||||||
import { getFriendList } from "./api";
|
import { getFriendList } from "./api";
|
||||||
import style from "./index.module.scss";
|
import style from "./index.module.scss";
|
||||||
|
import Layout from "@/components/Layout/Layout";
|
||||||
|
import PopupHeader from "@/components/PopuLayout/header";
|
||||||
|
import PopupFooter from "@/components/PopuLayout/footer";
|
||||||
|
|
||||||
// 微信好友接口类型
|
// 微信好友接口类型
|
||||||
interface WechatFriend {
|
interface WechatFriend {
|
||||||
@@ -104,35 +107,18 @@ export default function FriendSelection({
|
|||||||
params.keyword = keyword.trim();
|
params.keyword = keyword.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enableDeviceFilter) {
|
if (enableDeviceFilter && deviceIds.length > 0) {
|
||||||
if (deviceIds.length === 0) {
|
params.deviceIds = deviceIds;
|
||||||
setFriends([]);
|
|
||||||
setTotalFriends(0);
|
|
||||||
setTotalPages(1);
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
params.deviceIds = deviceIds.join(",");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await getFriendList(params);
|
const response = await getFriendList(params);
|
||||||
|
if (response && response.list) {
|
||||||
if (res && Array.isArray(res.list)) {
|
setFriends(response.list);
|
||||||
setFriends(
|
setTotalFriends(response.total || 0);
|
||||||
res.list.map((friend: any) => ({
|
setTotalPages(Math.ceil((response.total || 0) / 20));
|
||||||
id: friend.id?.toString() || "",
|
|
||||||
nickname: friend.nickname || "",
|
|
||||||
wechatId: friend.wechatId || "",
|
|
||||||
avatar: friend.avatar || "",
|
|
||||||
customer: friend.customer || "",
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
setTotalFriends(res.total || 0);
|
|
||||||
setTotalPages(Math.ceil((res.total || 0) / 20));
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取好友列表失败:", error);
|
console.error("获取好友列表失败:", error);
|
||||||
Toast.show({ content: "获取好友列表失败", position: "top" });
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -140,16 +126,20 @@ export default function FriendSelection({
|
|||||||
|
|
||||||
// 处理好友选择
|
// 处理好友选择
|
||||||
const handleFriendToggle = (friendId: string) => {
|
const handleFriendToggle = (friendId: string) => {
|
||||||
let newIds: string[];
|
if (readonly) return;
|
||||||
if (selectedFriends.includes(friendId)) {
|
|
||||||
newIds = selectedFriends.filter((id) => id !== friendId);
|
const newSelectedFriends = selectedFriends.includes(friendId)
|
||||||
} else {
|
? selectedFriends.filter((id) => id !== friendId)
|
||||||
newIds = [...selectedFriends, friendId];
|
: [...selectedFriends, friendId];
|
||||||
}
|
|
||||||
onSelect(newIds);
|
onSelect(newSelectedFriends);
|
||||||
|
|
||||||
|
// 如果有 onSelectDetail 回调,传递完整的好友对象
|
||||||
if (onSelectDetail) {
|
if (onSelectDetail) {
|
||||||
const selectedObjs = friends.filter((f) => newIds.includes(f.id));
|
const selectedFriendObjs = friends.filter((friend) =>
|
||||||
onSelectDetail(selectedObjs);
|
newSelectedFriends.includes(friend.id)
|
||||||
|
);
|
||||||
|
onSelectDetail(selectedFriendObjs);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -160,29 +150,22 @@ export default function FriendSelection({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 获取已选好友详细信息
|
// 获取已选好友详细信息
|
||||||
const selectedFriendObjs = selectedFriends
|
const selectedFriendObjs = friends.filter((friend) =>
|
||||||
.map((id) => friends.find((f) => f.id === id))
|
selectedFriends.includes(friend.id)
|
||||||
.filter(Boolean) as WechatFriend[];
|
);
|
||||||
|
|
||||||
// 删除已选好友
|
// 删除已选好友
|
||||||
const handleRemoveFriend = (id: string) => {
|
const handleRemoveFriend = (id: string) => {
|
||||||
if (readonly) return;
|
if (readonly) return;
|
||||||
onSelect(selectedFriends.filter((f) => f !== id));
|
onSelect(selectedFriends.filter((d) => d !== id));
|
||||||
};
|
};
|
||||||
|
|
||||||
// 确认按钮逻辑
|
// 确认选择
|
||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
setRealVisible(false);
|
|
||||||
if (onConfirm) {
|
if (onConfirm) {
|
||||||
onConfirm(selectedFriends, selectedFriendObjs);
|
onConfirm(selectedFriends, selectedFriendObjs);
|
||||||
}
|
}
|
||||||
};
|
setRealVisible(false);
|
||||||
|
|
||||||
// 清空搜索
|
|
||||||
const handleClearSearch = () => {
|
|
||||||
setSearchQuery("");
|
|
||||||
setCurrentPage(1);
|
|
||||||
fetchFriends(1, "");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -271,40 +254,30 @@ export default function FriendSelection({
|
|||||||
position="bottom"
|
position="bottom"
|
||||||
bodyStyle={{ height: "100vh" }}
|
bodyStyle={{ height: "100vh" }}
|
||||||
>
|
>
|
||||||
<div className={style.popupContainer}>
|
<Layout
|
||||||
<div className={style.popupHeader}>
|
header={
|
||||||
<div className={style.popupTitle}>选择微信好友</div>
|
<PopupHeader
|
||||||
<div className={style.searchWrapper}>
|
title="选择微信好友"
|
||||||
<Input
|
searchQuery={searchQuery}
|
||||||
placeholder="搜索好友"
|
setSearchQuery={setSearchQuery}
|
||||||
value={searchQuery}
|
searchPlaceholder="搜索好友"
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
loading={loading}
|
||||||
disabled={readonly}
|
onRefresh={() => fetchFriends(currentPage, searchQuery)}
|
||||||
prefix={<SearchOutlined />}
|
/>
|
||||||
allowClear
|
}
|
||||||
size="large"
|
footer={
|
||||||
/>
|
<PopupFooter
|
||||||
{searchQuery && !readonly && (
|
total={totalFriends}
|
||||||
<Button
|
currentPage={currentPage}
|
||||||
type="text"
|
totalPages={totalPages}
|
||||||
icon={<DeleteOutlined />}
|
loading={loading}
|
||||||
size="small"
|
selectedCount={selectedFriends.length}
|
||||||
className={style.clearBtn}
|
onPageChange={setCurrentPage}
|
||||||
onClick={handleClearSearch}
|
onCancel={() => setRealVisible(false)}
|
||||||
style={{
|
onConfirm={handleConfirm}
|
||||||
color: "#ff4d4f",
|
/>
|
||||||
border: "none",
|
}
|
||||||
background: "none",
|
>
|
||||||
minWidth: 24,
|
|
||||||
height: 24,
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={style.friendList}>
|
<div className={style.friendList}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className={style.loadingBox}>
|
<div className={style.loadingBox}>
|
||||||
@@ -372,50 +345,7 @@ export default function FriendSelection({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* 分页栏 */}
|
</Layout>
|
||||||
<div className={style.paginationRow}>
|
|
||||||
<div className={style.totalCount}>总计 {totalFriends} 个好友</div>
|
|
||||||
<div className={style.paginationControls}>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
|
|
||||||
disabled={currentPage === 1 || loading}
|
|
||||||
className={style.pageBtn}
|
|
||||||
style={{ borderRadius: 16 }}
|
|
||||||
>
|
|
||||||
<
|
|
||||||
</Button>
|
|
||||||
<span className={style.pageInfo}>
|
|
||||||
{currentPage} / {totalPages}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
onClick={() =>
|
|
||||||
setCurrentPage(Math.min(totalPages, currentPage + 1))
|
|
||||||
}
|
|
||||||
disabled={currentPage === totalPages || loading}
|
|
||||||
className={style.pageBtn}
|
|
||||||
style={{ borderRadius: 16 }}
|
|
||||||
>
|
|
||||||
>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* 底部按钮栏 */}
|
|
||||||
<div className={style.popupFooter}>
|
|
||||||
<div className={style.selectedCount}>
|
|
||||||
已选择 {selectedFriends.length} 个好友
|
|
||||||
</div>
|
|
||||||
<div className={style.footerBtnGroup}>
|
|
||||||
<Button onClick={() => setRealVisible(false)}>取消</Button>
|
|
||||||
<Button type="primary" onClick={handleConfirm}>
|
|
||||||
确定
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Popup>
|
</Popup>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import {
|
import { SearchOutlined, DeleteOutlined } from "@ant-design/icons";
|
||||||
SearchOutlined,
|
import { Button, Input } from "antd";
|
||||||
CloseOutlined,
|
import { Popup } from "antd-mobile";
|
||||||
LeftOutlined,
|
|
||||||
RightOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
import { Button as AntdButton, Input as AntdInput } from "antd";
|
|
||||||
import { Popup, Toast } from "antd-mobile";
|
|
||||||
import { getGroupList } from "./api";
|
import { getGroupList } from "./api";
|
||||||
import style from "./index.module.scss";
|
import style from "./index.module.scss";
|
||||||
|
import Layout from "@/components/Layout/Layout";
|
||||||
|
import PopupHeader from "@/components/PopuLayout/header";
|
||||||
|
import PopupFooter from "@/components/PopuLayout/footer";
|
||||||
|
|
||||||
// 群组接口类型
|
// 群组接口类型
|
||||||
interface WechatGroup {
|
interface WechatGroup {
|
||||||
@@ -61,9 +58,9 @@ export default function GroupSelection({
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
// 获取已选群聊详细信息
|
// 获取已选群聊详细信息
|
||||||
const selectedGroupObjs = selectedGroups
|
const selectedGroupObjs = groups.filter((group) =>
|
||||||
.map((id) => groups.find((g) => g.id === id))
|
selectedGroups.includes(group.id)
|
||||||
.filter(Boolean) as WechatGroup[];
|
);
|
||||||
|
|
||||||
// 删除已选群聊
|
// 删除已选群聊
|
||||||
const handleRemoveGroup = (id: string) => {
|
const handleRemoveGroup = (id: string) => {
|
||||||
@@ -101,58 +98,52 @@ export default function GroupSelection({
|
|||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
fetchGroups(1, searchQuery);
|
fetchGroups(1, searchQuery);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [searchQuery, realVisible]);
|
}, [searchQuery, realVisible]);
|
||||||
|
|
||||||
// 获取群组列表API - 支持keyword
|
// 获取群聊列表API
|
||||||
const fetchGroups = async (page: number, keyword: string = "") => {
|
const fetchGroups = async (page: number, keyword: string = "") => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const params: any = {
|
let params: any = {
|
||||||
page,
|
page,
|
||||||
limit: 20,
|
limit: 20,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (keyword.trim()) {
|
if (keyword.trim()) {
|
||||||
params.keyword = keyword.trim();
|
params.keyword = keyword.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await getGroupList(params);
|
const response = await getGroupList(params);
|
||||||
|
if (response && response.list) {
|
||||||
if (res && Array.isArray(res.list)) {
|
setGroups(response.list);
|
||||||
setGroups(
|
setTotalGroups(response.total || 0);
|
||||||
res.list.map((group: any) => ({
|
setTotalPages(Math.ceil((response.total || 0) / 20));
|
||||||
id: group.id?.toString() || "",
|
|
||||||
chatroomId: group.chatroomId || "",
|
|
||||||
name: group.name || "",
|
|
||||||
avatar: group.avatar || "",
|
|
||||||
ownerWechatId: group.ownerWechatId || "",
|
|
||||||
ownerNickname: group.ownerNickname || "",
|
|
||||||
ownerAvatar: group.ownerAvatar || "",
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
setTotalGroups(res.total || 0);
|
|
||||||
setTotalPages(Math.ceil((res.total || 0) / 20));
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取群组列表失败:", error);
|
console.error("获取群聊列表失败:", error);
|
||||||
Toast.show({ content: "获取群组列表失败", position: "top" });
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理群组选择
|
// 处理群聊选择
|
||||||
const handleGroupToggle = (groupId: string) => {
|
const handleGroupToggle = (groupId: string) => {
|
||||||
let newIds: string[];
|
if (readonly) return;
|
||||||
if (selectedGroups.includes(groupId)) {
|
|
||||||
newIds = selectedGroups.filter((id) => id !== groupId);
|
const newSelectedGroups = selectedGroups.includes(groupId)
|
||||||
} else {
|
? selectedGroups.filter((id) => id !== groupId)
|
||||||
newIds = [...selectedGroups, groupId];
|
: [...selectedGroups, groupId];
|
||||||
}
|
|
||||||
onSelect(newIds);
|
onSelect(newSelectedGroups);
|
||||||
|
|
||||||
|
// 如果有 onSelectDetail 回调,传递完整的群聊对象
|
||||||
if (onSelectDetail) {
|
if (onSelectDetail) {
|
||||||
const selectedObjs = groups.filter((g) => newIds.includes(g.id));
|
const selectedGroupObjs = groups.filter((group) =>
|
||||||
onSelectDetail(selectedObjs);
|
newSelectedGroups.includes(group.id)
|
||||||
|
);
|
||||||
|
onSelectDetail(selectedGroupObjs);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -162,19 +153,12 @@ export default function GroupSelection({
|
|||||||
return `已选择 ${selectedGroups.length} 个群聊`;
|
return `已选择 ${selectedGroups.length} 个群聊`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 确认按钮逻辑
|
// 确认选择
|
||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
setRealVisible(false);
|
|
||||||
if (onConfirm) {
|
if (onConfirm) {
|
||||||
onConfirm(selectedGroups, selectedGroupObjs);
|
onConfirm(selectedGroups, selectedGroupObjs);
|
||||||
}
|
}
|
||||||
};
|
setRealVisible(false);
|
||||||
|
|
||||||
// 清空搜索
|
|
||||||
const handleClearSearch = () => {
|
|
||||||
setSearchQuery("");
|
|
||||||
setCurrentPage(1);
|
|
||||||
fetchGroups(1, "");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -182,7 +166,7 @@ export default function GroupSelection({
|
|||||||
{/* 输入框 */}
|
{/* 输入框 */}
|
||||||
{showInput && (
|
{showInput && (
|
||||||
<div className={`${style.inputWrapper} ${className}`}>
|
<div className={`${style.inputWrapper} ${className}`}>
|
||||||
<AntdInput
|
<Input
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={getDisplayText()}
|
value={getDisplayText()}
|
||||||
onClick={openPopup}
|
onClick={openPopup}
|
||||||
@@ -234,7 +218,7 @@ export default function GroupSelection({
|
|||||||
{group.name || group.chatroomId || group.id}
|
{group.name || group.chatroomId || group.id}
|
||||||
</div>
|
</div>
|
||||||
{!readonly && (
|
{!readonly && (
|
||||||
<AntdButton
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined />}
|
||||||
size="small"
|
size="small"
|
||||||
@@ -263,41 +247,30 @@ export default function GroupSelection({
|
|||||||
position="bottom"
|
position="bottom"
|
||||||
bodyStyle={{ height: "100vh" }}
|
bodyStyle={{ height: "100vh" }}
|
||||||
>
|
>
|
||||||
<div className={style.popupContainer}>
|
<Layout
|
||||||
<div className={style.popupHeader}>
|
header={
|
||||||
<div className={style.popupTitle}>选择群聊</div>
|
<PopupHeader
|
||||||
<div className={style.searchWrapper}>
|
title="选择群聊"
|
||||||
<AntdInput
|
searchQuery={searchQuery}
|
||||||
placeholder="搜索群聊"
|
setSearchQuery={setSearchQuery}
|
||||||
value={searchQuery}
|
searchPlaceholder="搜索群聊"
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
loading={loading}
|
||||||
disabled={readonly}
|
onRefresh={() => fetchGroups(currentPage, searchQuery)}
|
||||||
prefix={<SearchOutlined />}
|
/>
|
||||||
allowClear
|
}
|
||||||
size="large"
|
footer={
|
||||||
/>
|
<PopupFooter
|
||||||
<SearchOutlined className={style.searchIcon} />
|
total={totalGroups}
|
||||||
{searchQuery && !readonly && (
|
currentPage={currentPage}
|
||||||
<AntdButton
|
totalPages={totalPages}
|
||||||
type="text"
|
loading={loading}
|
||||||
icon={<CloseOutlined />}
|
selectedCount={selectedGroups.length}
|
||||||
size="small"
|
onPageChange={setCurrentPage}
|
||||||
className={style.clearBtn}
|
onCancel={() => setRealVisible(false)}
|
||||||
onClick={handleClearSearch}
|
onConfirm={handleConfirm}
|
||||||
style={{
|
/>
|
||||||
color: "#ff4d4f",
|
}
|
||||||
border: "none",
|
>
|
||||||
background: "none",
|
|
||||||
minWidth: 24,
|
|
||||||
height: 24,
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={style.groupList}>
|
<div className={style.groupList}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className={style.loadingBox}>
|
<div className={style.loadingBox}>
|
||||||
@@ -361,52 +334,7 @@ export default function GroupSelection({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* 分页栏 */}
|
</Layout>
|
||||||
<div className={style.paginationRow}>
|
|
||||||
<div className={style.totalCount}>总计 {totalGroups} 个群聊</div>
|
|
||||||
<div className={style.paginationControls}>
|
|
||||||
<AntdButton
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
|
|
||||||
disabled={currentPage === 1 || loading}
|
|
||||||
className={style.pageBtn}
|
|
||||||
style={{ borderRadius: 16 }}
|
|
||||||
>
|
|
||||||
<LeftOutlined />
|
|
||||||
</AntdButton>
|
|
||||||
<span className={style.pageInfo}>
|
|
||||||
{currentPage} / {totalPages}
|
|
||||||
</span>
|
|
||||||
<AntdButton
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
onClick={() =>
|
|
||||||
setCurrentPage(Math.min(totalPages, currentPage + 1))
|
|
||||||
}
|
|
||||||
disabled={currentPage === totalPages || loading}
|
|
||||||
className={style.pageBtn}
|
|
||||||
style={{ borderRadius: 16 }}
|
|
||||||
>
|
|
||||||
<RightOutlined />
|
|
||||||
</AntdButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* 底部按钮栏 */}
|
|
||||||
<div className={style.popupFooter}>
|
|
||||||
<div className={style.selectedCount}>
|
|
||||||
已选择 {selectedGroups.length} 个群聊
|
|
||||||
</div>
|
|
||||||
<div className={style.footerBtnGroup}>
|
|
||||||
<AntdButton type="default" onClick={() => setRealVisible(false)}>
|
|
||||||
取消
|
|
||||||
</AntdButton>
|
|
||||||
<AntdButton type="primary" onClick={handleConfirm}>
|
|
||||||
确定
|
|
||||||
</AntdButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Popup>
|
</Popup>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
41
nkebao/src/components/NavCommon/index.tsx
Normal file
41
nkebao/src/components/NavCommon/index.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { NavBar } from "antd-mobile";
|
||||||
|
import { ArrowLeftOutlined } from "@ant-design/icons";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
interface NavCommonProps {
|
||||||
|
title: string;
|
||||||
|
backFn?: () => void;
|
||||||
|
right?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NavCommon: React.FC<NavCommonProps> = ({ title, backFn, right }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return (
|
||||||
|
<NavBar
|
||||||
|
back={null}
|
||||||
|
style={{ background: "#fff" }}
|
||||||
|
left={
|
||||||
|
<div className="nav-title">
|
||||||
|
<ArrowLeftOutlined
|
||||||
|
twoToneColor="#1677ff"
|
||||||
|
onClick={() => {
|
||||||
|
if (backFn) {
|
||||||
|
backFn();
|
||||||
|
} else {
|
||||||
|
navigate(-1);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
right={right}
|
||||||
|
>
|
||||||
|
<span style={{ color: "var(--primary-color)", fontWeight: 600 }}>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
</NavBar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NavCommon;
|
||||||
71
nkebao/src/components/PopuLayout/footer.module.scss
Normal file
71
nkebao/src/components/PopuLayout/footer.module.scss
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
.popupFooter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectedCount {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerBtnGroup {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paginationRow {
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.totalCount {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paginationControls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageBtn {
|
||||||
|
padding: 0 8px;
|
||||||
|
height: 32px;
|
||||||
|
min-width: 32px;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
color: #333;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
border-color: #1677ff;
|
||||||
|
color: #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageInfo {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #222;
|
||||||
|
margin: 0 8px;
|
||||||
|
min-width: 60px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
67
nkebao/src/components/PopuLayout/footer.tsx
Normal file
67
nkebao/src/components/PopuLayout/footer.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Button } from "antd";
|
||||||
|
import style from "./footer.module.scss";
|
||||||
|
import { ArrowLeftOutlined, ArrowRightOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
|
interface PopupFooterProps {
|
||||||
|
total: number;
|
||||||
|
currentPage: number;
|
||||||
|
totalPages: number;
|
||||||
|
loading: boolean;
|
||||||
|
selectedCount: number;
|
||||||
|
onPageChange: (page: number) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PopupFooter: React.FC<PopupFooterProps> = ({
|
||||||
|
total,
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
loading,
|
||||||
|
selectedCount,
|
||||||
|
onPageChange,
|
||||||
|
onCancel,
|
||||||
|
onConfirm,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* 分页栏 */}
|
||||||
|
<div className={style.paginationRow}>
|
||||||
|
<div className={style.totalCount}>总计 {total} 个设备</div>
|
||||||
|
<div className={style.paginationControls}>
|
||||||
|
<Button
|
||||||
|
onClick={() => onPageChange(Math.max(1, currentPage - 1))}
|
||||||
|
disabled={currentPage === 1 || loading}
|
||||||
|
className={style.pageBtn}
|
||||||
|
>
|
||||||
|
<ArrowLeftOutlined />
|
||||||
|
</Button>
|
||||||
|
<span className={style.pageInfo}>
|
||||||
|
{currentPage} / {totalPages}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
|
||||||
|
disabled={currentPage === totalPages || loading}
|
||||||
|
className={style.pageBtn}
|
||||||
|
>
|
||||||
|
<ArrowRightOutlined />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={style.popupFooter}>
|
||||||
|
<div className={style.selectedCount}>已选择 {selectedCount} 个设备</div>
|
||||||
|
<div className={style.footerBtnGroup}>
|
||||||
|
<Button color="primary" variant="filled" onClick={onCancel}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" onClick={onConfirm}>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PopupFooter;
|
||||||
52
nkebao/src/components/PopuLayout/header.module.scss
Normal file
52
nkebao/src/components/PopuLayout/header.module.scss
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
.popupHeader {
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popupTitle {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popupSearchRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popupSearchInputWrap {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputIcon {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: #bdbdbd;
|
||||||
|
z-index: 10;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.refreshBtn {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadingIcon {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
86
nkebao/src/components/PopuLayout/header.tsx
Normal file
86
nkebao/src/components/PopuLayout/header.tsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { SearchOutlined, ReloadOutlined } from "@ant-design/icons";
|
||||||
|
import { Input, Button } from "antd";
|
||||||
|
import { Tabs } from "antd-mobile";
|
||||||
|
import style from "./header.module.scss";
|
||||||
|
|
||||||
|
interface PopupHeaderProps {
|
||||||
|
title: string;
|
||||||
|
searchQuery: string;
|
||||||
|
setSearchQuery: (value: string) => void;
|
||||||
|
searchPlaceholder?: string;
|
||||||
|
loading?: boolean;
|
||||||
|
onRefresh?: () => void;
|
||||||
|
showRefresh?: boolean;
|
||||||
|
showSearch?: boolean;
|
||||||
|
showTabs?: boolean;
|
||||||
|
tabsConfig?: {
|
||||||
|
activeKey: string;
|
||||||
|
onChange: (key: string) => void;
|
||||||
|
tabs: Array<{ title: string; key: string }>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const PopupHeader: React.FC<PopupHeaderProps> = ({
|
||||||
|
title,
|
||||||
|
searchQuery,
|
||||||
|
setSearchQuery,
|
||||||
|
searchPlaceholder = "搜索...",
|
||||||
|
loading = false,
|
||||||
|
onRefresh,
|
||||||
|
showRefresh = true,
|
||||||
|
showSearch = true,
|
||||||
|
showTabs = false,
|
||||||
|
tabsConfig,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={style.popupHeader}>
|
||||||
|
<div className={style.popupTitle}>{title}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showSearch && (
|
||||||
|
<div className={style.popupSearchRow}>
|
||||||
|
<div className={style.popupSearchInputWrap}>
|
||||||
|
<Input
|
||||||
|
placeholder={searchPlaceholder}
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
prefix={<SearchOutlined />}
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showRefresh && onRefresh && (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
onClick={onRefresh}
|
||||||
|
disabled={loading}
|
||||||
|
className={style.refreshBtn}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<div className={style.loadingIcon}>⟳</div>
|
||||||
|
) : (
|
||||||
|
<ReloadOutlined />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showTabs && tabsConfig && (
|
||||||
|
<Tabs
|
||||||
|
activeKey={tabsConfig.activeKey}
|
||||||
|
onChange={tabsConfig.onChange}
|
||||||
|
style={{ marginTop: 8 }}
|
||||||
|
>
|
||||||
|
{tabsConfig.tabs.map((tab) => (
|
||||||
|
<Tabs.Tab key={tab.key} title={tab.title} />
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PopupHeader;
|
||||||
@@ -11,7 +11,7 @@ const StepIndicator: React.FC<StepIndicatorProps> = ({
|
|||||||
steps,
|
steps,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div style={{ marginBottom: 24, overflowX: "auto" }}>
|
<div style={{ overflowX: "auto", padding: "30px 0px", background: "#fff" }}>
|
||||||
<Steps current={currentStep - 1}>
|
<Steps current={currentStep - 1}>
|
||||||
{steps.map((step, idx) => (
|
{steps.map((step, idx) => (
|
||||||
<Steps.Step
|
<Steps.Step
|
||||||
|
|||||||
108
nkebao/src/pages/component-test/index.tsx
Normal file
108
nkebao/src/pages/component-test/index.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { NavBar, Tabs } from "antd-mobile";
|
||||||
|
import { ArrowLeftOutlined } from "@ant-design/icons";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import Layout from "@/components/Layout/Layout";
|
||||||
|
import NavCommon from "@/components/NavCommon";
|
||||||
|
import DeviceSelection from "@/components/DeviceSelection";
|
||||||
|
import FriendSelection from "@/components/FriendSelection";
|
||||||
|
import GroupSelection from "@/components/GroupSelection";
|
||||||
|
|
||||||
|
const ComponentTest: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [activeTab, setActiveTab] = useState("devices");
|
||||||
|
|
||||||
|
// 设备选择状态
|
||||||
|
const [selectedDevices, setSelectedDevices] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// 好友选择状态
|
||||||
|
const [selectedFriends, setSelectedFriends] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// 群组选择状态
|
||||||
|
const [selectedGroups, setSelectedGroups] = useState<string[]>([]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout header={<NavCommon title="组件调试" />}>
|
||||||
|
<div style={{ padding: 16 }}>
|
||||||
|
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
||||||
|
<Tabs.Tab title="设备选择" key="devices">
|
||||||
|
<div style={{ padding: "16px 0" }}>
|
||||||
|
<h3 style={{ marginBottom: 16 }}>DeviceSelection 组件测试</h3>
|
||||||
|
<DeviceSelection
|
||||||
|
selectedDevices={selectedDevices}
|
||||||
|
onSelect={setSelectedDevices}
|
||||||
|
placeholder="请选择设备"
|
||||||
|
showSelectedList={true}
|
||||||
|
selectedListMaxHeight={300}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 16,
|
||||||
|
padding: 12,
|
||||||
|
background: "#f5f5f5",
|
||||||
|
borderRadius: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<strong>已选设备:</strong> {selectedDevices.length} 个
|
||||||
|
<br />
|
||||||
|
<strong>设备ID:</strong> {selectedDevices.join(", ") || "无"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs.Tab>
|
||||||
|
|
||||||
|
<Tabs.Tab title="好友选择" key="friends">
|
||||||
|
<div style={{ padding: "16px 0" }}>
|
||||||
|
<h3 style={{ marginBottom: 16 }}>FriendSelection 组件测试</h3>
|
||||||
|
<FriendSelection
|
||||||
|
selectedFriends={selectedFriends}
|
||||||
|
onSelect={setSelectedFriends}
|
||||||
|
placeholder="请选择微信好友"
|
||||||
|
showSelectedList={true}
|
||||||
|
selectedListMaxHeight={300}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 16,
|
||||||
|
padding: 12,
|
||||||
|
background: "#f5f5f5",
|
||||||
|
borderRadius: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<strong>已选好友:</strong> {selectedFriends.length} 个
|
||||||
|
<br />
|
||||||
|
<strong>好友ID:</strong> {selectedFriends.join(", ") || "无"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs.Tab>
|
||||||
|
|
||||||
|
<Tabs.Tab title="群组选择" key="groups">
|
||||||
|
<div style={{ padding: "16px 0" }}>
|
||||||
|
<h3 style={{ marginBottom: 16 }}>GroupSelection 组件测试</h3>
|
||||||
|
<GroupSelection
|
||||||
|
selectedGroups={selectedGroups}
|
||||||
|
onSelect={setSelectedGroups}
|
||||||
|
placeholder="请选择微信群组"
|
||||||
|
showSelectedList={true}
|
||||||
|
selectedListMaxHeight={300}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 16,
|
||||||
|
padding: 12,
|
||||||
|
background: "#f5f5f5",
|
||||||
|
borderRadius: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<strong>已选群组:</strong> {selectedGroups.length} 个
|
||||||
|
<br />
|
||||||
|
<strong>群组ID:</strong> {selectedGroups.join(", ") || "无"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs.Tab>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ComponentTest;
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { message } from "antd";
|
import { message } from "antd";
|
||||||
import { NavBar } from "antd-mobile";
|
import NavCommon from "@/components/NavCommon";
|
||||||
import { ArrowLeftOutlined } from "@ant-design/icons";
|
|
||||||
import BasicSettings from "./steps/BasicSettings";
|
import BasicSettings from "./steps/BasicSettings";
|
||||||
import FriendRequestSettings from "./steps/FriendRequestSettings";
|
import FriendRequestSettings from "./steps/FriendRequestSettings";
|
||||||
import MessageSettings from "./steps/MessageSettings";
|
import MessageSettings from "./steps/MessageSettings";
|
||||||
@@ -205,25 +204,8 @@ export default function NewPlan() {
|
|||||||
<Layout
|
<Layout
|
||||||
header={
|
header={
|
||||||
<>
|
<>
|
||||||
<NavBar
|
<NavCommon title={isEdit ? "编辑朋友圈同步" : "新建朋友圈同步"} />
|
||||||
back={null}
|
<StepIndicator currentStep={currentStep} steps={steps} />
|
||||||
style={{ background: "#fff" }}
|
|
||||||
left={
|
|
||||||
<div className="nav-title">
|
|
||||||
<ArrowLeftOutlined
|
|
||||||
twoToneColor="#1677ff"
|
|
||||||
onClick={() => router(-1)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span className="nav-title">
|
|
||||||
{isEdit ? "编辑朋友圈同步" : "新建朋友圈同步"}
|
|
||||||
</span>
|
|
||||||
</NavBar>
|
|
||||||
<div className="px-4 py-6">
|
|
||||||
<StepIndicator currentStep={currentStep} steps={steps} />
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,17 +1,6 @@
|
|||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import {
|
import { Form, Input, Button, Tag, Switch, Modal } from "antd";
|
||||||
Form,
|
import { Button as ButtonMobile } from "antd-mobile";
|
||||||
Input,
|
|
||||||
Button,
|
|
||||||
Tag,
|
|
||||||
Switch,
|
|
||||||
// Upload,
|
|
||||||
Modal,
|
|
||||||
Alert,
|
|
||||||
Row,
|
|
||||||
Col,
|
|
||||||
message,
|
|
||||||
} from "antd";
|
|
||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
EyeOutlined,
|
EyeOutlined,
|
||||||
@@ -21,6 +10,8 @@ import {
|
|||||||
CheckOutlined,
|
CheckOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { uploadFile } from "@/api/common";
|
import { uploadFile } from "@/api/common";
|
||||||
|
import Layout from "@/components/Layout/Layout";
|
||||||
|
import styles from "./base.module.scss";
|
||||||
|
|
||||||
interface BasicSettingsProps {
|
interface BasicSettingsProps {
|
||||||
isEdit: boolean;
|
isEdit: boolean;
|
||||||
@@ -424,31 +415,31 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
|||||||
formData.scenario !== 1 ? { display: "none" } : { display: "block" };
|
formData.scenario !== 1 ? { display: "none" } : { display: "block" };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={styles["basic-container"]}>
|
||||||
{/* 场景选择区块 */}
|
{/* 场景选择区块 */}
|
||||||
{sceneLoading ? (
|
<div className={styles["basic-scene-select"]}>
|
||||||
<div style={{ padding: 16, textAlign: "center" }}>加载中...</div>
|
<div className={styles["basic-scene-grid"]}>
|
||||||
) : (
|
{sceneList.map((scene) => {
|
||||||
<Row gutter={20} className="grid grid-cols-3 gap-4">
|
const selected = formData.scenario === scene.id;
|
||||||
{sceneList.map((scene) => (
|
return (
|
||||||
<Col span={8} key={scene.id}>
|
<button
|
||||||
<Button
|
key={scene.id}
|
||||||
type="primary"
|
|
||||||
onClick={() => handleScenarioSelect(scene.id)}
|
onClick={() => handleScenarioSelect(scene.id)}
|
||||||
className={`w-full ${formData.scenario === scene.id ? "bg-blue-500" : "bg-gray-200"}`}
|
className={
|
||||||
|
styles["basic-scene-btn"] +
|
||||||
|
(selected ? " " + styles.selected : "")
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{scene.name.replace("获客", "")}
|
{scene.name.replace("获客", "")}
|
||||||
</Button>
|
</button>
|
||||||
</Col>
|
);
|
||||||
))}
|
})}
|
||||||
</Row>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{/* 计划名称输入区 */}
|
{/* 计划名称输入区 */}
|
||||||
<div className="mb-4">计划名称</div>
|
<div className={styles["basic-label"]}>计划名称</div>
|
||||||
<div className="border p-2 mb-4">
|
<div className={styles["basic-input-block"]}>
|
||||||
<Input
|
<Input
|
||||||
className="w-full"
|
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
onChange({ ...formData, name: String(e.target.value) })
|
onChange({ ...formData, name: String(e.target.value) })
|
||||||
@@ -456,17 +447,16 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
|||||||
placeholder="请输入计划名称"
|
placeholder="请输入计划名称"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className={styles["basic-label"]}>获客标签(可多选)</div>
|
||||||
<div className="mb-4">获客标签(可多选)</div>
|
|
||||||
{/* 标签选择区块 */}
|
{/* 标签选择区块 */}
|
||||||
{formData.scenario && (
|
{formData.scenario && (
|
||||||
<div className="flex pb-4" style={{ flexWrap: "wrap", gap: 8 }}>
|
<div className={styles["basic-tag-list"]}>
|
||||||
{(currentScene?.scenarioTags || []).map((tag: string) => (
|
{(currentScene?.scenarioTags || []).map((tag: string) => (
|
||||||
<Tag
|
<Tag
|
||||||
key={tag}
|
key={tag}
|
||||||
color={selectedScenarioTags.includes(tag) ? "blue" : "default"}
|
color={selectedScenarioTags.includes(tag) ? "blue" : "default"}
|
||||||
onClick={() => handleScenarioTagToggle(tag)}
|
onClick={() => handleScenarioTagToggle(tag)}
|
||||||
style={{ marginBottom: 4 }}
|
className={styles["basic-tag-item"]}
|
||||||
>
|
>
|
||||||
{tag}
|
{tag}
|
||||||
</Tag>
|
</Tag>
|
||||||
@@ -477,9 +467,9 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
|||||||
key={tag.id}
|
key={tag.id}
|
||||||
color={selectedScenarioTags.includes(tag.id) ? "blue" : "default"}
|
color={selectedScenarioTags.includes(tag.id) ? "blue" : "default"}
|
||||||
onClick={() => handleScenarioTagToggle(tag.id)}
|
onClick={() => handleScenarioTagToggle(tag.id)}
|
||||||
style={{ marginBottom: 4 }}
|
|
||||||
closable
|
closable
|
||||||
onClose={() => handleRemoveCustomTag(tag.id)}
|
onClose={() => handleRemoveCustomTag(tag.id)}
|
||||||
|
className={styles["basic-tag-item"]}
|
||||||
>
|
>
|
||||||
{tag.name}
|
{tag.name}
|
||||||
</Tag>
|
</Tag>
|
||||||
@@ -487,49 +477,33 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* 自定义标签输入区 */}
|
{/* 自定义标签输入区 */}
|
||||||
<div className="flex p" style={{ gap: 8 }}>
|
<div className={styles["basic-custom-tag-input"]}>
|
||||||
<div className="border p-2 flex-1">
|
<Input
|
||||||
<Input
|
type="text"
|
||||||
type="text"
|
value={customTagInput}
|
||||||
value={customTagInput}
|
onChange={(e) => setCustomTagInput(e.target.value)}
|
||||||
onChange={(e) => setCustomTagInput(e.target.value)}
|
placeholder="添加自定义标签"
|
||||||
placeholder="添加自定义标签"
|
/>
|
||||||
className="w-full"
|
<Button type="primary" onClick={handleAddCustomTag}>
|
||||||
/>
|
添加
|
||||||
</div>
|
</Button>
|
||||||
<div className="pt-1">
|
|
||||||
<Button type="primary" onClick={handleAddCustomTag}>
|
|
||||||
添加
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 输入获客成功提示 */}
|
{/* 输入获客成功提示 */}
|
||||||
<div className="flex pt-4">
|
<div className={styles["basic-success-tip"]}>
|
||||||
<div className="border p-2 flex-1">
|
<Input
|
||||||
<Input
|
type="text"
|
||||||
type="text"
|
value={tips}
|
||||||
value={tips}
|
onChange={(e) => {
|
||||||
onChange={(e) => {
|
setTips(e.target.value);
|
||||||
setTips(e.target.value);
|
onChange({ ...formData, tips: e.target.value });
|
||||||
onChange({ ...formData, tips: e.target.value });
|
|
||||||
}}
|
|
||||||
placeholder="请输入获客成功提示"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 选素材 */}
|
|
||||||
<div className="my-4" style={openPoster}>
|
|
||||||
<div className="mb-4">选择海报</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "repeat(3, 1fr)",
|
|
||||||
gap: 12,
|
|
||||||
}}
|
}}
|
||||||
>
|
placeholder="请输入获客成功提示"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* 选素材 */}
|
||||||
|
<div className={styles["basic-materials"]} style={openPoster}>
|
||||||
|
<div className={styles["basic-label"]}>选择海报</div>
|
||||||
|
<div className={styles["basic-materials-grid"]}>
|
||||||
{[...materials, ...customPosters].map((material) => {
|
{[...materials, ...customPosters].map((material) => {
|
||||||
const isSelected = selectedMaterials.some(
|
const isSelected = selectedMaterials.some(
|
||||||
(m) => m.id === material.id
|
(m) => m.id === material.id
|
||||||
@@ -538,35 +512,15 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={material.id}
|
key={material.id}
|
||||||
style={{
|
className={
|
||||||
border: isSelected ? "2px solid #1890ff" : "2px solid #eee",
|
styles["basic-material-card"] +
|
||||||
borderRadius: 8,
|
(isSelected ? " " + styles.selected : "")
|
||||||
padding: 6,
|
}
|
||||||
cursor: "pointer",
|
|
||||||
background: isSelected ? "#e6f7ff" : "#fff",
|
|
||||||
transition: "border 0.2s",
|
|
||||||
textAlign: "center",
|
|
||||||
position: "relative",
|
|
||||||
height: 180 + 12, // 图片高度180+上下padding
|
|
||||||
overflow: "hidden",
|
|
||||||
minHeight: 192,
|
|
||||||
}}
|
|
||||||
onClick={() => handleMaterialSelect(material)}
|
onClick={() => handleMaterialSelect(material)}
|
||||||
>
|
>
|
||||||
{/* 预览按钮:自定义海报在左上,内置海报在右上 */}
|
{/* 预览按钮:自定义海报在左上,内置海报在右上 */}
|
||||||
<Button
|
<span
|
||||||
style={{
|
className={styles["basic-material-preview"]}
|
||||||
position: "absolute",
|
|
||||||
top: 8,
|
|
||||||
left: isCustom ? 8 : "auto",
|
|
||||||
right: isCustom ? "auto" : 8,
|
|
||||||
background: "rgba(0,0,0,0.5)",
|
|
||||||
border: "none",
|
|
||||||
borderRadius: "50%",
|
|
||||||
padding: 2,
|
|
||||||
zIndex: 2,
|
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handlePreviewImage(material.preview);
|
handlePreviewImage(material.preview);
|
||||||
@@ -575,7 +529,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
|||||||
<EyeOutlined
|
<EyeOutlined
|
||||||
style={{ color: "#fff", width: 18, height: 18 }}
|
style={{ color: "#fff", width: 18, height: 18 }}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</span>
|
||||||
{/* 删除自定义海报按钮 */}
|
{/* 删除自定义海报按钮 */}
|
||||||
{isCustom && (
|
{isCustom && (
|
||||||
<Button
|
<Button
|
||||||
@@ -607,31 +561,9 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
|||||||
<img
|
<img
|
||||||
src={material.preview}
|
src={material.preview}
|
||||||
alt={material.name}
|
alt={material.name}
|
||||||
style={{
|
className={styles["basic-material-img"]}
|
||||||
width: 100,
|
|
||||||
height: 180,
|
|
||||||
objectFit: "cover",
|
|
||||||
borderRadius: 4,
|
|
||||||
marginBottom: 0,
|
|
||||||
display: "block",
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<div
|
<div className={styles["basic-material-name"]}>
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
left: 0,
|
|
||||||
bottom: 0,
|
|
||||||
width: "100%",
|
|
||||||
background: "rgba(0,0,0,0.5)",
|
|
||||||
color: "#fff",
|
|
||||||
fontSize: 14,
|
|
||||||
padding: "4px 0",
|
|
||||||
borderBottomLeftRadius: 4,
|
|
||||||
borderBottomRightRadius: 4,
|
|
||||||
textAlign: "center",
|
|
||||||
zIndex: 3,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{material.name}
|
{material.name}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -639,19 +571,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
|||||||
})}
|
})}
|
||||||
{/* 添加海报卡片 */}
|
{/* 添加海报卡片 */}
|
||||||
<div
|
<div
|
||||||
style={{
|
className={styles["basic-add-material"]}
|
||||||
border: "2px dashed #bbb",
|
|
||||||
borderRadius: 8,
|
|
||||||
padding: 6,
|
|
||||||
cursor: "pointer",
|
|
||||||
background: "#fafbfc",
|
|
||||||
textAlign: "center",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
height: 190,
|
|
||||||
}}
|
|
||||||
onClick={() => uploadInputRef.current?.click()}
|
onClick={() => uploadInputRef.current?.click()}
|
||||||
>
|
>
|
||||||
<span style={{ fontSize: 36, color: "#bbb", marginBottom: 8 }}>
|
<span style={{ fontSize: 36, color: "#bbb", marginBottom: 8 }}>
|
||||||
@@ -705,11 +625,10 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
|||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
{/* 订单导入区块优化 */}
|
{/* 订单导入区块优化 */}
|
||||||
<div style={openOrder} className="my-4">
|
<div className={styles["basic-order-upload"]} style={openOrder}>
|
||||||
<div style={{ fontWeight: 500, marginBottom: 8 }}>订单表格上传</div>
|
<div className={styles["basic-order-upload-label"]}>订单表格上传</div>
|
||||||
<div style={{ display: "flex", gap: 12, marginBottom: 4 }}>
|
<div className={styles["basic-order-upload-actions"]}>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
|
||||||
style={{ display: "flex", alignItems: "center", gap: 4 }}
|
style={{ display: "flex", alignItems: "center", gap: 4 }}
|
||||||
onClick={handleDownloadTemplate}
|
onClick={handleDownloadTemplate}
|
||||||
>
|
>
|
||||||
@@ -741,78 +660,68 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
|||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ color: "#888", fontSize: 13, marginBottom: 8 }}>
|
<div className={styles["basic-order-upload-tip"]}>
|
||||||
支持 CSV、Excel 格式,上传后将文件保存到服务器
|
支持 CSV、Excel 格式,上传后将文件保存到服务器
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* 电话获客设置区块,仅在选择电话获客场景时显示 */}
|
{/* 电话获客设置区块,仅在选择电话获客场景时显示 */}
|
||||||
{formData.scenario === 5 && (
|
{formData.scenario === 5 && (
|
||||||
<div style={{ margin: "16px 0" }}>
|
<div className={styles["basic-phone-settings"]}>
|
||||||
<div
|
<div style={{ fontWeight: 600, fontSize: 16, marginBottom: 16 }}>
|
||||||
style={{
|
电话获客设置
|
||||||
background: "#f7f8fa",
|
</div>
|
||||||
borderRadius: 10,
|
<div style={{ display: "flex", flexDirection: "column", gap: 18 }}>
|
||||||
padding: 20,
|
<div
|
||||||
boxShadow: "0 2px 8px rgba(0,0,0,0.03)",
|
style={{
|
||||||
marginBottom: 12,
|
display: "flex",
|
||||||
}}
|
alignItems: "center",
|
||||||
>
|
justifyContent: "space-between",
|
||||||
<div style={{ fontWeight: 600, fontSize: 16, marginBottom: 16 }}>
|
}}
|
||||||
电话获客设置
|
>
|
||||||
|
<span>自动加好友</span>
|
||||||
|
<Switch
|
||||||
|
checked={phoneSettings.autoAdd}
|
||||||
|
onChange={(v) =>
|
||||||
|
setPhoneSettings((s) => ({ ...s, autoAdd: v }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 18 }}>
|
<div
|
||||||
<div
|
style={{
|
||||||
style={{
|
display: "flex",
|
||||||
display: "flex",
|
alignItems: "center",
|
||||||
alignItems: "center",
|
justifyContent: "space-between",
|
||||||
justifyContent: "space-between",
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<span>语音转文字</span>
|
||||||
<span>自动加好友</span>
|
<Switch
|
||||||
<Switch
|
checked={phoneSettings.speechToText}
|
||||||
checked={phoneSettings.autoAdd}
|
onChange={(v) =>
|
||||||
onChange={(v) =>
|
setPhoneSettings((s) => ({ ...s, speechToText: v }))
|
||||||
setPhoneSettings((s) => ({ ...s, autoAdd: v }))
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
<div
|
||||||
<div
|
style={{
|
||||||
style={{
|
display: "flex",
|
||||||
display: "flex",
|
alignItems: "center",
|
||||||
alignItems: "center",
|
justifyContent: "space-between",
|
||||||
justifyContent: "space-between",
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<span>问题提取</span>
|
||||||
<span>语音转文字</span>
|
<Switch
|
||||||
<Switch
|
checked={phoneSettings.questionExtraction}
|
||||||
checked={phoneSettings.speechToText}
|
onChange={(v) =>
|
||||||
onChange={(v) =>
|
setPhoneSettings((s) => ({ ...s, questionExtraction: v }))
|
||||||
setPhoneSettings((s) => ({ ...s, speechToText: v }))
|
}
|
||||||
}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>问题提取</span>
|
|
||||||
<Switch
|
|
||||||
checked={phoneSettings.questionExtraction}
|
|
||||||
onChange={(v) =>
|
|
||||||
setPhoneSettings((s) => ({ ...s, questionExtraction: v }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* 微信群设置区块,仅在选择微信群场景时显示 */}
|
{/* 微信群设置区块,仅在选择微信群场景时显示 */}
|
||||||
{formData.scenario === 7 && (
|
{formData.scenario === 7 && (
|
||||||
<div style={{ margin: "16px 0" }}>
|
<div className={styles["basic-wechat-group"]}>
|
||||||
<div style={{ marginBottom: 8 }}>
|
<div style={{ marginBottom: 8 }}>
|
||||||
<Input
|
<Input
|
||||||
value={weixinqunName}
|
value={weixinqunName}
|
||||||
@@ -833,24 +742,19 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div className={styles["basic-footer-switch"]}>
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
margin: "16px 0",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>是否启用</span>
|
<span>是否启用</span>
|
||||||
<Switch
|
<Switch
|
||||||
checked={formData.enabled}
|
checked={formData.enabled}
|
||||||
onChange={(value) => onChange({ ...formData, enabled: value })}
|
onChange={(value) => onChange({ ...formData, enabled: value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button className="mt-4" type="primary" onClick={onNext}>
|
|
||||||
下一步
|
<div className={styles["basic-footer-switch"]}>
|
||||||
</Button>
|
<ButtonMobile block color="primary" onClick={onNext}>
|
||||||
|
下一步
|
||||||
|
</ButtonMobile>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
} from "antd";
|
} from "antd";
|
||||||
import { QuestionCircleOutlined, MessageOutlined } from "@ant-design/icons";
|
import { QuestionCircleOutlined, MessageOutlined } from "@ant-design/icons";
|
||||||
import DeviceSelection from "@/components/DeviceSelection";
|
import DeviceSelection from "@/components/DeviceSelection";
|
||||||
|
import Layout from "@/components/Layout/Layout";
|
||||||
|
|
||||||
interface FriendRequestSettingsProps {
|
interface FriendRequestSettingsProps {
|
||||||
formData: any;
|
formData: any;
|
||||||
@@ -97,143 +98,151 @@ const FriendRequestSettings: React.FC<FriendRequestSettingsProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-6">
|
<Layout
|
||||||
<div>
|
footer={
|
||||||
<span className="font-medium text-base">选择设备</span>
|
<div className="p-4 border-t bg-white">
|
||||||
<div className="mt-2">
|
<div className="flex justify-between">
|
||||||
<DeviceSelection
|
<Button onClick={onPrev}>上一步</Button>
|
||||||
selectedDevices={selectedDevices.map((d) => d.id)}
|
<Button type="primary" onClick={handleNext}>
|
||||||
onSelect={(deviceIds) => {
|
下一步
|
||||||
const newSelectedDevices = deviceIds.map((id) => ({
|
</Button>
|
||||||
id,
|
</div>
|
||||||
name: `设备 ${id}`,
|
</div>
|
||||||
status: "online",
|
}
|
||||||
}));
|
>
|
||||||
setSelectedDevices(newSelectedDevices);
|
<div className="p-4 space-y-6">
|
||||||
onChange({ ...formData, device: deviceIds });
|
<div>
|
||||||
}}
|
<span className="font-medium text-base">选择设备</span>
|
||||||
placeholder="选择设备"
|
<div className="mt-2">
|
||||||
/>
|
<DeviceSelection
|
||||||
|
selectedDevices={selectedDevices.map((d) => d.id)}
|
||||||
|
onSelect={(deviceIds) => {
|
||||||
|
const newSelectedDevices = deviceIds.map((id) => ({
|
||||||
|
id,
|
||||||
|
name: `设备 ${id}`,
|
||||||
|
status: "online",
|
||||||
|
}));
|
||||||
|
setSelectedDevices(newSelectedDevices);
|
||||||
|
onChange({ ...formData, device: deviceIds });
|
||||||
|
}}
|
||||||
|
placeholder="选择设备"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="flex items-center space-x-2 mb-1 relative">
|
<div className="flex items-center space-x-2 mb-1 relative">
|
||||||
<span className="font-medium text-base">好友备注</span>
|
<span className="font-medium text-base">好友备注</span>
|
||||||
<span
|
<span
|
||||||
className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-gray-200 text-gray-500 text-xs cursor-pointer hover:bg-gray-300 transition-colors"
|
className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-gray-200 text-gray-500 text-xs cursor-pointer hover:bg-gray-300 transition-colors"
|
||||||
onMouseEnter={() => setShowRemarkTip(true)}
|
onMouseEnter={() => setShowRemarkTip(true)}
|
||||||
onMouseLeave={() => setShowRemarkTip(false)}
|
onMouseLeave={() => setShowRemarkTip(false)}
|
||||||
onClick={() => setShowRemarkTip((v) => !v)}
|
onClick={() => setShowRemarkTip((v) => !v)}
|
||||||
>
|
>
|
||||||
?
|
?
|
||||||
</span>
|
</span>
|
||||||
{showRemarkTip && (
|
{showRemarkTip && (
|
||||||
<div className="absolute left-24 top-0 z-20 w-64 p-3 bg-white border border-gray-200 rounded shadow-lg text-sm text-gray-700">
|
<div className="absolute left-24 top-0 z-20 w-64 p-3 bg-white border border-gray-200 rounded shadow-lg text-sm text-gray-700">
|
||||||
<div>设置添加好友时的备注格式</div>
|
<div>设置添加好友时的备注格式</div>
|
||||||
<div className="mt-2 text-xs text-gray-500">备注格式预览:</div>
|
<div className="mt-2 text-xs text-gray-500">
|
||||||
<div className="mt-1 text-blue-600">
|
备注格式预览:
|
||||||
{formData.remarkType === "phone" &&
|
</div>
|
||||||
`138****1234+${getScenarioTitle()}`}
|
<div className="mt-1 text-blue-600">
|
||||||
{formData.remarkType === "nickname" &&
|
{formData.remarkType === "phone" &&
|
||||||
`小红书用户2851+${getScenarioTitle()}`}
|
`138****1234+${getScenarioTitle()}`}
|
||||||
{formData.remarkType === "source" &&
|
{formData.remarkType === "nickname" &&
|
||||||
`抖音直播+${getScenarioTitle()}`}
|
`小红书用户2851+${getScenarioTitle()}`}
|
||||||
|
{formData.remarkType === "source" &&
|
||||||
|
`抖音直播+${getScenarioTitle()}`}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
<Select
|
||||||
<Select
|
value={formData.remarkType || "phone"}
|
||||||
value={formData.remarkType || "phone"}
|
onChange={(value) => onChange({ ...formData, remarkType: value })}
|
||||||
onChange={(value) => onChange({ ...formData, remarkType: value })}
|
className="w-full mt-2"
|
||||||
className="w-full mt-2"
|
|
||||||
>
|
|
||||||
{remarkTypes.map((type) => (
|
|
||||||
<Select.Option key={type.value} value={type.value}>
|
|
||||||
{type.label}
|
|
||||||
</Select.Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="font-medium text-base">招呼语</span>
|
|
||||||
<Button
|
|
||||||
onClick={() => setIsTemplateDialogOpen(true)}
|
|
||||||
className="text-blue-500"
|
|
||||||
>
|
>
|
||||||
<MessageOutlined className="h-4 w-4 mr-2" />
|
{remarkTypes.map((type) => (
|
||||||
参考模板
|
<Select.Option key={type.value} value={type.value}>
|
||||||
</Button>
|
{type.label}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
|
||||||
value={formData.greeting}
|
|
||||||
onChange={(e) =>
|
|
||||||
onChange({ ...formData, greeting: e.target.value })
|
|
||||||
}
|
|
||||||
placeholder="请输入招呼语"
|
|
||||||
className="mt-2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium text-base">添加间隔</span>
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center space-x-2 mt-2">
|
<span className="font-medium text-base">招呼语</span>
|
||||||
|
<Button
|
||||||
|
onClick={() => setIsTemplateDialogOpen(true)}
|
||||||
|
className="text-blue-500"
|
||||||
|
>
|
||||||
|
<MessageOutlined className="h-4 w-4 mr-2" />
|
||||||
|
参考模板
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
value={formData.greeting}
|
||||||
value={formData.addFriendInterval || 1}
|
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
onChange({
|
onChange({ ...formData, greeting: e.target.value })
|
||||||
...formData,
|
|
||||||
addFriendInterval: Number(e.target.value),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
/>
|
placeholder="请输入招呼语"
|
||||||
<div className="w-10">分钟</div>
|
className="mt-2"
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<span className="font-medium text-base">允许加人的时间段</span>
|
|
||||||
<div className="flex items-center space-x-2 mt-2">
|
|
||||||
<Input
|
|
||||||
type="time"
|
|
||||||
value={formData.addFriendTimeStart || "09:00"}
|
|
||||||
onChange={(e) =>
|
|
||||||
onChange({ ...formData, addFriendTimeStart: e.target.value })
|
|
||||||
}
|
|
||||||
className="w-32"
|
|
||||||
/>
|
|
||||||
<span>至</span>
|
|
||||||
<Input
|
|
||||||
type="time"
|
|
||||||
value={formData.addFriendTimeEnd || "18:00"}
|
|
||||||
onChange={(e) =>
|
|
||||||
onChange({ ...formData, addFriendTimeEnd: e.target.value })
|
|
||||||
}
|
|
||||||
className="w-32"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{hasWarnings && (
|
<div>
|
||||||
<Alert
|
<span className="font-medium text-base">添加间隔</span>
|
||||||
message="警告"
|
<div className="flex items-center space-x-2 mt-2">
|
||||||
description="您有未完成的设置项,建议完善后再进入下一步。"
|
<Input
|
||||||
type="warning"
|
type="number"
|
||||||
showIcon
|
value={formData.addFriendInterval || 1}
|
||||||
className="bg-amber-50 border-amber-200"
|
onChange={(e) =>
|
||||||
/>
|
onChange({
|
||||||
)}
|
...formData,
|
||||||
|
addFriendInterval: Number(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="w-10">分钟</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between pt-4">
|
<div>
|
||||||
<Button onClick={onPrev}>上一步</Button>
|
<span className="font-medium text-base">允许加人的时间段</span>
|
||||||
<Button type="primary" onClick={handleNext}>
|
<div className="flex items-center space-x-2 mt-2">
|
||||||
下一步
|
<Input
|
||||||
</Button>
|
type="time"
|
||||||
|
value={formData.addFriendTimeStart || "09:00"}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange({ ...formData, addFriendTimeStart: e.target.value })
|
||||||
|
}
|
||||||
|
className="w-32"
|
||||||
|
/>
|
||||||
|
<span>至</span>
|
||||||
|
<Input
|
||||||
|
type="time"
|
||||||
|
value={formData.addFriendTimeEnd || "18:00"}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange({ ...formData, addFriendTimeEnd: e.target.value })
|
||||||
|
}
|
||||||
|
className="w-32"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hasWarnings && (
|
||||||
|
<Alert
|
||||||
|
message="警告"
|
||||||
|
description="您有未完成的设置项,建议完善后再进入下一步。"
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
className="bg-amber-50 border-amber-200"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Layout>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
open={isTemplateDialogOpen}
|
open={isTemplateDialogOpen}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
LinkOutlined,
|
LinkOutlined,
|
||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
|
import Layout from "@/components/Layout/Layout";
|
||||||
|
|
||||||
interface MessageContent {
|
interface MessageContent {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -533,23 +534,29 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-6">
|
<Layout
|
||||||
<div className="flex items-center justify-between">
|
footer={
|
||||||
<h2 className="text-lg font-semibold">消息设置</h2>
|
<div className="p-4 border-t bg-white">
|
||||||
<Button onClick={() => setIsAddDayPlanOpen(true)}>
|
<div className="flex justify-between">
|
||||||
<PlusOutlined className="h-4 w-4" />
|
<Button onClick={onPrev}>上一步</Button>
|
||||||
</Button>
|
<Button type="primary" onClick={onNext}>
|
||||||
</div>
|
下一步
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="p-4 space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-lg font-semibold">消息设置</h2>
|
||||||
|
<Button onClick={() => setIsAddDayPlanOpen(true)}>
|
||||||
|
<PlusOutlined className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Tabs defaultActiveKey="0" items={items} />
|
<Tabs defaultActiveKey="0" items={items} />
|
||||||
|
|
||||||
<div className="flex justify-between pt-4">
|
|
||||||
<Button onClick={onPrev}>上一步</Button>
|
|
||||||
<Button type="primary" onClick={onNext}>
|
|
||||||
下一步
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Layout>
|
||||||
|
|
||||||
{/* 添加天数计划弹窗 */}
|
{/* 添加天数计划弹窗 */}
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
164
nkebao/src/pages/scenarios/plan/new/steps/base.module.scss
Normal file
164
nkebao/src/pages/scenarios/plan/new/steps/base.module.scss
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
.basic-container {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
.basic-scene-select {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.basic-scene-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.basic-scene-btn {
|
||||||
|
height: 40px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 16px;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
background: rgba(#1677ff,0.1);
|
||||||
|
color: #1677ff;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.basic-scene-btn.selected {
|
||||||
|
background: #1677ff;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(22,119,255,0.08);
|
||||||
|
}
|
||||||
|
.basic-label {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.basic-input-block {
|
||||||
|
border: 1px solid #eee;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.basic-tag-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
}
|
||||||
|
.basic-tag-item{
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.basic-custom-tag-input {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.basic-success-tip {
|
||||||
|
display: flex;
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
.basic-materials {
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
.basic-materials-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.basic-material-preview{
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
padding-left: 2px;
|
||||||
|
right: 8px;
|
||||||
|
background:rgba(0,0,0,0.5);
|
||||||
|
border-radius: 50%;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 2;
|
||||||
|
cursor:pointer;
|
||||||
|
}
|
||||||
|
.basic-material-card {
|
||||||
|
border: 2px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #fff;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
min-height: 192px;
|
||||||
|
transition: border 0.2s;
|
||||||
|
}
|
||||||
|
.basic-material-card.selected {
|
||||||
|
border: 2px solid #1890ff;
|
||||||
|
background: #e6f7ff;
|
||||||
|
}
|
||||||
|
.basic-material-img {
|
||||||
|
width: 100px;
|
||||||
|
height: 180px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.basic-material-name {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 4px 0;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
text-align: center;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
.basic-add-material {
|
||||||
|
border: 2px dashed #bbb;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #fafbfc;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 190px;
|
||||||
|
}
|
||||||
|
.basic-order-upload {
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
.basic-order-upload-label {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.basic-order-upload-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.basic-order-upload-tip {
|
||||||
|
color: #888;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.basic-phone-settings {
|
||||||
|
margin: 16px 0;
|
||||||
|
background: #f7f8fa;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.03);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.basic-wechat-group {
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
.basic-footer-switch {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { NavBar } from "antd-mobile";
|
|
||||||
import Layout from "@/components/Layout/Layout";
|
|
||||||
import MeauMobile from "@/components/MeauMobile/MeauMoible";
|
|
||||||
|
|
||||||
const WechatAccountDetail: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<Layout
|
|
||||||
header={
|
|
||||||
<NavBar
|
|
||||||
backArrow
|
|
||||||
style={{ background: "#fff" }}
|
|
||||||
onBack={() => window.history.back()}
|
|
||||||
>
|
|
||||||
<div style={{ color: "var(--primary-color)", fontWeight: 600 }}>
|
|
||||||
微信号详情
|
|
||||||
</div>
|
|
||||||
</NavBar>
|
|
||||||
}
|
|
||||||
footer={<MeauMobile />}
|
|
||||||
>
|
|
||||||
<div style={{ padding: 20, textAlign: "center", color: "#666" }}>
|
|
||||||
<h3>微信号详情页面</h3>
|
|
||||||
<p>此页面正在开发中...</p>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default WechatAccountDetail;
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { NavBar, Button } from "antd-mobile";
|
|
||||||
import { PlusOutlined } from "@ant-design/icons";
|
|
||||||
import Layout from "@/components/Layout/Layout";
|
|
||||||
import MeauMobile from "@/components/MeauMobile/MeauMoible";
|
|
||||||
|
|
||||||
const WechatAccounts: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<Layout
|
|
||||||
header={
|
|
||||||
<NavBar
|
|
||||||
back={null}
|
|
||||||
style={{ background: "#fff" }}
|
|
||||||
left={
|
|
||||||
<div style={{ color: "var(--primary-color)", fontWeight: 600 }}>
|
|
||||||
微信号管理
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
right={
|
|
||||||
<Button size="small" color="primary">
|
|
||||||
<PlusOutlined />
|
|
||||||
<span style={{ marginLeft: 4, fontSize: 12 }}>添加微信号</span>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
footer={<MeauMobile />}
|
|
||||||
>
|
|
||||||
<div style={{ padding: 20, textAlign: "center", color: "#666" }}>
|
|
||||||
<h3>微信号管理页面</h3>
|
|
||||||
<p>此页面正在开发中...</p>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default WechatAccounts;
|
|
||||||
26
nkebao/src/pages/wechat-accounts/detail/api.ts
Normal file
26
nkebao/src/pages/wechat-accounts/detail/api.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import request from "@/api/request";
|
||||||
|
|
||||||
|
// 获取微信号详情
|
||||||
|
export function getWechatAccountDetail(id: string) {
|
||||||
|
return request("/WechatAccount/detail", { id }, "GET");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取微信号summary
|
||||||
|
export function getWechatAccountSummary(id: string) {
|
||||||
|
return request(`/v1/wechats/${id}/summary`, {}, "GET");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取微信号好友列表
|
||||||
|
export function getWechatFriends(params: {
|
||||||
|
wechatAccountKeyword: string;
|
||||||
|
pageIndex: number;
|
||||||
|
pageSize: number;
|
||||||
|
friendKeyword?: string;
|
||||||
|
}) {
|
||||||
|
return request("/WechatFriend/friendlistData", params, "POST");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取微信好友详情
|
||||||
|
export function getWechatFriendDetail(id: string) {
|
||||||
|
return request("/v1/WechatFriend/detail", { id }, "GET");
|
||||||
|
}
|
||||||
720
nkebao/src/pages/wechat-accounts/detail/detail.module.scss
Normal file
720
nkebao/src/pages/wechat-accounts/detail/detail.module.scss
Normal file
@@ -0,0 +1,720 @@
|
|||||||
|
.wechat-account-detail-page {
|
||||||
|
padding: 16px;
|
||||||
|
background: linear-gradient(to bottom, #f0f8ff, #ffffff);
|
||||||
|
min-height: 100vh;
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-card {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
border: 1px solid #e8f4fd;
|
||||||
|
|
||||||
|
.account-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.avatar-section {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 4px solid #e8f4fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 2px;
|
||||||
|
right: 2px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
|
||||||
|
&.status-normal {
|
||||||
|
background: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-abnormal {
|
||||||
|
background: #ff4d4f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.name-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
.nickname {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin: 0;
|
||||||
|
max-width: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-id {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
background: #fff;
|
||||||
|
color: #666;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-color: #bfbfbf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-card {
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
border: 1px solid #e8f4fd;
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
.adm-tabs-header {
|
||||||
|
border-bottom: 1px solid #e8e8e8;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
|
||||||
|
.adm-tabs-tab {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #666;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&.adm-tabs-tab-active {
|
||||||
|
color: #1677ff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.adm-tabs-tab-line {
|
||||||
|
background: #1677ff;
|
||||||
|
height: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.adm-tabs-content {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-content {
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.info-card {
|
||||||
|
background: linear-gradient(135deg, #e6f7ff, #f0f8ff);
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #bae7ff;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #1677ff;
|
||||||
|
padding: 6px;
|
||||||
|
background: #e6f7ff;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-title {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.title-text {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1677ff;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-sub {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
text-align: right;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1677ff;
|
||||||
|
|
||||||
|
.value-unit {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.weight-card {
|
||||||
|
background: linear-gradient(135deg, #fff7e6, #fff2d9);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #ffd591;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weight-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.weight-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #fa8c16;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weight-title {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fa8c16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weight-score {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&.text-green-600 {
|
||||||
|
background: #f6ffed;
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.text-yellow-600 {
|
||||||
|
background: #fffbe6;
|
||||||
|
color: #fa8c16;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.text-red-600 {
|
||||||
|
background: #fff2f0;
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-value {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-unit {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.weight-description {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #fa8c16;
|
||||||
|
background: #fff7e6;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #ffd591;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weight-items {
|
||||||
|
.weight-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.item-label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 64px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #fa8c16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0 12px;
|
||||||
|
height: 8px;
|
||||||
|
background: #ffd591;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #fa8c16, #ffa940);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: width 0.5s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-value {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 40px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #fa8c16;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.restrictions-card {
|
||||||
|
background: linear-gradient(135deg, #fff2f0, #fff1f0);
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #ffccc7;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
.restrictions-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.restrictions-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #ff4d4f;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restrictions-title {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restrictions-btn {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.restrictions-list {
|
||||||
|
.restriction-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #ffccc7;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restriction-info {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.restriction-reason {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restriction-date {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.restriction-level {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.text-red-600 {
|
||||||
|
background: #fff2f0;
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.text-yellow-600 {
|
||||||
|
background: #fffbe6;
|
||||||
|
color: #fa8c16;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.text-gray-600 {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.friends-content {
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.search-input-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.adm-input {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-btn {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.friends-list {
|
||||||
|
.empty {
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
padding: 40px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
text-align: center;
|
||||||
|
color: #ff4d4f;
|
||||||
|
padding: 40px 0;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.friend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.friend-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.friend-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.friend-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
.friend-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
max-width: 180px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.friend-remark {
|
||||||
|
color: #666;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.friend-arrow {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.friend-wechat-id {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.friend-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.friend-tag {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-more {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-content {
|
||||||
|
padding: 20px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.popup-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restrictions-detail {
|
||||||
|
.restriction-detail-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restriction-detail-info {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.restriction-detail-reason {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restriction-detail-date {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.restriction-detail-level {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.text-red-600 {
|
||||||
|
background: #fff2f0;
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.text-yellow-600 {
|
||||||
|
background: #fffbe6;
|
||||||
|
color: #fa8c16;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.text-gray-600 {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-detail {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-detail {
|
||||||
|
text-align: center;
|
||||||
|
color: #ff4d4f;
|
||||||
|
padding: 40px 0;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.friend-detail-content {
|
||||||
|
.friend-detail-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
.friend-detail-avatar {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.friend-detail-info {
|
||||||
|
.friend-detail-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.friend-detail-wechat-id {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.friend-detail-items {
|
||||||
|
.detail-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
text-align: right;
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 16px;
|
||||||
|
|
||||||
|
.detail-tag {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
940
nkebao/src/pages/wechat-accounts/detail/index.tsx
Normal file
940
nkebao/src/pages/wechat-accounts/detail/index.tsx
Normal file
@@ -0,0 +1,940 @@
|
|||||||
|
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||||
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
NavBar,
|
||||||
|
Card,
|
||||||
|
Tabs,
|
||||||
|
Button,
|
||||||
|
SpinLoading,
|
||||||
|
Popup,
|
||||||
|
Toast,
|
||||||
|
Input,
|
||||||
|
Avatar,
|
||||||
|
Tag,
|
||||||
|
} from "antd-mobile";
|
||||||
|
import NavCommon from "@/components/NavCommon";
|
||||||
|
import {
|
||||||
|
SearchOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
ClockCircleOutlined,
|
||||||
|
MessageOutlined,
|
||||||
|
StarOutlined,
|
||||||
|
ExclamationCircleOutlined,
|
||||||
|
RightOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import Layout from "@/components/Layout/Layout";
|
||||||
|
import style from "./detail.module.scss";
|
||||||
|
import {
|
||||||
|
getWechatAccountDetail,
|
||||||
|
getWechatAccountSummary,
|
||||||
|
getWechatFriends,
|
||||||
|
getWechatFriendDetail,
|
||||||
|
} from "./api";
|
||||||
|
|
||||||
|
interface WechatAccountSummary {
|
||||||
|
accountAge: string;
|
||||||
|
activityLevel: {
|
||||||
|
allTimes: number;
|
||||||
|
dayTimes: number;
|
||||||
|
};
|
||||||
|
accountWeight: {
|
||||||
|
scope: number;
|
||||||
|
ageWeight: number;
|
||||||
|
activityWeigth: number;
|
||||||
|
restrictWeight: number;
|
||||||
|
realNameWeight: number;
|
||||||
|
};
|
||||||
|
statistics: {
|
||||||
|
todayAdded: number;
|
||||||
|
addLimit: number;
|
||||||
|
};
|
||||||
|
restrictions: {
|
||||||
|
id: number;
|
||||||
|
level: string;
|
||||||
|
reason: string;
|
||||||
|
date: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Friend {
|
||||||
|
id: string;
|
||||||
|
avatar: string;
|
||||||
|
nickname: string;
|
||||||
|
wechatId: string;
|
||||||
|
remark: string;
|
||||||
|
addTime: string;
|
||||||
|
lastInteraction: string;
|
||||||
|
tags: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
}>;
|
||||||
|
region: string;
|
||||||
|
source: string;
|
||||||
|
notes: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WechatFriendDetail {
|
||||||
|
id: number;
|
||||||
|
avatar: string;
|
||||||
|
nickname: string;
|
||||||
|
region: string;
|
||||||
|
wechatId: string;
|
||||||
|
addDate: string;
|
||||||
|
tags: string[];
|
||||||
|
memo: string;
|
||||||
|
source: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WechatAccountDetail: React.FC = () => {
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [accountSummary, setAccountSummary] =
|
||||||
|
useState<WechatAccountSummary | null>(null);
|
||||||
|
const [accountInfo, setAccountInfo] = useState<any>(null);
|
||||||
|
const [showRestrictions, setShowRestrictions] = useState(false);
|
||||||
|
const [showTransferConfirm, setShowTransferConfirm] = useState(false);
|
||||||
|
const [showFriendDetail, setShowFriendDetail] = useState(false);
|
||||||
|
const [selectedFriend, setSelectedFriend] = useState<Friend | null>(null);
|
||||||
|
const [friendDetail, setFriendDetail] = useState<WechatFriendDetail | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [isLoadingFriendDetail, setIsLoadingFriendDetail] = useState(false);
|
||||||
|
const [friendDetailError, setFriendDetailError] = useState<string | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [activeTab, setActiveTab] = useState("overview");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [loadingInfo, setLoadingInfo] = useState(true);
|
||||||
|
const [loadingSummary, setLoadingSummary] = useState(true);
|
||||||
|
|
||||||
|
// 好友列表相关状态
|
||||||
|
const [friends, setFriends] = useState<Friend[]>([]);
|
||||||
|
const [friendsPage, setFriendsPage] = useState(1);
|
||||||
|
const [friendsTotal, setFriendsTotal] = useState(0);
|
||||||
|
const [hasMoreFriends, setHasMoreFriends] = useState(true);
|
||||||
|
const [isFetchingFriends, setIsFetchingFriends] = useState(false);
|
||||||
|
const [hasFriendLoadError, setHasFriendLoadError] = useState(false);
|
||||||
|
const [isFriendsEmpty, setIsFriendsEmpty] = useState(false);
|
||||||
|
const friendsObserver = useRef<IntersectionObserver | null>(null);
|
||||||
|
const friendsLoadingRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
// 获取基础信息
|
||||||
|
const fetchAccountInfo = useCallback(async () => {
|
||||||
|
if (!id) return;
|
||||||
|
setLoadingInfo(true);
|
||||||
|
try {
|
||||||
|
const response = await getWechatAccountDetail(id);
|
||||||
|
if (response && response.data) {
|
||||||
|
setAccountInfo(response.data);
|
||||||
|
} else {
|
||||||
|
Toast.show({
|
||||||
|
content: response?.msg || "获取账号信息失败",
|
||||||
|
position: "top",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Toast.show({ content: "获取账号信息失败", position: "top" });
|
||||||
|
} finally {
|
||||||
|
setLoadingInfo(false);
|
||||||
|
}
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
// 获取summary
|
||||||
|
const fetchAccountSummary = useCallback(async () => {
|
||||||
|
if (!id) return;
|
||||||
|
setLoadingSummary(true);
|
||||||
|
try {
|
||||||
|
const response = await getWechatAccountSummary(id);
|
||||||
|
if (response && response.data) {
|
||||||
|
setAccountSummary(response.data);
|
||||||
|
} else {
|
||||||
|
Toast.show({
|
||||||
|
content: response?.msg || "获取账号概览失败",
|
||||||
|
position: "top",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Toast.show({ content: "获取账号概览失败", position: "top" });
|
||||||
|
} finally {
|
||||||
|
setLoadingSummary(false);
|
||||||
|
}
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
// 获取好友列表
|
||||||
|
const fetchFriends = useCallback(
|
||||||
|
async (page: number = 1, isNewSearch: boolean = false) => {
|
||||||
|
if (!id || isFetchingFriends) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsFetchingFriends(true);
|
||||||
|
setHasFriendLoadError(false);
|
||||||
|
const response = await getWechatFriends({
|
||||||
|
wechatAccountKeyword: id,
|
||||||
|
pageIndex: page,
|
||||||
|
pageSize: 20,
|
||||||
|
friendKeyword: searchQuery,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response && response.data) {
|
||||||
|
const newFriends = response.data.list.map((friend: any) => ({
|
||||||
|
id: friend.id.toString(),
|
||||||
|
avatar: friend.avatar || "/placeholder.svg",
|
||||||
|
nickname: friend.nickname || "未知用户",
|
||||||
|
wechatId: friend.wechatId || "",
|
||||||
|
remark: friend.memo || "",
|
||||||
|
addTime:
|
||||||
|
friend.createTime || new Date().toISOString().split("T")[0],
|
||||||
|
lastInteraction:
|
||||||
|
friend.lastInteraction || new Date().toISOString().split("T")[0],
|
||||||
|
tags: friend.tags
|
||||||
|
? friend.tags.map((tag: string, index: number) => ({
|
||||||
|
id: `tag-${index}`,
|
||||||
|
name: tag,
|
||||||
|
color: getRandomTagColor(),
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
region: friend.region || "未知",
|
||||||
|
source: friend.source || "未知",
|
||||||
|
notes: friend.notes || "",
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (isNewSearch) {
|
||||||
|
setFriends(newFriends);
|
||||||
|
if (newFriends.length === 0) {
|
||||||
|
setIsFriendsEmpty(true);
|
||||||
|
setHasMoreFriends(false);
|
||||||
|
} else {
|
||||||
|
setIsFriendsEmpty(false);
|
||||||
|
setHasMoreFriends(newFriends.length === 20);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setFriends((prev) => [...prev, ...newFriends]);
|
||||||
|
setHasMoreFriends(newFriends.length === 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFriendsTotal(response.data.total);
|
||||||
|
setFriendsPage(page);
|
||||||
|
} else {
|
||||||
|
setHasFriendLoadError(true);
|
||||||
|
if (isNewSearch) {
|
||||||
|
setFriends([]);
|
||||||
|
setIsFriendsEmpty(true);
|
||||||
|
setHasMoreFriends(false);
|
||||||
|
}
|
||||||
|
Toast.show({
|
||||||
|
content: response?.msg || "获取好友列表失败",
|
||||||
|
position: "top",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取好友列表失败:", error);
|
||||||
|
setHasFriendLoadError(true);
|
||||||
|
if (isNewSearch) {
|
||||||
|
setFriends([]);
|
||||||
|
setIsFriendsEmpty(true);
|
||||||
|
setHasMoreFriends(false);
|
||||||
|
}
|
||||||
|
Toast.show({
|
||||||
|
content: "获取好友列表失败,请检查网络连接",
|
||||||
|
position: "top",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsFetchingFriends(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[id, searchQuery, isFetchingFriends]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 初始化数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
fetchAccountInfo();
|
||||||
|
fetchAccountSummary();
|
||||||
|
if (activeTab === "friends") {
|
||||||
|
fetchFriends(1, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
// 监听标签切换
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeTab === "friends" && id) {
|
||||||
|
setIsFriendsEmpty(false);
|
||||||
|
setHasFriendLoadError(false);
|
||||||
|
fetchFriends(1, true);
|
||||||
|
}
|
||||||
|
}, [activeTab, id, fetchFriends]);
|
||||||
|
|
||||||
|
// 无限滚动加载好友
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!friendsLoadingRef.current ||
|
||||||
|
!hasMoreFriends ||
|
||||||
|
isFetchingFriends ||
|
||||||
|
isFriendsEmpty
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
friendsObserver.current = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
if (
|
||||||
|
entries[0].isIntersecting &&
|
||||||
|
hasMoreFriends &&
|
||||||
|
!isFetchingFriends &&
|
||||||
|
!isFriendsEmpty
|
||||||
|
) {
|
||||||
|
fetchFriends(friendsPage + 1, false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 }
|
||||||
|
);
|
||||||
|
|
||||||
|
friendsObserver.current.observe(friendsLoadingRef.current);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (friendsObserver.current) {
|
||||||
|
friendsObserver.current.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
hasMoreFriends,
|
||||||
|
isFetchingFriends,
|
||||||
|
friendsPage,
|
||||||
|
fetchFriends,
|
||||||
|
isFriendsEmpty,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 工具函数
|
||||||
|
const getRandomTagColor = (): string => {
|
||||||
|
const colors = [
|
||||||
|
"bg-blue-100 text-blue-800",
|
||||||
|
"bg-green-100 text-green-800",
|
||||||
|
"bg-red-100 text-red-800",
|
||||||
|
"bg-pink-100 text-pink-800",
|
||||||
|
"bg-emerald-100 text-emerald-800",
|
||||||
|
"bg-amber-100 text-amber-800",
|
||||||
|
];
|
||||||
|
return colors[Math.floor(Math.random() * colors.length)];
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateAccountAge = (registerTime: string) => {
|
||||||
|
const registerDate = new Date(registerTime);
|
||||||
|
const now = new Date();
|
||||||
|
const diffTime = Math.abs(now.getTime() - registerDate.getTime());
|
||||||
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
const years = Math.floor(diffDays / 365);
|
||||||
|
const months = Math.floor((diffDays % 365) / 30);
|
||||||
|
return { years, months };
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatAccountAge = (age: { years: number; months: number }) => {
|
||||||
|
if (age.years > 0) {
|
||||||
|
return `${age.years}年${age.months}个月`;
|
||||||
|
}
|
||||||
|
return `${age.months}个月`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWeightColor = (weight: number) => {
|
||||||
|
if (weight >= 80) return "text-green-600";
|
||||||
|
if (weight >= 60) return "text-yellow-600";
|
||||||
|
return "text-red-600";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWeightDescription = (weight: number) => {
|
||||||
|
if (weight >= 80) return "账号质量优秀,可以正常使用";
|
||||||
|
if (weight >= 60) return "账号质量良好,需要注意使用频率";
|
||||||
|
return "账号质量较差,建议谨慎使用";
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTransferFriends = () => {
|
||||||
|
setShowTransferConfirm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmTransferFriends = () => {
|
||||||
|
Toast.show({
|
||||||
|
content: "好友转移计划已创建,请在场景获客中查看详情",
|
||||||
|
position: "top",
|
||||||
|
});
|
||||||
|
setShowTransferConfirm(false);
|
||||||
|
navigate("/scenarios");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFriendClick = async (friend: Friend) => {
|
||||||
|
setSelectedFriend(friend);
|
||||||
|
setShowFriendDetail(true);
|
||||||
|
setIsLoadingFriendDetail(true);
|
||||||
|
setFriendDetailError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await getWechatFriendDetail(friend.id);
|
||||||
|
if (response && response.data) {
|
||||||
|
setFriendDetail(response.data);
|
||||||
|
} else {
|
||||||
|
setFriendDetailError(response?.msg || "获取好友详情失败");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取好友详情失败:", error);
|
||||||
|
setFriendDetailError("网络错误,请稍后重试");
|
||||||
|
} finally {
|
||||||
|
setIsLoadingFriendDetail(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRestrictionLevelColor = (level: string) => {
|
||||||
|
switch (level) {
|
||||||
|
case "high":
|
||||||
|
return "text-red-600";
|
||||||
|
case "medium":
|
||||||
|
return "text-yellow-600";
|
||||||
|
default:
|
||||||
|
return "text-gray-600";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDateTime = (dateString: string) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date
|
||||||
|
.toLocaleString("zh-CN", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
})
|
||||||
|
.replace(/\//g, "-");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
setIsFriendsEmpty(false);
|
||||||
|
setHasFriendLoadError(false);
|
||||||
|
fetchFriends(1, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTabChange = (value: string) => {
|
||||||
|
setActiveTab(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loadingInfo || loadingSummary) {
|
||||||
|
return (
|
||||||
|
<Layout
|
||||||
|
header={
|
||||||
|
<NavBar back={null} style={{ background: "#fff" }}>
|
||||||
|
<span className={style["nav-title"]}>微信号详情</span>
|
||||||
|
</NavBar>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={style["loading"]}>
|
||||||
|
<SpinLoading color="primary" style={{ fontSize: 32 }} />
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout header={<NavCommon title="微信号详情" />}>
|
||||||
|
<div className={style["wechat-account-detail-page"]}>
|
||||||
|
{/* 账号基本信息卡片 */}
|
||||||
|
<Card className={style["account-card"]}>
|
||||||
|
<div className={style["account-info"]}>
|
||||||
|
<div className={style["avatar-section"]}>
|
||||||
|
<Avatar
|
||||||
|
src={accountInfo?.avatar || "/placeholder.svg"}
|
||||||
|
className={style["avatar"]}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`${style["status-dot"]} ${accountInfo?.wechatStatus === 1 ? style["status-normal"] : style["status-abnormal"]}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={style["info-section"]}>
|
||||||
|
<div className={style["name-row"]}>
|
||||||
|
<h2 className={style["nickname"]}>
|
||||||
|
{accountInfo?.nickname || "未知昵称"}
|
||||||
|
</h2>
|
||||||
|
<Tag
|
||||||
|
color={accountInfo?.wechatStatus === 1 ? "success" : "danger"}
|
||||||
|
className={style["status-tag"]}
|
||||||
|
>
|
||||||
|
{accountInfo?.wechatStatus === 1 ? "正常" : "异常"}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
<p className={style["wechat-id"]}>
|
||||||
|
微信号:{accountInfo?.wechatAccount || "未知"}
|
||||||
|
</p>
|
||||||
|
<div className={style["action-buttons"]}>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
fill="outline"
|
||||||
|
className={style["action-btn"]}
|
||||||
|
>
|
||||||
|
<UserOutlined /> {accountInfo?.deviceMemo || "未知设备"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
fill="outline"
|
||||||
|
className={style["action-btn"]}
|
||||||
|
onClick={handleTransferFriends}
|
||||||
|
>
|
||||||
|
<UserOutlined /> 好友转移
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 标签页 */}
|
||||||
|
<Card className={style["tabs-card"]}>
|
||||||
|
<Tabs
|
||||||
|
activeKey={activeTab}
|
||||||
|
onChange={handleTabChange}
|
||||||
|
className={style["tabs"]}
|
||||||
|
>
|
||||||
|
<Tabs.Tab title="账号概览" key="overview">
|
||||||
|
<div className={style["overview-content"]}>
|
||||||
|
{/* 账号基础信息 */}
|
||||||
|
<div className={style["info-grid"]}>
|
||||||
|
<div className={style["info-card"]}>
|
||||||
|
<div className={style["info-header"]}>
|
||||||
|
<ClockCircleOutlined className={style["info-icon"]} />
|
||||||
|
<div className={style["info-title"]}>
|
||||||
|
<div className={style["title-text"]}>账号年龄</div>
|
||||||
|
{accountSummary && (
|
||||||
|
<div className={style["title-sub"]}>
|
||||||
|
注册于{" "}
|
||||||
|
{new Date(
|
||||||
|
accountSummary.accountAge
|
||||||
|
).toLocaleDateString()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{accountSummary && (
|
||||||
|
<div className={style["info-value"]}>
|
||||||
|
{formatAccountAge(
|
||||||
|
calculateAccountAge(accountSummary.accountAge)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={style["info-card"]}>
|
||||||
|
<div className={style["info-header"]}>
|
||||||
|
<MessageOutlined className={style["info-icon"]} />
|
||||||
|
<div className={style["info-title"]}>
|
||||||
|
<div className={style["title-text"]}>活跃程度</div>
|
||||||
|
{accountSummary && (
|
||||||
|
<div className={style["title-sub"]}>
|
||||||
|
总聊天{" "}
|
||||||
|
{accountSummary.activityLevel.allTimes.toLocaleString()}{" "}
|
||||||
|
次
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{accountSummary && (
|
||||||
|
<div className={style["info-value"]}>
|
||||||
|
{accountSummary.activityLevel.dayTimes.toLocaleString()}
|
||||||
|
<span className={style["value-unit"]}>次/天</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 账号权重评估 */}
|
||||||
|
{accountSummary && (
|
||||||
|
<div className={style["weight-card"]}>
|
||||||
|
<div className={style["weight-header"]}>
|
||||||
|
<StarOutlined className={style["weight-icon"]} />
|
||||||
|
<span className={style["weight-title"]}>
|
||||||
|
账号权重评估
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
className={`${style["weight-score"]} ${getWeightColor(accountSummary.accountWeight.scope)}`}
|
||||||
|
>
|
||||||
|
<span className={style["score-value"]}>
|
||||||
|
{accountSummary.accountWeight.scope}
|
||||||
|
</span>
|
||||||
|
<span className={style["score-unit"]}>分</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className={style["weight-description"]}>
|
||||||
|
{getWeightDescription(accountSummary.accountWeight.scope)}
|
||||||
|
</p>
|
||||||
|
<div className={style["weight-items"]}>
|
||||||
|
<div className={style["weight-item"]}>
|
||||||
|
<span className={style["item-label"]}>账号年龄</span>
|
||||||
|
<div className={style["progress-bar"]}>
|
||||||
|
<div
|
||||||
|
className={style["progress-fill"]}
|
||||||
|
style={{
|
||||||
|
width: `${accountSummary.accountWeight.ageWeight}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className={style["item-value"]}>
|
||||||
|
{accountSummary.accountWeight.ageWeight}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={style["weight-item"]}>
|
||||||
|
<span className={style["item-label"]}>活跃度</span>
|
||||||
|
<div className={style["progress-bar"]}>
|
||||||
|
<div
|
||||||
|
className={style["progress-fill"]}
|
||||||
|
style={{
|
||||||
|
width: `${accountSummary.accountWeight.activityWeigth}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className={style["item-value"]}>
|
||||||
|
{accountSummary.accountWeight.activityWeigth}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 限制记录 */}
|
||||||
|
{accountSummary &&
|
||||||
|
accountSummary.restrictions &&
|
||||||
|
accountSummary.restrictions.length > 0 && (
|
||||||
|
<div className={style["restrictions-card"]}>
|
||||||
|
<div className={style["restrictions-header"]}>
|
||||||
|
<ExclamationCircleOutlined
|
||||||
|
className={style["restrictions-icon"]}
|
||||||
|
/>
|
||||||
|
<span className={style["restrictions-title"]}>
|
||||||
|
限制记录
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
fill="outline"
|
||||||
|
onClick={() => setShowRestrictions(true)}
|
||||||
|
className={style["restrictions-btn"]}
|
||||||
|
>
|
||||||
|
查看详情
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className={style["restrictions-list"]}>
|
||||||
|
{accountSummary.restrictions
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((restriction) => (
|
||||||
|
<div
|
||||||
|
key={restriction.id}
|
||||||
|
className={style["restriction-item"]}
|
||||||
|
>
|
||||||
|
<div className={style["restriction-info"]}>
|
||||||
|
<span className={style["restriction-reason"]}>
|
||||||
|
{restriction.reason}
|
||||||
|
</span>
|
||||||
|
<span className={style["restriction-date"]}>
|
||||||
|
{formatDateTime(restriction.date)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={`${style["restriction-level"]} ${getRestrictionLevelColor(restriction.level)}`}
|
||||||
|
>
|
||||||
|
{restriction.level === "high"
|
||||||
|
? "高风险"
|
||||||
|
: restriction.level === "medium"
|
||||||
|
? "中风险"
|
||||||
|
: "低风险"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Tabs.Tab>
|
||||||
|
|
||||||
|
<Tabs.Tab
|
||||||
|
title={`好友列表${activeTab === "friends" && friendsTotal > 0 ? ` (${friendsTotal.toLocaleString()})` : ""}`}
|
||||||
|
key="friends"
|
||||||
|
>
|
||||||
|
<div className={style["friends-content"]}>
|
||||||
|
{/* 搜索栏 */}
|
||||||
|
<div className={style["search-bar"]}>
|
||||||
|
<div className={style["search-input-wrapper"]}>
|
||||||
|
<Input
|
||||||
|
placeholder="搜索好友昵称/微信号"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
prefix={<SearchOutlined />}
|
||||||
|
allowClear
|
||||||
|
size="large"
|
||||||
|
onPressEnter={handleSearch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={handleSearch}
|
||||||
|
loading={isFetchingFriends}
|
||||||
|
className={style["search-btn"]}
|
||||||
|
>
|
||||||
|
<ReloadOutlined />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 好友列表 */}
|
||||||
|
<div className={style["friends-list"]}>
|
||||||
|
{isFriendsEmpty ? (
|
||||||
|
<div className={style["empty"]}>暂无好友数据</div>
|
||||||
|
) : hasFriendLoadError ? (
|
||||||
|
<div className={style["error"]}>
|
||||||
|
<p>加载失败,请重试</p>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={() => fetchFriends(1, true)}
|
||||||
|
>
|
||||||
|
重试
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{friends.map((friend) => (
|
||||||
|
<div
|
||||||
|
key={friend.id}
|
||||||
|
className={style["friend-item"]}
|
||||||
|
onClick={() => handleFriendClick(friend)}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
src={friend.avatar}
|
||||||
|
className={style["friend-avatar"]}
|
||||||
|
/>
|
||||||
|
<div className={style["friend-info"]}>
|
||||||
|
<div className={style["friend-header"]}>
|
||||||
|
<div className={style["friend-name"]}>
|
||||||
|
{friend.nickname}
|
||||||
|
{friend.remark && (
|
||||||
|
<span className={style["friend-remark"]}>
|
||||||
|
({friend.remark})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<RightOutlined
|
||||||
|
className={style["friend-arrow"]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={style["friend-wechat-id"]}>
|
||||||
|
{friend.wechatId}
|
||||||
|
</div>
|
||||||
|
<div className={style["friend-tags"]}>
|
||||||
|
{friend.tags?.map((tag, index) => (
|
||||||
|
<Tag
|
||||||
|
key={index}
|
||||||
|
size="small"
|
||||||
|
className={style["friend-tag"]}
|
||||||
|
>
|
||||||
|
{typeof tag === "string" ? tag : tag.name}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{hasMoreFriends && !isFriendsEmpty && (
|
||||||
|
<div
|
||||||
|
ref={friendsLoadingRef}
|
||||||
|
className={style["loading-more"]}
|
||||||
|
>
|
||||||
|
<SpinLoading
|
||||||
|
color="primary"
|
||||||
|
style={{ fontSize: 24 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs.Tab>
|
||||||
|
</Tabs>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 限制记录详情弹窗 */}
|
||||||
|
<Popup
|
||||||
|
visible={showRestrictions}
|
||||||
|
onMaskClick={() => setShowRestrictions(false)}
|
||||||
|
bodyStyle={{ borderRadius: "16px 16px 0 0" }}
|
||||||
|
>
|
||||||
|
<div className={style["popup-content"]}>
|
||||||
|
<div className={style["popup-header"]}>
|
||||||
|
<h3>限制记录详情</h3>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
fill="outline"
|
||||||
|
onClick={() => setShowRestrictions(false)}
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className={style["popup-description"]}>每次限制恢复时间为24小时</p>
|
||||||
|
{accountSummary && accountSummary.restrictions && (
|
||||||
|
<div className={style["restrictions-detail"]}>
|
||||||
|
{accountSummary.restrictions.map((restriction) => (
|
||||||
|
<div
|
||||||
|
key={restriction.id}
|
||||||
|
className={style["restriction-detail-item"]}
|
||||||
|
>
|
||||||
|
<div className={style["restriction-detail-info"]}>
|
||||||
|
<div className={style["restriction-detail-reason"]}>
|
||||||
|
{restriction.reason}
|
||||||
|
</div>
|
||||||
|
<div className={style["restriction-detail-date"]}>
|
||||||
|
{formatDateTime(restriction.date)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={`${style["restriction-detail-level"]} ${getRestrictionLevelColor(restriction.level)}`}
|
||||||
|
>
|
||||||
|
{restriction.level === "high"
|
||||||
|
? "高风险"
|
||||||
|
: restriction.level === "medium"
|
||||||
|
? "中风险"
|
||||||
|
: "低风险"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Popup>
|
||||||
|
|
||||||
|
{/* 好友转移确认弹窗 */}
|
||||||
|
<Popup
|
||||||
|
visible={showTransferConfirm}
|
||||||
|
onMaskClick={() => setShowTransferConfirm(false)}
|
||||||
|
bodyStyle={{ borderRadius: "16px 16px 0 0" }}
|
||||||
|
>
|
||||||
|
<div className={style["popup-content"]}>
|
||||||
|
<div className={style["popup-header"]}>
|
||||||
|
<h3>确认好友转移</h3>
|
||||||
|
</div>
|
||||||
|
<p className={style["popup-description"]}>
|
||||||
|
确定要将该微信号的好友转移到其他账号吗?此操作将创建一个好友转移计划。
|
||||||
|
</p>
|
||||||
|
<div className={style["popup-actions"]}>
|
||||||
|
<Button block color="primary" onClick={confirmTransferFriends}>
|
||||||
|
确认转移
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
color="danger"
|
||||||
|
fill="outline"
|
||||||
|
onClick={() => setShowTransferConfirm(false)}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popup>
|
||||||
|
|
||||||
|
{/* 好友详情弹窗 */}
|
||||||
|
<Popup
|
||||||
|
visible={showFriendDetail}
|
||||||
|
onMaskClick={() => setShowFriendDetail(false)}
|
||||||
|
bodyStyle={{ borderRadius: "16px 16px 0 0" }}
|
||||||
|
>
|
||||||
|
<div className={style["popup-content"]}>
|
||||||
|
<div className={style["popup-header"]}>
|
||||||
|
<h3>好友详情</h3>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
fill="outline"
|
||||||
|
onClick={() => setShowFriendDetail(false)}
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoadingFriendDetail ? (
|
||||||
|
<div className={style["loading-detail"]}>
|
||||||
|
<SpinLoading color="primary" style={{ fontSize: 32 }} />
|
||||||
|
</div>
|
||||||
|
) : friendDetailError ? (
|
||||||
|
<div className={style["error-detail"]}>
|
||||||
|
<p>{friendDetailError}</p>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={() => handleFriendClick(selectedFriend!)}
|
||||||
|
>
|
||||||
|
重试
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : friendDetail && selectedFriend ? (
|
||||||
|
<div className={style["friend-detail-content"]}>
|
||||||
|
<div className={style["friend-detail-header"]}>
|
||||||
|
<Avatar
|
||||||
|
src={selectedFriend.avatar}
|
||||||
|
className={style["friend-detail-avatar"]}
|
||||||
|
/>
|
||||||
|
<div className={style["friend-detail-info"]}>
|
||||||
|
<h4 className={style["friend-detail-name"]}>
|
||||||
|
{selectedFriend.nickname}
|
||||||
|
</h4>
|
||||||
|
<p className={style["friend-detail-wechat-id"]}>
|
||||||
|
微信号:{selectedFriend.wechatId}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={style["friend-detail-items"]}>
|
||||||
|
<div className={style["detail-item"]}>
|
||||||
|
<span className={style["detail-label"]}>地区</span>
|
||||||
|
<span className={style["detail-value"]}>
|
||||||
|
{friendDetail.region || "未知"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={style["detail-item"]}>
|
||||||
|
<span className={style["detail-label"]}>添加时间</span>
|
||||||
|
<span className={style["detail-value"]}>
|
||||||
|
{friendDetail.addDate}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={style["detail-item"]}>
|
||||||
|
<span className={style["detail-label"]}>来源</span>
|
||||||
|
<span className={style["detail-value"]}>
|
||||||
|
{friendDetail.source || "未知"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{friendDetail.memo && (
|
||||||
|
<div className={style["detail-item"]}>
|
||||||
|
<span className={style["detail-label"]}>备注</span>
|
||||||
|
<span className={style["detail-value"]}>
|
||||||
|
{friendDetail.memo}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{friendDetail.tags && friendDetail.tags.length > 0 && (
|
||||||
|
<div className={style["detail-item"]}>
|
||||||
|
<span className={style["detail-label"]}>标签</span>
|
||||||
|
<div className={style["detail-tags"]}>
|
||||||
|
{friendDetail.tags.map((tag, index) => (
|
||||||
|
<Tag
|
||||||
|
key={index}
|
||||||
|
size="small"
|
||||||
|
className={style["detail-tag"]}
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</Popup>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WechatAccountDetail;
|
||||||
30
nkebao/src/pages/wechat-accounts/list/api.ts
Normal file
30
nkebao/src/pages/wechat-accounts/list/api.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import request from "@/api/request";
|
||||||
|
|
||||||
|
// 获取微信号列表
|
||||||
|
export function getWechatAccounts(params: {
|
||||||
|
page: number;
|
||||||
|
page_size: number;
|
||||||
|
keyword?: string;
|
||||||
|
}) {
|
||||||
|
return request("v1/wechats", params, "GET");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取微信号详情
|
||||||
|
export function getWechatAccountDetail(id: string) {
|
||||||
|
return request("v1/WechatAccount/detail", { id }, "GET");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取微信号好友列表
|
||||||
|
export function getWechatFriends(params: {
|
||||||
|
wechatAccountKeyword: string;
|
||||||
|
pageIndex: number;
|
||||||
|
pageSize: number;
|
||||||
|
friendKeyword?: string;
|
||||||
|
}) {
|
||||||
|
return request("v1/WechatFriend/friendlistData", params, "POST");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取微信好友详情
|
||||||
|
export function getWechatFriendDetail(id: string) {
|
||||||
|
return request("v1/WechatFriend/detail", { id }, "GET");
|
||||||
|
}
|
||||||
171
nkebao/src/pages/wechat-accounts/list/index.module.scss
Normal file
171
nkebao/src/pages/wechat-accounts/list/index.module.scss
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
.wechat-accounts-page {
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||||
|
padding: 14px 14px 10px 14px;
|
||||||
|
transition: box-shadow 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 16px rgba(0,0,0,0.10);
|
||||||
|
border-color: #e6f7ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.avatar-wrapper {
|
||||||
|
position: relative;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
.avatar {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid #e6f0fa;
|
||||||
|
box-shadow: 0 0 0 2px #1677ff33;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.status-dot-normal {
|
||||||
|
position: absolute;
|
||||||
|
right: -2px;
|
||||||
|
bottom: -2px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
background: #52c41a;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.status-dot-abnormal {
|
||||||
|
position: absolute;
|
||||||
|
right: -2px;
|
||||||
|
bottom: -2px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
background: #ff4d4f;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.header-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.nickname-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.nickname {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
max-width: 120px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.status-label-normal {
|
||||||
|
background: #e6fffb;
|
||||||
|
color: #13c2c2;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
.status-label-abnormal {
|
||||||
|
background: #fff1f0;
|
||||||
|
color: #ff4d4f;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
.wechat-id {
|
||||||
|
color: #888;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.card-action {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
.card-body {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.row-group {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.row-item {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #555;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
.strong {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
.strong-green {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
.progress-bar {
|
||||||
|
margin: 6px 0 8px 0;
|
||||||
|
}
|
||||||
|
.progress-bg {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.progress-fill {
|
||||||
|
height: 8px;
|
||||||
|
background: linear-gradient(90deg, #1677ff 0%, #69c0ff 100%);
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: width 0.3s;
|
||||||
|
}
|
||||||
|
.pagination {
|
||||||
|
margin: 16px 0 0 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.popup-content {
|
||||||
|
padding: 16px 0 8px 0;
|
||||||
|
}
|
||||||
|
.popup-content img {
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
.empty {
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
padding: 48px 0 32px 0;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
309
nkebao/src/pages/wechat-accounts/list/index.tsx
Normal file
309
nkebao/src/pages/wechat-accounts/list/index.tsx
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
NavBar,
|
||||||
|
List,
|
||||||
|
Card,
|
||||||
|
Button,
|
||||||
|
SpinLoading,
|
||||||
|
Popup,
|
||||||
|
Toast,
|
||||||
|
} from "antd-mobile";
|
||||||
|
import { Pagination, Input, Tooltip } from "antd";
|
||||||
|
import {
|
||||||
|
ArrowLeftOutlined,
|
||||||
|
SearchOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import Layout from "@/components/Layout/Layout";
|
||||||
|
import style from "./index.module.scss";
|
||||||
|
import { getWechatAccounts } from "./api";
|
||||||
|
|
||||||
|
interface WechatAccount {
|
||||||
|
id: number;
|
||||||
|
nickname: string;
|
||||||
|
avatar: string;
|
||||||
|
wechatId: string;
|
||||||
|
wechatAccount: string;
|
||||||
|
deviceId: number;
|
||||||
|
times: number; // 今日可添加
|
||||||
|
addedCount: number; // 今日新增
|
||||||
|
wechatStatus: number; // 1正常 0异常
|
||||||
|
totalFriend: number;
|
||||||
|
deviceMemo: string; // 设备名
|
||||||
|
activeTime: string; // 最后活跃
|
||||||
|
}
|
||||||
|
|
||||||
|
const PAGE_SIZE = 10;
|
||||||
|
|
||||||
|
const WechatAccounts: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [accounts, setAccounts] = useState<WechatAccount[]>([]);
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [totalAccounts, setTotalAccounts] = useState(0);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
const [popupVisible, setPopupVisible] = useState(false);
|
||||||
|
const [selectedAccount, setSelectedAccount] = useState<WechatAccount | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchAccounts = async (page = 1, keyword = "") => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await getWechatAccounts({
|
||||||
|
page,
|
||||||
|
page_size: PAGE_SIZE,
|
||||||
|
keyword,
|
||||||
|
});
|
||||||
|
if (res && res.list) {
|
||||||
|
setAccounts(res.list);
|
||||||
|
setTotalAccounts(res.total || 0);
|
||||||
|
} else {
|
||||||
|
setAccounts([]);
|
||||||
|
setTotalAccounts(0);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Toast.show({ content: "获取微信号失败", position: "top" });
|
||||||
|
setAccounts([]);
|
||||||
|
setTotalAccounts(0);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAccounts(currentPage, searchTerm);
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [currentPage]);
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
setCurrentPage(1);
|
||||||
|
fetchAccounts(1, searchTerm);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
setIsRefreshing(true);
|
||||||
|
await fetchAccounts(currentPage, searchTerm);
|
||||||
|
setIsRefreshing(false);
|
||||||
|
Toast.show({ content: "刷新成功", position: "top" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAccountClick = (account: WechatAccount) => {
|
||||||
|
setSelectedAccount(account);
|
||||||
|
setPopupVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTransferFriends = (account: WechatAccount) => {
|
||||||
|
// TODO: 实现好友转移弹窗或跳转
|
||||||
|
Toast.show({ content: `好友转移:${account.nickname}` });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout
|
||||||
|
header={
|
||||||
|
<>
|
||||||
|
<NavBar
|
||||||
|
back={null}
|
||||||
|
style={{ background: "#fff" }}
|
||||||
|
left={
|
||||||
|
<div className={style["nav-title"]}>
|
||||||
|
<ArrowLeftOutlined
|
||||||
|
twoToneColor="#1677ff"
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className={style["nav-title"]}>微信号管理</span>
|
||||||
|
</NavBar>
|
||||||
|
<div className="search-bar">
|
||||||
|
<div className="search-input-wrapper">
|
||||||
|
<Input
|
||||||
|
placeholder="搜索微信号/昵称"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
prefix={<SearchOutlined />}
|
||||||
|
allowClear
|
||||||
|
size="large"
|
||||||
|
onPressEnter={handleSearch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={handleRefresh}
|
||||||
|
loading={isRefreshing}
|
||||||
|
className="refresh-btn"
|
||||||
|
>
|
||||||
|
<ReloadOutlined />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={style["wechat-accounts-page"]}>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className={style["loading"]}>
|
||||||
|
<SpinLoading color="primary" style={{ fontSize: 32 }} />
|
||||||
|
</div>
|
||||||
|
) : accounts.length === 0 ? (
|
||||||
|
<div className={style["empty"]}>暂无微信账号数据</div>
|
||||||
|
) : (
|
||||||
|
<div className={style["card-list"]}>
|
||||||
|
{accounts.map((account) => {
|
||||||
|
const percent =
|
||||||
|
account.times > 0
|
||||||
|
? Math.min((account.addedCount / account.times) * 100, 100)
|
||||||
|
: 0;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={account.id}
|
||||||
|
className={style["account-card"]}
|
||||||
|
onClick={() => handleAccountClick(account)}
|
||||||
|
>
|
||||||
|
<div className={style["card-header"]}>
|
||||||
|
<div className={style["avatar-wrapper"]}>
|
||||||
|
<img
|
||||||
|
src={account.avatar}
|
||||||
|
alt={account.nickname}
|
||||||
|
className={style["avatar"]}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
account.wechatStatus === 1
|
||||||
|
? style["status-dot-normal"]
|
||||||
|
: style["status-dot-abnormal"]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={style["header-info"]}>
|
||||||
|
<div className={style["nickname-row"]}>
|
||||||
|
<span className={style["nickname"]}>
|
||||||
|
{account.nickname}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
account.wechatStatus === 1
|
||||||
|
? style["status-label-normal"]
|
||||||
|
: style["status-label-abnormal"]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{account.wechatStatus === 1 ? "正常" : "异常"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={style["wechat-id"]}>
|
||||||
|
微信号:{account.wechatAccount}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={style["card-body"]}>
|
||||||
|
<div className={style["row-group"]}>
|
||||||
|
<div className={style["row-item"]}>
|
||||||
|
<span>好友数量:</span>
|
||||||
|
<span className={style["strong"]}>
|
||||||
|
{account.totalFriend}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={style["row-item"]}>
|
||||||
|
<span>今日新增:</span>
|
||||||
|
<span className={style["strong-green"]}>
|
||||||
|
+{account.addedCount}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={style["row-group"]}>
|
||||||
|
<div className={style["row-item"]}>
|
||||||
|
<span>今日可添加:</span>
|
||||||
|
<span>{account.times}</span>
|
||||||
|
</div>
|
||||||
|
<div className={style["row-item"]}>
|
||||||
|
<Tooltip title={`每日最多添加 ${account.times} 个好友`}>
|
||||||
|
<span>进度:</span>
|
||||||
|
<span>
|
||||||
|
{account.addedCount}/{account.times}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={style["progress-bar"]}>
|
||||||
|
<div className={style["progress-bg"]}>
|
||||||
|
<div
|
||||||
|
className={style["progress-fill"]}
|
||||||
|
style={{ width: `${percent}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={style["row-group"]}>
|
||||||
|
<div className={style["row-item"]}>
|
||||||
|
<span>所属设备:</span>
|
||||||
|
<span>{account.deviceMemo || "-"}</span>
|
||||||
|
</div>
|
||||||
|
<div className={style["row-item"]}>
|
||||||
|
<span>最后活跃:</span>
|
||||||
|
<span>{account.activeTime}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={style["pagination"]}>
|
||||||
|
{totalAccounts > PAGE_SIZE && (
|
||||||
|
<Pagination
|
||||||
|
total={Math.ceil(totalAccounts / PAGE_SIZE)}
|
||||||
|
current={currentPage}
|
||||||
|
onChange={setCurrentPage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Popup
|
||||||
|
visible={popupVisible}
|
||||||
|
onMaskClick={() => setPopupVisible(false)}
|
||||||
|
bodyStyle={{ borderRadius: "16px 16px 0 0" }}
|
||||||
|
>
|
||||||
|
{selectedAccount && (
|
||||||
|
<div className={style["popup-content"]}>
|
||||||
|
<div style={{ textAlign: "center", margin: 16 }}>
|
||||||
|
<img
|
||||||
|
src={selectedAccount.avatar}
|
||||||
|
alt="avatar"
|
||||||
|
style={{ width: 60, height: 60, borderRadius: 30 }}
|
||||||
|
/>
|
||||||
|
<div style={{ fontWeight: 600, marginTop: 8 }}>
|
||||||
|
{selectedAccount.nickname}
|
||||||
|
</div>
|
||||||
|
<div style={{ color: "#888", fontSize: 12 }}>
|
||||||
|
微信号:{selectedAccount.wechatAccount}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ margin: 16 }}>
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
navigate(`/wechat-accounts/detail/${selectedAccount.id}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
查看详情
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
color="danger"
|
||||||
|
fill="outline"
|
||||||
|
onClick={() => setPopupVisible(false)}
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Popup>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WechatAccounts;
|
||||||
32
nkebao/src/pages/workspace/moments-sync/new/api.ts
Normal file
32
nkebao/src/pages/workspace/moments-sync/new/api.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import request from "@/api/request";
|
||||||
|
|
||||||
|
// 创建朋友圈同步任务
|
||||||
|
export const createMomentsSync = (params: {
|
||||||
|
name: string;
|
||||||
|
devices: string[];
|
||||||
|
contentLibraries: string[];
|
||||||
|
syncCount: number;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
accountType: number;
|
||||||
|
status: number;
|
||||||
|
type: number;
|
||||||
|
}) => request("/v1/workbench/create", params, "POST");
|
||||||
|
|
||||||
|
// 更新朋友圈同步任务
|
||||||
|
export const updateMomentsSync = (params: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
devices: string[];
|
||||||
|
contentLibraries: string[];
|
||||||
|
syncCount: number;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
accountType: number;
|
||||||
|
status: number;
|
||||||
|
type: number;
|
||||||
|
}) => request("/v1/workbench/update", params, "POST");
|
||||||
|
|
||||||
|
// 获取朋友圈同步任务详情
|
||||||
|
export const getMomentsSyncDetail = (id: string) =>
|
||||||
|
request("/v1/workbench/detail", { id }, "GET");
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.inputTime {
|
.inputTime {
|
||||||
width: 90px;
|
width: 100px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
|||||||
@@ -7,8 +7,19 @@ import { NavBar } from "antd-mobile";
|
|||||||
import Layout from "@/components/Layout/Layout";
|
import Layout from "@/components/Layout/Layout";
|
||||||
import style from "./index.module.scss";
|
import style from "./index.module.scss";
|
||||||
import request from "@/api/request";
|
import request from "@/api/request";
|
||||||
|
import StepIndicator from "@/components/StepIndicator";
|
||||||
|
import {
|
||||||
|
createMomentsSync,
|
||||||
|
updateMomentsSync,
|
||||||
|
getMomentsSyncDetail,
|
||||||
|
} from "./api";
|
||||||
|
import DeviceSelection from "@/components/DeviceSelection";
|
||||||
|
|
||||||
const steps = ["基础设置", "设备选择", "内容库选择"];
|
const steps = [
|
||||||
|
{ id: 1, title: "基础设置", subtitle: "基础设置" },
|
||||||
|
{ id: 2, title: "设备选择", subtitle: "设备选择" },
|
||||||
|
{ id: 3, title: "内容库选择", subtitle: "内容库选择" },
|
||||||
|
];
|
||||||
|
|
||||||
const defaultForm = {
|
const defaultForm = {
|
||||||
taskName: "",
|
taskName: "",
|
||||||
@@ -34,7 +45,7 @@ const NewMomentsSync: React.FC = () => {
|
|||||||
if (!id) return;
|
if (!id) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await request("/v1/workbench/detail", { id }, "GET");
|
const res = await getMomentsSyncDetail(id);
|
||||||
if (res) {
|
if (res) {
|
||||||
setFormData({
|
setFormData({
|
||||||
taskName: res.name,
|
taskName: res.name,
|
||||||
@@ -95,11 +106,11 @@ const NewMomentsSync: React.FC = () => {
|
|||||||
type: 2,
|
type: 2,
|
||||||
};
|
};
|
||||||
if (isEditMode && id) {
|
if (isEditMode && id) {
|
||||||
await request("/v1/workbench/update", { id, ...params }, "POST");
|
await updateMomentsSync({ id, ...params });
|
||||||
message.success("更新成功");
|
message.success("更新成功");
|
||||||
navigate(`/workspace/moments-sync/${id}`);
|
navigate(`/workspace/moments-sync/${id}`);
|
||||||
} else {
|
} else {
|
||||||
await request("/v1/workbench/create", params, "POST");
|
await createMomentsSync(params);
|
||||||
message.success("创建成功");
|
message.success("创建成功");
|
||||||
navigate("/workspace/moments-sync");
|
navigate("/workspace/moments-sync");
|
||||||
}
|
}
|
||||||
@@ -214,18 +225,14 @@ const NewMomentsSync: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div className={style.formStep}>
|
<div className={style.formStep}>
|
||||||
<div className={style.formItem}>
|
<div className={style.formItem}>
|
||||||
<Input
|
<div className={style.formLabel}>选择设备</div>
|
||||||
placeholder="选择设备"
|
<DeviceSelection
|
||||||
prefix={<span className={style.searchIcon}>Q</span>}
|
selectedDevices={formData.selectedDevices}
|
||||||
className={style.searchInput}
|
onSelect={(devices) => updateForm({ selectedDevices: devices })}
|
||||||
onClick={() => message.info("这里应弹出设备选择器")}
|
placeholder="请选择设备"
|
||||||
readOnly
|
showSelectedList={true}
|
||||||
|
selectedListMaxHeight={200}
|
||||||
/>
|
/>
|
||||||
{formData.selectedDevices.length > 0 && (
|
|
||||||
<div className={style.selectedTip}>
|
|
||||||
已选设备: {formData.selectedDevices.length}个
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className={style.formStepBtnRow}>
|
<div className={style.formStepBtnRow}>
|
||||||
<Button onClick={prev} className={style.prevBtn}>
|
<Button onClick={prev} className={style.prevBtn}>
|
||||||
@@ -294,25 +301,7 @@ const NewMomentsSync: React.FC = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className={style.formBg}>
|
<div className={style.formBg}>
|
||||||
<div className={style.formSteps}>
|
<StepIndicator currentStep={currentStep + 1} steps={steps} />
|
||||||
{steps.map((s, i) => (
|
|
||||||
<div
|
|
||||||
key={s}
|
|
||||||
className={
|
|
||||||
style.formStepIndicator +
|
|
||||||
" " +
|
|
||||||
(i === currentStep
|
|
||||||
? style.formStepActive
|
|
||||||
: i < currentStep
|
|
||||||
? style.formStepDone
|
|
||||||
: "")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span className={style.formStepNum}>{i + 1}</span>
|
|
||||||
<span>{s}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className={style.formLoading}>
|
<div className={style.formLoading}>
|
||||||
<Spin />
|
<Spin />
|
||||||
|
|||||||
11
nkebao/src/router/module/component-test.tsx
Normal file
11
nkebao/src/router/module/component-test.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import ComponentTest from "@/pages/component-test";
|
||||||
|
|
||||||
|
const componentTestRoutes = [
|
||||||
|
{
|
||||||
|
path: "/component-test",
|
||||||
|
element: <ComponentTest />,
|
||||||
|
auth: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default componentTestRoutes;
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import Home from "@/pages/home/index";
|
import Home from "@/pages/home/index";
|
||||||
import Mine from "@/pages/mine/index";
|
import Mine from "@/pages/mine/index";
|
||||||
|
import WechatAccounts from "@/pages/wechat-accounts/list/index";
|
||||||
|
import WechatAccountDetail from "@/pages/wechat-accounts/detail/index";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
// 基础路由
|
// 基础路由
|
||||||
@@ -13,6 +15,17 @@ const routes = [
|
|||||||
element: <Mine />,
|
element: <Mine />,
|
||||||
auth: true,
|
auth: true,
|
||||||
},
|
},
|
||||||
|
// 微信号管理路由
|
||||||
|
{
|
||||||
|
path: "/wechat-accounts",
|
||||||
|
element: <WechatAccounts />,
|
||||||
|
auth: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/wechat-accounts/detail/:id",
|
||||||
|
element: <WechatAccountDetail />,
|
||||||
|
auth: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default routes;
|
export default routes;
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import WechatAccounts from "@/pages/wechat-accounts/WechatAccounts";
|
import WechatAccounts from "@/pages/wechat-accounts/list";
|
||||||
import WechatAccountDetail from "@/pages/wechat-accounts/WechatAccountDetail";
|
import WechatAccountDetail from "@/pages/wechat-accounts/detail";
|
||||||
|
|
||||||
const wechatAccountRoutes = [
|
const wechatAccountRoutes = [
|
||||||
{
|
{
|
||||||
path: "/wechat-accounts",
|
path: "/wechat-accounts",
|
||||||
element: <WechatAccounts />,
|
element: <WechatAccounts />,
|
||||||
auth: true,
|
auth: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/wechat-accounts/:id",
|
path: "/wechat-accounts/detail/:id",
|
||||||
element: <WechatAccountDetail />,
|
element: <WechatAccountDetail />,
|
||||||
auth: true,
|
auth: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default wechatAccountRoutes;
|
export default wechatAccountRoutes;
|
||||||
|
|||||||
Reference in New Issue
Block a user