Skip to content

社交媒体应用开发案例研究

本案例研究展示了如何使用uni-app开发一个全面的社交媒体应用,涵盖用户资料、内容分享、实时消息、社交互动和社区功能等核心特性。

项目概述

应用功能

  • 用户注册与资料管理
  • 内容创建与分享(文本、图片、视频)
  • 实时消息和聊天
  • 社交互动(点赞、评论、分享)
  • 好友/关注者系统
  • 新闻流和时间线
  • 故事和临时内容
  • 群组社区和讨论
  • 直播功能
  • 推送通知

技术架构

  • 前端:uni-app 与 Vue.js
  • 后端:Node.js 与 Express
  • 数据库:MongoDB 用于用户数据,Redis 用于缓存
  • 实时通信:WebSocket 用于消息和直播功能
  • 存储:云存储用于媒体文件
  • CDN:内容分发网络用于媒体优化
  • 认证:JWT 与 OAuth 集成

核心实现

1. 用户资料系统

vue
<template>
  <view class="user-profile">
    <view class="profile-header">
      <view class="cover-image">
        <image :src="userProfile.coverImage" mode="aspectFill" />
        <button v-if="isOwnProfile" @click="changeCoverImage" class="change-cover-btn">
          <uni-icons type="camera" size="20" />
        </button>
      </view>
      
      <view class="profile-info">
        <view class="avatar-section">
          <image :src="userProfile.avatar" class="avatar" />
          <button v-if="isOwnProfile" @click="changeAvatar" class="change-avatar-btn">
            <uni-icons type="camera" size="16" />
          </button>
        </view>
        
        <view class="user-details">
          <text class="username">{{ userProfile.username }}</text>
          <text class="display-name">{{ userProfile.displayName }}</text>
          <text class="bio">{{ userProfile.bio }}</text>
          
          <view class="stats">
            <view class="stat-item" @click="showFollowers">
              <text class="stat-number">{{ userProfile.followersCount }}</text>
              <text class="stat-label">粉丝</text>
            </view>
            
            <view class="stat-item" @click="showFollowing">
              <text class="stat-number">{{ userProfile.followingCount }}</text>
              <text class="stat-label">关注</text>
            </view>
            
            <view class="stat-item" @click="showPosts">
              <text class="stat-number">{{ userProfile.postsCount }}</text>
              <text class="stat-label">帖子</text>
            </view>
          </view>
          
          <view class="action-buttons" v-if="!isOwnProfile">
            <button 
              :class="['follow-btn', { 'following': userProfile.isFollowing }]"
              @click="toggleFollow"
            >
              {{ userProfile.isFollowing ? '已关注' : '关注' }}
            </button>
            
            <button class="message-btn" @click="sendMessage">
              <uni-icons type="chat" size="16" />
              发消息
            </button>
          </view>
          
          <view class="edit-profile" v-else>
            <button @click="editProfile" class="edit-btn">编辑资料</button>
          </view>
        </view>
      </view>
    </view>
    
    <uni-segmented-control 
      :current="currentTab"
      :values="tabItems"
      @clickItem="onTabChange"
      style-type="button"
    />
    
    <swiper 
      :current="currentTab" 
      @change="onSwiperChange"
      class="content-swiper"
    >
      <swiper-item>
        <scroll-view scroll-y class="posts-container">
          <view 
            v-for="post in userPosts" 
            :key="post.id"
            class="post-item"
            @click="viewPost(post)"
          >
            <image 
              v-if="post.type === 'image'" 
              :src="post.thumbnail" 
              mode="aspectFill"
              class="post-thumbnail"
            />
            <video 
              v-else-if="post.type === 'video'"
              :src="post.videoUrl"
              :poster="post.thumbnail"
              class="post-video"
            />
            <view v-else class="post-text">
              <text>{{ post.content }}</text>
            </view>
            
            <view class="post-overlay">
              <view class="post-stats">
                <view class="stat">
                  <uni-icons type="heart" size="14" />
                  <text>{{ post.likesCount }}</text>
                </view>
                <view class="stat">
                  <uni-icons type="chat" size="14" />
                  <text>{{ post.commentsCount }}</text>
                </view>
              </view>
            </view>
          </view>
        </scroll-view>
      </swiper-item>
      
      <swiper-item>
        <scroll-view scroll-y class="media-container">
          <view class="media-grid">
            <view 
              v-for="media in userMedia" 
              :key="media.id"
              class="media-item"
              @click="viewMedia(media)"
            >
              <image :src="media.thumbnail" mode="aspectFill" />
              <view v-if="media.type === 'video'" class="video-indicator">
                <uni-icons type="play" size="20" />
              </view>
            </view>
          </view>
        </scroll-view>
      </swiper-item>
      
      <swiper-item>
        <scroll-view scroll-y class="likes-container">
          <uni-list>
            <uni-list-item 
              v-for="likedPost in likedPosts" 
              :key="likedPost.id"
              :title="likedPost.title"
              :note="likedPost.author"
              :thumb="likedPost.thumbnail"
              clickable
              @click="viewPost(likedPost)"
            />
          </uni-list>
        </scroll-view>
      </swiper-item>
    </swiper>
  </view>
