/* global Page, wx, require, clearTimeout, setTimeout */ const { addRecord, searchRecords, removeRecord, updateRecord, getSettings, DEFAULT_VOICE_RECORD_CATEGORY_FALLBACK, normalizeVoiceRecordCategories } = require("../../utils/storage"); const { pageOf } = require("../../utils/pagination"); const { buildThemeStyle, applyNavigationBarTheme } = require("../../utils/themeStyle"); const { buildButtonIconThemeMaps } = require("../../utils/themedIcons"); const { getWindowMetrics } = require("../../utils/systemInfoCompat"); const { buildPageCopy, formatTemplate, normalizeUiLanguage, t } = require("../../utils/i18n"); const { buildSvgButtonPressData, createSvgButtonPressMethods } = require("../../utils/svgButtonFeedback"); const PAGE_SIZE = 15; /** * 左滑动作区固定为 2x2 四键:左列“废弃 / 已处理”,右列“复制 / 删除”。 * 这里提供一个总宽度兜底,实际交互仍优先读取真实节点宽度。 */ const SWIPE_ACTION_WIDTH_RPX = 240; const DEFAULT_WINDOW_WIDTH_PX = 375; const SWIPE_AXIS_LOCK_THRESHOLD_PX = 6; const RECORD_EDIT_AUTOSAVE_DELAY_MS = 400; const QUICK_CATEGORY_DIALOG_MIN_WIDTH_PX = 120; const QUICK_CATEGORY_DIALOG_MAX_WIDTH_PX = 240; const QUICK_CATEGORY_DIALOG_MIN_HEIGHT_PX = Math.round((QUICK_CATEGORY_DIALOG_MIN_WIDTH_PX * 3) / 4); const QUICK_CATEGORY_BUBBLE_GAP_PX = 8; const QUICK_CATEGORY_BUBBLE_RADIUS_BASE_PX = 12; const QUICK_CATEGORY_BUBBLE_RADIUS_STEP_PX = 7.6; const CATEGORY_COLOR_PALETTE = [ { background: "rgba(91, 210, 255, 0.18)", border: "rgba(91, 210, 255, 0.36)" }, { background: "rgba(255, 143, 107, 0.18)", border: "rgba(255, 143, 107, 0.36)" }, { background: "rgba(141, 216, 123, 0.18)", border: "rgba(141, 216, 123, 0.36)" }, { background: "rgba(255, 191, 105, 0.18)", border: "rgba(255, 191, 105, 0.36)" }, { background: "rgba(199, 146, 234, 0.18)", border: "rgba(199, 146, 234, 0.36)" }, { background: "rgba(255, 111, 145, 0.18)", border: "rgba(255, 111, 145, 0.36)" }, { background: "rgba(99, 210, 255, 0.18)", border: "rgba(99, 210, 255, 0.36)" }, { background: "rgba(78, 205, 196, 0.18)", border: "rgba(78, 205, 196, 0.36)" }, { background: "rgba(255, 209, 102, 0.18)", border: "rgba(255, 209, 102, 0.36)" }, { background: "rgba(144, 190, 109, 0.18)", border: "rgba(144, 190, 109, 0.36)" } ]; function resolveRecordCategories(settings) { const source = settings && typeof settings === "object" ? settings : {}; const categories = normalizeVoiceRecordCategories(source.voiceRecordCategories); return categories.length > 0 ? categories : [DEFAULT_VOICE_RECORD_CATEGORY_FALLBACK]; } function resolveVisibleCategory(rawCategory, categories) { const normalized = String(rawCategory || "").trim(); if (normalized && categories.includes(normalized)) return normalized; return DEFAULT_VOICE_RECORD_CATEGORY_FALLBACK; } function resolveDefaultCategory(settings, categories) { const source = settings && typeof settings === "object" ? settings : {}; const options = Array.isArray(categories) && categories.length > 0 ? categories : resolveRecordCategories(source); const normalized = String(source.voiceRecordDefaultCategory || "").trim(); if (normalized && options.includes(normalized)) return normalized; return options[0] || DEFAULT_VOICE_RECORD_CATEGORY_FALLBACK; } /** * 分类色按当前分类顺序稳定分配,优先保证当前页面里的分类彼此不同。 * 历史脏值已在外层先回退到可见分类,这里直接按展示分类映射即可。 */ function buildCategoryStyle(category, orderedCategories) { const label = String(category || DEFAULT_VOICE_RECORD_CATEGORY_FALLBACK).trim() || DEFAULT_VOICE_RECORD_CATEGORY_FALLBACK; const categories = Array.isArray(orderedCategories) ? orderedCategories : []; const paletteIndex = Math.max(0, categories.indexOf(label)); const palette = CATEGORY_COLOR_PALETTE[paletteIndex % CATEGORY_COLOR_PALETTE.length] || CATEGORY_COLOR_PALETTE[0]; return `background:${palette.background};border-color:${palette.border};`; } function clampNumber(value, min, max) { return Math.max(min, Math.min(max, value)); } function resolveQuickCategoryBubbleSize(category) { const length = Array.from(String(category || DEFAULT_VOICE_RECORD_CATEGORY_FALLBACK)).length; return Math.min(52, Math.max(34, 24 + length * 7)); } function intersectsPlacedBubble(left, top, size, placedBubbles) { const centerX = left + size / 2; const centerY = top + size / 2; return placedBubbles.some((bubble) => { const otherCenterX = bubble.left + bubble.size / 2; const otherCenterY = bubble.top + bubble.size / 2; const minDistance = size / 2 + bubble.size / 2 + QUICK_CATEGORY_BUBBLE_GAP_PX; return Math.hypot(centerX - otherCenterX, centerY - otherCenterY) < minDistance; }); } function buildBubbleCloudForBox(orderedCategories, currentCategory, width, height, colorCategories) { const centerX = width / 2; const centerY = height / 2; const placedBubbles = []; let collisionCount = 0; const items = orderedCategories.map((category, index) => { const size = resolveQuickCategoryBubbleSize(category); if (index === 0) { const left = Math.max(0, Math.min(width - size, centerX - size / 2)); const top = Math.max(0, Math.min(height - size, centerY - size / 2)); placedBubbles.push({ left, top, size }); return { active: category === currentCategory, category, style: `width:${size}px;height:${size}px;left:${left}px;top:${top}px;`, categoryStyle: buildCategoryStyle(category, colorCategories) }; } let fallbackLeft = Math.max(0, Math.min(width - size, centerX - size / 2)); let fallbackTop = Math.max(0, Math.min(height - size, centerY - size / 2)); let placed = false; for (let step = 1; step <= 320; step += 1) { const angle = step * 0.72; const radius = QUICK_CATEGORY_BUBBLE_RADIUS_BASE_PX + Math.sqrt(step) * QUICK_CATEGORY_BUBBLE_RADIUS_STEP_PX; const candidateLeft = centerX + Math.cos(angle) * radius - size / 2; const candidateTop = centerY + Math.sin(angle) * radius * 1.08 - size / 2; const isWithinWidth = candidateLeft >= 0 && candidateLeft + size <= width; const isWithinHeight = candidateTop >= 0 && candidateTop + size <= height; if (!isWithinWidth || !isWithinHeight) continue; if (intersectsPlacedBubble(candidateLeft, candidateTop, size, placedBubbles)) continue; fallbackLeft = candidateLeft; fallbackTop = candidateTop; placed = true; break; } if (!placed) collisionCount += 1; placedBubbles.push({ left: fallbackLeft, top: fallbackTop, size }); return { active: category === currentCategory, category, style: `width:${size}px;height:${size}px;left:${fallbackLeft}px;top:${fallbackTop}px;`, categoryStyle: buildCategoryStyle(category, colorCategories) }; }); return { collisionCount, items }; } function buildQuickCategoryLayout(categories, currentCategory, maxWidthPx) { const colorCategories = categories.slice(); const orderedCategories = categories.slice().sort((left, right) => { if (left === currentCategory) return -1; if (right === currentCategory) return 1; return categories.indexOf(left) - categories.indexOf(right); }); const longestLength = orderedCategories.reduce( (max, category) => Math.max(max, Array.from(String(category || "")).length), 0 ); const estimatedWidth = QUICK_CATEGORY_DIALOG_MIN_WIDTH_PX + Math.max(0, orderedCategories.length - 3) * 22 + Math.max(0, longestLength - 2) * 8; const maxWidth = clampNumber( maxWidthPx || QUICK_CATEGORY_DIALOG_MAX_WIDTH_PX, QUICK_CATEGORY_DIALOG_MIN_WIDTH_PX, QUICK_CATEGORY_DIALOG_MAX_WIDTH_PX ); let bestWidth = clampNumber(estimatedWidth, QUICK_CATEGORY_DIALOG_MIN_WIDTH_PX, maxWidth); let bestHeight = Math.max(QUICK_CATEGORY_DIALOG_MIN_HEIGHT_PX, Math.round((bestWidth * 3) / 4)); let bestLayout = buildBubbleCloudForBox( orderedCategories, currentCategory, bestWidth, bestHeight, colorCategories ); for (let width = QUICK_CATEGORY_DIALOG_MIN_WIDTH_PX; width <= maxWidth; width += 20) { const height = Math.max(QUICK_CATEGORY_DIALOG_MIN_HEIGHT_PX, Math.round((width * 3) / 4)); const candidateLayout = buildBubbleCloudForBox( orderedCategories, currentCategory, width, height, colorCategories ); if (candidateLayout.collisionCount < bestLayout.collisionCount) { bestWidth = width; bestHeight = height; bestLayout = candidateLayout; } if (candidateLayout.collisionCount === 0 && width >= bestWidth) { bestWidth = width; bestHeight = height; bestLayout = candidateLayout; break; } } return { width: bestWidth, height: bestHeight, items: bestLayout.items }; } /** * 闪念页(小程序对齐 Web v2.2.0): * 1. 顶部提供搜索和分类过滤; * 2. 支持左滑废弃 / 已处理 / 复制 / 删除、点击正文编辑、点击分类快速改分类; * 3. 过滤后分页,底部分页与新增 / 导出分区展示。 */ Page({ data: { ...buildSvgButtonPressData(), themeStyle: "", icons: {}, accentIcons: {}, copy: buildPageCopy("zh-Hans", "records"), page: 1, total: 0, totalPages: 1, rows: [], query: "", selectedCategory: "", categoryOptions: [], categoryMenuVisible: false, quickCategoryPopupVisible: false, quickCategoryRecordId: "", quickCategoryItems: [], quickCategoryWidthPx: QUICK_CATEGORY_DIALOG_MIN_WIDTH_PX, quickCategoryHeightPx: QUICK_CATEGORY_DIALOG_MIN_HEIGHT_PX, quickCategoryPanelStyle: "", editPopupVisible: false, editRecordId: "", editContent: "", editCategory: "", editUpdatedAtText: "", editUpdatedAtLabel: "", pageIndicatorText: t("zh-Hans", "common.pageIndicator", { page: 1, total: 1 }) }, swipeRuntime: null, editAutoSaveTimer: null, onShow() { this.syncWindowMetrics(); const settings = getSettings(); const language = normalizeUiLanguage(settings.uiLanguage); const copy = buildPageCopy(language, "records"); const { icons, accentIcons } = buildButtonIconThemeMaps(settings); applyNavigationBarTheme(settings); wx.setNavigationBarTitle({ title: copy.navTitle || "闪念" }); this.setData({ themeStyle: buildThemeStyle(settings), icons, accentIcons, copy }); this.reload(); }, onHide() { this.flushEditAutoSave(); }, onUnload() { this.flushEditAutoSave(); }, syncWindowMetrics() { try { const info = getWindowMetrics(wx); const width = Number(info.windowWidth); const height = Number(info.windowHeight); this.windowWidthPx = Number.isFinite(width) && width > 0 ? width : DEFAULT_WINDOW_WIDTH_PX; this.windowHeightPx = Number.isFinite(height) && height > 0 ? height : 667; } catch { this.windowWidthPx = DEFAULT_WINDOW_WIDTH_PX; this.windowHeightPx = 667; } }, buildQuickCategoryPanelStyle(anchorRect, layout) { const rect = anchorRect && typeof anchorRect === "object" ? anchorRect : {}; const width = Number(layout && layout.width) || QUICK_CATEGORY_DIALOG_MIN_WIDTH_PX; const height = Number(layout && layout.height) || QUICK_CATEGORY_DIALOG_MIN_HEIGHT_PX; const windowWidth = Number(this.windowWidthPx) || DEFAULT_WINDOW_WIDTH_PX; const windowHeight = Number(this.windowHeightPx) || 667; const panelPadding = 12; const gap = 8; const buttonLeft = Number(rect.left) || 0; const buttonTop = Number(rect.top) || 0; const buttonWidth = Number(rect.width) || 0; const buttonHeight = Number(rect.height) || 0; let left = buttonLeft + buttonWidth + gap; if (left + width + panelPadding > windowWidth) { left = buttonLeft - width - gap; } left = clampNumber(left, panelPadding, Math.max(panelPadding, windowWidth - width - panelPadding)); const buttonCenterY = buttonTop + buttonHeight / 2; let top = buttonCenterY - height / 2 - panelPadding; top = clampNumber(top, panelPadding, Math.max(panelPadding, windowHeight - height - panelPadding)); return `left:${left}px;top:${top}px;`; }, getSwipeRevealPx() { if (Number.isFinite(this.swipeRevealPx) && this.swipeRevealPx > 0) { return this.swipeRevealPx; } const windowWidth = Number(this.windowWidthPx) || DEFAULT_WINDOW_WIDTH_PX; const actionWidthPx = Math.round((windowWidth * SWIPE_ACTION_WIDTH_RPX) / 750); return Math.max(24, actionWidthPx); }, syncSwipeRevealWidth() { const query = this.createSelectorQuery(); query .select(".record-item-actions-wrap") .boundingClientRect((rect) => { const width = Number(rect && rect.width); if (!Number.isFinite(width) || width <= 0) return; this.swipeRevealPx = Math.max(24, Math.round(width)); }) .exec(); }, /** * 先按关键字过滤,再用当前有效分类做展示过滤,保持与 web 的语义一致。 */ reload() { const settings = getSettings(); const { icons, accentIcons } = buildButtonIconThemeMaps(settings); const categoryOptions = resolveRecordCategories(settings); const selectedCategory = categoryOptions.includes(this.data.selectedCategory) ? this.data.selectedCategory : ""; const keyword = String(this.data.query || "").trim(); const filtered = searchRecords({ keyword }).filter( (item) => !selectedCategory || resolveVisibleCategory(item.category, categoryOptions) === selectedCategory ); const paged = pageOf(filtered, this.data.page, PAGE_SIZE); const language = normalizeUiLanguage(settings.uiLanguage); const nextOffsets = {}; const rows = paged.rows.map((item) => { const displayCategory = resolveVisibleCategory(item.category, categoryOptions); const swipeOffsetX = this.swipeOffsets && Number(this.swipeOffsets[item.id]) ? this.swipeOffsets[item.id] : 0; nextOffsets[item.id] = swipeOffsetX; return { ...item, processed: item.processed === true, discarded: item.discarded === true, isMuted: false, displayCategory, categoryStyle: buildCategoryStyle(displayCategory, categoryOptions), timeText: this.formatTime(item.createdAt), updatedAtText: this.formatTime(item.updatedAt), contextLabelText: item.contextLabel || this.data.copy.contextFallback || "未设置上下文", swipeOffsetX }; }); this.swipeOffsets = nextOffsets; this.swipeRuntime = null; this.setData( { themeStyle: buildThemeStyle(settings), icons, accentIcons, categoryOptions, selectedCategory, categoryMenuVisible: false, page: paged.page, total: paged.total, totalPages: paged.totalPages, rows, pageIndicatorText: t(language, "common.pageIndicator", { page: paged.page, total: paged.totalPages }) }, () => this.syncSwipeRevealWidth() ); }, formatTime(input) { if (!input) return "--"; const date = new Date(input); if (Number.isNaN(+date)) return "--"; const pad = (value) => String(value).padStart(2, "0"); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; }, onQueryInput(event) { this.setData({ query: String(event.detail.value || ""), page: 1 }, () => this.reload()); }, onToggleCategoryMenu() { this.closeAllRows(); this.closeQuickCategoryPopup(); this.setData({ categoryMenuVisible: !this.data.categoryMenuVisible }); }, onSelectFilterCategory(event) { const category = String(event.currentTarget.dataset.category || ""); this.setData({ selectedCategory: category, categoryMenuVisible: false, page: 1 }, () => this.reload()); }, onPrev() { this.setData({ page: Math.max(1, this.data.page - 1) }, () => this.reload()); }, onNext() { this.setData({ page: Math.min(this.data.totalPages, this.data.page + 1) }, () => this.reload()); }, /** * 新增入口复用现有编辑弹层: * 1. 先以空草稿和默认分类打开; * 2. 首次输入非空内容后自动创建记录; * 3. 后续继续沿用原有编辑自动保存链路。 */ onOpenCreate() { const settings = getSettings(); const categoryOptions = this.data.categoryOptions.length ? this.data.categoryOptions.slice() : resolveRecordCategories(settings); this.clearEditAutoSaveTimer(); this.closeAllRows(); this.closeQuickCategoryPopup(); this.setData({ categoryMenuVisible: false, editPopupVisible: true, editRecordId: "", editContent: "", editCategory: resolveDefaultCategory(settings, categoryOptions), editUpdatedAtText: "", editUpdatedAtLabel: this.data.copy.newRecordHint || "输入后自动保存为新闪念" }); }, resolveTouchPoint(event) { const point = (event && event.touches && event.touches[0]) || (event && event.changedTouches && event.changedTouches[0]) || null; if (!point) return null; const x = Number(point.clientX); const y = Number(point.clientY); if (!Number.isFinite(x) || !Number.isFinite(y)) return null; return { x, y }; }, clampSwipeOffset(value) { const revealPx = this.getSwipeRevealPx(); const numeric = Number(value); if (!Number.isFinite(numeric)) return 0; if (numeric < -revealPx) return -revealPx; if (numeric > 0) return 0; return numeric; }, findRowIndexById(id) { return this.data.rows.findIndex((item) => item.id === id); }, updateRowSwipeOffset(id, offset) { const index = this.findRowIndexById(id); if (index < 0) return; const normalized = this.clampSwipeOffset(offset); this.swipeOffsets[id] = normalized; this.setData({ [`rows[${index}].swipeOffsetX`]: normalized }); }, closeOtherRows(exceptId) { const updates = {}; this.data.rows.forEach((item, index) => { if (item.id === exceptId) return; if (item.swipeOffsetX === 0) return; this.swipeOffsets[item.id] = 0; updates[`rows[${index}].swipeOffsetX`] = 0; }); if (Object.keys(updates).length > 0) this.setData(updates); }, closeAllRows() { const updates = {}; this.data.rows.forEach((item, index) => { if (item.swipeOffsetX === 0) return; this.swipeOffsets[item.id] = 0; updates[`rows[${index}].swipeOffsetX`] = 0; }); if (Object.keys(updates).length > 0) this.setData(updates); }, onListTap() { this.closeAllRows(); if (this.data.categoryMenuVisible) { this.setData({ categoryMenuVisible: false }); } this.closeQuickCategoryPopup(); }, onRecordTouchStart(event) { const id = event.currentTarget.dataset.id; if (!id) return; const point = this.resolveTouchPoint(event); if (!point) return; this.closeOtherRows(id); this.swipeRuntime = { id, startX: point.x, startY: point.y, startOffsetX: this.clampSwipeOffset((this.swipeOffsets && this.swipeOffsets[id]) || 0), dragging: false, blocked: false }; }, onRecordTouchMove(event) { const runtime = this.swipeRuntime; if (!runtime || runtime.blocked) return; const id = event.currentTarget.dataset.id; if (!id || id !== runtime.id) return; const point = this.resolveTouchPoint(event); if (!point) return; const deltaX = point.x - runtime.startX; const deltaY = point.y - runtime.startY; if (!runtime.dragging) { if ( Math.abs(deltaX) < SWIPE_AXIS_LOCK_THRESHOLD_PX && Math.abs(deltaY) < SWIPE_AXIS_LOCK_THRESHOLD_PX ) { return; } if (Math.abs(deltaY) > Math.abs(deltaX)) { runtime.blocked = true; return; } runtime.dragging = true; } this.updateRowSwipeOffset(id, runtime.startOffsetX + deltaX); }, onRecordTouchEnd(event) { const runtime = this.swipeRuntime; this.swipeRuntime = null; if (!runtime || runtime.blocked) return; const id = event.currentTarget.dataset.id; if (!id || id !== runtime.id) return; const revealPx = this.getSwipeRevealPx(); const current = this.clampSwipeOffset((this.swipeOffsets && this.swipeOffsets[id]) || 0); const shouldOpen = current <= -revealPx * 0.45; this.updateRowSwipeOffset(id, shouldOpen ? -revealPx : 0); }, onDelete(event) { const id = String(event.currentTarget.dataset.id || ""); if (!id) return; removeRecord(id); if (this.swipeOffsets) delete this.swipeOffsets[id]; wx.showToast({ title: this.data.copy?.toast?.deleted || "已删除", icon: "success" }); this.reload(); }, onCopy(event) { const id = String(event.currentTarget.dataset.id || ""); const record = this.data.rows.find((item) => item.id === id); if (!record) return; wx.setClipboardData({ data: String(record.content || ""), success: () => { this.closeAllRows(); wx.showToast({ title: this.data.copy?.toast?.copied || "已复制", icon: "success" }); } }); }, /** * “已处理”是一次性整理动作: * 1. 仅把当前闪念标记为已处理; * 2. 已处理状态继续允许编辑,但列表会整体降到 60% opacity; * 3. 重复点击保持幂等,只回显同一条成功提示。 */ onMarkProcessed(event) { const id = String(event.currentTarget.dataset.id || ""); const record = this.data.rows.find((item) => item.id === id); if (!record) return; if (record.processed && !record.discarded) { this.closeAllRows(); wx.showToast({ title: this.data.copy?.toast?.processed || "已处理", icon: "success" }); return; } const updated = updateRecord({ id: record.id, content: record.content, category: record.category || record.displayCategory, processed: true, discarded: false }); if (!updated) { wx.showToast({ title: this.data.copy?.toast?.updateFailed || "更新失败", icon: "none" }); return; } this.closeAllRows(); wx.showToast({ title: this.data.copy?.toast?.processed || "已处理", icon: "success" }); this.reload(); }, /** * “废弃”与“已处理”同属终态: * 1. 写入废弃后会自动取消已处理,保证两种状态互斥; * 2. 当前版本仅按需求收口按钮与状态,不额外改变正文内容; * 3. 列表视觉同样按 60% opacity 弱化,便于后续统一筛看。 */ onMarkDiscarded(event) { const id = String(event.currentTarget.dataset.id || ""); const record = this.data.rows.find((item) => item.id === id); if (!record) return; if (record.discarded && !record.processed) { this.closeAllRows(); wx.showToast({ title: this.data.copy?.toast?.discarded || "已废弃", icon: "success" }); return; } const updated = updateRecord({ id: record.id, content: record.content, category: record.category || record.displayCategory, processed: false, discarded: true }); if (!updated) { wx.showToast({ title: this.data.copy?.toast?.updateFailed || "更新失败", icon: "none" }); return; } this.closeAllRows(); wx.showToast({ title: this.data.copy?.toast?.discarded || "已废弃", icon: "success" }); this.reload(); }, /** * 点击左侧分类胶囊,改为页内弹层气泡云,避免原生 action sheet 破坏当前页面语义。 */ onQuickCategoryTap(event) { const recordId = String(event.currentTarget.dataset.id || ""); const record = this.data.rows.find((item) => item.id === recordId); if (!record) return; const categories = this.data.categoryOptions.slice(); if (categories.length === 0) return; const maxWidth = Math.max( QUICK_CATEGORY_DIALOG_MIN_WIDTH_PX, Math.min( QUICK_CATEGORY_DIALOG_MAX_WIDTH_PX, (Number(this.windowWidthPx) || DEFAULT_WINDOW_WIDTH_PX) - 48 ) ); const layout = buildQuickCategoryLayout(categories, record.displayCategory, maxWidth); const anchorId = `#quick-category-${record.id}`; const query = this.createSelectorQuery(); query.select(anchorId).boundingClientRect((rect) => { const panelStyle = this.buildQuickCategoryPanelStyle(rect, layout); this.closeAllRows(); this.setData({ quickCategoryPopupVisible: true, quickCategoryRecordId: record.id, quickCategoryItems: layout.items, quickCategoryWidthPx: layout.width, quickCategoryHeightPx: layout.height, quickCategoryPanelStyle: panelStyle }); }); query.exec(); }, closeQuickCategoryPopup() { if (!this.data.quickCategoryPopupVisible) return; this.setData({ quickCategoryPopupVisible: false, quickCategoryRecordId: "", quickCategoryItems: [], quickCategoryPanelStyle: "" }); }, onApplyQuickCategory(event) { const recordId = String(this.data.quickCategoryRecordId || ""); const record = this.data.rows.find((item) => item.id === recordId); const nextCategory = String(event.currentTarget.dataset.category || ""); if (!record || !nextCategory || nextCategory === record.displayCategory) { this.closeQuickCategoryPopup(); return; } const updated = updateRecord({ id: record.id, content: record.content, category: nextCategory }); if (!updated) { wx.showToast({ title: this.data.copy?.toast?.updateFailed || "更新失败", icon: "none" }); return; } wx.showToast({ title: this.data.copy?.toast?.categoryUpdated || "分类已更新", icon: "success" }); this.closeQuickCategoryPopup(); this.reload(); }, onOpenEdit(event) { const recordId = String(event.currentTarget.dataset.id || ""); const record = this.data.rows.find((item) => item.id === recordId); if (!record) return; if (Number(record.swipeOffsetX) < 0) { this.updateRowSwipeOffset(recordId, 0); return; } this.clearEditAutoSaveTimer(); this.setData({ editPopupVisible: true, editRecordId: record.id, editContent: record.content || "", editCategory: record.displayCategory, editUpdatedAtText: record.updatedAtText || this.formatTime(record.updatedAt), editUpdatedAtLabel: formatTemplate(this.data.copy.updatedAtPrefix, { time: record.updatedAtText || this.formatTime(record.updatedAt) }) }); }, resetEditState() { this.clearEditAutoSaveTimer(); this.setData({ editPopupVisible: false, editRecordId: "", editContent: "", editCategory: "", editUpdatedAtText: "", editUpdatedAtLabel: "" }); }, onCloseEdit() { this.flushEditAutoSave(); this.resetEditState(); }, onEditContentInput(event) { this.setData({ editContent: String(event.detail.value || "") }, () => this.scheduleEditAutoSave()); }, onEditCategoryTap(event) { const category = String(event.currentTarget.dataset.category || ""); if (!category) return; this.setData({ editCategory: category }, () => this.scheduleEditAutoSave()); }, /** * 编辑弹窗改为自动保存: * 1. 内容或分类变化后走 400ms 防抖; * 2. 关闭弹窗、页面隐藏、页面卸载时立即 flush; * 3. 空内容继续不写回存储,避免误清空历史闪念。 */ persistEditDraft() { const recordId = String(this.data.editRecordId || "").trim(); const content = String(this.data.editContent || "").trim(); const category = String(this.data.editCategory || "").trim(); if (!content) return null; if (!recordId) { const created = addRecord(content, "", { category: category || DEFAULT_VOICE_RECORD_CATEGORY_FALLBACK }); if (!created) return null; const updatedAtText = this.formatTime(created.updatedAt); this.reload(); this.setData({ editRecordId: created.id, editContent: created.content, editCategory: created.category, editUpdatedAtText: updatedAtText, editUpdatedAtLabel: formatTemplate(this.data.copy.updatedAtPrefix, { time: updatedAtText }) }); return created; } const currentRecord = searchRecords({ keyword: "" }).find((item) => item.id === recordId); const normalizedCategory = category || DEFAULT_VOICE_RECORD_CATEGORY_FALLBACK; if ( currentRecord && String(currentRecord.content || "").trim() === content && String(currentRecord.category || DEFAULT_VOICE_RECORD_CATEGORY_FALLBACK).trim() === normalizedCategory ) { return currentRecord; } const updated = updateRecord({ id: recordId, content, category: normalizedCategory }); if (!updated) return null; this.reload(); const updatedAtText = this.formatTime(updated.updatedAt); this.setData({ editUpdatedAtText: updatedAtText, editUpdatedAtLabel: formatTemplate(this.data.copy.updatedAtPrefix, { time: updatedAtText }) }); return updated; }, clearEditAutoSaveTimer() { if (!this.editAutoSaveTimer) return; clearTimeout(this.editAutoSaveTimer); this.editAutoSaveTimer = null; }, scheduleEditAutoSave() { if (!this.data.editPopupVisible) return; this.clearEditAutoSaveTimer(); this.editAutoSaveTimer = setTimeout(() => { this.editAutoSaveTimer = null; this.persistEditDraft(); }, RECORD_EDIT_AUTOSAVE_DELAY_MS); }, flushEditAutoSave() { this.clearEditAutoSaveTimer(); this.persistEditDraft(); }, onExport() { const settings = getSettings(); const categoryOptions = resolveRecordCategories(settings); const selectedCategory = categoryOptions.includes(this.data.selectedCategory) ? this.data.selectedCategory : ""; const keyword = String(this.data.query || "").trim(); const rows = searchRecords({ keyword }).filter( (item) => !selectedCategory || resolveVisibleCategory(item.category, categoryOptions) === selectedCategory ); const content = rows .map((item) => { const displayCategory = resolveVisibleCategory(item.category, categoryOptions); const exportFields = this.data.copy.exportFields || {}; return [ `${exportFields.createdAt || "创建时间"}:${this.formatTime(item.createdAt)}`, `${exportFields.updatedAt || "更新时间"}:${this.formatTime(item.updatedAt)}`, `${exportFields.server || "服务器"}:${item.serverId || "-"}`, `${exportFields.category || "分类"}:${displayCategory}`, `${exportFields.context || "上下文"}:${item.contextLabel || "-"}`, String(item.content || "") ].join("\n"); }) .join("\n\n"); wx.setClipboardData({ data: content || "", success: () => wx.showToast({ title: this.data.copy?.toast?.exported || "闪念已复制", icon: "success" }) }); }, noop() {}, ...createSvgButtonPressMethods() });