鸿蒙OSUniApp导航栏组件开发:打造清新简约的用户界面#三方框架 #Uniapp

news2025/6/3 3:48:56

UniApp 开发实战:打造符合鸿蒙设计风格的日历活动安排组件

在移动应用开发中,日历和活动安排是非常常见的需求。本文将详细介绍如何使用 UniApp 框架开发一个优雅的日历活动安排组件,并融入鸿蒙系统的设计理念,实现一个既美观又实用的功能模块。

设计理念

在开发这个组件之前,我们需要深入理解鸿蒙系统的设计哲学。鸿蒙系统强调"自然、流畅、统一"的设计原则,这些特点将贯穿我们的整个组件实现过程:

  1. 自然:日历的切换和选择要符合用户的直觉
  2. 流畅:动画过渡要平滑,操作响应要及时
  3. 统一:视觉元素要保持一致性,符合系统设计规范
  4. 高效:快速获取和展示日程信息

组件实现

1. 日历选择器组件

首先实现一个基础的日历选择器组件:

<!-- components/harmony-calendar/harmony-calendar.vue -->
<template>
  <view class="harmony-calendar">
    <view class="calendar-header">
      <view class="month-switcher">
        <text class="iconfont icon-left" @click="changeMonth(-1)"></text>
        <text class="current-month">{{ currentYear }}年{{ currentMonth + 1 }}月</text>
        <text class="iconfont icon-right" @click="changeMonth(1)"></text>
      </view>
      <view class="weekday-header">
        <text v-for="day in weekDays" :key="day">{{ day }}</text>
      </view>
    </view>
    
    <view class="calendar-body" :class="{ 'with-animation': enableAnimation }">
      <view class="days-grid">
        <view v-for="(day, index) in daysArray" 
              :key="index"
              class="day-cell"
              :class="[
                { 'current-month': day.currentMonth },
                { 'selected': isSelected(day.date) },
                { 'has-events': hasEvents(day.date) }
              ]"
              @click="selectDate(day)">
          <text class="day-number">{{ day.dayNumber }}</text>
          <view v-if="hasEvents(day.date)" class="event-indicator"></view>
        </view>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  name: 'HarmonyCalendar',
  props: {
    events: {
      type: Array,
      default: () => []
    },
    selectedDate: {
      type: Date,
      default: () => new Date()
    }
  },
  data() {
    return {
      currentDate: new Date(),
      currentMonth: new Date().getMonth(),
      currentYear: new Date().getFullYear(),
      weekDays: ['日', '一', '二', '三', '四', '五', '六'],
      enableAnimation: true,
      daysArray: []
    }
  },
  watch: {
    currentMonth: {
      handler() {
        this.generateCalendar()
      },
      immediate: true
    }
  },
  methods: {
    generateCalendar() {
      const firstDay = new Date(this.currentYear, this.currentMonth, 1)
      const lastDay = new Date(this.currentYear, this.currentMonth + 1, 0)
      const daysInMonth = lastDay.getDate()
      const startingDay = firstDay.getDay()
      
      let days = []
      
      // 上个月的日期
      const prevMonth = new Date(this.currentYear, this.currentMonth - 1, 1)
      const daysInPrevMonth = new Date(this.currentYear, this.currentMonth, 0).getDate()
      for (let i = startingDay - 1; i >= 0; i--) {
        days.push({
          date: new Date(prevMonth.getFullYear(), prevMonth.getMonth(), daysInPrevMonth - i),
          dayNumber: daysInPrevMonth - i,
          currentMonth: false
        })
      }
      
      // 当前月的日期
      for (let i = 1; i <= daysInMonth; i++) {
        days.push({
          date: new Date(this.currentYear, this.currentMonth, i),
          dayNumber: i,
          currentMonth: true
        })
      }
      
      // 下个月的日期
      const remainingDays = 42 - days.length // 保持6行固定高度
      for (let i = 1; i <= remainingDays; i++) {
        days.push({
          date: new Date(this.currentYear, this.currentMonth + 1, i),
          dayNumber: i,
          currentMonth: false
        })
      }
      
      this.daysArray = days
    },
    
    changeMonth(delta) {
      const newDate = new Date(this.currentYear, this.currentMonth + delta, 1)
      this.currentMonth = newDate.getMonth()
      this.currentYear = newDate.getFullYear()
      
      this.enableAnimation = true
      setTimeout(() => {
        this.enableAnimation = false
      }, 300)
    },
    
    isSelected(date) {
      return date.toDateString() === this.selectedDate.toDateString()
    },
    
    hasEvents(date) {
      return this.events.some(event => 
        new Date(event.date).toDateString() === date.toDateString()
      )
    },
    
    selectDate(day) {
      this.$emit('date-selected', day.date)
    }
  }
}
</script>