</template>

<script>
export default {
  data() {
    return {
      userId: '',
      userProfile: {
        username: '',
        displayName: '',
        bio: '',
        avatar: '',
        coverImage: '',
        followersCount: 0,
        followingCount: 0,
        postsCount: 0,
        isFollowing: false
      },
      isOwnProfile: false,
      currentTab: 0,
      tabItems: ['帖子', '媒体', '喜欢'],
      userPosts: [],
      userMedia: [],
      likedPosts: []
    }
  },
  
  onLoad(options) {
    this.userId = options.userId || uni.getStorageSync('currentUserId')
    this.isOwnProfile = this.userId === uni.getStorageSync('currentUserId')
    this.loadUserProfile()
    this.loadUserContent()
  },
  
  methods: {
    async loadUserProfile() {
      try {
        const response = await uni.request({
          url: `https://api.social.com/users/${this.userId}/profile`,
          method: 'GET',
          header: {
            'Authorization': `Bearer ${this.getToken()}`
          }
        })
        
        this.userProfile = response.data.profile
      } catch (error) {
        console.error('加载用户资料失败:', error)
      }
    },
    
    async toggleFollow() {
      try {
        const action = this.userProfile.isFollowing ? 'unfollow' : 'follow'
        
        await uni.request({
          url: `https://api.social.com/users/${this.userId}/${action}`,
          method: 'POST',
          header: {
            'Authorization': `Bearer ${this.getToken()}`
          }
        })
        
        this.userProfile.isFollowing = !this.userProfile.isFollowing
        this.userProfile.followersCount += this.userProfile.isFollowing ? 1 : -1
        
        uni.showToast({
          title: this.userProfile.isFollowing ? '已关注' : '已取消关注',
          icon: 'success'
        })
      } catch (error) {
        uni.showToast({
          title: '操作失败',
          icon: 'error'
        })
      }
    },
    
    getToken() {
      return uni.getStorageSync('authToken')
    }
  }
}
</script>

2. 新闻流和时间线

