Skip to content
0

时间轴-v2

时间轴-v2

image-20250913065722252

问题

请帮我生成一个 时间轴 前端页面(主要用于记录自己一些关键日期发生的事件)。

另外,请帮我生成 一个 vue3版本的组件,不要使用Tailwind CSS,样式直接使用普通css写在组件内。

v1-2025.9.13-豆包生成

豆包输出

我将为您创建一个 Vue3 版本的时间轴组件,用于记录关键日期发生的事件。这个组件将包含事件展示、添加新事件和删除事件的功能,所有样式将使用普通 CSS 编写在组件内部。

Vue3时间轴组件

V1 Timeline.vue

vue
<template>
  <div class="timeline-container">
    <h1 class="timeline-title">我的时间轴</h1>
    
    <!-- 添加新事件的表单 -->
    <div class="add-event-form">
      <h2>添加新事件</h2>
      <div class="form-group">
        <label for="event-date">日期:</label>
        <input type="date" id="event-date" v-model="newEvent.date">
      </div>
      <div class="form-group">
        <label for="event-title">标题:</label>
        <input type="text" id="event-title" v-model="newEvent.title" placeholder="输入事件标题">
      </div>
      <div class="form-group">
        <label for="event-description">描述:</label>
        <textarea id="event-description" v-model="newEvent.description" placeholder="输入事件描述"></textarea>
      </div>
      <button @click="addEvent" class="add-button">添加事件</button>
    </div>
    
    <!-- 时间轴事件列表 -->
    <div class="timeline-events">
      <div v-if="sortedEvents.length === 0" class="no-events">
        暂无事件,请添加第一个事件吧!
      </div>
      
      <div 
        v-for="(event, index) in sortedEvents" 
        :key="index" 
        class="timeline-item"
        :class="{ 'timeline-item-left': index % 2 === 0, 'timeline-item-right': index % 2 !== 0 }"
      >
        <div class="timeline-dot"></div>
        <div class="timeline-content">
          <div class="event-date">{{ formatDate(event.date) }}</div>
          <h3 class="event-title">{{ event.title }}</h3>
          <p class="event-description">{{ event.description }}</p>
          <button @click="deleteEvent(index)" class="delete-button">删除</button>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue';

// 初始化事件列表,从本地存储加载(如果有)
const events = ref(JSON.parse(localStorage.getItem('timelineEvents')) || [
  {
    date: '2023-01-15',
    title: '开始新项目',
    description: '今天开始了我的第一个大型项目,感到既兴奋又紧张。'
  },
  {
    date: '2023-03-22',
    title: '完成重要里程碑',
    description: '成功完成了项目的第一个重要里程碑,团队一起庆祝了一下。'
  },
  {
    date: '2023-06-10',
    title: '参加技术研讨会',
    description: '今天参加了一个很有意义的技术研讨会,学到了很多新知识。'
  }
]);

// 新事件的数据
const newEvent = ref({
  date: '',
  title: '',
  description: ''
});

// 按日期排序事件(从早到晚)
const sortedEvents = computed(() => {
  return [...events.value].sort((a, b) => new Date(a.date) - new Date(b.date));
});

// 添加新事件
const addEvent = () => {
  if (!newEvent.value.date || !newEvent.value.title) {
    alert('请填写日期和标题');
    return;
  }
  
  events.value.push({...newEvent.value});
  
  // 保存到本地存储,防止页面刷新后数据丢失
  localStorage.setItem('timelineEvents', JSON.stringify(events.value));
  
  // 重置表单
  newEvent.value = {
    date: '',
    title: '',
    description: ''
  };
};

// 删除事件
const deleteEvent = (index) => {
  if (confirm('确定要删除这个事件吗?')) {
    events.value.splice(index, 1);
    localStorage.setItem('timelineEvents', JSON.stringify(events.value));
  }
};

// 格式化日期显示
const formatDate = (dateString) => {
  const date = new Date(dateString);
  return date.toLocaleDateString('zh-CN', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  });
};
</script>

<style scoped>
.timeline-container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
  font-family: 'Arial', sans-serif;
  color: #333;
}

.timeline-title {
  text-align: center;
  color: #2c3e50;
  margin-bottom: 40px;
  font-size: 2.5rem;
}

.add-event-form {
  background-color: #f8f9fa;
  padding: 25px;
  border-radius: 8px;
  margin-bottom: 50px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.add-event-form h2 {
  margin-top: 0;
  color: #34495e;
  margin-bottom: 20px;
}

.form-group {
  margin-bottom: 15px;
}

.form-group label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
  color: #2c3e50;
}

.form-group input,
.form-group textarea {
  width: 100%;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 1rem;
}