<style lang="scss">
.harmony-calendar {
  background-color: #ffffff;
  border-radius: 24rpx;
  padding: 32rpx;
  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
  
  .calendar-header {
    margin-bottom: 32rpx;
    
    .month-switcher {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 24rpx;
      
      .current-month {
        font-size: 32rpx;
        font-weight: 500;
        color: #333333;
      }
      
      .iconfont {
        font-size: 40rpx;
        color: #666666;
        padding: 16rpx;
        
        &:active {
          opacity: 0.7;
        }
      }
    }
    
    .weekday-header {
      display: grid;
      grid-template-columns: repeat(7, 1fr);
      text-align: center;
      
      text {
        font-size: 28rpx;
        color: #999999;
        padding: 16rpx 0;
      }
    }
  }
  
  .calendar-body {
    .days-grid {
      display: grid;
      grid-template-columns: repeat(7, 1fr);
      gap: 8rpx;
      
      .day-cell {
        aspect-ratio: 1;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        position: relative;
        border-radius: 16rpx;
        transition: all 0.2s ease-in-out;
        
        &.current-month {
          background-color: #f8f8f8;
          
          .day-number {
            color: #333333;
          }
        }
        
        &.selected {
          background-color: #2979ff;
          
          .day-number {
            color: #ffffff;
          }
          
          .event-indicator {
            background-color: #ffffff;
          }
        }
        
        .day-number {
          font-size: 28rpx;
          color: #999999;
        }
        
        .event-indicator {
          width: 8rpx;
          height: 8rpx;
          border-radius: 4rpx;
          background-color: #2979ff;
          position: absolute;
          bottom: 12rpx;
        }
        
        &:active {
          transform: scale(0.95);
        }
      }
    }
  }
  
  &.with-animation {
    .days-grid {
      animation: fadeInScale 0.3s ease-out;
    }
  }
}

@keyframes fadeInScale {
  from {
    opacity: 0.5;
    transform: scale(0.95);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}
</style>

2. 活动列表组件

接下来实现活动列表组件,用于展示选中日期的活动:

<!-- components/schedule-list/schedule-list.vue -->
<template>
  <view class="schedule-list">
    <view class="list-header">
      <text class="title">活动安排</text>
      <text class="add-btn" @click="$emit('add')">添加</text>
    </view>
    
    <view class="list-content">
      <template v-if="filteredEvents.length">
        <view v-for="event in filteredEvents" 
              :key="event.id"
              class="schedule-item"
              :class="{ 'completed': event.completed }"
              @click="toggleEvent(event)">
          <view class="event-time">{{ formatTime(event.time) }}</view>
          <view class="event-info">
            <text class="event-title">{{ event.title }}</text>
            <text class="event-location" v-if="event.location">
              {{ event.location }}
            </text>
          </view>
          <text class="iconfont icon-check"></text>
        </view>
      </template>
      <view v-else class="empty-tip">
        <image src="/static/empty.png" mode="aspectFit"></image>
        <text>暂无活动安排</text>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  name: 'ScheduleList',
  props: {
    selectedDate: {
      type: Date,
      required: true
    },
    events: {
      type: Array,
      default: () => []
    }
  },
  computed: {
    filteredEvents() {
      return this.events
        .filter(event => 
          new Date(event.date).toDateString() === this.selectedDate.toDateString()
        )
        .sort((a, b) => new Date(`${a.date} ${a.time}`) - new Date(`${b.date} ${b.time}`))
    }
  },
  methods: {
    formatTime(time) {
      return time.slice(0, 5) // 只显示小时和分钟
    },
    
    toggleEvent(event) {
      this.$emit('toggle', event)
    }
  }
}
</script>