vue
<template>
  <view class="news-feed">
    <view class="feed-header">
      <view class="logo">
        <text class="app-name">社交应用</text>
      </view>
      
      <view class="header-actions">
        <button @click="createPost" class="create-post-btn">
          <uni-icons type="plus" size="20" />
        </button>
        
        <button @click="openMessages" class="messages-btn">
          <uni-icons type="chat" size="20" />
          <view v-if="unreadCount > 0" class="unread-badge">
            <text>{{ unreadCount > 99 ? '99+' : unreadCount }}</text>
          </view>
        </button>
      </view>
    </view>
    
    <scroll-view 
      scroll-y 
      class="feed-container"
      @scrolltolower="loadMorePosts"
      refresher-enabled
      @refresherrefresh="refreshFeed"
      :refresher-triggered="isRefreshing"
    >
      <view class="stories-section">
        <scroll-view scroll-x class="stories-container">
          <view class="story-item add-story" @click="createStory">
            <view class="story-avatar">
              <uni-icons type="plus" size="24" />
            </view>
            <text class="story-label">我的故事</text>
          </view>
          
          <view 
            v-for="story in stories" 
            :key="story.id"
            class="story-item"
            @click="viewStory(story)"
          >
            <view class="story-avatar">
              <image :src="story.user.avatar" />
              <view v-if="!story.viewed" class="story-ring"></view>
            </view>
            <text class="story-label">{{ story.user.name }}</text>
          </view>
        </scroll-view>
      </view>
      
      <view class="posts-section">
        <view 
          v-for="post in posts" 
          :key="post.id"
          class="post-card"
        >
          <view class="post-header">
            <view class="user-info" @click="viewProfile(post.user)">
              <image :src="post.user.avatar" class="user-avatar" />
              <view class="user-details">
                <text class="username">{{ post.user.name }}</text>
                <text class="post-time">{{ formatTime(post.createdAt) }}</text>
                <view v-if="post.location" class="post-location">
                  <uni-icons type="location" size="12" />
                  <text>{{ post.location.name }}</text>
                </view>
              </view>
            </view>
            
            <button @click="showPostOptions(post)" class="options-btn">
              <uni-icons type="more" size="16" />
            </button>
          </view>
          
          <view class="post-content">
            <text v-if="post.content" class="post-text">{{ post.content }}</text>
            
            <view v-if="post.media && post.media.length" class="post-media">
              <swiper 
                v-if="post.media.length > 1"
                class="media-swiper"
                indicator-dots
                indicator-color="rgba(255,255,255,0.5)"
                indicator-active-color="#fff"
              >
                <swiper-item v-for="media in post.media" :key="media.id">
                  <image 
                    v-if="media.type === 'image'"
                    :src="media.url" 
                    mode="aspectFill"
                    @click="previewMedia(post.media, media)"
                  />
                  <video 
                    v-else-if="media.type === 'video'"
                    :src="media.url"
                    :poster="media.thumbnail"
                    controls
                  />
                </swiper-item>
              </swiper>
              
              <image 
                v-else-if="post.

### 2. 新闻流和时间线

