refactor(store): 重构ckchat模块为子目录结构

feat(test): 添加数据库测试页面和工具

将ckchat相关代码移动到store/module/ckchat子目录,包含数据定义和实现文件
添加数据库测试页面和工具类,支持服务器ID与本地ID映射
移除不再使用的initSafeArea函数
This commit is contained in:
超级老白兔
2025-08-30 11:52:52 +08:00
parent c8d63bf93a
commit 13cb684abd
18 changed files with 1664 additions and 842 deletions

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react";
import { useCkChatStore } from "@/store/module/ckchat";
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
import { Form, Input, Button, Toast, Checkbox } from "antd-mobile";
import {
EyeInvisibleOutline,

View File

@@ -0,0 +1,597 @@
import React, { useState, useEffect } from "react";
import {
Button,
Card,
Statistic,
Row,
Col,
Alert,
Typography,
Space,
Spin,
Modal,
message,
} from "antd";
import {
UserOutlined,
TeamOutlined,
ContactsOutlined,
FolderOutlined,
PlayCircleOutlined,
DeleteOutlined,
ReloadOutlined,
} from "@ant-design/icons";
import { DatabaseExamples } from "@/utils/db-examples";
import {
db,
kfUserService,
groupService,
contractService,
DatabaseService,
} from "@/utils/db";
import { testDatabaseUpgrade, resetDatabase } from "@/utils/db-test";
import { KfUserListData, GroupData, ContractData } from "@/pages/pc/ckbox/data";
const { Title, Text } = Typography;
interface LogEntry {
id: number;
timestamp: string;
type: "info" | "success" | "error";
message: string;
}
const DatabaseTestPage: React.FC = () => {
const [logs, setLogs] = useState<LogEntry[]>([]);
const [isRunning, setIsRunning] = useState(false);
const [dbStats, setDbStats] = useState({
kfUsers: 0,
groups: 0,
contracts: 0,
newContactList: 0,
});
const addLog = (type: LogEntry["type"], msg: string) => {
const newLog: LogEntry = {
id: Date.now(),
timestamp: new Date().toLocaleTimeString(),
type,
message: msg,
};
setLogs(prev => [newLog, ...prev].slice(0, 100)); // 保留最新100条日志
// 使用 Ant Design 的 message 组件显示通知
if (type === "success") {
message.success(msg);
} else if (type === "error") {
message.error(msg);
} else {
message.info(msg);
}
};
const updateStats = async () => {
try {
const newContactListService = new DatabaseService(db.newContractList);
const stats = {
kfUsers: await kfUserService.count(),
groups: await groupService.count(),
contracts: await contractService.count(),
newContactList: await newContactListService.count(),
};
setDbStats(stats);
} catch (error) {
addLog("error", `统计数据更新失败: ${error}`);
}
};
const initDatabase = async () => {
try {
const result = await testDatabaseUpgrade();
if (result.success) {
addLog(
"success",
`数据库初始化成功 - 版本: ${result.version}, 表: ${result.tables?.join(", ")}`,
);
await updateStats();
} else {
addLog("error", `数据库初始化失败: ${result.error}`);
}
} catch (error) {
addLog("error", `数据库初始化失败: ${error}`);
}
};
const runKfUserExample = async () => {
setIsRunning(true);
addLog("info", "开始客服用户操作示例...");
try {
// 模拟从接口获取的客服用户数据包含服务器ID
const serverKfUser = {
id: Date.now(), // 模拟服务器返回的ID
tenantId: 1,
wechatId: `test_${Date.now()}`,
nickname: "测试客服",
alias: "客服小王",
avatar: "https://example.com/avatar.jpg",
gender: 1,
region: "北京",
signature: "专业客服,为您服务",
bindQQ: "123456789",
bindEmail: "test@example.com",
bindMobile: "13800138000",
createTime: new Date().toISOString(),
currentDeviceId: 1,
isDeleted: false,
deleteTime: "",
groupId: 1,
memo: "优秀客服",
wechatVersion: "8.0.0",
labels: ["VIP客服", "专业"],
lastUpdateTime: new Date().toISOString(),
isOnline: true,
};
const userId = await kfUserService.createWithServerId(serverKfUser);
addLog(
"success",
`创建客服用户成功本地ID: ${userId}, 服务器ID: ${serverKfUser.id}`,
);
// 查询用户按本地ID
const user = await kfUserService.findById(userId);
addLog("info", `查询到用户: ${user?.nickname}`);
// 根据服务器ID查询
const userByServerId = await kfUserService.findByServerId(
serverKfUser.id,
);
addLog("info", `根据服务器ID查询到用户: ${userByServerId?.nickname}`);
// 更新用户
await kfUserService.update(userId, {
nickname: "更新后的昵称",
lastUpdateTime: new Date().toISOString(),
});
addLog("success", "用户信息更新成功");
await updateStats();
} catch (error) {
addLog("error", `客服用户操作失败: ${error}`);
} finally {
setIsRunning(false);
}
};
const runGroupExample = async () => {
setIsRunning(true);
addLog("info", "开始群组操作示例...");
try {
// 模拟从接口获取的群组数据
const serverGroup = {
id: Date.now(), // 模拟服务器返回的ID
wechatAccountId: 1,
tenantId: 1,
accountId: 1,
chatroomId: `chatroom_${Date.now()}`,
chatroomOwner: "owner_001",
conRemark: "测试群组",
nickname: "产品讨论群",
chatroomAvatar: "https://example.com/group-avatar.jpg",
groupId: 1,
config: {
chat: true,
},
unreadCount: 0,
notice: "欢迎加入产品讨论群",
selfDisplyName: "群管理员",
};
const groupId = await groupService.createWithServerId(serverGroup);
addLog(
"success",
`创建群组成功本地ID: ${groupId}, 服务器ID: ${serverGroup.id}`,
);
// 更新群组
await groupService.update(groupId, {
unreadCount: 5,
notice: "更新的群公告",
});
addLog("success", "群组信息更新成功");
await updateStats();
} catch (error) {
addLog("error", `群组操作失败: ${error}`);
} finally {
setIsRunning(false);
}
};
const runContractExample = async () => {
setIsRunning(true);
addLog("info", "开始联系人操作示例...");
try {
// 模拟从接口获取的联系人数据
const serverContract = {
id: Date.now(), // 模拟服务器返回的ID
wechatAccountId: 1,
wechatId: `contact_${Date.now()}`,
alias: "张三",
conRemark: "重要客户",
nickname: "张总",
quanPin: "zhangsan",
avatar: "https://example.com/contact-avatar.jpg",
gender: 1,
region: "上海",
addFrom: 1,
phone: "13900139000",
labels: ["VIP客户", "重点关注"],
signature: "专业人士",
accountId: 1,
extendFields: null,
city: "上海",
lastUpdateTime: new Date().toISOString(),
isPassed: true,
tenantId: 1,
groupId: 1,
thirdParty: null,
additionalPicture: "",
desc: "优质客户",
config: {
chat: true,
},
lastMessageTime: Date.now(),
unreadCount: 0,
duplicate: false,
};
const contractId =
await contractService.createWithServerId(serverContract);
addLog(
"success",
`创建联系人成功本地ID: ${contractId}, 服务器ID: ${serverContract.id}`,
);
// 查询联系人
const searchResults = await contractService.findWhereStartsWith(
"nickname",
"张",
);
addLog("info", `找到姓张的联系人: ${searchResults.length}`);
await updateStats();
} catch (error) {
addLog("error", `联系人操作失败: ${error}`);
} finally {
setIsRunning(false);
}
};
const runBatchExample = async () => {
setIsRunning(true);
addLog("info", "开始批量操作示例...");
try {
// 模拟从接口获取的批量联系人数据
const batchServerContacts = Array.from({ length: 5 }, (_, i) => ({
id: Date.now() + i, // 模拟服务器返回的ID
wechatAccountId: 1,
wechatId: `batch_${Date.now()}_${i}`,
alias: `批量用户${i + 1}`,
conRemark: "批量导入",
nickname: `用户${i + 1}`,
quanPin: `yonghu${i + 1}`,
gender: (i % 2) + 1,
region: i % 2 === 0 ? "北京" : "上海",
addFrom: 3,
phone: `1380000000${i}`,
labels: ["批量导入"],
signature: "",
accountId: 1,
extendFields: null,
lastUpdateTime: new Date().toISOString(),
isPassed: true,
tenantId: 1,
groupId: 1,
thirdParty: null,
additionalPicture: "",
desc: "批量导入的用户",
lastMessageTime: Date.now(),
unreadCount: 0,
duplicate: false,
}));
const batchIds =
await contractService.createManyWithServerId(batchServerContacts);
addLog(
"success",
`批量创建联系人成功,创建了 ${batchIds.length} 个联系人`,
);
await updateStats();
} catch (error) {
addLog("error", `批量操作失败: ${error}`);
} finally {
setIsRunning(false);
}
};
const clearAllData = async () => {
Modal.confirm({
title: "确认清空数据",
content: "确定要清空所有测试数据吗?此操作不可恢复!",
okText: "确定",
cancelText: "取消",
onOk: async () => {
setIsRunning(true);
addLog("info", "开始清空数据...");
try {
await db.transaction(
"rw",
[db.kfUsers, db.groups, db.contracts, db.newContractList],
async () => {
await db.kfUsers.clear();
await db.groups.clear();
await db.contracts.clear();
await db.newContractList.clear();
},
);
addLog("success", "所有数据清空成功");
await updateStats();
} catch (error) {
addLog("error", `清空数据失败: ${error}`);
} finally {
setIsRunning(false);
}
},
});
};
const runAllExamples = async () => {
setIsRunning(true);
addLog("info", "开始运行所有示例...");
try {
await DatabaseExamples.runAllExamples();
addLog("success", "所有示例运行完成");
await updateStats();
} catch (error) {
addLog("error", `运行示例失败: ${error}`);
} finally {
setIsRunning(false);
}
};
useEffect(() => {
initDatabase();
}, []);
return (
<div style={{ padding: "24px", maxWidth: "1200px", margin: "0 auto" }}>
<Title level={2} style={{ textAlign: "center", marginBottom: "32px" }}>
</Title>
{/* 数据统计 */}
<Row gutter={[16, 16]} style={{ marginBottom: "32px" }}>
<Col xs={12} sm={6}>
<Card>
<Statistic
title="客服用户"
value={dbStats.kfUsers}
prefix={<UserOutlined />}
valueStyle={{ color: "#1890ff" }}
/>
</Card>
</Col>
<Col xs={12} sm={6}>
<Card>
<Statistic
title="群组"
value={dbStats.groups}
prefix={<TeamOutlined />}
valueStyle={{ color: "#52c41a" }}
/>
</Card>
</Col>
<Col xs={12} sm={6}>
<Card>
<Statistic
title="联系人"
value={dbStats.contracts}
prefix={<ContactsOutlined />}
valueStyle={{ color: "#faad14" }}
/>
</Card>
</Col>
<Col xs={12} sm={6}>
<Card>
<Statistic
title="联系人分组"
value={dbStats.newContactList}
prefix={<FolderOutlined />}
valueStyle={{ color: "#722ed1" }}
/>
</Card>
</Col>
</Row>
{/* 操作按钮 */}
<Card title="数据库操作" style={{ marginBottom: "24px" }}>
<Space wrap size="middle">
<Button
type="primary"
icon={<UserOutlined />}
onClick={runKfUserExample}
loading={isRunning}
size="large"
>
</Button>
<Button
type="primary"
icon={<TeamOutlined />}
onClick={runGroupExample}
loading={isRunning}
size="large"
style={{ backgroundColor: "#52c41a", borderColor: "#52c41a" }}
>
</Button>
<Button
type="primary"
icon={<ContactsOutlined />}
onClick={runContractExample}
loading={isRunning}
size="large"
style={{ backgroundColor: "#faad14", borderColor: "#faad14" }}
>
</Button>
<Button
type="primary"
icon={<FolderOutlined />}
onClick={runBatchExample}
loading={isRunning}
size="large"
style={{ backgroundColor: "#722ed1", borderColor: "#722ed1" }}
>
</Button>
<Button
type="primary"
icon={<PlayCircleOutlined />}
onClick={runAllExamples}
loading={isRunning}
size="large"
style={{ backgroundColor: "#13c2c2", borderColor: "#13c2c2" }}
>
</Button>
<Button
danger
icon={<DeleteOutlined />}
onClick={clearAllData}
loading={isRunning}
size="large"
>
</Button>
<Button
danger
icon={<ReloadOutlined />}
onClick={async () => {
Modal.confirm({
title: "确认重置数据库",
content:
"确定要重置数据库吗?这将删除所有数据并重新创建数据库!",
okText: "确定",
cancelText: "取消",
onOk: async () => {
setIsRunning(true);
try {
const result = await resetDatabase();
if (result.success) {
addLog(
"success",
`数据库重置成功 - 版本: ${result.version}, 表: ${result.tables?.join(", ")}`,
);
await updateStats();
} else {
addLog("error", `数据库重置失败: ${result.error}`);
}
} catch (error) {
addLog("error", `数据库重置失败: ${error}`);
} finally {
setIsRunning(false);
}
},
});
}}
loading={isRunning}
size="large"
>
</Button>
</Space>
</Card>
{/* 运行状态 */}
{isRunning && (
<Alert
message="操作进行中..."
type="info"
showIcon
icon={<Spin />}
style={{ marginBottom: "24px" }}
/>
)}
{/* 日志显示 */}
<Card
title="操作日志"
extra={
<Button
type="text"
icon={<ReloadOutlined />}
onClick={() => setLogs([])}
size="small"
>
</Button>
}
>
<div style={{ maxHeight: "400px", overflowY: "auto" }}>
{logs.length === 0 ? (
<div
style={{ textAlign: "center", padding: "40px 0", color: "#999" }}
>
<Text type="secondary"></Text>
</div>
) : (
<Space direction="vertical" style={{ width: "100%" }} size="small">
{logs.map(log => (
<Alert
key={log.id}
message={
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "flex-start",
}}
>
<span style={{ flex: 1 }}>{log.message}</span>
<Text
type="secondary"
style={{ fontSize: "12px", marginLeft: "8px" }}
>
{log.timestamp}
</Text>
</div>
}
type={
log.type === "success"
? "success"
: log.type === "error"
? "error"
: "info"
}
showIcon
style={{ margin: 0 }}
/>
))}
</Space>
)}
</div>
</Card>
</div>
);
};
export default DatabaseTestPage;