.form-group textarea {
  min-height: 100px;
  resize: vertical;
}

.add-button {
  background-color: #3498db;
  color: white;
  border: none;
  padding: 10px 20px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 1rem;
  transition: background-color 0.3s;
}

.add-button:hover {
  background-color: #2980b9;
}

.timeline-events {
  position: relative;
  padding: 20px 0;
}

/* 时间轴中心线 */
.timeline-events::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 4px;
  background-color: #e0e0e0;
  transform: translateX(-50%);
}

.timeline-item {
  margin-bottom: 40px;
  position: relative;
  width: 50%;
}

.timeline-item-left {
  left: 0;
  padding-right: 40px;
}

.timeline-item-right {
  left: 50%;
  padding-left: 40px;
}

/* 时间点 */
.timeline-dot {
  position: absolute;
  width: 20px;
  height: 20px;
  background-color: #3498db;
  border-radius: 50%;
  top: 10px;
  z-index: 1;
}

.timeline-item-left .timeline-dot {
  right: -10px;
}

.timeline-item-right .timeline-dot {
  left: -10px;
}

.timeline-content {
  background-color: white;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.event-date {
  color: #7f8c8d;
  font-size: 0.9rem;
  margin-bottom: 5px;
}

.event-title {
  margin: 0 0 10px 0;
  color: #2c3e50;
}

.event-description {
  margin: 0;
  color: #34495e;
  line-height: 1.6;
}

.delete-button {
  background-color: #e74c3c;
  color: white;
  border: none;
  padding: 5px 10px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.9rem;
  margin-top: 10px;
  transition: background-color 0.3s;
}

.delete-button:hover {
  background-color: #c0392b;
}

.no-events {
  text-align: center;
  color: #7f8c8d;
  padding: 40px;
  font-size: 1.2rem;
}

/* 响应式设计 - 适配移动设备 */
@media (max-width: 768px) {
  .timeline-events::before {
    left: 30px;
  }
  
  .timeline-item {
    width: 100%;
    padding-left: 70px;
    padding-right: 25px;
  }
  
  .timeline-item-left,
  .timeline-item-right {
    left: 0;
  }
  
  .timeline-item-left .timeline-dot,
  .timeline-item-right .timeline-dot {
    left: 20px;
  }
}
</style>

创建时间:06:22

这个时间轴组件具有以下特点:

  1. 完整的功能
    • 展示按时间顺序排列的事件
    • 支持添加新事件(包含日期、标题和描述)
    • 可以删除已有事件
    • 使用 localStorage 保存数据,页面刷新后不会丢失
  2. 视觉设计
    • 采用左右交替的布局,使时间轴更加美观
    • 中间有一条垂直线作为时间轴的主线
    • 每个事件都有一个圆点标记在时间轴上
    • 包含适当的阴影和圆角,增强视觉层次感
  3. 响应式布局
    • 在大屏幕上使用左右交替布局
    • 在移动设备上自动调整为单列布局
    • 适配不同尺寸的屏幕
  4. 用户体验
    • 表单验证确保日期和标题不为空
    • 删除前有确认提示
    • 按钮有悬停效果
    • 日期显示格式友好(例如:2023 年 1 月 15 日)

您可以直接使用这个组件,也可以根据需要调整样式或添加更多功能,如编辑事件、搜索事件等。

效果测试

image-20250913063035803

image-20250913063046920

存在问题

问题1:这个 添加新事件 在最前面,显得不太好看

image-20250913063035803

问题2:每个事件都有一个 红色的删除按钮,也显得很不好看

image-20250913063046920

v2-2025.9.13-豆包生成-解决2个存在的问题

提出问题

bash
存在如下2个问题:
问题1:这个 添加新事件 在最前面,显得不太好看
问题2:每个事件都有一个 红色的删除按钮,也显得很不好看

请帮我按上面提出的2个问题 继续 优化下这个代码

豆包生成

我根据您提出的两个问题进行了优化:

  1. 将 "添加新事件" 表单移动到了页面底部,使时间轴内容更加突出
  2. 改进了删除按钮的样式,使其平时隐藏,鼠标悬停在事件上时才显示,并且采用了更柔和的红色

优化后的Vue3时间轴组件

V2

vue
<template>
  <div class="timeline-container">
    <h1 class="timeline-title">我的时间轴</h1>
    
    <!-- 时间轴事件列表 -->
    <div class="timeline-events">
      <div v-if="sortedEvents.length === 0" class="no-events">
        暂无事件,请添加第一个事件吧!
      </div>
      
      <div 
        v-for="(event, index) in sortedEvents" 
        :key="index" 
        class="timeline-item"
        :class="{ 'timeline-item-left': index % 2 === 0, 'timeline-item-right': index % 2 !== 0 }"
      >
        <div class="timeline-dot"></div>
        <div class="timeline-content">
          <div class="event-date">{{ formatDate(event.date) }}</div>
          <h3 class="event-title">{{ event.title }}</h3>
          <p class="event-description">{{ event.description }}</p>
          <button @click="deleteEvent(index)" class="delete-button">删除</button>
        </div>
      </div>
    </div>
    
    <!-- 添加新事件的表单(移到了页面底部) -->
    <div class="add-event-form">
      <h2>添加新事件</h2>
      <div class="form-group">
        <label for="event-date">日期:</label>
        <input type="date" id="event-date" v-model="newEvent.date">
      </div>
      <div class="form-group">
        <label for="event-title">标题:</label>
        <input type="text" id="event-title" v-model="newEvent.title" placeholder="输入事件标题">
      </div>
      <div class="form-group">
        <label for="event-description">描述:</label>
        <textarea id="event-description" v-model="newEvent.description" placeholder="输入事件描述"></textarea>
      </div>
      <button @click="addEvent" class="add-button">添加事件</button>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue';

// 初始化事件列表,从本地存储加载(如果有)
const events = ref(JSON.parse(localStorage.getItem('timelineEvents')) || [
  {
    date: '2023-01-15',
    title: '开始新项目',
    description: '今天开始了我的第一个大型项目,感到既兴奋又紧张。'
  },
  {
    date: '2023-03-22',
    title: '完成重要里程碑',
    description: '成功完成了项目的第一个重要里程碑,团队一起庆祝了一下。'
  },
  {
    date: '2023-06-10',
    title: '参加技术研讨会',
    description: '今天参加了一个很有意义的技术研讨会,学到了很多新知识。'
  }
]);

// 新事件的数据
const newEvent = ref({
  date: '',
  title: '',
  description: ''
});

// 按日期排序事件(从早到晚)
const sortedEvents = computed(() => {
  return [...events.value].sort((a, b) => new Date(a.date) - new Date(b.date));
});

// 添加新事件
const addEvent = () => {
  if (!newEvent.value.date || !newEvent.value.title) {
    alert('请填写日期和标题');
    return;
  }
  
  events.value.push({...newEvent.value});
  
  // 保存到本地存储,防止页面刷新后数据丢失
  localStorage.setItem('timelineEvents', JSON.stringify(events.value));
  
  // 重置表单
  newEvent.value = {
    date: '',
    title: '',
    description: ''
  };
};

// 删除事件
const deleteEvent = (index) => {
  if (confirm('确定要删除这个事件吗?')) {
    events.value.splice(index, 1);
    localStorage.setItem('timelineEvents', JSON.stringify(events.value));
  }
};

// 格式化日期显示
const formatDate = (dateString) => {
  const date = new Date(dateString);
  return date.toLocaleDateString('zh-CN', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  });
};
</script>