```vue
<template>
  <view class="news-feed">
    <view class="feed-header">
      <view class="logo">
        <text class="app-name">社交应用</text>
      </view>
      
      <view class="header-actions">
        <button @click="createPost" class="create-post-btn">
          <uni-icons type="plus" size="20" />
        </button>
        
        <button @click="openMessages" class="messages-btn">
          <uni-icons type="chat" size="20" />
          <view v-if="unreadCount > 0" class="unread-badge">
            <text>{{ unreadCount > 99 ? '99+' : unreadCount }}</text>
          </view>
        </button>
      </view>
    </view>
    
    <scroll-view 
      scroll-y 
      class="feed-container"
      @scrolltolower="loadMorePosts"
      refresher-enabled
      @refresherrefresh="refreshFeed"
      :refresher-triggered="isRefreshing"
    >
      <view class="stories-section">
        <scroll-view scroll-x class="stories-container">
          <view class="story-item add-story" @click="createStory">
            <view class="story-avatar">
              <uni-icons type="plus" size="24" />
            </view>
            <text class="story-label">我的故事</text>
          </view>
          
          <view 
            v-for="story in stories" 
            :key="story.id"
            class="story-item"
            @click="viewStory(story)"
          >
            <view class="story-avatar">
              <image :src="story.user.avatar" />
              <view v-if="!story.viewed" class="story-ring"></view>
            </view>
            <text class="story-label">{{ story.user.name }}</text>
          </view>
        </scroll-view>
      </view>
      
      <view class="posts-section">
        <view 
          v-for="post in posts" 
          :key="post.id"
          class="post-card"
        >
          <view class="post-header">
            <view class="user-info" @click="viewProfile(post.user)">
              <image :src="post.user.avatar" class="user-avatar" />
              <view class="user-details">
                <text class="username">{{ post.user.name }}</text>
                <text class="post-time">{{ formatTime(post.createdAt) }}</text>
                <view v-if="post.location" class="post-location">
                  <uni-icons type="location" size="12" />
                  <text>{{ post.location.name }}</text>
                </view>
              </view>
            </view>
            
            <button @click="showPostOptions(post)" class="options-btn">
              <uni-icons type="more" size="16" />
            </button>
          </view>
          
          <view class="post-content">
            <text v-if="post.content" class="post-text">{{ post.content }}</text>
            
            <view v-if="post.media && post.media.length" class="post-media">
              <swiper 
                v-if="post.media.length > 1"
                class="media-swiper"
                indicator-dots
                indicator-color="rgba(255,255,255,0.5)"
                indicator-active-color="#fff"
              >
                <swiper-item v-for="media in post.media" :key="media.id">
                  <image 
                    v-if="media.type === 'image'"
                    :src="media.url" 
                    mode="aspectFill"
                    @click="previewMedia(post.media, media)"
                  />
                  <video 
                    v-else-if="media.type === 'video'"
                    :src="media.url"
                    :poster="media.thumbnail"
                    controls
                  />
                </swiper-item>
              </swiper>
              
              <image 
                v-else-if="post.media[0].type === 'image'"
                :src="post.media[0].url" 
                mode="aspectFill"
                class="single-image"
                @click="previewMedia(post.media, post.media[0])"
              />
              
              <video 
                v-else-if="post.media[0].type === 'video'"
                :src="post.media[0].url"
                :poster="post.media[0].thumbnail"
                controls
                class="single-video"
              />
            </view>
          </view>
          
          <view class="post-actions">
            <view class="action-stats">
              <text v-if="post.likesCount > 0">{{ post.likesCount }} 赞</text>
              <text v-if="post.commentsCount > 0">{{ post.commentsCount }} 评论</text>
              <text v-if="post.sharesCount > 0">{{ post.sharesCount }} 分享</text>
            </view>
            
            <view class="action-buttons">
              <button 
                @click="toggleLike(post)"
                :class="['action-btn', { 'liked': post.isLiked }]"
              >
                <uni-icons :type="post.isLiked ? 'heart-filled' : 'heart'" size="20" />
                <text>点赞</text>
              </button>
              
              <button @click="showComments(post)" class="action-btn">
                <uni-icons type="chat" size="20" />
                <text>评论</text>
              </button>
              
              <button @click="sharePost(post)" class="action-btn">
                <uni-icons type="redo" size="20" />
                <text>分享</text>
              </button>
            </view>
          </view>
        </view>
      </view>
      
      <view v-if="isLoading" class="loading-indicator">
        <uni-load-more status="loading" />
      </view>
    </scroll-view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      posts: [],
      stories: [],
      unreadCount: 0,
      isLoading: false,
      isRefreshing: false,
      page: 1,
      hasMore: true
    }
  },
  
  onLoad() {
    this.loadFeed()
    this.loadStories()
    this.loadUnreadCount()
  },
  
  methods: {
    async loadFeed() {
      if (this.isLoading || !this.hasMore) return
      
      this.isLoading = true
      
      try {
        const response = await uni.request({
          url: 'https://api.social.com/feed',
          method: 'GET',
          data: {
            page: this.page,
            limit: 10
          },
          header: {
            'Authorization': `Bearer ${this.getToken()}`
          }
        })
        
        const newPosts = response.data.posts
        
        if (this.page === 1) {
          this.posts = newPosts
        } else {
          this.posts.push(...newPosts)
        }
        
        this.hasMore = newPosts.length === 10
        this.page++
      } catch (error) {
        console.error('加载信息流失败:', error)
      } finally {
        this.isLoading = false
      }
    },
    
    async refreshFeed() {
      this.isRefreshing = true
      this.page = 1
      this.hasMore = true
      
      await this.loadFeed()
      await this.loadStories()
      
      this.isRefreshing = false
    },
    
    async toggleLike(post) {
      const wasLiked = post.isLiked
      
      // 乐观更新
      post.isLiked = !post.isLiked
      post.likesCount += post.isLiked ? 1 : -1
      
      try {
        await uni.request({
          url: `https://api.social.com/posts/${post.id}/like`,
          method: post.isLiked ? 'POST' : 'DELETE',
          header: {
            'Authorization': `Bearer ${this.getToken()}`
          }
        })
      } catch (error) {
        // 错误时恢复
        post.isLiked = wasLiked
        post.likesCount += wasLiked ? 1 : -1
        
        uni.showToast({
          title: '操作失败',
          icon: 'error'
        })
      }
    },
    
    createPost() {
      uni.navigateTo({
        url: '/pages/posts/create'
      })
    },
    
    getToken() {
      return uni.getStorageSync('authToken')
    }
  }
}
</script>

