重构: 清理技术债 + 数据健壮性增强(P0+P1)

- 删除 inline.ts(方案A遗留死代码,已被 richtext.ts 替代)
- 删除 ElementStyle.rich 字段(segments 方案后无用)
- 删除 PropsPanel 未使用的 CHART_TYPE_MAP
- 删除 editor.css 旧 class 兼容样式(.el-card-title 等)
- 生成整套/重置时自动备份当前 deck 到 .bak(防误覆盖)
- 新增 recoverBackup() 方法(可恢复上一次被覆盖的 deck)
- 旧版本数据不再直接丢弃,改为 migrateDeck() 补字段升级
- chart 元素自动补 chartType:'bar' 默认值
This commit is contained in:
2026-07-12 13:28:04 +08:00
parent e45df2f269
commit 9933a1711a
5 changed files with 49 additions and 67 deletions

View File

@@ -20,8 +20,8 @@ const CHART_TYPES: Array<{ k: ChartType; label: string; icon: string }> = [
{ k: 'radar', label: '雷达图', icon: '🕸' },
{ k: 'progress', label: '进度图', icon: '⭕' }
]
const CHART_TYPE_MAP: Record<string, { label: string; icon: string }> = Object.fromEntries(CHART_TYPES.map(t => [t.k, t]))
const emit = defineEmits
const selected = computed(() => store.getSelected())
const slide = computed(() => store.currentSlide.value)

View File

@@ -1,54 +0,0 @@
/* =====================================================================
* inline.ts — Markdown 行内格式渲染
* 把 **加粗** / *斜体* / ~~删除线~~ / ==高亮== / `代码` / ~下标~ / ^上标^ 转 HTML
* 先转义 HTML再按顺序替换避免互相破坏
* ===================================================================== */
function escapeHtml(s: string): string {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
}
/**
* 把 Markdown 行内语法转 HTML。
* 输入应为一行文本(不含换行)。
* 安全:先转义 HTML再插入受控标签。
*/
export function renderInline(text: string): string {
if (!text) return ''
let s = escapeHtml(text)
// 加粗 **text**(必须先于斜体,避免 ** 与 * 冲突)
s = s.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
// 斜体 *text* 或 _text_
s = s.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>')
s = s.replace(/(?<!\w)_([^_]+)_(?!\w)/g, '<em>$1</em>')
// 删除线 ~~text~~
s = s.replace(/~~([^~]+)~~/g, '<del>$1</del>')
// 高亮 ==text==
s = s.replace(/==([^=]+)==/g, '<mark>$1</mark>')
// 行内代码 `code`
s = s.replace(/`([^`]+)`/g, '<code>$1</code>')
// 下标 ~text~(与删除线区分:单个 ~
s = s.replace(/(?<!~)~([^~\s][^~]*?)~(?!~)/g, '<sub>$1</sub>')
// 上标 ^text^
s = s.replace(/\^([^\^\s][^\^]*?)\^/g, '<sup>$1</sup>')
return s
}
/**
* 渲染多行文本:逐行 renderInline用 <br> 连接。
* 用于 title / text / quote 元素的 content。
*/
export function renderInlineMulti(text: string): string {
if (!text) return ''
return text.split('\n').map(renderInline).join('<br>')
}
/**
* 是否包含 Markdown 行内语法(用于决定是否走 rich 渲染路径)
*/
export function hasInlineSyntax(text: string): boolean {
if (!text) return false
return /\*\*[^*]+\*\*|(?<!\*)\*[^*]+\*(?!\*)|~~[^~]+~~|==[^=]+==|`[^`]+`|\^[^\^]+\^/.test(text)
}

View File