<style lang="scss">
.schedule-list {
  margin-top: 32rpx;
  
  .list-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 24rpx;
    
    .title {
      font-size: 32rpx;
      font-weight: 500;
      color: #333333;
    }
    
    .add-btn {
      font-size: 28rpx;
      color: #2979ff;
      padding: 16rpx;
      
      &:active {
        opacity: 0.7;
      }
    }
  }
  
  .list-content {
    .schedule-item {
      display: flex;
      align-items: center;
      padding: 24rpx;
      background-color: #ffffff;
      border-radius: 16rpx;
      margin-bottom: 16rpx;
      box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
      transition: all 0.2s ease-in-out;
      
      &.completed {
        opacity: 0.6;
        
        .event-info {
          text-decoration: line-through;
        }
        
        .icon-check {
          color: #52c41a;
        }
      }
      
      .event-time {
        font-size: 28rpx;
        color: #666666;
        margin-right: 24rpx;
        min-width: 100rpx;
      }
      
      .event-info {
        flex: 1;
        
        .event-title {
          font-size: 28rpx;
          color: #333333;
          margin-bottom: 8rpx;
        }
        
        .event-location {
          font-size: 24rpx;
          color: #999999;
        }
      }
      
      .icon-check {
        font-size: 40rpx;
        color: #dddddd;
      }
      
      &:active {
        transform: scale(0.98);
      }
    }
    
    .empty-tip {
      text-align: center;
      padding: 64rpx 0;
      
      image {
        width: 200rpx;
        height: 200rpx;
        margin-bottom: 16rpx;
      }
      
      text {
        font-size: 28rpx;
        color: #999999;
      }
    }
  }
}
</style>

3. 使用示例

下面展示如何在页面中使用这两个组件:

<!-- pages/schedule/schedule.vue -->
<template>
  <view class="schedule-page">
    <harmony-calendar
      :events="events"
      :selected-date="selectedDate"
      @date-selected="handleDateSelect">
    </harmony-calendar>
    
    <schedule-list
      :events="events"
      :selected-date="selectedDate"
      @toggle="toggleEventStatus"
      @add="showAddEventPopup">
    </schedule-list>
    
    <!-- 添加活动弹窗 -->
    <uni-popup ref="addEventPopup" type="bottom">
      <view class="add-event-form">
        <input v-model="newEvent.title" 
               placeholder="活动标题" 
               class="input-field" />
        <input v-model="newEvent.location" 
               placeholder="活动地点" 
               class="input-field" />
        <picker mode="time" 
                :value="newEvent.time"
                @change="handleTimeChange"
                class="input-field">
          <view>{{ newEvent.time || '选择时间' }}</view>
        </picker>
        <button @click="saveEvent" 
                class="save-btn"
                :disabled="!newEvent.title || !newEvent.time">
          保存
        </button>
      </view>
    </uni-popup>
  </view>
</template>

<script>
import HarmonyCalendar from '@/components/harmony-calendar/harmony-calendar'
import ScheduleList from '@/components/schedule-list/schedule-list'

export default {
  components: {
    HarmonyCalendar,
    ScheduleList
  },
  data() {
    return {
      selectedDate: new Date(),
      events: [],
      newEvent: {
        title: '',
        location: '',
        time: '',
        completed: false
      }
    }
  },
  methods: {
    handleDateSelect(date) {
      this.selectedDate = date
    },
    
    showAddEventPopup() {
      this.$refs.addEventPopup.open()
    },
    
    handleTimeChange(e) {
      this.newEvent.time = e.detail.value
    },
    
    saveEvent() {
      const event = {
        ...this.newEvent,
        id: Date.now(),
        date: this.selectedDate
      }
      
      this.events.push(event)
      this.$refs.addEventPopup.close()
      
      // 重置表单
      this.newEvent = {
        title: '',
        location: '',
        time: '',
        completed: false
      }
      
      // 提示保存成功
      uni.showToast({
        title: '添加成功',
        icon: 'success'
      })
    },
    
    toggleEventStatus(event) {
      const index = this.events.findIndex(e => e.id === event.id)
      if (index > -1) {
        this.events[index].completed = !this.events[index].completed
      }
    }
  }
}
</script>