3. 实时消息系统

vue
<template>
  <view class="chat-list">
    <view class="chat-header">
      <text class="title">消息</text>
      <button @click="newChat" class="new-chat-btn">
        <uni-icons type="compose" size="20" />
      </button>
    </view>
    
    <view class="search-bar">
      <uni-search-bar 
        v-model="searchQuery"
        placeholder="搜索对话"
        @input="searchConversations"
      />
    </view>
    
    <scroll-view scroll-y class="conversations-list">
      <view 
        v-for="conversation in filteredConversations" 
        :key="conversation.id"
        class="conversation-item"
        @click="openConversation(conversation)"
      >
        <view class="conversation-avatar">
          <image :src="conversation.contact.avatar" />
          <view v-if="conversation.contact.isOnline" class="online-indicator"></view>
        </view>
        
        <view class="conversation-content">
          <view class="conversation-header">
            <text class="contact-name">{{ conversation.contact.name }}</text>
            <text class="last-message-time">{{ formatTime(conversation.lastMessage.timestamp) }}</text>
          </view>
          
          <view class="last-message">
            <text :class="{ 'unread': conversation.unreadCount > 0 }">
              {{ getLastMessagePreview(conversation.lastMessage) }}
            </text>
            
            <view v-if="conversation.unreadCount > 0" class="unread-count">
              <text>{{ conversation.unreadCount > 99 ? '99+' : conversation.unreadCount }}</text>
            </view>
          </view>
        </view>
      </view>
    </scroll-view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      conversations: [],
      filteredConversations: [],
      searchQuery: '',
      socketConnection: null
    }
  },
  
  onLoad() {
    this.loadConversations()
    this.initializeWebSocket()
  },
  
  methods: {
    async loadConversations() {
      try {
        const response = await uni.request({
          url: 'https://api.social.com/conversations',
          method: 'GET',
          header: {
            'Authorization': `Bearer ${this.getToken()}`
          }
        })
        
        this.conversations = response.data.conversations
        this.filteredConversations = this.conversations
      } catch (error) {
        console.error('加载对话失败:', error)
      }
    },
    
    initializeWebSocket() {
      this.socketConnection = uni.connectSocket({
        url: 'wss://api.social.com/messages',
        header: {
          'Authorization': `Bearer ${this.getToken()}`
        }
      })
      
      this.socketConnection.onMessage((res) => {
        const data = JSON.parse(res.data)
        
        if (data.type === 'new_message') {
          this.updateConversation(data.conversationId, data.message)
        }
      })
    },
    
    updateConversation(conversationId, message) {
      const conversation = this.conversations.find(c => c.id === conversationId)
      if (conversation) {
        conversation.lastMessage = message
        conversation.unreadCount++
        
        // 移至顶部
        const index = this.conversations.indexOf(conversation)
        this.conversations.splice(index, 1)
        this.conversations.unshift(conversation)
        
        this.filteredConversations = [...this.conversations]
      }
    },
    
    getToken() {
      return uni.getStorageSync('authToken')
    }
  }
}
</script>

4. 直播功能

