跳到主要内容

时间轴-v2

最后更新于:

时间轴-v2

image-20250913065722252

问题

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

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

v1-2025.9.13-豆包生成

豆包输出

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

Vue3时间轴组件

V1 Timeline.vue

  1<template>
  2  <div class="timeline-container">
  3    <h1 class="timeline-title">我的时间轴</h1>
  4    
  5    <!-- 添加新事件的表单 -->
  6    <div class="add-event-form">
  7      <h2>添加新事件</h2>
  8      <div class="form-group">
  9        <label for="event-date">日期:</label>
 10        <input type="date" id="event-date" v-model="newEvent.date">
 11      </div>
 12      <div class="form-group">
 13        <label for="event-title">标题:</label>
 14        <input type="text" id="event-title" v-model="newEvent.title" placeholder="输入事件标题">
 15      </div>
 16      <div class="form-group">
 17        <label for="event-description">描述:</label>
 18        <textarea id="event-description" v-model="newEvent.description" placeholder="输入事件描述"></textarea>
 19      </div>
 20      <button @click="addEvent" class="add-button">添加事件</button>
 21    </div>
 22    
 23    <!-- 时间轴事件列表 -->
 24    <div class="timeline-events">
 25      <div v-if="sortedEvents.length === 0" class="no-events">
 26        暂无事件请添加第一个事件吧
 27      </div>
 28      
 29      <div 
 30        v-for="(event, index) in sortedEvents" 
 31        :key="index" 
 32        class="timeline-item"
 33        :class="{ 'timeline-item-left': index % 2 === 0, 'timeline-item-right': index % 2 !== 0 }"
 34      >
 35        <div class="timeline-dot"></div>
 36        <div class="timeline-content">
 37          <div class="event-date">{{ formatDate(event.date) }}</div>
 38          <h3 class="event-title">{{ event.title }}</h3>
 39          <p class="event-description">{{ event.description }}</p>
 40          <button @click="deleteEvent(index)" class="delete-button">删除</button>
 41        </div>
 42      </div>
 43    </div>
 44  </div>
 45</template>
 46
 47<script setup>
 48import { ref, computed } from 'vue';
 49
 50// 初始化事件列表,从本地存储加载(如果有)
 51const events = ref(JSON.parse(localStorage.getItem('timelineEvents')) || [
 52  {
 53    date: '2023-01-15',
 54    title: '开始新项目',
 55    description: '今天开始了我的第一个大型项目,感到既兴奋又紧张。'
 56  },
 57  {
 58    date: '2023-03-22',
 59    title: '完成重要里程碑',
 60    description: '成功完成了项目的第一个重要里程碑,团队一起庆祝了一下。'
 61  },
 62  {
 63    date: '2023-06-10',
 64    title: '参加技术研讨会',
 65    description: '今天参加了一个很有意义的技术研讨会,学到了很多新知识。'
 66  }
 67]);
 68
 69// 新事件的数据
 70const newEvent = ref({
 71  date: '',
 72  title: '',
 73  description: ''
 74});
 75
 76// 按日期排序事件(从早到晚)
 77const sortedEvents = computed(() => {
 78  return [...events.value].sort((a, b) => new Date(a.date) - new Date(b.date));
 79});
 80
 81// 添加新事件
 82const addEvent = () => {
 83  if (!newEvent.value.date || !newEvent.value.title) {
 84    alert('请填写日期和标题');
 85    return;
 86  }
 87  
 88  events.value.push({...newEvent.value});
 89  
 90  // 保存到本地存储,防止页面刷新后数据丢失
 91  localStorage.setItem('timelineEvents', JSON.stringify(events.value));
 92  
 93  // 重置表单
 94  newEvent.value = {
 95    date: '',
 96    title: '',
 97    description: ''
 98  };
 99};