<style scoped>
.timeline-container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
  font-family: 'Arial', sans-serif;
  color: #333;
}

.timeline-title {
  text-align: center;
  color: #2c3e50;
  margin-bottom: 40px;
  font-size: 2.5rem;
}

.add-event-form {
  background-color: #f8f9fa;
  padding: 25px;
  border-radius: 8px;
  margin-top: 50px; /* 调整为顶部margin,因为移到了底部 */
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.add-event-form h2 {
  margin-top: 0;
  color: #34495e;
  margin-bottom: 20px;
}

.form-group {
  margin-bottom: 15px;
}

.form-group label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
  color: #2c3e50;
}

.form-group input,
.form-group textarea {
  width: 100%;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 1rem;
}

.form-group textarea {
  min-height: 100px;
  resize: vertical;
}

.add-button {
  background-color: #3498db;
  color: white;
  border: none;
  padding: 10px 20px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 1rem;
  transition: background-color 0.3s;
}

.add-button:hover {
  background-color: #2980b9;
}

.timeline-events {
  position: relative;
  padding: 20px 0;
}

/* 时间轴中心线 */
.timeline-events::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 4px;
  background-color: #e0e0e0;
  transform: translateX(-50%);
}

.timeline-item {
  margin-bottom: 40px;
  position: relative;
  width: 50%;
}

.timeline-item-left {
  left: 0;
  padding-right: 40px;
}

.timeline-item-right {
  left: 50%;
  padding-left: 40px;
}

/* 时间点 */
.timeline-dot {
  position: absolute;
  width: 20px;
  height: 20px;
  background-color: #3498db;
  border-radius: 50%;
  top: 10px;
  z-index: 1;
}

.timeline-item-left .timeline-dot {
  right: -10px;
}

