feat: 电子药箱小程序 - 4Tab药品管理
- 电子药箱: 药品库存管理 + 手动录入 + 今日用药打卡 - 药品百科: 搜索 + 分类筛选 + 20种药品静态数据 - 惠教中心: 4条药品使用指南课程 - 我的: 家庭成员信息管理 - 自定义TabBar + navigation-bar组件 - SVG药品分类插图
This commit is contained in:
@@ -0,0 +1,681 @@
|
||||
import { request } from "../../../utils/request";
|
||||
import { shopId } from "../../../env";
|
||||
import {
|
||||
join,
|
||||
leave,
|
||||
addCustomEventListener,
|
||||
removeCustomEventListener,
|
||||
} from "../../../utils/server-sent-events";
|
||||
|
||||
type CommentMessage = {
|
||||
id: string;
|
||||
name: string;
|
||||
msg: string;
|
||||
kind?: "system" | "chat";
|
||||
};
|
||||
|
||||
type CommentFeatureState = {
|
||||
approved: boolean;
|
||||
unreadReminder: boolean;
|
||||
optimizedInput: boolean;
|
||||
topTicker: boolean;
|
||||
};
|
||||
|
||||
interface CommentProperties {
|
||||
enhancementApproved: boolean;
|
||||
avatarUrl: string;
|
||||
}
|
||||
|
||||
interface CommentData {
|
||||
list: CommentMessage[];
|
||||
displayList: CommentMessage[];
|
||||
scrollTop: number;
|
||||
isAtBottom: boolean;
|
||||
maxVisible: number;
|
||||
itemHeight: number;
|
||||
MAX_HISTORY: number;
|
||||
joined: boolean;
|
||||
listener: boolean;
|
||||
lastMsgKey: string;
|
||||
inputValue: string;
|
||||
canSubmit: boolean;
|
||||
inputFocused: boolean;
|
||||
inputPlaceholderStyle: string;
|
||||
showGuidePopup: boolean;
|
||||
guideTitle: string;
|
||||
guideSubtitle: string;
|
||||
guideImages: string[];
|
||||
heartParticles: any[];
|
||||
heartButtonActive: boolean;
|
||||
unreadMessageCount: number;
|
||||
topNoticeText: string;
|
||||
topNoticeVisible: boolean;
|
||||
featureState: CommentFeatureState;
|
||||
}
|
||||
|
||||
interface CommentInstance extends WechatMiniprogram.Component.TrivialInstance {
|
||||
data: CommentData;
|
||||
properties: CommentProperties;
|
||||
_sseListener?: (payload: any) => void;
|
||||
_heartButtonTimer?: number | null;
|
||||
_heartTimers?: number[];
|
||||
_topNoticeTimer?: number | null;
|
||||
_topNoticeQueue?: string[];
|
||||
safeLeave: () => Promise<void>;
|
||||
bindSseListener: () => void;
|
||||
unbindSseListener: () => void;
|
||||
appendMsgFromSse: (
|
||||
payload: { name: string; msg: string },
|
||||
options?: { kind?: "system" | "chat"; skipTopNotice?: boolean },
|
||||
) => void;
|
||||
handleSubmit: (e: any) => Promise<void>;
|
||||
appendMsgText: (text: string) => void;
|
||||
createHeartParticle: (index: number) => any;
|
||||
removeHeartParticle: (id: string) => void;
|
||||
clearHeartEffects: () => void;
|
||||
safeVibrate: () => void;
|
||||
}
|
||||
|
||||
const ITEM_HEIGHT = 60;
|
||||
const MAX_VISIBLE = 5;
|
||||
const MAX_HISTORY = 50;
|
||||
const SSE_CHANNEL = "12";
|
||||
const SYSTEM_NOTICE = "欢迎来到直播间,请文明发言,禁止发布违规信息。";
|
||||
const DEFAULT_GUIDE_SUBTITLE = "个人中心";
|
||||
const DEFAULT_GUIDE_IMAGES = Object.freeze(["/assets/img/600.jpg"]);
|
||||
const DEFAULT_PLACEHOLDER_STYLE = "color:white;font-size:32rpx";
|
||||
const ENHANCED_PLACEHOLDER_STYLE =
|
||||
"color:rgba(255,255,255,0.62);font-size:28rpx";
|
||||
const TOP_NOTICE_DURATION = 3400;
|
||||
const REVIEW_READY_FEATURES = Object.freeze({
|
||||
unreadReminder: true,
|
||||
optimizedInput: true,
|
||||
topTicker: true,
|
||||
});
|
||||
|
||||
function getChatChannelName() {
|
||||
return `${shopId}-${SSE_CHANNEL}`;
|
||||
}
|
||||
|
||||
function createMessage(
|
||||
name: string,
|
||||
msg: string,
|
||||
kind: "system" | "chat" = "chat",
|
||||
): CommentMessage {
|
||||
return {
|
||||
id: `msg_${Date.now()}_${Math.floor(Math.random() * 1000)}`,
|
||||
name,
|
||||
msg,
|
||||
kind,
|
||||
};
|
||||
}
|
||||
|
||||
function buildWelcomeMessage() {
|
||||
return createMessage("系统", SYSTEM_NOTICE, "system");
|
||||
}
|
||||
|
||||
function buildFeatureState(approved: boolean): CommentFeatureState {
|
||||
return {
|
||||
approved,
|
||||
unreadReminder: approved && REVIEW_READY_FEATURES.unreadReminder,
|
||||
optimizedInput: approved && REVIEW_READY_FEATURES.optimizedInput,
|
||||
topTicker: approved && REVIEW_READY_FEATURES.topTicker,
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeInput(value: unknown): string {
|
||||
return String(value ?? "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function buildGuideImages(avatarUrl: unknown): string[] {
|
||||
const normalizedAvatarUrl = sanitizeInput(avatarUrl);
|
||||
|
||||
return normalizedAvatarUrl
|
||||
? [normalizedAvatarUrl]
|
||||
: [...DEFAULT_GUIDE_IMAGES];
|
||||
}
|
||||
|
||||
function syncGuideImages(instance: CommentInstance, avatarUrl: unknown) {
|
||||
const nextGuideImages = buildGuideImages(avatarUrl);
|
||||
const currentGuideImages = instance.data.guideImages || [];
|
||||
const unchanged =
|
||||
currentGuideImages.length === nextGuideImages.length &&
|
||||
currentGuideImages.every((item, index) => item === nextGuideImages[index]);
|
||||
|
||||
if (unchanged) return;
|
||||
|
||||
instance.setData({
|
||||
guideImages: nextGuideImages,
|
||||
});
|
||||
}
|
||||
|
||||
function trimMessages(messages: CommentMessage[], maxHistory: number) {
|
||||
if (messages.length <= maxHistory) return messages;
|
||||
|
||||
const pinnedMessage = messages[0]?.kind === "system" ? messages[0] : null;
|
||||
if (!pinnedMessage) {
|
||||
return messages.slice(-maxHistory);
|
||||
}
|
||||
|
||||
const normalMessages = messages.slice(1);
|
||||
return [pinnedMessage, ...normalMessages.slice(-(maxHistory - 1))];
|
||||
}
|
||||
|
||||
function formatTopNoticeText(name: string, msg: string) {
|
||||
const safeName = sanitizeInput(name) || "新消息";
|
||||
const preview = sanitizeInput(msg);
|
||||
const clippedPreview =
|
||||
preview.length > 18 ? `${preview.slice(0, 18)}...` : preview;
|
||||
|
||||
return clippedPreview
|
||||
? `${safeName}:${clippedPreview}`
|
||||
: `${safeName} 发来了一条新消息`;
|
||||
}
|
||||
|
||||
function clearTopNotice(instance: CommentInstance) {
|
||||
if (instance._topNoticeTimer) {
|
||||
clearTimeout(instance._topNoticeTimer);
|
||||
instance._topNoticeTimer = null;
|
||||
}
|
||||
|
||||
instance._topNoticeQueue = [];
|
||||
|
||||
if (instance.data.topNoticeVisible || instance.data.topNoticeText) {
|
||||
instance.setData({
|
||||
topNoticeVisible: false,
|
||||
topNoticeText: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function playTopNoticeQueue(instance: CommentInstance) {
|
||||
if (!instance.data.featureState.topTicker || instance.data.topNoticeVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
instance._topNoticeQueue = instance._topNoticeQueue || [];
|
||||
const nextNotice = instance._topNoticeQueue.shift();
|
||||
if (!nextNotice) return;
|
||||
|
||||
instance.setData(
|
||||
{
|
||||
topNoticeText: nextNotice,
|
||||
topNoticeVisible: false,
|
||||
},
|
||||
() => {
|
||||
wx.nextTick(() => {
|
||||
instance.setData({ topNoticeVisible: true });
|
||||
instance._topNoticeTimer = setTimeout(() => {
|
||||
instance.setData({ topNoticeVisible: false }, () => {
|
||||
instance._topNoticeTimer = null;
|
||||
playTopNoticeQueue(instance);
|
||||
});
|
||||
}, TOP_NOTICE_DURATION);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function enqueueTopNotice(instance: CommentInstance, text: string) {
|
||||
if (!instance.data.featureState.topTicker || !text) return;
|
||||
|
||||
instance._topNoticeQueue = instance._topNoticeQueue || [];
|
||||
instance._topNoticeQueue.push(text);
|
||||
|
||||
if (!instance.data.topNoticeVisible) {
|
||||
playTopNoticeQueue(instance);
|
||||
}
|
||||
}
|
||||
|
||||
function clearUnreadReminder(instance: CommentInstance) {
|
||||
if (!instance.data.unreadMessageCount) return;
|
||||
|
||||
instance.setData({ unreadMessageCount: 0 });
|
||||
}
|
||||
|
||||
function scrollToLatest(instance: CommentInstance, clearUnread = false) {
|
||||
if (clearUnread) {
|
||||
clearUnreadReminder(instance);
|
||||
}
|
||||
|
||||
instance.setData({ scrollTop: Date.now() });
|
||||
}
|
||||
|
||||
function syncFeatureApproval(instance: CommentInstance, approved: boolean) {
|
||||
const justEnabledTopTicker =
|
||||
!instance.data.featureState.topTicker &&
|
||||
approved &&
|
||||
REVIEW_READY_FEATURES.topTicker;
|
||||
const featureState = buildFeatureState(approved);
|
||||
|
||||
instance.setData(
|
||||
{
|
||||
featureState,
|
||||
inputPlaceholderStyle: featureState.optimizedInput
|
||||
? ENHANCED_PLACEHOLDER_STYLE
|
||||
: DEFAULT_PLACEHOLDER_STYLE,
|
||||
inputFocused: featureState.optimizedInput
|
||||
? instance.data.inputFocused
|
||||
: false,
|
||||
unreadMessageCount: featureState.unreadReminder
|
||||
? instance.data.unreadMessageCount
|
||||
: 0,
|
||||
topNoticeText: featureState.topTicker ? instance.data.topNoticeText : "",
|
||||
topNoticeVisible: featureState.topTicker
|
||||
? instance.data.topNoticeVisible
|
||||
: false,
|
||||
},
|
||||
() => {
|
||||
if (justEnabledTopTicker) {
|
||||
enqueueTopNotice(
|
||||
instance,
|
||||
formatTopNoticeText("直播提醒", SYSTEM_NOTICE),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (!featureState.topTicker) {
|
||||
clearTopNotice(instance);
|
||||
}
|
||||
}
|
||||
|
||||
Component({
|
||||
properties: {
|
||||
enhancementApproved: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
observer(this: CommentInstance, nextValue: boolean) {
|
||||
syncFeatureApproval(this, !!nextValue);
|
||||
},
|
||||
},
|
||||
avatarUrl: {
|
||||
type: String,
|
||||
value: "",
|
||||
observer(this: CommentInstance, nextValue: string) {
|
||||
syncGuideImages(this, nextValue);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
data: {
|
||||
list: [] as CommentMessage[],
|
||||
displayList: [] as CommentMessage[],
|
||||
scrollTop: 999999,
|
||||
isAtBottom: true,
|
||||
maxVisible: MAX_VISIBLE,
|
||||
itemHeight: ITEM_HEIGHT,
|
||||
MAX_HISTORY,
|
||||
joined: false,
|
||||
listener: false,
|
||||
lastMsgKey: "",
|
||||
inputValue: "",
|
||||
canSubmit: false,
|
||||
inputFocused: false,
|
||||
inputPlaceholderStyle: DEFAULT_PLACEHOLDER_STYLE,
|
||||
showGuidePopup: false,
|
||||
guideTitle: "",
|
||||
guideSubtitle: DEFAULT_GUIDE_SUBTITLE,
|
||||
guideImages: [...DEFAULT_GUIDE_IMAGES],
|
||||
heartParticles: [] as any[],
|
||||
heartButtonActive: false,
|
||||
unreadMessageCount: 0,
|
||||
topNoticeText: "",
|
||||
topNoticeVisible: false,
|
||||
featureState: buildFeatureState(false),
|
||||
} as CommentData,
|
||||
|
||||
lifetimes: {
|
||||
attached(this: CommentInstance) {
|
||||
const initialMessages = [buildWelcomeMessage()];
|
||||
this._topNoticeQueue = [];
|
||||
|
||||
this.setData(
|
||||
{
|
||||
list: initialMessages,
|
||||
displayList: initialMessages,
|
||||
},
|
||||
() => {
|
||||
syncGuideImages(this, this.properties.avatarUrl);
|
||||
syncFeatureApproval(this, !!this.properties.enhancementApproved);
|
||||
this.bindSseListener();
|
||||
|
||||
setTimeout(() => {
|
||||
scrollToLatest(this);
|
||||
}, 50);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
detached(this: CommentInstance) {
|
||||
this.unbindSseListener();
|
||||
this.safeLeave();
|
||||
this.clearHeartEffects();
|
||||
clearTopNotice(this);
|
||||
},
|
||||
},
|
||||
|
||||
pageLifetimes: {
|
||||
show(this: CommentInstance) {
|
||||
this.bindSseListener();
|
||||
|
||||
wx.nextTick(async () => {
|
||||
if (this.data.joined) return;
|
||||
|
||||
try {
|
||||
await join(getChatChannelName());
|
||||
this.setData({ joined: true });
|
||||
} catch (error) {
|
||||
console.warn("SSE join failed", error);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
hide(this: CommentInstance) {
|
||||
this.unbindSseListener();
|
||||
this.safeLeave();
|
||||
this.clearHeartEffects();
|
||||
clearTopNotice(this);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async safeLeave(this: CommentInstance) {
|
||||
if (!this.data.joined) return;
|
||||
|
||||
this.setData({ joined: false });
|
||||
try {
|
||||
await leave(getChatChannelName());
|
||||
} catch (error) {
|
||||
console.warn("SSE leave failed", error);
|
||||
}
|
||||
},
|
||||
|
||||
bindSseListener(this: CommentInstance) {
|
||||
if (this.data.listener) return;
|
||||
|
||||
this.setData({ listener: true });
|
||||
|
||||
this._sseListener = (payload: any) => {
|
||||
if (!payload) return;
|
||||
|
||||
const { name, msg } = payload;
|
||||
if (!msg && msg !== "") return;
|
||||
|
||||
const key = `${name ?? ""}:${msg}`;
|
||||
if (key === this.data.lastMsgKey) return;
|
||||
|
||||
this.setData({ lastMsgKey: key });
|
||||
this.appendMsgFromSse({
|
||||
name: name ?? "用户",
|
||||
msg,
|
||||
});
|
||||
};
|
||||
|
||||
addCustomEventListener("pushMsg", this._sseListener);
|
||||
},
|
||||
|
||||
unbindSseListener(this: CommentInstance) {
|
||||
if (!this.data.listener) return;
|
||||
|
||||
this.setData({ listener: false });
|
||||
|
||||
if (!this._sseListener) return;
|
||||
|
||||
removeCustomEventListener("pushMsg", this._sseListener);
|
||||
this._sseListener = undefined;
|
||||
},
|
||||
|
||||
onScroll(this: CommentInstance, e: any) {
|
||||
const { scrollTop, scrollHeight } = e.detail;
|
||||
|
||||
wx.createSelectorQuery()
|
||||
.in(this)
|
||||
.select("#scrollView")
|
||||
.boundingClientRect((rect) => {
|
||||
const clientHeight = rect?.height || 0;
|
||||
const isBottom = scrollTop + clientHeight >= scrollHeight - 20;
|
||||
if (isBottom !== this.data.isAtBottom) {
|
||||
this.setData({ isAtBottom: isBottom });
|
||||
}
|
||||
|
||||
if (isBottom) {
|
||||
clearUnreadReminder(this);
|
||||
}
|
||||
})
|
||||
.exec();
|
||||
},
|
||||
|
||||
appendMsgFromSse(
|
||||
this: CommentInstance,
|
||||
payload: { name: string; msg: string },
|
||||
options: { kind?: "system" | "chat"; skipTopNotice?: boolean } = {},
|
||||
) {
|
||||
const messageName =
|
||||
sanitizeInput(payload.name) ||
|
||||
(options.kind === "system" ? "系统" : "用户");
|
||||
const messageText = String(payload.msg ?? "");
|
||||
const nextMessage = createMessage(
|
||||
messageName,
|
||||
messageText,
|
||||
options.kind || "chat",
|
||||
);
|
||||
const nextList = trimMessages(
|
||||
[...this.data.list, nextMessage],
|
||||
this.data.MAX_HISTORY,
|
||||
);
|
||||
const shouldAutoScroll = this.data.isAtBottom;
|
||||
const shouldShowUnreadReminder =
|
||||
this.data.featureState.unreadReminder &&
|
||||
!shouldAutoScroll &&
|
||||
(options.kind || "chat") !== "system";
|
||||
|
||||
this.setData(
|
||||
{
|
||||
list: nextList,
|
||||
displayList: nextList,
|
||||
unreadMessageCount: shouldShowUnreadReminder
|
||||
? this.data.unreadMessageCount + 1
|
||||
: this.data.unreadMessageCount,
|
||||
},
|
||||
() => {
|
||||
if (shouldAutoScroll) {
|
||||
scrollToLatest(this);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (this.data.featureState.topTicker && !options.skipTopNotice) {
|
||||
enqueueTopNotice(this, formatTopNoticeText(messageName, messageText));
|
||||
}
|
||||
},
|
||||
|
||||
handleInput(this: CommentInstance, e: any) {
|
||||
const value = e.detail.value || "";
|
||||
|
||||
this.setData({
|
||||
inputValue: value,
|
||||
canSubmit: !!sanitizeInput(value),
|
||||
});
|
||||
},
|
||||
|
||||
handleInputFocus(this: CommentInstance) {
|
||||
if (!this.data.featureState.optimizedInput) return;
|
||||
|
||||
this.setData({ inputFocused: true });
|
||||
},
|
||||
|
||||
handleInputBlur(this: CommentInstance) {
|
||||
if (!this.data.inputFocused) return;
|
||||
|
||||
this.setData({ inputFocused: false });
|
||||
},
|
||||
|
||||
submitCurrentInput(this: CommentInstance) {
|
||||
if (!this.data.canSubmit) return;
|
||||
|
||||
this.handleSubmit({
|
||||
detail: {
|
||||
value: this.data.inputValue,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
handleUnreadReminderTap(this: CommentInstance) {
|
||||
scrollToLatest(this, true);
|
||||
},
|
||||
|
||||
openGuidePopup(this: CommentInstance) {
|
||||
this.setData({
|
||||
showGuidePopup: true,
|
||||
guideTitle: wx.getStorageSync("userName") || "当前用户",
|
||||
guideSubtitle: DEFAULT_GUIDE_SUBTITLE,
|
||||
});
|
||||
},
|
||||
|
||||
closeGuidePopup(this: CommentInstance) {
|
||||
this.setData({
|
||||
showGuidePopup: false,
|
||||
});
|
||||
},
|
||||
|
||||
async handleSubmit(this: CommentInstance, e: any) {
|
||||
const value = sanitizeInput(e?.detail?.value ?? this.data.inputValue);
|
||||
|
||||
if (!value) {
|
||||
wx.showToast({
|
||||
title: "发送内容不能为空",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await request({
|
||||
options: {
|
||||
url: "/app/mini-app/push-chat-msg",
|
||||
data: {
|
||||
shopId,
|
||||
id: Number(SSE_CHANNEL),
|
||||
name: wx.getStorageSync("userName") || "微信用户",
|
||||
msg: value,
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error, "push chat message failed");
|
||||
this.appendMsgText(value);
|
||||
} finally {
|
||||
this.setData({
|
||||
inputValue: "",
|
||||
canSubmit: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
appendMsgText(this: CommentInstance, text: string) {
|
||||
this.appendMsgFromSse({
|
||||
name: wx.getStorageSync("userName") || "微信用户",
|
||||
msg: text,
|
||||
});
|
||||
},
|
||||
|
||||
handleHeartTap(this: CommentInstance) {
|
||||
const particles = Array.from({ length: 6 }, (_, index) =>
|
||||
this.createHeartParticle(index),
|
||||
);
|
||||
|
||||
this.setData({
|
||||
heartParticles: [...this.data.heartParticles, ...particles],
|
||||
heartButtonActive: true,
|
||||
});
|
||||
|
||||
this.safeVibrate();
|
||||
|
||||
if (this._heartButtonTimer) {
|
||||
clearTimeout(this._heartButtonTimer);
|
||||
}
|
||||
|
||||
this._heartButtonTimer = setTimeout(() => {
|
||||
this.setData({ heartButtonActive: false });
|
||||
}, 220);
|
||||
|
||||
this._heartTimers = this._heartTimers || [];
|
||||
particles.forEach((particle: any) => {
|
||||
const timer = setTimeout(
|
||||
() => {
|
||||
this.removeHeartParticle(particle.id);
|
||||
},
|
||||
particle.duration + particle.delay + 260,
|
||||
);
|
||||
this._heartTimers?.push(timer);
|
||||
});
|
||||
},
|
||||
|
||||
createHeartParticle(index: number) {
|
||||
const palette = ["#ff4d6d", "#ff6b81", "#ff8399", "#ffb3c1"];
|
||||
const pathClasses = ["path-left", "path-center", "path-right"];
|
||||
const pulseClasses = ["pulse-small", "pulse-medium", "pulse-large"];
|
||||
|
||||
return {
|
||||
id: `heart_${Date.now()}_${index}_${Math.floor(Math.random() * 1000)}`,
|
||||
right: 16 + Math.floor(Math.random() * 28),
|
||||
bottom: 10 + Math.floor(Math.random() * 18),
|
||||
size: 34 + Math.floor(Math.random() * 10),
|
||||
color:
|
||||
palette[
|
||||
(index + Math.floor(Math.random() * palette.length)) %
|
||||
palette.length
|
||||
],
|
||||
duration: 920 + Math.floor(Math.random() * 320),
|
||||
delay: index * 40,
|
||||
rotate: -16 + Math.floor(Math.random() * 32),
|
||||
pathClass:
|
||||
pathClasses[
|
||||
(index + Math.floor(Math.random() * pathClasses.length)) %
|
||||
pathClasses.length
|
||||
],
|
||||
pulseClass: pulseClasses[index % pulseClasses.length],
|
||||
};
|
||||
},
|
||||
|
||||
removeHeartParticle(this: CommentInstance, id: string) {
|
||||
this.setData({
|
||||
heartParticles: this.data.heartParticles.filter(
|
||||
(item: any) => item.id !== id,
|
||||
),
|
||||
});
|
||||
},
|
||||
|
||||
clearHeartEffects(this: CommentInstance) {
|
||||
if (this._heartButtonTimer) {
|
||||
clearTimeout(this._heartButtonTimer);
|
||||
this._heartButtonTimer = null;
|
||||
}
|
||||
|
||||
if (Array.isArray(this._heartTimers)) {
|
||||
this._heartTimers.forEach((timer) => clearTimeout(timer));
|
||||
this._heartTimers = [];
|
||||
}
|
||||
|
||||
this.setData({
|
||||
heartParticles: [],
|
||||
heartButtonActive: false,
|
||||
});
|
||||
},
|
||||
|
||||
safeVibrate() {
|
||||
if (typeof wx.vibrateShort !== "function") return;
|
||||
|
||||
try {
|
||||
wx.vibrateShort();
|
||||
} catch (error) {
|
||||
console.warn("vibrateShort failed", error);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user