chore: migrate remaining ECharts components to Chart.js
- Migrated 4 components from ECharts to Chart.js: * MonthlyExpenseCard.vue (折线图) * DailyTrendChart.vue (双系列折线图) * ExpenseCategoryCard.vue (环形图) * BudgetChartAnalysis.vue (仪表盘 + 多种图表) - Removed all ECharts imports and environment variable switches - Unified all charts to use BaseChart.vue component - Build verified: pnpm build success ✓ - No echarts imports remaining ✓ Refs: openspec/changes/migrate-remaining-echarts-to-chartjs
This commit is contained in:
@@ -58,10 +58,10 @@
|
||||
>
|
||||
<van-cell :title="category.name">
|
||||
<template #icon>
|
||||
<div
|
||||
<Icon
|
||||
v-if="category.icon"
|
||||
class="category-icon"
|
||||
v-html="parseIcon(category.icon)"
|
||||
:icon-identifier="category.icon"
|
||||
:size="20"
|
||||
/>
|
||||
</template>
|
||||
<template #default>
|
||||
@@ -76,7 +76,7 @@
|
||||
</van-button>
|
||||
<van-button
|
||||
size="small"
|
||||
@click="handleEditOld(category)"
|
||||
@click="handleEdit(category)"
|
||||
>
|
||||
编辑
|
||||
</van-button>
|
||||
@@ -97,177 +97,110 @@
|
||||
|
||||
<!-- 底部安全距离 -->
|
||||
<div style="height: calc(55px + env(safe-area-inset-bottom, 0px))" />
|
||||
|
||||
<div class="bottom-button">
|
||||
<!-- 新增分类按钮 -->
|
||||
<van-button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon="plus"
|
||||
@click="handleAddCategory"
|
||||
>
|
||||
新增分类
|
||||
</van-button>
|
||||
</div>
|
||||
|
||||
<!-- 新增分类对话框 -->
|
||||
<PopupContainer
|
||||
v-model:show="showAddDialog"
|
||||
title="新增分类"
|
||||
show-cancel-button
|
||||
show-confirm-button
|
||||
confirm-text="确认"
|
||||
cancel-text="取消"
|
||||
@confirm="handleConfirmAdd"
|
||||
@cancel="resetAddForm"
|
||||
>
|
||||
<van-form ref="addFormRef">
|
||||
<van-field
|
||||
v-model="addForm.name"
|
||||
name="name"
|
||||
label="分类名称"
|
||||
placeholder="请输入分类名称"
|
||||
:rules="[{ required: true, message: '请输入分类名称' }]"
|
||||
/>
|
||||
</van-form>
|
||||
</PopupContainer>
|
||||
|
||||
<!-- 编辑分类对话框 -->
|
||||
<PopupContainer
|
||||
v-model:show="showEditDialog"
|
||||
title="编辑分类"
|
||||
show-cancel-button
|
||||
show-confirm-button
|
||||
confirm-text="保存"
|
||||
cancel-text="取消"
|
||||
@confirm="handleConfirmEdit"
|
||||
@cancel="showEditDialog = false"
|
||||
>
|
||||
<van-form ref="editFormRef">
|
||||
<van-field
|
||||
v-model="editForm.name"
|
||||
name="name"
|
||||
label="分类名称"
|
||||
placeholder="请输入分类名称"
|
||||
:rules="[{ required: true, message: '请输入分类名称' }]"
|
||||
/>
|
||||
</van-form>
|
||||
</PopupContainer>
|
||||
|
||||
<!-- 删除确认对话框 -->
|
||||
<PopupContainer
|
||||
v-model:show="showDeleteConfirm"
|
||||
title="删除分类"
|
||||
show-confirm-button
|
||||
show-cancel-button
|
||||
confirm-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="handleConfirmDelete"
|
||||
@cancel="showDeleteConfirm = false"
|
||||
>
|
||||
<p style="text-align: center; padding: 20px; color: var(--van-text-color-2)">
|
||||
删除后无法恢复,确定要删除吗?
|
||||
</p>
|
||||
</PopupContainer>
|
||||
|
||||
<!-- 删除图标确认对话框 -->
|
||||
<PopupContainer
|
||||
v-model:show="showDeleteIconConfirm"
|
||||
title="删除图标"
|
||||
show-confirm-button
|
||||
show-cancel-button
|
||||
confirm-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="handleConfirmDeleteIcon"
|
||||
@cancel="showDeleteIconConfirm = false"
|
||||
>
|
||||
<p style="text-align: center; padding: 20px; color: var(--van-text-color-2)">
|
||||
确定要删除图标吗?
|
||||
</p>
|
||||
</PopupContainer>
|
||||
|
||||
<!-- 图标选择对话框 -->
|
||||
<PopupContainer
|
||||
v-model:show="showIconDialog"
|
||||
title="选择图标"
|
||||
:closeable="false"
|
||||
>
|
||||
<div class="icon-selector">
|
||||
<div
|
||||
v-if="currentCategory && currentCategory.icon"
|
||||
class="icon-list"
|
||||
>
|
||||
<div
|
||||
v-for="(icon, index) in parseIconArray(currentCategory.icon)"
|
||||
:key="index"
|
||||
class="icon-item"
|
||||
:class="{ active: selectedIconIndex === index }"
|
||||
@click="selectedIconIndex = index"
|
||||
>
|
||||
<div
|
||||
class="icon-preview"
|
||||
v-html="icon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="empty-icons"
|
||||
>
|
||||
<van-empty description="暂无图标" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="icon-actions">
|
||||
<van-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="isGeneratingIcon"
|
||||
:disabled="isGeneratingIcon"
|
||||
@click="handleGenerateIcon"
|
||||
>
|
||||
{{ isGeneratingIcon ? 'AI生成中...' : '生成新图标' }}
|
||||
</van-button>
|
||||
<van-button
|
||||
v-if="currentCategory && currentCategory.icon"
|
||||
type="danger"
|
||||
size="small"
|
||||
plain
|
||||
:disabled="isDeletingIcon"
|
||||
style="margin-left: 20px;"
|
||||
@click="handleDeleteIcon"
|
||||
>
|
||||
{{ isDeletingIcon ? '删除中...' : '删除图标' }}
|
||||
</van-button>
|
||||
<van-button
|
||||
size="small"
|
||||
plain
|
||||
style="margin-left: 10px;"
|
||||
@click="showIconDialog = false"
|
||||
>
|
||||
关闭
|
||||
</van-button>
|
||||
</div>
|
||||
</template>
|
||||
</PopupContainer>
|
||||
</div>
|
||||
|
||||
<!-- 新增分类按钮 -->
|
||||
<div class="bottom-button">
|
||||
<van-button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon="plus"
|
||||
@click="handleAddCategory"
|
||||
>
|
||||
新增分类
|
||||
</van-button>
|
||||
</div>
|
||||
|
||||
<!-- 新增分类对话框 -->
|
||||
<PopupContainer
|
||||
v-model:show="showAddDialog"
|
||||
title="新增分类"
|
||||
show-cancel-button
|
||||
show-confirm-button
|
||||
confirm-text="确认"
|
||||
cancel-text="取消"
|
||||
@confirm="handleConfirmAdd"
|
||||
@cancel="resetAddForm"
|
||||
>
|
||||
<van-form ref="addFormRef">
|
||||
<van-field
|
||||
v-model="addForm.name"
|
||||
name="name"
|
||||
label="分类名称"
|
||||
placeholder="请输入分类名称"
|
||||
:rules="[{ required: true, message: '请输入分类名称' }]"
|
||||
/>
|
||||
</van-form>
|
||||
</PopupContainer>
|
||||
|
||||
<!-- 编辑分类对话框 -->
|
||||
<PopupContainer
|
||||
v-model:show="showEditDialog"
|
||||
title="编辑分类"
|
||||
show-cancel-button
|
||||
show-confirm-button
|
||||
confirm-text="保存"
|
||||
cancel-text="取消"
|
||||
@confirm="handleConfirmEdit"
|
||||
@cancel="showEditDialog = false"
|
||||
>
|
||||
<van-form ref="editFormRef">
|
||||
<van-field
|
||||
v-model="editForm.name"
|
||||
name="name"
|
||||
label="分类名称"
|
||||
placeholder="请输入分类名称"
|
||||
:rules="[{ required: true, message: '请输入分类名称' }]"
|
||||
/>
|
||||
</van-form>
|
||||
</PopupContainer>
|
||||
|
||||
<!-- 删除确认对话框 -->
|
||||
<PopupContainer
|
||||
v-model:show="showDeleteConfirm"
|
||||
title="删除分类"
|
||||
show-cancel-button
|
||||
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>
|
||||
</PopupContainer>
|
||||
|
||||
<!-- 图标选择对话框 -->
|
||||
<IconSelector
|
||||
v-model:show="showIconDialog"
|
||||
:icons="iconCandidates"
|
||||
:title="`为「${currentCategory?.name || ''}」选择图标`"
|
||||
:default-icon-identifier="currentCategory?.icon || ''"
|
||||
@confirm="handleConfirmIconSelect"
|
||||
@cancel="handleCancelIconSelect"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showSuccessToast, showToast, showLoadingToast, closeToast } from 'vant'
|
||||
import Icon from '@/components/Icon.vue'
|
||||
import IconSelector from '@/components/IconSelector.vue'
|
||||
import PopupContainer from '@/components/PopupContainer.vue'
|
||||
import {
|
||||
getCategoryList,
|
||||
createCategory,
|
||||
deleteCategory,
|
||||
updateCategory,
|
||||
generateIcon,
|
||||
updateSelectedIcon,
|
||||
deleteCategoryIcon
|
||||
updateCategory
|
||||
} from '@/api/transactionCategory'
|
||||
import PopupContainer from '@/components/PopupContainer.vue'
|
||||
import {
|
||||
generateSearchKeywords,
|
||||
searchIcons,
|
||||
updateCategoryIcon as updateCategoryIconApi
|
||||
} from '@/api/icons'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -279,7 +212,7 @@ const typeOptions = [
|
||||
]
|
||||
|
||||
// 层级状态
|
||||
const currentLevel = ref(0) // 0=类型选择, 1=分类管理
|
||||
const currentLevel = ref(0) // 0=类型选择, 1=分类管理
|
||||
const currentType = ref(null) // 当前选中的交易类型
|
||||
const currentTypeName = computed(() => {
|
||||
const type = typeOptions.find((t) => t.value === currentType.value)
|
||||
@@ -288,7 +221,6 @@ const currentTypeName = computed(() => {
|
||||
|
||||
// 分类数据
|
||||
const categories = ref([])
|
||||
|
||||
// 编辑对话框
|
||||
const showAddDialog = ref(false)
|
||||
const addFormRef = ref(null)
|
||||
@@ -310,13 +242,9 @@ const editForm = ref({
|
||||
|
||||
// 图标选择对话框
|
||||
const showIconDialog = ref(false)
|
||||
const currentCategory = ref(null) // 当前正在编辑图标的分类
|
||||
const selectedIconIndex = ref(0)
|
||||
const isGeneratingIcon = ref(false)
|
||||
|
||||
// 删除图标确认对话框
|
||||
const showDeleteIconConfirm = ref(false)
|
||||
const isDeletingIcon = ref(false)
|
||||
const currentCategory = ref(null)
|
||||
const iconCandidates = ref([])
|
||||
const isLoadingIcons = ref(false)
|
||||
|
||||
// 计算导航栏标题
|
||||
const navTitle = computed(() => {
|
||||
@@ -401,7 +329,6 @@ const handleAddCategory = () => {
|
||||
*/
|
||||
const handleConfirmAdd = async () => {
|
||||
try {
|
||||
// 表单验证
|
||||
await addFormRef.value?.validate()
|
||||
|
||||
showLoadingToast({
|
||||
@@ -432,68 +359,58 @@ const handleConfirmAdd = async () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑分类
|
||||
* 重置新增表单
|
||||
*/
|
||||
const handleEdit = (category) => {
|
||||
editForm.value = {
|
||||
id: category.id,
|
||||
name: category.name
|
||||
const resetAddForm = () => {
|
||||
addForm.value = {
|
||||
name: ''
|
||||
}
|
||||
showEditDialog.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开图标选择器
|
||||
*/
|
||||
const handleIconSelect = (category) => {
|
||||
const handleIconSelect = async (category) => {
|
||||
currentCategory.value = category
|
||||
selectedIconIndex.value = 0
|
||||
showIconDialog.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成新图标
|
||||
*/
|
||||
const handleGenerateIcon = async () => {
|
||||
if (!currentCategory.value) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isGeneratingIcon.value = true
|
||||
showLoadingToast({
|
||||
message: 'AI正在生成图标...',
|
||||
forbidClick: true,
|
||||
duration: 0
|
||||
})
|
||||
isLoadingIcons.value = true
|
||||
|
||||
const { success, data, message } = await generateIcon(currentCategory.value.id)
|
||||
const { success: keywordsSuccess, data: keywordsResponse } = await generateSearchKeywords(category.name)
|
||||
|
||||
if (success) {
|
||||
showSuccessToast('图标生成成功')
|
||||
// 重新加载分类列表以获取最新的图标
|
||||
await loadCategories()
|
||||
// 更新当前分类引用
|
||||
const updated = categories.value.find((c) => c.id === currentCategory.value.id)
|
||||
if (updated) {
|
||||
currentCategory.value = updated
|
||||
}
|
||||
} else {
|
||||
showToast(message || '生成图标失败')
|
||||
if (!keywordsSuccess || !keywordsResponse || !keywordsResponse.keywords || keywordsResponse.keywords.length === 0) {
|
||||
showToast('生成搜索关键字失败')
|
||||
return
|
||||
}
|
||||
|
||||
const { success: iconsSuccess, data: icons } = await searchIcons(keywordsResponse.keywords)
|
||||
|
||||
console.log('图标搜索响应:', { iconsSuccess, icons, iconsType: typeof icons, iconsIsArray: Array.isArray(icons) })
|
||||
|
||||
if (!iconsSuccess) {
|
||||
showToast('搜索图标失败')
|
||||
return
|
||||
}
|
||||
|
||||
if (!icons || icons.length === 0) {
|
||||
console.warn('图标数据为空')
|
||||
showToast('未找到匹配的图标')
|
||||
return
|
||||
}
|
||||
|
||||
iconCandidates.value = icons
|
||||
} catch (error) {
|
||||
console.error('生成图标失败:', error)
|
||||
showToast('生成图标失败: ' + (error.message || '未知错误'))
|
||||
} finally {
|
||||
isGeneratingIcon.value = false
|
||||
closeToast()
|
||||
console.error('搜索图标错误:', error)
|
||||
showToast('搜索图标失败')
|
||||
isLoadingIcons.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认选择图标
|
||||
*/
|
||||
const handleConfirmIconSelect = async () => {
|
||||
const handleConfirmIconSelect = async (iconIdentifier) => {
|
||||
if (!currentCategory.value) {
|
||||
return
|
||||
}
|
||||
@@ -505,75 +422,41 @@ const handleConfirmIconSelect = async () => {
|
||||
duration: 0
|
||||
})
|
||||
|
||||
const { success, message } = await updateSelectedIcon(
|
||||
const { success, message } = await updateCategoryIconApi(
|
||||
currentCategory.value.id,
|
||||
selectedIconIndex.value
|
||||
iconIdentifier
|
||||
)
|
||||
|
||||
if (success) {
|
||||
showSuccessToast('图标保存成功')
|
||||
showIconDialog.value = false
|
||||
currentCategory.value = null
|
||||
iconCandidates.value = []
|
||||
await loadCategories()
|
||||
} else {
|
||||
showToast(message || '保存失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存图标失败:', error)
|
||||
showToast('保存图标失败: ' + (error.message || '未知错误'))
|
||||
showToast('保存图标失败')
|
||||
} finally {
|
||||
closeToast()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除图标
|
||||
* 取消图标选择
|
||||
*/
|
||||
const handleDeleteIcon = () => {
|
||||
if (!currentCategory.value || !currentCategory.value.icon) {
|
||||
return
|
||||
}
|
||||
showDeleteIconConfirm.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认删除图标
|
||||
*/
|
||||
const handleConfirmDeleteIcon = async () => {
|
||||
if (!currentCategory.value) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isDeletingIcon.value = true
|
||||
showLoadingToast({
|
||||
message: '删除中...',
|
||||
forbidClick: true,
|
||||
duration: 0
|
||||
})
|
||||
|
||||
const { success, message } = await deleteCategoryIcon(currentCategory.value.id)
|
||||
|
||||
if (success) {
|
||||
showSuccessToast('图标删除成功')
|
||||
showDeleteIconConfirm.value = false
|
||||
showIconDialog.value = false
|
||||
await loadCategories()
|
||||
} else {
|
||||
showToast(message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除图标失败:', error)
|
||||
showToast('删除图标失败: ' + (error.message || '未知错误'))
|
||||
} finally {
|
||||
isDeletingIcon.value = false
|
||||
closeToast()
|
||||
}
|
||||
const handleCancelIconSelect = () => {
|
||||
showIconDialog.value = false
|
||||
currentCategory.value = null
|
||||
iconCandidates.value = []
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑分类
|
||||
*/
|
||||
const handleEditOld = (category) => {
|
||||
const handleEdit = (category) => {
|
||||
editForm.value = {
|
||||
id: category.id,
|
||||
name: category.name
|
||||
@@ -654,53 +537,9 @@ const handleConfirmDelete = async () => {
|
||||
closeToast()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置新增表单
|
||||
*/
|
||||
const resetAddForm = () => {
|
||||
addForm.value = {
|
||||
name: ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析图标数组(第一个图标为当前选中的)
|
||||
*/
|
||||
const parseIcon = (iconJson) => {
|
||||
if (!iconJson) {
|
||||
return ''
|
||||
}
|
||||
try {
|
||||
const icons = JSON.parse(iconJson)
|
||||
return Array.isArray(icons) && icons.length > 0 ? icons[0] : ''
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析图标数组为完整数组
|
||||
*/
|
||||
const parseIconArray = (iconJson) => {
|
||||
if (!iconJson) {
|
||||
return []
|
||||
}
|
||||
try {
|
||||
const icons = JSON.parse(iconJson)
|
||||
return Array.isArray(icons) ? icons : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化时显示类型选择
|
||||
currentLevel.value = 0
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="scss">
|
||||
.level-container {
|
||||
min-height: calc(100vh - 50px);
|
||||
margin-top: 16px;
|
||||
@@ -714,96 +553,17 @@ onMounted(() => {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.scroll-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.category-icon :deep(svg) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
fill: currentColor;
|
||||
.bottom-button {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.category-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.icon-selector {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.icon-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.icon-item {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border: 2px solid var(--van-border-color);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.icon-item:hover {
|
||||
border-color: var(--van-primary-color);
|
||||
background-color: var(--van-primary-color-light);
|
||||
}
|
||||
|
||||
.icon-item.active {
|
||||
border-color: var(--van-primary-color);
|
||||
background-color: var(--van-primary-color-light);
|
||||
box-shadow: 0 2px 8px rgba(25, 137, 250, 0.3);
|
||||
}
|
||||
|
||||
.icon-preview {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon-preview :deep(svg) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.empty-icons {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.icon-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
/* PopupContainer 的 footer 已有边框,所以这里不需要重复 */
|
||||
|
||||
/* 深色模式 */
|
||||
/* @media (prefers-color-scheme: dark) {
|
||||
.level-container {
|
||||
background: var(--van-background);
|
||||
}
|
||||
} */
|
||||
|
||||
/* 设置页面容器背景色 */
|
||||
:deep(.van-nav-bar) {
|
||||
background: transparent !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user