Merge branch 'yongpxu-dev' of https://e.coding.net/g-xtcy5189/cunkebao/cunkebao_v3 into yongpxu-dev
# Conflicts: # nkebao/src/pages/mobile/mine/devices/index.tsx resolved by origin/yongpxu-dev(远端) version
This commit is contained in:
@@ -7,7 +7,7 @@ import request from "./request";
|
||||
*/
|
||||
export async function uploadFile(
|
||||
file: File,
|
||||
uploadUrl: string = "/v1/attachment/upload"
|
||||
uploadUrl: string = "/v1/attachment/upload",
|
||||
): Promise<string> {
|
||||
try {
|
||||
// 创建 FormData 对象用于文件上传
|
||||
|
||||
@@ -19,7 +19,7 @@ export const fetchDeviceRelatedAccounts = (id: string | number) =>
|
||||
export const fetchDeviceHandleLogs = (
|
||||
id: string | number,
|
||||
page = 1,
|
||||
limit = 10
|
||||
limit = 10,
|
||||
) => request(`/v1/devices/${id}/handle-logs`, { page, limit }, "GET");
|
||||
|
||||
// 更新设备任务配置
|
||||
|
||||
@@ -47,7 +47,7 @@ instance.interceptors.response.use(
|
||||
err => {
|
||||
Toast.show({ content: err.message || "网络异常", position: "top" });
|
||||
return Promise.reject(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export function request(
|
||||
@@ -55,7 +55,7 @@ export function request(
|
||||
data?: any,
|
||||
method: Method = "GET",
|
||||
config?: AxiosRequestConfig,
|
||||
debounceGap?: number
|
||||
debounceGap?: number,
|
||||
): Promise<any> {
|
||||
const gap =
|
||||
typeof debounceGap === "number" ? debounceGap : DEFAULT_DEBOUNCE_GAP;
|
||||
|
||||
@@ -117,7 +117,7 @@ export default function AccountSelection({
|
||||
acc =>
|
||||
acc.userName.includes(searchQuery) ||
|
||||
acc.realName.includes(searchQuery) ||
|
||||
acc.departmentName.includes(searchQuery)
|
||||
acc.departmentName.includes(searchQuery),
|
||||
);
|
||||
|
||||
// 处理账号选择
|
||||
|
||||
@@ -58,7 +58,7 @@ interface ContentLibrarySelectionProps {
|
||||
readonly?: boolean;
|
||||
onConfirm?: (
|
||||
selectedIds: string[],
|
||||
selectedItems: ContentLibraryItem[]
|
||||
selectedItems: ContentLibraryItem[],
|
||||
) => void;
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export default function ContentLibrarySelection({
|
||||
|
||||
// 获取已选内容库详细信息
|
||||
const selectedLibraryObjs = libraries.filter(item =>
|
||||
selectedLibraries.includes(item.id)
|
||||
selectedLibraries.includes(item.id),
|
||||
);
|
||||
|
||||
// 删除已选内容库
|
||||
@@ -161,7 +161,7 @@ export default function ContentLibrarySelection({
|
||||
onSelect(newSelected);
|
||||
if (onSelectDetail) {
|
||||
const selectedObjs = libraries.filter(item =>
|
||||
newSelected.includes(item.id)
|
||||
newSelected.includes(item.id),
|
||||
);
|
||||
onSelectDetail(selectedObjs);
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
|
||||
wxid: d.wechatId || "",
|
||||
nickname: d.nickname || "",
|
||||
usedInPlans: d.usedInPlans || 0,
|
||||
}))
|
||||
})),
|
||||
);
|
||||
setTotal(res.total || 0);
|
||||
}
|
||||
@@ -71,7 +71,7 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
[],
|
||||
);
|
||||
|
||||
// 打开弹窗时获取第一页
|
||||
|
||||
@@ -137,7 +137,7 @@ export default function FriendSelection({
|
||||
// 如果有 onSelectDetail 回调,传递完整的好友对象
|
||||
if (onSelectDetail) {
|
||||
const selectedFriendObjs = friends.filter(friend =>
|
||||
newSelectedFriends.includes(friend.id)
|
||||
newSelectedFriends.includes(friend.id),
|
||||
);
|
||||
onSelectDetail(selectedFriendObjs);
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ export default function GroupSelection({
|
||||
|
||||
// 获取已选群聊详细信息
|
||||
const selectedGroupObjs = groups.filter(group =>
|
||||
selectedGroups.includes(group.id)
|
||||
selectedGroups.includes(group.id),
|
||||
);
|
||||
|
||||
// 删除已选群聊
|
||||
@@ -141,7 +141,7 @@ export default function GroupSelection({
|
||||
// 如果有 onSelectDetail 回调,传递完整的群聊对象
|
||||
if (onSelectDetail) {
|
||||
const selectedGroupObjs = groups.filter(group =>
|
||||
newSelectedGroups.includes(group.id)
|
||||
newSelectedGroups.includes(group.id),
|
||||
);
|
||||
onSelectDetail(selectedGroupObjs);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ const Layout: React.FC<LayoutProps> = ({
|
||||
const setRealHeight = () => {
|
||||
document.documentElement.style.setProperty(
|
||||
"--real-vh",
|
||||
`${window.innerHeight * 0.01}px`
|
||||
`${window.innerHeight * 0.01}px`,
|
||||
);
|
||||
};
|
||||
setRealHeight();
|
||||
|
||||
@@ -68,7 +68,7 @@ const UploadComponent: React.FC<UploadComponentProps> = ({
|
||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||
},
|
||||
body: formData,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -12,14 +12,14 @@ export function getContentLibraryDetail(id: string): Promise<any> {
|
||||
|
||||
// 创建内容库
|
||||
export function createContentLibrary(
|
||||
params: CreateContentLibraryParams
|
||||
params: CreateContentLibraryParams,
|
||||
): Promise<any> {
|
||||
return request("/v1/content/library/create", params, "POST");
|
||||
}
|
||||
|
||||
// 更新内容库
|
||||
export function updateContentLibrary(
|
||||
params: UpdateContentLibraryParams
|
||||
params: UpdateContentLibraryParams,
|
||||
): Promise<any> {
|
||||
const { id, ...data } = params;
|
||||
return request(`/v1/content/library/update`, { id, ...data }, "POST");
|
||||
|
||||
@@ -22,14 +22,14 @@ export function getContentLibraryDetail(id: string): Promise<any> {
|
||||
|
||||
// 创建内容库
|
||||
export function createContentLibrary(
|
||||
params: CreateContentLibraryParams
|
||||
params: CreateContentLibraryParams,
|
||||
): Promise<any> {
|
||||
return request("/v1/content/library/create", params, "POST");
|
||||
}
|
||||
|
||||
// 更新内容库
|
||||
export function updateContentLibrary(
|
||||
params: UpdateContentLibraryParams
|
||||
params: UpdateContentLibraryParams,
|
||||
): Promise<any> {
|
||||
const { id, ...data } = params;
|
||||
return request(`/v1/content/library/update`, { id, ...data }, "POST");
|
||||
@@ -43,7 +43,7 @@ export function deleteContentLibrary(id: string): Promise<any> {
|
||||
// 切换内容库状态
|
||||
export function toggleContentLibraryStatus(
|
||||
id: string,
|
||||
status: number
|
||||
status: number,
|
||||
): Promise<any> {
|
||||
return request("/v1/content/library/update-status", { id, status }, "POST");
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ const ContentLibraryList: React.FC = () => {
|
||||
const filteredLibraries = libraries.filter(
|
||||
library =>
|
||||
library.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
library.creatorName?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
library.creatorName?.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -213,8 +213,8 @@ const Home: React.FC = () => {
|
||||
onClick={() =>
|
||||
navigate(
|
||||
`/scenarios/list/${scenario.id}/${encodeURIComponent(
|
||||
scenario.name
|
||||
)}`
|
||||
scenario.name,
|
||||
)}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
|
||||
17
nkebao/src/pages/mobile/mine/consumption-records/api.ts
Normal file
17
nkebao/src/pages/mobile/mine/consumption-records/api.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import request from "@/api/request";
|
||||
import { ConsumptionRecordsResponse, ConsumptionRecordDetail } from "./data";
|
||||
|
||||
// 获取消费记录列表
|
||||
export function getConsumptionRecords(params: {
|
||||
page: number;
|
||||
limit: number;
|
||||
}): Promise<ConsumptionRecordsResponse> {
|
||||
return request("/v1/consumption-records", params, "GET");
|
||||
}
|
||||
|
||||
// 获取消费记录详情
|
||||
export function getConsumptionRecordDetail(
|
||||
id: string,
|
||||
): Promise<ConsumptionRecordDetail> {
|
||||
return request(`/v1/consumption-records/${id}`, {}, "GET");
|
||||
}
|
||||
26
nkebao/src/pages/mobile/mine/consumption-records/data.ts
Normal file
26
nkebao/src/pages/mobile/mine/consumption-records/data.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
// 消费记录类型定义
|
||||
export interface ConsumptionRecord {
|
||||
id: string;
|
||||
type: "recharge" | "ai_service" | "version_upgrade";
|
||||
amount: number;
|
||||
description: string;
|
||||
createTime: string;
|
||||
status: "success" | "pending" | "failed";
|
||||
balance?: number;
|
||||
}
|
||||
|
||||
// API响应类型
|
||||
export interface ConsumptionRecordsResponse {
|
||||
list: ConsumptionRecord[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
// 消费记录详情
|
||||
export interface ConsumptionRecordDetail extends ConsumptionRecord {
|
||||
orderNo?: string;
|
||||
paymentMethod?: string;
|
||||
remark?: string;
|
||||
operator?: string;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
.records-page {
|
||||
padding: 16px;
|
||||
background: #f7f8fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.records-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.record-card {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.record-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.record-info {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.type-icon-wrapper {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.type-icon {
|
||||
font-size: 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.record-details {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.record-description {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #222;
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.record-time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.time-icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.record-amount {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.amount-text {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
font-size: 11px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.balance-info {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.load-more {
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
color: var(--primary-color);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 48px;
|
||||
color: #ccc;
|
||||
}
|
||||
212
nkebao/src/pages/mobile/mine/consumption-records/index.tsx
Normal file
212
nkebao/src/pages/mobile/mine/consumption-records/index.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Card, List, Tag, SpinLoading, Empty } from "antd-mobile";
|
||||
import { useUserStore } from "@/store/module/user";
|
||||
import style from "./index.module.scss";
|
||||
import {
|
||||
WalletOutlined,
|
||||
RobotOutlined,
|
||||
CrownOutlined,
|
||||
ClockCircleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import { getConsumptionRecords } from "./api";
|
||||
import { ConsumptionRecord } from "./data";
|
||||
|
||||
const ConsumptionRecords: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useUserStore();
|
||||
const [records, setRecords] = useState<ConsumptionRecord[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
loadRecords();
|
||||
}, []);
|
||||
|
||||
const loadRecords = async (reset = false) => {
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const currentPage = reset ? 1 : page;
|
||||
const response = await getConsumptionRecords({
|
||||
page: currentPage,
|
||||
limit: 20,
|
||||
});
|
||||
|
||||
const newRecords = response.list || [];
|
||||
setRecords(prev => (reset ? newRecords : [...prev, ...newRecords]));
|
||||
setHasMore(newRecords.length === 20);
|
||||
if (reset) setPage(1);
|
||||
else setPage(currentPage + 1);
|
||||
} catch (error) {
|
||||
console.error("加载消费记录失败:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "recharge":
|
||||
return <WalletOutlined className={style["type-icon"]} />;
|
||||
case "ai_service":
|
||||
return <RobotOutlined className={style["type-icon"]} />;
|
||||
case "version_upgrade":
|
||||
return <CrownOutlined className={style["type-icon"]} />;
|
||||
default:
|
||||
return <WalletOutlined className={style["type-icon"]} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeColor = (type: string) => {
|
||||
switch (type) {
|
||||
case "recharge":
|
||||
return "#52c41a";
|
||||
case "ai_service":
|
||||
return "#1890ff";
|
||||
case "version_upgrade":
|
||||
return "#722ed1";
|
||||
default:
|
||||
return "#666";
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case "success":
|
||||
return "成功";
|
||||
case "pending":
|
||||
return "处理中";
|
||||
case "failed":
|
||||
return "失败";
|
||||
default:
|
||||
return "未知";
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "success":
|
||||
return "#52c41a";
|
||||
case "pending":
|
||||
return "#faad14";
|
||||
case "failed":
|
||||
return "#ff4d4f";
|
||||
default:
|
||||
return "#666";
|
||||
}
|
||||
};
|
||||
|
||||
const formatAmount = (amount: number, type: string) => {
|
||||
if (type === "recharge") {
|
||||
return `+¥${amount.toFixed(2)}`;
|
||||
} else {
|
||||
return `-¥${amount.toFixed(2)}`;
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (timeStr: string) => {
|
||||
const date = new Date(timeStr);
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (days === 0) {
|
||||
return date.toLocaleTimeString("zh-CN", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
} else if (days === 1) {
|
||||
return (
|
||||
"昨天 " +
|
||||
date.toLocaleTimeString("zh-CN", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
);
|
||||
} else if (days < 7) {
|
||||
return `${days}天前`;
|
||||
} else {
|
||||
return date.toLocaleDateString("zh-CN");
|
||||
}
|
||||
};
|
||||
|
||||
const renderRecordItem = (record: ConsumptionRecord) => (
|
||||
<Card key={record.id} className={style["record-card"]}>
|
||||
<div className={style["record-header"]}>
|
||||
<div className={style["record-info"]}>
|
||||
<div
|
||||
className={style["type-icon-wrapper"]}
|
||||
style={{ backgroundColor: `${getTypeColor(record.type)}20` }}
|
||||
>
|
||||
{getTypeIcon(record.type)}
|
||||
</div>
|
||||
<div className={style["record-details"]}>
|
||||
<div className={style["record-description"]}>
|
||||
{record.description}
|
||||
</div>
|
||||
<div className={style["record-time"]}>
|
||||
<ClockCircleOutlined className={style["time-icon"]} />
|
||||
{formatTime(record.createTime)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["record-amount"]}>
|
||||
<div
|
||||
className={style["amount-text"]}
|
||||
style={{
|
||||
color: record.type === "recharge" ? "#52c41a" : "#ff4d4f",
|
||||
}}
|
||||
>
|
||||
{formatAmount(record.amount, record.type)}
|
||||
</div>
|
||||
<Tag
|
||||
color={getStatusColor(record.status)}
|
||||
className={style["status-tag"]}
|
||||
>
|
||||
{getStatusText(record.status)}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
{record.balance !== undefined && (
|
||||
<div className={style["balance-info"]}>
|
||||
余额: ¥{record.balance.toFixed(2)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<Layout header={<NavCommon title="消费记录" />}>
|
||||
<div className={style["records-page"]}>
|
||||
{records.length === 0 && !loading ? (
|
||||
<Empty
|
||||
className={style["empty-state"]}
|
||||
description="暂无消费记录"
|
||||
image={<WalletOutlined className={style["empty-icon"]} />}
|
||||
/>
|
||||
) : (
|
||||
<div className={style["records-list"]}>
|
||||
{records.map(renderRecordItem)}
|
||||
{loading && (
|
||||
<div className={style["loading-container"]}>
|
||||
<SpinLoading color="primary" />
|
||||
<div className={style["loading-text"]}>加载中...</div>
|
||||
</div>
|
||||
)}
|
||||
{!loading && hasMore && (
|
||||
<div className={style["load-more"]} onClick={() => loadRecords()}>
|
||||
加载更多
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConsumptionRecords;
|
||||
@@ -23,7 +23,7 @@ const DeviceDetail: React.FC = () => {
|
||||
const [logs, setLogs] = useState<HandleLog[]>([]);
|
||||
const [logsLoading, setLogsLoading] = useState(false);
|
||||
const [featureSaving, setFeatureSaving] = useState<{ [k: string]: boolean }>(
|
||||
{}
|
||||
{},
|
||||
);
|
||||
|
||||
// 获取设备详情
|
||||
@@ -82,7 +82,7 @@ const DeviceDetail: React.FC = () => {
|
||||
// 功能开关
|
||||
const handleFeatureChange = async (
|
||||
feature: keyof Device["features"],
|
||||
checked: boolean
|
||||
checked: boolean,
|
||||
) => {
|
||||
if (!id) return;
|
||||
setFeatureSaving(prev => ({ ...prev, [feature]: true }));
|
||||
@@ -94,7 +94,7 @@ const DeviceDetail: React.FC = () => {
|
||||
...prev,
|
||||
features: { ...prev.features, [feature]: checked },
|
||||
}
|
||||
: prev
|
||||
: prev,
|
||||
);
|
||||
Toast.show({
|
||||
content: `${getFeatureName(feature)}已${checked ? "开启" : "关闭"}`,
|
||||
@@ -219,12 +219,12 @@ const DeviceDetail: React.FC = () => {
|
||||
onChange={checked =>
|
||||
handleFeatureChange(
|
||||
f as keyof Device["features"],
|
||||
checked
|
||||
checked,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
ArrowLeftOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import MeauMobile from "@/components/MeauMobile/MeauMoible";
|
||||
import {
|
||||
fetchDeviceList,
|
||||
fetchDeviceQRCode,
|
||||
@@ -20,9 +19,9 @@ import {
|
||||
import type { Device } from "@/types/device";
|
||||
import { comfirm } from "@/utils/common";
|
||||
import { useUserStore } from "@/store/module/user";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
|
||||
const Devices: React.FC = () => {
|
||||
const { user } = useUserStore();
|
||||
// 设备列表相关
|
||||
const [devices, setDevices] = useState<Device[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -49,6 +48,7 @@ const Devices: React.FC = () => {
|
||||
const [delLoading, setDelLoading] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { user } = useUserStore();
|
||||
// 加载设备列表
|
||||
const loadDevices = useCallback(
|
||||
async (reset = false) => {
|
||||
@@ -70,7 +70,7 @@ const Devices: React.FC = () => {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[loading, search, page]
|
||||
[loading, search, page],
|
||||
);
|
||||
|
||||
// 首次加载和搜索
|
||||
@@ -88,7 +88,7 @@ const Devices: React.FC = () => {
|
||||
setPage(p => p + 1);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.5 }
|
||||
{ threshold: 0.5 },
|
||||
);
|
||||
if (observerRef.current) observer.observe(observerRef.current);
|
||||
return () => observer.disconnect();
|
||||
@@ -114,10 +114,9 @@ const Devices: React.FC = () => {
|
||||
setQrLoading(true);
|
||||
setQrCode(null);
|
||||
try {
|
||||
// 获取s2_accountId
|
||||
const s2AccountId = user?.s2_accountId;
|
||||
if (!s2AccountId) throw new Error("未获取到用户信息");
|
||||
const res = await fetchDeviceQRCode(s2AccountId);
|
||||
const accountId = user.s2_accountId;
|
||||
if (!accountId) throw new Error("未获取到用户信息");
|
||||
const res = await fetchDeviceQRCode(accountId);
|
||||
setQrCode(res.qrCode);
|
||||
} catch (e: any) {
|
||||
Toast.show({ content: e.message || "获取二维码失败", position: "top" });
|
||||
@@ -125,10 +124,12 @@ const Devices: React.FC = () => {
|
||||
setQrLoading(false);
|
||||
}
|
||||
};
|
||||
const openAddDevice = async () => {
|
||||
|
||||
const addDevice = async () => {
|
||||
await handleGetQr();
|
||||
setAddVisible(true);
|
||||
};
|
||||
|
||||
// 手动添加设备
|
||||
const handleAddDevice = async () => {
|
||||
if (!imei.trim() || !name.trim()) {
|
||||
@@ -172,7 +173,7 @@ const Devices: React.FC = () => {
|
||||
try {
|
||||
await comfirm(
|
||||
`将删除${selected.length}个设备,删除后本设备配置的计划任务操作也将失效。确认删除?`,
|
||||
{ title: "确认删除", confirmText: "确认删除", cancelText: "取消" }
|
||||
{ title: "确认删除", confirmText: "确认删除", cancelText: "取消" },
|
||||
);
|
||||
handleDelete();
|
||||
} catch {
|
||||
@@ -195,29 +196,15 @@ const Devices: React.FC = () => {
|
||||
<Layout
|
||||
header={
|
||||
<>
|
||||
<NavBar
|
||||
back={null}
|
||||
left={
|
||||
<div className="nav-title">
|
||||
<ArrowLeftOutlined
|
||||
twoToneColor="#1677ff"
|
||||
onClick={() => navigate(-1)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
style={{ background: "#fff" }}
|
||||
<NavCommon
|
||||
title="设备管理"
|
||||
right={
|
||||
<Button size="small" type="primary" onClick={openAddDevice}>
|
||||
<Button size="small" type="primary" onClick={() => addDevice()}>
|
||||
<AddOutline />
|
||||
添加设备
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<span style={{ color: "var(--primary-color)", fontWeight: 600 }}>
|
||||
设备管理
|
||||
</span>
|
||||
</NavBar>
|
||||
|
||||
/>
|
||||
<div style={{ padding: "12px 12px 0 12px", background: "#fff" }}>
|
||||
{/* 搜索栏 */}
|
||||
<div style={{ display: "flex", gap: 8, marginBottom: 12 }}>
|
||||
@@ -303,7 +290,7 @@ const Devices: React.FC = () => {
|
||||
setSelected(prev =>
|
||||
e.target.checked
|
||||
? [...prev, device.id!]
|
||||
: prev.filter(id => id !== device.id)
|
||||
: prev.filter(id => id !== device.id),
|
||||
);
|
||||
}}
|
||||
onClick={e => e.stopPropagation()}
|
||||
|
||||
@@ -106,7 +106,6 @@ const Mine: React.FC = () => {
|
||||
// 清除本地存储的用户信息
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("token_expired");
|
||||
localStorage.removeItem("s2_accountId");
|
||||
localStorage.removeItem("userInfo");
|
||||
setShowLogoutDialog(false);
|
||||
navigate("/login");
|
||||
@@ -120,30 +119,6 @@ const Mine: React.FC = () => {
|
||||
navigate(path);
|
||||
};
|
||||
|
||||
// 渲染用户头像
|
||||
const renderUserAvatar = () => {
|
||||
if (currentUserInfo.avatar) {
|
||||
return <img src={currentUserInfo.avatar} alt="头像" />;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: "#1890ff",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "white",
|
||||
fontSize: "24px",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
售
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染功能模块图标
|
||||
const renderModuleIcon = (module: any) => (
|
||||
<div
|
||||
|
||||
@@ -1,11 +1,52 @@
|
||||
.recharge-page {
|
||||
padding: 16px 0 60px 0;
|
||||
background: #f7f8fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.record-btn {
|
||||
color: var(--primary-color);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(24, 142, 238, 0.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: rgba(24, 142, 238, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.recharge-tabs {
|
||||
:global(.adm-tabs-header) {
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
:global(.adm-tabs-tab) {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:global(.adm-tabs-tab-active) {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
:global(.adm-tabs-tab-line) {
|
||||
background: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
}
|
||||
|
||||
.balance-card {
|
||||
margin: 16px;
|
||||
margin-bottom: 16px;
|
||||
background: #f6ffed;
|
||||
border: 1px solid #b7eb8f;
|
||||
border-radius: 12px;
|
||||
@@ -43,24 +84,24 @@
|
||||
}
|
||||
|
||||
.quick-card {
|
||||
margin: 16px;
|
||||
margin-bottom: 16px;
|
||||
.quick-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.desc-card {
|
||||
margin: 16px;
|
||||
margin: 16px 0px;
|
||||
background: #fffbe6;
|
||||
border: 1px solid #ffe58f;
|
||||
}
|
||||
|
||||
.warn-card {
|
||||
margin: 16px;
|
||||
margin: 16px 0;
|
||||
background: #fff2e8;
|
||||
border: 1px solid #ffbb96;
|
||||
}
|
||||
@@ -125,3 +166,275 @@
|
||||
color: #faad14;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
// AI服务样式
|
||||
.ai-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ai-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.ai-icon {
|
||||
font-size: 24px;
|
||||
color: var(--primary-color);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.ai-tag {
|
||||
background: #ff6b35;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ai-description {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.ai-services {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.ai-service-card {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.service-header {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.service-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
font-size: 24px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f0f0f0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.service-details {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.service-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.service-price {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.service-description {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.service-features {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.feature-check {
|
||||
color: #52c41a;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.usage-progress {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.usage-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: var(--primary-color);
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.usage-text {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
// 版本套餐样式
|
||||
.version-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.version-icon {
|
||||
font-size: 24px;
|
||||
color: #722ed1;
|
||||
}
|
||||
|
||||
.version-description {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.version-packages {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.version-card {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.package-header {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.package-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.package-icon {
|
||||
font-size: 24px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f0f0f0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.package-details {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.package-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.package-tag {
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tag-blue {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.tag-green {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.package-price {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.package-description {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.package-features {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.features-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.package-status {
|
||||
text-align: center;
|
||||
color: #52c41a;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.upgrade-btn {
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,123 @@
|
||||
import React, { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Card, Button, Toast, NavBar } from "antd-mobile";
|
||||
import { Card, Button, Toast, NavBar, Tabs } from "antd-mobile";
|
||||
import { useUserStore } from "@/store/module/user";
|
||||
import style from "./index.module.scss";
|
||||
import { WalletOutlined, WarningOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
WalletOutlined,
|
||||
WarningOutlined,
|
||||
ClockCircleOutlined,
|
||||
RobotOutlined,
|
||||
CrownOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
|
||||
const quickAmounts = [50, 100, 200, 500, 1000];
|
||||
|
||||
// AI服务套餐数据
|
||||
const aiServicePackages = [
|
||||
{
|
||||
id: 1,
|
||||
name: "入门套餐",
|
||||
tag: "推荐",
|
||||
tagColor: "blue",
|
||||
description: "适合个人用户体验AI服务",
|
||||
usage: "可使用AI服务约110次",
|
||||
price: 100,
|
||||
originalPrice: 110,
|
||||
gift: 10,
|
||||
actualAmount: 110,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "标准套餐",
|
||||
tag: "热门",
|
||||
tagColor: "green",
|
||||
description: "适合小团队日常使用",
|
||||
usage: "可使用AI服务约580次",
|
||||
price: 500,
|
||||
originalPrice: 580,
|
||||
gift: 80,
|
||||
actualAmount: 580,
|
||||
},
|
||||
];
|
||||
|
||||
// AI服务列表数据
|
||||
const aiServices = [
|
||||
{
|
||||
id: 1,
|
||||
name: "添加好友及打招呼",
|
||||
icon: "💬",
|
||||
price: 1,
|
||||
description: "AI智能添加好友并发送个性化打招呼消息",
|
||||
features: ["智能筛选目标用户", "发送个性化打招呼消息", "自动记录添加结果"],
|
||||
usage: { current: 15, total: 450 },
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "小室AI内容生产",
|
||||
icon: "⚡",
|
||||
price: 1,
|
||||
description: "AI智能创建朋友圈内容,智能配文与朋友圈内容",
|
||||
features: ["智能生成朋友圈文案", "AI配文智能文案", "内容智能排版优化"],
|
||||
usage: { current: 28, total: 680 },
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "智能分发服务",
|
||||
icon: "📤",
|
||||
price: 1,
|
||||
description: "AI智能分发内容到多个平台",
|
||||
features: ["多平台智能分发", "内容智能优化", "分发效果分析"],
|
||||
usage: { current: 12, total: 300 },
|
||||
},
|
||||
];
|
||||
|
||||
// 版本套餐数据
|
||||
const versionPackages = [
|
||||
{
|
||||
id: 1,
|
||||
name: "普通版本",
|
||||
icon: "📦",
|
||||
price: "免费",
|
||||
description: "充值即可使用,包含基础AI功能",
|
||||
features: ["基础AI服务", "标准客服支持", "基础数据统计"],
|
||||
status: "当前使用中",
|
||||
buttonText: null,
|
||||
tagColor: undefined,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "标准版本",
|
||||
icon: "👑",
|
||||
price: "¥98/月",
|
||||
tag: "推荐",
|
||||
tagColor: "blue",
|
||||
description: "适合中小企业,AI功能更丰富",
|
||||
features: ["高级AI服务", "优先客服支持", "详细数据分析", "API接口访问"],
|
||||
status: null,
|
||||
buttonText: "立即升级",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "企业版本",
|
||||
icon: "🏢",
|
||||
price: "¥1980/月",
|
||||
description: "适合大型企业,提供专属服务",
|
||||
features: [
|
||||
"专属AI服务",
|
||||
"24小时专属客服",
|
||||
"高级数据分析",
|
||||
"API接口访问",
|
||||
"专属技术支持",
|
||||
],
|
||||
status: null,
|
||||
buttonText: "立即升级",
|
||||
tagColor: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
const Recharge: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useUserStore();
|
||||
@@ -16,6 +125,7 @@ const Recharge: React.FC = () => {
|
||||
const [balance, setBalance] = useState(0);
|
||||
const [selected, setSelected] = useState<number | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState("account");
|
||||
|
||||
// 充值操作
|
||||
const handleRecharge = async () => {
|
||||
@@ -31,68 +141,228 @@ const Recharge: React.FC = () => {
|
||||
}, 1200);
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout header={<NavCommon title="账户充值" />}>
|
||||
<div className={style["recharge-page"]}>
|
||||
<Card className={style["balance-card"]}>
|
||||
<div className={style["balance-content"]}>
|
||||
<WalletOutlined className={style["wallet-icon"]} />
|
||||
<div className={style["balance-info"]}>
|
||||
<div className={style["balance-label"]}>当前余额</div>
|
||||
<div className={style["balance-amount"]}>
|
||||
¥{balance.toFixed(2)}
|
||||
// 渲染账户充值tab内容
|
||||
const renderAccountRecharge = () => (
|
||||
<div className={style["tab-content"]}>
|
||||
<Card className={style["balance-card"]}>
|
||||
<div className={style["balance-content"]}>
|
||||
<WalletOutlined className={style["wallet-icon"]} />
|
||||
<div className={style["balance-info"]}>
|
||||
<div className={style["balance-label"]}>当前余额</div>
|
||||
<div className={style["balance-amount"]}>
|
||||
¥{balance.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={style["quick-card"]}>
|
||||
<div className={style["quick-title"]}>快捷充值</div>
|
||||
<div className={style["quick-list"]}>
|
||||
{quickAmounts.map(amt => (
|
||||
<Button
|
||||
key={amt}
|
||||
color={selected === amt ? "primary" : "default"}
|
||||
className={
|
||||
selected === amt
|
||||
? style["quick-btn-active"]
|
||||
: style["quick-btn"]
|
||||
}
|
||||
onClick={() => setSelected(amt)}
|
||||
>
|
||||
¥{amt}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
block
|
||||
color="primary"
|
||||
size="large"
|
||||
className={style["recharge-main-btn"]}
|
||||
loading={loading}
|
||||
onClick={handleRecharge}
|
||||
>
|
||||
立即充值
|
||||
</Button>
|
||||
</Card>
|
||||
<Card className={style["desc-card"]}>
|
||||
<div className={style["desc-title"]}>服务消耗</div>
|
||||
<div className={style["desc-text"]}>
|
||||
使用以下服务将从余额中扣除相应费用。
|
||||
</div>
|
||||
</Card>
|
||||
{balance < 10 && (
|
||||
<Card className={style["warn-card"]}>
|
||||
<div className={style["warn-content"]}>
|
||||
<WarningOutlined className={style["warn-icon"]} />
|
||||
<div className={style["warn-info"]}>
|
||||
<div className={style["warn-title"]}>余额不足提醒</div>
|
||||
<div className={style["warn-text"]}>
|
||||
当前余额较低,建议及时充值以免影响服务使用
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={style["quick-card"]}>
|
||||
<div className={style["quick-title"]}>快捷充值</div>
|
||||
<div className={style["quick-list"]}>
|
||||
{quickAmounts.map(amt => (
|
||||
<Button
|
||||
key={amt}
|
||||
color={selected === amt ? "primary" : "default"}
|
||||
className={
|
||||
selected === amt
|
||||
? style["quick-btn-active"]
|
||||
: style["quick-btn"]
|
||||
}
|
||||
onClick={() => setSelected(amt)}
|
||||
>
|
||||
¥{amt}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
block
|
||||
color="primary"
|
||||
size="large"
|
||||
className={style["recharge-main-btn"]}
|
||||
loading={loading}
|
||||
onClick={handleRecharge}
|
||||
>
|
||||
立即充值
|
||||
</Button>
|
||||
</Card>
|
||||
<Card className={style["desc-card"]}>
|
||||
<div className={style["desc-title"]}>服务消耗</div>
|
||||
<div className={style["desc-text"]}>
|
||||
使用以下服务将从余额中扣除相应费用。
|
||||
</div>
|
||||
</Card>
|
||||
{balance < 10 && (
|
||||
<Card className={style["warn-card"]}>
|
||||
<div className={style["warn-content"]}>
|
||||
<WarningOutlined className={style["warn-icon"]} />
|
||||
<div className={style["warn-info"]}>
|
||||
<div className={style["warn-title"]}>余额不足提醒</div>
|
||||
<div className={style["warn-text"]}>
|
||||
当前余额较低,建议及时充值以免影响服务使用
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
// 渲染AI服务tab内容
|
||||
const renderAiServices = () => (
|
||||
<div className={style["tab-content"]}>
|
||||
<div className={style["ai-header"]}>
|
||||
<div className={style["ai-title"]}>
|
||||
<RobotOutlined className={style["ai-icon"]} />
|
||||
AI智能服务收费
|
||||
</div>
|
||||
<div className={style["ai-tag"]}>统一按次收费</div>
|
||||
</div>
|
||||
<div className={style["ai-description"]}>
|
||||
三项核心AI服务,按使用次数收费,每次1元
|
||||
</div>
|
||||
|
||||
<div className={style["ai-services"]}>
|
||||
{aiServices.map(service => (
|
||||
<Card key={service.id} className={style["ai-service-card"]}>
|
||||
<div className={style["service-header"]}>
|
||||
<div className={style["service-info"]}>
|
||||
<div className={style["service-icon"]}>{service.icon}</div>
|
||||
<div className={style["service-details"]}>
|
||||
<div className={style["service-name"]}>{service.name}</div>
|
||||
<div className={style["service-price"]}>
|
||||
¥{service.price}/次
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["service-description"]}>
|
||||
{service.description}
|
||||
</div>
|
||||
<div className={style["service-features"]}>
|
||||
{service.features.map((feature, index) => (
|
||||
<div key={index} className={style["feature-item"]}>
|
||||
<span className={style["feature-check"]}>✓</span>
|
||||
{feature}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={style["usage-progress"]}>
|
||||
<div className={style["usage-label"]}>今日使用进度</div>
|
||||
<div className={style["progress-bar"]}>
|
||||
<div
|
||||
className={style["progress-fill"]}
|
||||
style={{
|
||||
width: `${(service.usage.current / service.usage.total) * 100}%`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<div className={style["usage-text"]}>
|
||||
{service.usage.current} / {service.usage.total}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 渲染版本套餐tab内容
|
||||
const renderVersionPackages = () => (
|
||||
<div className={style["tab-content"]}>
|
||||
<div className={style["version-header"]}>
|
||||
<CrownOutlined className={style["version-icon"]} />
|
||||
<span>存客宝版本套餐</span>
|
||||
</div>
|
||||
<div className={style["version-description"]}>
|
||||
选择适合的版本,享受不同级别的AI服务
|
||||
</div>
|
||||
|
||||
<div className={style["version-packages"]}>
|
||||
{versionPackages.map(pkg => (
|
||||
<Card key={pkg.id} className={style["version-card"]}>
|
||||
<div className={style["package-header"]}>
|
||||
<div className={style["package-info"]}>
|
||||
<div className={style["package-icon"]}>{pkg.icon}</div>
|
||||
<div className={style["package-details"]}>
|
||||
<div className={style["package-name"]}>
|
||||
{pkg.name}
|
||||
{pkg.tag && (
|
||||
<span
|
||||
className={`${style["package-tag"]} ${style[`tag-${pkg.tagColor || "blue"}`]}`}
|
||||
>
|
||||
{pkg.tag}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={style["package-price"]}>{pkg.price}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style["package-description"]}>
|
||||
{pkg.description}
|
||||
</div>
|
||||
<div className={style["package-features"]}>
|
||||
<div className={style["features-title"]}>包含功能:</div>
|
||||
{pkg.features.map((feature, index) => (
|
||||
<div key={index} className={style["feature-item"]}>
|
||||
<span className={style["feature-check"]}>✓</span>
|
||||
{feature}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{pkg.status && (
|
||||
<div className={style["package-status"]}>{pkg.status}</div>
|
||||
)}
|
||||
{pkg.buttonText && (
|
||||
<Button
|
||||
block
|
||||
color="primary"
|
||||
className={style["upgrade-btn"]}
|
||||
onClick={() => {
|
||||
Toast.show({ content: "升级功能开发中", position: "top" });
|
||||
}}
|
||||
>
|
||||
{pkg.buttonText}
|
||||
</Button>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<NavCommon
|
||||
title="充值中心"
|
||||
right={
|
||||
<div
|
||||
className={style["record-btn"]}
|
||||
onClick={() => navigate("/mine/consumption-records")}
|
||||
>
|
||||
<ClockCircleOutlined />
|
||||
记录
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={style["recharge-page"]}>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
className={style["recharge-tabs"]}
|
||||
>
|
||||
<Tabs.Tab title="账户充值" key="account">
|
||||
{renderAccountRecharge()}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab title="AI服务" key="ai">
|
||||
{renderAiServices()}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab title="版本套餐" key="version">
|
||||
{renderVersionPackages()}
|
||||
</Tabs.Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
import request from "@/api/request";
|
||||
import type {
|
||||
TrafficPoolUserDetail,
|
||||
UserJourneyResponse,
|
||||
UserTagsResponse,
|
||||
} from "./data";
|
||||
|
||||
export function getTrafficPoolDetail(id: string): Promise<any> {
|
||||
return request("/v1/workbench/detail", { id }, "GET");
|
||||
export function getTrafficPoolDetail(
|
||||
wechatId: string,
|
||||
): Promise<TrafficPoolUserDetail> {
|
||||
return request("/v1/wechats/getWechatInfo", { wechatId }, "GET");
|
||||
}
|
||||
|
||||
// 获取用户旅程记录
|
||||
export function getUserJourney(params: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
userId: string;
|
||||
}): Promise<UserJourneyResponse> {
|
||||
return request("/v1/traffic/pool/getUserJourney", params, "GET");
|
||||
}
|
||||
|
||||
// 获取用户标签
|
||||
export function getUserTags(userId: string): Promise<UserTagsResponse> {
|
||||
return request("/v1/traffic/pool/getUserTags", { userId }, "GET");
|
||||
}
|
||||
|
||||
// 添加用户标签
|
||||
export function addUserTag(userId: string, tagData: any): Promise<any> {
|
||||
return request("/v1/user/tags", { userId, ...tagData }, "POST");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
.container {
|
||||
padding: 0;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
// 头部样式
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.closeBtn {
|
||||
padding: 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
// 用户卡片
|
||||
.userCard {
|
||||
margin: 16px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.userInfo {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.userDetails {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.wechatId {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.userTag {
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
// 标签导航
|
||||
.tabNav {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
margin: 0 16px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.tabItem {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border-bottom: 2px solid transparent;
|
||||
|
||||
&.active {
|
||||
color: var(--primary-color);
|
||||
border-bottom-color: var(--primary-color);
|
||||
background: rgba(24, 142, 238, 0.05);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(24, 142, 238, 0.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 内容区域
|
||||
.content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.tabContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
// 信息卡片
|
||||
.infoCard {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
|
||||
:global(.adm-card-header) {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
:global(.adm-card-body) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// RFM评分网格
|
||||
.rfmGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.rfmItem {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.rfmLabel {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.rfmValue {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// 流量池区域
|
||||
.poolSection {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.currentPool,
|
||||
.availablePools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.poolLabel {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 统计数据网格
|
||||
.statsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.statItem {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.statValue {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.statLabel {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
// 用户旅程
|
||||
.journeyItem {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
// 加载状态
|
||||
.loadingContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loadingText {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.loadingMore {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.loadMoreBtn {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
// 标签区域
|
||||
.tagsSection {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.valueTagsSection {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.tagItem {
|
||||
font-size: 12px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.valueTagContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.valueTagRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.rfmScoreText {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.valueLevelLabel {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.valueTagItem {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.valueInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
// 添加标签按钮
|
||||
.addTagBtn {
|
||||
margin-top: 16px;
|
||||
border-radius: 8px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
// 空状态
|
||||
.emptyState {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.emptyIcon {
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.emptyText {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.emptyDesc {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
// 限制记录样式
|
||||
.restrictionTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.restrictionLevel {
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.restrictionContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 375px) {
|
||||
.rfmGrid,
|
||||
.statsGrid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.userInfo {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.restrictionTitle {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.restrictionContent {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,285 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Card,
|
||||
Button,
|
||||
Avatar,
|
||||
Tag,
|
||||
Tabs,
|
||||
List,
|
||||
Badge,
|
||||
SpinLoading,
|
||||
} from "antd-mobile";
|
||||
import {
|
||||
UserOutlined,
|
||||
CrownOutlined,
|
||||
PlusOutlined,
|
||||
CloseOutlined,
|
||||
EyeOutlined,
|
||||
DollarOutlined,
|
||||
MobileOutlined,
|
||||
TagOutlined,
|
||||
FileTextOutlined,
|
||||
UserAddOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import { getTrafficPoolDetail } from "./api";
|
||||
import type { TrafficPoolUserDetail } from "./data";
|
||||
import { Card, Button, Avatar, Tag, Spin } from "antd";
|
||||
|
||||
const tabList = [
|
||||
{ key: "base", label: "基本信息" },
|
||||
{ key: "journey", label: "用户旅程" },
|
||||
{ key: "tags", label: "用户标签" },
|
||||
];
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import { getTrafficPoolDetail, getUserJourney, getUserTags } from "./api";
|
||||
import type {
|
||||
TrafficPoolUserDetail,
|
||||
ExtendedUserDetail,
|
||||
InteractionRecord,
|
||||
UserJourneyRecord,
|
||||
UserTagsResponse,
|
||||
UserTagItem,
|
||||
} from "./data";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
const TrafficPoolDetail: React.FC = () => {
|
||||
const { id } = useParams();
|
||||
const { wxid, userId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [user, setUser] = useState<TrafficPoolUserDetail | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<"base" | "journey" | "tags">(
|
||||
"base"
|
||||
);
|
||||
const [user, setUser] = useState<ExtendedUserDetail | null>(null);
|
||||
const [activeTab, setActiveTab] = useState("basic");
|
||||
|
||||
// 用户旅程相关状态
|
||||
const [journeyLoading, setJourneyLoading] = useState(false);
|
||||
const [journeyList, setJourneyList] = useState<UserJourneyRecord[]>([]);
|
||||
const [journeyPage, setJourneyPage] = useState(1);
|
||||
const [journeyTotal, setJourneyTotal] = useState(0);
|
||||
const pageSize = 10;
|
||||
|
||||
// 用户标签相关状态
|
||||
const [tagsLoading, setTagsLoading] = useState(false);
|
||||
const [userTagsList, setUserTagsList] = useState<UserTagItem[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
if (!wxid) return;
|
||||
setLoading(true);
|
||||
getTrafficPoolDetail(id as string)
|
||||
.then(res => setUser(res))
|
||||
getTrafficPoolDetail(wxid as string)
|
||||
.then(res => {
|
||||
// 将API数据转换为扩展的用户详情数据
|
||||
const extendedUser: ExtendedUserDetail = {
|
||||
...res,
|
||||
// 模拟RFM评分数据
|
||||
rfmScore: {
|
||||
recency: 5,
|
||||
frequency: 5,
|
||||
monetary: 5,
|
||||
totalScore: 15,
|
||||
},
|
||||
// 模拟流量池数据
|
||||
trafficPools: {
|
||||
currentPool: "新用户池",
|
||||
availablePools: ["高价值客户池", "活跃用户池"],
|
||||
},
|
||||
// 模拟用户标签数据
|
||||
userTags: [
|
||||
{ id: "1", name: "近期活跃", color: "success", type: "user" },
|
||||
{ id: "2", name: "高频互动", color: "primary", type: "user" },
|
||||
{ id: "3", name: "高消费", color: "warning", type: "user" },
|
||||
{ id: "4", name: "老客户", color: "danger", type: "user" },
|
||||
],
|
||||
// 模拟价值标签数据
|
||||
valueTags: [
|
||||
{
|
||||
id: "1",
|
||||
name: "重要保持客户",
|
||||
color: "primary",
|
||||
icon: "crown",
|
||||
rfmScore: 14,
|
||||
valueLevel: "高价值",
|
||||
},
|
||||
],
|
||||
};
|
||||
setUser(extendedUser);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, [id]);
|
||||
}, [wxid]);
|
||||
|
||||
// 获取用户旅程数据
|
||||
const fetchUserJourney = async (page: number = 1) => {
|
||||
if (!userId) return;
|
||||
|
||||
setJourneyLoading(true);
|
||||
try {
|
||||
const response = await getUserJourney({
|
||||
page,
|
||||
pageSize,
|
||||
userId: userId,
|
||||
});
|
||||
|
||||
if (page === 1) {
|
||||
setJourneyList(response.list);
|
||||
} else {
|
||||
setJourneyList(prev => [...prev, ...response.list]);
|
||||
}
|
||||
setJourneyTotal(response.total);
|
||||
setJourneyPage(page);
|
||||
} catch (error) {
|
||||
console.error("获取用户旅程失败:", error);
|
||||
} finally {
|
||||
setJourneyLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取用户标签数据
|
||||
const fetchUserTags = async () => {
|
||||
if (!userId) return;
|
||||
|
||||
setTagsLoading(true);
|
||||
try {
|
||||
const response: UserTagsResponse = await getUserTags(userId);
|
||||
setUserTagsList(response.siteLabels || []);
|
||||
} catch (error) {
|
||||
console.error("获取用户标签失败:", error);
|
||||
} finally {
|
||||
setTagsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 标签切换处理
|
||||
const handleTabChange = (tab: string) => {
|
||||
setActiveTab(tab);
|
||||
if (tab === "journey" && journeyList.length === 0) {
|
||||
fetchUserJourney(1);
|
||||
}
|
||||
if (tab === "tags" && userTagsList.length === 0) {
|
||||
fetchUserTags();
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
navigate(-1);
|
||||
};
|
||||
|
||||
const getJourneyTypeIcon = (type: number) => {
|
||||
switch (type) {
|
||||
case 0: // 浏览
|
||||
return <EyeOutlined style={{ color: "#722ed1" }} />;
|
||||
case 2: // 提交订单
|
||||
return <FileTextOutlined style={{ color: "#52c41a" }} />;
|
||||
case 3: // 注册
|
||||
return <UserAddOutlined style={{ color: "#1677ff" }} />;
|
||||
default:
|
||||
return <MobileOutlined style={{ color: "#999" }} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getJourneyTypeText = (type: number) => {
|
||||
switch (type) {
|
||||
case 0:
|
||||
return "浏览行为";
|
||||
case 2:
|
||||
return "提交订单";
|
||||
case 3:
|
||||
return "注册行为";
|
||||
default:
|
||||
return "其他行为";
|
||||
}
|
||||
};
|
||||
|
||||
const formatDateTime = (dateTime: string) => {
|
||||
try {
|
||||
const date = new Date(dateTime);
|
||||
return date.toLocaleString("zh-CN", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
} catch (error) {
|
||||
return dateTime;
|
||||
}
|
||||
};
|
||||
|
||||
const getActionIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "click":
|
||||
return <MobileOutlined style={{ color: "#1677ff" }} />;
|
||||
case "view":
|
||||
return <EyeOutlined style={{ color: "#722ed1" }} />;
|
||||
case "purchase":
|
||||
return <DollarOutlined style={{ color: "#52c41a" }} />;
|
||||
default:
|
||||
return <MobileOutlined style={{ color: "#999" }} />;
|
||||
}
|
||||
};
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return `¥${amount.toLocaleString()}`;
|
||||
};
|
||||
|
||||
const getGenderText = (gender: number) => {
|
||||
switch (gender) {
|
||||
case 1:
|
||||
return "男";
|
||||
case 2:
|
||||
return "女";
|
||||
default:
|
||||
return "未知";
|
||||
}
|
||||
};
|
||||
|
||||
const getGenderColor = (gender: number) => {
|
||||
switch (gender) {
|
||||
case 1:
|
||||
return "#1677ff";
|
||||
case 2:
|
||||
return "#eb2f96";
|
||||
default:
|
||||
return "#999";
|
||||
}
|
||||
};
|
||||
|
||||
const getRestrictionLevelText = (level: number) => {
|
||||
switch (level) {
|
||||
case 1:
|
||||
return "轻微";
|
||||
case 2:
|
||||
return "中等";
|
||||
case 3:
|
||||
return "严重";
|
||||
default:
|
||||
return "未知";
|
||||
}
|
||||
};
|
||||
|
||||
const getRestrictionLevelColor = (level: number) => {
|
||||
switch (level) {
|
||||
case 1:
|
||||
return "warning";
|
||||
case 2:
|
||||
return "danger";
|
||||
case 3:
|
||||
return "danger";
|
||||
default:
|
||||
return "default";
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (timestamp: number | null) => {
|
||||
if (!timestamp) return "--";
|
||||
try {
|
||||
const date = new Date(timestamp * 1000);
|
||||
return date.toLocaleDateString("zh-CN");
|
||||
} catch (error) {
|
||||
return "--";
|
||||
}
|
||||
};
|
||||
|
||||
// 获取标签颜色
|
||||
const getTagColor = (index: number): string => {
|
||||
const colors = ["primary", "success", "warning", "danger", "default"];
|
||||
return colors[index % colors.length];
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Layout>
|
||||
<div style={{ textAlign: "center", padding: "64px 0" }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
if (!user) {
|
||||
return (
|
||||
<Layout>
|
||||
<div style={{ textAlign: "center", color: "#aaa", padding: "64px 0" }}>
|
||||
未找到该用户
|
||||
<Layout header={<NavCommon title="用户详情" />} loading={loading}>
|
||||
<div className={styles.emptyState}>
|
||||
<div className={styles.emptyText}>未找到该用户</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
@@ -49,249 +287,418 @@ const TrafficPoolDetail: React.FC = () => {
|
||||
|
||||
return (
|
||||
<Layout
|
||||
loading={loading}
|
||||
header={
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
height: 48,
|
||||
borderBottom: "1px solid #eee",
|
||||
background: "#fff",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => navigate(-1)}
|
||||
style={{ marginRight: 8 }}
|
||||
>
|
||||
< 返回
|
||||
</Button>
|
||||
<div style={{ fontWeight: 600, fontSize: 18 }}>用户详情</div>
|
||||
</div>
|
||||
<>
|
||||
<NavCommon title="用户详情" />
|
||||
{/* 用户基本信息 */}
|
||||
<Card className={styles.userCard}>
|
||||
<div className={styles.userInfo}>
|
||||
<Avatar
|
||||
src={user.userInfo.avatar}
|
||||
className={styles.avatar}
|
||||
fallback={<UserOutlined />}
|
||||
/>
|
||||
<div className={styles.userDetails}>
|
||||
<div className={styles.nickname}>{user.userInfo.nickname}</div>
|
||||
<div className={styles.wechatId}>{user.userInfo.wechatId}</div>
|
||||
<div className={styles.tags}>
|
||||
<Tag
|
||||
color="warning"
|
||||
fill="outline"
|
||||
className={styles.userTag}
|
||||
>
|
||||
<CrownOutlined />
|
||||
重要价值客户
|
||||
</Tag>
|
||||
<Tag color="danger" fill="outline" className={styles.userTag}>
|
||||
优先添加
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{/* 导航标签 */}
|
||||
<div className={styles.tabNav}>
|
||||
<div
|
||||
className={`${styles.tabItem} ${
|
||||
activeTab === "basic" ? styles.active : ""
|
||||
}`}
|
||||
onClick={() => handleTabChange("basic")}
|
||||
>
|
||||
基本信息
|
||||
</div>
|
||||
<div
|
||||
className={`${styles.tabItem} ${
|
||||
activeTab === "journey" ? styles.active : ""
|
||||
}`}
|
||||
onClick={() => handleTabChange("journey")}
|
||||
>
|
||||
用户旅程
|
||||
</div>
|
||||
<div
|
||||
className={`${styles.tabItem} ${
|
||||
activeTab === "tags" ? styles.active : ""
|
||||
}`}
|
||||
onClick={() => handleTabChange("tags")}
|
||||
>
|
||||
用户标签
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div style={{ padding: 16 }}>
|
||||
{/* 顶部信息 */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 16,
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<Avatar src={user.avatar} size={64} />
|
||||
<div>
|
||||
<div style={{ fontSize: 20, fontWeight: 600 }}>{user.nickname}</div>
|
||||
<div style={{ color: "#1677ff", fontSize: 14, margin: "4px 0" }}>
|
||||
{user.wechatId}
|
||||
</div>
|
||||
{user.packages &&
|
||||
user.packages.length > 0 &&
|
||||
user.packages.map(pkg => (
|
||||
<Tag color="purple" key={pkg} style={{ marginRight: 4 }}>
|
||||
{pkg}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* Tab栏 */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 24,
|
||||
borderBottom: "1px solid #eee",
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
{tabList.map(tab => (
|
||||
<div
|
||||
key={tab.key}
|
||||
style={{
|
||||
padding: "8px 0",
|
||||
fontWeight: 500,
|
||||
color: activeTab === tab.key ? "#1677ff" : "#888",
|
||||
borderBottom:
|
||||
activeTab === tab.key ? "2px solid #1677ff" : "none",
|
||||
cursor: "pointer",
|
||||
fontSize: 16,
|
||||
}}
|
||||
onClick={() => setActiveTab(tab.key as any)}
|
||||
>
|
||||
{tab.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Tab内容 */}
|
||||
{activeTab === "base" && (
|
||||
<>
|
||||
<Card style={{ marginBottom: 16 }} title="关键信息">
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 24 }}>
|
||||
<div>设备:{user.deviceName || "--"}</div>
|
||||
<div>微信号:{user.wechatAccountName || "--"}</div>
|
||||
<div>客服:{user.customerServiceName || "--"}</div>
|
||||
<div>添加时间:{user.addTime || "--"}</div>
|
||||
<div>最近互动:{user.lastInteraction || "--"}</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card style={{ marginBottom: 16 }} title="RFM评分">
|
||||
<div style={{ display: "flex", gap: 32 }}>
|
||||
<div>
|
||||
<div
|
||||
style={{ fontSize: 20, fontWeight: 600, color: "#1677ff" }}
|
||||
>
|
||||
{user.rfmScore?.recency ?? "-"}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: "#888" }}>最近性(R)</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
style={{ fontSize: 20, fontWeight: 600, color: "#52c41a" }}
|
||||
>
|
||||
{user.rfmScore?.frequency ?? "-"}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: "#888" }}>频率(F)</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
style={{ fontSize: 20, fontWeight: 600, color: "#eb2f96" }}
|
||||
>
|
||||
{user.rfmScore?.monetary ?? "-"}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: "#888" }}>金额(M)</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card style={{ marginBottom: 16 }} title="统计数据">
|
||||
<div style={{ display: "flex", gap: 32 }}>
|
||||
<div>
|
||||
<div
|
||||
style={{ fontSize: 18, fontWeight: 600, color: "#52c41a" }}
|
||||
>
|
||||
¥{user.totalSpent ?? "-"}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: "#888" }}>总消费</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
style={{ fontSize: 18, fontWeight: 600, color: "#1677ff" }}
|
||||
>
|
||||
{user.interactionCount ?? "-"}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: "#888" }}>互动次数</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
style={{ fontSize: 18, fontWeight: 600, color: "#faad14" }}
|
||||
>
|
||||
{user.conversionRate ?? "-"}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: "#888" }}>转化率</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
style={{ fontSize: 18, fontWeight: 600, color: "#ff4d4f" }}
|
||||
>
|
||||
{user.status === "failed"
|
||||
? "添加失败"
|
||||
: user.status === "added"
|
||||
? "添加成功"
|
||||
: "未添加"}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: "#888" }}>添加状态</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
{activeTab === "journey" && (
|
||||
<Card title="互动记录">
|
||||
{user.interactions && user.interactions.length > 0 ? (
|
||||
user.interactions.slice(0, 4).map(it => (
|
||||
<div
|
||||
key={it.id}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
borderBottom: "1px solid #f0f0f0",
|
||||
padding: "12px 0",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 22 }}>
|
||||
{it.type === "click" && "📱"}
|
||||
{it.type === "message" && "💬"}
|
||||
{it.type === "purchase" && "💲"}
|
||||
{it.type === "view" && "👁️"}
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontWeight: 500 }}>
|
||||
{it.type === "click" && "点击行为"}
|
||||
{it.type === "message" && "消息互动"}
|
||||
{it.type === "purchase" && "购买行为"}
|
||||
{it.type === "view" && "页面浏览"}
|
||||
<div className={styles.container}>
|
||||
{/* 内容区域 */}
|
||||
<div className={styles.content}>
|
||||
{activeTab === "basic" && (
|
||||
<div className={styles.tabContent}>
|
||||
{/* 关联信息 */}
|
||||
<Card title="关联信息" className={styles.infoCard}>
|
||||
<List>
|
||||
<List.Item extra="设备4">设备</List.Item>
|
||||
<List.Item extra="微信4-1">微信号</List.Item>
|
||||
<List.Item extra="客服1">客服</List.Item>
|
||||
<List.Item extra="2025/07/21">添加时间</List.Item>
|
||||
<List.Item extra="2025/07/25">最近互动</List.Item>
|
||||
</List>
|
||||
</Card>
|
||||
|
||||
{/* RFM评分 */}
|
||||
{user.rfmScore && (
|
||||
<Card title="RFM评分" className={styles.infoCard}>
|
||||
<div className={styles.rfmGrid}>
|
||||
<div className={styles.rfmItem}>
|
||||
<div className={styles.rfmLabel}>最近性(R)</div>
|
||||
<div
|
||||
className={styles.rfmValue}
|
||||
style={{ color: "#1677ff" }}
|
||||
>
|
||||
{user.rfmScore.recency}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ color: "#888", fontSize: 13 }}>
|
||||
{it.content}
|
||||
{it.type === "purchase" && it.value && (
|
||||
<span
|
||||
style={{
|
||||
color: "#52c41a",
|
||||
fontWeight: 600,
|
||||
marginLeft: 4,
|
||||
}}
|
||||
>
|
||||
¥{it.value}
|
||||
</span>
|
||||
)}
|
||||
<div className={styles.rfmItem}>
|
||||
<div className={styles.rfmLabel}>频率(F)</div>
|
||||
<div
|
||||
className={styles.rfmValue}
|
||||
style={{ color: "#52c41a" }}
|
||||
>
|
||||
{user.rfmScore.frequency}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.rfmItem}>
|
||||
<div className={styles.rfmLabel}>金额(M)</div>
|
||||
<div
|
||||
className={styles.rfmValue}
|
||||
style={{ color: "#722ed1" }}
|
||||
>
|
||||
{user.rfmScore.monetary}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.rfmItem}>
|
||||
<div className={styles.rfmLabel}>总分</div>
|
||||
<div
|
||||
className={styles.rfmValue}
|
||||
style={{ color: "#ff4d4f" }}
|
||||
>
|
||||
{user.rfmScore.totalScore}/15
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: "#aaa",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{it.timestamp}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
color: "#aaa",
|
||||
textAlign: "center",
|
||||
padding: "24px 0",
|
||||
}}
|
||||
>
|
||||
暂无互动记录
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
{activeTab === "tags" && (
|
||||
<Card title="用户标签">
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
{user.tags && user.tags.length > 0 ? (
|
||||
user.tags.map(tag => (
|
||||
<Tag
|
||||
key={tag}
|
||||
color="blue"
|
||||
style={{ marginRight: 8, marginBottom: 8 }}
|
||||
>
|
||||
{tag}
|
||||
</Tag>
|
||||
))
|
||||
) : (
|
||||
<span style={{ color: "#aaa" }}>暂无标签</span>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 流量池 */}
|
||||
{user.trafficPools && (
|
||||
<Card title="流量池" className={styles.infoCard}>
|
||||
<div className={styles.poolSection}>
|
||||
<div className={styles.currentPool}>
|
||||
<span className={styles.poolLabel}>当前池:</span>
|
||||
<Tag color="primary" fill="outline">
|
||||
{user.trafficPools.currentPool}
|
||||
</Tag>
|
||||
</div>
|
||||
<div className={styles.availablePools}>
|
||||
<span className={styles.poolLabel}>可选池:</span>
|
||||
{user.trafficPools.availablePools.map((pool, index) => (
|
||||
<Tag key={index} color="default" fill="outline">
|
||||
{pool}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 统计数据 */}
|
||||
<Card title="统计数据" className={styles.infoCard}>
|
||||
<div className={styles.statsGrid}>
|
||||
<div className={styles.statItem}>
|
||||
<div
|
||||
className={styles.statValue}
|
||||
style={{ color: "#52c41a" }}
|
||||
>
|
||||
¥9561
|
||||
</div>
|
||||
<div className={styles.statLabel}>总消费</div>
|
||||
</div>
|
||||
<div className={styles.statItem}>
|
||||
<div
|
||||
className={styles.statValue}
|
||||
style={{ color: "#1677ff" }}
|
||||
>
|
||||
6
|
||||
</div>
|
||||
<div className={styles.statLabel}>互动次数</div>
|
||||
</div>
|
||||
<div className={styles.statItem}>
|
||||
<div
|
||||
className={styles.statValue}
|
||||
style={{ color: "#722ed1" }}
|
||||
>
|
||||
3%
|
||||
</div>
|
||||
<div className={styles.statLabel}>转化率</div>
|
||||
</div>
|
||||
<div className={styles.statItem}>
|
||||
<div className={styles.statValue} style={{ color: "#999" }}>
|
||||
未添加
|
||||
</div>
|
||||
<div className={styles.statLabel}>添加状态</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 好友统计 */}
|
||||
<Card title="好友统计" className={styles.infoCard}>
|
||||
<div className={styles.statsGrid}>
|
||||
<div className={styles.statItem}>
|
||||
<div
|
||||
className={styles.statValue}
|
||||
style={{ color: "#1677ff" }}
|
||||
>
|
||||
{user.userInfo.friendShip.totalFriend}
|
||||
</div>
|
||||
<div className={styles.statLabel}>总好友</div>
|
||||
</div>
|
||||
<div className={styles.statItem}>
|
||||
<div
|
||||
className={styles.statValue}
|
||||
style={{ color: "#1677ff" }}
|
||||
>
|
||||
{user.userInfo.friendShip.maleFriend}
|
||||
</div>
|
||||
<div className={styles.statLabel}>男性好友</div>
|
||||
</div>
|
||||
<div className={styles.statItem}>
|
||||
<div
|
||||
className={styles.statValue}
|
||||
style={{ color: "#eb2f96" }}
|
||||
>
|
||||
{user.userInfo.friendShip.femaleFriend}
|
||||
</div>
|
||||
<div className={styles.statLabel}>女性好友</div>
|
||||
</div>
|
||||
<div className={styles.statItem}>
|
||||
<div className={styles.statValue} style={{ color: "#999" }}>
|
||||
{user.userInfo.friendShip.unknowFriend}
|
||||
</div>
|
||||
<div className={styles.statLabel}>未知性别</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 限制记录 */}
|
||||
<Card title="限制记录" className={styles.infoCard}>
|
||||
{user.restrictions && user.restrictions.length > 0 ? (
|
||||
<List>
|
||||
{user.restrictions.map(restriction => (
|
||||
<List.Item
|
||||
key={restriction.id}
|
||||
title={
|
||||
<div className={styles.restrictionTitle}>
|
||||
<span>{restriction.reason || "未知原因"}</span>
|
||||
<Tag
|
||||
color={getRestrictionLevelColor(
|
||||
restriction.level,
|
||||
)}
|
||||
fill="outline"
|
||||
className={styles.restrictionLevel}
|
||||
>
|
||||
{getRestrictionLevelText(restriction.level)}
|
||||
</Tag>
|
||||
</div>
|
||||
}
|
||||
description={
|
||||
<div className={styles.restrictionContent}>
|
||||
<span>限制ID: {restriction.id}</span>
|
||||
{restriction.date && (
|
||||
<span>
|
||||
限制时间: {formatDate(restriction.date)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
) : (
|
||||
<div className={styles.emptyState}>
|
||||
<div className={styles.emptyIcon}>
|
||||
<UserOutlined style={{ fontSize: 48, color: "#ccc" }} />
|
||||
</div>
|
||||
<div className={styles.emptyText}>暂无限制记录</div>
|
||||
<div className={styles.emptyDesc}>
|
||||
该用户没有任何限制记录
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
<Button type="dashed" block>
|
||||
➕ 添加新标签
|
||||
</Button>
|
||||
</Card>
|
||||
)}
|
||||
)}
|
||||
|
||||
{activeTab === "journey" && (
|
||||
<div className={styles.tabContent}>
|
||||
<Card title="互动记录" className={styles.infoCard}>
|
||||
{journeyLoading && journeyList.length === 0 ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<SpinLoading color="primary" style={{ fontSize: 24 }} />
|
||||
<div className={styles.loadingText}>加载中...</div>
|
||||
</div>
|
||||
) : journeyList.length === 0 ? (
|
||||
<div className={styles.emptyState}>
|
||||
<div className={styles.emptyIcon}>
|
||||
<EyeOutlined style={{ fontSize: 48, color: "#ccc" }} />
|
||||
</div>
|
||||
<div className={styles.emptyText}>暂无互动记录</div>
|
||||
<div className={styles.emptyDesc}>
|
||||
该用户还没有任何互动行为
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<List>
|
||||
{journeyList.map(record => (
|
||||
<List.Item
|
||||
key={record.id}
|
||||
prefix={getJourneyTypeIcon(record.type)}
|
||||
title={getJourneyTypeText(record.type)}
|
||||
description={
|
||||
<div className={styles.journeyItem}>
|
||||
<span>{record.remark}</span>
|
||||
<span className={styles.timestamp}>
|
||||
{formatDateTime(record.createTime)}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{journeyLoading && journeyList.length > 0 && (
|
||||
<div className={styles.loadingMore}>
|
||||
<SpinLoading color="primary" style={{ fontSize: 16 }} />
|
||||
<span>加载更多...</span>
|
||||
</div>
|
||||
)}
|
||||
{!journeyLoading && journeyList.length < journeyTotal && (
|
||||
<div className={styles.loadMoreBtn}>
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
onClick={() => fetchUserJourney(journeyPage + 1)}
|
||||
>
|
||||
加载更多
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "tags" && (
|
||||
<div className={styles.tabContent}>
|
||||
{/* 用户标签 */}
|
||||
<Card title="用户标签" className={styles.infoCard}>
|
||||
{tagsLoading && userTagsList.length === 0 ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<SpinLoading color="primary" style={{ fontSize: 24 }} />
|
||||
<div className={styles.loadingText}>加载中...</div>
|
||||
</div>
|
||||
) : userTagsList.length === 0 ? (
|
||||
<div className={styles.emptyState}>
|
||||
<div className={styles.emptyIcon}>
|
||||
<TagOutlined style={{ fontSize: 48, color: "#ccc" }} />
|
||||
</div>
|
||||
<div className={styles.emptyText}>暂无用户标签</div>
|
||||
<div className={styles.emptyDesc}>该用户还没有任何标签</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.tagsSection}>
|
||||
{userTagsList.map((tag, index) => (
|
||||
<Tag
|
||||
key={tag.id}
|
||||
color={getTagColor(index)}
|
||||
fill="outline"
|
||||
className={styles.tagItem}
|
||||
>
|
||||
{tag.name}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 价值标签 */}
|
||||
<Card title="价值标签" className={styles.infoCard}>
|
||||
{user.valueTags && user.valueTags.length > 0 ? (
|
||||
<div className={styles.valueTagsSection}>
|
||||
{user.valueTags.map(tag => (
|
||||
<div key={tag.id} className={styles.valueTagContainer}>
|
||||
<div className={styles.valueTagRow}>
|
||||
<Tag
|
||||
color={tag.color}
|
||||
fill="outline"
|
||||
className={styles.tagItem}
|
||||
>
|
||||
{tag.icon === "crown" && <CrownOutlined />}
|
||||
{tag.name}
|
||||
</Tag>
|
||||
<span className={styles.rfmScoreText}>
|
||||
RFM总分: {tag.rfmScore}/15
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.valueTagRow}>
|
||||
<span className={styles.valueLevelLabel}>
|
||||
价值等级:
|
||||
</span>
|
||||
<Tag color="danger" fill="outline">
|
||||
{tag.valueLevel}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.emptyState}>
|
||||
<div className={styles.emptyIcon}>
|
||||
<CrownOutlined style={{ fontSize: 48, color: "#ccc" }} />
|
||||
</div>
|
||||
<div className={styles.emptyText}>暂无价值标签</div>
|
||||
<div className={styles.emptyDesc}>
|
||||
该用户还没有任何价值标签
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 添加新标签按钮 */}
|
||||
<Button block color="primary" className={styles.addTagBtn}>
|
||||
<TagOutlined />
|
||||
添加新标签
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@@ -90,7 +90,7 @@ export function useTrafficPoolListLogic() {
|
||||
// 单选
|
||||
const handleSelect = (id: number, checked: boolean) => {
|
||||
setSelectedIds(prev =>
|
||||
checked ? [...prev, id] : prev.filter(i => i !== id)
|
||||
checked ? [...prev, id] : prev.filter(i => i !== id),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -191,7 +191,9 @@ const TrafficPoolList: React.FC = () => {
|
||||
<div
|
||||
className={styles.card}
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => navigate(`/traffic-pool/detail/${item.id}`)}
|
||||
onClick={() =>
|
||||
navigate(`/traffic-pool/detail/${item.sourceId}/${item.id}`)
|
||||
}
|
||||
>
|
||||
<div className={styles.cardContent}>
|
||||
<Checkbox
|
||||
|
||||
@@ -19,7 +19,7 @@ export function getWechatFriends(params: {
|
||||
limit: params.limit,
|
||||
keyword: params.keyword,
|
||||
},
|
||||
"GET"
|
||||
"GET",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ const WechatAccountDetail: React.FC = () => {
|
||||
setIsFetchingFriends(false);
|
||||
}
|
||||
},
|
||||
[id]
|
||||
[id],
|
||||
);
|
||||
|
||||
// 搜索好友
|
||||
@@ -153,7 +153,7 @@ const WechatAccountDetail: React.FC = () => {
|
||||
setFriendsPage(page);
|
||||
fetchFriendsList(page, searchQuery);
|
||||
},
|
||||
[searchQuery, fetchFriendsList]
|
||||
[searchQuery, fetchFriendsList],
|
||||
);
|
||||
|
||||
// 初始化数据
|
||||
|
||||
@@ -69,7 +69,7 @@ const Scene: React.FC = () => {
|
||||
|
||||
const handleScenarioClick = (scenarioId: string, scenarioName: string) => {
|
||||
navigate(
|
||||
`/scenarios/list/${scenarioId}/${encodeURIComponent(scenarioName)}`
|
||||
`/scenarios/list/${scenarioId}/${encodeURIComponent(scenarioName)}`,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -211,7 +211,7 @@ const ScenarioList: React.FC = () => {
|
||||
if (response) {
|
||||
// 处理webhook URL,使用工具函数构建完整地址
|
||||
const webhookUrl = buildApiUrl(
|
||||
response.textUrl?.fullUrl || `webhook/${taskId}`
|
||||
response.textUrl?.fullUrl || `webhook/${taskId}`,
|
||||
);
|
||||
|
||||
setCurrentApiSettings({
|
||||
@@ -286,7 +286,7 @@ const ScenarioList: React.FC = () => {
|
||||
};
|
||||
|
||||
const filteredTasks = tasks.filter(task =>
|
||||
task.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
task.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
);
|
||||
|
||||
// 生成操作菜单
|
||||
@@ -537,7 +537,7 @@ const ScenarioList: React.FC = () => {
|
||||
<span className={style["action-icon"]}>{item.icon}</span>
|
||||
<span className={style["action-text"]}>{item.text}</span>
|
||||
</div>
|
||||
)
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -317,7 +317,7 @@ HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.o
|
||||
onClick={() =>
|
||||
handleCopy(
|
||||
codeExamples[activeLanguage as keyof typeof codeExamples],
|
||||
"代码"
|
||||
"代码",
|
||||
)
|
||||
}
|
||||
className={style["copy-code-btn"]}
|
||||
|
||||
@@ -146,7 +146,7 @@ export default function NewPlan() {
|
||||
? error
|
||||
: isEdit
|
||||
? "更新计划失败,请重试"
|
||||
: "创建计划失败,请重试"
|
||||
: "创建计划失败,请重试",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -105,10 +105,10 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
const [accounts] = useState<Account[]>(generateRandomAccounts(50));
|
||||
const [materials] = useState<Material[]>(generatePosterMaterials());
|
||||
const [selectedAccounts, setSelectedAccounts] = useState<Account[]>(
|
||||
formData.accounts?.length > 0 ? formData.accounts : []
|
||||
formData.accounts?.length > 0 ? formData.accounts : [],
|
||||
);
|
||||
const [selectedMaterials, setSelectedMaterials] = useState<Material[]>(
|
||||
formData.materials?.length > 0 ? formData.materials : []
|
||||
formData.materials?.length > 0 ? formData.materials : [],
|
||||
);
|
||||
// showAllScenarios 默认为 true
|
||||
const [showAllScenarios, setShowAllScenarios] = useState(true);
|
||||
@@ -128,7 +128,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
const [customTags, setCustomTags] = useState(formData.customTags || []);
|
||||
const [tips, setTips] = useState(formData.tips || "");
|
||||
const [selectedScenarioTags, setSelectedScenarioTags] = useState(
|
||||
formData.scenarioTags || []
|
||||
formData.scenarioTags || [],
|
||||
);
|
||||
|
||||
// 电话获客相关状态
|
||||
@@ -140,10 +140,10 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
|
||||
// 群设置相关状态
|
||||
const [weixinqunName, setWeixinqunName] = useState(
|
||||
formData.weixinqunName || ""
|
||||
formData.weixinqunName || "",
|
||||
);
|
||||
const [weixinqunNotice, setWeixinqunNotice] = useState(
|
||||
formData.weixinqunNotice || ""
|
||||
formData.weixinqunNotice || "",
|
||||
);
|
||||
|
||||
// 新增:自定义海报相关状态
|
||||
@@ -232,7 +232,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
onChange({ ...formData, customTags: updatedCustomTags });
|
||||
// 同时从选中标签中移除
|
||||
const updatedSelectedTags = selectedScenarioTags.filter(
|
||||
(t: string) => t !== tagId
|
||||
(t: string) => t !== tagId,
|
||||
);
|
||||
setSelectedScenarioTags(updatedSelectedTags);
|
||||
onChange({
|
||||
@@ -292,12 +292,12 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
// 账号多选切换
|
||||
const handleAccountToggle = (account: Account) => {
|
||||
const isSelected = selectedAccounts.some(
|
||||
(a: Account) => a.id === account.id
|
||||
(a: Account) => a.id === account.id,
|
||||
);
|
||||
let newSelected;
|
||||
if (isSelected) {
|
||||
newSelected = selectedAccounts.filter(
|
||||
(a: Account) => a.id !== account.id
|
||||
(a: Account) => a.id !== account.id,
|
||||
);
|
||||
} else {
|
||||
newSelected = [...selectedAccounts, account];
|
||||
@@ -362,7 +362,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
const [orderUploaded, setOrderUploaded] = useState(false);
|
||||
|
||||
const handleOrderFileUpload = async (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
@@ -518,7 +518,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
<div className={styles["basic-materials-grid"]}>
|
||||
{[...materials, ...customPosters].map(material => {
|
||||
const isSelected = selectedMaterials.some(
|
||||
m => m.id === material.id
|
||||
m => m.id === material.id,
|
||||
);
|
||||
const isCustom = material.id.startsWith("custom-");
|
||||
return (
|
||||
|
||||
@@ -38,7 +38,7 @@ const FriendRequestSettings: React.FC<FriendRequestSettingsProps> = ({
|
||||
const [isTemplateDialogOpen, setIsTemplateDialogOpen] = useState(false);
|
||||
const [hasWarnings, setHasWarnings] = useState(false);
|
||||
const [selectedDevices, setSelectedDevices] = useState<string[]>(
|
||||
formData.device || []
|
||||
formData.device || [],
|
||||
);
|
||||
const [showRemarkTip, setShowRemarkTip] = useState(false);
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
|
||||
const handleUpdateMessage = (
|
||||
dayIndex: number,
|
||||
messageIndex: number,
|
||||
updates: Partial<MessageContent>
|
||||
updates: Partial<MessageContent>,
|
||||
) => {
|
||||
const updatedPlans = [...dayPlans];
|
||||
updatedPlans[dayIndex].messages[messageIndex] = {
|
||||
@@ -181,7 +181,7 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
|
||||
setSelectedGroupId(groupId);
|
||||
setIsGroupSelectOpen(false);
|
||||
message.success(
|
||||
`已选择群组:${mockGroups.find(g => g.id === groupId)?.name}`
|
||||
`已选择群组:${mockGroups.find(g => g.id === groupId)?.name}`,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -189,7 +189,7 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
|
||||
const triggerUpload = (
|
||||
dayIdx: number,
|
||||
msgIdx: number,
|
||||
type: "miniprogram" | "link"
|
||||
type: "miniprogram" | "link",
|
||||
) => {
|
||||
setUploadingDay(dayIdx);
|
||||
setUploadingMsgIdx(msgIdx);
|
||||
@@ -539,7 +539,7 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
|
||||
handleFileUpload(
|
||||
dayIndex,
|
||||
messageIndex,
|
||||
message.type as any
|
||||
message.type as any,
|
||||
)
|
||||
}
|
||||
>
|
||||
|
||||
@@ -33,7 +33,7 @@ export function deleteScenario(id: string) {
|
||||
export function getPlanList(
|
||||
scenarioId: string,
|
||||
page: number = 1,
|
||||
limit: number = 20
|
||||
limit: number = 20,
|
||||
) {
|
||||
return request(`/api/scenarios/${scenarioId}/plans`, { page, limit }, "GET");
|
||||
}
|
||||
@@ -214,7 +214,7 @@ export function deleteAutoLikeTask(taskId: string) {
|
||||
return request(
|
||||
`/api/workspace/auto-like/tasks/${taskId}`,
|
||||
undefined,
|
||||
"DELETE"
|
||||
"DELETE",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -240,7 +240,7 @@ export function deleteGroupPushTask(taskId: string) {
|
||||
return request(
|
||||
`/api/workspace/group-push/tasks/${taskId}`,
|
||||
undefined,
|
||||
"DELETE"
|
||||
"DELETE",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -266,7 +266,7 @@ export function deleteAutoGroupTask(taskId: string) {
|
||||
return request(
|
||||
`/api/workspace/auto-group/tasks/${taskId}`,
|
||||
undefined,
|
||||
"DELETE"
|
||||
"DELETE",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -287,7 +287,7 @@ export function getAIAnalysisReport() {
|
||||
return request(
|
||||
"/api/workspace/ai-assistant/analysis-report",
|
||||
undefined,
|
||||
"GET"
|
||||
"GET",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -379,6 +379,6 @@ export function markNotificationAsRead(notificationId: string) {
|
||||
return request(
|
||||
`/api/system/notifications/${notificationId}/read`,
|
||||
undefined,
|
||||
"PUT"
|
||||
"PUT",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ const mockTaskDetail: GroupTaskDetail = {
|
||||
nickname: `用户${mIndex + 1}`,
|
||||
wechatId: `wx_${mIndex}`,
|
||||
tags: [`标签${(mIndex % 3) + 1}`],
|
||||
})
|
||||
}),
|
||||
),
|
||||
})),
|
||||
createTime: "2024-11-20 19:04:14",
|
||||
@@ -169,10 +169,10 @@ const GroupCreationProgress: React.FC<{
|
||||
}> = ({ taskDetail, onComplete }) => {
|
||||
const [groups, setGroups] = useState<Group[]>(taskDetail.groups);
|
||||
const [currentGroupIndex, setCurrentGroupIndex] = useState(
|
||||
taskDetail.currentGroupIndex
|
||||
taskDetail.currentGroupIndex,
|
||||
);
|
||||
const [status, setStatus] = useState<GroupTaskDetail["status"]>(
|
||||
taskDetail.status
|
||||
taskDetail.status,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -207,7 +207,7 @@ const GroupCreationProgress: React.FC<{
|
||||
};
|
||||
}
|
||||
return group;
|
||||
})
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -149,8 +149,8 @@ const AutoGroupList: React.FC = () => {
|
||||
...task,
|
||||
status: task.status === "running" ? "paused" : "running",
|
||||
}
|
||||
: task
|
||||
)
|
||||
: task,
|
||||
),
|
||||
);
|
||||
Toast.show({ content: "状态已切换" });
|
||||
};
|
||||
@@ -160,7 +160,7 @@ const AutoGroupList: React.FC = () => {
|
||||
};
|
||||
|
||||
const filteredTasks = tasks.filter(task =>
|
||||
task.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
task.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
|
||||
// 获取自动点赞任务列表
|
||||
export function fetchAutoLikeTasks(
|
||||
params = { type: 1, page: 1, limit: 100 }
|
||||
params = { type: 1, page: 1, limit: 100 },
|
||||
): Promise<LikeTask[]> {
|
||||
return request("/v1/workbench/list", params, "GET");
|
||||
}
|
||||
@@ -49,7 +49,7 @@ export function fetchLikeRecords(
|
||||
workbenchId: string,
|
||||
page: number = 1,
|
||||
limit: number = 20,
|
||||
keyword?: string
|
||||
keyword?: string,
|
||||
): Promise<PaginatedResponse<LikeRecord>> {
|
||||
const params: any = {
|
||||
workbenchId,
|
||||
|
||||
@@ -224,7 +224,7 @@ const AutoLike: React.FC = () => {
|
||||
|
||||
// 过滤任务
|
||||
const filteredTasks = tasks.filter(task =>
|
||||
task.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
task.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -82,7 +82,7 @@ const NewAutoLike: React.FC = () => {
|
||||
});
|
||||
setAutoEnabled(
|
||||
(taskDetail as any).status === 1 ||
|
||||
(taskDetail as any).status === "running"
|
||||
(taskDetail as any).status === "running",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
|
||||
// 获取自动点赞任务列表
|
||||
export function fetchAutoLikeTasks(
|
||||
params = { type: 1, page: 1, limit: 100 }
|
||||
params = { type: 1, page: 1, limit: 100 },
|
||||
): Promise<LikeTask[]> {
|
||||
return request("/v1/workbench/list", params, "GET");
|
||||
}
|
||||
@@ -49,7 +49,7 @@ export function fetchLikeRecords(
|
||||
workbenchId: string,
|
||||
page: number = 1,
|
||||
limit: number = 20,
|
||||
keyword?: string
|
||||
keyword?: string,
|
||||
): Promise<PaginatedResponse<LikeRecord>> {
|
||||
const params: any = {
|
||||
workbenchId,
|
||||
|
||||
@@ -40,12 +40,12 @@ export async function deleteGroupPushTask(id: string): Promise<ApiResponse> {
|
||||
|
||||
export async function toggleGroupPushTask(
|
||||
id: string,
|
||||
status: string
|
||||
status: string,
|
||||
): Promise<ApiResponse> {
|
||||
return request(
|
||||
`/v1/workspace/group-push/tasks/${id}/toggle`,
|
||||
{ status },
|
||||
"POST"
|
||||
"POST",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,20 +54,20 @@ export async function copyGroupPushTask(id: string): Promise<ApiResponse> {
|
||||
}
|
||||
|
||||
export async function createGroupPushTask(
|
||||
taskData: Partial<GroupPushTask>
|
||||
taskData: Partial<GroupPushTask>,
|
||||
): Promise<ApiResponse> {
|
||||
return request("/v1/workspace/group-push/tasks", taskData, "POST");
|
||||
}
|
||||
|
||||
export async function updateGroupPushTask(
|
||||
id: string,
|
||||
taskData: Partial<GroupPushTask>
|
||||
taskData: Partial<GroupPushTask>,
|
||||
): Promise<ApiResponse> {
|
||||
return request(`/v1/workspace/group-push/tasks/${id}`, taskData, "PUT");
|
||||
}
|
||||
|
||||
export async function getGroupPushTaskDetail(
|
||||
id: string
|
||||
id: string,
|
||||
): Promise<GroupPushTask> {
|
||||
return request(`/v1/workspace/group-push/tasks/${id}`);
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ const Detail: React.FC = () => {
|
||||
</div>
|
||||
<Progress
|
||||
percent={Math.round(
|
||||
(task.pushCount / task.maxPushPerDay) * 100
|
||||
(task.pushCount / task.maxPushPerDay) * 100,
|
||||
)}
|
||||
size="small"
|
||||
/>
|
||||
|
||||
@@ -105,7 +105,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
||||
onChange={e =>
|
||||
handleChange(
|
||||
"dailyPushCount",
|
||||
Number.parseInt(e.target.value) || 1
|
||||
Number.parseInt(e.target.value) || 1,
|
||||
)
|
||||
}
|
||||
style={{ width: 80, textAlign: "center" }}
|
||||
|
||||
@@ -81,7 +81,7 @@ const ContentSelector: React.FC<ContentSelectorProps> = ({
|
||||
const [libraries] = useState<ContentLibrary[]>(mockLibraries);
|
||||
|
||||
const filteredLibraries = libraries.filter(library =>
|
||||
library.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
library.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
);
|
||||
|
||||
const handleLibraryToggle = (library: ContentLibrary, checked: boolean) => {
|
||||
|
||||
@@ -101,7 +101,9 @@ const GroupSelector: React.FC<GroupSelectorProps> = ({
|
||||
const filteredGroups = groups.filter(
|
||||
group =>
|
||||
group.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
group.serviceAccount.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
group.serviceAccount.name
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase()),
|
||||
);
|
||||
|
||||
const handleGroupToggle = (group: WechatGroup, checked: boolean) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import request from "@/api/request";
|
||||
export async function createGroupPushTask(
|
||||
taskData: Partial<GroupPushTask>
|
||||
taskData: Partial<GroupPushTask>,
|
||||
): Promise<ApiResponse> {
|
||||
return request("/v1/workspace/group-push/tasks", taskData, "POST");
|
||||
}
|
||||
|
||||
@@ -40,12 +40,12 @@ export async function deleteGroupPushTask(id: string): Promise<ApiResponse> {
|
||||
|
||||
export async function toggleGroupPushTask(
|
||||
id: string,
|
||||
status: string
|
||||
status: string,
|
||||
): Promise<ApiResponse> {
|
||||
return request(
|
||||
`/v1/workspace/group-push/tasks/${id}/toggle`,
|
||||
{ status },
|
||||
"POST"
|
||||
"POST",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,20 +54,20 @@ export async function copyGroupPushTask(id: string): Promise<ApiResponse> {
|
||||
}
|
||||
|
||||
export async function createGroupPushTask(
|
||||
taskData: Partial<GroupPushTask>
|
||||
taskData: Partial<GroupPushTask>,
|
||||
): Promise<ApiResponse> {
|
||||
return request("/v1/workspace/group-push/tasks", taskData, "POST");
|
||||
}
|
||||
|
||||
export async function updateGroupPushTask(
|
||||
id: string,
|
||||
taskData: Partial<GroupPushTask>
|
||||
taskData: Partial<GroupPushTask>,
|
||||
): Promise<ApiResponse> {
|
||||
return request(`/v1/workspace/group-push/tasks/${id}`, taskData, "PUT");
|
||||
}
|
||||
|
||||
export async function getGroupPushTaskDetail(
|
||||
id: string
|
||||
id: string,
|
||||
): Promise<GroupPushTask> {
|
||||
return request(`/v1/workspace/group-push/tasks/${id}`);
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ const GroupPush: React.FC = () => {
|
||||
};
|
||||
|
||||
const filteredTasks = tasks.filter(task =>
|
||||
task.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
task.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
);
|
||||
|
||||
const getStatusColor = (status: number) => {
|
||||
@@ -361,7 +361,7 @@ const GroupPush: React.FC = () => {
|
||||
</div>
|
||||
<Progress
|
||||
percent={Math.round(
|
||||
(task.pushCount / task.maxPushPerDay) * 100
|
||||
(task.pushCount / task.maxPushPerDay) * 100,
|
||||
)}
|
||||
size="small"
|
||||
/>
|
||||
|
||||
@@ -73,17 +73,6 @@ const Workspace: React.FC = () => {
|
||||
path: "/workspace/traffic-distribution",
|
||||
bgColor: "#e6f7ff",
|
||||
},
|
||||
{
|
||||
id: "ai-assistant",
|
||||
name: "AI对话助手",
|
||||
description: "智能回复,提高互动质量",
|
||||
icon: (
|
||||
<MessageOutlined className={styles.icon} style={{ color: "#1890ff" }} />
|
||||
),
|
||||
path: "/workspace/ai-assistant",
|
||||
bgColor: "#e6f7ff",
|
||||
isNew: true,
|
||||
},
|
||||
];
|
||||
|
||||
// AI智能助手
|
||||
@@ -176,7 +165,7 @@ const Workspace: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* AI智能助手 */}
|
||||
<div className={styles.section}>
|
||||
{/* <div className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>AI 智能助手</h2>
|
||||
<div className={styles.featuresGrid}>
|
||||
{aiFeatures.map(feature => (
|
||||
@@ -205,7 +194,7 @@ const Workspace: React.FC = () => {
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@@ -73,7 +73,7 @@ const MomentsSyncDetail: React.FC = () => {
|
||||
await request(
|
||||
"/v1/workbench/update-status",
|
||||
{ id, status: newStatus },
|
||||
"POST"
|
||||
"POST",
|
||||
);
|
||||
setTask({ ...task, status: newStatus });
|
||||
message.success(newStatus === 1 ? "任务已开启" : "任务已暂停");
|
||||
|
||||
@@ -55,7 +55,7 @@ const MomentsSync: React.FC = () => {
|
||||
const res = await request(
|
||||
"/v1/workbench/list",
|
||||
{ type: 2, page: 1, limit: 100 },
|
||||
"GET"
|
||||
"GET",
|
||||
);
|
||||
setTasks(res.list || []);
|
||||
} catch (e) {
|
||||
@@ -96,10 +96,10 @@ const MomentsSync: React.FC = () => {
|
||||
await request(
|
||||
"/v1/workbench/update-status",
|
||||
{ id, status: newStatus },
|
||||
"POST"
|
||||
"POST",
|
||||
);
|
||||
setTasks(prev =>
|
||||
prev.map(t => (t.id === id ? { ...t, status: newStatus } : t))
|
||||
prev.map(t => (t.id === id ? { ...t, status: newStatus } : t)),
|
||||
);
|
||||
message.success("操作成功");
|
||||
} catch {
|
||||
@@ -108,7 +108,7 @@ const MomentsSync: React.FC = () => {
|
||||
};
|
||||
|
||||
const filteredTasks = tasks.filter(task =>
|
||||
task.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
task.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
);
|
||||
|
||||
// 菜单
|
||||
|
||||
@@ -8,14 +8,14 @@ export const getTrafficDistributionDetail = (id: string) => {
|
||||
|
||||
// 更新流量分发
|
||||
export const updateTrafficDistribution = (
|
||||
data: TrafficDistributionFormData
|
||||
data: TrafficDistributionFormData,
|
||||
) => {
|
||||
return request("/v1/workbench/update", data, "POST");
|
||||
};
|
||||
|
||||
// 创建流量分发
|
||||
export const createTrafficDistribution = (
|
||||
data: TrafficDistributionFormData
|
||||
data: TrafficDistributionFormData,
|
||||
) => {
|
||||
return request("/v1/workbench/create", data, "POST");
|
||||
};
|
||||
|
||||
@@ -393,7 +393,7 @@ const TrafficDistributionForm: React.FC = () => {
|
||||
setSelectedPools(val =>
|
||||
e.target.checked
|
||||
? [...val, pool.id]
|
||||
: val.filter(v => v !== pool.id)
|
||||
: val.filter(v => v !== pool.id),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -17,7 +17,7 @@ export function updateDistributionRule(data: any): Promise<any> {
|
||||
// 暂停/启用计划
|
||||
export function toggleDistributionRuleStatus(
|
||||
id: number,
|
||||
status: 0 | 1
|
||||
status: 0 | 1,
|
||||
): Promise<any> {
|
||||
return request("/v1/workbench/update-status", { id, status }, "POST");
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ const TrafficDistributionList: React.FC = () => {
|
||||
// 新增:Switch点击切换计划状态
|
||||
const handleSwitchChange = async (
|
||||
checked: boolean,
|
||||
item: DistributionRule
|
||||
item: DistributionRule,
|
||||
) => {
|
||||
setMenuLoadingId(item.id);
|
||||
try {
|
||||
@@ -124,8 +124,8 @@ const TrafficDistributionList: React.FC = () => {
|
||||
// 本地只更新当前item的status,不刷新全列表
|
||||
setList(prevList =>
|
||||
prevList.map(rule =>
|
||||
rule.id === item.id ? { ...rule, status: checked ? 1 : 0 } : rule
|
||||
)
|
||||
rule.id === item.id ? { ...rule, status: checked ? 1 : 0 } : rule,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
message.error("操作失败");
|
||||
|
||||
@@ -161,7 +161,7 @@ export const getRouteTitle = (path: string): string => {
|
||||
// 检查路由权限
|
||||
export const checkRoutePermission = (
|
||||
path: string,
|
||||
userRole: string = "user"
|
||||
userRole: string = "user",
|
||||
): boolean => {
|
||||
const allowedRoutes =
|
||||
routePermissions[userRole as keyof typeof routePermissions] || [];
|
||||
|
||||
@@ -16,7 +16,7 @@ Object.values(modules).forEach((mod: any) => {
|
||||
|
||||
// 权限包装
|
||||
function wrapWithPermission(
|
||||
route: RouteObject & { auth?: boolean; requiredRole?: string }
|
||||
route: RouteObject & { auth?: boolean; requiredRole?: string },
|
||||
) {
|
||||
if (route.auth) {
|
||||
return {
|
||||
|
||||
@@ -29,7 +29,7 @@ const routes = [
|
||||
auth: true,
|
||||
},
|
||||
{
|
||||
path: "/traffic-pool/detail/:id",
|
||||
path: "/traffic-pool/detail/:wxid/:userId",
|
||||
element: <TrafficPoolDetail />,
|
||||
auth: true,
|
||||
},
|
||||
|
||||
@@ -5,12 +5,12 @@ import { persist, PersistOptions } from "zustand/middleware";
|
||||
export function createPersistStore<T>(
|
||||
createState: (set: any, get: any) => T,
|
||||
name: string,
|
||||
partialize?: (state: T) => Partial<T>
|
||||
partialize?: (state: T) => Partial<T>,
|
||||
) {
|
||||
return create<T>()(
|
||||
persist(createState, {
|
||||
name,
|
||||
partialize,
|
||||
} as PersistOptions<T>)
|
||||
} as PersistOptions<T>),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,5 +70,5 @@ export const useUserStore = createPersistStore<UserState>(
|
||||
user: state.user,
|
||||
token: state.token,
|
||||
isLoggedIn: state.isLoggedIn,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ export const comfirm = (
|
||||
title?: string;
|
||||
cancelText?: string;
|
||||
confirmText?: string;
|
||||
}
|
||||
},
|
||||
): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
Modal.show({
|
||||
|
||||
Reference in New Issue
Block a user