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
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:
165
.doc/popup-migration-checklist.md
Normal file
165
.doc/popup-migration-checklist.md
Normal 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、复杂布局等特性
|
||||||
@@ -1,30 +1,45 @@
|
|||||||
<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"
|
|
||||||
>
|
>
|
||||||
<van-form ref="addFormRef">
|
<div style="padding: 16px">
|
||||||
<van-field
|
<van-form ref="addFormRef">
|
||||||
v-model="classifyName"
|
<van-field
|
||||||
name="name"
|
v-model="classifyName"
|
||||||
label="分类名称"
|
name="name"
|
||||||
placeholder="请输入分类名称"
|
label="分类名称"
|
||||||
:rules="[{ required: true, message: '请输入分类名称' }]"
|
placeholder="请输入分类名称"
|
||||||
/>
|
:rules="[{ required: true, message: '请输入分类名称' }]"
|
||||||
</van-form>
|
/>
|
||||||
</PopupContainer>
|
</van-form>
|
||||||
|
</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'])
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'])
|
||||||
|
|||||||
@@ -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'])
|
||||||
|
|||||||
@@ -1,111 +1,120 @@
|
|||||||
<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 class="transactions">
|
<div style="padding: 0">
|
||||||
<!-- 加载状态 -->
|
<!-- Subtitle 作为内容区域顶部 -->
|
||||||
<van-loading
|
|
||||||
v-if="loading && transactions.length === 0"
|
|
||||||
class="txn-loading"
|
|
||||||
size="24px"
|
|
||||||
vertical
|
|
||||||
>
|
|
||||||
加载中...
|
|
||||||
</van-loading>
|
|
||||||
|
|
||||||
<!-- 空状态 -->
|
|
||||||
<div
|
<div
|
||||||
v-else-if="transactions.length === 0"
|
v-if="total > 0"
|
||||||
class="txn-empty"
|
style="padding: 12px 16px; text-align: center; color: #999; font-size: 14px; border-bottom: 1px solid var(--van-border-color)"
|
||||||
>
|
>
|
||||||
<div class="empty-icon">
|
共 {{ total }} 笔交易
|
||||||
<van-icon
|
|
||||||
name="balance-list-o"
|
|
||||||
size="48"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="empty-text">
|
|
||||||
暂无交易记录
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 交易列表 -->
|
<div class="transactions">
|
||||||
<div
|
<!-- 加载状态 -->
|
||||||
v-else
|
<van-loading
|
||||||
class="txn-list"
|
v-if="loading && transactions.length === 0"
|
||||||
>
|
class="txn-loading"
|
||||||
<div
|
size="24px"
|
||||||
v-for="txn in transactions"
|
vertical
|
||||||
:key="txn.id"
|
|
||||||
class="txn-card"
|
|
||||||
@click="onTransactionClick(txn)"
|
|
||||||
>
|
>
|
||||||
<div
|
加载中...
|
||||||
class="txn-icon"
|
</van-loading>
|
||||||
:style="{ backgroundColor: txn.iconBg }"
|
|
||||||
>
|
<!-- 空状态 -->
|
||||||
|
<div
|
||||||
|
v-else-if="transactions.length === 0"
|
||||||
|
class="txn-empty"
|
||||||
|
>
|
||||||
|
<div class="empty-icon">
|
||||||
<van-icon
|
<van-icon
|
||||||
:name="txn.icon"
|
name="balance-list-o"
|
||||||
:color="txn.iconColor"
|
size="48"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="txn-content">
|
<div class="empty-text">
|
||||||
<div class="txn-name">
|
暂无交易记录
|
||||||
{{ txn.reason }}
|
|
||||||
</div>
|
|
||||||
<div class="txn-footer">
|
|
||||||
<div class="txn-time">
|
|
||||||
{{ formatDateTime(txn.occurredAt) }}
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
v-if="txn.classify"
|
|
||||||
class="txn-classify-tag"
|
|
||||||
:class="txn.type === 1 ? 'tag-income' : 'tag-expense'"
|
|
||||||
>
|
|
||||||
{{ txn.classify }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="txn-amount">
|
|
||||||
{{ formatAmount(txn.amount, txn.type) }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 加载更多 -->
|
<!-- 交易列表 -->
|
||||||
<div
|
|
||||||
v-if="!finished"
|
|
||||||
class="load-more"
|
|
||||||
>
|
|
||||||
<van-loading
|
|
||||||
v-if="loading"
|
|
||||||
size="20px"
|
|
||||||
>
|
|
||||||
加载中...
|
|
||||||
</van-loading>
|
|
||||||
<van-button
|
|
||||||
v-else
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
@click="loadMore"
|
|
||||||
>
|
|
||||||
加载更多
|
|
||||||
</van-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 已加载全部 -->
|
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="finished-text"
|
class="txn-list"
|
||||||
>
|
>
|
||||||
已加载全部
|
<div
|
||||||
|
v-for="txn in transactions"
|
||||||
|
:key="txn.id"
|
||||||
|
class="txn-card"
|
||||||
|
@click="onTransactionClick(txn)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="txn-icon"
|
||||||
|
:style="{ backgroundColor: txn.iconBg }"
|
||||||
|
>
|
||||||
|
<van-icon
|
||||||
|
:name="txn.icon"
|
||||||
|
:color="txn.iconColor"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="txn-content">
|
||||||
|
<div class="txn-name">
|
||||||
|
{{ txn.reason }}
|
||||||
|
</div>
|
||||||
|
<div class="txn-footer">
|
||||||
|
<div class="txn-time">
|
||||||
|
{{ formatDateTime(txn.occurredAt) }}
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
v-if="txn.classify"
|
||||||
|
class="txn-classify-tag"
|
||||||
|
:class="txn.type === 1 ? 'tag-income' : 'tag-expense'"
|
||||||
|
>
|
||||||
|
{{ txn.classify }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="txn-amount">
|
||||||
|
{{ formatAmount(txn.amount, txn.type) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 加载更多 -->
|
||||||
|
<div
|
||||||
|
v-if="!finished"
|
||||||
|
class="load-more"
|
||||||
|
>
|
||||||
|
<van-loading
|
||||||
|
v-if="loading"
|
||||||
|
size="20px"
|
||||||
|
>
|
||||||
|
加载中...
|
||||||
|
</van-loading>
|
||||||
|
<van-button
|
||||||
|
v-else
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="loadMore"
|
||||||
|
>
|
||||||
|
加载更多
|
||||||
|
</van-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 已加载全部 -->
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="finished-text"
|
||||||
|
>
|
||||||
|
已加载全部
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PopupContainer>
|
</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({
|
||||||
|
|||||||
@@ -9,41 +9,43 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add Bill Modal -->
|
<!-- Add Bill Modal -->
|
||||||
<PopupContainer
|
<PopupContainerV2
|
||||||
v-model="showAddBill"
|
v-model:show="showAddBill"
|
||||||
title="记一笔"
|
title="记一笔"
|
||||||
height="75%"
|
:height="'75%'"
|
||||||
>
|
>
|
||||||
<van-tabs
|
<div style="padding: 0">
|
||||||
v-model:active="activeTab"
|
<van-tabs
|
||||||
shrink
|
v-model:active="activeTab"
|
||||||
>
|
shrink
|
||||||
<van-tab
|
|
||||||
title="一句话录账"
|
|
||||||
name="one"
|
|
||||||
>
|
>
|
||||||
<OneLineBillAdd
|
<van-tab
|
||||||
:key="componentKey"
|
title="一句话录账"
|
||||||
@success="handleSuccess"
|
name="one"
|
||||||
/>
|
>
|
||||||
</van-tab>
|
<OneLineBillAdd
|
||||||
<van-tab
|
:key="componentKey"
|
||||||
title="手动录账"
|
@success="handleSuccess"
|
||||||
name="manual"
|
/>
|
||||||
>
|
</van-tab>
|
||||||
<ManualBillAdd
|
<van-tab
|
||||||
:key="componentKey"
|
title="手动录账"
|
||||||
@success="handleSuccess"
|
name="manual"
|
||||||
/>
|
>
|
||||||
</van-tab>
|
<ManualBillAdd
|
||||||
</van-tabs>
|
:key="componentKey"
|
||||||
</PopupContainer>
|
@success="handleSuccess"
|
||||||
|
/>
|
||||||
|
</van-tab>
|
||||||
|
</van-tabs>
|
||||||
|
</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'
|
||||||
|
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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 插槽
|
||||||
|
|||||||
@@ -61,34 +61,42 @@
|
|||||||
</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">
|
||||||
<van-button
|
<!-- Subtitle 和操作按钮 -->
|
||||||
type="primary"
|
<div style="padding: 12px 16px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--van-border-color)">
|
||||||
size="small"
|
<span
|
||||||
class="batch-classify-btn"
|
v-if="groupTransactionsTotal"
|
||||||
@click.stop="handleBatchClassify(selectedGroup)"
|
style="color: #999; font-size: 14px"
|
||||||
>
|
>
|
||||||
批量分类
|
共 {{ groupTransactionsTotal }} 笔交易
|
||||||
</van-button>
|
</span>
|
||||||
</template>
|
<van-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
class="batch-classify-btn"
|
||||||
|
@click.stop="handleBatchClassify(selectedGroup)"
|
||||||
|
>
|
||||||
|
批量分类
|
||||||
|
</van-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<BillListComponent
|
<BillListComponent
|
||||||
data-source="custom"
|
data-source="custom"
|
||||||
:transactions="groupTransactions"
|
:transactions="groupTransactions"
|
||||||
:loading="transactionLoading"
|
:loading="transactionLoading"
|
||||||
:finished="transactionFinished"
|
:finished="transactionFinished"
|
||||||
:enable-filter="false"
|
:enable-filter="false"
|
||||||
@load="loadGroupTransactions"
|
@load="loadGroupTransactions"
|
||||||
@click="handleTransactionClick"
|
@click="handleTransactionClick"
|
||||||
@delete="handleGroupTransactionDelete"
|
@delete="handleGroupTransactionDelete"
|
||||||
/>
|
/>
|
||||||
</PopupContainer>
|
</div>
|
||||||
|
</PopupContainerV2>
|
||||||
|
|
||||||
<!-- 账单详情弹窗 -->
|
<!-- 账单详情弹窗 -->
|
||||||
<TransactionDetail
|
<TransactionDetail
|
||||||
@@ -98,76 +106,78 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 批量设置对话框 -->
|
<!-- 批量设置对话框 -->
|
||||||
<PopupContainer
|
<PopupContainerV2
|
||||||
v-model="showBatchDialog"
|
v-model:show="showBatchDialog"
|
||||||
title="批量设置分类"
|
title="批量设置分类"
|
||||||
height="60%"
|
:height="'60%'"
|
||||||
>
|
>
|
||||||
<van-form
|
<div style="padding: 0">
|
||||||
ref="batchFormRef"
|
<van-form
|
||||||
class="setting-form"
|
ref="batchFormRef"
|
||||||
>
|
class="setting-form"
|
||||||
<van-cell-group inset>
|
>
|
||||||
<!-- 显示选中的摘要 -->
|
<van-cell-group inset>
|
||||||
<van-field
|
<!-- 显示选中的摘要 -->
|
||||||
:model-value="batchGroup?.reason"
|
<van-field
|
||||||
label="交易摘要"
|
:model-value="batchGroup?.reason"
|
||||||
readonly
|
label="交易摘要"
|
||||||
input-align="left"
|
readonly
|
||||||
/>
|
input-align="left"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 显示记录数量 -->
|
<!-- 显示记录数量 -->
|
||||||
<van-field
|
<van-field
|
||||||
:model-value="`${batchGroup?.count || 0} 条`"
|
:model-value="`${batchGroup?.count || 0} 条`"
|
||||||
label="记录数量"
|
label="记录数量"
|
||||||
readonly
|
readonly
|
||||||
input-align="left"
|
input-align="left"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 交易类型 -->
|
<!-- 交易类型 -->
|
||||||
<van-field
|
<van-field
|
||||||
name="type"
|
name="type"
|
||||||
label="交易类型"
|
label="交易类型"
|
||||||
>
|
>
|
||||||
<template #input>
|
<template #input>
|
||||||
<van-radio-group
|
<van-radio-group
|
||||||
v-model="batchForm.type"
|
v-model="batchForm.type"
|
||||||
direction="horizontal"
|
direction="horizontal"
|
||||||
>
|
>
|
||||||
<van-radio :name="0">
|
<van-radio :name="0">
|
||||||
支出
|
支出
|
||||||
</van-radio>
|
</van-radio>
|
||||||
<van-radio :name="1">
|
<van-radio :name="1">
|
||||||
收入
|
收入
|
||||||
</van-radio>
|
</van-radio>
|
||||||
<van-radio :name="2">
|
<van-radio :name="2">
|
||||||
不计
|
不计
|
||||||
</van-radio>
|
</van-radio>
|
||||||
</van-radio-group>
|
</van-radio-group>
|
||||||
</template>
|
</template>
|
||||||
</van-field>
|
</van-field>
|
||||||
|
|
||||||
<!-- 分类选择 -->
|
<!-- 分类选择 -->
|
||||||
<van-field
|
<van-field
|
||||||
name="classify"
|
name="classify"
|
||||||
label="分类"
|
label="分类"
|
||||||
>
|
>
|
||||||
<template #input>
|
<template #input>
|
||||||
<span
|
<span
|
||||||
v-if="!batchForm.classify"
|
v-if="!batchForm.classify"
|
||||||
style="opacity: 0.4"
|
style="opacity: 0.4"
|
||||||
>请选择分类</span>
|
>请选择分类</span>
|
||||||
<span v-else>{{ batchForm.classify }}</span>
|
<span v-else>{{ batchForm.classify }}</span>
|
||||||
</template>
|
</template>
|
||||||
</van-field>
|
</van-field>
|
||||||
|
|
||||||
<!-- 分类选择组件 -->
|
<!-- 分类选择组件 -->
|
||||||
<ClassifySelector
|
<ClassifySelector
|
||||||
v-model="batchForm.classify"
|
v-model="batchForm.classify"
|
||||||
:type="batchForm.type"
|
:type="batchForm.type"
|
||||||
/>
|
/>
|
||||||
</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({
|
||||||
// 是否支持多选
|
// 是否支持多选
|
||||||
|
|||||||
@@ -1,134 +1,135 @@
|
|||||||
<template>
|
<template>
|
||||||
<PopupContainer
|
<PopupContainerV2
|
||||||
v-model="visible"
|
v-model:show="visible"
|
||||||
title="交易详情"
|
title="交易详情"
|
||||||
height="75%"
|
:height="'75%'"
|
||||||
:closeable="false"
|
|
||||||
>
|
>
|
||||||
<van-form style="margin-top: 12px">
|
<div style="padding: 0">
|
||||||
<van-cell-group inset>
|
<van-form style="margin-top: 12px">
|
||||||
<van-cell
|
<van-cell-group inset>
|
||||||
title="记录时间"
|
<van-cell
|
||||||
:value="formatDate(transaction.createTime)"
|
title="记录时间"
|
||||||
/>
|
:value="formatDate(transaction.createTime)"
|
||||||
</van-cell-group>
|
/>
|
||||||
|
</van-cell-group>
|
||||||
|
|
||||||
<van-cell-group
|
<van-cell-group
|
||||||
inset
|
inset
|
||||||
title="交易明细"
|
title="交易明细"
|
||||||
>
|
|
||||||
<van-field
|
|
||||||
v-model="occurredAtLabel"
|
|
||||||
name="occurredAt"
|
|
||||||
label="交易时间"
|
|
||||||
readonly
|
|
||||||
is-link
|
|
||||||
placeholder="请选择交易时间"
|
|
||||||
:rules="[{ required: true, message: '请选择交易时间' }]"
|
|
||||||
@click="showDatePicker = true"
|
|
||||||
/>
|
|
||||||
<van-field
|
|
||||||
v-model="editForm.reason"
|
|
||||||
name="reason"
|
|
||||||
label="交易摘要"
|
|
||||||
placeholder="请输入交易摘要"
|
|
||||||
type="textarea"
|
|
||||||
rows="2"
|
|
||||||
autosize
|
|
||||||
maxlength="200"
|
|
||||||
show-word-limit
|
|
||||||
/>
|
|
||||||
<van-field
|
|
||||||
v-model="editForm.amount"
|
|
||||||
name="amount"
|
|
||||||
label="交易金额"
|
|
||||||
placeholder="请输入交易金额"
|
|
||||||
type="number"
|
|
||||||
:rules="[{ required: true, message: '请输入交易金额' }]"
|
|
||||||
/>
|
|
||||||
<van-field
|
|
||||||
v-model="editForm.balance"
|
|
||||||
name="balance"
|
|
||||||
label="交易后余额"
|
|
||||||
placeholder="请输入交易后余额"
|
|
||||||
type="number"
|
|
||||||
:rules="[{ required: true, message: '请输入交易后余额' }]"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<van-field
|
|
||||||
name="type"
|
|
||||||
label="交易类型"
|
|
||||||
>
|
>
|
||||||
<template #input>
|
<van-field
|
||||||
<van-radio-group
|
v-model="occurredAtLabel"
|
||||||
v-model="editForm.type"
|
name="occurredAt"
|
||||||
direction="horizontal"
|
label="交易时间"
|
||||||
@change="handleTypeChange"
|
readonly
|
||||||
>
|
is-link
|
||||||
<van-radio :name="0">
|
placeholder="请选择交易时间"
|
||||||
支出
|
:rules="[{ required: true, message: '请选择交易时间' }]"
|
||||||
</van-radio>
|
@click="showDatePicker = true"
|
||||||
<van-radio :name="1">
|
/>
|
||||||
收入
|
<van-field
|
||||||
</van-radio>
|
v-model="editForm.reason"
|
||||||
<van-radio :name="2">
|
name="reason"
|
||||||
不计
|
label="交易摘要"
|
||||||
</van-radio>
|
placeholder="请输入交易摘要"
|
||||||
</van-radio-group>
|
type="textarea"
|
||||||
</template>
|
rows="2"
|
||||||
</van-field>
|
autosize
|
||||||
|
maxlength="200"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
<van-field
|
||||||
|
v-model="editForm.amount"
|
||||||
|
name="amount"
|
||||||
|
label="交易金额"
|
||||||
|
placeholder="请输入交易金额"
|
||||||
|
type="number"
|
||||||
|
:rules="[{ required: true, message: '请输入交易金额' }]"
|
||||||
|
/>
|
||||||
|
<van-field
|
||||||
|
v-model="editForm.balance"
|
||||||
|
name="balance"
|
||||||
|
label="交易后余额"
|
||||||
|
placeholder="请输入交易后余额"
|
||||||
|
type="number"
|
||||||
|
:rules="[{ required: true, message: '请输入交易后余额' }]"
|
||||||
|
/>
|
||||||
|
|
||||||
<van-field
|
<van-field
|
||||||
name="classify"
|
name="type"
|
||||||
label="交易分类"
|
label="交易类型"
|
||||||
>
|
>
|
||||||
<template #input>
|
<template #input>
|
||||||
<div style="flex: 1">
|
<van-radio-group
|
||||||
<div
|
v-model="editForm.type"
|
||||||
v-if="
|
direction="horizontal"
|
||||||
transaction &&
|
@change="handleTypeChange"
|
||||||
transaction.unconfirmedClassify &&
|
|
||||||
transaction.unconfirmedClassify !== editForm.classify
|
|
||||||
"
|
|
||||||
class="suggestion-tip"
|
|
||||||
@click="applySuggestion"
|
|
||||||
>
|
>
|
||||||
<van-icon
|
<van-radio :name="0">
|
||||||
name="bulb-o"
|
支出
|
||||||
class="suggestion-icon"
|
</van-radio>
|
||||||
/>
|
<van-radio :name="1">
|
||||||
<span class="suggestion-text">
|
收入
|
||||||
建议: {{ transaction.unconfirmedClassify }}
|
</van-radio>
|
||||||
<span
|
<van-radio :name="2">
|
||||||
v-if="
|
不计
|
||||||
transaction.unconfirmedType !== null &&
|
</van-radio>
|
||||||
transaction.unconfirmedType !== undefined &&
|
</van-radio-group>
|
||||||
transaction.unconfirmedType !== editForm.type
|
</template>
|
||||||
"
|
</van-field>
|
||||||
>
|
|
||||||
({{ getTypeName(transaction.unconfirmedType) }})
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<div class="suggestion-apply">
|
|
||||||
应用
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
v-else-if="!editForm.classify"
|
|
||||||
style="color: var(--van-gray-5)"
|
|
||||||
>请选择交易分类</span>
|
|
||||||
<span v-else>{{ editForm.classify }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</van-field>
|
|
||||||
|
|
||||||
<ClassifySelector
|
<van-field
|
||||||
v-model="editForm.classify"
|
name="classify"
|
||||||
:type="editForm.type"
|
label="交易分类"
|
||||||
@change="handleClassifyChange"
|
>
|
||||||
/>
|
<template #input>
|
||||||
</van-cell-group>
|
<div style="flex: 1">
|
||||||
</van-form>
|
<div
|
||||||
|
v-if="
|
||||||
|
transaction &&
|
||||||
|
transaction.unconfirmedClassify &&
|
||||||
|
transaction.unconfirmedClassify !== editForm.classify
|
||||||
|
"
|
||||||
|
class="suggestion-tip"
|
||||||
|
@click="applySuggestion"
|
||||||
|
>
|
||||||
|
<van-icon
|
||||||
|
name="bulb-o"
|
||||||
|
class="suggestion-icon"
|
||||||
|
/>
|
||||||
|
<span class="suggestion-text">
|
||||||
|
建议: {{ transaction.unconfirmedClassify }}
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
transaction.unconfirmedType !== null &&
|
||||||
|
transaction.unconfirmedType !== undefined &&
|
||||||
|
transaction.unconfirmedType !== editForm.type
|
||||||
|
"
|
||||||
|
>
|
||||||
|
({{ getTypeName(transaction.unconfirmedType) }})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<div class="suggestion-apply">
|
||||||
|
应用
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
v-else-if="!editForm.classify"
|
||||||
|
style="color: var(--van-gray-5)"
|
||||||
|
>请选择交易分类</span>
|
||||||
|
<span v-else>{{ editForm.classify }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</van-field>
|
||||||
|
|
||||||
|
<ClassifySelector
|
||||||
|
v-model="editForm.classify"
|
||||||
|
:type="editForm.type"
|
||||||
|
@change="handleClassifyChange"
|
||||||
|
/>
|
||||||
|
</van-cell-group>
|
||||||
|
</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'
|
||||||
|
|
||||||
|
|||||||
@@ -94,26 +94,41 @@
|
|||||||
</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"
|
|
||||||
>
|
>
|
||||||
<van-field
|
<div style="padding: 16px">
|
||||||
v-model="promptValue"
|
<van-field
|
||||||
rows="4"
|
v-model="promptValue"
|
||||||
autosize
|
rows="4"
|
||||||
type="textarea"
|
autosize
|
||||||
maxlength="2000"
|
type="textarea"
|
||||||
placeholder="输入自定义的分析提示词..."
|
maxlength="2000"
|
||||||
show-word-limit
|
placeholder="输入自定义的分析提示词..."
|
||||||
/>
|
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('')
|
||||||
|
|||||||
@@ -112,64 +112,107 @@
|
|||||||
</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"
|
|
||||||
>
|
>
|
||||||
<van-form ref="addFormRef">
|
<div style="padding: 16px">
|
||||||
<van-field
|
<van-form ref="addFormRef">
|
||||||
v-model="addForm.name"
|
<van-field
|
||||||
name="name"
|
v-model="addForm.name"
|
||||||
label="分类名称"
|
name="name"
|
||||||
placeholder="请输入分类名称"
|
label="分类名称"
|
||||||
:rules="[{ required: true, message: '请输入分类名称' }]"
|
placeholder="请输入分类名称"
|
||||||
/>
|
:rules="[{ required: true, message: '请输入分类名称' }]"
|
||||||
</van-form>
|
/>
|
||||||
</PopupContainer>
|
</van-form>
|
||||||
|
</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"
|
|
||||||
>
|
>
|
||||||
<van-form ref="editFormRef">
|
<div style="padding: 16px">
|
||||||
<van-field
|
<van-form ref="editFormRef">
|
||||||
v-model="editForm.name"
|
<van-field
|
||||||
name="name"
|
v-model="editForm.name"
|
||||||
label="分类名称"
|
name="name"
|
||||||
placeholder="请输入分类名称"
|
label="分类名称"
|
||||||
:rules="[{ required: true, message: '请输入分类名称' }]"
|
placeholder="请输入分类名称"
|
||||||
/>
|
:rules="[{ required: true, message: '请输入分类名称' }]"
|
||||||
</van-form>
|
/>
|
||||||
</PopupContainer>
|
</van-form>
|
||||||
|
</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,
|
||||||
|
|||||||
@@ -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('')
|
||||||
|
|||||||
@@ -73,23 +73,24 @@
|
|||||||
</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>
|
|
||||||
<van-button
|
|
||||||
size="small"
|
|
||||||
type="primary"
|
|
||||||
:loading="refreshingAnalysis"
|
|
||||||
@click="handleRefreshAnalysis"
|
|
||||||
>
|
|
||||||
重新分析
|
|
||||||
</van-button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div v-if="currentEmail">
|
<div v-if="currentEmail">
|
||||||
|
<!-- 操作按钮栏 -->
|
||||||
|
<div style="padding: 12px 16px; text-align: right; border-bottom: 1px solid var(--van-border-color)">
|
||||||
|
<van-button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
:loading="refreshingAnalysis"
|
||||||
|
@click="handleRefreshAnalysis"
|
||||||
|
>
|
||||||
|
重新分析
|
||||||
|
</van-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<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)
|
||||||
|
|||||||
@@ -71,22 +71,27 @@
|
|||||||
</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
|
<div style="padding: 16px">
|
||||||
v-if="currentMessage.messageType === 2"
|
<p style="color: #999; font-size: 14px; margin-bottom: 12px; margin-top: 0">
|
||||||
class="detail-content rich-html-content"
|
{{ currentMessage.createTime }}
|
||||||
v-html="currentMessage.content"
|
</p>
|
||||||
/>
|
<div
|
||||||
<div
|
v-if="currentMessage.messageType === 2"
|
||||||
v-else
|
class="rich-html-content"
|
||||||
class="detail-content"
|
style="font-size: 14px; line-height: 1.6"
|
||||||
>
|
v-html="currentMessage.content"
|
||||||
{{ currentMessage.content }}
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
style="font-size: 14px; line-height: 1.6; white-space: pre-wrap"
|
||||||
|
>
|
||||||
|
{{ currentMessage.content }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template
|
<template
|
||||||
v-if="currentMessage.url && currentMessage.messageType === 1"
|
v-if="currentMessage.url && currentMessage.messageType === 1"
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -107,141 +107,143 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 新增/编辑弹窗 -->
|
<!-- 新增/编辑弹窗 -->
|
||||||
<PopupContainer
|
<PopupContainerV2
|
||||||
v-model="dialogVisible"
|
v-model:show="dialogVisible"
|
||||||
:title="isEdit ? '编辑周期账单' : '新增周期账单'"
|
:title="isEdit ? '编辑周期账单' : '新增周期账单'"
|
||||||
height="75%"
|
:height="'75%'"
|
||||||
>
|
>
|
||||||
<van-form>
|
<div style="padding: 0">
|
||||||
<van-cell-group
|
<van-form>
|
||||||
inset
|
<van-cell-group
|
||||||
title="周期设置"
|
inset
|
||||||
>
|
title="周期设置"
|
||||||
<van-field
|
|
||||||
v-model="form.periodicTypeText"
|
|
||||||
is-link
|
|
||||||
readonly
|
|
||||||
name="periodicType"
|
|
||||||
label="周期"
|
|
||||||
placeholder="请选择周期类型"
|
|
||||||
:rules="[{ required: true, message: '请选择周期类型' }]"
|
|
||||||
@click="showPeriodicTypePicker = true"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 每周配置 -->
|
|
||||||
<van-field
|
|
||||||
v-if="form.periodicType === 1"
|
|
||||||
v-model="form.weekdaysText"
|
|
||||||
is-link
|
|
||||||
readonly
|
|
||||||
name="weekdays"
|
|
||||||
label="星期"
|
|
||||||
placeholder="请选择星期几"
|
|
||||||
:rules="[{ required: true, message: '请选择星期几' }]"
|
|
||||||
@click="showWeekdaysPicker = true"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 每月配置 -->
|
|
||||||
<van-field
|
|
||||||
v-if="form.periodicType === 2"
|
|
||||||
v-model="form.monthDaysText"
|
|
||||||
is-link
|
|
||||||
readonly
|
|
||||||
name="monthDays"
|
|
||||||
label="日期"
|
|
||||||
placeholder="请选择每月的日期"
|
|
||||||
:rules="[{ required: true, message: '请选择日期' }]"
|
|
||||||
@click="showMonthDaysPicker = true"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 每季度配置 -->
|
|
||||||
<van-field
|
|
||||||
v-if="form.periodicType === 3"
|
|
||||||
v-model="form.quarterDay"
|
|
||||||
name="quarterDay"
|
|
||||||
label="季度第几天"
|
|
||||||
placeholder="请输入季度开始后第几天"
|
|
||||||
type="number"
|
|
||||||
:rules="[{ required: true, message: '请输入季度开始后第几天' }]"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 每年配置 -->
|
|
||||||
<van-field
|
|
||||||
v-if="form.periodicType === 4"
|
|
||||||
v-model="form.yearDay"
|
|
||||||
name="yearDay"
|
|
||||||
label="年第几天"
|
|
||||||
placeholder="请输入年开始后第几天"
|
|
||||||
type="number"
|
|
||||||
:rules="[{ required: true, message: '请输入年开始后第几天' }]"
|
|
||||||
/>
|
|
||||||
</van-cell-group>
|
|
||||||
|
|
||||||
<van-cell-group
|
|
||||||
inset
|
|
||||||
title="基本信息"
|
|
||||||
>
|
|
||||||
<van-field
|
|
||||||
v-model="form.reason"
|
|
||||||
name="reason"
|
|
||||||
label="摘要"
|
|
||||||
placeholder="请输入交易摘要"
|
|
||||||
type="textarea"
|
|
||||||
rows="2"
|
|
||||||
autosize
|
|
||||||
maxlength="200"
|
|
||||||
show-word-limit
|
|
||||||
/>
|
|
||||||
<van-field
|
|
||||||
v-model="form.amount"
|
|
||||||
name="amount"
|
|
||||||
label="金额"
|
|
||||||
placeholder="请输入金额"
|
|
||||||
type="number"
|
|
||||||
:rules="[{ required: true, message: '请输入金额' }]"
|
|
||||||
/>
|
|
||||||
<van-field
|
|
||||||
v-model="form.type"
|
|
||||||
name="type"
|
|
||||||
label="类型"
|
|
||||||
>
|
>
|
||||||
<template #input>
|
<van-field
|
||||||
<van-radio-group
|
v-model="form.periodicTypeText"
|
||||||
v-model="form.type"
|
is-link
|
||||||
direction="horizontal"
|
readonly
|
||||||
>
|
name="periodicType"
|
||||||
<van-radio :value="0">
|
label="周期"
|
||||||
支出
|
placeholder="请选择周期类型"
|
||||||
</van-radio>
|
:rules="[{ required: true, message: '请选择周期类型' }]"
|
||||||
<van-radio :value="1">
|
@click="showPeriodicTypePicker = true"
|
||||||
收入
|
/>
|
||||||
</van-radio>
|
|
||||||
<van-radio :value="2">
|
|
||||||
不计
|
|
||||||
</van-radio>
|
|
||||||
</van-radio-group>
|
|
||||||
</template>
|
|
||||||
</van-field>
|
|
||||||
<van-field
|
|
||||||
name="classify"
|
|
||||||
label="分类"
|
|
||||||
>
|
|
||||||
<template #input>
|
|
||||||
<span
|
|
||||||
v-if="!form.classify"
|
|
||||||
style="color: var(--van-gray-5)"
|
|
||||||
>请选择交易分类</span>
|
|
||||||
<span v-else>{{ form.classify }}</span>
|
|
||||||
</template>
|
|
||||||
</van-field>
|
|
||||||
|
|
||||||
<!-- 分类选择组件 -->
|
<!-- 每周配置 -->
|
||||||
<ClassifySelector
|
<van-field
|
||||||
v-model="form.classify"
|
v-if="form.periodicType === 1"
|
||||||
:type="form.type"
|
v-model="form.weekdaysText"
|
||||||
/>
|
is-link
|
||||||
</van-cell-group>
|
readonly
|
||||||
</van-form>
|
name="weekdays"
|
||||||
|
label="星期"
|
||||||
|
placeholder="请选择星期几"
|
||||||
|
:rules="[{ required: true, message: '请选择星期几' }]"
|
||||||
|
@click="showWeekdaysPicker = true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 每月配置 -->
|
||||||
|
<van-field
|
||||||
|
v-if="form.periodicType === 2"
|
||||||
|
v-model="form.monthDaysText"
|
||||||
|
is-link
|
||||||
|
readonly
|
||||||
|
name="monthDays"
|
||||||
|
label="日期"
|
||||||
|
placeholder="请选择每月的日期"
|
||||||
|
:rules="[{ required: true, message: '请选择日期' }]"
|
||||||
|
@click="showMonthDaysPicker = true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 每季度配置 -->
|
||||||
|
<van-field
|
||||||
|
v-if="form.periodicType === 3"
|
||||||
|
v-model="form.quarterDay"
|
||||||
|
name="quarterDay"
|
||||||
|
label="季度第几天"
|
||||||
|
placeholder="请输入季度开始后第几天"
|
||||||
|
type="number"
|
||||||
|
:rules="[{ required: true, message: '请输入季度开始后第几天' }]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 每年配置 -->
|
||||||
|
<van-field
|
||||||
|
v-if="form.periodicType === 4"
|
||||||
|
v-model="form.yearDay"
|
||||||
|
name="yearDay"
|
||||||
|
label="年第几天"
|
||||||
|
placeholder="请输入年开始后第几天"
|
||||||
|
type="number"
|
||||||
|
:rules="[{ required: true, message: '请输入年开始后第几天' }]"
|
||||||
|
/>
|
||||||
|
</van-cell-group>
|
||||||
|
|
||||||
|
<van-cell-group
|
||||||
|
inset
|
||||||
|
title="基本信息"
|
||||||
|
>
|
||||||
|
<van-field
|
||||||
|
v-model="form.reason"
|
||||||
|
name="reason"
|
||||||
|
label="摘要"
|
||||||
|
placeholder="请输入交易摘要"
|
||||||
|
type="textarea"
|
||||||
|
rows="2"
|
||||||
|
autosize
|
||||||
|
maxlength="200"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
<van-field
|
||||||
|
v-model="form.amount"
|
||||||
|
name="amount"
|
||||||
|
label="金额"
|
||||||
|
placeholder="请输入金额"
|
||||||
|
type="number"
|
||||||
|
:rules="[{ required: true, message: '请输入金额' }]"
|
||||||
|
/>
|
||||||
|
<van-field
|
||||||
|
v-model="form.type"
|
||||||
|
name="type"
|
||||||
|
label="类型"
|
||||||
|
>
|
||||||
|
<template #input>
|
||||||
|
<van-radio-group
|
||||||
|
v-model="form.type"
|
||||||
|
direction="horizontal"
|
||||||
|
>
|
||||||
|
<van-radio :value="0">
|
||||||
|
支出
|
||||||
|
</van-radio>
|
||||||
|
<van-radio :value="1">
|
||||||
|
收入
|
||||||
|
</van-radio>
|
||||||
|
<van-radio :value="2">
|
||||||
|
不计
|
||||||
|
</van-radio>
|
||||||
|
</van-radio-group>
|
||||||
|
</template>
|
||||||
|
</van-field>
|
||||||
|
<van-field
|
||||||
|
name="classify"
|
||||||
|
label="分类"
|
||||||
|
>
|
||||||
|
<template #input>
|
||||||
|
<span
|
||||||
|
v-if="!form.classify"
|
||||||
|
style="color: var(--van-gray-5)"
|
||||||
|
>请选择交易分类</span>
|
||||||
|
<span v-else>{{ form.classify }}</span>
|
||||||
|
</template>
|
||||||
|
</van-field>
|
||||||
|
|
||||||
|
<!-- 分类选择组件 -->
|
||||||
|
<ClassifySelector
|
||||||
|
v-model="form.classify"
|
||||||
|
:type="form.type"
|
||||||
|
/>
|
||||||
|
</van-cell-group>
|
||||||
|
</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'
|
||||||
|
|
||||||
|
|||||||
@@ -151,183 +151,43 @@
|
|||||||
<!-- 储蓄配置弹窗 -->
|
<!-- 储蓄配置弹窗 -->
|
||||||
<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 class="uncovered-list">
|
<div style="padding: 0">
|
||||||
|
<!-- subtitle 作为内容区域顶部 -->
|
||||||
<div
|
<div
|
||||||
v-for="item in uncoveredCategories"
|
style="padding: 12px 16px; text-align: center; color: #999; font-size: 14px; border-bottom: 1px solid var(--van-border-color)"
|
||||||
:key="item.category"
|
v-html="`本月共 <b style='color:var(--van-primary-color)'>${uncoveredCategories.length}</b> 个分类未设置预算`"
|
||||||
class="uncovered-item"
|
/>
|
||||||
>
|
|
||||||
<div class="item-left">
|
<div class="uncovered-list">
|
||||||
<div class="category-name">
|
<div
|
||||||
{{ item.category }}
|
v-for="item in uncoveredCategories"
|
||||||
|
:key="item.category"
|
||||||
|
class="uncovered-item"
|
||||||
|
>
|
||||||
|
<div class="item-left">
|
||||||
|
<div class="category-name">
|
||||||
|
{{ item.category }}
|
||||||
|
</div>
|
||||||
|
<div class="transaction-count">
|
||||||
|
{{ item.transactionCount }} 笔记录
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="transaction-count">
|
<div class="item-right">
|
||||||
{{ item.transactionCount }} 笔记录
|
<div
|
||||||
</div>
|
class="item-amount"
|
||||||
</div>
|
:class="activeTab === BudgetCategory.Expense ? 'expense' : 'income'"
|
||||||
<div class="item-right">
|
>
|
||||||
<div
|
¥{{ formatMoney(item.totalAmount) }}
|
||||||
class="item-amount"
|
</div>
|
||||||
:class="activeTab === BudgetCategory.Expense ? 'expense' : 'income'"
|
|
||||||
>
|
|
||||||
¥{{ formatMoney(item.totalAmount) }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -343,25 +203,31 @@
|
|||||||
我知道了
|
我知道了
|
||||||
</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: 16px">
|
<div style="padding: 0">
|
||||||
<div
|
<!-- subtitle -->
|
||||||
class="rich-html-content"
|
<div style="padding: 12px 16px; text-align: center; color: #999; font-size: 14px; border-bottom: 1px solid var(--van-border-color)">
|
||||||
v-html="
|
{{ selectedDate.getFullYear() }}年{{ selectedDate.getMonth() + 1 }}月
|
||||||
archiveSummary ||
|
</div>
|
||||||
'<p style=\'text-align:center;color:var(--van-text-color-3)\'>暂无总结</p>'
|
|
||||||
"
|
<div style="padding: 16px">
|
||||||
/>
|
<div
|
||||||
|
class="rich-html-content"
|
||||||
|
v-html="
|
||||||
|
archiveSummary ||
|
||||||
|
'<p style=\'text-align:center;color:var(--van-text-color-3)\'>暂无总结</p>'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PopupContainer>
|
</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'
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-02-20
|
||||||
@@ -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.vue),header-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. 调整内容区域的 padding(V2 无默认 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 的视觉差异可能被用户感知
|
||||||
|
- **暂定答案**: 作为内部优化,不需要用户通知
|
||||||
@@ -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 使用硬编码颜色,暗色模式处理不同
|
||||||
@@ -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` 的引用(通过全局搜索验证)
|
||||||
@@ -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 调整内容区域的 padding(V2 无默认 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 在团队中分享迁移经验,更新最佳实践文档
|
||||||
108
openspec/specs/popup-v2-migration/spec.md
Normal file
108
openspec/specs/popup-v2-migration/spec.md
Normal 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` 的引用(通过全局搜索验证)
|
||||||
Reference in New Issue
Block a user