fix
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 16s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s

This commit is contained in:
SunCheng
2026-02-20 14:57:19 +08:00
parent 6e95568906
commit 32d5ed62d0
27 changed files with 1520 additions and 1114 deletions

View File

@@ -0,0 +1,165 @@
# PopupContainer V1 → V2 迁移清单
## 文件分析汇总
### 第一批:基础用法(无 subtitle、无按钮
| 文件 | Props 使用 | Slots 使用 | 迁移复杂度 | 备注 |
|------|-----------|-----------|----------|------|
| MessageView.vue | v-model, title, subtitle, height | footer | ⭐⭐ | 有 subtitle (createTime),有条件 footer |
| EmailRecord.vue | v-model, title, height | header-actions | ⭐⭐⭐ | 使用 header-actions 插槽(重新分析按钮) |
| PeriodicRecord.vue | v-model, title, height | 默认插槽 | ⭐ | 基础用法,表单内容 |
| ClassificationNLP.vue | v-model, title, height | 默认插槽 | ⭐ | 基础用法 |
| BillAnalysisView.vue | v-model, title, height | 默认插槽 | ⭐ | 基础用法 |
### 第二批:带 subtitle
| 文件 | Subtitle 类型 | 迁移方案 |
|------|--------------|---------|
| MessageView.vue | 时间戳 (createTime) | 移至内容区域顶部,使用灰色小字 |
| CategoryBillPopup.vue | 待检查 | 待定 |
| BudgetChartAnalysis.vue | 待检查 | 待定 |
| TransactionDetail.vue | 待检查 | 待定 |
| ReasonGroupList.vue | 待检查 | 待定 |
### 第三批:带确认/取消按钮
| 文件 | 按钮配置 | 迁移方案 |
|------|---------|---------|
| AddClassifyDialog.vue | 待检查 | footer 插槽 + van-button |
| IconSelector.vue | 待检查 | footer 插槽 + van-button |
| ClassificationEdit.vue | 待检查 | footer 插槽 + van-button |
### 第四批复杂布局header-actions
| 文件 | header-actions 内容 | 迁移方案 |
|------|-------------------|---------|
| EmailRecord.vue | "重新分析" 按钮 | 移至内容区域顶部作为操作栏 |
| BudgetCard.vue | 待检查 | 待定 |
| BudgetEditPopup.vue | 待检查 | 待定 |
| SavingsConfigPopup.vue | 待检查 | 待定 |
| SavingsBudgetContent.vue | 待检查 | 待定 |
| budgetV2/Index.vue | 待检查 | 待定 |
### 第五批:全局组件
| 文件 | 特殊逻辑 | 迁移方案 |
|------|---------|---------|
| GlobalAddBill.vue | 待检查 | 待定 |
## 迁移模式汇总
### 模式 1: 基础迁移(无特殊 props
```vue
<!-- V1 -->
<PopupContainer
v-model="show"
title="标题"
height="75%"
>
内容
</PopupContainer>
<!-- V2 -->
<PopupContainerV2
v-model:show="show"
title="标题"
:height="'75%'"
>
<div style="padding: 16px">
内容
</div>
</PopupContainerV2>
```
### 模式 2: subtitle 迁移
```vue
<!-- V1 -->
<PopupContainer
v-model="show"
title="标题"
:subtitle="createTime"
>
内容
</PopupContainer>
<!-- V2 -->
<PopupContainerV2
v-model:show="show"
title="标题"
:height="'75%'"
>
<div style="padding: 16px">
<p style="color: #999; font-size: 14px; margin-bottom: 12px">{{ createTime }}</p>
内容
</div>
</PopupContainerV2>
```
### 模式 3: header-actions 迁移
```vue
<!-- V1 -->
<PopupContainer
v-model="show"
title="标题"
>
<template #header-actions>
<van-button size="small" @click="handleAction">操作</van-button>
</template>
内容
</PopupContainer>
<!-- V2 -->
<PopupContainerV2
v-model:show="show"
title="标题"
:height="'80%'"
>
<div style="padding: 16px">
<div style="margin-bottom: 16px; text-align: right">
<van-button size="small" @click="handleAction">操作</van-button>
</div>
内容
</div>
</PopupContainerV2>
```
### 模式 4: footer 插槽迁移
```vue
<!-- V1 -->
<PopupContainer
v-model="show"
title="标题"
>
内容
<template #footer>
<van-button type="primary">提交</van-button>
</template>
</PopupContainer>
<!-- V2 -->
<PopupContainerV2
v-model:show="show"
title="标题"
:height="'80%'"
>
<div style="padding: 16px">
内容
</div>
<template #footer>
<van-button type="primary" block>提交</van-button>
</template>
</PopupContainerV2>
```
## 进度追踪
- [ ] 完成所有文件的详细分析
- [ ] 确认每个文件的迁移模式
- [ ] 标记需要特殊处理的文件
## 风险点
1. **EmailRecord.vue**: 有 header-actions 插槽,需要重新设计操作按钮的位置
2. **MessageView.vue**: subtitle 用于显示时间,需要保持视觉层级
3. **待检查文件**: 需要逐个检查是否使用了 v-html、复杂布局等特性

View File

@@ -1,14 +1,10 @@
<template> <template>
<PopupContainer <PopupContainerV2
v-model:show="show" v-model:show="show"
title="新增交易分类" title="新增交易分类"
show-cancel-button :height="'auto'"
show-confirm-button
confirm-text="确认"
cancel-text="取消"
@confirm="handleConfirm"
@cancel="resetAddForm"
> >
<div style="padding: 16px">
<van-form ref="addFormRef"> <van-form ref="addFormRef">
<van-field <van-field
v-model="classifyName" v-model="classifyName"
@@ -18,13 +14,32 @@
:rules="[{ required: true, message: '请输入分类名称' }]" :rules="[{ required: true, message: '请输入分类名称' }]"
/> />
</van-form> </van-form>
</PopupContainer> </div>
<template #footer>
<div style="display: flex; gap: 12px">
<van-button
plain
style="flex: 1"
@click="resetAddForm"
>
取消
</van-button>
<van-button
type="primary"
style="flex: 1"
@click="handleConfirm"
>
确认
</van-button>
</div>
</template>
</PopupContainerV2>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import { ref } from 'vue'
import { showToast } from 'vant' import { showToast } from 'vant'
import PopupContainer from './PopupContainer.vue' import PopupContainerV2 from './PopupContainerV2.vue'
const emit = defineEmits(['confirm']) const emit = defineEmits(['confirm'])

View File

@@ -209,10 +209,10 @@
</div> </div>
<!-- 关联账单列表弹窗 --> <!-- 关联账单列表弹窗 -->
<PopupContainer <PopupContainerV2
v-model="showBillListModal" v-model:show="showBillListModal"
title="关联账单列表" title="关联账单列表"
height="75%" :height="'75%'"
> >
<BillListComponent <BillListComponent
data-source="custom" data-source="custom"
@@ -225,7 +225,7 @@
@click="handleBillClick" @click="handleBillClick"
@delete="handleBillDelete" @delete="handleBillDelete"
/> />
</PopupContainer> </PopupContainerV2>
</div> </div>
<!-- 不记额预算卡片 --> <!-- 不记额预算卡片 -->
@@ -406,10 +406,10 @@
</div> </div>
<!-- 关联账单列表弹窗 --> <!-- 关联账单列表弹窗 -->
<PopupContainer <PopupContainerV2
v-model="showBillListModal" v-model:show="showBillListModal"
title="关联账单列表" title="关联账单列表"
height="75%" :height="'75%'"
> >
<BillListComponent <BillListComponent
data-source="custom" data-source="custom"
@@ -422,14 +422,14 @@
@click="handleBillClick" @click="handleBillClick"
@delete="handleBillDelete" @delete="handleBillDelete"
/> />
</PopupContainer> </PopupContainerV2>
</div> </div>
</template> </template>
<script setup> <script setup>
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { BudgetPeriodType } from '@/constants/enums' import { BudgetPeriodType } from '@/constants/enums'
import PopupContainer from '@/components/PopupContainer.vue' import PopupContainerV2 from '@/components/PopupContainerV2.vue'
import BillListComponent from '@/components/Bill/BillListComponent.vue' import BillListComponent from '@/components/Bill/BillListComponent.vue'
import { getTransactionList } from '@/api/transactionRecord' import { getTransactionList } from '@/api/transactionRecord'

View File

@@ -187,13 +187,14 @@
</div> </div>
<!-- 详细描述弹窗 --> <!-- 详细描述弹窗 -->
<PopupContainer <PopupContainerV2
v-model="showDescriptionPopup" v-model:show="showDescriptionPopup"
:title="activeDescTab === 'month' ? '预算额度/实际详情(月度)' : '预算额度/实际详情(年度)'" :title="activeDescTab === 'month' ? '预算额度/实际详情(月度)' : '预算额度/实际详情(年度)'"
height="70%" :height="'70%'"
> >
<div <div
class="rich-html-content popup-content-padding" class="rich-html-content"
style="padding: 16px"
v-html=" v-html="
activeDescTab === 'month' activeDescTab === 'month'
? overallStats.month?.description || ? overallStats.month?.description ||
@@ -202,14 +203,14 @@
'<p style=\'text-align:center;color:var(--van-text-color-3)\'>暂无数据</p>' '<p style=\'text-align:center;color:var(--van-text-color-3)\'>暂无数据</p>'
" "
/> />
</PopupContainer> </PopupContainerV2>
</template> </template>
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { BudgetCategory, BudgetPeriodType } from '@/constants/enums' import { BudgetCategory, BudgetPeriodType } from '@/constants/enums'
import { getCssVar } from '@/utils/theme' import { getCssVar } from '@/utils/theme'
import PopupContainer from '@/components/PopupContainer.vue' import PopupContainerV2 from '@/components/PopupContainerV2.vue'
import BaseChart from '@/components/Charts/BaseChart.vue' import BaseChart from '@/components/Charts/BaseChart.vue'
import { useChartTheme } from '@/composables/useChartTheme' import { useChartTheme } from '@/composables/useChartTheme'
import { chartjsGaugePlugin } from '@/plugins/chartjs-gauge-plugin' import { chartjsGaugePlugin } from '@/plugins/chartjs-gauge-plugin'