.timeline-item-right .timeline-dot {
  left: -10px;
}

.timeline-content {
  background-color: white;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  transition: transform 0.3s;
}

/* 鼠标悬停时轻微上浮效果 */
.timeline-item:hover .timeline-content {
  transform: translateY(-5px);
}

.event-date {
  color: #7f8c8d;
  font-size: 0.9rem;
  margin-bottom: 5px;
}

.event-title {
  margin: 0 0 10px 0;
  color: #2c3e50;
}

.event-description {
  margin: 0;
  color: #34495e;
  line-height: 1.6;
  margin-bottom: 10px;
}

/* 改进的删除按钮样式 */
.delete-button {
  background-color: #ff6b6b;
  color: white;
  border: none;
  padding: 5px 10px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.9rem;
  transition: all 0.3s;
  opacity: 0; /* 默认隐藏 */
  transform: translateY(5px);
}

/* 鼠标悬停在事件上时显示删除按钮 */
.timeline-item:hover .delete-button {
  opacity: 1;
  transform: translateY(0);
}

.delete-button:hover {
  background-color: #ff5252;
}

.no-events {
  text-align: center;
  color: #7f8c8d;
  padding: 40px;
  font-size: 1.2rem;
}

/* 响应式设计 - 适配移动设备 */
@media (max-width: 768px) {
  .timeline-events::before {
    left: 30px;
  }
  
  .timeline-item {
    width: 100%;
    padding-left: 70px;
    padding-right: 25px;
  }
  
  .timeline-item-left,
  .timeline-item-right {
    left: 0;
  }
  
  .timeline-item-left .timeline-dot,
  .timeline-item-right .timeline-dot {
    left: 20px;
  }
}
</style>

创建时间:06:32

除了您提出的两个主要优化点外,我还增加了一个小细节:当鼠标悬停在事件卡片上时,卡片会有一个轻微上浮的效果,增强了交互感。

这些改进使得整个时间轴更加整洁,删除操作不再干扰正常浏览,同时添加新事件的功能也不会喧宾夺主,而是作为一个辅助功能放在页面底部。

效果测试

存在问题

问题1:这个部署到自己博客后,其它用户是不是也能直接删除这个事件呢?或者 也能新增新事件呢? 正常情况,我不需要其他用户具有这个修改的权限,他们只需要查看的权限。 修改的权限只能是我自己。请帮我继续优化下,输出完整的代码。

v3-2025.9.13-豆包生成-带权限控制的Vue3时间轴组件(正在使用💖)

提出问题

这个部署到自己博客后,其它用户是不是也能直接删除这个事件呢?或者 也能新增新事件呢? 正常情况,我不需要其他用户具有这个修改的权限,他们只需要查看的权限。 修改的权限只能是我自己。请帮我继续优化下,输出完整的代码。

豆包生成

为了解决权限控制的问题,我可以添加一个简单的密码验证机制,只有输入正确密码的用户(也就是您自己)才能看到并使用添加和删除功能,其他用户只能查看时间轴内容。

带权限控制的Vue3时间轴组件

V3

vue
<template>
  <div class="timeline-container">
    <h1 class="timeline-title">我的时间轴</h1>
    
    <!-- 密码验证区域 -->
    <div v-if="!isAuthenticated && showAuthForm" class="auth-form">
      <h2>管理员验证</h2>
      <input 
        type="password" 
        v-model="adminPassword" 
        placeholder="输入管理员密码"
        @keyup.enter="authenticate"
        class="password-input"
      >
      <button @click="authenticate" class="auth-button">验证</button>
      <p v-if="authError" class="auth-error">密码错误,请重试</p>
    </div>
    
    <!-- 显示管理按钮(仅管理员可见) -->
    <div v-if="sortedEvents.length > 0 && !showAuthForm && !isAuthenticated" class="manage-controls">
      <button @click="showAuthForm = true" class="manage-button">管理时间轴</button>
    </div>
    
    <!-- 时间轴事件列表 -->
    <div class="timeline-events">
      <div v-if="sortedEvents.length === 0" class="no-events">
        暂无事件,请添加第一个事件吧!
      </div>
      
      <div 
        v-for="(event, index) in sortedEvents" 
        :key="index" 
        class="timeline-item"
        :class="{ 'timeline-item-left': index % 2 === 0, 'timeline-item-right': index % 2 !== 0 }"
      >
        <div class="timeline-dot"></div>
        <div class="timeline-content">
          <div class="event-date">{{ formatDate(event.date) }}</div>
          <h3 class="event-title">{{ event.title }}</h3>
          <p class="event-description">{{ event.description }}</p>
          <!-- 仅管理员可见的删除按钮 -->
          <button 
            @click="deleteEvent(index)" 
            class="delete-button"
            v-if="isAuthenticated"
          >
            删除
          </button>
        </div>
      </div>
    </div>
    
    <!-- 添加新事件的表单(仅管理员可见) -->
    <div class="add-event-form" v-if="isAuthenticated">
      <h2>添加新事件</h2>
      <div class="form-group">
        <label for="event-date">日期:</label>
        <input type="date" id="event-date" v-model="newEvent.date">
      </div>
      <div class="form-group">
        <label for="event-title">标题:</label>
        <input type="text" id="event-title" v-model="newEvent.title" placeholder="输入事件标题">
      </div>
      <div class="form-group">
        <label for="event-description">描述:</label>
        <textarea id="event-description" v-model="newEvent.description" placeholder="输入事件描述"></textarea>
      </div>
      <button @click="addEvent" class="add-button">添加事件</button>
      <button @click="logout" class="logout-button">退出管理模式</button>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue';