100
101// 删除事件
102const deleteEvent = (index) => {
103  if (confirm('确定要删除这个事件吗?')) {
104    events.value.splice(index, 1);
105    localStorage.setItem('timelineEvents', JSON.stringify(events.value));
106  }
107};
108
109// 格式化日期显示
110const formatDate = (dateString) => {
111  const date = new Date(dateString);
112  return date.toLocaleDateString('zh-CN', {
113    year: 'numeric',
114    month: 'long',
115    day: 'numeric'
116  });
117};
118</script>
119
120<style scoped>
121.timeline-container {
122  max-width: 1200px;
123  margin: 0 auto;
124  padding: 20px;
125  font-family: 'Arial', sans-serif;
126  color: #333;
127}
128
129.timeline-title {
130  text-align: center;
131  color: #2c3e50;
132  margin-bottom: 40px;
133  font-size: 2.5rem;
134}
135
136.add-event-form {
137  background-color: #f8f9fa;
138  padding: 25px;
139  border-radius: 8px;
140  margin-bottom: 50px;
141  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
142}
143
144.add-event-form h2 {
145  margin-top: 0;
146  color: #34495e;
147  margin-bottom: 20px;
148}
149
150.form-group {
151  margin-bottom: 15px;
152}
153
154.form-group label {
155  display: block;
156  margin-bottom: 5px;
157  font-weight: bold;
158  color: #2c3e50;
159}
160
161.form-group input,
162.form-group textarea {
163  width: 100%;
164  padding: 10px;
165  border: 1px solid #ddd;
166  border-radius: 4px;
167  font-size: 1rem;
168}
169
170.form-group textarea {
171  min-height: 100px;
172  resize: vertical;
173}
174
175.add-button {
176  background-color: #3498db;
177  color: white;
178  border: none;
179  padding: 10px 20px;
180  border-radius: 4px;
181  cursor: pointer;
182  font-size: 1rem;
183  transition: background-color 0.3s;
184}
185
186.add-button:hover {
187  background-color: #2980b9;
188}
189
190.timeline-events {
191  position: relative;
192  padding: 20px 0;
193}
194
195/* 时间轴中心线 */
196.timeline-events::before {
197  content: '';
198  position: absolute;
199  top: 0;
200  bottom: 0;
201  left: 50%;
202  width: 4px;
203  background-color: #e0e0e0;
204  transform: translateX(-50%);
205}
206
207.timeline-item {
208  margin-bottom: 40px;
209  position: relative;
210  width: 50%;
211}
212
213.timeline-item-left {
214  left: 0;
215  padding-right: 40px;
216}
217
218.timeline-item-right {
219  left: 50%;
220  padding-left: 40px;
221}
222
223/* 时间点 */
224.timeline-dot {
225  position: absolute;
226  width: 20px;
227  height: 20px;
228  background-color: #3498db;
229  border-radius: 50%;
230  top: 10px;
231  z-index: 1;
232}
233
234.timeline-item-left .timeline-dot {
235  right: -10px;
236}
237
238.timeline-item-right .timeline-dot {
239  left: -10px;
240}
241
242.timeline-content {
243  background-color: white;
244  padding: 20px;
245  border-radius: 8px;
246  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
247}
248
249.event-date {
250  color: #7f8c8d;
251  font-size: 0.9rem;
252  margin-bottom: 5px;
253}
254
255.event-title {
256  margin: 0 0 10px 0;
257  color: #2c3e50;
258}
259
260.event-description {
261  margin: 0;
262  color: #34495e;
263  line-height: 1.6;
264}
265
266.delete-button {
267  background-color: #e74c3c;
268  color: white;
269  border: none;
270  padding: 5px 10px;
271  border-radius: 4px;
272  cursor: pointer;
273  font-size: 0.9rem;
274  margin-top: 10px;
275  transition: background-color 0.3s;
276}
277
278.delete-button:hover {
279  background-color: #c0392b;
280}
281
282.no-events {
283  text-align: center;
284  color: #7f8c8d;
285  padding: 40px;
286  font-size: 1.2rem;
287}
288
289/* 响应式设计 - 适配移动设备 */
290@media (max-width: 768px) {
291  .timeline-events::before {
292    left: 30px;
293  }
294  
295  .timeline-item {
296    width: 100%;
297    padding-left: 70px;
298    padding-right: 25px;
299  }
300  
301  .timeline-item-left,
302  .timeline-item-right {
303    left: 0;
304  }
305  
306  .timeline-item-left .timeline-dot,
307  .timeline-item-right .timeline-dot {
308    left: 20px;
309  }
310}
311</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个存在的问题

提出问题

1存在如下2个问题:
2问题1:这个 添加新事件 在最前面,显得不太好看
3问题2:每个事件都有一个 红色的删除按钮,也显得很不好看
4
5请帮我按上面提出的2个问题 继续 优化下这个代码

豆包生成

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

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

优化后的Vue3时间轴组件

V2

  1<template>
  2  <div class="timeline-container">
  3    <h1 class="timeline-title">我的时间轴</h1>
  4    
  5    <!-- 时间轴事件列表 -->
  6    <div class="timeline-events">
  7      <div v-if="sortedEvents.length === 0" class="no-events">
  8        暂无事件请添加第一个事件吧
  9      </div>
 10      
 11      <div 
 12        v-for="(event, index) in sortedEvents" 
 13        :key="index" 
 14        class="timeline-item"
 15        :class="{ 'timeline-item-left': index % 2 === 0, 'timeline-item-right': index % 2 !== 0 }"
 16      >
 17        <div class="timeline-dot"></div>
 18        <div class="timeline-content">
 19          <div class="event-date">{{ formatDate(event.date) }}</div>
 20          <h3 class="event-title">{{ event.title }}</h3>
 21          <p class="event-description">{{ event.description }}</p>
 22          <button @click="deleteEvent(index)" class="delete-button">删除</button>
 23        </div>
 24      </div>
 25    </div>
 26    
 27    <!-- 添加新事件的表单移到了页面底部 -->
 28    <div class="add-event-form">
 29      <h2>添加新事件</h2>
 30      <div class="form-group">
 31        <label for="event-date">日期:</label>
 32        <input type="date" id="event-date" v-model="newEvent.date">
 33      </div>
 34      <div class="form-group">
 35        <label for="event-title">标题:</label>
 36        <input type="text" id="event-title" v-model="newEvent.title" placeholder="输入事件标题">
 37      </div>
 38      <div class="form-group">
 39        <label for="event-description">描述:</label>
 40        <textarea id="event-description" v-model="newEvent.description" placeholder="输入事件描述"></textarea>
 41      </div>
 42      <button @click="addEvent" class="add-button">添加事件</button>
 43    </div>
 44  </div>
 45</template>
 46
 47<script setup>
 48import { ref, computed } from 'vue';
 49
 50// 初始化事件列表,从本地存储加载(如果有)
 51const events = ref(JSON.parse(localStorage.getItem('timelineEvents')) || [
 52  {
 53    date: '2023-01-15',
 54    title: '开始新项目',
 55    description: '今天开始了我的第一个大型项目,感到既兴奋又紧张。'
 56  },
 57  {
 58    date: '2023-03-22',
 59    title: '完成重要里程碑',
 60    description: '成功完成了项目的第一个重要里程碑,团队一起庆祝了一下。'
 61  },
 62  {
 63    date: '2023-06-10',
 64    title: '参加技术研讨会',
 65    description: '今天参加了一个很有意义的技术研讨会,学到了很多新知识。'
 66  }
 67]);
 68
 69// 新事件的数据
 70const newEvent = ref({
 71  date: '',
 72  title: '',
 73  description: ''
 74});
 75
 76// 按日期排序事件(从早到晚)
 77const sortedEvents = computed(() => {
 78  return [...events.value].sort((a, b) => new Date(a.date) - new Date(b.date));
 79});
 80
 81// 添加新事件
 82const addEvent = () => {
 83  if (!newEvent.value.date || !newEvent.value.title) {
 84    alert('请填写日期和标题');
 85    return;
 86  }
 87  
 88  events.value.push({...newEvent.value});
 89  
 90  // 保存到本地存储,防止页面刷新后数据丢失
 91  localStorage.setItem('timelineEvents', JSON.stringify(events.value));
 92  
 93  // 重置表单
 94  newEvent.value = {
 95    date: '',
 96    title: '',
 97    description: ''
 98  };
 99};
