603 lines
15 KiB
Vue
603 lines
15 KiB
Vue
<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>
|