// 管理员密码(请在部署前修改为您自己的密码)
const ADMIN_PASSWORD = '1234'; // 这里请修改为您自己的密码

// 初始化事件列表,从本地存储加载(如果有)
const events = ref(JSON.parse(localStorage.getItem('timelineEvents')) || [
  {
    date: '2023-01-15',
    title: '开始新项目',
    description: '今天开始了我的第一个大型项目,感到既兴奋又紧张。'
  },
  {
    date: '2023-03-22',
    title: '完成重要里程碑',
    description: '成功完成了项目的第一个重要里程碑,团队一起庆祝了一下。'
  },
  {
    date: '2023-06-10',
    title: '参加技术研讨会',
    description: '今天参加了一个很有意义的技术研讨会,学到了很多新知识。'
  }
]);

// 权限控制相关
const isAuthenticated = ref(false);
const showAuthForm = ref(false);
const adminPassword = ref('');
const authError = ref(false);

// 新事件的数据
const newEvent = ref({
  date: '',
  title: '',
  description: ''
});

// 按日期排序事件(从早到晚)
const sortedEvents = computed(() => {
  return [...events.value].sort((a, b) => new Date(a.date) - new Date(b.date));
});

// 验证管理员身份
const authenticate = () => {
  if (adminPassword.value === ADMIN_PASSWORD) {
    isAuthenticated.value = true;
    showAuthForm.value = false;
    authError.value = false;
    adminPassword.value = '';
  } else {
    authError.value = true;
  }
};

// 退出管理模式
const logout = () => {
  isAuthenticated.value = false;
};

// 添加新事件(仅管理员可执行)
const addEvent = () => {
  if (!newEvent.value.date || !newEvent.value.title) {
    alert('请填写日期和标题');
    return;
  }
  
  events.value.push({...newEvent.value});
  
  // 保存到本地存储
  localStorage.setItem('timelineEvents', JSON.stringify(events.value));
  
  // 重置表单
  newEvent.value = {
    date: '',
    title: '',
    description: ''
  };
};

// 删除事件(仅管理员可执行)
const deleteEvent = (index) => {
  if (confirm('确定要删除这个事件吗?')) {
    events.value.splice(index, 1);
    localStorage.setItem('timelineEvents', JSON.stringify(events.value));
  }
};

// 格式化日期显示
const formatDate = (dateString) => {
  const date = new Date(dateString);
  return date.toLocaleDateString('zh-CN', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  });
};
</script>

<style scoped>
.timeline-container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
  font-family: 'Arial', sans-serif;
  color: #333;
}

.timeline-title {
  text-align: center;
  color: #2c3e50;
  margin-bottom: 40px;
  font-size: 2.5rem;
}

/* 权限验证样式 */
.auth-form {
  max-width: 400px;
  margin: 0 auto 40px;
  text-align: center;
  padding: 20px;
  background-color: #f8f9fa;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.password-input {
  width: 80%;
  padding: 10px;
  margin-bottom: 15px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 1rem;
}

.auth-button {
  background-color: #3498db;
  color: white;
  border: none;
  padding: 10px 20px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 1rem;
  transition: background-color 0.3s;
}

.auth-button:hover {
  background-color: #2980b9;
}

.auth-error {
  color: #e74c3c;
  margin-top: 10px;
  font-size: 0.9rem;
}

.manage-controls {
  text-align: center;
  margin-bottom: 30px;
}

.manage-button {
  background-color: #f1c40f;
  color: #333;
  border: none;
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.9rem;
  transition: background-color 0.3s;
}

.manage-button:hover {
  background-color: #d4ac0d;
}