100
101// 删除事件
102const deleteEvent = (index) => {
103  if (confirm('确定要删除这个事件吗?')) {
104    events.value.splice(index, 1);
105    localStorage.setItem('timelineEvents', JSON.stringify(events.value));
106  }
107};
108
109// 格式化日期显示
110const formatDate = (dateString) => {
111  const date = new Date(dateString);
112  return date.toLocaleDateString('zh-CN', {
113    year: 'numeric',
114    month: 'long',
115    day: 'numeric'
116  });
117};
118</script>
119
120<style scoped>
121.timeline-container {
122  max-width: 1200px;
123  margin: 0 auto;
124  padding: 20px;
125  font-family: 'Arial', sans-serif;
126  color: #333;
127}
128
129.timeline-title {
130  text-align: center;
131  color: #2c3e50;
132  margin-bottom: 40px;
133  font-size: 2.5rem;
134}
135
136.add-event-form {
137  background-color: #f8f9fa;
138  padding: 25px;
139  border-radius: 8px;
140  margin-top: 50px; /* 调整为顶部margin,因为移到了底部 */
141  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
142}
143
144.add-event-form h2 {
145  margin-top: 0;
146  color: #34495e;
147  margin-bottom: 20px;
148}
149
150.form-group {
151  margin-bottom: 15px;
152}
153
154.form-group label {
155  display: block;
156  margin-bottom: 5px;
157  font-weight: bold;
158  color: #2c3e50;
159}
160
161.form-group input,
162.form-group textarea {
163  width: 100%;
164  padding: 10px;
165  border: 1px solid #ddd;
166  border-radius: 4px;
167  font-size: 1rem;
168}
169
170.form-group textarea {
171  min-height: 100px;
172  resize: vertical;
173}
174
175.add-button {
176  background-color: #3498db;
177  color: white;
178  border: none;
179  padding: 10px 20px;
180  border-radius: 4px;
181  cursor: pointer;
182  font-size: 1rem;
183  transition: background-color 0.3s;
184}
185
186.add-button:hover {
187  background-color: #2980b9;
188}
189
190.timeline-events {
191  position: relative;
192  padding: 20px 0;
193}
194
195/* 时间轴中心线 */
196.timeline-events::before {
197  content: '';
198  position: absolute;
199  top: 0;
200  bottom: 0;
201  left: 50%;
202  width: 4px;
203  background-color: #e0e0e0;
204  transform: translateX(-50%);
205}
206
207.timeline-item {
208  margin-bottom: 40px;
209  position: relative;
210  width: 50%;
211}
212
213.timeline-item-left {
214  left: 0;
215  padding-right: 40px;
216}
217
218.timeline-item-right {
219  left: 50%;
220  padding-left: 40px;
221}
222
223/* 时间点 */
224.timeline-dot {
225  position: absolute;
226  width: 20px;
227  height: 20px;
228  background-color: #3498db;
229  border-radius: 50%;
230  top: 10px;
231  z-index: 1;
232}
233
234.timeline-item-left .timeline-dot {
235  right: -10px;
236}
237
238.timeline-item-right .timeline-dot {
239  left: -10px;
240}
241
242.timeline-content {
243  background-color: white;
244  padding: 20px;
245  border-radius: 8px;
246  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
247  transition: transform 0.3s;
248}
249
250/* 鼠标悬停时轻微上浮效果 */
251.timeline-item:hover .timeline-content {
252  transform: translateY(-5px);
253}
254
255.event-date {
256  color: #7f8c8d;
257  font-size: 0.9rem;
258  margin-bottom: 5px;
259}
260
261.event-title {
262  margin: 0 0 10px 0;
263  color: #2c3e50;
264}
265
266.event-description {
267  margin: 0;
268  color: #34495e;
269  line-height: 1.6;
270  margin-bottom: 10px;
271}
272
273/* 改进的删除按钮样式 */
274.delete-button {
275  background-color: #ff6b6b;
276  color: white;
277  border: none;
278  padding: 5px 10px;
279  border-radius: 4px;
280  cursor: pointer;
281  font-size: 0.9rem;
282  transition: all 0.3s;
283  opacity: 0; /* 默认隐藏 */
284  transform: translateY(5px);
285}
286
287/* 鼠标悬停在事件上时显示删除按钮 */
288.timeline-item:hover .delete-button {
289  opacity: 1;
290  transform: translateY(0);
291}
292
293.delete-button:hover {
294  background-color: #ff5252;
295}
296
297.no-events {
298  text-align: center;
299  color: #7f8c8d;
300  padding: 40px;
301  font-size: 1.2rem;
302}
303
304/* 响应式设计 - 适配移动设备 */
305@media (max-width: 768px) {
306  .timeline-events::before {
307    left: 30px;
308  }
309  
310  .timeline-item {
311    width: 100%;
312    padding-left: 70px;
313    padding-right: 25px;
314  }
315  
316  .timeline-item-left,
317  .timeline-item-right {
318    left: 0;
319  }
320  
321  .timeline-item-left .timeline-dot,
322  .timeline-item-right .timeline-dot {
323    left: 20px;
324  }
325}
326</style>

