Merge branch 'yongpxu-dev' into develop
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,3 +9,4 @@ Store_vue/.specstory/
|
|||||||
Store_vue/unpackage/
|
Store_vue/unpackage/
|
||||||
Store_vue/.vscode/
|
Store_vue/.vscode/
|
||||||
SuperAdmin/.specstory/
|
SuperAdmin/.specstory/
|
||||||
|
Cunkebao/dist
|
||||||
|
|||||||
50
Cunkebao/dist/.vite/manifest.json
vendored
50
Cunkebao/dist/.vite/manifest.json
vendored
@@ -1,50 +0,0 @@
|
|||||||
{
|
|
||||||
"_charts-B449e2xS.js": {
|
|
||||||
"file": "assets/charts-B449e2xS.js",
|
|
||||||
"name": "charts",
|
|
||||||
"imports": [
|
|
||||||
"_ui-DDu9FCjt.js",
|
|
||||||
"_vendor-0WYR1k4q.js"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"_ui-D0C0OGrH.css": {
|
|
||||||
"file": "assets/ui-D0C0OGrH.css",
|
|
||||||
"src": "_ui-D0C0OGrH.css"
|
|
||||||
},
|
|
||||||
"_ui-DDu9FCjt.js": {
|
|
||||||
"file": "assets/ui-DDu9FCjt.js",
|
|
||||||
"name": "ui",
|
|
||||||
"imports": [
|
|
||||||
"_vendor-0WYR1k4q.js"
|
|
||||||
],
|
|
||||||
"css": [
|
|
||||||
"assets/ui-D0C0OGrH.css"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"_utils-DC06x9DY.js": {
|
|
||||||
"file": "assets/utils-DC06x9DY.js",
|
|
||||||
"name": "utils",
|
|
||||||
"imports": [
|
|
||||||
"_vendor-0WYR1k4q.js"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"_vendor-0WYR1k4q.js": {
|
|
||||||
"file": "assets/vendor-0WYR1k4q.js",
|
|
||||||
"name": "vendor"
|
|
||||||
},
|
|
||||||
"index.html": {
|
|
||||||
"file": "assets/index-Czxez1-3.js",
|
|
||||||
"name": "index",
|
|
||||||
"src": "index.html",
|
|
||||||
"isEntry": true,
|
|
||||||
"imports": [
|
|
||||||
"_vendor-0WYR1k4q.js",
|
|
||||||
"_ui-DDu9FCjt.js",
|
|
||||||
"_utils-DC06x9DY.js",
|
|
||||||
"_charts-B449e2xS.js"
|
|
||||||
],
|
|
||||||
"css": [
|
|
||||||
"assets/index-QrS4Cvyc.css"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
Cunkebao/dist/assets/ui-D0C0OGrH.css
vendored
1
Cunkebao/dist/assets/ui-D0C0OGrH.css
vendored
File diff suppressed because one or more lines are too long
25
Cunkebao/dist/index.html
vendored
25
Cunkebao/dist/index.html
vendored
@@ -1,25 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>存客宝</title>
|
|
||||||
<style>
|
|
||||||
html {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<!-- 引入 uni-app web-view SDK(必须) -->
|
|
||||||
<script type="text/javascript" src="/websdk.js"></script>
|
|
||||||
<script type="module" crossorigin src="/assets/index-Czxez1-3.js"></script>
|
|
||||||
<link rel="modulepreload" crossorigin href="/assets/vendor-0WYR1k4q.js">
|
|
||||||
<link rel="modulepreload" crossorigin href="/assets/ui-DDu9FCjt.js">
|
|
||||||
<link rel="modulepreload" crossorigin href="/assets/utils-DC06x9DY.js">
|
|
||||||
<link rel="modulepreload" crossorigin href="/assets/charts-B449e2xS.js">
|
|
||||||
<link rel="stylesheet" crossorigin href="/assets/ui-D0C0OGrH.css">
|
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-QrS4Cvyc.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
BIN
Cunkebao/dist/logo.png
vendored
BIN
Cunkebao/dist/logo.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 488 KiB |
30
Cunkebao/dist/manifest.json
vendored
30
Cunkebao/dist/manifest.json
vendored
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Cunkebao",
|
|
||||||
"short_name": "Cunkebao",
|
|
||||||
"description": "Cunkebao Mobile App",
|
|
||||||
"theme_color": "#ffffff",
|
|
||||||
"background_color": "#ffffff",
|
|
||||||
"display": "standalone",
|
|
||||||
"orientation": "portrait",
|
|
||||||
"scope": "/",
|
|
||||||
"start_url": "/",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "favicon.ico",
|
|
||||||
"sizes": "64x64 32x32 24x24 16x16",
|
|
||||||
"type": "image/x-icon"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "logo.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "any maskable"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "logo.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "any maskable"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
308
Cunkebao/dist/websdk.js
vendored
308
Cunkebao/dist/websdk.js
vendored
@@ -1,308 +0,0 @@
|
|||||||
!(function (e, n) {
|
|
||||||
"object" == typeof exports && "undefined" != typeof module
|
|
||||||
? (module.exports = n())
|
|
||||||
: "function" == typeof define && define.amd
|
|
||||||
? define(n)
|
|
||||||
: ((e = e || self).uni = n());
|
|
||||||
})(this, function () {
|
|
||||||
"use strict";
|
|
||||||
try {
|
|
||||||
var e = {};
|
|
||||||
(Object.defineProperty(e, "passive", {
|
|
||||||
get: function () {
|
|
||||||
!0;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
window.addEventListener("test-passive", null, e));
|
|
||||||
} catch (e) {}
|
|
||||||
var n = Object.prototype.hasOwnProperty;
|
|
||||||
function i(e, i) {
|
|
||||||
return n.call(e, i);
|
|
||||||
}
|
|
||||||
var t = [];
|
|
||||||
function o() {
|
|
||||||
return window.__dcloud_weex_postMessage || window.__dcloud_weex_;
|
|
||||||
}
|
|
||||||
function a() {
|
|
||||||
return window.__uniapp_x_postMessage || window.__uniapp_x_;
|
|
||||||
}
|
|
||||||
var r = function (e, n) {
|
|
||||||
var i = { options: { timestamp: +new Date() }, name: e, arg: n };
|
|
||||||
if (a()) {
|
|
||||||
if ("postMessage" === e) {
|
|
||||||
var r = { data: n };
|
|
||||||
return window.__uniapp_x_postMessage
|
|
||||||
? window.__uniapp_x_postMessage(r)
|
|
||||||
: window.__uniapp_x_.postMessage(JSON.stringify(r));
|
|
||||||
}
|
|
||||||
var d = {
|
|
||||||
type: "WEB_INVOKE_APPSERVICE",
|
|
||||||
args: { data: i, webviewIds: t },
|
|
||||||
};
|
|
||||||
window.__uniapp_x_postMessage
|
|
||||||
? window.__uniapp_x_postMessageToService(d)
|
|
||||||
: window.__uniapp_x_.postMessageToService(JSON.stringify(d));
|
|
||||||
} else if (o()) {
|
|
||||||
if ("postMessage" === e) {
|
|
||||||
var s = { data: [n] };
|
|
||||||
return window.__dcloud_weex_postMessage
|
|
||||||
? window.__dcloud_weex_postMessage(s)
|
|
||||||
: window.__dcloud_weex_.postMessage(JSON.stringify(s));
|
|
||||||
}
|
|
||||||
var w = {
|
|
||||||
type: "WEB_INVOKE_APPSERVICE",
|
|
||||||
args: { data: i, webviewIds: t },
|
|
||||||
};
|
|
||||||
window.__dcloud_weex_postMessage
|
|
||||||
? window.__dcloud_weex_postMessageToService(w)
|
|
||||||
: window.__dcloud_weex_.postMessageToService(JSON.stringify(w));
|
|
||||||
} else {
|
|
||||||
if (!window.plus)
|
|
||||||
return window.parent.postMessage(
|
|
||||||
{ type: "WEB_INVOKE_APPSERVICE", data: i, pageId: "" },
|
|
||||||
"*",
|
|
||||||
);
|
|
||||||
if (0 === t.length) {
|
|
||||||
var u = plus.webview.currentWebview();
|
|
||||||
if (!u) throw new Error("plus.webview.currentWebview() is undefined");
|
|
||||||
var g = u.parent(),
|
|
||||||
v = "";
|
|
||||||
((v = g ? g.id : u.id), t.push(v));
|
|
||||||
}
|
|
||||||
if (plus.webview.getWebviewById("__uniapp__service"))
|
|
||||||
plus.webview.postMessageToUniNView(
|
|
||||||
{ type: "WEB_INVOKE_APPSERVICE", args: { data: i, webviewIds: t } },
|
|
||||||
"__uniapp__service",
|
|
||||||
);
|
|
||||||
else {
|
|
||||||
var c = JSON.stringify(i);
|
|
||||||
plus.webview
|
|
||||||
.getLaunchWebview()
|
|
||||||
.evalJS(
|
|
||||||
'UniPlusBridge.subscribeHandler("'
|
|
||||||
.concat("WEB_INVOKE_APPSERVICE", '",')
|
|
||||||
.concat(c, ",")
|
|
||||||
.concat(JSON.stringify(t), ");"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
d = {
|
|
||||||
navigateTo: function () {
|
|
||||||
var e =
|
|
||||||
arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
|
|
||||||
n = e.url;
|
|
||||||
r("navigateTo", { url: encodeURI(n) });
|
|
||||||
},
|
|
||||||
navigateBack: function () {
|
|
||||||
var e =
|
|
||||||
arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
|
|
||||||
n = e.delta;
|
|
||||||
r("navigateBack", { delta: parseInt(n) || 1 });
|
|
||||||
},
|
|
||||||
switchTab: function () {
|
|
||||||
var e =
|
|
||||||
arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
|
|
||||||
n = e.url;
|
|
||||||
r("switchTab", { url: encodeURI(n) });
|
|
||||||
},
|
|
||||||
reLaunch: function () {
|
|
||||||
var e =
|
|
||||||
arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
|
|
||||||
n = e.url;
|
|
||||||
r("reLaunch", { url: encodeURI(n) });
|
|
||||||
},
|
|
||||||
redirectTo: function () {
|
|
||||||
var e =
|
|
||||||
arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {},
|
|
||||||
n = e.url;
|
|
||||||
r("redirectTo", { url: encodeURI(n) });
|
|
||||||
},
|
|
||||||
getEnv: function (e) {
|
|
||||||
a()
|
|
||||||
? e({ uvue: !0 })
|
|
||||||
: o()
|
|
||||||
? e({ nvue: !0 })
|
|
||||||
: window.plus
|
|
||||||
? e({ plus: !0 })
|
|
||||||
: e({ h5: !0 });
|
|
||||||
},
|
|
||||||
postMessage: function () {
|
|
||||||
var e =
|
|
||||||
arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {};
|
|
||||||
r("postMessage", e.data || {});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
s = /uni-app/i.test(navigator.userAgent),
|
|
||||||
w = /Html5Plus/i.test(navigator.userAgent),
|
|
||||||
u = /complete|loaded|interactive/;
|
|
||||||
var g =
|
|
||||||
window.my &&
|
|
||||||
navigator.userAgent.indexOf(
|
|
||||||
["t", "n", "e", "i", "l", "C", "y", "a", "p", "i", "l", "A"]
|
|
||||||
.reverse()
|
|
||||||
.join(""),
|
|
||||||
) > -1;
|
|
||||||
var v =
|
|
||||||
window.swan && window.swan.webView && /swan/i.test(navigator.userAgent);
|
|
||||||
var c =
|
|
||||||
window.qq &&
|
|
||||||
window.qq.miniProgram &&
|
|
||||||
/QQ/i.test(navigator.userAgent) &&
|
|
||||||
/miniProgram/i.test(navigator.userAgent);
|
|
||||||
var p =
|
|
||||||
window.tt &&
|
|
||||||
window.tt.miniProgram &&
|
|
||||||
/toutiaomicroapp/i.test(navigator.userAgent);
|
|
||||||
var _ =
|
|
||||||
window.wx &&
|
|
||||||
window.wx.miniProgram &&
|
|
||||||
/micromessenger/i.test(navigator.userAgent) &&
|
|
||||||
/miniProgram/i.test(navigator.userAgent);
|
|
||||||
var m = window.qa && /quickapp/i.test(navigator.userAgent);
|
|
||||||
var f =
|
|
||||||
window.ks &&
|
|
||||||
window.ks.miniProgram &&
|
|
||||||
/micromessenger/i.test(navigator.userAgent) &&
|
|
||||||
/miniProgram/i.test(navigator.userAgent);
|
|
||||||
var l =
|
|
||||||
window.tt &&
|
|
||||||
window.tt.miniProgram &&
|
|
||||||
/Lark|Feishu/i.test(navigator.userAgent);
|
|
||||||
var E =
|
|
||||||
window.jd && window.jd.miniProgram && /jdmp/i.test(navigator.userAgent);
|
|
||||||
var x =
|
|
||||||
window.xhs &&
|
|
||||||
window.xhs.miniProgram &&
|
|
||||||
/xhsminiapp/i.test(navigator.userAgent);
|
|
||||||
for (
|
|
||||||
var S,
|
|
||||||
h = function () {
|
|
||||||
((window.UniAppJSBridge = !0),
|
|
||||||
document.dispatchEvent(
|
|
||||||
new CustomEvent("UniAppJSBridgeReady", {
|
|
||||||
bubbles: !0,
|
|
||||||
cancelable: !0,
|
|
||||||
}),
|
|
||||||
));
|
|
||||||
},
|
|
||||||
y = [
|
|
||||||
function (e) {
|
|
||||||
if (s || w)
|
|
||||||
return (
|
|
||||||
window.__uniapp_x_postMessage ||
|
|
||||||
window.__uniapp_x_ ||
|
|
||||||
window.__dcloud_weex_postMessage ||
|
|
||||||
window.__dcloud_weex_
|
|
||||||
? document.addEventListener("DOMContentLoaded", e)
|
|
||||||
: window.plus && u.test(document.readyState)
|
|
||||||
? setTimeout(e, 0)
|
|
||||||
: document.addEventListener("plusready", e),
|
|
||||||
d
|
|
||||||
);
|
|
||||||
},
|
|
||||||
function (e) {
|
|
||||||
if (_)
|
|
||||||
return (
|
|
||||||
window.WeixinJSBridge && window.WeixinJSBridge.invoke
|
|
||||||
? setTimeout(e, 0)
|
|
||||||
: document.addEventListener("WeixinJSBridgeReady", e),
|
|
||||||
window.wx.miniProgram
|
|
||||||
);
|
|
||||||
},
|
|
||||||
function (e) {
|
|
||||||
if (c)
|
|
||||||
return (
|
|
||||||
window.QQJSBridge && window.QQJSBridge.invoke
|
|
||||||
? setTimeout(e, 0)
|
|
||||||
: document.addEventListener("QQJSBridgeReady", e),
|
|
||||||
window.qq.miniProgram
|
|
||||||
);
|
|
||||||
},
|
|
||||||
function (e) {
|
|
||||||
if (g) {
|
|
||||||
document.addEventListener("DOMContentLoaded", e);
|
|
||||||
var n = window.my;
|
|
||||||
return {
|
|
||||||
navigateTo: n.navigateTo,
|
|
||||||
navigateBack: n.navigateBack,
|
|
||||||
switchTab: n.switchTab,
|
|
||||||
reLaunch: n.reLaunch,
|
|
||||||
redirectTo: n.redirectTo,
|
|
||||||
postMessage: n.postMessage,
|
|
||||||
getEnv: n.getEnv,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
function (e) {
|
|
||||||
if (v)
|
|
||||||
return (
|
|
||||||
document.addEventListener("DOMContentLoaded", e),
|
|
||||||
window.swan.webView
|
|
||||||
);
|
|
||||||
},
|
|
||||||
function (e) {
|
|
||||||
if (p)
|
|
||||||
return (
|
|
||||||
document.addEventListener("DOMContentLoaded", e),
|
|
||||||
window.tt.miniProgram
|
|
||||||
);
|
|
||||||
},
|
|
||||||
function (e) {
|
|
||||||
if (m) {
|
|
||||||
window.QaJSBridge && window.QaJSBridge.invoke
|
|
||||||
? setTimeout(e, 0)
|
|
||||||
: document.addEventListener("QaJSBridgeReady", e);
|
|
||||||
var n = window.qa;
|
|
||||||
return {
|
|
||||||
navigateTo: n.navigateTo,
|
|
||||||
navigateBack: n.navigateBack,
|
|
||||||
switchTab: n.switchTab,
|
|
||||||
reLaunch: n.reLaunch,
|
|
||||||
redirectTo: n.redirectTo,
|
|
||||||
postMessage: n.postMessage,
|
|
||||||
getEnv: n.getEnv,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
function (e) {
|
|
||||||
if (f)
|
|
||||||
return (
|
|
||||||
window.WeixinJSBridge && window.WeixinJSBridge.invoke
|
|
||||||
? setTimeout(e, 0)
|
|
||||||
: document.addEventListener("WeixinJSBridgeReady", e),
|
|
||||||
window.ks.miniProgram
|
|
||||||
);
|
|
||||||
},
|
|
||||||
function (e) {
|
|
||||||
if (l)
|
|
||||||
return (
|
|
||||||
document.addEventListener("DOMContentLoaded", e),
|
|
||||||
window.tt.miniProgram
|
|
||||||
);
|
|
||||||
},
|
|
||||||
function (e) {
|
|
||||||
if (E)
|
|
||||||
return (
|
|
||||||
window.JDJSBridgeReady && window.JDJSBridgeReady.invoke
|
|
||||||
? setTimeout(e, 0)
|
|
||||||
: document.addEventListener("JDJSBridgeReady", e),
|
|
||||||
window.jd.miniProgram
|
|
||||||
);
|
|
||||||
},
|
|
||||||
function (e) {
|
|
||||||
if (x) return window.xhs.miniProgram;
|
|
||||||
},
|
|
||||||
function (e) {
|
|
||||||
return (document.addEventListener("DOMContentLoaded", e), d);
|
|
||||||
},
|
|
||||||
],
|
|
||||||
M = 0;
|
|
||||||
M < y.length && !(S = y[M](h));
|
|
||||||
M++
|
|
||||||
);
|
|
||||||
S || (S = {});
|
|
||||||
var P = "undefined" != typeof uni ? uni : {};
|
|
||||||
if (!P.navigateTo) for (var b in S) i(S, b) && (P[b] = S[b]);
|
|
||||||
return ((P.webView = S), P);
|
|
||||||
});
|
|
||||||
@@ -1,57 +1,94 @@
|
|||||||
import React from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Popup, Selector } from "antd-mobile";
|
import { Popup, Selector, Button } from "antd-mobile";
|
||||||
|
import { fetchPackageOptions } from "./api";
|
||||||
import type { PackageOption } from "./data";
|
import type { PackageOption } from "./data";
|
||||||
|
|
||||||
interface BatchAddModalProps {
|
interface BatchAddModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
packageOptions: PackageOption[];
|
|
||||||
batchTarget: string;
|
|
||||||
setBatchTarget: (v: string) => void;
|
|
||||||
selectedCount: number;
|
selectedCount: number;
|
||||||
onConfirm: () => void;
|
onConfirm: (data: {
|
||||||
|
packageOptions: PackageOption[];
|
||||||
|
selectedPackageId: string;
|
||||||
|
}) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BatchAddModal: React.FC<BatchAddModalProps> = ({
|
const BatchAddModal: React.FC<BatchAddModalProps> = ({
|
||||||
visible,
|
visible,
|
||||||
onClose,
|
onClose,
|
||||||
packageOptions = [],
|
|
||||||
batchTarget,
|
|
||||||
setBatchTarget,
|
|
||||||
selectedCount,
|
selectedCount,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
}) => (
|
}) => {
|
||||||
// <Modal visible={visible} title="批量加入分组" onConfirm={onConfirm}>
|
const [packageOptions, setPackageOptions] = useState<PackageOption[]>([]);
|
||||||
// <div style={{ marginBottom: 12 }}>
|
const [selectedPackageId, setSelectedPackageId] = useState<string>("");
|
||||||
// <div>选择目标分组</div>
|
const [loading, setLoading] = useState(false);
|
||||||
// <Selector
|
|
||||||
// options={packageOptions.map(p => ({ label: p.name, value: p.id }))}
|
// 获取分组选项
|
||||||
// value={[batchTarget]}
|
useEffect(() => {
|
||||||
// onChange={v => setBatchTarget(v[0])}
|
if (visible) {
|
||||||
// />
|
setLoading(true);
|
||||||
// </div>
|
fetchPackageOptions()
|
||||||
// <div style={{ color: "#888", fontSize: 13 }}>
|
.then(res => {
|
||||||
// 将选中的{selectedCount}个用户加入所选分组
|
setPackageOptions(res.list || []);
|
||||||
// </div>
|
})
|
||||||
// </Modal>
|
.catch(error => {
|
||||||
<Popup
|
console.error("获取分组选项失败:", error);
|
||||||
visible={visible}
|
})
|
||||||
onMaskClick={() => onClose()}
|
.finally(() => {
|
||||||
position="bottom"
|
setLoading(false);
|
||||||
bodyStyle={{ height: "80vh" }}
|
});
|
||||||
>
|
}
|
||||||
<div style={{ marginBottom: 12 }}>
|
}, [visible]);
|
||||||
<div>选择目标分组</div>
|
|
||||||
<Selector
|
const handleSubmit = () => {
|
||||||
options={packageOptions.map(p => ({ label: p.name, value: p.id }))}
|
if (!selectedPackageId) {
|
||||||
value={[batchTarget]}
|
// 可以添加提示
|
||||||
onChange={v => setBatchTarget(v[0])}
|
return;
|
||||||
/>
|
}
|
||||||
</div>
|
onConfirm({
|
||||||
<div style={{ color: "#888", fontSize: 13 }}>
|
packageOptions,
|
||||||
将选中的{selectedCount}个用户加入所选分组
|
selectedPackageId,
|
||||||
</div>
|
});
|
||||||
</Popup>
|
};
|
||||||
);
|
return (
|
||||||
|
<Popup
|
||||||
|
visible={visible}
|
||||||
|
onMaskClick={() => onClose()}
|
||||||
|
position="bottom"
|
||||||
|
bodyStyle={{ height: "80vh" }}
|
||||||
|
>
|
||||||
|
<div style={{ marginBottom: 12, padding: 10 }}>
|
||||||
|
<div style={{ marginBottom: 12 }}>选择目标分组</div>
|
||||||
|
{loading ? (
|
||||||
|
<div style={{ textAlign: "center", padding: 20 }}>加载中...</div>
|
||||||
|
) : (
|
||||||
|
<Selector
|
||||||
|
options={packageOptions.map(p => ({ label: p.name, value: p.id }))}
|
||||||
|
value={[selectedPackageId]}
|
||||||
|
onChange={v => setSelectedPackageId(v[0])}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
color: "#888",
|
||||||
|
fontSize: 12,
|
||||||
|
paddingTop: 15,
|
||||||
|
marginBottom: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
将选中的{selectedCount}个用户加入所选分组
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
color="primary"
|
||||||
|
block
|
||||||
|
disabled={!selectedPackageId || loading}
|
||||||
|
>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Popup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default BatchAddModal;
|
export default BatchAddModal;
|
||||||
|
|||||||
@@ -1,24 +1,77 @@
|
|||||||
import React from "react";
|
import React, { useState, useEffect, useMemo } from "react";
|
||||||
import { Card, Button } from "antd-mobile";
|
import { Card, Button } from "antd-mobile";
|
||||||
|
import { fetchTrafficPoolList } from "./api";
|
||||||
|
import type { TrafficPoolUser } from "./data";
|
||||||
|
|
||||||
interface DataAnalysisPanelProps {
|
interface DataAnalysisPanelProps {
|
||||||
stats: {
|
showStats: boolean;
|
||||||
|
setShowStats: (v: boolean) => void;
|
||||||
|
onConfirm: (stats: {
|
||||||
total: number;
|
total: number;
|
||||||
highValue: number;
|
highValue: number;
|
||||||
added: number;
|
added: number;
|
||||||
pending: number;
|
pending: number;
|
||||||
failed: number;
|
failed: number;
|
||||||
addSuccessRate: number;
|
addSuccessRate: number;
|
||||||
};
|
}) => void;
|
||||||
showStats: boolean;
|
|
||||||
setShowStats: (v: boolean) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DataAnalysisPanel: React.FC<DataAnalysisPanelProps> = ({
|
const DataAnalysisPanel: React.FC<DataAnalysisPanelProps> = ({
|
||||||
stats,
|
|
||||||
showStats,
|
showStats,
|
||||||
setShowStats,
|
setShowStats,
|
||||||
|
onConfirm,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [list, setList] = useState<TrafficPoolUser[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// 计算统计数据
|
||||||
|
const stats = useMemo(() => {
|
||||||
|
const total = list.length;
|
||||||
|
const highValue = list.filter(
|
||||||
|
u => u.tags && u.tags.includes("高价值客户池"),
|
||||||
|
).length;
|
||||||
|
const added = list.filter(u => u.status === 1).length;
|
||||||
|
const pending = list.filter(u => u.status === 0).length;
|
||||||
|
const failed = list.filter(u => u.status === -1).length;
|
||||||
|
const addSuccessRate = total ? Math.round((added / total) * 100) : 0;
|
||||||
|
return { total, highValue, added, pending, failed, addSuccessRate };
|
||||||
|
}, [list]);
|
||||||
|
|
||||||
|
// 获取数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (showStats) {
|
||||||
|
setLoading(true);
|
||||||
|
fetchTrafficPoolList({ page: 1, pageSize: 1000 }) // 获取所有数据进行统计
|
||||||
|
.then(res => {
|
||||||
|
setList(res.list || []);
|
||||||
|
// 通过 onConfirm 抛出统计数据
|
||||||
|
const total = res.list?.length || 0;
|
||||||
|
const highValue =
|
||||||
|
res.list?.filter(u => u.tags && u.tags.includes("高价值客户池"))
|
||||||
|
.length || 0;
|
||||||
|
const added = res.list?.filter(u => u.status === 1).length || 0;
|
||||||
|
const pending = res.list?.filter(u => u.status === 0).length || 0;
|
||||||
|
const failed = res.list?.filter(u => u.status === -1).length || 0;
|
||||||
|
const addSuccessRate = total ? Math.round((added / total) * 100) : 0;
|
||||||
|
|
||||||
|
onConfirm({
|
||||||
|
total,
|
||||||
|
highValue,
|
||||||
|
added,
|
||||||
|
pending,
|
||||||
|
failed,
|
||||||
|
addSuccessRate,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("获取统计数据失败:", error);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [showStats, onConfirm]);
|
||||||
|
|
||||||
if (!showStats) return null;
|
if (!showStats) return null;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -30,46 +83,54 @@ const DataAnalysisPanel: React.FC<DataAnalysisPanelProps> = ({
|
|||||||
boxShadow: "0 2px 8px rgba(0,0,0,0.04)",
|
boxShadow: "0 2px 8px rgba(0,0,0,0.04)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex", gap: 16, marginBottom: 12 }}>
|
{loading ? (
|
||||||
<Card style={{ flex: 1 }}>
|
<div style={{ textAlign: "center", padding: 20 }}>
|
||||||
<div style={{ fontSize: 18, fontWeight: 600, color: "#1677ff" }}>
|
加载统计数据中...
|
||||||
{stats.total}
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div style={{ display: "flex", gap: 16, marginBottom: 12 }}>
|
||||||
|
<Card style={{ flex: 1 }}>
|
||||||
|
<div style={{ fontSize: 18, fontWeight: 600, color: "#1677ff" }}>
|
||||||
|
{stats.total}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 13, color: "#888" }}>总用户数</div>
|
||||||
|
</Card>
|
||||||
|
<Card style={{ flex: 1 }}>
|
||||||
|
<div style={{ fontSize: 18, fontWeight: 600, color: "#eb2f96" }}>
|
||||||
|
{stats.highValue}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 13, color: "#888" }}>高价值用户</div>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 13, color: "#888" }}>总用户数</div>
|
<div style={{ display: "flex", gap: 16 }}>
|
||||||
</Card>
|
<Card style={{ flex: 1 }}>
|
||||||
<Card style={{ flex: 1 }}>
|
<div style={{ fontSize: 18, fontWeight: 600, color: "#52c41a" }}>
|
||||||
<div style={{ fontSize: 18, fontWeight: 600, color: "#eb2f96" }}>
|
{stats.addSuccessRate}%
|
||||||
{stats.highValue}
|
</div>
|
||||||
|
<div style={{ fontSize: 13, color: "#888" }}>添加成功率</div>
|
||||||
|
</Card>
|
||||||
|
<Card style={{ flex: 1 }}>
|
||||||
|
<div style={{ fontSize: 18, fontWeight: 600, color: "#faad14" }}>
|
||||||
|
{stats.added}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 13, color: "#888" }}>已添加</div>
|
||||||
|
</Card>
|
||||||
|
<Card style={{ flex: 1 }}>
|
||||||
|
<div style={{ fontSize: 18, fontWeight: 600, color: "#bfbfbf" }}>
|
||||||
|
{stats.pending}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 13, color: "#888" }}>待添加</div>
|
||||||
|
</Card>
|
||||||
|
<Card style={{ flex: 1 }}>
|
||||||
|
<div style={{ fontSize: 18, fontWeight: 600, color: "#ff4d4f" }}>
|
||||||
|
{stats.failed}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 13, color: "#888" }}>添加失败</div>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 13, color: "#888" }}>高价值用户</div>
|
</>
|
||||||
</Card>
|
)}
|
||||||
</div>
|
|
||||||
<div style={{ display: "flex", gap: 16 }}>
|
|
||||||
<Card style={{ flex: 1 }}>
|
|
||||||
<div style={{ fontSize: 18, fontWeight: 600, color: "#52c41a" }}>
|
|
||||||
{stats.addSuccessRate}%
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 13, color: "#888" }}>添加成功率</div>
|
|
||||||
</Card>
|
|
||||||
<Card style={{ flex: 1 }}>
|
|
||||||
<div style={{ fontSize: 18, fontWeight: 600, color: "#faad14" }}>
|
|
||||||
{stats.added}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 13, color: "#888" }}>已添加</div>
|
|
||||||
</Card>
|
|
||||||
<Card style={{ flex: 1 }}>
|
|
||||||
<div style={{ fontSize: 18, fontWeight: 600, color: "#bfbfbf" }}>
|
|
||||||
{stats.pending}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 13, color: "#888" }}>待添加</div>
|
|
||||||
</Card>
|
|
||||||
<Card style={{ flex: 1 }}>
|
|
||||||
<div style={{ fontSize: 18, fontWeight: 600, color: "#ff4d4f" }}>
|
|
||||||
{stats.failed}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 13, color: "#888" }}>添加失败</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
style={{ marginTop: 12 }}
|
style={{ marginTop: 12 }}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react";
|
|||||||
import { Popup } from "antd-mobile";
|
import { Popup } from "antd-mobile";
|
||||||
import { Select, Button } from "antd";
|
import { Select, Button } from "antd";
|
||||||
import DeviceSelection from "@/components/DeviceSelection";
|
import DeviceSelection from "@/components/DeviceSelection";
|
||||||
import type { UserStatus, ScenarioOption } from "./data";
|
import type { ScenarioOption } from "./data";
|
||||||
import { fetchScenarioOptions, fetchPackageOptions } from "./api";
|
import { fetchScenarioOptions, fetchPackageOptions } from "./api";
|
||||||
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
|
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
|
||||||
|
|
||||||
@@ -10,13 +10,21 @@ interface FilterModalProps {
|
|||||||
visible: boolean;
|
visible: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onConfirm: (filters: {
|
onConfirm: (filters: {
|
||||||
deviceIds: string[];
|
selectedDevices: DeviceSelectionItem[]; // 更新为 deviceld
|
||||||
packageId: string;
|
packageld: number; // 更新为 packageld
|
||||||
scenarioId: string;
|
sceneId: number; // 更新为 sceneId
|
||||||
userValue: number;
|
userValue: number;
|
||||||
userStatus: number;
|
addStatus: number; // 更新为 addStatus
|
||||||
}) => void;
|
}) => void;
|
||||||
scenarioOptions: ScenarioOption[];
|
scenarioOptions: ScenarioOption[];
|
||||||
|
// 初始筛选值
|
||||||
|
initialFilters?: {
|
||||||
|
selectedDevices: DeviceSelectionItem[];
|
||||||
|
packageId: number;
|
||||||
|
scenarioId: number;
|
||||||
|
userValue: number;
|
||||||
|
userStatus: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const valueLevelOptions = [
|
const valueLevelOptions = [
|
||||||
@@ -37,17 +45,35 @@ const FilterModal: React.FC<FilterModalProps> = ({
|
|||||||
visible,
|
visible,
|
||||||
onClose,
|
onClose,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
|
initialFilters,
|
||||||
}) => {
|
}) => {
|
||||||
const [selectedDevices, setSelectedDevices] = useState<DeviceSelectionItem[]>(
|
const [selectedDevices, setSelectedDevices] = useState<DeviceSelectionItem[]>(
|
||||||
[],
|
initialFilters?.selectedDevices || [],
|
||||||
|
);
|
||||||
|
const [packageId, setPackageId] = useState<number>(initialFilters?.packageId);
|
||||||
|
const [scenarioId, setScenarioId] = useState<number>(
|
||||||
|
initialFilters?.scenarioId,
|
||||||
|
);
|
||||||
|
const [userValue, setUserValue] = useState<number>(
|
||||||
|
initialFilters?.userValue || 0,
|
||||||
|
);
|
||||||
|
const [userStatus, setUserStatus] = useState<number>(
|
||||||
|
initialFilters?.userStatus || 0,
|
||||||
);
|
);
|
||||||
const [packageId, setPackageId] = useState<string>("");
|
|
||||||
const [scenarioId, setScenarioId] = useState<string>("");
|
|
||||||
const [userValue, setUserValue] = useState<number>(0);
|
|
||||||
const [userStatus, setUserStatus] = useState<number>(0);
|
|
||||||
const [scenarioOptions, setScenarioOptions] = useState<any[]>([]);
|
const [scenarioOptions, setScenarioOptions] = useState<any[]>([]);
|
||||||
const [packageOptions, setPackageOptions] = useState<any[]>([]);
|
const [packageOptions, setPackageOptions] = useState<any[]>([]);
|
||||||
|
|
||||||
|
// 同步初始值变化
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialFilters) {
|
||||||
|
setSelectedDevices(initialFilters.selectedDevices || []);
|
||||||
|
setPackageId(initialFilters.packageId || 0);
|
||||||
|
setScenarioId(initialFilters.scenarioId || 0);
|
||||||
|
setUserValue(initialFilters.userValue || 0);
|
||||||
|
setUserStatus(initialFilters.userStatus || 0);
|
||||||
|
}
|
||||||
|
}, [initialFilters]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
fetchScenarioOptions()
|
fetchScenarioOptions()
|
||||||
@@ -72,11 +98,11 @@ const FilterModal: React.FC<FilterModalProps> = ({
|
|||||||
|
|
||||||
const handleApply = () => {
|
const handleApply = () => {
|
||||||
const params = {
|
const params = {
|
||||||
deviceIds: selectedDevices.map(d => d.id.toString()),
|
selectedDevices: selectedDevices, // 更新为 deviceld
|
||||||
packageId,
|
packageld: packageId, // 更新为 packageld
|
||||||
scenarioId,
|
sceneId: scenarioId, // 更新为 sceneId
|
||||||
userValue,
|
userValue,
|
||||||
userStatus,
|
addStatus: userStatus, // 更新为 addStatus
|
||||||
};
|
};
|
||||||
console.log(params);
|
console.log(params);
|
||||||
|
|
||||||
@@ -86,8 +112,8 @@ const FilterModal: React.FC<FilterModalProps> = ({
|
|||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
setSelectedDevices([]);
|
setSelectedDevices([]);
|
||||||
setPackageId("");
|
setPackageId(0);
|
||||||
setScenarioId("");
|
setScenarioId(0);
|
||||||
setUserValue(0);
|
setUserValue(0);
|
||||||
setUserStatus(0);
|
setUserStatus(0);
|
||||||
};
|
};
|
||||||
@@ -119,7 +145,7 @@ const FilterModal: React.FC<FilterModalProps> = ({
|
|||||||
value={packageId}
|
value={packageId}
|
||||||
onChange={setPackageId}
|
onChange={setPackageId}
|
||||||
options={[
|
options={[
|
||||||
{ label: "全部流量池", value: "" },
|
{ label: "全部流量池", value: 0 },
|
||||||
...packageOptions.map(p => ({ label: p.name, value: p.id })),
|
...packageOptions.map(p => ({ label: p.name, value: p.id })),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -131,7 +157,7 @@ const FilterModal: React.FC<FilterModalProps> = ({
|
|||||||
value={scenarioId}
|
value={scenarioId}
|
||||||
onChange={setScenarioId}
|
onChange={setScenarioId}
|
||||||
options={[
|
options={[
|
||||||
{ label: "全部场景", value: "" },
|
{ label: "全部场景", value: 0 },
|
||||||
...scenarioOptions.map(s => ({ label: s.name, value: s.id })),
|
...scenarioOptions.map(s => ({ label: s.name, value: s.id })),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -16,3 +16,19 @@ export async function fetchScenarioOptions() {
|
|||||||
export async function fetchPackageOptions() {
|
export async function fetchPackageOptions() {
|
||||||
return request("/v1/traffic/pool/getPackage", {}, "GET");
|
return request("/v1/traffic/pool/getPackage", {}, "GET");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function addPackage(params: {
|
||||||
|
type: string; // 类型 1搜索 2选择用户 3文件上传
|
||||||
|
addPackageId?: number;
|
||||||
|
addStatus?: number;
|
||||||
|
deviceId?: string;
|
||||||
|
keyword?: string;
|
||||||
|
packageId?: number;
|
||||||
|
packageName?: number; // 添加的流量池名称
|
||||||
|
tableFile?: number;
|
||||||
|
taskId?: number; // 任务id j及场景获客id
|
||||||
|
userIds?: number[];
|
||||||
|
userValue?: number;
|
||||||
|
}) {
|
||||||
|
return request("/v1/traffic/pool/addPackage", params, "POST");
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,182 +0,0 @@
|
|||||||
import { useState, useEffect, useMemo } from "react";
|
|
||||||
import {
|
|
||||||
fetchTrafficPoolList,
|
|
||||||
fetchPackageOptions,
|
|
||||||
fetchScenarioOptions,
|
|
||||||
} from "./api";
|
|
||||||
import type { TrafficPoolUser, PackageOption, ScenarioOption } from "./data";
|
|
||||||
import { Toast } from "antd-mobile";
|
|
||||||
|
|
||||||
export function useTrafficPoolListLogic() {
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [list, setList] = useState<TrafficPoolUser[]>([]);
|
|
||||||
const [page, setPage] = useState(1);
|
|
||||||
const [pageSize] = useState(10);
|
|
||||||
const [total, setTotal] = useState(0);
|
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
|
|
||||||
// 筛选相关
|
|
||||||
const [showFilter, setShowFilter] = useState(false);
|
|
||||||
const [packageOptions, setPackageOptions] = useState<PackageOption[]>([]);
|
|
||||||
const [scenarioOptions, setScenarioOptions] = useState<ScenarioOption[]>([]);
|
|
||||||
const [selectedDevices, setSelectedDevices] = useState<any[]>([]);
|
|
||||||
const [packageId, setPackageId] = useState<number>(0);
|
|
||||||
const [scenarioId, setScenarioId] = useState<number>(0);
|
|
||||||
const [userValue, setUserValue] = useState<number>(0);
|
|
||||||
const [userStatus, setUserStatus] = useState<number>(0);
|
|
||||||
|
|
||||||
// 批量相关
|
|
||||||
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
|
||||||
const [batchModal, setBatchModal] = useState(false);
|
|
||||||
const [batchTarget, setBatchTarget] = useState<string>("");
|
|
||||||
|
|
||||||
// 数据分析
|
|
||||||
const [showStats, setShowStats] = useState(false);
|
|
||||||
const stats = useMemo(() => {
|
|
||||||
const total = list.length;
|
|
||||||
const highValue = list.filter(
|
|
||||||
u => u.tags && u.tags.includes("高价值客户池"),
|
|
||||||
).length;
|
|
||||||
const added = list.filter(u => u.status === 1).length;
|
|
||||||
const pending = list.filter(u => u.status === 0).length;
|
|
||||||
const failed = list.filter(u => u.status === -1).length;
|
|
||||||
const addSuccessRate = total ? Math.round((added / total) * 100) : 0;
|
|
||||||
return { total, highValue, added, pending, failed, addSuccessRate };
|
|
||||||
}, [list]);
|
|
||||||
|
|
||||||
// 获取列表
|
|
||||||
const getList = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const params: any = {
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
keyword: search,
|
|
||||||
packageId,
|
|
||||||
taskId: scenarioId,
|
|
||||||
userValue,
|
|
||||||
addStatus: userStatus,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 添加筛选参数
|
|
||||||
if (selectedDevices.length > 0) {
|
|
||||||
params.deviceId = selectedDevices.map(d => d.id).join(",");
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetchTrafficPoolList(params);
|
|
||||||
setList(res.list || []);
|
|
||||||
setTotal(res.total || 0);
|
|
||||||
} catch (error) {
|
|
||||||
// 忽略请求过于频繁的错误,避免页面崩溃
|
|
||||||
if (error !== "请求过于频繁,请稍后再试") {
|
|
||||||
console.error("获取列表失败:", error);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取筛选项
|
|
||||||
useEffect(() => {
|
|
||||||
fetchPackageOptions().then(res => {
|
|
||||||
setPackageOptions(res.list || []);
|
|
||||||
});
|
|
||||||
fetchScenarioOptions().then(res => {
|
|
||||||
setScenarioOptions(res.list || []);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 筛选条件变化时刷新列表
|
|
||||||
useEffect(() => {
|
|
||||||
getList();
|
|
||||||
// eslint-disable-next-line
|
|
||||||
}, [
|
|
||||||
page,
|
|
||||||
search,
|
|
||||||
selectedDevices,
|
|
||||||
packageId,
|
|
||||||
scenarioId,
|
|
||||||
userValue,
|
|
||||||
userStatus,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 全选/反选
|
|
||||||
const handleSelectAll = (checked: boolean) => {
|
|
||||||
if (checked) {
|
|
||||||
setSelectedIds(list.map(item => item.id));
|
|
||||||
} else {
|
|
||||||
setSelectedIds([]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// 单选
|
|
||||||
const handleSelect = (id: number, checked: boolean) => {
|
|
||||||
setSelectedIds(prev =>
|
|
||||||
checked ? [...prev, id] : prev.filter(i => i !== id),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 批量加入分组/流量池
|
|
||||||
const handleBatchAdd = () => {
|
|
||||||
if (!batchTarget) {
|
|
||||||
Toast.show({ content: "请选择目标分组", position: "top" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// TODO: 调用后端批量接口,这里仅模拟
|
|
||||||
Toast.show({
|
|
||||||
content: `已将${selectedIds.length}个用户加入${packageOptions.find(p => p.id === batchTarget)?.name || ""}`,
|
|
||||||
position: "top",
|
|
||||||
});
|
|
||||||
setBatchModal(false);
|
|
||||||
setSelectedIds([]);
|
|
||||||
setBatchTarget("");
|
|
||||||
// 可刷新列表
|
|
||||||
};
|
|
||||||
|
|
||||||
// 筛选重置
|
|
||||||
const resetFilter = () => {
|
|
||||||
setSelectedDevices([]);
|
|
||||||
setPackageId(0);
|
|
||||||
setScenarioId(0);
|
|
||||||
setUserValue(0);
|
|
||||||
setUserStatus(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
loading,
|
|
||||||
list,
|
|
||||||
page,
|
|
||||||
setPage,
|
|
||||||
pageSize,
|
|
||||||
total,
|
|
||||||
search,
|
|
||||||
setSearch,
|
|
||||||
showFilter,
|
|
||||||
setShowFilter,
|
|
||||||
packageOptions,
|
|
||||||
scenarioOptions,
|
|
||||||
selectedDevices,
|
|
||||||
setSelectedDevices,
|
|
||||||
packageId,
|
|
||||||
setPackageId,
|
|
||||||
scenarioId,
|
|
||||||
setScenarioId,
|
|
||||||
userValue,
|
|
||||||
setUserValue,
|
|
||||||
userStatus,
|
|
||||||
setUserStatus,
|
|
||||||
selectedIds,
|
|
||||||
setSelectedIds,
|
|
||||||
handleSelectAll,
|
|
||||||
handleSelect,
|
|
||||||
batchModal,
|
|
||||||
setBatchModal,
|
|
||||||
batchTarget,
|
|
||||||
setBatchTarget,
|
|
||||||
handleBatchAdd,
|
|
||||||
showStats,
|
|
||||||
setShowStats,
|
|
||||||
stats,
|
|
||||||
getList,
|
|
||||||
resetFilter,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -5,51 +5,155 @@ import {
|
|||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
BarChartOutlined,
|
BarChartOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
|
import { Toast } from "antd-mobile";
|
||||||
import { Input, Button, Checkbox, Pagination } from "antd";
|
import { Input, Button, Checkbox, Pagination } from "antd";
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
import { Empty, Avatar } from "antd-mobile";
|
import { Empty, Avatar } from "antd-mobile";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import NavCommon from "@/components/NavCommon";
|
import NavCommon from "@/components/NavCommon";
|
||||||
import { useTrafficPoolListLogic } from "./dataAnyx";
|
import { fetchTrafficPoolList, fetchScenarioOptions, addPackage } from "./api";
|
||||||
|
import type { TrafficPoolUser, ScenarioOption } from "./data";
|
||||||
import DataAnalysisPanel from "./DataAnalysisPanel";
|
import DataAnalysisPanel from "./DataAnalysisPanel";
|
||||||
import FilterModal from "./FilterModal";
|
import FilterModal from "./FilterModal";
|
||||||
import BatchAddModal from "./BatchAddModal";
|
import BatchAddModal from "./BatchAddModal";
|
||||||
|
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
|
||||||
const defaultAvatar =
|
const defaultAvatar =
|
||||||
"https://cdn.jsdelivr.net/gh/maokaka/static/avatar-default.png";
|
"https://cdn.jsdelivr.net/gh/maokaka/static/avatar-default.png";
|
||||||
|
|
||||||
const TrafficPoolList: React.FC = () => {
|
const TrafficPoolList: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const {
|
|
||||||
loading,
|
// 基础状态
|
||||||
list,
|
const [loading, setLoading] = useState(false);
|
||||||
page,
|
const [list, setList] = useState<TrafficPoolUser[]>([]);
|
||||||
setPage,
|
const [page, setPage] = useState(1);
|
||||||
total,
|
const [pageSize] = useState(10);
|
||||||
search,
|
const [total, setTotal] = useState(0);
|
||||||
setSearch,
|
const [search, setSearch] = useState("");
|
||||||
showFilter,
|
|
||||||
setShowFilter,
|
// 筛选相关
|
||||||
packageOptions,
|
const [showFilter, setShowFilter] = useState(false);
|
||||||
scenarioOptions,
|
const [scenarioOptions, setScenarioOptions] = useState<ScenarioOption[]>([]);
|
||||||
setSelectedDevices,
|
|
||||||
setPackageId,
|
// 公共筛选条件状态
|
||||||
setScenarioId,
|
const [filterParams, setFilterParams] = useState({
|
||||||
setUserValue,
|
selectedDevices: [] as DeviceSelectionItem[],
|
||||||
setUserStatus,
|
packageId: 0,
|
||||||
selectedIds,
|
scenarioId: 0,
|
||||||
handleSelectAll,
|
userValue: 0,
|
||||||
handleSelect,
|
userStatus: 0,
|
||||||
batchModal,
|
});
|
||||||
setBatchModal,
|
|
||||||
batchTarget,
|
// 批量相关
|
||||||
setBatchTarget,
|
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
||||||
handleBatchAdd,
|
const [batchModal, setBatchModal] = useState(false);
|
||||||
showStats,
|
|
||||||
setShowStats,
|
// 数据分析
|
||||||
stats,
|
const [showStats, setShowStats] = useState(false);
|
||||||
getList,
|
|
||||||
} = useTrafficPoolListLogic();
|
// 获取列表
|
||||||
|
const getList = async (customParams?: any) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const params: any = {
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
keyword: search,
|
||||||
|
packageld: filterParams.packageId,
|
||||||
|
sceneId: filterParams.scenarioId,
|
||||||
|
userValue: filterParams.userValue,
|
||||||
|
addStatus: filterParams.userStatus,
|
||||||
|
deviceld: filterParams.selectedDevices.map(d => d.id).join(),
|
||||||
|
...customParams, // 允许传入自定义参数覆盖
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetchTrafficPoolList(params);
|
||||||
|
setList(res.list || []);
|
||||||
|
setTotal(res.total || 0);
|
||||||
|
} catch (error) {
|
||||||
|
// 忽略请求过于频繁的错误,避免页面崩溃
|
||||||
|
if (error !== "请求过于频繁,请稍后再试") {
|
||||||
|
console.error("获取列表失败:", error);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取筛选项
|
||||||
|
useEffect(() => {
|
||||||
|
fetchScenarioOptions().then(res => {
|
||||||
|
setScenarioOptions(res.list || []);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 全选/反选
|
||||||
|
const handleSelectAll = (checked: boolean) => {
|
||||||
|
if (checked) {
|
||||||
|
setSelectedIds(list.map(item => item.id));
|
||||||
|
} else {
|
||||||
|
setSelectedIds([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 单选
|
||||||
|
const handleSelect = (id: number, checked: boolean) => {
|
||||||
|
setSelectedIds(prev =>
|
||||||
|
checked ? [...prev, id] : prev.filter(i => i !== id),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 批量加入分组/流量池
|
||||||
|
const handleBatchAdd = async options => {
|
||||||
|
try {
|
||||||
|
// 构建请求参数
|
||||||
|
const params = {
|
||||||
|
type: "2", // 2选择用户
|
||||||
|
addPackageId: options.selectedPackageId, // 目标分组ID
|
||||||
|
userIds: selectedIds.map(id => id), // 选中的用户ID数组
|
||||||
|
// 如果有当前筛选条件,也可以传递
|
||||||
|
...(filterParams.packageId && {
|
||||||
|
packageId: filterParams.packageId,
|
||||||
|
}),
|
||||||
|
...(filterParams.scenarioId && {
|
||||||
|
taskId: filterParams.scenarioId,
|
||||||
|
}),
|
||||||
|
...(filterParams.userValue && {
|
||||||
|
userValue: filterParams.userValue,
|
||||||
|
}),
|
||||||
|
...(filterParams.userStatus && {
|
||||||
|
addStatus: filterParams.userStatus,
|
||||||
|
}),
|
||||||
|
...(filterParams.selectedDevices.length > 0 && {
|
||||||
|
deviceId: filterParams.selectedDevices.map(d => d.id).join(","),
|
||||||
|
}),
|
||||||
|
...(search && { keyword: search }),
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("批量加入请求参数:", params);
|
||||||
|
|
||||||
|
// 调用接口
|
||||||
|
const result = await addPackage(params);
|
||||||
|
console.log("批量加入结果:", result);
|
||||||
|
|
||||||
|
// 成功后刷新列表
|
||||||
|
getList();
|
||||||
|
|
||||||
|
// 关闭弹窗并清空选择
|
||||||
|
setBatchModal(false);
|
||||||
|
setSelectedIds([]);
|
||||||
|
|
||||||
|
// 可以添加成功提示
|
||||||
|
Toast.show({
|
||||||
|
content: `成功将用户加入分组`,
|
||||||
|
position: "top",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("批量加入失败:", error);
|
||||||
|
// 可以添加错误提示
|
||||||
|
Toast.show({ content: "批量加入失败,请重试", position: "top" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 搜索防抖处理
|
// 搜索防抖处理
|
||||||
const [searchInput, setSearchInput] = useState(search);
|
const [searchInput, setSearchInput] = useState(search);
|
||||||
@@ -57,16 +161,25 @@ const TrafficPoolList: React.FC = () => {
|
|||||||
const debouncedSearch = useCallback(() => {
|
const debouncedSearch = useCallback(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setSearch(searchInput);
|
setSearch(searchInput);
|
||||||
|
// 搜索时重置到第一页并请求列表
|
||||||
|
setPage(1);
|
||||||
|
getList({ keyword: searchInput, page: 1 });
|
||||||
}, 500); // 500ms 防抖延迟
|
}, 500); // 500ms 防抖延迟
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [searchInput, setSearch]);
|
}, [searchInput]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cleanup = debouncedSearch();
|
const cleanup = debouncedSearch();
|
||||||
return cleanup;
|
return cleanup;
|
||||||
}, [debouncedSearch]);
|
}, [debouncedSearch]);
|
||||||
|
|
||||||
|
const handSearch = (value: string) => {
|
||||||
|
setSearchInput(value);
|
||||||
|
setSelectedIds([]);
|
||||||
|
debouncedSearch();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
loading={loading}
|
loading={loading}
|
||||||
@@ -89,14 +202,14 @@ const TrafficPoolList: React.FC = () => {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="搜索计划名称"
|
placeholder="搜索计划名称"
|
||||||
value={searchInput}
|
value={searchInput}
|
||||||
onChange={e => setSearchInput(e.target.value)}
|
onChange={e => handSearch(e.target.value)}
|
||||||
prefix={<SearchOutlined />}
|
prefix={<SearchOutlined />}
|
||||||
allowClear
|
allowClear
|
||||||
size="large"
|
size="large"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={getList}
|
onClick={() => getList()}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
size="large"
|
size="large"
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined />}
|
||||||
@@ -104,9 +217,12 @@ const TrafficPoolList: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
{/* 数据分析面板 */}
|
{/* 数据分析面板 */}
|
||||||
<DataAnalysisPanel
|
<DataAnalysisPanel
|
||||||
stats={stats}
|
|
||||||
showStats={showStats}
|
showStats={showStats}
|
||||||
setShowStats={setShowStats}
|
setShowStats={setShowStats}
|
||||||
|
onConfirm={statsData => {
|
||||||
|
// 可以在这里处理统计数据,比如更新本地状态或发送到父组件
|
||||||
|
console.log("收到统计数据:", statsData);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 批量操作栏 */}
|
{/* 批量操作栏 */}
|
||||||
@@ -114,7 +230,7 @@ const TrafficPoolList: React.FC = () => {
|
|||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
padding: "8px 12px",
|
padding: "8px 12px 8px 26px",
|
||||||
background: "#fff",
|
background: "#fff",
|
||||||
borderBottom: "1px solid #f0f0f0",
|
borderBottom: "1px solid #f0f0f0",
|
||||||
}}
|
}}
|
||||||
@@ -140,6 +256,18 @@ const TrafficPoolList: React.FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{searchInput.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
style={{ marginLeft: 16 }}
|
||||||
|
onClick={() => setBatchModal(true)}
|
||||||
|
>
|
||||||
|
导入当前搜索结果
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<div style={{ flex: 1 }} />
|
<div style={{ flex: 1 }} />
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
@@ -158,7 +286,10 @@ const TrafficPoolList: React.FC = () => {
|
|||||||
pageSize={20}
|
pageSize={20}
|
||||||
total={total}
|
total={total}
|
||||||
showSizeChanger={false}
|
showSizeChanger={false}
|
||||||
onChange={setPage}
|
onChange={newPage => {
|
||||||
|
setPage(newPage);
|
||||||
|
getList({ page: newPage });
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -167,35 +298,40 @@ const TrafficPoolList: React.FC = () => {
|
|||||||
<BatchAddModal
|
<BatchAddModal
|
||||||
visible={batchModal}
|
visible={batchModal}
|
||||||
onClose={() => setBatchModal(false)}
|
onClose={() => setBatchModal(false)}
|
||||||
packageOptions={packageOptions}
|
|
||||||
batchTarget={batchTarget}
|
|
||||||
setBatchTarget={setBatchTarget}
|
|
||||||
selectedCount={selectedIds.length}
|
selectedCount={selectedIds.length}
|
||||||
onConfirm={handleBatchAdd}
|
onConfirm={data => {
|
||||||
|
// 处理批量加入逻辑
|
||||||
|
handleBatchAdd(data);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{/* 筛选弹窗 */}
|
{/* 筛选弹窗 */}
|
||||||
<FilterModal
|
<FilterModal
|
||||||
visible={showFilter}
|
visible={showFilter}
|
||||||
onClose={() => setShowFilter(false)}
|
onClose={() => setShowFilter(false)}
|
||||||
onConfirm={filters => {
|
onConfirm={filters => {
|
||||||
// 更新筛选条件
|
// 更新公共筛选条件状态
|
||||||
setSelectedDevices(
|
const newFilterParams = {
|
||||||
filters.deviceIds.map(id => ({
|
selectedDevices: filters.selectedDevices,
|
||||||
id: parseInt(id),
|
packageId: filters.packageld,
|
||||||
memo: "",
|
scenarioId: filters.sceneId,
|
||||||
imei: "",
|
userValue: filters.userValue,
|
||||||
wechatId: "",
|
userStatus: filters.addStatus,
|
||||||
status: "offline" as const,
|
};
|
||||||
})),
|
|
||||||
);
|
setFilterParams(newFilterParams);
|
||||||
setPackageId(filters.packageId ? parseInt(filters.packageId) : 0);
|
// 重置到第一页并请求列表
|
||||||
setScenarioId(filters.scenarioId ? parseInt(filters.scenarioId) : 0);
|
setPage(1);
|
||||||
setUserValue(filters.userValue);
|
getList({
|
||||||
setUserStatus(filters.userStatus);
|
page: 1,
|
||||||
// 重新获取列表
|
packageld: newFilterParams.packageId,
|
||||||
getList();
|
sceneId: newFilterParams.scenarioId,
|
||||||
|
userValue: newFilterParams.userValue,
|
||||||
|
addStatus: newFilterParams.userStatus,
|
||||||
|
deviceld: newFilterParams.selectedDevices.map(d => d.id).join(),
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
scenarioOptions={scenarioOptions}
|
scenarioOptions={scenarioOptions}
|
||||||
|
initialFilters={filterParams}
|
||||||
/>
|
/>
|
||||||
<div className={styles.listWrap}>
|
<div className={styles.listWrap}>
|
||||||
{list.length === 0 && !loading ? (
|
{list.length === 0 && !loading ? (
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export interface Allocation {
|
|||||||
/** 设备id */
|
/** 设备id */
|
||||||
deviceGroups: number[];
|
deviceGroups: number[];
|
||||||
/** 流量池 */
|
/** 流量池 */
|
||||||
pools?: JSON | null;
|
poolGroups?: number[];
|
||||||
|
|
||||||
/** 分配数量 */
|
/** 分配数量 */
|
||||||
num?: number | null;
|
num?: number | null;
|
||||||
@@ -72,7 +72,7 @@ export interface ContactImportTaskConfig {
|
|||||||
id: number;
|
id: number;
|
||||||
workbenchId: number;
|
workbenchId: number;
|
||||||
devices: number[];
|
devices: number[];
|
||||||
pools: number[];
|
poolGroups: number[];
|
||||||
num: number;
|
num: number;
|
||||||
clearContact: number;
|
clearContact: number;
|
||||||
remarkType: number;
|
remarkType: number;
|
||||||
@@ -114,7 +114,7 @@ export interface CreateContactImportTaskData {
|
|||||||
type: number;
|
type: number;
|
||||||
config: {
|
config: {
|
||||||
devices: number[];
|
devices: number[];
|
||||||
pools: number[];
|
poolGroups: number[];
|
||||||
num: number;
|
num: number;
|
||||||
clearContact: number;
|
clearContact: number;
|
||||||
remarkType: number;
|
remarkType: number;
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { PlusOutlined, MinusOutlined } from "@ant-design/icons";
|
import { PlusOutlined, MinusOutlined } from "@ant-design/icons";
|
||||||
import { Button, Input, message, TimePicker, Select, Switch } from "antd";
|
import { Button, Input, message, TimePicker, Select, Switch } from "antd";
|
||||||
import NavCommon from "@/components/NavCommon";
|
import NavCommon from "@/components/NavCommon";
|
||||||
import Layout from "@/components/Layout/Layout";
|
import Layout from "@/components/Layout/Layout";
|
||||||
import DeviceSelection from "@/components/DeviceSelection";
|
import DeviceSelection from "@/components/DeviceSelection";
|
||||||
|
import PoolSelection from "@/components/PoolSelection";
|
||||||
import {
|
import {
|
||||||
createContactImportTask,
|
createContactImportTask,
|
||||||
updateContactImportTask,
|
updateContactImportTask,
|
||||||
fetchContactImportTaskDetail,
|
fetchContactImportTaskDetail,
|
||||||
} from "./api";
|
} from "./api";
|
||||||
import { Allocation } from "./data";
|
import { Allocation } from "./data";
|
||||||
|
import { PoolSelectionItem } from "@/components/PoolSelection/data";
|
||||||
|
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
|
||||||
import style from "./index.module.scss";
|
import style from "./index.module.scss";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
@@ -27,7 +29,7 @@ const ContactImportForm: React.FC = () => {
|
|||||||
type: 6, // 任务类型,固定为6
|
type: 6, // 任务类型,固定为6
|
||||||
workbenchId: 1, // 默认工作台ID
|
workbenchId: 1, // 默认工作台ID
|
||||||
deviceGroups: [] as number[],
|
deviceGroups: [] as number[],
|
||||||
pools: [] as any[],
|
poolGroups: [] as number[],
|
||||||
num: 50,
|
num: 50,
|
||||||
clearContact: 0,
|
clearContact: 0,
|
||||||
remarkType: 0,
|
remarkType: 0,
|
||||||
@@ -35,7 +37,8 @@ const ContactImportForm: React.FC = () => {
|
|||||||
startTime: dayjs("09:00", "HH:mm"),
|
startTime: dayjs("09:00", "HH:mm"),
|
||||||
endTime: dayjs("21:00", "HH:mm"),
|
endTime: dayjs("21:00", "HH:mm"),
|
||||||
// 保留原有字段用于UI显示
|
// 保留原有字段用于UI显示
|
||||||
deviceGroupsOptions: [] as any[],
|
deviceGroupsOptions: [] as DeviceSelectionItem[],
|
||||||
|
poolGroupsOptions: [] as PoolSelectionItem[], // 流量池选择项
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理设备选择
|
// 处理设备选择
|
||||||
@@ -47,8 +50,17 @@ const ContactImportForm: React.FC = () => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 处理流量池选择
|
||||||
|
const handlePoolSelect = (selectedpoolGroups: PoolSelectionItem[]) => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
poolGroupsOptions: selectedpoolGroups,
|
||||||
|
poolGroups: selectedpoolGroups.map(pool => Number(pool.id)), // 提取流量池信息存储到pools数组
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
// 获取任务详情(编辑模式)
|
// 获取任务详情(编辑模式)
|
||||||
const loadTaskDetail = async () => {
|
const loadTaskDetail = useCallback(async () => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -59,6 +71,7 @@ const ContactImportForm: React.FC = () => {
|
|||||||
|
|
||||||
// 构造设备选择组件需要的数据格式
|
// 构造设备选择组件需要的数据格式
|
||||||
const deviceGroupsOptions = config.deviceGroupsOptions || [];
|
const deviceGroupsOptions = config.deviceGroupsOptions || [];
|
||||||
|
const poolGroupsOptions = config.poolGroupsOptions || [];
|
||||||
|
|
||||||
setFormData({
|
setFormData({
|
||||||
name: data.name || "",
|
name: data.name || "",
|
||||||
@@ -67,7 +80,7 @@ const ContactImportForm: React.FC = () => {
|
|||||||
workbenchId: config.workbenchId || 1,
|
workbenchId: config.workbenchId || 1,
|
||||||
deviceGroups:
|
deviceGroups:
|
||||||
deviceGroupsOptions.map((device: any) => device.id) || [],
|
deviceGroupsOptions.map((device: any) => device.id) || [],
|
||||||
pools: config.pools ? JSON.parse(JSON.stringify(config.pools)) : [],
|
poolGroups: config.poolGroups || [],
|
||||||
num: config.num || 50,
|
num: config.num || 50,
|
||||||
clearContact: config.clearContact || 0,
|
clearContact: config.clearContact || 0,
|
||||||
remarkType: config.remarkType || 0,
|
remarkType: config.remarkType || 0,
|
||||||
@@ -75,6 +88,7 @@ const ContactImportForm: React.FC = () => {
|
|||||||
startTime: config.startTime ? dayjs(config.startTime, "HH:mm") : null,
|
startTime: config.startTime ? dayjs(config.startTime, "HH:mm") : null,
|
||||||
endTime: config.endTime ? dayjs(config.endTime, "HH:mm") : null,
|
endTime: config.endTime ? dayjs(config.endTime, "HH:mm") : null,
|
||||||
deviceGroupsOptions,
|
deviceGroupsOptions,
|
||||||
|
poolGroupsOptions,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -84,7 +98,7 @@ const ContactImportForm: React.FC = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [id, navigate]);
|
||||||
|
|
||||||
// 更新表单数据
|
// 更新表单数据
|
||||||
const handleUpdateFormData = (data: Partial<typeof formData>) => {
|
const handleUpdateFormData = (data: Partial<typeof formData>) => {
|
||||||
@@ -125,7 +139,7 @@ const ContactImportForm: React.FC = () => {
|
|||||||
type: formData.type,
|
type: formData.type,
|
||||||
workbenchId: formData.workbenchId,
|
workbenchId: formData.workbenchId,
|
||||||
deviceGroups: formData.deviceGroups,
|
deviceGroups: formData.deviceGroups,
|
||||||
pools: JSON.parse(JSON.stringify(formData.pools)),
|
poolGroups: formData.poolGroups,
|
||||||
num: formData.num,
|
num: formData.num,
|
||||||
clearContact: formData.clearContact,
|
clearContact: formData.clearContact,
|
||||||
remarkType: formData.remarkType,
|
remarkType: formData.remarkType,
|
||||||
@@ -161,7 +175,7 @@ const ContactImportForm: React.FC = () => {
|
|||||||
type: 6,
|
type: 6,
|
||||||
workbenchId: 1,
|
workbenchId: 1,
|
||||||
deviceGroups: [],
|
deviceGroups: [],
|
||||||
pools: [],
|
poolGroups: [],
|
||||||
num: 50,
|
num: 50,
|
||||||
clearContact: 0,
|
clearContact: 0,
|
||||||
remarkType: 0,
|
remarkType: 0,
|
||||||
@@ -169,6 +183,7 @@ const ContactImportForm: React.FC = () => {
|
|||||||
startTime: dayjs("09:00", "HH:mm"),
|
startTime: dayjs("09:00", "HH:mm"),
|
||||||
endTime: dayjs("21:00", "HH:mm"),
|
endTime: dayjs("21:00", "HH:mm"),
|
||||||
deviceGroupsOptions: [],
|
deviceGroupsOptions: [],
|
||||||
|
poolGroupsOptions: [],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -176,7 +191,7 @@ const ContactImportForm: React.FC = () => {
|
|||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
loadTaskDetail();
|
loadTaskDetail();
|
||||||
}
|
}
|
||||||
}, [id, isEdit]);
|
}, [id, isEdit, loadTaskDetail]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
@@ -227,6 +242,17 @@ const ContactImportForm: React.FC = () => {
|
|||||||
<div className={style.counterTip}>选择要分配联系人的设备</div>
|
<div className={style.counterTip}>选择要分配联系人的设备</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={style.formItem}>
|
||||||
|
<div className={style.formLabel}>流量池选择</div>
|
||||||
|
<PoolSelection
|
||||||
|
selectedOptions={formData.poolGroupsOptions}
|
||||||
|
onSelect={handlePoolSelect}
|
||||||
|
placeholder="请选择流量池"
|
||||||
|
className={style.poolSelection}
|
||||||
|
/>
|
||||||
|
<div className={style.counterTip}>选择要导入的流量池</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={style.formItem}>
|
<div className={style.formItem}>
|
||||||
<div className={style.formLabel}>分配数量</div>
|
<div className={style.formLabel}>分配数量</div>
|
||||||
<div className={style.stepperContainer}>
|
<div className={style.stepperContainer}>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export interface ContactImportTaskConfig {
|
|||||||
id: number;
|
id: number;
|
||||||
workbenchId: number;
|
workbenchId: number;
|
||||||
devices: number[];
|
devices: number[];
|
||||||
pools: number[];
|
poolGroups: number[];
|
||||||
num: number;
|
num: number;
|
||||||
clearContact: number;
|
clearContact: number;
|
||||||
remarkType: number;
|
remarkType: number;
|
||||||
@@ -77,7 +77,7 @@ export interface CreateContactImportTaskData {
|
|||||||
type: number;
|
type: number;
|
||||||
config: {
|
config: {
|
||||||
devices: number[];
|
devices: number[];
|
||||||
pools: number[];
|
poolGroups: number[];
|
||||||
num: number;
|
num: number;
|
||||||
clearContact: number;
|
clearContact: number;
|
||||||
remarkType: number;
|
remarkType: number;
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export interface TrafficDistributionConfig {
|
|||||||
accountGroupsOptions: any[];
|
accountGroupsOptions: any[];
|
||||||
deviceGroups: any[];
|
deviceGroups: any[];
|
||||||
deviceGroupsOptions: any[];
|
deviceGroupsOptions: any[];
|
||||||
pools: any[];
|
poolGroups: any[];
|
||||||
exp: number;
|
exp: number;
|
||||||
createTime: string;
|
createTime: string;
|
||||||
updateTime: string;
|
updateTime: string;
|
||||||
@@ -58,7 +58,7 @@ export interface TrafficDistributionFormData {
|
|||||||
deviceGroupsOptions: any[];
|
deviceGroupsOptions: any[];
|
||||||
accountGroups: any[];
|
accountGroups: any[];
|
||||||
accountGroupsOptions: any[];
|
accountGroupsOptions: any[];
|
||||||
pools: any[];
|
poolGroups: any[];
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ const TrafficDistributionForm: React.FC = () => {
|
|||||||
deviceGroupsOptions: deviceGroupsOptions,
|
deviceGroupsOptions: deviceGroupsOptions,
|
||||||
accountGroups: accountGroupsOptions.map(v => v.id),
|
accountGroups: accountGroupsOptions.map(v => v.id),
|
||||||
accountGroupsOptions: accountGroupsOptions,
|
accountGroupsOptions: accountGroupsOptions,
|
||||||
pools: poolGroupsOptions.map(v => v.id),
|
poolGroups: poolGroupsOptions.map(v => v.id),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export interface DistributionRule {
|
|||||||
endTime: string;
|
endTime: string;
|
||||||
account: (string | number)[];
|
account: (string | number)[];
|
||||||
devices: string[];
|
devices: string[];
|
||||||
pools: string[];
|
poolGroups: string[];
|
||||||
exp: number;
|
exp: number;
|
||||||
createTime: string;
|
createTime: string;
|
||||||
updateTime: string;
|
updateTime: string;
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ const TrafficDistributionList: React.FC = () => {
|
|||||||
onClick={() => showPoolList(item)}
|
onClick={() => showPoolList(item)}
|
||||||
>
|
>
|
||||||
<div style={{ fontSize: 18, fontWeight: 600 }}>
|
<div style={{ fontSize: 18, fontWeight: 600 }}>
|
||||||
{item.config?.pools?.length || 0}
|
{item.config?.poolGroups?.length || 0}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 13, color: "#888" }}>流量池</div>
|
<div style={{ fontSize: 13, color: "#888" }}>流量池</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>存客宝</title>
|
<title>触客宝</title>
|
||||||
<style>
|
<style>
|
||||||
html {
|
html {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|||||||
120
Touchkebao/src/components/PowerNavtion/index.module.scss
Normal file
120
Touchkebao/src/components/PowerNavtion/index.module.scss
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* PowerNavigation 组件样式文件
|
||||||
|
*
|
||||||
|
* 按照图片设计,简洁的顶部导航栏
|
||||||
|
*/
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: #fff;
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 64px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.headerLeft {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
// 返回按钮样式
|
||||||
|
.backButton {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
background: #fff;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
padding: 0 16px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-color: #1890ff;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标题区域样式
|
||||||
|
.titleSection {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.header {
|
||||||
|
padding: 12px;
|
||||||
|
|
||||||
|
.headerLeft {
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.backButton {
|
||||||
|
height: 36px;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 0 12px;
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleSection {
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.header {
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
.headerLeft {
|
||||||
|
.backButton {
|
||||||
|
span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleSection {
|
||||||
|
.subtitle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
64
Touchkebao/src/components/PowerNavtion/index.tsx
Normal file
64
Touchkebao/src/components/PowerNavtion/index.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Button } from "antd";
|
||||||
|
import { ArrowLeftOutlined } from "@ant-design/icons";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import styles from "./index.module.scss";
|
||||||
|
|
||||||
|
// 简化的组件属性类型
|
||||||
|
export interface PowerNavigationProps {
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
showBackButton?: boolean;
|
||||||
|
backButtonText?: string;
|
||||||
|
onBackClick?: () => void;
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PowerNavigation: React.FC<PowerNavigationProps> = ({
|
||||||
|
title = "触客宝",
|
||||||
|
subtitle,
|
||||||
|
showBackButton = true,
|
||||||
|
backButtonText = "返回功能中心",
|
||||||
|
onBackClick,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
}) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// 处理返回按钮点击
|
||||||
|
const handleBackClick = () => {
|
||||||
|
if (onBackClick) {
|
||||||
|
onBackClick();
|
||||||
|
} else {
|
||||||
|
navigate(-1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles.header} ${className || ""}`} style={style}>
|
||||||
|
<div className={styles.headerLeft}>
|
||||||
|
{/* 返回按钮 */}
|
||||||
|
{showBackButton && (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="large"
|
||||||
|
icon={<ArrowLeftOutlined />}
|
||||||
|
onClick={handleBackClick}
|
||||||
|
className={styles.backButton}
|
||||||
|
>
|
||||||
|
{backButtonText}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 标题区域 */}
|
||||||
|
<div className={styles.titleSection}>
|
||||||
|
<span className={styles.title}>{title}</span>
|
||||||
|
{subtitle && <span className={styles.subtitle}>{subtitle}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PowerNavigation;
|
||||||
30
Touchkebao/src/components/PowerNavtion/types.ts
Normal file
30
Touchkebao/src/components/PowerNavtion/types.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PowerNavigation 组件类型定义文件
|
||||||
|
*
|
||||||
|
* 简化版本,按照图片设计
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PowerNavigation 组件属性类型
|
||||||
|
*/
|
||||||
|
export interface PowerNavigationProps {
|
||||||
|
/** 页面标题 */
|
||||||
|
title?: string;
|
||||||
|
/** 页面副标题 */
|
||||||
|
subtitle?: string;
|
||||||
|
/** 是否显示返回按钮 */
|
||||||
|
showBackButton?: boolean;
|
||||||
|
/** 返回按钮文本 */
|
||||||
|
backButtonText?: string;
|
||||||
|
/** 返回按钮点击事件 */
|
||||||
|
onBackClick?: () => void;
|
||||||
|
/** 自定义CSS类名 */
|
||||||
|
className?: string;
|
||||||
|
/** 自定义样式对象 */
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出所有类型
|
||||||
|
export type { PowerNavigationProps as default };
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import request from "@/api/request2";
|
import request from "@/api/request";
|
||||||
|
import request2 from "@/api/request2";
|
||||||
import {
|
import {
|
||||||
MessageData,
|
MessageData,
|
||||||
ChatHistoryResponse,
|
ChatHistoryResponse,
|
||||||
@@ -11,15 +12,21 @@ import {
|
|||||||
ChatSettings,
|
ChatSettings,
|
||||||
} from "./data";
|
} from "./data";
|
||||||
|
|
||||||
|
//获取好友接待配置
|
||||||
|
export function getFriendInjectConfig(params) {
|
||||||
|
return request("/v1/kefu/ai/friend/get", params, "GET");
|
||||||
|
}
|
||||||
//读取聊天信息
|
//读取聊天信息
|
||||||
//kf.quwanzhi.com:9991/api/WechatFriend/clearUnreadCount
|
|
||||||
|
|
||||||
export function WechatGroup(params) {
|
export function WechatGroup(params) {
|
||||||
return request("/api/WechatGroup/list", params, "GET");
|
return request2("/api/WechatGroup/list", params, "GET");
|
||||||
}
|
}
|
||||||
|
|
||||||
//获取聊天记录-1 清除未读
|
//获取聊天记录-1 清除未读
|
||||||
export function clearUnreadCount(params) {
|
export function clearUnreadCount1(params) {
|
||||||
|
return request("/v1/kefu/message/readMessage", params, "GET");
|
||||||
|
}
|
||||||
|
export function clearUnreadCount2(params) {
|
||||||
return request("/api/WechatFriend/clearUnreadCount", params, "PUT");
|
return request("/api/WechatFriend/clearUnreadCount", params, "PUT");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,12 +55,12 @@ export function getChatroomMessages(params: {
|
|||||||
Count: number;
|
Count: number;
|
||||||
olderData: boolean;
|
olderData: boolean;
|
||||||
}) {
|
}) {
|
||||||
return request("/api/ChatroomMessage/SearchMessage", params, "GET");
|
return request2("/api/ChatroomMessage/SearchMessage", params, "GET");
|
||||||
}
|
}
|
||||||
|
|
||||||
//获取群列表
|
//获取群列表
|
||||||
export function getGroupList(params: { prevId: number; count: number }) {
|
export function getGroupList(params: { prevId: number; count: number }) {
|
||||||
return request(
|
return request2(
|
||||||
"/api/wechatChatroom/listExcludeMembersByPage?",
|
"/api/wechatChatroom/listExcludeMembersByPage?",
|
||||||
params,
|
params,
|
||||||
"GET",
|
"GET",
|
||||||
@@ -62,7 +69,7 @@ export function getGroupList(params: { prevId: number; count: number }) {
|
|||||||
|
|
||||||
//获取群成员
|
//获取群成员
|
||||||
export function getGroupMembers(params: { id: number }) {
|
export function getGroupMembers(params: { id: number }) {
|
||||||
return request(
|
return request2(
|
||||||
"/api/WechatChatroom/listMembersByWechatChatroomId",
|
"/api/WechatChatroom/listMembersByWechatChatroomId",
|
||||||
params,
|
params,
|
||||||
"GET",
|
"GET",
|
||||||
@@ -71,7 +78,7 @@ export function getGroupMembers(params: { id: number }) {
|
|||||||
|
|
||||||
//触客宝登陆
|
//触客宝登陆
|
||||||
export function loginWithToken(params: any) {
|
export function loginWithToken(params: any) {
|
||||||
return request(
|
return request2(
|
||||||
"/token",
|
"/token",
|
||||||
params,
|
params,
|
||||||
"POST",
|
"POST",
|
||||||
@@ -86,17 +93,17 @@ export function loginWithToken(params: any) {
|
|||||||
|
|
||||||
// 获取触客宝用户信息
|
// 获取触客宝用户信息
|
||||||
export function getChuKeBaoUserInfo() {
|
export function getChuKeBaoUserInfo() {
|
||||||
return request("/api/account/self", {}, "GET");
|
return request2("/api/account/self", {}, "GET");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取联系人列表
|
// 获取联系人列表
|
||||||
export const getContactList = (params: { prevId: number; count: number }) => {
|
export const getContactList = (params: { prevId: number; count: number }) => {
|
||||||
return request("/api/wechatFriend/list", params, "GET");
|
return request2("/api/wechatFriend/list", params, "GET");
|
||||||
};
|
};
|
||||||
|
|
||||||
//获取控制终端列表
|
//获取控制终端列表
|
||||||
export const getControlTerminalList = params => {
|
export const getControlTerminalList = params => {
|
||||||
return request("/api/wechataccount", params, "GET");
|
return request2("/api/wechataccount", params, "GET");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取聊天历史
|
// 获取聊天历史
|
||||||
@@ -105,7 +112,7 @@ export const getChatHistory = (
|
|||||||
page: number = 1,
|
page: number = 1,
|
||||||
pageSize: number = 50,
|
pageSize: number = 50,
|
||||||
): Promise<ChatHistoryResponse> => {
|
): Promise<ChatHistoryResponse> => {
|
||||||
return request(`/v1/chats/${chatId}/messages`, { page, pageSize }, "GET");
|
return request2(`/v1/chats/${chatId}/messages`, { page, pageSize }, "GET");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 发送消息
|
// 发送消息
|
||||||
@@ -114,7 +121,7 @@ export const sendMessage = (
|
|||||||
content: string,
|
content: string,
|
||||||
type: MessageType = MessageType.TEXT,
|
type: MessageType = MessageType.TEXT,
|
||||||
): Promise<MessageData> => {
|
): Promise<MessageData> => {
|
||||||
return request(`/v1/chats/${chatId}/messages`, { content, type }, "POST");
|
return request2(`/v1/chats/${chatId}/messages`, { content, type }, "POST");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 发送文件消息
|
// 发送文件消息
|
||||||
@@ -126,17 +133,17 @@ export const sendFileMessage = (
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
formData.append("type", type);
|
formData.append("type", type);
|
||||||
return request(`/v1/chats/${chatId}/messages/file`, formData, "POST");
|
return request2(`/v1/chats/${chatId}/messages/file`, formData, "POST");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 标记消息为已读
|
// 标记消息为已读
|
||||||
export const markMessageAsRead = (messageId: string): Promise<void> => {
|
export const markMessageAsRead = (messageId: string): Promise<void> => {
|
||||||
return request(`/v1/messages/${messageId}/read`, {}, "PUT");
|
return request2(`/v1/messages/${messageId}/read`, {}, "PUT");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 标记聊天为已读
|
// 标记聊天为已读
|
||||||
export const markChatAsRead = (chatId: string): Promise<void> => {
|
export const markChatAsRead = (chatId: string): Promise<void> => {
|
||||||
return request(`/v1/chats/${chatId}/read`, {}, "PUT");
|
return request2(`/v1/chats/${chatId}/read`, {}, "PUT");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加群组成员
|
// 添加群组成员
|
||||||
@@ -144,7 +151,7 @@ export const addGroupMembers = (
|
|||||||
groupId: string,
|
groupId: string,
|
||||||
memberIds: string[],
|
memberIds: string[],
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
return request(`/v1/groups/${groupId}/members`, { memberIds }, "POST");
|
return request2(`/v1/groups/${groupId}/members`, { memberIds }, "POST");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 移除群组成员
|
// 移除群组成员
|
||||||
@@ -152,34 +159,34 @@ export const removeGroupMembers = (
|
|||||||
groupId: string,
|
groupId: string,
|
||||||
memberIds: string[],
|
memberIds: string[],
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
return request(`/v1/groups/${groupId}/members`, { memberIds }, "DELETE");
|
return request2(`/v1/groups/${groupId}/members`, { memberIds }, "DELETE");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取在线状态
|
// 获取在线状态
|
||||||
export const getOnlineStatus = (userId: string): Promise<OnlineStatus> => {
|
export const getOnlineStatus = (userId: string): Promise<OnlineStatus> => {
|
||||||
return request(`/v1/users/${userId}/status`, {}, "GET");
|
return request2(`/v1/users/${userId}/status`, {}, "GET");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取消息状态
|
// 获取消息状态
|
||||||
export const getMessageStatus = (messageId: string): Promise<MessageStatus> => {
|
export const getMessageStatus = (messageId: string): Promise<MessageStatus> => {
|
||||||
return request(`/v1/messages/${messageId}/status`, {}, "GET");
|
return request2(`/v1/messages/${messageId}/status`, {}, "GET");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 上传文件
|
// 上传文件
|
||||||
export const uploadFile = (file: File): Promise<FileUploadResponse> => {
|
export const uploadFile = (file: File): Promise<FileUploadResponse> => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
return request("/v1/upload", formData, "POST");
|
return request2("/v1/upload", formData, "POST");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取表情包列表
|
// 获取表情包列表
|
||||||
export const getEmojiList = (): Promise<EmojiData[]> => {
|
export const getEmojiList = (): Promise<EmojiData[]> => {
|
||||||
return request("/v1/emojis", {}, "GET");
|
return request2("/v1/emojis", {}, "GET");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取快捷回复列表
|
// 获取快捷回复列表
|
||||||
export const getQuickReplies = (): Promise<QuickReply[]> => {
|
export const getQuickReplies = (): Promise<QuickReply[]> => {
|
||||||
return request("/v1/quick-replies", {}, "GET");
|
return request2("/v1/quick-replies", {}, "GET");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加快捷回复
|
// 添加快捷回复
|
||||||
@@ -187,49 +194,49 @@ export const addQuickReply = (data: {
|
|||||||
content: string;
|
content: string;
|
||||||
category: string;
|
category: string;
|
||||||
}): Promise<QuickReply> => {
|
}): Promise<QuickReply> => {
|
||||||
return request("/v1/quick-replies", data, "POST");
|
return request2("/v1/quick-replies", data, "POST");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 删除快捷回复
|
// 删除快捷回复
|
||||||
export const deleteQuickReply = (id: string): Promise<void> => {
|
export const deleteQuickReply = (id: string): Promise<void> => {
|
||||||
return request(`/v1/quick-replies/${id}`, {}, "DELETE");
|
return request2(`/v1/quick-replies/${id}`, {}, "DELETE");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取聊天设置
|
// 获取聊天设置
|
||||||
export const getChatSettings = (): Promise<ChatSettings> => {
|
export const getChatSettings = (): Promise<ChatSettings> => {
|
||||||
return request("/v1/chat/settings", {}, "GET");
|
return request2("/v1/chat/settings", {}, "GET");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 更新聊天设置
|
// 更新聊天设置
|
||||||
export const updateChatSettings = (
|
export const updateChatSettings = (
|
||||||
settings: Partial<ChatSettings>,
|
settings: Partial<ChatSettings>,
|
||||||
): Promise<ChatSettings> => {
|
): Promise<ChatSettings> => {
|
||||||
return request("/v1/chat/settings", settings, "PUT");
|
return request2("/v1/chat/settings", settings, "PUT");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 删除聊天会话
|
// 删除聊天会话
|
||||||
export const deleteChatSession = (chatId: string): Promise<void> => {
|
export const deleteChatSession = (chatId: string): Promise<void> => {
|
||||||
return request(`/v1/chats/${chatId}`, {}, "DELETE");
|
return request2(`/v1/chats/${chatId}`, {}, "DELETE");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 置顶聊天会话
|
// 置顶聊天会话
|
||||||
export const pinChatSession = (chatId: string): Promise<void> => {
|
export const pinChatSession = (chatId: string): Promise<void> => {
|
||||||
return request(`/v1/chats/${chatId}/pin`, {}, "PUT");
|
return request2(`/v1/chats/${chatId}/pin`, {}, "PUT");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 取消置顶聊天会话
|
// 取消置顶聊天会话
|
||||||
export const unpinChatSession = (chatId: string): Promise<void> => {
|
export const unpinChatSession = (chatId: string): Promise<void> => {
|
||||||
return request(`/v1/chats/${chatId}/unpin`, {}, "PUT");
|
return request2(`/v1/chats/${chatId}/unpin`, {}, "PUT");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 静音聊天会话
|
// 静音聊天会话
|
||||||
export const muteChatSession = (chatId: string): Promise<void> => {
|
export const muteChatSession = (chatId: string): Promise<void> => {
|
||||||
return request(`/v1/chats/${chatId}/mute`, {}, "PUT");
|
return request2(`/v1/chats/${chatId}/mute`, {}, "PUT");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 取消静音聊天会话
|
// 取消静音聊天会话
|
||||||
export const unmuteChatSession = (chatId: string): Promise<void> => {
|
export const unmuteChatSession = (chatId: string): Promise<void> => {
|
||||||
return request(`/v1/chats/${chatId}/unmute`, {}, "PUT");
|
return request2(`/v1/chats/${chatId}/unmute`, {}, "PUT");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 转发消息
|
// 转发消息
|
||||||
@@ -237,10 +244,10 @@ export const forwardMessage = (
|
|||||||
messageId: string,
|
messageId: string,
|
||||||
targetChatIds: string[],
|
targetChatIds: string[],
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
return request("/v1/messages/forward", { messageId, targetChatIds }, "POST");
|
return request2("/v1/messages/forward", { messageId, targetChatIds }, "POST");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 撤回消息
|
// 撤回消息
|
||||||
export const recallMessage = (messageId: string): Promise<void> => {
|
export const recallMessage = (messageId: string): Promise<void> => {
|
||||||
return request(`/v1/messages/${messageId}/recall`, {}, "PUT");
|
return request2(`/v1/messages/${messageId}/recall`, {}, "PUT");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import {
|
|
||||||
BarChartOutlined,
|
|
||||||
RobotOutlined,
|
|
||||||
ThunderboltOutlined,
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
// 菜单项接口
|
|
||||||
export interface MenuItem {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
icon: React.ReactNode;
|
|
||||||
path?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 菜单列表数据
|
|
||||||
export const menuList: MenuItem[] = [
|
|
||||||
{
|
|
||||||
id: "wechat",
|
|
||||||
title: "AI智能客服",
|
|
||||||
icon: <RobotOutlined className="menuIcon" style={{ fontSize: 20 }} />,
|
|
||||||
path: "/pc/weChat",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "powerCenter",
|
|
||||||
title: "功能中心",
|
|
||||||
icon: <BarChartOutlined className="menuIcon" style={{ fontSize: 20 }} />,
|
|
||||||
path: "/pc/powerCenter",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 抽屉菜单配置数据
|
|
||||||
export const drawerMenuData = {
|
|
||||||
header: {
|
|
||||||
logoIcon: "✨",
|
|
||||||
appName: "触客宝",
|
|
||||||
appDesc: "AI智能营销系统",
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
balanceIcon: <ThunderboltOutlined size={20} />,
|
|
||||||
balanceLabel: "算力余额",
|
|
||||||
balanceValue: "9307.423",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// 导出默认配置
|
|
||||||
export default drawerMenuData;
|
|
||||||
@@ -4,15 +4,15 @@ import {
|
|||||||
MenuOutlined,
|
MenuOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
BellOutlined,
|
BellOutlined,
|
||||||
CloseOutlined,
|
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
UserSwitchOutlined,
|
UserSwitchOutlined,
|
||||||
ThunderboltOutlined,
|
ThunderboltOutlined,
|
||||||
|
SettingOutlined,
|
||||||
|
WechatOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
|
|
||||||
import { useUserStore } from "@/store/module/user";
|
import { useUserStore } from "@/store/module/user";
|
||||||
import { useNavigate, useLocation } from "react-router-dom";
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
import { drawerMenuData, menuList } from "./index.data";
|
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
|
|
||||||
const { Header } = Layout;
|
const { Header } = Layout;
|
||||||
@@ -22,26 +22,25 @@ interface NavCommonProps {
|
|||||||
onMenuClick?: () => void;
|
onMenuClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NavCommon: React.FC<NavCommonProps> = ({
|
const NavCommon: React.FC<NavCommonProps> = ({ title = "触客宝" }) => {
|
||||||
title = "触客宝",
|
|
||||||
onMenuClick,
|
|
||||||
}) => {
|
|
||||||
const [drawerVisible, setDrawerVisible] = useState(false);
|
|
||||||
const [messageDrawerVisible, setMessageDrawerVisible] = useState(false);
|
const [messageDrawerVisible, setMessageDrawerVisible] = useState(false);
|
||||||
const [messageCount] = useState(3); // 模拟消息数量
|
const [messageCount] = useState(3); // 模拟消息数量
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { user, logout } = useUserStore();
|
const { user, logout } = useUserStore();
|
||||||
|
|
||||||
// 处理菜单图标点击
|
// 处理菜单图标点击:在两个路由之间切换
|
||||||
const handleMenuClick = () => {
|
const handleMenuClick = () => {
|
||||||
setDrawerVisible(true);
|
const current = location.pathname;
|
||||||
onMenuClick?.();
|
if (current.startsWith("/pc/weChat")) {
|
||||||
|
navigate("/pc/powerCenter");
|
||||||
|
} else {
|
||||||
|
navigate("/pc/weChat");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理抽屉关闭
|
const isWeChat = () => {
|
||||||
const handleDrawerClose = () => {
|
return location.pathname.startsWith("/pc/weChat");
|
||||||
setDrawerVisible(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理消息中心点击
|
// 处理消息中心点击
|
||||||
@@ -87,75 +86,28 @@ const NavCommon: React.FC<NavCommonProps> = ({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 默认抽屉内容
|
|
||||||
const defaultDrawerContent = (
|
|
||||||
<div className={styles.drawerContent}>
|
|
||||||
<div className={styles.drawerHeader}>
|
|
||||||
<div className={styles.logoSection}>
|
|
||||||
<div className={styles.logoIcon}>
|
|
||||||
{drawerMenuData.header.logoIcon}
|
|
||||||
</div>
|
|
||||||
<div className={styles.logoText}>
|
|
||||||
<div className={styles.appName}>
|
|
||||||
{drawerMenuData.header.appName}
|
|
||||||
</div>
|
|
||||||
<div className={styles.appDesc}>
|
|
||||||
{drawerMenuData.header.appDesc}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span className={styles.anticon} onClick={handleDrawerClose}>
|
|
||||||
<CloseOutlined />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className={styles.drawerBody}>
|
|
||||||
<div className={styles.menuSection}>
|
|
||||||
{menuList.map((item, index) => {
|
|
||||||
const isActive = location.pathname === item.path;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={`${styles.menuItem} ${isActive ? styles.menuItemActive : ""}`}
|
|
||||||
onClick={() => {
|
|
||||||
if (item.path) {
|
|
||||||
navigate(item.path);
|
|
||||||
setDrawerVisible(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={styles.menuIcon}>{item.icon}</div>
|
|
||||||
<span>{item.title}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.drawerFooter}>
|
|
||||||
<div className={styles.balanceSection}>
|
|
||||||
<div className={styles.balanceIcon}>
|
|
||||||
<span className={styles.suanliIcon}>
|
|
||||||
{drawerMenuData.footer.balanceIcon}
|
|
||||||
</span>
|
|
||||||
{drawerMenuData.footer.balanceLabel}
|
|
||||||
</div>
|
|
||||||
<div className={styles.balanceText}>
|
|
||||||
{drawerMenuData.footer.balanceValue}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header className={styles.header}>
|
<Header className={styles.header}>
|
||||||
<div className={styles.headerLeft}>
|
<div className={styles.headerLeft}>
|
||||||
<Button
|
{!isWeChat() ? (
|
||||||
type="text"
|
<Button
|
||||||
icon={<MenuOutlined />}
|
type="text"
|
||||||
onClick={handleMenuClick}
|
size="large"
|
||||||
className={styles.menuButton}
|
icon={<WechatOutlined />}
|
||||||
/>
|
onClick={handleMenuClick}
|
||||||
|
className={styles.menuButton}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="large"
|
||||||
|
icon={<MenuOutlined />}
|
||||||
|
onClick={handleMenuClick}
|
||||||
|
className={styles.menuButton}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<span className={styles.title}>{title}</span>
|
<span className={styles.title}>{title}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -165,13 +117,16 @@ const NavCommon: React.FC<NavCommonProps> = ({
|
|||||||
<span className={styles.suanliIcon}>
|
<span className={styles.suanliIcon}>
|
||||||
<ThunderboltOutlined size={20} />
|
<ThunderboltOutlined size={20} />
|
||||||
</span>
|
</span>
|
||||||
9307.423
|
{user?.tokens}
|
||||||
</span>
|
</span>
|
||||||
<div className={styles.messageButton} onClick={handleMessageClick}>
|
<div className={styles.messageButton} onClick={handleMessageClick}>
|
||||||
<Badge count={messageCount} size="small">
|
<Badge count={messageCount} size="small">
|
||||||
<BellOutlined style={{ fontSize: 20 }} />
|
<BellOutlined style={{ fontSize: 20 }} />
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
<div className={styles.messageButton}>
|
||||||
|
<SettingOutlined style={{ fontSize: 20 }} />
|
||||||
|
</div>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
menu={{ items: userMenuItems }}
|
menu={{ items: userMenuItems }}
|
||||||
placement="bottomRight"
|
placement="bottomRight"
|
||||||
@@ -190,17 +145,6 @@ const NavCommon: React.FC<NavCommonProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
<Drawer
|
|
||||||
placement="left"
|
|
||||||
onClose={handleDrawerClose}
|
|
||||||
open={drawerVisible}
|
|
||||||
width={300}
|
|
||||||
className={styles.drawer}
|
|
||||||
closable={false}
|
|
||||||
>
|
|
||||||
{defaultDrawerContent}
|
|
||||||
</Drawer>
|
|
||||||
|
|
||||||
<Drawer
|
<Drawer
|
||||||
title="通知中心"
|
title="通知中心"
|
||||||
placement="right"
|
placement="right"
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ export interface MessageListData {
|
|||||||
avatar?: string; // 头像
|
avatar?: string; // 头像
|
||||||
groupId: number; // 分组ID
|
groupId: number; // 分组ID
|
||||||
config?: {
|
config?: {
|
||||||
chat: boolean;
|
chat?: boolean;
|
||||||
|
unreadCount: number; // 未读消息数
|
||||||
}; // 配置信息
|
}; // 配置信息
|
||||||
labels?: string[]; // 标签列表
|
labels?: string[]; // 标签列表
|
||||||
unreadCount: number; // 未读消息数
|
|
||||||
|
|
||||||
// 联系人特有字段(当dataType为'contracts'时使用)
|
// 联系人特有字段(当dataType为'contracts'时使用)
|
||||||
wechatId?: string; // 微信ID
|
wechatId?: string; // 微信ID
|
||||||
@@ -113,10 +113,10 @@ export interface weChatGroup {
|
|||||||
chatroomAvatar: string;
|
chatroomAvatar: string;
|
||||||
groupId: number;
|
groupId: number;
|
||||||
config?: {
|
config?: {
|
||||||
chat: boolean;
|
chat?: boolean;
|
||||||
|
unreadCount: number;
|
||||||
};
|
};
|
||||||
labels?: string[];
|
labels?: string[];
|
||||||
unreadCount: number;
|
|
||||||
notice: string;
|
notice: string;
|
||||||
selfDisplyName: string;
|
selfDisplyName: string;
|
||||||
wechatChatroomId: number;
|
wechatChatroomId: number;
|
||||||
@@ -152,10 +152,11 @@ export interface ContractData {
|
|||||||
additionalPicture: string;
|
additionalPicture: string;
|
||||||
desc: string;
|
desc: string;
|
||||||
config?: {
|
config?: {
|
||||||
chat: boolean;
|
chat?: boolean;
|
||||||
|
unreadCount: number;
|
||||||
};
|
};
|
||||||
lastMessageTime: number;
|
lastMessageTime: number;
|
||||||
unreadCount: number;
|
|
||||||
duplicate: boolean;
|
duplicate: boolean;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
@@ -246,7 +247,9 @@ export interface ChatSession {
|
|||||||
avatar?: string;
|
avatar?: string;
|
||||||
lastMessage: string;
|
lastMessage: string;
|
||||||
lastTime: string;
|
lastTime: string;
|
||||||
unreadCount: number;
|
config: {
|
||||||
|
unreadCount: number;
|
||||||
|
};
|
||||||
online: boolean;
|
online: boolean;
|
||||||
members?: string[];
|
members?: string[];
|
||||||
pinned?: boolean;
|
pinned?: boolean;
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import PowerNavigation from "@/components/PowerNavtion";
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
|
|
||||||
const AiTraining: React.FC = () => {
|
const AiTraining: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.header}>
|
<PowerNavigation
|
||||||
<h1>AI模型训练</h1>
|
title="AI模型训练"
|
||||||
<p>自定义AI模型训练,打造专属智能客服助手</p>
|
subtitle="自定义AI模型训练,打造专属智能客服助手"
|
||||||
</div>
|
showBackButton={true}
|
||||||
|
backButtonText="返回功能中心"
|
||||||
|
/>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
{/* 功能内容待开发 */}
|
{/* 功能内容待开发 */}
|
||||||
<div className={styles.placeholder}>
|
<div className={styles.placeholder}>
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import PowerNavigation from "@/components/PowerNavtion";
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
|
|
||||||
const AutoGreeting: React.FC = () => {
|
const AutoGreeting: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.header}>
|
<PowerNavigation
|
||||||
<h1>自动打招呼</h1>
|
title="自动打招呼"
|
||||||
<p>智能识别新好友,自动发送个性化欢迎消息</p>
|
subtitle="智能识别新好友,自动发送个性化欢迎消息"
|
||||||
</div>
|
showBackButton={true}
|
||||||
|
backButtonText="返回功能中心"
|
||||||
|
/>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
{/* 功能内容待开发 */}
|
{/* 功能内容待开发 */}
|
||||||
<div className={styles.placeholder}>
|
<div className={styles.placeholder}>
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import PowerNavigation from "@/components/PowerNavtion";
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
|
|
||||||
const CommunicationRecord: React.FC = () => {
|
const CommunicationRecord: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.header}>
|
<PowerNavigation
|
||||||
<h1>沟通记录</h1>
|
title="沟通记录"
|
||||||
<p>完整记录客户沟通历史,支持多维度查询分析</p>
|
subtitle="完整记录客户沟通历史,支持多维度查询分析"
|
||||||
</div>
|
showBackButton={true}
|
||||||
|
backButtonText="返回功能中心"
|
||||||
|
/>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
{/* 功能内容待开发 */}
|
{/* 功能内容待开发 */}
|
||||||
<div className={styles.placeholder}>
|
<div className={styles.placeholder}>
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import PowerNavigation from "@/components/PowerNavtion";
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
|
|
||||||
const ContentManagement: React.FC = () => {
|
const ContentManagement: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.header}>
|
<PowerNavigation
|
||||||
<h1>内容管理</h1>
|
title="内容管理"
|
||||||
<p>素材管理、数据词汇库、关键词自动回复</p>
|
subtitle="素材管理、数据词汇库、关键词自动回复"
|
||||||
</div>
|
showBackButton={true}
|
||||||
|
backButtonText="返回功能中心"
|
||||||
|
/>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
{/* 功能内容待开发 */}
|
{/* 功能内容待开发 */}
|
||||||
<div className={styles.placeholder}>
|
<div className={styles.placeholder}>
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import PowerNavigation from "@/components/PowerNavtion";
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
|
|
||||||
const CustomerManagement: React.FC = () => {
|
const CustomerManagement: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.header}>
|
<PowerNavigation
|
||||||
<h1>客户好友管理</h1>
|
title="客户好友管理"
|
||||||
<p>统一管理客户信息和好友关系,提升服务效率</p>
|
subtitle="统一管理客户信息和好友关系,提升服务效率"
|
||||||
</div>
|
showBackButton={true}
|
||||||
|
backButtonText="返回功能中心"
|
||||||
|
/>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
{/* 功能内容待开发 */}
|
{/* 功能内容待开发 */}
|
||||||
<div className={styles.placeholder}>
|
<div className={styles.placeholder}>
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import PowerNavigation from "@/components/PowerNavtion";
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
|
|
||||||
const MomentsMarketing: React.FC = () => {
|
const MomentsMarketing: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.header}>
|
<PowerNavigation
|
||||||
<h1>朋友圈营销</h1>
|
title="朋友圈营销"
|
||||||
<p>AI智能生成朋友圈内容,提升品牌曝光度</p>
|
subtitle="AI智能生成朋友圈内容,提升品牌曝光度"
|
||||||
</div>
|
showBackButton={true}
|
||||||
|
backButtonText="返回功能中心"
|
||||||
|
/>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
{/* 功能内容待开发 */}
|
{/* 功能内容待开发 */}
|
||||||
<div className={styles.placeholder}>
|
<div className={styles.placeholder}>
|
||||||
|
|||||||
@@ -1,10 +1,3 @@
|
|||||||
.container {
|
|
||||||
padding: 24px;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
|
|
||||||
@@ -22,15 +15,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
|
||||||
min-height: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stepsContainer {
|
.stepsContainer {
|
||||||
margin-bottom: 32px;
|
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
background: #fafafa;
|
background: #fff;
|
||||||
border-radius: 8px;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.steps {
|
.steps {
|
||||||
@@ -39,7 +27,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stepContent {
|
.stepContent {
|
||||||
margin-bottom: 24px;
|
margin: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stepCard {
|
.stepCard {
|
||||||
@@ -172,7 +160,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 24px 0;
|
padding: 24px 0;
|
||||||
border-top: 1px solid #f0f0f0;
|
background: #fff;
|
||||||
|
|
||||||
.ant-btn {
|
.ant-btn {
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
|
import Layout from "@/components/Layout/LayoutFiexd";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Button,
|
Button,
|
||||||
@@ -29,6 +30,7 @@ import {
|
|||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import type { UploadFile } from "antd";
|
import type { UploadFile } from "antd";
|
||||||
|
import PowerNavigation from "@/components/PowerNavtion";
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
|
|
||||||
const { Step } = Steps;
|
const { Step } = Steps;
|
||||||
@@ -649,23 +651,25 @@ const PrecisionSend: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<Layout
|
||||||
<div className={styles.header}>
|
header={
|
||||||
<h1>精准群发</h1>
|
<>
|
||||||
<p>基于客户标签和行为数据进行精准群发</p>
|
<PowerNavigation
|
||||||
</div>
|
title="精准群发1"
|
||||||
|
subtitle="基于客户标签和行为数据进行精准群发"
|
||||||
<div className={styles.content}>
|
showBackButton={true}
|
||||||
<div className={styles.stepsContainer}>
|
backButtonText="返回功能中心"
|
||||||
<Steps current={currentStep} className={styles.steps}>
|
/>
|
||||||
<Step title="客户筛选" icon={<UserOutlined />} />
|
<div className={styles.stepsContainer}>
|
||||||
<Step title="消息内容" icon={<MessageOutlined />} />
|
<Steps current={currentStep} className={styles.steps}>
|
||||||
<Step title="发送设置" icon={<SendOutlined />} />
|
<Step title="客户筛选" icon={<UserOutlined />} />
|
||||||
</Steps>
|
<Step title="消息内容" icon={<MessageOutlined />} />
|
||||||
</div>
|
<Step title="发送设置" icon={<SendOutlined />} />
|
||||||
|
</Steps>
|
||||||
<div className={styles.stepContent}>{renderStepContent()}</div>
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
footer={
|
||||||
<div className={styles.stepActions}>
|
<div className={styles.stepActions}>
|
||||||
<Space>
|
<Space>
|
||||||
{currentStep > 0 && (
|
{currentStep > 0 && (
|
||||||
@@ -710,8 +714,12 @@ const PrecisionSend: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.stepContent}>{renderStepContent()}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Layout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import PowerNavigation from "@/components/PowerNavtion";
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
|
|
||||||
const SopSend: React.FC = () => {
|
const SopSend: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.header}>
|
<PowerNavigation
|
||||||
<h1>SOP群发</h1>
|
title="SOP群发"
|
||||||
<p>使用触客宝SOP标准化流程进行批量消息发送</p>
|
subtitle="使用触客宝SOP标准化流程进行批量消息发送"
|
||||||
</div>
|
showBackButton={true}
|
||||||
|
backButtonText="返回功能中心"
|
||||||
|
/>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
{/* 功能内容待开发 */}
|
{/* 功能内容待开发 */}
|
||||||
<div className={styles.placeholder}>
|
<div className={styles.placeholder}>
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import PowerNavigation from "@/components/PowerNavtion";
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
|
|
||||||
const TagManagement: React.FC = () => {
|
const TagManagement: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.header}>
|
<PowerNavigation
|
||||||
<h1>标签管理</h1>
|
title="标签管理"
|
||||||
<p>智能客户标签分类,精准用户画像分析</p>
|
subtitle="智能客户标签分类,精准用户画像分析"
|
||||||
</div>
|
showBackButton={true}
|
||||||
|
backButtonText="返回功能中心"
|
||||||
|
/>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
{/* 功能内容待开发 */}
|
{/* 功能内容待开发 */}
|
||||||
<div className={styles.placeholder}>
|
<div className={styles.placeholder}>
|
||||||
|
|||||||
@@ -11,16 +11,46 @@ import {
|
|||||||
QuickReply,
|
QuickReply,
|
||||||
ChatSettings,
|
ChatSettings,
|
||||||
} from "./data";
|
} from "./data";
|
||||||
|
//流量池
|
||||||
|
|
||||||
|
//好友接待配置
|
||||||
|
export function setFriendInjectConfig(params) {
|
||||||
|
return request("/v1/kefu/ai/friend/set", params, "POST");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTrafficPoolList() {
|
||||||
|
return request(
|
||||||
|
"/v1/traffic/pool/getPackage",
|
||||||
|
{
|
||||||
|
page: 1,
|
||||||
|
limit: 9999,
|
||||||
|
},
|
||||||
|
"GET",
|
||||||
|
);
|
||||||
|
}
|
||||||
// 好友列表
|
// 好友列表
|
||||||
export function getWechatFriendList(params) {
|
export function getContactList(params) {
|
||||||
return request("/v1/kefu/wechatFriend/list", params, "GET");
|
return request("/v1/kefu/wechatFriend/list", params, "GET");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 群列表
|
// 群列表
|
||||||
export function getWechatChatroomList(params) {
|
export function getGroupList(params) {
|
||||||
return request("/v1/kefu/wechatChatroom/list", params, "GET");
|
return request("/v1/kefu/wechatChatroom/list", params, "GET");
|
||||||
}
|
}
|
||||||
|
//==============-原接口=================
|
||||||
|
// 获取联系人列表
|
||||||
|
// export const getContactList = (params: { prevId: number; count: number }) => {
|
||||||
|
// return request2("/api/wechatFriend/list", params, "GET");
|
||||||
|
// };
|
||||||
|
|
||||||
|
// //获取群列表
|
||||||
|
// export function getGroupList(params: { prevId: number; count: number }) {
|
||||||
|
// return request2(
|
||||||
|
// "/api/wechatChatroom/listExcludeMembersByPage?",
|
||||||
|
// params,
|
||||||
|
// "GET",
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
//群、好友聊天记录列表
|
//群、好友聊天记录列表
|
||||||
export function getMessageList() {
|
export function getMessageList() {
|
||||||
@@ -33,7 +63,6 @@ export function getAgentList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//读取聊天信息
|
//读取聊天信息
|
||||||
//kf.quwanzhi.com:9991/api/WechatFriend/clearUnreadCount
|
|
||||||
function jsonToQueryString(json) {
|
function jsonToQueryString(json) {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
for (const key in json) {
|
for (const key in json) {
|
||||||
@@ -80,14 +109,9 @@ export function WechatGroup(params) {
|
|||||||
return request2("/api/WechatGroup/list", params, "GET");
|
return request2("/api/WechatGroup/list", params, "GET");
|
||||||
}
|
}
|
||||||
|
|
||||||
//获取聊天记录-1 清除未读
|
|
||||||
export function clearUnreadCount(params) {
|
|
||||||
return request2("/api/WechatFriend/clearUnreadCount", params, "PUT");
|
|
||||||
}
|
|
||||||
|
|
||||||
//更新配置
|
//更新配置
|
||||||
export function updateConfig(params) {
|
export function updateConfig(params) {
|
||||||
return request2("/api/WechatFriend/updateConfig", params, "PUT");
|
return request("/api/WechatFriend/updateConfig", params, "PUT");
|
||||||
}
|
}
|
||||||
//获取聊天记录-2 获取列表
|
//获取聊天记录-2 获取列表
|
||||||
export function getChatMessages(params: {
|
export function getChatMessages(params: {
|
||||||
@@ -99,7 +123,7 @@ export function getChatMessages(params: {
|
|||||||
Count: number;
|
Count: number;
|
||||||
olderData: boolean;
|
olderData: boolean;
|
||||||
}) {
|
}) {
|
||||||
return request2("/api/FriendMessage/SearchMessage", params, "GET");
|
return request("/api/FriendMessage/SearchMessage", params, "GET");
|
||||||
}
|
}
|
||||||
export function getChatroomMessages(params: {
|
export function getChatroomMessages(params: {
|
||||||
wechatAccountId: number;
|
wechatAccountId: number;
|
||||||
@@ -113,15 +137,6 @@ export function getChatroomMessages(params: {
|
|||||||
return request2("/api/ChatroomMessage/SearchMessage", params, "GET");
|
return request2("/api/ChatroomMessage/SearchMessage", params, "GET");
|
||||||
}
|
}
|
||||||
|
|
||||||
//获取群列表
|
|
||||||
export function getGroupList(params: { prevId: number; count: number }) {
|
|
||||||
return request2(
|
|
||||||
"/api/wechatChatroom/listExcludeMembersByPage?",
|
|
||||||
params,
|
|
||||||
"GET",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
//获取群成员
|
//获取群成员
|
||||||
export function getGroupMembers(params: { id: number }) {
|
export function getGroupMembers(params: { id: number }) {
|
||||||
return request2(
|
return request2(
|
||||||
@@ -151,11 +166,6 @@ export function getChuKeBaoUserInfo() {
|
|||||||
return request2("/api/account/self", {}, "GET");
|
return request2("/api/account/self", {}, "GET");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取联系人列表
|
|
||||||
export const getContactList = (params: { prevId: number; count: number }) => {
|
|
||||||
return request2("/api/wechatFriend/list", params, "GET");
|
|
||||||
};
|
|
||||||
|
|
||||||
//获取控制终端列表
|
//获取控制终端列表
|
||||||
export const getControlTerminalList = params => {
|
export const getControlTerminalList = params => {
|
||||||
return request2("/api/wechataccount", params, "GET");
|
return request2("/api/wechataccount", params, "GET");
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
|
|||||||
|
|
||||||
const handleSend = async () => {
|
const handleSend = async () => {
|
||||||
if (!inputValue.trim()) return;
|
if (!inputValue.trim()) return;
|
||||||
console.log("发送消息", contract);
|
|
||||||
const params = {
|
const params = {
|
||||||
wechatAccountId: contract.wechatAccountId,
|
wechatAccountId: contract.wechatAccountId,
|
||||||
wechatChatroomId: contract?.chatroomId ? contract.id : 0,
|
wechatChatroomId: contract?.chatroomId ? contract.id : 0,
|
||||||
|
|||||||
@@ -3,241 +3,4 @@
|
|||||||
border-left: 1px solid #e8e8e8;
|
border-left: 1px solid #e8e8e8;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
.profileContainer {
|
|
||||||
padding: 16px;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profileHeader {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
|
|
||||||
.closeButton {
|
|
||||||
color: #8c8c8c;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #262626;
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.profileBasic {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
padding-bottom: 24px;
|
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
|
|
||||||
.profileInfo {
|
|
||||||
margin-top: 16px;
|
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.profileNickname {
|
|
||||||
margin: 0 0 8px 0;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #262626;
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profileRemark {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
|
|
||||||
.remarkText {
|
|
||||||
color: #8c8c8c;
|
|
||||||
font-size: 14px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #1890ff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.profileStatus {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 6px;
|
|
||||||
color: #52c41a;
|
|
||||||
font-size: 14px;
|
|
||||||
|
|
||||||
.statusDot {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #52c41a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.profileCard {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03);
|
|
||||||
|
|
||||||
:global(.ant-card-head) {
|
|
||||||
padding: 0 16px;
|
|
||||||
min-height: 40px;
|
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
|
|
||||||
:global(.ant-card-head-title) {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #262626;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.ant-card-body) {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infoItem {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infoIcon {
|
|
||||||
color: #8c8c8c;
|
|
||||||
margin-right: 8px;
|
|
||||||
width: 16px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infoLabel {
|
|
||||||
color: #8c8c8c;
|
|
||||||
font-size: 14px;
|
|
||||||
width: 60px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infoValue {
|
|
||||||
color: #262626;
|
|
||||||
font-size: 14px;
|
|
||||||
flex: 1;
|
|
||||||
word-break: break-all;
|
|
||||||
|
|
||||||
// 备注编辑区域样式
|
|
||||||
:global(.ant-input) {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.ant-btn) {
|
|
||||||
font-size: 12px;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tagsContainer {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
:global(.ant-tag) {
|
|
||||||
margin: 0;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bioText {
|
|
||||||
margin: 0;
|
|
||||||
color: #595959;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.groupManagement {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.groupMemberList {
|
|
||||||
.groupMember {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
margin-left: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #262626;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.profileActions {
|
|
||||||
margin-top: auto;
|
|
||||||
padding-top: 16px;
|
|
||||||
|
|
||||||
:global(.ant-btn) {
|
|
||||||
border-radius: 6px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 响应式设计
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.profileSider {
|
|
||||||
width: 280px !important;
|
|
||||||
|
|
||||||
.profileContainer {
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profileBasic {
|
|
||||||
.profileInfo {
|
|
||||||
.profileNickname {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.profileCard {
|
|
||||||
:global(.ant-card-body) {
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infoItem {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
|
|
||||||
.infoLabel {
|
|
||||||
width: 50px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infoValue {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
// 朋友圈相关的API接口
|
||||||
|
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
||||||
|
|
||||||
|
// 朋友圈请求参数接口
|
||||||
|
export interface FetchMomentParams {
|
||||||
|
wechatAccountId: number;
|
||||||
|
wechatFriendId?: number;
|
||||||
|
createTimeSec?: number;
|
||||||
|
prevSnsId?: number;
|
||||||
|
count?: number;
|
||||||
|
isTimeline?: boolean;
|
||||||
|
seq?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取朋友圈数据
|
||||||
|
export const fetchFriendsCircleData = async (params: FetchMomentParams) => {
|
||||||
|
const { sendCommand } = useWebSocketStore.getState();
|
||||||
|
sendCommand("CmdFetchMoment", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 点赞朋友圈
|
||||||
|
export const likeMoment = async (params: {
|
||||||
|
wechatAccountId: number;
|
||||||
|
wechatFriendId?: number;
|
||||||
|
snsId: string;
|
||||||
|
seq?: number;
|
||||||
|
}) => {
|
||||||
|
const { sendCommand } = useWebSocketStore.getState();
|
||||||
|
const requestData = {
|
||||||
|
cmdType: "CmdMomentInteract",
|
||||||
|
momentInteractType: 1,
|
||||||
|
wechatAccountId: params.wechatAccountId,
|
||||||
|
wechatFriendId: params.wechatFriendId || 0,
|
||||||
|
snsId: params.snsId,
|
||||||
|
seq: params.seq || Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sendCommand("CmdMomentInteract", requestData);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消点赞
|
||||||
|
export const cancelLikeMoment = async (params: {
|
||||||
|
wechatAccountId: number;
|
||||||
|
wechatFriendId?: number;
|
||||||
|
snsId: string;
|
||||||
|
seq?: number;
|
||||||
|
}) => {
|
||||||
|
const { sendCommand } = useWebSocketStore.getState();
|
||||||
|
const requestData = {
|
||||||
|
cmdType: "CmdMomentCancelInteract",
|
||||||
|
optType: 1,
|
||||||
|
wechatAccountId: params.wechatAccountId,
|
||||||
|
wechatFriendId: params.wechatFriendId || 0,
|
||||||
|
CommentId2: "",
|
||||||
|
CommentTime: 0,
|
||||||
|
snsId: params.snsId,
|
||||||
|
seq: params.seq || Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sendCommand("CmdMomentCancelInteract", requestData);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 评论朋友圈
|
||||||
|
export const commentMoment = async (params: {
|
||||||
|
wechatAccountId: number;
|
||||||
|
wechatFriendId?: number;
|
||||||
|
snsId: string;
|
||||||
|
sendWord: string;
|
||||||
|
seq?: number;
|
||||||
|
}) => {
|
||||||
|
const { sendCommand } = useWebSocketStore.getState();
|
||||||
|
const requestData = {
|
||||||
|
cmdType: "CmdMomentInteract",
|
||||||
|
wechatAccountId: params.wechatAccountId,
|
||||||
|
wechatFriendId: params.wechatFriendId || 0,
|
||||||
|
snsId: params.snsId,
|
||||||
|
sendWord: params.sendWord,
|
||||||
|
momentInteractType: 2,
|
||||||
|
seq: params.seq || Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sendCommand("CmdMomentInteract", requestData);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 撤销评论
|
||||||
|
export const cancelCommentMoment = async (params: {
|
||||||
|
wechatAccountId: number;
|
||||||
|
wechatFriendId?: number;
|
||||||
|
snsId: string;
|
||||||
|
CommentTime: number;
|
||||||
|
seq?: number;
|
||||||
|
}) => {
|
||||||
|
const { sendCommand } = useWebSocketStore.getState();
|
||||||
|
const requestData = {
|
||||||
|
cmdType: "CmdMomentCancelInteract",
|
||||||
|
optType: 2,
|
||||||
|
wechatAccountId: params.wechatAccountId,
|
||||||
|
wechatFriendId: params.wechatFriendId || 0,
|
||||||
|
CommentId2: "",
|
||||||
|
CommentTime: params.CommentTime,
|
||||||
|
snsId: params.snsId,
|
||||||
|
seq: params.seq || Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sendCommand("CmdMomentCancelInteract", requestData);
|
||||||
|
};
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
// 朋友圈相关的API接口
|
||||||
|
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
||||||
|
|
||||||
|
// 朋友圈请求参数接口
|
||||||
|
export interface FetchMomentParams {
|
||||||
|
wechatAccountId: number;
|
||||||
|
wechatFriendId?: number;
|
||||||
|
createTimeSec?: number;
|
||||||
|
prevSnsId?: number;
|
||||||
|
count?: number;
|
||||||
|
isTimeline?: boolean;
|
||||||
|
seq?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取朋友圈数据
|
||||||
|
export const fetchFriendsCircleData = async (params: FetchMomentParams) => {
|
||||||
|
const { sendCommand } = useWebSocketStore.getState();
|
||||||
|
sendCommand("CmdFetchMoment", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 点赞朋友圈
|
||||||
|
export const likeMoment = async (params: {
|
||||||
|
wechatAccountId: number;
|
||||||
|
wechatFriendId?: number;
|
||||||
|
snsId: string;
|
||||||
|
seq?: number;
|
||||||
|
}) => {
|
||||||
|
const { sendCommand } = useWebSocketStore.getState();
|
||||||
|
const requestData = {
|
||||||
|
cmdType: "CmdMomentInteract",
|
||||||
|
momentInteractType: 1,
|
||||||
|
wechatAccountId: params.wechatAccountId,
|
||||||
|
wechatFriendId: params.wechatFriendId || 0,
|
||||||
|
snsId: params.snsId,
|
||||||
|
seq: params.seq || Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sendCommand("CmdMomentInteract", requestData);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消点赞
|
||||||
|
export const cancelLikeMoment = async (params: {
|
||||||
|
wechatAccountId: number;
|
||||||
|
wechatFriendId?: number;
|
||||||
|
snsId: string;
|
||||||
|
seq?: number;
|
||||||
|
}) => {
|
||||||
|
const { sendCommand } = useWebSocketStore.getState();
|
||||||
|
const requestData = {
|
||||||
|
cmdType: "CmdMomentCancelInteract",
|
||||||
|
optType: 1,
|
||||||
|
wechatAccountId: params.wechatAccountId,
|
||||||
|
wechatFriendId: params.wechatFriendId || 0,
|
||||||
|
CommentId2: "",
|
||||||
|
CommentTime: 0,
|
||||||
|
snsId: params.snsId,
|
||||||
|
seq: params.seq || Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sendCommand("CmdMomentCancelInteract", requestData);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 评论朋友圈
|
||||||
|
export const commentMoment = async (params: {
|
||||||
|
wechatAccountId: number;
|
||||||
|
wechatFriendId?: number;
|
||||||
|
snsId: string;
|
||||||
|
sendWord: string;
|
||||||
|
seq?: number;
|
||||||
|
}) => {
|
||||||
|
const { sendCommand } = useWebSocketStore.getState();
|
||||||
|
const requestData = {
|
||||||
|
cmdType: "CmdMomentInteract",
|
||||||
|
wechatAccountId: params.wechatAccountId,
|
||||||
|
wechatFriendId: params.wechatFriendId || 0,
|
||||||
|
snsId: params.snsId,
|
||||||
|
sendWord: params.sendWord,
|
||||||
|
momentInteractType: 2,
|
||||||
|
seq: params.seq || Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sendCommand("CmdMomentInteract", requestData);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 撤销评论
|
||||||
|
export const cancelCommentMoment = async (params: {
|
||||||
|
wechatAccountId: number;
|
||||||
|
wechatFriendId?: number;
|
||||||
|
snsId: string;
|
||||||
|
commentTime: number;
|
||||||
|
commentId2: number;
|
||||||
|
seq?: number;
|
||||||
|
}) => {
|
||||||
|
const { sendCommand } = useWebSocketStore.getState();
|
||||||
|
const requestData = {
|
||||||
|
cmdType: "CmdMomentCancelInteract",
|
||||||
|
optType: 2,
|
||||||
|
wechatAccountId: params.wechatAccountId,
|
||||||
|
wechatFriendId: params.wechatFriendId || 0,
|
||||||
|
CommentId2: params.commentId2,
|
||||||
|
CommentTime: params.commentTime,
|
||||||
|
snsId: params.snsId,
|
||||||
|
seq: params.seq || Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
sendCommand("CmdMomentCancelInteract", requestData);
|
||||||
|
};
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { Avatar, Button, Image, Spin, Input, message } from "antd";
|
||||||
|
import {
|
||||||
|
HeartOutlined,
|
||||||
|
MessageOutlined,
|
||||||
|
LoadingOutlined,
|
||||||
|
SendOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import {
|
||||||
|
CommentItem,
|
||||||
|
likeListItem,
|
||||||
|
FriendCardProps,
|
||||||
|
MomentListProps,
|
||||||
|
FriendsCircleItem,
|
||||||
|
} from "@/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.data";
|
||||||
|
import styles from "../index.module.scss";
|
||||||
|
import {
|
||||||
|
likeMoment,
|
||||||
|
cancelLikeMoment,
|
||||||
|
commentMoment,
|
||||||
|
cancelCommentMoment,
|
||||||
|
} from "./api";
|
||||||
|
import { comfirm } from "@/utils/common";
|
||||||
|
|
||||||
|
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||||
|
// 单个朋友圈项目组件
|
||||||
|
export const FriendCard: React.FC<FriendCardProps> = ({
|
||||||
|
monent,
|
||||||
|
isNotMy = false,
|
||||||
|
currentKf,
|
||||||
|
wechatFriendId,
|
||||||
|
formatTime,
|
||||||
|
}) => {
|
||||||
|
const content = monent?.momentEntity?.content || "";
|
||||||
|
const images = monent?.momentEntity?.resUrls || [];
|
||||||
|
const time = formatTime(monent.createTime);
|
||||||
|
const likesCount = monent?.likeList?.length || 0;
|
||||||
|
const commentsCount = monent?.commentList?.length || 0;
|
||||||
|
const { updateLikeMoment, updateComment } = useWeChatStore();
|
||||||
|
|
||||||
|
// 评论相关状态
|
||||||
|
const [showCommentInput, setShowCommentInput] = useState(false);
|
||||||
|
const [commentText, setCommentText] = useState("");
|
||||||
|
|
||||||
|
const handleLike = (moment: FriendsCircleItem) => {
|
||||||
|
console.log(currentKf);
|
||||||
|
|
||||||
|
//判断是否已经点赞了
|
||||||
|
const isLiked = moment?.likeList?.some(
|
||||||
|
(item: likeListItem) => item.wechatId === currentKf?.wechatId,
|
||||||
|
);
|
||||||
|
if (isLiked) {
|
||||||
|
cancelLikeMoment({
|
||||||
|
wechatAccountId: currentKf?.id || 0,
|
||||||
|
wechatFriendId: wechatFriendId || 0,
|
||||||
|
snsId: moment.snsId,
|
||||||
|
seq: Date.now(),
|
||||||
|
});
|
||||||
|
// 更新点赞
|
||||||
|
updateLikeMoment(
|
||||||
|
moment.snsId,
|
||||||
|
moment.likeList.filter(v => v.wechatId !== currentKf?.wechatId),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
likeMoment({
|
||||||
|
wechatAccountId: currentKf?.id || 0,
|
||||||
|
wechatFriendId: wechatFriendId || 0,
|
||||||
|
snsId: moment.snsId,
|
||||||
|
seq: Date.now(),
|
||||||
|
});
|
||||||
|
// 更新点赞
|
||||||
|
updateLikeMoment(moment.snsId, [
|
||||||
|
...moment.likeList,
|
||||||
|
{
|
||||||
|
createTime: Date.now(),
|
||||||
|
nickName: currentKf?.nickname || "",
|
||||||
|
wechatId: currentKf?.wechatId || "",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendComment = monent => {
|
||||||
|
if (!commentText.trim()) {
|
||||||
|
message.warning("请输入评论内容");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 调用发送评论的API
|
||||||
|
commentMoment({
|
||||||
|
wechatAccountId: currentKf?.id || 0,
|
||||||
|
wechatFriendId: wechatFriendId || 0,
|
||||||
|
snsId: monent.snsId,
|
||||||
|
sendWord: commentText,
|
||||||
|
seq: Date.now(),
|
||||||
|
});
|
||||||
|
// 更新评论
|
||||||
|
updateComment(monent.snsId, [
|
||||||
|
...monent.commentList,
|
||||||
|
{
|
||||||
|
commentArg: 0,
|
||||||
|
commentId1: Date.now(),
|
||||||
|
commentId2: Date.now(),
|
||||||
|
commentTime: Date.now(),
|
||||||
|
content: commentText,
|
||||||
|
nickName: currentKf?.nickname || "",
|
||||||
|
wechatId: currentKf?.wechatId || "",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
// 清空输入框并隐藏
|
||||||
|
setCommentText("");
|
||||||
|
setShowCommentInput(false);
|
||||||
|
message.success("评论发送成功");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteComment = (snsId: string, comment: CommentItem) => {
|
||||||
|
// TODO: 调用删除评论的API
|
||||||
|
comfirm("确定删除评论吗?").then(() => {
|
||||||
|
cancelCommentMoment({
|
||||||
|
wechatAccountId: currentKf?.id || 0,
|
||||||
|
wechatFriendId: wechatFriendId || 0,
|
||||||
|
snsId: snsId,
|
||||||
|
seq: Date.now(),
|
||||||
|
commentId2: comment.commentId2,
|
||||||
|
commentTime: comment.commentTime,
|
||||||
|
});
|
||||||
|
// 更新评论
|
||||||
|
const commentList = monent.commentList.filter(v => {
|
||||||
|
return !(
|
||||||
|
v.commentId2 == comment.commentId2 &&
|
||||||
|
v.wechatId == currentKf?.wechatId
|
||||||
|
);
|
||||||
|
});
|
||||||
|
updateComment(snsId, commentList);
|
||||||
|
message.success("评论删除成功");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.circleItem}>
|
||||||
|
{isNotMy && (
|
||||||
|
<div className={styles.avatar}>
|
||||||
|
<Avatar size={36} shape="square" src="/public/assets/face/1.png" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.itemWrap}>
|
||||||
|
<div className={styles.itemHeader}>
|
||||||
|
<div className={styles.userInfo}>
|
||||||
|
{/* <div className={styles.username}>{nickName}</div> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.itemContent}>
|
||||||
|
<div className={styles.contentText}>{content}</div>
|
||||||
|
{images && images.length > 0 && (
|
||||||
|
<div className={styles.imageContainer}>
|
||||||
|
{images.map((image, index) => (
|
||||||
|
<Image
|
||||||
|
key={index}
|
||||||
|
src={image}
|
||||||
|
className={styles.contentImage}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.itemFooter}>
|
||||||
|
<div className={styles.timeInfo}>{time}</div>
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<HeartOutlined />}
|
||||||
|
onClick={() => handleLike(monent)}
|
||||||
|
className={styles.actionButton}
|
||||||
|
>
|
||||||
|
{likesCount > 0 && <span>{likesCount}</span>}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<MessageOutlined />}
|
||||||
|
onClick={() => setShowCommentInput(!showCommentInput)}
|
||||||
|
className={styles.actionButton}
|
||||||
|
>
|
||||||
|
{commentsCount > 0 && <span>{commentsCount}</span>}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 点赞和评论区域 */}
|
||||||
|
{(monent?.likeList?.length > 0 || monent?.commentList?.length > 0) && (
|
||||||
|
<div className={styles.interactionArea}>
|
||||||
|
{/* 点赞列表 */}
|
||||||
|
{monent?.likeList?.length > 0 && (
|
||||||
|
<div className={styles.likeArea}>
|
||||||
|
<HeartOutlined className={styles.likeIcon} />
|
||||||
|
<span className={styles.likeList}>
|
||||||
|
{monent?.likeList?.map((like, index) => (
|
||||||
|
<span key={`${like.wechatId}-${like.createTime}-${index}`}>
|
||||||
|
{like.nickName}
|
||||||
|
{index < (monent?.likeList?.length || 0) - 1 && "、"}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 评论列表 */}
|
||||||
|
{monent?.commentList?.length > 0 && (
|
||||||
|
<div className={styles.commentArea}>
|
||||||
|
{monent?.commentList?.map(comment => (
|
||||||
|
<div
|
||||||
|
key={`${comment.wechatId}-${comment.commentTime}`}
|
||||||
|
className={styles.commentItem}
|
||||||
|
>
|
||||||
|
<span className={styles.commentUser}>
|
||||||
|
{comment.nickName}
|
||||||
|
</span>
|
||||||
|
<span className={styles.commentSeparator}>: </span>
|
||||||
|
<span className={styles.commentContent}>
|
||||||
|
{comment.content}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={() => handleDeleteComment(monent.snsId, comment)}
|
||||||
|
className={styles.deleteCommentBtn}
|
||||||
|
danger
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 评论输入框 */}
|
||||||
|
{showCommentInput && (
|
||||||
|
<div className={styles.commentInputArea}>
|
||||||
|
<Input.TextArea
|
||||||
|
value={commentText}
|
||||||
|
onChange={e => setCommentText(e.target.value)}
|
||||||
|
placeholder="写评论..."
|
||||||
|
autoSize={{ minRows: 2, maxRows: 4 }}
|
||||||
|
className={styles.commentInput}
|
||||||
|
/>
|
||||||
|
<div className={styles.commentInputActions}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
icon={<SendOutlined />}
|
||||||
|
onClick={() => handleSendComment(monent)}
|
||||||
|
className={styles.sendCommentBtn}
|
||||||
|
>
|
||||||
|
发送
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 朋友圈列表组件
|
||||||
|
export const MomentList: React.FC<MomentListProps> = ({
|
||||||
|
MomentCommon,
|
||||||
|
MomentCommonLoading,
|
||||||
|
formatTime,
|
||||||
|
currentKf,
|
||||||
|
loadMomentData,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.myCircleContent}>
|
||||||
|
{MomentCommon.length > 0 ? (
|
||||||
|
<>
|
||||||
|
{MomentCommon.map((v, index) => (
|
||||||
|
<div
|
||||||
|
key={`${v.snsId}-${v.createTime}-${index}`}
|
||||||
|
className={styles.itemWrapper}
|
||||||
|
>
|
||||||
|
<FriendCard
|
||||||
|
monent={v}
|
||||||
|
isNotMy={false}
|
||||||
|
formatTime={formatTime}
|
||||||
|
currentKf={currentKf}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{MomentCommonLoading && (
|
||||||
|
<div className={styles.loadingMore}>
|
||||||
|
<Spin indicator={<LoadingOutlined spin />} /> 加载中...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!MomentCommonLoading && (
|
||||||
|
<div style={{ textAlign: "center" }}>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
onClick={() => loadMomentData(true)}
|
||||||
|
>
|
||||||
|
加载更多...
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : MomentCommonLoading ? (
|
||||||
|
<div className={styles.loadingMore}>
|
||||||
|
<Spin indicator={<LoadingOutlined spin />} /> 加载中...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className={styles.emptyText}>暂无我的朋友圈内容</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
// 评论数据类型
|
||||||
|
export interface CommentItem {
|
||||||
|
commentArg: number;
|
||||||
|
commentId1: number;
|
||||||
|
commentId2: number;
|
||||||
|
commentTime: number;
|
||||||
|
content: string;
|
||||||
|
nickName: string;
|
||||||
|
wechatId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点赞数据类型
|
||||||
|
export interface LikeItem {
|
||||||
|
createTime: number;
|
||||||
|
nickName: string;
|
||||||
|
wechatId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 朋友圈实体数据类型
|
||||||
|
export interface MomentEntity {
|
||||||
|
content: string;
|
||||||
|
createTime: number;
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
location: string;
|
||||||
|
objectType: number;
|
||||||
|
picSize: number;
|
||||||
|
resUrls: string[];
|
||||||
|
snsId: string;
|
||||||
|
urls: string[];
|
||||||
|
userName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 朋友圈数据类型定义
|
||||||
|
export interface FriendsCircleItem {
|
||||||
|
commentList: CommentItem[];
|
||||||
|
createTime: number;
|
||||||
|
likeList: LikeItem[];
|
||||||
|
momentEntity: MomentEntity;
|
||||||
|
snsId: string;
|
||||||
|
type: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API响应类型
|
||||||
|
export interface ApiResponse {
|
||||||
|
list: FriendsCircleItem[];
|
||||||
|
total: number;
|
||||||
|
hasMore: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FriendCardProps {
|
||||||
|
monent: FriendsCircleItem;
|
||||||
|
isNotMy?: boolean;
|
||||||
|
currentKf?: any;
|
||||||
|
wechatFriendId?: number;
|
||||||
|
formatTime: (time: number) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MomentListProps {
|
||||||
|
MomentCommon: FriendsCircleItem[];
|
||||||
|
MomentCommonLoading: boolean;
|
||||||
|
currentKf?: any;
|
||||||
|
wechatFriendId?: number;
|
||||||
|
formatTime: (time: number) => string;
|
||||||
|
loadMomentData: (loadMore: boolean) => void;
|
||||||
|
}
|
||||||
|
export interface likeListItem {
|
||||||
|
createTime: number;
|
||||||
|
nickName: string;
|
||||||
|
wechatId: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,450 @@
|
|||||||
|
/* ===== 组件根容器 ===== */
|
||||||
|
.friendsCircle {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
|
||||||
|
/* 滚动条样式 */
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #a8a8a8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 折叠面板样式 ===== */
|
||||||
|
.collapseContainer {
|
||||||
|
margin-bottom: 1px;
|
||||||
|
|
||||||
|
:global(.ant-collapse-item) {
|
||||||
|
border-bottom: 1px solid #e8e8e8;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: 1px solid #e8e8e8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ant-collapse-header) {
|
||||||
|
padding: 12px 16px !important;
|
||||||
|
background-color: #ffffff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ant-collapse-content-box) {
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 折叠面板头部 */
|
||||||
|
.collapseHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
.avatar {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
/* 特殊头像样式 */
|
||||||
|
.specialAvatar {
|
||||||
|
background-color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 群组头像样式 */
|
||||||
|
.groupAvatars {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
.groupAvatar {
|
||||||
|
position: absolute;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
background-color: #52c41a;
|
||||||
|
|
||||||
|
&:nth-child(1) {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(3) {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(4) {
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 特殊文本样式 */
|
||||||
|
.specialText {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 内容区域样式 ===== */
|
||||||
|
.myCircleContent,
|
||||||
|
.squareContent {
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
/* 项目包装器 */
|
||||||
|
.itemWrapper {
|
||||||
|
margin-bottom: 1px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 朋友圈项目样式 ===== */
|
||||||
|
.circleItem {
|
||||||
|
background-color: #ffffff;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
/* 头像样式 */
|
||||||
|
.avatar {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
/* 项目头部 */
|
||||||
|
.itemHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
/* 用户信息 */
|
||||||
|
.userInfo {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.username {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 项目内容 */
|
||||||
|
.itemContent {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
.contentText {
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片容器 */
|
||||||
|
.imageContainer {
|
||||||
|
margin: 8px 0;
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
display: table;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
.contentImage {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
float: left;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 蓝色链接 */
|
||||||
|
.blueLink {
|
||||||
|
color: #1890ff;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 项目底部 */
|
||||||
|
.itemFooter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
/* 时间信息 */
|
||||||
|
.timeInfo {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 操作按钮区域 */
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.actionButton {
|
||||||
|
padding: 4px 8px;
|
||||||
|
color: #666;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #1890ff;
|
||||||
|
background-color: #f0f8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点赞和评论交互区域
|
||||||
|
.interactionArea {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
.likeArea {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
.likeIcon {
|
||||||
|
color: #ff6b6b;
|
||||||
|
margin-right: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.likeList {
|
||||||
|
color: #576b95;
|
||||||
|
|
||||||
|
span {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.commentArea {
|
||||||
|
.commentItem {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
|
||||||
|
.commentUser {
|
||||||
|
color: #576b95;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.commentSeparator {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commentContent {
|
||||||
|
color: #333;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteCommentBtn {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
margin-left: 8px;
|
||||||
|
padding: 0;
|
||||||
|
height: auto;
|
||||||
|
min-width: auto;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 评论输入框样式
|
||||||
|
.commentInputArea {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
|
||||||
|
.commentInput {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
resize: none;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.commentInputActions {
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
.sendCommentBtn {
|
||||||
|
height: 28px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空状态样式 */
|
||||||
|
.emptyText {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载更多样式 */
|
||||||
|
.loadingMore {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 图片预览Modal样式 ===== */
|
||||||
|
.imagePreviewModal {
|
||||||
|
:global(.ant-modal-content) {
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ant-modal-header) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ant-modal-close) {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 1001;
|
||||||
|
color: white;
|
||||||
|
font-size: 24px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ant-modal-body) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewContainer {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
|
||||||
|
.previewImage {
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 90vh;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navButton {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 24px;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s;
|
||||||
|
z-index: 1000;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
transform: translateY(-50%) scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.prevButton {
|
||||||
|
left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.nextButton {
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageCounter {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
color: white;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { Collapse } from "antd";
|
||||||
|
import { ChromeOutlined } from "@ant-design/icons";
|
||||||
|
import { MomentList } from "./components/friendCard";
|
||||||
|
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import styles from "./index.module.scss";
|
||||||
|
import { fetchFriendsCircleData } from "./api";
|
||||||
|
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
|
||||||
|
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||||
|
|
||||||
|
interface FriendsCircleProps {
|
||||||
|
wechatFriendId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FriendsCircle: React.FC<FriendsCircleProps> = ({ wechatFriendId }) => {
|
||||||
|
const currentKf = useCkChatStore(state =>
|
||||||
|
state.kfUserList.find(kf => kf.id === state.kfSelected),
|
||||||
|
);
|
||||||
|
const { clearMomentCommon, updateMomentCommonLoading } = useWeChatStore();
|
||||||
|
const MomentCommon = useWeChatStore(state => state.MomentCommon);
|
||||||
|
const MomentCommonLoading = useWeChatStore(
|
||||||
|
state => state.MomentCommonLoading,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 页面重新渲染时重置MomentCommonLoading状态
|
||||||
|
useEffect(() => {
|
||||||
|
updateMomentCommonLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 状态管理
|
||||||
|
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// 加载更多我的朋友圈
|
||||||
|
const loadMomentData = async (loadMore: boolean = false) => {
|
||||||
|
updateMomentCommonLoading(true);
|
||||||
|
// 加载数据;
|
||||||
|
const requestData = {
|
||||||
|
cmdType: "CmdFetchMoment",
|
||||||
|
wechatAccountId: currentKf?.id || 0,
|
||||||
|
wechatFriendId: wechatFriendId || 0,
|
||||||
|
createTimeSec: Math.floor(dayjs().subtract(2, "month").valueOf() / 1000),
|
||||||
|
prevSnsId: loadMore
|
||||||
|
? Number(MomentCommon[MomentCommon.length - 1]?.snsId) || 0
|
||||||
|
: 0,
|
||||||
|
count: 10,
|
||||||
|
isTimeline: expandedKeys.includes("1"),
|
||||||
|
seq: Date.now(),
|
||||||
|
};
|
||||||
|
await fetchFriendsCircleData(requestData);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理折叠面板展开/收起
|
||||||
|
const handleCollapseChange = (keys: string | string[]) => {
|
||||||
|
const keyArray = Array.isArray(keys) ? keys : [keys];
|
||||||
|
setExpandedKeys(keyArray);
|
||||||
|
if (!MomentCommonLoading && keys.length > 0) {
|
||||||
|
clearMomentCommon();
|
||||||
|
loadMomentData(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化时间戳
|
||||||
|
const formatTime = (timestamp: number) => {
|
||||||
|
const date = new Date(timestamp * 1000);
|
||||||
|
return date.toLocaleString("zh-CN", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const collapseItems = [
|
||||||
|
{
|
||||||
|
key: "1",
|
||||||
|
label: (
|
||||||
|
<div className={styles.collapseHeader}>
|
||||||
|
<ChromeOutlined style={{ fontSize: 20 }} />
|
||||||
|
<span className={styles.specialText}>好友朋友圈</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
children: (
|
||||||
|
<MomentList
|
||||||
|
currentKf={currentKf}
|
||||||
|
MomentCommon={MomentCommon}
|
||||||
|
MomentCommonLoading={MomentCommonLoading}
|
||||||
|
loadMomentData={loadMomentData}
|
||||||
|
formatTime={formatTime}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.friendsCircle}>
|
||||||
|
{/* 可折叠的特殊模块,包含所有朋友圈数据 */}
|
||||||
|
<Collapse
|
||||||
|
items={collapseItems}
|
||||||
|
className={styles.collapseContainer}
|
||||||
|
ghost
|
||||||
|
accordion
|
||||||
|
activeKey={expandedKeys}
|
||||||
|
onChange={handleCollapseChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FriendsCircle;
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
.profileContainer {
|
||||||
|
padding: 16px;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profileHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.closeButton {
|
||||||
|
color: #8c8c8c;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #262626;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.profileBasic {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
.profileInfo {
|
||||||
|
margin-top: 16px;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.profileNickname {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #262626;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profileRemark {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.remarkText {
|
||||||
|
color: #8c8c8c;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.profileStatus {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: #52c41a;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
.statusDot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #52c41a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.profileCard {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03);
|
||||||
|
|
||||||
|
:global(.ant-card-head) {
|
||||||
|
padding: 0 16px;
|
||||||
|
min-height: 40px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
:global(.ant-card-head-title) {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #262626;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ant-card-body) {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoIcon {
|
||||||
|
color: #8c8c8c;
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoLabel {
|
||||||
|
color: #8c8c8c;
|
||||||
|
font-size: 14px;
|
||||||
|
width: 60px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoValue {
|
||||||
|
color: #262626;
|
||||||
|
font-size: 14px;
|
||||||
|
flex: 1;
|
||||||
|
word-break: break-all;
|
||||||
|
|
||||||
|
// 备注编辑区域样式
|
||||||
|
:global(.ant-input) {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ant-btn) {
|
||||||
|
font-size: 12px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagsContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
:global(.ant-tag) {
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bioText {
|
||||||
|
margin: 0;
|
||||||
|
color: #595959;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.groupManagement {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.groupMemberList {
|
||||||
|
.groupMember {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #262626;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.profileActions {
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 16px;
|
||||||
|
|
||||||
|
:global(.ant-btn) {
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.profileSider {
|
||||||
|
width: 280px !important;
|
||||||
|
|
||||||
|
.profileContainer {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profileBasic {
|
||||||
|
.profileInfo {
|
||||||
|
.profileNickname {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.profileCard {
|
||||||
|
:global(.ant-card-body) {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoItem {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.infoLabel {
|
||||||
|
width: 50px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoValue {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,137 @@
|
|||||||
|
import React, { useMemo, useState } from "react";
|
||||||
|
import { Card, Input, Button, Space, List, Tag } from "antd";
|
||||||
|
|
||||||
|
export interface QuickWordItem {
|
||||||
|
id: string | number;
|
||||||
|
text?: string; // 兼容旧结构
|
||||||
|
title?: string;
|
||||||
|
content?: string;
|
||||||
|
tag?: string; // 分类/标签
|
||||||
|
usageCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuickWordsProps {
|
||||||
|
title?: string;
|
||||||
|
words: QuickWordItem[];
|
||||||
|
onInsert?: (text: string) => void;
|
||||||
|
onAdd?: (text: string) => void;
|
||||||
|
onRemove?: (id: string | number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QuickWords: React.FC<QuickWordsProps> = ({
|
||||||
|
title = "快捷语录",
|
||||||
|
words,
|
||||||
|
onInsert,
|
||||||
|
|
||||||
|
onRemove,
|
||||||
|
}) => {
|
||||||
|
const [keyword, setKeyword] = useState("");
|
||||||
|
const sorted = useMemo(
|
||||||
|
() =>
|
||||||
|
[...(words || [])].sort((a, b) =>
|
||||||
|
String(a.id).localeCompare(String(b.id)),
|
||||||
|
),
|
||||||
|
[words],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title={title} style={{ marginTop: 12 }}>
|
||||||
|
<Space direction="vertical" style={{ width: "100%" }}>
|
||||||
|
<Input.Search
|
||||||
|
placeholder="搜索快捷语录..."
|
||||||
|
allowClear
|
||||||
|
value={keyword}
|
||||||
|
onChange={e => setKeyword(e.target.value)}
|
||||||
|
onSearch={v => setKeyword(v)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<List
|
||||||
|
itemLayout="vertical"
|
||||||
|
split={false}
|
||||||
|
dataSource={sorted.filter(item => {
|
||||||
|
const text = `${item.title || ""}${item.content || ""}${item.text || ""}`;
|
||||||
|
return text.toLowerCase().includes(keyword.trim().toLowerCase());
|
||||||
|
})}
|
||||||
|
renderItem={item => {
|
||||||
|
const displayTitle = item.title || item.text || "未命名";
|
||||||
|
const displayContent = item.content || item.text || "";
|
||||||
|
return (
|
||||||
|
<List.Item
|
||||||
|
style={{
|
||||||
|
padding: "12px 8px",
|
||||||
|
border: "1px solid #f0f0f0",
|
||||||
|
borderRadius: 8,
|
||||||
|
marginBottom: 12,
|
||||||
|
background: "#fff",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
marginBottom: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.tag && <Tag color="blue">{item.tag}</Tag>}
|
||||||
|
<span style={{ fontWeight: 600, color: "#262626" }}>
|
||||||
|
{displayTitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
color: "#8c8c8c",
|
||||||
|
fontSize: 13,
|
||||||
|
lineHeight: 1.6,
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{displayContent}
|
||||||
|
</div>
|
||||||
|
{typeof item.usageCount === "number" && (
|
||||||
|
<div
|
||||||
|
style={{ color: "#bfbfbf", fontSize: 12, marginTop: 6 }}
|
||||||
|
>
|
||||||
|
使用 {item.usageCount} 次
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{ display: "flex", alignItems: "center", gap: 8 }}
|
||||||
|
>
|
||||||
|
{onRemove && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
danger
|
||||||
|
onClick={() => onRemove(item.id)}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
onClick={() => onInsert?.(displayContent || displayTitle)}
|
||||||
|
>
|
||||||
|
使用
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</List.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QuickWords;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,669 +0,0 @@
|
|||||||
import React, { useEffect, useRef } from "react";
|
|
||||||
import { Layout, Button, Avatar, Space, Dropdown, Menu, Tooltip } from "antd";
|
|
||||||
import {
|
|
||||||
PhoneOutlined,
|
|
||||||
VideoCameraOutlined,
|
|
||||||
MoreOutlined,
|
|
||||||
UserOutlined,
|
|
||||||
DownloadOutlined,
|
|
||||||
FileOutlined,
|
|
||||||
FilePdfOutlined,
|
|
||||||
FileWordOutlined,
|
|
||||||
FileExcelOutlined,
|
|
||||||
FilePptOutlined,
|
|
||||||
PlayCircleFilled,
|
|
||||||
TeamOutlined,
|
|
||||||
FolderOutlined,
|
|
||||||
EnvironmentOutlined,
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
|
||||||
import styles from "./ChatWindow.module.scss";
|
|
||||||
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
|
||||||
import { formatWechatTime } from "@/utils/common";
|
|
||||||
import ProfileCard from "./components/ProfileCard";
|
|
||||||
import MessageEnter from "./components/MessageEnter";
|
|
||||||
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
|
||||||
const { Header, Content } = Layout;
|
|
||||||
|
|
||||||
interface ChatWindowProps {
|
|
||||||
contract: ContractData | weChatGroup;
|
|
||||||
showProfile?: boolean;
|
|
||||||
onToggleProfile?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChatWindow: React.FC<ChatWindowProps> = ({
|
|
||||||
contract,
|
|
||||||
showProfile = true,
|
|
||||||
onToggleProfile,
|
|
||||||
}) => {
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
||||||
const currentMessages = useWeChatStore(state => state.currentMessages);
|
|
||||||
const prevMessagesRef = useRef(currentMessages);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const prevMessages = prevMessagesRef.current;
|
|
||||||
|
|
||||||
const hasVideoStateChange = currentMessages.some((msg, index) => {
|
|
||||||
// 首先检查消息对象本身是否为null或undefined
|
|
||||||
if (!msg || !msg.content) return false;
|
|
||||||
|
|
||||||
const prevMsg = prevMessages[index];
|
|
||||||
if (!prevMsg || !prevMsg.content || prevMsg.id !== msg.id) return false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const currentContent =
|
|
||||||
typeof msg.content === "string"
|
|
||||||
? JSON.parse(msg.content)
|
|
||||||
: msg.content;
|
|
||||||
const prevContent =
|
|
||||||
typeof prevMsg.content === "string"
|
|
||||||
? JSON.parse(prevMsg.content)
|
|
||||||
: prevMsg.content;
|
|
||||||
|
|
||||||
// 检查视频状态是否发生变化(开始加载、完成加载、获得URL)
|
|
||||||
const currentHasVideo =
|
|
||||||
currentContent.previewImage && currentContent.tencentUrl;
|
|
||||||
const prevHasVideo = prevContent.previewImage && prevContent.tencentUrl;
|
|
||||||
|
|
||||||
if (currentHasVideo && prevHasVideo) {
|
|
||||||
// 检查加载状态变化或视频URL变化
|
|
||||||
return (
|
|
||||||
currentContent.isLoading !== prevContent.isLoading ||
|
|
||||||
currentContent.videoUrl !== prevContent.videoUrl
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 只有在没有视频状态变化时才自动滚动到底部
|
|
||||||
if (!hasVideoStateChange) {
|
|
||||||
scrollToBottom();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新上一次的消息状态
|
|
||||||
prevMessagesRef.current = currentMessages;
|
|
||||||
}, [currentMessages]);
|
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理视频播放请求,发送socket请求获取真实视频地址
|
|
||||||
const handleVideoPlayRequest = (tencentUrl: string, messageId: number) => {
|
|
||||||
console.log("发送视频下载请求:", { messageId, tencentUrl });
|
|
||||||
|
|
||||||
// 先设置加载状态
|
|
||||||
useWeChatStore.getState().setVideoLoading(messageId, true);
|
|
||||||
|
|
||||||
// 构建socket请求数据
|
|
||||||
useWebSocketStore.getState().sendCommand("CmdDownloadVideo", {
|
|
||||||
chatroomMessageId: contract.chatroomId ? messageId : 0,
|
|
||||||
friendMessageId: contract.chatroomId ? 0 : messageId,
|
|
||||||
seq: `${+new Date()}`, // 使用时间戳作为请求序列号
|
|
||||||
tencentUrl: tencentUrl,
|
|
||||||
wechatAccountId: contract.wechatAccountId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 解析消息内容,判断消息类型并返回对应的渲染内容
|
|
||||||
const parseMessageContent = (
|
|
||||||
content: string | null | undefined,
|
|
||||||
msg: ChatRecord,
|
|
||||||
) => {
|
|
||||||
// 处理null或undefined的内容
|
|
||||||
if (content === null || content === undefined) {
|
|
||||||
return <div className={styles.messageText}>消息内容不可用</div>;
|
|
||||||
}
|
|
||||||
// 检查是否为表情包
|
|
||||||
if (
|
|
||||||
typeof content === "string" &&
|
|
||||||
content.includes("ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com") &&
|
|
||||||
content.includes("#")
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<div className={styles.emojiMessage}>
|
|
||||||
<img
|
|
||||||
src={content}
|
|
||||||
alt="表情包"
|
|
||||||
style={{ maxWidth: "120px", maxHeight: "120px" }}
|
|
||||||
onClick={() => window.open(content, "_blank")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为带预览图的视频消息
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
typeof content === "string" &&
|
|
||||||
content.trim().startsWith("{") &&
|
|
||||||
content.trim().endsWith("}")
|
|
||||||
) {
|
|
||||||
const videoData = JSON.parse(content);
|
|
||||||
// 处理视频消息格式 {"previewImage":"https://...", "tencentUrl":"...", "videoUrl":"...", "isLoading":true}
|
|
||||||
if (videoData.previewImage && videoData.tencentUrl) {
|
|
||||||
// 提取预览图URL,去掉可能的引号
|
|
||||||
const previewImageUrl = videoData.previewImage.replace(/[`"']/g, "");
|
|
||||||
|
|
||||||
// 创建点击处理函数
|
|
||||||
const handlePlayClick = (e: React.MouseEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
// 如果没有视频URL且不在加载中,则发起下载请求
|
|
||||||
if (!videoData.videoUrl && !videoData.isLoading) {
|
|
||||||
handleVideoPlayRequest(videoData.tencentUrl, msg.id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 如果已有视频URL,显示视频播放器
|
|
||||||
if (videoData.videoUrl) {
|
|
||||||
return (
|
|
||||||
<div className={styles.videoMessage}>
|
|
||||||
<div className={styles.videoContainer}>
|
|
||||||
<video
|
|
||||||
controls
|
|
||||||
src={videoData.videoUrl}
|
|
||||||
style={{ maxWidth: "100%", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
<a
|
|
||||||
href={videoData.videoUrl}
|
|
||||||
download
|
|
||||||
className={styles.downloadButton}
|
|
||||||
style={{ display: "flex" }}
|
|
||||||
onClick={e => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<DownloadOutlined style={{ fontSize: "18px" }} />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示预览图,根据加载状态显示不同的图标
|
|
||||||
return (
|
|
||||||
<div className={styles.videoMessage}>
|
|
||||||
<div className={styles.videoContainer} onClick={handlePlayClick}>
|
|
||||||
<img
|
|
||||||
src={previewImageUrl}
|
|
||||||
alt="视频预览"
|
|
||||||
className={styles.videoThumbnail}
|
|
||||||
style={{
|
|
||||||
maxWidth: "100%",
|
|
||||||
borderRadius: "8px",
|
|
||||||
opacity: videoData.isLoading ? "0.7" : "1",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className={styles.videoPlayIcon}>
|
|
||||||
{videoData.isLoading ? (
|
|
||||||
<div className={styles.loadingSpinner}></div>
|
|
||||||
) : (
|
|
||||||
<PlayCircleFilled
|
|
||||||
style={{ fontSize: "48px", color: "#fff" }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// 保留原有的视频处理逻辑
|
|
||||||
else if (
|
|
||||||
videoData.type === "video" &&
|
|
||||||
videoData.url &&
|
|
||||||
videoData.thumb
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<div className={styles.videoMessage}>
|
|
||||||
<div
|
|
||||||
className={styles.videoContainer}
|
|
||||||
onClick={() => window.open(videoData.url, "_blank")}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={videoData.thumb}
|
|
||||||
alt="视频预览"
|
|
||||||
className={styles.videoThumbnail}
|
|
||||||
/>
|
|
||||||
<div className={styles.videoPlayIcon}>
|
|
||||||
<VideoCameraOutlined
|
|
||||||
style={{ fontSize: "32px", color: "#fff" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
href={videoData.url}
|
|
||||||
download
|
|
||||||
className={styles.downloadButton}
|
|
||||||
style={{ display: "flex" }}
|
|
||||||
onClick={e => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<DownloadOutlined style={{ fontSize: "18px" }} />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// 解析JSON失败,不是视频消息
|
|
||||||
console.log("解析视频消息失败:", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为图片链接
|
|
||||||
if (
|
|
||||||
typeof content === "string" &&
|
|
||||||
(content.match(/\.(jpg|jpeg|png|gif)$/i) ||
|
|
||||||
(content.includes("oss-cn-shenzhen.aliyuncs.com") &&
|
|
||||||
content.includes(".jpg")))
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<div className={styles.imageMessage}>
|
|
||||||
<img
|
|
||||||
src={content}
|
|
||||||
alt="图片消息"
|
|
||||||
onClick={() => window.open(content, "_blank")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为视频链接
|
|
||||||
if (
|
|
||||||
typeof content === "string" &&
|
|
||||||
(content.match(/\.(mp4|avi|mov|wmv|flv)$/i) ||
|
|
||||||
(content.includes("oss-cn-shenzhen.aliyuncs.com") &&
|
|
||||||
content.includes(".mp4")))
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<div className={styles.videoMessage}>
|
|
||||||
<video
|
|
||||||
controls
|
|
||||||
src={content}
|
|
||||||
style={{ maxWidth: "100%", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
<a
|
|
||||||
href={content}
|
|
||||||
download
|
|
||||||
className={styles.downloadButton}
|
|
||||||
style={{ display: "flex" }}
|
|
||||||
onClick={e => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<DownloadOutlined style={{ fontSize: "18px" }} />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为音频链接
|
|
||||||
if (
|
|
||||||
typeof content === "string" &&
|
|
||||||
(content.match(/\.(mp3|wav|ogg|m4a)$/i) ||
|
|
||||||
(content.includes("oss-cn-shenzhen.aliyuncs.com") &&
|
|
||||||
content.includes(".mp3")))
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<div className={styles.audioMessage}>
|
|
||||||
<audio controls src={content} style={{ maxWidth: "100%" }} />
|
|
||||||
<a
|
|
||||||
href={content}
|
|
||||||
download
|
|
||||||
className={styles.downloadButton}
|
|
||||||
style={{ display: "flex" }}
|
|
||||||
onClick={e => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<DownloadOutlined style={{ fontSize: "18px" }} />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为Office文件链接
|
|
||||||
if (
|
|
||||||
typeof content === "string" &&
|
|
||||||
content.match(/\.(doc|docx|xls|xlsx|ppt|pptx|pdf)$/i)
|
|
||||||
) {
|
|
||||||
const fileName = content.split("/").pop() || "文件";
|
|
||||||
const fileExt = fileName.split(".").pop()?.toLowerCase();
|
|
||||||
|
|
||||||
// 根据文件类型选择不同的图标
|
|
||||||
let fileIcon = (
|
|
||||||
<FileOutlined
|
|
||||||
style={{ fontSize: "24px", marginRight: "8px", color: "#1890ff" }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (fileExt === "pdf") {
|
|
||||||
fileIcon = (
|
|
||||||
<FilePdfOutlined
|
|
||||||
style={{ fontSize: "24px", marginRight: "8px", color: "#ff4d4f" }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (fileExt === "doc" || fileExt === "docx") {
|
|
||||||
fileIcon = (
|
|
||||||
<FileWordOutlined
|
|
||||||
style={{ fontSize: "24px", marginRight: "8px", color: "#2f54eb" }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (fileExt === "xls" || fileExt === "xlsx") {
|
|
||||||
fileIcon = (
|
|
||||||
<FileExcelOutlined
|
|
||||||
style={{ fontSize: "24px", marginRight: "8px", color: "#52c41a" }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (fileExt === "ppt" || fileExt === "pptx") {
|
|
||||||
fileIcon = (
|
|
||||||
<FilePptOutlined
|
|
||||||
style={{ fontSize: "24px", marginRight: "8px", color: "#fa8c16" }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.fileMessage}>
|
|
||||||
{fileIcon}
|
|
||||||
<div className={styles.fileInfo}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontWeight: "bold",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
overflow: "hidden",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{fileName}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
href={content}
|
|
||||||
download={fileExt !== "pdf" ? fileName : undefined}
|
|
||||||
target={fileExt === "pdf" ? "_blank" : undefined}
|
|
||||||
className={styles.downloadButton}
|
|
||||||
onClick={e => e.stopPropagation()}
|
|
||||||
style={{ display: "flex" }}
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
<DownloadOutlined style={{ fontSize: "18px" }} />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为文件消息(JSON格式)
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
typeof content === "string" &&
|
|
||||||
content.trim().startsWith("{") &&
|
|
||||||
content.trim().endsWith("}")
|
|
||||||
) {
|
|
||||||
const fileData = JSON.parse(content);
|
|
||||||
if (fileData.type === "file" && fileData.title) {
|
|
||||||
// 检查是否为Office文件
|
|
||||||
const fileExt = fileData.title.split(".").pop()?.toLowerCase();
|
|
||||||
let fileIcon = (
|
|
||||||
<FolderOutlined
|
|
||||||
style={{ fontSize: "24px", marginRight: "8px", color: "#1890ff" }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (fileExt === "pdf") {
|
|
||||||
fileIcon = (
|
|
||||||
<FilePdfOutlined
|
|
||||||
style={{
|
|
||||||
fontSize: "24px",
|
|
||||||
marginRight: "8px",
|
|
||||||
color: "#ff4d4f",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (fileExt === "doc" || fileExt === "docx") {
|
|
||||||
fileIcon = (
|
|
||||||
<FileWordOutlined
|
|
||||||
style={{
|
|
||||||
fontSize: "24px",
|
|
||||||
marginRight: "8px",
|
|
||||||
color: "#2f54eb",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (fileExt === "xls" || fileExt === "xlsx") {
|
|
||||||
fileIcon = (
|
|
||||||
<FileExcelOutlined
|
|
||||||
style={{
|
|
||||||
fontSize: "24px",
|
|
||||||
marginRight: "8px",
|
|
||||||
color: "#52c41a",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (fileExt === "ppt" || fileExt === "pptx") {
|
|
||||||
fileIcon = (
|
|
||||||
<FilePptOutlined
|
|
||||||
style={{
|
|
||||||
fontSize: "24px",
|
|
||||||
marginRight: "8px",
|
|
||||||
color: "#fa8c16",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.fileMessage}>
|
|
||||||
{fileIcon}
|
|
||||||
<div className={styles.fileInfo}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontWeight: "bold",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
overflow: "hidden",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{fileData.title}
|
|
||||||
</div>
|
|
||||||
{fileData.totalLen && (
|
|
||||||
<div style={{ fontSize: "12px", color: "#8c8c8c" }}>
|
|
||||||
{Math.round(fileData.totalLen / 1024)} KB
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
href={fileData.url || "#"}
|
|
||||||
download={fileExt !== "pdf" ? fileData.title : undefined}
|
|
||||||
target={fileExt === "pdf" ? "_blank" : undefined}
|
|
||||||
className={styles.downloadButton}
|
|
||||||
style={{ display: "flex" }}
|
|
||||||
onClick={e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (!fileData.url) {
|
|
||||||
console.log("文件URL不存在");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
<DownloadOutlined style={{ fontSize: "18px" }} />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// 解析JSON失败,不是文件消息
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为位置信息
|
|
||||||
if (
|
|
||||||
typeof content === "string" &&
|
|
||||||
(content.includes("<location") || content.includes("<msg><location"))
|
|
||||||
) {
|
|
||||||
// 提取位置信息
|
|
||||||
const labelMatch = content.match(/label="([^"]*)"/i);
|
|
||||||
const poiNameMatch = content.match(/poiname="([^"]*)"/i);
|
|
||||||
const xMatch = content.match(/x="([^"]*)"/i);
|
|
||||||
const yMatch = content.match(/y="([^"]*)"/i);
|
|
||||||
|
|
||||||
const label = labelMatch
|
|
||||||
? labelMatch[1]
|
|
||||||
: poiNameMatch
|
|
||||||
? poiNameMatch[1]
|
|
||||||
: "位置信息";
|
|
||||||
const coordinates = xMatch && yMatch ? `${yMatch[1]}, ${xMatch[1]}` : "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.locationMessage}>
|
|
||||||
<EnvironmentOutlined
|
|
||||||
style={{ fontSize: "24px", marginRight: "8px", color: "#ff4d4f" }}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<div style={{ fontWeight: "bold" }}>{label}</div>
|
|
||||||
{coordinates && (
|
|
||||||
<div style={{ fontSize: "12px", color: "#8c8c8c" }}>
|
|
||||||
{coordinates}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认为文本消息
|
|
||||||
return <div className={styles.messageText}>{content}</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 用于分组消息并添加时间戳的辅助函数
|
|
||||||
const groupMessagesByTime = (messages: ChatRecord[]) => {
|
|
||||||
return messages
|
|
||||||
.filter(msg => msg !== null && msg !== undefined) // 过滤掉null和undefined的消息
|
|
||||||
.map(msg => ({
|
|
||||||
time: formatWechatTime(msg?.wechatTime),
|
|
||||||
messages: [msg],
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderMessage = (msg: ChatRecord) => {
|
|
||||||
// 添加null检查,防止访问null对象的属性
|
|
||||||
if (!msg) return null;
|
|
||||||
|
|
||||||
const isOwn = msg?.isSend;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={msg.id || `msg-${Date.now()}`}
|
|
||||||
className={`${styles.messageItem} ${
|
|
||||||
isOwn ? styles.ownMessage : styles.otherMessage
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className={styles.messageContent}>
|
|
||||||
{!isOwn && (
|
|
||||||
<Avatar
|
|
||||||
size={32}
|
|
||||||
src={contract.avatar}
|
|
||||||
icon={<UserOutlined />}
|
|
||||||
className={styles.messageAvatar}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className={styles.messageBubble}>
|
|
||||||
{!isOwn && (
|
|
||||||
<div className={styles.messageSender}>{msg?.senderName}</div>
|
|
||||||
)}
|
|
||||||
{parseMessageContent(msg?.content, msg)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const chatMenu = (
|
|
||||||
<Menu>
|
|
||||||
<Menu.Item key="profile" icon={<UserOutlined />}>
|
|
||||||
查看资料
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key="call" icon={<PhoneOutlined />}>
|
|
||||||
语音通话
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key="video" icon={<VideoCameraOutlined />}>
|
|
||||||
视频通话
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Divider />
|
|
||||||
<Menu.Item key="pin">置顶聊天</Menu.Item>
|
|
||||||
<Menu.Item key="mute">消息免打扰</Menu.Item>
|
|
||||||
<Menu.Divider />
|
|
||||||
<Menu.Item key="clear" danger>
|
|
||||||
清空聊天记录
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Layout className={styles.chatWindow}>
|
|
||||||
{/* 聊天主体区域 */}
|
|
||||||
<Layout className={styles.chatMain}>
|
|
||||||
{/* 聊天头部 */}
|
|
||||||
<Header className={styles.chatHeader}>
|
|
||||||
<div className={styles.chatHeaderInfo}>
|
|
||||||
<Avatar
|
|
||||||
size={40}
|
|
||||||
src={contract.avatar || contract.chatroomAvatar}
|
|
||||||
icon={
|
|
||||||
contract.type === "group" ? <TeamOutlined /> : <UserOutlined />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div className={styles.chatHeaderDetails}>
|
|
||||||
<div className={styles.chatHeaderName}>
|
|
||||||
{contract.nickname || contract.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Space>
|
|
||||||
<Tooltip title="语音通话">
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={<PhoneOutlined />}
|
|
||||||
className={styles.headerButton}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="视频通话">
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={<VideoCameraOutlined />}
|
|
||||||
className={styles.headerButton}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Dropdown overlay={chatMenu} trigger={["click"]}>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={<MoreOutlined />}
|
|
||||||
className={styles.headerButton}
|
|
||||||
/>
|
|
||||||
</Dropdown>
|
|
||||||
</Space>
|
|
||||||
</Header>
|
|
||||||
|
|
||||||
{/* 聊天内容 */}
|
|
||||||
<Content className={styles.chatContent}>
|
|
||||||
<div className={styles.messagesContainer}>
|
|
||||||
{groupMessagesByTime(currentMessages).map((group, groupIndex) => (
|
|
||||||
<React.Fragment key={`group-${groupIndex}`}>
|
|
||||||
<div className={styles.messageTime}>{group.time}</div>
|
|
||||||
{group.messages.map(renderMessage)}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
<div ref={messagesEndRef} />
|
|
||||||
</div>
|
|
||||||
</Content>
|
|
||||||
|
|
||||||
{/* 消息输入组件 */}
|
|
||||||
<MessageEnter contract={contract} />
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
{/* 右侧个人资料卡片 */}
|
|
||||||
<ProfileCard
|
|
||||||
contract={contract}
|
|
||||||
showProfile={showProfile}
|
|
||||||
onToggleProfile={onToggleProfile}
|
|
||||||
/>
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ChatWindow;
|
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Layout, Button, Avatar, Space, Dropdown, Menu, Tooltip } from "antd";
|
import { Layout, Button, Avatar, Space, Tooltip, Dropdown } from "antd";
|
||||||
import {
|
import {
|
||||||
PhoneOutlined,
|
|
||||||
VideoCameraOutlined,
|
|
||||||
MoreOutlined,
|
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
InfoCircleOutlined,
|
InfoCircleOutlined,
|
||||||
|
RobotOutlined,
|
||||||
|
DownOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
||||||
import styles from "./ChatWindow.module.scss";
|
import styles from "./ChatWindow.module.scss";
|
||||||
@@ -14,7 +13,8 @@ import styles from "./ChatWindow.module.scss";
|
|||||||
import ProfileCard from "./components/ProfileCard";
|
import ProfileCard from "./components/ProfileCard";
|
||||||
import MessageEnter from "./components/MessageEnter";
|
import MessageEnter from "./components/MessageEnter";
|
||||||
import MessageRecord from "./components/MessageRecord";
|
import MessageRecord from "./components/MessageRecord";
|
||||||
|
import { setFriendInjectConfig } from "@/pages/pc/ckbox/weChat/api";
|
||||||
|
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||||
const { Header, Content } = Layout;
|
const { Header, Content } = Layout;
|
||||||
|
|
||||||
interface ChatWindowProps {
|
interface ChatWindowProps {
|
||||||
@@ -22,11 +22,50 @@ interface ChatWindowProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ChatWindow: React.FC<ChatWindowProps> = ({ contract }) => {
|
const ChatWindow: React.FC<ChatWindowProps> = ({ contract }) => {
|
||||||
|
const updateAiQuoteMessageContent = useWeChatStore(
|
||||||
|
state => state.updateAiQuoteMessageContent,
|
||||||
|
);
|
||||||
|
const aiQuoteMessageContent = useWeChatStore(
|
||||||
|
state => state.aiQuoteMessageContent,
|
||||||
|
);
|
||||||
const [showProfile, setShowProfile] = useState(true);
|
const [showProfile, setShowProfile] = useState(true);
|
||||||
const onToggleProfile = () => {
|
const onToggleProfile = () => {
|
||||||
setShowProfile(!showProfile);
|
setShowProfile(!showProfile);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const typeOptions = [
|
||||||
|
{ value: 0, label: "人工接待" },
|
||||||
|
{ value: 1, label: "AI辅助" },
|
||||||
|
{ value: 2, label: "AI接管" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const [currentConfig, setCurrentConfig] = useState(
|
||||||
|
typeOptions.find(option => option.value === aiQuoteMessageContent),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentConfig(
|
||||||
|
typeOptions.find(option => option.value === aiQuoteMessageContent),
|
||||||
|
);
|
||||||
|
}, [aiQuoteMessageContent]);
|
||||||
|
|
||||||
|
// 处理配置选择
|
||||||
|
const handleConfigChange = option => {
|
||||||
|
setCurrentConfig({
|
||||||
|
value: option.value,
|
||||||
|
label: option.label,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 保存配置到后端
|
||||||
|
setFriendInjectConfig({
|
||||||
|
type: option.value,
|
||||||
|
wechatAccountId: contract.wechatAccountId,
|
||||||
|
friendId: contract.id,
|
||||||
|
}).then(() => {
|
||||||
|
updateAiQuoteMessageContent(option.value);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout className={styles.chatWindow}>
|
<Layout className={styles.chatWindow}>
|
||||||
{/* 聊天主体区域 */}
|
{/* 聊天主体区域 */}
|
||||||
@@ -48,6 +87,25 @@ const ChatWindow: React.FC<ChatWindowProps> = ({ contract }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Space>
|
<Space>
|
||||||
|
{!contract.chatroomId && (
|
||||||
|
<Dropdown
|
||||||
|
menu={{
|
||||||
|
items: typeOptions.map(option => ({
|
||||||
|
key: option.value,
|
||||||
|
label: option.label,
|
||||||
|
onClick: () => handleConfigChange(option),
|
||||||
|
})),
|
||||||
|
}}
|
||||||
|
trigger={["click"]}
|
||||||
|
placement="bottomRight"
|
||||||
|
>
|
||||||
|
<Button type="default" icon={<RobotOutlined />}>
|
||||||
|
{currentConfig.label}
|
||||||
|
<DownOutlined />
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
)}
|
||||||
|
|
||||||
<Tooltip title="个人资料">
|
<Tooltip title="个人资料">
|
||||||
<Button
|
<Button
|
||||||
onClick={onToggleProfile}
|
onClick={onToggleProfile}
|
||||||
|
|||||||
@@ -1,258 +0,0 @@
|
|||||||
.header {
|
|
||||||
background: #fff;
|
|
||||||
padding: 0 16px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
height: 64px;
|
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.headerLeft {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menuButton {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 6px;
|
|
||||||
transition: all 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.anticon {
|
|
||||||
font-size: 18px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 18px;
|
|
||||||
color: #333;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.headerRight {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.userInfo {
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
transition: all 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
.suanli {
|
|
||||||
font-size: 16px;
|
|
||||||
color: #666;
|
|
||||||
.suanliIcon {
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
border: 2px solid #f0f0f0;
|
|
||||||
transition: all 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: #1890ff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.username {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #333;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 抽屉样式
|
|
||||||
.drawer {
|
|
||||||
:global(.ant-drawer-header) {
|
|
||||||
background: #fafafa;
|
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.ant-drawer-body) {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawerContent {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawerHeader {
|
|
||||||
padding: 20px;
|
|
||||||
background: #fff;
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logoSection {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logoIcon {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
border-radius: 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 24px;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logoText {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.appName {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.appDesc {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
margin: 2px 0 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawerBody {
|
|
||||||
flex: 1;
|
|
||||||
padding: 20px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.primaryButton {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 16px 20px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonIcon {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.menuSection {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menuItem {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 16px 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-size: 16px;
|
|
||||||
color: #333;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.menuIcon {
|
|
||||||
font-size: 20px;
|
|
||||||
margin-right: 12px;
|
|
||||||
width: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawerFooter {
|
|
||||||
padding: 20px;
|
|
||||||
background: #fff;
|
|
||||||
border-top: 1px solid #f0f0f0;
|
|
||||||
.balanceSection {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
.balanceIcon {
|
|
||||||
color: #666;
|
|
||||||
.suanliIcon {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.balanceText {
|
|
||||||
color: #3d9c0d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.balanceLabel {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.balanceAmount {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #52c41a;
|
|
||||||
margin: 2px 0 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 响应式设计
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.header {
|
|
||||||
padding: 0 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.username {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawer {
|
|
||||||
width: 280px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import { Layout, Drawer, Avatar, Dropdown, Space, Button } from "antd";
|
|
||||||
import {
|
|
||||||
MenuOutlined,
|
|
||||||
UserOutlined,
|
|
||||||
LogoutOutlined,
|
|
||||||
SettingOutlined,
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
import type { MenuProps } from "antd";
|
|
||||||
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import styles from "./index.module.scss";
|
|
||||||
|
|
||||||
const { Header } = Layout;
|
|
||||||
|
|
||||||
interface NavCommonProps {
|
|
||||||
title?: string;
|
|
||||||
onMenuClick?: () => void;
|
|
||||||
drawerContent?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const NavCommon: React.FC<NavCommonProps> = ({
|
|
||||||
title = "触客宝",
|
|
||||||
onMenuClick,
|
|
||||||
drawerContent,
|
|
||||||
}) => {
|
|
||||||
const [drawerVisible, setDrawerVisible] = useState(false);
|
|
||||||
|
|
||||||
const { userInfo } = useCkChatStore();
|
|
||||||
|
|
||||||
// 处理菜单图标点击
|
|
||||||
const handleMenuClick = () => {
|
|
||||||
setDrawerVisible(true);
|
|
||||||
onMenuClick?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理抽屉关闭
|
|
||||||
const handleDrawerClose = () => {
|
|
||||||
setDrawerVisible(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 默认抽屉内容
|
|
||||||
const defaultDrawerContent = (
|
|
||||||
<div className={styles.drawerContent}>
|
|
||||||
<div className={styles.drawerHeader}>
|
|
||||||
<div className={styles.logoSection}>
|
|
||||||
<div className={styles.logoIcon}>✨</div>
|
|
||||||
<div className={styles.logoText}>
|
|
||||||
<div className={styles.appName}>触客宝</div>
|
|
||||||
<div className={styles.appDesc}>AI智能营销系统</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.drawerBody}>
|
|
||||||
<div className={styles.primaryButton}>
|
|
||||||
<div className={styles.buttonIcon}>🔒</div>
|
|
||||||
<span>AI智能客服</span>
|
|
||||||
</div>
|
|
||||||
<div className={styles.menuSection}>
|
|
||||||
<div className={styles.menuItem}>
|
|
||||||
<div className={styles.menuIcon}>📊</div>
|
|
||||||
<span>功能中心</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.drawerFooter}>
|
|
||||||
<div className={styles.balanceSection}>
|
|
||||||
<div className={styles.balanceIcon}>
|
|
||||||
<span className={styles.suanliIcon}>⚡</span>算力余额
|
|
||||||
</div>
|
|
||||||
<div className={styles.balanceText}>9307.423</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Header className={styles.header}>
|
|
||||||
<div className={styles.headerLeft}>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={<MenuOutlined />}
|
|
||||||
onClick={handleMenuClick}
|
|
||||||
className={styles.menuButton}
|
|
||||||
/>
|
|
||||||
<span className={styles.title}>{title}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.headerRight}>
|
|
||||||
<Space className={styles.userInfo}>
|
|
||||||
<span className={styles.suanli}>
|
|
||||||
<span className={styles.suanliIcon}>⚡</span>
|
|
||||||
9307.423
|
|
||||||
</span>
|
|
||||||
<Avatar
|
|
||||||
size={40}
|
|
||||||
icon={<UserOutlined />}
|
|
||||||
src={userInfo?.account?.avatar}
|
|
||||||
className={styles.avatar}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
</Header>
|
|
||||||
|
|
||||||
<Drawer
|
|
||||||
title="菜单"
|
|
||||||
placement="left"
|
|
||||||
onClose={handleDrawerClose}
|
|
||||||
open={drawerVisible}
|
|
||||||
width={300}
|
|
||||||
className={styles.drawer}
|
|
||||||
>
|
|
||||||
{drawerContent || defaultDrawerContent}
|
|
||||||
</Drawer>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NavCommon;
|
|
||||||
@@ -24,9 +24,10 @@ export interface ContractData {
|
|||||||
thirdParty: null;
|
thirdParty: null;
|
||||||
additionalPicture: string;
|
additionalPicture: string;
|
||||||
desc: string;
|
desc: string;
|
||||||
config: null;
|
|
||||||
lastMessageTime: number;
|
lastMessageTime: number;
|
||||||
unreadCount: number;
|
config: {
|
||||||
|
unreadCount: number;
|
||||||
|
};
|
||||||
duplicate: boolean;
|
duplicate: boolean;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
@@ -40,7 +41,9 @@ export interface ChatSession {
|
|||||||
avatar?: string;
|
avatar?: string;
|
||||||
lastMessage: string;
|
lastMessage: string;
|
||||||
lastTime: string;
|
lastTime: string;
|
||||||
unreadCount: number;
|
config: {
|
||||||
|
unreadCount: number;
|
||||||
|
};
|
||||||
online: boolean;
|
online: boolean;
|
||||||
members?: string[];
|
members?: string[];
|
||||||
pinned?: boolean;
|
pinned?: boolean;
|
||||||
|
|||||||
@@ -79,7 +79,9 @@ const MessageList: React.FC<MessageListProps> = () => {
|
|||||||
<div
|
<div
|
||||||
className={styles.lastMessage}
|
className={styles.lastMessage}
|
||||||
data-count={
|
data-count={
|
||||||
session.unreadCount > 0 ? session.unreadCount : ""
|
session.config.unreadCount > 0
|
||||||
|
? session.config.unreadCount
|
||||||
|
: ""
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{session?.lastMessage}
|
{session?.lastMessage}
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ const VerticalUserList: React.FC = () => {
|
|||||||
const session = chatSessions.filter(
|
const session = chatSessions.filter(
|
||||||
v => v.wechatAccountId === wechatAccountId,
|
v => v.wechatAccountId === wechatAccountId,
|
||||||
);
|
);
|
||||||
return session.reduce((pre, cur) => pre + cur.unreadCount, 0);
|
return session.reduce((pre, cur) => pre + cur.config.unreadCount, 0);
|
||||||
} else {
|
} else {
|
||||||
return chatSessions.reduce((pre, cur) => pre + cur.unreadCount, 0);
|
return chatSessions.reduce((pre, cur) => pre + cur.config.unreadCount, 0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ export interface MessageListData {
|
|||||||
avatar?: string; // 头像
|
avatar?: string; // 头像
|
||||||
groupId: number; // 分组ID
|
groupId: number; // 分组ID
|
||||||
config?: {
|
config?: {
|
||||||
chat: boolean;
|
chat?: boolean;
|
||||||
|
unreadCount: number; // 未读消息数
|
||||||
}; // 配置信息
|
}; // 配置信息
|
||||||
labels?: string[]; // 标签列表
|
labels?: string[]; // 标签列表
|
||||||
unreadCount: number; // 未读消息数
|
|
||||||
|
|
||||||
// 联系人特有字段(当dataType为'contracts'时使用)
|
// 联系人特有字段(当dataType为'contracts'时使用)
|
||||||
wechatId?: string; // 微信ID
|
wechatId?: string; // 微信ID
|
||||||
@@ -113,10 +113,11 @@ export interface weChatGroup {
|
|||||||
chatroomAvatar: string;
|
chatroomAvatar: string;
|
||||||
groupId: number;
|
groupId: number;
|
||||||
config?: {
|
config?: {
|
||||||
chat: boolean;
|
chat?: boolean;
|
||||||
|
unreadCount: number;
|
||||||
};
|
};
|
||||||
labels?: string[];
|
labels?: string[];
|
||||||
unreadCount: number;
|
|
||||||
notice: string;
|
notice: string;
|
||||||
selfDisplyName: string;
|
selfDisplyName: string;
|
||||||
wechatChatroomId: number;
|
wechatChatroomId: number;
|
||||||
@@ -152,10 +153,10 @@ export interface ContractData {
|
|||||||
additionalPicture: string;
|
additionalPicture: string;
|
||||||
desc: string;
|
desc: string;
|
||||||
config?: {
|
config?: {
|
||||||
chat: boolean;
|
chat?: boolean;
|
||||||
|
unreadCount: number;
|
||||||
};
|
};
|
||||||
lastMessageTime: number;
|
lastMessageTime: number;
|
||||||
unreadCount: number;
|
|
||||||
duplicate: boolean;
|
duplicate: boolean;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
@@ -246,7 +247,9 @@ export interface ChatSession {
|
|||||||
avatar?: string;
|
avatar?: string;
|
||||||
lastMessage: string;
|
lastMessage: string;
|
||||||
lastTime: string;
|
lastTime: string;
|
||||||
unreadCount: number;
|
config: {
|
||||||
|
unreadCount: number;
|
||||||
|
};
|
||||||
online: boolean;
|
online: boolean;
|
||||||
members?: string[];
|
members?: string[];
|
||||||
pinned?: boolean;
|
pinned?: boolean;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
getControlTerminalList,
|
getControlTerminalList,
|
||||||
getContactList,
|
getContactList,
|
||||||
getGroupList,
|
getGroupList,
|
||||||
getMessageList,
|
getAgentList,
|
||||||
} from "./api";
|
} from "./api";
|
||||||
|
|
||||||
import { useUserStore } from "@/store/module/user";
|
import { useUserStore } from "@/store/module/user";
|
||||||
@@ -47,15 +47,14 @@ export const chatInitAPIdata = async () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
//获取控制终端列表
|
//获取控制终端列表
|
||||||
const kfUserList: KfUserListData[] =
|
const kfUserList: KfUserListData[] = await getAgentList();
|
||||||
await getControlTerminalListByWechatAccountIds(uniqueWechatAccountIds);
|
|
||||||
|
|
||||||
//获取用户列表
|
//获取用户列表
|
||||||
await asyncKfUserList(kfUserList);
|
await asyncKfUserList(kfUserList);
|
||||||
|
|
||||||
//获取标签列表
|
//获取标签列表
|
||||||
const countLables = await getCountLables();
|
// const countLables = await getCountLables();
|
||||||
await asyncCountLables(countLables);
|
// await asyncCountLables(countLables);
|
||||||
|
|
||||||
//获取消息会话列表并按lastUpdateTime排序
|
//获取消息会话列表并按lastUpdateTime排序
|
||||||
const filterUserSessions = contractList?.filter(
|
const filterUserSessions = contractList?.filter(
|
||||||
@@ -67,23 +66,24 @@ export const chatInitAPIdata = async () => {
|
|||||||
//排序功能
|
//排序功能
|
||||||
const sortedSessions = [...filterUserSessions, ...filterGroupSessions].sort(
|
const sortedSessions = [...filterUserSessions, ...filterGroupSessions].sort(
|
||||||
(a, b) => {
|
(a, b) => {
|
||||||
|
// 获取未读消息数量
|
||||||
|
const aUnread = a.config?.unreadCount || 0;
|
||||||
|
const bUnread = b.config?.unreadCount || 0;
|
||||||
|
|
||||||
|
// 首先按未读消息数量降序排列(未读消息多的排在前面)
|
||||||
|
if (aUnread !== bUnread) {
|
||||||
|
return bUnread - aUnread;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果未读消息数量相同,则按时间降序排列(最新的在前面)
|
||||||
// 如果lastUpdateTime不存在,则将其排在最后
|
// 如果lastUpdateTime不存在,则将其排在最后
|
||||||
if (!a.lastUpdateTime) return 1;
|
if (!a.lastUpdateTime) return 1;
|
||||||
if (!b.lastUpdateTime) return -1;
|
if (!b.lastUpdateTime) return -1;
|
||||||
|
|
||||||
// 首先按时间降序排列(最新的在前面)
|
|
||||||
const timeCompare =
|
const timeCompare =
|
||||||
new Date(b.lastUpdateTime).getTime() -
|
new Date(b.lastUpdateTime).getTime() -
|
||||||
new Date(a.lastUpdateTime).getTime();
|
new Date(a.lastUpdateTime).getTime();
|
||||||
|
|
||||||
// 如果时间相同,则按未读消息数量降序排列
|
|
||||||
if (timeCompare === 0) {
|
|
||||||
// 如果unreadCount不存在,则将其排在后面
|
|
||||||
const aUnread = a.unreadCount || 0;
|
|
||||||
const bUnread = b.unreadCount || 0;
|
|
||||||
return bUnread - aUnread; // 未读消息多的排在前面
|
|
||||||
}
|
|
||||||
|
|
||||||
return timeCompare;
|
return timeCompare;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -102,15 +102,6 @@ export const chatInitAPIdata = async () => {
|
|||||||
};
|
};
|
||||||
//发起soket连接
|
//发起soket连接
|
||||||
export const initSocket = () => {
|
export const initSocket = () => {
|
||||||
// 检查WebSocket是否已经连接
|
|
||||||
// const { status } = useWebSocketStore.getState();
|
|
||||||
|
|
||||||
// 如果已经连接或正在连接,则不重复连接
|
|
||||||
// if (["connected", "connecting"].includes(status)) {
|
|
||||||
// console.log("WebSocket已连接或正在连接,跳过重复连接", { status });
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 从store获取token和accountId
|
// 从store获取token和accountId
|
||||||
const { token2 } = useUserStore.getState();
|
const { token2 } = useUserStore.getState();
|
||||||
const { getAccountId } = useCkChatStore.getState();
|
const { getAccountId } = useCkChatStore.getState();
|
||||||
@@ -178,16 +169,16 @@ export const getControlTerminalListByWechatAccountIds = (
|
|||||||
export const getAllContactList = async () => {
|
export const getAllContactList = async () => {
|
||||||
try {
|
try {
|
||||||
let allContacts = [];
|
let allContacts = [];
|
||||||
let prevId = 0;
|
let page = 1;
|
||||||
const count = 1000;
|
const limit = 1000;
|
||||||
let hasMore = true;
|
let hasMore = true;
|
||||||
|
|
||||||
while (hasMore) {
|
while (hasMore) {
|
||||||
const contractList = await getContactList({
|
const Result = await getContactList({
|
||||||
prevId,
|
page,
|
||||||
count,
|
limit,
|
||||||
});
|
});
|
||||||
|
const contractList = Result.list;
|
||||||
if (
|
if (
|
||||||
!contractList ||
|
!contractList ||
|
||||||
!Array.isArray(contractList) ||
|
!Array.isArray(contractList) ||
|
||||||
@@ -199,15 +190,11 @@ export const getAllContactList = async () => {
|
|||||||
|
|
||||||
allContacts = [...allContacts, ...contractList];
|
allContacts = [...allContacts, ...contractList];
|
||||||
|
|
||||||
// console.log(contractList.length == 0);
|
|
||||||
|
|
||||||
// 如果返回的数据少于请求的数量,说明已经没有更多数据了
|
// 如果返回的数据少于请求的数量,说明已经没有更多数据了
|
||||||
if (contractList.length == 0) {
|
if (contractList.length == 0) {
|
||||||
hasMore = false;
|
hasMore = false;
|
||||||
} else {
|
} else {
|
||||||
// 获取最后一条数据的id作为下一次请求的prevId
|
page = page + 1;
|
||||||
const lastContact = contractList[contractList.length - 1];
|
|
||||||
prevId = lastContact.id;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return allContacts;
|
return allContacts;
|
||||||
@@ -250,16 +237,16 @@ export const getUniqueWechatAccountIds = (
|
|||||||
export const getAllGroupList = async () => {
|
export const getAllGroupList = async () => {
|
||||||
try {
|
try {
|
||||||
let allContacts = [];
|
let allContacts = [];
|
||||||
let prevId = 0;
|
let page = 1;
|
||||||
const count = 1000;
|
const limit = 1000;
|
||||||
let hasMore = true;
|
let hasMore = true;
|
||||||
|
|
||||||
while (hasMore) {
|
while (hasMore) {
|
||||||
const contractList = await getGroupList({
|
const Result = await getGroupList({
|
||||||
prevId,
|
page,
|
||||||
count,
|
limit,
|
||||||
});
|
});
|
||||||
|
const contractList = Result.list;
|
||||||
if (
|
if (
|
||||||
!contractList ||
|
!contractList ||
|
||||||
!Array.isArray(contractList) ||
|
!Array.isArray(contractList) ||
|
||||||
@@ -272,12 +259,12 @@ export const getAllGroupList = async () => {
|
|||||||
allContacts = [...allContacts, ...contractList];
|
allContacts = [...allContacts, ...contractList];
|
||||||
|
|
||||||
// 如果返回的数据少于请求的数量,说明已经没有更多数据了
|
// 如果返回的数据少于请求的数量,说明已经没有更多数据了
|
||||||
if (contractList.length < count) {
|
if (contractList.length < limit) {
|
||||||
hasMore = false;
|
hasMore = false;
|
||||||
} else {
|
} else {
|
||||||
// 获取最后一条数据的id作为下一次请求的prevId
|
// 获取最后一条数据的id作为下一次请求的page
|
||||||
const lastContact = contractList[contractList.length - 1];
|
const lastContact = contractList[contractList.length - 1];
|
||||||
prevId = lastContact.id;
|
page = lastContact.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export interface CkChatState {
|
|||||||
clearkfUserList: () => void;
|
clearkfUserList: () => void;
|
||||||
addChatSession: (session: any) => void;
|
addChatSession: (session: any) => void;
|
||||||
deleteChatSession: (sessionId: number) => void;
|
deleteChatSession: (sessionId: number) => void;
|
||||||
|
pinChatSessionToTop: (sessionId: number) => void;
|
||||||
setUserInfo: (userInfo: CkUserInfo) => void;
|
setUserInfo: (userInfo: CkUserInfo) => void;
|
||||||
clearUserInfo: () => void;
|
clearUserInfo: () => void;
|
||||||
updateAccount: (account: Partial<CkAccount>) => void;
|
updateAccount: (account: Partial<CkAccount>) => void;
|
||||||
|
|||||||
@@ -375,11 +375,11 @@ export const useCkChatStore = createPersistStore<CkChatState>(
|
|||||||
set(state => {
|
set(state => {
|
||||||
// 检查是否已存在相同id的会话
|
// 检查是否已存在相同id的会话
|
||||||
const exists = state.chatSessions.some(item => item.id === session.id);
|
const exists = state.chatSessions.some(item => item.id === session.id);
|
||||||
// 如果已存在则不添加,否则添加到列表中
|
// 如果已存在则不添加,否则添加到列表顶部
|
||||||
return {
|
return {
|
||||||
chatSessions: exists
|
chatSessions: exists
|
||||||
? state.chatSessions
|
? state.chatSessions
|
||||||
: [...state.chatSessions, session as ContractData | weChatGroup],
|
: [session as ContractData | weChatGroup, ...state.chatSessions],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -399,6 +399,24 @@ export const useCkChatStore = createPersistStore<CkChatState>(
|
|||||||
//当前选中的客户清空
|
//当前选中的客户清空
|
||||||
getClearCurrentContact();
|
getClearCurrentContact();
|
||||||
},
|
},
|
||||||
|
// 置顶聊天会话到列表顶部
|
||||||
|
pinChatSessionToTop: (sessionId: number) => {
|
||||||
|
set(state => {
|
||||||
|
const sessionIndex = state.chatSessions.findIndex(
|
||||||
|
item => item.id === sessionId,
|
||||||
|
);
|
||||||
|
if (sessionIndex === -1) return state; // 会话不存在
|
||||||
|
|
||||||
|
const session = state.chatSessions[sessionIndex];
|
||||||
|
const otherSessions = state.chatSessions.filter(
|
||||||
|
item => item.id !== sessionId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
chatSessions: [session, ...otherSessions],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
// 设置用户信息
|
// 设置用户信息
|
||||||
setUserInfo: (userInfo: CkUserInfo) => {
|
setUserInfo: (userInfo: CkUserInfo) => {
|
||||||
set({ userInfo, isLoggedIn: true });
|
set({ userInfo, isLoggedIn: true });
|
||||||
@@ -524,4 +542,6 @@ export const clearSearchKeyword = () =>
|
|||||||
useCkChatStore.getState().clearSearchKeyword();
|
useCkChatStore.getState().clearSearchKeyword();
|
||||||
export const searchContactsAndGroups = () =>
|
export const searchContactsAndGroups = () =>
|
||||||
useCkChatStore.getState().searchContactsAndGroups();
|
useCkChatStore.getState().searchContactsAndGroups();
|
||||||
|
export const pinChatSessionToTop = (sessionId: number) =>
|
||||||
|
useCkChatStore.getState().pinChatSessionToTop(sessionId);
|
||||||
useCkChatStore.getState().getKfSelectedUser();
|
useCkChatStore.getState().getKfSelectedUser();
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface User {
|
|||||||
s2_accountId: string;
|
s2_accountId: string;
|
||||||
createTime: string;
|
createTime: string;
|
||||||
updateTime: string | null;
|
updateTime: string | null;
|
||||||
|
tokens: number;
|
||||||
lastLoginIp: string;
|
lastLoginIp: string;
|
||||||
lastLoginTime: number;
|
lastLoginTime: number;
|
||||||
}
|
}
|
||||||
@@ -61,6 +62,7 @@ export const useUserStore = createPersistStore<UserState>(
|
|||||||
s2_accountId: userInfo.s2_accountId,
|
s2_accountId: userInfo.s2_accountId,
|
||||||
createTime: userInfo.createTime,
|
createTime: userInfo.createTime,
|
||||||
updateTime: userInfo.updateTime,
|
updateTime: userInfo.updateTime,
|
||||||
|
tokens: userInfo.tokens,
|
||||||
lastLoginIp: userInfo.lastLoginIp,
|
lastLoginIp: userInfo.lastLoginIp,
|
||||||
lastLoginTime: userInfo.lastLoginTime,
|
lastLoginTime: userInfo.lastLoginTime,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
* 包含聊天消息、联系人管理、朋友圈等功能的状态和方法
|
* 包含聊天消息、联系人管理、朋友圈等功能的状态和方法
|
||||||
*/
|
*/
|
||||||
export interface WeChatState {
|
export interface WeChatState {
|
||||||
|
aiQuoteMessageContent: number;
|
||||||
|
updateAiQuoteMessageContent: (message: number) => void;
|
||||||
quoteMessageContent: string;
|
quoteMessageContent: string;
|
||||||
updateQuoteMessageContent: (value: string) => void;
|
updateQuoteMessageContent: (value: string) => void;
|
||||||
// ==================== Transmit Module =========Start===========
|
// ==================== Transmit Module =========Start===========
|
||||||
|
|||||||
@@ -11,13 +11,19 @@ import {
|
|||||||
likeListItem,
|
likeListItem,
|
||||||
CommentItem,
|
CommentItem,
|
||||||
} from "@/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.data";
|
} from "@/pages/pc/ckbox/weChat/components/SidebarMenu/FriendsCicle/index.data";
|
||||||
import { clearUnreadCount, updateConfig } from "@/pages/pc/ckbox/api";
|
import {
|
||||||
|
clearUnreadCount1,
|
||||||
|
clearUnreadCount2,
|
||||||
|
updateConfig,
|
||||||
|
getFriendInjectConfig,
|
||||||
|
} from "@/pages/pc/ckbox/api";
|
||||||
import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
||||||
import { weChatGroupService, contractService } from "@/utils/db";
|
import { weChatGroupService, contractService } from "@/utils/db";
|
||||||
import {
|
import {
|
||||||
addChatSession,
|
addChatSession,
|
||||||
updateChatSession,
|
updateChatSession,
|
||||||
useCkChatStore,
|
useCkChatStore,
|
||||||
|
pinChatSessionToTop,
|
||||||
} from "@/store/module/ckchat/ckchat";
|
} from "@/store/module/ckchat/ckchat";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,6 +33,11 @@ import {
|
|||||||
export const useWeChatStore = create<WeChatState>()(
|
export const useWeChatStore = create<WeChatState>()(
|
||||||
persist(
|
persist(
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
|
//当前用户的ai接管状态
|
||||||
|
aiQuoteMessageContent: 0,
|
||||||
|
updateAiQuoteMessageContent: (message: number) => {
|
||||||
|
set({ aiQuoteMessageContent: message });
|
||||||
|
},
|
||||||
quoteMessageContent: "",
|
quoteMessageContent: "",
|
||||||
updateQuoteMessageContent: (message: string) => {
|
updateQuoteMessageContent: (message: string) => {
|
||||||
set({ quoteMessageContent: message });
|
set({ quoteMessageContent: message });
|
||||||
@@ -140,19 +151,37 @@ export const useWeChatStore = create<WeChatState>()(
|
|||||||
const state = useWeChatStore.getState();
|
const state = useWeChatStore.getState();
|
||||||
// 切换联系人时清空当前消息,等待重新加载
|
// 切换联系人时清空当前消息,等待重新加载
|
||||||
set({ currentMessages: [], openTransmitModal: false });
|
set({ currentMessages: [], openTransmitModal: false });
|
||||||
clearUnreadCount([contract.id]).then(() => {
|
|
||||||
if (isExist) {
|
const params: any = {};
|
||||||
updateChatSession({ ...contract, unreadCount: 0 });
|
|
||||||
} else {
|
if (!contract.chatroomId) {
|
||||||
addChatSession(contract);
|
params.wechatFriendId = contract.id;
|
||||||
}
|
} else {
|
||||||
set({ currentContract: contract });
|
params.wechatChatroomId = contract.id;
|
||||||
updateConfig({
|
}
|
||||||
id: contract.id,
|
|
||||||
config: { chat: true },
|
clearUnreadCount1(params);
|
||||||
});
|
clearUnreadCount2([contract.id]);
|
||||||
state.loadChatMessages(true, 4704624000000);
|
getFriendInjectConfig({
|
||||||
|
friendId: contract.id,
|
||||||
|
wechatAccountId: contract.wechatAccountId,
|
||||||
|
}).then(result => {
|
||||||
|
set({ aiQuoteMessageContent: result });
|
||||||
});
|
});
|
||||||
|
if (isExist) {
|
||||||
|
updateChatSession({
|
||||||
|
...contract,
|
||||||
|
config: { unreadCount: 0 },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
addChatSession(contract);
|
||||||
|
}
|
||||||
|
set({ currentContract: contract });
|
||||||
|
updateConfig({
|
||||||
|
id: contract.id,
|
||||||
|
config: { chat: true },
|
||||||
|
});
|
||||||
|
state.loadChatMessages(true, 4704624000000);
|
||||||
},
|
},
|
||||||
|
|
||||||
// ==================== 消息加载方法 ====================
|
// ==================== 消息加载方法 ====================
|
||||||
@@ -285,6 +314,8 @@ export const useWeChatStore = create<WeChatState>()(
|
|||||||
if (session) {
|
if (session) {
|
||||||
session.unreadCount = Number(session.unreadCount) + 1;
|
session.unreadCount = Number(session.unreadCount) + 1;
|
||||||
updateChatSession(session);
|
updateChatSession(session);
|
||||||
|
// 将接收到新消息的会话置顶到列表顶部
|
||||||
|
pinChatSessionToTop(getMessageId);
|
||||||
} else {
|
} else {
|
||||||
// 如果会话不存在,创建新会话
|
// 如果会话不存在,创建新会话
|
||||||
if (isWechatGroup) {
|
if (isWechatGroup) {
|
||||||
@@ -294,6 +325,7 @@ export const useWeChatStore = create<WeChatState>()(
|
|||||||
...group,
|
...group,
|
||||||
unreadCount: 1,
|
unreadCount: 1,
|
||||||
});
|
});
|
||||||
|
// 新创建的会话会自动添加到列表顶部,无需额外置顶
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const [user] = await contractService.findByIds(getMessageId);
|
const [user] = await contractService.findByIds(getMessageId);
|
||||||
@@ -301,6 +333,7 @@ export const useWeChatStore = create<WeChatState>()(
|
|||||||
...user,
|
...user,
|
||||||
unreadCount: 1,
|
unreadCount: 1,
|
||||||
});
|
});
|
||||||
|
// 新创建的会话会自动添加到列表顶部,无需额外置顶
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -392,7 +392,7 @@ export const useWebSocketStore = createPersistStore<WebSocketState>(
|
|||||||
|
|
||||||
set({
|
set({
|
||||||
messages: [...currentState.messages, newMessage],
|
messages: [...currentState.messages, newMessage],
|
||||||
unreadCount: currentState.unreadCount + 1,
|
unreadCount: currentState.config.unreadCount + 1,
|
||||||
});
|
});
|
||||||
//消息处理器
|
//消息处理器
|
||||||
msgManageCore(data);
|
msgManageCore(data);
|
||||||
|
|||||||
@@ -64,12 +64,12 @@ class CunkebaoDatabase extends Dexie {
|
|||||||
kfUsers:
|
kfUsers:
|
||||||
"serverId, id, tenantId, wechatId, nickname, alias, avatar, gender, region, signature, bindQQ, bindEmail, bindMobile, createTime, currentDeviceId, isDeleted, deleteTime, groupId, memo, wechatVersion, lastUpdateTime, isOnline",
|
"serverId, id, tenantId, wechatId, nickname, alias, avatar, gender, region, signature, bindQQ, bindEmail, bindMobile, createTime, currentDeviceId, isDeleted, deleteTime, groupId, memo, wechatVersion, lastUpdateTime, isOnline",
|
||||||
weChatGroup:
|
weChatGroup:
|
||||||
"serverId, id, wechatAccountId, tenantId, accountId, chatroomId, chatroomOwner, conRemark, nickname, chatroomAvatar,wechatChatroomId, groupId, config, unreadCount, notice, selfDisplyName",
|
"serverId, id, wechatAccountId, tenantId, accountId, chatroomId, chatroomOwner, conRemark, nickname, chatroomAvatar,wechatChatroomId, groupId, config, notice, selfDisplyName",
|
||||||
contracts:
|
contracts:
|
||||||
"serverId, id, wechatAccountId, wechatId, alias, conRemark, nickname, quanPin, avatar, gender, region, addFrom, phone, signature, accountId, extendFields, city, lastUpdateTime, isPassed, tenantId, groupId, thirdParty, additionalPicture, desc, config, lastMessageTime, unreadCount, duplicate",
|
"serverId, id, wechatAccountId, wechatId, alias, conRemark, nickname, quanPin, avatar, gender, region, addFrom, phone, signature, accountId, extendFields, city, lastUpdateTime, isPassed, tenantId, groupId, thirdParty, additionalPicture, desc, config, lastMessageTime, duplicate",
|
||||||
newContractList: "serverId, id, groupName, contacts",
|
newContractList: "serverId, id, groupName, contacts",
|
||||||
messageList:
|
messageList:
|
||||||
"serverId, id, dataType, wechatAccountId, tenantId, accountId, nickname, avatar, groupId, config, labels, unreadCount, wechatId, alias, conRemark, quanPin, gender, region, addFrom, phone, signature, extendFields, city, lastUpdateTime, isPassed, thirdParty, additionalPicture, desc, lastMessageTime, duplicate, chatroomId, chatroomOwner, chatroomAvatar, notice, selfDisplyName",
|
"serverId, id, dataType, wechatAccountId, tenantId, accountId, nickname, avatar, groupId, config, labels, wechatId, alias, conRemark, quanPin, gender, region, addFrom, phone, signature, extendFields, city, lastUpdateTime, isPassed, thirdParty, additionalPicture, desc, lastMessageTime, duplicate, chatroomId, chatroomOwner, chatroomAvatar, notice, selfDisplyName",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user