<style lang="scss">
.schedule-page {
  padding: 32rpx;
  min-height: 100vh;
  background-color: #f5f5f5;
}

.add-event-form {
  background-color: #ffffff;
  padding: 32rpx;
  border-radius: 24rpx 24rpx 0 0;
  
  .input-field {
    width: 100%;
    height: 88rpx;
    border-bottom: 2rpx solid #f0f0f0;
    margin-bottom: 24rpx;
    padding: 0 24rpx;
    font-size: 28rpx;
  }
  
  .save-btn {
    margin-top: 32rpx;
    background-color: #2979ff;
    color: #ffffff;
    border-radius: 44rpx;
    height: 88rpx;
    line-height: 88rpx;
    font-size: 32rpx;
    
    &:active {
      opacity: 0.9;
    }
    
    &[disabled] {
      background-color: #cccccc;
    }
  }
}
</style>

性能优化

  1. 虚拟列表优化

当活动数量较多时,可以使用虚拟列表优化性能:

// 在 schedule-list 组件中添加
import VirtualList from '@/components/virtual-list'

export default {
  components: {
    VirtualList
  }
  // ...其他配置
}
  1. 状态管理

对于大型应用,建议使用 Vuex 管理日程数据:

// store/modules/schedule.js
export default {
  state: {
    events: []
  },
  mutations: {
    ADD_EVENT(state, event) {
      state.events.push(event)
    },
    TOGGLE_EVENT(state, eventId) {
      const event = state.events.find(e => e.id === eventId)
      if (event) {
        event.completed = !event.completed
      }
    }
  },
  actions: {
    async fetchEvents({ commit }) {
      // 从服务器获取数据
      const events = await api.getEvents()
      commit('SET_EVENTS', events)
    }
  }
}
  1. 缓存优化

使用本地存储缓存日程数据:

// utils/storage.js
export const saveEvents = (events) => {
  try {
    uni.setStorageSync('schedule_events', JSON.stringify(events))
  } catch (e) {
    console.error('保存失败:', e)
  }
}

export const getEvents = () => {
  try {
    const events = uni.getStorageSync('schedule_events')
    return events ? JSON.parse(events) : []
  } catch (e) {
    console.error('读取失败:', e)
    return []
  }
}

适配建议

  1. 暗黑模式支持
// 在组件样式中添加
.harmony-calendar {
  &.dark {
    background-color: #1c1c1e;
    
    .calendar-header {
      .current-month {
        color: #ffffff;
      }
    }
    
    .day-cell {
      &.current-month {
        background-color: #2c2c2e;
        
        .day-number {
          color: #ffffff;
        }
      }
    }
  }
}
  1. 响应式布局

使用 flex 布局和 rpx 单位确保在不同设备上的适配:

.schedule-page {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  
  @media screen and (min-width: 768px) {
    flex-direction: row;
    
    .harmony-calendar {
      flex: 0 0 400rpx;
    }
    
    .schedule-list {
      flex: 1;
      margin-left: 32rpx;
    }
  }
}

总结

通过本文的讲解,我们实现了一个功能完整的日历活动安排组件。该组件不仅具有优雅的界面设计,还融入了鸿蒙系统的设计理念,同时考虑了性能优化和多端适配等实际开发中的重要因素。

在实际开发中,我们可以根据具体需求对组件进行扩展,例如:

  • 添加重复活动功能
  • 实现活动提醒
  • 支持多人协作
  • 添加数据同步功能

希望这篇文章能够帮助你更好地理解如何在 UniApp 中开发高质量的组件,同时也能为你的实际项目开发提供有价值的参考。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2394412.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

力扣HOT100之动态规划:300. 最长递增子序列