创建时间:06:32

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

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

效果测试

存在问题

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

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

提出问题

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

豆包生成

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

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

V3

  1<template>
  2  <div class="timeline-container">
  3    <h1 class="timeline-title">我的时间轴</h1>
  4    
  5    <!-- 密码验证区域 -->
  6    <div v-if="!isAuthenticated && showAuthForm" class="auth-form">
  7      <h2>管理员验证</h2>
  8      <input 
  9        type="password" 
 10        v-model="adminPassword" 
 11        placeholder="输入管理员密码"
 12        @keyup.enter="authenticate"
 13        class="password-input"
 14      >
 15      <button @click="authenticate" class="auth-button">验证</button>
 16      <p v-if="authError" class="auth-error">密码错误请重试</p>
 17    </div>
 18    
 19    <!-- 显示管理按钮仅管理员可见 -->
 20    <div v-if="sortedEvents.length > 0 && !showAuthForm && !isAuthenticated" class="manage-controls">
 21      <button @click="showAuthForm = true" class="manage-button">管理时间轴</button>
 22    </div>
 23    
 24    <!-- 时间轴事件列表 -->
 25    <div class="timeline-events">
 26      <div v-if="sortedEvents.length === 0" class="no-events">
 27        暂无事件请添加第一个事件吧
 28      </div>
 29      
 30      <div 
 31        v-for="(event, index) in sortedEvents" 
 32        :key="index" 
 33        class="timeline-item"
 34        :class="{ 'timeline-item-left': index % 2 === 0, 'timeline-item-right': index % 2 !== 0 }"
 35      >
 36        <div class="timeline-dot"></div>
 37        <div class="timeline-content">
 38          <div class="event-date">{{ formatDate(event.date) }}</div>
 39          <h3 class="event-title">{{ event.title }}</h3>
 40          <p class="event-description">{{ event.description }}</p>
 41          <!-- 仅管理员可见的删除按钮 -->
 42          <button 
 43            @click="deleteEvent(index)" 
 44            class="delete-button"
 45            v-if="isAuthenticated"
 46          >
 47            删除
 48          </button>
 49        </div>
 50      </div>
 51    </div>
 52    
 53    <!-- 添加新事件的表单仅管理员可见 -->
 54    <div class="add-event-form" v-if="isAuthenticated">
 55      <h2>添加新事件</h2>
 56      <div class="form-group">
 57        <label for="event-date">日期:</label>
 58        <input type="date" id="event-date" v-model="newEvent.date">
 59      </div>
 60      <div class="form-group">
 61        <label for="event-title">标题:</label>
 62        <input type="text" id="event-title" v-model="newEvent.title" placeholder="输入事件标题">
 63      </div>
 64      <div class="form-group">
 65        <label for="event-description">描述:</label>
 66        <textarea id="event-description" v-model="newEvent.description" placeholder="输入事件描述"></textarea>
 67      </div>
 68      <button @click="addEvent" class="add-button">添加事件</button>
 69      <button @click="logout" class="logout-button">退出管理模式</button>
 70    </div>
 71  </div>
 72</template>
 73
 74<script setup>
 75import { ref, computed } from 'vue';
 76
 77// 管理员密码(请在部署前修改为您自己的密码)
 78const ADMIN_PASSWORD = '1234'; // 这里请修改为您自己的密码
 79
 80// 初始化事件列表,从本地存储加载(如果有)
 81const events = ref(JSON.parse(localStorage.getItem('timelineEvents')) || [
 82  {
 83    date: '2023-01-15',
 84    title: '开始新项目',
 85    description: '今天开始了我的第一个大型项目,感到既兴奋又紧张。'
 86  },
 87  {
 88    date: '2023-03-22',
 89    title: '完成重要里程碑',
 90    description: '成功完成了项目的第一个重要里程碑,团队一起庆祝了一下。'
 91  },
 92  {
 93    date: '2023-06-10',
 94    title: '参加技术研讨会',
 95    description: '今天参加了一个很有意义的技术研讨会,学到了很多新知识。'
 96  }
 97]);
 98
 99// 权限控制相关
100const isAuthenticated = ref(false);
101const showAuthForm = ref(false);
102const adminPassword = ref('');
103const authError = ref(false);
104
105// 新事件的数据
106const newEvent = ref({
107  date: '',
108  title: '',
109  description: ''
110});
111
112// 按日期排序事件(从早到晚)
113const sortedEvents = computed(() => {
114  return [...events.value].sort((a, b) => new Date(a.date) - new Date(b.date));
115});
116
117// 验证管理员身份
118const authenticate = () => {
119  if (adminPassword.value === ADMIN_PASSWORD) {
120    isAuthenticated.value = true;
121    showAuthForm.value = false;
122    authError.value = false;
123    adminPassword.value = '';
124  } else {
125    authError.value = true;
126  }
127};
128
129// 退出管理模式
130const logout = () => {
131  isAuthenticated.value = false;
132};
133
134// 添加新事件(仅管理员可执行)
135const addEvent = () => {
136  if (!newEvent.value.date || !newEvent.value.title) {
137    alert('请填写日期和标题');
138    return;
139  }
140  
141  events.value.push({...newEvent.value});
142  
143  // 保存到本地存储
144  localStorage.setItem('timelineEvents', JSON.stringify(events.value));
145  
146  // 重置表单
147  newEvent.value = {
148    date: '',
149    title: '',
150    description: ''
151  };
152};
153
154// 删除事件(仅管理员可执行)
155const deleteEvent = (index) => {
156  if (confirm('确定要删除这个事件吗?')) {
157    events.value.splice(index, 1);
158    localStorage.setItem('timelineEvents', JSON.stringify(events.value));
159  }
160};
161
162// 格式化日期显示
163const formatDate = (dateString) => {
164  const date = new Date(dateString);
165  return date.toLocaleDateString('zh-CN', {
166    year: 'numeric',
167    month: 'long',
168    day: 'numeric'
169  });
170};
171</script>
172
173<style scoped>
174.timeline-container {
175  max-width: 1200px;
176  margin: 0 auto;
177  padding: 20px;
178  font-family: 'Arial', sans-serif;
179  color: #333;
180}
181
182.timeline-title {
183  text-align: center;
184  color: #2c3e50;
185  margin-bottom: 40px;
186  font-size: 2.5rem;
187}
188
189/* 权限验证样式 */
190.auth-form {
191  max-width: 400px;
192  margin: 0 auto 40px;
193  text-align: center;
194  padding: 20px;
195  background-color: #f8f9fa;
196  border-radius: 8px;
197  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
198}
199
200.password-input {
201  width: 80%;
202  padding: 10px;
203  margin-bottom: 15px;
204  border: 1px solid #ddd;
205  border-radius: 4px;
206  font-size: 1rem;
207}
208
209.auth-button {
210  background-color: #3498db;
211  color: white;
212  border: none;
213  padding: 10px 20px;
214  border-radius: 4px;
215  cursor: pointer;
216  font-size: 1rem;
217  transition: background-color 0.3s;
218}
219
220.auth-button:hover {
221  background-color: #2980b9;
222}
223
224.auth-error {
225  color: #e74c3c;
226  margin-top: 10px;
227  font-size: 0.9rem;
228}
229
230.manage-controls {
231  text-align: center;
232  margin-bottom: 30px;
233}
234
235.manage-button {
236  background-color: #f1c40f;
237  color: #333;
238  border: none;
239  padding: 8px 16px;
240  border-radius: 4px;
241  cursor: pointer;
242  font-size: 0.9rem;
243  transition: background-color 0.3s;
244}
245
246.manage-button:hover {
247  background-color: #d4ac0d;
248}
249
250.add-event-form {
251  background-color: #f8f9fa;
252  padding: 25px;
253  border-radius: 8px;
254  margin-top: 50px;
255  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
256}
257
258.add-event-form h2 {
259  margin-top: 0;
260  color: #34495e;
261  margin-bottom: 20px;
262}
263
264.form-group {
265  margin-bottom: 15px;
266}
267
268.form-group label {
269  display: block;
270  margin-bottom: 5px;
271  font-weight: bold;
272  color: #2c3e50;
273}
274
275.form-group input,
276.form-group textarea {
277  width: 100%;
278  padding: 10px;
279  border: 1px solid #ddd;
280  border-radius: 4px;
281  font-size: 1rem;
282}
283
284.form-group textarea {
285  min-height: 100px;
286  resize: vertical;
287}
288
289.add-button {
290  background-color: #3498db;
291  color: white;
292  border: none;
293  padding: 10px 20px;
294  border-radius: 4px;
295  cursor: pointer;
296  font-size: 1rem;
297  transition: background-color 0.3s;
298  margin-right: 10px;
299}
300
301.add-button:hover {
302  background-color: #2980b9;
303}
304
305.logout-button {
306  background-color: #95a5a6;
307  color: white;
308  border: none;
309  padding: 10px 20px;
310  border-radius: 4px;
311  cursor: pointer;
312  font-size: 1rem;
313  transition: background-color 0.3s;
314}
315
316.logout-button:hover {
317  background-color: #7f8c8d;
318}
319
320.timeline-events {
321  position: relative;
322  padding: 20px 0;
323}
324
325/* 时间轴中心线 */
326.timeline-events::before {
327  content: '';
328  position: absolute;
329  top: 0;
330  bottom: 0;
331  left: 50%;
332  width: 4px;
333  background-color: #e0e0e0;
334  transform: translateX(-50%);
335}
336
337.timeline-item {
338  margin-bottom: 40px;
339  position: relative;
340  width: 50%;
341}
342
343.timeline-item-left {
344  left: 0;
345  padding-right: 40px;
346}
347
348.timeline-item-right {
349  left: 50%;
350  padding-left: 40px;
351}
352
353/* 时间点 */
354.timeline-dot {
355  position: absolute;
356  width: 20px;
357  height: 20px;
358  background-color: #3498db;
359  border-radius: 50%;
360  top: 10px;
361  z-index: 1;
362}
363
364.timeline-item-left .timeline-dot {
365  right: -10px;
366}
367
368.timeline-item-right .timeline-dot {
369  left: -10px;
370}
371
372.timeline-content {
373  background-color: white;
374  padding: 20px;
375  border-radius: 8px;
376  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
377  transition: transform 0.3s;
378}
379
380/* 鼠标悬停时轻微上浮效果 */
381.timeline-item:hover .timeline-content {
382  transform: translateY(-5px);
383}
384
385.event-date {
386  color: #7f8c8d;
387  font-size: 0.9rem;
388  margin-bottom: 5px;
389}
390
391.event-title {
392  margin: 0 0 10px 0;
393  color: #2c3e50;
394}
395
396.event-description {
397  margin: 0;
398  color: #34495e;
399  line-height: 1.6;
400  margin-bottom: 10px;
401}
402
403/* 删除按钮样式 */
404.delete-button {
405  background-color: #ff6b6b;
406  color: white;
407  border: none;
408  padding: 5px 10px;
409  border-radius: 4px;
410  cursor: pointer;
411  font-size: 0.9rem;
412  transition: all 0.3s;
413  opacity: 0;
414  transform: translateY(5px);
415}
416
417.timeline-item:hover .delete-button {
418  opacity: 1;
419  transform: translateY(0);
420}
421
422.delete-button:hover {
423  background-color: #ff5252;
424}
425
426.no-events {
427  text-align: center;
428  color: #7f8c8d;
429  padding: 40px;
430  font-size: 1.2rem;
431}
432
433/* 响应式设计 - 适配移动设备 */
434@media (max-width: 768px) {
435  .timeline-events::before {
436    left: 30px;
437  }
438  
439  .timeline-item {
440    width: 100%;
441    padding-left: 70px;
442    padding-right: 25px;
443  }
444  
445  .timeline-item-left,
446  .timeline-item-right {
447    left: 0;
448  }
449  
450  .timeline-item-left .timeline-dot,
451  .timeline-item-right .timeline-dot {
452    left: 20px;
453  }
454  
455  .auth-form {
456    width: 90%;
457  }
458  
459  .password-input {
460    width: 90%;
461  }
462}
463</style>

创建时间:06:38

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

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

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

分享

Tee接入

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

  1<template>
  2  <div class="timeline-container">
  3    <h1 class="timeline-title">我的时间轴</h1>
  4    
  5    <!-- 密码验证区域 -->
  6    <div v-if="!isAuthenticated && showAuthForm" class="auth-form">
  7      <h2>管理员验证</h2>
  8      <input 
  9        type="password" 
 10        v-model="adminPassword" 
 11        placeholder="输入管理员密码"
 12        @keyup.enter="authenticate"
 13        class="password-input"
 14      >
 15      <button @click="authenticate" class="auth-button">验证</button>
 16      <p v-if="authError" class="auth-error">密码错误请重试</p>
 17    </div>
 18    
 19    <!-- 显示管理按钮仅管理员可见 -->
 20    <div v-if="sortedEvents.length > 0 && !showAuthForm && !isAuthenticated" class="manage-controls">
 21      <button @click="showAuthForm = true" class="manage-button">管理时间轴</button>
 22    </div>
 23    
 24    <!-- 时间轴事件列表 -->
 25    <div class="timeline-events">
 26      <div v-if="sortedEvents.length === 0" class="no-events">
 27        暂无事件请添加第一个事件吧
 28      </div>
 29      
 30      <div 
 31        v-for="(event, index) in sortedEvents" 
 32        :key="index" 
 33        class="timeline-item"
 34        :class="{ 'timeline-item-left': index % 2 === 0, 'timeline-item-right': index % 2 !== 0 }"
 35      >
 36        <div class="timeline-dot"></div>
 37        <div class="timeline-content">
 38          <div class="event-date">{{ formatDate(event.date) }}</div>
 39          <h3 class="event-title">{{ event.title }}</h3>
 40          <p class="event-description">{{ event.description }}</p>
 41          <!-- 仅管理员可见的删除按钮 -->
 42          <button 
 43            @click="deleteEvent(index)" 
 44            class="delete-button"
 45            v-if="isAuthenticated"
 46          >
 47            删除
 48          </button>
 49        </div>
 50      </div>
 51    </div>
 52    
 53    <!-- 添加新事件的表单仅管理员可见 -->
 54    <div class="add-event-form" v-if="isAuthenticated">
 55      <h2>添加新事件</h2>
 56      <div class="form-group">
 57        <label for="event-date">日期:</label>
 58        <input type="date" id="event-date" v-model="newEvent.date">
 59      </div>
 60      <div class="form-group">
 61        <label for="event-title">标题:</label>
 62        <input type="text" id="event-title" v-model="newEvent.title" placeholder="输入事件标题">
 63      </div>
 64      <div class="form-group">
 65        <label for="event-description">描述:</label>
 66        <textarea id="event-description" v-model="newEvent.description" placeholder="输入事件描述"></textarea>
 67      </div>
 68      <button @click="addEvent" class="add-button">添加事件</button>
 69      <button @click="logout" class="logout-button">退出管理模式</button>
 70    </div>
 71  </div>
 72</template>
 73
 74<script setup>
 75import { ref, computed } from 'vue';
 76
 77// 管理员密码(请在部署前修改为您自己的密码)
 78const ADMIN_PASSWORD = '1234'; // 这里请修改为您自己的密码
 79
 80// 初始化事件列表,从本地存储加载(如果有)
 81const events = ref(JSON.parse(localStorage.getItem('timelineEvents')) || [
 82  {
 83    date: '2023-01-15',
 84    title: '开始新项目',
 85    description: '今天开始了我的第一个大型项目,感到既兴奋又紧张。'
 86  },
 87  {
 88    date: '2023-03-22',
 89    title: '完成重要里程碑',
 90    description: '成功完成了项目的第一个重要里程碑,团队一起庆祝了一下。'
 91  },
 92  {
 93    date: '2023-06-10',
 94    title: '参加技术研讨会',
 95    description: '今天参加了一个很有意义的技术研讨会,学到了很多新知识。'
 96  }
 97]);
 98
 99// 权限控制相关
100const isAuthenticated = ref(false);
101const showAuthForm = ref(false);
102const adminPassword = ref('');
103const authError = ref(false);
104
105// 新事件的数据
106const newEvent = ref({
107  date: '',
108  title: '',
109  description: ''
110});
111
112// 按日期排序事件(从早到晚)
113const sortedEvents = computed(() => {
114  return [...events.value].sort((a, b) => new Date(a.date) - new Date(b.date));
115});
116
117// 验证管理员身份
118const authenticate = () => {
119  if (adminPassword.value === ADMIN_PASSWORD) {
120    isAuthenticated.value = true;
121    showAuthForm.value = false;
122    authError.value = false;
123    adminPassword.value = '';
124  } else {
125    authError.value = true;
126  }
127};
128
129// 退出管理模式
130const logout = () => {
131  isAuthenticated.value = false;
132};
133
134// 添加新事件(仅管理员可执行)
135const addEvent = () => {
136  if (!newEvent.value.date || !newEvent.value.title) {
137    alert('请填写日期和标题');
138    return;
139  }
140  
141  events.value.push({...newEvent.value});
142  
143  // 保存到本地存储
144  localStorage.setItem('timelineEvents', JSON.stringify(events.value));
145  
146  // 重置表单
147  newEvent.value = {
148    date: '',
149    title: '',
150    description: ''
151  };
152};
153
154// 删除事件(仅管理员可执行)
155const deleteEvent = (index) => {
156  if (confirm('确定要删除这个事件吗?')) {
157    events.value.splice(index, 1);
158    localStorage.setItem('timelineEvents', JSON.stringify(events.value));
159  }
160};
161
162// 格式化日期显示
163const formatDate = (dateString) => {
164  const date = new Date(dateString);
165  return date.toLocaleDateString('zh-CN', {
166    year: 'numeric',
167    month: 'long',
168    day: 'numeric'
169  });
170};
171</script>
172
173<style scoped>
174.timeline-container {
175  max-width: 1200px;
176  margin: 0 auto;
177  padding: 20px;
178  font-family: 'Arial', sans-serif;
179  color: #333;
180}
181
182.timeline-title {
183  text-align: center;
184  color: #2c3e50;
185  margin-bottom: 40px;
186  font-size: 2.5rem;
187}
188
189/* 权限验证样式 */
190.auth-form {
191  max-width: 400px;
192  margin: 0 auto 40px;
193  text-align: center;
194  padding: 20px;
195  background-color: #f8f9fa;
196  border-radius: 8px;
197  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
198}
199
200.password-input {
201  width: 80%;
202  padding: 10px;
203  margin-bottom: 15px;
204  border: 1px solid #ddd;
205  border-radius: 4px;
206  font-size: 1rem;
207}
208
209.auth-button {
210  background-color: #3498db;
211  color: white;
212  border: none;
213  padding: 10px 20px;
214  border-radius: 4px;
215  cursor: pointer;
216  font-size: 1rem;
217  transition: background-color 0.3s;
218}
219
220.auth-button:hover {
221  background-color: #2980b9;
222}
223
224.auth-error {
225  color: #e74c3c;
226  margin-top: 10px;
227  font-size: 0.9rem;
228}
229
230.manage-controls {
231  text-align: center;
232  margin-bottom: 30px;
233}
234
235.manage-button {
236  background-color: #f1c40f;
237  color: #333;
238  border: none;
239  padding: 8px 16px;
240  border-radius: 4px;
241  cursor: pointer;
242  font-size: 0.9rem;
243  transition: background-color 0.3s;
244}
245
246.manage-button:hover {
247  background-color: #d4ac0d;
248}
249
250.add-event-form {
251  background-color: #f8f9fa;
252  padding: 25px;
253  border-radius: 8px;
254  margin-top: 50px;
255  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
256}
257
258.add-event-form h2 {
259  margin-top: 0;
260  color: #34495e;
261  margin-bottom: 20px;
262}
263
264.form-group {
265  margin-bottom: 15px;
266}
267
268.form-group label {
269  display: block;
270  margin-bottom: 5px;
271  font-weight: bold;
272  color: #2c3e50;
273}
274
275.form-group input,
276.form-group textarea {
277  width: 100%;
278  padding: 10px;
279  border: 1px solid #ddd;
280  border-radius: 4px;
281  font-size: 1rem;
282}
283
284.form-group textarea {
285  min-height: 100px;
286  resize: vertical;
287}
288
289.add-button {
290  background-color: #3498db;
291  color: white;
292  border: none;
293  padding: 10px 20px;
294  border-radius: 4px;
295  cursor: pointer;
296  font-size: 1rem;
297  transition: background-color 0.3s;
298  margin-right: 10px;
299}
300
301.add-button:hover {
302  background-color: #2980b9;
303}
304
305.logout-button {
306  background-color: #95a5a6;
307  color: white;
308  border: none;
309  padding: 10px 20px;
310  border-radius: 4px;
311  cursor: pointer;
312  font-size: 1rem;
313  transition: background-color 0.3s;
314}
315
316.logout-button:hover {
317  background-color: #7f8c8d;
318}
319
320.timeline-events {
321  position: relative;
322  padding: 20px 0;
323}
324
325/* 时间轴中心线 */
326.timeline-events::before {
327  content: '';
328  position: absolute;
329  top: 0;
330  bottom: 0;
331  left: 50%;
332  width: 4px;
333  background-color: #e0e0e0;
334  transform: translateX(-50%);
335}
336
337.timeline-item {
338  margin-bottom: 40px;
339  position: relative;
340  width: 50%;
341}
342
343.timeline-item-left {
344  left: 0;
345  padding-right: 40px;
346}
347
348.timeline-item-right {
349  left: 50%;
350  padding-left: 40px;
351}
352
353/* 时间点 */
354.timeline-dot {
355  position: absolute;
356  width: 20px;
357  height: 20px;
358  background-color: #3498db;
359  border-radius: 50%;
360  top: 10px;
361  z-index: 1;
362}
363
364.timeline-item-left .timeline-dot {
365  right: -10px;
366}
367
368.timeline-item-right .timeline-dot {
369  left: -10px;
370}
371
372.timeline-content {
373  background-color: white;
374  padding: 20px;
375  border-radius: 8px;
376  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
377  transition: transform 0.3s;
378}
379
380/* 鼠标悬停时轻微上浮效果 */
381.timeline-item:hover .timeline-content {
382  transform: translateY(-5px);
383}
384
385.event-date {
386  color: #7f8c8d;
387  font-size: 0.9rem;
388  margin-bottom: 5px;
389}
390
391.event-title {
392  margin: 0 0 10px 0;
393  color: #2c3e50;
394}
395
396.event-description {
397  margin: 0;
398  color: #34495e;
399  line-height: 1.6;
400  margin-bottom: 10px;
401}
402
403/* 删除按钮样式 */
404.delete-button {
405  background-color: #ff6b6b;
406  color: white;
407  border: none;
408  padding: 5px 10px;
409  border-radius: 4px;
410  cursor: pointer;
411  font-size: 0.9rem;
412  transition: all 0.3s;
413  opacity: 0;
414  transform: translateY(5px);
415}
416
417.timeline-item:hover .delete-button {
418  opacity: 1;
419  transform: translateY(0);
420}
421
422.delete-button:hover {
423  background-color: #ff5252;
424}
425
426.no-events {
427  text-align: center;
428  color: #7f8c8d;
429  padding: 40px;
430  font-size: 1.2rem;
431}
432
433/* 响应式设计 - 适配移动设备 */
434@media (max-width: 768px) {
435  .timeline-events::before {
436    left: 30px;
437  }
438  
439  .timeline-item {
440    width: 100%;
441    padding-left: 70px;
442    padding-right: 25px;
443  }
444  
445  .timeline-item-left,
446  .timeline-item-right {
447    left: 0;
448  }
449  
450  .timeline-item-left .timeline-dot,
451  .timeline-item-right .timeline-dot {
452    left: 20px;
453  }
454  
455  .auth-form {
456    width: 90%;
457  }
458  
459  .password-input {
460    width: 90%;
461  }
462}
463</style>

(2)新建``文件

(3)测试

见下方。

效果测试

结束。

最新文章

文档导航