重构: 清理技术债 + 数据健壮性增强(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:
@@ -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)
|
||||
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
/* =====================================================================
|
||||
* inline.ts — Markdown 行内格式渲染
|
||||
* 把 **加粗** / *斜体* / ~~删除线~~ / ==高亮== / `代码` / ~下标~ / ^上标^ 转 HTML
|
||||
* 先转义 HTML,再按顺序替换(避免互相破坏)
|
||||
* ===================================================================== */
|
||||
|
||||
function escapeHtml(s: string): string {
|
||||
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 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)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -85,8 +85,6 @@ export interface ElementStyle {
|
||||
// card
|
||||
icon?: string
|
||||
accent?: ColorKey
|
||||
/** 文字元素是否启用 Markdown 行内格式(加粗/斜体/高亮等) */
|
||||
rich?: boolean
|
||||
}
|
||||
|
||||
/** 幻灯片元素 */
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user