vue
<template>
  <view class="live-stream">
    <view class="stream-container">
      <live-pusher 
        id="livePusher"
        :url="streamUrl"
        mode="RTC"
        autopush
        enable-camera
        enable-mic
        @statechange="onStreamStateChange"
        @error="onStreamError"
        class="live-pusher"
      />
      
      <view class="stream-overlay">
        <view class="stream-info">
          <view class="viewer-count">
            <uni-icons type="eye" size="16" />
            <text>{{ viewerCount }} 人观看</text>
          </view>
          
          <view class="stream-duration">
            <text>{{ formatDuration(streamDuration) }}</text>
          </view>
        </view>
        
        <view class="stream-controls">
          <button 
            @click="toggleMute"
            :class="['control-btn', { 'muted': isMuted }]"
          >
            <uni-icons :type="isMuted ? 'mic-off' : 'mic'" size="20" />
          </button>
          
          <button 
            @click="switchCamera"
            class="control-btn"
          >
            <uni-icons type="loop" size="20" />
          </button>
          
          <button 
            @click="toggleCamera"
            :class="['control-btn', { 'camera-off': !cameraEnabled }]"
          >
            <uni-icons :type="cameraEnabled ? 'videocam' : 'videocam-off'" size="20" />
          </button>
          
          <button 
            @click="endStream"
            class="control-btn end-stream"
          >
            <uni-icons type="close" size="20" />
          </button>
        </view>
      </view>
    </view>
    
    <view class="chat-section">
      <scroll-view 
        scroll-y 
        class="live-chat"
        :scroll-top="chatScrollTop"
      >
        <view 
          v-for="message in chatMessages" 
          :key="message.id"
          class="chat-message"
        >
          <text class="username">{{ message.user.name }}:</text>
          <text class="message-text">{{ message.text }}</text>
        </view>
      </scroll-view>
      
      <view class="chat-input">
        <input 
          v-model="newMessage"
          placeholder="说点什么..."
          @confirm="sendChatMessage"
        />
        <button @click="sendChatMessage" class="send-btn">
          <uni-icons type="paperplane" size="16" />
        </button>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      streamUrl: '',
      streamId: '',
      viewerCount: 0,
      streamDuration: 0,
      isMuted: false,
      cameraEnabled: true,
      chatMessages: [],
      newMessage: '',
      chatScrollTop: 0,
      streamTimer: null,
      socketConnection: null
    }
  },
  
  onLoad() {
    this.initializeStream()
  },
  
  onUnload() {
    this.endStream()
  },
  
  methods: {
    async initializeStream() {
      try {
        const response = await uni.request({
          url: 'https://api.social.com/live/start',
          method: 'POST',
          header: {
            'Authorization': `Bearer ${this.getToken()}`
          }
        })
        
        this.streamUrl = response.data.streamUrl
        this.streamId = response.data.streamId
        
        this.initializeWebSocket()
        this.startTimer()
      } catch (error) {
        uni.showToast({
          title: '启动直播失败',
          icon: 'error'
        })
      }
    },
    
    initializeWebSocket() {
      this.socketConnection = uni.connectSocket({
        url: `wss://api.social.com/live/${this.streamId}`,
        header: {
          'Authorization': `Bearer ${this.getToken()}`
        }
      })
      
      this.socketConnection.onMessage((res) => {
        const data = JSON.parse(res.data)
        
        switch (data.type) {
          case 'viewer_count':
            this.viewerCount = data.count
            break
          case 'chat_message':
            this.chatMessages.push(data.message)
            this.scrollChatToBottom()
            break
        }
      })
    },
    
    startTimer() {
      this.streamTimer = setInterval(() => {
        this.streamDuration++
      }, 1000)
    },
    
    toggleMute() {
      this.isMuted = !this.isMuted
      const livePusher = uni.createLivePusherContext('livePusher', this)
      
      if (this.isMuted) {
        livePusher.mute()
      } else {
        livePusher.unmute()
      }
    },
    
    switchCamera() {
      const livePusher = uni.createLivePusherContext('livePusher', this)
      livePusher.switchCamera()
    },
    
    toggleCamera() {
      this.cameraEnabled = !this.cameraEnabled
      const livePusher = uni.createLivePusherContext('livePusher', this)
      
      if (this.cameraEnabled) {
        livePusher.start()
      } else {
        livePusher.stop()
      }
    },
    
    async endStream() {
      try {
        await uni.request({
          url: `https://api.social.com/live/${this.streamId}/end`,
          method: 'POST',
          header: {
            'Authorization': `Bearer ${this.getToken()}`
          }
        })
        
        if (this.streamTimer) {
          clearInterval(this.streamTimer)
        }
        
        if (this.socketConnection) {
          uni.closeSocket()
        }
        
        uni.navigateBack()
      } catch (error) {
        console.error('结束直播失败:', error)
      }
    },
    
    sendChatMessage() {
      if (!this.newMessage.trim()) return
      
      const message = {
        id: Date.now(),
        text: this.newMessage,
        user: {
          id: uni.getStorageSync('currentUserId'),
          name: uni.getStorageSync('currentUser').name
        },
        timestamp: new Date()
      }
      
      if (this.socketConnection) {
        uni.sendSocketMessage({
          data: JSON.stringify({
            type: 'chat_message',
            message: message
          })
        })
      }
      
      this.newMessage = ''
    },
    
    scrollChatToBottom() {
      this.$nextTick(() => {
        this.chatScrollTop = 999999
      })
    },
    
    formatDuration(seconds) {
      const hours = Math.floor(seconds / 3600)
      const minutes = Math.floor((seconds % 3600) / 60)
      const secs = seconds % 60
      
      if (hours > 0) {
        return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
      }
      return `${minutes}:${secs.toString().padStart(2, '0')}`
    },
    
    getToken() {
      return uni.getStorageSync('authToken')
    }
  }
}
</script>

