首次提交:初始化项目代码

This commit is contained in:
sunct
2026-07-31 14:42:05 +08:00
commit ecca407485
84 changed files with 24158 additions and 0 deletions
+602
View File
@@ -0,0 +1,602 @@
<template>
<view class="review-list-page" :style="{ backgroundColor: themeConfig.bgColor }">
<PageHeader
title="复盘历史"
subtitle="回顾与成长"
:showBack="false"
:showRight="true"
>
<template #right>
<view class="header-actions">
<view
class="action-btn"
:class="{ 'action-btn--active': searchVisible }"
:style="{ backgroundColor: themeConfig.cardBgColor }"
@click="toggleSearch"
>
<FaIcon :icon="searchVisible ? 'times' : 'search'" :size="18" :color="searchVisible ? themeConfig.primaryColor : themeConfig.textPrimary" />
</view>
<view
class="action-btn"
:class="{ 'action-btn--active': calendarVisible }"
:style="{ backgroundColor: themeConfig.cardBgColor }"
@click="toggleCalendar"
>
<FaIcon :icon="calendarVisible ? 'times' : 'calendar'" :size="18" :color="calendarVisible ? themeConfig.primaryColor : themeConfig.textPrimary" />
</view>
</view>
</template>
</PageHeader>
<!-- 筛选区域 -->
<view class="filter-section" v-if="searchVisible || calendarVisible" :style="{ backgroundColor: themeConfig.bgColor }">
<!-- 搜索栏 -->
<view v-if="searchVisible" class="search-bar" :style="{ backgroundColor: themeConfig.cardBgColor }">
<FaIcon icon="search" :size="16" :color="themeConfig.textSecondary" />
<input
class="search-input"
:style="{ color: themeConfig.textPrimary }"
v-model="searchKeyword"
placeholder="搜索复盘内容..."
:placeholder-style="{ color: themeConfig.textDisabled }"
confirm-type="search"
@confirm="onSearch"
/>
<view v-if="searchKeyword" class="clear-btn" @click="clearSearch">
<FaIcon icon="times-circle" :size="16" :color="themeConfig.textSecondary" />
</view>
</view>
<!-- 日历范围选择 -->
<view v-if="calendarVisible" class="calendar-section">
<InlineCalendar
rangeSelect
:startDate="dateRange.start"
:endDate="dateRange.end"
@rangeChange="onDateRangeChange"
/>
<view v-if="dateRange.start" class="date-range-info">
<text class="range-text" :style="{ color: themeConfig.textSecondary }">
{{ dateRange.start }} ~ {{ dateRange.end || '至今' }}
</text>
<view class="clear-range-btn" @click="clearDateRange">
<text class="clear-range-text" :style="{ color: themeConfig.primaryColor }">清除</text>
</view>
</view>
</view>
</view>
<!-- 列表 -->
<scroll-view
class="list-scroll"
scroll-y
refresher-enabled
:refresher-triggered="refresherTriggered"
@refresherrefresh="onRefresh"
:show-scrollbar="false"
@scrolltolower="loadMore"
>
<view class="list-content" v-if="reviewList.length > 0">
<SwipeableItem
v-for="item in reviewList"
:key="item.id"
:actions="swipeActions"
:index="item.id"
@action="(key) => onSwipeAction(key, item)"
@open="onSwipeOpen"
>
<AppCard
:padding="24"
:radius="20"
:clickable="true"
:marginBottom="16"
@click="goToDetail(item)"
>
<view class="review-item">
<!-- 头部日期 + 时间 -->
<view class="item-header">
<view class="date-wrap">
<text class="item-date" :style="{ color: themeConfig.textPrimary }">{{ formatDate(item.date) }}</text>
<text class="item-weekday" :style="{ color: themeConfig.textSecondary }">{{ getWeekday(item.date) }}</text>
</view>
<text class="item-time" :style="{ color: themeConfig.textDisabled }">{{ formatTime(item.created_at || item.date) }}</text>
</view>
<!-- KPT 三字段 -->
<view class="kpt-fields">
<view
class="kpt-field"
:style="{ backgroundColor: themeConfig.successColor + '10' }"
v-if="item.kpt && item.kpt.keep"
>
<view class="kpt-label" :style="{ backgroundColor: themeConfig.successColor, color: '#fff' }">
<text>Keep</text>
</view>
<text class="kpt-text" :style="{ color: themeConfig.textSecondary }">{{ item.kpt.keep }}</text>
</view>
<view
class="kpt-field"
:style="{ backgroundColor: themeConfig.warningColor + '10' }"
v-if="item.kpt && item.kpt.problem"
>
<view class="kpt-label" :style="{ backgroundColor: themeConfig.warningColor, color: '#fff' }">
<text>Problem</text>
</view>
<text class="kpt-text" :style="{ color: themeConfig.textSecondary }">{{ item.kpt.problem }}</text>
</view>
<view
class="kpt-field"
:style="{ backgroundColor: (themeConfig.infoColor || themeConfig.primaryColor) + '10' }"
v-if="item.kpt && item.kpt.try"
>
<view class="kpt-label" :style="{ backgroundColor: (themeConfig.infoColor || themeConfig.primaryColor), color: '#fff' }">
<text>Try</text>
</view>
<text class="kpt-text" :style="{ color: themeConfig.textSecondary }">{{ item.kpt.try }}</text>
</view>
</view>
</view>
</AppCard>
</SwipeableItem>
<!-- 底部状态 -->
<view class="list-footer" v-if="loading && hasMore">
<view class="loading-dot" :style="{ backgroundColor: themeConfig.primaryColor }"></view>
<text class="footer-text" :style="{ color: themeConfig.textSecondary }">加载中...</text>
</view>
<view class="list-footer" v-else-if="!hasMore">
<text class="footer-text" :style="{ color: themeConfig.textDisabled }"> 没有更多了 </text>
</view>
<view class="bottom-spacer"></view>
</view>
<!-- 空状态 -->
<view class="empty-wrap" v-else-if="!loading">
<EmptyState
type="review"
title="还没有复盘记录"
desc="坚持每日复盘,见证成长轨迹"
actionText="开始复盘"
@action="goToTodayReview"
/>
</view>
<!-- 首次加载中 -->
<view class="initial-loading" v-if="loading && reviewList.length === 0">
<view class="loading-spinner" :style="{ borderColor: themeConfig.primaryColor }"></view>
<text :style="{ color: themeConfig.textSecondary }">加载中...</text>
</view>
</scroll-view>
</view>
</template>
<script>
import { getReviewList, deleteReview } from '@/api'
import themeMixin from '@/mixins/themeMixin.js'
import PageHeader from '@/components/PageHeader.vue'
import AppCard from '@/components/AppCard.vue'
import EmptyState from '@/components/EmptyState.vue'
import InlineCalendar from '@/components/InlineCalendar.vue'
import SwipeableItem from '@/components/SwipeableItem.vue'
import FaIcon from '@/components/FaIcon.vue'
export default {
name: 'ReviewList',
mixins: [themeMixin],
components: {
PageHeader,
AppCard,
EmptyState,
InlineCalendar,
SwipeableItem,
FaIcon
},
data() {
return {
reviewList: [],
searchKeyword: '',
searchVisible: false,
calendarVisible: false,
dateRange: {
start: '',
end: ''
},
page: 1,
pageSize: 10,
hasMore: true,
loading: false,
loaded: false,
refresherTriggered: false,
swipeActions: [
{ key: 'delete', label: '删除' }
]
}
},
onLoad() {
this.loadList(true)
},
onShow() {
this.syncThemeConfig()
// 从编辑页返回时刷新列表
if (this.loaded) {
this.loadList(true)
}
},
methods: {
async loadList(reset = false) {
if (this.loading) return
this.loading = true
if (reset) {
this.page = 1
this.hasMore = true
this.reviewList = []
}
try {
const res = await getReviewList({
keyword: this.searchKeyword,
startDate: this.dateRange.start,
endDate: this.dateRange.end,
page: this.page,
pageSize: this.pageSize
})
const data = res
const list = (data && data.list) ? data.list : (Array.isArray(data) ? data : [])
const total = (data && data.total) != null ? data.total : list.length
if (reset) {
this.reviewList = list
} else {
this.reviewList = [...this.reviewList, ...list]
}
this.hasMore = this.reviewList.length < total
this.page++
this.loaded = true
} catch (e) {
console.error('加载复盘列表失败:', e)
if (!this.loaded) {
uni.showToast({ title: '加载失败,下拉刷新重试', icon: 'none' })
}
} finally {
this.loading = false
this.refresherTriggered = false
}
},
loadMore() {
if (!this.hasMore || this.loading) return
this.loadList(false)
},
async onRefresh() {
this.refresherTriggered = true
await this.loadList(true)
},
onSearch() {
this.loadList(true)
},
clearSearch() {
this.searchKeyword = ''
this.loadList(true)
},
toggleSearch() {
this.searchVisible = !this.searchVisible
if (!this.searchVisible && this.searchKeyword) {
this.searchKeyword = ''
this.loadList(true)
}
},
toggleCalendar() {
this.calendarVisible = !this.calendarVisible
},
onDateRangeChange(range) {
this.dateRange.start = range.start || ''
this.dateRange.end = range.end || ''
this.searchVisible = false
this.calendarVisible = false
this.loadList(true)
},
clearDateRange() {
this.dateRange.start = ''
this.dateRange.end = ''
this.loadList(true)
},
onSwipeOpen() {},
async onSwipeAction(actionKey, item) {
if (actionKey === 'delete') {
uni.showModal({
title: '确认删除',
content: '确定要删除这条复盘记录吗?删除后不可恢复。',
success: async (res) => {
if (res.confirm) {
await this.handleDelete(item.id)
}
}
})
}
},
async handleDelete(itemId) {
try {
await deleteReview({ id: itemId })
this.reviewList = this.reviewList.filter(item => item.id !== itemId)
uni.showToast({ title: '删除成功', icon: 'success' })
} catch (e) {
console.error('删除失败:', e)
uni.showToast({ title: '删除失败,请重试', icon: 'none' })
}
},
goToDetail(item) {
uni.navigateTo({
url: `/pages/review/review?date=${item.date}`
})
},
goToTodayReview() {
const now = new Date()
const y = now.getFullYear()
const m = String(now.getMonth() + 1).padStart(2, '0')
const d = String(now.getDate()).padStart(2, '0')
const today = `${y}-${m}-${d}`
uni.navigateTo({
url: `/pages/review/review?date=${today}`
})
},
formatDate(dateStr) {
if (!dateStr) return ''
const date = new Date(dateStr)
const m = date.getMonth() + 1
const d = date.getDate()
return `${m}${d}`
},
getWeekday(dateStr) {
if (!dateStr) return ''
const date = new Date(dateStr)
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
return weekdays[date.getDay()]
},
formatTime(dateStr) {
if (!dateStr) return ''
// 处理 '2026-07-28 20:30' 或 '2026-07-28' 格式
const parts = dateStr.split(' ')
if (parts.length > 1) {
const timeParts = parts[1].split(':')
return `${timeParts[0]}:${timeParts[1]}`
}
return ''
}
}
}
</script>
<style lang="scss" scoped>
.review-list-page {
min-height: 100vh;
display: flex;
flex-direction: column;
}
// 头部操作按钮
.header-actions {
display: flex;
gap: 12rpx;
}
.action-btn {
width: 60rpx;
height: 60rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.action-btn--active {
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.action-btn:active {
transform: scale(0.92);
}
// 筛选区域
.filter-section {
padding: 16rpx 32rpx 24rpx;
}
.search-bar {
display: flex;
align-items: center;
gap: 12rpx;
padding: 16rpx 24rpx;
border-radius: 16rpx;
margin-bottom: 16rpx;
}
.search-input {
flex: 1;
font-size: 28rpx;
background: transparent;
border: none;
outline: none;
}
.clear-btn {
flex-shrink: 0;
padding: 4rpx;
}
.calendar-section {
margin-top: 8rpx;
}
.date-range-info {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx 24rpx 0;
}
.range-text {
font-size: 24rpx;
}
.clear-range-btn {
padding: 8rpx 16rpx;
}
.clear-range-text {
font-size: 24rpx;
}
// 列表
.list-scroll {
flex: 1;
height: 0;
}
.list-content {
padding: 8rpx 32rpx 0;
}
// 复盘卡片
.review-item {
padding: 4rpx 0;
}
.item-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
}
.date-wrap {
display: flex;
align-items: baseline;
gap: 12rpx;
}
.item-date {
font-size: 32rpx;
font-weight: 600;
}
.item-weekday {
font-size: 24rpx;
}
.item-time {
font-size: 22rpx;
}
// KPT 字段
.kpt-fields {
display: flex;
flex-direction: column;
gap: 10rpx;
}
.kpt-field {
display: flex;
align-items: flex-start;
gap: 12rpx;
padding: 14rpx 16rpx;
border-radius: 12rpx;
}
.kpt-label {
flex-shrink: 0;
padding: 4rpx 14rpx;
border-radius: 8rpx;
font-size: 20rpx;
font-weight: 600;
line-height: 1.4;
min-width: 80rpx;
text-align: center;
}
.kpt-text {
flex: 1;
font-size: 26rpx;
line-height: 1.6;
word-break: break-all;
}
// 底部状态
.list-footer {
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
padding: 36rpx 0;
}
.loading-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
animation: loadingPulse 1s ease-in-out infinite;
}
@keyframes loadingPulse {
0%, 100% { opacity: 0.3; transform: scale(1); }
50% { opacity: 1; transform: scale(1.3); }
}
.footer-text {
font-size: 24rpx;
}
.bottom-spacer {
height: 60rpx;
}
// 空状态
.empty-wrap {
padding: 120rpx 32rpx;
}
// 首次加载
.initial-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 200rpx 0;
gap: 20rpx;
font-size: 26rpx;
}
.loading-spinner {
width: 48rpx;
height: 48rpx;
border: 4rpx solid;
border-top-color: transparent !important;
border-right-color: transparent !important;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>