Initial Commit
This commit is contained in:
commit
1d71a02738
237 changed files with 64293 additions and 0 deletions
546
components/message-action-menu/message-action-menu.js
Normal file
546
components/message-action-menu/message-action-menu.js
Normal file
|
|
@ -0,0 +1,546 @@
|
|||
// ✨ 消息操作菜单组件逻辑
|
||||
const messageInteractionManager = require('../../utils/message-interaction-manager.js');
|
||||
|
||||
Component({
|
||||
properties: {
|
||||
// 是否显示菜单
|
||||
visible: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
|
||||
// 消息对象
|
||||
message: {
|
||||
type: Object,
|
||||
value: {}
|
||||
},
|
||||
|
||||
// 是否是自己的消息
|
||||
isOwnMessage: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
|
||||
// 可用的操作
|
||||
actions: {
|
||||
type: Object,
|
||||
value: {
|
||||
quote: true, // 引用回复
|
||||
forward: true, // 转发
|
||||
favorite: true, // 收藏
|
||||
multiSelect: true, // 多选
|
||||
copy: true, // 复制
|
||||
recall: true, // 撤回
|
||||
delete: true, // 删除
|
||||
report: true // 举报
|
||||
}
|
||||
},
|
||||
|
||||
// 是否显示表情回应
|
||||
showReactions: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
},
|
||||
|
||||
// 是否显示消息信息
|
||||
showMessageInfo: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
}
|
||||
},
|
||||
|
||||
data: {
|
||||
// 常用表情
|
||||
commonEmojis: ['👍', '❤️', '😂', '😮', '😢', '😡'],
|
||||
|
||||
// 是否可以撤回
|
||||
canRecall: false,
|
||||
|
||||
// 表情选择器
|
||||
showEmojiPicker: false,
|
||||
currentEmojiCategory: 'recent',
|
||||
currentEmojiList: [],
|
||||
|
||||
// 表情分类
|
||||
emojiCategories: {
|
||||
recent: ['👍', '❤️', '😂', '😮', '😢', '😡', '🎉', '🔥'],
|
||||
smileys: ['😀', '😃', '😄', '😁', '😆', '😅', '😂', '🤣', '😊', '😇', '🙂', '🙃', '😉', '😌', '😍', '🥰', '😘', '😗', '😙', '😚', '😋', '😛', '😝', '😜', '🤪', '🤨', '🧐', '🤓', '😎', '🤩', '🥳'],
|
||||
gestures: ['👍', '👎', '👌', '✌️', '🤞', '🤟', '🤘', '🤙', '👈', '👉', '👆', '🖕', '👇', '☝️', '👋', '🤚', '🖐️', '✋', '🖖', '👏', '🙌', '🤲', '🤝', '🙏'],
|
||||
hearts: ['❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '💔', '❣️', '💕', '💞', '💓', '💗', '💖', '💘', '💝', '💟']
|
||||
}
|
||||
},
|
||||
|
||||
observers: {
|
||||
'message, isOwnMessage': function(message, isOwnMessage) {
|
||||
if (message && message.messageId) {
|
||||
this.checkRecallPermission();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
console.log('✨ 消息操作菜单组件已加载');
|
||||
this.initEmojiList();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// ✨ ===== 基础操作 =====
|
||||
|
||||
// 阻止事件冒泡
|
||||
stopPropagation() {
|
||||
// 阻止点击事件冒泡到遮罩层
|
||||
},
|
||||
|
||||
// 遮罩点击
|
||||
onMaskTap() {
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
// 关闭菜单
|
||||
closeMenu() {
|
||||
this.setData({
|
||||
visible: false,
|
||||
showEmojiPicker: false
|
||||
});
|
||||
|
||||
this.triggerEvent('close');
|
||||
},
|
||||
|
||||
// 👍 ===== 表情回应操作 =====
|
||||
|
||||
// 表情点击
|
||||
async onReactionTap(e) {
|
||||
const emoji = e.currentTarget.dataset.emoji;
|
||||
console.log('👍 表情点击:', emoji);
|
||||
|
||||
try {
|
||||
const userId = wx.getStorageSync('userId');
|
||||
if (!userId) {
|
||||
wx.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加表情回应
|
||||
const result = await messageInteractionManager.addReaction(
|
||||
this.data.message.messageId,
|
||||
emoji,
|
||||
userId
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
// 触发表情回应事件
|
||||
this.triggerEvent('reaction', {
|
||||
messageId: this.data.message.messageId,
|
||||
emoji: emoji,
|
||||
action: 'add'
|
||||
});
|
||||
|
||||
// 关闭菜单
|
||||
this.closeMenu();
|
||||
|
||||
wx.showToast({
|
||||
title: '表情回应已添加',
|
||||
icon: 'success'
|
||||
});
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: result.error || '添加失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 添加表情回应失败:', error);
|
||||
wx.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 显示更多表情
|
||||
showMoreEmojis() {
|
||||
this.setData({
|
||||
showEmojiPicker: true,
|
||||
currentEmojiCategory: 'recent'
|
||||
});
|
||||
this.updateEmojiList();
|
||||
},
|
||||
|
||||
// 关闭表情选择器
|
||||
closeEmojiPicker() {
|
||||
this.setData({
|
||||
showEmojiPicker: false
|
||||
});
|
||||
},
|
||||
|
||||
// 切换表情分类
|
||||
switchEmojiCategory(e) {
|
||||
const category = e.currentTarget.dataset.category;
|
||||
this.setData({
|
||||
currentEmojiCategory: category
|
||||
});
|
||||
this.updateEmojiList();
|
||||
},
|
||||
|
||||
// 表情选择
|
||||
async onEmojiSelect(e) {
|
||||
const emoji = e.currentTarget.dataset.emoji;
|
||||
|
||||
// 添加到最近使用
|
||||
this.addToRecentEmojis(emoji);
|
||||
|
||||
// 执行表情回应
|
||||
await this.onReactionTap({ currentTarget: { dataset: { emoji } } });
|
||||
},
|
||||
|
||||
// 初始化表情列表
|
||||
initEmojiList() {
|
||||
this.setData({
|
||||
currentEmojiList: this.data.emojiCategories.recent
|
||||
});
|
||||
},
|
||||
|
||||
// 更新表情列表
|
||||
updateEmojiList() {
|
||||
const category = this.data.currentEmojiCategory;
|
||||
const emojiList = this.data.emojiCategories[category] || [];
|
||||
|
||||
this.setData({
|
||||
currentEmojiList: emojiList
|
||||
});
|
||||
},
|
||||
|
||||
// 添加到最近使用表情
|
||||
addToRecentEmojis(emoji) {
|
||||
let recentEmojis = [...this.data.emojiCategories.recent];
|
||||
|
||||
// 移除已存在的
|
||||
recentEmojis = recentEmojis.filter(e => e !== emoji);
|
||||
|
||||
// 添加到开头
|
||||
recentEmojis.unshift(emoji);
|
||||
|
||||
// 限制数量
|
||||
if (recentEmojis.length > 20) {
|
||||
recentEmojis = recentEmojis.slice(0, 20);
|
||||
}
|
||||
|
||||
// 更新数据
|
||||
this.setData({
|
||||
[`emojiCategories.recent`]: recentEmojis
|
||||
});
|
||||
|
||||
// 如果当前显示的是最近分类,更新列表
|
||||
if (this.data.currentEmojiCategory === 'recent') {
|
||||
this.setData({
|
||||
currentEmojiList: recentEmojis
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 🎯 ===== 操作按钮处理 =====
|
||||
|
||||
// 操作点击
|
||||
async onActionTap(e) {
|
||||
const action = e.currentTarget.dataset.action;
|
||||
console.log('🎯 操作点击:', action);
|
||||
|
||||
switch (action) {
|
||||
case 'quote':
|
||||
this.handleQuote();
|
||||
break;
|
||||
case 'forward':
|
||||
this.handleForward();
|
||||
break;
|
||||
case 'favorite':
|
||||
this.handleFavorite();
|
||||
break;
|
||||
case 'multiSelect':
|
||||
this.handleMultiSelect();
|
||||
break;
|
||||
case 'copy':
|
||||
this.handleCopy();
|
||||
break;
|
||||
case 'recall':
|
||||
this.handleRecall();
|
||||
break;
|
||||
case 'delete':
|
||||
this.handleDelete();
|
||||
break;
|
||||
case 'report':
|
||||
this.handleReport();
|
||||
break;
|
||||
default:
|
||||
console.warn('⚠️ 未知操作:', action);
|
||||
}
|
||||
},
|
||||
|
||||
// 处理引用回复
|
||||
handleQuote() {
|
||||
console.log('💬 处理引用回复');
|
||||
|
||||
this.triggerEvent('action', {
|
||||
action: 'quote',
|
||||
message: this.data.message
|
||||
});
|
||||
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
// 处理转发
|
||||
handleForward() {
|
||||
console.log('📤 处理转发');
|
||||
|
||||
this.triggerEvent('action', {
|
||||
action: 'forward',
|
||||
message: this.data.message
|
||||
});
|
||||
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
// 处理收藏
|
||||
async handleFavorite() {
|
||||
console.log('⭐ 处理收藏');
|
||||
|
||||
try {
|
||||
const userId = wx.getStorageSync('userId');
|
||||
const messageId = this.data.message.messageId;
|
||||
const isFavorited = this.data.message.favorited;
|
||||
|
||||
let result;
|
||||
if (isFavorited) {
|
||||
result = await messageInteractionManager.unfavoriteMessage(messageId, userId);
|
||||
} else {
|
||||
result = await messageInteractionManager.favoriteMessage(messageId, userId);
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
this.triggerEvent('action', {
|
||||
action: 'favorite',
|
||||
message: this.data.message,
|
||||
favorited: !isFavorited
|
||||
});
|
||||
|
||||
wx.showToast({
|
||||
title: isFavorited ? '已取消收藏' : '已收藏',
|
||||
icon: 'success'
|
||||
});
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: result.error || '操作失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 收藏操作失败:', error);
|
||||
wx.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
// 处理多选
|
||||
handleMultiSelect() {
|
||||
console.log('📋 处理多选');
|
||||
|
||||
this.triggerEvent('action', {
|
||||
action: 'multiSelect',
|
||||
message: this.data.message
|
||||
});
|
||||
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
// 处理复制
|
||||
handleCopy() {
|
||||
console.log('📄 处理复制');
|
||||
|
||||
if (this.data.message.msgType === 'text') {
|
||||
wx.setClipboardData({
|
||||
data: this.data.message.content,
|
||||
success: () => {
|
||||
wx.showToast({
|
||||
title: '已复制到剪贴板',
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
// 处理撤回
|
||||
async handleRecall() {
|
||||
console.log('🔄 处理撤回');
|
||||
|
||||
try {
|
||||
const userId = wx.getStorageSync('userId');
|
||||
const messageId = this.data.message.messageId;
|
||||
|
||||
const result = await messageInteractionManager.recallMessage(messageId, userId);
|
||||
|
||||
if (result.success) {
|
||||
this.triggerEvent('action', {
|
||||
action: 'recall',
|
||||
message: this.data.message
|
||||
});
|
||||
|
||||
wx.showToast({
|
||||
title: '消息已撤回',
|
||||
icon: 'success'
|
||||
});
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: result.error || '撤回失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 撤回消息失败:', error);
|
||||
wx.showToast({
|
||||
title: '撤回失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
// 处理删除
|
||||
handleDelete() {
|
||||
console.log('🗑️ 处理删除');
|
||||
|
||||
wx.showModal({
|
||||
title: '删除消息',
|
||||
content: '确定要删除这条消息吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.triggerEvent('action', {
|
||||
action: 'delete',
|
||||
message: this.data.message
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
// 处理举报
|
||||
handleReport() {
|
||||
console.log('⚠️ 处理举报');
|
||||
|
||||
wx.showActionSheet({
|
||||
itemList: ['垃圾信息', '违法违规', '色情内容', '暴力内容', '其他'],
|
||||
success: (res) => {
|
||||
const reasons = ['spam', 'illegal', 'sexual', 'violence', 'other'];
|
||||
const reason = reasons[res.tapIndex];
|
||||
|
||||
this.triggerEvent('action', {
|
||||
action: 'report',
|
||||
message: this.data.message,
|
||||
reason: reason
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
// 🔧 ===== 工具方法 =====
|
||||
|
||||
// 检查撤回权限
|
||||
async checkRecallPermission() {
|
||||
try {
|
||||
const userId = wx.getStorageSync('userId');
|
||||
const messageId = this.data.message.messageId;
|
||||
|
||||
if (!userId || !messageId) {
|
||||
this.setData({ canRecall: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await messageInteractionManager.checkRecallPermission(messageId, userId);
|
||||
this.setData({ canRecall: result.allowed });
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 检查撤回权限失败:', error);
|
||||
this.setData({ canRecall: false });
|
||||
}
|
||||
},
|
||||
|
||||
// 格式化时间
|
||||
formatTime(timestamp) {
|
||||
if (!timestamp) return '';
|
||||
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 0) {
|
||||
// 今天
|
||||
return date.toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
} else if (diffDays === 1) {
|
||||
// 昨天
|
||||
return '昨天 ' + date.toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
} else {
|
||||
// 更早
|
||||
return date.toLocaleDateString('zh-CN') + ' ' + date.toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 获取消息类型文本
|
||||
getMessageTypeText(msgType) {
|
||||
const typeMap = {
|
||||
'text': '文本',
|
||||
'image': '图片',
|
||||
'video': '视频',
|
||||
'voice': '语音',
|
||||
'file': '文件',
|
||||
'location': '位置',
|
||||
'card': '名片'
|
||||
};
|
||||
|
||||
return typeMap[msgType] || '未知';
|
||||
},
|
||||
|
||||
// 格式化文件大小
|
||||
formatFileSize(size) {
|
||||
if (!size) return '';
|
||||
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
let unitIndex = 0;
|
||||
let fileSize = size;
|
||||
|
||||
while (fileSize >= 1024 && unitIndex < units.length - 1) {
|
||||
fileSize /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${fileSize.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
4
components/message-action-menu/message-action-menu.json
Normal file
4
components/message-action-menu/message-action-menu.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
191
components/message-action-menu/message-action-menu.wxml
Normal file
191
components/message-action-menu/message-action-menu.wxml
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
<!-- ✨ 消息操作菜单组件 -->
|
||||
<view class="message-action-menu" wx:if="{{visible}}" bindtap="onMaskTap">
|
||||
<!-- 背景遮罩 -->
|
||||
<view class="menu-mask"></view>
|
||||
|
||||
<!-- 菜单内容 -->
|
||||
<view class="menu-content" catchtap="stopPropagation">
|
||||
<!-- 表情回应区域 -->
|
||||
<view class="reactions-section" wx:if="{{showReactions}}">
|
||||
<view class="reactions-title">
|
||||
<text class="title-text">添加表情回应</text>
|
||||
</view>
|
||||
|
||||
<view class="reactions-grid">
|
||||
<view class="reaction-item"
|
||||
wx:for="{{commonEmojis}}"
|
||||
wx:key="index"
|
||||
bindtap="onReactionTap"
|
||||
data-emoji="{{item}}">
|
||||
<text class="reaction-emoji">{{item}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 更多表情按钮 -->
|
||||
<view class="reaction-item more-emoji" bindtap="showMoreEmojis">
|
||||
<text class="more-icon">➕</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮区域 -->
|
||||
<view class="actions-section">
|
||||
<!-- 引用回复 -->
|
||||
<view class="action-item"
|
||||
wx:if="{{actions.quote}}"
|
||||
bindtap="onActionTap"
|
||||
data-action="quote">
|
||||
<view class="action-icon">
|
||||
<text class="icon-text">💬</text>
|
||||
</view>
|
||||
<text class="action-text">引用</text>
|
||||
</view>
|
||||
|
||||
<!-- 转发 -->
|
||||
<view class="action-item"
|
||||
wx:if="{{actions.forward}}"
|
||||
bindtap="onActionTap"
|
||||
data-action="forward">
|
||||
<view class="action-icon">
|
||||
<text class="icon-text">📤</text>
|
||||
</view>
|
||||
<text class="action-text">转发</text>
|
||||
</view>
|
||||
|
||||
<!-- 收藏 -->
|
||||
<view class="action-item"
|
||||
wx:if="{{actions.favorite}}"
|
||||
bindtap="onActionTap"
|
||||
data-action="favorite">
|
||||
<view class="action-icon">
|
||||
<text class="icon-text">{{message.favorited ? '⭐' : '☆'}}</text>
|
||||
</view>
|
||||
<text class="action-text">{{message.favorited ? '取消收藏' : '收藏'}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 多选 -->
|
||||
<view class="action-item"
|
||||
wx:if="{{actions.multiSelect}}"
|
||||
bindtap="onActionTap"
|
||||
data-action="multiSelect">
|
||||
<view class="action-icon">
|
||||
<text class="icon-text">📋</text>
|
||||
</view>
|
||||
<text class="action-text">多选</text>
|
||||
</view>
|
||||
|
||||
<!-- 复制 -->
|
||||
<view class="action-item"
|
||||
wx:if="{{actions.copy && message.msgType === 'text'}}"
|
||||
bindtap="onActionTap"
|
||||
data-action="copy">
|
||||
<view class="action-icon">
|
||||
<text class="icon-text">📄</text>
|
||||
</view>
|
||||
<text class="action-text">复制</text>
|
||||
</view>
|
||||
|
||||
<!-- 撤回 -->
|
||||
<view class="action-item"
|
||||
wx:if="{{actions.recall && canRecall}}"
|
||||
bindtap="onActionTap"
|
||||
data-action="recall">
|
||||
<view class="action-icon">
|
||||
<text class="icon-text">🔄</text>
|
||||
</view>
|
||||
<text class="action-text">撤回</text>
|
||||
</view>
|
||||
|
||||
<!-- 删除 -->
|
||||
<view class="action-item danger"
|
||||
wx:if="{{actions.delete}}"
|
||||
bindtap="onActionTap"
|
||||
data-action="delete">
|
||||
<view class="action-icon">
|
||||
<text class="icon-text">🗑️</text>
|
||||
</view>
|
||||
<text class="action-text">删除</text>
|
||||
</view>
|
||||
|
||||
<!-- 举报 -->
|
||||
<view class="action-item danger"
|
||||
wx:if="{{actions.report && !isOwnMessage}}"
|
||||
bindtap="onActionTap"
|
||||
data-action="report">
|
||||
<view class="action-icon">
|
||||
<text class="icon-text">⚠️</text>
|
||||
</view>
|
||||
<text class="action-text">举报</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 消息信息区域 -->
|
||||
<view class="message-info-section" wx:if="{{showMessageInfo}}">
|
||||
<view class="info-item">
|
||||
<text class="info-label">发送时间:</text>
|
||||
<text class="info-value">{{formatTime(message.timestamp)}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item" wx:if="{{message.editedAt}}">
|
||||
<text class="info-label">编辑时间:</text>
|
||||
<text class="info-value">{{formatTime(message.editedAt)}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item" wx:if="{{message.msgType !== 'text'}}">
|
||||
<text class="info-label">消息类型:</text>
|
||||
<text class="info-value">{{getMessageTypeText(message.msgType)}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-item" wx:if="{{message.size}}">
|
||||
<text class="info-label">文件大小:</text>
|
||||
<text class="info-value">{{formatFileSize(message.size)}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 表情选择器弹窗 -->
|
||||
<view class="emoji-picker-modal" wx:if="{{showEmojiPicker}}" bindtap="closeEmojiPicker">
|
||||
<view class="emoji-picker-content" catchtap="stopPropagation">
|
||||
<view class="emoji-picker-header">
|
||||
<text class="picker-title">选择表情</text>
|
||||
<view class="close-btn" bindtap="closeEmojiPicker">
|
||||
<text class="close-icon">✕</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="emoji-categories">
|
||||
<view class="category-tab {{currentEmojiCategory === 'recent' ? 'active' : ''}}"
|
||||
bindtap="switchEmojiCategory"
|
||||
data-category="recent">
|
||||
<text class="tab-text">最近</text>
|
||||
</view>
|
||||
<view class="category-tab {{currentEmojiCategory === 'smileys' ? 'active' : ''}}"
|
||||
bindtap="switchEmojiCategory"
|
||||
data-category="smileys">
|
||||
<text class="tab-text">笑脸</text>
|
||||
</view>
|
||||
<view class="category-tab {{currentEmojiCategory === 'gestures' ? 'active' : ''}}"
|
||||
bindtap="switchEmojiCategory"
|
||||
data-category="gestures">
|
||||
<text class="tab-text">手势</text>
|
||||
</view>
|
||||
<view class="category-tab {{currentEmojiCategory === 'hearts' ? 'active' : ''}}"
|
||||
bindtap="switchEmojiCategory"
|
||||
data-category="hearts">
|
||||
<text class="tab-text">爱心</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="emoji-grid-container" scroll-y="true">
|
||||
<view class="emoji-grid">
|
||||
<view class="emoji-grid-item"
|
||||
wx:for="{{currentEmojiList}}"
|
||||
wx:key="index"
|
||||
bindtap="onEmojiSelect"
|
||||
data-emoji="{{item}}">
|
||||
<text class="grid-emoji">{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
446
components/message-action-menu/message-action-menu.wxss
Normal file
446
components/message-action-menu/message-action-menu.wxss
Normal file
|
|
@ -0,0 +1,446 @@
|
|||
/* ✨ 消息操作菜单组件样式 */
|
||||
|
||||
/* CSS变量定义 */
|
||||
.message-action-menu {
|
||||
--menu-bg: rgba(0, 0, 0, 0.8);
|
||||
--content-bg: #FFFFFF;
|
||||
--border-color: #E5E5EA;
|
||||
--text-primary: #000000;
|
||||
--text-secondary: #8E8E93;
|
||||
--text-danger: #FF3B30;
|
||||
--button-bg: #F2F2F7;
|
||||
--button-active: #E5E5EA;
|
||||
--shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.2);
|
||||
--radius: 16rpx;
|
||||
}
|
||||
|
||||
/* 🌙 深色模式支持 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.message-action-menu {
|
||||
--content-bg: #1C1C1E;
|
||||
--border-color: #38383A;
|
||||
--text-primary: #FFFFFF;
|
||||
--text-secondary: #8E8E93;
|
||||
--text-danger: #FF453A;
|
||||
--button-bg: #2C2C2E;
|
||||
--button-active: #3A3A3C;
|
||||
--shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
/* 🎨 菜单容器 */
|
||||
.message-action-menu {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9998;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 🎨 背景遮罩 */
|
||||
.menu-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--menu-bg);
|
||||
}
|
||||
|
||||
/* 🎨 菜单内容 */
|
||||
.menu-content {
|
||||
width: 100%;
|
||||
max-width: 750rpx;
|
||||
background: var(--content-bg);
|
||||
border-radius: var(--radius) var(--radius) 0 0;
|
||||
box-shadow: var(--shadow);
|
||||
animation: slideUp 0.3s ease-out;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 🎨 表情回应区域 */
|
||||
.reactions-section {
|
||||
padding: 32rpx;
|
||||
border-bottom: 1rpx solid var(--border-color);
|
||||
}
|
||||
|
||||
.reactions-title {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.reactions-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.reaction-item {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
background: var(--button-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
border: 2rpx solid transparent;
|
||||
}
|
||||
|
||||
.reaction-item:active {
|
||||
background: var(--button-active);
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.reaction-emoji {
|
||||
font-size: 48rpx;
|
||||
}
|
||||
|
||||
.more-emoji {
|
||||
border: 2rpx dashed var(--border-color);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.more-icon {
|
||||
font-size: 32rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 🎨 操作按钮区域 */
|
||||
.actions-section {
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 32rpx;
|
||||
transition: all 0.2s ease;
|
||||
border-bottom: 1rpx solid var(--border-color);
|
||||
}
|
||||
|
||||
.action-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.action-item:active {
|
||||
background: var(--button-bg);
|
||||
}
|
||||
|
||||
.action-item.danger {
|
||||
color: var(--text-danger);
|
||||
}
|
||||
|
||||
.action-item.danger .action-text {
|
||||
color: var(--text-danger);
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 36rpx;
|
||||
background: var(--button-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24rpx;
|
||||
}
|
||||
|
||||
.action-item.danger .action-icon {
|
||||
background: rgba(255, 59, 48, 0.1);
|
||||
}
|
||||
|
||||
.icon-text {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 32rpx;
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 🎨 消息信息区域 */
|
||||
.message-info-section {
|
||||
padding: 32rpx;
|
||||
background: var(--button-bg);
|
||||
border-top: 1rpx solid var(--border-color);
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.info-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 28rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 28rpx;
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 🎨 表情选择器弹窗 */
|
||||
.emoji-picker-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
background: var(--menu-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.emoji-picker-content {
|
||||
width: 90%;
|
||||
max-width: 600rpx;
|
||||
height: 80%;
|
||||
max-height: 800rpx;
|
||||
background: var(--content-bg);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
animation: scaleIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
transform: scale(0.8);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.emoji-picker-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx;
|
||||
border-bottom: 1rpx solid var(--border-color);
|
||||
}
|
||||
|
||||
.picker-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 32rpx;
|
||||
background: var(--button-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.close-btn:active {
|
||||
background: var(--button-active);
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
font-size: 28rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 🎨 表情分类标签 */
|
||||
.emoji-categories {
|
||||
display: flex;
|
||||
border-bottom: 1rpx solid var(--border-color);
|
||||
}
|
||||
|
||||
.category-tab {
|
||||
flex: 1;
|
||||
padding: 24rpx 16rpx;
|
||||
text-align: center;
|
||||
transition: all 0.2s ease;
|
||||
border-bottom: 4rpx solid transparent;
|
||||
}
|
||||
|
||||
.category-tab.active {
|
||||
border-bottom-color: #007AFF;
|
||||
}
|
||||
|
||||
.category-tab:active {
|
||||
background: var(--button-bg);
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 28rpx;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.category-tab.active .tab-text {
|
||||
color: #007AFF;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 🎨 表情网格 */
|
||||
.emoji-grid-container {
|
||||
flex: 1;
|
||||
padding: 16rpx;
|
||||
}
|
||||
|
||||
.emoji-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.emoji-grid-item {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.emoji-grid-item:active {
|
||||
background: var(--button-bg);
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.grid-emoji {
|
||||
font-size: 48rpx;
|
||||
}
|
||||
|
||||
/* 📱 响应式设计 */
|
||||
@media screen and (max-width: 375px) {
|
||||
.reactions-section {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.reaction-item {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 36rpx;
|
||||
}
|
||||
|
||||
.reaction-emoji {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
padding: 20rpx 24rpx;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 32rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.icon-text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.emoji-grid-item {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
}
|
||||
|
||||
.grid-emoji {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 414px) {
|
||||
.reactions-section {
|
||||
padding: 40rpx;
|
||||
}
|
||||
|
||||
.reaction-item {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 48rpx;
|
||||
}
|
||||
|
||||
.reaction-emoji {
|
||||
font-size: 52rpx;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
padding: 28rpx 40rpx;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
margin-right: 28rpx;
|
||||
}
|
||||
|
||||
.icon-text {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.emoji-grid-item {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
}
|
||||
|
||||
.grid-emoji {
|
||||
font-size: 52rpx;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue