upload project
31
.eslintrc.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Eslint config file
|
||||
* Documentation: https://eslint.org/docs/user-guide/configuring/
|
||||
* Install the Eslint extension before using this feature.
|
||||
*/
|
||||
module.exports = {
|
||||
env: {
|
||||
es6: true,
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
ecmaFeatures: {
|
||||
modules: true,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
},
|
||||
globals: {
|
||||
wx: true,
|
||||
App: true,
|
||||
Page: true,
|
||||
getCurrentPages: true,
|
||||
getApp: true,
|
||||
Component: true,
|
||||
requirePlugin: true,
|
||||
requireMiniProgram: true,
|
||||
},
|
||||
// extends: 'eslint:recommended',
|
||||
rules: {},
|
||||
}
|
||||
17
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Windows
|
||||
[Dd]esktop.ini
|
||||
Thumbs.db
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
|
||||
# Node.js
|
||||
node_modules/
|
||||
|
||||
# Local overrides
|
||||
config/config.private.js
|
||||
11
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"cSpell.enabled": true,
|
||||
"cSpell.ignorePaths": [
|
||||
"**/*.wxml",
|
||||
"**/*.wxss"
|
||||
],
|
||||
"files.associations": {
|
||||
"*.wxml": "html",
|
||||
"*.wxss": "css"
|
||||
}
|
||||
}
|
||||
0
README.md
Normal file
184
app.json
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
{
|
||||
"requestDomain": [
|
||||
"https://newkmsapi.qixincha.com",
|
||||
"https://api.faxianwo.me"
|
||||
],
|
||||
"sdkVersion": "2.21.0",
|
||||
"pages": [
|
||||
"pages/splash/splash",
|
||||
"pages/login/login",
|
||||
"pages/login/mobile-login-page",
|
||||
"pages/login/profile-page",
|
||||
"pages/map/map",
|
||||
"pages/message/message",
|
||||
"pages/message/chat/chat",
|
||||
"pages/social/friends/friends",
|
||||
"pages/circle/circle",
|
||||
"pages/webview/webview"
|
||||
],
|
||||
"window": {
|
||||
"backgroundTextStyle": "light",
|
||||
"navigationBarBackgroundColor": "#000000",
|
||||
"navigationBarTitleText": "FindMe",
|
||||
"navigationBarTextStyle": "white",
|
||||
"backgroundColor": "#f8f9fa",
|
||||
"enablePullDownRefresh": false,
|
||||
"onReachBottomDistance": 50
|
||||
},
|
||||
"networkTimeout": {
|
||||
"request": 15000,
|
||||
"downloadFile": 15000,
|
||||
"uploadFile": 20000,
|
||||
"connectSocket": 20000
|
||||
},
|
||||
"debug": false,
|
||||
"darkmode": true,
|
||||
"themeLocation": "theme.json",
|
||||
"permission": {
|
||||
"scope.userLocation": {
|
||||
"desc": "你的位置信息将用于小程序位置接口的效果展示"
|
||||
},
|
||||
"scope.camera": {
|
||||
"desc": "需使用摄像头扫描二维码及控制手电筒,实现扫码、照明功能"
|
||||
}
|
||||
},
|
||||
"usingComponents": {},
|
||||
"requiredBackgroundModes": [
|
||||
"location"
|
||||
],
|
||||
"requiredPrivateInfos": [
|
||||
"getLocation",
|
||||
"chooseLocation"
|
||||
],
|
||||
"sitemapLocation": "sitemap.json",
|
||||
"lazyCodeLoading": "requiredComponents",
|
||||
"subpackages": [
|
||||
{
|
||||
"root": "subpackages/social-remark",
|
||||
"pages": [
|
||||
"friend-remark"
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "subpackages/realname",
|
||||
"pages": [
|
||||
"realname",
|
||||
"camera-verify",
|
||||
"faceid-webview",
|
||||
"verify-success"
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "subpackages/social",
|
||||
"pages": [
|
||||
"friend-detail/friend-detail",
|
||||
"user-preview/user-preview",
|
||||
"search/search",
|
||||
"friend-requests/friend-requests",
|
||||
"tag-friends/tag-friends",
|
||||
"friend-selector/friend-selector"
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "subpackages/profile",
|
||||
"pages": [
|
||||
"profile/profile",
|
||||
"personal-details/personal-details",
|
||||
"avatar-edit/avatar-edit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "subpackages/qr",
|
||||
"pages": [
|
||||
"qr-code/qr-code",
|
||||
"qr-scan/qr-scan"
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "subpackages/media",
|
||||
"pages": [
|
||||
"camera/camera",
|
||||
"edit/edit",
|
||||
"edits/edits",
|
||||
"visibility-selector/visibility-selector"
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "subpackages/map-extras",
|
||||
"pages": [
|
||||
"locationList/locationList",
|
||||
"searchLocation/searchLocation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "subpackages/group",
|
||||
"pages": [
|
||||
"create-group/create-group",
|
||||
"group-info/group-info",
|
||||
"group-members/group-members",
|
||||
"group-announcement/group-announcement",
|
||||
"create-group-chat/create-group-chat",
|
||||
"caht-groups-list/caht-groups-list"
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "subpackages/settings",
|
||||
"pages": [
|
||||
"notification-settings/notification-settings",
|
||||
"about/update-log/update-log",
|
||||
"about/about",
|
||||
"settingss/settingss",
|
||||
"feedback/feedback",
|
||||
"account-security/account-security",
|
||||
"phone-binding/phone-binding",
|
||||
"chat-settings/chat-settings"
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "subpackages/search",
|
||||
"pages": [
|
||||
"global-search/global-search"
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "subpackages/dev-tools",
|
||||
"pages": [
|
||||
"websocket-test/websocket-test"
|
||||
],
|
||||
"independent": true
|
||||
}
|
||||
],
|
||||
"tabBar": {
|
||||
"color": "#999999",
|
||||
"selectedColor": "#ffffff",
|
||||
"backgroundColor": "rgba(30, 30, 30, 0)",
|
||||
"borderStyle": "white",
|
||||
"custom": true,
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/map/map",
|
||||
"text": "发现",
|
||||
"iconPath": "images/index/location.png",
|
||||
"selectedIconPath": "images/index/location-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/circle/circle",
|
||||
"text": "圈子",
|
||||
"iconPath": "images/index/circle.png",
|
||||
"selectedIconPath": "images/index/circle-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/message/message",
|
||||
"text": "聊天",
|
||||
"iconPath": "images/index/message.png",
|
||||
"selectedIconPath": "images/index/message-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/social/friends/friends",
|
||||
"text": "我的",
|
||||
"iconPath": "images/index/friend.png",
|
||||
"selectedIconPath": "images/index/friend-active.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
101
app.wxss
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
/**app.wxss**/
|
||||
/* 导入设计系统和组件样式 */
|
||||
@import './styles/design-system.wxss';
|
||||
@import './styles/components.wxss';
|
||||
@import './styles/responsive.wxss';
|
||||
/* 导入通用屏幕适配样式 */
|
||||
@import './styles/screen-adaption.wxss';
|
||||
|
||||
/* 全局基础样式 */
|
||||
page {
|
||||
/* height: 100%; */
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
background-color: #f8f9fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 默认容器样式 - 保持向后兼容 */
|
||||
.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 200rpx 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 全屏容器 - 新的推荐方式 */
|
||||
.app-container {
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
max-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 内容区域 */
|
||||
.content-area {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* 防止长按选择 */
|
||||
text, view, image {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* 玻璃效果样式 */
|
||||
.tab-bar-glass {
|
||||
/* 基础玻璃效果 */
|
||||
background: rgba(30, 30, 30, 0.7);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
|
||||
/* 添加细微边框增强效果 */
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
|
||||
/* 阴影效果增强层次感 */
|
||||
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 修复iOS上的样式问题 */
|
||||
.tab-bar-glass::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: inherit;
|
||||
z-index: -1;
|
||||
}
|
||||
550
components/media-preview/media-preview.js
Normal file
|
|
@ -0,0 +1,550 @@
|
|||
// 🎨 媒体预览组件逻辑
|
||||
const mediaManager = require('../../subpackages/media/utils/media-manager.js');
|
||||
|
||||
Component({
|
||||
properties: {
|
||||
// 是否显示预览
|
||||
visible: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
|
||||
// 媒体列表
|
||||
mediaList: {
|
||||
type: Array,
|
||||
value: []
|
||||
},
|
||||
|
||||
// 当前索引
|
||||
currentIndex: {
|
||||
type: Number,
|
||||
value: 0
|
||||
},
|
||||
|
||||
// 是否可以分享
|
||||
canShare: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
},
|
||||
|
||||
// 是否可以编辑
|
||||
canEdit: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
|
||||
// 是否可以删除
|
||||
canDelete: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
|
||||
// 是否显示底部操作栏
|
||||
showFooter: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
},
|
||||
|
||||
// 是否显示手势提示
|
||||
showGestureTips: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
}
|
||||
},
|
||||
|
||||
data: {
|
||||
// 当前媒体
|
||||
currentMedia: {},
|
||||
|
||||
// 音频播放状态
|
||||
audioPlaying: false,
|
||||
audioProgress: 0,
|
||||
audioCurrentTime: 0,
|
||||
|
||||
// 手势提示定时器
|
||||
gestureTimer: null
|
||||
},
|
||||
|
||||
observers: {
|
||||
'mediaList, currentIndex': function(mediaList, currentIndex) {
|
||||
if (mediaList && mediaList.length > 0 && currentIndex >= 0 && currentIndex < mediaList.length) {
|
||||
this.setData({
|
||||
currentMedia: mediaList[currentIndex]
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
|
||||
},
|
||||
|
||||
detached() {
|
||||
|
||||
this.cleanup();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 🎨 ===== 基础操作 =====
|
||||
|
||||
// 阻止事件冒泡
|
||||
stopPropagation() {
|
||||
// 阻止点击事件冒泡到遮罩层
|
||||
},
|
||||
|
||||
// 遮罩点击
|
||||
onMaskTap() {
|
||||
this.closePreview();
|
||||
},
|
||||
|
||||
// 关闭预览
|
||||
closePreview() {
|
||||
this.setData({
|
||||
visible: false
|
||||
});
|
||||
|
||||
this.triggerEvent('close');
|
||||
this.cleanup();
|
||||
},
|
||||
|
||||
// 清理资源
|
||||
cleanup() {
|
||||
// 停止音频播放
|
||||
if (this.data.audioPlaying) {
|
||||
this.stopAudio();
|
||||
}
|
||||
|
||||
// 清理定时器
|
||||
if (this.data.gestureTimer) {
|
||||
clearTimeout(this.data.gestureTimer);
|
||||
}
|
||||
},
|
||||
|
||||
// 🎨 ===== 图片操作 =====
|
||||
|
||||
// 轮播图切换
|
||||
onSwiperChange(e) {
|
||||
const currentIndex = e.detail.current;
|
||||
this.setData({
|
||||
currentIndex: currentIndex
|
||||
});
|
||||
|
||||
this.triggerEvent('indexchange', {
|
||||
currentIndex: currentIndex
|
||||
});
|
||||
},
|
||||
|
||||
// 图片加载完成
|
||||
onImageLoad(e) {
|
||||
|
||||
const index = e.currentTarget.dataset.index;
|
||||
const mediaList = this.data.mediaList;
|
||||
|
||||
if (mediaList[index]) {
|
||||
mediaList[index].loading = false;
|
||||
mediaList[index].error = false;
|
||||
|
||||
this.setData({
|
||||
mediaList: mediaList
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 图片加载失败
|
||||
onImageError(e) {
|
||||
console.error('❌ 图片加载失败');
|
||||
|
||||
const index = e.currentTarget.dataset.index;
|
||||
const mediaList = this.data.mediaList;
|
||||
|
||||
if (mediaList[index]) {
|
||||
mediaList[index].loading = false;
|
||||
mediaList[index].error = true;
|
||||
|
||||
this.setData({
|
||||
mediaList: mediaList
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 图片点击
|
||||
onImageTap(e) {
|
||||
// 可以实现双击放大等功能
|
||||
|
||||
},
|
||||
|
||||
// 重试加载
|
||||
retryLoad(e) {
|
||||
const index = e.currentTarget.dataset.index;
|
||||
const mediaList = this.data.mediaList;
|
||||
|
||||
if (mediaList[index]) {
|
||||
mediaList[index].loading = true;
|
||||
mediaList[index].error = false;
|
||||
|
||||
this.setData({
|
||||
mediaList: mediaList
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 🎨 ===== 视频操作 =====
|
||||
|
||||
// 视频播放
|
||||
onVideoPlay() {
|
||||
|
||||
this.triggerEvent('videoplay');
|
||||
},
|
||||
|
||||
// 视频暂停
|
||||
onVideoPause() {
|
||||
|
||||
this.triggerEvent('videopause');
|
||||
},
|
||||
|
||||
// 视频结束
|
||||
onVideoEnded() {
|
||||
|
||||
this.triggerEvent('videoended');
|
||||
},
|
||||
|
||||
// 视频错误
|
||||
onVideoError(e) {
|
||||
console.error('❌ 视频播放错误:', e.detail);
|
||||
wx.showToast({
|
||||
title: '视频播放失败',
|
||||
icon: 'none'
|
||||
});
|
||||
},
|
||||
|
||||
// 视频时间更新
|
||||
onVideoTimeUpdate(e) {
|
||||
// 可以用于显示播放进度
|
||||
|
||||
},
|
||||
|
||||
// 🎨 ===== 音频操作 =====
|
||||
|
||||
// 切换音频播放
|
||||
toggleAudioPlay() {
|
||||
if (this.data.audioPlaying) {
|
||||
this.pauseAudio();
|
||||
} else {
|
||||
this.playAudio();
|
||||
}
|
||||
},
|
||||
|
||||
// 播放音频
|
||||
playAudio() {
|
||||
// 这里需要实现音频播放逻辑
|
||||
|
||||
this.setData({
|
||||
audioPlaying: true
|
||||
});
|
||||
|
||||
// 模拟播放进度
|
||||
this.startAudioProgress();
|
||||
},
|
||||
|
||||
// 暂停音频
|
||||
pauseAudio() {
|
||||
|
||||
this.setData({
|
||||
audioPlaying: false
|
||||
});
|
||||
|
||||
this.stopAudioProgress();
|
||||
},
|
||||
|
||||
// 停止音频
|
||||
stopAudio() {
|
||||
|
||||
this.setData({
|
||||
audioPlaying: false,
|
||||
audioProgress: 0,
|
||||
audioCurrentTime: 0
|
||||
});
|
||||
|
||||
this.stopAudioProgress();
|
||||
},
|
||||
|
||||
// 开始音频进度更新
|
||||
startAudioProgress() {
|
||||
this.audioProgressTimer = setInterval(() => {
|
||||
const currentTime = this.data.audioCurrentTime + 1;
|
||||
const duration = this.data.currentMedia.duration || 100;
|
||||
const progress = (currentTime / duration) * 100;
|
||||
|
||||
this.setData({
|
||||
audioCurrentTime: currentTime,
|
||||
audioProgress: Math.min(progress, 100)
|
||||
});
|
||||
|
||||
if (progress >= 100) {
|
||||
this.stopAudio();
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
// 停止音频进度更新
|
||||
stopAudioProgress() {
|
||||
if (this.audioProgressTimer) {
|
||||
clearInterval(this.audioProgressTimer);
|
||||
this.audioProgressTimer = null;
|
||||
}
|
||||
},
|
||||
|
||||
// 🎨 ===== 文件操作 =====
|
||||
|
||||
// 打开文件
|
||||
openFile() {
|
||||
const currentMedia = this.data.currentMedia;
|
||||
|
||||
wx.openDocument({
|
||||
filePath: currentMedia.tempFilePath || currentMedia.url,
|
||||
fileType: currentMedia.extension,
|
||||
success: () => {
|
||||
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('❌ 文件打开失败:', error);
|
||||
wx.showToast({
|
||||
title: '无法打开此文件',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 保存文件
|
||||
async saveFile() {
|
||||
const currentMedia = this.data.currentMedia;
|
||||
|
||||
try {
|
||||
wx.showLoading({
|
||||
title: '保存中...'
|
||||
});
|
||||
|
||||
// 如果是网络文件,先下载
|
||||
let filePath = currentMedia.tempFilePath;
|
||||
if (!filePath && currentMedia.url) {
|
||||
const downloadResult = await mediaManager.downloadFile(currentMedia.url);
|
||||
if (downloadResult.success) {
|
||||
filePath = downloadResult.tempFilePath;
|
||||
} else {
|
||||
throw new Error('下载失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 保存到本地
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
wx.saveFile({
|
||||
tempFilePath: filePath,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
|
||||
wx.hideLoading();
|
||||
wx.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
wx.hideLoading();
|
||||
console.error('❌ 文件保存失败:', error);
|
||||
wx.showToast({
|
||||
title: '保存失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 🎨 ===== 操作按钮 =====
|
||||
|
||||
// 下载媒体
|
||||
async downloadMedia() {
|
||||
const currentMedia = this.data.currentMedia;
|
||||
|
||||
if (!currentMedia.url) {
|
||||
wx.showToast({
|
||||
title: '无法下载',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
wx.showLoading({
|
||||
title: '下载中...'
|
||||
});
|
||||
|
||||
const result = await mediaManager.downloadFile(currentMedia.url, {
|
||||
fileName: currentMedia.name
|
||||
});
|
||||
|
||||
wx.hideLoading();
|
||||
|
||||
if (result.success) {
|
||||
wx.showToast({
|
||||
title: '下载完成',
|
||||
icon: 'success'
|
||||
});
|
||||
|
||||
this.triggerEvent('download', {
|
||||
media: currentMedia,
|
||||
filePath: result.tempFilePath
|
||||
});
|
||||
} else {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
wx.hideLoading();
|
||||
console.error('❌ 下载失败:', error);
|
||||
wx.showToast({
|
||||
title: '下载失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 分享媒体
|
||||
shareMedia() {
|
||||
const currentMedia = this.data.currentMedia;
|
||||
|
||||
this.triggerEvent('share', {
|
||||
media: currentMedia
|
||||
});
|
||||
},
|
||||
|
||||
// 编辑媒体
|
||||
editMedia() {
|
||||
const currentMedia = this.data.currentMedia;
|
||||
|
||||
this.triggerEvent('edit', {
|
||||
media: currentMedia,
|
||||
index: this.data.currentIndex
|
||||
});
|
||||
},
|
||||
|
||||
// 删除媒体
|
||||
deleteMedia() {
|
||||
const currentMedia = this.data.currentMedia;
|
||||
|
||||
wx.showModal({
|
||||
title: '删除确认',
|
||||
content: '确定要删除这个文件吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.triggerEvent('delete', {
|
||||
media: currentMedia,
|
||||
index: this.data.currentIndex
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 收藏媒体
|
||||
favoriteMedia() {
|
||||
const currentMedia = this.data.currentMedia;
|
||||
const favorited = !currentMedia.favorited;
|
||||
|
||||
// 更新收藏状态
|
||||
currentMedia.favorited = favorited;
|
||||
this.setData({
|
||||
currentMedia: currentMedia
|
||||
});
|
||||
|
||||
this.triggerEvent('favorite', {
|
||||
media: currentMedia,
|
||||
favorited: favorited
|
||||
});
|
||||
|
||||
wx.showToast({
|
||||
title: favorited ? '已收藏' : '已取消收藏',
|
||||
icon: 'success'
|
||||
});
|
||||
},
|
||||
|
||||
// 显示更多操作
|
||||
showMoreActions() {
|
||||
const actions = ['转发', '设为壁纸', '添加到相册', '举报'];
|
||||
|
||||
wx.showActionSheet({
|
||||
itemList: actions,
|
||||
success: (res) => {
|
||||
this.triggerEvent('moreaction', {
|
||||
action: actions[res.tapIndex],
|
||||
media: this.data.currentMedia
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 🎨 ===== 工具方法 =====
|
||||
|
||||
// 格式化文件大小
|
||||
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]}`;
|
||||
},
|
||||
|
||||
// 格式化时长
|
||||
formatDuration(duration) {
|
||||
if (!duration) return '00:00';
|
||||
|
||||
const minutes = Math.floor(duration / 60);
|
||||
const seconds = Math.floor(duration % 60);
|
||||
|
||||
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
},
|
||||
|
||||
// 格式化时间
|
||||
formatTime(time) {
|
||||
if (!time) return '00:00';
|
||||
|
||||
const minutes = Math.floor(time / 60);
|
||||
const seconds = Math.floor(time % 60);
|
||||
|
||||
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
},
|
||||
|
||||
// 获取文件图标
|
||||
getFileIcon(extension) {
|
||||
const iconMap = {
|
||||
'pdf': '📄',
|
||||
'doc': '📝',
|
||||
'docx': '📝',
|
||||
'xls': '📊',
|
||||
'xlsx': '📊',
|
||||
'ppt': '📽️',
|
||||
'pptx': '📽️',
|
||||
'txt': '📃',
|
||||
'zip': '🗜️',
|
||||
'rar': '🗜️',
|
||||
'mp3': '🎵',
|
||||
'wav': '🎵',
|
||||
'mp4': '🎬',
|
||||
'avi': '🎬'
|
||||
};
|
||||
|
||||
return iconMap[extension] || '📄';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
4
components/media-preview/media-preview.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
192
components/media-preview/media-preview.wxml
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
<!-- 🎨 媒体预览组件 -->
|
||||
<view class="media-preview-container" wx:if="{{visible}}" bindtap="onMaskTap">
|
||||
<!-- 背景遮罩 -->
|
||||
<view class="preview-mask"></view>
|
||||
|
||||
<!-- 预览内容 -->
|
||||
<view class="preview-content" catchtap="stopPropagation">
|
||||
<!-- 头部工具栏 -->
|
||||
<view class="preview-header">
|
||||
<view class="header-info">
|
||||
<text class="media-title">{{currentMedia.name || '媒体预览'}}</text>
|
||||
<text class="media-info" wx:if="{{currentMedia.size}}">
|
||||
{{formatFileSize(currentMedia.size)}}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="header-actions">
|
||||
<!-- 下载按钮 -->
|
||||
<view class="action-btn" bindtap="downloadMedia" wx:if="{{currentMedia.url}}">
|
||||
<text class="action-icon">📥</text>
|
||||
</view>
|
||||
|
||||
<!-- 分享按钮 -->
|
||||
<view class="action-btn" bindtap="shareMedia" wx:if="{{canShare}}">
|
||||
<text class="action-icon">📤</text>
|
||||
</view>
|
||||
|
||||
<!-- 关闭按钮 -->
|
||||
<view class="action-btn close-btn" bindtap="closePreview">
|
||||
<text class="action-icon">✕</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 媒体内容区域 -->
|
||||
<view class="media-container">
|
||||
<!-- 图片预览 -->
|
||||
<view class="image-preview" wx:if="{{currentMedia.type === 'image'}}">
|
||||
<swiper class="image-swiper"
|
||||
current="{{currentIndex}}"
|
||||
bindchange="onSwiperChange"
|
||||
indicator-dots="{{mediaList.length > 1}}"
|
||||
indicator-color="rgba(255, 255, 255, 0.3)"
|
||||
indicator-active-color="rgba(255, 255, 255, 0.8)">
|
||||
<swiper-item wx:for="{{mediaList}}" wx:key="index" wx:if="{{item.type === 'image'}}">
|
||||
<view class="image-item">
|
||||
<image class="preview-image"
|
||||
src="{{item.url || item.tempFilePath}}"
|
||||
mode="aspectFit"
|
||||
bindload="onImageLoad"
|
||||
binderror="onImageError"
|
||||
bindtap="onImageTap"
|
||||
data-index="{{index}}" />
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading-overlay" wx:if="{{item.loading}}">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<view class="error-overlay" wx:if="{{item.error}}">
|
||||
<text class="error-icon">❌</text>
|
||||
<text class="error-text">加载失败</text>
|
||||
<view class="retry-btn" bindtap="retryLoad" data-index="{{index}}">
|
||||
<text class="retry-text">重试</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
|
||||
<!-- 图片计数 -->
|
||||
<view class="image-counter" wx:if="{{mediaList.length > 1}}">
|
||||
<text class="counter-text">{{currentIndex + 1}} / {{mediaList.length}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 视频预览 -->
|
||||
<view class="video-preview" wx:if="{{currentMedia.type === 'video'}}">
|
||||
<video class="preview-video"
|
||||
src="{{currentMedia.url || currentMedia.tempFilePath}}"
|
||||
poster="{{currentMedia.thumbnailPath}}"
|
||||
controls="{{true}}"
|
||||
autoplay="{{false}}"
|
||||
loop="{{false}}"
|
||||
muted="{{false}}"
|
||||
show-center-play-btn="{{true}}"
|
||||
show-play-btn="{{true}}"
|
||||
show-fullscreen-btn="{{true}}"
|
||||
bindplay="onVideoPlay"
|
||||
bindpause="onVideoPause"
|
||||
bindended="onVideoEnded"
|
||||
binderror="onVideoError"
|
||||
bindtimeupdate="onVideoTimeUpdate">
|
||||
</video>
|
||||
|
||||
<!-- 视频信息 -->
|
||||
<view class="video-info">
|
||||
<text class="video-duration">{{formatDuration(currentMedia.duration)}}</text>
|
||||
<text class="video-size">{{currentMedia.width}}×{{currentMedia.height}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 文件预览 -->
|
||||
<view class="file-preview" wx:if="{{currentMedia.type === 'file'}}">
|
||||
<view class="file-icon-container">
|
||||
<text class="file-icon">{{getFileIcon(currentMedia.extension)}}</text>
|
||||
</view>
|
||||
|
||||
<view class="file-details">
|
||||
<text class="file-name">{{currentMedia.name}}</text>
|
||||
<text class="file-size">{{formatFileSize(currentMedia.size)}}</text>
|
||||
<text class="file-type">{{currentMedia.extension.toUpperCase()}} 文件</text>
|
||||
</view>
|
||||
|
||||
<view class="file-actions">
|
||||
<view class="file-action-btn" bindtap="openFile">
|
||||
<text class="action-text">打开文件</text>
|
||||
</view>
|
||||
<view class="file-action-btn" bindtap="saveFile">
|
||||
<text class="action-text">保存到本地</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 音频预览 -->
|
||||
<view class="audio-preview" wx:if="{{currentMedia.type === 'audio'}}">
|
||||
<view class="audio-player">
|
||||
<view class="audio-cover">
|
||||
<text class="audio-icon">🎵</text>
|
||||
</view>
|
||||
|
||||
<view class="audio-controls">
|
||||
<view class="play-btn {{audioPlaying ? 'playing' : ''}}" bindtap="toggleAudioPlay">
|
||||
<text class="play-icon">{{audioPlaying ? '⏸️' : '▶️'}}</text>
|
||||
</view>
|
||||
|
||||
<view class="audio-progress">
|
||||
<view class="progress-bar">
|
||||
<view class="progress-fill" style="width: {{audioProgress}}"></view>
|
||||
</view>
|
||||
<view class="time-info">
|
||||
<text class="current-time">{{formatTime(audioCurrentTime)}}</text>
|
||||
<text class="total-time">{{formatTime(currentMedia.duration)}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="audio-info">
|
||||
<text class="audio-name">{{currentMedia.name}}</text>
|
||||
<text class="audio-size">{{formatFileSize(currentMedia.size)}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="preview-footer" wx:if="{{showFooter}}">
|
||||
<view class="footer-actions">
|
||||
<!-- 编辑按钮 -->
|
||||
<view class="footer-btn" bindtap="editMedia" wx:if="{{canEdit}}">
|
||||
<text class="footer-icon">✏️</text>
|
||||
<text class="footer-text">编辑</text>
|
||||
</view>
|
||||
|
||||
<!-- 删除按钮 -->
|
||||
<view class="footer-btn" bindtap="deleteMedia" wx:if="{{canDelete}}">
|
||||
<text class="footer-icon">🗑️</text>
|
||||
<text class="footer-text">删除</text>
|
||||
</view>
|
||||
|
||||
<!-- 收藏按钮 -->
|
||||
<view class="footer-btn" bindtap="favoriteMedia">
|
||||
<text class="footer-icon">{{currentMedia.favorited ? '❤️' : '🤍'}}</text>
|
||||
<text class="footer-text">{{currentMedia.favorited ? '已收藏' : '收藏'}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 更多按钮 -->
|
||||
<view class="footer-btn" bindtap="showMoreActions">
|
||||
<text class="footer-icon">⋯</text>
|
||||
<text class="footer-text">更多</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 手势操作提示 -->
|
||||
<view class="gesture-tips" wx:if="{{showGestureTips}}">
|
||||
<text class="tips-text">双击放大 · 滑动切换 · 点击关闭</text>
|
||||
</view>
|
||||
</view>
|
||||
586
components/media-preview/media-preview.wxss
Normal file
|
|
@ -0,0 +1,586 @@
|
|||
/* 🎨 媒体预览组件样式 */
|
||||
|
||||
/* CSS变量定义 */
|
||||
.media-preview-container {
|
||||
--preview-bg: rgba(0, 0, 0, 0.9);
|
||||
--header-bg: rgba(0, 0, 0, 0.7);
|
||||
--text-primary: #FFFFFF;
|
||||
--text-secondary: rgba(255, 255, 255, 0.7);
|
||||
--button-bg: rgba(255, 255, 255, 0.1);
|
||||
--button-active: rgba(255, 255, 255, 0.2);
|
||||
--border-color: rgba(255, 255, 255, 0.2);
|
||||
--shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 🎨 预览容器 */
|
||||
.media-preview-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--preview-bg);
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 🎨 背景遮罩 */
|
||||
.preview-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 🎨 预览内容 */
|
||||
.preview-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 🎨 头部工具栏 */
|
||||
.preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx;
|
||||
background: var(--header-bg);
|
||||
backdrop-filter: blur(20rpx);
|
||||
border-bottom: 1rpx solid var(--border-color);
|
||||
}
|
||||
|
||||
.header-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.media-title {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.media-info {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 36rpx;
|
||||
background: var(--button-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10rpx);
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
background: var(--button-active);
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.action-btn.close-btn {
|
||||
background: rgba(255, 59, 48, 0.8);
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 32rpx;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* 🎨 媒体容器 */
|
||||
.media-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 🎨 图片预览 */
|
||||
.image-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.image-swiper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.image-item {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.image-counter {
|
||||
position: absolute;
|
||||
bottom: 40rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 12rpx 24rpx;
|
||||
background: var(--header-bg);
|
||||
border-radius: 24rpx;
|
||||
backdrop-filter: blur(20rpx);
|
||||
}
|
||||
|
||||
.counter-text {
|
||||
font-size: 28rpx;
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 🎨 视频预览 */
|
||||
.video-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.preview-video {
|
||||
width: 100%;
|
||||
max-height: 80%;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.video-info {
|
||||
position: absolute;
|
||||
bottom: 40rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
padding: 12rpx 24rpx;
|
||||
background: var(--header-bg);
|
||||
border-radius: 24rpx;
|
||||
backdrop-filter: blur(20rpx);
|
||||
}
|
||||
|
||||
.video-duration,
|
||||
.video-size {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 🎨 文件预览 */
|
||||
.file-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx 40rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.file-icon-container {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border-radius: 32rpx;
|
||||
background: var(--button-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 40rpx;
|
||||
backdrop-filter: blur(20rpx);
|
||||
border: 2rpx solid var(--border-color);
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 120rpx;
|
||||
}
|
||||
|
||||
.file-details {
|
||||
margin-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
display: block;
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 16rpx;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.file-size,
|
||||
.file-type {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.file-actions {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.file-action-btn {
|
||||
padding: 24rpx 48rpx;
|
||||
background: var(--button-bg);
|
||||
border-radius: 32rpx;
|
||||
border: 1rpx solid var(--border-color);
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(20rpx);
|
||||
}
|
||||
|
||||
.file-action-btn:active {
|
||||
background: var(--button-active);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 30rpx;
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 🎨 音频预览 */
|
||||
.audio-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx 40rpx;
|
||||
}
|
||||
|
||||
.audio-player {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 40rpx;
|
||||
margin-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.audio-cover {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border-radius: 100rpx;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.audio-icon {
|
||||
font-size: 80rpx;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.audio-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 32rpx;
|
||||
width: 100%;
|
||||
max-width: 600rpx;
|
||||
}
|
||||
|
||||
.play-btn {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 48rpx;
|
||||
background: var(--button-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(20rpx);
|
||||
border: 2rpx solid var(--border-color);
|
||||
}
|
||||
|
||||
.play-btn:active {
|
||||
background: var(--button-active);
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.play-btn.playing {
|
||||
background: rgba(52, 199, 89, 0.8);
|
||||
}
|
||||
|
||||
.play-icon {
|
||||
font-size: 40rpx;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.audio-progress {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 8rpx;
|
||||
background: var(--button-bg);
|
||||
border-radius: 4rpx;
|
||||
margin-bottom: 16rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 4rpx;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.time-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.current-time,
|
||||
.total-time {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.audio-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.audio-name {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 12rpx;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.audio-size {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 🎨 加载和错误状态 */
|
||||
.loading-overlay,
|
||||
.error-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--preview-bg);
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border: 4rpx solid var(--border-color);
|
||||
border-top: 4rpx solid var(--text-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text,
|
||||
.error-text {
|
||||
font-size: 28rpx;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 80rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
padding: 16rpx 32rpx;
|
||||
background: var(--button-bg);
|
||||
border-radius: 24rpx;
|
||||
border: 1rpx solid var(--border-color);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.retry-btn:active {
|
||||
background: var(--button-active);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.retry-text {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* 🎨 底部操作栏 */
|
||||
.preview-footer {
|
||||
padding: 32rpx;
|
||||
background: var(--header-bg);
|
||||
backdrop-filter: blur(20rpx);
|
||||
border-top: 1rpx solid var(--border-color);
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 16rpx;
|
||||
border-radius: 16rpx;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 120rpx;
|
||||
}
|
||||
|
||||
.footer-btn:active {
|
||||
background: var(--button-bg);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.footer-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 🎨 手势提示 */
|
||||
.gesture-tips {
|
||||
position: absolute;
|
||||
bottom: 160rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 16rpx 32rpx;
|
||||
background: var(--header-bg);
|
||||
border-radius: 32rpx;
|
||||
backdrop-filter: blur(20rpx);
|
||||
animation: tipsFadeIn 0.5s ease-out 1s both;
|
||||
}
|
||||
|
||||
@keyframes tipsFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.tips-text {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 📱 响应式设计 */
|
||||
@media screen and (max-width: 375px) {
|
||||
.preview-header {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.media-title {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.file-icon-container {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 96rpx;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 414px) {
|
||||
.preview-header {
|
||||
padding: 40rpx;
|
||||
}
|
||||
|
||||
.media-title {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.file-icon-container {
|
||||
width: 240rpx;
|
||||
height: 240rpx;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 140rpx;
|
||||
}
|
||||
}
|
||||
179
components/mention-selector/mention-selector.js
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
// 💬 @提醒选择组件逻辑
|
||||
const groupChatManager = require('../../utils/group-chat-manager.js');
|
||||
|
||||
Component({
|
||||
properties: {
|
||||
// 是否显示
|
||||
visible: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
|
||||
// 群ID
|
||||
groupId: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
|
||||
// 当前用户ID
|
||||
currentUserId: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
|
||||
// 是否显示@全体成员
|
||||
showMentionAll: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
}
|
||||
},
|
||||
|
||||
data: {
|
||||
// 成员数据
|
||||
allMembers: [],
|
||||
filteredMembers: [],
|
||||
|
||||
// 搜索关键词
|
||||
searchKeyword: '',
|
||||
|
||||
// 加载状态
|
||||
loading: false
|
||||
},
|
||||
|
||||
observers: {
|
||||
'visible': function(visible) {
|
||||
if (visible && this.data.groupId) {
|
||||
this.loadGroupMembers();
|
||||
}
|
||||
},
|
||||
|
||||
'groupId': function(groupId) {
|
||||
if (groupId && this.data.visible) {
|
||||
this.loadGroupMembers();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 加载群成员
|
||||
async loadGroupMembers() {
|
||||
if (!this.data.groupId) return;
|
||||
|
||||
try {
|
||||
this.setData({
|
||||
loading: true
|
||||
});
|
||||
|
||||
const result = await groupChatManager.getGroupMembers(this.data.groupId);
|
||||
|
||||
if (result.success) {
|
||||
// 过滤掉当前用户
|
||||
const members = result.data.filter(member => member.userId !== this.data.currentUserId);
|
||||
|
||||
this.setData({
|
||||
allMembers: members,
|
||||
loading: false
|
||||
});
|
||||
|
||||
// 应用搜索过滤
|
||||
this.applyFilter();
|
||||
|
||||
} else {
|
||||
throw new Error(result.error || '获取群成员失败');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.setData({
|
||||
loading: false
|
||||
});
|
||||
|
||||
console.error('❌ 加载群成员失败:', error);
|
||||
wx.showToast({
|
||||
title: '加载成员失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 搜索输入
|
||||
onSearchInput(e) {
|
||||
const keyword = e.detail.value;
|
||||
this.setData({
|
||||
searchKeyword: keyword
|
||||
});
|
||||
this.applyFilter();
|
||||
},
|
||||
|
||||
// 清除搜索
|
||||
clearSearch() {
|
||||
this.setData({
|
||||
searchKeyword: ''
|
||||
});
|
||||
this.applyFilter();
|
||||
},
|
||||
|
||||
// 应用搜索过滤
|
||||
applyFilter() {
|
||||
const keyword = this.data.searchKeyword.toLowerCase();
|
||||
let filtered = this.data.allMembers;
|
||||
|
||||
if (keyword) {
|
||||
filtered = this.data.allMembers.filter(member => {
|
||||
const name = (member.nickname || member.username || '').toLowerCase();
|
||||
return name.includes(keyword);
|
||||
});
|
||||
}
|
||||
|
||||
this.setData({
|
||||
filteredMembers: filtered
|
||||
});
|
||||
},
|
||||
|
||||
// @全体成员
|
||||
onMentionAll() {
|
||||
|
||||
this.triggerEvent('mention', {
|
||||
type: 'all',
|
||||
text: '所有人',
|
||||
userIds: this.data.allMembers.map(member => member.userId)
|
||||
});
|
||||
|
||||
this.onClose();
|
||||
},
|
||||
|
||||
// @特定成员
|
||||
onMentionMember(e) {
|
||||
const member = e.currentTarget.dataset.member;
|
||||
|
||||
this.triggerEvent('mention', {
|
||||
type: 'user',
|
||||
text: member.nickname || member.username,
|
||||
userId: member.userId,
|
||||
userIds: [member.userId]
|
||||
});
|
||||
|
||||
this.onClose();
|
||||
},
|
||||
|
||||
// 关闭选择器
|
||||
onClose() {
|
||||
this.setData({
|
||||
searchKeyword: '',
|
||||
filteredMembers: this.data.allMembers
|
||||
});
|
||||
|
||||
this.triggerEvent('close');
|
||||
},
|
||||
|
||||
// 点击遮罩
|
||||
onMaskTap() {
|
||||
this.onClose();
|
||||
},
|
||||
|
||||
// 阻止事件冒泡
|
||||
stopPropagation() {
|
||||
// 阻止点击事件冒泡
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
4
components/mention-selector/mention-selector.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
85
components/mention-selector/mention-selector.wxml
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<!-- 💬 @提醒选择组件 -->
|
||||
<view class="mention-selector-container" wx:if="{{visible}}" bindtap="onMaskTap">
|
||||
<view class="selector-content" catchtap="stopPropagation">
|
||||
<!-- 选择器头部 -->
|
||||
<view class="selector-header">
|
||||
<text class="header-title">选择要@的成员</text>
|
||||
<view class="close-btn" bindtap="onClose">
|
||||
<text class="close-icon">✕</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<view class="search-container">
|
||||
<view class="search-input-wrapper">
|
||||
<text class="search-icon">🔍</text>
|
||||
<input class="search-input"
|
||||
placeholder="搜索群成员"
|
||||
value="{{searchKeyword}}"
|
||||
bindinput="onSearchInput" />
|
||||
<view wx:if="{{searchKeyword}}"
|
||||
class="clear-search"
|
||||
bindtap="clearSearch">
|
||||
<text class="clear-icon">✕</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 成员列表 -->
|
||||
<scroll-view class="members-list" scroll-y="true">
|
||||
<!-- @全体成员 -->
|
||||
<view wx:if="{{showMentionAll && !searchKeyword}}"
|
||||
class="member-item mention-all"
|
||||
bindtap="onMentionAll">
|
||||
<view class="member-avatar-container">
|
||||
<view class="mention-all-avatar">
|
||||
<text class="mention-all-icon">@</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="member-info">
|
||||
<text class="member-name">所有人</text>
|
||||
<text class="member-desc">@全体成员</text>
|
||||
</view>
|
||||
|
||||
<view class="member-action">
|
||||
<text class="action-text">@</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 群成员 -->
|
||||
<view class="member-item"
|
||||
wx:for="{{filteredMembers}}"
|
||||
wx:key="userId"
|
||||
bindtap="onMentionMember"
|
||||
data-member="{{item}}">
|
||||
<view class="member-avatar-container">
|
||||
<image class="member-avatar"
|
||||
src="{{item.avatar || '/images/default-avatar.svg'}}"
|
||||
mode="aspectFill" />
|
||||
<view wx:if="{{item.role === 'owner'}}" class="role-badge owner">
|
||||
<text class="role-text">群主</text>
|
||||
</view>
|
||||
<view wx:elif="{{item.role === 'admin'}}" class="role-badge admin">
|
||||
<text class="role-text">管理员</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="member-info">
|
||||
<text class="member-name">{{item.nickname || item.username}}</text>
|
||||
<text class="member-desc">{{item.status || ''}}</text>
|
||||
</view>
|
||||
|
||||
<view class="member-action">
|
||||
<text class="action-text">@</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" wx:if="{{filteredMembers.length === 0 && searchKeyword}}">
|
||||
<text class="empty-icon">👥</text>
|
||||
<text class="empty-text">没有找到相关成员</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
378
components/mention-selector/mention-selector.wxss
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
/* 💬 @提醒选择组件样式 */
|
||||
|
||||
/* CSS变量定义 */
|
||||
:host {
|
||||
--primary-color: #007AFF;
|
||||
--primary-light: #5AC8FA;
|
||||
--success-color: #34C759;
|
||||
--warning-color: #FF9500;
|
||||
--background-color: #F2F2F7;
|
||||
--surface-color: #FFFFFF;
|
||||
--text-primary: #000000;
|
||||
--text-secondary: #8E8E93;
|
||||
--text-tertiary: #C7C7CC;
|
||||
--border-color: #E5E5EA;
|
||||
--shadow-light: 0 1rpx 3rpx rgba(0, 0, 0, 0.1);
|
||||
--shadow-medium: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
|
||||
--radius-small: 8rpx;
|
||||
--radius-medium: 12rpx;
|
||||
--radius-large: 20rpx;
|
||||
}
|
||||
|
||||
/* 🌙 深色模式支持 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:host {
|
||||
--primary-color: #0A84FF;
|
||||
--primary-light: #64D2FF;
|
||||
--success-color: #30D158;
|
||||
--warning-color: #FF9F0A;
|
||||
--background-color: #000000;
|
||||
--surface-color: #1C1C1E;
|
||||
--text-primary: #FFFFFF;
|
||||
--text-secondary: #8E8E93;
|
||||
--text-tertiary: #48484A;
|
||||
--border-color: #38383A;
|
||||
--shadow-light: 0 1rpx 3rpx rgba(0, 0, 0, 0.3);
|
||||
--shadow-medium: 0 4rpx 12rpx rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.mention-selector-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.selector-content {
|
||||
width: 100%;
|
||||
max-height: 80vh;
|
||||
background: var(--surface-color);
|
||||
border-radius: var(--radius-large) var(--radius-large) 0 0;
|
||||
box-shadow: var(--shadow-medium);
|
||||
animation: slideUp 0.3s ease-out;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 🎨 选择器头部 */
|
||||
.selector-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx;
|
||||
border-bottom: 1rpx solid var(--border-color);
|
||||
background: var(--background-color);
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 32rpx;
|
||||
background: var(--surface-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.close-btn:active {
|
||||
background: var(--border-color);
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
font-size: 28rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 🎨 搜索框 */
|
||||
.search-container {
|
||||
padding: 24rpx 32rpx;
|
||||
background: var(--surface-color);
|
||||
border-bottom: 1rpx solid var(--border-color);
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--background-color);
|
||||
border: 1rpx solid var(--border-color);
|
||||
border-radius: var(--radius-small);
|
||||
padding: 0 24rpx;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.search-input-wrapper:focus-within {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 4rpx rgba(0, 122, 255, 0.1);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
font-size: 28rpx;
|
||||
color: var(--text-secondary);
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
font-size: 28rpx;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.clear-search {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 24rpx;
|
||||
background: var(--text-tertiary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.clear-search:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.clear-icon {
|
||||
font-size: 24rpx;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 🎨 成员列表 */
|
||||
.members-list {
|
||||
flex: 1;
|
||||
background: var(--surface-color);
|
||||
}
|
||||
|
||||
.member-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 32rpx;
|
||||
border-bottom: 1rpx solid var(--border-color);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.member-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.member-item:active {
|
||||
background: var(--background-color);
|
||||
}
|
||||
|
||||
.member-item.mention-all {
|
||||
background: rgba(0, 122, 255, 0.05);
|
||||
}
|
||||
|
||||
.member-item.mention-all:active {
|
||||
background: rgba(0, 122, 255, 0.1);
|
||||
}
|
||||
|
||||
.member-avatar-container {
|
||||
position: relative;
|
||||
margin-right: 24rpx;
|
||||
}
|
||||
|
||||
.member-avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
border: 2rpx solid var(--border-color);
|
||||
}
|
||||
|
||||
.mention-all-avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2rpx solid var(--border-color);
|
||||
}
|
||||
|
||||
.mention-all-icon {
|
||||
font-size: 36rpx;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.role-badge {
|
||||
position: absolute;
|
||||
bottom: -6rpx;
|
||||
right: -6rpx;
|
||||
padding: 4rpx 8rpx;
|
||||
border-radius: 12rpx;
|
||||
border: 2rpx solid var(--surface-color);
|
||||
}
|
||||
|
||||
.role-badge.owner {
|
||||
background: var(--warning-color);
|
||||
}
|
||||
|
||||
.role-badge.admin {
|
||||
background: var(--primary-color);
|
||||
}
|
||||
|
||||
.role-text {
|
||||
font-size: 20rpx;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.member-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.member-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.member-desc {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-secondary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.member-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 32rpx;
|
||||
background: var(--primary-color);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.member-action:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 28rpx;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 🎨 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx 40rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 120rpx;
|
||||
margin-bottom: 24rpx;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 📱 响应式设计 */
|
||||
@media screen and (max-width: 375px) {
|
||||
.selector-header,
|
||||
.search-container,
|
||||
.member-item {
|
||||
padding-left: 24rpx;
|
||||
padding-right: 24rpx;
|
||||
}
|
||||
|
||||
.member-avatar,
|
||||
.mention-all-avatar {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
|
||||
.mention-all-icon {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.member-action {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 414px) {
|
||||
.selector-header,
|
||||
.search-container,
|
||||
.member-item {
|
||||
padding-left: 40rpx;
|
||||
padding-right: 40rpx;
|
||||
}
|
||||
|
||||
.member-avatar,
|
||||
.mention-all-avatar {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 48rpx;
|
||||
}
|
||||
|
||||
.mention-all-icon {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.member-action {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
}
|
||||
535
components/message-action-menu/message-action-menu.js
Normal file
|
|
@ -0,0 +1,535 @@
|
|||
// ✨ 消息操作菜单组件逻辑
|
||||
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() {
|
||||
|
||||
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;
|
||||
|
||||
try {
|
||||
const userId = wx.getStorageSync('userId');
|
||||
if (!userId) {
|
||||
// 未登录用户不显示提示,直接跳转到登录页
|
||||
wx.navigateTo({ url: '/pages/login/login' });
|
||||
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;
|
||||
|
||||
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() {
|
||||
|
||||
this.triggerEvent('action', {
|
||||
action: 'quote',
|
||||
message: this.data.message
|
||||
});
|
||||
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
// 处理转发
|
||||
handleForward() {
|
||||
|
||||
this.triggerEvent('action', {
|
||||
action: 'forward',
|
||||
message: this.data.message
|
||||
});
|
||||
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
// 处理收藏
|
||||
async handleFavorite() {
|
||||
|
||||
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() {
|
||||
|
||||
this.triggerEvent('action', {
|
||||
action: 'multiSelect',
|
||||
message: this.data.message
|
||||
});
|
||||
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
// 处理复制
|
||||
handleCopy() {
|
||||
|
||||
if (this.data.message.msgType === 'text') {
|
||||
wx.setClipboardData({
|
||||
data: this.data.message.content,
|
||||
success: () => {
|
||||
wx.showToast({
|
||||
title: '已复制到剪贴板',
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
// 处理撤回
|
||||
async handleRecall() {
|
||||
|
||||
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() {
|
||||
|
||||
wx.showModal({
|
||||
title: '删除消息',
|
||||
content: '确定要删除这条消息吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.triggerEvent('action', {
|
||||
action: 'delete',
|
||||
message: this.data.message
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
// 处理举报
|
||||
handleReport() {
|
||||
|
||||
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
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
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
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
161
components/voice-message/voice-message.js
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
|
||||
Component({
|
||||
properties: {
|
||||
// 语音消息数据
|
||||
voiceData: {
|
||||
type: Object,
|
||||
value: {},
|
||||
observer: 'onVoiceDataChange'
|
||||
},
|
||||
|
||||
// 是否为自己发送的消息
|
||||
isSelf: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
|
||||
// 消息ID
|
||||
messageId: {
|
||||
type: String,
|
||||
value: ''
|
||||
}
|
||||
},
|
||||
|
||||
data: {
|
||||
// 播放状态
|
||||
isPlaying: false,
|
||||
|
||||
// 波形数据
|
||||
waveformData: [],
|
||||
|
||||
// 语音信息
|
||||
voiceUrl: '',
|
||||
voiceDuration: 0
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
this.initComponent();
|
||||
},
|
||||
|
||||
detached() {
|
||||
this.cleanup();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 初始化组件
|
||||
initComponent() {
|
||||
// 生成波形数据
|
||||
this.generateWaveform();
|
||||
|
||||
// 获取全局音频上下文
|
||||
const app = getApp();
|
||||
this.audioContext = app.globalData.audioContext || wx.createInnerAudioContext();
|
||||
|
||||
// 注册音频事件
|
||||
this.setupAudioEvents();
|
||||
},
|
||||
|
||||
// 语音数据变化处理
|
||||
onVoiceDataChange(newData, oldData) {
|
||||
if (!newData || JSON.stringify(newData) === JSON.stringify(oldData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setData({
|
||||
voiceUrl: newData.url || '',
|
||||
voiceDuration: newData.duration || 0
|
||||
});
|
||||
|
||||
// 重新生成波形
|
||||
this.generateWaveform();
|
||||
},
|
||||
|
||||
// 设置音频事件监听
|
||||
setupAudioEvents() {
|
||||
if (!this.audioContext) return;
|
||||
|
||||
this.audioContext.onPlay(() => {
|
||||
if (this.isCurrentAudio()) {
|
||||
this.setData({ isPlaying: true });
|
||||
}
|
||||
});
|
||||
|
||||
this.audioContext.onPause(() => {
|
||||
if (this.isCurrentAudio()) {
|
||||
this.setData({ isPlaying: false });
|
||||
}
|
||||
});
|
||||
|
||||
this.audioContext.onEnded(() => {
|
||||
if (this.isCurrentAudio()) {
|
||||
this.setData({ isPlaying: false });
|
||||
}
|
||||
});
|
||||
|
||||
this.audioContext.onError((err) => {
|
||||
console.error('语音播放错误:', err);
|
||||
if (this.isCurrentAudio()) {
|
||||
this.setData({ isPlaying: false });
|
||||
wx.showToast({ title: '播放失败', icon: 'none' });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 检查是否为当前音频
|
||||
isCurrentAudio() {
|
||||
return this.audioContext && this.audioContext.src === this.data.voiceUrl;
|
||||
},
|
||||
|
||||
// 切换播放状态
|
||||
togglePlay() {
|
||||
if (!this.data.voiceUrl) {
|
||||
wx.showToast({ title: '语音地址无效', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.data.isPlaying) {
|
||||
this.audioContext.pause();
|
||||
} else {
|
||||
// 停止其他正在播放的音频
|
||||
if (this.audioContext.src !== this.data.voiceUrl) {
|
||||
this.audioContext.src = this.data.voiceUrl;
|
||||
}
|
||||
this.audioContext.play();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('播放语音失败:', error);
|
||||
wx.showToast({ title: '播放失败', icon: 'none' });
|
||||
}
|
||||
},
|
||||
|
||||
// 生成波形数据
|
||||
generateWaveform() {
|
||||
const duration = this.data.voiceDuration || 1000;
|
||||
const barCount = Math.min(Math.max(Math.floor(duration / 200), 8), 30); // 8-30个波形条
|
||||
|
||||
const waveformData = [];
|
||||
for (let i = 0; i < barCount; i++) {
|
||||
// 生成随机高度,模拟真实波形
|
||||
const height = Math.random() * 60 + 20; // 20-80%的高度
|
||||
waveformData.push(height);
|
||||
}
|
||||
|
||||
this.setData({ waveformData });
|
||||
},
|
||||
|
||||
// 清理资源
|
||||
cleanup() {
|
||||
// 如果当前正在播放,停止播放
|
||||
if (this.data.isPlaying && this.audioContext) {
|
||||
try {
|
||||
this.audioContext.stop();
|
||||
} catch (e) {
|
||||
// 忽略停止错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
4
components/voice-message/voice-message.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
53
components/voice-message/voice-message.wxml
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<!-- 🎤 语音消息组件 -->
|
||||
<view class="voice-message-container {{isSelf ? 'self' : 'other'}} {{isPlaying ? 'playing' : ''}}">
|
||||
<!-- 语音消息气泡 -->
|
||||
<view class="voice-bubble" bindtap="togglePlay">
|
||||
<!-- 播放按钮 -->
|
||||
<view class="play-button">
|
||||
<view class="play-icon {{isPlaying ? 'pause' : 'play'}}">
|
||||
<text wx:if="{{!isPlaying}}" class="icon">▶</text>
|
||||
<text wx:else class="icon">⏸</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 语音波形 -->
|
||||
<view class="voice-waveform">
|
||||
<view class="waveform-container">
|
||||
<view wx:for="{{waveformData}}"
|
||||
wx:key="index"
|
||||
class="wave-bar {{index <= currentWaveIndex ? 'active' : ''}}"
|
||||
style="height: {{item}}%;">
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 语音时长 -->
|
||||
<view class="voice-duration">
|
||||
<text class="duration-text">{{formatDuration(duration)}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 播放进度条 -->
|
||||
<view wx:if="{{isPlaying}}" class="progress-container">
|
||||
<view class="progress-bar">
|
||||
<view class="progress-fill" style="width: {{playProgress}}%;"></view>
|
||||
</view>
|
||||
<view class="progress-time">
|
||||
<text class="current-time">{{formatTime(currentTime)}}</text>
|
||||
<text class="total-time">{{formatTime(duration)}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view wx:if="{{isLoading}}" class="loading-container">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<view wx:if="{{hasError}}" class="error-container">
|
||||
<text class="error-icon">⚠️</text>
|
||||
<text class="error-text">播放失败</text>
|
||||
<text class="retry-button" bindtap="retryPlay">重试</text>
|
||||
</view>
|
||||
</view>
|
||||
392
components/voice-message/voice-message.wxss
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
/* 🎤 语音消息组件样式 */
|
||||
|
||||
/* CSS变量定义 */
|
||||
.voice-message-container {
|
||||
--primary-color: #007AFF;
|
||||
--primary-light: #5AC8FA;
|
||||
--success-color: #34C759;
|
||||
--warning-color: #FF9500;
|
||||
--danger-color: #FF3B30;
|
||||
--background-light: #F2F2F7;
|
||||
--background-dark: #1C1C1E;
|
||||
--text-primary: #000000;
|
||||
--text-secondary: #8E8E93;
|
||||
--border-color: #E5E5EA;
|
||||
--shadow-light: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
--radius-medium: 12rpx;
|
||||
--radius-large: 20rpx;
|
||||
}
|
||||
|
||||
/* 🌙 深色模式支持 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.voice-message-container {
|
||||
--primary-color: #0A84FF;
|
||||
--background-light: #2C2C2E;
|
||||
--background-dark: #1C1C1E;
|
||||
--text-primary: #FFFFFF;
|
||||
--text-secondary: #8E8E93;
|
||||
--border-color: #38383A;
|
||||
--shadow-light: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.voice-message-container {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
margin: 8rpx 0;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 🎨 语音气泡 */
|
||||
.voice-bubble {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx;
|
||||
border-radius: var(--radius-large);
|
||||
box-shadow: var(--shadow-light);
|
||||
transition: all 0.3s ease;
|
||||
min-width: 200rpx;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 自己发送的消息 - 使用聊天气泡的绿色 */
|
||||
.voice-message-container.self .voice-bubble {
|
||||
background: #4DD1A1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 他人发送的消息 - 使用聊天气泡的灰色 */
|
||||
.voice-message-container.other .voice-bubble {
|
||||
background: #D9D9D9;
|
||||
color: var(--text-primary);
|
||||
border: 1rpx solid rgba(255,255,255,0.03);
|
||||
}
|
||||
|
||||
/* 播放状态 */
|
||||
.voice-message-container.playing .voice-bubble {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.voice-message-container.self.playing .voice-bubble {
|
||||
background: #3CB88F; /* 稍微深一点的绿色表示播放中 */
|
||||
}
|
||||
|
||||
.voice-message-container.other.playing .voice-bubble {
|
||||
background: #C0C0C0; /* 稍微深一点的灰色表示播放中 */
|
||||
border-color: #A0A0A0;
|
||||
}
|
||||
|
||||
/* 🎵 播放按钮 */
|
||||
.play-button {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24rpx;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0; /* 不允许在flex布局中被压缩 */
|
||||
flex: 0 0 auto; /* 宽高由自身决定 */
|
||||
min-width: 80rpx; /* 保底宽度,维持正圆 */
|
||||
}
|
||||
|
||||
.voice-message-container.self .play-button {
|
||||
background: rgba(255, 255, 255, 0.3); /* 在绿色背景上更明显的白色按钮 */
|
||||
}
|
||||
|
||||
.voice-message-container.other .play-button {
|
||||
background: rgba(0, 0, 0, 0.15); /* 在灰色背景上的深色按钮 */
|
||||
}
|
||||
|
||||
.play-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.play-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.play-icon .icon {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.voice-message-container.self .play-icon .icon {
|
||||
color: white; /* 白色图标在绿色背景上 */
|
||||
}
|
||||
|
||||
.voice-message-container.other .play-icon .icon {
|
||||
color: #333333; /* 深色图标在灰色背景上 */
|
||||
}
|
||||
|
||||
/* 播放动画 */
|
||||
.play-icon.play .icon {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
/* 🌊 语音波形 */
|
||||
.voice-waveform {
|
||||
flex: 1;
|
||||
margin-right: 24rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0; /* 允许在狭窄容器中收缩,避免溢出 */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.waveform-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.wave-bar {
|
||||
width: 6rpx;
|
||||
border-radius: 3rpx;
|
||||
transition: all 0.3s ease;
|
||||
min-height: 8rpx;
|
||||
}
|
||||
|
||||
.voice-message-container.self .wave-bar {
|
||||
background: rgba(255, 255, 255, 0.5); /* 在绿色背景上更明显的波形 */
|
||||
}
|
||||
|
||||
.voice-message-container.other .wave-bar {
|
||||
background: rgba(0, 0, 0, 0.2); /* 在灰色背景上的深色波形 */
|
||||
}
|
||||
|
||||
.voice-message-container.self .wave-bar.active {
|
||||
background: white;
|
||||
transform: scaleY(1.2);
|
||||
}
|
||||
|
||||
.voice-message-container.other .wave-bar.active {
|
||||
background: rgba(0, 0, 0, 0.5); /* 深色波形激活状态 */
|
||||
transform: scaleY(1.2);
|
||||
}
|
||||
|
||||
/* 波形动画 */
|
||||
.voice-message-container.playing .wave-bar.active {
|
||||
animation: waveAnimation 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes waveAnimation {
|
||||
0%, 100% { transform: scaleY(1); }
|
||||
50% { transform: scaleY(1.5); }
|
||||
}
|
||||
|
||||
/* ⏱️ 语音时长 */
|
||||
.voice-duration {
|
||||
min-width: 60rpx;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.duration-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.voice-message-container.self .duration-text {
|
||||
color: rgba(255, 255, 255, 0.95); /* 白色时长文字在绿色背景上 */
|
||||
}
|
||||
|
||||
.voice-message-container.other .duration-text {
|
||||
color: rgba(0, 0, 0, 0.6); /* 深色时长文字在灰色背景上 */
|
||||
}
|
||||
|
||||
/* 📊 播放进度条 */
|
||||
.progress-container {
|
||||
margin-top: 16rpx;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 4rpx;
|
||||
background: var(--border-color);
|
||||
border-radius: 2rpx;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: var(--primary-color);
|
||||
border-radius: 2rpx;
|
||||
transition: width 0.1s linear;
|
||||
}
|
||||
|
||||
.progress-time {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.current-time,
|
||||
.total-time {
|
||||
font-size: 20rpx;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 🔄 加载状态 */
|
||||
.loading-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: var(--radius-large);
|
||||
backdrop-filter: blur(10rpx);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border: 3rpx solid rgba(255, 255, 255, 0.3);
|
||||
border-top: 3rpx solid white;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 24rpx;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* ⚠️ 错误状态 */
|
||||
.error-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 59, 48, 0.1);
|
||||
border-radius: var(--radius-large);
|
||||
backdrop-filter: blur(10rpx);
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
font-size: 24rpx;
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.retry-button {
|
||||
font-size: 24rpx;
|
||||
color: var(--primary-color);
|
||||
text-decoration: underline;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.retry-button:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 🎨 特殊效果 */
|
||||
.voice-bubble::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(45deg, transparent 30%, rgba(255, 255, 255, 0.1) 50%, transparent 70%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.voice-bubble:active::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 📱 响应式设计 */
|
||||
/* 去除设备宽度相关的硬编码,统一依赖父容器宽度以避免溢出 */
|
||||
|
||||
/* 🎭 动画增强 */
|
||||
.voice-message-container {
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 长按效果 */
|
||||
.voice-bubble {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.voice-bubble:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* 可访问性 */
|
||||
.voice-bubble[aria-pressed="true"] {
|
||||
outline: 2rpx solid var(--primary-color);
|
||||
outline-offset: 4rpx;
|
||||
}
|
||||
|
||||
/* 状态指示器 */
|
||||
.voice-message-container::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -4rpx;
|
||||
right: -4rpx;
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 8rpx;
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.voice-message-container.playing::after {
|
||||
opacity: 1;
|
||||
background: var(--success-color);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
253
config/config.js
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
// 小程序配置文件 - 对应Fl`utter的app_config.dart
|
||||
|
||||
function mergeDeep(target = {}, source = {}) {
|
||||
const result = Array.isArray(target) ? target.slice() : Object.assign({}, target);
|
||||
if (!source || typeof source !== 'object') {
|
||||
return result;
|
||||
}
|
||||
|
||||
Object.keys(source).forEach((key) => {
|
||||
const sourceValue = source[key];
|
||||
const targetValue = result[key];
|
||||
|
||||
if (Array.isArray(sourceValue)) {
|
||||
result[key] = sourceValue.slice();
|
||||
} else if (
|
||||
sourceValue &&
|
||||
typeof sourceValue === 'object' &&
|
||||
!Array.isArray(sourceValue) &&
|
||||
targetValue &&
|
||||
typeof targetValue === 'object' &&
|
||||
!Array.isArray(targetValue)
|
||||
) {
|
||||
result[key] = mergeDeep(targetValue, sourceValue);
|
||||
} else if (sourceValue !== undefined) {
|
||||
result[key] = sourceValue;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
let privateConfig = {};
|
||||
try {
|
||||
// eslint-disable-next-line global-require, import/no-unresolved
|
||||
privateConfig = require('./config.private.js');
|
||||
} catch (error) {
|
||||
const message = error && error.message ? String(error.message) : '';
|
||||
const isMissing = error?.code === 'MODULE_NOT_FOUND' || message.includes('not defined');
|
||||
if (!isMissing) {
|
||||
console.warn('加载私有配置失败:', error);
|
||||
} else if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'development') {
|
||||
console.info('未检测到 config.private.js,使用默认配置');
|
||||
}
|
||||
}
|
||||
|
||||
const baseConfig = {
|
||||
// 应用信息
|
||||
appName: "FindMe",
|
||||
appVersion: "1.0.30", // 全局版本检查功能版本
|
||||
// API配置 - 使用正确的域名
|
||||
api: {
|
||||
baseUrl: "https://api.faxianwo.me",
|
||||
baseAmapUrl: "https://restapi.amap.com/v3",
|
||||
timeout: 15000
|
||||
},
|
||||
|
||||
// scana配置
|
||||
scana: {
|
||||
textBusinessId: '2084889082944577806',
|
||||
imageBusinessId: '2084888991278039310',
|
||||
appId: '68ce8147f243650001486434',
|
||||
secretKey: 'd5b263da-99b8-11f0-97a4-fa163e917534'
|
||||
},
|
||||
|
||||
// WebSocket配置 - 使用正确的WebSocket路径
|
||||
websocket: {
|
||||
baseUrl: "wss://api.faxianwo.me",
|
||||
url: "wss://api.faxianwo.me/api/v1/ws", // 🔥 恢复正确的WebSocket路径
|
||||
heartbeatInterval: 30000, // 30秒
|
||||
reconnectBaseDelay: 2000, // 重连基础延迟
|
||||
maxReconnectAttempts: 10 // 最大重连次数
|
||||
},
|
||||
|
||||
// 高德地图配置
|
||||
// amapKey: '55e1d9a4c6c0f9fa4c75656fe3060641',
|
||||
// amapKey: '9212b693317725fca66ae697ec444fd5',
|
||||
amapKey: '9da0f2a1538b5857dba24ac9aa9f30cc',
|
||||
// amapKey: '97320669caa10f1f8168b0f1aecc2c08',
|
||||
|
||||
// NIM配置
|
||||
nim: {
|
||||
appkey: '008ea0d044bd46e5b2281cb5fc3f627b',
|
||||
apiVersion: 'v2',
|
||||
enableV2CloudConversation: true
|
||||
},
|
||||
|
||||
// 腾讯云COS配置 - 用于文件存储
|
||||
cos: {
|
||||
// 存储桶配置
|
||||
bucket: 'findme-1375214531',
|
||||
region: 'ap-guangzhou',
|
||||
// 临时密钥服务地址 (后端提供)
|
||||
stsUrl: '/api/v1/cos/sts',
|
||||
// 文件检查接口 (检查文件是否已存在)
|
||||
fileCheckUrl: '/api/v1/cos/file/check',
|
||||
// 文件记录保存接口
|
||||
fileRecordUrl: '/api/v1/cos/file/record',
|
||||
// 是否使用全球加速
|
||||
useAccelerate: false,
|
||||
// 分块上传配置
|
||||
sliceSize: 1024 * 1024 * 5, // 5MB 大于此大小使用分块上传
|
||||
chunkSize: 1024 * 1024 * 1, // 1MB 分块大小
|
||||
// 文件路径格式: user/{userId}/{fileType}/{timestamp}_{fileName}
|
||||
pathFormat: 'user/{userId}/{fileType}/{timestamp}_{fileName}'
|
||||
},
|
||||
|
||||
// 网络超时设置 (保持向后兼容)
|
||||
networkTimeout: {
|
||||
request: 15000,
|
||||
upload: 60000,
|
||||
download: 60000
|
||||
},
|
||||
|
||||
// 位置刷新间隔
|
||||
locationUpdateInterval: 30000, // 30秒
|
||||
|
||||
// 文件大小限制 (小程序有自己的限制,这里作为前端验证)
|
||||
fileSizeLimit: {
|
||||
image: 10 * 1024 * 1024, // 10MB
|
||||
video: 50 * 1024 * 1024, // 50MB
|
||||
audio: 5 * 1024 * 1024, // 5MB
|
||||
file: 50 * 1024 * 1024 // 50MB
|
||||
},
|
||||
|
||||
// 支持的文件类型
|
||||
supportedFileTypes: {
|
||||
image: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'],
|
||||
video: ['mp4', 'avi', 'wmv', 'mov', '3gp'],
|
||||
audio: ['mp3', 'wav', 'ogg', 'aac', 'm4a']
|
||||
},
|
||||
|
||||
// 缓存设置
|
||||
cache: {
|
||||
maxAge: 7 * 24 * 60 * 60, // 7天
|
||||
resourceExpireTime: {
|
||||
image: 4 * 3600, // 4小时
|
||||
video: 2 * 3600, // 2小时
|
||||
audio: 2 * 3600, // 2小时
|
||||
file: 1 * 3600, // 1小时
|
||||
avatar: 24 * 3600 // 24小时
|
||||
},
|
||||
// 版本管理配置
|
||||
versionManagement: {
|
||||
enableAutoClear: true, // 版本更新时自动清理缓存
|
||||
periodicClearInterval: 7 * 24 * 60 * 60 * 1000, // 7天定期清理
|
||||
showUpdateNotification: true // 显示更新通知
|
||||
}
|
||||
},
|
||||
|
||||
// API端点路径
|
||||
apiPaths: {
|
||||
// 身份验证相关(统一使用user路径)
|
||||
auth: {
|
||||
sendVerifyCode: '/api/v1/user/send-verify-code',
|
||||
login: '/api/v1/user/login',
|
||||
wechatLogin: '/api/v1/user/wechat-login',
|
||||
refresh: '/api/v1/auth/refresh',
|
||||
logout: '/api/v1/user/logout'
|
||||
},
|
||||
// 用户相关
|
||||
user: {
|
||||
profile: '/api/v1/user/profile',
|
||||
updateProfile: '/api/v1/user/profile',
|
||||
setting: '/api/v1/user/setting',
|
||||
bindPhone: '/api/v1/user/bind-phone',
|
||||
detectMerge: '/api/v1/user/detect-merge',
|
||||
mergeAccount: '/api/v1/user/merge-account'
|
||||
},
|
||||
|
||||
// 位置相关
|
||||
location: {
|
||||
update: '/api/v1/location/update',
|
||||
getFriends: '/api/v1/location/friends',
|
||||
getStrangers: '/api/v1/location/strangers',
|
||||
privacy: '/api/v1/location/privacy'
|
||||
},
|
||||
|
||||
// 聊天相关 - 根据接口文档修正API路径
|
||||
chat: {
|
||||
conversations: '/api/v1/chat/conversations',
|
||||
history: '/api/v1/chat/history', // 修正:使用正确的历史消息接口
|
||||
messages: '/api/v1/chat/history', // 保持向后兼容
|
||||
send: '/api/v1/chat/send',
|
||||
batchRead: '/api/v1/chat/batch-read', // 新增:批量已读
|
||||
markAllRead: '/api/v1/chat/mark-all-read', // 新增:全部已读
|
||||
unreadTotal: '/api/v1/chat/unread/total', // 新增:总未读数
|
||||
settings: '/api/v1/chat/settings', // 新增:聊天设置
|
||||
backup: '/api/v1/chat/backup', // 新增:备份
|
||||
restore: '/api/v1/chat/restore', // 新增:恢复
|
||||
danmaku: '/api/v1/chat/danmaku', // 新增:弹幕
|
||||
emoji: '/api/v1/chat/emoji/packages', // 新增:表情包
|
||||
syncPull: '/api/v1/chat/sync/pull', // 新增:同步拉取
|
||||
syncAck: '/api/v1/chat/sync/ack', // 新增:同步确认
|
||||
upload: '/api/v1/file/upload'
|
||||
},
|
||||
|
||||
// 社交相关
|
||||
social: {
|
||||
friends: '/api/v1/social/friends',
|
||||
addFriend: '/api/v1/social/friend/add',
|
||||
acceptFriend: '/api/v1/social/friend/handle-request',
|
||||
deleteFriend: '/api/v1/social/friend',
|
||||
search: '/api/v1/social/search',
|
||||
friendRequests: '/api/v1/social/friend/requests',
|
||||
friendRequestsCount: '/api/v1/social/friend/requests/count',
|
||||
groups: '/api/v1/social/groups',
|
||||
groupsCount: '/api/v1/social/groups/count'
|
||||
},
|
||||
|
||||
|
||||
},
|
||||
|
||||
// 小程序特有配置
|
||||
miniprogram: {
|
||||
// 页面路径
|
||||
pages: {
|
||||
splash: '/pages/splash/splash',
|
||||
login: '/pages/login/login',
|
||||
main: '/pages/main/main',
|
||||
map: '/pages/map/map',
|
||||
friends: '/pages/social/friends/friends',
|
||||
message: '/pages/message/message',
|
||||
profile: '/subpackages/profile/profile/profile'
|
||||
},
|
||||
|
||||
// 主题配置
|
||||
theme: {
|
||||
primaryColor: '#3cc51f',
|
||||
backgroundColor: '#F5F5F5',
|
||||
textColor: '#333333',
|
||||
subTextColor: '#999999'
|
||||
},
|
||||
|
||||
// 地图配置
|
||||
map: {
|
||||
defaultZoom: 16,
|
||||
minZoom: 3,
|
||||
maxZoom: 20,
|
||||
showLocation: true,
|
||||
showScale: true,
|
||||
showCompass: true
|
||||
}
|
||||
},
|
||||
|
||||
// 调试配置
|
||||
debug: {
|
||||
enabled: true,
|
||||
showLog: true,
|
||||
mockData: false
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = mergeDeep(baseConfig, privateConfig);
|
||||
22
config/config.private.example.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* 私有配置示例文件
|
||||
* 复制为 config.private.js 并填入真实密钥,仅在本地开发或安全的CI环境中使用。
|
||||
*/
|
||||
module.exports = {
|
||||
scana: {
|
||||
textBusinessId: '',
|
||||
imageBusinessId: '',
|
||||
appId: '',
|
||||
secretKey: ''
|
||||
},
|
||||
amapKey: '',
|
||||
|
||||
// 腾讯云COS配置 - 已配置正式环境
|
||||
cos: {
|
||||
bucket: 'findme-1375214531', // 存储桶名称
|
||||
region: 'ap-guangzhou', // 地域
|
||||
stsUrl: '/api/v1/cos/sts', // 临时密钥接口
|
||||
fileCheckUrl: '/api/v1/cos/file/check', // 文件去重检查接口
|
||||
fileRecordUrl: '/api/v1/cos/file/record' // 文件记录保存接口
|
||||
}
|
||||
};
|
||||
352
custom-tab-bar/index.js
Normal file
|
|
@ -0,0 +1,352 @@
|
|||
const config = require('../config/config.js');
|
||||
const apiClient = require('../utils/api-client.js');
|
||||
|
||||
Component({
|
||||
data: {
|
||||
selected: 0,
|
||||
color: '#999999',
|
||||
selectedColor: '#ffffff',
|
||||
showCameraAction: false, // 拍照弹窗显示状态
|
||||
cameraActive: false, // 发布按钮激活状态
|
||||
list: [
|
||||
{
|
||||
pagePath: '/pages/map/map',
|
||||
text: '发现',
|
||||
iconPath: '/images/index/location.png',
|
||||
selectedIconPath: '/images/index/location-active.png'
|
||||
},
|
||||
{
|
||||
pagePath: '/pages/circle/circle',
|
||||
text: '圈子',
|
||||
iconPath: '/images/index/circle.png',
|
||||
selectedIconPath: '/images/index/circle-active.png'
|
||||
},
|
||||
{
|
||||
pagePath: '/subpackages/media/camera/camera',
|
||||
iconPath: '/images/index/phone.png',
|
||||
text: '发布',
|
||||
selectedIconPath: '/images/index/phone-active.png'
|
||||
},
|
||||
|
||||
{
|
||||
pagePath: '/pages/message/message',
|
||||
text: '聊天',
|
||||
iconPath: '/images/index/message.png',
|
||||
selectedIconPath: '/images/index/message-active.png'
|
||||
},
|
||||
{
|
||||
pagePath: '/pages/social/friends/friends',
|
||||
text: '好友',
|
||||
iconPath: '/images/index/friend.png',
|
||||
selectedIconPath: '/images/index/friend-active.png'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage(options) {
|
||||
// options.from 可以是 'button' 或 'menu'
|
||||
// 'button' 表示通过页面内的分享按钮触发
|
||||
// 'menu' 表示通过右上角菜单的分享按钮触发
|
||||
|
||||
return {
|
||||
title: 'Find Me', // 分享标题
|
||||
path: '/custom-tab-bar/index/index', // 分享路径,必须是以 / 开头的完整路径
|
||||
imageUrl: '/images/findme-logo.png', // 分享图标,可以是本地图片或网络图片
|
||||
success(res) {
|
||||
// 分享成功后的回调
|
||||
console.log('分享成功', res);
|
||||
// 可以在这里添加统计代码等
|
||||
},
|
||||
fail(res) {
|
||||
// 分享失败后的回调
|
||||
console.log('分享失败', res);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
attached() {
|
||||
// 已通过CSS调整适配所有设备
|
||||
try {
|
||||
const counts = wx.getStorageSync('unreadCounts') || {};
|
||||
const friends = Number(counts.friends || 0);
|
||||
const messages = Number(counts.messages || 0);
|
||||
if (friends || messages) {
|
||||
this.setData({
|
||||
friendsBadge: friends,
|
||||
messagesBadge: messages
|
||||
});
|
||||
}
|
||||
} catch (_) {}
|
||||
},
|
||||
methods: {
|
||||
// 对外暴露:设置好友请求角标数量
|
||||
setFriendsBadge(count) {
|
||||
const n = Number(count) || 0;
|
||||
if (this.data.friendsBadge === n) return;
|
||||
this.setData({ friendsBadge: n < 0 ? 0 : n });
|
||||
},
|
||||
// 对外暴露:设置消息未读角标数量
|
||||
setMessagesBadge(count) {
|
||||
const n = Number(count) || 0;
|
||||
if (this.data.messagesBadge === n) return;
|
||||
this.setData({ messagesBadge: n < 0 ? 0 : n });
|
||||
},
|
||||
// 清除所有角标
|
||||
clearBadges() {
|
||||
this.setData({ friendsBadge: 0, messagesBadge: 0 });
|
||||
},
|
||||
switchTab(e) {
|
||||
const data = e.currentTarget.dataset;
|
||||
const url = data.path;
|
||||
const index = data.index;
|
||||
|
||||
// 获取当前页面信息
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
|
||||
// 检查是否是"发布"按钮(索引2)- 直接跳转到编辑页面
|
||||
if (index === 2) {
|
||||
// 切换发布按钮激活状态
|
||||
this.setData({ cameraActive: !this.data.cameraActive });
|
||||
// 检查登录状态
|
||||
const app = getApp();
|
||||
if (!app.globalData.isLoggedIn) {
|
||||
wx.navigateTo({
|
||||
url: '/pages/login/login?from=camera'
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 已登录时直接跳转到编辑页面
|
||||
wx.navigateTo({
|
||||
url: '/subpackages/media/edits/edits',
|
||||
fail: (err) => {
|
||||
console.error('跳转编辑页面失败:', err);
|
||||
wx.showToast({
|
||||
title: '跳转失败,请重试',
|
||||
icon: 'none'
|
||||
});
|
||||
// 跳转失败时重置激活状态
|
||||
this.setData({ cameraActive: false });
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 切换到其他按钮时,取消发布按钮激活状态
|
||||
if (this.data.cameraActive) {
|
||||
this.setData({ cameraActive: false });
|
||||
}
|
||||
|
||||
// 先更新选中状态(其他 tabBar 页面)
|
||||
this.setData({
|
||||
selected: index
|
||||
});
|
||||
|
||||
// 检查是否是"发现"按钮(索引0)
|
||||
if (index === 0) {
|
||||
// 检查当前是否已经在地图页面
|
||||
if (currentPage.route === 'pages/map/map') {
|
||||
// 如果已经在地图页面,直接打开地点收藏栏
|
||||
currentPage.onOpenLocationFavoriteBar();
|
||||
} else {
|
||||
const app = getApp();
|
||||
// 如果不在地图页面,先跳转到地图页面,然后通过全局变量标记需要打开收藏栏
|
||||
wx.switchTab({
|
||||
url,
|
||||
success: () => {
|
||||
// 设置全局变量,让地图页面加载后知道需要打开收藏栏
|
||||
app.globalData.needOpenLocationFavoriteBar = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否是"圈子"按钮(索引1)
|
||||
if (index === 1) {
|
||||
// 检查登录状态
|
||||
const app = getApp();
|
||||
if (!app.globalData.isLoggedIn) {
|
||||
wx.navigateTo({
|
||||
url: '/pages/login/login?from=circle'
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 正常跳转到圈子页面
|
||||
wx.switchTab({
|
||||
url
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否是"聊天"按钮(索引3)
|
||||
if (index === 3) {
|
||||
// 检查登录状态
|
||||
const app = getApp();
|
||||
if (!app.globalData.isLoggedIn) {
|
||||
wx.navigateTo({
|
||||
url: '/pages/login/login?from=message'
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 已登录时正常跳转
|
||||
wx.switchTab({
|
||||
url
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否是"我的"按钮(索引4)- 跳转到好友列表页面
|
||||
if (index === 4) {
|
||||
// 检查登录状态
|
||||
const app = getApp();
|
||||
if (!app.globalData.isLoggedIn) {
|
||||
wx.navigateTo({
|
||||
url: '/pages/login/login?from=friends'
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 已登录时跳转到好友列表页面(使用 switchTab,因为它在 tabBar 中)
|
||||
wx.switchTab({
|
||||
url: '/pages/social/friends/friends'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 其他按钮正常跳转
|
||||
wx.switchTab({
|
||||
url
|
||||
});
|
||||
},
|
||||
|
||||
// 显示拍照弹窗
|
||||
showCameraActionSheet() {
|
||||
this.setData({ showCameraAction: true });
|
||||
},
|
||||
|
||||
// 隐藏拍照弹窗
|
||||
hideCameraActionSheet() {
|
||||
this.setData({ showCameraAction: false, cameraActive: false });
|
||||
},
|
||||
|
||||
// 拍照
|
||||
takePhoto() {
|
||||
this.hideCameraActionSheet();
|
||||
wx.navigateTo({
|
||||
url: '/subpackages/media/camera/camera',
|
||||
fail: (err) => {
|
||||
console.error('跳转拍照页面失败:', err);
|
||||
wx.showToast({
|
||||
title: '跳转失败,请重试',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 从相册选择
|
||||
chooseImage() {
|
||||
this.hideCameraActionSheet();
|
||||
wx.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['original', 'compressed'],
|
||||
sourceType: ['album'],
|
||||
success: (res) => {
|
||||
const tempFilePath = res.tempFilePaths[0];
|
||||
wx.showLoading({ title: '上传中...', mask: true });
|
||||
|
||||
// 上传图片
|
||||
this.uploadImage(tempFilePath)
|
||||
.then(imageUrl => {
|
||||
wx.hideLoading();
|
||||
if (imageUrl) {
|
||||
wx.navigateTo({
|
||||
url: `/subpackages/media/edits/edits?imagePath=${encodeURIComponent(imageUrl)}`,
|
||||
fail: (err) => {
|
||||
console.error('跳转编辑页失败:', err);
|
||||
wx.showToast({
|
||||
title: '跳转失败,请重试',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
wx.hideLoading();
|
||||
console.error('上传失败:', error);
|
||||
wx.showToast({
|
||||
title: error.message || '上传失败,请重试',
|
||||
icon: 'none'
|
||||
});
|
||||
});
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('选择图片失败:', error);
|
||||
if (error.errMsg && !error.errMsg.includes('cancel')) {
|
||||
wx.showToast({
|
||||
title: '选择图片失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 上传图片
|
||||
uploadImage(tempFilePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.uploadFile({
|
||||
url: `${config.api.baseUrl}/api/v1/file/upload`,
|
||||
filePath: tempFilePath,
|
||||
name: 'file',
|
||||
formData: {
|
||||
file_type: 'image',
|
||||
usage_type: 'feed'
|
||||
},
|
||||
header: {
|
||||
'Authorization': `Bearer ${apiClient.getToken()}`
|
||||
},
|
||||
success: (uploadRes) => {
|
||||
if (apiClient.is401Error(uploadRes)) {
|
||||
const app = getApp();
|
||||
const isLoggedIn = app?.globalData?.isLoggedIn || false;
|
||||
apiClient.handle401Error(isLoggedIn);
|
||||
if (isLoggedIn) {
|
||||
reject(new Error('登录已过期,请重新登录'));
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(uploadRes.data);
|
||||
if (data.code === 0) {
|
||||
const fileData = data?.data?.data || data?.data || {};
|
||||
const imageUrl = fileData.file_url || fileData.fileUrl || fileData.url;
|
||||
if (imageUrl) {
|
||||
resolve(imageUrl);
|
||||
} else {
|
||||
reject(new Error('上传成功但未获取到图片URL'));
|
||||
}
|
||||
} else {
|
||||
reject(new Error(data.message || '上传失败'));
|
||||
}
|
||||
} catch (error) {
|
||||
reject(new Error('响应解析失败'));
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('上传失败:', error);
|
||||
reject(new Error(error.errMsg || '上传失败'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
3
custom-tab-bar/index.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"component": true
|
||||
}
|
||||
33
custom-tab-bar/index.wxml
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<!--custom-tab-bar/index.wxml-->
|
||||
<view class="tab-bar-glass tab-bar">
|
||||
<view wx:for="{{list}}" wx:key="index"
|
||||
class="tab-bar-item {{selected === index ? 'tab-active' : ''}} {{index === 2 ? 'camera-tab-item' : ''}}"
|
||||
data-path="{{item.pagePath}}"
|
||||
data-index="{{index}}"
|
||||
bindtap="switchTab">
|
||||
<!-- 拍照按钮(索引2)根据激活状态显示图标,其他按钮根据选中状态显示 -->
|
||||
<image class="tab-bar-icon {{selected === index ? 'tab-icon-active' : ''}} {{index === 2 && cameraActive ? 'tab-icon-active camera-icon-large' : ''}} {{index === 2 && !cameraActive ? 'camera-icon-normal' : ''}}"
|
||||
src="{{index === 2 ? (cameraActive ? item.selectedIconPath : item.iconPath) : (selected === index ? item.selectedIconPath : item.iconPath)}}"
|
||||
mode="aspectFit" />
|
||||
<!-- 所有按钮未选中/未激活时显示文字 -->
|
||||
<text wx:if="{{index === 2 ? !cameraActive : selected !== index}}" class="tab-bar-text" style="color: {{selected === index ? selectedColor : color}};">{{item.text}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 拍照功能弹窗 -->
|
||||
<view class="camera-action-sheet {{showCameraAction ? 'show' : ''}}">
|
||||
<view class="action-sheet-mask" bindtap="hideCameraActionSheet"></view>
|
||||
<view class="action-sheet-content">
|
||||
<view class="action-sheet-item primary" bindtap="takePhoto">
|
||||
<text class="action-text">拍照</text>
|
||||
</view>
|
||||
<view class="action-sheet-divider-thin"></view>
|
||||
<view class="action-sheet-item primary" bindtap="chooseImage">
|
||||
<text class="action-text">从相册选择</text>
|
||||
</view>
|
||||
<view class="action-sheet-divider"></view>
|
||||
<view class="action-sheet-item cancel" bindtap="hideCameraActionSheet">
|
||||
<text class="action-text">取消</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
163
custom-tab-bar/index.wxss
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
/* custom-tab-bar/index.wxss - 自定义底部导航栏样式文件 */
|
||||
|
||||
.tab-bar {
|
||||
position: fixed; /* 固定定位,使导航栏始终显示在屏幕底部 */
|
||||
bottom: 40rpx; /* 距离屏幕底部20px -> 40rpx */
|
||||
left: 100rpx; /* 距离屏幕左侧50px -> 100rpx */
|
||||
right: 100rpx; /* 距离屏幕右侧50px -> 100rpx */
|
||||
min-height: 200rpx; /* 导航栏最小高度(用户希望保持较大值) */
|
||||
height: auto; /* 高度自适应内容 */
|
||||
display: flex; /* 使用flex布局 */
|
||||
justify-content: space-around; /* 水平均匀分布子元素 */
|
||||
align-items: center; /* 垂直居中对齐子元素 */
|
||||
padding: 20rpx; /* 内边距10px -> 20rpx */
|
||||
box-sizing: border-box; /* 盒模型为border-box,宽高包含内边距和边框 */
|
||||
z-index: 9999; /* 层级设为最高,确保在其他元素之上 */
|
||||
border-radius: 52rpx; /* 设置圆角,创建胶囊形状 26px -> 52rpx */
|
||||
}
|
||||
|
||||
/* 深灰色玻璃效果 - 继承自app.wxss并增强 */
|
||||
.tab-bar-glass {
|
||||
/* 基础玻璃效果 */
|
||||
background: rgba(30, 30, 30, 0.7); /* 设置半透明深灰色背景,透明度0.7 */
|
||||
backdrop-filter: blur(20rpx); /* 设置背景模糊效果,10px -> 20rpx */
|
||||
-webkit-backdrop-filter: blur(20rpx); /* WebKit内核浏览器兼容,设置背景模糊效果 */
|
||||
|
||||
/* 添加细微边框增强效果 */
|
||||
border-top: 2rpx solid rgba(255, 255, 255, 0.1); /* 顶部边框为1px -> 2rpx白色半透明线 */
|
||||
|
||||
/* 阴影效果增强层次感 */
|
||||
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.1); /* 设置底部阴影,增强悬浮感 2px -> 4rpx, 10px -> 20rpx */
|
||||
}
|
||||
|
||||
.tab-bar-item {
|
||||
flex: 1; /* 让每个按钮占据相同的宽度 */
|
||||
display: flex; /* 使用flex布局 */
|
||||
flex-direction: column; /* 垂直方向排列子元素(图标在上,文字在下) */
|
||||
align-items: center; /* 水平居中对齐子元素 */
|
||||
justify-content: center; /* 垂直居中对齐子元素 */
|
||||
height: 100%; /* 高度充满父容器 */
|
||||
box-sizing: border-box; /* 盒模型为border-box */
|
||||
}
|
||||
|
||||
.tab-bar-icon {
|
||||
width: 55rpx; /* 图标宽度 */
|
||||
height: 55rpx; /* 图标高度 */
|
||||
margin-bottom: 8rpx; /* 与下方文字的间距 */
|
||||
display: block; /* 显示为块级元素 */
|
||||
transition: transform 0.3s ease, width 0.3s ease, height 0.3s ease; /* 添加过渡效果 */
|
||||
}
|
||||
|
||||
/* 🔥 所有tab选中时图标放大 */
|
||||
.tab-bar-icon.tab-icon-active {
|
||||
width: 80rpx; /* 选中时图标宽度增大 */
|
||||
height: 80rpx; /* 选中时图标高度增大 */
|
||||
margin-bottom: 0; /* 选中时没有文字,不需要间距 */
|
||||
}
|
||||
|
||||
/* 拍照按钮(索引2)未激活时:正常大小图标,显示文字 */
|
||||
.camera-icon-normal {
|
||||
width: 55rpx !important; /* 正常大小 */
|
||||
height: 55rpx !important; /* 正常大小 */
|
||||
margin-bottom: 8rpx !important; /* 有文字,需要间距 */
|
||||
}
|
||||
|
||||
/* 拍照按钮(索引2)激活时:大图标,不显示文字 */
|
||||
.camera-icon-large {
|
||||
width: 80rpx !important; /* 拍照按钮图标更大 */
|
||||
height: 80rpx !important; /* 拍照按钮图标更大 */
|
||||
margin-bottom: 0 !important; /* 没有文字,不需要间距 */
|
||||
}
|
||||
|
||||
.tab-bar-text {
|
||||
font-size: 24rpx; /* 文字大小12px -> 24rpx */
|
||||
line-height: 24rpx; /* 行高12px -> 24rpx,与字体大小一致确保单行显示 */
|
||||
}
|
||||
|
||||
/* 触摸反馈效果 */
|
||||
.tab-bar-item:active {
|
||||
opacity: 0.7; /* 触摸时透明度降低到0.7,提供视觉反馈 */
|
||||
}
|
||||
|
||||
/* 拍照功能弹窗样式 */
|
||||
.camera-action-sheet {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 10000;
|
||||
transform: none !important;
|
||||
-webkit-transform: none !important;
|
||||
}
|
||||
|
||||
.camera-action-sheet.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.action-sheet-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.action-sheet-content {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 2;
|
||||
background: #222;
|
||||
border-radius: 16px 16px 0 0;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.action-sheet-item {
|
||||
padding: 20px 0;
|
||||
text-align: center;
|
||||
background: #222;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.action-sheet-item::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.action-sheet-item.primary {
|
||||
background: #222;
|
||||
}
|
||||
|
||||
.action-sheet-item.cancel {
|
||||
background: #222;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.action-sheet-item.cancel::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.action-sheet-divider {
|
||||
height: 8px;
|
||||
background-color: #111;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.action-sheet-divider-thin {
|
||||
height: 2px;
|
||||
background-color: #111;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
}
|
||||
1
dist/cos-wx-sdk-v5.min.js
vendored
Normal file
2
dist/nim.js
vendored
Normal file
1
dist/nim.js.LICENSE.txt
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
|
||||
1
dist/spark-md5.min.js
vendored
Normal file
1
images/AddFriend.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="10" fill="none" viewBox="0 0 14 10"><path fill="#f4f6fa" d="M13.75 5.901a.45.45 0 0 1-.45.45h-.9v.9a.45.45 0 1 1-.9 0v-.9h-.9a.45.45 0 1 1 0-.9h.9v-.9a.45.45 0 1 1 .9 0v.9h.9a.45.45 0 0 1 .45.45m-3.255 3.31a.45.45 0 1 1-.69.58C8.673 8.443 7.117 7.7 5.425 7.7c-1.693 0-3.249.742-4.38 2.09a.45.45 0 1 1-.69-.58c.841-1 1.886-1.71 3.048-2.09a3.825 3.825 0 1 1 4.044 0c1.162.38 2.207 1.09 3.048 2.09M5.425 6.8a2.925 2.925 0 1 0 0-5.85 2.925 2.925 0 0 0 0 5.85"/></svg>
|
||||
|
After Width: | Height: | Size: 522 B |
1
images/Album.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761916771951" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15628" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M0 85.344v853.344h1024V85.344H0z m938.656 85.312v452.256l-128-128L640 665.568l-298.656-298.656-256 256V170.656h853.344zM85.344 853.344V742.4l256-256L640 785.056l170.656-170.656 128 128v110.944H85.312z" p-id="15629" fill="#ffffff"></path><path d="M853.344 341.344a85.344 85.344 0 1 1-170.688 0 85.344 85.344 0 0 1 170.688 0z" p-id="15630" fill="#ffffff"></path></svg>
|
||||
|
After Width: | Height: | Size: 700 B |
BIN
images/Edit3.png
Normal file
|
After Width: | Height: | Size: 201 B |
BIN
images/Findme avatar.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
images/Icon.png
Normal file
|
After Width: | Height: | Size: 421 B |
1
images/Image.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 21h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2m0 0 11-11 5 5M10 8.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0"/></svg>
|
||||
|
After Width: | Height: | Size: 321 B |
BIN
images/Search.png
Normal file
|
After Width: | Height: | Size: 377 B |
1
images/Selected.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><rect width="22" height="22" x="1" y="1" stroke="#f4f0eb" stroke-width="2" rx="5"/><rect width="12" height="12" x="6" y="6" fill="#4dd1a1" rx="2"/></svg>
|
||||
|
After Width: | Height: | Size: 248 B |
3
images/StartGroupChat.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.59615 5.95932C9.59615 6.09704 9.54145 6.22912 9.44408 6.3265C9.3467 6.42389 9.21463 6.4786 9.07692 6.4786H4.92308C4.78537 6.4786 4.6533 6.42389 4.55593 6.3265C4.45855 6.22912 4.40385 6.09704 4.40385 5.95932C4.40385 5.8216 4.45855 5.68951 4.55593 5.59213C4.6533 5.49475 4.78537 5.44004 4.92308 5.44004H9.07692C9.21463 5.44004 9.3467 5.49475 9.44408 5.59213C9.54145 5.68951 9.59615 5.8216 9.59615 5.95932ZM9.07692 7.51716H4.92308C4.78537 7.51716 4.6533 7.57187 4.55593 7.66925C4.45855 7.76664 4.40385 7.89872 4.40385 8.03644C4.40385 8.17416 4.45855 8.30624 4.55593 8.40363C4.6533 8.50101 4.78537 8.55572 4.92308 8.55572H9.07692C9.21463 8.55572 9.3467 8.50101 9.44408 8.40363C9.54145 8.30624 9.59615 8.17416 9.59615 8.03644C9.59615 7.89872 9.54145 7.76664 9.44408 7.66925C9.3467 7.57187 9.21463 7.51716 9.07692 7.51716ZM13.75 6.99788C13.7503 8.16336 13.4488 9.30905 12.8749 10.3234C12.3011 11.3378 11.4744 12.1864 10.4754 12.7864C9.47638 13.3865 8.33902 13.7177 7.17403 13.7478C6.00905 13.7778 4.85613 13.5057 3.8275 12.9579L1.61752 13.6946C1.43455 13.7557 1.2382 13.7645 1.05048 13.7202C0.862754 13.6759 0.691078 13.5802 0.554692 13.4438C0.418305 13.3074 0.322598 13.1357 0.278296 12.948C0.233995 12.7602 0.24285 12.5639 0.30387 12.3809L1.04053 10.1707C0.559039 9.26534 0.290109 8.26219 0.254152 7.23738C0.218196 6.21257 0.416157 5.19303 0.833011 4.25616C1.24987 3.31929 1.87466 2.48971 2.65996 1.83038C3.44526 1.17106 4.37043 0.699315 5.36525 0.450968C6.36006 0.202621 7.39838 0.184194 8.40138 0.397088C9.40438 0.609981 10.3457 1.0486 11.1539 1.67965C11.9621 2.3107 12.6159 3.11759 13.0657 4.03908C13.5156 4.96057 13.7496 5.97244 13.75 6.99788ZM12.7115 6.99788C12.7113 6.12167 12.5095 5.25727 12.1218 4.47154C11.734 3.68582 11.1707 2.99983 10.4755 2.46666C9.78023 1.93348 8.97165 1.56742 8.11231 1.39678C7.25296 1.22614 6.36588 1.2555 5.51969 1.4826C4.67351 1.70969 3.89091 2.12843 3.23243 2.70641C2.57396 3.28439 2.05726 4.00612 1.72232 4.81577C1.38738 5.62542 1.24317 6.50128 1.30085 7.37558C1.35854 8.24989 1.61657 9.0992 2.05498 9.85782C2.09177 9.9215 2.11462 9.99228 2.122 10.0655C2.12939 10.1387 2.12114 10.2126 2.09781 10.2823L1.28846 12.71L3.71587 11.9005C3.76874 11.8825 3.82421 11.8733 3.88007 11.8733C3.97126 11.8734 4.06081 11.8976 4.13969 11.9434C5.00797 12.4458 5.99325 12.7106 6.99639 12.7113C7.99953 12.7119 8.98515 12.4483 9.85406 11.947C10.723 11.4457 11.4446 10.7243 11.9462 9.85552C12.4478 8.98672 12.7118 8.00112 12.7115 6.99788Z" fill="#F4F6FA"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
BIN
images/Subtract.png
Normal file
|
After Width: | Height: | Size: 432 B |
3
images/User.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 21V19C20 17.9391 19.5786 16.9217 18.8284 16.1716C18.0783 15.4214 17.0609 15 16 15H8C6.93913 15 5.92172 15.4214 5.17157 16.1716C4.42143 16.9217 4 17.9391 4 19V21M16 7C16 9.20914 14.2091 11 12 11C9.79086 11 8 9.20914 8 7C8 4.79086 9.79086 3 12 3C14.2091 3 16 4.79086 16 7Z" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 471 B |
BIN
images/avatar.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
1
images/back_arrow.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="18" fill="none" viewBox="0 0 9 18"><path fill="#fff" fill-opacity=".9" fill-rule="evenodd" d="M9 16.438 7.955 17.5.29 9.71a1.02 1.02 0 0 1 0-1.42L7.955.5 9 1.563 1.682 9z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 257 B |
BIN
images/btn.png
Normal file
|
After Width: | Height: | Size: 281 B |
BIN
images/bus_stop.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
1
images/cam.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="none" viewBox="0 0 40 40"><g stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" clip-path="url(#a)"><path d="M31 27a2 2 0 0 1-2 2H11a2 2 0 0 1-2-2V16a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><path d="M20 25a4 4 0 1 0 0-8 4 4 0 0 0 0 8"/></g><defs><clipPath id="a"><path fill="#fff" d="M8 8h24v24H8z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 427 B |
BIN
images/comment.png
Normal file
|
After Width: | Height: | Size: 286 B |
1
images/cross.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="23" height="28" fill="none" viewBox="0 0 23 28"><path fill="#f8f8f8" d="M8.864 16.557 2.83 15.28 14.48 1.789l-1.831 8.73 6.033 1.278-11.65 13.491z"/><path stroke="#040404" stroke-width="1.25" d="m1 3 20.973 21.077"/><path stroke="#fffeff" d="m1 3 20.973 21.077"/></svg>
|
||||
|
After Width: | Height: | Size: 316 B |
BIN
images/default-avatar.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
1
images/default-avatar.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120"><circle cx="60" cy="60" r="60" fill="#f0f0f0"/><circle cx="60" cy="40" r="20" fill="#4a90e2"/><path fill="#4a90e2" d="M60 60c-20 0-30 20 0 40 30-20 20-40 0-40"/></svg>
|
||||
|
After Width: | Height: | Size: 254 B |
BIN
images/default-stranger.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
images/download.png
Normal file
|
After Width: | Height: | Size: 1 KiB |
1
images/download.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761881486657" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4669" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M960.64 499.2c-28.8-91.52-116.48-150.4-223.36-150.4h-16a345.216 345.216 0 0 0-283.52-224c-140.8-16.64-278.4 56.96-343.68 182.4a346.24 346.24 0 0 0 47.36 386.56c15.36 17.28 42.24 19.2 60.16 3.84 17.28-15.36 19.2-42.24 3.84-60.16a260.672 260.672 0 0 1-35.84-290.56c49.28-94.08 152.96-149.76 258.56-136.96a259.84 259.84 0 0 1 220.8 192.64c5.12 18.56 21.76 32 40.96 32h47.36c68.48 0 124.16 35.84 142.08 91.52 19.2 60.8-2.56 126.08-55.04 163.2a42.88 42.88 0 0 0-10.24 59.52 42.112 42.112 0 0 0 58.88 10.24c83.2-59.52 118.4-163.2 87.68-259.84z" fill="#ffffff" p-id="4670"></path><path d="M611.84 698.88l-56.96 56.96V490.88c0-23.68-19.2-42.24-42.88-42.24-23.68 0-42.24 19.2-42.24 42.88v264.96l-57.6-57.6a42.496 42.496 0 1 0-60.16 60.16l129.92 129.92c3.2 3.2 6.4 4.48 9.6 6.4 1.28 0.64 2.56 1.92 3.84 2.56 5.12 1.92 10.88 3.2 16.64 3.2 1.92 0 3.2-0.64 5.12-1.28 3.84-0.64 7.68-0.64 10.88-2.56 5.76-1.92 10.24-5.76 14.72-9.6l129.28-129.28c16.64-16.64 16.64-43.52 0-60.16s-43.52-16-60.16 0.64z" fill="#ffffff" p-id="4671"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
images/emoji/add-circle.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="33" fill="none" viewBox="0 0 32 33"><path stroke="#f4f6fa" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M10.667 16.5H16m5.333 0H16m0 0v-5.334m0 5.334v5.333m0 8c7.364 0 13.333-5.97 13.333-13.333S23.363 3.167 16 3.167 2.667 9.136 2.667 16.5 8.636 29.833 16 29.833"/></svg>
|
||||
|
After Width: | Height: | Size: 353 B |
1
images/emoji/f-message.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" fill="none" viewBox="0 0 17 17"><path fill="#f4f6fa" d="M8.417.5a8.01 8.01 0 0 0-8 8v6.72a1.28 1.28 0 0 0 1.28 1.28h6.72a8 8 0 0 0 0-16m0 14.72h-6.72V8.5a6.72 6.72 0 1 1 6.72 6.72m.96-6.4a.96.96 0 1 1-1.92 0 .96.96 0 0 1 1.92 0m-3.52 0a.96.96 0 1 1-1.92 0 .96.96 0 0 1 1.92 0m7.04 0a.96.96 0 1 1-1.92 0 .96.96 0 0 1 1.92 0"/></svg>
|
||||
|
After Width: | Height: | Size: 394 B |
1
images/emoji/f-more.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="4" fill="none" viewBox="0 0 22 4"><path fill="#b4bbc5" d="M12.25 2a1.25 1.25 0 1 0-2.5 0 1.25 1.25 0 0 0 2.5 0M3.5 2A1.25 1.25 0 1 0 1 2a1.25 1.25 0 0 0 2.5 0M21 2a1.25 1.25 0 1 0-2.5 0A1.25 1.25 0 0 0 21 2"/><path stroke="#b4bbc5" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12.25 2a1.25 1.25 0 1 0-2.5 0 1.25 1.25 0 0 0 2.5 0M3.5 2A1.25 1.25 0 1 0 1 2a1.25 1.25 0 0 0 2.5 0M21 2a1.25 1.25 0 1 0-2.5 0A1.25 1.25 0 0 0 21 2"/></svg>
|
||||
|
After Width: | Height: | Size: 517 B |
1
images/emoji/m-emoji.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="33" fill="none" viewBox="0 0 32 33"><path stroke="#f4f6fa" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.334 12.5h-2.667m-8 4c0 7.364 5.97 13.333 13.333 13.333s13.334-5.97 13.334-13.333S23.364 3.167 16 3.167 2.667 9.136 2.667 16.5"/><path stroke="#f4f6fa" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M22 19.834S20 22.5 16 22.5s-6-2.666-6-2.666"/><path fill="#f4f6fa" stroke="#f4f6fa" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20.667 12.5a.667.667 0 1 1 0-1.333.667.667 0 0 1 0 1.333"/></svg>
|
||||
|
After Width: | Height: | Size: 630 B |
1
images/emoji/r-Return.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="8" height="14" fill="none" viewBox="0 0 8 14"><path stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6-6 6"/></svg>
|
||||
|
After Width: | Height: | Size: 201 B |
1
images/emoji/s-input.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="27" fill="none" viewBox="0 0 28 27"><path fill="#f4f6fa" d="M14 .167c7.363 0 13.332 5.969 13.333 13.333S21.363 26.833 14 26.833.666 20.863.666 13.5 6.636.166 14 .166m0 1.6C7.52 1.766 2.265 7.02 2.265 13.5S7.52 25.233 14 25.233s11.733-5.253 11.733-11.734S20.48 1.766 13.999 1.766m1.543 4.19a10.63 10.63 0 0 1 3.124 7.542c0 2.946-1.194 5.613-3.125 7.544L14.41 19.91a9.03 9.03 0 0 0 2.656-6.412c0-2.438-.966-4.72-2.656-6.41zm-2.64 2.64a6.9 6.9 0 0 1 2.03 4.903 6.9 6.9 0 0 1-2.03 4.903L11.77 17.27a5.3 5.3 0 0 0 1.563-3.772 5.3 5.3 0 0 0-1.563-3.77zm-2.64 2.64c.578.579.937 1.379.937 2.262a3.2 3.2 0 0 1-.938 2.263L8 13.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 687 B |
1
images/error-icon.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1762182168950" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13566" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M806.2 281.8L281.9 806.2c-17.5 17.5-46 17.5-63.6 0-17.5-17.5-17.5-46 0-63.6l524.3-524.3c17.5-17.5 46-17.5 63.6 0s17.6 46 0 63.5z" fill="#333333" p-id="13567"></path><path d="M806.2 806.2c-17.5 17.5-46 17.5-63.6 0L218.3 281.8c-17.5-17.5-17.5-46 0-63.6s46-17.5 63.6 0l524.3 524.3c17.6 17.7 17.6 46.1 0 63.7z" fill="#333333" p-id="13568"></path></svg>
|
||||
|
After Width: | Height: | Size: 682 B |
BIN
images/findme-logo.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
1
images/flashlight.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761914055490" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10588" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M768 154.352941c14.546824 0 26.352941 11.806118 26.352941 26.352941v38.580706c0 116.736-54.964706 226.635294-148.359529 296.658824l7.80047-6.02353v373.519059a26.352941 26.352941 0 0 1-23.04 26.172235l-3.312941 0.180706h-245.940706a26.352941 26.352941 0 0 1-26.352941-26.352941l-0.030118-373.519059-0.602352-0.451764A370.838588 370.838588 0 0 1 214.738824 229.888l-0.150589-10.601412V180.705882c0-14.546824 11.806118-26.352941 26.352941-26.352941h527.058824zM504.470588 576.150588a26.352941 26.352941 0 0 0-26.142117 23.04l-0.210824 3.312941v70.264471a26.352941 26.352941 0 0 0 52.495059 3.312941l0.210823-3.312941v-70.264471a26.352941 26.352941 0 0 0-26.352941-26.352941z m231.30353-298.827294H273.167059c3.463529 18.040471 8.432941 35.659294 14.848 52.705882h432.911059c6.384941-17.046588 11.384471-34.665412 14.848-52.705882z" fill="#ffffff" p-id="10589"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
1
images/fram.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><rect width="22" height="22" x="1" y="1" stroke="#f4f0eb" stroke-width="2" rx="5"/></svg>
|
||||
|
After Width: | Height: | Size: 184 B |
BIN
images/friend-avatar.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
1
images/friend-avatar.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120"><defs><linearGradient id="a" x1="0%" x2="100%" y1="0%" y2="100%"><stop offset="0%" stop-color="#4a90e2"/><stop offset="100%" stop-color="#5ac8fa"/></linearGradient></defs><circle cx="60" cy="60" r="60" fill="url(#a)"/><circle cx="60" cy="45" r="18" fill="#fff" fill-opacity=".9"/><path fill="#fff" fill-opacity=".9" d="M60 63c-15 0-25 20 0 37 25-17 15-37 0-37"/></svg>
|
||||
|
After Width: | Height: | Size: 455 B |
1
images/group/Search-d.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12.7 19a7 7 0 1 0 0-14 7 7 0 0 0 0 14m9 2-3.5-3.5"/></svg>
|
||||
|
After Width: | Height: | Size: 243 B |
1
images/group/Search.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1762275203421" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8796" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M1010.68 946.39L777.85 713.56q45.17-56.76 71.24-127.42 26.06-70.66 26.06-148.28 0-91.51-33.59-170.28-34.75-79.92-94.41-139.58-59.66-59.66-139.58-94.41Q528.8 0 437.29 0 346.93 0 267 33.59 188.24 68.34 128.58 128q-59.66 59.66-94.41 139.58Q0.58 346.35 0.58 437.86q0 90.36 33.59 170.28 34.75 79.93 94.41 139.01 59.66 59.08 138.42 93.83 79.93 33.59 170.29 33.59 78.76 0 148.84-25.49 70.09-25.49 126.85-70.66l232.83 232.83q6.95 6.95 15.06 9.85 8.11 2.89 17.37 2.89 8.11 0 16.8-2.89 8.69-2.89 15.64-9.85 12.74-13.9 12.74-32.43 0-18.54-12.74-32.44zM576.29 765.68q-32.43 13.9-67.18 20.85-34.75 6.95-71.81 6.95-35.91 0-70.67-6.95-34.75-6.95-67.19-20.85-32.44-13.9-60.82-33.01-28.38-19.11-52.7-43.44-24.33-24.33-43.44-52.71-19.11-28.38-33.01-60.81-13.9-32.43-20.85-67.19-6.95-34.75-6.95-70.66 0-37.06 6.95-71.81t20.85-67.19q13.9-31.28 33.01-59.66 19.11-28.38 43.44-53.86 24.32-24.33 52.7-43.44 28.38-19.11 60.82-31.85 32.43-13.91 67.19-20.86 34.75-6.95 70.67-6.95 37.06 0 71.81 6.95t67.18 20.86q31.28 12.74 59.66 31.85 28.38 19.11 53.86 43.44 24.33 25.48 43.44 53.86t31.85 59.66q13.9 32.43 20.85 67.19 6.95 34.75 6.95 71.81 0 35.91-6.95 70.66t-20.85 67.19q-12.74 32.43-31.85 60.81-19.11 28.38-43.44 52.71-25.48 24.33-53.86 43.44t-59.66 33.01z" fill="#2c2c2c" p-id="8797"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
1
images/group/add.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1762278271448" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="49438" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M332.8 511.8976a25.6 25.6 0 0 1 25.6-25.6h128V358.4a25.6 25.6 0 0 1 51.2 0v128H665.6a25.6 25.6 0 1 1 0 51.2h-128V665.6a25.6 25.6 0 0 1-51.2 0v-128.0512L358.4 537.4976a25.6 25.6 0 0 1-25.6-25.6z" fill="#ffffff" p-id="49439"></path><path d="M938.6496 511.9488c0 212.0704-191.0272 384-426.7008 384a466.944 466.944 0 0 1-160.8192-28.2112l-200.192 53.4016a42.6496 42.6496 0 0 1-52.9408-49.2544l34.9696-183.3984a351.9488 351.9488 0 0 1-47.6672-176.5376c0-212.0704 191.0272-384 426.6496-384 235.6736 0 426.7008 171.9296 426.7008 384z m-751.872 167.424l-35.9424 188.7744 202.752-54.0672 15.1552 5.5808a415.744 415.744 0 0 0 143.2064 25.088c212.5824 0 375.5008-153.9584 375.5008-332.8s-162.9184-332.8-375.5008-332.8c-212.5312 0-375.4496 153.9584-375.4496 332.8 0 54.272 14.6432 105.472 40.8064 150.9376l9.472 16.4864z" fill="#ffffff" p-id="49440"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
1
images/group/edit.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1762407653901" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4625" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M57.6 748.8v172.8h172.8l499.2-505.6-172.8-172.8-499.2 505.6z m806.4-467.2c19.2-19.2 19.2-44.8 0-64l-102.4-102.4c-19.2-19.2-44.8-19.2-64 0L614.4 198.4l172.8 172.8 76.8-89.6zM467.2 832l-89.6 89.6h595.2V832H467.2z" p-id="4626" fill="#ffffff"></path></svg>
|
||||
|
After Width: | Height: | Size: 585 B |
1
images/group/groupChat.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1762279393022" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="58497" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M621.8 545.2c81.8-52.2 115.1-155.4 79.3-245.8S570.6 156.8 475.4 175.1c-95.2 18.3-164 101.9-163.9 199.2 0.1 68.8 34.6 133 92 170.8-114.6 45.6-190 156.7-190.1 280.4 0 14.9 12 27 26.9 27 14.9 0 26.9-12.1 26.9-27-3-89.7 43-173.9 120-219.7 77-45.7 172.7-45.7 249.7 0s123 130 120 219.7c0 14.9 12 27 26.9 27 14.9 0 26.9-12.1 26.9-27-0.3-123.2-74.9-234.1-188.9-280.3z m-109.1-20.8c-60.7 0.6-115.7-35.5-139.3-91.5-23.6-56-11.3-120.8 31.4-164.1 42.6-43.3 107.1-56.4 163.1-33.3 56.1 23.1 92.7 77.9 92.7 138.8 0.3 82.4-65.9 149.5-147.9 150.1z m0 0" p-id="58498" fill="#ffffff"></path><path d="M112.7 724.8c-13.6 0.1-25.1-10.6-26.4-24.6-7.1-89.6 29.6-160.3 98.8-191.1-36.2-32.3-53.6-81.7-46-130.5 14.8-80.7 89.8-134.1 168-119.6 6.7 1 12.7 4.6 16.9 10.1 5.8 8.9 6 20.4 0.6 29.5-5.4 9.1-15.4 14.1-25.7 12.8-50.1-10-98.6 23.6-108.6 75.4-6.3 51.3 28.4 98.3 77.9 105.7h4.8c6.9 0 13.6 2.9 18.5 7.9 4.9 5.1 7.6 12 7.7 19.1 0 7.2-2.8 14.2-7.7 19.3-5 5.1-11.7 8-18.7 8h-8.5c-36-0.8-70.7 13.6-96.2 39.9-24 30.6-34.5 70.2-29.1 109.2 0.5 7.1-1.6 14.2-6.1 19.7-4.6 5.4-11 8.8-18 9.3l-2.2-0.1zM911.1 724.8H909c-7-0.6-13.4-4-18-9.6-4.4-5.5-6.6-12.5-6.1-19.7 5.6-39-4.9-78.6-28.8-109.2-25.3-26.8-60.2-41.5-96.4-40.7h-8.5c-7 0-13.7-2.9-18.7-8s-7.7-12.1-7.7-19.3c0-7.2 2.8-14.2 7.7-19.3 5-5.1 11.7-8 18.7-8h5.8c48.9-8.1 82.9-54.6 77.1-105.4-10.1-51.6-58.5-85.2-108.6-75.4-14.2 2.1-27.4-7.8-29.9-22.4-1.1-7 0.6-14.1 4.5-19.9 4.5-5.2 10.7-8.4 17.4-9 78.5-14.8 153.9 38.8 168.6 119.9 7.4 49.1-10.5 98.7-47.3 130.8 69.2 30.8 105.7 101.6 98.8 191.1-1.6 13.7-12.8 24-26.2 24l-0.3 0.1z" p-id="58499" fill="#ffffff"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
1
images/group/menu.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1762367574366" class="icon" viewBox="0 0 1152 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5459" xmlns:xlink="http://www.w3.org/1999/xlink" width="225" height="200"><path d="M0 0h1152v128H0V0z m0 448h1152v128H0V448z m0 448h1152v128H0v-128z" fill="#ffffff" p-id="5460"></path></svg>
|
||||
|
After Width: | Height: | Size: 440 B |
BIN
images/group/testImg.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
images/heart-red.png
Normal file
|
After Width: | Height: | Size: 313 B |
BIN
images/heart-white.png
Normal file
|
After Width: | Height: | Size: 315 B |
BIN
images/img-agreement-check-next.png
Normal file
|
After Width: | Height: | Size: 247 B |
BIN
images/img-agreement-check.png
Normal file
|
After Width: | Height: | Size: 351 B |
BIN
images/index/circle-active.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
images/index/circle.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
images/index/friend-active.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
images/index/friend.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
images/index/location-active.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
images/index/location.png
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
images/index/message-active.png
Normal file
|
After Width: | Height: | Size: 3 KiB |
BIN
images/index/message.png
Normal file
|
After Width: | Height: | Size: 809 B |
BIN
images/index/phone-active.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
images/index/phone.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
3
images/lightning.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="17" height="25" viewBox="0 0 17 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.86369 15.5572L0.830754 14.2796L12.4808 0.788695L10.65 9.5199L16.6829 10.7975L5.03289 24.2885L6.86369 15.5572Z" fill="#F8F8F8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 243 B |
1
images/like-active.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1765472496140" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2061" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M508.765841 890.615107c-22.497387 0-239.805668-174.754093-322.129628-280.598746C130.011564 558.358841 84.231915 461.96442 84.231915 394.016889c0-144.30664 105.672738-261.651208 235.55997-261.651208 79.31851 0 146.614196 65.640999 188.169637 133.459594 41.529859-67.818595 108.825544-133.459594 188.16759-133.459594 129.888256 0 235.557924 117.344568 235.557924 261.651208 0 68.12354-46.020126 164.689876-102.881159 216.434377C745.698065 717.647707 532.525987 890.615107 508.765841 890.615107z" fill="#d81e06" p-id="2062"></path></svg>
|
||||
|
After Width: | Height: | Size: 867 B |
1
images/like.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1765472164121" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1904" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M510.340709 900.892171c-23.173792 0-247.254314-180.180685-332.157006-289.301965-58.338686-53.264111-105.562221-152.650679-105.562221-222.732827 0-148.765185 108.952434-269.77626 242.874563-269.77626 81.781608 0 151.191444 67.679425 194.013739 137.605008 42.822294-69.925582 112.207571-137.605008 194.013739-137.605008 133.922129 0 242.874563 121.011074 242.874563 269.77626 0 70.239737-47.445592 169.805384-106.055454 223.182059C754.630505 722.553437 534.862195 900.892171 510.340709 900.892171zM315.497069 165.0685c-108.570741 0-196.887182 100.397594-196.887182 223.788879 0 58.382689 42.059931 145.353482 91.864244 189.903118 1.033539 0.943488 2.022053 1.977028 2.874467 3.098571 83.937714 109.042485 251.138784 234.421031 297.17119 267.923052 48.054459-34.715663 213.05542-161.193239 294.611901-267.451308 0.899486-1.12359 1.888-2.178619 2.964518-3.142573 50.03251-44.663222 92.312452-131.813095 92.312452-190.33086 0-123.391285-88.316441-223.788879-196.885136-223.788879-70.487378 0-137.808646 75.135234-173.241646 149.597133-7.634888 16.033162-33.908274 16.033162-41.543162 0C453.303668 240.203734 385.983423 165.0685 315.497069 165.0685z" fill="#5D5D5D" p-id="1905"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
1
images/loca.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 20 20"><g stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" clip-path="url(#a)"><path d="M17.5 8.333c0 5.834-7.5 10.834-7.5 10.834s-7.5-5-7.5-10.834a7.5 7.5 0 0 1 15 0"/><path d="M10 10.833a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h20v20H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 432 B |
BIN
images/location.png
Normal file
|
After Width: | Height: | Size: 339 B |
BIN
images/login/findme.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
1
images/map/AddFriend.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="10" fill="none" viewBox="0 0 14 10"><path fill="#f4f6fa" d="M13.75 5.901a.45.45 0 0 1-.45.45h-.9v.9a.45.45 0 1 1-.9 0v-.9h-.9a.45.45 0 1 1 0-.9h.9v-.9a.45.45 0 1 1 .9 0v.9h.9a.45.45 0 0 1 .45.45m-3.255 3.31a.45.45 0 1 1-.69.58C8.673 8.443 7.117 7.7 5.425 7.7c-1.693 0-3.249.742-4.38 2.09a.45.45 0 1 1-.69-.58c.841-1 1.886-1.71 3.048-2.09a3.825 3.825 0 1 1 4.044 0c1.162.38 2.207 1.09 3.048 2.09M5.425 6.8a2.925 2.925 0 1 0 0-5.85 2.925 2.925 0 0 0 0 5.85"/></svg>
|
||||
|
After Width: | Height: | Size: 522 B |
1
images/map/Camera.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="22" fill="none" viewBox="0 0 24 22"><path stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M23 18a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><path stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 16a4 4 0 1 0 0-8 4 4 0 0 0 0 8"/></svg>
|
||||
|
After Width: | Height: | Size: 400 B |
BIN
images/map/Copy_logo.png
Normal file
|
After Width: | Height: | Size: 870 B |
BIN
images/map/Email_logo.png
Normal file
|
After Width: | Height: | Size: 847 B |
1
images/map/Mapsettings.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="none" viewBox="0 0 22 22"><path fill="#fff" d="m7 0 6.074 3.544L20 .658v8.5h-2v-5.5l-4 1.667v3.833h-2V5.232L8 2.899v12.185l1.868 1.09-1.008 1.727-1.934-1.129L0 19.658V4.084zM6 14.991V2.899L2 5.232v11.426z"/><path fill="#fff" d="M17 10.906v1.376c.715.184 1.352.56 1.854 1.072l1.193-.689 1 1.732-1.192.688a4 4 0 0 1 0 2.142l1.192.688-1 1.732-1.193-.689A4 4 0 0 1 17 20.03v1.376h-2V20.03a4 4 0 0 1-1.854-1.072l-1.193.69-1-1.733 1.192-.688a4 4 0 0 1 0-2.142l-1.192-.688 1-1.732 1.193.688A4 4 0 0 1 15 12.283v-1.377zm-2.75 4.283a2 2 0 0 0 0 1.934l.035.063a2 2 0 0 0 3.43 0l.036-.063a2 2 0 0 0 0-1.934l-.036-.063a2 2 0 0 0-3.43 0z"/></svg>
|
||||
|
After Width: | Height: | Size: 702 B |