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
+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>