Files
simple-memo-wx/pages/review/reviewList.vue
T

603 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>