support transaction tag group

This commit is contained in:
MaysWind
2026-01-17 00:47:51 +08:00
parent b556efa510
commit 7d9cfc4ced
59 changed files with 3289 additions and 795 deletions
+31 -17
View File
@@ -259,15 +259,17 @@
<template :key="categoryType"
v-for="(categories, categoryType) in allPrimaryCategories">
<v-divider />
<v-list-item density="compact" v-show="categories && categories.length">
<v-list-item-title>
<span class="text-sm">{{ getTransactionTypeName(categoryTypeToTransactionType(parseInt(categoryType)), 'Type') }}</span>
</v-list-item-title>
</v-list-item>
<v-list-group :key="category.id" v-for="category in categories">
<v-list-group :key="category.id" v-for="(category, index) in categories">
<template #activator="{ props }" v-if="!category.hidden || queryAllFilterCategoryIds[category.id] || allCategories[query.categoryIds]?.parentId === category.id || hasSubCategoryInQuery(category)">
<v-divider />
<v-divider v-if="index > 0" />
<v-list-item class="text-sm" density="compact"
:class="getCategoryListItemCheckedClass(category, queryAllFilterCategoryIds)"
v-bind="props">
@@ -474,24 +476,33 @@
</v-list-item-title>
</v-list-item>
<v-divider v-if="query.tagFilter && query.tagFilter !== TransactionTagFilter.TransactionNoTagFilterValue" />
<template :key="transactionTagGroup.id"
v-for="transactionTagGroup in allTransactionTagGroupsWithDefault">
<v-divider v-if="allTransactionTagsByGroup[transactionTagGroup.id] && allTransactionTagsByGroup[transactionTagGroup.id]?.length && hasVisibleTagsInTagGroup(transactionTagGroup)" />
<template :key="transactionTag.id"
v-for="transactionTag in allTransactionTags">
<v-divider v-if="!transactionTag.hidden || isDefined(queryAllFilterTagIds[transactionTag.id])" />
<v-list-item class="text-sm" density="compact"
:value="transactionTag.id"
:class="{ 'list-item-selected': queryAllFilterTagIdsCount === 1 && isDefined(queryAllFilterTagIds[transactionTag.id]), 'item-in-multiple-selection': queryAllFilterTagIdsCount > 1 && isDefined(queryAllFilterTagIds[transactionTag.id]) }"
:append-icon="(queryAllFilterTagIds[transactionTag.id] === true ? mdiCheck : (queryAllFilterTagIds[transactionTag.id] === false ? mdiClose : undefined))"
v-if="!transactionTag.hidden || isDefined(queryAllFilterTagIds[transactionTag.id])">
<v-list-item-title class="cursor-pointer"
@click="changeTagFilter(TransactionTagFilter.of(transactionTag.id).toTextualTagFilter())">
<div class="d-flex align-center">
<v-icon size="24" :icon="mdiPound"/>
<span class="text-sm ms-3">{{ transactionTag.name }}</span>
</div>
<v-list-item density="compact" v-if="allTransactionTagsByGroup[transactionTagGroup.id] && allTransactionTagsByGroup[transactionTagGroup.id]?.length && hasVisibleTagsInTagGroup(transactionTagGroup)">
<v-list-item-title>
<span class="text-sm">{{ transactionTagGroup.name }}</span>
</v-list-item-title>
</v-list-item>
<template :key="transactionTag.id"
v-for="(transactionTag, index) in (allTransactionTagsByGroup[transactionTagGroup.id] ?? [])">
<v-divider v-if="index > 0 && (!transactionTag.hidden || isDefined(queryAllFilterTagIds[transactionTag.id]))" />
<v-list-item class="text-sm" density="compact"
:value="transactionTag.id"
:class="{ 'list-item-selected': queryAllFilterTagIdsCount === 1 && isDefined(queryAllFilterTagIds[transactionTag.id]), 'item-in-multiple-selection': queryAllFilterTagIdsCount > 1 && isDefined(queryAllFilterTagIds[transactionTag.id]) }"
:append-icon="(queryAllFilterTagIds[transactionTag.id] === true ? mdiCheck : (queryAllFilterTagIds[transactionTag.id] === false ? mdiClose : undefined))"
v-if="!transactionTag.hidden || isDefined(queryAllFilterTagIds[transactionTag.id])">
<v-list-item-title class="cursor-pointer"
@click="changeTagFilter(TransactionTagFilter.of(transactionTag.id).toTextualTagFilter())">
<div class="d-flex align-center">
<v-icon size="24" :icon="mdiPound"/>
<span class="text-sm ms-3">{{ transactionTag.name }}</span>
</div>
</v-list-item-title>
</v-list-item>
</template>
</template>
</v-list>
</v-menu>
@@ -785,6 +796,8 @@ const {
allCategories,
allPrimaryCategories,
allAvailableCategoriesCount,
allTransactionTagGroupsWithDefault,
allTransactionTagsByGroup,
allTransactionTags,
allAvailableTagsCount,
query,
@@ -806,6 +819,7 @@ const {
transactionCalendarMaxDate,
currentMonthTransactionData,
hasSubCategoryInQuery,
hasVisibleTagsInTagGroup,
isSameAsDefaultTimezoneOffsetMinutes,
canAddTransaction,
getDisplayTime,
@@ -78,6 +78,7 @@ import { type NameValue, values } from '@/core/base.ts';
import { CategoryType } from '@/core/category.ts';
import { AUTOMATICALLY_CREATED_CATEGORY_ICON_ID } from '@/consts/icon.ts';
import { DEFAULT_CATEGORY_COLOR } from '@/consts/color.ts';
import { DEFAULT_TAG_GROUP_ID } from '@/consts/tag.ts';
import { type TransactionCategoryCreateRequest, type TransactionCategoryCreateWithSubCategories, TransactionCategory } from '@/models/transaction_category.ts';
import { type TransactionTagCreateRequest, TransactionTag } from '@/models/transaction_tag.ts';
@@ -284,12 +285,13 @@ function confirm(): void {
const submitTags: TransactionTagCreateRequest[] = [];
for (const item of selectedNames.value) {
const tag: TransactionTag = TransactionTag.createNewTag(item);
const tag: TransactionTag = TransactionTag.createNewTag(item, DEFAULT_TAG_GROUP_ID);
submitTags.push(tag.toCreateRequest());
}
transactionTagsStore.addTags({
tags: submitTags,
groupId: DEFAULT_TAG_GROUP_ID,
skipExists: true
}).then(response => {
transactionTagsStore.loadAllTags({ force: false }).then(() => {
@@ -327,57 +327,14 @@
</v-select>
</v-col>
<v-col cols="12" md="12">
<v-autocomplete
item-title="name"
item-value="id"
auto-select-first
persistent-placeholder
multiple
chips
:closable-chips="mode !== TransactionEditPageMode.View"
<transaction-tag-auto-complete
:readonly="mode === TransactionEditPageMode.View"
:disabled="loading || submitting"
:label="tt('Tags')"
:placeholder="tt('None')"
:items="allTags"
:show-label="true"
:allow-add-new-tag="true"
v-model="transaction.tagIds"
v-model:search="tagSearchContent"
>
<template #chip="{ props, item }">
<v-chip :prepend-icon="mdiPound" :text="item.title" v-bind="props"/>
</template>
<template #item="{ props, item }">
<v-list-item :value="item.value" v-bind="props" v-if="!item.raw.hidden">
<template #title>
<v-list-item-title>
<div class="d-flex align-center">
<v-icon size="20" start :icon="mdiPound"/>
<span>{{ item.title }}</span>
</div>
</v-list-item-title>
</template>
</v-list-item>
<v-list-item :disabled="true" v-bind="props"
v-if="item.raw.hidden && item.raw.name.toLowerCase().indexOf(tagSearchContent.toLowerCase()) >= 0 && isAllFilteredTagHidden">
<template #title>
<v-list-item-title>
<div class="d-flex align-center">
<v-icon size="20" start :icon="mdiPound"/>
<span>{{ item.title }}</span>
</div>
</v-list-item-title>
</template>
</v-list-item>
</template>
<template #no-data>
<v-list class="py-0">
<v-list-item v-if="tagSearchContent" @click="saveNewTag(tagSearchContent)">{{ tt('format.misc.addNewTag', { tag: tagSearchContent }) }}</v-list-item>
<v-list-item v-else-if="!tagSearchContent">{{ tt('No available tag') }}</v-list-item>
</v-list>
</template>
</v-autocomplete>
@tag:saving="onSavingTag"
/>
</v-col>
<v-col cols="12" md="12">
<v-textarea
@@ -535,7 +492,6 @@ import { TemplateType, ScheduledTemplateFrequencyType } from '@/core/template.ts
import { KnownErrorCode } from '@/consts/api.ts';
import { SUPPORTED_IMAGE_EXTENSIONS } from '@/consts/file.ts';
import { TransactionTag } from '@/models/transaction_tag.ts';
import { TransactionTemplate } from '@/models/transaction_template.ts';
import type { TransactionPictureInfoBasicResponse } from '@/models/transaction_picture_info.ts';
import { Transaction } from '@/models/transaction.ts';
@@ -567,7 +523,6 @@ import {
mdiSwapHorizontal,
mdiMapMarkerOutline,
mdiCheck,
mdiPound,
mdiMenuDown,
mdiImagePlusOutline,
mdiTrashCanOutline,
@@ -621,7 +576,6 @@ const {
allVisibleCategorizedAccounts,
allCategories,
allCategoriesMap,
allTags,
allTagsMap,
firstVisibleAccountId,
hasVisibleExpenseCategories,
@@ -669,7 +623,6 @@ const activeTab = ref<string>('basicInfo');
const originalTransactionEditable = ref<boolean>(false);
const noTransactionDraft = ref<boolean>(false);
const geoMenuState = ref<boolean>(false);
const tagSearchContent = ref<string>('');
const removingPictureId = ref<string>('');
const initAmount = ref<number | undefined>(undefined);
@@ -692,22 +645,7 @@ const sourceAmountColor = computed<string | undefined>(() => {
return undefined;
});
const isAllFilteredTagHidden = computed<boolean>(() => {
const lowerCaseTagSearchContent = tagSearchContent.value.toLowerCase();
let hiddenCount = 0;
for (const tag of allTags.value) {
if (!lowerCaseTagSearchContent || tag.name.toLowerCase().indexOf(lowerCaseTagSearchContent) >= 0) {
if (!tag.hidden) {
return false;
}
hiddenCount++;
}
}
return hiddenCount > 0;
});
const isTransactionModified = computed<boolean>(() => {
if (mode.value === TransactionEditPageMode.Add) {
@@ -1141,26 +1079,6 @@ function clearGeoLocation(): void {
transaction.value.removeGeoLocation();
}
function saveNewTag(tagName: string): void {
submitting.value = true;
transactionTagsStore.saveTag({
tag: TransactionTag.createNewTag(tagName)
}).then(tag => {
submitting.value = false;
if (tag && tag.id) {
transaction.value.tagIds.push(tag.id);
}
}).catch(error => {
submitting.value = false;
if (!error.processed) {
snackbar.value?.showError(error);
}
});
}
function showOpenPictureDialog(): void {
if (!canAddTransactionPicture.value || submitting.value) {
return;
@@ -1231,6 +1149,10 @@ function viewOrRemovePicture(pictureInfo: TransactionPictureInfoBasicResponse):
});
}
function onSavingTag(state: boolean): void {
submitting.value = state;
}
function onShowDateTimeError(error: string): void {
snackbar.value?.showError(error);
}