需求背景:
很多时候,我们会拿微信或者QQ作为自己的备忘录,一是方便记录,二是可以借助聊天工具自带的功能对消息进行管理,从而实现备忘录的同等功效,但是聊天工具在备忘录管理的功能上还是缺少,例如分类管理、内容碎片化管理、归档等功能。基于此,在常见的聊天工具背景下,也实现一个既类似于聊天的一个界面(单向聊天)、也可以实现消息的管理。
需求描述:
-
实现聊天的功能:包括消息的发送、引用、撤回、置顶、提醒、多选、合并、删除等功能;
-
实现消息管理的功能:消息分类、消息的再编辑、消息的合并、删除、归档、数据迁移等功能;
-
实现消息的离线本地持久化存储,采用SQLite,实现本地存储。
-
所有功能的实现采用AI工具cursor实现。
预览效果:
1.消息的构造、发送
在主页面中,参照主流聊天工具设计自己的功能,不同的是我们不需要对方回复,也就不需要向后台发送数据。这里设计获取输入框的文本构造自己的消息newMessage
,将列表数据发送给插入到SQLite。
//发送新消息
sendMessage() {
if (this.inputMessage.trim()) {
const now = new Date()
const timeString = now.toISOString()
const newMessage = {
uuid: this.generateUUID(),
text: this.inputMessage.trim(),
time: timeString,
category:'',
isPinned: false,
isRecalled: false,
isQuoted: this.quotingMessage !== null,
quotedText: this.quotingMessage ? this.quotingMessage.text : '',
quotedIndex: this.quotingMessage ? this.quotingMessage.uuid : null,
isMerged: false
}
this.messages.unshift(newMessage)
this.inputMessage = ''
this.saveMessagedb(newMessage)
this.$refs.messageInput.focus()
})
}
},
将newMessage
装到现在的messages
里,并把this.saveMessagedb(newMessage)
更新到数据库里。
为保证数据的唯一性,添加了一个generateUUID()
,作为uuid
//uuid生成
generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
2.消息的存储
数据的本地存储,考虑过本地缓存,但因为本地缓存的大小限制,以及可能影响性能,采用SQLite的方式将聊天/备忘录的数据存到本地数据库。在uniapp
中,可以直接启用SQLite的插件,不需要额外的插件。
消息的存储方法放到dbHelper.js
中,实现数据新增。
//数据新增
export async function saveMessageToDb(message) {
try {
const db = await getDatabase();
const { uuid, text, time, category, isPinned, isRecalled, isQuoted, quotedText, quotedIndex, isMerged } = message;
const sql = `
INSERT INTO messages
(uuid, text, time, category, isPinned, isRecalled, isQuoted, quotedText, quotedIndex, isMerged)
VALUES
('${uuid}', '${escapeSqlString(text)}', '${time}', '${category || ''}',
${isPinned ? 1 : 0}, ${isRecalled ? 1 : 0}, ${isQuoted ? 1 : 0},
'${quotedText ? escapeSqlString(quotedText): ''}', ${quotedIndex ? `'${quotedIndex}'` : 'NULL'}, ${isMerged ? 1 : 0})
`;
return new Promise((resolve, reject) => {
db.executeSql({
name: 'chat_database',
sql: sql,
success: function(res) {
console.log('消息保存成功:', res);
resolve(res);
},
fail: function(e) {
console.error('保存消息失败:', JSON.stringify(e));
reject(e);
}
});
});
} catch (error) {
console.error('保存消息到数据库失败:', error);
throw error;
}
}
这里的escapeSqlString
方法,是防止文本输入时,存在sql注入,因此增加一个特殊符号的替换方法,查询的时候会反替换回去。
其中,getDatabase()
会去打开数据库,增加一个判断,如果数据库连接存在,则使用已存在的数据库连接。
export function getDatabase() {
return new Promise((resolve, reject) => {
if (db) {
console.log('使用已存在的数据库连接');
resolve(db);
return;
}
plus.sqlite.openDatabase({
name: 'chat_database',
path: '_doc/chat_database.db',
success: function(e) {
console.log('数据库打开成功');
db = plus.sqlite;
resolve(db);
},
fail: function(e) {
console.error('数据库打开失败:', JSON.stringify(e));
if (e.code === -1402) {
console.log('数据库已经打开,尝试使用已打开的数据库');
db = plus.sqlite;
resolve(db);
} else {
reject(e);
}
}
});
setTimeout(() => {
if (!db) {
console.error('获取数据库连接超时');
reject(new Error('获取数据库连接超时'));
}
}, 10000);
});
}
到这里,我们基本实现了消息的构造、发送、存储。
3.数据加载
我们现在的数据是基于messages
里进行v-for,因此,我们可以将应用退出,this.messages
会清空。设置一个初始化加载方法,从数据库加载。
//数据初始化加载
async loadInitialMessages() {
try {
const messages = await loadMessagesFromDb(0, this.pageSize,true);
this.messages = messages
this.loadedMessages = [...this.messages];
} catch (error) {
console.error('加载初始消息失败:', error);
}
}
mounted() {
this.$nextTick(() => {
this.loadInitialMessages();
});
}
这里,使用unescapeSqlString
对防注入替换的内容反替换回去,保留原本消息。
//数据加载,添加分页查询的参数
export async function loadMessagesFromDb(offset, limit, sortDesc = false) {
try {
const db = await getDatabase();
return new Promise((resolve, reject) => {
const sql = `
SELECT * FROM messages
WHERE isRecalled = 0
ORDER BY time ${sortDesc ? 'DESC' : 'ASC'}
LIMIT ${limit} OFFSET ${offset}
`;
db.selectSql({
name: 'chat_database',
sql: sql,
success: function(res) {
// 处理查询结果,还原特殊字符
const processedRes = res.map(item => ({
...item,
text: unescapeSqlString(item.text),
quotedText: item.quotedText ? unescapeSqlString(item.quotedText) : ''
}));
console.log('成功从数据库加载消息:', processedRes);
resolve(processedRes);
},
fail: function(e) {
console.error('加载消息失败:', JSON.stringify(e));
reject(e);
}
});
});
} catch (error) {
console.error('加载消息时发生错误:', error);
throw error;
}
}
这会便可以验证我们的数据存储是否生效,当然,我们在开发的时候,可以打印输出日志,查看我们的数据是否能够从数据库获取。