.add-event-form {
  background-color: #f8f9fa;
  padding: 25px;
  border-radius: 8px;
  margin-top: 50px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.add-event-form h2 {
  margin-top: 0;
  color: #34495e;
  margin-bottom: 20px;
}

.form-group {
  margin-bottom: 15px;
}

.form-group label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
  color: #2c3e50;
}

.form-group input,
.form-group textarea {
  width: 100%;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 1rem;
}

.form-group textarea {
  min-height: 100px;
  resize: vertical;
}

.add-button {
  background-color: #3498db;
  color: white;
  border: none;
  padding: 10px 20px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 1rem;
  transition: background-color 0.3s;
  margin-right: 10px;
}

.add-button:hover {
  background-color: #2980b9;
}

.logout-button {
  background-color: #95a5a6;
  color: white;
  border: none;
  padding: 10px 20px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 1rem;
  transition: background-color 0.3s;
}

.logout-button:hover {
  background-color: #7f8c8d;
}

.timeline-events {
  position: relative;
  padding: 20px 0;
}

/* 时间轴中心线 */
.timeline-events::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 4px;
  background-color: #e0e0e0;
  transform: translateX(-50%);
}

.timeline-item {
  margin-bottom: 40px;
  position: relative;
  width: 50%;
}

.timeline-item-left {
  left: 0;
  padding-right: 40px;
}

.timeline-item-right {
  left: 50%;
  padding-left: 40px;
}

/* 时间点 */
.timeline-dot {
  position: absolute;
  width: 20px;
  height: 20px;
  background-color: #3498db;
  border-radius: 50%;
  top: 10px;
  z-index: 1;
}

.timeline-item-left .timeline-dot {
  right: -10px;
}

.timeline-item-right .timeline-dot {
  left: -10px;
}

.timeline-content {
  background-color: white;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  transition: transform 0.3s;
}

/* 鼠标悬停时轻微上浮效果 */
.timeline-item:hover .timeline-content {
  transform: translateY(-5px);
}

.event-date {
  color: #7f8c8d;
  font-size: 0.9rem;
  margin-bottom: 5px;
}

.event-title {
  margin: 0 0 10px 0;
  color: #2c3e50;
}

.event-description {
  margin: 0;
  color: #34495e;
  line-height: 1.6;
  margin-bottom: 10px;
}

/* 删除按钮样式 */
.delete-button {
  background-color: #ff6b6b;
  color: white;
  border: none;
  padding: 5px 10px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.9rem;
  transition: all 0.3s;
  opacity: 0;
  transform: translateY(5px);
}

.timeline-item:hover .delete-button {
  opacity: 1;
  transform: translateY(0);
}

.delete-button:hover {
  background-color: #ff5252;
}

.no-events {
  text-align: center;
  color: #7f8c8d;
  padding: 40px;
  font-size: 1.2rem;
}

/* 响应式设计 - 适配移动设备 */
@media (max-width: 768px) {
  .timeline-events::before {
    left: 30px;
  }
  
  .timeline-item {
    width: 100%;
    padding-left: 70px;
    padding-right: 25px;
  }
  
  .timeline-item-left,
  .timeline-item-right {
    left: 0;
  }
  
  .timeline-item-left .timeline-dot,
  .timeline-item-right .timeline-dot {
    left: 20px;
  }
  
  .auth-form {
    width: 90%;
  }
  
  .password-input {
    width: 90%;
  }
}
</style>

创建时间:06:38

这个优化版本增加了以下功能:

  1. 权限控制系统
    • 普通访客只能查看时间轴事件,看不到添加和删除按钮
    • 只有输入正确密码的用户(管理员)才能看到并使用添加和删除功能
    • 管理员可以退出管理模式,回到普通查看模式
  2. 使用说明
    • 请务必将代码中的 your_secure_password 替换为您自己的安全密码
    • 部署后,其他用户只能浏览时间轴内容,无法修改
    • 您自己可以通过点击 "管理时间轴" 按钮,输入密码后进入管理模式
  3. 用户体验优化
    • 密码输入区域有错误提示
    • 支持按 Enter 键提交密码
    • 管理模式下有退出按钮

这种方式提供了基本的权限控制,适合个人博客使用。如果需要更高级的安全控制,可以考虑集成后端用户认证系统。

分享

Tee接入

(1)新建docs\.vitepress\theme\components\Timeline.vue文件

