first commit
This commit is contained in:
868
apps/miniprogram/pages/records/index.js
Normal file
868
apps/miniprogram/pages/records/index.js
Normal file
@@ -0,0 +1,868 @@
|
||||
/* 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()
|
||||
});
|
||||
7
apps/miniprogram/pages/records/index.json
Normal file
7
apps/miniprogram/pages/records/index.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationBarTitleText": "闪念",
|
||||
"disableScroll": true,
|
||||
"usingComponents": {
|
||||
"bottom-nav": "/components/bottom-nav/index"
|
||||
}
|
||||
}
|
||||
194
apps/miniprogram/pages/records/index.wxml
Normal file
194
apps/miniprogram/pages/records/index.wxml
Normal file
@@ -0,0 +1,194 @@
|
||||
<view class="page-root records-page" style="{{themeStyle}}">
|
||||
<view class="page-content">
|
||||
<view class="surface-panel records-panel">
|
||||
<view class="records-search-wrap">
|
||||
<view class="records-search-shell">
|
||||
<input
|
||||
class="records-search-input"
|
||||
type="text"
|
||||
placeholder="{{copy.searchPlaceholder}}"
|
||||
placeholder-class="records-search-input-placeholder"
|
||||
value="{{query}}"
|
||||
bindinput="onQueryInput"
|
||||
/>
|
||||
<button class="records-filter-btn" bindtap="onToggleCategoryMenu">
|
||||
<text class="records-filter-arrow">{{categoryMenuVisible ? '▲' : '▼'}}</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{categoryMenuVisible}}" class="records-filter-menu">
|
||||
<view
|
||||
class="records-filter-option {{selectedCategory === '' ? 'active' : ''}}"
|
||||
data-category=""
|
||||
bindtap="onSelectFilterCategory"
|
||||
>{{copy.allCategories}}</view
|
||||
>
|
||||
<view
|
||||
wx:for="{{categoryOptions}}"
|
||||
wx:key="*this"
|
||||
class="records-filter-option {{selectedCategory === item ? 'active' : ''}}"
|
||||
data-category="{{item}}"
|
||||
bindtap="onSelectFilterCategory"
|
||||
>{{item}}</view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="surface-scroll records-list-scroll" scroll-y="true">
|
||||
<view class="list-stack records-list" bindtap="onListTap">
|
||||
<view
|
||||
wx:for="{{rows}}"
|
||||
wx:key="id"
|
||||
class="record-item-shell {{item.isMuted ? 'record-item-shell-muted' : ''}}"
|
||||
data-id="{{item.id}}"
|
||||
bindtouchstart="onRecordTouchStart"
|
||||
bindtouchmove="onRecordTouchMove"
|
||||
bindtouchend="onRecordTouchEnd"
|
||||
bindtouchcancel="onRecordTouchEnd"
|
||||
>
|
||||
<view class="record-item-actions-wrap {{(item.swipeOffsetX || 0) < 0 ? 'opened' : ''}}">
|
||||
<button class="record-swipe-copy-btn" data-id="{{item.id}}" catchtap="onCopy">
|
||||
<text class="record-swipe-btn-text">{{copy.swipeCopy}}</text>
|
||||
</button>
|
||||
<button class="record-swipe-processed-btn" data-id="{{item.id}}" catchtap="onMarkProcessed">
|
||||
<text class="record-swipe-btn-text">{{copy.swipeProcessed}}</text>
|
||||
</button>
|
||||
<button class="record-swipe-discarded-btn" data-id="{{item.id}}" catchtap="onMarkDiscarded">
|
||||
<text class="record-swipe-btn-text">{{copy.swipeDiscarded}}</text>
|
||||
</button>
|
||||
<button class="record-swipe-delete-btn" data-id="{{item.id}}" catchtap="onDelete">
|
||||
<text class="record-swipe-btn-text">{{copy.swipeDelete}}</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view class="record-item-track" style="transform: translateX({{item.swipeOffsetX || 0}}px);">
|
||||
<view class="record-item-main">
|
||||
<view class="record-item-category-hitbox" data-id="{{item.id}}" catchtap="onQuickCategoryTap">
|
||||
<view
|
||||
id="quick-category-{{item.id}}"
|
||||
class="record-item-category"
|
||||
style="{{item.categoryStyle}}"
|
||||
>
|
||||
<text class="record-item-category-text">{{item.displayCategory}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="card record-item {{item.processed ? 'record-item-processed' : ''}}"
|
||||
data-id="{{item.id}}"
|
||||
bindtap="onOpenEdit"
|
||||
>
|
||||
<view class="record-item-header">
|
||||
<text class="record-item-time">{{item.timeText}}</text>
|
||||
<text class="record-item-context">{{item.contextLabelText}}</text>
|
||||
</view>
|
||||
<text class="record-item-content {{item.discarded ? 'record-item-content-discarded' : ''}}">{{item.content || '--'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text wx:if="{{rows.length === 0}}" class="empty">{{copy.empty}}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="records-footer">
|
||||
<view class="records-pagination">
|
||||
<button class="btn" disabled="{{page <= 1}}" bindtap="onPrev">{{copy.prev}}</button>
|
||||
<text class="records-pagination-text">{{pageIndicatorText}}</text>
|
||||
<button class="btn" disabled="{{page >= totalPages}}" bindtap="onNext">{{copy.next}}</button>
|
||||
</view>
|
||||
<view class="records-footer-actions">
|
||||
<button
|
||||
class="btn records-footer-action-btn svg-press-btn"
|
||||
hover-class="svg-press-btn-hover"
|
||||
hover-start-time="0"
|
||||
hover-stay-time="80"
|
||||
data-press-key="records:add"
|
||||
bindtouchstart="onSvgButtonTouchStart"
|
||||
bindtouchend="onSvgButtonTouchEnd"
|
||||
bindtouchcancel="onSvgButtonTouchEnd"
|
||||
bindtap="onOpenCreate"
|
||||
>
|
||||
<image
|
||||
class="records-footer-action-icon svg-press-icon"
|
||||
src="{{pressedSvgButtonKey === 'records:add' ? (accentIcons.add || icons.add || '/assets/icons/add.svg') : (icons.add || '/assets/icons/add.svg')}}"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text>{{copy.addButton}}</text>
|
||||
</button>
|
||||
<button class="btn" bindtap="onExport">{{copy.exportButton}}</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{quickCategoryPopupVisible}}" class="records-quick-mask" bindtap="closeQuickCategoryPopup">
|
||||
<view class="records-quick-panel" style="{{quickCategoryPanelStyle}}" catchtap="noop">
|
||||
<view
|
||||
class="records-quick-cloud"
|
||||
style="width: {{quickCategoryWidthPx}}px; height: {{quickCategoryHeightPx}}px;"
|
||||
>
|
||||
<button
|
||||
wx:for="{{quickCategoryItems}}"
|
||||
wx:key="category"
|
||||
class="records-quick-bubble {{item.active ? 'active' : ''}}"
|
||||
style="{{item.style}} {{item.categoryStyle}}"
|
||||
data-category="{{item.category}}"
|
||||
bindtap="onApplyQuickCategory"
|
||||
>
|
||||
{{item.category}}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{editPopupVisible}}" class="records-edit-mask" bindtap="onCloseEdit">
|
||||
<view class="records-edit-panel" catchtap="noop">
|
||||
<scroll-view class="records-edit-category-scroll" scroll-x="true" show-scrollbar="false">
|
||||
<view class="records-edit-category-row">
|
||||
<view
|
||||
wx:for="{{categoryOptions}}"
|
||||
wx:key="*this"
|
||||
class="records-edit-category-pill {{editCategory === item ? 'active' : ''}}"
|
||||
data-category="{{item}}"
|
||||
bindtap="onEditCategoryTap"
|
||||
>{{item}}</view
|
||||
>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<textarea
|
||||
class="records-edit-textarea"
|
||||
value="{{editContent}}"
|
||||
maxlength="-1"
|
||||
auto-height
|
||||
placeholder="{{copy.editPlaceholder}}"
|
||||
bindinput="onEditContentInput"
|
||||
/>
|
||||
|
||||
<text class="records-edit-time">{{editUpdatedAtLabel}}</text>
|
||||
|
||||
<button
|
||||
class="records-edit-close-btn svg-press-btn"
|
||||
hover-class="svg-press-btn-hover"
|
||||
hover-start-time="0"
|
||||
hover-stay-time="80"
|
||||
data-press-key="records:close-edit"
|
||||
bindtouchstart="onSvgButtonTouchStart"
|
||||
bindtouchend="onSvgButtonTouchEnd"
|
||||
bindtouchcancel="onSvgButtonTouchEnd"
|
||||
bindtap="onCloseEdit"
|
||||
aria-label="{{copy.closeEditAriaLabel}}"
|
||||
>
|
||||
<image
|
||||
class="records-edit-close-icon svg-press-icon"
|
||||
src="{{pressedSvgButtonKey === 'records:close-edit' ? (accentIcons.cancel || icons.cancel || '/assets/icons/cancel.svg') : (icons.cancel || '/assets/icons/cancel.svg')}}"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<bottom-nav page="records" />
|
||||
</view>
|
||||
498
apps/miniprogram/pages/records/index.wxss
Normal file
498
apps/miniprogram/pages/records/index.wxss
Normal file
@@ -0,0 +1,498 @@
|
||||
.records-page .page-content {
|
||||
padding-top: 16rpx;
|
||||
}
|
||||
|
||||
.records-panel {
|
||||
padding: 0 0 16rpx;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.records-search-wrap {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.records-search-shell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 64rpx;
|
||||
border: 1rpx solid var(--surface-border);
|
||||
border-radius: 54rpx;
|
||||
overflow: hidden;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.records-search-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
font-size: 22rpx;
|
||||
line-height: normal;
|
||||
padding: 0 16rpx;
|
||||
}
|
||||
|
||||
.records-search-input-placeholder {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.records-filter-btn {
|
||||
flex: 0 0 88rpx;
|
||||
width: 88rpx !important;
|
||||
min-width: 88rpx !important;
|
||||
height: 100% !important;
|
||||
margin: 0 !important;
|
||||
border: 0 !important;
|
||||
border-left: 1rpx solid var(--surface-border);
|
||||
border-radius: 0 !important;
|
||||
background: var(--bg) !important;
|
||||
color: var(--text) !important;
|
||||
padding: 0 16rpx !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.records-filter-arrow {
|
||||
flex: 0 0 auto;
|
||||
font-size: 22rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.records-filter-menu {
|
||||
margin-top: 12rpx;
|
||||
padding: 10rpx;
|
||||
border: 1rpx solid var(--surface-border);
|
||||
border-radius: 18rpx;
|
||||
background: var(--surface);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10rpx;
|
||||
box-shadow: 0 14rpx 30rpx var(--surface-shadow);
|
||||
}
|
||||
|
||||
.records-filter-option {
|
||||
padding: 10rpx 18rpx;
|
||||
border-radius: 999rpx;
|
||||
border: 1rpx solid var(--surface-border);
|
||||
background: var(--bg);
|
||||
color: var(--muted);
|
||||
font-size: 22rpx;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.records-filter-option.active {
|
||||
border-color: rgba(91, 210, 255, 0.78);
|
||||
background: rgba(91, 210, 255, 0.18);
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.records-list-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.records-list {
|
||||
min-height: 0;
|
||||
padding-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.record-item-shell {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 20rpx;
|
||||
transition: opacity 160ms ease;
|
||||
}
|
||||
|
||||
.record-item-shell-muted {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.record-item-actions-wrap {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 240rpx;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 0;
|
||||
z-index: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 160ms ease;
|
||||
}
|
||||
|
||||
.record-item-actions-wrap.opened {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.record-swipe-copy-btn,
|
||||
.record-swipe-delete-btn,
|
||||
.record-swipe-processed-btn,
|
||||
.record-swipe-discarded-btn {
|
||||
width: 25% !important;
|
||||
flex: 1 1 0;
|
||||
height: 100% !important;
|
||||
min-width: 0 !important;
|
||||
margin: 0 !important;
|
||||
border: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
color: #f7fbff !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 26rpx !important;
|
||||
line-height: 1 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.record-swipe-copy-btn {
|
||||
border-radius: 20rpx 0 0 20rpx !important;
|
||||
background: rgba(101, 130, 149, 0.8) !important;
|
||||
}
|
||||
|
||||
.record-swipe-delete-btn {
|
||||
border-radius: 0 20rpx 20rpx 0 !important;
|
||||
background: rgba(164, 118, 118, 0.8) !important;
|
||||
}
|
||||
|
||||
.record-swipe-processed-btn {
|
||||
border-radius: 0 !important;
|
||||
background: rgba(124, 145, 114, 0.8) !important;
|
||||
}
|
||||
|
||||
.record-swipe-discarded-btn {
|
||||
border-radius: 0 !important;
|
||||
background: rgba(118, 124, 136, 0.8) !important;
|
||||
}
|
||||
|
||||
.record-swipe-btn-text {
|
||||
/* 左滑动作区维持竖排文案,适配四个窄按钮且不引入额外 gap。 */
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: upright;
|
||||
letter-spacing: 2rpx;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.05;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.record-item-track {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
transition: transform 160ms ease;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.record-item-main {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 2rpx;
|
||||
}
|
||||
|
||||
.record-item-category-hitbox {
|
||||
/* 分类条视觉宽度不变,只把横向热区扩到约 2 倍。 */
|
||||
flex: 0 0 auto;
|
||||
display: inline-flex;
|
||||
align-items: stretch;
|
||||
padding: 0 24rpx;
|
||||
margin: 0 -24rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.record-item-category {
|
||||
flex: 0 0 auto;
|
||||
width: 48rpx;
|
||||
min-width: 48rpx;
|
||||
border: 1rpx solid transparent;
|
||||
border-radius: 18rpx 0 0 18rpx;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
padding: 10rpx 6rpx;
|
||||
}
|
||||
|
||||
.record-item-category-text {
|
||||
color: var(--text);
|
||||
font-size: 20rpx;
|
||||
line-height: 1.1;
|
||||
text-align: center;
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: mixed;
|
||||
}
|
||||
|
||||
.record-item {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border-radius: 0 18rpx 18rpx 0;
|
||||
}
|
||||
|
||||
.record-item-processed {
|
||||
/* 已处理态改成更亮的浅绿底,拉开与深绿正文的反差,避免在移动端上糊成一片。 */
|
||||
background: linear-gradient(180deg, rgba(226, 238, 213, 0.96), rgba(206, 224, 188, 0.94));
|
||||
border-color: rgba(109, 136, 95, 0.62);
|
||||
}
|
||||
|
||||
.record-item-processed .record-item-time,
|
||||
.record-item-processed .record-item-context {
|
||||
color: #58714f;
|
||||
}
|
||||
|
||||
.record-item-processed .record-item-content {
|
||||
color: #34513a;
|
||||
}
|
||||
|
||||
.record-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.record-item-time {
|
||||
flex: 0 0 auto;
|
||||
font-size: 22rpx;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.record-item-context {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 22rpx;
|
||||
color: var(--muted);
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.record-item-content {
|
||||
display: block;
|
||||
margin-top: 10rpx;
|
||||
color: var(--text);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
line-height: 1.6;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.record-item-content-discarded {
|
||||
text-decoration: line-through;
|
||||
text-decoration-thickness: 2rpx;
|
||||
}
|
||||
|
||||
.records-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.records-footer-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.records-footer-action-btn {
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
--svg-press-active-radius: 16rpx;
|
||||
--svg-press-active-bg: var(--btn-bg-active);
|
||||
--svg-press-active-shadow: none;
|
||||
--svg-press-active-scale: 1;
|
||||
--svg-press-icon-opacity: 0.96;
|
||||
--svg-press-icon-active-opacity: 0.72;
|
||||
--svg-press-icon-active-scale: 0.92;
|
||||
}
|
||||
|
||||
.records-footer-action-icon {
|
||||
width: 26rpx;
|
||||
height: 26rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.records-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.records-pagination-text {
|
||||
font-size: 22rpx;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.records-edit-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 40;
|
||||
background: rgba(5, 11, 24, 0.54);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.records-quick-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 38;
|
||||
background: rgba(5, 11, 24, 0.2);
|
||||
}
|
||||
|
||||
.records-quick-panel {
|
||||
position: absolute;
|
||||
padding: 12rpx;
|
||||
border-radius: 24rpx;
|
||||
background: rgba(13, 24, 42, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.records-quick-cloud {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.records-quick-bubble {
|
||||
position: absolute;
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 !important;
|
||||
border: 1rpx solid transparent !important;
|
||||
border-radius: 999rpx !important;
|
||||
padding: 0 !important;
|
||||
color: var(--text) !important;
|
||||
font-size: 20rpx !important;
|
||||
line-height: 1.15 !important;
|
||||
font-weight: 700 !important;
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
box-shadow: 0 10rpx 24rpx rgba(15, 35, 68, 0.18);
|
||||
}
|
||||
|
||||
.records-quick-bubble.active {
|
||||
box-shadow:
|
||||
0 0 0 2rpx rgba(255, 255, 255, 0.18),
|
||||
0 10rpx 24rpx rgba(42, 92, 182, 0.24);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.records-edit-panel {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 680rpx;
|
||||
border-radius: 28rpx;
|
||||
background: var(--surface);
|
||||
border: 1rpx solid var(--surface-border);
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
box-shadow: 0 18rpx 42rpx var(--surface-shadow);
|
||||
}
|
||||
|
||||
.records-edit-category-scroll {
|
||||
width: calc(100% - 56rpx);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.records-edit-category-row {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.records-edit-category-pill {
|
||||
flex: 0 0 auto;
|
||||
min-height: 52rpx;
|
||||
padding: 8rpx 18rpx;
|
||||
border-radius: 999rpx;
|
||||
border: 1rpx solid var(--surface-border);
|
||||
background: var(--bg);
|
||||
color: var(--muted);
|
||||
font-size: 22rpx;
|
||||
line-height: 1.1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.records-edit-category-pill.active {
|
||||
border-color: rgba(91, 210, 255, 0.78);
|
||||
background: rgba(91, 210, 255, 0.18);
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.records-edit-textarea {
|
||||
width: 100%;
|
||||
min-height: 240rpx;
|
||||
border: 1rpx solid var(--surface-border);
|
||||
border-radius: 20rpx;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-size: 26rpx;
|
||||
line-height: 1.6;
|
||||
box-sizing: border-box;
|
||||
padding: 18rpx 20rpx;
|
||||
}
|
||||
|
||||
.records-edit-time {
|
||||
font-size: 22rpx;
|
||||
color: var(--muted);
|
||||
padding-right: 48rpx;
|
||||
}
|
||||
|
||||
.records-edit-close-btn {
|
||||
position: absolute;
|
||||
right: 24rpx;
|
||||
top: 24rpx;
|
||||
width: 40rpx !important;
|
||||
min-width: 40rpx !important;
|
||||
height: 40rpx !important;
|
||||
margin: 0 !important;
|
||||
border: 0 !important;
|
||||
border-radius: 999rpx !important;
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
--svg-press-active-radius: 999rpx;
|
||||
--svg-press-active-bg: rgba(156, 169, 191, 0.24);
|
||||
--svg-press-active-shadow:
|
||||
inset 0 0 0 1rpx rgba(210, 220, 236, 0.34),
|
||||
0 0 0 8rpx rgba(156, 169, 191, 0.12);
|
||||
--svg-press-active-scale: 0.9;
|
||||
--svg-press-icon-opacity: 0.96;
|
||||
--svg-press-icon-active-opacity: 0.68;
|
||||
--svg-press-icon-active-scale: 0.88;
|
||||
}
|
||||
|
||||
.records-edit-close-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
}
|
||||
Reference in New Issue
Block a user