歌词改了好多版怎么管理?HarmonyOS版本记录与对比
如果你对歌词创作感兴趣,可以去鸿蒙应用市场搜一下**「词藻本」**,下载下来体验体验。写歌词时反复修改,App自动保存每个版本,可以回看历史版本和对比差异。体验完了再回来看这篇文章,你会更清楚版本管理功能是怎么实现的。
写在前面
大家好,我是一名写了十多年Web前端的老兵。从jQuery时代一路走到React/Vue,Git版本管理是每天都在用的工具。去年开始转战鸿蒙生态,用ArkTS开发App,发现App内的文档版本管理需要自己实现。
比如:
- 版本存储:Git用
.git目录存储;App里用preferences存储多个版本。 - 版本对比:Git用
diff命令;App里需要自己实现文本差异对比。 - 版本回滚:Git用
checkout;App里用状态恢复。
别担心,接下来这篇文章,我会用"词藻本"的歌词版本管理,带你看看HarmonyOS怎么实现文档版本控制。
这篇文章聊什么
词藻本的版本管理功能,核心要解决:
- 版本保存:每次修改自动保存版本
- 版本列表:展示所有历史版本
- 版本对比:对比两个版本的差异
- 版本回滚:恢复到某个历史版本
第一步:设计版本数据结构
// Web前端同学看这里:React里我们用state管理当前内容
// 鸿蒙里也需要设计版本数据结构来存储历史
// 歌词版本
interface LyricVersion {
id: string;
lyricId: string;
content: string;
version: number;
changeDescription: string;
createdAt: string;
}
// 歌词草稿(含版本信息)
interface LyricDraft {
id: string;
title: string;
content: string;
currentVersion: number;
versions: LyricVersion[];
rhymeGroup: string;
mood: string;
createdAt: string;
updatedAt: string;
}
第二步:实现版本管理逻辑
// Web前端同学看这里:版本管理的核心是"快照"思想
// 每次保存时,把当前内容存为一个新版本
import { preferences } from '@kit.ArkData';
let prefInstance: preferences.Preferences | null = null;
async function getPreferences(context: Context): Promise<preferences.Preferences> {
if (!prefInstance) {
prefInstance = await preferences.getPreferences(context, 'cizaoben_data');
}
return prefInstance;
}
// 保存新版本
async function saveVersion(context: Context, lyric: LyricDraft, description: string): Promise<boolean> {
try {
const pref = await getPreferences(context);
// 创建版本快照
const newVersion: LyricVersion = {
id: `ver_${Date.now()}`,
lyricId: lyric.id,
content: lyric.content,
version: lyric.currentVersion + 1,
changeDescription: description,
createdAt: new Date().toISOString()
};
// 更新歌词的版本列表
lyric.versions.push(newVersion);
lyric.currentVersion = newVersion.version;
lyric.updatedAt = new Date().toISOString().slice(0, 10);
// 存储到preferences
const drafts = await getItem<LyricDraft[]>(context, 'drafts', []);
const index = drafts.findIndex(d => d.id === lyric.id);
if (index !== -1) {
drafts[index] = lyric;
}
await pref.put('drafts', JSON.stringify(drafts));
await pref.flush();
return true;
} catch (err) {
console.error(`保存版本失败: ${err}`);
return false;
}
}
// 获取版本历史
async function getVersionHistory(context: Context, lyricId: string): Promise<LyricVersion[]> {
const drafts = await getItem<LyricDraft[]>(context, 'drafts', []);
const lyric = drafts.find(d => d.id === lyricId);
return lyric?.versions || [];
}
// 回滚到指定版本
async function rollbackToVersion(context: Context, lyricId: string, versionId: string): Promise<boolean> {
try {
const drafts = await getItem<LyricDraft[]>(context, 'drafts', []);
const lyric = drafts.find(d => d.id === lyricId);
if (!lyric) return false;
const targetVersion = lyric.versions.find(v => v.id === versionId);
if (!targetVersion) return false;
// 恢复内容
lyric.content = targetVersion.content;
lyric.updatedAt = new Date().toISOString().slice(0, 10);
// 保存当前状态为新版本
await saveVersion(context, lyric, `回滚到版本 ${targetVersion.version}`);
return true;
} catch (err) {
console.error(`回滚失败: ${err}`);
return false;
}
}
// 通用存储方法
async function getItem<T>(context: Context, key: string, defaultValue: T): Promise<T> {
try {
const pref = await getPreferences(context);
const value = await pref.get(key, '');
if (typeof value === 'string' && value.length > 0) {
return JSON.parse(value) as T;
}
return defaultValue;
} catch (err) {
return defaultValue;
}
}
第三步:实现版本对比
// Web前端同学看这里:文本diff算法
// 简单实现:按行对比,标记新增和删除
interface DiffLine {
type: 'same' | 'added' | 'removed';
content: string;
}
function diffText(oldText: string, newText: string): DiffLine[] {
const oldLines = oldText.split('\n');
const newLines = newText.split('\n');
const result: DiffLine[] = [];
const maxLen = Math.max(oldLines.length, newLines.length);
for (let i = 0; i < maxLen; i++) {
const oldLine = i < oldLines.length ? oldLines[i] : undefined;
const newLine = i < newLines.length ? newLines[i] : undefined;
if (oldLine === undefined) {
result.push({ type: 'added', content: newLine! });
} else if (newLine === undefined) {
result.push({ type: 'removed', content: oldLine });
} else if (oldLine === newLine) {
result.push({ type: 'same', content: oldLine });
} else {
result.push({ type: 'removed', content: oldLine });
result.push({ type: 'added', content: newLine });
}
}
return result;
}
// 版本对比页面
@Entry
@Component
struct VersionComparePage {
@State diffResult: DiffLine[] = []
@State oldVersion: LyricVersion | null = null
@State newVersion: LyricVersion | null = null
compareVersions(oldVer: LyricVersion, newVer: LyricVersion) {
this.oldVersion = oldVer;
this.newVersion = newVer;
this.diffResult = diffText(oldVer.content, newVer.content);
}
build() {
Column() {
Text('版本对比')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
if (this.oldVersion && this.newVersion) {
Row() {
Text(`版本 ${this.oldVersion.version}`)
.fontSize(14)
.fontColor('#6b7280')
Text(' → ')
.fontSize(14)
Text(`版本 ${this.newVersion.version}`)
.fontSize(14)
.fontColor('#6b7280')
}
.margin({ bottom: 12 })
// 差异显示
ForEach(this.diffResult, (line: DiffLine, index: number) => {
Text(line.content)
.fontSize(14)
.fontFamily('monospace')
.fontColor(this.getLineFontColor(line.type))
.backgroundColor(this.getLineBgColor(line.type))
.width('100%')
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
})
}
}
.padding(16)
}
private getLineFontColor(type: string): string {
if (type === 'added') return '#166534';
if (type === 'removed') return '#991b1b';
return '#374151';
}
private getLineBgColor(type: string): string {
if (type === 'added') return '#dcfce7';
if (type === 'removed') return '#fee2e2';
return 'transparent';
}
}
第四步:实现版本列表页面
@Entry
@Component
struct VersionListPage {
@State versions: LyricVersion[] = []
@State lyricId: string = ''
async aboutToAppear() {
this.versions = await getVersionHistory(getContext(this) as Context, this.lyricId);
// 按版本号倒序
this.versions.sort((a, b) => b.version - a.version);
}
build() {
Column() {
Text('版本历史')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
ForEach(this.versions, (version: LyricVersion) => {
Row() {
Column() {
Text(`版本 ${version.version}`)
.fontSize(16)
.fontWeight(FontWeight.Medium)
Text(version.changeDescription)
.fontSize(12)
.fontColor('#6b7280')
Text(version.createdAt)
.fontSize(10)
.fontColor('#9ca3af')
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Button('恢复')
.fontSize(12)
.height(32)
.backgroundColor('#3b82f6')
.borderRadius(8)
.onClick(() => {
rollbackToVersion(getContext(this) as Context, this.lyricId, version.id);
})
}
.width('100%')
.padding(12)
.backgroundColor('#ffffff')
.borderRadius(12)
.margin({ bottom: 8 })
})
}
.padding(16)
}
}
第五步:常见问题
5.1 版本数量过多
问题:长期使用后版本数量很大。
解决:设置最大版本数(如50个),超出时删除最早的版本。
5.2 存储空间
问题:每个版本都存储完整内容,占用空间大。
解决:可以只存储差异(delta),但实现复杂度高;简单方案是限制版本数量。
总结
这篇文章围绕"词藻本"的歌词版本管理,讲解了:
版本存储
- 版本快照的创建和存储
- preferences数据持久化
- 版本列表管理
版本对比
- 文本差异对比算法
- 差异可视化展示
版本回滚
- 恢复历史版本内容
- 回滚操作的版本记录
如果你对"词藻本"感兴趣,欢迎去鸿蒙应用市场搜索下载体验。