add transaction gallery mode in transaction list page

This commit is contained in:
MaysWind
2026-05-11 00:25:55 +08:00
parent 11f2c9fff7
commit 563bef69cf
33 changed files with 453 additions and 87 deletions
+1 -1
View File
@@ -419,7 +419,7 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType
minTransactionTime = utils.GetMinTransactionTimeFromUnixTime(exportTransactionDataReq.MinTime)
}
allTransactions, err := a.transactions.GetAllSpecifiedTransactions(c, uid, maxTransactionTime, minTransactionTime, exportTransactionDataReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, exportTransactionDataReq.AmountFilter, exportTransactionDataReq.Keyword, pageCountForDataExport, true)
allTransactions, err := a.transactions.GetAllSpecifiedTransactions(c, uid, maxTransactionTime, minTransactionTime, exportTransactionDataReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, exportTransactionDataReq.AmountFilter, exportTransactionDataReq.Keyword, false, pageCountForDataExport, true)
if err != nil {
log.Errorf(c, "[data_managements.getExportedFileContent] failed to all transactions user \"uid:%d\", because %s", uid, err.Error())
+5 -5
View File
@@ -97,7 +97,7 @@ func (a *TransactionsApi) TransactionCountHandler(c *core.WebContext) (any, *err
}
}
totalCount, err := a.transactions.GetTransactionCount(c, uid, transactionCountReq.MaxTime, transactionCountReq.MinTime, transactionCountReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionCountReq.AmountFilter, transactionCountReq.Keyword)
totalCount, err := a.transactions.GetTransactionCount(c, uid, transactionCountReq.MaxTime, transactionCountReq.MinTime, transactionCountReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionCountReq.AmountFilter, transactionCountReq.Keyword, transactionCountReq.MustHavePictures)
if err != nil {
log.Errorf(c, "[transactions.TransactionCountHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error())
@@ -168,7 +168,7 @@ func (a *TransactionsApi) TransactionListHandler(c *core.WebContext) (any, *errs
var totalCount int64
if transactionListReq.WithCount {
totalCount, err = a.transactions.GetTransactionCount(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionListReq.AmountFilter, transactionListReq.Keyword)
totalCount, err = a.transactions.GetTransactionCount(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionListReq.AmountFilter, transactionListReq.Keyword, transactionListReq.MustHavePictures)
if err != nil {
log.Errorf(c, "[transactions.TransactionListHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error())
@@ -176,7 +176,7 @@ func (a *TransactionsApi) TransactionListHandler(c *core.WebContext) (any, *errs
}
}
transactions, err := a.transactions.GetTransactionsByMaxTime(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionListReq.AmountFilter, transactionListReq.Keyword, transactionListReq.Page, transactionListReq.Count, true, true)
transactions, err := a.transactions.GetTransactionsByMaxTime(c, uid, transactionListReq.MaxTime, transactionListReq.MinTime, transactionListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionListReq.AmountFilter, transactionListReq.Keyword, transactionListReq.MustHavePictures, transactionListReq.Page, transactionListReq.Count, true, true)
if err != nil {
log.Errorf(c, "[transactions.TransactionListHandler] failed to get transactions earlier than \"%d\" for user \"uid:%d\", because %s", transactionListReq.MaxTime, uid, err.Error())
@@ -276,7 +276,7 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.WebContext) (any,
}
}
transactions, err := a.transactions.GetTransactionsInMonthByPage(c, uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionListReq.AmountFilter, transactionListReq.Keyword)
transactions, err := a.transactions.GetTransactionsInMonthByPage(c, uid, transactionListReq.Year, transactionListReq.Month, transactionListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionListReq.AmountFilter, transactionListReq.Keyword, transactionListReq.MustHavePictures)
if err != nil {
log.Errorf(c, "[transactions.TransactionMonthListHandler] failed to get transactions in month \"%d-%d\" for user \"uid:%d\", because %s", transactionListReq.Year, transactionListReq.Month, uid, err.Error())
@@ -371,7 +371,7 @@ func (a *TransactionsApi) TransactionListAllHandler(c *core.WebContext) (any, *e
minTransactionTime = utils.GetMinTransactionTimeFromUnixTime(transactionAllListReq.StartTime)
}
allTransactions, err := a.transactions.GetAllSpecifiedTransactions(c, uid, maxTransactionTime, minTransactionTime, transactionAllListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionAllListReq.AmountFilter, transactionAllListReq.Keyword, pageCountForDataExport, true)
allTransactions, err := a.transactions.GetAllSpecifiedTransactions(c, uid, maxTransactionTime, minTransactionTime, transactionAllListReq.Type, allCategoryIds, allAccountIds, tagFilters, noTags, transactionAllListReq.AmountFilter, transactionAllListReq.Keyword, transactionAllListReq.MustHavePictures, pageCountForDataExport, true)
if err != nil {
log.Errorf(c, "[transactions.TransactionListAllHandler] failed to get all transactions for user \"uid:%d\", because %s", uid, err.Error())
+2 -2
View File
@@ -153,14 +153,14 @@ func (h *mcpQueryTransactionsToolHandler) Handle(c *core.WebContext, callToolReq
}
}
totalCount, err := services.GetTransactionService().GetTransactionCount(c, uid, maxTransactionTime, minTransactionTime, transactionType, filterCategoryIds, filterAccountIds, nil, false, "", queryTransactionsRequest.Keyword)
totalCount, err := services.GetTransactionService().GetTransactionCount(c, uid, maxTransactionTime, minTransactionTime, transactionType, filterCategoryIds, filterAccountIds, nil, false, "", queryTransactionsRequest.Keyword, false)
if err != nil {
log.Errorf(c, "[transactions.TransactionListHandler] failed to get transaction count for user \"uid:%d\", because %s", uid, err.Error())
return nil, nil, err
}
transactions, err := services.GetTransactionService().GetTransactionsByMaxTime(c, uid, maxTransactionTime, minTransactionTime, transactionType, filterCategoryIds, filterAccountIds, nil, false, "", queryTransactionsRequest.Keyword, queryTransactionsRequest.Page, queryTransactionsRequest.Count, false, true)
transactions, err := services.GetTransactionService().GetTransactionsByMaxTime(c, uid, maxTransactionTime, minTransactionTime, transactionType, filterCategoryIds, filterAccountIds, nil, false, "", queryTransactionsRequest.Keyword, false, queryTransactionsRequest.Page, queryTransactionsRequest.Count, false, true)
structuredResponse, response, err := h.createNewMCPQueryTransactionsResponse(c, &queryTransactionsRequest, transactions, totalCount, services.GetAccountService().GetAccountMapByList(allAccounts), services.GetTransactionCategoryService().GetCategoryMapByList(allCategories))
if err != nil {
+51 -47
View File
@@ -210,65 +210,69 @@ type TransactionTagFilter struct {
// TransactionCountRequest represents transaction count request
type TransactionCountRequest struct {
Type TransactionType `form:"type" binding:"min=0,max=4"`
CategoryIds string `form:"category_ids"`
AccountIds string `form:"account_ids"`
TagFilter string `form:"tag_filter" binding:"validTagFilter"`
AmountFilter string `form:"amount_filter" binding:"validAmountFilter"`
Keyword string `form:"keyword"`
MaxTime int64 `form:"max_time" binding:"min=0"` // Transaction time sequence id
MinTime int64 `form:"min_time" binding:"min=0"` // Transaction time sequence id
Type TransactionType `form:"type" binding:"min=0,max=4"`
CategoryIds string `form:"category_ids"`
AccountIds string `form:"account_ids"`
TagFilter string `form:"tag_filter" binding:"validTagFilter"`
AmountFilter string `form:"amount_filter" binding:"validAmountFilter"`
Keyword string `form:"keyword"`
MustHavePictures bool `form:"must_have_pictures"`
MaxTime int64 `form:"max_time" binding:"min=0"` // Transaction time sequence id
MinTime int64 `form:"min_time" binding:"min=0"` // Transaction time sequence id
}
// TransactionListByMaxTimeRequest represents all parameters of transaction listing by max time request
type TransactionListByMaxTimeRequest struct {
Type TransactionType `form:"type" binding:"min=0,max=4"`
CategoryIds string `form:"category_ids"`
AccountIds string `form:"account_ids"`
TagFilter string `form:"tag_filter" binding:"validTagFilter"`
AmountFilter string `form:"amount_filter" binding:"validAmountFilter"`
Keyword string `form:"keyword"`
MaxTime int64 `form:"max_time" binding:"min=0"` // Transaction time sequence id
MinTime int64 `form:"min_time" binding:"min=0"` // Transaction time sequence id
Page int32 `form:"page" binding:"min=0"`
Count int32 `form:"count" binding:"required,min=1,max=50"`
WithCount bool `form:"with_count"`
WithPictures bool `form:"with_pictures"`
TrimAccount bool `form:"trim_account"`
TrimCategory bool `form:"trim_category"`
TrimTag bool `form:"trim_tag"`
Type TransactionType `form:"type" binding:"min=0,max=4"`
CategoryIds string `form:"category_ids"`
AccountIds string `form:"account_ids"`
TagFilter string `form:"tag_filter" binding:"validTagFilter"`
AmountFilter string `form:"amount_filter" binding:"validAmountFilter"`
Keyword string `form:"keyword"`
MustHavePictures bool `form:"must_have_pictures"`
MaxTime int64 `form:"max_time" binding:"min=0"` // Transaction time sequence id
MinTime int64 `form:"min_time" binding:"min=0"` // Transaction time sequence id
Page int32 `form:"page" binding:"min=0"`
Count int32 `form:"count" binding:"required,min=1,max=50"`
WithCount bool `form:"with_count"`
WithPictures bool `form:"with_pictures"`
TrimAccount bool `form:"trim_account"`
TrimCategory bool `form:"trim_category"`
TrimTag bool `form:"trim_tag"`
}
// TransactionListInMonthByPageRequest represents all parameters of transaction listing by month request
type TransactionListInMonthByPageRequest struct {
Year int32 `form:"year" binding:"required,min=1"`
Month int32 `form:"month" binding:"required,min=1"`
Type TransactionType `form:"type" binding:"min=0,max=4"`
CategoryIds string `form:"category_ids"`
AccountIds string `form:"account_ids"`
TagFilter string `form:"tag_filter" binding:"validTagFilter"`
AmountFilter string `form:"amount_filter" binding:"validAmountFilter"`
Keyword string `form:"keyword"`
WithPictures bool `form:"with_pictures"`
TrimAccount bool `form:"trim_account"`
TrimCategory bool `form:"trim_category"`
TrimTag bool `form:"trim_tag"`
Year int32 `form:"year" binding:"required,min=1"`
Month int32 `form:"month" binding:"required,min=1"`
Type TransactionType `form:"type" binding:"min=0,max=4"`
CategoryIds string `form:"category_ids"`
AccountIds string `form:"account_ids"`
TagFilter string `form:"tag_filter" binding:"validTagFilter"`
AmountFilter string `form:"amount_filter" binding:"validAmountFilter"`
Keyword string `form:"keyword"`
MustHavePictures bool `form:"must_have_pictures"`
WithPictures bool `form:"with_pictures"`
TrimAccount bool `form:"trim_account"`
TrimCategory bool `form:"trim_category"`
TrimTag bool `form:"trim_tag"`
}
// TransactionAllListRequest represents all parameters of all transaction listing request
type TransactionAllListRequest struct {
Type TransactionType `form:"type" binding:"min=0,max=4"`
CategoryIds string `form:"category_ids"`
AccountIds string `form:"account_ids"`
TagFilter string `form:"tag_filter" binding:"validTagFilter"`
AmountFilter string `form:"amount_filter" binding:"validAmountFilter"`
Keyword string `form:"keyword"`
StartTime int64 `form:"start_time" binding:"min=0"`
EndTime int64 `form:"end_time" binding:"min=0"`
WithPictures bool `form:"with_pictures"`
TrimAccount bool `form:"trim_account"`
TrimCategory bool `form:"trim_category"`
TrimTag bool `form:"trim_tag"`
Type TransactionType `form:"type" binding:"min=0,max=4"`
CategoryIds string `form:"category_ids"`
AccountIds string `form:"account_ids"`
TagFilter string `form:"tag_filter" binding:"validTagFilter"`
AmountFilter string `form:"amount_filter" binding:"validAmountFilter"`
Keyword string `form:"keyword"`
MustHavePictures bool `form:"must_have_pictures"`
StartTime int64 `form:"start_time" binding:"min=0"`
EndTime int64 `form:"end_time" binding:"min=0"`
WithPictures bool `form:"with_pictures"`
TrimAccount bool `form:"trim_account"`
TrimCategory bool `form:"trim_category"`
TrimTag bool `form:"trim_tag"`
}
// TransactionReconciliationStatementRequest represents all parameters of transaction reconciliation statement request
+24 -10
View File
@@ -76,11 +76,11 @@ func (s *TransactionService) GetAllTransactions(c core.Context, uid int64, pageC
// GetAllTransactionsByMaxTime returns all transactions before given time
func (s *TransactionService) GetAllTransactionsByMaxTime(c core.Context, uid int64, maxTransactionTime int64, count int32, noDuplicated bool) ([]*models.Transaction, error) {
return s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, nil, nil, false, "", "", 1, count, false, noDuplicated)
return s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, nil, nil, false, "", "", false, 1, count, false, noDuplicated)
}
// GetAllSpecifiedTransactions returns all transactions that match given conditions
func (s *TransactionService) GetAllSpecifiedTransactions(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionType, categoryIds []int64, accountIds []int64, tagFilters []*models.TransactionTagFilter, noTags bool, amountFilter string, keyword string, pageCount int32, noDuplicated bool) ([]*models.Transaction, error) {
func (s *TransactionService) GetAllSpecifiedTransactions(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionType, categoryIds []int64, accountIds []int64, tagFilters []*models.TransactionTagFilter, noTags bool, amountFilter string, keyword string, mustHavePictures bool, pageCount int32, noDuplicated bool) ([]*models.Transaction, error) {
if maxTransactionTime <= 0 {
maxTransactionTime = utils.GetMaxTransactionTimeFromUnixTime(time.Now().Unix())
}
@@ -88,7 +88,7 @@ func (s *TransactionService) GetAllSpecifiedTransactions(c core.Context, uid int
var allTransactions []*models.Transaction
for maxTransactionTime > 0 {
transactions, err := s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, tagFilters, noTags, amountFilter, keyword, 1, pageCount, false, noDuplicated)
transactions, err := s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, tagFilters, noTags, amountFilter, keyword, mustHavePictures, 1, pageCount, false, noDuplicated)
if err != nil {
return nil, err
@@ -116,7 +116,7 @@ func (s *TransactionService) GetAllTransactionsInOneAccountWithAccountBalanceByM
var allTransactions []*models.Transaction
for maxTransactionTime > 0 {
transactions, err := s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, []int64{accountId}, nil, false, "", "", 1, pageCount, false, true)
transactions, err := s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, []int64{accountId}, nil, false, "", "", false, 1, pageCount, false, true)
if err != nil {
return nil, 0, 0, 0, 0, err
@@ -206,7 +206,7 @@ func (s *TransactionService) GetAllAccountsDailyOpeningAndClosingBalance(c core.
var allTransactions []*models.Transaction
for maxTransactionTime > 0 {
transactions, err := s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, nil, nil, false, "", "", 1, pageCountForLoadTransactionAmounts, false, false)
transactions, err := s.GetTransactionsByMaxTime(c, uid, maxTransactionTime, 0, 0, nil, nil, nil, false, "", "", false, 1, pageCountForLoadTransactionAmounts, false, false)
if err != nil {
return nil, err
@@ -323,7 +323,7 @@ func (s *TransactionService) GetAllAccountsDailyOpeningAndClosingBalance(c core.
}
// GetTransactionsByMaxTime returns transactions before given time
func (s *TransactionService) GetTransactionsByMaxTime(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionType, categoryIds []int64, accountIds []int64, tagFilters []*models.TransactionTagFilter, noTags bool, amountFilter string, keyword string, page int32, count int32, needOneMoreItem bool, noDuplicated bool) ([]*models.Transaction, error) {
func (s *TransactionService) GetTransactionsByMaxTime(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionType, categoryIds []int64, accountIds []int64, tagFilters []*models.TransactionTagFilter, noTags bool, amountFilter string, keyword string, mustHavePictures bool, page int32, count int32, needOneMoreItem bool, noDuplicated bool) ([]*models.Transaction, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
@@ -360,6 +360,7 @@ func (s *TransactionService) GetTransactionsByMaxTime(c core.Context, uid int64,
condition, conditionParams := s.buildTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionDbType, categoryIds, accountIds, tagFilters, amountFilter, keyword, noDuplicated)
sess := s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...)
sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagFilters, noTags)
sess = s.appendFilterPicturesConditionToQuery(sess, uid, mustHavePictures)
err = sess.Limit(int(actualCount), int(count*(page-1))).OrderBy("transaction_time desc").Find(&transactions)
@@ -367,7 +368,7 @@ func (s *TransactionService) GetTransactionsByMaxTime(c core.Context, uid int64,
}
// GetTransactionsInMonthByPage returns all transactions in given year and month
func (s *TransactionService) GetTransactionsInMonthByPage(c core.Context, uid int64, year int32, month int32, transactionType models.TransactionType, categoryIds []int64, accountIds []int64, tagFilters []*models.TransactionTagFilter, noTags bool, amountFilter string, keyword string) ([]*models.Transaction, error) {
func (s *TransactionService) GetTransactionsInMonthByPage(c core.Context, uid int64, year int32, month int32, transactionType models.TransactionType, categoryIds []int64, accountIds []int64, tagFilters []*models.TransactionTagFilter, noTags bool, amountFilter string, keyword string, mustHavePictures bool) ([]*models.Transaction, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
@@ -394,6 +395,7 @@ func (s *TransactionService) GetTransactionsInMonthByPage(c core.Context, uid in
condition, conditionParams := s.buildTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionDbType, categoryIds, accountIds, tagFilters, amountFilter, keyword, true)
sess := s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...)
sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagFilters, noTags)
sess = s.appendFilterPicturesConditionToQuery(sess, uid, mustHavePictures)
err = sess.OrderBy("transaction_time desc").Find(&transactions)
@@ -452,11 +454,11 @@ func (s *TransactionService) GetTransactionsByTransactionIds(c core.Context, uid
// GetAllTransactionCount returns total count of transactions
func (s *TransactionService) GetAllTransactionCount(c core.Context, uid int64) (int64, error) {
return s.GetTransactionCount(c, uid, 0, 0, 0, nil, nil, nil, false, "", "")
return s.GetTransactionCount(c, uid, 0, 0, 0, nil, nil, nil, false, "", "", false)
}
// GetTransactionCount returns count of transactions
func (s *TransactionService) GetTransactionCount(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionType, categoryIds []int64, accountIds []int64, tagFilters []*models.TransactionTagFilter, noTags bool, amountFilter string, keyword string) (int64, error) {
func (s *TransactionService) GetTransactionCount(c core.Context, uid int64, maxTransactionTime int64, minTransactionTime int64, transactionType models.TransactionType, categoryIds []int64, accountIds []int64, tagFilters []*models.TransactionTagFilter, noTags bool, amountFilter string, keyword string, mustHavePictures bool) (int64, error) {
if uid <= 0 {
return 0, errs.ErrUserIdInvalid
}
@@ -475,6 +477,7 @@ func (s *TransactionService) GetTransactionCount(c core.Context, uid int64, maxT
condition, conditionParams := s.buildTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionDbType, categoryIds, accountIds, tagFilters, amountFilter, keyword, true)
sess := s.UserDataDB(uid).NewSession(c).Where(condition, conditionParams...)
sess = s.appendFilterTagIdsConditionToQuery(sess, uid, maxTransactionTime, minTransactionTime, tagFilters, noTags)
sess = s.appendFilterPicturesConditionToQuery(sess, uid, mustHavePictures)
return sess.Count(&models.Transaction{})
}
@@ -1964,7 +1967,7 @@ func (s *TransactionService) DeleteAllTransactionsOfAccount(c core.Context, uid
return errs.ErrAccountIdInvalid
}
transactions, err := s.GetAllSpecifiedTransactions(c, uid, 0, 0, 0, nil, []int64{accountId}, nil, false, "", "", pageCount, true)
transactions, err := s.GetAllSpecifiedTransactions(c, uid, 0, 0, 0, nil, []int64{accountId}, nil, false, "", "", false, pageCount, true)
if err != nil {
return err
@@ -2897,6 +2900,17 @@ func (s *TransactionService) appendFilterTagIdsConditionToQuery(sess *xorm.Sessi
return sess
}
func (s *TransactionService) appendFilterPicturesConditionToQuery(sess *xorm.Session, uid int64, mustHavePictures bool) *xorm.Session {
if !mustHavePictures {
return sess
}
subQuery := builder.Select("transaction_id").From("transaction_picture_info").Where(builder.And(builder.Eq{"uid": uid}, builder.Eq{"deleted": false}, builder.Neq{"transaction_id": models.TransactionPictureNewPictureTransactionId}))
sess.And(builder.Or(builder.In("transaction_id", subQuery), builder.In("related_id", subQuery)))
return sess
}
func (s *TransactionService) isAccountIdValid(transaction *models.Transaction) error {
if transaction.Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
if transaction.RelatedAccountId != 0 && transaction.RelatedAccountId != transaction.AccountId {
+97
View File
@@ -0,0 +1,97 @@
<template>
<div :class="imageBoxClass" :style="style">
<img class="image-with-placeholder" :class="{ 'image-loading': loading }"
:src="src" :alt="alt" v-if="!link && !loadError"
@load="onLoad" @error="onError"/>
<f7-link class="image-link" :class="{ 'image-loading': loading }"
:href="link" v-if="link && !loadError">
<img class="image-with-placeholder" :src="src" :alt="alt"
@load="onLoad" @error="onError"/>
</f7-link>
<div class="image-loading-hint" v-if="loading && !loadError">
<f7-preloader size="28" />
</div>
<div class="image-error-hint" v-if="!link && !loading && loadError">
<slot name="error"></slot>
</div>
<f7-link class="image-error-hint" :href="link" v-if="link && !loading && loadError">
<slot name="error"></slot>
</f7-link>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
const props = defineProps<{
src: string;
class?: string;
style?: string
alt?: string;
link?: string;
}>();
const loading = ref<boolean>(true);
const loadError = ref<boolean>(false);
const imageBoxClass = computed<string>(() => {
let classes = 'image-box';
if (props.class) {
classes += ` ${props.class}`;
}
return classes;
});
function onLoad(): void {
loading.value = false;
}
function onError(): void {
loading.value = false;
loadError.value = true;
}
watch(() => props.src, () => {
loading.value = true;
loadError.value = false;
});
</script>
<style scoped>
.image-box > .image-with-placeholder,
.image-box > .image-link,
.image-box > .image-link > .image-with-placeholder {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
}
.image-box > .image-with-placeholder.image-loading,
.image-box > .image-link.image-loading {
display: none !important;
}
.image-box > .image-loading-hint {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.image-box > .image-error-hint {
display: flex;
position: absolute;
inset: 0;
align-items: center;
justify-content: center;
text-align: center;
font-size: var(--f7-list-item-footer-font-size);
color: var(--f7-list-item-footer-text-color);
padding: 4px;
overflow: hidden;
}
</style>
+2 -2
View File
@@ -526,13 +526,13 @@ export default {
const tagFilter = encodeURIComponent(req.tagFilter);
const amountFilter = encodeURIComponent(req.amountFilter);
const keyword = encodeURIComponent(req.keyword);
return axios.get<ApiResponse<TransactionInfoPageWrapperResponse>>(`v1/transactions/list.json?max_time=${req.maxTime}&min_time=${req.minTime}&type=${req.type}&category_ids=${req.categoryIds}&account_ids=${req.accountIds}&tag_filter=${tagFilter}&amount_filter=${amountFilter}&keyword=${keyword}&count=${req.count}&page=${req.page}&with_count=${req.withCount}&trim_account=true&trim_category=true&trim_tag=true`);
return axios.get<ApiResponse<TransactionInfoPageWrapperResponse>>(`v1/transactions/list.json?max_time=${req.maxTime}&min_time=${req.minTime}&type=${req.type}&category_ids=${req.categoryIds}&account_ids=${req.accountIds}&tag_filter=${tagFilter}&amount_filter=${amountFilter}&keyword=${keyword}&must_have_pictures=${!!req.mustHavePictures}&count=${req.count}&page=${req.page}&with_count=${req.withCount}&with_pictures=${!!req.withPictures}&trim_account=true&trim_category=true&trim_tag=true`);
},
getAllTransactionsByMonth: (req: TransactionListInMonthByPageRequest): ApiResponsePromise<TransactionInfoPageWrapperResponse2> => {
const tagFilter = encodeURIComponent(req.tagFilter);
const amountFilter = encodeURIComponent(req.amountFilter);
const keyword = encodeURIComponent(req.keyword);
return axios.get<ApiResponse<TransactionInfoPageWrapperResponse2>>(`v1/transactions/list/by_month.json?year=${req.year}&month=${req.month}&type=${req.type}&category_ids=${req.categoryIds}&account_ids=${req.accountIds}&tag_filter=${tagFilter}&amount_filter=${amountFilter}&keyword=${keyword}&trim_account=true&trim_category=true&trim_tag=true`);
return axios.get<ApiResponse<TransactionInfoPageWrapperResponse2>>(`v1/transactions/list/by_month.json?year=${req.year}&month=${req.month}&type=${req.type}&category_ids=${req.categoryIds}&account_ids=${req.accountIds}&tag_filter=${tagFilter}&amount_filter=${amountFilter}&keyword=${keyword}&must_have_pictures=${!!req.mustHavePictures}&with_pictures=${!!req.withPictures}&trim_account=true&trim_category=true&trim_tag=true`);
},
getAllTransactions: (req: TransactionAllListRequest): ApiResponsePromise<TransactionInfoResponse[]> => {
return axios.get<ApiResponse<TransactionInfoResponse[]>>(`v1/transactions/list/all.json?trim_account=true&with_pictures=${!!req.withPictures}&trim_category=true&trim_tag=true&start_time=${req.startTime}&end_time=${req.endTime}`);
+11 -1
View File
@@ -3,7 +3,7 @@ import { TransactionType } from '@/core/transaction.ts';
import { Account } from '@/models/account.ts';
import { TransactionCategory } from '@/models/transaction_category.ts';
import { TransactionTag } from '@/models/transaction_tag.ts';
import { TransactionPicture } from '@/models/transaction_picture_info.ts';
import { TransactionPicture, type TransactionPictureInfoBasicResponse } from '@/models/transaction_picture_info.ts';
import { Transaction } from '@/models/transaction.ts';
import {
@@ -32,6 +32,16 @@ export interface SetTransactionOptions {
comment?: string;
}
export function* allTransactionPictures(transactions: Transaction[]): Iterable<[Transaction, TransactionPictureInfoBasicResponse]> {
for (const transaction of transactions) {
if (transaction.pictures) {
for (const pictureInfo of transaction.pictures) {
yield [transaction, pictureInfo];
}
}
}
}
export function setTransactionModelByTransaction(transaction: Transaction, transaction2: Transaction | null | undefined, allCategories: Record<number, TransactionCategory[]>, allCategoriesMap: Record<string, TransactionCategory>, allVisibleAccounts: Account[], allAccountsMap: Record<string, Account>, allTagsMap: Record<string, TransactionTag>, defaultAccountId: string, options: SetTransactionOptions, setContextData: boolean): void {
if (isDefined(options.time)) {
transaction.time = options.time;
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "Details anzeigen",
"Transaction List": "Transaktionsliste",
"Transaction Calendar": "Transaktionskalender",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "Transaktionsdetails",
"Statistics & Analysis": "Statistiken & Analysen",
"Insights Explorer": "Daten-Explorer",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "View Details",
"Transaction List": "Transaction List",
"Transaction Calendar": "Transaction Calendar",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "Transaction Details",
"Statistics & Analysis": "Statistics & Analysis",
"Insights Explorer": "Insights Explorer",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "Ver Detalles",
"Transaction List": "Listado de Transacciones",
"Transaction Calendar": "Calendario de Transacciones",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "Detalles",
"Statistics & Analysis": "Estadísticas y Análisis",
"Insights Explorer": "Explorador de Datos",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "Voir les détails",
"Transaction List": "Liste des transactions",
"Transaction Calendar": "Calendrier des transactions",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "Détails de la transaction",
"Statistics & Analysis": "Statistiques et analyse",
"Insights Explorer": "Insights Explorer",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "Visualizza dettagli",
"Transaction List": "Elenco transazioni",
"Transaction Calendar": "Transaction Calendar",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "Dettagli transazione",
"Statistics & Analysis": "Statistiche e analisi",
"Insights Explorer": "Insights Explorer",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "詳細を表示",
"Transaction List": "取引リスト",
"Transaction Calendar": "Transaction Calendar",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "取引の詳細",
"Statistics & Analysis": "統計と分析",
"Insights Explorer": "Insights Explorer",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "ವಿವರಗಳನ್ನು ನೋಡಿ",
"Transaction List": "ವಹಿವಾಟು ಪಟ್ಟಿ",
"Transaction Calendar": "ವಹಿವಾಟು ಕ್ಯಾಲೆಂಡರ್",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "ವಹಿವಾಟಿನ ವಿವರಗಳು",
"Statistics & Analysis": "ಸಂಖ್ಯಾಶಾಸ್ತ್ರ ಮತ್ತು ವಿಶ್ಲೇಷಣೆ",
"Insights Explorer": "Insights Explorer",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "세부사항 보기",
"Transaction List": "거래 목록",
"Transaction Calendar": "거래 달력",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "거래 세부사항",
"Statistics & Analysis": "통계 및 분석",
"Insights Explorer": "인사이트 탐색기",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "Details bekijken",
"Transaction List": "Transactielijst",
"Transaction Calendar": "Transactiekalender",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "Transactiedetails",
"Statistics & Analysis": "Statistieken & Analyse",
"Insights Explorer": "Insights Explorer",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "Ver Detalhes",
"Transaction List": "Lista de Transações",
"Transaction Calendar": "Calendário de Transações",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "Detalhes da Transação",
"Statistics & Analysis": "Estatísticas e Análise",
"Insights Explorer": "Explorador de Insights",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "Просмотреть детали",
"Transaction List": "Список транзакций",
"Transaction Calendar": "Календарь транзакций",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "Детали транзакции",
"Statistics & Analysis": "Статистика и анализ",
"Insights Explorer": "Исследователь идей",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "Ogled podrobnosti",
"Transaction List": "Seznam transakcij",
"Transaction Calendar": "Koledar transakcij",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "Podrobnosti transakcije",
"Statistics & Analysis": "Statistika in analiza",
"Insights Explorer": "Vpogledi in raziskovanje",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "விவரங்களை பார்க்கவும்",
"Transaction List": "பரிவர்த்தனை பட்டியல்",
"Transaction Calendar": "பரிவர்த்தனை நாட்காட்டி",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "பரிவர்த்தனையின் விவரங்கள்",
"Statistics & Analysis": "புள்ளியியல் மற்றும் பகுப்பாய்வு",
"Insights Explorer": "நுண்ணறிவு ஆய்வுக்கருவி",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "ดูรายละเอียด",
"Transaction List": "รายการธุรกรรม",
"Transaction Calendar": "ปฏิทินธุรกรรม",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "รายละเอียดธุรกรรม",
"Statistics & Analysis": "สถิติ & การวิเคราะห์",
"Insights Explorer": "Insights Explorer",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "Detayları Gör",
"Transaction List": "İşlem Listesi",
"Transaction Calendar": "İşlem Takvimi",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "İşlem Detayları",
"Statistics & Analysis": "İstatistik & Analiz",
"Insights Explorer": "Insights Explorer",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "Переглянути деталі",
"Transaction List": "Список транзакцій",
"Transaction Calendar": "Transaction Calendar",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "Деталі по транзакціях",
"Statistics & Analysis": "Статистика та аналіз",
"Insights Explorer": "Insights Explorer",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "Xem chi tiết",
"Transaction List": "Danh sách giao dịch",
"Transaction Calendar": "Transaction Calendar",
"Transaction Gallery": "Transaction Gallery",
"Transaction Details": "Chi tiết giao dịch",
"Statistics & Analysis": "Thống kê & Phân tích",
"Insights Explorer": "Insights Explorer",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "查看详情",
"Transaction List": "交易列表",
"Transaction Calendar": "交易日历",
"Transaction Gallery": "交易相册",
"Transaction Details": "交易详情",
"Statistics & Analysis": "统计分析",
"Insights Explorer": "洞察探索",
+1
View File
@@ -1755,6 +1755,7 @@
"View Details": "查看詳情",
"Transaction List": "交易清單",
"Transaction Calendar": "交易日曆",
"Transaction Gallery": "交易相簿",
"Transaction Details": "交易詳情",
"Statistics & Analysis": "統計分析",
"Insights Explorer": "洞察探索",
+2
View File
@@ -55,6 +55,7 @@ import MonthPicker from '@/components/common/MonthPicker.vue';
import TransactionCalendar from '@/components/common/TransactionCalendar.vue';
import ItemIcon from '@/components/mobile/ItemIcon.vue';
import ImageBox from '@/components/mobile/ImageBox.vue';
import LanguageSelectButton from '@/components/mobile/LanguageSelectButton.vue';
import PieChart from '@/components/mobile/PieChart.vue';
import TrendsBarChart from '@/components/mobile/TrendsBarChart.vue';
@@ -150,6 +151,7 @@ app.component('MonthPicker', MonthPicker);
app.component('TransactionCalendar', TransactionCalendar);
app.component('ItemIcon', ItemIcon);
app.component('ImageBox', ImageBox);
app.component('LanguageSelectButton', LanguageSelectButton);
app.component('PieChart', PieChart);
app.component('TrendsBarChart', TrendsBarChart);
+4
View File
@@ -614,6 +614,8 @@ export interface TransactionListByMaxTimeRequest {
readonly tagFilter: string;
readonly amountFilter: string;
readonly keyword: string;
readonly mustHavePictures?: boolean;
readonly withPictures?: boolean;
}
export interface TransactionListInMonthByPageRequest {
@@ -625,6 +627,8 @@ export interface TransactionListInMonthByPageRequest {
readonly tagFilter: string;
readonly amountFilter: string;
readonly keyword: string;
readonly mustHavePictures?: boolean;
readonly withPictures?: boolean;
}
export interface TransactionAllListRequest {
+7 -3
View File
@@ -827,7 +827,7 @@ export const useTransactionsStore = defineStore('transactions', () => {
};
}
function loadTransactions({ reload, count, page, withCount, autoExpand, defaultCurrency }: { reload?: boolean, count?: number, page?: number, withCount?: boolean, autoExpand: boolean, defaultCurrency: string }): Promise<TransactionPageWrapper> {
function loadTransactions({ reload, count, page, mustHavePictures, withCount, withPictures, autoExpand, defaultCurrency }: { reload?: boolean, count?: number, page?: number, mustHavePictures?: boolean, withCount?: boolean, withPictures?: boolean, autoExpand: boolean, defaultCurrency: string }): Promise<TransactionPageWrapper> {
let actualMaxTime = transactionsNextTimeId.value;
if (reload && transactionsFilter.value.maxTime > 0) {
@@ -843,6 +843,8 @@ export const useTransactionsStore = defineStore('transactions', () => {
count: count || 50,
page: page || 1,
withCount: !!withCount,
withPictures: !!withPictures,
mustHavePictures: !!mustHavePictures,
type: transactionsFilter.value.type,
categoryIds: transactionsFilter.value.categoryIds,
accountIds: transactionsFilter.value.accountIds,
@@ -917,7 +919,7 @@ export const useTransactionsStore = defineStore('transactions', () => {
});
}
function loadMonthlyAllTransactions({ year, month, autoExpand, defaultCurrency }: { year: number, month: number, autoExpand: boolean, defaultCurrency: string }): Promise<TransactionPageWrapper> {
function loadMonthlyAllTransactions({ year, month, mustHavePictures, withPictures, autoExpand, defaultCurrency }: { year: number, month: number, mustHavePictures?: boolean, withPictures?: boolean, autoExpand: boolean, defaultCurrency: string }): Promise<TransactionPageWrapper> {
return new Promise((resolve, reject) => {
services.getAllTransactionsByMonth({
year: year,
@@ -927,7 +929,9 @@ export const useTransactionsStore = defineStore('transactions', () => {
accountIds: transactionsFilter.value.accountIds,
tagFilter: transactionsFilter.value.tagFilter,
amountFilter: transactionsFilter.value.amountFilter,
keyword: transactionsFilter.value.keyword
keyword: transactionsFilter.value.keyword,
mustHavePictures: !!mustHavePictures,
withPictures: !!withPictures
}).then(response => {
const data = response.data;
@@ -22,6 +22,7 @@ import type { TransactionCategory } from '@/models/transaction_category.ts';
import { TransactionTagGroup } from '@/models/transaction_tag_group.ts';
import type { TransactionTag } from '@/models/transaction_tag.ts';
import { type Transaction, TransactionTagFilter } from '@/models/transaction.ts';
import type { TransactionPictureInfoBasicResponse } from '@/models/transaction_picture_info.ts';
import {
getUtcOffsetByUtcOffsetMinutes,
@@ -49,6 +50,7 @@ export class TransactionListPageType implements TypeAndName {
public static readonly List = new TransactionListPageType(0, 'Transaction List');
public static readonly Calendar = new TransactionListPageType(1, 'Transaction Calendar');
public static readonly Gallery = new TransactionListPageType(2, 'Transaction Gallery');
public static readonly Default = TransactionListPageType.List;
@@ -401,6 +403,10 @@ export function useTransactionListPageBase() {
}
}
function getTransactionPictureUrl(pictureInfo?: TransactionPictureInfoBasicResponse | null): string | undefined {
return transactionsStore.getTransactionPictureUrl(pictureInfo);
}
return {
// states
pageType,
@@ -458,5 +464,6 @@ export function useTransactionListPageBase() {
getDisplayAmount,
getDisplayMonthTotalAmount,
getTransactionTypeName,
getTransactionPictureUrl
};
}
+81 -6
View File
@@ -31,7 +31,7 @@
v-model="queryType"
/>
</div>
<div class="mx-6 mt-4" v-if="pageType === TransactionListPageType.List.type">
<div class="mx-6 mt-4" v-if="pageType === TransactionListPageType.List.type || pageType === TransactionListPageType.Gallery.type">
<span class="text-subtitle-2">{{ tt('Transactions Per Page') }}</span>
<v-select class="mt-2" density="compact"
item-title="name"
@@ -182,7 +182,7 @@
v-model="currentCalendarDate"></transaction-calendar>
</v-card-text>
<v-table class="transaction-table" :hover="!loading">
<v-table class="transaction-table" :hover="!loading" v-if="pageType !== TransactionListPageType.Gallery.type">
<thead>
<tr>
<th class="transaction-table-column-time text-no-wrap">
@@ -595,7 +595,53 @@
</tbody>
</v-table>
<div class="mt-2 mb-4" v-if="pageType === TransactionListPageType.List.type">
<v-card-text class="transaction-gallery-container" v-if="pageType === TransactionListPageType.Gallery.type">
<div v-if="loading && (!transactions || !transactions.length || transactions.length < 1)">
<v-skeleton-loader class="skeleton-no-margin mt-2" type="text" :loading="true"></v-skeleton-loader>
</div>
<div v-if="!loading && (!transactions || !transactions.length || transactions.length < 1)">
{{ tt('No transaction data') }}
</div>
<div :key="date" :class="{ 'disabled': loading }"
v-for="(transactions, date) in transactionsByDay">
<div class="text-sm text-body-2 font-weight-bold">
<div class="d-flex align-center">
<span>{{ getDisplayLongDate(transactions[0] as Transaction) }}</span>
<v-chip class="ms-1" color="default" size="x-small"
v-if="(transactions[0] as Transaction).displayDayOfWeek">
{{ getWeekdayLongName((transactions[0] as Transaction).displayDayOfWeek as WeekDay) }}
</v-chip>
</div>
</div>
<div class="d-flex flex-wrap gap-2 py-2">
<v-avatar rounded="lg" variant="tonal" size="160"
class="cursor-pointer transaction-picture" color="rgba(0,0,0,0)"
:key="pictureInfo.pictureId"
v-for="[transaction, pictureInfo] in allTransactionPictures(transactions)"
@click="show(transaction)">
<v-img :src="getTransactionPictureUrl(pictureInfo)">
<template #placeholder>
<div class="d-flex align-center justify-center fill-height bg-light-primary">
<v-progress-circular color="grey-500" indeterminate size="48"></v-progress-circular>
</div>
</template>
<template #error>
<div class="d-flex align-center justify-center fill-height bg-light-primary">
<span class="text-body-1">{{ tt('Failed to load image, please check whether the config "domain" and "root_url" are set correctly.') }}</span>
</div>
</template>
</v-img>
<div class="picture-control-icon">
<v-icon size="64" :icon="mdiTextBoxEditOutline"/>
</div>
</v-avatar>
</div>
</div>
</v-card-text>
<div class="mt-2 mb-4" v-if="pageType === TransactionListPageType.List.type || pageType === TransactionListPageType.Gallery.type">
<pagination-buttons :totalPageCount="totalPageCount"
v-model="paginationCurrentPage"></pagination-buttons>
</div>
@@ -681,6 +727,7 @@ import {
type Year0BasedMonth,
type LocalizedRecentMonthDateRange,
type TimeRangeAndDateType,
type WeekDay,
DateRangeScene,
DateRange
} from '@/core/datetime.ts';
@@ -720,6 +767,7 @@ import {
categoryTypeToTransactionType,
transactionTypeToCategoryType
} from '@/lib/category.ts';
import { allTransactionPictures } from '@/lib/transaction.ts';
import { isDataExportingEnabled, isDataImportingEnabled, isTransactionFromAIImageRecognitionEnabled } from '@/lib/server_settings.ts';
import { scrollToSelectedItem, startDownloadFile } from '@/lib/ui/common.ts';
import logger from '@/lib/logger.ts';
@@ -739,7 +787,8 @@ import {
mdiArrowRight,
mdiPound,
mdiMagicStaff,
mdiTextBoxOutline
mdiTextBoxOutline,
mdiTextBoxEditOutline
} from '@mdi/js';
interface TransactionListProps {
@@ -830,6 +879,7 @@ const {
getDisplayAmount,
getDisplayMonthTotalAmount,
getTransactionTypeName,
getTransactionPictureUrl
} = useTransactionListPageBase();
const settingsStore = useSettingsStore();
@@ -886,7 +936,7 @@ const allPageCounts = computed<NameNumeralValue[]>(() => {
return pageCounts;
});
const recentMonthDateRanges = computed<LocalizedRecentMonthDateRange[]>(() => getAllRecentMonthDateRanges(pageType.value === TransactionListPageType.List.type, true));
const recentMonthDateRanges = computed<LocalizedRecentMonthDateRange[]>(() => getAllRecentMonthDateRanges(pageType.value === TransactionListPageType.List.type || pageType.value === TransactionListPageType.Gallery.type, true));
const allTransactionTemplates = computed<TransactionTemplate[]>(() => {
const allTemplates = transactionTemplatesStore.allVisibleTemplates;
@@ -902,7 +952,7 @@ const allowCategoryTypes = computed<string>(() => {
});
const transactions = computed<Transaction[]>(() => {
if (pageType.value === TransactionListPageType.List.type) {
if (pageType.value === TransactionListPageType.List.type || pageType.value === TransactionListPageType.Gallery.type) {
if (queryMonthlyData.value) {
const transactionData = currentMonthTransactionData.value;
@@ -942,6 +992,22 @@ const transactions = computed<Transaction[]>(() => {
}
});
const transactionsByDay = computed<Record<string, Transaction[]>>(() => {
const transactionsByDay: Record<string, Transaction[]> = {};
for (const transaction of transactions.value) {
if (!transaction.gregorianCalendarYearDashMonthDashDay) {
continue;
}
const transactions: Transaction[] = transactionsByDay[transaction.gregorianCalendarYearDashMonthDashDay] ?? [];
transactions.push(transaction);
transactionsByDay[transaction.gregorianCalendarYearDashMonthDashDay] = transactions;
}
return transactionsByDay;
});
const recentDateRangeIndex = computed<number>({
get: () => getRecentDateRangeIndex(recentMonthDateRanges.value, query.value.dateType, query.value.minTime, query.value.maxTime, firstDayOfWeek.value, fiscalYearStart.value),
set: (value) => {
@@ -1166,6 +1232,7 @@ function init(initProps: TransactionListProps): void {
function reload(force: boolean, init: boolean): void {
loading.value = true;
const isGalleryMode = pageType.value === TransactionListPageType.Gallery.type;
const page = currentPage.value;
Promise.all([
@@ -1188,6 +1255,8 @@ function reload(force: boolean, init: boolean): void {
return transactionsStore.loadMonthlyAllTransactions({
year: currentYear,
month: currentMonth,
mustHavePictures: isGalleryMode,
withPictures: isGalleryMode,
autoExpand: true,
defaultCurrency: defaultCurrency.value
});
@@ -1196,7 +1265,9 @@ function reload(force: boolean, init: boolean): void {
reload: true,
count: countPerPage.value,
page: page,
mustHavePictures: isGalleryMode,
withCount: page <= 1,
withPictures: isGalleryMode,
autoExpand: true,
defaultCurrency: defaultCurrency.value
});
@@ -1896,4 +1967,8 @@ init(props);
.transaction-calendar-container .dp__main .dp__calendar .dp__calendar_row > .dp__calendar_item .transaction-calendar-daily-amounts > span.transaction-calendar-daily-amount {
font-size: 0.95rem;
}
.transaction-gallery-container {
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
}
</style>
+140 -10
View File
@@ -80,10 +80,10 @@
</f7-block>
<div class="skeleton-text" v-if="loading">
<f7-block class="combination-list-wrapper margin-vertical" :class="{ 'no-accordion-toggle': pageType !== TransactionListPageType.List.type }"
<f7-block class="combination-list-wrapper margin-vertical" :class="{ 'no-accordion-toggle': pageType !== TransactionListPageType.List.type && pageType !== TransactionListPageType.Gallery.type }"
:key="blockIdx" v-for="blockIdx in (pageType === TransactionListPageType.List.type ? [ 1, 2 ] : [ 1 ])">
<f7-accordion-item>
<f7-block-title v-if="pageType === TransactionListPageType.List.type">
<f7-block-title v-if="pageType === TransactionListPageType.List.type || pageType === TransactionListPageType.Gallery.type">
<f7-accordion-toggle>
<f7-list strong inset dividers media-list
class="transaction-amount-list combination-list-header combination-list-opened">
@@ -101,7 +101,8 @@
</f7-accordion-toggle>
</f7-block-title>
<f7-accordion-content style="height: auto">
<f7-list strong inset dividers media-list accordion-list class="transaction-info-list combination-list-content">
<f7-list strong inset dividers media-list accordion-list class="transaction-info-list combination-list-content"
v-if="pageType === TransactionListPageType.List.type || pageType === TransactionListPageType.Calendar.type">
<f7-list-item link="#" chevron-center class="transaction-info"
:key="itemIdx" v-for="itemIdx in (pageType === TransactionListPageType.List.type && blockIdx === 1 ? [ 1, 2, 3, 4, 5, 6, 7 ] : [ 1, 2, 3 ])">
<template #media>
@@ -147,6 +148,21 @@
</template>
</f7-list-item>
</f7-list>
<f7-list strong inset dividers media-list accordion-list class="transaction-info-list combination-list-content transaction-gallery-list"
v-if="pageType === TransactionListPageType.Gallery.type">
<f7-list-item class="transaction-gallery-container">
<template #default>
<div class="transaction-gallery-grid">
<div class="transaction-picture-cell"
:key="itemIdx" v-for="itemIdx in [ 1, 2, 3, 4, 5 ]">
<div class="display-flex justify-content-center align-items-center" style="height: 100%">
<f7-skeleton-block class="transaction-picture-img" style="width: 40px; height: 40px; border-radius: 50%" />
</div>
</div>
</div>
</template>
</f7-list-item>
</f7-list>
</f7-accordion-content>
</f7-accordion-item>
</f7-block>
@@ -156,14 +172,14 @@
<f7-list-item :title="tt('No transaction data')"></f7-list-item>
</f7-list>
<f7-block class="combination-list-wrapper margin-vertical" :class="{ 'no-accordion-toggle': pageType !== TransactionListPageType.List.type }"
<f7-block class="combination-list-wrapper margin-vertical" :class="{ 'no-accordion-toggle': pageType !== TransactionListPageType.List.type && pageType !== TransactionListPageType.Gallery.type }"
:key="transactionMonthList.yearDashMonth" v-for="(transactionMonthList) in transactions">
<f7-accordion-item :opened="transactionMonthList.opened"
@accordion:open="collapseTransactionMonthList(transactionMonthList, false)"
@accordion:opened="onTransactionMonthListCollapseStateChanged"
@accordion:close="collapseTransactionMonthList(transactionMonthList, true)"
@accordion:closed="onTransactionMonthListCollapseStateChanged">
<f7-block-title :id="getTransactionMonthTitleDomId(transactionMonthList.yearDashMonth)" v-if="pageType === TransactionListPageType.List.type">
<f7-block-title :id="getTransactionMonthTitleDomId(transactionMonthList.yearDashMonth)" v-if="pageType === TransactionListPageType.List.type || pageType === TransactionListPageType.Gallery.type">
<f7-accordion-toggle>
<f7-list strong inset dividers media-list
class="transaction-amount-list combination-list-header"
@@ -193,7 +209,7 @@
<f7-list strong inset dividers media-list accordion-list
class="transaction-info-list transaction-month-list combination-list-content"
:id="getTransactionMonthListDomId(transactionMonthList.yearDashMonth)"
v-if="!isTransactionMonthListInvisible(transactionMonthList)"
v-if="!isTransactionMonthListInvisible(transactionMonthList) && (pageType === TransactionListPageType.List.type || pageType === TransactionListPageType.Calendar.type)"
>
<f7-list-item swipeout chevron-center accordion-item
class="transaction-info"
@@ -293,12 +309,33 @@
</f7-swipeout-actions>
</f7-list-item>
</f7-list>
<f7-list strong inset dividers media-list accordion-list
class="transaction-info-list transaction-month-list combination-list-content transaction-gallery-list"
:id="getTransactionMonthListDomId(transactionMonthList.yearDashMonth)"
v-if="!isTransactionMonthListInvisible(transactionMonthList) && (pageType === TransactionListPageType.Gallery.type)">
<f7-list-item class="transaction-gallery-container">
<template #default>
<div class="transaction-gallery-grid">
<div class="transaction-picture-cell" :key="pictureInfo.pictureId"
v-for="[transaction, pictureInfo] in allTransactionPictures(transactionMonthList.items)">
<image-box class="transaction-picture-img" alt="picture"
:link="`/transaction/detail?id=${transaction.id}&type=${transaction.type}`"
:src="getTransactionPictureUrl(pictureInfo)">
<template #error>
{{ tt('Failed to load image, please check whether the config "domain" and "root_url" are set correctly.') }}
</template>
</image-box>
</div>
</div>
</template>
</f7-list-item>
</f7-list>
</f7-accordion-content>
</f7-accordion-item>
</f7-block>
<f7-block class="text-align-center" :class="{ 'disabled': loadingMore }" v-show="!loading && hasMoreTransaction"
v-if="pageType === TransactionListPageType.List.type">
v-if="pageType === TransactionListPageType.List.type || pageType === TransactionListPageType.Gallery.type">
<f7-link href="#" @click="loadMore(false)">{{ tt('Load More') }}</f7-link>
</f7-block>
@@ -309,7 +346,7 @@
:class="{ 'list-item-selected': query.dateType === dateRange.type }"
:key="dateRange.type"
v-for="dateRange in allDateRanges"
v-show="pageType === TransactionListPageType.List.type || dateRange.type === DateRange.ThisMonth.type || dateRange.type === DateRange.LastMonth.type || dateRange.type === DateRange.Custom.type"
v-show="pageType === TransactionListPageType.List.type || pageType === TransactionListPageType.Gallery.type || dateRange.type === DateRange.ThisMonth.type || dateRange.type === DateRange.LastMonth.type || dateRange.type === DateRange.Custom.type"
@click="changeDateFilter(dateRange.type)">
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.dateType === dateRange.type"></f7-icon>
@@ -661,6 +698,7 @@ import {
categoryTypeToTransactionType,
transactionTypeToCategoryType
} from '@/lib/category.ts';
import { allTransactionPictures } from '@/lib/transaction.ts';
const props = defineProps<{
f7route: Router.Route;
@@ -727,6 +765,7 @@ const {
getDisplayAmount,
getDisplayMonthTotalAmount,
getTransactionTypeName,
getTransactionPictureUrl
} = useTransactionListPageBase();
const environmentsStore = useEnvironmentsStore();
@@ -753,7 +792,7 @@ const transactions = computed<TransactionMonthList[]>(() => {
return [];
}
if (pageType.value === TransactionListPageType.List.type) {
if (pageType.value === TransactionListPageType.List.type || pageType.value === TransactionListPageType.Gallery.type) {
return transactionsStore.transactions;
} else if (pageType.value === TransactionListPageType.Calendar.type) {
if (queryMonthlyData.value) {
@@ -796,7 +835,7 @@ const transactions = computed<TransactionMonthList[]>(() => {
});
const noTransaction = computed<boolean>(() => {
if (pageType.value === TransactionListPageType.List.type) {
if (pageType.value === TransactionListPageType.List.type || pageType.value === TransactionListPageType.Gallery.type) {
return transactionsStore.noTransaction;
} else if (pageType.value === TransactionListPageType.Calendar.type) {
return !transactions.value || !transactions.value.length || !transactions.value[0]!.items || !transactions.value[0]!.items.length;
@@ -962,6 +1001,8 @@ function reload(done?: () => void): void {
loading.value = true;
}
const isGalleryMode = pageType.value === TransactionListPageType.Gallery.type;
transactionInvisibleYearMonths.value = {};
transactionYearMonthListHeights.value = {};
@@ -978,12 +1019,16 @@ function reload(done?: () => void): void {
return transactionsStore.loadMonthlyAllTransactions({
year: currentYear,
month: currentMonth,
mustHavePictures: isGalleryMode,
withPictures: isGalleryMode,
autoExpand: true,
defaultCurrency: defaultCurrency.value
});
} else {
return transactionsStore.loadTransactions({
reload: true,
mustHavePictures: isGalleryMode,
withPictures: isGalleryMode,
autoExpand: true,
defaultCurrency: defaultCurrency.value
});
@@ -1023,10 +1068,14 @@ function loadMore(autoExpand: boolean): void {
return;
}
const isGalleryMode = pageType.value === TransactionListPageType.Gallery.type;
loadingMore.value = true;
transactionsStore.loadTransactions({
reload: false,
mustHavePictures: isGalleryMode,
withPictures: isGalleryMode,
autoExpand: autoExpand,
defaultCurrency: defaultCurrency.value
}).then(() => {
@@ -1060,6 +1109,8 @@ function changePageType(type: number): void {
reload();
}
}
} else {
reload();
}
}
@@ -1702,4 +1753,83 @@ html[dir="rtl"] .list.transaction-info-list li.transaction-info .transaction-foo
.transaction-calendar-container .dp__main .dp__calendar .dp__calendar_row > .dp__calendar_item .transaction-calendar-daily-amounts > span.transaction-calendar-daily-amount {
font-size: var(--ebk-transaction-calendar-amount-font-size);
}
.transaction-gallery-list.list > ul {
overflow: hidden;
}
.transaction-gallery-list.list > ul::before,
.transaction-gallery-list.list > ul::after {
display: none;
}
.transaction-gallery-container > .item-content {
padding: 0;
}
.transaction-gallery-container > .item-content > .item-inner {
padding: 0;
min-height: 0;
}
.transaction-gallery-container > .item-content > .item-inner::after {
display: none;
}
.transaction-gallery-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2px;
width: 100%;
}
@media (min-width: 480px) {
.transaction-gallery-grid {
grid-template-columns: repeat(4, 1fr);
}
}
@media (min-width: 640px) {
.transaction-gallery-grid {
grid-template-columns: repeat(5, 1fr);
}
}
@media (min-width: 800px) {
.transaction-gallery-grid {
grid-template-columns: repeat(6, 1fr);
}
}
@media (min-width: 960px) {
.transaction-gallery-grid {
grid-template-columns: repeat(7, 1fr);
}
}
@media (min-width: 1024px) {
.transaction-gallery-grid {
grid-template-columns: repeat(8, 1fr);
}
}
@media (min-width: 1280px) {
.transaction-gallery-grid {
grid-template-columns: repeat(9, 1fr);
}
}
.transaction-picture-cell {
position: relative;
display: block;
aspect-ratio: 1;
overflow: hidden;
background-color: var(--f7-list-bg-color);
}
.transaction-picture-img {
width: 100%;
height: 100%;
display: block;
}
</style>