@@ -189,17 +189,41 @@ function touchHistory(key: string) {
touchTimer = setTimeout(() => { touchKey = null }, 900)
}
/* ---------- 数据版本迁移(旧版数据补字段升级,不丢弃) ---------- */
function migrateDeck(d: any): Deck {
// v2→v3: 给元素补默认 chartType / 新元素类型无需处理
if (!d.v || d.v < 3) {
// 确保每个元素有 style 对象
(d.slides || []).forEach((s: any) => {
(s.elements || []).forEach((e: any) => {
if (!e.style) e.style = {};
if (e.type === 'chart' && !e.style.chartType) e.style.chartType = 'bar';
});
});
}
d.v = DECK_VERSION;
return d as Deck;
}
/* ---------- 初始化 ---------- */
function init() {
let loaded: Deck | null = null
try { loaded = JSON.parse(localStorage.getItem(LS_KEY) || 'null') } catch (e) { /* ignore */ }
const curV = SAMPLE_DECK.v
const valid = loaded && loaded.slides && loaded.slides.length && loaded.v === curV
if (!valid && loaded && loaded.slides) {
try { localStorage.setItem(LS_KEY + '.bak', JSON.stringify(loaded)) } catch (e) {} // 旧版备份
let deck: Deck
if (loaded && loaded.slides && loaded.slides.length) {
if (loaded.v !== curV) {
// 版本不匹配 → 尝试迁移
try { localStorage.setItem(LS_KEY + '.bak', JSON.stringify(loaded)) } catch (e) {}
deck = migrateDeck(loaded)
} else {
deck = loaded
}
} else {
deck = clone(SAMPLE_DECK)
}
Object.assign(state, {
deck: valid ? loaded! : clone(SAMPLE_DECK),
deck,
currentIndex: 0,
selectedId: null,
activeLibId: null
@@ -377,6 +401,10 @@ function redo(): boolean {
/* ---------- 整体替换AI 生成 / 导入 / 重置用) ---------- */
function replaceDeck(newDeck: Deck | Record<string, unknown>, opts?: { keepHistory?: boolean; newChat?: boolean }) {
if (!opts?.keepHistory) pushHistory()
// 自动备份newChat=true 时说明是生成整套/重置等“覆盖性操作”,把当前 deck 存入 .bak
if (opts?.newChat && state.deck?.slides?.length) {
try { localStorage.setItem(LS_KEY + '.bak', JSON.stringify(state.deck)) } catch (e) {}
}
suppressHistory = true
let d = newDeck as Deck
if (!d || typeof d !== 'object') d = {} as Deck
@@ -663,6 +691,19 @@ function clearChat(chatId: string) {
try { localStorage.removeItem(CHAT_PREFIX + chatId) } catch (e) {}
}
/** 恢复上一次被覆盖的 deck生成整套/重置前的自动备份) */
function recoverBackup(): boolean {
try {
const bak = localStorage.getItem(LS_KEY + '.bak')
if (!bak) return false
const d = JSON.parse(bak)
if (!d || !d.slides || !d.slides.length) return false
replaceDeck(d)
localStorage.removeItem(LS_KEY + '.bak')
return true
} catch (e) { return false }
}
/* ---------- 导出 ---------- */
export const store = {
// 状态(响应式)
@@ -699,4 +740,6 @@ export const store = {
getTemplates, getUserTemplates, saveCurrentAsTemplate, addSlideFromTemplate, deleteTemplate,
// 会话管理
getChatId, getChat, setChat, clearChat, newChatId,
// 备份恢复
recoverBackup,
}

View File

@@ -85,8 +85,6 @@ export interface ElementStyle {
// card
icon?: string
accent?: ColorKey
/** 文字元素是否启用 Markdown 行内格式(加粗/斜体/高亮等) */
rich?: boolean
}
/** 幻灯片元素 */

View File

@@ -275,11 +275,6 @@
white-space: pre-wrap;
flex: 1;
}
/* 兼容旧 class 名 */
.el-card-title { font-weight: 700; font-size: 1.5em; line-height: 1.15; display: flex; align-items: center; gap: .45em; }
.el-card-text { font-size: 1em; line-height: 1.5; opacity: .88; white-space: pre-wrap; }
.el-card-body { padding: 1.1em 1.3em 1.2em; flex: 1; display: flex; flex-direction: column; gap: .5em; justify-content: flex-start; }
/* 渐变形状由 renderElement 内联 backgroundImage此处仅兜底圆角 */
.el-shape.gradient { border-radius: var(--shape-radius, 0); }
@@ -314,7 +309,7 @@
.canvas-placeholder .ph-sub { font-size: 14px; opacity: .8; }
/* 长内容溢出兆底 */
.el-text, .el-card-title, .el-card-text { overflow: hidden; }
.el-text { overflow: hidden; }
.el-stat .num { overflow-wrap: anywhere; }
.el-chart .bar-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }