重命名前端项目目录:web-flowith -> web

- 将前端项目目录从 web-flowith 重命名为 web,使目录结构更简洁
- 保持所有前端代码和配置文件不变
- 统一项目目录命名规范
This commit is contained in:
2025-07-24 22:20:19 +08:00
parent ca42a7d9a4
commit bbe8fcd776
57 changed files with 0 additions and 0 deletions
+778
View File
@@ -0,0 +1,778 @@
<template>
<div class="life-trajectory-page">
<!-- 头部 -->
<header class="page-header">
<div class="header-content">
<div class="header-left">
<a-button type="text" @click="$router.back()" class="back-btn">
<ArrowLeftOutlined />
</a-button>
<h1 class="page-title">人生轨迹</h1>
</div>
<a-button type="primary" @click="showNewEventModal = true" class="new-event-btn">
<PlusOutlined />
添加事件
</a-button>
</div>
</header>
<!-- 主要内容 -->
<main class="page-main">
<div class="container">
<!-- 筛选控制 -->
<div class="filter-section">
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索事件..."
style="width: 300px"
@search="handleSearch"
/>
<a-select
v-model:value="typeFilter"
placeholder="类型筛选"
style="width: 120px"
@change="handleFilterChange"
>
<a-select-option value="">全部</a-select-option>
<a-select-option value="milestone">里程碑</a-select-option>
<a-select-option value="achievement">成就</a-select-option>
<a-select-option value="memory">回忆</a-select-option>
<a-select-option value="goal">目标</a-select-option>
</a-select>
<a-range-picker
v-model:value="dateRange"
@change="handleDateRangeChange"
style="width: 240px"
/>
</div>
<!-- 时间线视图 -->
<div class="timeline-container">
<a-timeline class="life-timeline">
<a-timeline-item
v-for="event in filteredEvents"
:key="event.id"
:color="getEventColor(event.type)"
class="timeline-item"
>
<template #dot>
<div class="timeline-dot" :class="`dot-${event.type}`">
<component :is="getEventIcon(event.type)" />
</div>
</template>
<div class="event-card" @click="viewEventDetail(event)">
<div class="event-header">
<div class="event-meta">
<a-tag :color="getEventColor(event.type)" size="small">
{{ getEventTypeText(event.type) }}
</a-tag>
<span class="event-date">{{ formatTime.date(event.date) }}</span>
<div class="importance-stars">
<StarFilled
v-for="i in event.importance"
:key="i"
class="star-filled"
/>
<StarOutlined
v-for="i in (5 - event.importance)"
:key="i + event.importance"
class="star-empty"
/>
</div>
</div>
<a-dropdown @click.stop>
<a-button type="text" size="small">
<MoreOutlined />
</a-button>
<template #overlay>
<a-menu>
<a-menu-item @click="editEvent(event)">
<EditOutlined />
编辑
</a-menu-item>
<a-menu-item @click="deleteEvent(event.id)" danger>
<DeleteOutlined />
删除
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<div class="event-content">
<h3 class="event-title">{{ event.title }}</h3>
<p class="event-description" v-if="event.description">
{{ event.description }}
</p>
<div class="event-tags" v-if="event.tags && event.tags.length">
<a-tag
v-for="tag in event.tags.slice(0, 3)"
:key="tag"
size="small"
class="event-tag"
>
{{ tag }}
</a-tag>
<span v-if="event.tags.length > 3" class="more-tags">
+{{ event.tags.length - 3 }}
</span>
</div>
</div>
</div>
</a-timeline-item>
</a-timeline>
</div>
<!-- 空状态 -->
<div v-if="filteredEvents.length === 0" class="empty-state">
<a-empty
description="暂无人生事件记录"
:image="Empty.PRESENTED_IMAGE_SIMPLE"
>
<a-button type="primary" @click="showNewEventModal = true">
记录第一个事件
</a-button>
</a-empty>
</div>
</div>
</main>
<!-- 新建事件模态框 -->
<a-modal
v-model:open="showNewEventModal"
title="添加人生事件"
@ok="createEvent"
@cancel="resetEventForm"
:confirm-loading="isCreating"
width="600px"
>
<a-form :model="eventForm" layout="vertical">
<a-form-item label="事件标题" required>
<a-input
v-model:value="eventForm.title"
placeholder="请输入事件标题"
:maxlength="50"
show-count
/>
</a-form-item>
<a-form-item label="事件描述">
<a-textarea
v-model:value="eventForm.description"
placeholder="请输入事件描述(可选)"
:rows="3"
:maxlength="300"
show-count
/>
</a-form-item>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="事件类型" required>
<a-select v-model:value="eventForm.type" placeholder="选择事件类型">
<a-select-option value="milestone">里程碑</a-select-option>
<a-select-option value="achievement">成就</a-select-option>
<a-select-option value="memory">回忆</a-select-option>
<a-select-option value="goal">目标</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="事件日期" required>
<a-date-picker
v-model:value="eventForm.date"
placeholder="选择日期"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="重要程度">
<a-rate v-model:value="eventForm.importance" :count="5" />
<div class="importance-desc">
<span v-if="eventForm.importance === 1">一般重要</span>
<span v-else-if="eventForm.importance === 2">比较重要</span>
<span v-else-if="eventForm.importance === 3">重要</span>
<span v-else-if="eventForm.importance === 4">非常重要</span>
<span v-else-if="eventForm.importance === 5">极其重要</span>
<span v-else>请选择重要程度</span>
</div>
</a-form-item>
<a-form-item label="标签">
<div class="tags-input-section">
<a-input
v-model:value="newTagInput"
placeholder="添加标签,按回车确认"
@press-enter="addTag"
style="margin-bottom: 8px"
/>
<div class="selected-tags" v-if="eventForm.tags.length">
<a-tag
v-for="tag in eventForm.tags"
:key="tag"
closable
@close="removeTag(tag)"
color="blue"
>
{{ tag }}
</a-tag>
</div>
</div>
</a-form-item>
</a-form>
</a-modal>
<!-- 事件详情模态框 -->
<a-modal
v-model:open="showDetailModal"
:title="selectedEvent?.title"
:footer="null"
width="700px"
>
<div v-if="selectedEvent" class="event-detail">
<div class="detail-header">
<div class="detail-meta">
<a-tag :color="getEventColor(selectedEvent.type)" size="large">
{{ getEventTypeText(selectedEvent.type) }}
</a-tag>
<span class="detail-date">{{ formatTime.standard(selectedEvent.date) }}</span>
</div>
<div class="detail-importance">
<span class="importance-label">重要程度</span>
<a-rate :value="selectedEvent.importance" disabled />
</div>
</div>
<div class="detail-description" v-if="selectedEvent.description">
<h4>详细描述</h4>
<p>{{ selectedEvent.description }}</p>
</div>
<div class="detail-tags" v-if="selectedEvent.tags && selectedEvent.tags.length">
<h4>相关标签</h4>
<div class="tags-list">
<a-tag
v-for="tag in selectedEvent.tags"
:key="tag"
color="blue"
>
{{ tag }}
</a-tag>
</div>
</div>
<div class="detail-actions">
<a-button type="primary" @click="editEvent(selectedEvent)">
<EditOutlined />
编辑事件
</a-button>
</div>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import {
ArrowLeftOutlined,
PlusOutlined,
MoreOutlined,
EditOutlined,
DeleteOutlined,
StarFilled,
StarOutlined,
TrophyOutlined,
FlagOutlined,
HeartOutlined,
BulbOutlined,
} from '@ant-design/icons-vue'
import { Empty, message } from 'ant-design-vue'
import { formatTime } from '@/utils'
import type { LifeEvent } from '@/types'
import type { Dayjs } from 'dayjs'
// 响应式数据
const showNewEventModal = ref(false)
const showDetailModal = ref(false)
const isCreating = ref(false)
const searchKeyword = ref('')
const typeFilter = ref('')
const dateRange = ref<[Dayjs, Dayjs] | null>(null)
const newTagInput = ref('')
const selectedEvent = ref<LifeEvent | null>(null)
// 事件数据
const events = ref<LifeEvent[]>([
{
id: '1',
title: '大学毕业',
description: '完成了四年的大学学习,获得了计算机科学学士学位',
date: '2020-06-15',
type: 'milestone',
importance: 5,
tags: ['教育', '毕业', '成长']
},
{
id: '2',
title: '第一份工作',
description: '加入了一家科技公司,开始了职业生涯',
date: '2020-08-01',
type: 'achievement',
importance: 4,
tags: ['工作', '职业', '新开始']
},
{
id: '3',
title: '学会游泳',
description: '终于克服了对水的恐惧,学会了游泳',
date: '2021-07-20',
type: 'achievement',
importance: 3,
tags: ['运动', '技能', '突破']
},
{
id: '4',
title: '第一次独自旅行',
description: '一个人去了云南,体验了不同的文化和风景',
date: '2022-03-10',
type: 'memory',
importance: 4,
tags: ['旅行', '独立', '体验']
}
])
// 表单数据
const eventForm = reactive({
title: '',
description: '',
type: 'milestone' as LifeEvent['type'],
date: null as Dayjs | null,
importance: 3 as LifeEvent['importance'],
tags: [] as string[]
})
// 计算属性
const filteredEvents = computed(() => {
let result = [...events.value]
// 关键词搜索
if (searchKeyword.value) {
result = result.filter(event =>
event.title.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
event.description?.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
event.tags?.some(tag => tag.toLowerCase().includes(searchKeyword.value.toLowerCase()))
)
}
// 类型筛选
if (typeFilter.value) {
result = result.filter(event => event.type === typeFilter.value)
}
// 日期范围筛选
if (dateRange.value && dateRange.value.length === 2) {
const [start, end] = dateRange.value
result = result.filter(event => {
const eventDate = new Date(event.date)
return eventDate >= start.toDate() && eventDate <= end.toDate()
})
}
// 按日期排序(最新的在前)
return result.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
})
// 方法
const getEventColor = (type: LifeEvent['type']) => {
const colors = {
milestone: 'blue',
achievement: 'green',
memory: 'orange',
goal: 'purple'
}
return colors[type]
}
const getEventTypeText = (type: LifeEvent['type']) => {
const texts = {
milestone: '里程碑',
achievement: '成就',
memory: '回忆',
goal: '目标'
}
return texts[type]
}
const getEventIcon = (type: LifeEvent['type']) => {
const icons = {
milestone: FlagOutlined,
achievement: TrophyOutlined,
memory: HeartOutlined,
goal: BulbOutlined
}
return icons[type]
}
const handleSearch = () => {
// 搜索逻辑已在计算属性中处理
}
const handleFilterChange = () => {
// 筛选逻辑已在计算属性中处理
}
const handleDateRangeChange = () => {
// 日期范围筛选逻辑已在计算属性中处理
}
const createEvent = async () => {
if (!eventForm.title.trim()) {
message.warning('请输入事件标题')
return
}
if (!eventForm.date) {
message.warning('请选择事件日期')
return
}
isCreating.value = true
try {
const newEvent: LifeEvent = {
id: Date.now().toString(),
title: eventForm.title.trim(),
description: eventForm.description.trim() || undefined,
date: eventForm.date.format('YYYY-MM-DD'),
type: eventForm.type,
importance: eventForm.importance,
tags: eventForm.tags.length ? eventForm.tags : undefined
}
events.value.push(newEvent)
message.success('事件添加成功')
showNewEventModal.value = false
resetEventForm()
} catch (error) {
message.error('添加失败,请重试')
} finally {
isCreating.value = false
}
}
const resetEventForm = () => {
eventForm.title = ''
eventForm.description = ''
eventForm.type = 'milestone'
eventForm.date = null
eventForm.importance = 3
eventForm.tags = []
newTagInput.value = ''
}
const addTag = () => {
const tag = newTagInput.value.trim()
if (tag && !eventForm.tags.includes(tag)) {
eventForm.tags.push(tag)
newTagInput.value = ''
}
}
const removeTag = (tag: string) => {
const index = eventForm.tags.indexOf(tag)
if (index > -1) {
eventForm.tags.splice(index, 1)
}
}
const viewEventDetail = (event: LifeEvent) => {
selectedEvent.value = event
showDetailModal.value = true
}
const editEvent = (event: LifeEvent) => {
// TODO: 实现编辑功能
message.info('编辑功能开发中...')
}
const deleteEvent = (id: string) => {
const index = events.value.findIndex(e => e.id === id)
if (index > -1) {
events.value.splice(index, 1)
message.success('事件删除成功')
}
}
// 组件挂载
onMounted(() => {
// 初始化数据
})
</script>
<style lang="scss" scoped>
.life-trajectory-page {
min-height: 100vh;
background: $light-gray;
}
.page-header {
background: white;
box-shadow: $shadow-sm;
position: sticky;
top: 0;
z-index: 10;
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: $spacing-md $spacing-lg;
max-width: 1000px;
margin: 0 auto;
}
.header-left {
display: flex;
align-items: center;
gap: $spacing-md;
}
.back-btn {
color: $text-medium;
&:hover {
color: $tech-blue;
}
}
.page-title {
font-size: $font-size-lg;
font-weight: $font-weight-bold;
color: $text-dark;
margin: 0;
}
.new-event-btn {
border-radius: $border-radius-full;
}
.page-main {
padding: $spacing-lg;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
.filter-section {
display: flex;
gap: $spacing-md;
margin-bottom: $spacing-xl;
flex-wrap: wrap;
}
.timeline-container {
.life-timeline {
:deep(.ant-timeline-item-tail) {
border-left: 2px solid #e8e8e8;
}
}
.timeline-item {
margin-bottom: $spacing-xl;
}
.timeline-dot {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: $font-size-base;
&.dot-milestone {
background: #1890ff;
}
&.dot-achievement {
background: #52c41a;
}
&.dot-memory {
background: #fa8c16;
}
&.dot-goal {
background: #722ed1;
}
}
.event-card {
background: white;
border-radius: $border-radius-lg;
padding: $spacing-lg;
box-shadow: $shadow-sm;
cursor: pointer;
transition: all $transition-normal;
margin-left: $spacing-md;
&:hover {
box-shadow: $shadow-md;
transform: translateY(-2px);
}
.event-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: $spacing-md;
.event-meta {
display: flex;
flex-direction: column;
gap: $spacing-xs;
.event-date {
color: $text-medium;
font-size: $font-size-sm;
}
.importance-stars {
display: flex;
gap: 2px;
.star-filled {
color: #faad14;
}
.star-empty {
color: #d9d9d9;
}
}
}
}
.event-content {
.event-title {
font-size: $font-size-lg;
font-weight: $font-weight-semibold;
color: $text-dark;
margin-bottom: $spacing-sm;
line-height: 1.4;
}
.event-description {
color: $text-medium;
line-height: 1.6;
margin-bottom: $spacing-md;
}
.event-tags {
display: flex;
flex-wrap: wrap;
gap: $spacing-xs;
.event-tag {
margin: 0;
}
.more-tags {
font-size: $font-size-sm;
color: $text-medium;
}
}
}
}
}
.empty-state {
display: flex;
justify-content: center;
align-items: center;
padding: $spacing-xxl;
}
// 模态框样式
.tags-input-section {
.selected-tags {
display: flex;
flex-wrap: wrap;
gap: $spacing-xs;
}
}
.importance-desc {
margin-top: $spacing-xs;
font-size: $font-size-sm;
color: $text-medium;
}
.event-detail {
.detail-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: $spacing-lg;
padding-bottom: $spacing-md;
border-bottom: 1px solid #f0f0f0;
.detail-meta {
display: flex;
flex-direction: column;
gap: $spacing-xs;
.detail-date {
color: $text-medium;
font-size: $font-size-sm;
}
}
.detail-importance {
display: flex;
align-items: center;
gap: $spacing-sm;
.importance-label {
font-size: $font-size-sm;
color: $text-medium;
}
}
}
.detail-description,
.detail-tags {
margin-bottom: $spacing-lg;
h4 {
font-size: $font-size-base;
font-weight: $font-weight-semibold;
color: $text-dark;
margin-bottom: $spacing-sm;
}
p {
color: $text-dark;
line-height: 1.6;
margin: 0;
}
}
.tags-list {
display: flex;
flex-wrap: wrap;
gap: $spacing-xs;
}
.detail-actions {
padding-top: $spacing-md;
border-top: 1px solid #f0f0f0;
}
}
</style>