vue
<template>
  <div class="timeline-container">
    <h1 class="timeline-title">我的时间轴</h1>
    
    <!-- 密码验证区域 -->
    <div v-if="!isAuthenticated && showAuthForm" class="auth-form">
      <h2>管理员验证</h2>
      <input 
        type="password" 
        v-model="adminPassword" 
        placeholder="输入管理员密码"
        @keyup.enter="authenticate"
        class="password-input"
      >
      <button @click="authenticate" class="auth-button">验证</button>
      <p v-if="authError" class="auth-error">密码错误,请重试</p>
    </div>
    
    <!-- 显示管理按钮(仅管理员可见) -->
    <div v-if="sortedEvents.length > 0 && !showAuthForm && !isAuthenticated" class="manage-controls">
      <button @click="showAuthForm = true" class="manage-button">管理时间轴</button>
    </div>
    
    <!-- 时间轴事件列表 -->
    <div class="timeline-events">
      <div v-if="sortedEvents.length === 0" class="no-events">
        暂无事件,请添加第一个事件吧!
      </div>
      
      <div 
        v-for="(event, index) in sortedEvents" 
        :key="index" 
        class="timeline-item"
        :class="{ 'timeline-item-left': index % 2 === 0, 'timeline-item-right': index % 2 !== 0 }"
      >
        <div class="timeline-dot"></div>
        <div class="timeline-content">
          <div class="event-date">{{ formatDate(event.date) }}</div>
          <h3 class="event-title">{{ event.title }}</h3>
          <p class="event-description">{{ event.description }}</p>
          <!-- 仅管理员可见的删除按钮 -->
          <button 
            @click="deleteEvent(index)" 
            class="delete-button"
            v-if="isAuthenticated"
          >
            删除
          </button>
        </div>
      </div>
    </div>
    
    <!-- 添加新事件的表单(仅管理员可见) -->
    <div class="add-event-form" v-if="isAuthenticated">
      <h2>添加新事件</h2>
      <div class="form-group">
        <label for="event-date">日期:</label>
        <input type="date" id="event-date" v-model="newEvent.date">
      </div>
      <div class="form-group">
        <label for="event-title">标题:</label>
        <input type="text" id="event-title" v-model="newEvent.title" placeholder="输入事件标题">
      </div>
      <div class="form-group">
        <label for="event-description">描述:</label>
        <textarea id="event-description" v-model="newEvent.description" placeholder="输入事件描述"></textarea>
      </div>
      <button @click="addEvent" class="add-button">添加事件</button>
      <button @click="logout" class="logout-button">退出管理模式</button>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue';

// 管理员密码(请在部署前修改为您自己的密码)
const ADMIN_PASSWORD = '1234'; // 这里请修改为您自己的密码

// 初始化事件列表,从本地存储加载(如果有)
const events = ref(JSON.parse(localStorage.getItem('timelineEvents')) || [
  {
    date: '2023-01-15',
    title: '开始新项目',
    description: '今天开始了我的第一个大型项目,感到既兴奋又紧张。'
  },
  {
    date: '2023-03-22',
    title: '完成重要里程碑',
    description: '成功完成了项目的第一个重要里程碑,团队一起庆祝了一下。'
  },
  {
    date: '2023-06-10',
    title: '参加技术研讨会',
    description: '今天参加了一个很有意义的技术研讨会,学到了很多新知识。'
  }
]);

// 权限控制相关
const isAuthenticated = ref(false);
const showAuthForm = ref(false);
const adminPassword = ref('');
const authError = ref(false);

// 新事件的数据
const newEvent = ref({
  date: '',
  title: '',
  description: ''
});

// 按日期排序事件(从早到晚)
const sortedEvents = computed(() => {
  return [...events.value].sort((a, b) => new Date(a.date) - new Date(b.date));
});

// 验证管理员身份
const authenticate = () => {
  if (adminPassword.value === ADMIN_PASSWORD) {
    isAuthenticated.value = true;
    showAuthForm.value = false;
    authError.value = false;
    adminPassword.value = '';
  } else {
    authError.value = true;
  }
};

// 退出管理模式
const logout = () => {
  isAuthenticated.value = false;
};

// 添加新事件(仅管理员可执行)
const addEvent = () => {
  if (!newEvent.value.date || !newEvent.value.title) {
    alert('请填写日期和标题');
    return;
  }
  
  events.value.push({...newEvent.value});
  
  // 保存到本地存储
  localStorage.setItem('timelineEvents', JSON.stringify(events.value));
  
  // 重置表单
  newEvent.value = {
    date: '',
    title: '',
    description: ''
  };
};

// 删除事件(仅管理员可执行)
const deleteEvent = (index) => {
  if (confirm('确定要删除这个事件吗?')) {
    events.value.splice(index, 1);
    localStorage.setItem('timelineEvents', JSON.stringify(events.value));
  }
};

// 格式化日期显示
const formatDate = (dateString) => {
  const date = new Date(dateString);
  return date.toLocaleDateString('zh-CN', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  });
};
</script>

