Merge branch 'yongpxu-dev' into yongpxu-dev2
# Conflicts: # nkebao/src/pages/mobile/mine/devices/DeviceDetail.tsx resolved by yongpxu-dev2 version
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
# 基础环境变量示例
|
# 基础环境变量示例
|
||||||
# VITE_API_BASE_URL=http://www.yishi.com
|
VITE_API_BASE_URL=http://www.yishi.com
|
||||||
VITE_API_BASE_URL=https://ckbapi.quwanzhi.com
|
# VITE_API_BASE_URL=https://ckbapi.quwanzhi.com
|
||||||
VITE_APP_TITLE=Nkebao Base
|
VITE_APP_TITLE=Nkebao Base
|
||||||
|
|||||||
BIN
nkebao/public/logo.png
Normal file
BIN
nkebao/public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 488 KiB |
@@ -7,7 +7,7 @@ import request from "./request";
|
|||||||
*/
|
*/
|
||||||
export async function uploadFile(
|
export async function uploadFile(
|
||||||
file: File,
|
file: File,
|
||||||
uploadUrl: string = "/v1/attachment/upload"
|
uploadUrl: string = "/v1/attachment/upload",
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
try {
|
try {
|
||||||
// 创建 FormData 对象用于文件上传
|
// 创建 FormData 对象用于文件上传
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ instance.interceptors.response.use(
|
|||||||
err => {
|
err => {
|
||||||
Toast.show({ content: err.message || "网络异常", position: "top" });
|
Toast.show({ content: err.message || "网络异常", position: "top" });
|
||||||
return Promise.reject(err);
|
return Promise.reject(err);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export function request(
|
export function request(
|
||||||
@@ -55,7 +55,7 @@ export function request(
|
|||||||
data?: any,
|
data?: any,
|
||||||
method: Method = "GET",
|
method: Method = "GET",
|
||||||
config?: AxiosRequestConfig,
|
config?: AxiosRequestConfig,
|
||||||
debounceGap?: number
|
debounceGap?: number,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const gap =
|
const gap =
|
||||||
typeof debounceGap === "number" ? debounceGap : DEFAULT_DEBOUNCE_GAP;
|
typeof debounceGap === "number" ? debounceGap : DEFAULT_DEBOUNCE_GAP;
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export default function AccountSelection({
|
|||||||
acc =>
|
acc =>
|
||||||
acc.userName.includes(searchQuery) ||
|
acc.userName.includes(searchQuery) ||
|
||||||
acc.realName.includes(searchQuery) ||
|
acc.realName.includes(searchQuery) ||
|
||||||
acc.departmentName.includes(searchQuery)
|
acc.departmentName.includes(searchQuery),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 处理账号选择
|
// 处理账号选择
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ interface ContentLibrarySelectionProps {
|
|||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
onConfirm?: (
|
onConfirm?: (
|
||||||
selectedIds: string[],
|
selectedIds: string[],
|
||||||
selectedItems: ContentLibraryItem[]
|
selectedItems: ContentLibraryItem[],
|
||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ export default function ContentLibrarySelection({
|
|||||||
|
|
||||||
// 获取已选内容库详细信息
|
// 获取已选内容库详细信息
|
||||||
const selectedLibraryObjs = libraries.filter(item =>
|
const selectedLibraryObjs = libraries.filter(item =>
|
||||||
selectedLibraries.includes(item.id)
|
selectedLibraries.includes(item.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 删除已选内容库
|
// 删除已选内容库
|
||||||
@@ -161,7 +161,7 @@ export default function ContentLibrarySelection({
|
|||||||
onSelect(newSelected);
|
onSelect(newSelected);
|
||||||
if (onSelectDetail) {
|
if (onSelectDetail) {
|
||||||
const selectedObjs = libraries.filter(item =>
|
const selectedObjs = libraries.filter(item =>
|
||||||
newSelected.includes(item.id)
|
newSelected.includes(item.id),
|
||||||
);
|
);
|
||||||
onSelectDetail(selectedObjs);
|
onSelectDetail(selectedObjs);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
|
|||||||
wxid: d.wechatId || "",
|
wxid: d.wechatId || "",
|
||||||
nickname: d.nickname || "",
|
nickname: d.nickname || "",
|
||||||
usedInPlans: d.usedInPlans || 0,
|
usedInPlans: d.usedInPlans || 0,
|
||||||
}))
|
})),
|
||||||
);
|
);
|
||||||
setTotal(res.total || 0);
|
setTotal(res.total || 0);
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 打开弹窗时获取第一页
|
// 打开弹窗时获取第一页
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ export default function FriendSelection({
|
|||||||
// 如果有 onSelectDetail 回调,传递完整的好友对象
|
// 如果有 onSelectDetail 回调,传递完整的好友对象
|
||||||
if (onSelectDetail) {
|
if (onSelectDetail) {
|
||||||
const selectedFriendObjs = friends.filter(friend =>
|
const selectedFriendObjs = friends.filter(friend =>
|
||||||
newSelectedFriends.includes(friend.id)
|
newSelectedFriends.includes(friend.id),
|
||||||
);
|
);
|
||||||
onSelectDetail(selectedFriendObjs);
|
onSelectDetail(selectedFriendObjs);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export default function GroupSelection({
|
|||||||
|
|
||||||
// 获取已选群聊详细信息
|
// 获取已选群聊详细信息
|
||||||
const selectedGroupObjs = groups.filter(group =>
|
const selectedGroupObjs = groups.filter(group =>
|
||||||
selectedGroups.includes(group.id)
|
selectedGroups.includes(group.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 删除已选群聊
|
// 删除已选群聊
|
||||||
@@ -141,7 +141,7 @@ export default function GroupSelection({
|
|||||||
// 如果有 onSelectDetail 回调,传递完整的群聊对象
|
// 如果有 onSelectDetail 回调,传递完整的群聊对象
|
||||||
if (onSelectDetail) {
|
if (onSelectDetail) {
|
||||||
const selectedGroupObjs = groups.filter(group =>
|
const selectedGroupObjs = groups.filter(group =>
|
||||||
newSelectedGroups.includes(group.id)
|
newSelectedGroups.includes(group.id),
|
||||||
);
|
);
|
||||||
onSelectDetail(selectedGroupObjs);
|
onSelectDetail(selectedGroupObjs);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const Layout: React.FC<LayoutProps> = ({
|
|||||||
const setRealHeight = () => {
|
const setRealHeight = () => {
|
||||||
document.documentElement.style.setProperty(
|
document.documentElement.style.setProperty(
|
||||||
"--real-vh",
|
"--real-vh",
|
||||||
`${window.innerHeight * 0.01}px`
|
`${window.innerHeight * 0.01}px`,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
setRealHeight();
|
setRealHeight();
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ const UploadComponent: React.FC<UploadComponentProps> = ({
|
|||||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
},
|
},
|
||||||
body: formData,
|
body: formData,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
13
nkebao/src/pages/guide/api.ts
Normal file
13
nkebao/src/pages/guide/api.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import request from "@/api/request";
|
||||||
|
|
||||||
|
// 获取设备二维码
|
||||||
|
export const fetchDeviceQRCode = (accountId: string) =>
|
||||||
|
request("/v1/api/device/add", { accountId }, "POST");
|
||||||
|
|
||||||
|
// 通过IMEI添加设备
|
||||||
|
export const addDeviceByImei = (imei: string, name: string) =>
|
||||||
|
request("/v1/api/device/add-by-imei", { imei, name }, "POST");
|
||||||
|
|
||||||
|
// 获取设备列表
|
||||||
|
export const fetchDeviceList = (params: { accountId?: string }) =>
|
||||||
|
request("/v1/devices/add-results", params, "GET");
|
||||||
341
nkebao/src/pages/guide/index.module.scss
Normal file
341
nkebao/src/pages/guide/index.module.scss
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
.guideContainer {
|
||||||
|
height: 100vh;
|
||||||
|
background: var(--primary-color);
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="25" cy="25" r="1" fill="rgba(255,255,255,0.1)"/><circle cx="75" cy="75" r="1" fill="rgba(255,255,255,0.1)"/><circle cx="50" cy="10" r="0.5" fill="rgba(255,255,255,0.1)"/><circle cx="10" cy="60" r="0.5" fill="rgba(255,255,255,0.1)"/><circle cx="90" cy="40" r="0.5" fill="rgba(255,255,255,0.1)"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
|
||||||
|
opacity: 0.3;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadingContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100vh;
|
||||||
|
background: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadingText {
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-top: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconContainer {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 0 auto 12px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: white;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
max-width: 280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deviceStatus {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusCard {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusIcon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: var(--primary-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 12px;
|
||||||
|
color: white;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusInfo {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusTitle {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusValue {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deviceCount {
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guideSteps {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stepsTitle {
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stepList {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stepItem {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stepNumber {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background: var(--primary-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stepContent {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stepTitle {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stepDesc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tipsTitle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tipsIcon {
|
||||||
|
color: #ff6b6b;
|
||||||
|
margin-right: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tipsContent {
|
||||||
|
p {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: 16px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primaryButton {
|
||||||
|
background: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
height: 44px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary-color);
|
||||||
|
box-shadow: 0 2px 12px rgba(255, 255, 255, 0.4);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
box-shadow: 0 1px 6px rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonIcon {
|
||||||
|
margin-left: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondaryButton {
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.8);
|
||||||
|
border-radius: 8px;
|
||||||
|
height: 44px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
background: transparent;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.guideContainer {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusCard {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stepItem {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动画效果
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header,
|
||||||
|
.deviceStatus,
|
||||||
|
.guideSteps,
|
||||||
|
.tips {
|
||||||
|
animation: fadeInUp 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guideSteps {
|
||||||
|
animation-delay: 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
animation: fadeInUp 0.5s ease-out 0.3s both;
|
||||||
|
}
|
||||||
349
nkebao/src/pages/guide/index.tsx
Normal file
349
nkebao/src/pages/guide/index.tsx
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
import React, { useEffect, useState, useCallback, useRef } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { Button, Toast, Popup, Tabs, Input } from "antd-mobile";
|
||||||
|
import {
|
||||||
|
MobileOutlined,
|
||||||
|
ExclamationCircleOutlined,
|
||||||
|
ArrowRightOutlined,
|
||||||
|
QrcodeOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import Layout from "@/components/Layout/Layout";
|
||||||
|
import { fetchDeviceQRCode, addDeviceByImei, fetchDeviceList } from "./api";
|
||||||
|
import { useUserStore } from "@/store/module/user";
|
||||||
|
import styles from "./index.module.scss";
|
||||||
|
const Guide: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { user } = useUserStore();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [deviceCount, setDeviceCount] = useState(user?.deviceTotal || 0);
|
||||||
|
|
||||||
|
// 添加设备弹窗状态
|
||||||
|
const [addVisible, setAddVisible] = useState(false);
|
||||||
|
const [addTab, setAddTab] = useState("scan");
|
||||||
|
const [qrLoading, setQrLoading] = useState(false);
|
||||||
|
const [qrCode, setQrCode] = useState<string | null>(null);
|
||||||
|
const [imei, setImei] = useState("");
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [addLoading, setAddLoading] = useState(false);
|
||||||
|
|
||||||
|
// 轮询监听相关
|
||||||
|
const [isPolling, setIsPolling] = useState(false);
|
||||||
|
const pollingRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const initialDeviceCountRef = useRef(deviceCount);
|
||||||
|
|
||||||
|
// 检查设备绑定状态
|
||||||
|
const checkDeviceStatus = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
// 使用store中的设备数量
|
||||||
|
const deviceNum = user?.deviceTotal || 0;
|
||||||
|
setDeviceCount(deviceNum);
|
||||||
|
|
||||||
|
// 如果已有设备,直接跳转到首页
|
||||||
|
if (deviceNum > 0) {
|
||||||
|
navigate("/");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("检查设备状态失败:", error);
|
||||||
|
Toast.show({
|
||||||
|
content: "检查设备状态失败,请重试",
|
||||||
|
position: "top",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [user?.deviceTotal, navigate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkDeviceStatus();
|
||||||
|
}, [checkDeviceStatus]);
|
||||||
|
|
||||||
|
// 开始轮询监听设备状态
|
||||||
|
const startPolling = useCallback(() => {
|
||||||
|
if (isPolling) return;
|
||||||
|
|
||||||
|
setIsPolling(true);
|
||||||
|
initialDeviceCountRef.current = deviceCount;
|
||||||
|
|
||||||
|
const pollDeviceStatus = async () => {
|
||||||
|
try {
|
||||||
|
// 这里可以调用一个简单的设备数量接口来检查是否有新设备
|
||||||
|
// 或者使用其他方式检测设备状态变化
|
||||||
|
// 暂时使用store中的数量,实际项目中可能需要调用专门的接口
|
||||||
|
let currentDeviceCount = user?.deviceTotal || 0;
|
||||||
|
const res = await fetchDeviceList({ accountId: user?.s2_accountId });
|
||||||
|
if (res.added) {
|
||||||
|
currentDeviceCount = 1;
|
||||||
|
Toast.show({ content: "设备添加成功!", position: "top" });
|
||||||
|
setAddVisible(false);
|
||||||
|
setDeviceCount(currentDeviceCount);
|
||||||
|
setIsPolling(false);
|
||||||
|
if (pollingRef.current) {
|
||||||
|
clearInterval(pollingRef.current);
|
||||||
|
pollingRef.current = null;
|
||||||
|
}
|
||||||
|
// 可以选择跳转到首页或继续留在当前页面
|
||||||
|
navigate("/");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("轮询检查设备状态失败:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 每3秒检查一次设备状态
|
||||||
|
pollingRef.current = setInterval(pollDeviceStatus, 3000);
|
||||||
|
}, [isPolling, user?.s2_accountId]);
|
||||||
|
|
||||||
|
// 停止轮询
|
||||||
|
const stopPolling = useCallback(() => {
|
||||||
|
setIsPolling(false);
|
||||||
|
if (pollingRef.current) {
|
||||||
|
clearInterval(pollingRef.current);
|
||||||
|
pollingRef.current = null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 组件卸载时清理轮询
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (pollingRef.current) {
|
||||||
|
clearInterval(pollingRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 获取二维码
|
||||||
|
const handleGetQr = async () => {
|
||||||
|
setQrLoading(true);
|
||||||
|
setQrCode(null);
|
||||||
|
try {
|
||||||
|
const accountId = user?.s2_accountId;
|
||||||
|
if (!accountId) throw new Error("未获取到用户信息");
|
||||||
|
const res = await fetchDeviceQRCode(accountId);
|
||||||
|
setQrCode(res.qrCode);
|
||||||
|
// 获取二维码后开始轮询监听
|
||||||
|
startPolling();
|
||||||
|
} catch (e: any) {
|
||||||
|
Toast.show({ content: e.message || "获取二维码失败", position: "top" });
|
||||||
|
} finally {
|
||||||
|
setQrLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 跳转到设备管理页面
|
||||||
|
const handleGoToDevices = () => {
|
||||||
|
handleGetQr();
|
||||||
|
setAddVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 手动添加设备
|
||||||
|
const handleAddDevice = async () => {
|
||||||
|
if (!imei.trim() || !name.trim()) {
|
||||||
|
Toast.show({ content: "请填写完整信息", position: "top" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setAddLoading(true);
|
||||||
|
try {
|
||||||
|
await addDeviceByImei(imei, name);
|
||||||
|
Toast.show({ content: "添加成功", position: "top" });
|
||||||
|
setAddVisible(false);
|
||||||
|
setImei("");
|
||||||
|
setName("");
|
||||||
|
// 重新检查设备状态
|
||||||
|
await checkDeviceStatus();
|
||||||
|
} catch (e: any) {
|
||||||
|
Toast.show({ content: e.message || "添加失败", position: "top" });
|
||||||
|
} finally {
|
||||||
|
setAddLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 关闭弹窗时停止轮询
|
||||||
|
const handleClosePopup = () => {
|
||||||
|
setAddVisible(false);
|
||||||
|
stopPolling();
|
||||||
|
setQrCode(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Layout loading={true}>
|
||||||
|
<div className={styles.loadingContainer}>
|
||||||
|
<div className={styles.loadingText}>检查设备状态中...</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className={styles.guideContainer}>
|
||||||
|
{/* 头部区域 */}
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div className={styles.iconContainer}>
|
||||||
|
<img src="/logo.png" alt="存客宝" className={styles.logo} />
|
||||||
|
</div>
|
||||||
|
<h1 className={styles.title}>欢迎使用存客宝</h1>
|
||||||
|
<p className={styles.subtitle}>请先绑定设备以获得完整功能体验</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 内容区域 */}
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.deviceStatus}>
|
||||||
|
<div className={styles.statusCard}>
|
||||||
|
<div className={styles.statusIcon}>
|
||||||
|
<MobileOutlined />
|
||||||
|
</div>
|
||||||
|
<div className={styles.statusInfo}>
|
||||||
|
<div className={styles.statusTitle}>设备绑定状态</div>
|
||||||
|
<div className={styles.statusValue}>
|
||||||
|
已绑定:
|
||||||
|
<span className={styles.deviceCount}>{deviceCount}</span> 台
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.guideSteps}>
|
||||||
|
<h2 className={styles.stepsTitle}>绑定步骤</h2>
|
||||||
|
<div className={styles.stepList}>
|
||||||
|
<div className={styles.stepItem}>
|
||||||
|
<div className={styles.stepNumber}>1</div>
|
||||||
|
<div className={styles.stepContent}>
|
||||||
|
<div className={styles.stepTitle}>准备设备</div>
|
||||||
|
<div className={styles.stepDesc}>
|
||||||
|
确保手机已安装存客宝应用
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.stepItem}>
|
||||||
|
<div className={styles.stepNumber}>2</div>
|
||||||
|
<div className={styles.stepContent}>
|
||||||
|
<div className={styles.stepTitle}>扫描二维码</div>
|
||||||
|
<div className={styles.stepDesc}>在设备管理页面扫描绑定</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.stepItem}>
|
||||||
|
<div className={styles.stepNumber}>3</div>
|
||||||
|
<div className={styles.stepContent}>
|
||||||
|
<div className={styles.stepTitle}>开始使用</div>
|
||||||
|
<div className={styles.stepDesc}>
|
||||||
|
绑定成功后即可使用所有功能
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.tips}>
|
||||||
|
<div className={styles.tipsTitle}>
|
||||||
|
<ExclamationCircleOutlined className={styles.tipsIcon} />
|
||||||
|
温馨提示
|
||||||
|
</div>
|
||||||
|
<div className={styles.tipsContent}>
|
||||||
|
<p>• 绑定设备后可享受完整功能体验</p>
|
||||||
|
<p>• 每个账号最多可绑定10台设备</p>
|
||||||
|
<p>• 如需帮助请联系客服</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 底部按钮区域 */}
|
||||||
|
<div className={styles.footer}>
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
className={styles.primaryButton}
|
||||||
|
onClick={handleGoToDevices}
|
||||||
|
>
|
||||||
|
立即绑定设备
|
||||||
|
<ArrowRightOutlined className={styles.buttonIcon} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 添加设备弹窗 */}
|
||||||
|
<Popup
|
||||||
|
visible={addVisible}
|
||||||
|
onMaskClick={handleClosePopup}
|
||||||
|
bodyStyle={{
|
||||||
|
borderTopLeftRadius: 16,
|
||||||
|
borderTopRightRadius: 16,
|
||||||
|
minHeight: 320,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ padding: 20 }}>
|
||||||
|
<Tabs
|
||||||
|
activeKey={addTab}
|
||||||
|
onChange={setAddTab}
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
>
|
||||||
|
<Tabs.Tab title="扫码添加" key="scan" />
|
||||||
|
<Tabs.Tab title="手动添加" key="manual" />
|
||||||
|
</Tabs>
|
||||||
|
{addTab === "scan" && (
|
||||||
|
<div style={{ textAlign: "center", minHeight: 200 }}>
|
||||||
|
<Button color="primary" onClick={handleGetQr} loading={qrLoading}>
|
||||||
|
<QrcodeOutlined />
|
||||||
|
获取二维码
|
||||||
|
</Button>
|
||||||
|
{qrCode && (
|
||||||
|
<div style={{ marginTop: 16 }}>
|
||||||
|
<img
|
||||||
|
src={qrCode}
|
||||||
|
alt="二维码"
|
||||||
|
style={{
|
||||||
|
width: 180,
|
||||||
|
height: 180,
|
||||||
|
background: "#f5f5f5",
|
||||||
|
borderRadius: 8,
|
||||||
|
margin: "0 auto",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style={{ color: "#888", fontSize: 12, marginTop: 8 }}>
|
||||||
|
请用手机扫码添加设备
|
||||||
|
</div>
|
||||||
|
{isPolling && (
|
||||||
|
<div
|
||||||
|
style={{ color: "#1890ff", fontSize: 12, marginTop: 8 }}
|
||||||
|
>
|
||||||
|
正在监听设备添加状态...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{addTab === "manual" && (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
||||||
|
<Input
|
||||||
|
placeholder="设备名称"
|
||||||
|
value={name}
|
||||||
|
onChange={val => setName(val)}
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="设备IMEI"
|
||||||
|
value={imei}
|
||||||
|
onChange={val => setImei(val)}
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
onClick={handleAddDevice}
|
||||||
|
loading={addLoading}
|
||||||
|
>
|
||||||
|
添加
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Popup>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Guide;
|
||||||
@@ -11,6 +11,7 @@ export interface LoginResponse {
|
|||||||
data: {
|
data: {
|
||||||
token: string;
|
token: string;
|
||||||
token_expired: string;
|
token_expired: string;
|
||||||
|
deviceTotal: number; // 设备总数
|
||||||
member: {
|
member: {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { Form, Input, Button, Toast, Tabs, Checkbox } from "antd-mobile";
|
import { Form, Input, Button, Toast, Checkbox } from "antd-mobile";
|
||||||
import {
|
import {
|
||||||
EyeInvisibleOutline,
|
EyeInvisibleOutline,
|
||||||
EyeOutline,
|
EyeOutline,
|
||||||
@@ -95,22 +95,32 @@ const Login: React.FC = () => {
|
|||||||
}
|
}
|
||||||
console.log(response, "response");
|
console.log(response, "response");
|
||||||
|
|
||||||
|
// 获取设备总数
|
||||||
|
const deviceTotal = response.deviceTotal || 0;
|
||||||
|
console.log(deviceTotal, "deviceTotal");
|
||||||
|
|
||||||
// 更新状态管理(token会自动存储到localStorage,用户信息存储在状态管理中)
|
// 更新状态管理(token会自动存储到localStorage,用户信息存储在状态管理中)
|
||||||
login(response.token, response.member);
|
login(response.token, response.member, deviceTotal);
|
||||||
|
|
||||||
Toast.show({ content: "登录成功", position: "top" });
|
Toast.show({ content: "登录成功", position: "top" });
|
||||||
|
|
||||||
// 跳转到首页或重定向URL
|
// 根据设备数量判断跳转
|
||||||
const returnUrl = searchParams.get("returnUrl");
|
if (deviceTotal > 0) {
|
||||||
if (returnUrl) {
|
// 有设备,跳转到首页或重定向URL
|
||||||
const decodedUrl = decodeURIComponent(returnUrl);
|
const returnUrl = searchParams.get("returnUrl");
|
||||||
if (isLoginPage(decodedUrl)) {
|
if (returnUrl) {
|
||||||
navigate("/");
|
const decodedUrl = decodeURIComponent(returnUrl);
|
||||||
|
if (isLoginPage(decodedUrl)) {
|
||||||
|
navigate("/");
|
||||||
|
} else {
|
||||||
|
window.location.href = decodedUrl;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
window.location.href = decodedUrl;
|
navigate("/");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
navigate("/");
|
// 没有设备,跳转到引导页面
|
||||||
|
navigate("/guide");
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// 错误已在request中处理,这里不需要额外处理
|
// 错误已在request中处理,这里不需要额外处理
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ export function getContentLibraryDetail(id: string): Promise<any> {
|
|||||||
|
|
||||||
// 创建内容库
|
// 创建内容库
|
||||||
export function createContentLibrary(
|
export function createContentLibrary(
|
||||||
params: CreateContentLibraryParams
|
params: CreateContentLibraryParams,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return request("/v1/content/library/create", params, "POST");
|
return request("/v1/content/library/create", params, "POST");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新内容库
|
// 更新内容库
|
||||||
export function updateContentLibrary(
|
export function updateContentLibrary(
|
||||||
params: UpdateContentLibraryParams
|
params: UpdateContentLibraryParams,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const { id, ...data } = params;
|
const { id, ...data } = params;
|
||||||
return request(`/v1/content/library/update`, { id, ...data }, "POST");
|
return request(`/v1/content/library/update`, { id, ...data }, "POST");
|
||||||
|
|||||||
@@ -22,14 +22,14 @@ export function getContentLibraryDetail(id: string): Promise<any> {
|
|||||||
|
|
||||||
// 创建内容库
|
// 创建内容库
|
||||||
export function createContentLibrary(
|
export function createContentLibrary(
|
||||||
params: CreateContentLibraryParams
|
params: CreateContentLibraryParams,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return request("/v1/content/library/create", params, "POST");
|
return request("/v1/content/library/create", params, "POST");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新内容库
|
// 更新内容库
|
||||||
export function updateContentLibrary(
|
export function updateContentLibrary(
|
||||||
params: UpdateContentLibraryParams
|
params: UpdateContentLibraryParams,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const { id, ...data } = params;
|
const { id, ...data } = params;
|
||||||
return request(`/v1/content/library/update`, { id, ...data }, "POST");
|
return request(`/v1/content/library/update`, { id, ...data }, "POST");
|
||||||
@@ -43,7 +43,7 @@ export function deleteContentLibrary(id: string): Promise<any> {
|
|||||||
// 切换内容库状态
|
// 切换内容库状态
|
||||||
export function toggleContentLibraryStatus(
|
export function toggleContentLibraryStatus(
|
||||||
id: string,
|
id: string,
|
||||||
status: number
|
status: number,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return request("/v1/content/library/update-status", { id, status }, "POST");
|
return request("/v1/content/library/update-status", { id, status }, "POST");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ const ContentLibraryList: React.FC = () => {
|
|||||||
const filteredLibraries = libraries.filter(
|
const filteredLibraries = libraries.filter(
|
||||||
library =>
|
library =>
|
||||||
library.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
library.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
library.creatorName?.toLowerCase().includes(searchQuery.toLowerCase())
|
library.creatorName?.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -140,6 +140,10 @@ const Home: React.FC = () => {
|
|||||||
navigate("/wechat-accounts");
|
navigate("/wechat-accounts");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAliveWechatClick = () => {
|
||||||
|
navigate("/wechat-accounts?wechatStatus=1");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
header={
|
header={
|
||||||
@@ -170,7 +174,10 @@ const Home: React.FC = () => {
|
|||||||
<TeamOutlined style={{ fontSize: 20, color: "#3b82f6" }} />
|
<TeamOutlined style={{ fontSize: 20, color: "#3b82f6" }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={style["stat-card"]}>
|
<div
|
||||||
|
className={style["stat-card"]}
|
||||||
|
onClick={handleAliveWechatClick}
|
||||||
|
>
|
||||||
<div className={style["stat-label"]}>在线微信号</div>
|
<div className={style["stat-label"]}>在线微信号</div>
|
||||||
<div className={style["stat-value"]}>
|
<div className={style["stat-value"]}>
|
||||||
<span>{dashboard.aliveWechatNum || 35}</span>
|
<span>{dashboard.aliveWechatNum || 35}</span>
|
||||||
@@ -206,8 +213,8 @@ const Home: React.FC = () => {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigate(
|
navigate(
|
||||||
`/scenarios/list/${scenario.id}/${encodeURIComponent(
|
`/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;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import request from "./request";
|
import request from "@/api/request";
|
||||||
|
|
||||||
// 获取设备列表
|
// 获取设备列表
|
||||||
export const fetchDeviceList = (params: {
|
export const fetchDeviceList = (params: {
|
||||||
@@ -19,7 +19,7 @@ export const fetchDeviceRelatedAccounts = (id: string | number) =>
|
|||||||
export const fetchDeviceHandleLogs = (
|
export const fetchDeviceHandleLogs = (
|
||||||
id: string | number,
|
id: string | number,
|
||||||
page = 1,
|
page = 1,
|
||||||
limit = 10
|
limit = 10,
|
||||||
) => request(`/v1/devices/${id}/handle-logs`, { page, limit }, "GET");
|
) => request(`/v1/devices/${id}/handle-logs`, { page, limit }, "GET");
|
||||||
|
|
||||||
// 更新设备任务配置
|
// 更新设备任务配置
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useRef, useState, useCallback } from "react";
|
import React, { useEffect, useRef, useState, useCallback } from "react";
|
||||||
import { NavBar, Popup, Tabs, Toast, SpinLoading, Dialog } from "antd-mobile";
|
import { Popup, Tabs, Toast, SpinLoading } from "antd-mobile";
|
||||||
import { Button, Input, Pagination, Checkbox } from "antd";
|
import { Button, Input, Pagination, Checkbox } from "antd";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { AddOutline, DeleteOutline } from "antd-mobile-icons";
|
import { AddOutline, DeleteOutline } from "antd-mobile-icons";
|
||||||
@@ -7,18 +7,18 @@ import {
|
|||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
QrcodeOutlined,
|
QrcodeOutlined,
|
||||||
ArrowLeftOutlined,
|
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import Layout from "@/components/Layout/Layout";
|
import Layout from "@/components/Layout/Layout";
|
||||||
import MeauMobile from "@/components/MeauMobile/MeauMoible";
|
|
||||||
import {
|
import {
|
||||||
fetchDeviceList,
|
fetchDeviceList,
|
||||||
fetchDeviceQRCode,
|
fetchDeviceQRCode,
|
||||||
addDeviceByImei,
|
addDeviceByImei,
|
||||||
deleteDevice,
|
deleteDevice,
|
||||||
} from "@/api/devices";
|
} from "./api";
|
||||||
import type { Device } from "@/types/device";
|
import type { Device } from "@/types/device";
|
||||||
import { comfirm } from "@/utils/common";
|
import { comfirm } from "@/utils/common";
|
||||||
|
import { useUserStore } from "@/store/module/user";
|
||||||
|
import NavCommon from "@/components/NavCommon";
|
||||||
|
|
||||||
const Devices: React.FC = () => {
|
const Devices: React.FC = () => {
|
||||||
// 设备列表相关
|
// 设备列表相关
|
||||||
@@ -47,6 +47,7 @@ const Devices: React.FC = () => {
|
|||||||
const [delLoading, setDelLoading] = useState(false);
|
const [delLoading, setDelLoading] = useState(false);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { user } = useUserStore();
|
||||||
// 加载设备列表
|
// 加载设备列表
|
||||||
const loadDevices = useCallback(
|
const loadDevices = useCallback(
|
||||||
async (reset = false) => {
|
async (reset = false) => {
|
||||||
@@ -68,7 +69,7 @@ const Devices: React.FC = () => {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[loading, search, page]
|
[loading, search, page],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 首次加载和搜索
|
// 首次加载和搜索
|
||||||
@@ -86,7 +87,7 @@ const Devices: React.FC = () => {
|
|||||||
setPage(p => p + 1);
|
setPage(p => p + 1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ threshold: 0.5 }
|
{ threshold: 0.5 },
|
||||||
);
|
);
|
||||||
if (observerRef.current) observer.observe(observerRef.current);
|
if (observerRef.current) observer.observe(observerRef.current);
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
@@ -112,7 +113,7 @@ const Devices: React.FC = () => {
|
|||||||
setQrLoading(true);
|
setQrLoading(true);
|
||||||
setQrCode(null);
|
setQrCode(null);
|
||||||
try {
|
try {
|
||||||
const accountId = localStorage.getItem("s2_accountId") || "";
|
const accountId = user.s2_accountId;
|
||||||
if (!accountId) throw new Error("未获取到用户信息");
|
if (!accountId) throw new Error("未获取到用户信息");
|
||||||
const res = await fetchDeviceQRCode(accountId);
|
const res = await fetchDeviceQRCode(accountId);
|
||||||
setQrCode(res.qrCode);
|
setQrCode(res.qrCode);
|
||||||
@@ -123,6 +124,11 @@ const Devices: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addDevice = async () => {
|
||||||
|
await handleGetQr();
|
||||||
|
setAddVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
// 手动添加设备
|
// 手动添加设备
|
||||||
const handleAddDevice = async () => {
|
const handleAddDevice = async () => {
|
||||||
if (!imei.trim() || !name.trim()) {
|
if (!imei.trim() || !name.trim()) {
|
||||||
@@ -166,7 +172,7 @@ const Devices: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
await comfirm(
|
await comfirm(
|
||||||
`将删除${selected.length}个设备,删除后本设备配置的计划任务操作也将失效。确认删除?`,
|
`将删除${selected.length}个设备,删除后本设备配置的计划任务操作也将失效。确认删除?`,
|
||||||
{ title: "确认删除", confirmText: "确认删除", cancelText: "取消" }
|
{ title: "确认删除", confirmText: "确认删除", cancelText: "取消" },
|
||||||
);
|
);
|
||||||
handleDelete();
|
handleDelete();
|
||||||
} catch {
|
} catch {
|
||||||
@@ -189,33 +195,15 @@ const Devices: React.FC = () => {
|
|||||||
<Layout
|
<Layout
|
||||||
header={
|
header={
|
||||||
<>
|
<>
|
||||||
<NavBar
|
<NavCommon
|
||||||
back={null}
|
title="设备管理"
|
||||||
left={
|
|
||||||
<div className="nav-title">
|
|
||||||
<ArrowLeftOutlined
|
|
||||||
twoToneColor="#1677ff"
|
|
||||||
onClick={() => navigate(-1)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
style={{ background: "#fff" }}
|
|
||||||
right={
|
right={
|
||||||
<Button
|
<Button size="small" type="primary" onClick={() => addDevice()}>
|
||||||
size="small"
|
|
||||||
type="primary"
|
|
||||||
onClick={() => setAddVisible(true)}
|
|
||||||
>
|
|
||||||
<AddOutline />
|
<AddOutline />
|
||||||
添加设备
|
添加设备
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
/>
|
||||||
<span style={{ color: "var(--primary-color)", fontWeight: 600 }}>
|
|
||||||
设备管理
|
|
||||||
</span>
|
|
||||||
</NavBar>
|
|
||||||
|
|
||||||
<div style={{ padding: "12px 12px 0 12px", background: "#fff" }}>
|
<div style={{ padding: "12px 12px 0 12px", background: "#fff" }}>
|
||||||
{/* 搜索栏 */}
|
{/* 搜索栏 */}
|
||||||
<div style={{ display: "flex", gap: 8, marginBottom: 12 }}>
|
<div style={{ display: "flex", gap: 8, marginBottom: 12 }}>
|
||||||
@@ -301,7 +289,7 @@ const Devices: React.FC = () => {
|
|||||||
setSelected(prev =>
|
setSelected(prev =>
|
||||||
e.target.checked
|
e.target.checked
|
||||||
? [...prev, device.id!]
|
? [...prev, device.id!]
|
||||||
: prev.filter(id => id !== device.id)
|
: prev.filter(id => id !== device.id),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onClick={e => e.stopPropagation()}
|
onClick={e => e.stopPropagation()}
|
||||||
@@ -373,7 +361,7 @@ const Devices: React.FC = () => {
|
|||||||
{addTab === "scan" && (
|
{addTab === "scan" && (
|
||||||
<div style={{ textAlign: "center", minHeight: 200 }}>
|
<div style={{ textAlign: "center", minHeight: 200 }}>
|
||||||
<Button
|
<Button
|
||||||
type="error"
|
type="primary"
|
||||||
onClick={handleGetQr}
|
onClick={handleGetQr}
|
||||||
loading={qrLoading}
|
loading={qrLoading}
|
||||||
icon={<QrcodeOutlined />}
|
icon={<QrcodeOutlined />}
|
||||||
@@ -390,6 +378,7 @@ const Devices: React.FC = () => {
|
|||||||
height: 180,
|
height: 180,
|
||||||
background: "#f5f5f5",
|
background: "#f5f5f5",
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
|
margin: "0 auto",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div style={{ color: "#888", fontSize: 12, marginTop: 8 }}>
|
<div style={{ color: "#888", fontSize: 12, marginTop: 8 }}>
|
||||||
|
|||||||
@@ -106,7 +106,6 @@ const Mine: React.FC = () => {
|
|||||||
// 清除本地存储的用户信息
|
// 清除本地存储的用户信息
|
||||||
localStorage.removeItem("token");
|
localStorage.removeItem("token");
|
||||||
localStorage.removeItem("token_expired");
|
localStorage.removeItem("token_expired");
|
||||||
localStorage.removeItem("s2_accountId");
|
|
||||||
localStorage.removeItem("userInfo");
|
localStorage.removeItem("userInfo");
|
||||||
setShowLogoutDialog(false);
|
setShowLogoutDialog(false);
|
||||||
navigate("/login");
|
navigate("/login");
|
||||||
@@ -120,30 +119,6 @@ const Mine: React.FC = () => {
|
|||||||
navigate(path);
|
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) => (
|
const renderModuleIcon = (module: any) => (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,11 +1,52 @@
|
|||||||
.recharge-page {
|
.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 {
|
.balance-card {
|
||||||
margin: 16px;
|
margin-bottom: 16px;
|
||||||
background: #f6ffed;
|
background: #f6ffed;
|
||||||
border: 1px solid #b7eb8f;
|
border: 1px solid #b7eb8f;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@@ -43,24 +84,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.quick-card {
|
.quick-card {
|
||||||
margin: 16px;
|
margin-bottom: 16px;
|
||||||
.quick-list {
|
.quick-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.desc-card {
|
.desc-card {
|
||||||
margin: 16px;
|
margin: 16px 0px;
|
||||||
background: #fffbe6;
|
background: #fffbe6;
|
||||||
border: 1px solid #ffe58f;
|
border: 1px solid #ffe58f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.warn-card {
|
.warn-card {
|
||||||
margin: 16px;
|
margin: 16px 0;
|
||||||
background: #fff2e8;
|
background: #fff2e8;
|
||||||
border: 1px solid #ffbb96;
|
border: 1px solid #ffbb96;
|
||||||
}
|
}
|
||||||
@@ -125,3 +166,275 @@
|
|||||||
color: #faad14;
|
color: #faad14;
|
||||||
font-size: 14px;
|
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 React, { useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
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 { useUserStore } from "@/store/module/user";
|
||||||
import style from "./index.module.scss";
|
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 NavCommon from "@/components/NavCommon";
|
||||||
import Layout from "@/components/Layout/Layout";
|
import Layout from "@/components/Layout/Layout";
|
||||||
|
|
||||||
const quickAmounts = [50, 100, 200, 500, 1000];
|
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 Recharge: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user } = useUserStore();
|
const { user } = useUserStore();
|
||||||
@@ -16,6 +125,7 @@ const Recharge: React.FC = () => {
|
|||||||
const [balance, setBalance] = useState(0);
|
const [balance, setBalance] = useState(0);
|
||||||
const [selected, setSelected] = useState<number | null>(null);
|
const [selected, setSelected] = useState<number | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [activeTab, setActiveTab] = useState("account");
|
||||||
|
|
||||||
// 充值操作
|
// 充值操作
|
||||||
const handleRecharge = async () => {
|
const handleRecharge = async () => {
|
||||||
@@ -31,68 +141,228 @@ const Recharge: React.FC = () => {
|
|||||||
}, 1200);
|
}, 1200);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
// 渲染账户充值tab内容
|
||||||
<Layout header={<NavCommon title="账户充值" />}>
|
const renderAccountRecharge = () => (
|
||||||
<div className={style["recharge-page"]}>
|
<div className={style["tab-content"]}>
|
||||||
<Card className={style["balance-card"]}>
|
<Card className={style["balance-card"]}>
|
||||||
<div className={style["balance-content"]}>
|
<div className={style["balance-content"]}>
|
||||||
<WalletOutlined className={style["wallet-icon"]} />
|
<WalletOutlined className={style["wallet-icon"]} />
|
||||||
<div className={style["balance-info"]}>
|
<div className={style["balance-info"]}>
|
||||||
<div className={style["balance-label"]}>当前余额</div>
|
<div className={style["balance-label"]}>当前余额</div>
|
||||||
<div className={style["balance-amount"]}>
|
<div className={style["balance-amount"]}>
|
||||||
¥{balance.toFixed(2)}
|
¥{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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className={style["quick-card"]}>
|
)}
|
||||||
<div className={style["quick-title"]}>快捷充值</div>
|
</div>
|
||||||
<div className={style["quick-list"]}>
|
);
|
||||||
{quickAmounts.map(amt => (
|
|
||||||
<Button
|
// 渲染AI服务tab内容
|
||||||
key={amt}
|
const renderAiServices = () => (
|
||||||
color={selected === amt ? "primary" : "default"}
|
<div className={style["tab-content"]}>
|
||||||
className={
|
<div className={style["ai-header"]}>
|
||||||
selected === amt
|
<div className={style["ai-title"]}>
|
||||||
? style["quick-btn-active"]
|
<RobotOutlined className={style["ai-icon"]} />
|
||||||
: style["quick-btn"]
|
AI智能服务收费
|
||||||
}
|
</div>
|
||||||
onClick={() => setSelected(amt)}
|
<div className={style["ai-tag"]}>统一按次收费</div>
|
||||||
>
|
</div>
|
||||||
¥{amt}
|
<div className={style["ai-description"]}>
|
||||||
</Button>
|
三项核心AI服务,按使用次数收费,每次1元
|
||||||
))}
|
</div>
|
||||||
</div>
|
|
||||||
<Button
|
<div className={style["ai-services"]}>
|
||||||
block
|
{aiServices.map(service => (
|
||||||
color="primary"
|
<Card key={service.id} className={style["ai-service-card"]}>
|
||||||
size="large"
|
<div className={style["service-header"]}>
|
||||||
className={style["recharge-main-btn"]}
|
<div className={style["service-info"]}>
|
||||||
loading={loading}
|
<div className={style["service-icon"]}>{service.icon}</div>
|
||||||
onClick={handleRecharge}
|
<div className={style["service-details"]}>
|
||||||
>
|
<div className={style["service-name"]}>{service.name}</div>
|
||||||
立即充值
|
<div className={style["service-price"]}>
|
||||||
</Button>
|
¥{service.price}/次
|
||||||
</Card>
|
</div>
|
||||||
<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>
|
</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>
|
</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>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
} from "./data";
|
} from "./data";
|
||||||
|
|
||||||
export function getTrafficPoolDetail(
|
export function getTrafficPoolDetail(
|
||||||
wechatId: string
|
wechatId: string,
|
||||||
): Promise<TrafficPoolUserDetail> {
|
): Promise<TrafficPoolUserDetail> {
|
||||||
return request("/v1/wechats/getWechatInfo", { wechatId }, "GET");
|
return request("/v1/wechats/getWechatInfo", { wechatId }, "GET");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -520,7 +520,7 @@ const TrafficPoolDetail: React.FC = () => {
|
|||||||
<span>{restriction.reason || "未知原因"}</span>
|
<span>{restriction.reason || "未知原因"}</span>
|
||||||
<Tag
|
<Tag
|
||||||
color={getRestrictionLevelColor(
|
color={getRestrictionLevelColor(
|
||||||
restriction.level
|
restriction.level,
|
||||||
)}
|
)}
|
||||||
fill="outline"
|
fill="outline"
|
||||||
className={styles.restrictionLevel}
|
className={styles.restrictionLevel}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import request from "@/api/request";
|
import request from "@/api/request";
|
||||||
import type { TrafficPoolListResponse, DeviceOption } from "./data";
|
import type { TrafficPoolListResponse, DeviceOption } from "./data";
|
||||||
import { fetchDeviceList } from "@/api/devices";
|
import { fetchDeviceList } from "@/pages/guide/api";
|
||||||
|
|
||||||
// 获取流量池列表
|
// 获取流量池列表
|
||||||
export function fetchTrafficPoolList(params: {
|
export function fetchTrafficPoolList(params: {
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export function useTrafficPoolListLogic() {
|
|||||||
// 单选
|
// 单选
|
||||||
const handleSelect = (id: number, checked: boolean) => {
|
const handleSelect = (id: number, checked: boolean) => {
|
||||||
setSelectedIds(prev =>
|
setSelectedIds(prev =>
|
||||||
checked ? [...prev, id] : prev.filter(i => i !== id)
|
checked ? [...prev, id] : prev.filter(i => i !== id),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export function getWechatFriends(params: {
|
|||||||
limit: params.limit,
|
limit: params.limit,
|
||||||
keyword: params.keyword,
|
keyword: params.keyword,
|
||||||
},
|
},
|
||||||
"GET"
|
"GET",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ const WechatAccountDetail: React.FC = () => {
|
|||||||
setIsFetchingFriends(false);
|
setIsFetchingFriends(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[id]
|
[id],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 搜索好友
|
// 搜索好友
|
||||||
@@ -153,7 +153,7 @@ const WechatAccountDetail: React.FC = () => {
|
|||||||
setFriendsPage(page);
|
setFriendsPage(page);
|
||||||
fetchFriendsList(page, searchQuery);
|
fetchFriendsList(page, searchQuery);
|
||||||
},
|
},
|
||||||
[searchQuery, fetchFriendsList]
|
[searchQuery, fetchFriendsList],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 初始化数据
|
// 初始化数据
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export function getWechatAccounts(params: {
|
|||||||
page: number;
|
page: number;
|
||||||
page_size: number;
|
page_size: number;
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
|
wechatStatus?: string;
|
||||||
}) {
|
}) {
|
||||||
return request("v1/wechats", params, "GET");
|
return request("v1/wechats", params, "GET");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react";
|
|||||||
import { Button, SpinLoading, Toast } from "antd-mobile";
|
import { Button, SpinLoading, Toast } from "antd-mobile";
|
||||||
import { Pagination, Input, Tooltip } from "antd";
|
import { Pagination, Input, Tooltip } from "antd";
|
||||||
import { SearchOutlined, ReloadOutlined } from "@ant-design/icons";
|
import { SearchOutlined, ReloadOutlined } from "@ant-design/icons";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import Layout from "@/components/Layout/Layout";
|
import Layout from "@/components/Layout/Layout";
|
||||||
import style from "./index.module.scss";
|
import style from "./index.module.scss";
|
||||||
import { getWechatAccounts } from "./api";
|
import { getWechatAccounts } from "./api";
|
||||||
@@ -26,6 +26,7 @@ const PAGE_SIZE = 10;
|
|||||||
|
|
||||||
const WechatAccounts: React.FC = () => {
|
const WechatAccounts: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
const [accounts, setAccounts] = useState<WechatAccount[]>([]);
|
const [accounts, setAccounts] = useState<WechatAccount[]>([]);
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
@@ -33,14 +34,24 @@ const WechatAccounts: React.FC = () => {
|
|||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
|
||||||
|
// 获取路由参数 wechatStatus
|
||||||
|
const wechatStatus = searchParams.get("wechatStatus");
|
||||||
|
|
||||||
const fetchAccounts = async (page = 1, keyword = "") => {
|
const fetchAccounts = async (page = 1, keyword = "") => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await getWechatAccounts({
|
const params: any = {
|
||||||
page,
|
page,
|
||||||
page_size: PAGE_SIZE,
|
page_size: PAGE_SIZE,
|
||||||
keyword,
|
keyword,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// 如果有 wechatStatus 参数,添加到请求参数中
|
||||||
|
if (wechatStatus) {
|
||||||
|
params.wechatStatus = wechatStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await getWechatAccounts(params);
|
||||||
if (res && res.list) {
|
if (res && res.list) {
|
||||||
setAccounts(res.list);
|
setAccounts(res.list);
|
||||||
setTotalAccounts(res.total || 0);
|
setTotalAccounts(res.total || 0);
|
||||||
@@ -87,7 +98,9 @@ const WechatAccounts: React.FC = () => {
|
|||||||
<Layout
|
<Layout
|
||||||
header={
|
header={
|
||||||
<>
|
<>
|
||||||
<NavCommon title="微信号管理" />
|
<NavCommon
|
||||||
|
title={wechatStatus === "1" ? "在线微信号" : "微信号管理"}
|
||||||
|
/>
|
||||||
<div className="search-bar">
|
<div className="search-bar">
|
||||||
<div className="search-input-wrapper">
|
<div className="search-input-wrapper">
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -46,9 +46,7 @@ const Scene: React.FC = () => {
|
|||||||
image:
|
image:
|
||||||
item.image ||
|
item.image ||
|
||||||
"https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-api.png",
|
"https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-api.png",
|
||||||
description:
|
description: "",
|
||||||
scenarioDescriptions[item.name?.toLowerCase()] ||
|
|
||||||
"通过该平台进行获客",
|
|
||||||
count: item.count,
|
count: item.count,
|
||||||
growth: item.growth,
|
growth: item.growth,
|
||||||
status: item.status,
|
status: item.status,
|
||||||
@@ -69,7 +67,7 @@ const Scene: React.FC = () => {
|
|||||||
|
|
||||||
const handleScenarioClick = (scenarioId: string, scenarioName: string) => {
|
const handleScenarioClick = (scenarioId: string, scenarioName: string) => {
|
||||||
navigate(
|
navigate(
|
||||||
`/scenarios/list/${scenarioId}/${encodeURIComponent(scenarioName)}`
|
`/scenarios/list/${scenarioId}/${encodeURIComponent(scenarioName)}`,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -144,11 +142,7 @@ const Scene: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={style["card-title"]}>{scenario.name}</div>
|
<div className={style["card-title"]}>{scenario.name}</div>
|
||||||
{scenario.description && (
|
|
||||||
<div className={style["card-desc"]}>
|
|
||||||
{scenario.description}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={style["card-stats"]}>
|
<div className={style["card-stats"]}>
|
||||||
<span className={style["card-count"]}>
|
<span className={style["card-count"]}>
|
||||||
今日: {scenario.count}
|
今日: {scenario.count}
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ const ScenarioList: React.FC = () => {
|
|||||||
if (response) {
|
if (response) {
|
||||||
// 处理webhook URL,使用工具函数构建完整地址
|
// 处理webhook URL,使用工具函数构建完整地址
|
||||||
const webhookUrl = buildApiUrl(
|
const webhookUrl = buildApiUrl(
|
||||||
response.textUrl?.fullUrl || `webhook/${taskId}`
|
response.textUrl?.fullUrl || `webhook/${taskId}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
setCurrentApiSettings({
|
setCurrentApiSettings({
|
||||||
@@ -286,7 +286,7 @@ const ScenarioList: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const filteredTasks = tasks.filter(task =>
|
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-icon"]}>{item.icon}</span>
|
||||||
<span className={style["action-text"]}>{item.text}</span>
|
<span className={style["action-text"]}>{item.text}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
),
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -317,7 +317,7 @@ HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.o
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleCopy(
|
handleCopy(
|
||||||
codeExamples[activeLanguage as keyof typeof codeExamples],
|
codeExamples[activeLanguage as keyof typeof codeExamples],
|
||||||
"代码"
|
"代码",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className={style["copy-code-btn"]}
|
className={style["copy-code-btn"]}
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ export default function NewPlan() {
|
|||||||
? error
|
? error
|
||||||
: isEdit
|
: isEdit
|
||||||
? "更新计划失败,请重试"
|
? "更新计划失败,请重试"
|
||||||
: "创建计划失败,请重试"
|
: "创建计划失败,请重试",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -105,10 +105,10 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
|||||||
const [accounts] = useState<Account[]>(generateRandomAccounts(50));
|
const [accounts] = useState<Account[]>(generateRandomAccounts(50));
|
||||||
const [materials] = useState<Material[]>(generatePosterMaterials());
|
const [materials] = useState<Material[]>(generatePosterMaterials());
|
||||||
const [selectedAccounts, setSelectedAccounts] = useState<Account[]>(
|
const [selectedAccounts, setSelectedAccounts] = useState<Account[]>(
|
||||||
formData.accounts?.length > 0 ? formData.accounts : []
|
formData.accounts?.length > 0 ? formData.accounts : [],
|
||||||
);
|
);
|
||||||
const [selectedMaterials, setSelectedMaterials] = useState<Material[]>(
|
const [selectedMaterials, setSelectedMaterials] = useState<Material[]>(
|
||||||
formData.materials?.length > 0 ? formData.materials : []
|
formData.materials?.length > 0 ? formData.materials : [],
|
||||||
);
|
);
|
||||||
// showAllScenarios 默认为 true
|
// showAllScenarios 默认为 true
|
||||||
const [showAllScenarios, setShowAllScenarios] = useState(true);
|
const [showAllScenarios, setShowAllScenarios] = useState(true);
|
||||||
@@ -128,7 +128,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
|||||||
const [customTags, setCustomTags] = useState(formData.customTags || []);
|
const [customTags, setCustomTags] = useState(formData.customTags || []);
|
||||||
const [tips, setTips] = useState(formData.tips || "");
|
const [tips, setTips] = useState(formData.tips || "");
|
||||||
const [selectedScenarioTags, setSelectedScenarioTags] = useState(
|
const [selectedScenarioTags, setSelectedScenarioTags] = useState(
|
||||||
formData.scenarioTags || []
|
formData.scenarioTags || [],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 电话获客相关状态
|
// 电话获客相关状态
|
||||||
@@ -140,10 +140,10 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
|||||||
|
|
||||||
// 群设置相关状态
|
// 群设置相关状态
|
||||||
const [weixinqunName, setWeixinqunName] = useState(
|
const [weixinqunName, setWeixinqunName] = useState(
|
||||||
formData.weixinqunName || ""
|
formData.weixinqunName || "",
|
||||||
);
|
);
|
||||||
const [weixinqunNotice, setWeixinqunNotice] = useState(
|
const [weixinqunNotice, setWeixinqunNotice] = useState(
|
||||||
formData.weixinqunNotice || ""
|
formData.weixinqunNotice || "",
|
||||||
);
|
);
|
||||||
|
|
||||||
// 新增:自定义海报相关状态
|
// 新增:自定义海报相关状态
|
||||||
@@ -232,7 +232,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
|||||||
onChange({ ...formData, customTags: updatedCustomTags });
|
onChange({ ...formData, customTags: updatedCustomTags });
|
||||||
// 同时从选中标签中移除
|
// 同时从选中标签中移除
|
||||||
const updatedSelectedTags = selectedScenarioTags.filter(
|
const updatedSelectedTags = selectedScenarioTags.filter(
|
||||||
(t: string) => t !== tagId
|
(t: string) => t !== tagId,
|
||||||
);
|
);
|
||||||
setSelectedScenarioTags(updatedSelectedTags);
|
setSelectedScenarioTags(updatedSelectedTags);
|
||||||
onChange({
|
onChange({
|
||||||
@@ -292,12 +292,12 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
|||||||
// 账号多选切换
|
// 账号多选切换
|
||||||
const handleAccountToggle = (account: Account) => {
|
const handleAccountToggle = (account: Account) => {
|
||||||
const isSelected = selectedAccounts.some(
|
const isSelected = selectedAccounts.some(
|
||||||
(a: Account) => a.id === account.id
|
(a: Account) => a.id === account.id,
|
||||||
);
|
);
|
||||||
let newSelected;
|
let newSelected;
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
newSelected = selectedAccounts.filter(
|
newSelected = selectedAccounts.filter(
|
||||||
(a: Account) => a.id !== account.id
|
(a: Account) => a.id !== account.id,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
newSelected = [...selectedAccounts, account];
|
newSelected = [...selectedAccounts, account];
|
||||||
@@ -362,7 +362,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
|||||||
const [orderUploaded, setOrderUploaded] = useState(false);
|
const [orderUploaded, setOrderUploaded] = useState(false);
|
||||||
|
|
||||||
const handleOrderFileUpload = async (
|
const handleOrderFileUpload = async (
|
||||||
event: React.ChangeEvent<HTMLInputElement>
|
event: React.ChangeEvent<HTMLInputElement>,
|
||||||
) => {
|
) => {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
@@ -518,7 +518,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
|||||||
<div className={styles["basic-materials-grid"]}>
|
<div className={styles["basic-materials-grid"]}>
|
||||||
{[...materials, ...customPosters].map(material => {
|
{[...materials, ...customPosters].map(material => {
|
||||||
const isSelected = selectedMaterials.some(
|
const isSelected = selectedMaterials.some(
|
||||||
m => m.id === material.id
|
m => m.id === material.id,
|
||||||
);
|
);
|
||||||
const isCustom = material.id.startsWith("custom-");
|
const isCustom = material.id.startsWith("custom-");
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const FriendRequestSettings: React.FC<FriendRequestSettingsProps> = ({
|
|||||||
const [isTemplateDialogOpen, setIsTemplateDialogOpen] = useState(false);
|
const [isTemplateDialogOpen, setIsTemplateDialogOpen] = useState(false);
|
||||||
const [hasWarnings, setHasWarnings] = useState(false);
|
const [hasWarnings, setHasWarnings] = useState(false);
|
||||||
const [selectedDevices, setSelectedDevices] = useState<string[]>(
|
const [selectedDevices, setSelectedDevices] = useState<string[]>(
|
||||||
formData.device || []
|
formData.device || [],
|
||||||
);
|
);
|
||||||
const [showRemarkTip, setShowRemarkTip] = useState(false);
|
const [showRemarkTip, setShowRemarkTip] = useState(false);
|
||||||
|
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
|
|||||||
const handleUpdateMessage = (
|
const handleUpdateMessage = (
|
||||||
dayIndex: number,
|
dayIndex: number,
|
||||||
messageIndex: number,
|
messageIndex: number,
|
||||||
updates: Partial<MessageContent>
|
updates: Partial<MessageContent>,
|
||||||
) => {
|
) => {
|
||||||
const updatedPlans = [...dayPlans];
|
const updatedPlans = [...dayPlans];
|
||||||
updatedPlans[dayIndex].messages[messageIndex] = {
|
updatedPlans[dayIndex].messages[messageIndex] = {
|
||||||
@@ -181,7 +181,7 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
|
|||||||
setSelectedGroupId(groupId);
|
setSelectedGroupId(groupId);
|
||||||
setIsGroupSelectOpen(false);
|
setIsGroupSelectOpen(false);
|
||||||
message.success(
|
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 = (
|
const triggerUpload = (
|
||||||
dayIdx: number,
|
dayIdx: number,
|
||||||
msgIdx: number,
|
msgIdx: number,
|
||||||
type: "miniprogram" | "link"
|
type: "miniprogram" | "link",
|
||||||
) => {
|
) => {
|
||||||
setUploadingDay(dayIdx);
|
setUploadingDay(dayIdx);
|
||||||
setUploadingMsgIdx(msgIdx);
|
setUploadingMsgIdx(msgIdx);
|
||||||
@@ -539,7 +539,7 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
|
|||||||
handleFileUpload(
|
handleFileUpload(
|
||||||
dayIndex,
|
dayIndex,
|
||||||
messageIndex,
|
messageIndex,
|
||||||
message.type as any
|
message.type as any,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export function deleteScenario(id: string) {
|
|||||||
export function getPlanList(
|
export function getPlanList(
|
||||||
scenarioId: string,
|
scenarioId: string,
|
||||||
page: number = 1,
|
page: number = 1,
|
||||||
limit: number = 20
|
limit: number = 20,
|
||||||
) {
|
) {
|
||||||
return request(`/api/scenarios/${scenarioId}/plans`, { page, limit }, "GET");
|
return request(`/api/scenarios/${scenarioId}/plans`, { page, limit }, "GET");
|
||||||
}
|
}
|
||||||
@@ -214,7 +214,7 @@ export function deleteAutoLikeTask(taskId: string) {
|
|||||||
return request(
|
return request(
|
||||||
`/api/workspace/auto-like/tasks/${taskId}`,
|
`/api/workspace/auto-like/tasks/${taskId}`,
|
||||||
undefined,
|
undefined,
|
||||||
"DELETE"
|
"DELETE",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,7 +240,7 @@ export function deleteGroupPushTask(taskId: string) {
|
|||||||
return request(
|
return request(
|
||||||
`/api/workspace/group-push/tasks/${taskId}`,
|
`/api/workspace/group-push/tasks/${taskId}`,
|
||||||
undefined,
|
undefined,
|
||||||
"DELETE"
|
"DELETE",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,7 +266,7 @@ export function deleteAutoGroupTask(taskId: string) {
|
|||||||
return request(
|
return request(
|
||||||
`/api/workspace/auto-group/tasks/${taskId}`,
|
`/api/workspace/auto-group/tasks/${taskId}`,
|
||||||
undefined,
|
undefined,
|
||||||
"DELETE"
|
"DELETE",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,7 +287,7 @@ export function getAIAnalysisReport() {
|
|||||||
return request(
|
return request(
|
||||||
"/api/workspace/ai-assistant/analysis-report",
|
"/api/workspace/ai-assistant/analysis-report",
|
||||||
undefined,
|
undefined,
|
||||||
"GET"
|
"GET",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,6 +379,6 @@ export function markNotificationAsRead(notificationId: string) {
|
|||||||
return request(
|
return request(
|
||||||
`/api/system/notifications/${notificationId}/read`,
|
`/api/system/notifications/${notificationId}/read`,
|
||||||
undefined,
|
undefined,
|
||||||
"PUT"
|
"PUT",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ const mockTaskDetail: GroupTaskDetail = {
|
|||||||
nickname: `用户${mIndex + 1}`,
|
nickname: `用户${mIndex + 1}`,
|
||||||
wechatId: `wx_${mIndex}`,
|
wechatId: `wx_${mIndex}`,
|
||||||
tags: [`标签${(mIndex % 3) + 1}`],
|
tags: [`标签${(mIndex % 3) + 1}`],
|
||||||
})
|
}),
|
||||||
),
|
),
|
||||||
})),
|
})),
|
||||||
createTime: "2024-11-20 19:04:14",
|
createTime: "2024-11-20 19:04:14",
|
||||||
@@ -169,10 +169,10 @@ const GroupCreationProgress: React.FC<{
|
|||||||
}> = ({ taskDetail, onComplete }) => {
|
}> = ({ taskDetail, onComplete }) => {
|
||||||
const [groups, setGroups] = useState<Group[]>(taskDetail.groups);
|
const [groups, setGroups] = useState<Group[]>(taskDetail.groups);
|
||||||
const [currentGroupIndex, setCurrentGroupIndex] = useState(
|
const [currentGroupIndex, setCurrentGroupIndex] = useState(
|
||||||
taskDetail.currentGroupIndex
|
taskDetail.currentGroupIndex,
|
||||||
);
|
);
|
||||||
const [status, setStatus] = useState<GroupTaskDetail["status"]>(
|
const [status, setStatus] = useState<GroupTaskDetail["status"]>(
|
||||||
taskDetail.status
|
taskDetail.status,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -207,7 +207,7 @@ const GroupCreationProgress: React.FC<{
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
return group;
|
return group;
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -149,8 +149,8 @@ const AutoGroupList: React.FC = () => {
|
|||||||
...task,
|
...task,
|
||||||
status: task.status === "running" ? "paused" : "running",
|
status: task.status === "running" ? "paused" : "running",
|
||||||
}
|
}
|
||||||
: task
|
: task,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
Toast.show({ content: "状态已切换" });
|
Toast.show({ content: "状态已切换" });
|
||||||
};
|
};
|
||||||
@@ -160,7 +160,7 @@ const AutoGroupList: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const filteredTasks = tasks.filter(task =>
|
const filteredTasks = tasks.filter(task =>
|
||||||
task.name.toLowerCase().includes(searchTerm.toLowerCase())
|
task.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
|
|
||||||
// 获取自动点赞任务列表
|
// 获取自动点赞任务列表
|
||||||
export function fetchAutoLikeTasks(
|
export function fetchAutoLikeTasks(
|
||||||
params = { type: 1, page: 1, limit: 100 }
|
params = { type: 1, page: 1, limit: 100 },
|
||||||
): Promise<LikeTask[]> {
|
): Promise<LikeTask[]> {
|
||||||
return request("/v1/workbench/list", params, "GET");
|
return request("/v1/workbench/list", params, "GET");
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,7 @@ export function fetchLikeRecords(
|
|||||||
workbenchId: string,
|
workbenchId: string,
|
||||||
page: number = 1,
|
page: number = 1,
|
||||||
limit: number = 20,
|
limit: number = 20,
|
||||||
keyword?: string
|
keyword?: string,
|
||||||
): Promise<PaginatedResponse<LikeRecord>> {
|
): Promise<PaginatedResponse<LikeRecord>> {
|
||||||
const params: any = {
|
const params: any = {
|
||||||
workbenchId,
|
workbenchId,
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ const AutoLike: React.FC = () => {
|
|||||||
|
|
||||||
// 过滤任务
|
// 过滤任务
|
||||||
const filteredTasks = tasks.filter(task =>
|
const filteredTasks = tasks.filter(task =>
|
||||||
task.name.toLowerCase().includes(searchTerm.toLowerCase())
|
task.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ const NewAutoLike: React.FC = () => {
|
|||||||
});
|
});
|
||||||
setAutoEnabled(
|
setAutoEnabled(
|
||||||
(taskDetail as any).status === 1 ||
|
(taskDetail as any).status === 1 ||
|
||||||
(taskDetail as any).status === "running"
|
(taskDetail as any).status === "running",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
|
|
||||||
// 获取自动点赞任务列表
|
// 获取自动点赞任务列表
|
||||||
export function fetchAutoLikeTasks(
|
export function fetchAutoLikeTasks(
|
||||||
params = { type: 1, page: 1, limit: 100 }
|
params = { type: 1, page: 1, limit: 100 },
|
||||||
): Promise<LikeTask[]> {
|
): Promise<LikeTask[]> {
|
||||||
return request("/v1/workbench/list", params, "GET");
|
return request("/v1/workbench/list", params, "GET");
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,7 @@ export function fetchLikeRecords(
|
|||||||
workbenchId: string,
|
workbenchId: string,
|
||||||
page: number = 1,
|
page: number = 1,
|
||||||
limit: number = 20,
|
limit: number = 20,
|
||||||
keyword?: string
|
keyword?: string,
|
||||||
): Promise<PaginatedResponse<LikeRecord>> {
|
): Promise<PaginatedResponse<LikeRecord>> {
|
||||||
const params: any = {
|
const params: any = {
|
||||||
workbenchId,
|
workbenchId,
|
||||||
|
|||||||
@@ -40,12 +40,12 @@ export async function deleteGroupPushTask(id: string): Promise<ApiResponse> {
|
|||||||
|
|
||||||
export async function toggleGroupPushTask(
|
export async function toggleGroupPushTask(
|
||||||
id: string,
|
id: string,
|
||||||
status: string
|
status: string,
|
||||||
): Promise<ApiResponse> {
|
): Promise<ApiResponse> {
|
||||||
return request(
|
return request(
|
||||||
`/v1/workspace/group-push/tasks/${id}/toggle`,
|
`/v1/workspace/group-push/tasks/${id}/toggle`,
|
||||||
{ status },
|
{ status },
|
||||||
"POST"
|
"POST",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,20 +54,20 @@ export async function copyGroupPushTask(id: string): Promise<ApiResponse> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createGroupPushTask(
|
export async function createGroupPushTask(
|
||||||
taskData: Partial<GroupPushTask>
|
taskData: Partial<GroupPushTask>,
|
||||||
): Promise<ApiResponse> {
|
): Promise<ApiResponse> {
|
||||||
return request("/v1/workspace/group-push/tasks", taskData, "POST");
|
return request("/v1/workspace/group-push/tasks", taskData, "POST");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateGroupPushTask(
|
export async function updateGroupPushTask(
|
||||||
id: string,
|
id: string,
|
||||||
taskData: Partial<GroupPushTask>
|
taskData: Partial<GroupPushTask>,
|
||||||
): Promise<ApiResponse> {
|
): Promise<ApiResponse> {
|
||||||
return request(`/v1/workspace/group-push/tasks/${id}`, taskData, "PUT");
|
return request(`/v1/workspace/group-push/tasks/${id}`, taskData, "PUT");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getGroupPushTaskDetail(
|
export async function getGroupPushTaskDetail(
|
||||||
id: string
|
id: string,
|
||||||
): Promise<GroupPushTask> {
|
): Promise<GroupPushTask> {
|
||||||
return request(`/v1/workspace/group-push/tasks/${id}`);
|
return request(`/v1/workspace/group-push/tasks/${id}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ const Detail: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<Progress
|
<Progress
|
||||||
percent={Math.round(
|
percent={Math.round(
|
||||||
(task.pushCount / task.maxPushPerDay) * 100
|
(task.pushCount / task.maxPushPerDay) * 100,
|
||||||
)}
|
)}
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
|
|||||||
onChange={e =>
|
onChange={e =>
|
||||||
handleChange(
|
handleChange(
|
||||||
"dailyPushCount",
|
"dailyPushCount",
|
||||||
Number.parseInt(e.target.value) || 1
|
Number.parseInt(e.target.value) || 1,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
style={{ width: 80, textAlign: "center" }}
|
style={{ width: 80, textAlign: "center" }}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ const ContentSelector: React.FC<ContentSelectorProps> = ({
|
|||||||
const [libraries] = useState<ContentLibrary[]>(mockLibraries);
|
const [libraries] = useState<ContentLibrary[]>(mockLibraries);
|
||||||
|
|
||||||
const filteredLibraries = libraries.filter(library =>
|
const filteredLibraries = libraries.filter(library =>
|
||||||
library.name.toLowerCase().includes(searchTerm.toLowerCase())
|
library.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleLibraryToggle = (library: ContentLibrary, checked: boolean) => {
|
const handleLibraryToggle = (library: ContentLibrary, checked: boolean) => {
|
||||||
|
|||||||
@@ -101,7 +101,9 @@ const GroupSelector: React.FC<GroupSelectorProps> = ({
|
|||||||
const filteredGroups = groups.filter(
|
const filteredGroups = groups.filter(
|
||||||
group =>
|
group =>
|
||||||
group.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
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) => {
|
const handleGroupToggle = (group: WechatGroup, checked: boolean) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import request from "@/api/request";
|
import request from "@/api/request";
|
||||||
export async function createGroupPushTask(
|
export async function createGroupPushTask(
|
||||||
taskData: Partial<GroupPushTask>
|
taskData: Partial<GroupPushTask>,
|
||||||
): Promise<ApiResponse> {
|
): Promise<ApiResponse> {
|
||||||
return request("/v1/workspace/group-push/tasks", taskData, "POST");
|
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(
|
export async function toggleGroupPushTask(
|
||||||
id: string,
|
id: string,
|
||||||
status: string
|
status: string,
|
||||||
): Promise<ApiResponse> {
|
): Promise<ApiResponse> {
|
||||||
return request(
|
return request(
|
||||||
`/v1/workspace/group-push/tasks/${id}/toggle`,
|
`/v1/workspace/group-push/tasks/${id}/toggle`,
|
||||||
{ status },
|
{ status },
|
||||||
"POST"
|
"POST",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,20 +54,20 @@ export async function copyGroupPushTask(id: string): Promise<ApiResponse> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createGroupPushTask(
|
export async function createGroupPushTask(
|
||||||
taskData: Partial<GroupPushTask>
|
taskData: Partial<GroupPushTask>,
|
||||||
): Promise<ApiResponse> {
|
): Promise<ApiResponse> {
|
||||||
return request("/v1/workspace/group-push/tasks", taskData, "POST");
|
return request("/v1/workspace/group-push/tasks", taskData, "POST");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateGroupPushTask(
|
export async function updateGroupPushTask(
|
||||||
id: string,
|
id: string,
|
||||||
taskData: Partial<GroupPushTask>
|
taskData: Partial<GroupPushTask>,
|
||||||
): Promise<ApiResponse> {
|
): Promise<ApiResponse> {
|
||||||
return request(`/v1/workspace/group-push/tasks/${id}`, taskData, "PUT");
|
return request(`/v1/workspace/group-push/tasks/${id}`, taskData, "PUT");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getGroupPushTaskDetail(
|
export async function getGroupPushTaskDetail(
|
||||||
id: string
|
id: string,
|
||||||
): Promise<GroupPushTask> {
|
): Promise<GroupPushTask> {
|
||||||
return request(`/v1/workspace/group-push/tasks/${id}`);
|
return request(`/v1/workspace/group-push/tasks/${id}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ const GroupPush: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const filteredTasks = tasks.filter(task =>
|
const filteredTasks = tasks.filter(task =>
|
||||||
task.name.toLowerCase().includes(searchTerm.toLowerCase())
|
task.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||||
);
|
);
|
||||||
|
|
||||||
const getStatusColor = (status: number) => {
|
const getStatusColor = (status: number) => {
|
||||||
@@ -361,7 +361,7 @@ const GroupPush: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<Progress
|
<Progress
|
||||||
percent={Math.round(
|
percent={Math.round(
|
||||||
(task.pushCount / task.maxPushPerDay) * 100
|
(task.pushCount / task.maxPushPerDay) * 100,
|
||||||
)}
|
)}
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ const MomentsSyncDetail: React.FC = () => {
|
|||||||
await request(
|
await request(
|
||||||
"/v1/workbench/update-status",
|
"/v1/workbench/update-status",
|
||||||
{ id, status: newStatus },
|
{ id, status: newStatus },
|
||||||
"POST"
|
"POST",
|
||||||
);
|
);
|
||||||
setTask({ ...task, status: newStatus });
|
setTask({ ...task, status: newStatus });
|
||||||
message.success(newStatus === 1 ? "任务已开启" : "任务已暂停");
|
message.success(newStatus === 1 ? "任务已开启" : "任务已暂停");
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ const MomentsSync: React.FC = () => {
|
|||||||
const res = await request(
|
const res = await request(
|
||||||
"/v1/workbench/list",
|
"/v1/workbench/list",
|
||||||
{ type: 2, page: 1, limit: 100 },
|
{ type: 2, page: 1, limit: 100 },
|
||||||
"GET"
|
"GET",
|
||||||
);
|
);
|
||||||
setTasks(res.list || []);
|
setTasks(res.list || []);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -96,10 +96,10 @@ const MomentsSync: React.FC = () => {
|
|||||||
await request(
|
await request(
|
||||||
"/v1/workbench/update-status",
|
"/v1/workbench/update-status",
|
||||||
{ id, status: newStatus },
|
{ id, status: newStatus },
|
||||||
"POST"
|
"POST",
|
||||||
);
|
);
|
||||||
setTasks(prev =>
|
setTasks(prev =>
|
||||||
prev.map(t => (t.id === id ? { ...t, status: newStatus } : t))
|
prev.map(t => (t.id === id ? { ...t, status: newStatus } : t)),
|
||||||
);
|
);
|
||||||
message.success("操作成功");
|
message.success("操作成功");
|
||||||
} catch {
|
} catch {
|
||||||
@@ -108,7 +108,7 @@ const MomentsSync: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const filteredTasks = tasks.filter(task =>
|
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 = (
|
export const updateTrafficDistribution = (
|
||||||
data: TrafficDistributionFormData
|
data: TrafficDistributionFormData,
|
||||||
) => {
|
) => {
|
||||||
return request("/v1/workbench/update", data, "POST");
|
return request("/v1/workbench/update", data, "POST");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建流量分发
|
// 创建流量分发
|
||||||
export const createTrafficDistribution = (
|
export const createTrafficDistribution = (
|
||||||
data: TrafficDistributionFormData
|
data: TrafficDistributionFormData,
|
||||||
) => {
|
) => {
|
||||||
return request("/v1/workbench/create", data, "POST");
|
return request("/v1/workbench/create", data, "POST");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -393,7 +393,7 @@ const TrafficDistributionForm: React.FC = () => {
|
|||||||
setSelectedPools(val =>
|
setSelectedPools(val =>
|
||||||
e.target.checked
|
e.target.checked
|
||||||
? [...val, pool.id]
|
? [...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(
|
export function toggleDistributionRuleStatus(
|
||||||
id: number,
|
id: number,
|
||||||
status: 0 | 1
|
status: 0 | 1,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return request("/v1/workbench/update-status", { id, status }, "POST");
|
return request("/v1/workbench/update-status", { id, status }, "POST");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ const TrafficDistributionList: React.FC = () => {
|
|||||||
// 新增:Switch点击切换计划状态
|
// 新增:Switch点击切换计划状态
|
||||||
const handleSwitchChange = async (
|
const handleSwitchChange = async (
|
||||||
checked: boolean,
|
checked: boolean,
|
||||||
item: DistributionRule
|
item: DistributionRule,
|
||||||
) => {
|
) => {
|
||||||
setMenuLoadingId(item.id);
|
setMenuLoadingId(item.id);
|
||||||
try {
|
try {
|
||||||
@@ -124,8 +124,8 @@ const TrafficDistributionList: React.FC = () => {
|
|||||||
// 本地只更新当前item的status,不刷新全列表
|
// 本地只更新当前item的status,不刷新全列表
|
||||||
setList(prevList =>
|
setList(prevList =>
|
||||||
prevList.map(rule =>
|
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) {
|
} catch (e) {
|
||||||
message.error("操作失败");
|
message.error("操作失败");
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const routeGroups = {
|
|||||||
// 基础路由
|
// 基础路由
|
||||||
basic: {
|
basic: {
|
||||||
name: "基础功能",
|
name: "基础功能",
|
||||||
routes: ["/", "/login", "/scene", "/work", "/mine"],
|
routes: ["/", "/login", "/guide", "/scene", "/work", "/mine"],
|
||||||
},
|
},
|
||||||
|
|
||||||
// 设备管理
|
// 设备管理
|
||||||
@@ -109,6 +109,7 @@ export const routePermissions = {
|
|||||||
user: [
|
user: [
|
||||||
"/",
|
"/",
|
||||||
"/login",
|
"/login",
|
||||||
|
"/guide",
|
||||||
"/scene",
|
"/scene",
|
||||||
"/work",
|
"/work",
|
||||||
"/mine",
|
"/mine",
|
||||||
@@ -136,6 +137,7 @@ export const routePermissions = {
|
|||||||
export const routeTitles: Record<string, string> = {
|
export const routeTitles: Record<string, string> = {
|
||||||
"/": "首页",
|
"/": "首页",
|
||||||
"/login": "登录",
|
"/login": "登录",
|
||||||
|
"/guide": "设备绑定引导",
|
||||||
"/scene": "场景获客",
|
"/scene": "场景获客",
|
||||||
"/work": "工作台",
|
"/work": "工作台",
|
||||||
"/mine": "我的",
|
"/mine": "我的",
|
||||||
@@ -159,7 +161,7 @@ export const getRouteTitle = (path: string): string => {
|
|||||||
// 检查路由权限
|
// 检查路由权限
|
||||||
export const checkRoutePermission = (
|
export const checkRoutePermission = (
|
||||||
path: string,
|
path: string,
|
||||||
userRole: string = "user"
|
userRole: string = "user",
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const allowedRoutes =
|
const allowedRoutes =
|
||||||
routePermissions[userRole as keyof typeof routePermissions] || [];
|
routePermissions[userRole as keyof typeof routePermissions] || [];
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ Object.values(modules).forEach((mod: any) => {
|
|||||||
|
|
||||||
// 权限包装
|
// 权限包装
|
||||||
function wrapWithPermission(
|
function wrapWithPermission(
|
||||||
route: RouteObject & { auth?: boolean; requiredRole?: string }
|
route: RouteObject & { auth?: boolean; requiredRole?: string },
|
||||||
) {
|
) {
|
||||||
if (route.auth) {
|
if (route.auth) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Login from "@/pages/login/login";
|
import Login from "@/pages/login/login";
|
||||||
|
import Guide from "@/pages/guide";
|
||||||
|
|
||||||
const authRoutes = [
|
const authRoutes = [
|
||||||
{
|
{
|
||||||
@@ -6,6 +7,11 @@ const authRoutes = [
|
|||||||
element: <Login />,
|
element: <Login />,
|
||||||
auth: false, // 不需要权限
|
auth: false, // 不需要权限
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/guide",
|
||||||
|
element: <Guide />,
|
||||||
|
auth: true, // 需要登录权限
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default authRoutes;
|
export default authRoutes;
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import { persist, PersistOptions } from "zustand/middleware";
|
|||||||
export function createPersistStore<T>(
|
export function createPersistStore<T>(
|
||||||
createState: (set: any, get: any) => T,
|
createState: (set: any, get: any) => T,
|
||||||
name: string,
|
name: string,
|
||||||
partialize?: (state: T) => Partial<T>
|
partialize?: (state: T) => Partial<T>,
|
||||||
) {
|
) {
|
||||||
return create<T>()(
|
return create<T>()(
|
||||||
persist(createState, {
|
persist(createState, {
|
||||||
name,
|
name,
|
||||||
partialize,
|
partialize,
|
||||||
} as PersistOptions<T>)
|
} as PersistOptions<T>),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export interface User {
|
|||||||
updateTime: string | null;
|
updateTime: string | null;
|
||||||
lastLoginIp: string;
|
lastLoginIp: string;
|
||||||
lastLoginTime: number;
|
lastLoginTime: number;
|
||||||
|
deviceTotal: number; // 设备总数
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserState {
|
interface UserState {
|
||||||
@@ -24,7 +25,7 @@ interface UserState {
|
|||||||
setUser: (user: User) => void;
|
setUser: (user: User) => void;
|
||||||
setToken: (token: string) => void;
|
setToken: (token: string) => void;
|
||||||
clearUser: () => void;
|
clearUser: () => void;
|
||||||
login: (token: string, userInfo: User) => void;
|
login: (token: string, userInfo: User, deviceTotal: number) => void;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +37,7 @@ export const useUserStore = createPersistStore<UserState>(
|
|||||||
setUser: user => set({ user, isLoggedIn: true }),
|
setUser: user => set({ user, isLoggedIn: true }),
|
||||||
setToken: token => set({ token }),
|
setToken: token => set({ token }),
|
||||||
clearUser: () => set({ user: null, token: null, isLoggedIn: false }),
|
clearUser: () => set({ user: null, token: null, isLoggedIn: false }),
|
||||||
login: (token, userInfo) => {
|
login: (token, userInfo, deviceTotal) => {
|
||||||
// 只将token存储到localStorage
|
// 只将token存储到localStorage
|
||||||
localStorage.setItem("token", token);
|
localStorage.setItem("token", token);
|
||||||
|
|
||||||
@@ -56,6 +57,7 @@ export const useUserStore = createPersistStore<UserState>(
|
|||||||
updateTime: userInfo.updateTime,
|
updateTime: userInfo.updateTime,
|
||||||
lastLoginIp: userInfo.lastLoginIp,
|
lastLoginIp: userInfo.lastLoginIp,
|
||||||
lastLoginTime: userInfo.lastLoginTime,
|
lastLoginTime: userInfo.lastLoginTime,
|
||||||
|
deviceTotal: deviceTotal,
|
||||||
};
|
};
|
||||||
set({ user, token, isLoggedIn: true });
|
set({ user, token, isLoggedIn: true });
|
||||||
},
|
},
|
||||||
@@ -70,5 +72,5 @@ export const useUserStore = createPersistStore<UserState>(
|
|||||||
user: state.user,
|
user: state.user,
|
||||||
token: state.token,
|
token: state.token,
|
||||||
isLoggedIn: state.isLoggedIn,
|
isLoggedIn: state.isLoggedIn,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const comfirm = (
|
|||||||
title?: string;
|
title?: string;
|
||||||
cancelText?: string;
|
cancelText?: string;
|
||||||
confirmText?: string;
|
confirmText?: string;
|
||||||
}
|
},
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
Modal.show({
|
Modal.show({
|
||||||
|
|||||||
Reference in New Issue
Block a user