这道题之前刷代码随想录的时候也刷过&#xff0c;现在又给忘完了。自己尝试着写了一下&#xff0c;发现怎么写都写不对&#xff0c;直接去看视频了。。我自己写的时候的定义是&#xff1a;考虑下标0 ~ i范围内索赔能取到的最长严格递增子序列的长度&#xff0c;后面发现在写递推…

在win10/11下Node.js安装配置教程

下载安装 官网链接https://nodejs.org/zh-cn 下载好以后双击打开&#xff0c;点击下一步 勾选&#xff0c;然后下一步 选择路径、下一步 下一步 配置环境 找到我们安装的文件夹&#xff0c;创建两个文件夹 node_global node_cache 在CMD中配置路径 npm config set p…

飞致云开源社区月度动态报告(2025年5月)

自2023年6月起&#xff0c;中国领先的开源软件公司飞致云以月度为单位发布《飞致云开源社区月度动态报告》&#xff0c;旨在向广大社区用户同步飞致云旗下系列开源软件的发展情况&#xff0c;以及当月主要的产品新版本发布、社区运营成果等相关信息。 飞致云开源运营数据概览&…

压缩包方式在Linux和Windows下安装mongodb

目录 安装流程安装实例1. Linux安装2. Windows安装 总结 安装流程 zip方式安装 优点&#xff1a;自定义性较高&#xff0c;可以自己控制数据、日志等文件的位置 1、下载安装包 2、解压安装包 3、创建各类文件路径 4、配置conf文件 5、使用自定义配置文件启动 安装实例 1. Li…

智慧场馆:科技赋能的艺术盛宴

智慧场馆作为城市公共服务设施数字化转型的典型代表&#xff0c;通过深度融合新一代信息技术&#xff0c;构建起全方位、智能化的运营管理体系。其功能架构不仅提升了场馆本身的运营效能&#xff0c;更重塑了公共服务体验模式&#xff0c;展现出显著的社会价值和商业潜力。 一…

《ChatGPT o3抗命:AI失控警钟还是成长阵痛?》

ChatGPT o3 “抗命” 事件起底 在人工智能的飞速发展进程中&#xff0c;OpenAI 于 2025 年推出的 ChatGPT o3 推理模型&#xff0c;犹如一颗重磅炸弹投入了技术的海洋&#xff0c;激起千层浪。它被视为 “推理模型” 系列的巅峰之作&#xff0c;承载着赋予 ChatGPT 更强大问题解…

【sa-token】 sa-token非 web 上下文无法获取 HttpServletRequest。

Springboot cloud gateway集成sa-token中报错 cn.dev33.satoken.exception.NotWebContextException: 非 web 上下文无法获取 HttpServletRequestat cn.dev33.satoken.spring.SpringMVCUtil.getRequest(SpringMVCUtil.java:45) ~[sa-token-spring-boot-starter-1.38.0.jar:?]官…

多台电脑共用一个ip地址可以吗?会怎么样

在互联网使用日益普及的今天&#xff0c;许多人都面临着多台设备共享网络的需求。一个常见的问题随之而来&#xff1a;多台电脑共用一个IP地址可以吗&#xff1f;这样做会带来哪些影响&#xff1f;本文将深入探讨这一话题。 一、多台电脑共用一个‌IP地址可以吗&#xff1f; 多…

线程(上)【Linux操作系统】

文章目录 线程概念及其相关知识线程的概念及一些重要认识重要认识Linux中线程的实现Linux中的被调度的执行流是被task_struct描述的 线程是如何瓜分进程的代码和数据的&#xff1f;对于数据&#xff1a;对于代码&#xff1a; 线程的优点线程的缺点线程调度细节调度&#xff1a;…

进程同步:生产者-消费者 题目

正确答案&#xff1a; 问题类型&#xff1a; 经典生产者 - 消费者问题 同时涉及同步和互斥。 同步&#xff1a;生产者与消费者通过信号量协调生产 / 消费节奏&#xff08;如缓冲区满时生产者等待&#xff0c;空时消费者等待&#xff09;。互斥&#xff1a;对共享缓冲区的访问需…

展会聚焦丨漫途科技亮相2025西北水务博览会!