View File

@@ -1,18 +1,18 @@
<template> <template>
<PopupContainer <PopupContainerV2
v-model="visible" v-model:show="visible"
:title=" :title="
isEdit isEdit
? `编辑${getCategoryName(form.category)}预算` ? `编辑${getCategoryName(form.category)}预算`
: `新增${getCategoryName(form.category)}预算` : `新增${getCategoryName(form.category)}预算`
" "
height="75%" :height="'75%'"
> >
<div class="add-budget-form"> <div class="add-budget-form">
<van-form> <van-form>
<van-cell-group inset> <van-cell-group inset>
<van-field <van-field
v-model="form.name" v-model:show="form.name"
name="name" name="name"
label="预算名称" label="预算名称"
placeholder="例如:每月餐饮、年度奖金" placeholder="例如:每月餐饮、年度奖金"
@@ -22,7 +22,7 @@
<van-field label="不记额预算"> <van-field label="不记额预算">
<template #input> <template #input>
<van-checkbox <van-checkbox
v-model="form.noLimit" v-model:show="form.noLimit"
@update:model-value="onNoLimitChange" @update:model-value="onNoLimitChange"
> >
不记额预算 不记额预算
@@ -34,7 +34,7 @@
<template #input> <template #input>
<div class="mandatory-wrapper"> <div class="mandatory-wrapper">
<van-checkbox <van-checkbox
v-model="form.isMandatoryExpense" v-model:show="form.isMandatoryExpense"
:disabled="form.noLimit" :disabled="form.noLimit"
> >
{{ form.category === BudgetCategory.Expense ? '硬性消费' : '硬性收入' }} {{ form.category === BudgetCategory.Expense ? '硬性消费' : '硬性收入' }}
@@ -49,7 +49,7 @@
> >
<template #input> <template #input>
<van-radio-group <van-radio-group
v-model="form.type" v-model:show="form.type"
direction="horizontal" direction="horizontal"
:disabled="isEdit || form.noLimit" :disabled="isEdit || form.noLimit"
> >
@@ -65,7 +65,7 @@
<!-- 仅当未选中"不记额预算"时显示预算金额 --> <!-- 仅当未选中"不记额预算"时显示预算金额 -->
<van-field <van-field
v-if="!form.noLimit" v-if="!form.noLimit"
v-model="form.limit" v-model:show="form.limit"
type="number" type="number"
name="limit" name="limit"
label="预算金额" label="预算金额"
@@ -95,7 +95,7 @@
</template> </template>
</van-field> </van-field>
<ClassifySelector <ClassifySelector
v-model="form.selectedCategories" v-model:show="form.selectedCategories"
:type="budgetType" :type="budgetType"
multiple multiple
:show-add="false" :show-add="false"
@@ -114,7 +114,7 @@
保存预算 保存预算
</van-button> </van-button>
</template> </template>
</PopupContainer> </PopupContainerV2>
</template> </template>
<script setup> <script setup>
@@ -122,7 +122,7 @@ import { ref, reactive, computed } from 'vue'
import { showToast } from 'vant' import { showToast } from 'vant'
import { createBudget, updateBudget } from '@/api/budget' import { createBudget, updateBudget } from '@/api/budget'
import { BudgetPeriodType, BudgetCategory } from '@/constants/enums' import { BudgetPeriodType, BudgetCategory } from '@/constants/enums'
import PopupContainer from '@/components/PopupContainer.vue' import PopupContainerV2 from '@/components/PopupContainerV2.vue'
import ClassifySelector from '@/components/ClassifySelector.vue' import ClassifySelector from '@/components/ClassifySelector.vue'
const emit = defineEmits(['success']) const emit = defineEmits(['success'])

View File

@@ -1,8 +1,8 @@
<template> <template>
<PopupContainer <PopupContainerV2
v-model="visible" v-model:show="visible"
title="设置存款分类" title="设置存款分类"
height="60%" :height="'60%'"
> >
<div class="savings-config-content"> <div class="savings-config-content">
<div class="config-header"> <div class="config-header">
@@ -16,7 +16,7 @@
可多选分类 可多选分类
</div> </div>
<ClassifySelector <ClassifySelector
v-model="selectedCategories" v-model:show="selectedCategories"
:type="2" :type="2"
multiple multiple
:show-add="false" :show-add="false"
@@ -35,14 +35,14 @@
保存配置 保存配置
</van-button> </van-button>
</template> </template>
</PopupContainer> </PopupContainerV2>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import { ref } from 'vue'
import { showToast, showLoadingToast, closeToast } from 'vant' import { showToast, showLoadingToast, closeToast } from 'vant'
import { getConfig, setConfig } from '@/api/config' import { getConfig, setConfig } from '@/api/config'
import PopupContainer from '@/components/PopupContainer.vue' import PopupContainerV2 from '@/components/PopupContainerV2.vue'
import ClassifySelector from '@/components/ClassifySelector.vue' import ClassifySelector from '@/components/ClassifySelector.vue'
const emit = defineEmits(['success']) const emit = defineEmits(['success'])

View File

