Files
2026-03-21 18:57:10 +08:00

869 lines
31 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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()
});