View File

@@ -24,7 +24,7 @@ import {
CheckOutlined,
} from "@ant-design/icons";
import { ContractData } from "@/pages/pc/ckbox/data";
import { useCkChatStore } from "@/store/module/ckchat";
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
import styles from "./Person.module.scss";
const { Sider } = Layout;

View File

@@ -40,7 +40,7 @@ import { clearUnreadCount, getMessages } from "@/pages/pc/ckbox/api";
import styles from "./ChatWindow.module.scss";
import { useWebSocketStore, WebSocketMessage } from "@/store/module/websocket";
import { formatWechatTime } from "@/utils/common";
import { useCkChatStore } from "@/store/module/ckchat";
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
import Person from "./components/Person";
const { Header, Content, Footer } = Layout;
const { TextArea } = Input;

View File

@@ -2,7 +2,7 @@ import React, { useState, useCallback, useEffect } from "react";
import { List, Avatar, Collapse, Button } from "antd";
import type { CollapseProps } from "antd";
import styles from "./WechatFriends.module.scss";
import { useCkChatStore } from "@/store/module/ckchat";
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
import { ContractData, GroupData } from "@/pages/pc/ckbox/data";
interface WechatFriendsProps {

View File

@@ -10,7 +10,7 @@ import { ContractData, GroupData } from "@/pages/pc/ckbox/data";
import WechatFriends from "./WechatFriends";
import MessageList from "./MessageList/index";
import styles from "./SidebarMenu.module.scss";
import { getChatSessions } from "@/store/module/ckchat";
import { getChatSessions } from "@/store/module/ckchat/ckchat";
interface SidebarMenuProps {
contracts: ContractData[] | GroupData[];

View File

@@ -1,7 +1,7 @@
import React, { useState } from "react";
import { Avatar, Badge, Tooltip } from "antd";
import styles from "./VerticalUserList.module.scss";
import { useCkChatStore, asyncKfSelected } from "@/store/module/ckchat";
import { useCkChatStore, asyncKfSelected } from "@/store/module/ckchat/ckchat";
import { TeamOutlined } from "@ant-design/icons";
const VerticalUserList: React.FC = () => {

View File

@@ -7,7 +7,7 @@ import SidebarMenu from "./components/SidebarMenu/index";
import VerticalUserList from "./components/VerticalUserList";
import PageSkeleton from "./components/Skeleton";
import styles from "./index.module.scss";
import { addChatSession } from "@/store/module/ckchat";
import { addChatSession } from "@/store/module/ckchat/ckchat";
const { Header, Content, Sider } = Layout;
import { chatInitAPIdata } from "./main";
import { KfUserListData, GroupData, ContractData } from "@/pages/pc/ckbox/data";

View File

@@ -4,7 +4,7 @@ import {
asyncContractList,
asyncChatSessions,
asyncNewContractList,
} from "@/store/module/ckchat";
} from "@/store/module/ckchat/ckchat";
import { useWebSocketStore } from "@/store/module/websocket";
import {

View File

@@ -1,6 +1,7 @@
import SelectTest from "@/pages/mobile/test/select";
import TestIndex from "@/pages/mobile/test/index";
import UploadTest from "@/pages/mobile/test/upload";
import DbTest from "@/pages/mobile/test/db";
import UpdateNotificationTest from "@/pages/mobile/test/update-notification";
import IframeDebugPage from "@/pages/iframe";
import { DEV_FEATURES } from "@/utils/env";
@@ -33,6 +34,11 @@ const componentTestRoutes = DEV_FEATURES.SHOW_TEST_PAGES
element: <IframeDebugPage />,
auth: false, // 不需要认证,方便调试
},
{
path: "/test/db",
element: <DbTest />,
auth: false, // 不需要认证,方便调试
},
]
: [];

View File

@@ -1,5 +1,5 @@
import { createPersistStore } from "@/store/createPersistStore";
import { CkChatState, CkUserInfo, CkTenant } from "./ckchat.data";
import { CkChatState, CkUserInfo, CkTenant } from "./ckchat/ckchat.data";
import {
ContractData,
GroupData,

View File

@@ -0,0 +1,205 @@
import { createPersistStore } from "@/store/createPersistStore";
import { CkChatState, CkUserInfo, CkTenant } from "./ckchat.data";
import {
ContractData,
GroupData,
CkAccount,
KfUserListData,
} from "@/pages/pc/ckbox/data";
export const useCkChatStore = createPersistStore<CkChatState>(
set => ({
userInfo: null,
isLoggedIn: false,
contractList: [], //联系人列表
chatSessions: [], //聊天会话
kfUserList: [], //客服列表
kfSelected: 0,
newContractList: [], //联系人分组
kfSelectedUser: () => {
const state = useCkChatStore.getState();
return state.kfUserList.find(item => item.id === state.kfSelected);
},
asyncKfSelected: (data: number) => {
set({ kfSelected: data });
},
// 异步设置会话列表
asyncNewContractList: data => {
set({ newContractList: data });
},
getNewContractList: () => {
const state = useCkChatStore.getState();
return state.newContractList;
},
// 异步设置会话列表
asyncChatSessions: data => {
set({ chatSessions: data });
},
// 异步设置联系人列表
asyncContractList: data => {
set({ contractList: data });
},
// 控制终端用户列表
getkfUserList: () => {
const state = useCkChatStore.getState();
return state.kfUserList;
},
asyncKfUserList: data => {
set({ kfUserList: data });
},
// 删除控制终端用户
deleteCtrlUser: (userId: number) => {
set(state => ({
kfUserList: state.kfUserList.filter(item => item.id !== userId),
}));
},
// 更新控制终端用户
updateCtrlUser: (user: KfUserListData) => {
set(state => ({
kfUserList: state.kfUserList.map(item =>
item.id === user.id ? user : item,
),
}));
},
// 清空控制终端用户列表
clearkfUserList: () => {
set({ kfUserList: [] });
},
// 获取聊天会话
getChatSessions: () => {
const state = useCkChatStore.getState();
return state.chatSessions;
},
// 添加聊天会话
addChatSession: (session: ContractData | GroupData) => {
set(state => {
// 检查是否已存在相同id的会话
const exists = state.chatSessions.some(item => item.id === session.id);
// 如果已存在则不添加,否则添加到列表中
return {
chatSessions: exists
? state.chatSessions
: [...state.chatSessions, session as ContractData | GroupData],
};
});
},
// 更新聊天会话
updateChatSession: (session: ContractData | GroupData) => {
set(state => ({
chatSessions: state.chatSessions.map(item =>
item.id === session.id ? session : item,
),
}));
},
// 删除聊天会话
deleteChatSession: (sessionId: string) => {
set(state => ({
chatSessions: state.chatSessions.filter(item => item.id !== sessionId),
}));
},
// 设置用户信息
setUserInfo: (userInfo: CkUserInfo) => {
set({ userInfo, isLoggedIn: true });
},
// 清除用户信息
clearUserInfo: () => {
set({ userInfo: null, isLoggedIn: false });
},
// 更新账户信息
updateAccount: (account: Partial<CkAccount>) => {
set(state => ({
userInfo: state.userInfo
? {
...state.userInfo,
account: { ...state.userInfo.account, ...account },
}
: null,
}));
},
// 更新租户信息
updateTenant: (tenant: Partial<CkTenant>) => {
set(state => ({
userInfo: state.userInfo
? {
...state.userInfo,
tenant: { ...state.userInfo.tenant, ...tenant },
}
: null,
}));
},
// 获取账户ID
getAccountId: () => {
const state = useCkChatStore.getState();
return Number(state.userInfo?.account?.id) || null;
},
// 获取租户ID
getTenantId: () => {
const state = useCkChatStore.getState();
return state.userInfo?.tenant?.id || null;
},
// 获取账户名称
getAccountName: () => {
const state = useCkChatStore.getState();
return (
state.userInfo?.account?.realName ||
state.userInfo?.account?.userName ||
null
);
},
// 获取租户名称
getTenantName: () => {
const state = useCkChatStore.getState();
return state.userInfo?.tenant?.name || null;
},
}),
{
name: "ckchat-store",
partialize: state => ({
userInfo: state.userInfo,
isLoggedIn: state.isLoggedIn,
}),
onRehydrateStorage: () => state => {
// console.log("CkChat store hydrated:", state);
},
},
);
// 导出便捷的获取方法
export const getCkAccountId = () => useCkChatStore.getState().getAccountId();
export const getCkTenantId = () => useCkChatStore.getState().getTenantId();
export const getCkAccountName = () =>
useCkChatStore.getState().getAccountName();
export const getCkTenantName = () => useCkChatStore.getState().getTenantName();
export const getChatSessions = () =>
useCkChatStore.getState().getChatSessions();
export const addChatSession = (session: ContractData | GroupData) =>
useCkChatStore.getState().addChatSession(session);
export const updateChatSession = (session: ContractData | GroupData) =>
useCkChatStore.getState().updateChatSession(session);
export const deleteChatSession = (sessionId: string) =>
useCkChatStore.getState().deleteChatSession(sessionId);
export const getkfUserList = () => useCkChatStore.getState().kfUserList;
export const addCtrlUser = (user: KfUserListData) =>
useCkChatStore.getState().addCtrlUser(user);
export const deleteCtrlUser = (userId: number) =>
useCkChatStore.getState().deleteCtrlUser(userId);
export const updateCtrlUser = (user: KfUserListData) =>
useCkChatStore.getState().updateCtrlUser(user);
export const asyncKfUserList = (data: KfUserListData[]) =>
useCkChatStore.getState().asyncKfUserList(data);
export const asyncContractList = (data: ContractData[]) =>
useCkChatStore.getState().asyncContractList(data);
export const asyncChatSessions = (data: ContractData[]) =>
useCkChatStore.getState().asyncChatSessions(data);
export const asyncNewContractList = (
data: { groupName: string; contacts: any[] }[],
) => useCkChatStore.getState().asyncNewContractList(data);
export const asyncKfSelected = (data: number) =>
useCkChatStore.getState().asyncKfSelected(data);

View File

@@ -1,7 +1,7 @@
import { createPersistStore } from "@/store/createPersistStore";
import { Toast } from "antd-mobile";
import { useUserStore } from "./user";
import { useCkChatStore } from "@/store/module/ckchat";
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
const { getAccountId } = useCkChatStore.getState();
// WebSocket消息类型

View File

@@ -113,9 +113,3 @@ export function getSafeAreaHeight() {
// 3. 默认值
return 0;
}
// 设置全局 CSS 变量
export function initSafeArea() {
const root = document.documentElement;
const height = getSafeAreaHeight();
root.style.setProperty("--safe-area-top", `${height}px`);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,146 @@
/**
* 数据库版本升级测试脚本
* 用于验证数据库版本升级逻辑是否正常工作
*/
import { db } from "./db";
// 重置数据库(完全删除并重新创建)
export async function resetDatabase() {
try {
console.log("开始重置数据库...");
// 关闭数据库连接
if (db.isOpen()) {
db.close();
}
// 删除数据库
await db.delete();
console.log("旧数据库已删除");
// 重新打开数据库(这会创建新的数据库)
await db.open();
console.log("数据库重置成功!");
console.log("当前数据库版本:", db.verno);
console.log("数据库名称:", db.name);
return {
success: true,
version: db.verno,
tables: db.tables.map(table => table.name),
message: "数据库重置成功",
};
} catch (error) {
console.error("数据库重置失败:", error);
return {
success: false,
error: error instanceof Error ? error.message : String(error),
message: "数据库重置失败",
};
}
}
// 测试数据库初始化和版本升级
export async function testDatabaseUpgrade() {
try {
console.log("开始测试数据库初始化...");
// 首先尝试正常打开数据库
try {
await db.open();
} catch (upgradeError) {
// 如果遇到升级错误,尝试重置数据库
if (
upgradeError.message &&
upgradeError.message.includes("primary key")
) {
console.log("检测到主键冲突,尝试重置数据库...");
const resetResult = await resetDatabase();
if (!resetResult.success) {
throw new Error(`数据库重置失败: ${resetResult.error}`);
}
} else {
throw upgradeError;
}
}
console.log("数据库初始化成功!");
console.log("当前数据库版本:", db.verno);
console.log("数据库名称:", db.name);
// 检查表是否存在
const tables = db.tables.map(table => table.name);
console.log("数据库表:", tables);
// 测试基本操作
const testData = {
tenantId: 1, // 修正为number类型
wechatId: "test-wechat-id",
nickname: "测试用户",
alias: "测试别名",
};
// 测试创建数据
const userId = await db.kfUsers.add({
...testData,
id: 0, // 添加必需的id字段
currentDeviceId: 0,
isDeleted: false,
deleteTime: "",
groupId: 0,
memo: "", // 备注信息
wechatVersion: "",
labels: [],
lastUpdateTime: new Date().toISOString(), // 修复语法错误,使用字符串类型
serverId: "test-server-id-001", // 提供有意义的测试值
// 移除不属于KfUserListData接口的字段
signature: "",
bindQQ: "",
bindEmail: "",
bindMobile: "",
bindWeixin: "",
bindAlipay: "",
bindTaobao: "",
bindJd: "",
bindDouyin: "",
bindKuaishou: "",
bindBilibili: "",
avatar: "",
gender: 0,
region: "",
createTime: new Date().toISOString(), // 使用字符串类型
});
console.log("创建测试用户成功ID:", userId);
// 测试查询数据
const user = await db.kfUsers.get(userId);
console.log("查询测试用户:", user);
// 清理测试数据
await db.kfUsers.delete(userId);
console.log("清理测试数据完成");
return {
success: true,
version: db.verno,
tables: tables,
message: "数据库版本升级测试通过",
};
} catch (error) {
console.error("数据库测试失败:", error);
return {
success: false,
error: error instanceof Error ? error.message : String(error),
message: "数据库版本升级测试失败",
};
}
}
// 如果直接运行此文件,执行测试
if (typeof window === "undefined") {
testDatabaseUpgrade().then(result => {
console.log("测试结果:", result);
});
}

View File

@@ -1,102 +1,111 @@
/**
* 数据库工具类 - 解决服务器ID与本地自增主键冲突问题
*
* 问题描述:
* 接口返回的数据包含id字段直接存储到数据库会与Dexie的自增主键(++id)产生冲突
*
* 解决方案:
* 1. 将服务器返回的id字段映射为serverId字段存储
* 2. 数据库使用自增的id作为主键
* 3. 提供专门的方法处理服务器数据的存储和查询
*
* 使用方法:
* - 存储接口数据:使用 createWithServerId() 或 createManyWithServerId()
* - 查询服务器数据:使用 findByServerId()
* - 常规操作:使用原有的 create(), findById() 等方法
*
* 示例:
* const serverData = { id: 1001, name: '测试', ... }; // 接口返回的数据
* const localId = await service.createWithServerId(serverData); // 存储返回本地ID
* const data = await service.findByServerId(1001); // 根据服务器ID查询
*/
import Dexie, { Table } from "dexie";
import { KfUserListData, GroupData, ContractData } from "@/pages/pc/ckbox/data";
// 定义数据库表结构接口
export interface BaseEntity {
// 扩展数据类型添加serverId字段
export interface KfUserWithServerId extends KfUserListData {
serverId?: number | string; // 服务器返回的原始ID
}
export interface GroupWithServerId extends GroupData {
serverId?: number | string; // 服务器返回的原始ID
}
export interface ContractWithServerId extends ContractData {
serverId?: number | string; // 服务器返回的原始ID
}
// 新联系人列表数据接口
export interface NewContactListData {
id?: number;
createdAt?: Date;
updatedAt?: Date;
}
export interface User extends BaseEntity {
name: string;
email: string;
avatar?: string;
status: "active" | "inactive";
}
export interface Message extends BaseEntity {
userId: number;
content: string;
type: "text" | "image" | "file";
isRead: boolean;
}
export interface ChatRoom extends BaseEntity {
name: string;
description?: string;
memberIds: number[];
lastMessageAt?: Date;
}
export interface Setting extends BaseEntity {
key: string;
value: any;
category: string;
serverId?: number | string; // 服务器返回的原始ID
groupName: string;
contacts: ContractData[] | GroupData[];
}
// 数据库类
class AppDatabase extends Dexie {
users!: Table<User>;
messages!: Table<Message>;
chatRooms!: Table<ChatRoom>;
settings!: Table<Setting>;
class CunkebaoDatabase extends Dexie {
kfUsers!: Table<KfUserWithServerId>;
groups!: Table<GroupWithServerId>;
contracts!: Table<ContractWithServerId>;
newContractList!: Table<NewContactListData>;
constructor() {
super("CunkebaoDatabase");
// 版本1初始版本不包含serverId字段
this.version(1).stores({
users: "++id, name, email, status, createdAt",
messages: "++id, userId, type, isRead, createdAt",
chatRooms: "++id, name, lastMessageAt, createdAt",
settings: "++id, key, category, createdAt",
kfUsers:
"++id, tenantId, wechatId, nickname, alias, avatar, gender, region, signature, bindQQ, bindEmail, bindMobile, createTime, currentDeviceId, isDeleted, deleteTime, groupId, memo, wechatVersion, lastUpdateTime, isOnline",
groups:
"++id, wechatAccountId, tenantId, accountId, chatroomId, chatroomOwner, conRemark, nickname, chatroomAvatar, groupId, config, unreadCount, notice, selfDisplyName",
contracts:
"++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: "++id, groupName, contacts",
});
// 自动添加时间戳
this.users.hook("creating", (primKey, obj, trans) => {
obj.createdAt = new Date();
obj.updatedAt = new Date();
});
this.users.hook("updating", (modifications, primKey, obj, trans) => {
modifications.updatedAt = new Date();
});
this.messages.hook("creating", (primKey, obj, trans) => {
obj.createdAt = new Date();
obj.updatedAt = new Date();
});
this.chatRooms.hook("creating", (primKey, obj, trans) => {
obj.createdAt = new Date();
obj.updatedAt = new Date();
});
this.settings.hook("creating", (primKey, obj, trans) => {
obj.createdAt = new Date();
obj.updatedAt = new Date();
});
// 版本2添加serverId字段支持
this.version(2)
.stores({
kfUsers:
"++id, serverId, tenantId, wechatId, nickname, alias, avatar, gender, region, signature, bindQQ, bindEmail, bindMobile, createTime, currentDeviceId, isDeleted, deleteTime, groupId, memo, wechatVersion, lastUpdateTime, isOnline",
groups:
"++id, serverId, wechatAccountId, tenantId, accountId, chatroomId, chatroomOwner, conRemark, nickname, chatroomAvatar, groupId, config, unreadCount, notice, selfDisplyName",
contracts:
"++id, serverId, 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: "++id, serverId, groupName, contacts",
})
.upgrade(tx => {
// 数据库升级逻辑为现有数据添加serverId字段可选
console.log("数据库升级到版本2添加serverId字段支持");
// 注意这里不需要迁移数据因为serverId是可选字段
// 如果需要迁移现有数据,可以在这里添加相应逻辑
});
}
}
// 创建数据库实例
export const db = new AppDatabase();
export const db = new CunkebaoDatabase();
// 通用数据库操作类
export class DatabaseService<T extends BaseEntity> {
// 简单的数据库操作类
export class DatabaseService<T> {
constructor(private table: Table<T>) {}
// 基础 CRUD 操作
async create(
data: Omit<T, "id" | "createdAt" | "updatedAt">,
): Promise<number> {
async create(data: Omit<T, "id">): Promise<number> {
return await this.table.add(data as T);
}
async createMany(
dataList: Omit<T, "id" | "createdAt" | "updatedAt">[],
): Promise<number[]> {
return await this.table.bulkAdd(dataList as T[], { allKeys: true });
// 创建数据处理服务器ID映射
// 用于存储从接口获取的数据将服务器的id字段映射为serverId避免与数据库自增主键冲突
async createWithServerId(data: any): Promise<number> {
const { id, ...restData } = data;
const dataToInsert = {
...restData,
serverId: id, // 将服务器的id映射为serverId
};
return await this.table.add(dataToInsert as T);
}
async findById(id: number): Promise<T | undefined> {
@@ -107,49 +116,46 @@ export class DatabaseService<T extends BaseEntity> {
return await this.table.toArray();
}
async findByIds(ids: number[]): Promise<T[]> {
return await this.table.where("id").anyOf(ids).toArray();
}
async update(
id: number,
data: Partial<Omit<T, "id" | "createdAt">>,
): Promise<number> {
return await this.table.update(id, data);
async update(id: number, data: Partial<T>): Promise<number> {
return await this.table.update(id, data as any);
}
async updateMany(
updates: { id: number; data: Partial<Omit<T, "id" | "createdAt">> }[],
dataList: { id: number; data: Partial<T> }[],
): Promise<number> {
return await this.table.bulkUpdate(
updates.map(u => ({ key: u.id, changes: u.data })),
dataList.map(item => ({
key: item.id,
changes: item.data as any,
})),
);
}
async createMany(dataList: Omit<T, "id">[]): Promise<number[]> {
return await this.table.bulkAdd(dataList as T[], { allKeys: true });
}
// 批量创建数据处理服务器ID映射
// 用于批量存储从接口获取的数据将服务器的id字段映射为serverId
async createManyWithServerId(dataList: any[]): Promise<number[]> {
const processedData = dataList.map(item => {
const { id, ...restData } = item;
return {
...restData,
serverId: id, // 将服务器的id映射为serverId
};
});
return await this.table.bulkAdd(processedData as T[], { allKeys: true });
}
async delete(id: number): Promise<void> {
await this.table.delete(id);
}
async deleteMany(ids: number[]): Promise<void> {
await this.table.bulkDelete(ids);
}
async deleteAll(): Promise<void> {
async clear(): Promise<void> {
await this.table.clear();
}
// 分页查询
async paginate(
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 findWhere(field: keyof T, value: any): Promise<T[]> {
return await this.table
@@ -158,6 +164,13 @@ export class DatabaseService<T extends BaseEntity> {
.toArray();
}
// 根据服务器ID查询
// 用于根据原始的服务器ID查找数据
async findByServerId(serverId: any): Promise<T | undefined> {
return await this.table.where("serverId").equals(serverId).first();
}
// 多值查询IN 查询)
async findWhereIn(field: keyof T, values: any[]): Promise<T[]> {
return await this.table
.where(field as string)
@@ -165,6 +178,23 @@ export class DatabaseService<T extends BaseEntity> {
.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)
@@ -172,6 +202,68 @@ export class DatabaseService<T extends BaseEntity> {
.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,
@@ -183,264 +275,24 @@ export class DatabaseService<T extends BaseEntity> {
: await collection.toArray();
}
// 搜索功能
async search(field: keyof T, keyword: string): Promise<T[]> {
return await this.table
.where(field as string)
.startsWithIgnoreCase(keyword)
.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();
}
// 存在性检查
async exists(id: number): Promise<boolean> {
const item = await this.table.get(id);
return !!item;
}
async existsWhere(field: keyof T, value: any): Promise<boolean> {
const count = await this.table
.where(field as string)
.equals(value)
.count();
return count > 0;
}
}
// 创建各表的服务实例
export const userService = new DatabaseService(db.users);
export const messageService = new DatabaseService(db.messages);
export const chatRoomService = new DatabaseService(db.chatRooms);
export const settingService = new DatabaseService(db.settings);
// 专门的业务方法
export class UserService extends DatabaseService<User> {
constructor() {
super(db.users);
}
async findByEmail(email: string): Promise<User | undefined> {
return await db.users.where("email").equals(email).first();
}
async findActiveUsers(): Promise<User[]> {
return await db.users.where("status").equals("active").toArray();
}
async searchByName(name: string): Promise<User[]> {
return await db.users.where("name").startsWithIgnoreCase(name).toArray();
}
async updateStatus(
id: number,
status: "active" | "inactive",
): Promise<number> {
return await this.update(id, { status });
}
}
export class MessageService extends DatabaseService<Message> {
constructor() {
super(db.messages);
}
async findByUserId(userId: number): Promise<Message[]> {
return await db.messages.where("userId").equals(userId).toArray();
}
async findUnreadMessages(): Promise<Message[]> {
return await db.messages.where("isRead").equals(false).toArray();
}
async markAsRead(id: number): Promise<number> {
return await this.update(id, { isRead: true });
}
async markAllAsRead(userId: number): Promise<number> {
const messages = await db.messages
.where("userId")
.equals(userId)
.and(msg => !msg.isRead)
.toArray();
const updates = messages.map(msg => ({
id: msg.id!,
data: { isRead: true },
}));
return await this.updateMany(updates);
}
async getRecentMessages(limit: number = 50): Promise<Message[]> {
return await db.messages
.orderBy("createdAt")
.reverse()
.limit(limit)
.toArray();
}
}
export class SettingService extends DatabaseService<Setting> {
constructor() {
super(db.settings);
}
async getSetting(key: string): Promise<any> {
const setting = await db.settings.where("key").equals(key).first();
return setting?.value;
}
async setSetting(
key: string,
value: any,
category: string = "general",
): Promise<number> {
const existing = await db.settings.where("key").equals(key).first();
if (existing) {
return await this.update(existing.id!, { value });
} else {
return await this.create({ key, value, category });
}
}
async getSettingsByCategory(category: string): Promise<Setting[]> {
return await db.settings.where("category").equals(category).toArray();
}
async deleteSetting(key: string): Promise<void> {
await db.settings.where("key").equals(key).delete();
}
}
// 数据库工具类
export class DatabaseUtils {
// 数据导出
static async exportData(): Promise<string> {
const data = {
users: await db.users.toArray(),
messages: await db.messages.toArray(),
chatRooms: await db.chatRooms.toArray(),
settings: await db.settings.toArray(),
exportedAt: new Date().toISOString(),
};
return JSON.stringify(data, null, 2);
}
// 数据导入
static async importData(jsonData: string): Promise<void> {
try {
const data = JSON.parse(jsonData);
await db.transaction(
"rw",
[db.users, db.messages, db.chatRooms, db.settings],
async () => {
if (data.users) await db.users.bulkPut(data.users);
if (data.messages) await db.messages.bulkPut(data.messages);
if (data.chatRooms) await db.chatRooms.bulkPut(data.chatRooms);
if (data.settings) await db.settings.bulkPut(data.settings);
},
);
} catch (error) {
throw new Error("导入数据失败: " + error);
}
}
// 清空所有数据
static async clearAllData(): Promise<void> {
await db.transaction(
"rw",
[db.users, db.messages, db.chatRooms, db.settings],
async () => {
await db.users.clear();
await db.messages.clear();
await db.chatRooms.clear();
await db.settings.clear();
},
);
}
// 获取数据库统计信息
static async getStats(): Promise<{
users: number;
messages: number;
chatRooms: number;
settings: number;
totalSize: number;
}> {
const [users, messages, chatRooms, settings] = await Promise.all([
db.users.count(),
db.messages.count(),
db.chatRooms.count(),
db.settings.count(),
]);
// 估算数据库大小(简单估算)
const totalSize = users + messages + chatRooms + settings;
return { users, messages, chatRooms, settings, totalSize };
}
// 数据库健康检查
static async healthCheck(): Promise<{
status: "healthy" | "error";
message: string;
}> {
try {
await db.users.limit(1).toArray();
return { status: "healthy", message: "数据库连接正常" };
} catch (error) {
return { status: "error", message: "数据库连接异常: " + error };
}
}
// 数据备份到文件
static async backupToFile(): Promise<void> {
const data = await this.exportData();
const blob = new Blob([data], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `cunkebao-backup-${new Date().toISOString().split("T")[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// 从文件恢复数据
static async restoreFromFile(file: File): Promise<void> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = async e => {
try {
const jsonData = e.target?.result as string;
await this.importData(jsonData);
resolve();
} catch (error) {
reject(error);
}
};
reader.onerror = () => reject(new Error("文件读取失败"));
reader.readAsText(file);
});
}
}
// 创建业务服务实例
export const userBusinessService = new UserService();
export const messageBusinessService = new MessageService();
export const settingBusinessService = new SettingService();
export const kfUserService = new DatabaseService(db.kfUsers);
export const groupService = new DatabaseService(db.groups);
export const contractService = new DatabaseService(db.contracts);
// 默认导出数据库实例
export default db;