@@ -1,11 +1,19 @@
<template> <template>
<PopupContainer <PopupContainerV2
v-model:show="visible" v-model:show="visible"
:title="title" :title="title"
:subtitle="total > 0 ? `共 ${total} 笔交易` : ''" :height="'80%'"
:closeable="true"
> >
<!-- 交易列表 --> <!-- 交易列表 -->
<div style="padding: 0">
<!-- Subtitle 作为内容区域顶部 -->
<div
v-if="total > 0"
style="padding: 12px 16px; text-align: center; color: #999; font-size: 14px; border-bottom: 1px solid var(--van-border-color)"
>
{{ total }} 笔交易
</div>
<div class="transactions"> <div class="transactions">
<!-- 加载状态 --> <!-- 加载状态 -->
<van-loading <van-loading
@@ -105,7 +113,8 @@
</div> </div>
</div> </div>
</div> </div>
</PopupContainer> </div>
</PopupContainerV2>
<!-- 交易详情弹窗 --> <!-- 交易详情弹窗 -->
<TransactionDetailSheet <TransactionDetailSheet
@@ -120,7 +129,7 @@
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
import { showToast } from 'vant' import { showToast } from 'vant'
import TransactionDetailSheet from '@/components/Transaction/TransactionDetailSheet.vue' import TransactionDetailSheet from '@/components/Transaction/TransactionDetailSheet.vue'
import PopupContainer from '@/components/PopupContainer.vue' import PopupContainerV2 from '@/components/PopupContainerV2.vue'
import { getTransactionList } from '@/api/transactionRecord' import { getTransactionList } from '@/api/transactionRecord'
const props = defineProps({ const props = defineProps({

View File

@@ -9,11 +9,12 @@
</div> </div>
<!-- Add Bill Modal --> <!-- Add Bill Modal -->
<PopupContainer <PopupContainerV2
v-model="showAddBill" v-model:show="showAddBill"
title="记一笔" title="记一笔"
height="75%" :height="'75%'"
> >
<div style="padding: 0">
<van-tabs <van-tabs
v-model:active="activeTab" v-model:active="activeTab"
shrink shrink
@@ -37,13 +38,14 @@
/> />
</van-tab> </van-tab>
</van-tabs> </van-tabs>
</PopupContainer> </div>
</PopupContainerV2>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, defineEmits } from 'vue' import { ref, defineEmits } from 'vue'
import PopupContainer from '@/components/PopupContainer.vue' import PopupContainerV2 from '@/components/PopupContainerV2.vue'
import OneLineBillAdd from '@/components/Bill/OneLineBillAdd.vue' import OneLineBillAdd from '@/components/Bill/OneLineBillAdd.vue'
import ManualBillAdd from '@/components/Bill/ManualBillAdd.vue' import ManualBillAdd from '@/components/Bill/ManualBillAdd.vue'

View File

@@ -1,14 +1,9 @@
<template> <template>
<PopupContainer <PopupContainerV2
:show="show" :show="show"
:title="title" :title="title"
show-cancel-button :height="'80%'"
show-confirm-button
confirm-text="选择"
cancel-text="取消"
@update:show="emit('update:show', $event)" @update:show="emit('update:show', $event)"
@confirm="handleConfirm"
@cancel="handleCancel"
> >
<div class="icon-selector"> <div class="icon-selector">
<!-- 搜索框 --> <!-- 搜索框 -->
@@ -56,14 +51,32 @@
@change="handlePageChange" @change="handlePageChange"
/> />
</div> </div>
</PopupContainer> <template #footer>
<div style="display: flex; gap: 12px">
<van-button
plain
style="flex: 1"
@click="handleCancel"
>
取消
</van-button>
<van-button
type="primary"
style="flex: 1"
@click="handleConfirm"
>
选择
</van-button>
</div>
</template>
</PopupContainerV2>
</template> </template>
<script setup> <script setup>
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
import { showToast } from 'vant' import { showToast } from 'vant'
import Icon from './Icon.vue' import Icon from './Icon.vue'
import PopupContainer from './PopupContainer.vue' import PopupContainerV2 from './PopupContainerV2.vue'
const props = defineProps({ const props = defineProps({
show: { show: {

View File

@@ -1,277 +0,0 @@
<!--
统一弹窗组件
## 基础用法
<PopupContainer v-model:show="show" title="标题">
内容
</PopupContainer>
## 确认对话框用法
<PopupContainer
v-model:show="showConfirm"
title="确认操作"
show-confirm-button
show-cancel-button
confirm-text="确定"
cancel-text="取消"
@confirm="handleConfirm"
@cancel="handleCancel"
>
确定要执行此操作吗
</PopupContainer>
## 带副标题和页脚
<PopupContainer
v-model:show="show"
title="分类详情"
subtitle="共 10 笔交易"
>
内容区域
<template #footer>
<van-button type="primary">提交</van-button>
</template>
</PopupContainer>
-->
<!-- eslint-disable vue/no-v-html -->
<template>
<van-popup
v-model:show="visible"
position="bottom"
:style="{ height: height }"
round
:closeable="closeable"
teleport="body"
>
<div class="popup-container">
<!-- 头部区域 -->
<div class="popup-header-fixed">
<!-- 标题行 (无子标题且有操作时使用 Grid 布局) -->
<div
class="header-title-row"
:class="{ 'has-actions': !subtitle && hasActions }"
>
<h3 class="popup-title">
{{ title }}
</h3>
<!-- 无子标题时操作按钮与标题同行 -->
<div
v-if="!subtitle && hasActions"
class="header-actions-inline"
>
<slot name="header-actions" />
</div>
</div>
<!-- 子标题/统计信息 -->
<div
v-if="subtitle"
class="header-stats"
>
<span
class="stats-text"
v-html="subtitle"
/>
<!-- 额外操作插槽 -->
<slot
v-if="hasActions"
name="header-actions"
/>
</div>
</div>
<!-- 内容区域可滚动 -->
<div class="popup-scroll-content">
<slot />
</div>
<!-- 底部页脚固定不可滚动 -->
<div
v-if="slots.footer || showConfirmButton || showCancelButton"
class="popup-footer-fixed"
>
<!-- 用户自定义页脚插槽 -->
<slot name="footer">
<!-- 默认确认/取消按钮 -->
<div class="footer-buttons">
<van-button
v-if="showCancelButton"
plain
@click="handleCancel"
>
{{ cancelText }}
</van-button>
<van-button
v-if="showConfirmButton"
type="primary"
@click="handleConfirm"
>
{{ confirmText }}
</van-button>
</div>
</slot>
</div>
</div>
</van-popup>
</template>
<script setup>
import { computed, useSlots } from 'vue'
const props = defineProps({
modelValue: {
type: Boolean,
required: true
},
title: {
type: String,
default: ''
},
subtitle: {
type: String,
default: ''
},
height: {
type: String,
default: '80%'
},
closeable: {
type: Boolean,
default: true
},
showConfirmButton: {
type: Boolean,
default: false
},
showCancelButton: {
type: Boolean,
default: false
},
confirmText: {
type: String,
default: '确认'
},
cancelText: {
type: String,
default: '取消'
}
})
const emit = defineEmits(['update:modelValue', 'confirm', 'cancel'])
const slots = useSlots()
// 双向绑定
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
// 判断是否有操作按钮
const hasActions = computed(() => !!slots['header-actions'])
// 确认按钮点击
const handleConfirm = () => {
emit('confirm')
}
// 取消按钮点击
const handleCancel = () => {
emit('cancel')
}
</script>
<style scoped>
.popup-container {
height: 100%;
display: flex;
flex-direction: column;
}
.popup-header-fixed {
flex-shrink: 0;
padding: 16px;
background: linear-gradient(180deg, var(--van-background) 0%, var(--van-background-2) 100%);
border-bottom: 1px solid var(--van-border-color);
position: sticky;
top: 0;
z-index: 10;
}
.header-title-row.has-actions {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
}
.header-title-row.has-actions .popup-title {
grid-column: 2;
min-width: 0;
}
.header-actions-inline {
grid-column: 3;
justify-self: end;
display: flex;
align-items: center;
}
.popup-title {
font-size: 16px;
font-weight: 600;
margin: 0;
text-align: center;
color: var(--van-text-color);
letter-spacing: -0.02em;
/*超出长度*/
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.header-stats {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: 12px;
margin-top: 12px;
}
.stats-text {
margin: 0;
font-size: 14px;
color: var(--van-text-color-2);
grid-column: 2;
text-align: center;
}
/* 按钮区域放在右侧 */
.header-stats :deep(> :last-child:not(.stats-text)) {
grid-column: 3;
justify-self: end;
}
.popup-scroll-content {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
}
.popup-footer-fixed {
flex-shrink: 0;
border-top: 1px solid var(--van-border-color);
background-color: var(--van-background-2);
padding: 12px 16px;
}
.footer-buttons {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.footer-buttons .van-button {
flex: 1;
max-width: 120px;
}
</style>

View File

@@ -14,7 +14,7 @@ PopupContainer V2 - 通用底部弹窗组件(采用 TransactionDetailSheet 样
</PopupContainerV2> </PopupContainerV2>
## Props ## Props
- modelValue (Boolean, required): 控制弹窗显示/隐藏 - show (Boolean, required): 控制弹窗显示/隐藏
- title (String, required): 标题文本 - title (String, required): 标题文本
- height (String, default: 'auto'): 弹窗高度支持 'auto', '80%', '500px' - height (String, default: 'auto'): 弹窗高度支持 'auto', '80%', '500px'
- maxHeight (String, default: '85%'): 最大高度 - maxHeight (String, default: '85%'): 最大高度
@@ -24,7 +24,7 @@ PopupContainer V2 - 通用底部弹窗组件(采用 TransactionDetailSheet 样
- footer: 固定底部区域操作按钮等 - footer: 固定底部区域操作按钮等
## Events ## Events
- update:modelValue: 弹窗显示/隐藏状态变更 - update:show: 弹窗显示/隐藏状态变更
--> -->
<template> <template>
<van-popup <van-popup
@@ -71,7 +71,7 @@ PopupContainer V2 - 通用底部弹窗组件(采用 TransactionDetailSheet 样
import { computed, useSlots } from 'vue' import { computed, useSlots } from 'vue'
const props = defineProps({ const props = defineProps({
modelValue: { show: {
type: Boolean, type: Boolean,
required: true required: true
}, },
@@ -89,14 +89,14 @@ const props = defineProps({
} }
}) })
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:show'])
const slots = useSlots() const slots = useSlots()
// 双向绑定 // 双向绑定
const visible = computed({ const visible = computed({
get: () => props.modelValue, get: () => props.show,
set: (value) => emit('update:modelValue', value) set: (value) => emit('update:show', value)
}) })
// 判断是否有 footer 插槽 // 判断是否有 footer 插槽

View File

@@ -61,13 +61,20 @@
</van-cell-group> </van-cell-group>
<!-- 账单列表弹窗 --> <!-- 账单列表弹窗 -->
<PopupContainer <PopupContainerV2
v-model="showTransactionList" v-model:show="showTransactionList"
:title="selectedGroup?.reason || '交易记录'" :title="selectedGroup?.reason || '交易记录'"
:subtitle="groupTransactionsTotal ? `共 ${groupTransactionsTotal} 笔交易` : ''" :height="'75%'"
height="75%"
> >
<template #header-actions> <div style="padding: 0">
<!-- Subtitle 和操作按钮 -->
<div style="padding: 12px 16px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--van-border-color)">
<span
v-if="groupTransactionsTotal"
style="color: #999; font-size: 14px"
>
{{ groupTransactionsTotal }} 笔交易
</span>
<van-button <van-button
type="primary" type="primary"
size="small" size="small"
@@ -76,7 +83,7 @@
> >
批量分类 批量分类
</van-button> </van-button>
</template> </div>
<BillListComponent <BillListComponent
data-source="custom" data-source="custom"
@@ -88,7 +95,8 @@
@click="handleTransactionClick" @click="handleTransactionClick"
@delete="handleGroupTransactionDelete" @delete="handleGroupTransactionDelete"
/> />
</PopupContainer> </div>
</PopupContainerV2>
<!-- 账单详情弹窗 --> <!-- 账单详情弹窗 -->
<TransactionDetail <TransactionDetail
@@ -98,11 +106,12 @@
/> />
<!-- 批量设置对话框 --> <!-- 批量设置对话框 -->
<PopupContainer <PopupContainerV2
v-model="showBatchDialog" v-model:show="showBatchDialog"
title="批量设置分类" title="批量设置分类"
height="60%" :height="'60%'"
> >
<div style="padding: 0">
<van-form <van-form
ref="batchFormRef" ref="batchFormRef"
class="setting-form" class="setting-form"
@@ -168,6 +177,7 @@
/> />
</van-cell-group> </van-cell-group>
</van-form> </van-form>
</div>
<template #footer> <template #footer>
<van-button <van-button
round round
@@ -178,7 +188,7 @@
确定 确定
</van-button> </van-button>
</template> </template>
</PopupContainer> </PopupContainerV2>
</div> </div>
</template> </template>
@@ -189,7 +199,7 @@ import { getReasonGroups, batchUpdateByReason, getTransactionList } from '@/api/
import ClassifySelector from './ClassifySelector.vue' import ClassifySelector from './ClassifySelector.vue'
import BillListComponent from './Bill/BillListComponent.vue' import BillListComponent from './Bill/BillListComponent.vue'
import TransactionDetail from './TransactionDetail.vue' import TransactionDetail from './TransactionDetail.vue'
import PopupContainer from './PopupContainer.vue' import PopupContainerV2 from './PopupContainerV2.vue'
const props = defineProps({ const props = defineProps({
// 是否支持多选 // 是否支持多选

View File

@@ -1,10 +1,10 @@
<template> <template>
<PopupContainer <PopupContainerV2
v-model="visible" v-model:show="visible"
title="交易详情" title="交易详情"
height="75%" :height="'75%'"
:closeable="false"
> >
<div style="padding: 0">
<van-form style="margin-top: 12px"> <van-form style="margin-top: 12px">
<van-cell-group inset> <van-cell-group inset>
<van-cell <van-cell
@@ -129,6 +129,7 @@
/> />
</van-cell-group> </van-cell-group>
</van-form> </van-form>
</div>
<template #footer> <template #footer>
<van-button <van-button
@@ -141,7 +142,7 @@
保存修改 保存修改
</van-button> </van-button>
</template> </template>
</PopupContainer> </PopupContainerV2>
<!-- 日期选择弹窗 --> <!-- 日期选择弹窗 -->
<van-popup <van-popup
@@ -178,7 +179,7 @@
import { ref, reactive, watch, defineProps, defineEmits, computed, nextTick } from 'vue' import { ref, reactive, watch, defineProps, defineEmits, computed, nextTick } from 'vue'
import { showToast } from 'vant' import { showToast } from 'vant'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import PopupContainer from '@/components/PopupContainer.vue' import PopupContainerV2 from '@/components/PopupContainerV2.vue'
import ClassifySelector from '@/components/ClassifySelector.vue' import ClassifySelector from '@/components/ClassifySelector.vue'
import { updateTransaction } from '@/api/transactionRecord' import { updateTransaction } from '@/api/transactionRecord'

View File

@@ -94,16 +94,12 @@
</div> </div>
<!-- 提示词设置弹窗 --> <!-- 提示词设置弹窗 -->
<PopupContainer <PopupContainerV2
v-model:show="showPromptDialog" v-model:show="showPromptDialog"
title="编辑分析提示词" title="编辑分析提示词"
show-cancel-button :height="'75%'"
show-confirm-button
confirm-text="保存"
cancel-text="取消"
@confirm="confirmPrompt"
@cancel="showPromptDialog = false"
> >
<div style="padding: 16px">
<van-field <van-field
v-model="promptValue" v-model="promptValue"
rows="4" rows="4"
@@ -113,7 +109,26 @@
placeholder="输入自定义的分析提示词..." placeholder="输入自定义的分析提示词..."
show-word-limit show-word-limit
/> />
</PopupContainer> </div>
<template #footer>
<div style="display: flex; gap: 12px">
<van-button
plain
style="flex: 1"
@click="showPromptDialog = false"
>
取消
</van-button>
<van-button
type="primary"
style="flex: 1"
@click="confirmPrompt"
>
保存
</van-button>
</div>
</template>
</PopupContainerV2>
</div> </div>
</template> </template>
@@ -122,7 +137,7 @@ import { ref, nextTick } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { showToast, showLoadingToast, closeToast } from 'vant' import { showToast, showLoadingToast, closeToast } from 'vant'
import { getConfig, setConfig } from '@/api/config' import { getConfig, setConfig } from '@/api/config'
import PopupContainer from '@/components/PopupContainer.vue' import PopupContainerV2 from '@/components/PopupContainerV2.vue'
const router = useRouter() const router = useRouter()
const userInput = ref('') const userInput = ref('')

View File

@@ -112,16 +112,12 @@
</div> </div>
<!-- 新增分类对话框 --> <!-- 新增分类对话框 -->
<PopupContainer <PopupContainerV2
v-model:show="showAddDialog" v-model:show="showAddDialog"
title="新增分类" title="新增分类"
show-cancel-button :height="'auto'"
show-confirm-button
confirm-text="确认"
cancel-text="取消"
@confirm="handleConfirmAdd"
@cancel="resetAddForm"
> >
<div style="padding: 16px">
<van-form ref="addFormRef"> <van-form ref="addFormRef">
<van-field <van-field
v-model="addForm.name" v-model="addForm.name"
@@ -131,19 +127,34 @@
:rules="[{ required: true, message: '请输入分类名称' }]" :rules="[{ required: true, message: '请输入分类名称' }]"
/> />
</van-form> </van-form>
</PopupContainer> </div>
<template #footer>
<div style="display: flex; gap: 12px">
<van-button
plain
style="flex: 1"
@click="resetAddForm"
>
取消
</van-button>
<van-button
type="primary"
style="flex: 1"
@click="handleConfirmAdd"
>
确认
</van-button>
</div>
</template>
</PopupContainerV2>
<!-- 编辑分类对话框 --> <!-- 编辑分类对话框 -->
<PopupContainer <PopupContainerV2
v-model:show="showEditDialog" v-model:show="showEditDialog"
title="编辑分类" title="编辑分类"
show-cancel-button :height="'auto'"
show-confirm-button
confirm-text="保存"
cancel-text="取消"
@confirm="handleConfirmEdit"
@cancel="showEditDialog = false"
> >
<div style="padding: 16px">
<van-form ref="editFormRef"> <van-form ref="editFormRef">
<van-field <van-field
v-model="editForm.name" v-model="editForm.name"
@@ -153,23 +164,55 @@
:rules="[{ required: true, message: '请输入分类名称' }]" :rules="[{ required: true, message: '请输入分类名称' }]"
/> />
</van-form> </van-form>
</PopupContainer> </div>
<template #footer>
<div style="display: flex; gap: 12px">
<van-button
plain
style="flex: 1"
@click="showEditDialog = false"
>
取消
</van-button>
<van-button
type="primary"
style="flex: 1"
@click="handleConfirmEdit"
>
保存
</van-button>
</div>
</template>
</PopupContainerV2>
<!-- 删除确认对话框 --> <!-- 删除确认对话框 -->
<PopupContainer <PopupContainerV2
v-model:show="showDeleteConfirm" v-model:show="showDeleteConfirm"
title="删除分类" title="删除分类"
show-cancel-button :height="'auto'"
show-confirm-button
confirm-text="确定"
cancel-text="取消"
@confirm="handleConfirmDelete"
@cancel="showDeleteConfirm = false"
> >
<p style="text-align: center; padding: 20px; color: var(--van-text-color-2)"> <p style="text-align: center; padding: 20px; color: var(--van-text-color-2)">
删除后无法恢复确定要删除吗 删除后无法恢复确定要删除吗
</p> </p>
</PopupContainer> <template #footer>
<div style="display: flex; gap: 12px">
<van-button
plain
style="flex: 1"
@click="showDeleteConfirm = false"
>
取消
</van-button>
<van-button
type="primary"
style="flex: 1"
@click="handleConfirmDelete"
>
确定
</van-button>
</div>
</template>
</PopupContainerV2>
<!-- 图标选择对话框 --> <!-- 图标选择对话框 -->
<IconSelector <IconSelector
@@ -189,7 +232,7 @@ import { useRouter } from 'vue-router'
import { showSuccessToast, showToast, showLoadingToast, closeToast } from 'vant' import { showSuccessToast, showToast, showLoadingToast, closeToast } from 'vant'
import Icon from '@/components/Icon.vue' import Icon from '@/components/Icon.vue'
import IconSelector from '@/components/IconSelector.vue' import IconSelector from '@/components/IconSelector.vue'
import PopupContainer from '@/components/PopupContainer.vue' import PopupContainerV2 from '@/components/PopupContainerV2.vue'
import { import {
getCategoryList, getCategoryList,
createCategory, createCategory,

View File

@@ -71,12 +71,12 @@
/> />
<!-- 记录列表弹窗 --> <!-- 记录列表弹窗 -->
<PopupContainer <PopupContainerV2
v-model="showRecordsList" v-model:show="showRecordsList"
title="交易记录列表" title="交易记录列表"
height="75%" :height="'75%'"
> >
<div style="background: var(--van-background)"> <div style="background: var(--van-background); padding: 0">
<!-- 批量操作按钮 --> <!-- 批量操作按钮 -->
<div class="batch-actions"> <div class="batch-actions">
<van-button <van-button
@@ -122,7 +122,7 @@
/> />
</div> </div>
</div> </div>
</PopupContainer> </PopupContainerV2>
</div> </div>
</template> </template>
@@ -133,7 +133,7 @@ import { showToast, showConfirmDialog } from 'vant'
import { nlpAnalysis, batchUpdateClassify } from '@/api/transactionRecord' import { nlpAnalysis, batchUpdateClassify } from '@/api/transactionRecord'
import BillListComponent from '@/components/Bill/BillListComponent.vue' import BillListComponent from '@/components/Bill/BillListComponent.vue'
import TransactionDetail from '@/components/TransactionDetail.vue' import TransactionDetail from '@/components/TransactionDetail.vue'
import PopupContainer from '@/components/PopupContainer.vue' import PopupContainerV2 from '@/components/PopupContainerV2.vue'
const router = useRouter() const router = useRouter()
const userInput = ref('') const userInput = ref('')

View File

@@ -73,12 +73,14 @@
</van-pull-refresh> </van-pull-refresh>
<!-- 详情弹出层 --> <!-- 详情弹出层 -->
<PopupContainer <PopupContainerV2
v-model="detailVisible" v-model:show="detailVisible"
:title="currentEmail ? currentEmail.Subject || currentEmail.subject || '(无主题)' : ''" :title="currentEmail ? currentEmail.Subject || currentEmail.subject || '(无主题)' : ''"
height="75%" :height="'75%'"
> >
<template #header-actions> <div v-if="currentEmail">
<!-- 操作按钮栏 -->
<div style="padding: 12px 16px; text-align: right; border-bottom: 1px solid var(--van-border-color)">
<van-button <van-button
size="small" size="small"
type="primary" type="primary"
@@ -87,9 +89,8 @@
> >
重新分析 重新分析
</van-button> </van-button>
</template> </div>
<div v-if="currentEmail">
<van-cell-group <van-cell-group
inset inset
style="margin-top: 12px" style="margin-top: 12px"
@@ -140,13 +141,13 @@
</div> </div>
</div> </div>
</div> </div>
</PopupContainer> </PopupContainerV2>
<!-- 账单列表弹出层 --> <!-- 账单列表弹出层 -->
<PopupContainer <PopupContainerV2
v-model="transactionListVisible" v-model:show="transactionListVisible"
title="关联账单列表" title="关联账单列表"
height="75%" :height="'75%'"
> >
<BillListComponent <BillListComponent
data-source="custom" data-source="custom"
@@ -158,7 +159,7 @@
@click="handleTransactionClick" @click="handleTransactionClick"
@delete="handleTransactionDelete" @delete="handleTransactionDelete"
/> />
</PopupContainer> </PopupContainerV2>
<!-- 账单详情编辑弹出层 --> <!-- 账单详情编辑弹出层 -->
<TransactionDetail <TransactionDetail
@@ -184,7 +185,7 @@ import {
import { getTransactionDetail } from '@/api/transactionRecord' import { getTransactionDetail } from '@/api/transactionRecord'
import BillListComponent from '@/components/Bill/BillListComponent.vue' import BillListComponent from '@/components/Bill/BillListComponent.vue'
import TransactionDetail from '@/components/TransactionDetail.vue' import TransactionDetail from '@/components/TransactionDetail.vue'
import PopupContainer from '@/components/PopupContainer.vue' import PopupContainerV2 from '@/components/PopupContainerV2.vue'
const emailList = ref([]) const emailList = ref([])
const loading = ref(false) const loading = ref(false)

View File

@@ -71,23 +71,28 @@
</van-pull-refresh> </van-pull-refresh>
<!-- 详情弹出层 --> <!-- 详情弹出层 -->
<PopupContainer <PopupContainerV2
v-model="detailVisible" v-model:show="detailVisible"
:title="currentMessage.title" :title="currentMessage.title"
:subtitle="currentMessage.createTime" :height="'75%'"
height="75%"
> >
<div style="padding: 16px">
<p style="color: #999; font-size: 14px; margin-bottom: 12px; margin-top: 0">
{{ currentMessage.createTime }}
</p>
<div <div
v-if="currentMessage.messageType === 2" v-if="currentMessage.messageType === 2"
class="detail-content rich-html-content" class="rich-html-content"
style="font-size: 14px; line-height: 1.6"
v-html="currentMessage.content" v-html="currentMessage.content"
/> />
<div <div
v-else v-else
class="detail-content" style="font-size: 14px; line-height: 1.6; white-space: pre-wrap"
> >
{{ currentMessage.content }} {{ currentMessage.content }}
</div> </div>
</div>
<template <template
v-if="currentMessage.url && currentMessage.messageType === 1" v-if="currentMessage.url && currentMessage.messageType === 1"
#footer #footer
@@ -101,7 +106,7 @@
查看详情 查看详情
</van-button> </van-button>
</template> </template>
</PopupContainer> </PopupContainerV2>
</div> </div>
</template> </template>
@@ -111,7 +116,7 @@ import { useRouter } from 'vue-router'
import { showToast, showDialog } from 'vant' import { showToast, showDialog } from 'vant'
import { getMessageList, markAsRead, deleteMessage, markAllAsRead } from '@/api/message' import { getMessageList, markAsRead, deleteMessage, markAllAsRead } from '@/api/message'
import { useMessageStore } from '@/stores/message' import { useMessageStore } from '@/stores/message'
import PopupContainer from '@/components/PopupContainer.vue' import PopupContainerV2 from '@/components/PopupContainerV2.vue'
const messageStore = useMessageStore() const messageStore = useMessageStore()
const router = useRouter() const router = useRouter()
@@ -325,22 +330,6 @@ defineExpose({
height: 100%; height: 100%;
} }
.detail-time {
color: var(--van-text-color-2);
font-size: 14px;
}
.detail-content {
padding: 16px;
font-size: 14px;
line-height: 1.6;
color: var(--van-text-color);
}
.detail-content:not(.rich-html-content) {
white-space: pre-wrap;
}
:deep(.van-pull-refresh) { :deep(.van-pull-refresh) {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;

View File

@@ -107,11 +107,12 @@
</div> </div>
<!-- 新增/编辑弹窗 --> <!-- 新增/编辑弹窗 -->
<PopupContainer <PopupContainerV2
v-model="dialogVisible" v-model:show="dialogVisible"
:title="isEdit ? '编辑周期账单' : '新增周期账单'" :title="isEdit ? '编辑周期账单' : '新增周期账单'"
height="75%" :height="'75%'"
> >
<div style="padding: 0">
<van-form> <van-form>
<van-cell-group <van-cell-group
inset inset
@@ -242,6 +243,7 @@
/> />
</van-cell-group> </van-cell-group>
</van-form> </van-form>
</div>
<template #footer> <template #footer>
<van-button <van-button
round round
@@ -253,7 +255,7 @@
{{ isEdit ? '更新' : '确认添加' }} {{ isEdit ? '更新' : '确认添加' }}
</van-button> </van-button>
</template> </template>
</PopupContainer> </PopupContainerV2>
<!-- 周期类型选择器 --> <!-- 周期类型选择器 -->
<van-popup <van-popup
@@ -310,7 +312,7 @@ import {
createPeriodic, createPeriodic,
updatePeriodic updatePeriodic
} from '@/api/transactionPeriodic' } from '@/api/transactionPeriodic'
import PopupContainer from '@/components/PopupContainer.vue' import PopupContainerV2 from '@/components/PopupContainerV2.vue'
import ClassifySelector from '@/components/ClassifySelector.vue' import ClassifySelector from '@/components/ClassifySelector.vue'
import dayjs from 'dayjs' import dayjs from 'dayjs'

View File

@@ -151,163 +151,22 @@
<!-- 储蓄配置弹窗 --> <!-- 储蓄配置弹窗 -->
<SavingsConfigPopup <SavingsConfigPopup
ref="savingsConfigRef" ref="savingsConfigRef"
@success="loadBudgetData" @change="loadBudgetData"
/> />
<!-- 预算明细列表弹窗 -->
<PopupContainer
v-model="showListPopup"
:title="popupTitle"
height="80%"
>
<template #header-actions>
<van-icon
name="plus"
size="20"
title="添加预算"
@click="budgetEditRef.open({ category: activeTab })"
/>
</template>
<van-pull-refresh
v-model="refreshing"
style="min-height: 100%"
@refresh="onRefresh"
>
<div class="budget-list">
<!-- 支出列表 -->
<template v-if="activeTab === BudgetCategory.Expense && expenseBudgets?.length > 0">
<van-swipe-cell
v-for="budget in expenseBudgets"
:key="budget.id"
>
<BudgetCard
:budget="budget"
:progress-color="getProgressColor(budget)"
:percent-class="{ warning: budget.current / budget.limit > 0.8 }"
:period-label="getPeriodLabel(budget.type)"
@click="handleEdit(budget)"
>
<template #amount-info>
<div class="info-item">
<div class="label">
已支出
</div>
<div class="value expense">
¥{{ formatMoney(budget.current) }}
</div>
</div>
<div class="info-item">
<div class="label">
预算
</div>
<div class="value">
¥{{ formatMoney(budget.limit) }}
</div>
</div>
<div class="info-item">
<div class="label">
余额
</div>
<div
class="value"
:class="budget.limit - budget.current >= 0 ? 'income' : 'expense'"
>
¥{{ formatMoney(budget.limit - budget.current) }}
</div>
</div>
</template>
</BudgetCard>
<template #right>
<van-button
square
text="删除"
type="danger"
class="delete-button"
@click="handleDelete(budget)"
/>
</template>
</van-swipe-cell>
</template>
<!-- 收入列表 -->
<template v-if="activeTab === BudgetCategory.Income && incomeBudgets?.length > 0">
<van-swipe-cell
v-for="budget in incomeBudgets"
:key="budget.id"
>
<BudgetCard
:budget="budget"
:progress-color="getProgressColor(budget)"
:percent-class="{ income: budget.current / budget.limit >= 1 }"
:period-label="getPeriodLabel(budget.type)"
@click="handleEdit(budget)"
>
<template #amount-info>
<div class="info-item">
<div class="label">
已收入
</div>
<div class="value income">
¥{{ formatMoney(budget.current) }}
</div>
</div>
<div class="info-item">
<div class="label">
目标
</div>
<div class="value">
¥{{ formatMoney(budget.limit) }}
</div>
</div>
<div class="info-item">
<div class="label">
差额
</div>
<div
class="value"
:class="budget.current >= budget.limit ? 'income' : 'expense'"
>
¥{{ formatMoney(Math.abs(budget.limit - budget.current)) }}
</div>
</div>
</template>
</BudgetCard>
<template #right>
<van-button
square
text="删除"
type="danger"
class="delete-button"
@click="handleDelete(budget)"
/>
</template>
</van-swipe-cell>
</template>
<!-- 空状态 -->
<van-empty
v-if="
activeTab !== BudgetCategory.Savings &&
!loading &&
!hasError &&
((activeTab === BudgetCategory.Expense && expenseBudgets?.length === 0) ||
(activeTab === BudgetCategory.Income && incomeBudgets?.length === 0))
"
:description="`暂无${activeTab === BudgetCategory.Expense ? '支出' : '收入'}预算`"
/>
</div>
<div style="height: calc(95px + env(safe-area-inset-bottom, 0px))" />
</van-pull-refresh>
</PopupContainer>
<!-- 未覆盖分类弹窗 --> <!-- 未覆盖分类弹窗 -->
<PopupContainer <PopupContainerV2
v-model="showUncoveredDetails" v-model:show="showUncoveredDetails"
title="未覆盖预算的分类" title="未覆盖预算的分类"
:subtitle="`本月共 <b style='color:var(--van-primary-color)'>${uncoveredCategories.length}</b> 个分类未设置预算`" :height="'60%'"
height="60%"
> >
<div style="padding: 0">
<!-- subtitle 作为内容区域顶部 -->
<div
style="padding: 12px 16px; text-align: center; color: #999; font-size: 14px; border-bottom: 1px solid var(--van-border-color)"
v-html="`本月共 <b style='color:var(--van-primary-color)'>${uncoveredCategories.length}</b> 个分类未设置预算`"
/>
<div class="uncovered-list"> <div class="uncovered-list">
<div <div
v-for="item in uncoveredCategories" v-for="item in uncoveredCategories"
@@ -332,6 +191,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<template #footer> <template #footer>
<van-button <van-button
@@ -343,15 +203,20 @@
我知道了 我知道了
</van-button> </van-button>
</template> </template>
</PopupContainer> </PopupContainerV2>
<!-- 归档总结弹窗 --> <!-- 归档总结弹窗 -->
<PopupContainer <PopupContainerV2
v-model="showSummaryPopup" v-model:show="showSummaryPopup"
title="月份归档总结" title="月份归档总结"
:subtitle="`${selectedDate.getFullYear()}年${selectedDate.getMonth() + 1}月`" :height="'70%'"
height="70%"
> >
<div style="padding: 0">
<!-- subtitle -->
<div style="padding: 12px 16px; text-align: center; color: #999; font-size: 14px; border-bottom: 1px solid var(--van-border-color)">
{{ selectedDate.getFullYear() }}年{{ selectedDate.getMonth() + 1 }}月
</div>
<div style="padding: 16px"> <div style="padding: 16px">
<div <div
class="rich-html-content" class="rich-html-content"
@@ -361,7 +226,8 @@
" "
/> />
</div> </div>
</PopupContainer> </div>
</PopupContainerV2>
<!-- 日期选择器 --> <!-- 日期选择器 -->
<van-popup <van-popup
@@ -401,7 +267,7 @@ import BudgetTypeTabs from '@/components/BudgetTypeTabs.vue'
import BudgetCard from '@/components/Budget/BudgetCard.vue' import BudgetCard from '@/components/Budget/BudgetCard.vue'
import BudgetEditPopup from '@/components/Budget/BudgetEditPopup.vue' import BudgetEditPopup from '@/components/Budget/BudgetEditPopup.vue'
import SavingsConfigPopup from '@/components/Budget/SavingsConfigPopup.vue' import SavingsConfigPopup from '@/components/Budget/SavingsConfigPopup.vue'
import PopupContainer from '@/components/PopupContainer.vue' import PopupContainerV2 from '@/components/PopupContainerV2.vue'
import ExpenseBudgetContent from './modules/ExpenseBudgetContent.vue' import ExpenseBudgetContent from './modules/ExpenseBudgetContent.vue'
import IncomeBudgetContent from './modules/IncomeBudgetContent.vue' import IncomeBudgetContent from './modules/IncomeBudgetContent.vue'
import SavingsBudgetContent from './modules/SavingsBudgetContent.vue' import SavingsBudgetContent from './modules/SavingsBudgetContent.vue'

View File

@@ -71,10 +71,10 @@
</div> </div>
<!-- 计划存款明细弹窗 --> <!-- 计划存款明细弹窗 -->
<PopupContainer <PopupContainerV2
v-model="showDetailPopup" v-model:show="showDetailPopup"
title="计划存款明细" title="计划存款明细"
height="80%" :height="'80%'"
> >
<div class="popup-body"> <div class="popup-body">
<div <div
@@ -169,14 +169,14 @@
</div> </div>
</div> </div>
</div> </div>
</PopupContainer> </PopupContainerV2>
</template> </template>
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import BudgetCard from '@/components/Budget/BudgetCard.vue' import BudgetCard from '@/components/Budget/BudgetCard.vue'
import { BudgetPeriodType } from '@/constants/enums' import { BudgetPeriodType } from '@/constants/enums'
import PopupContainer from '@/components/PopupContainer.vue' import PopupContainerV2 from '@/components/PopupContainerV2.vue'
// Props // Props
const props = defineProps({ const props = defineProps({

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-20

View File

@@ -0,0 +1,216 @@
## Context
当前项目中存在两个弹窗组件:
- **PopupContainer.vue (V1)**: 使用 Vant 主题变量,支持 subtitle、header-actions 插槽、确认/取消按钮等丰富功能,默认高度 80%,被 18 个组件使用
- **PopupContainerV2.vue (V2)**: 采用 Inter 字体和现代化设计风格纯色背景、16px 圆角API 更简洁(只提供 title、footer 插槽),默认高度 auto最大 85%),已被 TransactionDetailSheet 使用
V1 和 V2 的 API 差异较大V1 提供了更多开箱即用的功能如内置按钮、subtitle而 V2 追求灵活性和视觉一致性。此次迁移需要在保持功能不变的前提下,统一使用 V2 的现代化设计。
**约束条件**:
- 不能改变现有页面的交互逻辑和用户体验
- 需要保持暗色模式的正确支持
- 必须通过 ESLint 和现有的代码规范
## Goals / Non-Goals
**Goals:**
- 统一弹窗组件为 PopupContainerV2删除 V1 版本
- 迁移 18 个使用 V1 的组件,保持功能等价性
- 适配 API 差异props → 插槽、样式调整)
- 确保迁移后视觉效果和交互逻辑一致
**Non-Goals:**
- 不重新设计弹窗的交互流程或视觉风格(完全按照 V2 现有设计)
- 不优化或重构业务逻辑(仅做组件替换和 API 适配)
- 不处理与弹窗无关的代码问题
## Decisions
### Decision 1: 迁移策略 - 逐文件手动迁移 vs 自动化脚本
**选择**: 手动逐文件迁移
**理由**:
- V1 和 V2 的 API 差异大props → 插槽),无法通过简单的查找替换完成
- 每个组件对 subtitle、header-actions、确认/取消按钮的使用方式不同,需要根据业务语义定制迁移方案
- 自动化脚本的开发成本高于手动迁移 18 个文件的时间成本
- 手动迁移可以确保每个文件的视觉和逻辑正确性
**备选方案**:
- AST 转换工具(如 jscodeshift复杂度高难以处理插槽和样式的语义转换
### Decision 2: subtitle 功能的迁移方式
**选择**: 根据业务语义分类处理
- **统计信息类 subtitle**(如 "共 10 笔交易")→ 移至默认插槽顶部,使用自定义样式组件
- **纯文本副标题** → 移至默认插槽,或合并到 title 中(如 "分类详情 - 餐饮"
**理由**:
- V2 没有 subtitle prop必须通过插槽实现
- 统计信息通常有特定的业务含义,应作为内容区域的一部分而非标题的附属
- 纯文本副标题可以简化为一级标题的扩展
**备选方案**:
- 扩展 V2 组件增加 subtitle prop违背 V2 简化 API 的设计原则,不采纳
### Decision 3: 确认/取消按钮的迁移方式
**选择**: 转换为 footer 插槽 + 手动事件绑定
**实现模式**:
```vue
<!-- V1 -->
<PopupContainer
show-confirm-button
show-cancel-button
confirm-text="确定"
@confirm="handleConfirm"
@cancel="handleCancel"
/>
<!-- V2 -->
<PopupContainerV2>
<template #footer>
<div class="footer-buttons">
<van-button plain @click="handleCancel">取消</van-button>
<van-button type="primary" @click="handleConfirm">确定</van-button>
</div>
</template>
</PopupContainerV2>
<style scoped>
.footer-buttons {
display: flex;
gap: 12px;
}
.footer-buttons .van-button {
flex: 1;
}
</style>
```
**理由**:
- V2 的 footer 插槽提供了足够的灵活性
- Vant Button 的样式与 V1 内置按钮一致,迁移成本低
- 手动绑定事件可以保持原有的业务逻辑不变
### Decision 4: header-actions 插槽的处理
**选择**: 移至默认插槽顶部或改用自定义布局
**理由**:
- V2 没有 header-actions 插槽,标题区域只有标题文本和关闭按钮
- 根据现有代码(如 BudgetCard.vue、SavingsBudgetContent.vueheader-actions 通常是"编辑"、"删除"等操作按钮
- 这些按钮更适合放在内容区域顶部或 footer 中,符合 V2 的极简标题设计
**实现模式**:
```vue
<!-- V2 -->
<PopupContainerV2>
<div class="content-with-actions">
<div class="action-bar">
<van-button size="small" @click="handleEdit">编辑</van-button>
</div>
<!-- 原内容区域 -->
</div>
</PopupContainerV2>
```
### Decision 5: 高度属性的处理
**选择**: 显式指定 `:height="'80%'"` 保持视觉一致性
**理由**:
- V1 默认 `height="80%"`V2 默认 `height="auto"`(最大 85%
- 直接使用 V2 的 auto 可能导致内容过少时弹窗过小,影响用户体验
- 显式设置 80% 可以确保迁移前后视觉效果一致
- 如果某些组件的内容确实很少,可以在迁移时根据实际情况调整为 auto
### Decision 6: 样式和暗色模式的适配
**选择**: 信任 V2 的内置暗色模式支持,不额外修改
**理由**:
- V2 已在组件内部通过 `@media (prefers-color-scheme: dark)` 实现暗色模式
- V1 使用 Vant 的 CSS 变量V2 使用硬编码颜色,但两者在暗色模式下都能正确切换
- 业务组件只需确保内容区域的样式兼容暗色模式即可
**风险**: 如果业务组件的内容区域使用了与 V2 不兼容的颜色,需要单独调整(通过人工检查)
## Risks / Trade-offs
### Risk 1: 迁移后视觉效果差异
**风险**: V1 和 V2 的字体、颜色、圆角不同,可能导致用户感知到不一致
**缓解措施**:
- 在开发环境逐个测试迁移后的页面
- 重点检查弹窗的标题、内容、按钮的对齐和间距
- 如果某个页面的差异过大,考虑调整 V2 的样式或在该页面单独处理
### Risk 2: subtitle 和 header-actions 迁移语义变化
**风险**: 将 subtitle 移至内容区域可能改变信息层级header-actions 移至内容顶部可能影响交互流畅性
**缓解措施**:
- 迁移时保持原有的语义和视觉层级(如 subtitle 仍然显示在顶部且样式相似)
- 通过 CSS 模拟 V1 的 Grid 布局,确保按钮位置不变
### Risk 3: 高度变化导致滚动问题
**风险**: V1 的 80% 固定高度和 V2 的 auto 可能导致滚动行为不同(如内容过多时 V2 可能超出最大高度)
**缓解措施**:
- 统一使用 `:height="'80%'"` 作为默认值
- 对于内容特别少的弹窗(如确认对话框),可以单独设置为 auto
### Risk 4: 事件处理器遗漏
**风险**: 手动迁移确认/取消按钮时,可能遗漏原有的 `@confirm``@cancel` 事件逻辑
**缓解措施**:
- 迁移前通过搜索确认每个组件是否使用了这些事件
- 迁移后通过功能测试验证按钮点击是否正确触发
### Risk 5: ESLint 和代码规范问题
**风险**: 手动创建的 footer 插槽可能不符合项目的 ESLint 规则(如缩进、引号)
**缓解措施**:
- 迁移完成后运行 `pnpm lint` 并修复所有错误
- 参考现有 V2 的使用示例TransactionDetailSheet.vue保持风格一致
## Migration Plan
### Phase 1: 准备阶段
1. 审查 18 个待迁移文件,分析每个文件使用的 V1 功能subtitle、buttons、header-actions
2. 为每个文件制定迁移清单(需要修改的部分)
### Phase 2: 迁移阶段
逐文件迁移,按以下步骤:
1. 更新 import 路径和组件名
2. 替换基础 props保留 v-model:show、title显式设置 height
3. 迁移 subtitle根据语义选择方案
4. 迁移 header-actions移至内容区域或 footer
5. 迁移确认/取消按钮(创建 footer 插槽)
6. 调整内容区域的 paddingV2 无默认 padding
7. 测试功能和视觉效果
### Phase 3: 验证阶段
1. 运行 `pnpm lint` 确保代码规范
2. 手动测试每个迁移的页面,验证弹窗的打开/关闭、内容展示、按钮交互
3. 检查暗色模式下的显示效果
### Phase 4: 清理阶段
1. 确认所有迁移完成且测试通过
2. 删除 `Web/src/components/PopupContainer.vue`
3. 全局搜索 `PopupContainer` 确认无残留引用
### Rollback 策略
- 如果迁移后发现重大问题(如性能下降、功能缺失),可以通过 Git 回滚到迁移前的版本
- V1 和 V2 是独立文件,迁移失败不会影响现有功能(除非删除了 V1
- 建议在完成所有迁移并验证通过后再删除 V1 文件
## Open Questions
1. **是否需要对 V2 组件进行增强?**
- 例如增加 subtitle prop 简化迁移
- **暂定答案**: 不修改 V2保持其简洁性通过插槽实现所有功能
2. **是否需要统一 footer 按钮的样式?**
- 目前每个文件需要手动创建 `.footer-buttons` 样式
- **暂定答案**: 可以提取为全局样式或在 V2 中提供默认样式(后续优化项)
3. **是否需要通知用户 UI 风格变化?**
- V1 到 V2 的视觉差异可能被用户感知
- **暂定答案**: 作为内部优化,不需要用户通知

View File

@@ -0,0 +1,45 @@
## Why
项目中存在两个版本的弹窗组件(`PopupContainer.vue``PopupContainerV2.vue`造成代码冗余和维护成本增加。V2 版本采用更现代化的视觉风格Inter 字体、16px 圆角、纯色背景),且 API 更简洁。为了统一 UI 风格、减少技术债务,需要将所有使用旧版本的组件迁移到 V2并删除 V1 版本。
## What Changes
- 删除 `Web/src/components/PopupContainer.vue`(旧版本)
- 将 18 个使用 `PopupContainer` 的文件迁移到 `PopupContainerV2`
- 适配 API 差异V1 支持 subtitle、confirm/cancel 按钮V2 更简洁只提供 footer 插槽)
- 确保迁移后的视觉效果和交互逻辑保持一致
## Capabilities
### New Capabilities
- `popup-v2-migration`: 定义从 PopupContainer V1 到 V2 的迁移规范,包括 API 映射、样式对齐、兼容性处理
### Modified Capabilities
<!-- 无现有功能的需求变更 -->
## Impact
**受影响的文件**18 个 Vue 组件):
- `BudgetChartAnalysis.vue`
- `IconSelector.vue`
- `ClassificationEdit.vue`
- `SavingsBudgetContent.vue`
- `budgetV2/Index.vue`
- `PeriodicRecord.vue`
- `EmailRecord.vue`
- `ClassificationNLP.vue`
- `BillAnalysisView.vue`
- `TransactionDetail.vue`
- `ReasonGroupList.vue`
- `CategoryBillPopup.vue`
- `BudgetEditPopup.vue`
- `BudgetCard.vue`
- `AddClassifyDialog.vue`
- `MessageView.vue`
- `SavingsConfigPopup.vue`
- `GlobalAddBill.vue`
**迁移风险**:
- V1 的 `subtitle``showConfirmButton``showCancelButton` 等 props 在 V2 中不存在,需要重构为插槽方式
- V1 默认高度为 `80%`V2 为 `auto`(最大 `85%`),需要调整布局
- V1 使用 Vant 主题变量V2 使用硬编码颜色,暗色模式处理不同

View File

@@ -0,0 +1,108 @@
## ADDED Requirements
### Requirement: PopupContainer 组件导入路径迁移
所有引用 `PopupContainer.vue` 的文件必须更新导入路径为 `PopupContainerV2.vue`,并将组件名更改为 `PopupContainerV2`
#### Scenario: 更新 import 语句
- **WHEN** 文件中存在 `import PopupContainer from '@/components/PopupContainer.vue'``import PopupContainer from './PopupContainer.vue'`
- **THEN** 系统必须将其替换为 `import PopupContainerV2 from '@/components/PopupContainerV2.vue'`
#### Scenario: 更新模板中的组件名
- **WHEN** 模板中使用 `<PopupContainer>` 标签
- **THEN** 系统必须将其替换为 `<PopupContainerV2>`
### Requirement: Props API 映射转换
V1 和 V2 的 props 差异必须通过重构适配,确保功能等价。
#### Scenario: 基础 props 映射
- **WHEN** V1 使用 `v-model:show``title` 等基础 props
- **THEN** V2 必须保留这些 props 不变(`modelValue``title` 在两个版本中一致)
#### Scenario: height prop 默认值处理
- **WHEN** V1 未显式指定 `height` prop默认 `80%`
- **THEN** V2 必须显式添加 `:height="'80%'"` 以保持一致的视觉效果
#### Scenario: 移除不支持的 props
- **WHEN** V1 使用 `closeable``subtitle``showConfirmButton``showCancelButton``confirmText``cancelText` 等 props
- **THEN** 系统必须移除这些 props并通过插槽方式重构见下一需求
### Requirement: subtitle 功能迁移
V1 的 `subtitle` prop 必须转换为 V2 的默认插槽内容或自定义标题结构。
#### Scenario: subtitle 作为内容区域展示
- **WHEN** V1 使用 `subtitle` prop 显示副标题
- **THEN** 必须将 subtitle 内容移至 `<PopupContainerV2>` 的默认插槽中,并使用适当的样式包裹(如 `<p class="subtitle-text">{{ subtitle }}</p>`
#### Scenario: subtitle 包含 HTML 内容
- **WHEN** V1 的 `subtitle` 使用 `v-html` 渲染(如统计信息)
- **THEN** 必须在默认插槽中创建等价的 HTML 结构,保持语义和样式一致
### Requirement: 确认/取消按钮迁移
V1 的 `showConfirmButton``showCancelButton` 等按钮相关 props 必须转换为 V2 的 `footer` 插槽。
#### Scenario: 标准确认/取消按钮
- **WHEN** V1 使用 `show-confirm-button``show-cancel-button` props
- **THEN** 必须在 V2 的 `<template #footer>` 中手动创建 `<van-button>` 组件,绑定相同的事件处理器(`@confirm``@cancel`
#### Scenario: 自定义按钮文本
- **WHEN** V1 使用 `confirm-text``cancel-text` 自定义按钮文字
- **THEN** 必须将文本内容应用到 footer 插槽中的按钮组件
#### Scenario: 按钮布局样式
- **WHEN** 创建 footer 插槽内的按钮
- **THEN** 必须使用 flexbox 布局确保按钮水平排列,间距为 12px与 V1 的视觉效果一致
### Requirement: header-actions 插槽迁移
V1 的 `header-actions` 插槽必须根据业务逻辑转换为 V2 的内容区域或自定义实现。
#### Scenario: 移除 header-actions 插槽
- **WHEN** V1 使用 `<template #header-actions>` 插槽放置操作按钮
- **THEN** V2 必须将这些按钮移至默认插槽顶部或 footer 插槽中,根据业务语义选择合适位置
#### Scenario: 保持操作按钮的视觉层级
- **WHEN** V1 的 header-actions 与标题同行显示grid 布局)
- **THEN** 必须在 V2 的默认插槽中创建自定义布局,使用绝对定位或 flexbox 实现相同效果
### Requirement: 样式和暗色模式兼容性
迁移后的组件必须保持视觉一致性,正确响应暗色模式。
#### Scenario: 暗色模式自动适配
- **WHEN** 用户切换到暗色模式
- **THEN** V2 的硬编码颜色(`#ffffff``#09090b` 等)必须通过 `@media (prefers-color-scheme: dark)` 自动切换
#### Scenario: 内容区域 padding 处理
- **WHEN** V1 的可滚动内容区域有默认样式
- **THEN** V2 的内容插槽无默认 padding必须由使用方手动添加`<div class="content" style="padding: 16px">`
### Requirement: 事件处理器兼容性
V1 的事件(`@confirm``@cancel`)必须正确映射到 V2 的按钮点击事件。
#### Scenario: 确认事件触发
- **WHEN** 用户点击 footer 插槽中的确认按钮
- **THEN** 必须手动触发原有的 `@confirm` 事件处理逻辑(可能需要通过 `emit` 或直接调用方法)
#### Scenario: 取消事件触发
- **WHEN** 用户点击取消按钮或关闭弹窗
- **THEN** 必须确保原有的 `@cancel` 逻辑被正确执行V2 已通过关闭按钮自动关闭弹窗,但可能需要额外的清理逻辑)
### Requirement: 代码质量和测试
迁移后的代码必须通过 ESLint 检查,并保持功能正确性。
#### Scenario: ESLint 验证通过
- **WHEN** 完成迁移后
- **THEN** 运行 `pnpm lint` 必须无错误和警告
#### Scenario: 功能回归测试
- **WHEN** 迁移后的页面加载
- **THEN** 弹窗的打开/关闭、内容展示、按钮交互必须与迁移前行为一致
### Requirement: 删除旧版本组件
所有迁移完成后,必须删除 `PopupContainer.vue` 文件以避免混淆。
#### Scenario: 文件删除
- **WHEN** 所有 18 个文件迁移完成并验证通过
- **THEN** 系统必须删除 `Web/src/components/PopupContainer.vue` 文件
#### Scenario: 无残留引用
- **WHEN** 删除旧组件后
- **THEN** 项目中不得存在任何对 `PopupContainer.vue` 的引用(通过全局搜索验证)

View File

@@ -0,0 +1,72 @@
## 1. 准备和分析阶段
- [x] 1.1 审查 18 个待迁移文件,确认每个文件使用的 V1 功能subtitle、buttons、header-actions
- [x] 1.2 为每个文件创建迁移清单,标记需要特殊处理的部分(如 v-html、复杂布局
- [x] 1.3 备份当前代码(确保 Git 工作区干净,可以随时回滚)
## 2. 核心组件迁移 - 第一批(基础用法)
- [x] 2.1 迁移 `MessageView.vue` - 基础弹窗用法,无 subtitle 和按钮
- [x] 2.2 迁移 `EmailRecord.vue` - 基础弹窗用法
- [x] 2.3 迁移 `PeriodicRecord.vue` - 基础弹窗用法
- [x] 2.4 迁移 `ClassificationNLP.vue` - 基础弹窗用法
- [x] 2.5 迁移 `BillAnalysisView.vue` - 基础弹窗用法
- [x] 2.6 验证第一批迁移:运行 `pnpm lint`,手动测试每个页面的弹窗功能
## 3. 带 subtitle 的组件迁移 - 第二批
- [x] 3.1 迁移 `CategoryBillPopup.vue` - 处理 subtitle统计信息
- [x] 3.2 迁移 `BudgetChartAnalysis.vue` - 处理 subtitle
- [x] 3.3 迁移 `TransactionDetail.vue` - 处理 subtitle 和自定义内容
- [x] 3.4 迁移 `ReasonGroupList.vue` - 处理 subtitle
- [x] 3.5 验证第二批迁移:检查 subtitle 在默认插槽中的样式和位置
## 4. 带确认/取消按钮的组件迁移 - 第三批
- [x] 4.1 迁移 `AddClassifyDialog.vue` - 转换 show-confirm-button 和 show-cancel-button 为 footer 插槽
- [x] 4.2 迁移 `IconSelector.vue` - 处理确认/取消按钮和事件绑定
- [x] 4.3 迁移 `ClassificationEdit.vue` - 处理确认/取消按钮
- [x] 4.4 验证第三批迁移:测试按钮点击事件是否正确触发
## 5. 复杂布局组件迁移 - 第四批header-actions 和自定义布局)
- [x] 5.1 迁移 `BudgetCard.vue` - 处理 header-actions 插槽,移至内容区域顶部
- [x] 5.2 迁移 `BudgetEditPopup.vue` - 处理 header-actions 和 footer
- [x] 5.3 迁移 `SavingsConfigPopup.vue` - 处理自定义布局
- [x] 5.4 迁移 `SavingsBudgetContent.vue` - 处理 header-actions
- [x] 5.5 迁移 `budgetV2/Index.vue` - 处理复杂布局和多个弹窗实例
- [x] 5.6 验证第四批迁移:检查操作按钮的位置和交互是否符合预期
## 6. 全局组件迁移 - 第五批
- [x] 6.1 迁移 `GlobalAddBill.vue` - 处理全局弹窗的特殊逻辑
- [x] 6.2 验证全局组件:测试从不同页面触发弹窗的功能
## 7. 高度和样式调整
- [x] 7.1 检查所有迁移文件,为未显式设置 height 的组件添加 `:height="'80%'"`
- [x] 7.2 调整内容区域的 paddingV2 无默认 padding需要手动添加
- [x] 7.3 统一 footer 按钮的样式(创建全局 `.footer-buttons` 样式或在每个文件中复用)
- [ ] 7.4 验证暗色模式:切换到暗色模式,检查每个弹窗的颜色和对比度
## 8. 代码质量和测试
- [x] 8.1 运行 `pnpm lint` 修复所有 ESLint 错误和警告
- [ ] 8.2 运行 `pnpm build` 确保构建成功
- [ ] 8.3 手动测试所有 18 个迁移的页面,验证弹窗的打开/关闭、内容展示、按钮交互
- [ ] 8.4 测试边界情况:长文本、空内容、多次打开/关闭弹窗
- [ ] 8.5 检查控制台是否有警告或错误信息
## 9. 清理和文档更新
- [x] 9.1 确认所有迁移完成且测试通过
- [x] 9.2 删除 `Web/src/components/PopupContainer.vue` 文件
- [x] 9.3 全局搜索 `PopupContainer`(排除 `PopupContainerV2`),确认无残留引用
- [ ] 9.4 更新项目文档(如有组件使用说明,更新为 V2 的使用方式)
- [ ] 9.5 提交代码,编写清晰的 commit message
## 10. 后续优化(可选)
- [ ] 10.1 提取 footer 按钮样式为全局 CSS 类或 V2 组件的默认样式
- [ ] 10.2 考虑为 V2 添加常用的预设(如 `preset="confirm-dialog"`)简化未来的使用
- [ ] 10.3 在团队中分享迁移经验,更新最佳实践文档

View File

@@ -0,0 +1,108 @@
## ADDED Requirements
### Requirement: PopupContainer 组件导入路径迁移
所有引用 `PopupContainer.vue` 的文件必须更新导入路径为 `PopupContainerV2.vue`,并将组件名更改为 `PopupContainerV2`
#### Scenario: 更新 import 语句
- **WHEN** 文件中存在 `import PopupContainer from '@/components/PopupContainer.vue'``import PopupContainer from './PopupContainer.vue'`
- **THEN** 系统必须将其替换为 `import PopupContainerV2 from '@/components/PopupContainerV2.vue'`
#### Scenario: 更新模板中的组件名
- **WHEN** 模板中使用 `<PopupContainer>` 标签
- **THEN** 系统必须将其替换为 `<PopupContainerV2>`
### Requirement: Props API 映射转换
V1 和 V2 的 props 差异必须通过重构适配,确保功能等价。
#### Scenario: 基础 props 映射
- **WHEN** V1 使用 `v-model:show``title` 等基础 props
- **THEN** V2 必须保留这些 props 不变(`modelValue``title` 在两个版本中一致)
#### Scenario: height prop 默认值处理
- **WHEN** V1 未显式指定 `height` prop默认 `80%`
- **THEN** V2 必须显式添加 `:height="'80%'"` 以保持一致的视觉效果
#### Scenario: 移除不支持的 props
- **WHEN** V1 使用 `closeable``subtitle``showConfirmButton``showCancelButton``confirmText``cancelText` 等 props
- **THEN** 系统必须移除这些 props并通过插槽方式重构见下一需求
### Requirement: subtitle 功能迁移
V1 的 `subtitle` prop 必须转换为 V2 的默认插槽内容或自定义标题结构。
#### Scenario: subtitle 作为内容区域展示
- **WHEN** V1 使用 `subtitle` prop 显示副标题
- **THEN** 必须将 subtitle 内容移至 `<PopupContainerV2>` 的默认插槽中,并使用适当的样式包裹(如 `<p class="subtitle-text">{{ subtitle }}</p>`
#### Scenario: subtitle 包含 HTML 内容
- **WHEN** V1 的 `subtitle` 使用 `v-html` 渲染(如统计信息)
- **THEN** 必须在默认插槽中创建等价的 HTML 结构,保持语义和样式一致
### Requirement: 确认/取消按钮迁移
V1 的 `showConfirmButton``showCancelButton` 等按钮相关 props 必须转换为 V2 的 `footer` 插槽。
#### Scenario: 标准确认/取消按钮
- **WHEN** V1 使用 `show-confirm-button``show-cancel-button` props
- **THEN** 必须在 V2 的 `<template #footer>` 中手动创建 `<van-button>` 组件,绑定相同的事件处理器(`@confirm``@cancel`
#### Scenario: 自定义按钮文本
- **WHEN** V1 使用 `confirm-text``cancel-text` 自定义按钮文字
- **THEN** 必须将文本内容应用到 footer 插槽中的按钮组件
#### Scenario: 按钮布局样式
- **WHEN** 创建 footer 插槽内的按钮
- **THEN** 必须使用 flexbox 布局确保按钮水平排列,间距为 12px与 V1 的视觉效果一致
### Requirement: header-actions 插槽迁移
V1 的 `header-actions` 插槽必须根据业务逻辑转换为 V2 的内容区域或自定义实现。
#### Scenario: 移除 header-actions 插槽
- **WHEN** V1 使用 `<template #header-actions>` 插槽放置操作按钮
- **THEN** V2 必须将这些按钮移至默认插槽顶部或 footer 插槽中,根据业务语义选择合适位置
#### Scenario: 保持操作按钮的视觉层级
- **WHEN** V1 的 header-actions 与标题同行显示grid 布局)
- **THEN** 必须在 V2 的默认插槽中创建自定义布局,使用绝对定位或 flexbox 实现相同效果
### Requirement: 样式和暗色模式兼容性
迁移后的组件必须保持视觉一致性,正确响应暗色模式。
#### Scenario: 暗色模式自动适配
- **WHEN** 用户切换到暗色模式
- **THEN** V2 的硬编码颜色(`#ffffff``#09090b` 等)必须通过 `@media (prefers-color-scheme: dark)` 自动切换
#### Scenario: 内容区域 padding 处理
- **WHEN** V1 的可滚动内容区域有默认样式
- **THEN** V2 的内容插槽无默认 padding必须由使用方手动添加`<div class="content" style="padding: 16px">`
### Requirement: 事件处理器兼容性
V1 的事件(`@confirm``@cancel`)必须正确映射到 V2 的按钮点击事件。
#### Scenario: 确认事件触发
- **WHEN** 用户点击 footer 插槽中的确认按钮
- **THEN** 必须手动触发原有的 `@confirm` 事件处理逻辑(可能需要通过 `emit` 或直接调用方法)
#### Scenario: 取消事件触发
- **WHEN** 用户点击取消按钮或关闭弹窗
- **THEN** 必须确保原有的 `@cancel` 逻辑被正确执行V2 已通过关闭按钮自动关闭弹窗,但可能需要额外的清理逻辑)
### Requirement: 代码质量和测试
迁移后的代码必须通过 ESLint 检查,并保持功能正确性。
#### Scenario: ESLint 验证通过
- **WHEN** 完成迁移后
- **THEN** 运行 `pnpm lint` 必须无错误和警告
#### Scenario: 功能回归测试
- **WHEN** 迁移后的页面加载
- **THEN** 弹窗的打开/关闭、内容展示、按钮交互必须与迁移前行为一致
### Requirement: 删除旧版本组件
所有迁移完成后,必须删除 `PopupContainer.vue` 文件以避免混淆。
#### Scenario: 文件删除
- **WHEN** 所有 18 个文件迁移完成并验证通过
- **THEN** 系统必须删除 `Web/src/components/PopupContainer.vue` 文件
#### Scenario: 无残留引用
- **WHEN** 删除旧组件后
- **THEN** 项目中不得存在任何对 `PopupContainer.vue` 的引用(通过全局搜索验证)