2025第三届西北水务数字化发展论坛暨供排水节水灌溉新技术设备博览会在兰州甘肃国际会展中心圆满落幕。本届展会以“科技赋能水资源&#xff0c;数智引领新动能”为主题&#xff0c;活动汇集水务集团、科研院所、技术供应商等全产业链参与者&#xff0c;旨在通过前沿技术展示与…

【数据结构初阶】顺序表的应用

文章目录 顺序表的应用基于动态顺序表实现通讯录前言1.定义联系人数据2.给顺序表改名3.通讯录的初始化4.通讯录的销毁5.通讯录添加数据6.通讯录删除数据7.通讯录修改数据8.通讯录查找数据9.展示通讯录数据10.通讯录的最终实现 顺序表的应用 基于动态顺序表实现通讯录 前言 功…

C#数字图像处理(一)

文章目录 1.C#图像处理基础1.1 Bitmap类1.2 Bitmapdata类1.3 Graphics类1.4 Image类 2.彩色图像灰度化1.提取像素法2.内存法3.指针法三种方法的比较4.灰度图像二值化&#xff1a; 3.相关链接 Bitmap类、 Bitmapdata类和 Graphics类是C#图像处理中最重要的3个类,如果要用C# 进行…

麻省理工新突破:家庭场景下机器人实现精准控制,real-to-sim-to-real学习助力

麻省理工学院电气工程与计算机科学系Pulkit Agrawal教授&#xff0c;介绍了一种新方法&#xff0c;可以让机器人在扫描的家庭环境模拟中接受训练&#xff0c;为任何人都可以实现定制的家庭自动化铺平了道路。 本文将探讨通过Franka机器人在虚拟环境中训练的特点&#xff0c;研…

从零实现本地语音识别(FunASR)

FunASR 是达摩院开源的综合性语音处理工具包&#xff0c;提供语音识别&#xff08;ASR&#xff09;、语音活动检测&#xff08;VAD&#xff09;、标点恢复&#xff08;PUNC&#xff09;等全流程功能&#xff0c;支持多种主流模型&#xff08;如 Paraformer、Whisper、SenseVoic…

已解决:.NetCore控制台程序(WebAPI)假死,程序挂起接口不通

本问题已得到解决&#xff0c;请看以下小结&#xff1a; 关于《.NetCore控制台程序(WebAPI)假死,程序暂停接口不通》的解决方案 记录备注报错时间2025年报错版本VS2022 WINDOWS10报错复现鼠标点一下控制台&#xff0c;会卡死报错描述——报错截图——报错原因 控制台启用了“快…

Excel如何分开查看工作表方便数据撰写

首先我这里有2class和3class两个工作表 接下来我们点击视图 按照顺序分别点击新建窗口和全部重排 ### 然后就是这样 接下来就OK了

微软技术赋能:解锁开发、交互与数据潜力,共探未来创新路

在微软 Build 2025 大会以及创想未来峰会上&#xff0c;微软展示的一系列前沿技术与创新应用&#xff0c;不仅展现了其在科技领域的深厚底蕴与前瞻视野&#xff0c;更为开发者和企业带来了前所未有的机遇与变革动力。 领驭科技作为微软中国南区核心合作伙伴及 HKCSP 1T 首批授…

VR看房系统,新生代看房新体验

VR看房系统的概念 虚拟现实&#xff08;VirtualReality,VR&#xff09;看房系统&#xff0c;是近年来随着科技进步在房地产行业中兴起的一种创新看房方式。看房系统利用先进的计算机技术模拟出一个三维环境&#xff0c;使用户能够身临其境地浏览和体验房源&#xff0c;无需亲自…

【Linux笔记】Shell-脚本(下)|(常用命令详细版)

在&#xff08;上&#xff09;篇&#xff0c;我们详细的讲解了Shell脚本的基础知识和些许命令与实验&#xff0c;这次的的&#xff08;下&#xff09;篇&#xff0c;我们会详细讲解Shell脚本的常用命令 关于脚本的基础知识请各位移步到&#xff08;上&#xff09;篇啦~ Shell…