平台特定优化

iOS 优化

javascript
// iOS特定社交分享
// #ifdef APP-PLUS-IOS
export default {
  methods: {
    async shareToiOS(content) {
      const shareOptions = {
        title: content.title,
        summary: content.description,
        href: content.url,
        imageUrl: content.image
      }
      
      plus.share.sendWithSystem(shareOptions, (res) => {
        console.log('分享成功:', res)
      }, (error) => {
        console.error('分享失败:', error)
      })
    }
  }
}
// #endif

Android 优化

javascript
// Android特定通知
// #ifdef APP-PLUS-ANDROID
export default {
  methods: {
    setupPushNotifications() {
      const main = plus.android.runtimeMainActivity()
      const Intent = plus.android.importClass('android.content.Intent')
      const PendingIntent = plus.android.importClass('android.app.PendingIntent')
      
      // 配置通知渠道
      this.createNotificationChannel('messages', '消息', '新消息通知')
      this.createNotificationChannel('likes', '点赞', '点赞通知')
    }
  }
}
// #endif

性能优化

图片懒加载

javascript
// utils/lazyLoad.js
export class LazyImageLoader {
  constructor() {
    this.imageCache = new Map()
    this.loadingImages = new Set()
  }
  
  async loadImage(url, placeholder = '/static/placeholder.png') {
    if (this.imageCache.has(url)) {
      return this.imageCache.get(url)
    }
    
    if (this.loadingImages.has(url)) {
      return placeholder
    }
    
    this.loadingImages.add(url)
    
    try {
      const compressedUrl = await this.compressImage(url)
      this.imageCache.set(url, compressedUrl)
      this.loadingImages.delete(url)
      return compressedUrl
    } catch (error) {
      this.loadingImages.delete(url)
      console.error('图片加载失败:', error)
      return placeholder
    }
  }
  
  async compressImage(url) {
    // 实现图片压缩逻辑
    return url
  }
  
  clearCache() {
    this.imageCache.clear()
  }
}

虚拟列表

javascript
// components/VirtualList.vue
export default {
  props: {
    items: {
      type: Array,
      required: true
    },
    itemHeight: {
      type: Number,
      default: 80
    },
    visibleItems: {
      type: Number,
      default: 10
    }
  },
  
  data() {
    return {
      startIndex: 0,
      scrollTop: 0
    }
  },
  
  computed: {
    visibleData() {
      const start = this.startIndex
      const end = Math.min(this.items.length, start + this.visibleItems + 4)
      return this.items.slice(start, end)
    },
    
    totalHeight() {
      return this.items.length * this.itemHeight
    },
    
    containerStyle() {
      return {
        height: `${this.visibleItems * this.itemHeight}px`,
        position: 'relative',
        overflow: 'auto'
      }
    },
    
    contentStyle() {
      return {
        height: `${this.totalHeight}px`,
        position: 'relative'
      }
    }
  },
  
  methods: {
    onScroll(e) {
      this.scrollTop = e.detail.scrollTop
      this.updateStartIndex()
    },
    
    updateStartIndex() {
      const newIndex = Math.floor(this.scrollTop / this.itemHeight)
      this.startIndex = Math.max(0, newIndex - 2)
    }
  }
}

一次开发,多端部署 - 让跨平台开发更简单