feat: 前端新增中英文多语言支持
- 引入 vue-i18n,新增 locales/messages.ts 以中英对照方式维护词条 - 新增 LocaleSwitch 组件,在登录/注册页与主布局顶部栏提供切换入口 - 语言持久化到 localStorage 并同步 <html lang>,Naive UI 组件语言联动 - 各视图、表格列、弹窗与提示文案全部接入国际化 - 修复 confirmImport 缺失的 tagIds 类型定义
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
// 分类管理组件:展示收入/支出分类列表,支持新增、编辑、删除分类
|
||||
import { ref, onMounted, computed, h } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useMessage, useDialog, NButton, NSpace, NDataTable, NModal, NForm, NFormItem, NInput, NInputNumber, NSelect } from 'naive-ui'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { getCategoryList, createCategory, updateCategory, deleteCategory } from '../api/category'
|
||||
|
||||
const message = useMessage() // 消息提示
|
||||
const dialog = useDialog() // 对话框
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
/** 分类数据结构 */
|
||||
interface CategoryItem {
|
||||
@@ -26,10 +28,10 @@ const editingId = ref<number | null>(null) // 当前编辑的分类 ID(null
|
||||
const form = ref({ name: '', type: 'EXPENSE', icon: '', sortOrder: 0 }) // 表单数据
|
||||
|
||||
// 类型选项
|
||||
const typeOptions = [
|
||||
{ label: '支出', value: 'EXPENSE' },
|
||||
{ label: '收入', value: 'INCOME' },
|
||||
]
|
||||
const typeOptions = computed(() => [
|
||||
{ label: t('common.types.expense'), value: 'EXPENSE' },
|
||||
{ label: t('common.types.income'), value: 'INCOME' },
|
||||
])
|
||||
|
||||
// 按类型过滤分类列表
|
||||
const incomeList = computed(() => categories.value.filter(c => c.type === 'INCOME')) // 收入分类
|
||||
@@ -38,18 +40,18 @@ const expenseList = computed(() => categories.value.filter(c => c.type === 'EXPE
|
||||
/** 创建表格列配置 */
|
||||
function createColumns(): DataTableColumns<CategoryItem> {
|
||||
return [
|
||||
{ title: '名称', key: 'name' },
|
||||
{ title: '图标', key: 'icon' },
|
||||
{ title: '排序', key: 'sortOrder', width: 80 },
|
||||
{ title: t('common.name'), key: 'name' },
|
||||
{ title: t('category.icon'), key: 'icon' },
|
||||
{ title: t('common.sort'), key: 'sortOrder', width: 80 },
|
||||
{
|
||||
title: '操作', key: 'actions', width: 180,
|
||||
title: t('common.operation'), key: 'actions', width: 180,
|
||||
render(row) {
|
||||
// 编辑和删除按钮
|
||||
return [
|
||||
h(NSpace, null, {
|
||||
default: () => [
|
||||
h(NButton, { size: 'small', onClick: () => handleEdit(row) }, { default: () => '编辑' }),
|
||||
h(NButton, { size: 'small', type: 'error', onClick: () => handleDelete(row.id) }, { default: () => '删除' }),
|
||||
h(NButton, { size: 'small', onClick: () => handleEdit(row) }, { default: () => t('common.edit') }),
|
||||
h(NButton, { size: 'small', type: 'error', onClick: () => handleDelete(row.id) }, { default: () => t('common.delete') }),
|
||||
],
|
||||
}),
|
||||
]
|
||||
@@ -58,7 +60,7 @@ function createColumns(): DataTableColumns<CategoryItem> {
|
||||
]
|
||||
}
|
||||
|
||||
const columns = createColumns() // 表格列配置
|
||||
const columns = computed(() => createColumns()) // 表格列配置(随语言切换响应式更新)
|
||||
|
||||
/** 加载分类列表数据 */
|
||||
async function fetchData() {
|
||||
@@ -67,7 +69,7 @@ async function fetchData() {
|
||||
const res: any = await getCategoryList()
|
||||
categories.value = res.data
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '加载失败')
|
||||
message.error(e.message || t('common.loadFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -90,40 +92,40 @@ function handleEdit(row: CategoryItem) {
|
||||
/** 提交表单:校验 -> 新增或更新 -> 刷新列表 */
|
||||
async function handleSubmit() {
|
||||
if (!form.value.name) {
|
||||
message.warning('请输入分类名称')
|
||||
message.warning(t('category.nameRequired'))
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (editingId.value) {
|
||||
// 更新已有分类
|
||||
await updateCategory(editingId.value, form.value)
|
||||
message.success('修改成功')
|
||||
message.success(t('common.updateSuccess'))
|
||||
} else {
|
||||
// 创建新分类
|
||||
await createCategory(form.value)
|
||||
message.success('创建成功')
|
||||
message.success(t('common.createSuccess'))
|
||||
}
|
||||
showModal.value = false
|
||||
await fetchData() // 刷新列表
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '操作失败')
|
||||
message.error(e.message || t('common.operationFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除分类:弹出确认对话框,确认后执行删除并刷新列表 */
|
||||
function handleDelete(id: number) {
|
||||
dialog.warning({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这个分类吗?',
|
||||
positiveText: '删除',
|
||||
negativeText: '取消',
|
||||
title: t('common.confirmDelete'),
|
||||
content: t('category.deleteConfirm'),
|
||||
positiveText: t('common.delete'),
|
||||
negativeText: t('common.cancel'),
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await deleteCategory(id)
|
||||
message.success('删除成功')
|
||||
message.success(t('common.deleteSuccess'))
|
||||
await fetchData()
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '删除失败')
|
||||
message.error(e.message || t('common.deleteFailed'))
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -137,34 +139,34 @@ onMounted(fetchData)
|
||||
<div>
|
||||
<!-- 顶部:标题和新增按钮 -->
|
||||
<NSpace justify="space-between" style="margin-bottom: 16px;">
|
||||
<h2 style="margin: 0;">分类管理</h2>
|
||||
<NButton type="primary" @click="handleAdd">新增分类</NButton>
|
||||
<h2 style="margin: 0;">{{ t('category.title') }}</h2>
|
||||
<NButton type="primary" @click="handleAdd">{{ t('category.add') }}</NButton>
|
||||
</NSpace>
|
||||
|
||||
<!-- 支出分类列表 -->
|
||||
<h3>支出分类</h3>
|
||||
<h3>{{ t('category.expenseSection') }}</h3>
|
||||
<NDataTable :columns="columns" :data="expenseList" :loading="loading" :bordered="false" />
|
||||
|
||||
<!-- 收入分类列表 -->
|
||||
<h3 style="margin-top: 24px;">收入分类</h3>
|
||||
<h3 style="margin-top: 24px;">{{ t('category.incomeSection') }}</h3>
|
||||
<NDataTable :columns="columns" :data="incomeList" :loading="loading" :bordered="false" />
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<NModal v-model:show="showModal" preset="card" :title="editingId ? '编辑分类' : '新增分类'" style="width: 450px;">
|
||||
<NModal v-model:show="showModal" preset="card" :title="editingId ? t('category.editTitle') : t('category.addTitle')" style="width: 450px;">
|
||||
<NForm>
|
||||
<NFormItem label="分类名称">
|
||||
<NInput v-model:value="form.name" placeholder="请输入分类名称" />
|
||||
<NFormItem :label="t('category.name')">
|
||||
<NInput v-model:value="form.name" :placeholder="t('category.namePlaceholder')" />
|
||||
</NFormItem>
|
||||
<NFormItem label="类型">
|
||||
<NFormItem :label="t('category.type')">
|
||||
<NSelect v-model:value="form.type" :options="typeOptions" />
|
||||
</NFormItem>
|
||||
<NFormItem label="图标">
|
||||
<NInput v-model:value="form.icon" placeholder="可选,如 emoji" />
|
||||
<NFormItem :label="t('category.icon')">
|
||||
<NInput v-model:value="form.icon" :placeholder="t('category.iconPlaceholder')" />
|
||||
</NFormItem>
|
||||
<NFormItem label="排序">
|
||||
<NFormItem :label="t('category.sort')">
|
||||
<NInputNumber v-model:value="form.sortOrder" :min="0" />
|
||||
</NFormItem>
|
||||
<NButton type="primary" block @click="handleSubmit">确定</NButton>
|
||||
<NButton type="primary" block @click="handleSubmit">{{ t('common.confirm') }}</NButton>
|
||||
</NForm>
|
||||
</NModal>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user