Merge branch 'yongpxu-dev' into yongpxu-dev2
This commit is contained in:
@@ -10,7 +10,6 @@
|
|||||||
"antd-mobile-icons": "^0.3.0",
|
"antd-mobile-icons": "^0.3.0",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"dexie": "^4.2.0",
|
|
||||||
"echarts": "^5.6.0",
|
"echarts": "^5.6.0",
|
||||||
"echarts-for-react": "^3.0.2",
|
"echarts-for-react": "^3.0.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|||||||
@@ -7,38 +7,14 @@ import dayjs from "dayjs";
|
|||||||
import "dayjs/locale/zh-cn";
|
import "dayjs/locale/zh-cn";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import "./styles/global.scss";
|
import "./styles/global.scss";
|
||||||
import { db } from "@/utils/db"; // 引入数据库实例
|
|
||||||
|
|
||||||
// 设置dayjs为中文
|
// 设置dayjs为中文
|
||||||
dayjs.locale("zh-cn");
|
dayjs.locale("zh-cn");
|
||||||
|
|
||||||
// 数据库初始化
|
// 渲染应用
|
||||||
async function initializeApp() {
|
const root = createRoot(document.getElementById("root")!);
|
||||||
try {
|
root.render(
|
||||||
// 确保数据库已打开
|
<ConfigProvider locale={zhCN}>
|
||||||
await db.open();
|
<App />
|
||||||
console.log("数据库初始化成功");
|
</ConfigProvider>,
|
||||||
|
);
|
||||||
// 调试模式:清理所有数据
|
|
||||||
console.log("调试模式:开始清理数据库数据...");
|
|
||||||
await db.kfUsers.clear();
|
|
||||||
await db.weChatGroup.clear();
|
|
||||||
await db.contracts.clear();
|
|
||||||
await db.newContractList.clear();
|
|
||||||
console.log("数据库数据清理完成");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("数据库初始化失败:", error);
|
|
||||||
// 可以选择显示错误提示或使用降级方案
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染应用
|
|
||||||
const root = createRoot(document.getElementById("root")!);
|
|
||||||
root.render(
|
|
||||||
<ConfigProvider locale={zhCN}>
|
|
||||||
<App />
|
|
||||||
</ConfigProvider>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 启动应用
|
|
||||||
initializeApp();
|
|
||||||
|
|||||||
35
Cunkebao/src/pages/mobile/mine/recharge/index/api.ts
Normal file
35
Cunkebao/src/pages/mobile/mine/recharge/index/api.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import request from "@/api/request";
|
||||||
|
|
||||||
|
interface taocanItem {
|
||||||
|
id: 1;
|
||||||
|
name: "试用套餐";
|
||||||
|
tokens: "2,800";
|
||||||
|
price: 9800;
|
||||||
|
originalPrice: 140;
|
||||||
|
description: ["适合新用户体验", "包含基础AI功能", "永久有效", "客服支持"];
|
||||||
|
sort: 1;
|
||||||
|
isTrial: 1;
|
||||||
|
isRecommend: 0;
|
||||||
|
isHot: 0;
|
||||||
|
isVip: 0;
|
||||||
|
status: 1;
|
||||||
|
isDel: 0;
|
||||||
|
delTime: null;
|
||||||
|
createTime: "2025-09-29 16:53:06";
|
||||||
|
updateTime: "2025-09-29 16:53:06";
|
||||||
|
discount: 30;
|
||||||
|
unitPrice: 3.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface taocanList {
|
||||||
|
list: taocanItem[];
|
||||||
|
}
|
||||||
|
// 套餐列表
|
||||||
|
export function getTaocanList(): Promise<taocanList> {
|
||||||
|
return request("/v1/tokens/list", {}, "GET");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支付id和price 从套餐列表取对应的价格
|
||||||
|
export function pay(params: { id: string; price: number }) {
|
||||||
|
return request("/v1/tokens/pay", params, "POST");
|
||||||
|
}
|
||||||
@@ -123,10 +123,18 @@
|
|||||||
margin: 4px 0;
|
margin: 4px 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.quick-btn-active {
|
.quick-btn-active {
|
||||||
@extend .quick-btn;
|
@extend .quick-btn;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
border: 2px solid var(--primary-color);
|
||||||
}
|
}
|
||||||
.recharge-main-btn {
|
.recharge-main-btn {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Card, Button, Toast, Tabs } from "antd-mobile";
|
import { Card, Button, Toast, Tabs, Dialog } from "antd-mobile";
|
||||||
import { useUserStore } from "@/store/module/user";
|
|
||||||
import style from "./index.module.scss";
|
import style from "./index.module.scss";
|
||||||
import {
|
import {
|
||||||
WalletOutlined,
|
WalletOutlined,
|
||||||
@@ -12,36 +11,7 @@ import {
|
|||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import NavCommon from "@/components/NavCommon";
|
import NavCommon from "@/components/NavCommon";
|
||||||
import Layout from "@/components/Layout/Layout";
|
import Layout from "@/components/Layout/Layout";
|
||||||
|
import { getTaocanList, pay } from "./api";
|
||||||
const quickAmounts = [50, 100, 200, 500, 1000];
|
|
||||||
|
|
||||||
// AI服务套餐数据
|
|
||||||
const aiServicePackages = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: "入门套餐",
|
|
||||||
tag: "推荐",
|
|
||||||
tagColor: "blue",
|
|
||||||
description: "适合个人用户体验AI服务",
|
|
||||||
usage: "可使用AI服务约110次",
|
|
||||||
price: 100,
|
|
||||||
originalPrice: 110,
|
|
||||||
gift: 10,
|
|
||||||
actualAmount: 110,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: "标准套餐",
|
|
||||||
tag: "热门",
|
|
||||||
tagColor: "green",
|
|
||||||
description: "适合小团队日常使用",
|
|
||||||
usage: "可使用AI服务约580次",
|
|
||||||
price: 500,
|
|
||||||
originalPrice: 580,
|
|
||||||
gift: 80,
|
|
||||||
actualAmount: 580,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// AI服务列表数据
|
// AI服务列表数据
|
||||||
const aiServices = [
|
const aiServices = [
|
||||||
@@ -120,25 +90,77 @@ const versionPackages = [
|
|||||||
|
|
||||||
const Recharge: React.FC = () => {
|
const Recharge: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user } = useUserStore();
|
|
||||||
// 假设余额从后端接口获取,实际可用props或store传递
|
// 假设余额从后端接口获取,实际可用props或store传递
|
||||||
const [balance, setBalance] = useState(0);
|
const [balance] = useState(0);
|
||||||
const [selected, setSelected] = useState<number | null>(null);
|
const [selected, setSelected] = useState<any | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [activeTab, setActiveTab] = useState("account");
|
const [activeTab, setActiveTab] = useState("account");
|
||||||
|
const [taocanList, setTaocanList] = useState<any[]>([]);
|
||||||
|
|
||||||
|
// 加载套餐列表
|
||||||
|
useEffect(() => {
|
||||||
|
const loadTaocanList = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getTaocanList();
|
||||||
|
if (res.list) {
|
||||||
|
setTaocanList(res.list);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("加载套餐列表失败:", error);
|
||||||
|
Toast.show({ content: "加载套餐列表失败", position: "top" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadTaocanList();
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 充值操作
|
// 充值操作
|
||||||
const handleRecharge = async () => {
|
const handleRecharge = async () => {
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
Toast.show({ content: "请选择充值金额", position: "top" });
|
Toast.show({ content: "请选择充值套餐", position: "top" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setTimeout(() => {
|
try {
|
||||||
setBalance(b => b + selected);
|
const res = await pay({
|
||||||
Toast.show({ content: `充值成功,已到账¥${selected}` });
|
id: selected.id,
|
||||||
|
price: selected.price,
|
||||||
|
});
|
||||||
|
// 假设返回的是二维码链接,存储在res中
|
||||||
|
if (res) {
|
||||||
|
// 显示二维码弹窗
|
||||||
|
Dialog.show({
|
||||||
|
content: (
|
||||||
|
<div style={{ textAlign: "center", padding: "20px" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginBottom: "16px",
|
||||||
|
fontSize: "16px",
|
||||||
|
fontWeight: "500",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
请使用微信扫码支付
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
src={res.code_url as any}
|
||||||
|
alt="支付二维码"
|
||||||
|
style={{ width: "250px", height: "250px", margin: "0 auto" }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{ marginTop: "16px", color: "#666", fontSize: "14px" }}
|
||||||
|
>
|
||||||
|
支付金额: ¥{selected.price / 100}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
closeOnMaskClick: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("支付失败:", error);
|
||||||
|
Toast.show({ content: "支付失败,请重试", position: "top" });
|
||||||
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, 1200);
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 渲染账户充值tab内容
|
// 渲染账户充值tab内容
|
||||||
@@ -156,23 +178,80 @@ const Recharge: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className={style["quick-card"]}>
|
<Card className={style["quick-card"]}>
|
||||||
<div className={style["quick-title"]}>快捷充值</div>
|
<div className={style["quick-title"]}>选择套餐</div>
|
||||||
<div className={style["quick-list"]}>
|
<div className={style["quick-list"]}>
|
||||||
{quickAmounts.map(amt => (
|
{taocanList.map(item => (
|
||||||
<Button
|
<Button
|
||||||
key={amt}
|
key={item.id}
|
||||||
color={selected === amt ? "primary" : "default"}
|
color={selected?.id === item.id ? "primary" : "default"}
|
||||||
className={
|
className={
|
||||||
selected === amt
|
selected?.id === item.id
|
||||||
? style["quick-btn-active"]
|
? style["quick-btn-active"]
|
||||||
: style["quick-btn"]
|
: style["quick-btn"]
|
||||||
}
|
}
|
||||||
onClick={() => setSelected(amt)}
|
onClick={() => setSelected(item)}
|
||||||
>
|
>
|
||||||
¥{amt}
|
<div>
|
||||||
|
<div>¥{item.price / 100}</div>
|
||||||
|
{item.discount && (
|
||||||
|
<div style={{ fontSize: "12px", color: "#999" }}>
|
||||||
|
{item.discount}折
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{selected && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginBottom: "12px",
|
||||||
|
padding: "12px",
|
||||||
|
background: "#f5f5f5",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ marginBottom: "6px" }}>
|
||||||
|
<span style={{ fontWeight: "500" }}>{selected.name}</span>
|
||||||
|
{selected.isRecommend === 1 && (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
marginLeft: "8px",
|
||||||
|
fontSize: "12px",
|
||||||
|
color: "#1890ff",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
推荐
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{selected.isHot === 1 && (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
marginLeft: "8px",
|
||||||
|
fontSize: "12px",
|
||||||
|
color: "#ff4d4f",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
热门
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: "14px", color: "#666" }}>
|
||||||
|
包含 {selected.tokens} Tokens
|
||||||
|
</div>
|
||||||
|
{selected.originalPrice && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "12px",
|
||||||
|
color: "#999",
|
||||||
|
textDecoration: "line-through",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
原价: ¥{selected.originalPrice / 100}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
block
|
block
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|||||||
@@ -1,319 +0,0 @@
|
|||||||
/**
|
|
||||||
* 数据库工具类 - 使用serverId作为主键的优化架构
|
|
||||||
*
|
|
||||||
* 架构设计:
|
|
||||||
* 1. 使用serverId作为数据库主键,直接对应接口返回的id字段
|
|
||||||
* 2. 保留原始的id字段,用于存储接口数据的完整性
|
|
||||||
* 3. 简化数据处理逻辑,避免ID映射的复杂性
|
|
||||||
*
|
|
||||||
* 优势:
|
|
||||||
* - 直接使用服务器ID作为主键,避免ID冲突
|
|
||||||
* - 保持数据的一致性和可追溯性
|
|
||||||
* - 简化查询逻辑,提高性能
|
|
||||||
* - 支持重复数据检测和去重
|
|
||||||
*
|
|
||||||
* 使用方法:
|
|
||||||
* - 存储接口数据:使用 createWithServerId() 或 createManyWithServerId()
|
|
||||||
* - 查询数据:使用 findById(id) 根据原始ID查询,或 findByPrimaryKey(serverId) 根据主键查询
|
|
||||||
* - 批量查询:使用 findByIds([id1, id2, ...]) 根据原始ID批量查询
|
|
||||||
* - 内部操作:serverId作为主键用于数据库内部管理
|
|
||||||
*
|
|
||||||
* 示例:
|
|
||||||
* const serverData = { id: 1001, name: '测试', ... }; // 接口返回的数据
|
|
||||||
* const serverId = await service.createWithServerId(serverData); // 存储,返回serverId
|
|
||||||
* const data = await service.findById(1001); // 根据原始ID查询(用户友好)
|
|
||||||
* const dataByPK = await service.findByPrimaryKey(serverId); // 根据主键查询(内部使用)
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Dexie, { Table } from "dexie";
|
|
||||||
import {
|
|
||||||
KfUserListData,
|
|
||||||
weChatGroup,
|
|
||||||
ContractData,
|
|
||||||
MessageListData,
|
|
||||||
} from "@/pages/pc/ckbox/data";
|
|
||||||
|
|
||||||
// 数据类型定义,使用serverId作为主键
|
|
||||||
export interface KfUserWithServerId extends Omit<KfUserListData, "id"> {
|
|
||||||
serverId: number | string; // 服务器ID作为主键
|
|
||||||
id?: number; // 接口数据的原始ID字段
|
|
||||||
}
|
|
||||||
|
|
||||||
// 新联系人列表数据接口
|
|
||||||
export interface NewContactListData {
|
|
||||||
serverId: number | string; // 服务器ID作为主键
|
|
||||||
id?: number; // 接口数据的原始ID字段
|
|
||||||
groupName: string;
|
|
||||||
contacts: ContractData[];
|
|
||||||
weChatGroup: weChatGroup[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 数据库类
|
|
||||||
class CunkebaoDatabase extends Dexie {
|
|
||||||
kfUsers!: Table<KfUserWithServerId>;
|
|
||||||
weChatGroup!: Table<weChatGroup>;
|
|
||||||
contracts!: Table<ContractData>;
|
|
||||||
newContractList!: Table<NewContactListData>;
|
|
||||||
messageList!: Table<MessageListData>;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super("CunkebaoDatabase");
|
|
||||||
|
|
||||||
// 版本1:使用serverId作为主键的架构
|
|
||||||
this.version(1).stores({
|
|
||||||
kfUsers:
|
|
||||||
"serverId, id, tenantId, wechatId, nickname, alias, avatar, gender, region, signature, bindQQ, bindEmail, bindMobile, createTime, currentDeviceId, isDeleted, deleteTime, groupId, memo, wechatVersion, lastUpdateTime, isOnline",
|
|
||||||
weChatGroup:
|
|
||||||
"serverId, id, wechatAccountId, tenantId, accountId, chatroomId, chatroomOwner, conRemark, nickname, chatroomAvatar,wechatChatroomId, groupId, config, unreadCount, notice, selfDisplyName",
|
|
||||||
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",
|
|
||||||
newContractList: "serverId, id, groupName, contacts",
|
|
||||||
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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建数据库实例
|
|
||||||
export const db = new CunkebaoDatabase();
|
|
||||||
|
|
||||||
// 简单的数据库操作类
|
|
||||||
export class DatabaseService<T> {
|
|
||||||
constructor(private table: Table<T>) {}
|
|
||||||
|
|
||||||
// 基础 CRUD 操作 - 使用serverId作为主键
|
|
||||||
async create(data: Omit<T, "serverId">): Promise<string | number> {
|
|
||||||
return await this.table.add(data as T);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建数据(直接使用接口数据)
|
|
||||||
// 接口数据的id字段直接作为serverId主键,原id字段保留
|
|
||||||
async createWithServerId(data: any): Promise<string | number> {
|
|
||||||
const dataToInsert = {
|
|
||||||
...data,
|
|
||||||
serverId: data.id, // 使用接口的id作为serverId主键
|
|
||||||
};
|
|
||||||
return await this.table.add(dataToInsert as T);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据原始ID查询(用户友好的查询方法)
|
|
||||||
async findById(id: string | number): Promise<T | undefined> {
|
|
||||||
return await this.table.where("id").equals(id).first();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据serverId查询(内部主键查询)
|
|
||||||
async findByPrimaryKey(serverId: string | number): Promise<T | undefined> {
|
|
||||||
return await this.table.get(serverId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async findAll(): Promise<T[]> {
|
|
||||||
return await this.table.toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(serverId: string | number, data: Partial<T>): Promise<number> {
|
|
||||||
return await this.table.update(serverId, data as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateMany(
|
|
||||||
dataList: { serverId: string | number; data: Partial<T> }[],
|
|
||||||
): Promise<number> {
|
|
||||||
return await this.table.bulkUpdate(
|
|
||||||
dataList.map(item => ({
|
|
||||||
key: item.serverId,
|
|
||||||
changes: item.data as any,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async createMany(
|
|
||||||
dataList: Omit<T, "serverId">[],
|
|
||||||
): Promise<(string | number)[]> {
|
|
||||||
return await this.table.bulkAdd(dataList as T[], { allKeys: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量创建数据(直接使用接口数据)
|
|
||||||
// 接口数据的id字段直接作为serverId主键
|
|
||||||
async createManyWithServerId(dataList: any[]): Promise<(string | number)[]> {
|
|
||||||
// 检查是否存在重复的serverId
|
|
||||||
const serverIds = dataList.map(item => item.id);
|
|
||||||
const existingData = await this.table
|
|
||||||
.where("serverId")
|
|
||||||
.anyOf(serverIds)
|
|
||||||
.toArray();
|
|
||||||
const existingServerIds = new Set(
|
|
||||||
existingData.map((item: any) => item.serverId),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 过滤掉已存在的数据
|
|
||||||
const newData = dataList.filter(item => !existingServerIds.has(item.id));
|
|
||||||
|
|
||||||
if (newData.length === 0) {
|
|
||||||
console.log("所有数据都已存在,跳过插入");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const processedData = newData.map(item => ({
|
|
||||||
...item,
|
|
||||||
serverId: item.id, // 使用接口的id作为serverId主键
|
|
||||||
}));
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`插入 ${processedData.length} 条新数据,跳过 ${dataList.length - newData.length} 条重复数据`,
|
|
||||||
);
|
|
||||||
return await this.table.bulkAdd(processedData as T[], { allKeys: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(serverId: string | number): Promise<void> {
|
|
||||||
await this.table.delete(serverId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async clear(): Promise<void> {
|
|
||||||
await this.table.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 条件查询
|
|
||||||
async findWhere(field: keyof T, value: any): Promise<T[]> {
|
|
||||||
return await this.table
|
|
||||||
.where(field as string)
|
|
||||||
.equals(value)
|
|
||||||
.toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据服务器ID查询(兼容性方法)
|
|
||||||
async findByServerId(serverId: any): Promise<T | undefined> {
|
|
||||||
return await this.table.get(serverId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据原始ID批量查询
|
|
||||||
async findByIds(ids: (string | number)[]): Promise<T[]> {
|
|
||||||
return await this.table.where("id").anyOf(ids).toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 多值查询(IN 查询)
|
|
||||||
async findWhereIn(field: keyof T, values: any[]): Promise<T[]> {
|
|
||||||
return await this.table
|
|
||||||
.where(field as string)
|
|
||||||
.anyOf(values)
|
|
||||||
.toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 范围查询
|
|
||||||
async findWhereBetween(field: keyof T, min: any, max: any): Promise<T[]> {
|
|
||||||
return await this.table
|
|
||||||
.where(field as string)
|
|
||||||
.between(min, max)
|
|
||||||
.toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 模糊查询(以指定字符串开头)
|
|
||||||
async findWhereStartsWith(field: keyof T, prefix: string): Promise<T[]> {
|
|
||||||
return await this.table
|
|
||||||
.where(field as string)
|
|
||||||
.startsWith(prefix)
|
|
||||||
.toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 不等于查询
|
|
||||||
async findWhereNot(field: keyof T, value: any): Promise<T[]> {
|
|
||||||
return await this.table
|
|
||||||
.where(field as string)
|
|
||||||
.notEqual(value)
|
|
||||||
.toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 大于查询
|
|
||||||
async findWhereGreaterThan(field: keyof T, value: any): Promise<T[]> {
|
|
||||||
return await this.table
|
|
||||||
.where(field as string)
|
|
||||||
.above(value)
|
|
||||||
.toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 小于查询
|
|
||||||
async findWhereLessThan(field: keyof T, value: any): Promise<T[]> {
|
|
||||||
return await this.table
|
|
||||||
.where(field as string)
|
|
||||||
.below(value)
|
|
||||||
.toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 复合条件查询
|
|
||||||
async findWhereMultiple(
|
|
||||||
conditions: {
|
|
||||||
field: keyof T;
|
|
||||||
operator: "equals" | "above" | "below" | "startsWith";
|
|
||||||
value: any;
|
|
||||||
}[],
|
|
||||||
): Promise<T[]> {
|
|
||||||
let collection = this.table.toCollection();
|
|
||||||
|
|
||||||
for (const condition of conditions) {
|
|
||||||
const { field, operator, value } = condition;
|
|
||||||
collection = collection.and(item => {
|
|
||||||
const fieldValue = (item as any)[field];
|
|
||||||
switch (operator) {
|
|
||||||
case "equals":
|
|
||||||
return fieldValue === value;
|
|
||||||
case "above":
|
|
||||||
return fieldValue > value;
|
|
||||||
case "below":
|
|
||||||
return fieldValue < value;
|
|
||||||
case "startsWith":
|
|
||||||
return (
|
|
||||||
typeof fieldValue === "string" && fieldValue.startsWith(value)
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return await collection.toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分页查询
|
|
||||||
async findWithPagination(
|
|
||||||
page: number = 1,
|
|
||||||
limit: number = 10,
|
|
||||||
): Promise<{ data: T[]; total: number; page: number; limit: number }> {
|
|
||||||
const offset = (page - 1) * limit;
|
|
||||||
const total = await this.table.count();
|
|
||||||
const data = await this.table.offset(offset).limit(limit).toArray();
|
|
||||||
|
|
||||||
return { data, total, page, limit };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 排序查询
|
|
||||||
async findAllSorted(
|
|
||||||
field: keyof T,
|
|
||||||
direction: "asc" | "desc" = "asc",
|
|
||||||
): Promise<T[]> {
|
|
||||||
const collection = this.table.orderBy(field as string);
|
|
||||||
return direction === "desc"
|
|
||||||
? await collection.reverse().toArray()
|
|
||||||
: await collection.toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 统计
|
|
||||||
async count(): Promise<number> {
|
|
||||||
return await this.table.count();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 条件统计
|
|
||||||
async countWhere(field: keyof T, value: any): Promise<number> {
|
|
||||||
return await this.table
|
|
||||||
.where(field as string)
|
|
||||||
.equals(value)
|
|
||||||
.count();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建各表的服务实例
|
|
||||||
export const kfUserService = new DatabaseService(db.kfUsers);
|
|
||||||
export const weChatGroupService = new DatabaseService(db.weChatGroup);
|
|
||||||
export const contractService = new DatabaseService(db.contracts);
|
|
||||||
export const newContactListService = new DatabaseService(db.newContractList);
|
|
||||||
export const messageListService = new DatabaseService(db.messageList);
|
|
||||||
|
|
||||||
// 默认导出数据库实例
|
|
||||||
export default db;
|
|
||||||
@@ -15,8 +15,4 @@ Route::group('v1/ai', function () {
|
|||||||
Route::group('doubao', function () {
|
Route::group('doubao', function () {
|
||||||
Route::post('text', 'app\ai\controller\DouBaoAI@text');
|
Route::post('text', 'app\ai\controller\DouBaoAI@text');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
})->middleware(['jwt']);
|
})->middleware(['jwt']);
|
||||||
370
Server/application/ai/controller/CozeAI.php
Normal file
370
Server/application/ai/controller/CozeAI.php
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\ai\controller;
|
||||||
|
|
||||||
|
use think\facade\Env;
|
||||||
|
use think\Controller;
|
||||||
|
|
||||||
|
class CozeAI extends Controller
|
||||||
|
{
|
||||||
|
protected $apiUrl;
|
||||||
|
protected $accessToken;
|
||||||
|
protected $headers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
// 从环境变量获取配置
|
||||||
|
$this->apiUrl = Env::get('cozeAi.api_url');
|
||||||
|
$this->accessToken = Env::get('cozeAi.token');
|
||||||
|
|
||||||
|
if (empty($this->accessToken) || empty($this->apiUrl)) {
|
||||||
|
return json_encode(['code' => 500, 'msg' => '参数缺失']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置请求头
|
||||||
|
$this->headers = [
|
||||||
|
'Authorization: Bearer ' . $this->accessToken,
|
||||||
|
'Content-Type: application/json'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建智能体
|
||||||
|
* @param $data
|
||||||
|
* @return false|string|\think\response\Json
|
||||||
|
*/
|
||||||
|
public function createBot($data = [])
|
||||||
|
{
|
||||||
|
$space_id = Env::get('cozeAi.space_id');
|
||||||
|
$name = !empty($data['name']) ? $data['name'] : '';
|
||||||
|
$model_id = !empty($data['model_id']) ? $data['model_id'] : '';
|
||||||
|
$prompt_info = !empty($data['prompt_info']) ? $data['prompt_info'] : '';
|
||||||
|
$plugin_id_list = [
|
||||||
|
'id_list' => [
|
||||||
|
['api_id' => '7362852017859035163', 'plugin_id' => '7362852017859018779'],
|
||||||
|
['api_id' => '7472045461050851367', 'plugin_id' => '7472045461050834983'],
|
||||||
|
]
|
||||||
|
];
|
||||||
|
if (empty($name)) {
|
||||||
|
return json_encode(['code' => 500, 'msg' => '参数缺失']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$model_info_config = [
|
||||||
|
'model_id' => (string)$model_id,
|
||||||
|
];
|
||||||
|
|
||||||
|
$params = [
|
||||||
|
'space_id' => $space_id,
|
||||||
|
'name' => $name,
|
||||||
|
'model_info_config' => (object)$model_info_config,
|
||||||
|
'plugin_id_list' => (object)$plugin_id_list
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!empty($prompt_info)){
|
||||||
|
$new_prompt_info = [
|
||||||
|
'prompt' => $prompt_info
|
||||||
|
];
|
||||||
|
$params['prompt_info'] = (object) $new_prompt_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = requestCurl($this->apiUrl . '/v1/bot/create', $params, 'POST', $this->headers, 'json');
|
||||||
|
$result = json_decode($result, true);
|
||||||
|
if ($result['code'] != 0) {
|
||||||
|
return errorJson($result['msg'], $result['code']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return json_encode(['code' => 200, 'msg' => '创建成功', 'data' => $result['data']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建智能体
|
||||||
|
* @param $data
|
||||||
|
* @return false|string|\think\response\Json
|
||||||
|
*/
|
||||||
|
public function updateBot($data = [])
|
||||||
|
{
|
||||||
|
$space_id = Env::get('cozeAi.space_id');
|
||||||
|
$bot_id = !empty($data['bot_id']) ? $data['bot_id'] : '';
|
||||||
|
$name = !empty($data['name']) ? $data['name'] : '';
|
||||||
|
$model_id = !empty($data['model_id']) ? $data['model_id'] : '';
|
||||||
|
$prompt_info = !empty($data['prompt_info']) ? $data['prompt_info'] : '';
|
||||||
|
$dataset_ids = !empty($data['dataset_ids']) ? $data['dataset_ids'] : '';
|
||||||
|
$plugin_id_list = [
|
||||||
|
'id_list' => [
|
||||||
|
['api_id' => '7362852017859035163', 'plugin_id' => '7362852017859018779'],
|
||||||
|
['api_id' => '7472045461050851367', 'plugin_id' => '7472045461050834983'],
|
||||||
|
]
|
||||||
|
];
|
||||||
|
if (empty($name) || empty($bot_id)) {
|
||||||
|
return json_encode(['code' => 500, 'msg' => '参数缺失']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$model_info_config = [
|
||||||
|
'model_id' => (string)$model_id,
|
||||||
|
];
|
||||||
|
|
||||||
|
$params = [
|
||||||
|
'bot_id' => $bot_id,
|
||||||
|
'space_id' => $space_id,
|
||||||
|
'name' => $name,
|
||||||
|
'model_info_config' => (object)$model_info_config,
|
||||||
|
'plugin_id_list' => (object)$plugin_id_list
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
if (!empty($prompt_info)){
|
||||||
|
$new_prompt_info = [
|
||||||
|
'prompt' => $prompt_info
|
||||||
|
];
|
||||||
|
$params['prompt_info'] = (object) $new_prompt_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($dataset_ids)){
|
||||||
|
$knowledge = [
|
||||||
|
'dataset_ids' => $dataset_ids
|
||||||
|
];
|
||||||
|
$params['knowledge'] = (object) $knowledge;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = requestCurl($this->apiUrl . '/v1/bot/update', $params, 'POST', $this->headers, 'json');
|
||||||
|
$result = json_decode($result, true);
|
||||||
|
if ($result['code'] != 0) {
|
||||||
|
return errorJson($result['msg'], $result['code']);
|
||||||
|
}
|
||||||
|
return json_encode(['code' => 200, 'msg' => '获取成功']);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布智能体
|
||||||
|
* @param $data
|
||||||
|
* @return false|string|\think\response\Json
|
||||||
|
*/
|
||||||
|
public function botPublish($data = [])
|
||||||
|
{
|
||||||
|
$bot_id = !empty($data['bot_id']) ? $data['bot_id'] : '';
|
||||||
|
$connector_ids = ['1024', '999'];
|
||||||
|
if (empty($bot_id) || empty($connector_ids)) {
|
||||||
|
return json_encode(['code' => 500, 'msg' => '参数缺失']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = [
|
||||||
|
'bot_id' => $bot_id,
|
||||||
|
'connector_ids' => $connector_ids,
|
||||||
|
];
|
||||||
|
$result = requestCurl($this->apiUrl . '/v1/bot/publish', $params, 'POST', $this->headers, 'json');
|
||||||
|
$result = json_decode($result, true);
|
||||||
|
if ($result['code'] != 0) {
|
||||||
|
return errorJson($result['msg'], $result['code']);
|
||||||
|
}
|
||||||
|
return json_encode(['code' => 200, 'msg' => '发布成功']);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建知识库
|
||||||
|
* @param $data
|
||||||
|
* @return false|string|\think\response\Json
|
||||||
|
*/
|
||||||
|
public function createKnowledge($data = [])
|
||||||
|
{
|
||||||
|
|
||||||
|
$space_id = Env::get('cozeAi.space_id');
|
||||||
|
$name = !empty($data['name']) ? $data['name'] : '';
|
||||||
|
if (empty($space_id) || empty($name)) {
|
||||||
|
return json_encode(['code' => 500, 'msg' => '参数缺失']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = [
|
||||||
|
'space_id' => $space_id,
|
||||||
|
'format_type' => 0,
|
||||||
|
'name' => $name,
|
||||||
|
];
|
||||||
|
$result = requestCurl($this->apiUrl . '/v1/datasets', $params, 'POST', $this->headers, 'json');
|
||||||
|
$result = json_decode($result, true);
|
||||||
|
|
||||||
|
if ($result['code'] != 0) {
|
||||||
|
return errorJson($result['msg'], $result['code']);
|
||||||
|
}
|
||||||
|
return json_encode(['code' => 200, 'msg' => '创建成功','data' => $result['data']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function createDocument($data = [])
|
||||||
|
{
|
||||||
|
// 文件路径
|
||||||
|
$filePath = !empty($data['filePath']) ? $data['filePath'] : '';
|
||||||
|
$fileName = !empty($data['fileName']) ? $data['fileName'] : '';
|
||||||
|
if (empty($filePath)) {
|
||||||
|
return json_encode(['code' => 500, 'msg' => '参数缺失']);
|
||||||
|
}
|
||||||
|
// 读取文件内容
|
||||||
|
$fileContent = file_get_contents($filePath);
|
||||||
|
// 将文件内容编码为Base64
|
||||||
|
$base64EncodedContent = base64_encode($fileContent);
|
||||||
|
|
||||||
|
|
||||||
|
$dataset_id = !empty($data['dataset_id']) ? $data['dataset_id'] : '';
|
||||||
|
|
||||||
|
$document_bases = [
|
||||||
|
['name' => $fileName,'source_info' => ['file_base64' => $base64EncodedContent]]
|
||||||
|
];
|
||||||
|
|
||||||
|
$chunk_strategy = [
|
||||||
|
'chunk_type' => 0,
|
||||||
|
'remove_extra_spaces' => true
|
||||||
|
];
|
||||||
|
$params = [
|
||||||
|
'dataset_id' => (string) $dataset_id,
|
||||||
|
'document_bases' => $document_bases,
|
||||||
|
'chunk_strategy' => (object) $chunk_strategy,
|
||||||
|
'format_type' => 0
|
||||||
|
];
|
||||||
|
$headers = array_merge($this->headers, ['Agw-Js-Conv: str']);
|
||||||
|
$result = requestCurl($this->apiUrl . '/open_api/knowledge/document/create', $params, 'POST', $headers, 'json');
|
||||||
|
$result = json_decode($result, true);
|
||||||
|
if ($result['code'] != 0) {
|
||||||
|
return errorJson($result['msg'], $result['code']);
|
||||||
|
}
|
||||||
|
return json_encode(['code' => 200, 'msg' => '创建成功','data' => $result['document_infos']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除知识库文件
|
||||||
|
* @param $data
|
||||||
|
* @return false|string|\think\response\Json
|
||||||
|
*/
|
||||||
|
public function deleteDocument($data = [])
|
||||||
|
{
|
||||||
|
if (empty($data)) {
|
||||||
|
return json_encode(['code' => 500, 'msg' => '参数缺失']);
|
||||||
|
}
|
||||||
|
$params = [
|
||||||
|
'document_ids' => $data,
|
||||||
|
];
|
||||||
|
$headers = array_merge($this->headers, ['Agw-Js-Conv: str']);
|
||||||
|
$result = requestCurl($this->apiUrl . '/open_api/knowledge/document/delete', $params, 'POST', $headers, 'json');
|
||||||
|
$result = json_decode($result, true);
|
||||||
|
if ($result['code'] != 0) {
|
||||||
|
return errorJson($result['msg'], $result['code']);
|
||||||
|
}
|
||||||
|
return json_encode(['code' => 200, 'msg' => '删除成功']);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建会话
|
||||||
|
* @param $data
|
||||||
|
* @return false|string|\think\response\Json
|
||||||
|
*/
|
||||||
|
public function createConversation($data = [])
|
||||||
|
{
|
||||||
|
$bot_id = !empty($data['bot_id']) ? $data['bot_id'] : '';
|
||||||
|
$name = !empty($data['name']) ? $data['name'] : '';
|
||||||
|
$meta_data = !empty($data['meta_data']) ? $data['meta_data'] : [];
|
||||||
|
|
||||||
|
if (empty($bot_id) || empty($name)) {
|
||||||
|
return json_encode(['code' => 500, 'msg' => '参数缺失']);
|
||||||
|
}
|
||||||
|
$params = [
|
||||||
|
'bot_id' => $bot_id,
|
||||||
|
'name' => $name,
|
||||||
|
];
|
||||||
|
if (!empty($meta_data)){
|
||||||
|
$params['meta_data'] = $meta_data;
|
||||||
|
}
|
||||||
|
$result = requestCurl($this->apiUrl . '/v1/conversation/create', $params, 'POST', $this->headers, 'json');
|
||||||
|
$result = json_decode($result, true);
|
||||||
|
if ($result['code'] != 0) {
|
||||||
|
return errorJson($result['msg'], $result['code']);
|
||||||
|
}
|
||||||
|
return json_encode(['code' => 200, 'msg' => '创建成功','data' => $result['data']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始对话
|
||||||
|
* @param $data
|
||||||
|
* @return false|string|\think\response\Json
|
||||||
|
*/
|
||||||
|
public function createChat($data = [])
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$bot_id = !empty($data['bot_id']) ? $data['bot_id'] : '';
|
||||||
|
$uid = !empty($data['uid']) ? $data['uid'] : '';
|
||||||
|
$conversation_id = !empty($data['conversation_id']) ? $data['conversation_id'] : '';
|
||||||
|
$question = !empty($data['question']) ? $data['question'] : [];
|
||||||
|
|
||||||
|
|
||||||
|
if(empty($bot_id)){
|
||||||
|
return errorJson('智能体ID不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(empty($conversation_id)){
|
||||||
|
return errorJson('会话ID不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(empty($question)){
|
||||||
|
return errorJson('问题不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建请求数据
|
||||||
|
$params = [
|
||||||
|
'bot_id' => strval($bot_id),
|
||||||
|
'user_id' => strval($uid),
|
||||||
|
'additional_messages' => $question,
|
||||||
|
'stream' => false,
|
||||||
|
'auto_save_history' => true
|
||||||
|
];
|
||||||
|
|
||||||
|
$url = $this->apiUrl . '/v3/chat?conversation_id='.$conversation_id;
|
||||||
|
$result = requestCurl($url, $params, 'POST', $this->headers, 'json');
|
||||||
|
$result = json_decode($result, true);
|
||||||
|
if ($result['code'] != 0) {
|
||||||
|
return errorJson($result['msg'], $result['code']);
|
||||||
|
}
|
||||||
|
return json_encode(['code' => 200, 'msg' => '发送成功','data' => $result['data']]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return errorJson('创建对话失败:' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function getConversationChat($data = [])
|
||||||
|
{
|
||||||
|
$conversation_id = !empty($data['conversation_id']) ? $data['conversation_id'] : '';
|
||||||
|
$chat_id = !empty($data['chat_id']) ? $data['chat_id'] : '';
|
||||||
|
$url = $this->apiUrl . '/v3/chat/retrieve?conversation_id='.$conversation_id.'&chat_id='.$chat_id;
|
||||||
|
$result = requestCurl($url, [], 'GET', $this->headers, 'json');
|
||||||
|
$result = json_decode($result, true);
|
||||||
|
if ($result['code'] != 0) {
|
||||||
|
return errorJson($result['msg'], $result['code']);
|
||||||
|
}
|
||||||
|
return json_encode(['code' => 200, 'msg' => '发送成功','data' => $result['data']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function listConversationMessage($data = [])
|
||||||
|
{
|
||||||
|
$conversation_id = !empty($data['conversation_id']) ? $data['conversation_id'] : '';
|
||||||
|
$chat_id = !empty($data['chat_id']) ? $data['chat_id'] : '';
|
||||||
|
$url = $this->apiUrl . '/v3/chat/message/list?conversation_id='.$conversation_id.'&chat_id='.$chat_id;
|
||||||
|
$result = requestCurl($url, [], 'GET', $this->headers, 'json');
|
||||||
|
$result = json_decode($result, true);
|
||||||
|
if ($result['code'] != 0) {
|
||||||
|
return errorJson($result['msg'], $result['code']);
|
||||||
|
}
|
||||||
|
return json_encode(['code' => 200, 'msg' => '发送成功','data' => $result['data']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace app\ai\controller;
|
namespace app\ai\controller;
|
||||||
|
|
||||||
use think\facade\Env;
|
use think\facade\Env;
|
||||||
class OpenAi
|
class OpenAI
|
||||||
{
|
{
|
||||||
protected $apiUrl;
|
protected $apiUrl;
|
||||||
protected $apiKey;
|
protected $apiKey;
|
||||||
|
|||||||
@@ -349,7 +349,7 @@ class MessageController extends BaseController
|
|||||||
* 保存消息记录到数据库
|
* 保存消息记录到数据库
|
||||||
* @param array $item 消息记录数据
|
* @param array $item 消息记录数据
|
||||||
*/
|
*/
|
||||||
private function saveMessage($item)
|
public function saveMessage($item)
|
||||||
{
|
{
|
||||||
// 检查消息是否已存在
|
// 检查消息是否已存在
|
||||||
$exists = WechatMessageModel::where('id', $item['id']) ->find();
|
$exists = WechatMessageModel::where('id', $item['id']) ->find();
|
||||||
@@ -449,7 +449,7 @@ class MessageController extends BaseController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -457,7 +457,7 @@ class MessageController extends BaseController
|
|||||||
* @param array $item 消息记录数据
|
* @param array $item 消息记录数据
|
||||||
* @return bool 是否保存成功
|
* @return bool 是否保存成功
|
||||||
*/
|
*/
|
||||||
private function saveChatroomMessage($item)
|
public function saveChatroomMessage($item)
|
||||||
{
|
{
|
||||||
// 检查消息是否已存在
|
// 检查消息是否已存在
|
||||||
$exists = WechatMessageModel::where('id', $item['id'])->find();
|
$exists = WechatMessageModel::where('id', $item['id'])->find();
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ class WechatChatroomController extends BaseController
|
|||||||
'deleteTime' => !empty($item['isDeleted']) ? strtotime($item['deleteTime']) : 0,
|
'deleteTime' => !empty($item['isDeleted']) ? strtotime($item['deleteTime']) : 0,
|
||||||
'createTime' => isset($item['createTime']) ? strtotime($item['createTime']) : 0,
|
'createTime' => isset($item['createTime']) ? strtotime($item['createTime']) : 0,
|
||||||
'accountId' => isset($item['accountId']) ? $item['accountId'] : 0,
|
'accountId' => isset($item['accountId']) ? $item['accountId'] : 0,
|
||||||
'accountUserName' => isset($item['accountUserName']) ? $item['accountUserName'] : '',
|
'accountUserName' => isset($item['accountUserName']) ? $item['accountUserName'] : '',
|
||||||
'accountRealName' => isset($item['accountRealName']) ? $item['accountRealName'] : '',
|
'accountRealName' => isset($item['accountRealName']) ? $item['accountRealName'] : '',
|
||||||
'accountNickname' => isset($item['accountNickname']) ? $item['accountNickname'] : '',
|
'accountNickname' => isset($item['accountNickname']) ? $item['accountNickname'] : '',
|
||||||
'groupId' => isset($item['groupId']) ? $item['groupId'] : 0,
|
'groupId' => isset($item['groupId']) ? $item['groupId'] : 0,
|
||||||
@@ -218,7 +218,7 @@ class WechatChatroomController extends BaseController
|
|||||||
])->find();
|
])->find();
|
||||||
|
|
||||||
if ($member) {
|
if ($member) {
|
||||||
$member->save($data);
|
$member->savea($data);
|
||||||
} else {
|
} else {
|
||||||
$data['createTime'] = time();
|
$data['createTime'] = time();
|
||||||
WechatChatroomMemberModel::create($data);
|
WechatChatroomMemberModel::create($data);
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class WechatFriendController extends BaseController
|
|||||||
// 发送请求获取好友列表
|
// 发送请求获取好友列表
|
||||||
$result = requestCurl($this->baseUrl . 'api/WechatFriend/friendlistData', $params, 'POST', $header, 'json');
|
$result = requestCurl($this->baseUrl . 'api/WechatFriend/friendlistData', $params, 'POST', $header, 'json');
|
||||||
$response = handleApiResponse($result);
|
$response = handleApiResponse($result);
|
||||||
|
|
||||||
// 保存数据到数据库
|
// 保存数据到数据库
|
||||||
if (is_array($response)) {
|
if (is_array($response)) {
|
||||||
$isUpdate = false;
|
$isUpdate = false;
|
||||||
|
|||||||
@@ -8,4 +8,9 @@ class WechatFriendModel extends Model
|
|||||||
{
|
{
|
||||||
// 设置表名
|
// 设置表名
|
||||||
protected $table = 's2_wechat_friend';
|
protected $table = 's2_wechat_friend';
|
||||||
|
protected $pk = 'id';
|
||||||
|
|
||||||
|
/*protected $pk = [
|
||||||
|
'uk_owner_wechat_account' => ['ownerWechatId', 'wechatId','wechatAccountId'] // uk_owner_wechat_account 是数据库中组合唯一键的名称
|
||||||
|
];*/
|
||||||
}
|
}
|
||||||
@@ -166,6 +166,7 @@ Route::group('v1/', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
Route::post('dataProcessing', 'app\chukebao\controller\DataProcessing@index'); // 修改数据
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,37 +2,448 @@
|
|||||||
|
|
||||||
namespace app\chukebao\controller;
|
namespace app\chukebao\controller;
|
||||||
|
|
||||||
|
use app\ai\controller\CozeAI;
|
||||||
use app\ai\controller\DouBaoAI;
|
use app\ai\controller\DouBaoAI;
|
||||||
|
use app\api\model\WechatFriendModel;
|
||||||
use app\chukebao\controller\TokensRecordController as tokensRecord;
|
use app\chukebao\controller\TokensRecordController as tokensRecord;
|
||||||
|
use app\chukebao\model\AiSettings;
|
||||||
|
use app\chukebao\model\FriendSettings;
|
||||||
use app\chukebao\model\TokensCompany;
|
use app\chukebao\model\TokensCompany;
|
||||||
use library\ResponseHelper;
|
use library\ResponseHelper;
|
||||||
use think\Db;
|
use think\Db;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI聊天控制器
|
||||||
|
* 负责处理与好友的AI对话功能
|
||||||
|
*/
|
||||||
class AiChatController extends BaseController
|
class AiChatController extends BaseController
|
||||||
{
|
{
|
||||||
public function index(){
|
// 对话状态常量
|
||||||
|
const STATUS_CREATED = 'created'; // 对话已创建
|
||||||
|
const STATUS_IN_PROGRESS = 'in_progress'; // 智能体正在处理中
|
||||||
|
const STATUS_COMPLETED = 'completed'; // 智能体已完成处理
|
||||||
|
const STATUS_FAILED = 'failed'; // 对话失败
|
||||||
|
const STATUS_REQUIRES_ACTION = 'requires_action'; // 对话中断,需要进一步处理
|
||||||
|
const STATUS_CANCELED = 'canceled'; // 对话已取消
|
||||||
|
|
||||||
|
// 轮询配置
|
||||||
|
const MAX_RETRY_TIMES = 30; // 最大重试次数
|
||||||
|
const RETRY_INTERVAL = 2; // 重试间隔(秒)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI聊天主入口
|
||||||
|
*
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// 1. 参数验证和初始化
|
||||||
|
$params = $this->validateAndInitParams();
|
||||||
|
if ($params === false) {
|
||||||
|
return ResponseHelper::error('参数验证失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 验证Tokens余额
|
||||||
|
if (!$this->checkTokensBalance($params['companyId'])) {
|
||||||
|
return ResponseHelper::error('Tokens余额不足,请充值后再试');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 获取AI配置
|
||||||
|
$setting = $this->getAiSettings($params['companyId']);
|
||||||
|
if (!$setting) {
|
||||||
|
return ResponseHelper::error('未找到AI配置信息,请先配置AI策略');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 获取好友AI设置
|
||||||
|
$friendSettings = $this->getFriendSettings($params['companyId'], $params['friendId']);
|
||||||
|
if (!$friendSettings) {
|
||||||
|
return ResponseHelper::error('该好友未配置或未开启AI功能');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 确保会话存在
|
||||||
|
$conversationId = $this->ensureConversation($friendSettings, $setting, $params);
|
||||||
|
if (!$conversationId) {
|
||||||
|
return ResponseHelper::error('创建会话失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 获取历史消息
|
||||||
|
$msgData = $this->getHistoryMessages($params['friendId'], $friendSettings);
|
||||||
|
|
||||||
|
// 7. 创建AI对话
|
||||||
|
$chatId = $this->createAiChat($setting, $friendSettings, $msgData);
|
||||||
|
if (!$chatId) {
|
||||||
|
return ResponseHelper::error('创建对话失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. 等待AI处理完成(轮询)
|
||||||
|
$chatResult = $this->waitForChatCompletion($conversationId, $chatId);
|
||||||
|
if (!$chatResult) {
|
||||||
|
return ResponseHelper::error('AI处理超时或失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. 扣除Tokens
|
||||||
|
$this->consumeTokens($chatResult, $params, $friendSettings);
|
||||||
|
|
||||||
|
// 10. 获取对话消息
|
||||||
|
$messages = $this->getChatMessages($conversationId, $chatId);
|
||||||
|
if (!$messages) {
|
||||||
|
return ResponseHelper::error('获取对话消息失败');
|
||||||
|
}
|
||||||
|
return ResponseHelper::success($messages[1]['content'], '对话成功');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
\think\facade\Log::error('AI聊天异常:' . $e->getMessage());
|
||||||
|
return ResponseHelper::error('系统异常:' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证和初始化参数
|
||||||
|
*
|
||||||
|
* @return array|false
|
||||||
|
*/
|
||||||
|
private function validateAndInitParams()
|
||||||
|
{
|
||||||
$userId = $this->getUserInfo('id');
|
$userId = $this->getUserInfo('id');
|
||||||
$companyId = $this->getUserInfo('companyId');
|
$companyId = $this->getUserInfo('companyId');
|
||||||
$friendId = $this->request->param('friendId', '');
|
$friendId = $this->request->param('friendId', '');
|
||||||
$wechatAccountId = $this->request->param('wechatAccountId', '');
|
$wechatAccountId = $this->request->param('wechatAccountId', '');
|
||||||
$content = $this->request->param('content', '');
|
|
||||||
|
|
||||||
if (empty($wechatAccountId) || empty($friendId)){
|
if (empty($wechatAccountId) || empty($friendId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'userId' => $userId,
|
||||||
|
'companyId' => $companyId,
|
||||||
|
'friendId' => $friendId,
|
||||||
|
'wechatAccountId' => $wechatAccountId
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查Tokens余额
|
||||||
|
*
|
||||||
|
* @param int $companyId 公司ID
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function checkTokensBalance($companyId)
|
||||||
|
{
|
||||||
|
$tokens = TokensCompany::where(['companyId' => $companyId])->value('tokens');
|
||||||
|
return !empty($tokens) && $tokens > 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取AI配置
|
||||||
|
*
|
||||||
|
* @param int $companyId 公司ID
|
||||||
|
* @return AiSettings|null
|
||||||
|
*/
|
||||||
|
private function getAiSettings($companyId)
|
||||||
|
{
|
||||||
|
return AiSettings::where(['companyId' => $companyId])->find();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取好友AI设置
|
||||||
|
*
|
||||||
|
* @param int $companyId 公司ID
|
||||||
|
* @param string $friendId 好友ID
|
||||||
|
* @return FriendSettings|null
|
||||||
|
*/
|
||||||
|
private function getFriendSettings($companyId, $friendId)
|
||||||
|
{
|
||||||
|
$friendSettings = FriendSettings::where([
|
||||||
|
'companyId' => $companyId,
|
||||||
|
'friendId' => $friendId
|
||||||
|
])->find();
|
||||||
|
|
||||||
|
if (empty($friendSettings) || $friendSettings->type == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $friendSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保会话存在
|
||||||
|
*
|
||||||
|
* @param FriendSettings $friendSettings 好友设置
|
||||||
|
* @param AiSettings $setting AI设置
|
||||||
|
* @param array $params 参数
|
||||||
|
* @return string|null 会话ID
|
||||||
|
*/
|
||||||
|
private function ensureConversation($friendSettings, $setting, $params)
|
||||||
|
{
|
||||||
|
if (!empty($friendSettings->conversationId)) {
|
||||||
|
return $friendSettings->conversationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新会话
|
||||||
|
$cozeAI = new CozeAI();
|
||||||
|
$data = [
|
||||||
|
'bot_id' => $setting->botId,
|
||||||
|
'name' => '与好友' . $params['friendId'] . '的对话',
|
||||||
|
'meta_data' => [
|
||||||
|
'friendId' => (string)$friendSettings->friendId,
|
||||||
|
'wechatAccountId' => (string)$params['wechatAccountId'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$res = $cozeAI->createConversation($data);
|
||||||
|
$res = json_decode($res, true);
|
||||||
|
|
||||||
|
if ($res['code'] != 200) {
|
||||||
|
\think\facade\Log::error('创建会话失败:' . ($res['msg'] ?? '未知错误'));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存会话ID
|
||||||
|
$conversationId = $res['data']['id'];
|
||||||
|
$friendSettings->conversationId = $conversationId;
|
||||||
|
$friendSettings->conversationTime = time();
|
||||||
|
$friendSettings->save();
|
||||||
|
|
||||||
|
return $conversationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取历史消息
|
||||||
|
*
|
||||||
|
* @param string $friendId 好友ID
|
||||||
|
* @param FriendSettings $friendSettings 好友设置
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function getHistoryMessages($friendId, $friendSettings)
|
||||||
|
{
|
||||||
|
$msgData = [];
|
||||||
|
|
||||||
|
// 会话创建时间小于1分钟,加载最近10条消息
|
||||||
|
if ($friendSettings->conversationTime >= time() - 60) {
|
||||||
|
$messages = Db::table('s2_wechat_message')
|
||||||
|
->where('wechatFriendId', $friendId)
|
||||||
|
->where('msgType', '<', 50)
|
||||||
|
->order('wechatTime desc')
|
||||||
|
->field('id,content,msgType,isSend,wechatTime')
|
||||||
|
->limit(10)
|
||||||
|
->select();
|
||||||
|
|
||||||
|
// 按时间正序排列
|
||||||
|
usort($messages, function ($a, $b) {
|
||||||
|
return $a['wechatTime'] <=> $b['wechatTime'];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理聊天数据
|
||||||
|
foreach ($messages as $val) {
|
||||||
|
if (empty($val['content'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$msg = [
|
||||||
|
'role' => empty($val['isSend']) ? 'user' : 'assistant',
|
||||||
|
'content' => $val['content'],
|
||||||
|
'type' => empty($val['isSend']) ? 'question' : 'answer',
|
||||||
|
'content_type' => 'text'
|
||||||
|
];
|
||||||
|
$msgData[] = $msg;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 只加载最新一条用户消息
|
||||||
|
$message = Db::table('s2_wechat_message')
|
||||||
|
->where('wechatFriendId', $friendId)
|
||||||
|
->where('msgType', '<', 50)
|
||||||
|
->where('isSend', 0)
|
||||||
|
->order('wechatTime desc')
|
||||||
|
->field('id,content,msgType,isSend,wechatTime')
|
||||||
|
->find();
|
||||||
|
|
||||||
|
if (!empty($message) && !empty($message['content'])) {
|
||||||
|
$msgData[] = [
|
||||||
|
'role' => 'user',
|
||||||
|
'content' => $message['content'],
|
||||||
|
'type' => 'question',
|
||||||
|
'content_type' => 'text'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $msgData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建AI对话
|
||||||
|
*
|
||||||
|
* @param AiSettings $setting AI设置
|
||||||
|
* @param FriendSettings $friendSettings 好友设置
|
||||||
|
* @param array $msgData 消息数据
|
||||||
|
* @return string|null 对话ID
|
||||||
|
*/
|
||||||
|
private function createAiChat($setting, $friendSettings, $msgData)
|
||||||
|
{
|
||||||
|
$cozeAI = new CozeAI();
|
||||||
|
$data = [
|
||||||
|
'bot_id' => $setting->botId,
|
||||||
|
'uid' => $friendSettings->friendId,
|
||||||
|
'conversation_id' => $friendSettings->conversationId,
|
||||||
|
'question' => $msgData,
|
||||||
|
];
|
||||||
|
|
||||||
|
$res = $cozeAI->createChat($data);
|
||||||
|
$res = json_decode($res, true);
|
||||||
|
|
||||||
|
if ($res['code'] != 200) {
|
||||||
|
\think\facade\Log::error('创建对话失败:' . ($res['msg'] ?? '未知错误'));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $res['data']['id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等待AI处理完成(轮询机制)
|
||||||
|
*
|
||||||
|
* @param string $conversationId 会话ID
|
||||||
|
* @param string $chatId 对话ID
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
private function waitForChatCompletion($conversationId, $chatId)
|
||||||
|
{
|
||||||
|
$cozeAI = new CozeAI();
|
||||||
|
$retryCount = 0;
|
||||||
|
|
||||||
|
while ($retryCount < self::MAX_RETRY_TIMES) {
|
||||||
|
// 获取对话状态
|
||||||
|
$res = $cozeAI->getConversationChat([
|
||||||
|
'conversation_id' => $conversationId,
|
||||||
|
'chat_id' => $chatId,
|
||||||
|
]);
|
||||||
|
$res = json_decode($res, true);
|
||||||
|
|
||||||
|
if ($res['code'] != 200) {
|
||||||
|
\think\facade\Log::error('获取对话状态失败:' . ($res['msg'] ?? '未知错误'));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = $res['data']['status'] ?? '';
|
||||||
|
|
||||||
|
// 处理不同的状态
|
||||||
|
switch ($status) {
|
||||||
|
case self::STATUS_COMPLETED:
|
||||||
|
// 对话完成,返回结果
|
||||||
|
return $res['data'];
|
||||||
|
|
||||||
|
case self::STATUS_IN_PROGRESS:
|
||||||
|
case self::STATUS_CREATED:
|
||||||
|
// 继续等待
|
||||||
|
$retryCount++;
|
||||||
|
sleep(self::RETRY_INTERVAL);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case self::STATUS_FAILED:
|
||||||
|
\think\facade\Log::error('对话失败,chat_id: ' . $chatId);
|
||||||
|
return null;
|
||||||
|
|
||||||
|
case self::STATUS_CANCELED:
|
||||||
|
\think\facade\Log::error('对话已取消,chat_id: ' . $chatId);
|
||||||
|
return null;
|
||||||
|
|
||||||
|
case self::STATUS_REQUIRES_ACTION:
|
||||||
|
\think\facade\Log::warning('对话需要进一步处理,chat_id: ' . $chatId);
|
||||||
|
return null;
|
||||||
|
|
||||||
|
default:
|
||||||
|
\think\facade\Log::error('未知状态:' . $status);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 超时
|
||||||
|
\think\facade\Log::error('对话处理超时,chat_id: ' . $chatId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扣除Tokens
|
||||||
|
*
|
||||||
|
* @param array $chatResult 对话结果
|
||||||
|
* @param array $params 参数
|
||||||
|
* @param FriendSettings $friendSettings 好友设置
|
||||||
|
*/
|
||||||
|
private function consumeTokens($chatResult, $params, $friendSettings)
|
||||||
|
{
|
||||||
|
$tokenCount = $chatResult['usage']['token_count'] ?? 0;
|
||||||
|
|
||||||
|
if (empty($tokenCount)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取好友昵称
|
||||||
|
$nickname = WechatFriendModel::where('id', $friendSettings->friendId)->value('nickname');
|
||||||
|
$remarks = !empty($nickname) ? '与好友【' . $nickname . '】聊天' : '与好友聊天';
|
||||||
|
|
||||||
|
// 扣除Tokens
|
||||||
|
$tokensRecord = new tokensRecord();
|
||||||
|
$data = [
|
||||||
|
'tokens' => $tokenCount * 20,
|
||||||
|
'type' => 0,
|
||||||
|
'form' => 1,
|
||||||
|
'wechatAccountId' => $params['wechatAccountId'],
|
||||||
|
'friendIdOrGroupId' => $params['friendId'],
|
||||||
|
'remarks' => $remarks,
|
||||||
|
];
|
||||||
|
|
||||||
|
$tokensRecord->consumeTokens($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取对话消息
|
||||||
|
*
|
||||||
|
* @param string $conversationId 会话ID
|
||||||
|
* @param string $chatId 对话ID
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
private function getChatMessages($conversationId, $chatId)
|
||||||
|
{
|
||||||
|
$cozeAI = new CozeAI();
|
||||||
|
$res = $cozeAI->listConversationMessage([
|
||||||
|
'conversation_id' => $conversationId,
|
||||||
|
'chat_id' => $chatId,
|
||||||
|
]);
|
||||||
|
$res = json_decode($res, true);
|
||||||
|
|
||||||
|
if ($res['code'] != 200) {
|
||||||
|
\think\facade\Log::error('获取对话消息失败:' . ($res['msg'] ?? '未知错误'));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $res['data'] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function index2222()
|
||||||
|
{
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
$friendId = $this->request->param('friendId', '');
|
||||||
|
$wechatAccountId = $this->request->param('wechatAccountId', '');
|
||||||
|
$content = $this->request->param('content', '');
|
||||||
|
|
||||||
|
if (empty($wechatAccountId) || empty($friendId)) {
|
||||||
return ResponseHelper::error('参数缺失');
|
return ResponseHelper::error('参数缺失');
|
||||||
}
|
}
|
||||||
|
|
||||||
$tokens = TokensCompany::where(['companyId' => $companyId])->value('tokens');
|
$tokens = TokensCompany::where(['companyId' => $companyId])->value('tokens');
|
||||||
if (empty($tokens) || $tokens <= 0){
|
if (empty($tokens) || $tokens <= 0) {
|
||||||
return ResponseHelper::error('用户Tokens余额不足');
|
return ResponseHelper::error('用户Tokens余额不足');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//读取AI配置
|
//读取AI配置
|
||||||
$setting = Db::name('ai_settings')->where(['companyId' => $companyId,'userId' => $userId])->find();
|
$setting = Db::name('ai_settings')->where(['companyId' => $companyId, 'userId' => $userId])->find();
|
||||||
if(empty($setting)){
|
if (empty($setting)) {
|
||||||
return ResponseHelper::error('未找到配置信息,请先配置AI策略');
|
return ResponseHelper::error('未找到配置信息,请先配置AI策略');
|
||||||
}
|
}
|
||||||
$config = json_decode($setting['config'],true);
|
$config = json_decode($setting['config'], true);
|
||||||
$modelSetting = $config['modelSetting'];
|
$modelSetting = $config['modelSetting'];
|
||||||
$round = isset($config['round']) ? $config['round'] : 10;
|
$round = isset($config['round']) ? $config['round'] : 10;
|
||||||
|
|
||||||
@@ -45,20 +456,20 @@ class AiChatController extends BaseController
|
|||||||
->limit($round)
|
->limit($round)
|
||||||
->select();
|
->select();
|
||||||
|
|
||||||
usort($messages, function($a, $b) {
|
usort($messages, function ($a, $b) {
|
||||||
return $a['wechatTime'] <=> $b['wechatTime'];
|
return $a['wechatTime'] <=> $b['wechatTime'];
|
||||||
});
|
});
|
||||||
|
|
||||||
//处理聊天数据
|
//处理聊天数据
|
||||||
$msg = [];
|
$msg = [];
|
||||||
foreach ($messages as $val){
|
foreach ($messages as $val) {
|
||||||
if (empty($val['content'])){
|
if (empty($val['content'])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!empty($val['isSend'])){
|
if (!empty($val['isSend'])) {
|
||||||
$msg[] = '客服:' . $val['content'];
|
$msg[] = '客服:' . $val['content'];
|
||||||
}else{
|
} else {
|
||||||
$msg[] = '用户:' . $val['content'];
|
$msg[] = '用户:' . $val['content'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$content = implode("\n", $msg);
|
$content = implode("\n", $msg);
|
||||||
@@ -78,13 +489,13 @@ class AiChatController extends BaseController
|
|||||||
//AI处理
|
//AI处理
|
||||||
$ai = new DouBaoAI();
|
$ai = new DouBaoAI();
|
||||||
$res = $ai->text($params);
|
$res = $ai->text($params);
|
||||||
$res = json_decode($res,true);
|
$res = json_decode($res, true);
|
||||||
|
|
||||||
if ($res['code'] == 200) {
|
if ($res['code'] == 200) {
|
||||||
//扣除Tokens
|
//扣除Tokens
|
||||||
$tokensRecord = new tokensRecord();
|
$tokensRecord = new tokensRecord();
|
||||||
$nickname = Db::table('s2_wechat_friend')->where(['id' => $friendId])->value('nickname');
|
$nickname = Db::table('s2_wechat_friend')->where(['id' => $friendId])->value('nickname');
|
||||||
$remarks = !empty($nickname) ? '与好友【'.$nickname.'】聊天' : '与好友聊天';
|
$remarks = !empty($nickname) ? '与好友【' . $nickname . '】聊天' : '与好友聊天';
|
||||||
$data = [
|
$data = [
|
||||||
'tokens' => $res['data']['token'],
|
'tokens' => $res['data']['token'],
|
||||||
'type' => 0,
|
'type' => 0,
|
||||||
@@ -95,7 +506,7 @@ class AiChatController extends BaseController
|
|||||||
];
|
];
|
||||||
$tokensRecord->consumeTokens($data);
|
$tokensRecord->consumeTokens($data);
|
||||||
return ResponseHelper::success($res['data']['content']);
|
return ResponseHelper::success($res['data']['content']);
|
||||||
}else{
|
} else {
|
||||||
return ResponseHelper::error($res['msg']);
|
return ResponseHelper::error($res['msg']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
113
Server/application/chukebao/controller/DataProcessing.php
Normal file
113
Server/application/chukebao/controller/DataProcessing.php
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\chukebao\controller;
|
||||||
|
|
||||||
|
use library\ResponseHelper;
|
||||||
|
use app\api\model\WechatFriendModel;
|
||||||
|
use app\api\controller\MessageController;
|
||||||
|
|
||||||
|
|
||||||
|
class DataProcessing extends BaseController
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
$type = $this->request->param('type', '');
|
||||||
|
$wechatAccountId = $this->request->param('wechatAccountId', '');
|
||||||
|
//微信好友
|
||||||
|
$toAccountId = $this->request->param('toAccountId', '');
|
||||||
|
$wechatFriendId = $this->request->param('wechatFriendId', '');
|
||||||
|
$newRemark = $this->request->param('newRemark', '');
|
||||||
|
$labels = $this->request->param('labels', []);
|
||||||
|
//微信群
|
||||||
|
$wechatChatroomId = $this->request->param('wechatChatroomId', '');
|
||||||
|
|
||||||
|
//新消息
|
||||||
|
$friendMessage = $this->request->param('friendMessage', '');
|
||||||
|
$chatroomMessage = $this->request->param('chatroomMessage', '');
|
||||||
|
|
||||||
|
$typeData = [
|
||||||
|
'CmdModifyFriendRemark', //好友修改备注 {newRemark、wechatAccountId、wechatFriendId}
|
||||||
|
'CmdModifyFriendLabel', //好友修改标签 {labels、wechatAccountId、wechatFriendId}
|
||||||
|
'CmdAllotFriend', //转让好友 {labels、wechatAccountId、wechatFriendId}
|
||||||
|
'CmdChatroomOperate', //修改群信息 {chatroomName(群名)、announce(公告)、extra(公告)、wechatAccountId、wechatChatroomId}
|
||||||
|
'CmdNewMessage', //接收消息
|
||||||
|
];
|
||||||
|
|
||||||
|
if (empty($type) || empty($wechatAccountId)) {
|
||||||
|
return ResponseHelper::error('参数缺失');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($type, $typeData)) {
|
||||||
|
return ResponseHelper::error('类型错误');
|
||||||
|
}
|
||||||
|
$msg = '';
|
||||||
|
$codee = 200;
|
||||||
|
switch ($type) {
|
||||||
|
case 'CmdModifyFriendRemark': //修改好友备注
|
||||||
|
if(empty($wechatFriendId) || empty($newRemark)){
|
||||||
|
return ResponseHelper::error('参数缺失');
|
||||||
|
}
|
||||||
|
$friend = WechatFriendModel::where(['id' => $wechatFriendId,'wechatAccountId' => $wechatAccountId])->find();
|
||||||
|
if(empty($friend)){
|
||||||
|
return ResponseHelper::error('好友不存在');
|
||||||
|
}
|
||||||
|
$friend->conRemark = $newRemark;
|
||||||
|
$friend->updateTime = time();
|
||||||
|
$friend->save();
|
||||||
|
$msg = '修改备成功';
|
||||||
|
break;
|
||||||
|
case 'CmdModifyFriendLabel': //修改好友标签
|
||||||
|
if(empty($wechatFriendId)){
|
||||||
|
return ResponseHelper::error('参数缺失');
|
||||||
|
}
|
||||||
|
$friend = WechatFriendModel::where(['id' => $wechatFriendId,'wechatAccountId' => $wechatAccountId])->find();
|
||||||
|
if(empty($friend)){
|
||||||
|
return ResponseHelper::error('好友不存在');
|
||||||
|
}
|
||||||
|
$friend->labels = json_encode($labels,256);
|
||||||
|
$friend->updateTime = time();
|
||||||
|
$friend->save();
|
||||||
|
$msg = '修标签成功';
|
||||||
|
break;
|
||||||
|
case 'CmdAllotFriend': //迁移好友
|
||||||
|
if(empty($toAccountId)){
|
||||||
|
return ResponseHelper::error('参数缺失');
|
||||||
|
}
|
||||||
|
|
||||||
|
$friend = WechatFriendModel::where(['id' => $wechatFriendId,'wechatAccountId' => $wechatAccountId])->find();
|
||||||
|
if(empty($friend)){
|
||||||
|
return ResponseHelper::error('好友不存在');
|
||||||
|
}
|
||||||
|
$friend->accountId = $toAccountId;
|
||||||
|
$friend->updateTime = time();
|
||||||
|
$friend->save();
|
||||||
|
$msg = '好友转移成功';
|
||||||
|
break;
|
||||||
|
case 'CmdNewMessage':
|
||||||
|
if(empty($friendMessage) && empty($chatroomMessage)){
|
||||||
|
return ResponseHelper::error('参数缺失');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(is_array($friendMessage) && is_array($chatroomMessage)){
|
||||||
|
return ResponseHelper::error('数据类型错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$messageController = new MessageController();
|
||||||
|
if (!empty($friendMessage)){
|
||||||
|
$res = $messageController->saveMessage($friendMessage[0]);
|
||||||
|
}else{
|
||||||
|
$res = $messageController->saveChatroomMessage($chatroomMessage[0]);
|
||||||
|
}
|
||||||
|
if (!empty($res)){
|
||||||
|
$msg = '消息记录成功';
|
||||||
|
}else{
|
||||||
|
$msg = '消息记录失败';
|
||||||
|
$codee = 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ResponseHelper::success('',$msg,$codee);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,67 +8,139 @@ use think\Db;
|
|||||||
class MessageController extends BaseController
|
class MessageController extends BaseController
|
||||||
{
|
{
|
||||||
|
|
||||||
public function getList(){
|
public function getList()
|
||||||
|
{
|
||||||
$page = $this->request->param('page', 1);
|
$page = $this->request->param('page', 1);
|
||||||
$limit = $this->request->param('limit', 10);
|
$limit = $this->request->param('limit', 10);
|
||||||
$accountId = $this->getUserInfo('s2_accountId');
|
$accountId = $this->getUserInfo('s2_accountId');
|
||||||
if (empty($accountId)){
|
if (empty($accountId)) {
|
||||||
return ResponseHelper::error('请先登录');
|
return ResponseHelper::error('请先登录');
|
||||||
}
|
}
|
||||||
|
|
||||||
$chatroomList = Db::table('s2_wechat_chatroom')->alias('wc')
|
$friends = Db::table('s2_wechat_friend')
|
||||||
->join(['s2_wechat_message' => 'm'], 'wc.id = m.wechatChatroomId', 'LEFT')
|
->where(['accountId' => $accountId, 'isDeleted' => 0])
|
||||||
->where(['wc.accountId' => $accountId,'m.type' => 2,' wc.isDeleted' => 0])
|
->column('id,nickname,avatar,conRemark,labels,groupId,wechatAccountId,wechatId');
|
||||||
->order('m.id desc')
|
|
||||||
->group('m.wechatChatroomId')
|
|
||||||
->page($page, $limit)
|
|
||||||
->select();
|
|
||||||
$friendIds = Db::table('s2_wechat_friend')
|
|
||||||
->where(['accountId' => $accountId,' isDeleted' => 0])
|
|
||||||
->group('id')
|
|
||||||
->column('id');
|
|
||||||
$friendList = Db::table('s2_wechat_message')
|
|
||||||
->where(['type' => 1])
|
|
||||||
->whereIn('wechatFriendId',$friendIds)
|
|
||||||
->order('id desc')
|
|
||||||
->group('wechatFriendId')
|
|
||||||
->page($page, $limit)
|
|
||||||
->select();
|
|
||||||
|
|
||||||
$list = array_merge($chatroomList,$friendList);
|
|
||||||
|
// 构建好友子查询
|
||||||
// 按createTime字段从大到小排序
|
$friendSubQuery = Db::table('s2_wechat_friend')
|
||||||
usort($list, function($a, $b) {
|
->where(['accountId' => $accountId, 'isDeleted' => 0])
|
||||||
return $b['createTime'] <=> $a['createTime'];
|
->field('id')
|
||||||
|
->buildSql();
|
||||||
|
|
||||||
|
// 优化后的查询:使用MySQL兼容的查询方式
|
||||||
|
$unionQuery = "
|
||||||
|
(SELECT m.id, m.content, m.wechatFriendId, m.wechatChatroomId, m.createTime, m.wechatTime, 2 as msgType, wc.nickname, wc.chatroomAvatar as avatar, wc.chatroomId
|
||||||
|
FROM s2_wechat_chatroom wc
|
||||||
|
INNER JOIN s2_wechat_message m ON wc.id = m.wechatChatroomId AND m.type = 2
|
||||||
|
INNER JOIN (
|
||||||
|
SELECT wechatChatroomId, MAX(wechatTime) as maxTime, MAX(id) as maxId
|
||||||
|
FROM s2_wechat_message
|
||||||
|
WHERE type = 2
|
||||||
|
GROUP BY wechatChatroomId
|
||||||
|
) latest ON m.wechatChatroomId = latest.wechatChatroomId AND m.wechatTime = latest.maxTime AND m.id = latest.maxId
|
||||||
|
WHERE wc.accountId = {$accountId} AND wc.isDeleted = 0
|
||||||
|
)
|
||||||
|
UNION ALL
|
||||||
|
(SELECT m.id, m.content, m.wechatFriendId, m.wechatChatroomId, m.createTime, m.wechatTime, 1 as msgType, 1 as nickname, 1 as avatar, 1 as chatroomId
|
||||||
|
FROM s2_wechat_message m
|
||||||
|
INNER JOIN (
|
||||||
|
SELECT wechatFriendId, MAX(wechatTime) as maxTime, MAX(id) as maxId
|
||||||
|
FROM s2_wechat_message
|
||||||
|
WHERE type = 1 AND wechatFriendId IN {$friendSubQuery}
|
||||||
|
GROUP BY wechatFriendId
|
||||||
|
) latest ON m.wechatFriendId = latest.wechatFriendId AND m.wechatTime = latest.maxTime AND m.id = latest.maxId
|
||||||
|
WHERE m.type = 1 AND m.wechatFriendId IN {$friendSubQuery}
|
||||||
|
)
|
||||||
|
ORDER BY wechatTime DESC
|
||||||
|
LIMIT " . (($page - 1) * $limit) . ", {$limit}
|
||||||
|
";
|
||||||
|
|
||||||
|
$list = Db::query($unionQuery);
|
||||||
|
|
||||||
|
// 对分页后的结果进行排序(按wechatTime降序)
|
||||||
|
usort($list, function ($a, $b) {
|
||||||
|
return $b['wechatTime'] <=> $a['wechatTime'];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
foreach ($list as $k=>&$v){
|
// 批量统计未读数量(isRead=0),按好友/群聊分别聚合
|
||||||
$v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s',$v['createTime']) : '';
|
$friendIds = [];
|
||||||
$v['wechatTime'] = !empty($v['wechatTime']) ? date('Y-m-d H:i:s',$v['wechatTime']) : '';
|
$chatroomIds = [];
|
||||||
|
foreach ($list as $row) {
|
||||||
|
if (!empty($row['wechatFriendId'])) {
|
||||||
|
$friendIds[] = $row['wechatFriendId'];
|
||||||
|
}
|
||||||
|
if (!empty($row['wechatChatroomId'])) {
|
||||||
|
$chatroomIds[] = $row['wechatChatroomId'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$friendIds = array_values(array_unique(array_filter($friendIds)));
|
||||||
|
$chatroomIds = array_values(array_unique(array_filter($chatroomIds)));
|
||||||
|
|
||||||
if (!empty($v['wechatFriendId'])){
|
$friendUnreadMap = [];
|
||||||
$friend = Db::table('s2_wechat_friend')
|
if (!empty($friendIds)) {
|
||||||
->where(['id'=>$v['wechatFriendId']])
|
// 获取未读消息数量
|
||||||
->field('id,nickname,avatar')
|
$friendUnreadMap = Db::table('s2_wechat_message')
|
||||||
->find();
|
->where(['isRead' => 0])
|
||||||
$v['msgInfo'] = $friend;
|
->whereIn('wechatFriendId', $friendIds)
|
||||||
$v['unreadCount'] = Db::table('s2_wechat_message')
|
->group('wechatFriendId')
|
||||||
->where(['wechatFriendId' => $v['wechatFriendId'],'isRead' => 0])
|
->column('COUNT(*) AS cnt', 'wechatFriendId');
|
||||||
->count();
|
}
|
||||||
|
|
||||||
|
$chatroomUnreadMap = [];
|
||||||
|
if (!empty($chatroomIds)) {
|
||||||
|
// 获取未读消息数量
|
||||||
|
$chatroomUnreadMap = Db::table('s2_wechat_message')
|
||||||
|
->where(['isRead' => 0])
|
||||||
|
->whereIn('wechatChatroomId', $chatroomIds)
|
||||||
|
->group('wechatChatroomId')
|
||||||
|
->column('COUNT(*) AS cnt', 'wechatChatroomId');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
foreach ($list as $k => &$v) {
|
||||||
|
|
||||||
|
$createTime = !empty($v['createTime']) ? date('Y-m-d H:i:s', $v['createTime']) : '';
|
||||||
|
$wechatTime = !empty($v['wechatTime']) ? date('Y-m-d H:i:s', $v['wechatTime']) : '';
|
||||||
|
|
||||||
|
|
||||||
|
$unreadCount = 0;
|
||||||
|
if (!empty($v['wechatFriendId'])) {
|
||||||
|
$v['nickname'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['nickname'] : '';
|
||||||
|
$v['avatar'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['avatar'] : '';
|
||||||
|
$v['conRemark'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['conRemark'] : '';
|
||||||
|
$v['groupId'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['groupId'] : '';
|
||||||
|
$v['wechatAccountId'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['wechatAccountId'] : '';
|
||||||
|
$v['wechatId'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['wechatId'] : '';
|
||||||
|
$v['labels'] = !empty($friends[$v['wechatFriendId']]) ? json_decode($friends[$v['wechatFriendId']]['labels'], true) : [];
|
||||||
|
$unreadCount = isset($friendUnreadMap[$v['wechatFriendId']]) ? (int)$friendUnreadMap[$v['wechatFriendId']] : 0;
|
||||||
|
unset($v['chatroomId']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($v['wechatChatroomId'])){
|
if (!empty($v['wechatChatroomId'])) {
|
||||||
$chatroom = Db::table('s2_wechat_chatroom')
|
$v['conRemark'] = '';
|
||||||
->where(['id'=>$v['wechatChatroomId']])
|
$unreadCount = isset($chatroomUnreadMap[$v['wechatChatroomId']]) ? (int)$chatroomUnreadMap[$v['wechatChatroomId']] : 0;
|
||||||
->field('id,nickname,chatroomAvatar as avatar')
|
|
||||||
->find();
|
|
||||||
$v['msgInfo'] = $chatroom;
|
|
||||||
$v['unreadCount'] = Db::table('s2_wechat_message')
|
|
||||||
->where(['wechatChatroomId' => $v['wechatChatroomId'],'isRead' => 0])
|
|
||||||
->count();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$v['id'] = !empty($v['wechatFriendId']) ? $v['wechatFriendId'] : $v['wechatChatroomId'];
|
||||||
|
$v['config'] = [
|
||||||
|
'top' => false,
|
||||||
|
'unreadCount' => $unreadCount,
|
||||||
|
'chat' => true,
|
||||||
|
'msgTime' => $v['wechatTime'],
|
||||||
|
];
|
||||||
|
$v['createTime'] = $createTime;
|
||||||
|
$v['lastUpdateTime'] = $wechatTime;
|
||||||
|
|
||||||
|
// 最新消息内容已经在UNION查询中获取,直接使用
|
||||||
|
$v['latestMessage'] = [
|
||||||
|
'content' => $v['content'],
|
||||||
|
'wechatTime' => $wechatTime
|
||||||
|
];
|
||||||
|
|
||||||
|
unset($v['wechatFriendId'], $v['wechatChatroomId']);
|
||||||
|
|
||||||
}
|
}
|
||||||
unset($v);
|
unset($v);
|
||||||
|
|
||||||
@@ -76,24 +148,25 @@ class MessageController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function readMessage(){
|
public function readMessage()
|
||||||
|
{
|
||||||
$wechatFriendId = $this->request->param('wechatFriendId', '');
|
$wechatFriendId = $this->request->param('wechatFriendId', '');
|
||||||
$wechatChatroomId = $this->request->param('wechatChatroomId', '');
|
$wechatChatroomId = $this->request->param('wechatChatroomId', '');
|
||||||
$accountId = $this->getUserInfo('s2_accountId');
|
$accountId = $this->getUserInfo('s2_accountId');
|
||||||
if (empty($accountId)){
|
if (empty($accountId)) {
|
||||||
return ResponseHelper::error('请先登录');
|
return ResponseHelper::error('请先登录');
|
||||||
}
|
}
|
||||||
if (empty($wechatChatroomId) && empty($wechatFriendId)){
|
if (empty($wechatChatroomId) && empty($wechatFriendId)) {
|
||||||
return ResponseHelper::error('参数缺失');
|
return ResponseHelper::error('参数缺失');
|
||||||
}
|
}
|
||||||
|
|
||||||
$where = [];
|
$where = [];
|
||||||
if (!empty($wechatChatroomId)){
|
if (!empty($wechatChatroomId)) {
|
||||||
$where[] = ['wechatChatroomId','=',$wechatChatroomId];
|
$where[] = ['wechatChatroomId', '=', $wechatChatroomId];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($wechatFriendId)){
|
if (!empty($wechatFriendId)) {
|
||||||
$where[] = ['wechatFriendId','=',$wechatFriendId];
|
$where[] = ['wechatFriendId', '=', $wechatFriendId];
|
||||||
}
|
}
|
||||||
|
|
||||||
Db::table('s2_wechat_message')->where($where)->update(['isRead' => 1]);
|
Db::table('s2_wechat_message')->where($where)->update(['isRead' => 1]);
|
||||||
@@ -101,49 +174,47 @@ class MessageController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function details()
|
||||||
|
{
|
||||||
public function details(){
|
|
||||||
$wechatFriendId = $this->request->param('wechatFriendId', '');
|
$wechatFriendId = $this->request->param('wechatFriendId', '');
|
||||||
$wechatChatroomId = $this->request->param('wechatChatroomId', '');
|
$wechatChatroomId = $this->request->param('wechatChatroomId', '');
|
||||||
$wechatAccountId = $this->request->param('wechatAccountId', '');
|
$wechatAccountId = $this->request->param('wechatAccountId', '');
|
||||||
$page = $this->request->param('page', 1);
|
$page = $this->request->param('page', 1);
|
||||||
$limit = $this->request->param('limit', 10);
|
$limit = $this->request->param('limit', 10);
|
||||||
$from = $this->request->param('From', '');
|
$from = $this->request->param('From', '');
|
||||||
$to = $this->request->param('To', '');
|
$to = $this->request->param('To', '');
|
||||||
$olderData = $this->request->param('olderData', false);
|
$olderData = $this->request->param('olderData', false);
|
||||||
$accountId = $this->getUserInfo('s2_accountId');
|
$accountId = $this->getUserInfo('s2_accountId');
|
||||||
if (empty($accountId)){
|
if (empty($accountId)) {
|
||||||
return ResponseHelper::error('请先登录');
|
return ResponseHelper::error('请先登录');
|
||||||
}
|
}
|
||||||
if (empty($wechatChatroomId) && empty($wechatFriendId)){
|
if (empty($wechatChatroomId) && empty($wechatFriendId)) {
|
||||||
return ResponseHelper::error('参数缺失');
|
return ResponseHelper::error('参数缺失');
|
||||||
}
|
}
|
||||||
|
|
||||||
$where = [];
|
$where = [];
|
||||||
if (!empty($wechatChatroomId)){
|
if (!empty($wechatChatroomId)) {
|
||||||
$where[] = ['wechatChatroomId','=',$wechatChatroomId];
|
$where[] = ['wechatChatroomId', '=', $wechatChatroomId];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($wechatFriendId)){
|
if (!empty($wechatFriendId)) {
|
||||||
$where[] = ['wechatFriendId','=',$wechatFriendId];
|
$where[] = ['wechatFriendId', '=', $wechatFriendId];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($From) && !empty($To)){
|
if (!empty($From) && !empty($To)) {
|
||||||
$where[] = ['wechatTime','between',[$from,$to]];
|
$where[] = ['wechatTime', 'between', [$from, $to]];
|
||||||
}
|
}
|
||||||
|
|
||||||
$total = Db::table('s2_wechat_message')->where($where)->count();
|
$total = Db::table('s2_wechat_message')->where($where)->count();
|
||||||
$list = Db::table('s2_wechat_message')->where($where)->page($page,$limit)->order('id DESC')->select();
|
$list = Db::table('s2_wechat_message')->where($where)->page($page, $limit)->order('id DESC')->select();
|
||||||
|
|
||||||
|
|
||||||
|
foreach ($list as $k => &$v) {
|
||||||
foreach ($list as $k=>&$v){
|
$v['wechatTime'] = !empty($v['wechatTime']) ? date('Y-m-d H:i:s', $v['wechatTime']) : '';
|
||||||
$v['wechatTime'] = !empty($v['wechatTime']) ? date('Y-m-d H:i:s',$v['wechatTime']) : '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return ResponseHelper::success(['total'=>$total,'list'=>$list]);
|
return ResponseHelper::success(['total' => $total, 'list' => $list]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -370,18 +370,17 @@ class MomentsController extends BaseController
|
|||||||
// 处理数据
|
// 处理数据
|
||||||
$data = [];
|
$data = [];
|
||||||
foreach ($list as $item) {
|
foreach ($list as $item) {
|
||||||
$sendData = json_encode($item->sendData,true);
|
$sendData = json_decode($item->sendData,true);
|
||||||
$data[] = [
|
$data[] = [
|
||||||
'id' => $item->id,
|
'id' => $item->id,
|
||||||
'text' => $sendData['text'] ?? '',
|
'content' => $sendData['text'] ?? '',
|
||||||
'momentContentType' => $sendData['momentContentType'] ?? 1,
|
'momentContentType' => $sendData['momentContentType'] ?? 1,
|
||||||
'picUrlList' => $sendData['picUrlList'] ?? [],
|
'picUrlList' => $sendData['picUrlList'] ?? [],
|
||||||
'videoUrl' => $sendData['videoUrl'] ?? '',
|
'videoUrl' => $sendData['videoUrl'] ?? '',
|
||||||
'link' => $sendData['link'] ?? [],
|
'link' => $sendData['link'] ?? [],
|
||||||
'publicMode' => $sendData['publicMode'] ?? 2,
|
'publicMode' => $sendData['publicMode'] ?? 2,
|
||||||
'isSend' => $item->isSend,
|
'isSend' => $item->isSend,
|
||||||
'createTime' => $item->createTime,
|
'sendTime' => date('Y-m-d H:i:s',$item->sendTime),
|
||||||
'sendTime' => $item->sendTime,
|
|
||||||
'accountCount' => count($sendData['jobPublishWechatMomentsItems'] ?? [])
|
'accountCount' => count($sendData['jobPublishWechatMomentsItems'] ?? [])
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,25 +9,25 @@ use think\Db;
|
|||||||
class WechatFriendController extends BaseController
|
class WechatFriendController extends BaseController
|
||||||
{
|
{
|
||||||
|
|
||||||
public function getList(){
|
public function getList()
|
||||||
|
{
|
||||||
$page = $this->request->param('page', 1);
|
$page = $this->request->param('page', 1);
|
||||||
$limit = $this->request->param('limit', 10);
|
$limit = $this->request->param('limit', 10);
|
||||||
$accountId = $this->getUserInfo('s2_accountId');
|
$accountId = $this->getUserInfo('s2_accountId');
|
||||||
if (empty($accountId)){
|
if (empty($accountId)) {
|
||||||
return ResponseHelper::error('请先登录');
|
return ResponseHelper::error('请先登录');
|
||||||
}
|
}
|
||||||
$query = Db::table('s2_wechat_friend')
|
$query = Db::table('s2_wechat_friend')
|
||||||
->where(['accountId' => $accountId,'isDeleted' => 0])
|
->where(['accountId' => $accountId, 'isDeleted' => 0])
|
||||||
->order('id desc');
|
->order('id desc');
|
||||||
$total = $query->count();
|
$total = $query->count();
|
||||||
$list = $query->page($page, $limit)->select();
|
$list = $query->page($page, $limit)->select();
|
||||||
|
|
||||||
|
|
||||||
|
/* // 提取所有好友ID
|
||||||
// 提取所有好友ID
|
|
||||||
$friendIds = array_column($list, 'id');
|
$friendIds = array_column($list, 'id');
|
||||||
|
|
||||||
// 一次性查询所有好友的未读消息数量
|
// 一次性查询所有好友的未读消息数量
|
||||||
$unreadCounts = [];
|
$unreadCounts = [];
|
||||||
if (!empty($friendIds)) {
|
if (!empty($friendIds)) {
|
||||||
$unreadResults = Db::table('s2_wechat_message')
|
$unreadResults = Db::table('s2_wechat_message')
|
||||||
@@ -36,14 +36,15 @@ class WechatFriendController extends BaseController
|
|||||||
->where('isRead', 0)
|
->where('isRead', 0)
|
||||||
->group('wechatFriendId')
|
->group('wechatFriendId')
|
||||||
->select();
|
->select();
|
||||||
|
if (!empty($unreadResults)) {
|
||||||
foreach ($unreadResults as $result) {
|
foreach ($unreadResults as $result) {
|
||||||
$unreadCounts[$result['wechatFriendId']] = $result['count'];
|
$unreadCounts[$result['wechatFriendId']] = $result['count'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 一次性查询所有好友的最新消息
|
// 一次性查询所有好友的最新消息
|
||||||
$latestMessages = [];
|
$latestMessages = [];
|
||||||
if (!empty($friendIds)) {
|
if (!empty($friendIds)) {
|
||||||
// 使用子查询获取每个好友的最新消息ID
|
// 使用子查询获取每个好友的最新消息ID
|
||||||
$subQuery = Db::table('s2_wechat_message')
|
$subQuery = Db::table('s2_wechat_message')
|
||||||
@@ -51,18 +52,22 @@ class WechatFriendController extends BaseController
|
|||||||
->where('wechatFriendId', 'in', $friendIds)
|
->where('wechatFriendId', 'in', $friendIds)
|
||||||
->group('wechatFriendId')
|
->group('wechatFriendId')
|
||||||
->buildSql();
|
->buildSql();
|
||||||
|
|
||||||
// 查询最新消息的详细信息
|
if (!empty($subQuery)) {
|
||||||
$messageResults = Db::table('s2_wechat_message')
|
// 查询最新消息的详细信息
|
||||||
->alias('m')
|
$messageResults = Db::table('s2_wechat_message')
|
||||||
->join([$subQuery => 'sub'], 'm.id = sub.max_id')
|
->alias('m')
|
||||||
->field('m.*, sub.wechatFriendId')
|
->join([$subQuery => 'sub'], 'm.id = sub.max_id')
|
||||||
->select();
|
->field('m.*, sub.wechatFriendId')
|
||||||
|
->select();
|
||||||
foreach ($messageResults as $message) {
|
|
||||||
$latestMessages[$message['wechatFriendId']] = $message;
|
if (!empty($messageResults)) {
|
||||||
|
foreach ($messageResults as $message) {
|
||||||
|
$latestMessages[$message['wechatFriendId']] = $message;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
|
|
||||||
$aiTypeData = [];
|
$aiTypeData = [];
|
||||||
@@ -80,18 +85,20 @@ class WechatFriendController extends BaseController
|
|||||||
$v['passTime'] = !empty($v['passTime']) ? date('Y-m-d H:i:s', $v['passTime']) : '';
|
$v['passTime'] = !empty($v['passTime']) ? date('Y-m-d H:i:s', $v['passTime']) : '';
|
||||||
|
|
||||||
|
|
||||||
$config = [
|
/* $config = [
|
||||||
'unreadCount' => isset($unreadCounts[$v['id']]) ? $unreadCounts[$v['id']] : 0,
|
'unreadCount' => isset($unreadCounts[$v['id']]) ? $unreadCounts[$v['id']] : 0,
|
||||||
'chat' => isset($latestMessages[$v['id']]),
|
'chat' => isset($latestMessages[$v['id']]),
|
||||||
'msgTime' => isset($latestMessages[$v['id']]) ? $latestMessages[$v['id']]['wechatTime'] : 0
|
'msgTime' => isset($latestMessages[$v['id']]) ? $latestMessages[$v['id']]['wechatTime'] : 0
|
||||||
];
|
];
|
||||||
|
|
||||||
// 将消息配置添加到好友数据中
|
// 将消息配置添加到好友数据中
|
||||||
$v['config'] = $config;
|
$v['config'] = $config;*/
|
||||||
|
|
||||||
|
|
||||||
$v['aiType'] = isset($aiTypeData[$v['id']]) ? $aiTypeData[$v['id']] : 0;
|
$v['aiType'] = isset($aiTypeData[$v['id']]) ? $aiTypeData[$v['id']] : 0;
|
||||||
}
|
}
|
||||||
unset($v);
|
unset($v);
|
||||||
|
|
||||||
return ResponseHelper::success(['list'=>$list,'total'=>$total]);
|
return ResponseHelper::success(['list' => $list, 'total' => $total]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
52
Server/application/chukebao/model/AiKnowledgeBase.php
Normal file
52
Server/application/chukebao/model/AiKnowledgeBase.php
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\chukebao\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI知识库模型
|
||||||
|
*/
|
||||||
|
class AiKnowledgeBase extends Model
|
||||||
|
{
|
||||||
|
// 设置表名
|
||||||
|
protected $name = 'ai_knowledge_base';
|
||||||
|
|
||||||
|
// 设置主键
|
||||||
|
protected $pk = 'id';
|
||||||
|
|
||||||
|
// 设置JSON字段
|
||||||
|
protected $json = ['label'];
|
||||||
|
|
||||||
|
// 设置JSON字段自动转换为数组
|
||||||
|
protected $jsonAssoc = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关联知识库类型
|
||||||
|
*/
|
||||||
|
public function type()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(AiKnowledgeBaseType::class, 'typeId', 'id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取有效的知识库列表(未删除)
|
||||||
|
*/
|
||||||
|
public static function getValidList($companyId, $typeId = null)
|
||||||
|
{
|
||||||
|
$where = [
|
||||||
|
['isDel', '=', 0],
|
||||||
|
['companyId', '=', $companyId]
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($typeId !== null) {
|
||||||
|
$where[] = ['typeId', '=', $typeId];
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::where($where)
|
||||||
|
->with(['type'])
|
||||||
|
->order('createTime', 'desc')
|
||||||
|
->select();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
57
Server/application/chukebao/model/AiKnowledgeBaseType.php
Normal file
57
Server/application/chukebao/model/AiKnowledgeBaseType.php
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\chukebao\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI知识库类型模型
|
||||||
|
*/
|
||||||
|
class AiKnowledgeBaseType extends Model
|
||||||
|
{
|
||||||
|
// 设置表名
|
||||||
|
protected $name = 'ai_knowledge_base_type';
|
||||||
|
|
||||||
|
// 设置主键
|
||||||
|
protected $pk = 'id';
|
||||||
|
|
||||||
|
// 设置JSON字段
|
||||||
|
protected $json = ['label'];
|
||||||
|
|
||||||
|
// 设置JSON字段自动转换为数组
|
||||||
|
protected $jsonAssoc = true;
|
||||||
|
|
||||||
|
// 类型常量
|
||||||
|
const TYPE_SYSTEM = 0; // 系统类型
|
||||||
|
const TYPE_USER = 1; // 用户创建类型
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取有效的类型列表(未删除)
|
||||||
|
*/
|
||||||
|
public static function getValidList($companyId, $includeSystem = true)
|
||||||
|
{
|
||||||
|
$where = [
|
||||||
|
['isDel', '=', 0]
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($includeSystem) {
|
||||||
|
$where[] = ['type|companyId', 'in', [0, $companyId]];
|
||||||
|
} else {
|
||||||
|
$where[] = ['companyId', '=', $companyId];
|
||||||
|
$where[] = ['type', '=', self::TYPE_USER];
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::where($where)
|
||||||
|
->order('createTime', 'desc')
|
||||||
|
->select();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否为系统类型
|
||||||
|
*/
|
||||||
|
public function isSystemType()
|
||||||
|
{
|
||||||
|
return $this->type == self::TYPE_SYSTEM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
17
Server/application/chukebao/model/AiSettings.php
Normal file
17
Server/application/chukebao/model/AiSettings.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\chukebao\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
class AiSettings extends Model
|
||||||
|
{
|
||||||
|
protected $pk = 'id';
|
||||||
|
protected $name = 'ai_settings';
|
||||||
|
|
||||||
|
// 自动写入时间戳
|
||||||
|
protected $autoWriteTimestamp = true;
|
||||||
|
protected $createTime = 'createTime';
|
||||||
|
protected $updateTime = 'updateTime';
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@ class WechatChatroomCommand extends Command
|
|||||||
|
|
||||||
// 检查队列是否已经在运行
|
// 检查队列是否已经在运行
|
||||||
$queueLockKey = "queue_lock:{$this->queueName}:{$isDel}";
|
$queueLockKey = "queue_lock:{$this->queueName}:{$isDel}";
|
||||||
|
Cache::rm($queueLockKey);
|
||||||
if (Cache::get($queueLockKey)) {
|
if (Cache::get($queueLockKey)) {
|
||||||
$output->writeln("队列 {$this->queueName} 已经在运行中,删除状态:{$isDel},跳过执行");
|
$output->writeln("队列 {$this->queueName} 已经在运行中,删除状态:{$isDel},跳过执行");
|
||||||
Log::warning("队列 {$this->queueName} 已经在运行中,删除状态:{$isDel},跳过执行");
|
Log::warning("队列 {$this->queueName} 已经在运行中,删除状态:{$isDel},跳过执行");
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class WechatFriendCommand extends Command
|
|||||||
|
|
||||||
// 检查队列是否已经在运行
|
// 检查队列是否已经在运行
|
||||||
$queueLockKey = "queue_lock:{$this->queueName}:{$isDel}";
|
$queueLockKey = "queue_lock:{$this->queueName}:{$isDel}";
|
||||||
|
Cache::rm($queueLockKey);
|
||||||
if (Cache::get($queueLockKey)) {
|
if (Cache::get($queueLockKey)) {
|
||||||
$output->writeln("队列 {$this->queueName} 已经在运行中,删除状态:{$isDel},跳过执行");
|
$output->writeln("队列 {$this->queueName} 已经在运行中,删除状态:{$isDel},跳过执行");
|
||||||
Log::warning("队列 {$this->queueName} 已经在运行中,删除状态:{$isDel},跳过执行");
|
Log::warning("队列 {$this->queueName} 已经在运行中,删除状态:{$isDel},跳过执行");
|
||||||
|
|||||||
@@ -488,6 +488,38 @@ if (!function_exists('getUserAction')) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!function_exists('formatRelativeTime')) {
|
||||||
|
/**
|
||||||
|
* 将时间戳格式化为相对时间(中文)
|
||||||
|
* 例:半年前 / 1个月前 / 3周前 / 1天前 / 5小时前 / 5分钟前 / 刚刚
|
||||||
|
* @param int $timestamp Unix 时间戳(秒)
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function formatRelativeTime($timestamp)
|
||||||
|
{
|
||||||
|
if (empty($timestamp) || !is_numeric($timestamp)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$now = time();
|
||||||
|
$diff = max(0, $now - (int)$timestamp);
|
||||||
|
|
||||||
|
$minute = 60;
|
||||||
|
$hour = 60 * $minute;
|
||||||
|
$day = 24 * $hour;
|
||||||
|
$week = 7 * $day;
|
||||||
|
$month = 30 * $day; // 近似
|
||||||
|
$halfYear = 6 * $month; // 近似
|
||||||
|
|
||||||
|
if ($diff >= $halfYear) return '半年前';
|
||||||
|
if ($diff >= $month) return floor($diff / $month) . '个月前';
|
||||||
|
if ($diff >= $week) return floor($diff / $week) . '周前';
|
||||||
|
if ($diff >= $day) return floor($diff / $day) . '天前';
|
||||||
|
if ($diff >= $hour) return floor($diff / $hour) . '小时前';
|
||||||
|
if ($diff >= $minute) return floor($diff / $minute) . '分钟前';
|
||||||
|
return '刚刚';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!function_exists('exit_data')) {
|
if (!function_exists('exit_data')) {
|
||||||
|
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ class TaskServer extends Server
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3个进程处理获客新任务
|
// 进程处理获客新任务
|
||||||
if ($current_worker_id < 3) {
|
if ($current_worker_id == 2) {
|
||||||
Timer::add(1, function () use ($current_worker_id, $process_count_for_status_0, $adapter) {
|
Timer::add(1, function () use ($current_worker_id, $process_count_for_status_0, $adapter) {
|
||||||
$adapter->handleCustomerTaskWithStatusIsNew($current_worker_id, $process_count_for_status_0);
|
$adapter->handleCustomerTaskWithStatusIsNew($current_worker_id, $process_count_for_status_0);
|
||||||
});
|
});
|
||||||
|
|||||||
17
Server/application/common/model/TrafficSourcePackage.php
Normal file
17
Server/application/common/model/TrafficSourcePackage.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\common\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流量池模型类
|
||||||
|
*/
|
||||||
|
class TrafficSourcePackage extends Model
|
||||||
|
{
|
||||||
|
|
||||||
|
// 设置数据表名
|
||||||
|
protected $name = 'traffic_source_package';
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
17
Server/application/common/model/TrafficSourcePackageItem.php
Normal file
17
Server/application/common/model/TrafficSourcePackageItem.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\common\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流量池模型类
|
||||||
|
*/
|
||||||
|
class TrafficSourcePackageItem extends Model
|
||||||
|
{
|
||||||
|
|
||||||
|
// 设置数据表名
|
||||||
|
protected $name = 'traffic_source_package_item';
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -62,12 +62,23 @@ Route::group('v1/', function () {
|
|||||||
|
|
||||||
// 流量池相关
|
// 流量池相关
|
||||||
Route::group('traffic/pool', function () {
|
Route::group('traffic/pool', function () {
|
||||||
Route::get('', 'app\cunkebao\controller\traffic\GetPotentialListWithInCompanyV1Controller@index');
|
Route::get('getPackage', 'app\cunkebao\controller\TrafficController@getPackage');
|
||||||
|
Route::post('addPackage', 'app\cunkebao\controller\TrafficController@addPackage');
|
||||||
|
Route::post('editPackage', 'app\cunkebao\controller\TrafficController@editPackage');
|
||||||
|
Route::delete('deletePackage', 'app\cunkebao\controller\TrafficController@deletePackage');
|
||||||
|
|
||||||
|
Route::get('', 'app\cunkebao\controller\TrafficController@getTrafficPoolList');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//Route::get('', 'app\cunkebao\controller\traffic\GetPotentialListWithInCompanyV1Controller@index');
|
||||||
Route::get('getUserJourney', 'app\cunkebao\controller\traffic\GetPotentialListWithInCompanyV1Controller@getUserJourney');
|
Route::get('getUserJourney', 'app\cunkebao\controller\traffic\GetPotentialListWithInCompanyV1Controller@getUserJourney');
|
||||||
Route::get('getUserTags', 'app\cunkebao\controller\traffic\GetPotentialListWithInCompanyV1Controller@getUserTags');
|
Route::get('getUserTags', 'app\cunkebao\controller\traffic\GetPotentialListWithInCompanyV1Controller@getUserTags');
|
||||||
Route::get('getUserInfo', 'app\cunkebao\controller\traffic\GetPotentialListWithInCompanyV1Controller@getUser');
|
Route::get('getUserInfo', 'app\cunkebao\controller\traffic\GetPotentialListWithInCompanyV1Controller@getUser');
|
||||||
Route::get('getPackage', 'app\cunkebao\controller\traffic\GetPotentialListWithInCompanyV1Controller@getPackage');
|
// Route::post('addPackage', 'app\cunkebao\controller\traffic\GetPotentialListWithInCompanyV1Controller@addPackage');
|
||||||
Route::post('addPackage', 'app\cunkebao\controller\traffic\GetPotentialListWithInCompanyV1Controller@addPackage');
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Route::get('converted', 'app\cunkebao\controller\traffic\GetConvertedListWithInCompanyV1Controller@index');
|
Route::get('converted', 'app\cunkebao\controller\traffic\GetConvertedListWithInCompanyV1Controller@index');
|
||||||
@@ -149,6 +160,26 @@ Route::group('v1/', function () {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//AI知识库
|
||||||
|
Route::group('knowledge', function () {
|
||||||
|
Route::get('init', 'app\cunkebao\controller\AiSettingsController@init');
|
||||||
|
Route::get('release', 'app\cunkebao\controller\AiSettingsController@release');
|
||||||
|
Route::get('typeList', 'app\cunkebao\controller\AiKnowledgeBaseController@typeList');
|
||||||
|
Route::get('getList', 'app\cunkebao\controller\AiKnowledgeBaseController@getList');
|
||||||
|
Route::post('add', 'app\cunkebao\controller\AiKnowledgeBaseController@add');
|
||||||
|
//Route::post('edit', 'app\cunkebao\controller\AiKnowledgeBaseController@edit');
|
||||||
|
Route::delete('delete', 'app\cunkebao\controller\AiKnowledgeBaseController@delete');
|
||||||
|
//Route::get('detail', 'app\cunkebao\controller\AiKnowledgeBaseController@detail');
|
||||||
|
Route::post('update', 'app\cunkebao\controller\AiKnowledgeBaseController@update');
|
||||||
|
Route::post('delete', 'app\cunkebao\controller\AiKnowledgeBaseController@delete');
|
||||||
|
Route::post('addType', 'app\cunkebao\controller\AiKnowledgeBaseController@addType');
|
||||||
|
Route::post('editType', 'app\cunkebao\controller\AiKnowledgeBaseController@editType');
|
||||||
|
Route::delete('deleteType', 'app\cunkebao\controller\AiKnowledgeBaseController@deleteType');
|
||||||
|
Route::get('detailType', 'app\cunkebao\controller\AiKnowledgeBaseController@detailType');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
})->middleware(['jwt']);
|
})->middleware(['jwt']);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,570 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\cunkebao\controller;
|
||||||
|
|
||||||
|
use app\ai\controller\CozeAI;
|
||||||
|
use app\chukebao\model\AiKnowledgeBaseType;
|
||||||
|
use app\chukebao\model\AiKnowledgeBase;
|
||||||
|
use app\chukebao\model\AiSettings as AiSettingsModel;
|
||||||
|
use library\ResponseHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI知识库管理控制器
|
||||||
|
* 负责管理AI知识库类型和知识库内容
|
||||||
|
*/
|
||||||
|
class AiKnowledgeBaseController extends BaseController
|
||||||
|
{
|
||||||
|
// ==================== 知识库类型管理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取知识库类型列表
|
||||||
|
*
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function typeList()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
if (empty($companyId)) {
|
||||||
|
return ResponseHelper::error('公司信息获取失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取分页参数
|
||||||
|
$page = $this->request->param('page', 1);
|
||||||
|
$pageSize = $this->request->param('pageSize', 20);
|
||||||
|
$includeSystem = $this->request->param('includeSystem', 1); // 是否包含系统类型
|
||||||
|
|
||||||
|
// 构建查询条件
|
||||||
|
$where = [['isDel', '=', 0]];
|
||||||
|
|
||||||
|
if ($includeSystem == 1) {
|
||||||
|
// 包含系统类型和本公司创建的类型
|
||||||
|
$where[] = ['type', '=', AiKnowledgeBaseType::TYPE_SYSTEM];
|
||||||
|
$where[] = ['companyId|type', 'in', [$companyId, 0]];
|
||||||
|
} else {
|
||||||
|
// 只显示本公司创建的类型
|
||||||
|
$where[] = ['companyId', '=', $companyId];
|
||||||
|
$where[] = ['type', '=', AiKnowledgeBaseType::TYPE_USER];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询数据
|
||||||
|
$list = AiKnowledgeBaseType::where($where)
|
||||||
|
->order('type', 'asc') // 系统类型排在前面
|
||||||
|
->order('createTime', 'desc')
|
||||||
|
->paginate($pageSize, false, ['page' => $page]);
|
||||||
|
|
||||||
|
return ResponseHelper::success($list, '获取成功');
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ResponseHelper::error('系统异常:' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加知识库类型
|
||||||
|
*
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function addType()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
|
||||||
|
if (empty($companyId)) {
|
||||||
|
return ResponseHelper::error('公司信息获取失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取参数
|
||||||
|
$name = $this->request->param('name', '');
|
||||||
|
$description = $this->request->param('description', '');
|
||||||
|
$label = $this->request->param('label', []);
|
||||||
|
$prompt = $this->request->param('prompt', '');
|
||||||
|
|
||||||
|
// 参数验证
|
||||||
|
if (empty($name)) {
|
||||||
|
return ResponseHelper::error('类型名称不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查名称是否重复
|
||||||
|
$exists = AiKnowledgeBaseType::where([
|
||||||
|
['companyId', '=', $companyId],
|
||||||
|
['name', '=', $name],
|
||||||
|
['isDel', '=', 0]
|
||||||
|
])->find();
|
||||||
|
|
||||||
|
if ($exists) {
|
||||||
|
return ResponseHelper::error('该类型名称已存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建类型
|
||||||
|
$typeModel = new AiKnowledgeBaseType();
|
||||||
|
$data = [
|
||||||
|
'type' => AiKnowledgeBaseType::TYPE_USER,
|
||||||
|
'name' => $name,
|
||||||
|
'description' => $description,
|
||||||
|
'label' => json_decode($label,256),
|
||||||
|
'prompt' => $prompt,
|
||||||
|
'companyId' => $companyId,
|
||||||
|
'userId' => $userId,
|
||||||
|
'createTime' => time(),
|
||||||
|
'updateTime' => time(),
|
||||||
|
'isDel' => 0
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($typeModel->save($data)) {
|
||||||
|
return ResponseHelper::success(['id' => $typeModel->id], '添加成功');
|
||||||
|
} else {
|
||||||
|
return ResponseHelper::error('添加失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ResponseHelper::error('系统异常:' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑知识库类型
|
||||||
|
*
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function editType()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
if (empty($companyId)) {
|
||||||
|
return ResponseHelper::error('公司信息获取失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取参数
|
||||||
|
$id = $this->request->param('id', 0);
|
||||||
|
$name = $this->request->param('name', '');
|
||||||
|
$description = $this->request->param('description', '');
|
||||||
|
$label = $this->request->param('label', []);
|
||||||
|
$prompt = $this->request->param('prompt', '');
|
||||||
|
|
||||||
|
// 参数验证
|
||||||
|
if (empty($id)) {
|
||||||
|
return ResponseHelper::error('类型ID不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($name)) {
|
||||||
|
return ResponseHelper::error('类型名称不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找类型
|
||||||
|
$typeModel = AiKnowledgeBaseType::where([
|
||||||
|
['id', '=', $id],
|
||||||
|
['isDel', '=', 0]
|
||||||
|
])->find();
|
||||||
|
|
||||||
|
if (!$typeModel) {
|
||||||
|
return ResponseHelper::error('类型不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为系统类型
|
||||||
|
if ($typeModel->isSystemType()) {
|
||||||
|
return ResponseHelper::error('系统类型不允许编辑');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查权限(只能编辑本公司的类型)
|
||||||
|
if ($typeModel->companyId != $companyId) {
|
||||||
|
return ResponseHelper::error('无权限编辑该类型');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查名称是否重复(排除自己)
|
||||||
|
$exists = AiKnowledgeBaseType::where([
|
||||||
|
['companyId', '=', $companyId],
|
||||||
|
['name', '=', $name],
|
||||||
|
['id', '<>', $id],
|
||||||
|
['isDel', '=', 0]
|
||||||
|
])->find();
|
||||||
|
|
||||||
|
if ($exists) {
|
||||||
|
return ResponseHelper::error('该类型名称已存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新数据
|
||||||
|
$typeModel->name = $name;
|
||||||
|
$typeModel->description = $description;
|
||||||
|
$typeModel->label = $label;
|
||||||
|
$typeModel->prompt = $prompt;
|
||||||
|
$typeModel->updateTime = time();
|
||||||
|
|
||||||
|
if ($typeModel->save()) {
|
||||||
|
return ResponseHelper::success([], '更新成功');
|
||||||
|
} else {
|
||||||
|
return ResponseHelper::error('更新失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ResponseHelper::error('系统异常:' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除知识库类型
|
||||||
|
*
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function deleteType()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
if (empty($companyId)) {
|
||||||
|
return ResponseHelper::error('公司信息获取失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取参数
|
||||||
|
$id = $this->request->param('id', 0);
|
||||||
|
|
||||||
|
// 参数验证
|
||||||
|
if (empty($id)) {
|
||||||
|
return ResponseHelper::error('类型ID不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找类型
|
||||||
|
$typeModel = AiKnowledgeBaseType::where([
|
||||||
|
['id', '=', $id],
|
||||||
|
['isDel', '=', 0]
|
||||||
|
])->find();
|
||||||
|
|
||||||
|
if (!$typeModel) {
|
||||||
|
return ResponseHelper::error('类型不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为系统类型
|
||||||
|
if ($typeModel->isSystemType()) {
|
||||||
|
return ResponseHelper::error('系统类型不允许删除');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查权限(只能删除本公司的类型)
|
||||||
|
if ($typeModel->companyId != $companyId) {
|
||||||
|
return ResponseHelper::error('无权限删除该类型');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有关联的知识库
|
||||||
|
$hasKnowledge = AiKnowledgeBase::where([
|
||||||
|
['typeId', '=', $id],
|
||||||
|
['isDel', '=', 0]
|
||||||
|
])->count();
|
||||||
|
|
||||||
|
if ($hasKnowledge > 0) {
|
||||||
|
return ResponseHelper::error('该类型下存在知识库,无法删除');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 软删除
|
||||||
|
$typeModel->isDel = 1;
|
||||||
|
$typeModel->delTime = time();
|
||||||
|
|
||||||
|
if ($typeModel->save()) {
|
||||||
|
return ResponseHelper::success([], '删除成功');
|
||||||
|
} else {
|
||||||
|
return ResponseHelper::error('删除失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ResponseHelper::error('系统异常:' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 知识库管理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取知识库列表
|
||||||
|
*
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function getList()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
if (empty($companyId)) {
|
||||||
|
return ResponseHelper::error('公司信息获取失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取分页参数
|
||||||
|
$page = $this->request->param('page', 1);
|
||||||
|
$pageSize = $this->request->param('pageSize', 20);
|
||||||
|
$typeId = $this->request->param('typeId', 0); // 类型筛选
|
||||||
|
$keyword = $this->request->param('keyword', ''); // 关键词搜索
|
||||||
|
|
||||||
|
// 构建查询条件
|
||||||
|
$where = [
|
||||||
|
['isDel', '=', 0],
|
||||||
|
['companyId', '=', $companyId]
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($typeId > 0) {
|
||||||
|
$where[] = ['typeId', '=', $typeId];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($keyword)) {
|
||||||
|
$where[] = ['name', 'like', '%' . $keyword . '%'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询数据
|
||||||
|
$list = AiKnowledgeBase::where($where)
|
||||||
|
->with(['type'])
|
||||||
|
->order('createTime', 'desc')
|
||||||
|
->paginate($pageSize, false, ['page' => $page]);
|
||||||
|
|
||||||
|
return ResponseHelper::success($list, '获取成功');
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ResponseHelper::error('系统异常:' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加知识库
|
||||||
|
*
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function add()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
|
||||||
|
if (empty($companyId)) {
|
||||||
|
return ResponseHelper::error('公司信息获取失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
$datasetId = AiSettingsModel::where(['companyId' => $companyId])->value('datasetId');
|
||||||
|
|
||||||
|
|
||||||
|
// 获取参数
|
||||||
|
$typeId = $this->request->param('typeId', 0);
|
||||||
|
$name = $this->request->param('name', '');
|
||||||
|
$label = $this->request->param('label', []);
|
||||||
|
$fileUrl = $this->request->param('fileUrl', '');
|
||||||
|
|
||||||
|
// 参数验证
|
||||||
|
if (empty($typeId)) {
|
||||||
|
return ResponseHelper::error('请选择知识库类型');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($name)) {
|
||||||
|
return ResponseHelper::error('知识库名称不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($fileUrl)) {
|
||||||
|
return ResponseHelper::error('文件地址不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查类型是否存在
|
||||||
|
$typeExists = AiKnowledgeBaseType::where([
|
||||||
|
['id', '=', $typeId],
|
||||||
|
['isDel', '=', 0]
|
||||||
|
])->find();
|
||||||
|
|
||||||
|
if (!$typeExists) {
|
||||||
|
return ResponseHelper::error('知识库类型不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建知识库
|
||||||
|
$knowledgeModel = new AiKnowledgeBase();
|
||||||
|
$data = [
|
||||||
|
'typeId' => $typeId,
|
||||||
|
'name' => $name,
|
||||||
|
'label' => json_encode($label, 256),
|
||||||
|
'fileUrl' => $fileUrl,
|
||||||
|
'companyId' => $companyId,
|
||||||
|
'userId' => $userId,
|
||||||
|
'createTime' => time(),
|
||||||
|
'updateTime' => time(),
|
||||||
|
'isDel' => 0
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($knowledgeModel->save($data)) {
|
||||||
|
if (!empty($datasetId)) {
|
||||||
|
$createDocumentData = [
|
||||||
|
'filePath' => $fileUrl,
|
||||||
|
'fileName' => $name,
|
||||||
|
'dataset_id' => $datasetId
|
||||||
|
];
|
||||||
|
$cozeAI = new CozeAI();
|
||||||
|
$result = $cozeAI->createDocument($createDocumentData);
|
||||||
|
$result = json_decode($result, true);
|
||||||
|
if ($result['code'] == 200) {
|
||||||
|
$documentId = $result['data'][0]['document_id'];
|
||||||
|
AiKnowledgeBase::where('id', $knowledgeModel->id)->update(['documentId' => $documentId, 'updateTime' => time()]);
|
||||||
|
AiSettingsModel::where(['companyId' => $companyId])->update(['isRelease' => 0,'updateTime' => time()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ResponseHelper::success(['id' => $knowledgeModel->id], '添加成功');
|
||||||
|
} else {
|
||||||
|
return ResponseHelper::error('添加失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ResponseHelper::error('系统异常:' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑知识库
|
||||||
|
*
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function edit()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
if (empty($companyId)) {
|
||||||
|
return ResponseHelper::error('公司信息获取失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取参数
|
||||||
|
$id = $this->request->param('id', 0);
|
||||||
|
$typeId = $this->request->param('typeId', 0);
|
||||||
|
$name = $this->request->param('name', '');
|
||||||
|
$label = $this->request->param('label', []);
|
||||||
|
$fileUrl = $this->request->param('fileUrl', '');
|
||||||
|
|
||||||
|
// 参数验证
|
||||||
|
if (empty($id)) {
|
||||||
|
return ResponseHelper::error('知识库ID不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($typeId)) {
|
||||||
|
return ResponseHelper::error('请选择知识库类型');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($name)) {
|
||||||
|
return ResponseHelper::error('知识库名称不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找知识库
|
||||||
|
$knowledgeModel = AiKnowledgeBase::where([
|
||||||
|
['id', '=', $id],
|
||||||
|
['companyId', '=', $companyId],
|
||||||
|
['isDel', '=', 0]
|
||||||
|
])->find();
|
||||||
|
|
||||||
|
if (!$knowledgeModel) {
|
||||||
|
return ResponseHelper::error('知识库不存在或无权限编辑');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查类型是否存在
|
||||||
|
$typeExists = AiKnowledgeBaseType::where([
|
||||||
|
['id', '=', $typeId],
|
||||||
|
['isDel', '=', 0]
|
||||||
|
])->find();
|
||||||
|
|
||||||
|
if (!$typeExists) {
|
||||||
|
return ResponseHelper::error('知识库类型不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新数据
|
||||||
|
$knowledgeModel->typeId = $typeId;
|
||||||
|
$knowledgeModel->name = $name;
|
||||||
|
$knowledgeModel->label = json_encode($label, 256);
|
||||||
|
if (!empty($fileUrl)) {
|
||||||
|
$knowledgeModel->fileUrl = $fileUrl;
|
||||||
|
}
|
||||||
|
$knowledgeModel->updateTime = time();
|
||||||
|
|
||||||
|
if ($knowledgeModel->save()) {
|
||||||
|
return ResponseHelper::success([], '更新成功');
|
||||||
|
} else {
|
||||||
|
return ResponseHelper::error('更新失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ResponseHelper::error('系统异常:' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除知识库
|
||||||
|
*
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function delete()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
if (empty($companyId)) {
|
||||||
|
return ResponseHelper::error('公司信息获取失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取参数
|
||||||
|
$id = $this->request->param('id', 0);
|
||||||
|
|
||||||
|
// 参数验证
|
||||||
|
if (empty($id)) {
|
||||||
|
return ResponseHelper::error('知识库ID不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找知识库
|
||||||
|
$knowledgeModel = AiKnowledgeBase::where([
|
||||||
|
['id', '=', $id],
|
||||||
|
['companyId', '=', $companyId],
|
||||||
|
['isDel', '=', 0]
|
||||||
|
])->find();
|
||||||
|
|
||||||
|
if (!$knowledgeModel) {
|
||||||
|
return ResponseHelper::error('知识库不存在或无权限删除');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 软删除
|
||||||
|
$knowledgeModel->isDel = 1;
|
||||||
|
$knowledgeModel->delTime = time();
|
||||||
|
|
||||||
|
if ($knowledgeModel->save()) {
|
||||||
|
if (!empty($knowledgeModel->documentId)){
|
||||||
|
$cozeAI = new CozeAI();
|
||||||
|
$cozeAI->deleteDocument([$knowledgeModel->documentId]);
|
||||||
|
AiSettingsModel::where(['companyId' => $companyId])->update(['isRelease' => 0,'updateTime' => time()]);
|
||||||
|
}
|
||||||
|
return ResponseHelper::success([], '删除成功');
|
||||||
|
} else {
|
||||||
|
return ResponseHelper::error('删除失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ResponseHelper::error('系统异常:' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取知识库详情
|
||||||
|
*
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function detail()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
if (empty($companyId)) {
|
||||||
|
return ResponseHelper::error('公司信息获取失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取参数
|
||||||
|
$id = $this->request->param('id', 0);
|
||||||
|
|
||||||
|
// 参数验证
|
||||||
|
if (empty($id)) {
|
||||||
|
return ResponseHelper::error('知识库ID不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找知识库
|
||||||
|
$knowledge = AiKnowledgeBase::where([
|
||||||
|
['id', '=', $id],
|
||||||
|
['companyId', '=', $companyId],
|
||||||
|
['isDel', '=', 0]
|
||||||
|
])->with(['type'])->find();
|
||||||
|
|
||||||
|
if (!$knowledge) {
|
||||||
|
return ResponseHelper::error('知识库不存在或无权限查看');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseHelper::success($knowledge, '获取成功');
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ResponseHelper::error('系统异常:' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
344
Server/application/cunkebao/controller/AiSettingsController.php
Normal file
344
Server/application/cunkebao/controller/AiSettingsController.php
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\cunkebao\controller;
|
||||||
|
|
||||||
|
use app\ai\controller\CozeAI;
|
||||||
|
use app\api\model\CompanyModel;
|
||||||
|
use app\chukebao\model\AiSettings as AiSettingsModel;
|
||||||
|
use library\ResponseHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI设置控制器
|
||||||
|
* 负责管理公司的AI智能体配置,包括创建智能体、知识库等
|
||||||
|
*/
|
||||||
|
class AiSettingsController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 初始化AI设置
|
||||||
|
* 检查公司是否已有AI配置,如果没有则创建默认配置
|
||||||
|
* 自动创建智能体和知识库
|
||||||
|
*
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function init()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// 获取当前用户信息
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
|
||||||
|
if (empty($companyId)) {
|
||||||
|
return ResponseHelper::error('公司信息获取失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找公司AI设置
|
||||||
|
$settings = $this->getOrCreateAiSettings($companyId, $userId);
|
||||||
|
|
||||||
|
if (!$settings) {
|
||||||
|
return ResponseHelper::error('AI设置初始化失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保智能体已创建
|
||||||
|
if (empty($settings->botId)) {
|
||||||
|
$botCreated = $this->createBot($settings);
|
||||||
|
if (!$botCreated) {
|
||||||
|
return ResponseHelper::error('智能体创建失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保知识库已创建
|
||||||
|
if (empty($settings->datasetId)) {
|
||||||
|
$knowledgeCreated = $this->createKnowledge($settings);
|
||||||
|
if (!$knowledgeCreated) {
|
||||||
|
return ResponseHelper::error('知识库创建失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!empty($settings->botId) && !empty($settings->datasetId)) {
|
||||||
|
$cozeAI = new CozeAI();
|
||||||
|
$config = json_decode($settings->config,true);
|
||||||
|
$config['bot_id'] = $settings->botId;
|
||||||
|
$config['dataset_ids'] = [$settings->datasetId];
|
||||||
|
$cozeAI->updateBot($config);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析配置信息
|
||||||
|
$settings->config = json_decode($settings->config, true);
|
||||||
|
|
||||||
|
return ResponseHelper::success($settings, 'AI设置初始化成功');
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ResponseHelper::error('系统异常:' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取或创建AI设置
|
||||||
|
*
|
||||||
|
* @param int $companyId 公司ID
|
||||||
|
* @param int $userId 用户ID
|
||||||
|
* @return AiSettingsModel|false
|
||||||
|
*/
|
||||||
|
private function getOrCreateAiSettings($companyId, $userId)
|
||||||
|
{
|
||||||
|
// 查找现有设置
|
||||||
|
$settings = AiSettingsModel::where(['companyId' => $companyId])->find();
|
||||||
|
|
||||||
|
if (empty($settings)) {
|
||||||
|
// 获取公司信息
|
||||||
|
$company = CompanyModel::where('id', $companyId)->find();
|
||||||
|
if (empty($company)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建默认配置
|
||||||
|
$config = $this->getDefaultConfig($company['name']);
|
||||||
|
|
||||||
|
// 保存AI设置
|
||||||
|
$settings = $this->saveAiSettings($companyId, $userId, $config);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取默认AI配置
|
||||||
|
*
|
||||||
|
* @param string $companyName 公司名称
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function getDefaultConfig($companyName)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => $companyName,
|
||||||
|
'model_id' => '1737521813', // 默认模型ID
|
||||||
|
'prompt_info' => $this->getDefaultPrompt()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取默认提示词
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function getDefaultPrompt()
|
||||||
|
{
|
||||||
|
return '# 角色
|
||||||
|
你是一位全能知识客服,作为专业的客服智能体,具备全面的知识储备,能够回答用户提出的各类问题。在回答问题前,会仔细查阅知识库内容,并且始终严格遵守中国法律法规。
|
||||||
|
|
||||||
|
## 技能
|
||||||
|
### 技能 1: 回答用户问题
|
||||||
|
1. 当用户提出问题时,首先在知识库中进行搜索查找相关信息。
|
||||||
|
2. 依据知识库中的内容,为用户提供准确、清晰、完整的回答。
|
||||||
|
|
||||||
|
## 限制
|
||||||
|
- 仅依据知识库内容回答问题,对于知识库中没有的信息,如实告知用户无法回答。
|
||||||
|
- 回答必须严格遵循中国法律法规,不得出现任何违法违规内容。
|
||||||
|
- 回答需简洁明了,避免冗长复杂的表述。';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存AI设置到数据库
|
||||||
|
*
|
||||||
|
* @param int $companyId 公司ID
|
||||||
|
* @param int $userId 用户ID
|
||||||
|
* @param array $config 配置信息
|
||||||
|
* @return AiSettingsModel|false
|
||||||
|
*/
|
||||||
|
private function saveAiSettings($companyId, $userId, $config)
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
'companyId' => $companyId,
|
||||||
|
'userId' => $userId,
|
||||||
|
'config' => json_encode($config, JSON_UNESCAPED_UNICODE),
|
||||||
|
'createTime' => time(),
|
||||||
|
'updateTime' => time(),
|
||||||
|
'botId' => 0,
|
||||||
|
'datasetId' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
$aiSettingsModel = new AiSettingsModel();
|
||||||
|
$result = $aiSettingsModel->save($data);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
return AiSettingsModel::where(['companyId' => $companyId])->find();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建AI智能体
|
||||||
|
*
|
||||||
|
* @param AiSettingsModel $settings AI设置对象
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function createBot($settings)
|
||||||
|
{
|
||||||
|
if (empty($settings)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$config = json_decode($settings->config, true);
|
||||||
|
if (empty($config)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用CozeAI创建智能体
|
||||||
|
$cozeAI = new CozeAI();
|
||||||
|
$result = $cozeAI->createBot($config);
|
||||||
|
$result = json_decode($result, true);
|
||||||
|
|
||||||
|
if ($result['code'] != 200) {
|
||||||
|
\think\facade\Log::error('智能体创建失败:' . ($result['msg'] ?? '未知错误'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新智能体ID
|
||||||
|
$settings->botId = $result['data']['bot_id'];
|
||||||
|
$settings->updateTime = time();
|
||||||
|
|
||||||
|
return $settings->save();
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
\think\facade\Log::error('创建智能体异常:' . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建知识库
|
||||||
|
*
|
||||||
|
* @param AiSettingsModel $settings AI设置对象
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function createKnowledge($settings)
|
||||||
|
{
|
||||||
|
if (empty($settings)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$config = json_decode($settings->config, true);
|
||||||
|
if (empty($config)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用CozeAI创建知识库
|
||||||
|
$cozeAI = new CozeAI();
|
||||||
|
$result = $cozeAI->createKnowledge(['name' => $config['name']]);
|
||||||
|
$result = json_decode($result, true);
|
||||||
|
|
||||||
|
if ($result['code'] != 200) {
|
||||||
|
\think\facade\Log::error('知识库创建失败:' . ($result['msg'] ?? '未知错误'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新知识库ID
|
||||||
|
$settings->datasetId = $result['data']['dataset_id'];
|
||||||
|
$settings->updateTime = time();
|
||||||
|
|
||||||
|
return $settings->save();
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
\think\facade\Log::error('创建知识库异常:' . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新AI配置
|
||||||
|
*
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function updateConfig()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
if (empty($companyId)) {
|
||||||
|
return ResponseHelper::error('公司信息获取失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取请求参数
|
||||||
|
$config = $this->request->param('config', []);
|
||||||
|
if (empty($config)) {
|
||||||
|
return ResponseHelper::error('配置参数不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找现有设置
|
||||||
|
$settings = AiSettingsModel::where(['companyId' => $companyId])->find();
|
||||||
|
if (empty($settings)) {
|
||||||
|
return ResponseHelper::error('AI设置不存在,请先初始化');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新配置
|
||||||
|
$settings->config = json_encode($config, JSON_UNESCAPED_UNICODE);
|
||||||
|
$settings->updateTime = time();
|
||||||
|
|
||||||
|
if ($settings->save()) {
|
||||||
|
return ResponseHelper::success([], '配置更新成功');
|
||||||
|
} else {
|
||||||
|
return ResponseHelper::error('配置更新失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ResponseHelper::error('系统异常:' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取AI设置详情
|
||||||
|
*
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function getSettings()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
if (empty($companyId)) {
|
||||||
|
return ResponseHelper::error('公司信息获取失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
$settings = AiSettingsModel::where(['companyId' => $companyId])->find();
|
||||||
|
if (empty($settings)) {
|
||||||
|
return ResponseHelper::error('AI设置不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析配置信息
|
||||||
|
$settings->config = json_decode($settings->config, true);
|
||||||
|
|
||||||
|
return ResponseHelper::success($settings, '获取成功');
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ResponseHelper::error('系统异常:' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布智能体
|
||||||
|
* @return \think\response\Json
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function release()
|
||||||
|
{
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
$settings = AiSettingsModel::where(['companyId' => $companyId])->find();
|
||||||
|
if (!empty($settings->isRelease)) {
|
||||||
|
return ResponseHelper::success('', '已发布,无需重复发布');
|
||||||
|
}
|
||||||
|
|
||||||
|
$cozeAI = new CozeAI();
|
||||||
|
$res = $cozeAI->botPublish(['bot_id' => $settings->botId]);
|
||||||
|
$res = json_decode($res, true);
|
||||||
|
|
||||||
|
if ($res['code'] != 200) {
|
||||||
|
$msg = '发布失败失败:' . ($res['msg'] ?? '未知错误');
|
||||||
|
return ResponseHelper::error($msg);
|
||||||
|
}
|
||||||
|
$settings->isRelease = 1;
|
||||||
|
$settings->releaseTime = time();
|
||||||
|
$settings->save();
|
||||||
|
return ResponseHelper::success('', '发布成功');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -247,6 +247,7 @@ class ContentLibraryController extends Controller
|
|||||||
$friendsInfo = Db::name('wechat_friendship')->alias('wf')
|
$friendsInfo = Db::name('wechat_friendship')->alias('wf')
|
||||||
->field('wf.id,wf.wechatId, wa.nickname, wa.avatar')
|
->field('wf.id,wf.wechatId, wa.nickname, wa.avatar')
|
||||||
->join('wechat_account wa', 'wf.wechatId = wa.wechatId')
|
->join('wechat_account wa', 'wf.wechatId = wa.wechatId')
|
||||||
|
->order('wa.id DESC')
|
||||||
->whereIn('wf.id', $friendIds)
|
->whereIn('wf.id', $friendIds)
|
||||||
->select();
|
->select();
|
||||||
}
|
}
|
||||||
|
|||||||
55
Server/application/cunkebao/controller/RFMController.php
Normal file
55
Server/application/cunkebao/controller/RFMController.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\cunkebao\controller;
|
||||||
|
|
||||||
|
class RFMController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 计算 RFM 评分(默认规则)
|
||||||
|
* @param int|null $recencyDays 最近购买天数
|
||||||
|
* @param int $frequency 购买次数
|
||||||
|
* @param float $monetary 购买金额
|
||||||
|
* @return array{R:int,F:int,M:int}
|
||||||
|
*/
|
||||||
|
public static function calcRfmScores($recencyDays = 30, $frequency, $monetary)
|
||||||
|
{
|
||||||
|
$recencyDays = is_numeric($recencyDays) ? (int)$recencyDays : 9999;
|
||||||
|
$frequency = max(0, (int)$frequency);
|
||||||
|
$monetary = max(0, (float)$monetary);
|
||||||
|
return [
|
||||||
|
'R' => self::scoreR_Default($recencyDays),
|
||||||
|
'F' => self::scoreF_Default($frequency),
|
||||||
|
'M' => self::scoreM_Default($monetary),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认规则
|
||||||
|
protected static function scoreR_Default(int $days): int
|
||||||
|
{
|
||||||
|
if ($days <= 30) return 5;
|
||||||
|
if ($days <= 60) return 4;
|
||||||
|
if ($days <= 90) return 3;
|
||||||
|
if ($days <= 120) return 2;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
protected static function scoreF_Default(int $times): int
|
||||||
|
{
|
||||||
|
if ($times >= 10) return 5;
|
||||||
|
if ($times >= 6) return 4;
|
||||||
|
if ($times >= 3) return 3;
|
||||||
|
if ($times >= 2) return 2;
|
||||||
|
if ($times >= 1) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
protected static function scoreM_Default(float $amount): int
|
||||||
|
{
|
||||||
|
if ($amount >= 2000) return 5;
|
||||||
|
if ($amount >= 1000) return 4;
|
||||||
|
if ($amount >= 500) return 3;
|
||||||
|
if ($amount >= 200) return 2;
|
||||||
|
if ($amount > 0) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
321
Server/application/cunkebao/controller/TrafficController.php
Normal file
321
Server/application/cunkebao/controller/TrafficController.php
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\cunkebao\controller;
|
||||||
|
|
||||||
|
use app\common\model\TrafficSourcePackage;
|
||||||
|
use app\common\model\TrafficSourcePackageItem;
|
||||||
|
use library\ResponseHelper;
|
||||||
|
use think\Db;
|
||||||
|
use app\cunkebao\controller\RFMController;
|
||||||
|
|
||||||
|
class TrafficController extends BaseController
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流量池包
|
||||||
|
* @return \think\response\Json
|
||||||
|
* @throws \think\db\exception\DataNotFoundException
|
||||||
|
* @throws \think\db\exception\ModelNotFoundException
|
||||||
|
* @throws \think\exception\DbException
|
||||||
|
*/
|
||||||
|
public function getPackage()
|
||||||
|
{
|
||||||
|
$page = $this->request->param('page', 1);
|
||||||
|
$limit = $this->request->param('limit', 10);
|
||||||
|
$keyword = $this->request->param('keyword', '');
|
||||||
|
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
$package = Db::name('traffic_source_package')->alias('tsp')
|
||||||
|
->join('traffic_source_package_item tspi', 'tspi.packageId=tsp.id', 'left')
|
||||||
|
->whereIn('tsp.companyId', [$companyId, 0])
|
||||||
|
->field('tsp.id,tsp.name,tsp.description,tsp.pic,tsp.isSys as type,tsp.createTime,count(tspi.id) as num')
|
||||||
|
->group('tsp.id');
|
||||||
|
|
||||||
|
if (!empty($keyword)) {
|
||||||
|
$package->where('tsp.name|tsp.description', 'like', '%' . $keyword . '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
$list = $package->page($page, $limit)->order('isSys ASC,id DESC')->select();
|
||||||
|
$total = $package->count();
|
||||||
|
|
||||||
|
$rfmRule = 'default';
|
||||||
|
foreach ($list as $k => &$v) {
|
||||||
|
if ($v['type'] != 1) {
|
||||||
|
$v['createTime'] = !empty($v['createTime']) ? formatRelativeTime($v['createTime']) : '';
|
||||||
|
} else {
|
||||||
|
$v['createTime'] = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// RFM 评分(示例:以创建时间近似最近活跃,num 近似频次;金额若无则为 0)
|
||||||
|
$recencyDays = isset($v['createTime']) && is_numeric($v['createTime']) ? floor((time() - (int)$v['createTime']) / 86400) : null;
|
||||||
|
// 如果上方被格式化为文本,则尝试从原始结果集取原值
|
||||||
|
if (!is_numeric($recencyDays) || $recencyDays === null) {
|
||||||
|
$rawCreate = isset($list[$k]['createTime']) ? $list[$k]['createTime'] : null;
|
||||||
|
$recencyDays = is_numeric($rawCreate) ? floor((time() - (int)$rawCreate) / 86400) : 9999;
|
||||||
|
}
|
||||||
|
$frequency = (int)($v['num'] ?? 0);
|
||||||
|
$monetary = (float)($v['monetary'] ?? 0);
|
||||||
|
|
||||||
|
$scores = RFMController::calcRfmScores($recencyDays, $frequency, $monetary);
|
||||||
|
$v['R'] = $scores['R'];
|
||||||
|
$v['F'] = $scores['F'];
|
||||||
|
$v['M'] = $scores['M'];
|
||||||
|
$v['RFM'] = $scores['R'] + $scores['F'] + $scores['M'];
|
||||||
|
}
|
||||||
|
unset($v);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'total' => $total,
|
||||||
|
'list' => $list,
|
||||||
|
];
|
||||||
|
|
||||||
|
return ResponseHelper::success($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加流量池
|
||||||
|
* @return \think\response\Json
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function addPackage()
|
||||||
|
{
|
||||||
|
$packageName = $this->request->param('packageName', '');
|
||||||
|
$description = $this->request->param('description', '');
|
||||||
|
$pic = $this->request->param('pic', '');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
|
||||||
|
if (empty($packageName)) {
|
||||||
|
return ResponseHelper::error('流量池名称不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
$package = TrafficSourcePackage::where(['isDel' => 0, 'name' => $packageName])
|
||||||
|
->whereIn('companyId', [$companyId, 0])
|
||||||
|
->field('id,name')
|
||||||
|
->find();
|
||||||
|
if (!empty($package)) {
|
||||||
|
return ResponseHelper::error('该流量池名称已存在');
|
||||||
|
}
|
||||||
|
$packageId = TrafficSourcePackage::insertGetId([
|
||||||
|
'userId' => $userId,
|
||||||
|
'companyId' => $companyId,
|
||||||
|
'name' => $packageName,
|
||||||
|
'description' => $description,
|
||||||
|
'pic' => $pic,
|
||||||
|
'matchingRules' => json_encode([]),
|
||||||
|
'createTime' => time(),
|
||||||
|
'isDel' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!empty($packageId)) {
|
||||||
|
return ResponseHelper::success($packageId, '该流量添加成功');
|
||||||
|
} else {
|
||||||
|
return ResponseHelper::error('该流量添加失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑流量池
|
||||||
|
* @return \think\response\Json
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function editPackage()
|
||||||
|
{
|
||||||
|
$packageId = $this->request->param('packageId', '');
|
||||||
|
$packageName = $this->request->param('packageName', '');
|
||||||
|
$description = $this->request->param('description', '');
|
||||||
|
$pic = $this->request->param('pic', '');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
|
||||||
|
if (empty($packageId)) {
|
||||||
|
return ResponseHelper::error('流量池ID不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($packageName)) {
|
||||||
|
return ResponseHelper::error('流量池名称不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查流量池是否存在且属于当前公司
|
||||||
|
$package = TrafficSourcePackage::where(['id' => $packageId, 'isDel' => 0])
|
||||||
|
->whereIn('companyId', [$companyId, 0])
|
||||||
|
->find();
|
||||||
|
if (empty($package)) {
|
||||||
|
return ResponseHelper::error('流量池不存在或已删除');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查系统流量池是否可编辑
|
||||||
|
if ($package['isSys'] == 1) {
|
||||||
|
return ResponseHelper::error('系统流量池不允许编辑');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查名称是否重复(排除当前记录)
|
||||||
|
$existPackage = TrafficSourcePackage::where(['isDel' => 0, 'name' => $packageName])
|
||||||
|
->whereIn('companyId', [$companyId, 0])
|
||||||
|
->where('id', '<>', $packageId)
|
||||||
|
->field('id,name')
|
||||||
|
->find();
|
||||||
|
if (!empty($existPackage)) {
|
||||||
|
return ResponseHelper::error('该流量池名称已存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新流量池信息
|
||||||
|
$updateData = [
|
||||||
|
'name' => $packageName,
|
||||||
|
'updateTime' => time(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// 更新描述字段(允许为空)
|
||||||
|
$updateData['description'] = $description;
|
||||||
|
|
||||||
|
// 更新图片字段(允许为空)
|
||||||
|
$updateData['pic'] = $pic;
|
||||||
|
|
||||||
|
$result = TrafficSourcePackage::where('id', $packageId)->update($updateData);
|
||||||
|
|
||||||
|
if ($result !== false) {
|
||||||
|
return ResponseHelper::success($packageId, '流量池编辑成功');
|
||||||
|
} else {
|
||||||
|
return ResponseHelper::error('流量池编辑失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除流量池(假删除)
|
||||||
|
* @return \think\response\Json
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function deletePackage()
|
||||||
|
{
|
||||||
|
$packageId = $this->request->param('packageId', '');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
|
||||||
|
if (empty($packageId)) {
|
||||||
|
return ResponseHelper::error('流量池ID不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查流量池是否存在且属于当前公司
|
||||||
|
$package = TrafficSourcePackage::where(['id' => $packageId, 'isDel' => 0])
|
||||||
|
->whereIn('companyId', [$companyId, 0])
|
||||||
|
->find();
|
||||||
|
if (empty($package)) {
|
||||||
|
return ResponseHelper::error('流量池不存在或已删除');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查系统流量池是否可删除
|
||||||
|
if ($package['isSys'] == 1) {
|
||||||
|
return ResponseHelper::error('系统流量池不允许删除');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开启事务
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
// 执行流量池假删除
|
||||||
|
$result = TrafficSourcePackage::where('id', $packageId)->update([
|
||||||
|
'isDel' => 1,
|
||||||
|
'deleteTime' => time()
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($result === false) {
|
||||||
|
throw new \Exception('流量池删除失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除流量池内容(TrafficSourcePackageItem)假删除
|
||||||
|
$itemResult = TrafficSourcePackageItem::where([
|
||||||
|
'packageId' => $packageId,
|
||||||
|
'companyId' => $companyId,
|
||||||
|
'isDel' => 0
|
||||||
|
])->update([
|
||||||
|
'isDel' => 1,
|
||||||
|
'deleteTime' => time()
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 提交事务
|
||||||
|
Db::commit();
|
||||||
|
|
||||||
|
return ResponseHelper::success($packageId, '流量池及内容删除成功');
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// 回滚事务
|
||||||
|
Db::rollback();
|
||||||
|
return ResponseHelper::error('删除失败:' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流量池列表
|
||||||
|
* @return \think\response\Json
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function getTrafficPoolList()
|
||||||
|
{
|
||||||
|
$page = $this->request->param('page', 1);
|
||||||
|
$limit = $this->request->param('limit', 10);
|
||||||
|
$keyword = $this->request->param('keyword', '');
|
||||||
|
$packageId = $this->request->param('packageId', '');
|
||||||
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
$userId = $this->getUserInfo('id');
|
||||||
|
|
||||||
|
if (empty($packageId)) {
|
||||||
|
return ResponseHelper::error('流量包id不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
$trafficSourcePackage = TrafficSourcePackage::where(['id' => $packageId, 'isDel' => 0])->whereIn('companyId', [$companyId, 0])->find();
|
||||||
|
if (empty($trafficSourcePackage)) {
|
||||||
|
return ResponseHelper::error('流量包不存在或已删除');
|
||||||
|
}
|
||||||
|
$where = [
|
||||||
|
['tspi.companyId', '=', $companyId],
|
||||||
|
['tspi.packageId', '=', $packageId],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (empty($keyword)) {
|
||||||
|
$where[] = ['wa.nickname|wa.phone|wa.alias|wa.wechatId|p.mobile|p.identifier', 'like', '%' . $keyword . '%'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = TrafficSourcePackageItem::alias('tspi')
|
||||||
|
->field(
|
||||||
|
[
|
||||||
|
'p.id', 'p.identifier', 'p.mobile', 'p.wechatId', 'tspi.companyId',
|
||||||
|
'wa.nickname', 'wa.avatar', 'wa.gender', 'wa.phone', 'wa.alias'
|
||||||
|
]
|
||||||
|
)
|
||||||
|
->join('traffic_pool p', 'p.identifier=tspi.identifier', 'left')
|
||||||
|
->join('wechat_account wa', 'tspi.identifier=wa.wechatId', 'left')
|
||||||
|
->where($where);
|
||||||
|
|
||||||
|
$query->order('tspi.id DESC,p.id DESC')->group('p.identifier');
|
||||||
|
|
||||||
|
$list = $query->page($page, $limit)->select()->toArray();
|
||||||
|
$total = $query->count();
|
||||||
|
|
||||||
|
foreach ($list as $k => &$v) {
|
||||||
|
//流量池筛选
|
||||||
|
$package = TrafficSourcePackageItem::alias('tspi')
|
||||||
|
->join('traffic_source_package p', 'tspi.packageId=p.id AND tspi.companyId=p.companyId')
|
||||||
|
->where(['tspi.identifier' => $v['identifier']])
|
||||||
|
->whereIn('tspi.companyId', [0, $v['companyId']])
|
||||||
|
->column('p.name');
|
||||||
|
$v['packages'] = $package;
|
||||||
|
$v['phone'] = !empty($v['phone']) ? $v['phone'] : $v['mobile'];
|
||||||
|
unset($v['mobile']);
|
||||||
|
|
||||||
|
|
||||||
|
$scores = RFMController::calcRfmScores(30, 30, 30);
|
||||||
|
$v['R'] = $scores['R'];
|
||||||
|
$v['F'] = $scores['F'];
|
||||||
|
$v['M'] = $scores['M'];
|
||||||
|
$v['RFM'] = $scores['R'] + $scores['F'] + $scores['M'];
|
||||||
|
$v['money'] = 2222;
|
||||||
|
$v['msgCount'] = 2222;
|
||||||
|
$v['tag'] = ['test', 'test2'];
|
||||||
|
}
|
||||||
|
unset($v);
|
||||||
|
|
||||||
|
|
||||||
|
$data = ['list' => $list, 'total' => $total];
|
||||||
|
return ResponseHelper::success($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -22,19 +22,19 @@ class GetChatroomListV1Controller extends BaseController
|
|||||||
$keyword = $this->request->param('keyword', '');
|
$keyword = $this->request->param('keyword', '');
|
||||||
try {
|
try {
|
||||||
|
|
||||||
/*$companyId = (int)$this->getUserInfo('companyId');
|
$companyId = (int)$this->getUserInfo('companyId');
|
||||||
$wechatIds = Db::name('device')->alias('d')
|
$wechatIds = Db::name('device')->alias('d')
|
||||||
// 仅关联每个设备在 device_wechat_login 中的最新一条记录
|
// 仅关联每个设备在 device_wechat_login 中的最新一条记录
|
||||||
->join('(SELECT MAX(id) AS id, deviceId FROM ck_device_wechat_login WHERE companyId='.$companyId.' GROUP BY deviceId) dwl_max','dwl_max.deviceId = d.id')
|
->join('(SELECT MAX(id) AS id, deviceId FROM ck_device_wechat_login WHERE companyId='.$companyId.' GROUP BY deviceId) dwl_max','dwl_max.deviceId = d.id')
|
||||||
->join('device_wechat_login dwl','dwl.id = dwl_max.id')
|
->join('device_wechat_login dwl','dwl.id = dwl_max.id')
|
||||||
->where(['d.companyId' => $companyId,'d.deleteTime' => 0])
|
->where(['d.companyId' => $companyId,'d.deleteTime' => 0])
|
||||||
->column('dwl.wechatId');*/
|
->column('dwl.wechatId');
|
||||||
|
|
||||||
|
|
||||||
$wechatIds = Db::name('device')->alias('d')
|
/* $wechatIds = Db::name('device')->alias('d')
|
||||||
->join('device_wechat_login dwl','dwl.deviceId=d.id AND dwl.companyId='.$this->getUserInfo('companyId'))
|
->join('device_wechat_login dwl','dwl.deviceId=d.id AND dwl.companyId='.$this->getUserInfo('companyId'))
|
||||||
->where(['d.companyId' => $this->getUserInfo('companyId'),'d.deleteTime' => 0])
|
->where(['d.companyId' => $this->getUserInfo('companyId'),'d.deleteTime' => 0])
|
||||||
->column('dwl.wechatId');
|
->column('dwl.wechatId');*/
|
||||||
|
|
||||||
|
|
||||||
$where = [];
|
$where = [];
|
||||||
|
|||||||
@@ -42,16 +42,20 @@ class GetFriendListV1Controller extends BaseController
|
|||||||
$where[] = ['nickname|alias|wechatId','like','%'.$keyword.'%'];
|
$where[] = ['nickname|alias|wechatId','like','%'.$keyword.'%'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$wechatIds = Db::name('device')->alias('d')
|
/* $wechatIds = Db::name('device')->alias('d')
|
||||||
->join('device_wechat_login dwl','dwl.deviceId=d.id AND dwl.companyId='.$this->getUserInfo('companyId'))
|
->join('device_wechat_login dwl','dwl.deviceId=d.id AND dwl.companyId='.$this->getUserInfo('companyId'))
|
||||||
->where(['d.companyId' => $this->getUserInfo('companyId'),'d.deleteTime' => 0])->group('dwl.deviceId')->order('dwl.id desc');
|
->where(['d.companyId' => $this->getUserInfo('companyId'),'d.deleteTime' => 0])
|
||||||
|
->group('dwl.deviceId')
|
||||||
|
->order('dwl.id desc');*/
|
||||||
|
|
||||||
|
|
||||||
/*$wechatIds = Db::name('device')->alias('d')
|
$companyId = $this->getUserInfo('companyId');
|
||||||
|
|
||||||
|
$wechatIds = Db::name('device')->alias('d')
|
||||||
// 仅关联每个设备在 device_wechat_login 中的最新一条记录
|
// 仅关联每个设备在 device_wechat_login 中的最新一条记录
|
||||||
->join('(SELECT MAX(id) AS id, deviceId FROM ck_device_wechat_login WHERE companyId='.$companyId.' GROUP BY deviceId) dwl_max','dwl_max.deviceId = d.id')
|
->join('(SELECT MAX(id) AS id, deviceId FROM ck_device_wechat_login WHERE companyId='.$companyId.' GROUP BY deviceId) dwl_max','dwl_max.deviceId = d.id')
|
||||||
->join('device_wechat_login dwl','dwl.id = dwl_max.id')
|
->join('device_wechat_login dwl','dwl.id = dwl_max.id')
|
||||||
->where(['d.companyId' => $companyId,'d.deleteTime' => 0]);*/
|
->where(['d.companyId' => $companyId,'d.deleteTime' => 0]);
|
||||||
|
|
||||||
|
|
||||||
if (!empty($deviceIds)){
|
if (!empty($deviceIds)){
|
||||||
@@ -59,7 +63,6 @@ class GetFriendListV1Controller extends BaseController
|
|||||||
}
|
}
|
||||||
$wechatIds = $wechatIds->column('dwl.wechatId');
|
$wechatIds = $wechatIds->column('dwl.wechatId');
|
||||||
|
|
||||||
|
|
||||||
$where[] = ['ownerWechatId','in',$wechatIds];
|
$where[] = ['ownerWechatId','in',$wechatIds];
|
||||||
|
|
||||||
$data = Db::table('s2_wechat_friend')
|
$data = Db::table('s2_wechat_friend')
|
||||||
|
|||||||
@@ -458,7 +458,7 @@ class PlanSceneV1Controller extends BaseController
|
|||||||
$total = $query->count();
|
$total = $query->count();
|
||||||
$list = $query->page($page, $pageSize)->order('id', 'desc')->select();
|
$list = $query->page($page, $pageSize)->order('id', 'desc')->select();
|
||||||
foreach ($list as &$item) {
|
foreach ($list as &$item) {
|
||||||
unset($item['fail_reason'],$item['processed_wechat_ids'],$item['task_id']);
|
unset($item['processed_wechat_ids'],$item['task_id']);
|
||||||
$userinfo = Db::table('s2_wechat_friend')
|
$userinfo = Db::table('s2_wechat_friend')
|
||||||
->field('alias,wechatId,nickname,avatar')
|
->field('alias,wechatId,nickname,avatar')
|
||||||
->where('alias|wechatId|phone|conRemark','like','%'.$item['phone'].'%')
|
->where('alias|wechatId|phone|conRemark','like','%'.$item['phone'].'%')
|
||||||
|
|||||||
@@ -223,7 +223,6 @@ class PostCreateAddFriendPlanV1Controller extends BaseController
|
|||||||
if (!empty($params['wechatGroups']) && is_array($params['wechatGroups'])) {
|
if (!empty($params['wechatGroups']) && is_array($params['wechatGroups'])) {
|
||||||
$rows = Db::name('wechat_group_member')->alias('gm')
|
$rows = Db::name('wechat_group_member')->alias('gm')
|
||||||
->join('wechat_account wa', 'gm.identifier = wa.wechatId')
|
->join('wechat_account wa', 'gm.identifier = wa.wechatId')
|
||||||
->where('gm.companyId', $this->getUserInfo('companyId'))
|
|
||||||
->whereIn('gm.groupId', $params['wechatGroups'])
|
->whereIn('gm.groupId', $params['wechatGroups'])
|
||||||
->group('gm.identifier')
|
->group('gm.identifier')
|
||||||
->column('wa.id,wa.wechatId,wa.alias,wa.phone');
|
->column('wa.id,wa.wechatId,wa.alias,wa.phone');
|
||||||
|
|||||||
@@ -198,7 +198,6 @@ class PostUpdateAddFriendPlanV1Controller extends BaseController
|
|||||||
if (!empty($params['wechatGroups']) && is_array($params['wechatGroups'])) {
|
if (!empty($params['wechatGroups']) && is_array($params['wechatGroups'])) {
|
||||||
$rows = Db::name('wechat_group_member')->alias('gm')
|
$rows = Db::name('wechat_group_member')->alias('gm')
|
||||||
->join('wechat_account wa', 'gm.identifier = wa.wechatId')
|
->join('wechat_account wa', 'gm.identifier = wa.wechatId')
|
||||||
->where('gm.companyId', $this->getUserInfo('companyId'))
|
|
||||||
->whereIn('gm.groupId', $params['wechatGroups'])
|
->whereIn('gm.groupId', $params['wechatGroups'])
|
||||||
->group('gm.identifier')
|
->group('gm.identifier')
|
||||||
->column('wa.id,wa.wechatId,wa.alias,wa.phone');
|
->column('wa.id,wa.wechatId,wa.alias,wa.phone');
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ class PosterWeChatMiniProgram extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$phoneData = explode(',', $phoneData);
|
$phoneData = explode("\n", $phoneData);
|
||||||
foreach ($phoneData as $phone) {
|
foreach ($phoneData as $phone) {
|
||||||
if (empty($phone)) {
|
if (empty($phone)) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -359,39 +359,7 @@ class GetPotentialListWithInCompanyV1Controller extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function getPackage()
|
|
||||||
{
|
|
||||||
|
|
||||||
$page = $this->request->param('page', 1);
|
|
||||||
$limit = $this->request->param('limit', 10);
|
|
||||||
$keyword = $this->request->param('keyword', '');
|
|
||||||
|
|
||||||
$companyId = $this->getUserInfo('companyId');
|
|
||||||
$package = Db::name('traffic_source_package')->alias('tsp')
|
|
||||||
->join('traffic_source_package_item tspi', 'tspi.packageId=tsp.id', 'left')
|
|
||||||
->whereIn('tsp.companyId', [$companyId, 0])
|
|
||||||
->field('tsp.id,tsp.name,tsp.description,tsp.createTime,count(tspi.id) as num')
|
|
||||||
->group('tsp.id');
|
|
||||||
|
|
||||||
if (!empty($keyword)) {
|
|
||||||
$package->where('tsp.name|tsp.description', 'like', '%' . $keyword . '%');
|
|
||||||
}
|
|
||||||
|
|
||||||
$list = $package->page($page, $limit)->select();
|
|
||||||
$total = $package->count();
|
|
||||||
|
|
||||||
foreach ($list as $k => &$v) {
|
|
||||||
$v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s', $v['createTime']) : '';
|
|
||||||
}
|
|
||||||
unset($v);
|
|
||||||
|
|
||||||
$data = [
|
|
||||||
'total' => $total,
|
|
||||||
'list' => $list,
|
|
||||||
];
|
|
||||||
|
|
||||||
return ResponseHelper::success($data);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public function addPackage()
|
public function addPackage()
|
||||||
|
|||||||
11
Server/application/cunkebao/model/AiKnowledgeBase.php
Normal file
11
Server/application/cunkebao/model/AiKnowledgeBase.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
namespace app\cunkebao\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
|
||||||
|
class AiKnowledgeBase extends Model
|
||||||
|
{
|
||||||
|
// 设置表名
|
||||||
|
protected $name = 'ai_knowledge_base';
|
||||||
|
}
|
||||||
11
Server/application/cunkebao/model/AiKnowledgeBaseType.php
Normal file
11
Server/application/cunkebao/model/AiKnowledgeBaseType.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
namespace app\cunkebao\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
|
||||||
|
class AiKnowledgeBaseType extends Model
|
||||||
|
{
|
||||||
|
// 设置表名
|
||||||
|
protected $name = 'ai_knowledge_base_type';
|
||||||
|
}
|
||||||
@@ -79,7 +79,7 @@ class WechatFriendJob
|
|||||||
'preFriendId' => $preFriendId,
|
'preFriendId' => $preFriendId,
|
||||||
], true, $isDel);
|
], true, $isDel);
|
||||||
$response = json_decode($result, true);
|
$response = json_decode($result, true);
|
||||||
|
|
||||||
// 判断是否成功
|
// 判断是否成功
|
||||||
if ($response['code'] == 200) {
|
if ($response['code'] == 200) {
|
||||||
$data = $response['data'];
|
$data = $response['data'];
|
||||||
|
|||||||
@@ -108,93 +108,33 @@ class WorkbenchMomentsJob
|
|||||||
public function execute2()
|
public function execute2()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
// 每日重置发送次数(允许10分钟误差)
|
// 1) 每日重置
|
||||||
$now = time();
|
$this->resetDailyCountersIfNeeded();
|
||||||
$todayStart = strtotime(date('Y-m-d 00:00:00'));
|
|
||||||
if ($now - $todayStart >= 0 && $now - $todayStart <= 600) {
|
// 2) 获取发送窗口内的任务
|
||||||
$cacheKey = 'moments_settings_reset_' . date('Ymd');
|
[$nowTs, $kfMoments] = $this->getWindowTasks();
|
||||||
if (!Cache::has($cacheKey)) {
|
|
||||||
Db::table('ck_kf_moments_settings')->where('sendNum', '<>', 0)
|
|
||||||
->update(['sendNum' => 0, 'updateTime' => $now]);
|
|
||||||
Cache::set($cacheKey, 1, 7200); // 2小时缓存,防止重复重置
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 获取所有工作台
|
|
||||||
$kfMoments = KfMoments::where(['isSend' => 0, 'isDel' => 0])->where('sendTime', '<=', time() + 120)->order('id desc')->select();
|
|
||||||
foreach ($kfMoments as $val) {
|
foreach ($kfMoments as $val) {
|
||||||
$sendData = json_decode($val->sendData,true);
|
$companyId = (int)($val['companyId'] ?? 0);
|
||||||
$endTime = strtotime($sendData['endTime']);
|
$userId = (int)($val['userId'] ?? 0);
|
||||||
if ($endTime <= time() + 1800){
|
|
||||||
$endTime = time() + 3600;
|
// 2.1) 数据规范化
|
||||||
$sendData['endTime'] = date('Y-m-d H:i:s', $endTime);
|
$sendData = json_decode($val->sendData, true);
|
||||||
}
|
$sendData = $this->normalizeSendData($sendData);
|
||||||
switch ($sendData['momentContentType']) {
|
|
||||||
case 1:
|
// 2.2) 账号额度过滤
|
||||||
$sendData['link'] = ['image' => ''];
|
$items = $sendData['jobPublishWechatMomentsItems'] ?? [];
|
||||||
$sendData['picUrlList'] = [];
|
if (empty($items)) { continue; }
|
||||||
$sendData['videoUrl'] = '';
|
$allowed = $this->filterAccountsByQuota($companyId, $userId, $items);
|
||||||
break;
|
if (empty($allowed)) { continue; }
|
||||||
case 2:
|
$sendData['jobPublishWechatMomentsItems'] = $allowed;
|
||||||
$sendData['link'] = ['image' => ''];
|
|
||||||
$sendData['videoUrl'] = '';
|
// 3) 下发
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
$sendData['link'] = ['image' => ''];
|
|
||||||
$sendData['picUrlList'] = [];
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
$sendData['picUrlList'] = [];
|
|
||||||
$sendData['videoUrl'] = '';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$sendData['link'] = ['image' => ''];
|
|
||||||
$sendData['picUrlList'] = [];
|
|
||||||
$sendData['videoUrl'] = '';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
$moments = new Moments();
|
$moments = new Moments();
|
||||||
$moments->addJob($sendData);
|
$moments->addJob($sendData);
|
||||||
KfMoments::where(['id' => $val['id']])->update(['isSend' => 1]);
|
KfMoments::where(['id' => $val['id']])->update(['isSend' => 1]);
|
||||||
|
|
||||||
// 统计发送次数(ck_kf_moments_settings)
|
// 4) 统计
|
||||||
try {
|
$this->incrementSendStats($companyId, $userId, $allowed);
|
||||||
$nowTs = time();
|
|
||||||
$companyId = (int)($val['companyId'] ?? 0);
|
|
||||||
$userId = (int)($val['userId'] ?? 0);
|
|
||||||
$items = $sendData['jobPublishWechatMomentsItems'] ?? [];
|
|
||||||
foreach ($items as $it) {
|
|
||||||
$wechatId = (int)($it['wechatAccountId'] ?? 0);
|
|
||||||
if ($wechatId <= 0) { continue; }
|
|
||||||
|
|
||||||
$cond = [
|
|
||||||
'companyId' => $companyId,
|
|
||||||
'userId' => $userId,
|
|
||||||
'wechatId' => $wechatId,
|
|
||||||
];
|
|
||||||
|
|
||||||
$setting = Db::table('ck_kf_moments_settings')->where($cond)->find();
|
|
||||||
if ($setting) {
|
|
||||||
Db::table('ck_kf_moments_settings')
|
|
||||||
->where('id', $setting['id'])
|
|
||||||
->update([
|
|
||||||
'sendNum' => Db::raw('sendNum + 1'),
|
|
||||||
'updateTime' => $nowTs,
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
Db::table('ck_kf_moments_settings')->insert([
|
|
||||||
'companyId' => $companyId,
|
|
||||||
'userId' => $userId,
|
|
||||||
'wechatId' => $wechatId,
|
|
||||||
'max' => 5,
|
|
||||||
'sendNum' => 1,
|
|
||||||
'createTime' => $nowTs,
|
|
||||||
'updateTime' => $nowTs,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (\Throwable $statE) {
|
|
||||||
Log::error('朋友圈发送统计失败: ' . $statE->getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::error("朋友圈同步任务异常: " . $e->getMessage());
|
Log::error("朋友圈同步任务异常: " . $e->getMessage());
|
||||||
@@ -202,6 +142,118 @@ class WorkbenchMomentsJob
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function resetDailyCountersIfNeeded()
|
||||||
|
{
|
||||||
|
$now = time();
|
||||||
|
$todayStart = strtotime(date('Y-m-d 00:00:00'));
|
||||||
|
if ($now - $todayStart >= 0 && $now - $todayStart <= 600) {
|
||||||
|
$cacheKey = 'moments_settings_reset_' . date('Ymd');
|
||||||
|
if (!Cache::has($cacheKey)) {
|
||||||
|
Db::table('ck_kf_moments_settings')->where('sendNum', '<>', 0)
|
||||||
|
->update(['sendNum' => 0, 'updateTime' => $now]);
|
||||||
|
Cache::set($cacheKey, 1, 7200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getWindowTasks()
|
||||||
|
{
|
||||||
|
$nowTs = time();
|
||||||
|
$windowStart = $nowTs - 300;
|
||||||
|
$windowEnd = $nowTs + 300;
|
||||||
|
$kfMoments = KfMoments::where(['isSend' => 0, 'isDel' => 0])
|
||||||
|
->whereBetween('sendTime', [$windowStart, $windowEnd])
|
||||||
|
->order('id desc')->select();
|
||||||
|
return [$nowTs, $kfMoments];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function normalizeSendData(array $sendData)
|
||||||
|
{
|
||||||
|
$endTime = strtotime($sendData['endTime'] ?? '');
|
||||||
|
if ($endTime <= time() + 1800) {
|
||||||
|
$endTime = time() + 3600;
|
||||||
|
$sendData['endTime'] = date('Y-m-d H:i:s', $endTime);
|
||||||
|
}
|
||||||
|
switch ($sendData['momentContentType'] ?? 1) {
|
||||||
|
case 1:
|
||||||
|
$sendData['link'] = ['image' => ''];
|
||||||
|
$sendData['picUrlList'] = [];
|
||||||
|
$sendData['videoUrl'] = '';
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
$sendData['link'] = ['image' => ''];
|
||||||
|
$sendData['videoUrl'] = '';
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
$sendData['link'] = ['image' => ''];
|
||||||
|
$sendData['picUrlList'] = [];
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
$sendData['picUrlList'] = [];
|
||||||
|
$sendData['videoUrl'] = '';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$sendData['link'] = ['image' => ''];
|
||||||
|
$sendData['picUrlList'] = [];
|
||||||
|
$sendData['videoUrl'] = '';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return $sendData;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function filterAccountsByQuota(int $companyId, int $userId, array $items)
|
||||||
|
{
|
||||||
|
$wechatIds = array_values(array_filter(array_map(function($it){ return (int)($it['wechatAccountId'] ?? 0); }, $items)));
|
||||||
|
if (empty($wechatIds)) { return []; }
|
||||||
|
$settings = Db::table('ck_kf_moments_settings')
|
||||||
|
->where('companyId', $companyId)
|
||||||
|
->where('userId', $userId)
|
||||||
|
->whereIn('wechatId', $wechatIds)
|
||||||
|
->column('id,max,sendNum', 'wechatId');
|
||||||
|
$allowed = [];
|
||||||
|
foreach ($items as $it) {
|
||||||
|
$wid = (int)($it['wechatAccountId'] ?? 0);
|
||||||
|
if ($wid <= 0) { continue; }
|
||||||
|
if (isset($settings[$wid])) {
|
||||||
|
$max = (int)$settings[$wid]['max'];
|
||||||
|
$sent = (int)$settings[$wid]['sendNum'];
|
||||||
|
if ($sent < ($max > 0 ? $max : 5)) { $allowed[] = $it; }
|
||||||
|
} else {
|
||||||
|
$allowed[] = $it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function incrementSendStats(int $companyId, int $userId, array $items)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$nowTs = time();
|
||||||
|
foreach ($items as $it) {
|
||||||
|
$wechatId = (int)($it['wechatAccountId'] ?? 0);
|
||||||
|
if ($wechatId <= 0) { continue; }
|
||||||
|
$cond = ['companyId' => $companyId, 'userId' => $userId, 'wechatId' => $wechatId];
|
||||||
|
$setting = Db::table('ck_kf_moments_settings')->where($cond)->find();
|
||||||
|
if ($setting) {
|
||||||
|
Db::table('ck_kf_moments_settings')->where('id', $setting['id'])
|
||||||
|
->update(['sendNum' => Db::raw('sendNum + 1'), 'updateTime' => $nowTs]);
|
||||||
|
} else {
|
||||||
|
Db::table('ck_kf_moments_settings')->insert([
|
||||||
|
'companyId' => $companyId,
|
||||||
|
'userId' => $userId,
|
||||||
|
'wechatId' => $wechatId,
|
||||||
|
'max' => 5,
|
||||||
|
'sendNum' => 1,
|
||||||
|
'createTime' => $nowTs,
|
||||||
|
'updateTime' => $nowTs,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::error('朋友圈发送统计失败: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理内容发送
|
* 处理内容发送
|
||||||
|
|||||||
@@ -7,42 +7,42 @@ use think\facade\Route;
|
|||||||
Route::group('v1/store', function () {
|
Route::group('v1/store', function () {
|
||||||
// 流量套餐相关路由
|
// 流量套餐相关路由
|
||||||
Route::group('flow-packages', function () {
|
Route::group('flow-packages', function () {
|
||||||
Route::get('', 'app\\store\\controller\\FlowPackageController@getList'); // 获取流量套餐列表
|
Route::get('', 'app\store\controller\FlowPackageController@getList'); // 获取流量套餐列表
|
||||||
Route::get('remaining-flow', 'app\\store\\controller\\FlowPackageController@remainingFlow'); // 获取用户剩余流量
|
Route::get('remaining-flow', 'app\store\controller\FlowPackageController@remainingFlow'); // 获取用户剩余流量
|
||||||
Route::get(':id', 'app\\store\\controller\\FlowPackageController@detail'); // 获取流量套餐详情
|
Route::get(':id', 'app\store\controller\FlowPackageController@detail'); // 获取流量套餐详情
|
||||||
Route::post('order', 'app\\store\\controller\\FlowPackageController@createOrder'); // 创建流量采购订单
|
Route::post('order', 'app\store\controller\FlowPackageController@createOrder'); // 创建流量采购订单
|
||||||
});
|
});
|
||||||
|
|
||||||
// 流量订单相关路由
|
// 流量订单相关路由
|
||||||
Route::group('flow-orders', function () {
|
Route::group('flow-orders', function () {
|
||||||
Route::get('list', 'app\\store\\controller\\FlowPackageController@getOrderList'); // 获取订单列表
|
Route::get('list', 'app\store\controller\FlowPackageController@getOrderList'); // 获取订单列表
|
||||||
Route::get(':orderNo', 'app\\store\\controller\\FlowPackageController@getOrderDetail'); // 获取订单详情
|
Route::get(':orderNo', 'app\store\controller\FlowPackageController@getOrderDetail'); // 获取订单详情
|
||||||
});
|
});
|
||||||
|
|
||||||
// 客户相关路由
|
// 客户相关路由
|
||||||
Route::group('customers', function () {
|
Route::group('customers', function () {
|
||||||
Route::get('list', 'app\\store\\controller\\CustomerController@getList'); // 获取客户列表
|
Route::get('list', 'app\store\controller\CustomerController@getList'); // 获取客户列表
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// 系统配置相关路由
|
// 系统配置相关路由
|
||||||
Route::group('system-config', function () {
|
Route::group('system-config', function () {
|
||||||
Route::get('switch-status', 'app\\store\\controller\\SystemConfigController@getSwitchStatus'); // 获取系统开关状态
|
Route::get('switch-status', 'app\store\controller\SystemConfigController@getSwitchStatus'); // 获取系统开关状态
|
||||||
Route::post('update-switch-status', 'app\\store\\controller\\SystemConfigController@updateSwitchStatus'); // 更新系统开关状态
|
Route::post('update-switch-status', 'app\store\controller\SystemConfigController@updateSwitchStatus'); // 更新系统开关状态
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// 数据统计相关路由
|
// 数据统计相关路由
|
||||||
Route::group('statistics', function () {
|
Route::group('statistics', function () {
|
||||||
Route::get('overview', 'app\\store\\controller\\StatisticsController@getOverview'); // 获取数据概览
|
Route::get('overview', 'app\store\controller\StatisticsController@getOverview'); // 获取数据概览
|
||||||
Route::get('customer-analysis', 'app\\store\\controller\\StatisticsController@getCustomerAnalysis'); // 获取客户分析数据
|
Route::get('customer-analysis', 'app\store\controller\StatisticsController@getCustomerAnalysis'); // 获取客户分析数据
|
||||||
Route::get('interaction-analysis', 'app\\store\\controller\\StatisticsController@getInteractionAnalysis'); // 获取互动分析数据
|
Route::get('interaction-analysis', 'app\store\controller\StatisticsController@getInteractionAnalysis'); // 获取互动分析数据
|
||||||
});
|
});
|
||||||
|
|
||||||
// 供应商相关路由
|
// 供应商相关路由
|
||||||
Route::group('vendor', function () {
|
Route::group('vendor', function () {
|
||||||
Route::get('list', 'app\\store\\controller\\VendorController@getList'); // 获取供应商列表
|
Route::get('list', 'app\store\controller\VendorController@getList'); // 获取供应商列表
|
||||||
Route::get('detail', 'app\\store\\controller\\VendorController@detail'); // 获取供应商详情
|
Route::get('detail', 'app\store\controller\VendorController@detail'); // 获取供应商详情
|
||||||
Route::post('order', 'app\\store\\controller\\VendorController@createOrder'); // 创建订单
|
Route::post('order', 'app\store\controller\VendorController@createOrder'); // 创建订单
|
||||||
});
|
});
|
||||||
})->middleware(['jwt']);
|
})->middleware(['jwt']);
|
||||||
@@ -50,15 +50,11 @@ class StatisticsController extends BaseController
|
|||||||
|
|
||||||
// 1. 总客户数
|
// 1. 总客户数
|
||||||
$totalCustomers = WechatFriendModel::whereIn('ownerWechatId',$ownerWechatIds)
|
$totalCustomers = WechatFriendModel::whereIn('ownerWechatId',$ownerWechatIds)
|
||||||
->whereTime('createTime', '>=', $startTime)
|
->where('isDeleted',0)
|
||||||
->whereTime('createTime', '<', $endTime)
|
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
// 上期总客户数
|
// 上期总客户数
|
||||||
$lastTotalCustomers = WechatFriendModel::whereIn('ownerWechatId',$ownerWechatIds)
|
$lastTotalCustomers = WechatFriendModel::whereIn('ownerWechatId',$ownerWechatIds)->count();
|
||||||
->whereTime('createTime', '>=', $lastStartTime)
|
|
||||||
->whereTime('createTime', '<', $lastEndTime)
|
|
||||||
->count();
|
|
||||||
|
|
||||||
// 2. 新增客户数
|
// 2. 新增客户数
|
||||||
$newCustomers = WechatFriendModel::whereIn('ownerWechatId',$ownerWechatIds)
|
$newCustomers = WechatFriendModel::whereIn('ownerWechatId',$ownerWechatIds)
|
||||||
@@ -143,35 +139,38 @@ class StatisticsController extends BaseController
|
|||||||
|
|
||||||
// 1. 客户增长趋势数据
|
// 1. 客户增长趋势数据
|
||||||
$totalCustomers = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds)
|
$totalCustomers = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds)
|
||||||
|
->where('isDeleted',0)
|
||||||
->whereTime('createTime', '<', $endTime)
|
->whereTime('createTime', '<', $endTime)
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
$newCustomers = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds)
|
$newCustomers = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds)
|
||||||
|
->where('isDeleted',0)
|
||||||
->whereTime('createTime', '>=', $startTime)
|
->whereTime('createTime', '>=', $startTime)
|
||||||
->whereTime('createTime', '<', $endTime)
|
->whereTime('createTime', '<', $endTime)
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
// 计算流失客户数(假设超过30天未互动的客户为流失客户)
|
// 计算流失客户数
|
||||||
$thirtyDaysAgo = strtotime('-30 days');
|
|
||||||
$lostCustomers = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds)
|
$lostCustomers = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds)
|
||||||
|
->where('isDeleted',1)
|
||||||
->where('createTime', '>', 0)
|
->where('createTime', '>', 0)
|
||||||
->where('deleteTime', '<', $thirtyDaysAgo)
|
->whereTime('deleteTime', '>=', $startTime)
|
||||||
|
->whereTime('deleteTime', '<', $endTime)
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
// 2. 客户来源分布数据
|
// 2. 客户来源分布数据
|
||||||
// 朋友推荐
|
// 朋友推荐
|
||||||
$friendRecommend = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds)
|
$friendRecommend = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds)
|
||||||
// ->whereIn('addFrom', [17, 1000017])
|
->whereIn('addFrom', [17, 1000017])
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
// 微信搜索
|
// 微信搜索
|
||||||
$wechatSearch = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds)
|
$wechatSearch = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds)
|
||||||
// ->whereIn('addFrom', [3, 15, 1000003, 1000015])
|
->whereIn('addFrom', [3, 15, 1000003, 1000015])
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
// 微信群
|
// 微信群
|
||||||
$wechatGroup = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds)
|
$wechatGroup = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds)
|
||||||
// ->whereIn('addFrom', [14, 1000014])
|
->whereIn('addFrom', [14, 1000014])
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
// 其他渠道(总数减去已知渠道)
|
// 其他渠道(总数减去已知渠道)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use think\Model;
|
|||||||
|
|
||||||
class WechatFriendModel extends Model
|
class WechatFriendModel extends Model
|
||||||
{
|
{
|
||||||
protected $name = 'wechat_friendship';
|
//protected $name = 'wechat_friendship';
|
||||||
|
protected $table = 's2_wechat_friend';
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace AccountWeight\UnitWeight;
|
namespace AccountWeight\UnitWeight;
|
||||||
|
|
||||||
use app\common\model\WechatCustomer as WechatCustomerModel;
|
use app\common\model\WechatCustomer as WechatCustomerModel;
|
||||||
use library\interfaces\WechatAccountWeightResultSet as WechatAccountWeightResultSetInterface;
|
use library\Interfaces\WechatAccountWeightResultSet as WechatAccountWeightResultSetInterface;
|
||||||
|
|
||||||
class ActivityWeigth implements WechatAccountWeightResultSetInterface
|
class ActivityWeigth implements WechatAccountWeightResultSetInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace AccountWeight\UnitWeight;
|
namespace AccountWeight\UnitWeight;
|
||||||
|
|
||||||
use app\common\model\WechatCustomer as WechatCustomerModel;
|
use app\common\model\WechatCustomer as WechatCustomerModel;
|
||||||
use library\interfaces\WechatAccountWeightResultSet as WechatAccountWeightResultSetInterface;
|
use library\Interfaces\WechatAccountWeightResultSet as WechatAccountWeightResultSetInterface;
|
||||||
|
|
||||||
class AgeWeight implements WechatAccountWeightResultSetInterface
|
class AgeWeight implements WechatAccountWeightResultSetInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
namespace AccountWeight\UnitWeight;
|
namespace AccountWeight\UnitWeight;
|
||||||
|
|
||||||
use library\interfaces\WechatAccountWeightResultSet as WechatAccountWeightResultSetInterface;
|
use library\Interfaces\WechatAccountWeightResultSet as WechatAccountWeightResultSetInterface;
|
||||||
|
|
||||||
class RealNameWeight implements WechatAccountWeightResultSetInterface
|
class RealNameWeight implements WechatAccountWeightResultSetInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace AccountWeight\UnitWeight;
|
namespace AccountWeight\UnitWeight;
|
||||||
|
|
||||||
use app\common\model\WechatRestricts as WechatRestrictsModel;
|
use app\common\model\WechatRestricts as WechatRestrictsModel;
|
||||||
use library\interfaces\WechatAccountWeightResultSet as WechatAccountWeightResultSetInterface;
|
use library\Interfaces\WechatAccountWeightResultSet as WechatAccountWeightResultSetInterface;
|
||||||
|
|
||||||
class RestrictWeight implements WechatAccountWeightResultSetInterface
|
class RestrictWeight implements WechatAccountWeightResultSetInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ namespace AccountWeight;
|
|||||||
|
|
||||||
use AccountWeight\Exceptions\WechatAccountWeightAssessmentException as WeightAssessmentException;
|
use AccountWeight\Exceptions\WechatAccountWeightAssessmentException as WeightAssessmentException;
|
||||||
use library\ClassTable;
|
use library\ClassTable;
|
||||||
use library\interfaces\WechatAccountWeightResultSet as WechatAccountWeightResultSetInterface;
|
use library\Interfaces\WechatAccountWeightResultSet as WechatAccountWeightResultSetInterface;
|
||||||
use library\interfaces\WechatAccountWeightAssessment as WechatAccountWeightAssessmentInterface;
|
use library\Interfaces\WechatAccountWeightAssessment as WechatAccountWeightAssessmentInterface;
|
||||||
use AccountWeight\UnitWeight;
|
use AccountWeight\UnitWeight;
|
||||||
use app\common\service\ClassTableService;
|
use app\common\service\ClassTableService;
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace AccountWeight;
|
namespace AccountWeight;
|
||||||
|
|
||||||
use library\interfaces\WechatAccountWeightAssessment as WechatAccountWeightAssessmentInterface;
|
use library\Interfaces\WechatAccountWeightAssessment as WechatAccountWeightAssessmentInterface;
|
||||||
use library\interfaces\WechatFriendAddLimitAssessment as WechatFriendAddLimitAssessmentInterface;
|
use library\Interfaces\WechatFriendAddLimitAssessment as WechatFriendAddLimitAssessmentInterface;
|
||||||
|
|
||||||
class WechatFriendAddLimitAssessment implements WechatFriendAddLimitAssessmentInterface
|
class WechatFriendAddLimitAssessment implements WechatFriendAddLimitAssessmentInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ class Adapter implements WeChatServiceInterface
|
|||||||
{
|
{
|
||||||
$task = Db::name('customer_acquisition_task')
|
$task = Db::name('customer_acquisition_task')
|
||||||
->where(['status' => 1, 'deleteTime' => 0])
|
->where(['status' => 1, 'deleteTime' => 0])
|
||||||
->whereRaw("id % $process_count_for_status_0 = {$current_worker_id}")
|
/* ->whereRaw("id % $process_count_for_status_0 = {$current_worker_id}")*/
|
||||||
->order('id desc')
|
->order('id desc')
|
||||||
->select();
|
->select();
|
||||||
|
|
||||||
@@ -236,15 +236,17 @@ class Adapter implements WeChatServiceInterface
|
|||||||
$task['processed_wechat_ids'] = $task['processed_wechat_ids'] . ',' . $wechatId; // 处理失败任务用,用于过滤已处理的微信号
|
$task['processed_wechat_ids'] = $task['processed_wechat_ids'] . ',' . $wechatId; // 处理失败任务用,用于过滤已处理的微信号
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
$res = Db::name('task_customer')
|
if (!empty($friendAddTaskCreated)){
|
||||||
->where('id', $task['id'])
|
Db::name('task_customer')
|
||||||
->update([
|
->where('id', $task['id'])
|
||||||
'status' => $friendAddTaskCreated ? 1 : 3,
|
->update([
|
||||||
'fail_reason' => '',
|
'status' => $friendAddTaskCreated ? 1 : 3,
|
||||||
'processed_wechat_ids' => $task['processed_wechat_ids'],
|
'fail_reason' => '',
|
||||||
'addTime' => time(),
|
'processed_wechat_ids' => $task['processed_wechat_ids'],
|
||||||
'updateTime' => time()
|
'addTime' => time(),
|
||||||
]);
|
'updateTime' => time()
|
||||||
|
]);
|
||||||
|
}
|
||||||
// ~~不用管,回头再添加再判断即可~~
|
// ~~不用管,回头再添加再判断即可~~
|
||||||
// 失败一定是另一个进程/定时器在检查的
|
// 失败一定是另一个进程/定时器在检查的
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace library;
|
namespace library;
|
||||||
|
|
||||||
use library\interfaces\CallMap as CallMapInterface;
|
use library\Interfaces\CallMap as CallMapInterface;
|
||||||
|
|
||||||
class ClassTable implements CallMapInterface
|
class ClassTable implements CallMapInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace library\interfaces;
|
namespace library\Interfaces;
|
||||||
|
|
||||||
interface CallMap
|
interface CallMap
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace library\interfaces;
|
namespace library\Interfaces;
|
||||||
|
|
||||||
use AccountWeight\Exceptions\WechatAccountWeightAssessmentException as WeightAssessmentException;
|
use AccountWeight\Exceptions\WechatAccountWeightAssessmentException as WeightAssessmentException;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace library\interfaces;
|
namespace library\Interfaces;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 微信账号加友权重评估结果数据
|
* 微信账号加友权重评估结果数据
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace library\interfaces;
|
namespace library\Interfaces;
|
||||||
|
|
||||||
use library\interfaces\WechatAccountWeightAssessment as WechatAccountWeightAssessmentInterface;
|
use library\Interfaces\WechatAccountWeightAssessment as WechatAccountWeightAssessmentInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 微信账号加友权重评估
|
* 微信账号加友权重评估
|
||||||
|
|||||||
@@ -557,7 +557,7 @@
|
|||||||
tempEndDate: parseInt(today.getTime()),
|
tempEndDate: parseInt(today.getTime()),
|
||||||
minDate: parseInt(new Date(new Date().getFullYear() - 1, 0, 1).getTime()),
|
minDate: parseInt(new Date(new Date().getFullYear() - 1, 0, 1).getTime()),
|
||||||
maxDate: parseInt(new Date().getTime()),
|
maxDate: parseInt(new Date().getTime()),
|
||||||
subsectionList: ['客户分析', '互动分析', '转化分析', '收入分析'],
|
subsectionList: ['客户分析', '互动分析'/* , '转化分析', '收入分析' */],
|
||||||
currentSubsection: 0,
|
currentSubsection: 0,
|
||||||
overviewData: {
|
overviewData: {
|
||||||
totalCustomers: 0,
|
totalCustomers: 0,
|
||||||
|
|||||||
@@ -1,5 +1,42 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useUserStore } from "@/store/module/user";
|
import { useUserStore } from "@/store/module/user";
|
||||||
|
import { request } from "@/api/request";
|
||||||
|
|
||||||
|
//ai对话接口
|
||||||
|
export interface AiChatParams {
|
||||||
|
friendId: number;
|
||||||
|
wechatAccountId: number;
|
||||||
|
[property: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function aiChat(params: AiChatParams) {
|
||||||
|
return request("/v1/kefu/ai/chat", params, "POST");
|
||||||
|
}
|
||||||
|
|
||||||
|
//soket消息传入数据中心
|
||||||
|
export interface DataProcessingParams {
|
||||||
|
/**
|
||||||
|
* 群消息
|
||||||
|
*/
|
||||||
|
chatroomMessage?: string[];
|
||||||
|
/**
|
||||||
|
* 个人信息
|
||||||
|
*/
|
||||||
|
friendMessage?: string[];
|
||||||
|
/**
|
||||||
|
* 类型固定值
|
||||||
|
*/
|
||||||
|
type?: string;
|
||||||
|
/**
|
||||||
|
* 公共
|
||||||
|
*/
|
||||||
|
wechatAccountId?: string;
|
||||||
|
[property: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dataProcessing(params: DataProcessingParams) {
|
||||||
|
return request("/v1/kefu/dataProcessing", params, "POST");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AI文本生成接口
|
* AI文本生成接口
|
||||||
@@ -32,7 +69,7 @@ export async function generateAiText(
|
|||||||
Authorization: token ? `Bearer ${token}` : undefined,
|
Authorization: token ? `Bearer ${token}` : undefined,
|
||||||
},
|
},
|
||||||
timeout: 30000, // AI生成可能需要更长时间
|
timeout: 30000, // AI生成可能需要更长时间
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// 新接口返回:{ code: 200, msg: 'success', data: '...公告内容...' }
|
// 新接口返回:{ code: 200, msg: 'success', data: '...公告内容...' }
|
||||||
|
|||||||
5
Touchkebao/src/api/module/wechat.ts
Normal file
5
Touchkebao/src/api/module/wechat.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import request from "@/api/request";
|
||||||
|
//获取客服列表
|
||||||
|
export function getCustomerList() {
|
||||||
|
return request("/v1/kefu/customerService/list", {}, "GET");
|
||||||
|
}
|
||||||
@@ -7,24 +7,94 @@ import dayjs from "dayjs";
|
|||||||
import "dayjs/locale/zh-cn";
|
import "dayjs/locale/zh-cn";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import "./styles/global.scss";
|
import "./styles/global.scss";
|
||||||
import { db } from "@/utils/db"; // 引入数据库实例
|
import { db } from "./utils/db"; // 引入数据库实例
|
||||||
|
|
||||||
// 设置dayjs为中文
|
// 设置dayjs为中文
|
||||||
dayjs.locale("zh-cn");
|
dayjs.locale("zh-cn");
|
||||||
|
|
||||||
|
// 清理旧数据库
|
||||||
|
async function cleanupOldDatabase() {
|
||||||
|
try {
|
||||||
|
// 获取所有数据库
|
||||||
|
const databases = await indexedDB.databases();
|
||||||
|
|
||||||
|
for (const dbInfo of databases) {
|
||||||
|
if (dbInfo.name === "CunkebaoDatabase") {
|
||||||
|
console.log("检测到旧版数据库,开始清理...");
|
||||||
|
|
||||||
|
// 打开数据库检查版本
|
||||||
|
const openRequest = indexedDB.open(dbInfo.name);
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
openRequest.onsuccess = async event => {
|
||||||
|
const database = (event.target as IDBOpenDBRequest).result;
|
||||||
|
const objectStoreNames = Array.from(database.objectStoreNames);
|
||||||
|
|
||||||
|
// 检查是否存在旧表
|
||||||
|
const hasOldTables = objectStoreNames.some(name =>
|
||||||
|
[
|
||||||
|
"kfUsers",
|
||||||
|
"weChatGroup",
|
||||||
|
"contracts",
|
||||||
|
"newContactList",
|
||||||
|
"messageList",
|
||||||
|
].includes(name),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasOldTables) {
|
||||||
|
console.log("发现旧表,删除整个数据库:", objectStoreNames);
|
||||||
|
database.close();
|
||||||
|
|
||||||
|
// 删除整个数据库
|
||||||
|
const deleteRequest = indexedDB.deleteDatabase(dbInfo.name);
|
||||||
|
deleteRequest.onsuccess = () => {
|
||||||
|
console.log("旧数据库已删除");
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
deleteRequest.onerror = () => {
|
||||||
|
console.error("删除旧数据库失败");
|
||||||
|
reject();
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.log("数据库结构正确,无需清理");
|
||||||
|
database.close();
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
openRequest.onerror = () => {
|
||||||
|
console.error("无法打开数据库进行检查");
|
||||||
|
reject();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("清理旧数据库时出错(可忽略):", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 数据库初始化
|
// 数据库初始化
|
||||||
async function initializeApp() {
|
async function initializeApp() {
|
||||||
try {
|
try {
|
||||||
// 确保数据库已打开
|
// 1. 清理旧数据库
|
||||||
await db.open();
|
await cleanupOldDatabase();
|
||||||
|
|
||||||
await db.kfUsers.clear();
|
// 2. 打开新数据库
|
||||||
await db.weChatGroup.clear();
|
await db.open();
|
||||||
await db.contracts.clear();
|
console.log("数据库初始化成功");
|
||||||
await db.newContractList.clear();
|
|
||||||
|
// 3. 开发环境清空数据(可选)
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
console.log("开发环境:跳过数据清理");
|
||||||
|
// 如需清空数据,取消下面的注释
|
||||||
|
// await db.chatSessions.clear();
|
||||||
|
// await db.contactsUnified.clear();
|
||||||
|
// await db.contactLabelMap.clear();
|
||||||
|
// await db.userLoginRecords.clear();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("数据库初始化失败:", error);
|
console.error("数据库初始化失败:", error);
|
||||||
// 可以选择显示错误提示或使用降级方案
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染应用
|
// 渲染应用
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ import {
|
|||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
ThunderboltOutlined,
|
ThunderboltOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
WechatOutlined,
|
CalendarOutlined,
|
||||||
|
RetweetOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { noticeList, readMessage, readAll } from "./api";
|
import { noticeList, readMessage, readAll } from "./api";
|
||||||
import { useUserStore } from "@/store/module/user";
|
import { useUserStore } from "@/store/module/user";
|
||||||
@@ -64,8 +65,8 @@ const NavCommon: React.FC<NavCommonProps> = ({ title = "触客宝" }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 处理菜单图标点击:在两个路由之间切换
|
// 处理菜单图标点击:在两个路由之间切换
|
||||||
const handleMenuClick = (index: number) => {
|
const handleMenuClick = () => {
|
||||||
if (index === 0) {
|
if (!location.pathname.startsWith("/pc/powerCenter")) {
|
||||||
navigate("/pc/powerCenter");
|
navigate("/pc/powerCenter");
|
||||||
} else {
|
} else {
|
||||||
navigate("/pc/weChat");
|
navigate("/pc/weChat");
|
||||||
@@ -205,24 +206,24 @@ const NavCommon: React.FC<NavCommonProps> = ({ title = "触客宝" }) => {
|
|||||||
onClick: handleLogout,
|
onClick: handleLogout,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
const handleContentManagementClick = () => {
|
||||||
|
navigate("/pc/powerCenter/content-management");
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header className={styles.header}>
|
<Header className={styles.header}>
|
||||||
<div className={styles.headerLeft}>
|
<div className={styles.headerLeft}>
|
||||||
<Button
|
<Button
|
||||||
icon={<BarChartOutlined />}
|
icon={<BarChartOutlined style={{ fontSize: 18 }} />}
|
||||||
type={!isWeChat() ? "primary" : "default"}
|
type="primary"
|
||||||
onClick={() => handleMenuClick(0)}
|
onClick={handleMenuClick}
|
||||||
>
|
></Button>
|
||||||
功能中心
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
icon={<WechatOutlined />}
|
icon={<CalendarOutlined />}
|
||||||
type={isWeChat() ? "primary" : "default"}
|
onClick={handleContentManagementClick}
|
||||||
onClick={() => handleMenuClick(1)}
|
|
||||||
>
|
>
|
||||||
Ai智能客服
|
内容管理
|
||||||
</Button>
|
</Button>
|
||||||
<span className={styles.title}>{title}</span>
|
<span className={styles.title}>{title}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ export interface KfUserListData {
|
|||||||
labels: string[];
|
labels: string[];
|
||||||
lastUpdateTime: string;
|
lastUpdateTime: string;
|
||||||
isOnline?: boolean;
|
isOnline?: boolean;
|
||||||
|
momentsMax: number;
|
||||||
|
momentsNum: number;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,8 +115,9 @@ export interface weChatGroup {
|
|||||||
chatroomAvatar: string;
|
chatroomAvatar: string;
|
||||||
groupId: number;
|
groupId: number;
|
||||||
config?: {
|
config?: {
|
||||||
|
top?: false;
|
||||||
chat?: boolean;
|
chat?: boolean;
|
||||||
unreadCount: number;
|
unreadCount?: number;
|
||||||
};
|
};
|
||||||
labels?: string[];
|
labels?: string[];
|
||||||
notice: string;
|
notice: string;
|
||||||
@@ -191,6 +194,7 @@ export interface ChatRecord {
|
|||||||
wechatId: string;
|
wechatId: string;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
|
seq?: number;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { Modal, Form, Input, Select, Button, message } from "antd";
|
||||||
|
import { listData, updateMoment } from "./api";
|
||||||
|
import UploadComponent from "@/components/Upload/ImageUpload/ImageUpload";
|
||||||
|
import VideoUpload from "@/components/Upload/VideoUpload";
|
||||||
|
|
||||||
|
interface EditMomentModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onCancel: () => void;
|
||||||
|
onSuccess: () => void;
|
||||||
|
momentData?: listData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditMomentModal: React.FC<EditMomentModalProps> = ({
|
||||||
|
visible,
|
||||||
|
onCancel,
|
||||||
|
onSuccess,
|
||||||
|
momentData,
|
||||||
|
}) => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [contentType, setContentType] = useState<number>(1);
|
||||||
|
const [resUrls, setResUrls] = useState<string[]>([]);
|
||||||
|
const [linkData, setLinkData] = useState({
|
||||||
|
desc: "",
|
||||||
|
image: "",
|
||||||
|
url: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible && momentData) {
|
||||||
|
// 填充表单数据
|
||||||
|
form.setFieldsValue({
|
||||||
|
content: momentData.text,
|
||||||
|
type: momentData.momentContentType.toString(),
|
||||||
|
sendTime: momentData.sendTime
|
||||||
|
? new Date(momentData.sendTime * 1000).toISOString().slice(0, 16)
|
||||||
|
: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
setContentType(momentData.momentContentType);
|
||||||
|
setResUrls(momentData.picUrlList || []);
|
||||||
|
|
||||||
|
// 处理链接数据
|
||||||
|
if (momentData.link && momentData.link.length > 0) {
|
||||||
|
setLinkData({
|
||||||
|
desc: momentData.link[0] || "",
|
||||||
|
image: "",
|
||||||
|
url: momentData.link[0] || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [visible, momentData, form]);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
const values = await form.validateFields();
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const updateData: any = {
|
||||||
|
id: momentData?.id,
|
||||||
|
content: values.content,
|
||||||
|
type: values.type,
|
||||||
|
"wechatIds[]": [momentData?.accountCount || 1], // 这里需要根据实际情况调整
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据内容类型添加相应字段
|
||||||
|
switch (parseInt(values.type)) {
|
||||||
|
case 1: // 文本
|
||||||
|
break;
|
||||||
|
case 2: // 图文
|
||||||
|
if (resUrls.length > 0) {
|
||||||
|
updateData["picUrlList[]"] = resUrls;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 3: // 视频
|
||||||
|
if (resUrls[0]) {
|
||||||
|
updateData.videoUrl = resUrls[0];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 4: // 链接
|
||||||
|
if (linkData.url) {
|
||||||
|
updateData["link[url]"] = [linkData.url];
|
||||||
|
if (linkData.desc) {
|
||||||
|
updateData["link[desc]"] = [linkData.desc];
|
||||||
|
}
|
||||||
|
if (linkData.image) {
|
||||||
|
updateData["link[image]"] = [linkData.image];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加定时发布时间
|
||||||
|
if (values.sendTime) {
|
||||||
|
updateData.timingTime = values.sendTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = await updateMoment(updateData);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
message.success("更新成功!");
|
||||||
|
onSuccess();
|
||||||
|
onCancel();
|
||||||
|
} else {
|
||||||
|
message.error("更新失败,请重试");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("更新失败:", error);
|
||||||
|
message.error("更新失败,请重试");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
form.resetFields();
|
||||||
|
setResUrls([]);
|
||||||
|
setLinkData({ desc: "", image: "", url: "" });
|
||||||
|
onCancel();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title="编辑朋友圈"
|
||||||
|
open={visible}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
width={600}
|
||||||
|
footer={[
|
||||||
|
<Button key="cancel" onClick={handleCancel}>
|
||||||
|
取消
|
||||||
|
</Button>,
|
||||||
|
<Button
|
||||||
|
key="submit"
|
||||||
|
type="primary"
|
||||||
|
loading={loading}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical">
|
||||||
|
<Form.Item
|
||||||
|
name="content"
|
||||||
|
label="内容"
|
||||||
|
rules={[{ required: true, message: "请输入内容" }]}
|
||||||
|
>
|
||||||
|
<Input.TextArea rows={4} placeholder="请输入内容" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item name="type" label="类型" rules={[{ required: true }]}>
|
||||||
|
<Select
|
||||||
|
placeholder="请选择类型"
|
||||||
|
onChange={value => setContentType(parseInt(value))}
|
||||||
|
>
|
||||||
|
<Select.Option value="1">文本</Select.Option>
|
||||||
|
<Select.Option value="2">图文</Select.Option>
|
||||||
|
<Select.Option value="3">视频</Select.Option>
|
||||||
|
<Select.Option value="4">链接</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item name="sendTime" label="发布时间">
|
||||||
|
<Input type="datetime-local" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* 图文类型 */}
|
||||||
|
{contentType === 2 && (
|
||||||
|
<Form.Item label="图片">
|
||||||
|
<UploadComponent value={resUrls} onChange={setResUrls} count={9} />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 视频类型 */}
|
||||||
|
{contentType === 3 && (
|
||||||
|
<Form.Item label="视频">
|
||||||
|
<VideoUpload
|
||||||
|
value={resUrls[0] || ""}
|
||||||
|
onChange={url => setResUrls([url as string])}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 链接类型 */}
|
||||||
|
{contentType === 4 && (
|
||||||
|
<>
|
||||||
|
<Form.Item label="链接地址">
|
||||||
|
<Input
|
||||||
|
value={linkData.url}
|
||||||
|
onChange={e =>
|
||||||
|
setLinkData(prev => ({ ...prev, url: e.target.value }))
|
||||||
|
}
|
||||||
|
placeholder="请输入链接地址"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="链接描述">
|
||||||
|
<Input
|
||||||
|
value={linkData.desc}
|
||||||
|
onChange={e =>
|
||||||
|
setLinkData(prev => ({ ...prev, desc: e.target.value }))
|
||||||
|
}
|
||||||
|
placeholder="请输入链接描述"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="链接封面">
|
||||||
|
<UploadComponent
|
||||||
|
value={linkData.image ? [linkData.image] : []}
|
||||||
|
onChange={urls =>
|
||||||
|
setLinkData(prev => ({ ...prev, image: urls[0] || "" }))
|
||||||
|
}
|
||||||
|
count={1}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditMomentModal;
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
.momentPublish {
|
||||||
|
padding: 24px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #262626;
|
||||||
|
margin: 0 0 24px 0;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #595959;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accountSection {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
.accountList {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accountCard {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 120px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid #f0f0f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover:not(.disabled) {
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
border-color: #1890ff;
|
||||||
|
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ant-card-body) {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accountInfo {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.accountName {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #262626;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usageBadge {
|
||||||
|
:global(.ant-badge-count) {
|
||||||
|
font-size: 12px;
|
||||||
|
min-width: 32px;
|
||||||
|
height: 20px;
|
||||||
|
line-height: 18px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.limitText {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ff4d4f;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentSection {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
.formItem {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
.formSelect {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.contentInput {
|
||||||
|
:global(.ant-input) {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
resize: none;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: #1890ff;
|
||||||
|
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ant-input::placeholder) {
|
||||||
|
color: #bfbfbf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageSection {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
:global(.ant-upload-list-picture-card) {
|
||||||
|
.ant-upload-list-item {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadButton {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
color: #8c8c8c;
|
||||||
|
font-size: 14px;
|
||||||
|
border: 2px dashed #d9d9d9;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fafafa;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #1890ff;
|
||||||
|
color: #1890ff;
|
||||||
|
background: #f0f8ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.publishSection {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
.publishButton {
|
||||||
|
width: 200px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.3);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(135deg, #40a9ff 0%, #69c0ff 100%);
|
||||||
|
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.4);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.momentPublish {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
.accountSection {
|
||||||
|
.accountList {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accountCard {
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.publishSection {
|
||||||
|
.publishButton {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
|
import { Button, Input, Select, message, Card, Badge } from "antd";
|
||||||
|
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
|
||||||
|
import { addMoment } from "./api";
|
||||||
|
import styles from "./MomentPublish.module.scss";
|
||||||
|
import { KfUserListData } from "@/pages/pc/ckbox/data";
|
||||||
|
import UploadComponent from "@/components/Upload/ImageUpload/ImageUpload";
|
||||||
|
import VideoUpload from "@/components/Upload/VideoUpload";
|
||||||
|
|
||||||
|
interface MomentPublishProps {
|
||||||
|
onPublishSuccess?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MomentPublish: React.FC<MomentPublishProps> = ({ onPublishSuccess }) => {
|
||||||
|
const [selectedAccounts, setSelectedAccounts] = useState<string[]>([]);
|
||||||
|
const [content, setContent] = useState<string>("");
|
||||||
|
// 发布类型:1文本 2图文 3视频 4链接
|
||||||
|
const [contentType, setContentType] = useState<number>(1);
|
||||||
|
const [timingTime, setTimingTime] = useState<string>("");
|
||||||
|
const [resUrls, setResUrls] = useState<string[]>([]); // 图片/视频等资源
|
||||||
|
// 链接
|
||||||
|
const [linkDesc, setLinkDesc] = useState<string>("");
|
||||||
|
const [linkImage, setLinkImage] = useState<string>("");
|
||||||
|
const [linkUrl, setLinkUrl] = useState<string>("");
|
||||||
|
const [accounts, setAccounts] = useState<KfUserListData[]>([]);
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// 从store获取客服列表
|
||||||
|
const kfUserList = useCkChatStore(state => state.kfUserList);
|
||||||
|
|
||||||
|
// 获取账号使用情况
|
||||||
|
const fetchAccountUsage = useCallback(async () => {
|
||||||
|
if (kfUserList.length === 0) return;
|
||||||
|
|
||||||
|
// 直接使用客服列表数据,不需要额外的API调用
|
||||||
|
const accountData = kfUserList.map((kf, index) => ({
|
||||||
|
...kf,
|
||||||
|
name: kf.nickname || `客服${index + 1}`,
|
||||||
|
isSelected: selectedAccounts.includes(kf.id.toString()),
|
||||||
|
isDisabled: kf.momentsNum >= kf.momentsMax,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setAccounts(accountData);
|
||||||
|
}, [kfUserList, selectedAccounts]);
|
||||||
|
|
||||||
|
// 如果没有选中的账号且有可用账号,自动选择第一个可用账号
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedAccounts.length === 0 && accounts.length > 0) {
|
||||||
|
const firstAvailable = accounts.find(acc => !acc.isDisabled);
|
||||||
|
if (firstAvailable) {
|
||||||
|
setSelectedAccounts([firstAvailable.id.toString()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [accounts, selectedAccounts.length]);
|
||||||
|
|
||||||
|
// 当客服列表变化时,重新获取使用情况
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAccountUsage();
|
||||||
|
}, [kfUserList, selectedAccounts, fetchAccountUsage]);
|
||||||
|
|
||||||
|
const handleAccountSelect = (accountId: string) => {
|
||||||
|
const account = accounts.find(acc => acc.id.toString() === accountId);
|
||||||
|
if (!account || account.isDisabled) return;
|
||||||
|
|
||||||
|
setSelectedAccounts(prev =>
|
||||||
|
prev.includes(accountId)
|
||||||
|
? prev.filter(id => id !== accountId)
|
||||||
|
: [...prev, accountId],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
setContent(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePublish = async () => {
|
||||||
|
if (!content.trim()) {
|
||||||
|
message.warning("请输入朋友圈内容");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedAccounts.length === 0) {
|
||||||
|
message.warning("请选择发布账号");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// 根据API要求构造数据
|
||||||
|
const publishData: any = {
|
||||||
|
content: content.trim(),
|
||||||
|
type: contentType.toString(),
|
||||||
|
wechatIds: selectedAccounts,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据内容类型添加相应字段
|
||||||
|
switch (contentType) {
|
||||||
|
case 1: // 文本
|
||||||
|
// 文本类型只需要content
|
||||||
|
break;
|
||||||
|
case 2: // 图文
|
||||||
|
if (resUrls.length > 0) {
|
||||||
|
publishData.picUrlList = resUrls;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 3: // 视频
|
||||||
|
if (resUrls[0]) {
|
||||||
|
publishData.videoUrl = resUrls[0];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 4: // 链接
|
||||||
|
if (linkUrl) {
|
||||||
|
publishData["link[url]"] = [linkUrl];
|
||||||
|
if (linkDesc) {
|
||||||
|
publishData["link[desc]"] = [linkDesc];
|
||||||
|
}
|
||||||
|
if (linkImage) {
|
||||||
|
publishData["link[image]"] = [linkImage];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加定时发布时间
|
||||||
|
if (timingTime) {
|
||||||
|
// 将 datetime-local 格式转换为 "2025年10月20日17:06:34" 格式
|
||||||
|
const date = new Date(timingTime);
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = date.getMonth() + 1;
|
||||||
|
const day = date.getDate();
|
||||||
|
const hours = date.getHours().toString().padStart(2, "0");
|
||||||
|
const minutes = date.getMinutes().toString().padStart(2, "0");
|
||||||
|
const seconds = date.getSeconds().toString().padStart(2, "0");
|
||||||
|
|
||||||
|
publishData.timingTime = `${year}年${month}月${day}日${hours}:${minutes}:${seconds}`;
|
||||||
|
}
|
||||||
|
console.log(publishData);
|
||||||
|
|
||||||
|
await addMoment(publishData);
|
||||||
|
|
||||||
|
message.success("发布成功!");
|
||||||
|
// 重置表单
|
||||||
|
setContent("");
|
||||||
|
setResUrls([]);
|
||||||
|
setLinkDesc("");
|
||||||
|
setLinkImage("");
|
||||||
|
setLinkUrl("");
|
||||||
|
setTimingTime("");
|
||||||
|
// 重新获取账号使用情况
|
||||||
|
await fetchAccountUsage();
|
||||||
|
// 触发父组件的刷新回调
|
||||||
|
onPublishSuccess?.();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("发布失败:", error);
|
||||||
|
message.error("发布失败,请重试");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.momentPublish}>
|
||||||
|
<h3 className={styles.title}>发布朋友圈</h3>
|
||||||
|
|
||||||
|
{/* 选择发布账号 */}
|
||||||
|
<div className={styles.accountSection}>
|
||||||
|
<h4 className={styles.sectionTitle}>选择发布账号</h4>
|
||||||
|
<div className={styles.accountList}>
|
||||||
|
{accounts.map(account => (
|
||||||
|
<Card
|
||||||
|
key={account.id}
|
||||||
|
className={`${styles.accountCard} ${
|
||||||
|
selectedAccounts.includes(account.id.toString())
|
||||||
|
? styles.selected
|
||||||
|
: ""
|
||||||
|
} ${account.isDisabled ? styles.disabled : ""}`}
|
||||||
|
onClick={() => handleAccountSelect(account.id.toString())}
|
||||||
|
>
|
||||||
|
<div className={styles.accountInfo}>
|
||||||
|
<div className={styles.accountName}>{account.name}</div>
|
||||||
|
<Badge
|
||||||
|
count={`${account.momentsNum}/${account.momentsMax}`}
|
||||||
|
className={styles.usageBadge}
|
||||||
|
color={
|
||||||
|
account.momentsNum >= account.momentsMax ? "red" : "blue"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{account.isDisabled && (
|
||||||
|
<div className={styles.limitText}>今日已达上限</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 朋友圈内容 */}
|
||||||
|
<div className={styles.contentSection}>
|
||||||
|
{/* 发布时间 */}
|
||||||
|
<div className={styles.formItem}>
|
||||||
|
<label className={styles.formLabel}>发布时间</label>
|
||||||
|
<Input
|
||||||
|
type="datetime-local"
|
||||||
|
value={timingTime}
|
||||||
|
onChange={e => setTimingTime((e.target as HTMLInputElement).value)}
|
||||||
|
placeholder="请选择发布时间"
|
||||||
|
className={styles.contentInput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 类型选择 */}
|
||||||
|
<div className={styles.formItem}>
|
||||||
|
<label className={styles.formLabel}>类型</label>
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
value={contentType}
|
||||||
|
onChange={value => setContentType(value)}
|
||||||
|
className={styles.formSelect}
|
||||||
|
>
|
||||||
|
<Select.Option value={1}>文本</Select.Option>
|
||||||
|
<Select.Option value={2}>图文</Select.Option>
|
||||||
|
<Select.Option value={3}>视频</Select.Option>
|
||||||
|
<Select.Option value={4}>链接</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 文本内容 */}
|
||||||
|
<div className={styles.formItem}>
|
||||||
|
<label className={styles.formLabel}>内容</label>
|
||||||
|
<Input.TextArea
|
||||||
|
value={content}
|
||||||
|
onChange={handleContentChange}
|
||||||
|
placeholder="请输入内容"
|
||||||
|
className={styles.contentInput}
|
||||||
|
rows={6}
|
||||||
|
showCount
|
||||||
|
maxLength={500}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 链接类型 */}
|
||||||
|
{contentType === 4 && (
|
||||||
|
<>
|
||||||
|
<div className={styles.formItem}>
|
||||||
|
<label className={styles.formLabel}>描述</label>
|
||||||
|
<Input
|
||||||
|
value={linkDesc}
|
||||||
|
onChange={e =>
|
||||||
|
setLinkDesc((e.target as HTMLInputElement).value)
|
||||||
|
}
|
||||||
|
placeholder="请输入描述"
|
||||||
|
className={styles.contentInput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.formItem}>
|
||||||
|
<label className={styles.formLabel}>封面图</label>
|
||||||
|
<UploadComponent
|
||||||
|
value={linkImage ? [linkImage] : []}
|
||||||
|
onChange={urls => setLinkImage(urls[0] || "")}
|
||||||
|
count={1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.formItem}>
|
||||||
|
<label className={styles.formLabel}>链接地址</label>
|
||||||
|
<Input
|
||||||
|
value={linkUrl}
|
||||||
|
onChange={e => setLinkUrl((e.target as HTMLInputElement).value)}
|
||||||
|
placeholder="请输入链接地址"
|
||||||
|
className={styles.contentInput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 视频类型 */}
|
||||||
|
{contentType === 3 && (
|
||||||
|
<div className={styles.formItem}>
|
||||||
|
<label className={styles.formLabel}>视频上传</label>
|
||||||
|
<div style={{ width: "40%" }}>
|
||||||
|
<VideoUpload
|
||||||
|
value={resUrls[0] || ""}
|
||||||
|
onChange={url => setResUrls([url as string])}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 图片/小程序 素材上传 */}
|
||||||
|
{contentType === 2 && (
|
||||||
|
<div className={styles.formItem}>
|
||||||
|
<label className={styles.formLabel}>图片上传 (最多9张)</label>
|
||||||
|
<UploadComponent value={resUrls} onChange={setResUrls} count={9} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 备注已移除 */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 发布按钮 */}
|
||||||
|
<div className={styles.publishSection}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
onClick={handlePublish}
|
||||||
|
loading={loading}
|
||||||
|
disabled={selectedAccounts.length === 0 || !content.trim()}
|
||||||
|
className={styles.publishButton}
|
||||||
|
>
|
||||||
|
{loading ? "发布中..." : "发布朋友圈"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MomentPublish;
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Modal, Card, Tag, Badge } from "antd";
|
||||||
|
import {
|
||||||
|
PictureOutlined,
|
||||||
|
VideoCameraOutlined,
|
||||||
|
LinkOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import { listData } from "./api";
|
||||||
|
|
||||||
|
interface PreviewMomentModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onCancel: () => void;
|
||||||
|
momentData?: listData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PreviewMomentModal: React.FC<PreviewMomentModalProps> = ({
|
||||||
|
visible,
|
||||||
|
onCancel,
|
||||||
|
momentData,
|
||||||
|
}) => {
|
||||||
|
if (!momentData) return null;
|
||||||
|
|
||||||
|
// 获取内容类型信息
|
||||||
|
const getContentTypeInfo = (type: number) => {
|
||||||
|
switch (type) {
|
||||||
|
case 1:
|
||||||
|
return { icon: <FileTextOutlined />, label: "文本", color: "blue" };
|
||||||
|
case 2:
|
||||||
|
return { icon: <PictureOutlined />, label: "图文", color: "green" };
|
||||||
|
case 3:
|
||||||
|
return {
|
||||||
|
icon: <VideoCameraOutlined />,
|
||||||
|
label: "视频",
|
||||||
|
color: "purple",
|
||||||
|
};
|
||||||
|
case 4:
|
||||||
|
return { icon: <LinkOutlined />, label: "链接", color: "orange" };
|
||||||
|
default:
|
||||||
|
return { icon: <FileTextOutlined />, label: "未知", color: "default" };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化时间显示
|
||||||
|
const formatTime = (timestamp: number) => {
|
||||||
|
if (!timestamp) return "未设置";
|
||||||
|
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 getStatusInfo = (isSend: number) => {
|
||||||
|
switch (isSend) {
|
||||||
|
case 0:
|
||||||
|
return { status: "processing" as const, text: "待发布" };
|
||||||
|
case 1:
|
||||||
|
return { status: "success" as const, text: "已发布" };
|
||||||
|
case 2:
|
||||||
|
return { status: "error" as const, text: "发布失败" };
|
||||||
|
default:
|
||||||
|
return { status: "default" as const, text: "未知" };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const contentTypeInfo = getContentTypeInfo(momentData.momentContentType);
|
||||||
|
const statusInfo = getStatusInfo(momentData.isSend);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title="朋友圈预览"
|
||||||
|
open={visible}
|
||||||
|
onCancel={onCancel}
|
||||||
|
width={500}
|
||||||
|
footer={null}
|
||||||
|
>
|
||||||
|
<Card className="preview-card">
|
||||||
|
<div className="preview-header">
|
||||||
|
<div className="preview-status">
|
||||||
|
<Badge status={statusInfo.status} text={statusInfo.text} />
|
||||||
|
<Tag
|
||||||
|
color={contentTypeInfo.color}
|
||||||
|
icon={contentTypeInfo.icon}
|
||||||
|
style={{ marginLeft: 8 }}
|
||||||
|
>
|
||||||
|
{contentTypeInfo.label}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="preview-content">
|
||||||
|
<div className="preview-text">{momentData.text || "无文本内容"}</div>
|
||||||
|
|
||||||
|
{/* 图片预览 */}
|
||||||
|
{momentData.picUrlList && momentData.picUrlList.length > 0 && (
|
||||||
|
<div className="preview-images">
|
||||||
|
{momentData.picUrlList.map((image, index) => (
|
||||||
|
<div key={index} className="preview-image-item">
|
||||||
|
<img
|
||||||
|
src={image}
|
||||||
|
alt={`图片${index + 1}`}
|
||||||
|
className="preview-image"
|
||||||
|
onError={e => {
|
||||||
|
(e.target as HTMLImageElement).style.display = "none";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 视频预览 */}
|
||||||
|
{momentData.videoUrl && (
|
||||||
|
<div className="preview-video">
|
||||||
|
<div className="preview-video-icon">
|
||||||
|
<VideoCameraOutlined />
|
||||||
|
</div>
|
||||||
|
<span className="preview-video-text">视频内容</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 链接预览 */}
|
||||||
|
{momentData.link && momentData.link.length > 0 && (
|
||||||
|
<div className="preview-link">
|
||||||
|
<LinkOutlined className="preview-link-icon" />
|
||||||
|
<span className="preview-link-text">
|
||||||
|
{momentData.link.length} 个链接
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="preview-details">
|
||||||
|
<div className="preview-detail-item">
|
||||||
|
<span className="preview-detail-label">发布时间:</span>
|
||||||
|
<span className="preview-detail-value">
|
||||||
|
{formatTime(momentData.sendTime)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="preview-detail-item">
|
||||||
|
<span className="preview-detail-label">账号数量:</span>
|
||||||
|
<span className="preview-detail-value">
|
||||||
|
{momentData.accountCount} 个账号
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="preview-detail-item">
|
||||||
|
<span className="preview-detail-label">创建时间:</span>
|
||||||
|
<span className="preview-detail-value">
|
||||||
|
{formatTime(momentData.createTime)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<style jsx>{`
|
||||||
|
.preview-card {
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-header {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-text {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #262626;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-images {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image-item {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-video {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f0f8ff;
|
||||||
|
border: 1px solid #d6e4ff;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-video-icon {
|
||||||
|
color: #1890ff;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-video-text {
|
||||||
|
color: #1890ff;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #fff7e6;
|
||||||
|
border: 1px solid #ffd591;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-link-icon {
|
||||||
|
color: #fa8c16;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-link-text {
|
||||||
|
color: #fa8c16;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-details {
|
||||||
|
padding: 12px;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 3px solid #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-detail-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-detail-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-detail-label {
|
||||||
|
color: #8c8c8c;
|
||||||
|
min-width: 80px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-detail-value {
|
||||||
|
color: #595959;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PreviewMomentModal;
|
||||||
@@ -0,0 +1,352 @@
|
|||||||
|
.publishSchedule {
|
||||||
|
padding: 24px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #262626;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerActions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadingContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 200px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
|
||||||
|
.loadingText {
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scheduleList {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #a8a8a8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scheduleCard {
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ant-card-body) {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.headerLeft {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.statusBadge {
|
||||||
|
:global(.ant-badge) {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ant-badge-status-text) {
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeTag {
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerActions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.actionButton {
|
||||||
|
color: #1890ff;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background-color: #f0f8ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteButton {
|
||||||
|
color: #ff4d4f;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background-color: #fff2f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContent {
|
||||||
|
.postContent {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #262626;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.postImages {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.imagePlaceholder {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.imagePreview {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageIcon {
|
||||||
|
font-size: 20px;
|
||||||
|
opacity: 0.6;
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.moreImages {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoPreview {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f0f8ff;
|
||||||
|
border: 1px solid #d6e4ff;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
.videoIcon {
|
||||||
|
color: #1890ff;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoText {
|
||||||
|
color: #1890ff;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.linkPreview {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #fff7e6;
|
||||||
|
border: 1px solid #ffd591;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
.linkIcon {
|
||||||
|
color: #fa8c16;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linkText {
|
||||||
|
color: #fa8c16;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.postDetails {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 3px solid #1890ff;
|
||||||
|
|
||||||
|
.detailItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
.detailIcon {
|
||||||
|
color: #8c8c8c;
|
||||||
|
margin-right: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailLabel {
|
||||||
|
color: #8c8c8c;
|
||||||
|
min-width: 60px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailValue {
|
||||||
|
color: #595959;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyState {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
text-align: center;
|
||||||
|
background: #fafafa;
|
||||||
|
border: 1px dashed #d9d9d9;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 20px 0;
|
||||||
|
|
||||||
|
.emptyIcon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyText {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptySubText {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #bfbfbf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.paginationInfo {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.paginationText {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.publishSchedule {
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
.scheduleList {
|
||||||
|
max-height: calc(100vh - 150px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scheduleCard {
|
||||||
|
:global(.ant-card-body) {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardContent {
|
||||||
|
.postDetails {
|
||||||
|
.detailItem {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.detailLabel {
|
||||||
|
min-width: auto;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,371 @@
|
|||||||
|
import React, {
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
forwardRef,
|
||||||
|
useImperativeHandle,
|
||||||
|
} from "react";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
message,
|
||||||
|
Popconfirm,
|
||||||
|
Empty,
|
||||||
|
Spin,
|
||||||
|
Tag,
|
||||||
|
Tooltip,
|
||||||
|
} from "antd";
|
||||||
|
import {
|
||||||
|
DeleteOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
ClockCircleOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
PictureOutlined,
|
||||||
|
VideoCameraOutlined,
|
||||||
|
LinkOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
AppstoreOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import { getMomentList, deleteMoment, listData } from "./api";
|
||||||
|
import EditMomentModal from "./EditMomentModal";
|
||||||
|
import PreviewMomentModal from "./PreviewMomentModal";
|
||||||
|
import styles from "./PublishSchedule.module.scss";
|
||||||
|
|
||||||
|
// 定义组件暴露的方法接口
|
||||||
|
export interface PublishScheduleRef {
|
||||||
|
refresh: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PublishSchedule = forwardRef<PublishScheduleRef>((props, ref) => {
|
||||||
|
const [scheduledPosts, setScheduledPosts] = useState<listData[]>([]);
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [pagination, setPagination] = useState({
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||||
|
const [previewModalVisible, setPreviewModalVisible] = useState(false);
|
||||||
|
const [selectedMoment, setSelectedMoment] = useState<listData | undefined>();
|
||||||
|
|
||||||
|
// 获取内容类型图标和标签
|
||||||
|
const getContentTypeInfo = (type: number) => {
|
||||||
|
switch (type) {
|
||||||
|
case 1:
|
||||||
|
return { icon: <FileTextOutlined />, label: "文本", color: "blue" };
|
||||||
|
case 2:
|
||||||
|
return { icon: <PictureOutlined />, label: "图文", color: "green" };
|
||||||
|
case 3:
|
||||||
|
return {
|
||||||
|
icon: <VideoCameraOutlined />,
|
||||||
|
label: "视频",
|
||||||
|
color: "purple",
|
||||||
|
};
|
||||||
|
case 4:
|
||||||
|
return { icon: <LinkOutlined />, label: "链接", color: "orange" };
|
||||||
|
case 5:
|
||||||
|
return { icon: <AppstoreOutlined />, label: "小程序", color: "cyan" };
|
||||||
|
default:
|
||||||
|
return { icon: <FileTextOutlined />, label: "未知", color: "default" };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化时间显示
|
||||||
|
const formatTime = (timestamp: number) => {
|
||||||
|
if (!timestamp) return "未设置";
|
||||||
|
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 fetchScheduledMoments = useCallback(
|
||||||
|
async (page = 1) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await getMomentList({ page, limit: pagination.limit });
|
||||||
|
setScheduledPosts(response.list);
|
||||||
|
setPagination(prev => ({ ...prev, page, total: response.total }));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取发布计划失败:", error);
|
||||||
|
message.error("获取发布计划失败");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[pagination.limit],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 暴露给父组件的方法
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
refresh: () => {
|
||||||
|
fetchScheduledMoments(pagination.page);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 组件挂载时获取数据
|
||||||
|
useEffect(() => {
|
||||||
|
fetchScheduledMoments();
|
||||||
|
}, [fetchScheduledMoments]);
|
||||||
|
|
||||||
|
const handleDeletePost = async (postId: number) => {
|
||||||
|
try {
|
||||||
|
const success = await deleteMoment({ id: postId });
|
||||||
|
if (success) {
|
||||||
|
setScheduledPosts(prev => prev.filter(post => post.id !== postId));
|
||||||
|
message.success("删除成功");
|
||||||
|
// 如果当前页没有数据了,回到上一页
|
||||||
|
if (scheduledPosts.length === 1 && pagination.page > 1) {
|
||||||
|
fetchScheduledMoments(pagination.page - 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.error("删除失败,请重试");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("删除发布计划失败:", error);
|
||||||
|
message.error("删除失败,请重试");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditPost = (post: listData) => {
|
||||||
|
setSelectedMoment(post);
|
||||||
|
setEditModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewPost = (post: listData) => {
|
||||||
|
setSelectedMoment(post);
|
||||||
|
setPreviewModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditSuccess = () => {
|
||||||
|
// 编辑成功后刷新列表
|
||||||
|
fetchScheduledMoments(pagination.page);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusBadge = (isSend: number) => {
|
||||||
|
switch (isSend) {
|
||||||
|
case 0:
|
||||||
|
return <Badge status="processing" text="待发布" />;
|
||||||
|
case 1:
|
||||||
|
return <Badge status="success" text="已发布" />;
|
||||||
|
case 2:
|
||||||
|
return <Badge status="error" text="发布失败" />;
|
||||||
|
default:
|
||||||
|
return <Badge status="default" text="未知" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
fetchScheduledMoments(pagination.page);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.publishSchedule}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<h3 className={styles.title}>发布计划</h3>
|
||||||
|
<div className={styles.headerActions}>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<ClockCircleOutlined />}
|
||||||
|
onClick={handleRefresh}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.scheduleList}>
|
||||||
|
{loading ? (
|
||||||
|
<div className={styles.loadingContainer}>
|
||||||
|
<Spin size="large" />
|
||||||
|
<div className={styles.loadingText}>加载中...</div>
|
||||||
|
</div>
|
||||||
|
) : scheduledPosts.length === 0 ? (
|
||||||
|
<Empty
|
||||||
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||||
|
description={
|
||||||
|
<div>
|
||||||
|
<div className={styles.emptyText}>暂无发布计划</div>
|
||||||
|
<div className={styles.emptySubText}>
|
||||||
|
创建朋友圈内容后可以设置定时发布
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
scheduledPosts.map(post => {
|
||||||
|
const contentTypeInfo = getContentTypeInfo(post.momentContentType);
|
||||||
|
return (
|
||||||
|
<Card key={post.id} className={styles.scheduleCard}>
|
||||||
|
<div className={styles.cardHeader}>
|
||||||
|
<div className={styles.headerLeft}>
|
||||||
|
<div className={styles.statusBadge}>
|
||||||
|
{getStatusBadge(post.isSend)}
|
||||||
|
</div>
|
||||||
|
<Tag
|
||||||
|
color={contentTypeInfo.color}
|
||||||
|
icon={contentTypeInfo.icon}
|
||||||
|
className={styles.typeTag}
|
||||||
|
>
|
||||||
|
{contentTypeInfo.label}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
<div className={styles.headerActions}>
|
||||||
|
<Tooltip title="预览">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<EyeOutlined />}
|
||||||
|
onClick={() => handleViewPost(post)}
|
||||||
|
size="small"
|
||||||
|
className={styles.actionButton}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="编辑">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={() => handleEditPost(post)}
|
||||||
|
size="small"
|
||||||
|
className={styles.actionButton}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Popconfirm
|
||||||
|
title="确定要删除这条发布计划吗?"
|
||||||
|
onConfirm={() => handleDeletePost(post.id)}
|
||||||
|
okText="确定"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<Tooltip title="删除">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
size="small"
|
||||||
|
className={styles.deleteButton}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Popconfirm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.cardContent}>
|
||||||
|
<div className={styles.postContent}>
|
||||||
|
{post.content || "无文本内容"}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 图片展示 */}
|
||||||
|
{post.picUrlList && post.picUrlList.length > 0 && (
|
||||||
|
<div className={styles.postImages}>
|
||||||
|
{post.picUrlList.slice(0, 3).map((image, index) => (
|
||||||
|
<div key={index} className={styles.imagePlaceholder}>
|
||||||
|
<img
|
||||||
|
src={image}
|
||||||
|
alt={`图片${index + 1}`}
|
||||||
|
className={styles.imagePreview}
|
||||||
|
onError={e => {
|
||||||
|
(e.target as HTMLImageElement).style.display =
|
||||||
|
"none";
|
||||||
|
(
|
||||||
|
e.target as HTMLImageElement
|
||||||
|
).nextElementSibling?.classList.add(styles.show);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className={styles.imageIcon}>
|
||||||
|
<PictureOutlined />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{post.picUrlList.length > 3 && (
|
||||||
|
<div className={styles.moreImages}>
|
||||||
|
+{post.picUrlList.length - 3}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 视频展示 */}
|
||||||
|
{post.videoUrl && (
|
||||||
|
<div className={styles.videoPreview}>
|
||||||
|
<div className={styles.videoIcon}>
|
||||||
|
<VideoCameraOutlined />
|
||||||
|
</div>
|
||||||
|
<span className={styles.videoText}>视频内容</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 链接展示 */}
|
||||||
|
{post.link && post.link.length > 0 && (
|
||||||
|
<div className={styles.linkPreview}>
|
||||||
|
<LinkOutlined className={styles.linkIcon} />
|
||||||
|
<span className={styles.linkText}>
|
||||||
|
{post.link.length} 个链接
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.postDetails}>
|
||||||
|
<div className={styles.detailItem}>
|
||||||
|
<ClockCircleOutlined className={styles.detailIcon} />
|
||||||
|
<span className={styles.detailLabel}>发布时间:</span>
|
||||||
|
<span className={styles.detailValue}>
|
||||||
|
{formatTime(post.sendTime)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.detailItem}>
|
||||||
|
<UserOutlined className={styles.detailIcon} />
|
||||||
|
<span className={styles.detailLabel}>账号数量:</span>
|
||||||
|
<span className={styles.detailValue}>
|
||||||
|
{post.accountCount} 个账号
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.detailItem}>
|
||||||
|
<span className={styles.detailLabel}>创建时间:</span>
|
||||||
|
<span className={styles.detailValue}>
|
||||||
|
{formatTime(post.createTime)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分页信息 */}
|
||||||
|
{scheduledPosts.length > 0 && (
|
||||||
|
<div className={styles.paginationInfo}>
|
||||||
|
<span className={styles.paginationText}>
|
||||||
|
共 {pagination.total} 条记录,当前第 {pagination.page} 页
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 编辑弹窗 */}
|
||||||
|
<EditMomentModal
|
||||||
|
visible={editModalVisible}
|
||||||
|
onCancel={() => setEditModalVisible(false)}
|
||||||
|
onSuccess={handleEditSuccess}
|
||||||
|
momentData={selectedMoment}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 预览弹窗 */}
|
||||||
|
<PreviewMomentModal
|
||||||
|
visible={previewModalVisible}
|
||||||
|
onCancel={() => setPreviewModalVisible(false)}
|
||||||
|
momentData={selectedMoment}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
PublishSchedule.displayName = "PublishSchedule";
|
||||||
|
|
||||||
|
export default PublishSchedule;
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import request from "@/api/request";
|
||||||
|
export interface listData {
|
||||||
|
id: number;
|
||||||
|
content: "";
|
||||||
|
momentContentType: number;
|
||||||
|
picUrlList: string[];
|
||||||
|
videoUrl: string;
|
||||||
|
link: string[];
|
||||||
|
publicMode: number;
|
||||||
|
isSend: number;
|
||||||
|
createTime: number;
|
||||||
|
sendTime: number;
|
||||||
|
accountCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface listResponse {
|
||||||
|
list: listData[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
// 朋友圈定时发布 - 列表
|
||||||
|
export const getMomentList = (data: {
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
}): Promise<listResponse> => {
|
||||||
|
return request("/v1/kefu/moments/list", data, "GET");
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface MomentRequest {
|
||||||
|
id?: number;
|
||||||
|
/**
|
||||||
|
* 朋友圈内容
|
||||||
|
*/
|
||||||
|
content: string;
|
||||||
|
/**
|
||||||
|
* 标签列表
|
||||||
|
*/
|
||||||
|
"labels[]"?: string[];
|
||||||
|
/**
|
||||||
|
* 链接信息-描述 type4有效
|
||||||
|
*/
|
||||||
|
"link[desc]"?: string[];
|
||||||
|
/**
|
||||||
|
* 链接信息-图标 type4有效
|
||||||
|
*/
|
||||||
|
"link[image]"?: string[];
|
||||||
|
/**
|
||||||
|
* 链接信息-链接 type4有效
|
||||||
|
*/
|
||||||
|
"link[url]"?: string[];
|
||||||
|
/**
|
||||||
|
* 图片列表 type2有效
|
||||||
|
*/
|
||||||
|
"picUrlList[]"?: string[];
|
||||||
|
/**
|
||||||
|
* 定时发布时间
|
||||||
|
*/
|
||||||
|
timingTime?: string;
|
||||||
|
/**
|
||||||
|
* 内容类型 1文本 2图文 3视频 4链接
|
||||||
|
*/
|
||||||
|
type: string;
|
||||||
|
/**
|
||||||
|
* 视频链接 type3有效
|
||||||
|
*/
|
||||||
|
videoUrl?: string;
|
||||||
|
/**
|
||||||
|
* 微信账号ID列表
|
||||||
|
*/
|
||||||
|
"wechatIds[]"?: string[];
|
||||||
|
[property: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 朋友圈定时发布 - 添加
|
||||||
|
export const addMoment = (data: MomentRequest) => {
|
||||||
|
return request("/v1/kefu/moments/add", data, "POST");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 朋友圈定时发布 - 编辑
|
||||||
|
export const updateMoment = (data: MomentRequest) => {
|
||||||
|
return request("/v1/kefu/moments/update", data, "POST");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 朋友圈定时发布 - 删除
|
||||||
|
export const deleteMoment = (data: { id: number }) => {
|
||||||
|
return request("/v1/kefu/moments/delete", data, "DELETE");
|
||||||
|
};
|
||||||
@@ -1,9 +1,4 @@
|
|||||||
// 管理组件导出
|
export { default as MomentPublish } from "./MomentPublish";
|
||||||
export { default as MaterialManagement } from "./management/MaterialManagement";
|
export { default as PublishSchedule } from "./PublishSchedule";
|
||||||
export { default as SensitiveWordManagement } from "./management/SensitiveWordManagement";
|
export { default as EditMomentModal } from "./EditMomentModal";
|
||||||
export { default as KeywordManagement } from "./management/KeywordManagement";
|
export { default as PreviewMomentModal } from "./PreviewMomentModal";
|
||||||
|
|
||||||
// 模态框组件导出
|
|
||||||
export { default as MaterialModal } from "./modals/MaterialModal";
|
|
||||||
export { default as SensitiveWordModal } from "./modals/SensitiveWordModal";
|
|
||||||
export { default as KeywordModal } from "./modals/KeywordModal";
|
|
||||||
|
|||||||
@@ -1,318 +0,0 @@
|
|||||||
import React, {
|
|
||||||
useState,
|
|
||||||
useEffect,
|
|
||||||
forwardRef,
|
|
||||||
useImperativeHandle,
|
|
||||||
} from "react";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Input,
|
|
||||||
Tag,
|
|
||||||
Switch,
|
|
||||||
message,
|
|
||||||
Popconfirm,
|
|
||||||
Pagination,
|
|
||||||
} from "antd";
|
|
||||||
import {
|
|
||||||
SearchOutlined,
|
|
||||||
FormOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
import styles from "./index.module.scss";
|
|
||||||
import {
|
|
||||||
getKeywordList,
|
|
||||||
deleteKeyword,
|
|
||||||
setKeywordStatus,
|
|
||||||
type KeywordListParams,
|
|
||||||
} from "../../api";
|
|
||||||
import KeywordModal from "../modals/KeywordModal";
|
|
||||||
|
|
||||||
const { Search } = Input;
|
|
||||||
|
|
||||||
interface KeywordItem {
|
|
||||||
id?: number;
|
|
||||||
type: number;
|
|
||||||
replyType: number;
|
|
||||||
title: string;
|
|
||||||
keywords: string;
|
|
||||||
status: number;
|
|
||||||
content: string;
|
|
||||||
metailGroupsOptions: { title: string; id: number }[];
|
|
||||||
level: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const KeywordManagement = forwardRef<any, Record<string, never>>(
|
|
||||||
(props, ref) => {
|
|
||||||
const [searchValue, setSearchValue] = useState<string>("");
|
|
||||||
const [keywordsList, setKeywordsList] = useState<KeywordItem[]>([]);
|
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
|
||||||
const [editModalVisible, setEditModalVisible] = useState<boolean>(false);
|
|
||||||
const [editingKeywordId, setEditingKeywordId] = useState<number | null>(
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
const [pagination, setPagination] = useState({
|
|
||||||
current: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
total: 0,
|
|
||||||
});
|
|
||||||
// 已提交的搜索关键词(仅在点击搜索时更新,用于服务端查询)
|
|
||||||
const [keywordQuery, setKeywordQuery] = useState<string>("");
|
|
||||||
|
|
||||||
//匹配类型
|
|
||||||
const getMatchTypeText = (type: number) => {
|
|
||||||
switch (type) {
|
|
||||||
case 0:
|
|
||||||
return "模糊匹配";
|
|
||||||
case 1:
|
|
||||||
return "精确匹配";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//匹配优先级
|
|
||||||
const getPriorityText = (level: number) => {
|
|
||||||
switch (level) {
|
|
||||||
case 0:
|
|
||||||
return "低优先级";
|
|
||||||
case 1:
|
|
||||||
return "中优先级";
|
|
||||||
case 2:
|
|
||||||
return "高优先级";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// 回复类型映射
|
|
||||||
const getReplyTypeText = (replyType: number) => {
|
|
||||||
switch (replyType) {
|
|
||||||
case 0:
|
|
||||||
return "素材回复";
|
|
||||||
case 1:
|
|
||||||
return "自定义";
|
|
||||||
default:
|
|
||||||
return "未知类型";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 回复类型颜色
|
|
||||||
const getReplyTypeColor = (replyType: number) => {
|
|
||||||
switch (replyType) {
|
|
||||||
case 0:
|
|
||||||
return "blue";
|
|
||||||
case 1:
|
|
||||||
return "purple";
|
|
||||||
default:
|
|
||||||
return "gray";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取关键词列表(服务端搜索)
|
|
||||||
const fetchKeywords = async (params?: KeywordListParams) => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const requestParams = {
|
|
||||||
page: pagination.current.toString(),
|
|
||||||
limit: pagination.pageSize.toString(),
|
|
||||||
keyword: keywordQuery || undefined,
|
|
||||||
...params,
|
|
||||||
} as KeywordListParams;
|
|
||||||
const response = await getKeywordList(requestParams);
|
|
||||||
if (response) {
|
|
||||||
setKeywordsList(response.list || []);
|
|
||||||
setPagination(prev => ({
|
|
||||||
...prev,
|
|
||||||
total: response.total || 0,
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
setKeywordsList([]);
|
|
||||||
message.error(response?.message || "获取关键词列表失败");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("获取关键词列表失败:", error);
|
|
||||||
setKeywordsList([]);
|
|
||||||
message.error("获取关键词列表失败");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 暴露方法给父组件
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
fetchKeywords,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 关键词管理相关函数
|
|
||||||
const handleToggleKeyword = async (id: number) => {
|
|
||||||
try {
|
|
||||||
await setKeywordStatus({ id });
|
|
||||||
setKeywordsList(prev =>
|
|
||||||
prev.map(item =>
|
|
||||||
item.id === id
|
|
||||||
? { ...item, status: item.status === 1 ? 0 : 1 }
|
|
||||||
: item,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
message.success("状态更新成功");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("状态更新失败:", error);
|
|
||||||
message.error("状态更新失败");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditKeyword = (id: number) => {
|
|
||||||
setEditingKeywordId(id);
|
|
||||||
setEditModalVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 编辑弹窗成功回调
|
|
||||||
const handleEditSuccess = () => {
|
|
||||||
fetchKeywords(); // 重新获取数据
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteKeyword = async (id: number) => {
|
|
||||||
try {
|
|
||||||
await deleteKeyword(id);
|
|
||||||
setKeywordsList(prev => prev.filter(item => item.id !== id));
|
|
||||||
message.success("删除成功");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("删除失败:", error);
|
|
||||||
message.error("删除失败");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 移除本地筛选,改为服务端搜索,列表直接使用 keywordsList
|
|
||||||
|
|
||||||
// 搜索处理函数
|
|
||||||
const handleSearch = (value: string) => {
|
|
||||||
setKeywordQuery(value || "");
|
|
||||||
setPagination(prev => ({ ...prev, current: 1 }));
|
|
||||||
};
|
|
||||||
|
|
||||||
// 分页处理函数
|
|
||||||
const handlePageChange = (page: number, pageSize?: number) => {
|
|
||||||
setPagination(prev => ({
|
|
||||||
...prev,
|
|
||||||
current: page,
|
|
||||||
pageSize: pageSize || prev.pageSize,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
// 初始化与依赖变化时获取数据(依赖分页与搜索关键字)
|
|
||||||
useEffect(() => {
|
|
||||||
fetchKeywords();
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [pagination.current, pagination.pageSize, keywordQuery]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.keywordContent}>
|
|
||||||
<div className={styles.searchSection}>
|
|
||||||
<Search
|
|
||||||
placeholder="搜索关键词..."
|
|
||||||
value={searchValue}
|
|
||||||
onChange={e => setSearchValue(e.target.value)}
|
|
||||||
onSearch={handleSearch}
|
|
||||||
style={{ width: 300 }}
|
|
||||||
prefix={<SearchOutlined />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.keywordList}>
|
|
||||||
{loading ? (
|
|
||||||
<div className={styles.loading}>加载中...</div>
|
|
||||||
) : keywordsList.length === 0 ? (
|
|
||||||
<div className={styles.empty}>暂无关键词数据</div>
|
|
||||||
) : (
|
|
||||||
keywordsList.map(item => (
|
|
||||||
<div key={item.id} className={styles.keywordItem}>
|
|
||||||
<div className={styles.itemContent}>
|
|
||||||
<div className={styles.leftSection}>
|
|
||||||
<div className={styles.titleRow}>
|
|
||||||
<div className={styles.title}>{item.title}</div>
|
|
||||||
<Tag color="default">{getMatchTypeText(item.type)}</Tag>
|
|
||||||
<Tag color="default">{getPriorityText(item.level)}</Tag>
|
|
||||||
</div>
|
|
||||||
{item.content.length ? (
|
|
||||||
<div className={styles.description}>{item.content}</div>
|
|
||||||
) : (
|
|
||||||
<div className={styles.description}>
|
|
||||||
已选素材:
|
|
||||||
{item.metailGroupsOptions.map(v => (
|
|
||||||
<Tag color="success" key={v.id}>
|
|
||||||
{v.title}
|
|
||||||
</Tag>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={styles.footer}>
|
|
||||||
<Tag color={getReplyTypeColor(item.replyType)}>
|
|
||||||
{getReplyTypeText(item.replyType)}
|
|
||||||
</Tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.rightSection}>
|
|
||||||
<Switch
|
|
||||||
checked={item.status === 1}
|
|
||||||
onChange={() => handleToggleKeyword(item.id)}
|
|
||||||
className={styles.toggleSwitch}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
icon={<FormOutlined className={styles.editIcon} />}
|
|
||||||
onClick={() => handleEditKeyword(item.id)}
|
|
||||||
className={styles.actionBtn}
|
|
||||||
/>
|
|
||||||
<Popconfirm
|
|
||||||
title="确认删除"
|
|
||||||
description="确定要删除这个关键词吗?删除后无法恢复。"
|
|
||||||
onConfirm={() => handleDeleteKeyword(item.id)}
|
|
||||||
okText="确定"
|
|
||||||
cancelText="取消"
|
|
||||||
okType="danger"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
icon={<DeleteOutlined className={styles.deleteIcon} />}
|
|
||||||
className={styles.actionBtn}
|
|
||||||
/>
|
|
||||||
</Popconfirm>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 分页组件 */}
|
|
||||||
<div style={{ marginTop: 16, textAlign: "right" }}>
|
|
||||||
<Pagination
|
|
||||||
current={pagination.current}
|
|
||||||
pageSize={pagination.pageSize}
|
|
||||||
total={pagination.total}
|
|
||||||
showSizeChanger
|
|
||||||
showQuickJumper
|
|
||||||
showTotal={(total, range) =>
|
|
||||||
`第 ${range[0]}-${range[1]} 条/共 ${total} 条`
|
|
||||||
}
|
|
||||||
onChange={handlePageChange}
|
|
||||||
onShowSizeChange={handlePageChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 编辑弹窗 */}
|
|
||||||
<KeywordModal
|
|
||||||
visible={editModalVisible}
|
|
||||||
mode="edit"
|
|
||||||
keywordId={editingKeywordId}
|
|
||||||
onCancel={() => {
|
|
||||||
setEditModalVisible(false);
|
|
||||||
setEditingKeywordId(null);
|
|
||||||
}}
|
|
||||||
onSuccess={handleEditSuccess}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
KeywordManagement.displayName = "KeywordManagement";
|
|
||||||
|
|
||||||
export default KeywordManagement;
|
|
||||||
@@ -1,320 +0,0 @@
|
|||||||
import React, {
|
|
||||||
useState,
|
|
||||||
useEffect,
|
|
||||||
forwardRef,
|
|
||||||
useImperativeHandle,
|
|
||||||
useRef,
|
|
||||||
} from "react";
|
|
||||||
import { Button, Input, Card, message, Popconfirm, Pagination } from "antd";
|
|
||||||
import {
|
|
||||||
SearchOutlined,
|
|
||||||
FilterOutlined,
|
|
||||||
FormOutlined,
|
|
||||||
FileTextOutlined,
|
|
||||||
FileImageOutlined,
|
|
||||||
PlayCircleOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
import styles from "../../index.module.scss";
|
|
||||||
import {
|
|
||||||
getMaterialList,
|
|
||||||
deleteMaterial,
|
|
||||||
type MaterialListParams,
|
|
||||||
} from "../../api";
|
|
||||||
import MaterialModal from "../modals/MaterialModal";
|
|
||||||
|
|
||||||
const { Search } = Input;
|
|
||||||
|
|
||||||
interface MaterialItem {
|
|
||||||
id: number;
|
|
||||||
companyId: number;
|
|
||||||
userId: number;
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
cover: string;
|
|
||||||
status: number;
|
|
||||||
type: string; // 素材类型:文本、图片、视频
|
|
||||||
createTime: string;
|
|
||||||
updateTime: string;
|
|
||||||
isDel: number;
|
|
||||||
delTime: string | null;
|
|
||||||
userName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MaterialManagement = forwardRef<any, Record<string, never>>(
|
|
||||||
(props, ref) => {
|
|
||||||
const [searchValue, setSearchValue] = useState<string>("");
|
|
||||||
const [materialsList, setMaterialsList] = useState<MaterialItem[]>([]);
|
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
|
||||||
const [editModalVisible, setEditModalVisible] = useState<boolean>(false);
|
|
||||||
const [editingMaterialId, setEditingMaterialId] = useState<number | null>(
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
const [pagination, setPagination] = useState({
|
|
||||||
current: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
total: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 使用 ref 来存储最新的分页状态
|
|
||||||
const paginationRef = useRef(pagination);
|
|
||||||
paginationRef.current = pagination;
|
|
||||||
|
|
||||||
// 获取类型图标
|
|
||||||
const getTypeIcon = (type: string) => {
|
|
||||||
switch (type) {
|
|
||||||
case "文本":
|
|
||||||
return <FileTextOutlined className={styles.typeIcon} />;
|
|
||||||
case "图片":
|
|
||||||
return <FileImageOutlined className={styles.typeIcon} />;
|
|
||||||
case "视频":
|
|
||||||
return <PlayCircleOutlined className={styles.typeIcon} />;
|
|
||||||
default:
|
|
||||||
return <FileTextOutlined className={styles.typeIcon} />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取素材列表
|
|
||||||
const fetchMaterials = async (params?: MaterialListParams) => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const currentPagination = paginationRef.current;
|
|
||||||
const requestParams = {
|
|
||||||
page: currentPagination.current.toString(),
|
|
||||||
limit: currentPagination.pageSize.toString(),
|
|
||||||
...params,
|
|
||||||
};
|
|
||||||
const response = await getMaterialList(requestParams);
|
|
||||||
if (response) {
|
|
||||||
setMaterialsList(response.list || []);
|
|
||||||
setPagination(prev => ({
|
|
||||||
...prev,
|
|
||||||
total: response.total || 0,
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
setMaterialsList([]);
|
|
||||||
message.error(response?.message || "获取素材列表失败");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("获取素材列表失败:", error);
|
|
||||||
setMaterialsList([]);
|
|
||||||
message.error("获取素材列表失败");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 暴露方法给父组件
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
fetchMaterials,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 素材管理相关函数
|
|
||||||
const handleDeleteMaterial = async (id: number) => {
|
|
||||||
try {
|
|
||||||
await deleteMaterial(id.toString());
|
|
||||||
setMaterialsList(prev => prev.filter(item => item.id !== id));
|
|
||||||
message.success("删除成功");
|
|
||||||
} catch (error) {
|
|
||||||
message.error("删除失败");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 编辑素材
|
|
||||||
const handleEditMaterial = (id: number) => {
|
|
||||||
setEditingMaterialId(id);
|
|
||||||
setEditModalVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 编辑弹窗成功回调
|
|
||||||
const handleEditSuccess = () => {
|
|
||||||
fetchMaterials(); // 重新获取数据
|
|
||||||
};
|
|
||||||
|
|
||||||
// 搜索处理函数
|
|
||||||
const handleSearch = (value: string) => {
|
|
||||||
setPagination(prev => ({ ...prev, current: 1 }));
|
|
||||||
fetchMaterials({ keyword: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
// 分页处理函数
|
|
||||||
const handlePageChange = (page: number, pageSize?: number) => {
|
|
||||||
setPagination(prev => ({
|
|
||||||
...prev,
|
|
||||||
current: page,
|
|
||||||
pageSize: pageSize || prev.pageSize,
|
|
||||||
}));
|
|
||||||
// 分页变化后立即获取数据
|
|
||||||
setTimeout(() => {
|
|
||||||
fetchMaterials();
|
|
||||||
}, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 组件挂载时获取数据
|
|
||||||
useEffect(() => {
|
|
||||||
fetchMaterials();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.materialContent}>
|
|
||||||
<div className={styles.searchSection}>
|
|
||||||
<Search
|
|
||||||
placeholder="搜索素材..."
|
|
||||||
value={searchValue}
|
|
||||||
onChange={e => setSearchValue(e.target.value)}
|
|
||||||
onSearch={handleSearch}
|
|
||||||
style={{ width: 300 }}
|
|
||||||
prefix={<SearchOutlined />}
|
|
||||||
/>
|
|
||||||
<Button icon={<FilterOutlined />}>筛选</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.materialGrid}>
|
|
||||||
{loading ? (
|
|
||||||
<div className={styles.loading}>加载中...</div>
|
|
||||||
) : materialsList.length === 0 ? (
|
|
||||||
<div className={styles.empty}>暂无素材数据</div>
|
|
||||||
) : (
|
|
||||||
materialsList.map(item => (
|
|
||||||
<Card
|
|
||||||
key={item.id}
|
|
||||||
className={styles.materialCard}
|
|
||||||
hoverable
|
|
||||||
actions={[
|
|
||||||
<Button
|
|
||||||
key="edit"
|
|
||||||
type="text"
|
|
||||||
icon={<FormOutlined />}
|
|
||||||
onClick={e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleEditMaterial(item.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
编辑
|
|
||||||
</Button>,
|
|
||||||
<Popconfirm
|
|
||||||
key="delete"
|
|
||||||
title="确认删除"
|
|
||||||
description="确定要删除这个素材吗?删除后无法恢复。"
|
|
||||||
onConfirm={() => handleDeleteMaterial(item.id)}
|
|
||||||
okText="确定"
|
|
||||||
cancelText="取消"
|
|
||||||
okType="danger"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
danger
|
|
||||||
icon={<DeleteOutlined />}
|
|
||||||
onClick={e => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={styles.thumbnail}
|
|
||||||
onClick={() => handleEditMaterial(item.id)}
|
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
>
|
|
||||||
{item.cover ? (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "relative",
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={item.cover}
|
|
||||||
alt={item.title}
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
objectFit: "cover",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: "4px",
|
|
||||||
right: "4px",
|
|
||||||
background: "rgba(0, 0, 0, 0.6)",
|
|
||||||
color: "white",
|
|
||||||
padding: "2px 6px",
|
|
||||||
borderRadius: "4px",
|
|
||||||
fontSize: "10px",
|
|
||||||
fontWeight: "500",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.type}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
background: "#f5f5f5",
|
|
||||||
color: "#999",
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{getTypeIcon(item.type)}
|
|
||||||
<span style={{ marginTop: "8px", fontSize: "12px" }}>
|
|
||||||
{item.type}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.cardContent}>
|
|
||||||
<div className={styles.title}>{item.title}</div>
|
|
||||||
<div className={styles.meta}>
|
|
||||||
<div>创建人: {item.userName}</div>
|
|
||||||
<div>{item.createTime}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 分页组件 */}
|
|
||||||
<div style={{ marginTop: 16, textAlign: "right" }}>
|
|
||||||
<Pagination
|
|
||||||
current={pagination.current}
|
|
||||||
pageSize={pagination.pageSize}
|
|
||||||
total={pagination.total}
|
|
||||||
showSizeChanger
|
|
||||||
showQuickJumper
|
|
||||||
showTotal={(total, range) =>
|
|
||||||
`第 ${range[0]}-${range[1]} 条/共 ${total} 条`
|
|
||||||
}
|
|
||||||
onChange={handlePageChange}
|
|
||||||
onShowSizeChange={handlePageChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 编辑弹窗 */}
|
|
||||||
<MaterialModal
|
|
||||||
visible={editModalVisible}
|
|
||||||
mode="edit"
|
|
||||||
materialId={editingMaterialId}
|
|
||||||
onCancel={() => {
|
|
||||||
setEditModalVisible(false);
|
|
||||||
setEditingMaterialId(null);
|
|
||||||
}}
|
|
||||||
onSuccess={handleEditSuccess}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
MaterialManagement.displayName = "MaterialManagement";
|
|
||||||
|
|
||||||
export default MaterialManagement;
|
|
||||||
@@ -1,293 +0,0 @@
|
|||||||
import React, {
|
|
||||||
useState,
|
|
||||||
useEffect,
|
|
||||||
forwardRef,
|
|
||||||
useImperativeHandle,
|
|
||||||
} from "react";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Input,
|
|
||||||
Tag,
|
|
||||||
Switch,
|
|
||||||
message,
|
|
||||||
Popconfirm,
|
|
||||||
Pagination,
|
|
||||||
} from "antd";
|
|
||||||
import {
|
|
||||||
SearchOutlined,
|
|
||||||
FilterOutlined,
|
|
||||||
FormOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
import styles from "../../index.module.scss";
|
|
||||||
import {
|
|
||||||
getSensitiveWordList,
|
|
||||||
deleteSensitiveWord,
|
|
||||||
setSensitiveWordStatus,
|
|
||||||
type SensitiveWordListParams,
|
|
||||||
} from "../../api";
|
|
||||||
import SensitiveWordModal from "../modals/SensitiveWordModal";
|
|
||||||
|
|
||||||
const { Search } = Input;
|
|
||||||
|
|
||||||
interface SensitiveWordItem {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
keywords: string;
|
|
||||||
content: string;
|
|
||||||
operation: number;
|
|
||||||
status: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SensitiveWordManagement = forwardRef<any, Record<string, never>>(
|
|
||||||
(props, ref) => {
|
|
||||||
const [searchValue, setSearchValue] = useState<string>("");
|
|
||||||
const [sensitiveWordsList, setSensitiveWordsList] = useState<
|
|
||||||
SensitiveWordItem[]
|
|
||||||
>([]);
|
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
|
||||||
const [editModalVisible, setEditModalVisible] = useState<boolean>(false);
|
|
||||||
const [editingSensitiveWordId, setEditingSensitiveWordId] = useState<
|
|
||||||
string | null
|
|
||||||
>(null);
|
|
||||||
const [pagination, setPagination] = useState({
|
|
||||||
current: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
total: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const getTagColor = (tag: string) => {
|
|
||||||
switch (tag) {
|
|
||||||
case "政治":
|
|
||||||
return "#ff4d4f";
|
|
||||||
case "色情":
|
|
||||||
return "#ff4d4f";
|
|
||||||
case "暴力":
|
|
||||||
return "#ff4d4f";
|
|
||||||
default:
|
|
||||||
return "#ff4d4f";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 操作类型映射
|
|
||||||
const getOperationText = (operation: number) => {
|
|
||||||
switch (operation) {
|
|
||||||
case 0:
|
|
||||||
return "不操作";
|
|
||||||
case 1:
|
|
||||||
return "替换";
|
|
||||||
case 2:
|
|
||||||
return "删除";
|
|
||||||
case 3:
|
|
||||||
return "警告";
|
|
||||||
case 4:
|
|
||||||
return "禁止发送";
|
|
||||||
default:
|
|
||||||
return "未知操作";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取敏感词列表
|
|
||||||
const fetchSensitiveWords = async (params?: SensitiveWordListParams) => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const requestParams = {
|
|
||||||
page: pagination.current.toString(),
|
|
||||||
limit: pagination.pageSize.toString(),
|
|
||||||
...params,
|
|
||||||
};
|
|
||||||
const response = await getSensitiveWordList(requestParams);
|
|
||||||
if (response) {
|
|
||||||
setSensitiveWordsList(response.list || []);
|
|
||||||
setPagination(prev => ({
|
|
||||||
...prev,
|
|
||||||
total: response.total || 0,
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
setSensitiveWordsList([]);
|
|
||||||
message.error(response?.message || "获取敏感词列表失败");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("获取敏感词列表失败:", error);
|
|
||||||
setSensitiveWordsList([]);
|
|
||||||
message.error("获取敏感词列表失败");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 暴露方法给父组件
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
fetchSensitiveWords,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 敏感词管理相关函数
|
|
||||||
const handleToggleSensitiveWord = async (id: string) => {
|
|
||||||
try {
|
|
||||||
const response = await setSensitiveWordStatus({ id });
|
|
||||||
if (response) {
|
|
||||||
setSensitiveWordsList(prev =>
|
|
||||||
prev.map(item =>
|
|
||||||
item.id === id
|
|
||||||
? { ...item, status: item.status === 1 ? 0 : 1 }
|
|
||||||
: item,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
message.success("状态更新成功");
|
|
||||||
} else {
|
|
||||||
message.error(response?.message || "状态更新失败");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("状态更新失败:", error);
|
|
||||||
message.error("状态更新失败");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditSensitiveWord = (id: string) => {
|
|
||||||
setEditingSensitiveWordId(id);
|
|
||||||
setEditModalVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 编辑弹窗成功回调
|
|
||||||
const handleEditSuccess = () => {
|
|
||||||
fetchSensitiveWords(); // 重新获取数据
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteSensitiveWord = async (id: string) => {
|
|
||||||
try {
|
|
||||||
await deleteSensitiveWord(id);
|
|
||||||
setSensitiveWordsList(prev => prev.filter(item => item.id !== id));
|
|
||||||
message.success("删除成功");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("删除失败:", error);
|
|
||||||
message.error("删除失败");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 搜索和筛选功能
|
|
||||||
const filteredSensitiveWords = sensitiveWordsList.filter(item => {
|
|
||||||
if (!searchValue) return true;
|
|
||||||
return (
|
|
||||||
item.title.toLowerCase().includes(searchValue.toLowerCase()) ||
|
|
||||||
item.keywords.toLowerCase().includes(searchValue.toLowerCase()) ||
|
|
||||||
item.content.toLowerCase().includes(searchValue.toLowerCase())
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 搜索处理函数
|
|
||||||
const handleSearch = (value: string) => {
|
|
||||||
setPagination(prev => ({ ...prev, current: 1 }));
|
|
||||||
fetchSensitiveWords({ keyword: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
// 分页处理函数
|
|
||||||
const handlePageChange = (page: number, pageSize?: number) => {
|
|
||||||
setPagination(prev => ({
|
|
||||||
...prev,
|
|
||||||
current: page,
|
|
||||||
pageSize: pageSize || prev.pageSize,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
// 组件挂载和分页变化时获取数据
|
|
||||||
useEffect(() => {
|
|
||||||
fetchSensitiveWords();
|
|
||||||
}, [pagination.current, pagination.pageSize]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.sensitiveContent}>
|
|
||||||
<div className={styles.searchSection}>
|
|
||||||
<Search
|
|
||||||
placeholder="搜索敏感词..."
|
|
||||||
value={searchValue}
|
|
||||||
onChange={e => setSearchValue(e.target.value)}
|
|
||||||
onSearch={handleSearch}
|
|
||||||
style={{ width: 300 }}
|
|
||||||
prefix={<SearchOutlined />}
|
|
||||||
/>
|
|
||||||
<Button icon={<FilterOutlined />}>筛选</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.sensitiveList}>
|
|
||||||
{loading ? (
|
|
||||||
<div className={styles.loading}>加载中...</div>
|
|
||||||
) : filteredSensitiveWords.length === 0 ? (
|
|
||||||
<div className={styles.empty}>暂无敏感词数据</div>
|
|
||||||
) : (
|
|
||||||
filteredSensitiveWords.map(item => (
|
|
||||||
<div key={item.id} className={styles.sensitiveItem}>
|
|
||||||
<div className={styles.itemContent}>
|
|
||||||
<div className={styles.categoryName}>{item.title}</div>
|
|
||||||
<div className={styles.actionText}>
|
|
||||||
{getOperationText(item.operation)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.itemActions}>
|
|
||||||
<Switch
|
|
||||||
checked={item.status == 1}
|
|
||||||
onChange={() => handleToggleSensitiveWord(item.id)}
|
|
||||||
className={styles.toggleSwitch}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
icon={<FormOutlined className={styles.editIcon} />}
|
|
||||||
onClick={() => handleEditSensitiveWord(item.id)}
|
|
||||||
className={styles.actionBtn}
|
|
||||||
/>
|
|
||||||
<Popconfirm
|
|
||||||
title="确认删除"
|
|
||||||
description="确定要删除这个敏感词吗?删除后无法恢复。"
|
|
||||||
onConfirm={() => handleDeleteSensitiveWord(item.id)}
|
|
||||||
okText="确定"
|
|
||||||
cancelText="取消"
|
|
||||||
okType="danger"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
icon={<DeleteOutlined className={styles.deleteIcon} />}
|
|
||||||
className={styles.actionBtn}
|
|
||||||
/>
|
|
||||||
</Popconfirm>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 分页组件 */}
|
|
||||||
<div style={{ marginTop: 16, textAlign: "right" }}>
|
|
||||||
<Pagination
|
|
||||||
current={pagination.current}
|
|
||||||
pageSize={pagination.pageSize}
|
|
||||||
total={pagination.total}
|
|
||||||
showSizeChanger
|
|
||||||
showQuickJumper
|
|
||||||
showTotal={(total, range) =>
|
|
||||||
`第 ${range[0]}-${range[1]} 条/共 ${total} 条`
|
|
||||||
}
|
|
||||||
onChange={handlePageChange}
|
|
||||||
onShowSizeChange={handlePageChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 编辑弹窗 */}
|
|
||||||
<SensitiveWordModal
|
|
||||||
visible={editModalVisible}
|
|
||||||
mode="edit"
|
|
||||||
sensitiveWordId={editingSensitiveWordId}
|
|
||||||
onCancel={() => {
|
|
||||||
setEditModalVisible(false);
|
|
||||||
setEditingSensitiveWordId(null);
|
|
||||||
}}
|
|
||||||
onSuccess={handleEditSuccess}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
SensitiveWordManagement.displayName = "SensitiveWordManagement";
|
|
||||||
|
|
||||||
export default SensitiveWordManagement;
|
|
||||||
@@ -1,252 +0,0 @@
|
|||||||
// 关键词管理样式
|
|
||||||
.keywordContent {
|
|
||||||
.searchSection {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
|
|
||||||
:global(.ant-input-search) {
|
|
||||||
width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.ant-btn) {
|
|
||||||
height: 32px;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.keywordList {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.keywordItem {
|
|
||||||
padding: 16px 20px;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #f0f0f0;
|
|
||||||
border-radius: 8px;
|
|
||||||
transition: all 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: #d9d9d9;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemContent {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 16px;
|
|
||||||
|
|
||||||
.leftSection {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
.titleRow {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #262626;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
.matchTag,
|
|
||||||
.priorityTag {
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #f0f0f0;
|
|
||||||
color: #666;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
line-height: 1.5;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.replyTypeTag {
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 20px;
|
|
||||||
background: #fff;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.rightSection {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
.toggleSwitch {
|
|
||||||
:global(.ant-switch) {
|
|
||||||
background-color: #d9d9d9;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.ant-switch-checked) {
|
|
||||||
background-color: #1890ff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.actionBtn {
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
padding: 0;
|
|
||||||
border-radius: 4px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editIcon {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #1890ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deleteIcon {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #ff4d4f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 响应式设计
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.container {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.headerActions {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
|
|
||||||
:global(.ant-btn) {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs {
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
.tab {
|
|
||||||
border-bottom: 1px solid #e8e8e8;
|
|
||||||
border-radius: 0;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.materialContent {
|
|
||||||
.searchSection {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: stretch;
|
|
||||||
|
|
||||||
:global(.ant-input-search) {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.materialGrid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sensitiveContent {
|
|
||||||
.searchSection {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: stretch;
|
|
||||||
|
|
||||||
:global(.ant-input-search) {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sensitiveItem {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
gap: 12px;
|
|
||||||
|
|
||||||
.itemContent {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
.categoryName {
|
|
||||||
min-width: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemActions {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.keywordContent {
|
|
||||||
.searchSection {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: stretch;
|
|
||||||
|
|
||||||
:global(.ant-input-search) {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.keywordItem {
|
|
||||||
.itemContent {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
|
|
||||||
.leftSection {
|
|
||||||
.titleRow {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
.tags {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.rightSection {
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
// 管理组件统一导出
|
|
||||||
export { default as MaterialManagement } from "./MaterialManagement";
|
|
||||||
export { default as SensitiveWordManagement } from "./SensitiveWordManagement";
|
|
||||||
export { default as KeywordManagement } from "./KeywordManagement";
|
|
||||||
@@ -1,276 +0,0 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import { Button, Input, Select } from "antd";
|
|
||||||
import {
|
|
||||||
PlusOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
FileTextOutlined,
|
|
||||||
FileImageOutlined,
|
|
||||||
PlayCircleOutlined,
|
|
||||||
FileOutlined,
|
|
||||||
SoundOutlined,
|
|
||||||
LinkOutlined,
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
import ImageUpload from "@/components/Upload/ImageUpload/ImageUpload";
|
|
||||||
import VideoUpload from "@/components/Upload/VideoUpload";
|
|
||||||
import FileUpload from "@/components/Upload/FileUpload";
|
|
||||||
import AudioUpload from "@/components/Upload/AudioUpload";
|
|
||||||
import type { ContentItem, LinkData } from "../../api";
|
|
||||||
|
|
||||||
const { TextArea } = Input;
|
|
||||||
const { Option } = Select;
|
|
||||||
|
|
||||||
interface ContentManagerProps {
|
|
||||||
value?: ContentItem[];
|
|
||||||
onChange?: (content: ContentItem[]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ContentManager: React.FC<ContentManagerProps> = ({
|
|
||||||
value = [],
|
|
||||||
onChange,
|
|
||||||
}) => {
|
|
||||||
const [contentItems, setContentItems] = useState<ContentItem[]>(value);
|
|
||||||
|
|
||||||
// 内容类型配置
|
|
||||||
const contentTypes = [
|
|
||||||
{ value: "text", label: "文本", icon: <FileTextOutlined /> },
|
|
||||||
{ value: "image", label: "图片", icon: <FileImageOutlined /> },
|
|
||||||
{ value: "video", label: "视频", icon: <PlayCircleOutlined /> },
|
|
||||||
{ value: "file", label: "文件", icon: <FileOutlined /> },
|
|
||||||
{ value: "audio", label: "音频", icon: <SoundOutlined /> },
|
|
||||||
{ value: "link", label: "链接", icon: <LinkOutlined /> },
|
|
||||||
];
|
|
||||||
|
|
||||||
// 同步外部value到内部state
|
|
||||||
useEffect(() => {
|
|
||||||
setContentItems(value);
|
|
||||||
}, [value]);
|
|
||||||
|
|
||||||
// 初始化时添加默认文本内容项
|
|
||||||
useEffect(() => {
|
|
||||||
if (contentItems.length === 0) {
|
|
||||||
const defaultTextItem: ContentItem = {
|
|
||||||
type: "text",
|
|
||||||
data: "",
|
|
||||||
};
|
|
||||||
setContentItems([defaultTextItem]);
|
|
||||||
onChange?.([defaultTextItem]);
|
|
||||||
}
|
|
||||||
}, [contentItems.length, onChange]);
|
|
||||||
|
|
||||||
// 更新内容项
|
|
||||||
const updateContentItems = (newItems: ContentItem[]) => {
|
|
||||||
setContentItems(newItems);
|
|
||||||
onChange?.(newItems);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 添加新内容项
|
|
||||||
const handleAddItem = () => {
|
|
||||||
const newItem: ContentItem = {
|
|
||||||
type: "text",
|
|
||||||
data: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
const newItems = [...contentItems, newItem];
|
|
||||||
updateContentItems(newItems);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 删除内容项
|
|
||||||
const handleDeleteItem = (index: number) => {
|
|
||||||
const newItems = contentItems.filter((_, i) => i !== index);
|
|
||||||
updateContentItems(newItems);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 更新内容项数据
|
|
||||||
const updateItemData = (index: number, data: any) => {
|
|
||||||
const newItems = [...contentItems];
|
|
||||||
newItems[index] = { ...newItems[index], data };
|
|
||||||
updateContentItems(newItems);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 更新内容项类型
|
|
||||||
const updateItemType = (index: number, newType: string) => {
|
|
||||||
const newItems = [...contentItems];
|
|
||||||
|
|
||||||
// 根据新类型重置数据
|
|
||||||
let newData: any;
|
|
||||||
if (newType === "link") {
|
|
||||||
newData = { title: "", url: "", cover: "" };
|
|
||||||
} else {
|
|
||||||
newData = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
newItems[index] = {
|
|
||||||
type: newType as any,
|
|
||||||
data: newData,
|
|
||||||
};
|
|
||||||
updateContentItems(newItems);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 渲染内容项
|
|
||||||
const renderContentItem = (item: ContentItem, index: number) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
style={{
|
|
||||||
border: "1px solid #d9d9d9",
|
|
||||||
borderRadius: "6px",
|
|
||||||
padding: "12px",
|
|
||||||
marginBottom: "8px",
|
|
||||||
backgroundColor: "#fafafa",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
marginBottom: "8px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
|
||||||
<Select
|
|
||||||
value={item.type}
|
|
||||||
onChange={newType => updateItemType(index, newType)}
|
|
||||||
style={{ width: 120 }}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{contentTypes.map(type => (
|
|
||||||
<Option key={type.value} value={type.value}>
|
|
||||||
{type.icon} {type.label}
|
|
||||||
</Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
{index !== 0 && (
|
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
danger
|
|
||||||
size="small"
|
|
||||||
icon={<DeleteOutlined />}
|
|
||||||
onClick={() => handleDeleteItem(index)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{renderContentInput(item, index)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 渲染内容输入
|
|
||||||
const renderContentInput = (item: ContentItem, index: number) => {
|
|
||||||
switch (item.type) {
|
|
||||||
case "text":
|
|
||||||
return (
|
|
||||||
<TextArea
|
|
||||||
value={item.data as string}
|
|
||||||
onChange={e => updateItemData(index, e.target.value)}
|
|
||||||
placeholder="请输入文本内容"
|
|
||||||
rows={3}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case "image":
|
|
||||||
return (
|
|
||||||
<ImageUpload
|
|
||||||
count={1}
|
|
||||||
accept="image/*"
|
|
||||||
value={item.data ? [item.data as string] : []}
|
|
||||||
onChange={urls => updateItemData(index, urls[0] || "")}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case "video":
|
|
||||||
return (
|
|
||||||
<VideoUpload
|
|
||||||
value={item.data as string}
|
|
||||||
onChange={url => {
|
|
||||||
const videoUrl = Array.isArray(url) ? url[0] || "" : url || "";
|
|
||||||
updateItemData(index, videoUrl);
|
|
||||||
}}
|
|
||||||
maxSize={50}
|
|
||||||
showPreview={true}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case "file":
|
|
||||||
return (
|
|
||||||
<FileUpload
|
|
||||||
value={item.data as string}
|
|
||||||
onChange={url => {
|
|
||||||
const fileUrl = Array.isArray(url) ? url[0] || "" : url || "";
|
|
||||||
updateItemData(index, fileUrl);
|
|
||||||
}}
|
|
||||||
maxSize={10}
|
|
||||||
showPreview={true}
|
|
||||||
acceptTypes={["excel", "word", "ppt", "pdf", "txt"]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case "audio":
|
|
||||||
return (
|
|
||||||
<AudioUpload
|
|
||||||
value={item.data as string}
|
|
||||||
onChange={url => {
|
|
||||||
const audioUrl = Array.isArray(url) ? url[0] || "" : url || "";
|
|
||||||
updateItemData(index, audioUrl);
|
|
||||||
}}
|
|
||||||
maxSize={50}
|
|
||||||
showPreview={true}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case "link": {
|
|
||||||
const linkData = (item.data as LinkData) || {
|
|
||||||
title: "",
|
|
||||||
url: "",
|
|
||||||
cover: "",
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Input
|
|
||||||
value={linkData.title}
|
|
||||||
onChange={e =>
|
|
||||||
updateItemData(index, { ...linkData, title: e.target.value })
|
|
||||||
}
|
|
||||||
placeholder="链接标题"
|
|
||||||
style={{ marginBottom: 8 }}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
value={linkData.url}
|
|
||||||
onChange={e =>
|
|
||||||
updateItemData(index, { ...linkData, url: e.target.value })
|
|
||||||
}
|
|
||||||
placeholder="链接URL"
|
|
||||||
style={{ marginBottom: 8 }}
|
|
||||||
/>
|
|
||||||
<ImageUpload
|
|
||||||
count={1}
|
|
||||||
accept="image/*"
|
|
||||||
value={linkData.cover ? [linkData.cover] : []}
|
|
||||||
onChange={urls =>
|
|
||||||
updateItemData(index, { ...linkData, cover: urls[0] || "" })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{/* 内容列表 */}
|
|
||||||
{contentItems.map((item, index) => renderContentItem(item, index))}
|
|
||||||
|
|
||||||
{/* 添加内容区域 */}
|
|
||||||
<Button
|
|
||||||
type="dashed"
|
|
||||||
block
|
|
||||||
icon={<PlusOutlined />}
|
|
||||||
onClick={handleAddItem}
|
|
||||||
>
|
|
||||||
添加内容
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ContentManager;
|
|
||||||
@@ -1,288 +0,0 @@
|
|||||||
import React, { useState, useEffect, useCallback } from "react";
|
|
||||||
import { Modal, Form, Input, Button, message, Select } from "antd";
|
|
||||||
import {
|
|
||||||
addKeyword,
|
|
||||||
updateKeyword,
|
|
||||||
getKeywordDetails,
|
|
||||||
type KeywordAddRequest,
|
|
||||||
type KeywordUpdateRequest,
|
|
||||||
} from "../../api";
|
|
||||||
import MetailSelection from "@/components/MetailSelection";
|
|
||||||
const { TextArea } = Input;
|
|
||||||
const { Option } = Select;
|
|
||||||
|
|
||||||
interface KeywordModalProps {
|
|
||||||
visible: boolean;
|
|
||||||
mode: "add" | "edit";
|
|
||||||
keywordId?: number | null;
|
|
||||||
onCancel: () => void;
|
|
||||||
onSuccess: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const KeywordModal: React.FC<KeywordModalProps> = ({
|
|
||||||
visible,
|
|
||||||
mode,
|
|
||||||
keywordId,
|
|
||||||
onCancel,
|
|
||||||
onSuccess,
|
|
||||||
}) => {
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const title = mode === "add" ? "添加关键词回复" : "编辑关键词回复";
|
|
||||||
const [selectedOptions, setSelectedOptions] = useState<any[]>([]);
|
|
||||||
|
|
||||||
// 获取关键词详情
|
|
||||||
const fetchKeywordDetails = useCallback(
|
|
||||||
async (id: number) => {
|
|
||||||
try {
|
|
||||||
const response = await getKeywordDetails(id);
|
|
||||||
if (response) {
|
|
||||||
const keyword = response;
|
|
||||||
form.setFieldsValue({
|
|
||||||
title: keyword.title,
|
|
||||||
keywords: keyword.keywords,
|
|
||||||
content: keyword.content,
|
|
||||||
type: keyword.type,
|
|
||||||
level: keyword.level,
|
|
||||||
replyType: keyword.replyType,
|
|
||||||
status: keyword.status,
|
|
||||||
metailGroups: keyword.metailGroups,
|
|
||||||
});
|
|
||||||
setSelectedOptions(keyword.metailGroupsOptions);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("获取关键词详情失败:", error);
|
|
||||||
message.error("获取关键词详情失败");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[form],
|
|
||||||
);
|
|
||||||
|
|
||||||
// 当弹窗打开时处理数据
|
|
||||||
useEffect(() => {
|
|
||||||
if (visible) {
|
|
||||||
if (mode === "edit" && keywordId) {
|
|
||||||
// 编辑模式:获取详情
|
|
||||||
fetchKeywordDetails(keywordId);
|
|
||||||
} else if (mode === "add") {
|
|
||||||
// 添加模式:重置表单
|
|
||||||
form.resetFields();
|
|
||||||
setSelectedOptions([]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [visible, mode, keywordId, fetchKeywordDetails, form]);
|
|
||||||
|
|
||||||
const handleSubmit = async (values: any) => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
if (mode === "add") {
|
|
||||||
const data: KeywordAddRequest = {
|
|
||||||
title: values.title,
|
|
||||||
keywords: values.keywords,
|
|
||||||
content: values.content,
|
|
||||||
type: values.type,
|
|
||||||
level: values.level,
|
|
||||||
replyType: values.replyType,
|
|
||||||
status: values.status || "1",
|
|
||||||
metailGroups: values.metailGroups,
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await addKeyword(data);
|
|
||||||
if (response) {
|
|
||||||
message.success("添加关键词成功");
|
|
||||||
form.resetFields();
|
|
||||||
onSuccess();
|
|
||||||
onCancel();
|
|
||||||
} else {
|
|
||||||
message.error(response?.message || "添加关键词失败");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const data: KeywordUpdateRequest = {
|
|
||||||
id: keywordId,
|
|
||||||
title: values.title,
|
|
||||||
keywords: values.keywords,
|
|
||||||
content: values.content,
|
|
||||||
type: values.type,
|
|
||||||
level: values.level,
|
|
||||||
replyType: values.replyType,
|
|
||||||
status: values.status,
|
|
||||||
metailGroups: values.metailGroups,
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await updateKeyword(data);
|
|
||||||
if (response) {
|
|
||||||
message.success("更新关键词回复成功");
|
|
||||||
form.resetFields();
|
|
||||||
onSuccess();
|
|
||||||
onCancel();
|
|
||||||
} else {
|
|
||||||
message.error(response?.message || "更新关键词回复失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`${mode === "add" ? "添加" : "更新"}关键词失败:`, error);
|
|
||||||
message.error(`${mode === "add" ? "添加" : "更新"}关键词失败`);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
form.resetFields();
|
|
||||||
setSelectedOptions([]);
|
|
||||||
onCancel();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handSelectMaterial = (options: any[]) => {
|
|
||||||
if (options.length === 0) {
|
|
||||||
form.setFieldsValue({
|
|
||||||
metailGroups: [],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 在单选模式下,只取第一个选项的ID
|
|
||||||
form.setFieldsValue({
|
|
||||||
metailGroups: options.map(v => v.id),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setSelectedOptions(options);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 监听表单值变化
|
|
||||||
const handleFormValuesChange = (changedValues: any) => {
|
|
||||||
// 当回复类型切换时,清空素材选择
|
|
||||||
if (changedValues.replyType !== undefined) {
|
|
||||||
setSelectedOptions([]);
|
|
||||||
if (changedValues.replyType === 1) {
|
|
||||||
// 切换到自定义回复时,清空materialId
|
|
||||||
form.setFieldsValue({
|
|
||||||
materialId: null,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 切换到素材回复时,清空content
|
|
||||||
form.setFieldsValue({
|
|
||||||
content: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
title={title}
|
|
||||||
open={visible}
|
|
||||||
onCancel={handleCancel}
|
|
||||||
footer={null}
|
|
||||||
width={600}
|
|
||||||
>
|
|
||||||
<Form
|
|
||||||
form={form}
|
|
||||||
layout="vertical"
|
|
||||||
onFinish={handleSubmit}
|
|
||||||
onValuesChange={handleFormValuesChange}
|
|
||||||
initialValues={{
|
|
||||||
status: 1,
|
|
||||||
type: "模糊匹配",
|
|
||||||
level: 1,
|
|
||||||
replyType: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Form.Item
|
|
||||||
name="title"
|
|
||||||
label="关键词标题"
|
|
||||||
rules={[{ required: true, message: "请输入关键词标题" }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="请输入关键词标题" />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="keywords"
|
|
||||||
label="关键词"
|
|
||||||
rules={[{ required: true, message: "请输入关键词" }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="请输入关键词" />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="replyType"
|
|
||||||
label="回复类型"
|
|
||||||
rules={[{ required: true, message: "请选择回复类型" }]}
|
|
||||||
>
|
|
||||||
<Select placeholder="请选择回复类型">
|
|
||||||
<Option value={0}>素材回复</Option>
|
|
||||||
<Option value={1}>自定义</Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
{form.getFieldValue("replyType") === 1 ? (
|
|
||||||
<Form.Item
|
|
||||||
name="content"
|
|
||||||
label="回复内容"
|
|
||||||
rules={[{ required: true, message: "请输入回复内容" }]}
|
|
||||||
>
|
|
||||||
<TextArea rows={4} placeholder="请输入回复内容" />
|
|
||||||
</Form.Item>
|
|
||||||
) : (
|
|
||||||
<Form.Item
|
|
||||||
name="metailGroups"
|
|
||||||
label="回复内容"
|
|
||||||
rules={[{ required: true, message: "请输入回复内容" }]}
|
|
||||||
>
|
|
||||||
<MetailSelection
|
|
||||||
selectedOptions={selectedOptions}
|
|
||||||
onSelect={handSelectMaterial}
|
|
||||||
selectionMode="single"
|
|
||||||
placeholder="选择素材"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="type"
|
|
||||||
label="匹配类型"
|
|
||||||
rules={[{ required: true, message: "请选择匹配类型" }]}
|
|
||||||
>
|
|
||||||
<Select placeholder="请选择匹配类型">
|
|
||||||
<Option value={0}>模糊匹配</Option>
|
|
||||||
<Option value={1}>精确匹配</Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="level"
|
|
||||||
label="优先级"
|
|
||||||
rules={[{ required: true, message: "请选择优先级" }]}
|
|
||||||
>
|
|
||||||
<Select placeholder="请选择优先级">
|
|
||||||
<Option value={0}>低优先级</Option>
|
|
||||||
<Option value={1}>中优先级</Option>
|
|
||||||
<Option value={2}>高优先级</Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="status"
|
|
||||||
label="状态"
|
|
||||||
rules={[{ required: true, message: "请选择状态" }]}
|
|
||||||
>
|
|
||||||
<Select placeholder="请选择状态">
|
|
||||||
<Option value={1}>启用</Option>
|
|
||||||
<Option value={0}>禁用</Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item>
|
|
||||||
<div
|
|
||||||
style={{ display: "flex", justifyContent: "flex-end", gap: "8px" }}
|
|
||||||
>
|
|
||||||
<Button onClick={handleCancel}>取消</Button>
|
|
||||||
<Button type="primary" htmlType="submit" loading={loading}>
|
|
||||||
确定
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default KeywordModal;
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
import React, { useState, useEffect, useCallback } from "react";
|
|
||||||
import { Modal, Form, Input, Button, message, Select } from "antd";
|
|
||||||
import {
|
|
||||||
addMaterial,
|
|
||||||
updateMaterial,
|
|
||||||
getMaterialDetails,
|
|
||||||
type MaterialAddRequest,
|
|
||||||
type MaterialUpdateRequest,
|
|
||||||
type ContentItem,
|
|
||||||
} from "../../api";
|
|
||||||
import ImageUpload from "@/components/Upload/ImageUpload/ImageUpload";
|
|
||||||
import ContentManager from "./ContentManager";
|
|
||||||
|
|
||||||
const { Option } = Select;
|
|
||||||
|
|
||||||
interface MaterialModalProps {
|
|
||||||
visible: boolean;
|
|
||||||
mode: "add" | "edit";
|
|
||||||
materialId?: number | null;
|
|
||||||
onCancel: () => void;
|
|
||||||
onSuccess: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MaterialModal: React.FC<MaterialModalProps> = ({
|
|
||||||
visible,
|
|
||||||
mode,
|
|
||||||
materialId,
|
|
||||||
onCancel,
|
|
||||||
onSuccess,
|
|
||||||
}) => {
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [contentItems, setContentItems] = useState<ContentItem[]>([]);
|
|
||||||
|
|
||||||
// 获取素材详情
|
|
||||||
const fetchMaterialDetails = useCallback(
|
|
||||||
async (id: number) => {
|
|
||||||
try {
|
|
||||||
const response = await getMaterialDetails(id.toString());
|
|
||||||
if (response) {
|
|
||||||
const material = response;
|
|
||||||
form.setFieldsValue({
|
|
||||||
title: material.title,
|
|
||||||
cover: material.cover ? [material.cover] : [],
|
|
||||||
status: material.status,
|
|
||||||
});
|
|
||||||
// 设置内容项
|
|
||||||
setContentItems(material.content || []);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("获取素材详情失败:", error);
|
|
||||||
message.error("获取素材详情失败");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[form],
|
|
||||||
);
|
|
||||||
|
|
||||||
// 当弹窗打开时处理数据
|
|
||||||
useEffect(() => {
|
|
||||||
if (visible) {
|
|
||||||
if (mode === "edit" && materialId) {
|
|
||||||
// 编辑模式:获取详情
|
|
||||||
fetchMaterialDetails(materialId);
|
|
||||||
} else if (mode === "add") {
|
|
||||||
// 添加模式:重置表单
|
|
||||||
form.resetFields();
|
|
||||||
setContentItems([]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [visible, mode, materialId, fetchMaterialDetails, form]);
|
|
||||||
|
|
||||||
const handleSubmit = async (values: any) => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
// 验证内容项
|
|
||||||
if (contentItems.length === 0) {
|
|
||||||
message.warning("请至少添加一个内容项");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const coverValue = Array.isArray(values.cover)
|
|
||||||
? values.cover[0] || ""
|
|
||||||
: values.cover || "";
|
|
||||||
|
|
||||||
const data: MaterialAddRequest = {
|
|
||||||
title: values.title,
|
|
||||||
status: values.status || 1,
|
|
||||||
content: contentItems,
|
|
||||||
...(coverValue && { cover: coverValue }),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (mode === "add") {
|
|
||||||
const response = await addMaterial(data);
|
|
||||||
if (response) {
|
|
||||||
message.success("添加素材成功");
|
|
||||||
form.resetFields();
|
|
||||||
setContentItems([]);
|
|
||||||
onSuccess();
|
|
||||||
onCancel();
|
|
||||||
} else {
|
|
||||||
message.error(response?.message || "添加素材失败");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const updateData: MaterialUpdateRequest = {
|
|
||||||
...data,
|
|
||||||
id: materialId?.toString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await updateMaterial(updateData);
|
|
||||||
if (response) {
|
|
||||||
message.success("更新素材成功");
|
|
||||||
form.resetFields();
|
|
||||||
setContentItems([]);
|
|
||||||
onSuccess();
|
|
||||||
onCancel();
|
|
||||||
} else {
|
|
||||||
message.error(response?.message || "更新素材失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`${mode === "add" ? "添加" : "更新"}素材失败:`, error);
|
|
||||||
message.error(`${mode === "add" ? "添加" : "更新"}素材失败`);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
form.resetFields();
|
|
||||||
setContentItems([]);
|
|
||||||
onCancel();
|
|
||||||
};
|
|
||||||
|
|
||||||
const title = mode === "add" ? "添加素材" : "编辑素材";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
title={title}
|
|
||||||
open={visible}
|
|
||||||
onCancel={handleCancel}
|
|
||||||
footer={null}
|
|
||||||
width={800}
|
|
||||||
>
|
|
||||||
<Form
|
|
||||||
form={form}
|
|
||||||
layout="vertical"
|
|
||||||
onFinish={handleSubmit}
|
|
||||||
initialValues={{ status: 1 }}
|
|
||||||
>
|
|
||||||
<Form.Item
|
|
||||||
name="title"
|
|
||||||
label="素材标题"
|
|
||||||
rules={[{ required: true, message: "请输入素材标题" }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="请输入素材标题" />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item label="素材内容">
|
|
||||||
<ContentManager value={contentItems} onChange={setContentItems} />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item name="cover" label="封面图片">
|
|
||||||
<ImageUpload
|
|
||||||
count={1}
|
|
||||||
accept="image/*"
|
|
||||||
className="material-cover-upload"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="status"
|
|
||||||
label="状态"
|
|
||||||
rules={[{ required: true, message: "请选择状态" }]}
|
|
||||||
>
|
|
||||||
<Select placeholder="请选择状态">
|
|
||||||
<Option value={1}>启用</Option>
|
|
||||||
<Option value={0}>禁用</Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item>
|
|
||||||
<div
|
|
||||||
style={{ display: "flex", justifyContent: "flex-end", gap: "8px" }}
|
|
||||||
>
|
|
||||||
<Button onClick={handleCancel}>取消</Button>
|
|
||||||
<Button type="primary" htmlType="submit" loading={loading}>
|
|
||||||
确定
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MaterialModal;
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
import React, { useState, useEffect, useCallback } from "react";
|
|
||||||
import { Modal, Form, Input, Button, message, Select } from "antd";
|
|
||||||
import {
|
|
||||||
addSensitiveWord,
|
|
||||||
updateSensitiveWord,
|
|
||||||
getSensitiveWordDetails,
|
|
||||||
type SensitiveWordAddRequest,
|
|
||||||
type SensitiveWordUpdateRequest,
|
|
||||||
} from "../../api";
|
|
||||||
|
|
||||||
const { TextArea } = Input;
|
|
||||||
const { Option } = Select;
|
|
||||||
|
|
||||||
interface SensitiveWordModalProps {
|
|
||||||
visible: boolean;
|
|
||||||
mode: "add" | "edit";
|
|
||||||
sensitiveWordId?: string | null;
|
|
||||||
onCancel: () => void;
|
|
||||||
onSuccess: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SensitiveWordModal: React.FC<SensitiveWordModalProps> = ({
|
|
||||||
visible,
|
|
||||||
mode,
|
|
||||||
sensitiveWordId,
|
|
||||||
onCancel,
|
|
||||||
onSuccess,
|
|
||||||
}) => {
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
// 获取敏感词详情
|
|
||||||
const fetchSensitiveWordDetails = useCallback(
|
|
||||||
async (id: string) => {
|
|
||||||
try {
|
|
||||||
const response = await getSensitiveWordDetails(id);
|
|
||||||
if (response) {
|
|
||||||
const sensitiveWord = response;
|
|
||||||
form.setFieldsValue({
|
|
||||||
title: sensitiveWord.title,
|
|
||||||
keywords: sensitiveWord.keywords,
|
|
||||||
content: sensitiveWord.content,
|
|
||||||
operation: sensitiveWord.operation,
|
|
||||||
status: sensitiveWord.status,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("获取敏感词详情失败:", error);
|
|
||||||
message.error("获取敏感词详情失败");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[form],
|
|
||||||
);
|
|
||||||
|
|
||||||
// 当弹窗打开时处理数据
|
|
||||||
useEffect(() => {
|
|
||||||
if (visible) {
|
|
||||||
if (mode === "edit" && sensitiveWordId) {
|
|
||||||
// 编辑模式:获取详情
|
|
||||||
fetchSensitiveWordDetails(sensitiveWordId);
|
|
||||||
} else if (mode === "add") {
|
|
||||||
// 添加模式:重置表单
|
|
||||||
form.resetFields();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [visible, mode, sensitiveWordId, fetchSensitiveWordDetails, form]);
|
|
||||||
|
|
||||||
const handleSubmit = async (values: any) => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
if (mode === "add") {
|
|
||||||
const data: SensitiveWordAddRequest = {
|
|
||||||
title: values.title,
|
|
||||||
keywords: values.keywords,
|
|
||||||
content: values.content,
|
|
||||||
operation: values.operation,
|
|
||||||
status: values.status || "1",
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await addSensitiveWord(data);
|
|
||||||
if (response) {
|
|
||||||
message.success("添加敏感词成功");
|
|
||||||
form.resetFields();
|
|
||||||
onSuccess();
|
|
||||||
onCancel();
|
|
||||||
} else {
|
|
||||||
message.error(response?.message || "添加敏感词失败");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const data: SensitiveWordUpdateRequest = {
|
|
||||||
id: sensitiveWordId,
|
|
||||||
title: values.title,
|
|
||||||
keywords: values.keywords,
|
|
||||||
content: values.content,
|
|
||||||
operation: values.operation,
|
|
||||||
status: values.status,
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await updateSensitiveWord(data);
|
|
||||||
if (response) {
|
|
||||||
message.success("更新敏感词成功");
|
|
||||||
form.resetFields();
|
|
||||||
onSuccess();
|
|
||||||
onCancel();
|
|
||||||
} else {
|
|
||||||
message.error(response?.message || "更新敏感词失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`${mode === "add" ? "添加" : "更新"}敏感词失败:`, error);
|
|
||||||
message.error(`${mode === "add" ? "添加" : "更新"}敏感词失败`);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
form.resetFields();
|
|
||||||
onCancel();
|
|
||||||
};
|
|
||||||
|
|
||||||
const title = mode === "add" ? "添加敏感词" : "编辑敏感词";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
title={title}
|
|
||||||
open={visible}
|
|
||||||
onCancel={handleCancel}
|
|
||||||
footer={null}
|
|
||||||
width={600}
|
|
||||||
>
|
|
||||||
<Form
|
|
||||||
form={form}
|
|
||||||
layout="vertical"
|
|
||||||
onFinish={handleSubmit}
|
|
||||||
initialValues={{ status: "1", operation: "1" }}
|
|
||||||
>
|
|
||||||
<Form.Item
|
|
||||||
name="title"
|
|
||||||
label="敏感词标题"
|
|
||||||
rules={[{ required: true, message: "请输入敏感词标题" }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="请输入敏感词标题" />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="keywords"
|
|
||||||
label="关键词"
|
|
||||||
rules={[{ required: true, message: "请输入关键词" }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="请输入关键词" />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="content"
|
|
||||||
label="敏感词内容"
|
|
||||||
rules={[{ required: true, message: "请输入敏感词内容" }]}
|
|
||||||
>
|
|
||||||
<TextArea rows={4} placeholder="请输入敏感词内容" />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="operation"
|
|
||||||
label="操作类型"
|
|
||||||
rules={[{ required: true, message: "请选择操作类型" }]}
|
|
||||||
>
|
|
||||||
<Select placeholder="请选择操作类型">
|
|
||||||
<Option value={0}>不操作</Option>
|
|
||||||
<Option value={1}>替换</Option>
|
|
||||||
<Option value={2}>删除</Option>
|
|
||||||
<Option value={3}>警告</Option>
|
|
||||||
<Option value={4}>禁止发送</Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="status"
|
|
||||||
label="状态"
|
|
||||||
rules={[{ required: true, message: "请选择状态" }]}
|
|
||||||
>
|
|
||||||
<Select placeholder="请选择状态">
|
|
||||||
<Option value={1}>启用</Option>
|
|
||||||
<Option value={0}>禁用</Option>
|
|
||||||
</Select>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item>
|
|
||||||
<div
|
|
||||||
style={{ display: "flex", justifyContent: "flex-end", gap: "8px" }}
|
|
||||||
>
|
|
||||||
<Button onClick={handleCancel}>取消</Button>
|
|
||||||
<Button type="primary" htmlType="submit" loading={loading}>
|
|
||||||
确定
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SensitiveWordModal;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
// 模态框组件统一导出
|
|
||||||
export { default as MaterialModal } from "./MaterialModal";
|
|
||||||
export { default as SensitiveWordModal } from "./SensitiveWordModal";
|
|
||||||
export { default as KeywordModal } from "./KeywordModal";
|
|
||||||
export { default as ContentManager } from "./ContentManager";
|
|
||||||
@@ -53,10 +53,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
background: #fff;
|
background: #f5f5f5;
|
||||||
border-radius: 0 0 8px 8px;
|
border-radius: 8px;
|
||||||
padding: 24px;
|
padding: 0;
|
||||||
min-height: 400px;
|
min-height: calc(100vh - 120px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainLayout {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
height: 100%;
|
||||||
|
margin: 24px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftSection {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rightSection {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.materialContent {
|
.materialContent {
|
||||||
@@ -385,6 +402,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mainLayout {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftSection,
|
||||||
|
.rightSection {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
@@ -1,89 +1,19 @@
|
|||||||
import React, { useState, useRef } from "react";
|
import React, { useRef } from "react";
|
||||||
import { Button } from "antd";
|
|
||||||
import { PlusOutlined } from "@ant-design/icons";
|
|
||||||
import PowerNavigation from "@/components/PowerNavtion";
|
import PowerNavigation from "@/components/PowerNavtion";
|
||||||
|
import MomentPublish from "./components/MomentPublish";
|
||||||
|
import PublishSchedule from "./components/PublishSchedule";
|
||||||
import styles from "./index.module.scss";
|
import styles from "./index.module.scss";
|
||||||
import {
|
|
||||||
MaterialManagement,
|
|
||||||
SensitiveWordManagement,
|
|
||||||
KeywordManagement,
|
|
||||||
MaterialModal,
|
|
||||||
SensitiveWordModal,
|
|
||||||
KeywordModal,
|
|
||||||
} from "./components";
|
|
||||||
|
|
||||||
const ContentManagement: React.FC = () => {
|
const ContentManagement: React.FC = () => {
|
||||||
const [activeTab, setActiveTab] = useState<string>("material");
|
// 用于触发 PublishSchedule 刷新的引用
|
||||||
const [materialModalVisible, setMaterialModalVisible] = useState(false);
|
const publishScheduleRef = useRef<{ refresh: () => void }>(null);
|
||||||
const [sensitiveWordModalVisible, setSensitiveWordModalVisible] =
|
|
||||||
useState(false);
|
|
||||||
const [keywordModalVisible, setKeywordModalVisible] = useState(false);
|
|
||||||
|
|
||||||
// 引用管理组件
|
// 发布成功后的回调函数
|
||||||
const materialManagementRef = useRef<any>(null);
|
const handlePublishSuccess = () => {
|
||||||
const keywordManagementRef = useRef<any>(null);
|
// 触发 PublishSchedule 组件刷新
|
||||||
const sensitiveWordManagementRef = useRef<any>(null);
|
if (publishScheduleRef.current) {
|
||||||
|
publishScheduleRef.current.refresh();
|
||||||
const tabs = [
|
|
||||||
{ key: "material", label: "素材资源库" },
|
|
||||||
{ key: "sensitive", label: "敏感词管理" },
|
|
||||||
{ key: "keyword", label: "关键词回复" },
|
|
||||||
];
|
|
||||||
|
|
||||||
// 按钮点击处理函数
|
|
||||||
const handleAddMaterial = () => {
|
|
||||||
setMaterialModalVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddSensitiveWord = () => {
|
|
||||||
setSensitiveWordModalVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddKeyword = () => {
|
|
||||||
setKeywordModalVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 弹窗成功回调
|
|
||||||
const handleModalSuccess = () => {
|
|
||||||
console.log("handleModalSuccess");
|
|
||||||
// 刷新素材列表
|
|
||||||
if (materialManagementRef.current?.fetchMaterials) {
|
|
||||||
console.log("刷新素材列表");
|
|
||||||
materialManagementRef.current.fetchMaterials();
|
|
||||||
}
|
|
||||||
// 刷新敏感词列表
|
|
||||||
if (sensitiveWordManagementRef.current?.fetchSensitiveWords) {
|
|
||||||
console.log("刷新敏感词列表");
|
|
||||||
sensitiveWordManagementRef.current.fetchSensitiveWords();
|
|
||||||
}
|
|
||||||
// 刷新关键词列表
|
|
||||||
if (keywordManagementRef.current?.fetchKeywords) {
|
|
||||||
console.log("刷新关键词列表");
|
|
||||||
keywordManagementRef.current.fetchKeywords();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderTabContent = () => {
|
|
||||||
switch (activeTab) {
|
|
||||||
case "material":
|
|
||||||
return (
|
|
||||||
<MaterialManagement ref={materialManagementRef} {...({} as any)} />
|
|
||||||
);
|
|
||||||
case "sensitive":
|
|
||||||
return (
|
|
||||||
<SensitiveWordManagement
|
|
||||||
ref={sensitiveWordManagementRef}
|
|
||||||
{...({} as any)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case "keyword":
|
|
||||||
return (
|
|
||||||
<KeywordManagement ref={keywordManagementRef} {...({} as any)} />
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<MaterialManagement ref={materialManagementRef} {...({} as any)} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -94,66 +24,22 @@ const ContentManagement: React.FC = () => {
|
|||||||
subtitle="可以讲聊天过程的信息收录到素材库中,也调用。"
|
subtitle="可以讲聊天过程的信息收录到素材库中,也调用。"
|
||||||
showBackButton={true}
|
showBackButton={true}
|
||||||
backButtonText="返回功能中心"
|
backButtonText="返回功能中心"
|
||||||
rightContent={
|
rightContent={<div className={styles.headerActions}></div>}
|
||||||
<div className={styles.headerActions}>
|
|
||||||
<Button
|
|
||||||
icon={<PlusOutlined />}
|
|
||||||
type="primary"
|
|
||||||
onClick={handleAddMaterial}
|
|
||||||
>
|
|
||||||
添加素材
|
|
||||||
</Button>
|
|
||||||
<Button icon={<PlusOutlined />} onClick={handleAddSensitiveWord}>
|
|
||||||
添加敏感词
|
|
||||||
</Button>
|
|
||||||
<Button icon={<PlusOutlined />} onClick={handleAddKeyword}>
|
|
||||||
添加关键词回复
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.tabsSection}>
|
<div className={styles.content}>
|
||||||
<br />
|
<div className={styles.mainLayout}>
|
||||||
<div className={styles.tabs}>
|
{/* 左侧:发布朋友圈 */}
|
||||||
{tabs.map(tab => (
|
<div className={styles.leftSection}>
|
||||||
<div
|
<MomentPublish onPublishSuccess={handlePublishSuccess} />
|
||||||
key={tab.key}
|
</div>
|
||||||
className={`${styles.tab} ${
|
|
||||||
activeTab === tab.key ? styles.tabActive : ""
|
{/* 右侧:发布计划 */}
|
||||||
}`}
|
<div className={styles.rightSection}>
|
||||||
onClick={() => setActiveTab(tab.key)}
|
<PublishSchedule ref={publishScheduleRef} />
|
||||||
>
|
</div>
|
||||||
{tab.label}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.content}>{renderTabContent()}</div>
|
|
||||||
|
|
||||||
{/* 弹窗组件 */}
|
|
||||||
<MaterialModal
|
|
||||||
visible={materialModalVisible}
|
|
||||||
mode="add"
|
|
||||||
onCancel={() => setMaterialModalVisible(false)}
|
|
||||||
onSuccess={handleModalSuccess}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SensitiveWordModal
|
|
||||||
visible={sensitiveWordModalVisible}
|
|
||||||
mode="add"
|
|
||||||
onCancel={() => setSensitiveWordModalVisible(false)}
|
|
||||||
onSuccess={handleModalSuccess}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<KeywordModal
|
|
||||||
visible={keywordModalVisible}
|
|
||||||
mode="add"
|
|
||||||
keywordId={null}
|
|
||||||
onCancel={() => setKeywordModalVisible(false)}
|
|
||||||
onSuccess={handleModalSuccess}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -58,12 +58,12 @@ export function getLabelsListByGroup(params) {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
//群、好友聊天记录列表
|
//群、好友聊天记录列表
|
||||||
export function getMessageList() {
|
export function getMessageList(params: { page: number; limit: number }) {
|
||||||
return request("/v1/kefu/message/list", {}, "GET");
|
return request("/v1/kefu/message/list", params, "GET");
|
||||||
}
|
}
|
||||||
|
|
||||||
//获取客服列表
|
//获取客服列表
|
||||||
export function getAgentList() {
|
export function getCustomerList() {
|
||||||
return request("/v1/kefu/customerService/list", {}, "GET");
|
return request("/v1/kefu/customerService/list", {}, "GET");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -227,3 +227,250 @@
|
|||||||
min-width: 60px;
|
min-width: 60px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AI 加载动效样式
|
||||||
|
.aiLoadingContainer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 190px;
|
||||||
|
background: linear-gradient(135deg, #f8f9ff 0%, #fef5ff 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 8px 0;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: -50%;
|
||||||
|
left: -50%;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
background: linear-gradient(
|
||||||
|
45deg,
|
||||||
|
transparent 30%,
|
||||||
|
rgba(138, 99, 210, 0.05) 50%,
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
animation: shimmer 3s infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-100%) translateY(-100%) rotate(45deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(100%) translateY(100%) rotate(45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.aiLoadingContent {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aiLoadingIcon {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brainIcon {
|
||||||
|
font-size: 36px;
|
||||||
|
animation: float 2s ease-in-out infinite;
|
||||||
|
filter: drop-shadow(0 4px 8px rgba(138, 99, 210, 0.3));
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
margin: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-8px) scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WiFi波纹容器 - 左侧
|
||||||
|
.waveLeft {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// WiFi波纹容器 - 右侧
|
||||||
|
.waveRight {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 波纹条样式
|
||||||
|
.wave1,
|
||||||
|
.wave2,
|
||||||
|
.wave3 {
|
||||||
|
height: 3px;
|
||||||
|
background: #8a63d2;
|
||||||
|
border-radius: 2px;
|
||||||
|
animation: waveExpand 1.5s ease-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave1 {
|
||||||
|
width: 12px;
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave2 {
|
||||||
|
width: 20px;
|
||||||
|
animation-delay: 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave3 {
|
||||||
|
width: 28px;
|
||||||
|
animation-delay: 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes waveExpand {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scaleX(0.3);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scaleX(1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.aiLoadingText {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadingTextMain {
|
||||||
|
background: linear-gradient(90deg, #8a63d2 0%, #b794f6 50%, #8a63d2 100%);
|
||||||
|
background-size: 200% auto;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
animation: textShine 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes textShine {
|
||||||
|
0% {
|
||||||
|
background-position: 0% center;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 200% center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadingDots {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 2px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
animation: dotFlashing 1.4s infinite;
|
||||||
|
color: #8a63d2;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&:nth-child(1) {
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
&:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
&:nth-child(3) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dotFlashing {
|
||||||
|
0%,
|
||||||
|
80%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: scale(0.8);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.aiLoadingSubText {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
animation: fadeInOut 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInOut {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载文字和取消按钮同行容器
|
||||||
|
.aiLoadingTextRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消AI按钮(显眼版)
|
||||||
|
.cancelAiButton {
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #ff7875;
|
||||||
|
background: #fff;
|
||||||
|
color: #ff4d4f;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 2px 4px rgba(255, 77, 79, 0.15);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #fff;
|
||||||
|
background: #ff4d4f;
|
||||||
|
border-color: #ff4d4f;
|
||||||
|
box-shadow: 0 4px 8px rgba(255, 77, 79, 0.3);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: 0 2px 4px rgba(255, 77, 79, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ import {
|
|||||||
WechatFriendRebackAllot,
|
WechatFriendRebackAllot,
|
||||||
} from "@/pages/pc/ckbox/weChat/api";
|
} from "@/pages/pc/ckbox/weChat/api";
|
||||||
import { useCurrentContact } from "@/store/module/weChat/weChat";
|
import { useCurrentContact } from "@/store/module/weChat/weChat";
|
||||||
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
|
import { ContactManager } from "@/utils/dbAction/contact";
|
||||||
import { contractService, weChatGroupService } from "@/utils/db";
|
import { MessageManager } from "@/utils/dbAction/message";
|
||||||
|
import { triggerRefresh } from "@/store/module/weChat/message";
|
||||||
|
import { useUserStore } from "@/store/module/user";
|
||||||
|
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
||||||
@@ -32,6 +35,9 @@ const ToContract: React.FC<ToContractProps> = ({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
}) => {
|
}) => {
|
||||||
const currentContact = useCurrentContact();
|
const currentContact = useCurrentContact();
|
||||||
|
const clearCurrentContact = useWeChatStore(
|
||||||
|
state => state.clearCurrentContact,
|
||||||
|
);
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [selectedTarget, setSelectedTarget] = useState<number | null>(null);
|
const [selectedTarget, setSelectedTarget] = useState<number | null>(null);
|
||||||
const [comment, setComment] = useState<string>("");
|
const [comment, setComment] = useState<string>("");
|
||||||
@@ -39,7 +45,6 @@ const ToContract: React.FC<ToContractProps> = ({
|
|||||||
const [customerServiceList, setCustomerServiceList] = useState<DepartItem[]>(
|
const [customerServiceList, setCustomerServiceList] = useState<DepartItem[]>(
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
const deleteChatSession = useCkChatStore(state => state.deleteChatSession);
|
|
||||||
// 打开弹窗
|
// 打开弹窗
|
||||||
const openModal = () => {
|
const openModal = () => {
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
@@ -66,8 +71,6 @@ const ToContract: React.FC<ToContractProps> = ({
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
console.log(currentContact);
|
|
||||||
|
|
||||||
// 调用转接接口
|
// 调用转接接口
|
||||||
if (currentContact) {
|
if (currentContact) {
|
||||||
if ("chatroomId" in currentContact && currentContact.chatroomId) {
|
if ("chatroomId" in currentContact && currentContact.chatroomId) {
|
||||||
@@ -87,20 +90,37 @@ const ToContract: React.FC<ToContractProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message.success("转接成功");
|
// 先关闭弹窗
|
||||||
|
closeModal();
|
||||||
|
|
||||||
|
// 删除本地数据库记录并关闭聊天窗口
|
||||||
try {
|
try {
|
||||||
// 删除聊天会话
|
const currentUserId = useUserStore.getState().user?.id || 0;
|
||||||
deleteChatSession(currentContact.id);
|
const contactType = "chatroomId" in currentContact ? "group" : "friend";
|
||||||
// 删除本地数据库记录
|
|
||||||
if ("chatroomId" in currentContact) {
|
// 1. 从会话列表数据库删除
|
||||||
await weChatGroupService.delete(currentContact.id);
|
await MessageManager.deleteSession(
|
||||||
} else {
|
currentUserId,
|
||||||
await contractService.delete(currentContact.id);
|
currentContact.id,
|
||||||
}
|
contactType,
|
||||||
|
);
|
||||||
|
console.log("✅ 已从会话列表删除");
|
||||||
|
|
||||||
|
// 2. 从联系人数据库删除
|
||||||
|
await ContactManager.deleteContact(currentContact.id);
|
||||||
|
console.log("✅ 已从联系人数据库删除");
|
||||||
|
|
||||||
|
// 3. 触发会话列表刷新
|
||||||
|
triggerRefresh();
|
||||||
|
|
||||||
|
// 4. 清空当前选中的联系人(关闭聊天窗口)
|
||||||
|
clearCurrentContact();
|
||||||
|
|
||||||
|
message.success("转接成功,已清理本地数据");
|
||||||
} catch (deleteError) {
|
} catch (deleteError) {
|
||||||
console.error("删除本地数据失败:", deleteError);
|
console.error("删除本地数据失败:", deleteError);
|
||||||
|
message.error("删除本地数据失败");
|
||||||
}
|
}
|
||||||
closeModal();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("转接失败:", error);
|
console.error("转接失败:", error);
|
||||||
message.error("转接失败,请重试");
|
message.error("转接失败,请重试");
|
||||||
@@ -127,20 +147,37 @@ const ToContract: React.FC<ToContractProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message.success("转回成功");
|
// 先关闭弹窗
|
||||||
|
closeModal();
|
||||||
|
|
||||||
|
// 删除本地数据库记录并关闭聊天窗口
|
||||||
try {
|
try {
|
||||||
// 删除聊天会话
|
const currentUserId = useUserStore.getState().user?.id || 0;
|
||||||
deleteChatSession(currentContact.id);
|
const contactType = "chatroomId" in currentContact ? "group" : "friend";
|
||||||
// 删除本地数据库记录
|
|
||||||
if ("chatroomId" in currentContact) {
|
// 1. 从会话列表数据库删除
|
||||||
await weChatGroupService.delete(currentContact.id);
|
await MessageManager.deleteSession(
|
||||||
} else {
|
currentUserId,
|
||||||
await contractService.delete(currentContact.id);
|
currentContact.id,
|
||||||
}
|
contactType,
|
||||||
|
);
|
||||||
|
console.log("✅ 已从会话列表删除");
|
||||||
|
|
||||||
|
// 2. 从联系人数据库删除
|
||||||
|
await ContactManager.deleteContact(currentContact.id);
|
||||||
|
console.log("✅ 已从联系人数据库删除");
|
||||||
|
|
||||||
|
// 3. 触发会话列表刷新
|
||||||
|
triggerRefresh();
|
||||||
|
|
||||||
|
// 4. 清空当前选中的联系人(关闭聊天窗口)
|
||||||
|
clearCurrentContact();
|
||||||
|
|
||||||
|
message.success("转回成功,已清理本地数据");
|
||||||
} catch (deleteError) {
|
} catch (deleteError) {
|
||||||
console.error("删除本地数据失败:", deleteError);
|
console.error("删除本地数据失败:", deleteError);
|
||||||
|
message.error("删除本地数据失败");
|
||||||
}
|
}
|
||||||
closeModal();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("转回失败:", error);
|
console.error("转回失败:", error);
|
||||||
message.error("转回失败,请重试");
|
message.error("转回失败,请重试");
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Layout, Input, Button, Modal } from "antd";
|
import { Layout, Input, Button, Modal, message } from "antd";
|
||||||
import {
|
import {
|
||||||
SendOutlined,
|
SendOutlined,
|
||||||
FolderOutlined,
|
FolderOutlined,
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
CloseOutlined,
|
CloseOutlined,
|
||||||
MessageOutlined,
|
MessageOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
import { ContractData, weChatGroup, ChatRecord } from "@/pages/pc/ckbox/data";
|
||||||
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
||||||
import { EmojiPicker } from "@/components/EmojiSeclection";
|
import { EmojiPicker } from "@/components/EmojiSeclection";
|
||||||
import { EmojiInfo } from "@/components/EmojiSeclection/wechatEmoji";
|
import { EmojiInfo } from "@/components/EmojiSeclection/wechatEmoji";
|
||||||
@@ -17,6 +17,7 @@ import AudioRecorder from "@/components/Upload/AudioRecorder";
|
|||||||
import ToContract from "./components/toContract";
|
import ToContract from "./components/toContract";
|
||||||
import styles from "./MessageEnter.module.scss";
|
import styles from "./MessageEnter.module.scss";
|
||||||
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||||
|
import { useContactStore } from "@/store/module/weChat/contacts";
|
||||||
const { Footer } = Layout;
|
const { Footer } = Layout;
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
||||||
@@ -32,9 +33,8 @@ const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
|
|||||||
const EnterModule = useWeChatStore(state => state.EnterModule);
|
const EnterModule = useWeChatStore(state => state.EnterModule);
|
||||||
const updateShowCheckbox = useWeChatStore(state => state.updateShowCheckbox);
|
const updateShowCheckbox = useWeChatStore(state => state.updateShowCheckbox);
|
||||||
const updateEnterModule = useWeChatStore(state => state.updateEnterModule);
|
const updateEnterModule = useWeChatStore(state => state.updateEnterModule);
|
||||||
const updateTransmitModal = useWeChatStore(
|
const setTransmitModal = useContactStore(state => state.setTransmitModal);
|
||||||
state => state.updateTransmitModal,
|
const addMessage = useWeChatStore(state => state.addMessage);
|
||||||
);
|
|
||||||
const showChatRecordModel = useWeChatStore(
|
const showChatRecordModel = useWeChatStore(
|
||||||
state => state.showChatRecordModel,
|
state => state.showChatRecordModel,
|
||||||
);
|
);
|
||||||
@@ -45,15 +45,59 @@ const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
|
|||||||
const quoteMessageContent = useWeChatStore(
|
const quoteMessageContent = useWeChatStore(
|
||||||
state => state.quoteMessageContent,
|
state => state.quoteMessageContent,
|
||||||
);
|
);
|
||||||
|
const isLoadingAiChat = useWeChatStore(state => state.isLoadingAiChat);
|
||||||
|
const updateIsLoadingAiChat = useWeChatStore(
|
||||||
|
state => state.updateIsLoadingAiChat,
|
||||||
|
);
|
||||||
|
const updateQuoteMessageContent = useWeChatStore(
|
||||||
|
state => state.updateQuoteMessageContent,
|
||||||
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (quoteMessageContent) {
|
if (quoteMessageContent) {
|
||||||
setInputValue(quoteMessageContent);
|
setInputValue(quoteMessageContent);
|
||||||
}
|
}
|
||||||
}, [quoteMessageContent]);
|
}, [quoteMessageContent]);
|
||||||
|
|
||||||
|
// 取消AI生成
|
||||||
|
const handleCancelAi = () => {
|
||||||
|
// 停止AI加载状态
|
||||||
|
updateIsLoadingAiChat(false);
|
||||||
|
// 清空AI回复内容
|
||||||
|
updateQuoteMessageContent("");
|
||||||
|
message.info("已取消AI生成");
|
||||||
|
};
|
||||||
|
|
||||||
const handleSend = async () => {
|
const handleSend = async () => {
|
||||||
if (!inputValue.trim()) return;
|
if (!inputValue.trim()) return;
|
||||||
|
const messageId = +Date.now();
|
||||||
|
// 构造本地消息对象
|
||||||
|
const localMessage: ChatRecord = {
|
||||||
|
id: messageId, // 使用时间戳作为临时ID
|
||||||
|
wechatAccountId: contract.wechatAccountId,
|
||||||
|
wechatFriendId: contract?.chatroomId ? 0 : contract.id,
|
||||||
|
wechatChatroomId: contract?.chatroomId ? contract.id : 0,
|
||||||
|
tenantId: 0,
|
||||||
|
accountId: 0,
|
||||||
|
synergyAccountId: 0,
|
||||||
|
content: inputValue,
|
||||||
|
msgType: 1,
|
||||||
|
msgSubType: 0,
|
||||||
|
msgSvrId: "",
|
||||||
|
isSend: true, // 标记为发送中
|
||||||
|
createTime: new Date().toISOString(),
|
||||||
|
isDeleted: false,
|
||||||
|
deleteTime: "",
|
||||||
|
sendStatus: 1,
|
||||||
|
wechatTime: Date.now(),
|
||||||
|
origin: 0,
|
||||||
|
msgId: 0,
|
||||||
|
recalled: false,
|
||||||
|
seq: messageId,
|
||||||
|
};
|
||||||
|
// 先插入本地数据
|
||||||
|
addMessage(localMessage);
|
||||||
|
|
||||||
|
// 再发送消息到服务器
|
||||||
const params = {
|
const params = {
|
||||||
wechatAccountId: contract.wechatAccountId,
|
wechatAccountId: contract.wechatAccountId,
|
||||||
wechatChatroomId: contract?.chatroomId ? contract.id : 0,
|
wechatChatroomId: contract?.chatroomId ? contract.id : 0,
|
||||||
@@ -61,6 +105,7 @@ const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
|
|||||||
msgSubType: 0,
|
msgSubType: 0,
|
||||||
msgType: 1,
|
msgType: 1,
|
||||||
content: inputValue,
|
content: inputValue,
|
||||||
|
seq: messageId,
|
||||||
};
|
};
|
||||||
sendCommand("CmdSendMessage", params);
|
sendCommand("CmdSendMessage", params);
|
||||||
setInputValue("");
|
setInputValue("");
|
||||||
@@ -158,7 +203,7 @@ const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
|
|||||||
updateEnterModule("common");
|
updateEnterModule("common");
|
||||||
};
|
};
|
||||||
const handTurnRignt = () => {
|
const handTurnRignt = () => {
|
||||||
updateTransmitModal(true);
|
setTransmitModal(true);
|
||||||
};
|
};
|
||||||
const openChatRecordModel = () => {
|
const openChatRecordModel = () => {
|
||||||
updateShowChatRecordModel(!showChatRecordModel);
|
updateShowChatRecordModel(!showChatRecordModel);
|
||||||
@@ -168,102 +213,156 @@ const MessageEnter: React.FC<MessageEnterProps> = ({ contract }) => {
|
|||||||
<>
|
<>
|
||||||
{/* 聊天输入 */}
|
{/* 聊天输入 */}
|
||||||
<Footer className={styles.chatFooter}>
|
<Footer className={styles.chatFooter}>
|
||||||
{["common"].includes(EnterModule) && (
|
{isLoadingAiChat ? (
|
||||||
<div className={styles.inputContainer}>
|
<div className={styles.aiLoadingContainer}>
|
||||||
<div className={styles.inputToolbar}>
|
<div className={styles.aiLoadingContent}>
|
||||||
<div className={styles.leftTool}>
|
<div className={styles.aiLoadingIcon}>
|
||||||
<EmojiPicker onEmojiSelect={handleEmojiSelect} />
|
{/* WiFi式波纹 - 左侧 */}
|
||||||
<SimpleFileUpload
|
<div className={styles.waveLeft}>
|
||||||
onFileUploaded={filePath =>
|
<div className={styles.wave1}></div>
|
||||||
handleFileUploaded(filePath, FileType.FILE)
|
<div className={styles.wave2}></div>
|
||||||
}
|
<div className={styles.wave3}></div>
|
||||||
maxSize={1}
|
</div>
|
||||||
type={4}
|
|
||||||
slot={
|
|
||||||
<Button
|
|
||||||
className={styles.toolbarButton}
|
|
||||||
type="text"
|
|
||||||
icon={<FolderOutlined />}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<SimpleFileUpload
|
|
||||||
onFileUploaded={filePath =>
|
|
||||||
handleFileUploaded(filePath, FileType.IMAGE)
|
|
||||||
}
|
|
||||||
maxSize={1}
|
|
||||||
type={1}
|
|
||||||
slot={
|
|
||||||
<Button
|
|
||||||
className={styles.toolbarButton}
|
|
||||||
type="text"
|
|
||||||
icon={<PictureOutlined />}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AudioRecorder
|
{/* 中心大脑图标 */}
|
||||||
onAudioUploaded={audioData =>
|
<div className={styles.brainIcon}>🧠</div>
|
||||||
handleFileUploaded(audioData, FileType.AUDIO)
|
|
||||||
}
|
{/* WiFi式波纹 - 右侧 */}
|
||||||
className={styles.toolbarButton}
|
<div className={styles.waveRight}>
|
||||||
/>
|
<div className={styles.wave1}></div>
|
||||||
|
<div className={styles.wave2}></div>
|
||||||
|
<div className={styles.wave3}></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.rightTool}>
|
<div className={styles.aiLoadingTextRow}>
|
||||||
<ToContract className={styles.rightToolItem} />
|
<div className={styles.aiLoadingText}>
|
||||||
<div
|
<span className={styles.loadingTextMain}>AI 正在思考</span>
|
||||||
style={{
|
<span className={styles.loadingDots}>
|
||||||
fontSize: "12px",
|
<span>.</span>
|
||||||
cursor: "pointer",
|
<span>.</span>
|
||||||
color: "#666",
|
<span>.</span>
|
||||||
}}
|
</span>
|
||||||
onClick={openChatRecordModel}
|
</div>
|
||||||
|
<Button
|
||||||
|
className={styles.cancelAiButton}
|
||||||
|
onClick={handleCancelAi}
|
||||||
|
size="small"
|
||||||
|
icon={<CloseOutlined />}
|
||||||
>
|
>
|
||||||
<MessageOutlined />
|
取消生成
|
||||||
聊天记录
|
</Button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className={styles.aiLoadingSubText}>
|
||||||
<div className={styles.inputArea}>
|
正在分析消息内容,为您生成智能回复
|
||||||
<div className={styles.inputWrapper}>
|
|
||||||
<TextArea
|
|
||||||
value={inputValue}
|
|
||||||
onChange={e => setInputValue(e.target.value)}
|
|
||||||
onKeyDown={handleKeyPress}
|
|
||||||
placeholder="输入消息..."
|
|
||||||
className={styles.messageInput}
|
|
||||||
autoSize={{ minRows: 2, maxRows: 6 }}
|
|
||||||
/>
|
|
||||||
<div className={styles.sendButtonArea}>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={<SendOutlined />}
|
|
||||||
onClick={handleSend}
|
|
||||||
disabled={!inputValue.trim()}
|
|
||||||
className={styles.sendButton}
|
|
||||||
>
|
|
||||||
发送
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.inputHint}>
|
|
||||||
按下Ctrl+Enter换行,Enter发送
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : (
|
||||||
{["multipleForwarding"].includes(EnterModule) && (
|
<>
|
||||||
<div className={styles.multipleForwardingBar}>
|
{["common"].includes(EnterModule) && (
|
||||||
<div className={styles.actionButton} onClick={handTurnRignt}>
|
<div className={styles.inputContainer}>
|
||||||
<ExportOutlined className={styles.actionIcon} />
|
<div className={styles.inputToolbar}>
|
||||||
<span className={styles.actionText}>转发</span>
|
<div className={styles.leftTool}>
|
||||||
</div>
|
<EmojiPicker onEmojiSelect={handleEmojiSelect} />
|
||||||
|
<SimpleFileUpload
|
||||||
|
onFileUploaded={filePath =>
|
||||||
|
handleFileUploaded(filePath, FileType.FILE)
|
||||||
|
}
|
||||||
|
maxSize={1}
|
||||||
|
type={4}
|
||||||
|
slot={
|
||||||
|
<Button
|
||||||
|
className={styles.toolbarButton}
|
||||||
|
type="text"
|
||||||
|
icon={<FolderOutlined />}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<SimpleFileUpload
|
||||||
|
onFileUploaded={filePath =>
|
||||||
|
handleFileUploaded(filePath, FileType.IMAGE)
|
||||||
|
}
|
||||||
|
maxSize={1}
|
||||||
|
type={1}
|
||||||
|
slot={
|
||||||
|
<Button
|
||||||
|
className={styles.toolbarButton}
|
||||||
|
type="text"
|
||||||
|
icon={<PictureOutlined />}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className={styles.actionButton} onClick={handleCancelAction}>
|
<AudioRecorder
|
||||||
<CloseOutlined className={styles.actionIcon} />
|
onAudioUploaded={audioData =>
|
||||||
<span className={styles.actionText}>取消</span>
|
handleFileUploaded(audioData, FileType.AUDIO)
|
||||||
</div>
|
}
|
||||||
</div>
|
className={styles.toolbarButton}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.rightTool}>
|
||||||
|
<ToContract className={styles.rightToolItem} />
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "12px",
|
||||||
|
cursor: "pointer",
|
||||||
|
color: "#666",
|
||||||
|
}}
|
||||||
|
onClick={openChatRecordModel}
|
||||||
|
>
|
||||||
|
<MessageOutlined />
|
||||||
|
聊天记录
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.inputArea}>
|
||||||
|
<div className={styles.inputWrapper}>
|
||||||
|
<TextArea
|
||||||
|
value={inputValue}
|
||||||
|
onChange={e => setInputValue(e.target.value)}
|
||||||
|
onKeyDown={handleKeyPress}
|
||||||
|
placeholder="输入消息..."
|
||||||
|
className={styles.messageInput}
|
||||||
|
autoSize={{ minRows: 2, maxRows: 6 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.sendButtonArea}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<SendOutlined />}
|
||||||
|
onClick={handleSend}
|
||||||
|
disabled={!inputValue.trim()}
|
||||||
|
className={styles.sendButton}
|
||||||
|
>
|
||||||
|
发送
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.inputHint}>
|
||||||
|
按下Ctrl+Enter换行,Enter发送
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{["multipleForwarding"].includes(EnterModule) && (
|
||||||
|
<div className={styles.multipleForwardingBar}>
|
||||||
|
<div className={styles.actionButton} onClick={handTurnRignt}>
|
||||||
|
<ExportOutlined className={styles.actionIcon} />
|
||||||
|
<span className={styles.actionText}>转发</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={styles.actionButton}
|
||||||
|
onClick={handleCancelAction}
|
||||||
|
>
|
||||||
|
<CloseOutlined className={styles.actionIcon} />
|
||||||
|
<span className={styles.actionText}>取消</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Footer>
|
</Footer>
|
||||||
|
|
||||||
|
|||||||
@@ -13,3 +13,20 @@ export const fetchReCallApi = async (params: FetchMomentParams) => {
|
|||||||
const { sendCommand } = useWebSocketStore.getState();
|
const { sendCommand } = useWebSocketStore.getState();
|
||||||
sendCommand("CmdRecallMessage", params);
|
sendCommand("CmdRecallMessage", params);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 音频转文字请求参数接口
|
||||||
|
export interface VoiceToTextParams {
|
||||||
|
friendMessageId: number;
|
||||||
|
chatroomMessageId: number;
|
||||||
|
seq: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 音频转文字
|
||||||
|
export const fetchVoiceToTextApi = async (params: VoiceToTextParams) => {
|
||||||
|
const { sendCommand } = useWebSocketStore.getState();
|
||||||
|
sendCommand("CmdVoiceToText", {
|
||||||
|
friendMessageId: params.friendMessageId,
|
||||||
|
chatroomMessageId: params.chatroomMessageId,
|
||||||
|
seq: params.seq,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -104,10 +104,6 @@
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #666;
|
color: #666;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
cursor: help;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 进度条容器
|
// 进度条容器
|
||||||
|
|||||||
@@ -215,13 +215,7 @@ const AudioMessage: React.FC<AudioMessageProps> = ({ audioUrl, msgId }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{audioText && (
|
{audioText && <div className={styles.audioText}>{audioText}</div>}
|
||||||
<div className={styles.audioText} title={audioText}>
|
|
||||||
{audioText.length > 10
|
|
||||||
? `${audioText.substring(0, 10)}...`
|
|
||||||
: audioText}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
RollbackOutlined,
|
RollbackOutlined,
|
||||||
ExportOutlined,
|
ExportOutlined,
|
||||||
LinkOutlined,
|
LinkOutlined,
|
||||||
|
SoundOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { ChatRecord } from "@/pages/pc/ckbox/data";
|
import { ChatRecord } from "@/pages/pc/ckbox/data";
|
||||||
@@ -111,27 +112,49 @@ const ClickMenu: React.FC<ClickMenuProps> = ({
|
|||||||
|
|
||||||
return timeDiffInSeconds <= 108;
|
return timeDiffInSeconds <= 108;
|
||||||
};
|
};
|
||||||
const isText = (): boolean => {
|
// 检查是否为文本消息
|
||||||
|
const isTextMessage = (): boolean => {
|
||||||
return messageData.msgType === 1;
|
return messageData.msgType === 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 检查是否为音频消息
|
||||||
|
const isAudioMessage = (): boolean => {
|
||||||
|
return messageData.msgType === 34;
|
||||||
|
};
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{
|
{
|
||||||
key: "transmit",
|
key: "transmit",
|
||||||
icon: <ExportOutlined />,
|
icon: <ExportOutlined />,
|
||||||
label: "转发",
|
label: "转发",
|
||||||
},
|
},
|
||||||
{
|
// 只在文本消息时显示复制选项
|
||||||
key: "copy",
|
...(isTextMessage()
|
||||||
icon: <CopyOutlined />,
|
? [
|
||||||
label: "复制",
|
{
|
||||||
},
|
key: "copy",
|
||||||
|
icon: <CopyOutlined />,
|
||||||
|
label: "复制",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
// 只在音频消息时显示转文字选项
|
||||||
|
...(isAudioMessage()
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: "voiceToText",
|
||||||
|
icon: <SoundOutlined />,
|
||||||
|
label: "转换文字",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
key: "multipleForwarding",
|
key: "multipleForwarding",
|
||||||
icon: <CheckSquareOutlined />,
|
icon: <CheckSquareOutlined />,
|
||||||
label: "多条转发",
|
label: "多条转发",
|
||||||
},
|
},
|
||||||
|
// 只在文本消息时显示引用选项
|
||||||
...(isText()
|
...(isTextMessage()
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
key: "quote",
|
key: "quote",
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// 系统推荐备注消息样式
|
||||||
|
.systemRecommendRemarkMessage {
|
||||||
|
.systemMessageText {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #8c8c8c;
|
||||||
|
line-height: 1.4;
|
||||||
|
word-break: break-word;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.systemRecommendRemarkMessage {
|
||||||
|
.systemMessageText {
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styles from "./index.module.scss";
|
||||||
|
import { WarningOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
|
interface SystemRecommendRemarkMessageProps {
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SystemRecommendRemarkMessage: React.FC<
|
||||||
|
SystemRecommendRemarkMessageProps
|
||||||
|
> = ({ content }) => {
|
||||||
|
// 解析XML内容
|
||||||
|
const parseSystemMessage = (xmlContent: string) => {
|
||||||
|
try {
|
||||||
|
// 使用正则表达式提取关键信息
|
||||||
|
const templateMatch = xmlContent.match(
|
||||||
|
/<template><!\[CDATA\[(.*?)\]\]><\/template>/,
|
||||||
|
);
|
||||||
|
const phoneMatch = xmlContent.match(/<phone>(.*?)<\/phone>/);
|
||||||
|
const talkerMatch = xmlContent.match(/<talker>(.*?)<\/talker>/);
|
||||||
|
const remarkMatch = xmlContent.match(/<remark>(.*?)<\/remark>/);
|
||||||
|
|
||||||
|
const template = templateMatch ? templateMatch[1] : "";
|
||||||
|
const phone = phoneMatch ? phoneMatch[1] : "";
|
||||||
|
const talker = talkerMatch ? talkerMatch[1] : "";
|
||||||
|
const remark = remarkMatch ? remarkMatch[1] : "";
|
||||||
|
|
||||||
|
// 处理模板文本,替换占位符
|
||||||
|
let displayText = template;
|
||||||
|
if (phone) {
|
||||||
|
displayText = displayText.replace(/\$remark_msg_native_url\$/, phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
template: displayText,
|
||||||
|
phone,
|
||||||
|
talker,
|
||||||
|
remark,
|
||||||
|
hasRemark: !!remark.trim(),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("解析系统推荐备注消息失败:", error);
|
||||||
|
return {
|
||||||
|
template: "系统推荐添加备注",
|
||||||
|
phone: "",
|
||||||
|
talker: "",
|
||||||
|
remark: "",
|
||||||
|
hasRemark: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const messageData = parseSystemMessage(content);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.systemRecommendRemarkMessage}>
|
||||||
|
<div className={styles.systemMessageText}>
|
||||||
|
<WarningOutlined style={{ fontSize: 16 }} />
|
||||||
|
|
||||||
|
{messageData.template}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SystemRecommendRemarkMessage;
|
||||||
@@ -17,8 +17,10 @@ import {
|
|||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import styles from "./TransmitModal.module.scss";
|
import styles from "./TransmitModal.module.scss";
|
||||||
import { weChatGroupService, contractService } from "@/utils/db";
|
import { ContactManager } from "@/utils/dbAction";
|
||||||
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||||
|
import { useContactStore } from "@/store/module/weChat/contacts";
|
||||||
|
import { useUserStore } from "@/store/module/user";
|
||||||
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
||||||
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
||||||
const TransmitModal: React.FC = () => {
|
const TransmitModal: React.FC = () => {
|
||||||
@@ -33,11 +35,11 @@ const TransmitModal: React.FC = () => {
|
|||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const pageSize = 20;
|
const pageSize = 20;
|
||||||
const { sendCommand } = useWebSocketStore.getState();
|
const { sendCommand } = useWebSocketStore.getState();
|
||||||
|
const currentUserId = useUserStore(state => state.user?.id) || 0;
|
||||||
|
|
||||||
// 从 Zustand store 获取更新方法
|
// 从 Zustand store 获取更新方法
|
||||||
const openTransmitModal = useWeChatStore(state => state.openTransmitModal);
|
const openTransmitModal = useContactStore(state => state.openTransmitModal);
|
||||||
const updateTransmitModal = useWeChatStore(
|
const setTransmitModal = useContactStore(state => state.setTransmitModal);
|
||||||
state => state.updateTransmitModal,
|
|
||||||
);
|
|
||||||
const updateSelectedChatRecords = useWeChatStore(
|
const updateSelectedChatRecords = useWeChatStore(
|
||||||
state => state.updateSelectedChatRecords,
|
state => state.updateSelectedChatRecords,
|
||||||
);
|
);
|
||||||
@@ -50,14 +52,10 @@ const TransmitModal: React.FC = () => {
|
|||||||
const loadContacts = async () => {
|
const loadContacts = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
// 并行加载联系人和群组数据
|
// 从统一联系人表加载所有联系人
|
||||||
const [contractsData, groupsData] = await Promise.all([
|
const allContactsData =
|
||||||
contractService.findAll(),
|
await ContactManager.getUserContacts(currentUserId);
|
||||||
weChatGroupService.findAll(),
|
setAllContacts(allContactsData as any);
|
||||||
]);
|
|
||||||
// 合并并排序(联系人在前,群组在后)
|
|
||||||
const allContactsData = [...contractsData, ...groupsData];
|
|
||||||
setAllContacts(allContactsData);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("加载联系人数据失败:", err);
|
console.error("加载联系人数据失败:", err);
|
||||||
message.error("加载联系人数据失败");
|
message.error("加载联系人数据失败");
|
||||||
@@ -132,7 +130,7 @@ const TransmitModal: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateSelectedChatRecords([]);
|
updateSelectedChatRecords([]);
|
||||||
updateTransmitModal(false);
|
setTransmitModal(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 检查联系人是否已选择
|
// 检查联系人是否已选择
|
||||||
|
|||||||
@@ -6,13 +6,15 @@ import SmallProgramMessage from "./components/SmallProgramMessage";
|
|||||||
import VideoMessage from "./components/VideoMessage";
|
import VideoMessage from "./components/VideoMessage";
|
||||||
import ClickMenu from "./components/ClickMeau";
|
import ClickMenu from "./components/ClickMeau";
|
||||||
import LocationMessage from "./components/LocationMessage";
|
import LocationMessage from "./components/LocationMessage";
|
||||||
|
import SystemRecommendRemarkMessage from "./components/SystemRecommendRemarkMessage/index";
|
||||||
import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
||||||
import { formatWechatTime } from "@/utils/common";
|
import { formatWechatTime } from "@/utils/common";
|
||||||
import { getEmojiPath } from "@/components/EmojiSeclection/wechatEmoji";
|
import { getEmojiPath } from "@/components/EmojiSeclection/wechatEmoji";
|
||||||
import styles from "./MessageRecord.module.scss";
|
import styles from "./com.module.scss";
|
||||||
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||||
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
|
import { useContactStore } from "@/store/module/weChat/contacts";
|
||||||
import { fetchReCallApi } from "./api";
|
import { useCustomerStore } from "@weChatStore/customer";
|
||||||
|
import { fetchReCallApi, fetchVoiceToTextApi } from "./api";
|
||||||
import TransmitModal from "./components/TransmitModal";
|
import TransmitModal from "./components/TransmitModal";
|
||||||
|
|
||||||
interface MessageRecordProps {
|
interface MessageRecordProps {
|
||||||
@@ -43,17 +45,15 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
|||||||
const prevMessagesRef = useRef(currentMessages);
|
const prevMessagesRef = useRef(currentMessages);
|
||||||
const updateShowCheckbox = useWeChatStore(state => state.updateShowCheckbox);
|
const updateShowCheckbox = useWeChatStore(state => state.updateShowCheckbox);
|
||||||
const updateEnterModule = useWeChatStore(state => state.updateEnterModule);
|
const updateEnterModule = useWeChatStore(state => state.updateEnterModule);
|
||||||
const currentKf = useCkChatStore(state =>
|
const currentCustomer = useCustomerStore(state =>
|
||||||
state.kfUserList.find(kf => kf.id === contract.wechatAccountId),
|
state.customerList.find(kf => kf.id === contract.wechatAccountId),
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateSelectedChatRecords = useWeChatStore(
|
const updateSelectedChatRecords = useWeChatStore(
|
||||||
state => state.updateSelectedChatRecords,
|
state => state.updateSelectedChatRecords,
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateTransmitModal = useWeChatStore(
|
const setTransmitModal = useContactStore(state => state.setTransmitModal);
|
||||||
state => state.updateTransmitModal,
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentContract = useWeChatStore(state => state.currentContract);
|
const currentContract = useWeChatStore(state => state.currentContract);
|
||||||
const updateQuoteMessageContent = useWeChatStore(
|
const updateQuoteMessageContent = useWeChatStore(
|
||||||
@@ -277,6 +277,9 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
|||||||
case 49: // 小程序/文章/其他:图文、文件
|
case 49: // 小程序/文章/其他:图文、文件
|
||||||
return <SmallProgramMessage content={content || ""} />;
|
return <SmallProgramMessage content={content || ""} />;
|
||||||
|
|
||||||
|
case 10002: // 系统推荐备注消息
|
||||||
|
return <SystemRecommendRemarkMessage content={content || ""} />;
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
// 兼容旧版本和未知消息类型的处理逻辑
|
// 兼容旧版本和未知消息类型的处理逻辑
|
||||||
if (typeof content !== "string" || !content.trim()) {
|
if (typeof content !== "string" || !content.trim()) {
|
||||||
@@ -539,7 +542,7 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
|||||||
return messages
|
return messages
|
||||||
.filter(msg => msg !== null && msg !== undefined) // 过滤掉null和undefined的消息
|
.filter(msg => msg !== null && msg !== undefined) // 过滤掉null和undefined的消息
|
||||||
.map(msg => ({
|
.map(msg => ({
|
||||||
time: formatWechatTime(msg?.wechatTime),
|
time: formatWechatTime(String(msg?.wechatTime)),
|
||||||
messages: [msg],
|
messages: [msg],
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
@@ -633,13 +636,29 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Avatar
|
<Avatar
|
||||||
size={32}
|
size={32}
|
||||||
src={currentKf.avatar}
|
src={currentCustomer?.avatar || ""}
|
||||||
icon={<UserOutlined />}
|
icon={<UserOutlined />}
|
||||||
className={styles.messageAvatar}
|
className={styles.messageAvatar}
|
||||||
/>
|
/>
|
||||||
<div>{parseMessageContent(msg?.content, msg, msg?.msgType)}</div>
|
<div>{parseMessageContent(msg?.content, msg, msg?.msgType)}</div>
|
||||||
|
{/* 发送状态 loading 图标 */}
|
||||||
|
{msg.sendStatus === 1 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginRight: "8px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LoadingOutlined
|
||||||
|
spin
|
||||||
|
style={{ fontSize: "16px", color: "#1890ff" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -667,7 +686,7 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
|||||||
console.warn("Invalid createTime format:", firstMessage.createTime);
|
console.warn("Invalid createTime format:", firstMessage.createTime);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
timestamp = date.getTime() - 20000;
|
timestamp = date.getTime() - 24 * 36000 * 1000;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error parsing createTime:", error);
|
console.error("Error parsing createTime:", error);
|
||||||
return;
|
return;
|
||||||
@@ -678,7 +697,7 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
|||||||
|
|
||||||
const handleForwardMessage = (messageData: ChatRecord) => {
|
const handleForwardMessage = (messageData: ChatRecord) => {
|
||||||
updateSelectedChatRecords([messageData]);
|
updateSelectedChatRecords([messageData]);
|
||||||
updateTransmitModal(true);
|
setTransmitModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handRecall = messageData => {
|
const handRecall = messageData => {
|
||||||
@@ -690,6 +709,15 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handVoiceToText = messageData => {
|
||||||
|
// 音频转文字的处理逻辑
|
||||||
|
fetchVoiceToTextApi({
|
||||||
|
friendMessageId: messageData?.wechatFriendId ? messageData.id : 0,
|
||||||
|
chatroomMessageId: messageData?.wechatFriendId ? 0 : messageData.id,
|
||||||
|
seq: +new Date(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handQuote = (messageData: ChatRecord) => {
|
const handQuote = (messageData: ChatRecord) => {
|
||||||
const isGroupUser = !!currentContract?.chatroomId;
|
const isGroupUser = !!currentContract?.chatroomId;
|
||||||
const isSend = !!messageData.isSend;
|
const isSend = !!messageData.isSend;
|
||||||
@@ -732,6 +760,10 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
|||||||
// 撤回逻辑
|
// 撤回逻辑
|
||||||
handRecall(contextMenu.messageData);
|
handRecall(contextMenu.messageData);
|
||||||
break;
|
break;
|
||||||
|
case "voiceToText":
|
||||||
|
// 音频转文字逻辑
|
||||||
|
handVoiceToText(contextMenu.messageData);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -753,9 +785,34 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
|||||||
dangerouslySetInnerHTML={{ __html: msg.content }}
|
dangerouslySetInnerHTML={{ __html: msg.content }}
|
||||||
></div>
|
></div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{group.messages
|
||||||
|
.filter(v => [570425393, 90000].includes(v.msgType))
|
||||||
|
.map(msg => {
|
||||||
|
// 解析JSON字符串
|
||||||
|
let displayContent = msg.content;
|
||||||
|
try {
|
||||||
|
const parsedContent = JSON.parse(msg.content);
|
||||||
|
if (
|
||||||
|
parsedContent &&
|
||||||
|
typeof parsedContent === "object" &&
|
||||||
|
parsedContent.content
|
||||||
|
) {
|
||||||
|
displayContent = parsedContent.content;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 如果解析失败,使用原始内容
|
||||||
|
displayContent = msg.content;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div key={`divider-${msg.id}`} className={styles.messageTime}>
|
||||||
|
{displayContent}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
<div className={styles.messageTime}>{group.time}</div>
|
<div className={styles.messageTime}>{group.time}</div>
|
||||||
{group.messages
|
{group.messages
|
||||||
.filter(v => ![10000].includes(v.msgType))
|
.filter(v => ![10000, 570425393, 90000].includes(v.msgType))
|
||||||
.map(msg => {
|
.map(msg => {
|
||||||
return renderMessage(msg);
|
return renderMessage(msg);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
|||||||
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
|
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
|
||||||
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
||||||
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||||
|
import { useContactStore } from "@/store/module/weChat/contacts";
|
||||||
import { generateAiText } from "@/api/ai";
|
import { generateAiText } from "@/api/ai";
|
||||||
import TwoColumnSelection from "@/components/TwoColumnSelection/TwoColumnSelection";
|
import TwoColumnSelection from "@/components/TwoColumnSelection/TwoColumnSelection";
|
||||||
import TwoColumnMemberSelection from "@/components/MemberSelection/TwoColumnMemberSelection";
|
import TwoColumnMemberSelection from "@/components/MemberSelection/TwoColumnMemberSelection";
|
||||||
@@ -212,9 +213,7 @@ const Person: React.FC<PersonProps> = ({ contract }) => {
|
|||||||
state.getKfUserInfo(contract.wechatAccountId || 0),
|
state.getKfUserInfo(contract.wechatAccountId || 0),
|
||||||
);
|
);
|
||||||
|
|
||||||
const getSomeContractList = useCkChatStore(
|
const { getContactsByCustomer } = useContactStore();
|
||||||
state => state.getSomeContractList,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { sendCommand } = useWebSocketStore();
|
const { sendCommand } = useWebSocketStore();
|
||||||
|
|
||||||
@@ -931,10 +930,10 @@ const Person: React.FC<PersonProps> = ({ contract }) => {
|
|||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
const contractData = await getSomeContractList(
|
const contractData = getContactsByCustomer(
|
||||||
contract.wechatAccountId,
|
contract.wechatAccountId,
|
||||||
);
|
);
|
||||||
// 转换 ContractData[] 为 FriendSelectionItem[]
|
// 转换 Contact[] 为 FriendSelectionItem[]
|
||||||
const friendSelectionData = (contractData || []).map(
|
const friendSelectionData = (contractData || []).map(
|
||||||
item => ({
|
item => ({
|
||||||
id: item.id || item.serverId,
|
id: item.id || item.serverId,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user