fix
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 24s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 3s
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 24s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 3s
This commit is contained in:
@@ -18,7 +18,7 @@ public class TransactionRecord : BaseEntity
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 交易金额
|
/// 交易金额
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public decimal Amount { get; set; }
|
public decimal Amount { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 交易后余额
|
/// 交易后余额
|
||||||
@@ -60,7 +60,7 @@ public decimal Amount { get; set; }
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string ImportNo { get; set; } = string.Empty;
|
public string ImportNo { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 导入来源
|
/// 导入来源
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ImportFrom { get; set; } = string.Empty;
|
public string ImportFrom { get; set; } = string.Empty;
|
||||||
|
|||||||
@@ -375,14 +375,14 @@ public class BudgetSavingsService(
|
|||||||
var currentActual = 0m;
|
var currentActual = 0m;
|
||||||
if (!string.IsNullOrEmpty(savingsCategories))
|
if (!string.IsNullOrEmpty(savingsCategories))
|
||||||
{
|
{
|
||||||
var cats = new HashSet<string>(savingsCategories.Split(',', StringSplitOptions.RemoveEmptyEntries));
|
var cats = new HashSet<string>(savingsCategories.Split(',', StringSplitOptions.RemoveEmptyEntries));
|
||||||
foreach(var kvp in transactionClassify)
|
foreach (var kvp in transactionClassify)
|
||||||
{
|
{
|
||||||
if (cats.Contains(kvp.Key.Item1))
|
if (cats.Contains(kvp.Key.Item1))
|
||||||
{
|
{
|
||||||
currentActual += kvp.Value;
|
currentActual += kvp.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var record = new BudgetRecord
|
var record = new BudgetRecord
|
||||||
@@ -601,10 +601,8 @@ public class BudgetSavingsService(
|
|||||||
预算收入合计:
|
预算收入合计:
|
||||||
<span class='expense-value'>
|
<span class='expense-value'>
|
||||||
<strong>
|
<strong>
|
||||||
{
|
{currentMonthlyIncomeItems.Sum(i => i.limit * i.factor)
|
||||||
currentMonthlyIncomeItems.Sum(i => i.limit * i.factor)
|
+ currentYearlyIncomeItems.Sum(i => i.limit):N0}
|
||||||
+ currentYearlyIncomeItems.Sum(i => i.limit)
|
|
||||||
:N0}
|
|
||||||
</strong>
|
</strong>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -714,10 +712,8 @@ public class BudgetSavingsService(
|
|||||||
支出预算合计:
|
支出预算合计:
|
||||||
<span class='expense-value'>
|
<span class='expense-value'>
|
||||||
<strong>
|
<strong>
|
||||||
{
|
{currentMonthlyExpenseItems.Sum(i => i.limit * i.factor)
|
||||||
currentMonthlyExpenseItems.Sum(i => i.limit * i.factor)
|
+ currentYearlyExpenseItems.Sum(i => i.limit):N0}
|
||||||
+ currentYearlyExpenseItems.Sum(i => i.limit)
|
|
||||||
:N0}
|
|
||||||
</strong>
|
</strong>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -773,14 +769,14 @@ public class BudgetSavingsService(
|
|||||||
var currentActual = 0m;
|
var currentActual = 0m;
|
||||||
if (!string.IsNullOrEmpty(savingsCategories))
|
if (!string.IsNullOrEmpty(savingsCategories))
|
||||||
{
|
{
|
||||||
var cats = new HashSet<string>(savingsCategories.Split(',', StringSplitOptions.RemoveEmptyEntries));
|
var cats = new HashSet<string>(savingsCategories.Split(',', StringSplitOptions.RemoveEmptyEntries));
|
||||||
foreach(var kvp in transactionClassify)
|
foreach (var kvp in transactionClassify)
|
||||||
{
|
{
|
||||||
if (cats.Contains(kvp.Key.Item1))
|
if (cats.Contains(kvp.Key.Item1))
|
||||||
{
|
{
|
||||||
currentActual += kvp.Value;
|
currentActual += kvp.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var record = new BudgetRecord
|
var record = new BudgetRecord
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ public class EmailHandleService(
|
|||||||
{
|
{
|
||||||
var clone = records.ToArray().DeepClone();
|
var clone = records.ToArray().DeepClone();
|
||||||
|
|
||||||
if(clone?.Any() != true)
|
if (clone?.Any() != true)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ public class EmailParseForm95555(
|
|||||||
var balanceStr = match.Groups["balance"].Value;
|
var balanceStr = match.Groups["balance"].Value;
|
||||||
var typeStr = match.Groups["type"].Value;
|
var typeStr = match.Groups["type"].Value;
|
||||||
var reason = match.Groups["reason"].Value;
|
var reason = match.Groups["reason"].Value;
|
||||||
if(string.IsNullOrEmpty(reason))
|
if (string.IsNullOrEmpty(reason))
|
||||||
{
|
{
|
||||||
reason = typeStr;
|
reason = typeStr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ public partial class EmailParseFormCcsvc(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 招商信用卡特殊,消费金额为正数,退款为负数
|
// 招商信用卡特殊,消费金额为正数,退款为负数
|
||||||
if(amount > 0)
|
if (amount > 0)
|
||||||
{
|
{
|
||||||
type = TransactionType.Expense;
|
type = TransactionType.Expense;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ public abstract class EmailParseServicesBase(
|
|||||||
// AI兜底
|
// AI兜底
|
||||||
result = await ParseByAiAsync(emailContent) ?? [];
|
result = await ParseByAiAsync(emailContent) ?? [];
|
||||||
|
|
||||||
if(result.Length == 0)
|
if (result.Length == 0)
|
||||||
{
|
{
|
||||||
logger.LogWarning("AI解析邮件内容也未能提取到任何交易记录");
|
logger.LogWarning("AI解析邮件内容也未能提取到任何交易记录");
|
||||||
}
|
}
|
||||||
@@ -175,7 +175,7 @@ public abstract class EmailParseServicesBase(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var occurredAt = (DateTime?)null;
|
var occurredAt = (DateTime?)null;
|
||||||
if(DateTime.TryParse(occurredAtStr, out var occurredAtValue))
|
if (DateTime.TryParse(occurredAtStr, out var occurredAtValue))
|
||||||
{
|
{
|
||||||
occurredAt = occurredAtValue;
|
occurredAt = occurredAtValue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,12 +199,12 @@ public class EmailSyncService(
|
|||||||
message.TextBody ?? message.HtmlBody ?? string.Empty
|
message.TextBody ?? message.HtmlBody ?? string.Empty
|
||||||
) || (DateTime.Now - message.Date.DateTime > TimeSpan.FromDays(3)))
|
) || (DateTime.Now - message.Date.DateTime > TimeSpan.FromDays(3)))
|
||||||
{
|
{
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
logger.LogDebug("DEBUG 模式下,跳过标记已读步骤");
|
logger.LogDebug("DEBUG 模式下,跳过标记已读步骤");
|
||||||
#else
|
#else
|
||||||
// 标记邮件为已读
|
// 标记邮件为已读
|
||||||
await emailFetchService.MarkAsReadAsync(uid);
|
await emailFetchService.MarkAsReadAsync(uid);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ public class ImportService(
|
|||||||
|
|
||||||
DateTime GetDateTimeValue(IDictionary<string, string> row, string key)
|
DateTime GetDateTimeValue(IDictionary<string, string> row, string key)
|
||||||
{
|
{
|
||||||
if(!row.ContainsKey(key))
|
if (!row.ContainsKey(key))
|
||||||
{
|
{
|
||||||
return DateTime.MinValue;
|
return DateTime.MinValue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,12 +144,12 @@ public class EmailSyncJob(
|
|||||||
message.TextBody ?? message.HtmlBody ?? string.Empty
|
message.TextBody ?? message.HtmlBody ?? string.Empty
|
||||||
) || (DateTime.Now - message.Date.DateTime > TimeSpan.FromDays(3)))
|
) || (DateTime.Now - message.Date.DateTime > TimeSpan.FromDays(3)))
|
||||||
{
|
{
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
logger.LogDebug("DEBUG 模式下,跳过标记已读步骤");
|
logger.LogDebug("DEBUG 模式下,跳过标记已读步骤");
|
||||||
#else
|
#else
|
||||||
// 标记邮件为已读
|
// 标记邮件为已读
|
||||||
await emailFetchService.MarkAsReadAsync(uid);
|
await emailFetchService.MarkAsReadAsync(uid);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ export const createTransaction = (data) => {
|
|||||||
* @param {number} data.balance - 交易后余额
|
* @param {number} data.balance - 交易后余额
|
||||||
* @param {number} data.type - 交易类型 (0:支出, 1:收入, 2:不计入收支)
|
* @param {number} data.type - 交易类型 (0:支出, 1:收入, 2:不计入收支)
|
||||||
* @param {string} data.classify - 交易分类
|
* @param {string} data.classify - 交易分类
|
||||||
|
* @param {string} [data.occurredAt] - 交易时间
|
||||||
* @returns {Promise<{success: boolean}>}
|
* @returns {Promise<{success: boolean}>}
|
||||||
*/
|
*/
|
||||||
export const updateTransaction = (data) => {
|
export const updateTransaction = (data) => {
|
||||||
|
|||||||
184
Web/src/assets/theme.css
Normal file
184
Web/src/assets/theme.css
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
/**
|
||||||
|
* EmailBill 主题系统 - 根据 v2.pen 设计稿
|
||||||
|
* 用于保持整个应用色彩和布局一致性
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* ============ 颜色变量 - 浅色主题 ============ */
|
||||||
|
|
||||||
|
/* 背景色 */
|
||||||
|
--bg-primary: #FFFFFF;
|
||||||
|
--bg-secondary: #F6F7F8;
|
||||||
|
--bg-tertiary: #F3F4F6;
|
||||||
|
--bg-button: #F5F5F5;
|
||||||
|
|
||||||
|
/* 文字颜色 */
|
||||||
|
--text-primary: #1A1A1A;
|
||||||
|
--text-secondary: #6B7280;
|
||||||
|
--text-tertiary: #9CA3AF;
|
||||||
|
|
||||||
|
/* 强调色 */
|
||||||
|
--accent-primary: #FF6B6B;
|
||||||
|
--accent-danger: #EF4444;
|
||||||
|
--accent-warning: #D97706;
|
||||||
|
--accent-warning-bg: #FFFBEB;
|
||||||
|
--accent-success: #22C55E;
|
||||||
|
--accent-success-bg: #F0FDF4;
|
||||||
|
--accent-info: #6366F1;
|
||||||
|
--accent-info-bg: #E0E7FF;
|
||||||
|
|
||||||
|
/* 图标色 */
|
||||||
|
--icon-star: #FF6B6B;
|
||||||
|
--icon-coffee: #FCD34D;
|
||||||
|
|
||||||
|
/* ============ 布局变量 ============ */
|
||||||
|
|
||||||
|
/* 间距 */
|
||||||
|
--spacing-xs: 2px;
|
||||||
|
--spacing-sm: 4px;
|
||||||
|
--spacing-md: 8px
|
||||||
|
--spacing-lg: 12px;
|
||||||
|
--spacing-xl: 16px;
|
||||||
|
--spacing-2xl: 20px;
|
||||||
|
--spacing-3xl: 24px;
|
||||||
|
|
||||||
|
/* 圆角 */
|
||||||
|
--radius-sm: 12px;
|
||||||
|
--radius-md: 16px;
|
||||||
|
--radius-lg: 20px;
|
||||||
|
--radius-full: 22px;
|
||||||
|
|
||||||
|
/* 字体大小 */
|
||||||
|
--font-xs: 9px;
|
||||||
|
--font-sm: 11px;
|
||||||
|
--font-base: 12px;
|
||||||
|
--font-md: 13px;
|
||||||
|
--font-lg: 15px;
|
||||||
|
--font-xl: 18px;
|
||||||
|
--font-2xl: 24px;
|
||||||
|
--font-3xl: 32px;
|
||||||
|
|
||||||
|
/* 字体粗细 */
|
||||||
|
--font-medium: 500;
|
||||||
|
--font-semibold: 600;
|
||||||
|
--font-bold: 700;
|
||||||
|
--font-extrabold: 800;
|
||||||
|
|
||||||
|
/* 字体 */
|
||||||
|
--font-primary: 'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
--font-display: 'Bricolage Grotesque', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
|
||||||
|
/* 阴影 (可选) */
|
||||||
|
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||||
|
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ 深色主题 ============ */
|
||||||
|
[data-theme="dark"] {
|
||||||
|
/* 背景色 */
|
||||||
|
--bg-primary: #09090B;
|
||||||
|
--bg-secondary: #18181b;
|
||||||
|
--bg-tertiary: #27272a;
|
||||||
|
--bg-button: #27272a;
|
||||||
|
|
||||||
|
/* 文字颜色 */
|
||||||
|
--text-primary: #f4f4f5;
|
||||||
|
--text-secondary: #a1a1aa;
|
||||||
|
--text-tertiary: #71717a;
|
||||||
|
|
||||||
|
/* 强调色 (深色主题调整) */
|
||||||
|
--accent-primary: #FF6B6B;
|
||||||
|
--accent-danger: #f87171;
|
||||||
|
--accent-warning: #fbbf24;
|
||||||
|
--accent-warning-bg: #451a03;
|
||||||
|
--accent-success: #4ade80;
|
||||||
|
--accent-success-bg: #064e3b;
|
||||||
|
--accent-info: #818cf8;
|
||||||
|
--accent-info-bg: #312e81;
|
||||||
|
|
||||||
|
/* 图标色 (深色主题) */
|
||||||
|
--icon-star: #FF6B6B;
|
||||||
|
--icon-coffee: #FCD34D;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ 通用工具类 ============ */
|
||||||
|
|
||||||
|
/* 文字 */
|
||||||
|
.text-primary {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-secondary {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-tertiary {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-danger {
|
||||||
|
color: var(--accent-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 背景 */
|
||||||
|
.bg-primary {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-secondary {
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-tertiary {
|
||||||
|
background-color: var(--bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 布局容器 */
|
||||||
|
.container-fluid {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 402px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flex 布局 */
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-col {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.justify-between {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.justify-center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 间距 */
|
||||||
|
.gap-xs { gap: var(--spacing-xs); }
|
||||||
|
.gap-sm { gap: var(--spacing-sm); }
|
||||||
|
.gap-md { gap: var(--spacing-md); }
|
||||||
|
.gap-lg { gap: var(--spacing-lg); }
|
||||||
|
.gap-xl { gap: var(--spacing-xl); }
|
||||||
|
.gap-2xl { gap: var(--spacing-2xl); }
|
||||||
|
.gap-3xl { gap: var(--spacing-3xl); }
|
||||||
|
|
||||||
|
/* 内边距 */
|
||||||
|
.p-sm { padding: var(--spacing-md); }
|
||||||
|
.p-md { padding: var(--spacing-xl); }
|
||||||
|
.p-lg { padding: var(--spacing-2xl); }
|
||||||
|
.p-xl { padding: var(--spacing-3xl); }
|
||||||
|
|
||||||
|
/* 圆角 */
|
||||||
|
.rounded-sm { border-radius: var(--radius-sm); }
|
||||||
|
.rounded-md { border-radius: var(--radius-md); }
|
||||||
|
.rounded-lg { border-radius: var(--radius-lg); }
|
||||||
|
.rounded-full { border-radius: var(--radius-full); }
|
||||||
@@ -275,7 +275,7 @@ const handleTypeChange = () => {
|
|||||||
const onConfirmDate = ({ selectedValues }) => {
|
const onConfirmDate = ({ selectedValues }) => {
|
||||||
const dateStr = selectedValues.join('-')
|
const dateStr = selectedValues.join('-')
|
||||||
const timeStr = currentTime.value.join(':')
|
const timeStr = currentTime.value.join(':')
|
||||||
editForm.occurredAt = dayjs(`${dateStr} ${timeStr}`).toISOString()
|
editForm.occurredAt = dayjs(`${dateStr} ${timeStr}`).format('YYYY-MM-DDTHH:mm:ss')
|
||||||
showDatePicker.value = false
|
showDatePicker.value = false
|
||||||
// 接着选时间
|
// 接着选时间
|
||||||
showTimePicker.value = true
|
showTimePicker.value = true
|
||||||
@@ -285,7 +285,7 @@ const onConfirmTime = ({ selectedValues }) => {
|
|||||||
currentTime.value = selectedValues
|
currentTime.value = selectedValues
|
||||||
const dateStr = currentDate.value.join('-')
|
const dateStr = currentDate.value.join('-')
|
||||||
const timeStr = selectedValues.join(':')
|
const timeStr = selectedValues.join(':')
|
||||||
editForm.occurredAt = dayjs(`${dateStr} ${timeStr}`).toISOString()
|
editForm.occurredAt = dayjs(`${dateStr} ${timeStr}`).format('YYYY-MM-DDTHH:mm:ss')
|
||||||
showTimePicker.value = false
|
showTimePicker.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import './assets/main.css'
|
import './assets/main.css'
|
||||||
|
import './assets/theme.css'
|
||||||
import './styles/common.css'
|
import './styles/common.css'
|
||||||
import './styles/rich-content.css'
|
import './styles/rich-content.css'
|
||||||
|
|
||||||
|
|||||||
630
Web/src/views/CalendarV2.vue
Normal file
630
Web/src/views/CalendarV2.vue
Normal file
@@ -0,0 +1,630 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="calendar-v2"
|
||||||
|
:data-theme="theme"
|
||||||
|
>
|
||||||
|
<!-- 头部 -->
|
||||||
|
<header class="calendar-header">
|
||||||
|
<div class="header-content">
|
||||||
|
<h1 class="header-title">
|
||||||
|
{{ currentMonth }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="notif-btn"
|
||||||
|
aria-label="通知"
|
||||||
|
>
|
||||||
|
<van-icon name="bell" />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 日历容器 -->
|
||||||
|
<div class="calendar-container">
|
||||||
|
<!-- 星期标题 -->
|
||||||
|
<div class="week-days">
|
||||||
|
<span
|
||||||
|
v-for="day in weekDays"
|
||||||
|
:key="day"
|
||||||
|
class="week-day"
|
||||||
|
>{{ day }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 日历网格 -->
|
||||||
|
<div class="calendar-grid">
|
||||||
|
<div
|
||||||
|
v-for="(week, weekIndex) in calendarWeeks"
|
||||||
|
:key="weekIndex"
|
||||||
|
class="calendar-week"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="day in week"
|
||||||
|
:key="day.date"
|
||||||
|
class="day-cell"
|
||||||
|
@click="onDayClick(day)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="day-number"
|
||||||
|
:class="{
|
||||||
|
'day-today': day.isToday,
|
||||||
|
'day-selected': day.isSelected,
|
||||||
|
'day-has-data': day.hasData,
|
||||||
|
'day-over-limit': day.isOverLimit,
|
||||||
|
'day-other-month': !day.isCurrentMonth
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ day.dayNumber }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="day.amount"
|
||||||
|
class="day-amount"
|
||||||
|
:class="{ 'amount-over': day.isOverLimit }"
|
||||||
|
>
|
||||||
|
{{ day.amount }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 每日统计 -->
|
||||||
|
<div class="daily-stats">
|
||||||
|
<div class="stats-header">
|
||||||
|
<h2 class="stats-title">
|
||||||
|
Daily Stats
|
||||||
|
</h2>
|
||||||
|
<span class="stats-date">{{ selectedDateFormatted }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stats-card">
|
||||||
|
<div class="stats-row">
|
||||||
|
<span class="stats-label">Total Spent</span>
|
||||||
|
<div class="stats-badge">
|
||||||
|
Daily Limit: {{ dailyLimit }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stats-value">
|
||||||
|
¥ {{ totalSpent }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 交易列表 -->
|
||||||
|
<div class="transactions">
|
||||||
|
<div class="txn-header">
|
||||||
|
<h2 class="txn-title">
|
||||||
|
Transactions
|
||||||
|
</h2>
|
||||||
|
<div class="txn-actions">
|
||||||
|
<div class="txn-badge badge-success">
|
||||||
|
{{ transactionCount }} Items
|
||||||
|
</div>
|
||||||
|
<button class="smart-btn">
|
||||||
|
<van-icon name="star-o" />
|
||||||
|
<span>Smart</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 交易卡片 -->
|
||||||
|
<div class="txn-list">
|
||||||
|
<div
|
||||||
|
v-for="txn in transactions"
|
||||||
|
:key="txn.id"
|
||||||
|
class="txn-card"
|
||||||
|
@click="onTransactionClick(txn)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="txn-icon"
|
||||||
|
:style="{ backgroundColor: txn.iconBg }"
|
||||||
|
>
|
||||||
|
<van-icon
|
||||||
|
:name="txn.icon"
|
||||||
|
:color="txn.iconColor"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="txn-content">
|
||||||
|
<div class="txn-name">
|
||||||
|
{{ txn.name }}
|
||||||
|
</div>
|
||||||
|
<div class="txn-time">
|
||||||
|
{{ txn.time }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="txn-amount">
|
||||||
|
{{ txn.amount }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部安全距离 -->
|
||||||
|
<div class="bottom-spacer" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
|
||||||
|
// 当前主题
|
||||||
|
const theme = ref('light') // 'light' | 'dark'
|
||||||
|
|
||||||
|
// 星期标题
|
||||||
|
const weekDays = ['M', 'T', 'W', 'T', 'F', 'S', 'S']
|
||||||
|
|
||||||
|
// 当前日期
|
||||||
|
const currentDate = ref(new Date())
|
||||||
|
const selectedDate = ref(new Date())
|
||||||
|
|
||||||
|
// 当前月份格式化
|
||||||
|
const currentMonth = computed(() => {
|
||||||
|
return currentDate.value.toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 选中日期格式化
|
||||||
|
const selectedDateFormatted = computed(() => {
|
||||||
|
return selectedDate.value.toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 生成日历数据
|
||||||
|
const calendarWeeks = computed(() => {
|
||||||
|
const year = currentDate.value.getFullYear()
|
||||||
|
const month = currentDate.value.getMonth()
|
||||||
|
|
||||||
|
// 获取当月第一天
|
||||||
|
const firstDay = new Date(year, month, 1)
|
||||||
|
// 获取当月最后一天
|
||||||
|
const lastDay = new Date(year, month + 1, 0)
|
||||||
|
|
||||||
|
// 获取第一天是星期几 (0=Sunday, 调整为 0=Monday)
|
||||||
|
let startDayOfWeek = firstDay.getDay() - 1
|
||||||
|
if (startDayOfWeek === -1) {startDayOfWeek = 6}
|
||||||
|
|
||||||
|
const weeks = []
|
||||||
|
let currentWeek = []
|
||||||
|
|
||||||
|
// 填充上月日期
|
||||||
|
for (let i = 0; i < startDayOfWeek; i++) {
|
||||||
|
const date = new Date(year, month, -(startDayOfWeek - i - 1))
|
||||||
|
currentWeek.push(createDayObject(date, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填充当月日期
|
||||||
|
for (let day = 1; day <= lastDay.getDate(); day++) {
|
||||||
|
const date = new Date(year, month, day)
|
||||||
|
currentWeek.push(createDayObject(date, true))
|
||||||
|
|
||||||
|
if (currentWeek.length === 7) {
|
||||||
|
weeks.push(currentWeek)
|
||||||
|
currentWeek = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填充下月日期
|
||||||
|
if (currentWeek.length > 0) {
|
||||||
|
const remainingDays = 7 - currentWeek.length
|
||||||
|
for (let i = 1; i <= remainingDays; i++) {
|
||||||
|
const date = new Date(year, month + 1, i)
|
||||||
|
currentWeek.push(createDayObject(date, false))
|
||||||
|
}
|
||||||
|
weeks.push(currentWeek)
|
||||||
|
}
|
||||||
|
|
||||||
|
return weeks
|
||||||
|
})
|
||||||
|
|
||||||
|
// 创建日期对象
|
||||||
|
const createDayObject = (date, isCurrentMonth) => {
|
||||||
|
const today = new Date()
|
||||||
|
const isToday =
|
||||||
|
date.getDate() === today.getDate() &&
|
||||||
|
date.getMonth() === today.getMonth() &&
|
||||||
|
date.getFullYear() === today.getFullYear()
|
||||||
|
|
||||||
|
const isSelected =
|
||||||
|
date.getDate() === selectedDate.value.getDate() &&
|
||||||
|
date.getMonth() === selectedDate.value.getMonth() &&
|
||||||
|
date.getFullYear() === selectedDate.value.getFullYear()
|
||||||
|
|
||||||
|
// 模拟数据 - 实际应该从 API 获取
|
||||||
|
const mockData = getMockDataForDate(date)
|
||||||
|
|
||||||
|
return {
|
||||||
|
date: date.getTime(),
|
||||||
|
dayNumber: date.getDate(),
|
||||||
|
isCurrentMonth,
|
||||||
|
isToday,
|
||||||
|
isSelected,
|
||||||
|
hasData: mockData.hasData,
|
||||||
|
amount: mockData.amount,
|
||||||
|
isOverLimit: mockData.isOverLimit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟数据获取
|
||||||
|
const getMockDataForDate = (date) => {
|
||||||
|
const day = date.getDate()
|
||||||
|
|
||||||
|
// 模拟一些有数据的日期
|
||||||
|
if (day >= 4 && day <= 28 && date.getMonth() === currentDate.value.getMonth()) {
|
||||||
|
const amounts = [128, 45, 230, 12, 88, 223, 15, 34, 120, 56, 442]
|
||||||
|
const amount = amounts[day % amounts.length]
|
||||||
|
return {
|
||||||
|
hasData: true,
|
||||||
|
amount: amount || '',
|
||||||
|
isOverLimit: amount > 200 // 超过限额标红
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { hasData: false, amount: '', isOverLimit: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
|
const dailyLimit = ref('2500')
|
||||||
|
const totalSpent = ref('1,248.50')
|
||||||
|
const transactionCount = computed(() => transactions.value.length)
|
||||||
|
|
||||||
|
// 交易列表数据
|
||||||
|
const transactions = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Lunch',
|
||||||
|
time: '12:30 PM',
|
||||||
|
amount: '-58.00',
|
||||||
|
icon: 'star',
|
||||||
|
iconColor: '#FF6B6B',
|
||||||
|
iconBg: '#FFFFFF'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Coffee',
|
||||||
|
time: '08:15 AM',
|
||||||
|
amount: '-24.50',
|
||||||
|
icon: 'coffee-o',
|
||||||
|
iconColor: '#FCD34D',
|
||||||
|
iconBg: '#FFFFFF'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 点击日期
|
||||||
|
const onDayClick = (day) => {
|
||||||
|
if (!day.isCurrentMonth) {return}
|
||||||
|
selectedDate.value = new Date(day.date)
|
||||||
|
// TODO: 加载选中日期的数据
|
||||||
|
console.log('Selected date:', day)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击交易
|
||||||
|
const onTransactionClick = (txn) => {
|
||||||
|
console.log('Transaction clicked:', txn)
|
||||||
|
// TODO: 打开交易详情
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换主题
|
||||||
|
const toggleTheme = () => {
|
||||||
|
theme.value = theme.value === 'light' ? 'dark' : 'light'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露切换主题方法供外部调用
|
||||||
|
defineExpose({
|
||||||
|
toggleTheme
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@import '@/assets/theme.css';
|
||||||
|
|
||||||
|
.calendar-v2 {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
font-family: var(--font-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 头部 ========== */
|
||||||
|
.calendar-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 24px;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-family: var(--font-primary);
|
||||||
|
font-size: var(--font-xl);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notif-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
background-color: var(--bg-button);
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notif-btn:active {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 日历容器 ========== */
|
||||||
|
.calendar-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.week-days {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.week-day {
|
||||||
|
width: 44px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: var(--font-base);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-week {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
width: 44px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-number {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 16px;
|
||||||
|
font-size: var(--font-md);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-number.day-has-data {
|
||||||
|
background-color: var(--bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-number.day-selected {
|
||||||
|
background-color: var(--accent-primary);
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-number.day-other-month {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-amount {
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-amount.amount-over {
|
||||||
|
color: var(--accent-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 统计卡片 ========== */
|
||||||
|
.daily-stats {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-title {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: var(--font-xl);
|
||||||
|
font-weight: var(--font-bold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-date {
|
||||||
|
font-size: var(--font-md);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-label {
|
||||||
|
font-size: var(--font-md);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-badge {
|
||||||
|
padding: 6px 10px;
|
||||||
|
background-color: var(--accent-warning-bg);
|
||||||
|
color: var(--accent-warning);
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-value {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: var(--font-3xl);
|
||||||
|
font-weight: var(--font-extrabold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 交易列表 ========== */
|
||||||
|
.transactions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txn-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txn-title {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: var(--font-xl);
|
||||||
|
font-weight: var(--font-bold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txn-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txn-badge {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: var(--font-base);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-success {
|
||||||
|
background-color: var(--accent-success-bg);
|
||||||
|
color: var(--accent-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.smart-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background-color: var(--accent-info-bg);
|
||||||
|
color: var(--accent-info);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: var(--font-base);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smart-btn:active {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txn-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txn-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 16px;
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txn-card:active {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txn-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txn-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txn-name {
|
||||||
|
font-size: var(--font-lg);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.txn-time {
|
||||||
|
font-size: var(--font-md);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.txn-amount {
|
||||||
|
font-size: var(--font-lg);
|
||||||
|
font-weight: var(--font-bold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部安全距离 */
|
||||||
|
.bottom-spacer {
|
||||||
|
height: calc(60px + env(safe-area-inset-bottom, 0px));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -51,7 +51,7 @@ public class TransactionRecordRepositoryTest : TransactionTestBase
|
|||||||
results.Should().HaveCount(2);
|
results.Should().HaveCount(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task QueryAsync_按年月筛选_Test()
|
public async Task QueryAsync_按年月筛选_Test()
|
||||||
{
|
{
|
||||||
await _repository.AddAsync(CreateExpense(100, new DateTime(2023, 1, 15)));
|
await _repository.AddAsync(CreateExpense(100, new DateTime(2023, 1, 15)));
|
||||||
|
|||||||
@@ -192,13 +192,13 @@ public class TransactionStatisticsServiceTest : BaseTest
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Mock filtering by Type
|
// Mock filtering by Type
|
||||||
_transactionRepository.QueryAsync(
|
_transactionRepository.QueryAsync(
|
||||||
Arg.Any<int?>(), Arg.Any<int?>(), Arg.Any<DateTime?>(), Arg.Any<DateTime?>(), Arg.Any<TransactionType?>(), Arg.Any<string[]>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<bool>()
|
Arg.Any<int?>(), Arg.Any<int?>(), Arg.Any<DateTime?>(), Arg.Any<DateTime?>(), Arg.Any<TransactionType?>(), Arg.Any<string[]>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<bool>()
|
||||||
).Returns(callInfo =>
|
).Returns(callInfo =>
|
||||||
{
|
{
|
||||||
var type = callInfo.ArgAt<TransactionType?>(4);
|
var type = callInfo.ArgAt<TransactionType?>(4);
|
||||||
return testData.Where(t => !type.HasValue || t.Type == type).ToList();
|
return testData.Where(t => !type.HasValue || t.Type == type).ToList();
|
||||||
});
|
});
|
||||||
|
|
||||||
var result = await _service.GetCategoryStatisticsAsync(year, month, TransactionType.Expense);
|
var result = await _service.GetCategoryStatisticsAsync(year, month, TransactionType.Expense);
|
||||||
|
|
||||||
|
|||||||
@@ -9,15 +9,15 @@ public class LogController(ILogger<LogController> logger) : ControllerBase
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取日志列表(分页)
|
/// 获取日志列表(分页)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<PagedResponse<LogEntry>> GetListAsync(
|
public async Task<PagedResponse<LogEntry>> GetListAsync(
|
||||||
[FromQuery] int pageIndex = 1,
|
[FromQuery] int pageIndex = 1,
|
||||||
[FromQuery] int pageSize = 50,
|
[FromQuery] int pageSize = 50,
|
||||||
[FromQuery] string? searchKeyword = null,
|
[FromQuery] string? searchKeyword = null,
|
||||||
[FromQuery] string? logLevel = null,
|
[FromQuery] string? logLevel = null,
|
||||||
[FromQuery] string? date = null,
|
[FromQuery] string? date = null,
|
||||||
[FromQuery] string? className = null
|
[FromQuery] string? className = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -221,7 +221,7 @@ public async Task<PagedResponse<LogEntry>> GetListAsync(
|
|||||||
logger.LogError(ex, "获取类名列表失败");
|
logger.LogError(ex, "获取类名列表失败");
|
||||||
return $"获取类名列表失败: {ex.Message}".Fail<string[]>();
|
return $"获取类名列表失败: {ex.Message}".Fail<string[]>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 合并多行日志(已废弃,现在在流式读取中处理)
|
/// 合并多行日志(已废弃,现在在流式读取中处理)
|
||||||
@@ -385,27 +385,27 @@ public async Task<PagedResponse<LogEntry>> GetListAsync(
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 读取日志
|
/// 读取日志
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<(List<LogEntry> entries, int total)> ReadLogsAsync(
|
private async Task<(List<LogEntry> entries, int total)> ReadLogsAsync(
|
||||||
string path,
|
string path,
|
||||||
int pageIndex,
|
int pageIndex,
|
||||||
int pageSize,
|
int pageSize,
|
||||||
string? searchKeyword,
|
string? searchKeyword,
|
||||||
string? logLevel,
|
string? logLevel,
|
||||||
string? className)
|
string? className)
|
||||||
{
|
|
||||||
var allLines = await ReadAllLinesAsync(path);
|
|
||||||
|
|
||||||
var merged = MergeMultiLineLog(allLines);
|
|
||||||
|
|
||||||
var parsed = new List<LogEntry>();
|
|
||||||
foreach (var line in merged)
|
|
||||||
{
|
{
|
||||||
var entry = ParseLogLine(line);
|
var allLines = await ReadAllLinesAsync(path);
|
||||||
if (entry != null && PassFilter(entry, searchKeyword, logLevel, className))
|
|
||||||
|
var merged = MergeMultiLineLog(allLines);
|
||||||
|
|
||||||
|
var parsed = new List<LogEntry>();
|
||||||
|
foreach (var line in merged)
|
||||||
{
|
{
|
||||||
parsed.Add(entry);
|
var entry = ParseLogLine(line);
|
||||||
|
if (entry != null && PassFilter(entry, searchKeyword, logLevel, className))
|
||||||
|
{
|
||||||
|
parsed.Add(entry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
parsed.Reverse();
|
parsed.Reverse();
|
||||||
|
|
||||||
@@ -419,28 +419,28 @@ private async Task<(List<LogEntry> entries, int total)> ReadLogsAsync(
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 检查日志条目是否通过过滤条件
|
/// 检查日志条目是否通过过滤条件
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private bool PassFilter(LogEntry logEntry, string? searchKeyword, string? logLevel, string? className)
|
private bool PassFilter(LogEntry logEntry, string? searchKeyword, string? logLevel, string? className)
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(searchKeyword) &&
|
|
||||||
!logEntry.Message.Contains(searchKeyword, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
return false;
|
if (!string.IsNullOrEmpty(searchKeyword) &&
|
||||||
}
|
!logEntry.Message.Contains(searchKeyword, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(logLevel) &&
|
if (!string.IsNullOrEmpty(logLevel) &&
|
||||||
!logEntry.Level.Equals(logLevel, StringComparison.OrdinalIgnoreCase))
|
!logEntry.Level.Equals(logLevel, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(className) &&
|
if (!string.IsNullOrEmpty(className) &&
|
||||||
!logEntry.ClassName.Equals(className, StringComparison.OrdinalIgnoreCase))
|
!logEntry.ClassName.Equals(className, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 读取文件所有行(支持共享读取)
|
/// 读取文件所有行(支持共享读取)
|
||||||
|
|||||||
@@ -38,18 +38,18 @@ public class TransactionRecordController(
|
|||||||
: classify.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
: classify.Split(',', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
TransactionType? transactionType = type.HasValue ? (TransactionType)type.Value : null;
|
TransactionType? transactionType = type.HasValue ? (TransactionType)type.Value : null;
|
||||||
var list = await transactionRepository.QueryAsync(
|
var list = await transactionRepository.QueryAsync(
|
||||||
year: year,
|
year: year,
|
||||||
month: month,
|
month: month,
|
||||||
startDate: startDate,
|
startDate: startDate,
|
||||||
endDate: endDate,
|
endDate: endDate,
|
||||||
type: transactionType,
|
type: transactionType,
|
||||||
classifies: classifies,
|
classifies: classifies,
|
||||||
searchKeyword: searchKeyword,
|
searchKeyword: searchKeyword,
|
||||||
reason: reason,
|
reason: reason,
|
||||||
pageIndex: pageIndex,
|
pageIndex: pageIndex,
|
||||||
pageSize: pageSize,
|
pageSize: pageSize,
|
||||||
sortByAmount: sortByAmount);
|
sortByAmount: sortByAmount);
|
||||||
var total = await transactionRepository.CountAsync(
|
var total = await transactionRepository.CountAsync(
|
||||||
year: year,
|
year: year,
|
||||||
month: month,
|
month: month,
|
||||||
@@ -214,6 +214,12 @@ var list = await transactionRepository.QueryAsync(
|
|||||||
transaction.Type = dto.Type;
|
transaction.Type = dto.Type;
|
||||||
transaction.Classify = dto.Classify ?? string.Empty;
|
transaction.Classify = dto.Classify ?? string.Empty;
|
||||||
|
|
||||||
|
// 更新交易时间
|
||||||
|
if (!string.IsNullOrEmpty(dto.OccurredAt) && DateTime.TryParse(dto.OccurredAt, out var occurredAt))
|
||||||
|
{
|
||||||
|
transaction.OccurredAt = occurredAt;
|
||||||
|
}
|
||||||
|
|
||||||
// 清除待确认状态
|
// 清除待确认状态
|
||||||
transaction.UnconfirmedClassify = null;
|
transaction.UnconfirmedClassify = null;
|
||||||
transaction.UnconfirmedType = null;
|
transaction.UnconfirmedType = null;
|
||||||
@@ -693,7 +699,7 @@ var list = await transactionRepository.QueryAsync(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task WriteEventAsync(string eventType, string data)
|
private async Task WriteEventAsync(string eventType, string data)
|
||||||
{
|
{
|
||||||
var message = $"event: {eventType}\ndata: {data}\n\n";
|
var message = $"event: {eventType}\ndata: {data}\n\n";
|
||||||
await Response.WriteAsync(message);
|
await Response.WriteAsync(message);
|
||||||
@@ -728,7 +734,8 @@ public record UpdateTransactionDto(
|
|||||||
decimal Amount,
|
decimal Amount,
|
||||||
decimal Balance,
|
decimal Balance,
|
||||||
TransactionType Type,
|
TransactionType Type,
|
||||||
string? Classify
|
string? Classify,
|
||||||
|
string? OccurredAt = null
|
||||||
);
|
);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user