<style scoped>
.timeline-container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
  font-family: 'Arial', sans-serif;
  color: #333;
}

.timeline-title {
  text-align: center;
  color: #2c3e50;
  margin-bottom: 40px;
  font-size: 2.5rem;
}

/* 权限验证样式 */
.auth-form {
  max-width: 400px;
  margin: 0 auto 40px;
  text-align: center;
  padding: 20px;
  background-color: #f8f9fa;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.password-input {
  width: 80%;
  padding: 10px;
  margin-bottom: 15px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 1rem;
}

.auth-button {
  background-color: #3498db;
  color: white;
  border: none;
  padding: 10px 20px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 1rem;
  transition: background-color 0.3s;
}

.auth-button:hover {
  background-color: #2980b9;
}

.auth-error {
  color: #e74c3c;
  margin-top: 10px;
  font-size: 0.9rem;
}

.manage-controls {
  text-align: center;
  margin-bottom: 30px;
}

.manage-button {
  background-color: #f1c40f;
  color: #333;
  border: none;
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.9rem;
  transition: background-color 0.3s;
}

.manage-button:hover {
  background-color: #d4ac0d;
}

.add-event-form {
  background-color: #f8f9fa;
  padding: 25px;
  border-radius: 8px;
  margin-top: 50px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.add-event-form h2 {
  margin-top: 0;
  color: #34495e;
  margin-bottom: 20px;
}

.form-group {
  margin-bottom: 15px;
}

.form-group label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
  color: #2c3e50;
}

.form-group input,
.form-group textarea {
  width: 100%;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 1rem;
}

.form-group textarea {
  min-height: 100px;
  resize: vertical;
}

.add-button {
  background-color: #3498db;
  color: white;
  border: none;
  padding: 10px 20px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 1rem;
  transition: background-color 0.3s;
  margin-right: 10px;
}

.add-button:hover {
  background-color: #2980b9;
}

.logout-button {
  background-color: #95a5a6;
  color: white;
  border: none;
  padding: 10px 20px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 1rem;
  transition: background-color 0.3s;
}

.logout-button:hover {
  background-color: #7f8c8d;
}

.timeline-events {
  position: relative;
  padding: 20px 0;
}

/* 时间轴中心线 */
.timeline-events::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 4px;
  background-color: #e0e0e0;
  transform: translateX(-50%);
}

.timeline-item {
  margin-bottom: 40px;
  position: relative;
  width: 50%;
}

.timeline-item-left {
  left: 0;
  padding-right: 40px;
}

.timeline-item-right {
  left: 50%;
  padding-left: 40px;
}

/* 时间点 */
.timeline-dot {
  position: absolute;
  width: 20px;
  height: 20px;
  background-color: #3498db;
  border-radius: 50%;
  top: 10px;
  z-index: 1;
}

.timeline-item-left .timeline-dot {
  right: -10px;
}

.timeline-item-right .timeline-dot {
  left: -10px;
}

.timeline-content {
  background-color: white;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  transition: transform 0.3s;
}

/* 鼠标悬停时轻微上浮效果 */
.timeline-item:hover .timeline-content {
  transform: translateY(-5px);
}

.event-date {
  color: #7f8c8d;
  font-size: 0.9rem;
  margin-bottom: 5px;
}

.event-title {
  margin: 0 0 10px 0;
  color: #2c3e50;
}

.event-description {
  margin: 0;
  color: #34495e;
  line-height: 1.6;
  margin-bottom: 10px;
}

/* 删除按钮样式 */
.delete-button {
  background-color: #ff6b6b;
  color: white;
  border: none;
  padding: 5px 10px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.9rem;
  transition: all 0.3s;
  opacity: 0;
  transform: translateY(5px);
}

.timeline-item:hover .delete-button {
  opacity: 1;
  transform: translateY(0);
}

.delete-button:hover {
  background-color: #ff5252;
}

.no-events {
  text-align: center;
  color: #7f8c8d;
  padding: 40px;
  font-size: 1.2rem;
}

/* 响应式设计 - 适配移动设备 */
@media (max-width: 768px) {
  .timeline-events::before {
    left: 30px;
  }
  
  .timeline-item {
    width: 100%;
    padding-left: 70px;
    padding-right: 25px;
  }
  
  .timeline-item-left,
  .timeline-item-right {
    left: 0;
  }
  
  .timeline-item-left .timeline-dot,
  .timeline-item-right .timeline-dot {
    left: 20px;
  }
  
  .auth-form {
    width: 90%;
  }
  
  .password-input {
    width: 90%;
  }
}
</style>

(2)新建``文件

(3)测试

见下方。

效果测试

结束。

最近更新