重构预算管理模块,添加预算记录和服务,更新相关API,优化预算统计逻辑
This commit is contained in:
83
Entity/BudgetRecord.cs
Normal file
83
Entity/BudgetRecord.cs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
namespace Entity;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 预算管理
|
||||||
|
/// </summary>
|
||||||
|
public class BudgetRecord : BaseEntity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 预算名称
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 统计周期
|
||||||
|
/// </summary>
|
||||||
|
public BudgetPeriodType Type { get; set; } = BudgetPeriodType.Month;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 预算金额
|
||||||
|
/// </summary>
|
||||||
|
public decimal Limit { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 预算类别
|
||||||
|
/// </summary>
|
||||||
|
public BudgetCategory Category { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 相关分类 (逗号分隔的分类名称)
|
||||||
|
/// </summary>
|
||||||
|
public string SelectedCategories { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否停止
|
||||||
|
/// </summary>
|
||||||
|
public bool IsStopped { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 开始日期
|
||||||
|
/// </summary>
|
||||||
|
public DateTime StartDate { get; set; } = DateTime.Now;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 上次同步时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? LastSync { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum BudgetPeriodType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 周
|
||||||
|
/// </summary>
|
||||||
|
Week,
|
||||||
|
/// <summary>
|
||||||
|
/// 月
|
||||||
|
/// </summary>
|
||||||
|
Month,
|
||||||
|
/// <summary>
|
||||||
|
/// 年
|
||||||
|
/// </summary>
|
||||||
|
Year,
|
||||||
|
/// <summary>
|
||||||
|
/// 长期
|
||||||
|
/// </summary>
|
||||||
|
Longterm
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum BudgetCategory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 支出
|
||||||
|
/// </summary>
|
||||||
|
Expense = 0,
|
||||||
|
/// <summary>
|
||||||
|
/// 收入
|
||||||
|
/// </summary>
|
||||||
|
Income = 1,
|
||||||
|
/// <summary>
|
||||||
|
/// 存款
|
||||||
|
/// </summary>
|
||||||
|
Savings = 2
|
||||||
|
}
|
||||||
36
Repository/BudgetRepository.cs
Normal file
36
Repository/BudgetRepository.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
namespace Repository;
|
||||||
|
|
||||||
|
public interface IBudgetRepository : IBaseRepository<BudgetRecord>
|
||||||
|
{
|
||||||
|
Task<decimal> GetCurrentAmountAsync(BudgetRecord budget, DateTime startDate, DateTime endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BudgetRepository(IFreeSql freeSql) : BaseRepository<BudgetRecord>(freeSql), IBudgetRepository
|
||||||
|
{
|
||||||
|
public async Task<decimal> GetCurrentAmountAsync(BudgetRecord budget, DateTime startDate, DateTime endDate)
|
||||||
|
{
|
||||||
|
var query = FreeSql.Select<TransactionRecord>()
|
||||||
|
.Where(t => t.OccurredAt >= startDate && t.OccurredAt <= endDate);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(budget.SelectedCategories))
|
||||||
|
{
|
||||||
|
var categoryList = budget.SelectedCategories.Split(',');
|
||||||
|
query = query.Where(t => categoryList.Contains(t.Classify));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (budget.Category == BudgetCategory.Expense)
|
||||||
|
{
|
||||||
|
query = query.Where(t => t.Type == TransactionType.Expense);
|
||||||
|
}
|
||||||
|
else if (budget.Category == BudgetCategory.Income)
|
||||||
|
{
|
||||||
|
query = query.Where(t => t.Type == TransactionType.Income);
|
||||||
|
}
|
||||||
|
else if (budget.Category == BudgetCategory.Savings)
|
||||||
|
{
|
||||||
|
query = query.Where(t => t.Type == TransactionType.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await query.SumAsync(t => t.Amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
94
Service/BudgetService.cs
Normal file
94
Service/BudgetService.cs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
namespace Service;
|
||||||
|
|
||||||
|
public interface IBudgetService
|
||||||
|
{
|
||||||
|
Task<List<BudgetRecord>> GetAllAsync();
|
||||||
|
Task<BudgetRecord?> GetByIdAsync(long id);
|
||||||
|
Task<bool> AddAsync(BudgetRecord budget);
|
||||||
|
Task<bool> DeleteAsync(long id);
|
||||||
|
Task<bool> UpdateAsync(BudgetRecord budget);
|
||||||
|
Task<decimal> CalculateCurrentAmountAsync(BudgetRecord budget, DateTime? now = null);
|
||||||
|
Task<bool> ToggleStopAsync(long id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BudgetService(
|
||||||
|
IBudgetRepository budgetRepository,
|
||||||
|
ILogger<BudgetService> logger) : IBudgetService
|
||||||
|
{
|
||||||
|
public async Task<List<BudgetRecord>> GetAllAsync()
|
||||||
|
{
|
||||||
|
var list = await budgetRepository.GetAllAsync();
|
||||||
|
return list.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<BudgetRecord?> GetByIdAsync(long id)
|
||||||
|
{
|
||||||
|
return await budgetRepository.GetByIdAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> AddAsync(BudgetRecord budget)
|
||||||
|
{
|
||||||
|
return await budgetRepository.AddAsync(budget);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteAsync(long id)
|
||||||
|
{
|
||||||
|
return await budgetRepository.DeleteAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UpdateAsync(BudgetRecord budget)
|
||||||
|
{
|
||||||
|
return await budgetRepository.UpdateAsync(budget);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ToggleStopAsync(long id)
|
||||||
|
{
|
||||||
|
var budget = await budgetRepository.GetByIdAsync(id);
|
||||||
|
if (budget == null) return false;
|
||||||
|
budget.IsStopped = !budget.IsStopped;
|
||||||
|
return await budgetRepository.UpdateAsync(budget);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<decimal> CalculateCurrentAmountAsync(BudgetRecord budget, DateTime? now = null)
|
||||||
|
{
|
||||||
|
if (budget.IsStopped) return 0;
|
||||||
|
|
||||||
|
var referenceDate = now ?? DateTime.Now;
|
||||||
|
var (startDate, endDate) = GetPeriodRange(budget.StartDate, budget.Type, referenceDate);
|
||||||
|
|
||||||
|
return await budgetRepository.GetCurrentAmountAsync(budget, startDate, endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (DateTime start, DateTime end) GetPeriodRange(DateTime startDate, BudgetPeriodType type, DateTime referenceDate)
|
||||||
|
{
|
||||||
|
if (type == BudgetPeriodType.Longterm) return (startDate, DateTime.MaxValue);
|
||||||
|
|
||||||
|
DateTime start;
|
||||||
|
DateTime end;
|
||||||
|
|
||||||
|
if (type == BudgetPeriodType.Week)
|
||||||
|
{
|
||||||
|
var daysFromStart = (referenceDate.Date - startDate.Date).Days;
|
||||||
|
var weeksFromStart = daysFromStart / 7;
|
||||||
|
start = startDate.Date.AddDays(weeksFromStart * 7);
|
||||||
|
end = start.AddDays(6).AddHours(23).AddMinutes(59).AddSeconds(59);
|
||||||
|
}
|
||||||
|
else if (type == BudgetPeriodType.Month)
|
||||||
|
{
|
||||||
|
start = new DateTime(referenceDate.Year, referenceDate.Month, 1);
|
||||||
|
end = start.AddMonths(1).AddDays(-1).AddHours(23).AddMinutes(59).AddSeconds(59);
|
||||||
|
}
|
||||||
|
else if (type == BudgetPeriodType.Year)
|
||||||
|
{
|
||||||
|
start = new DateTime(referenceDate.Year, 1, 1);
|
||||||
|
end = new DateTime(referenceDate.Year, 12, 31).AddHours(23).AddMinutes(59).AddSeconds(59);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
start = startDate;
|
||||||
|
end = DateTime.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (start, end);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,9 +9,6 @@
|
|||||||
<van-tabbar-item name="statistics" icon="chart-trending-o" to="/" @click="handleTabClick('/statistics')">
|
<van-tabbar-item name="statistics" icon="chart-trending-o" to="/" @click="handleTabClick('/statistics')">
|
||||||
统计
|
统计
|
||||||
</van-tabbar-item>
|
</van-tabbar-item>
|
||||||
<van-tabbar-item name="budget" icon="bill-o" to="/budget" @click="handleTabClick('/budget')">
|
|
||||||
预算
|
|
||||||
</van-tabbar-item>
|
|
||||||
<van-tabbar-item
|
<van-tabbar-item
|
||||||
name="balance"
|
name="balance"
|
||||||
icon="balance-list"
|
icon="balance-list"
|
||||||
@@ -21,6 +18,9 @@
|
|||||||
>
|
>
|
||||||
账单
|
账单
|
||||||
</van-tabbar-item>
|
</van-tabbar-item>
|
||||||
|
<van-tabbar-item name="budget" icon="bill-o" to="/budget" @click="handleTabClick('/budget')">
|
||||||
|
预算
|
||||||
|
</van-tabbar-item>
|
||||||
<van-tabbar-item name="setting" icon="setting" to="/setting">
|
<van-tabbar-item name="setting" icon="setting" to="/setting">
|
||||||
设置
|
设置
|
||||||
</van-tabbar-item>
|
</van-tabbar-item>
|
||||||
|
|||||||
74
Web/src/api/budget.js
Normal file
74
Web/src/api/budget.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import request from './request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取预算列表
|
||||||
|
* @param {string} referenceDate 参考日期 (可选)
|
||||||
|
*/
|
||||||
|
export function getBudgetList(referenceDate) {
|
||||||
|
return request({
|
||||||
|
url: '/Budget/GetList',
|
||||||
|
method: 'get',
|
||||||
|
params: { referenceDate }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个预算统计
|
||||||
|
* @param {number} id 预算ID
|
||||||
|
* @param {string} referenceDate 参考日期
|
||||||
|
*/
|
||||||
|
export function getBudgetStatistics(id, referenceDate) {
|
||||||
|
return request({
|
||||||
|
url: '/Budget/GetStatistics',
|
||||||
|
method: 'get',
|
||||||
|
params: { id, referenceDate }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建预算
|
||||||
|
* @param {object} data 预算数据
|
||||||
|
*/
|
||||||
|
export function createBudget(data) {
|
||||||
|
return request({
|
||||||
|
url: '/Budget/Create',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除预算
|
||||||
|
* @param {number} id 预算ID
|
||||||
|
*/
|
||||||
|
export function deleteBudget(id) {
|
||||||
|
return request({
|
||||||
|
url: `/Budget/DeleteById/${id}`,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换预算状态 (停止/恢复)
|
||||||
|
* @param {number} id 预算ID
|
||||||
|
*/
|
||||||
|
export function toggleStopBudget(id) {
|
||||||
|
return request({
|
||||||
|
url: '/Budget/ToggleStop',
|
||||||
|
method: 'post',
|
||||||
|
params: { id }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步预算进度
|
||||||
|
* @param {number} id 预算ID
|
||||||
|
* @param {string} referenceDate 参考日期 (可选)
|
||||||
|
*/
|
||||||
|
export function syncBudget(id, referenceDate) {
|
||||||
|
return request({
|
||||||
|
url: '/Budget/Sync',
|
||||||
|
method: 'post',
|
||||||
|
params: { id, referenceDate }
|
||||||
|
})
|
||||||
|
}
|
||||||
27
Web/src/constants/enums.js
Normal file
27
Web/src/constants/enums.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* 预算周期类型
|
||||||
|
*/
|
||||||
|
export const BudgetPeriodType = {
|
||||||
|
Week: 0,
|
||||||
|
Month: 1,
|
||||||
|
Year: 2,
|
||||||
|
Longterm: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预算类别
|
||||||
|
*/
|
||||||
|
export const BudgetCategory = {
|
||||||
|
Expense: 0,
|
||||||
|
Income: 1,
|
||||||
|
Savings: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 交易类型 (与后端 TransactionType 对应)
|
||||||
|
*/
|
||||||
|
export const TransactionType = {
|
||||||
|
Expense: 0,
|
||||||
|
Income: 1,
|
||||||
|
None: 2
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<van-tabs v-model:active="activeTab" sticky offset-top="46" type="card">
|
<van-tabs v-model:active="activeTab" sticky offset-top="46" type="card">
|
||||||
<van-tab title="支出" name="expense">
|
<van-tab title="支出" :name="BudgetCategory.Expense">
|
||||||
<div class="budget-list">
|
<div class="budget-list">
|
||||||
<template v-if="expenseBudgets?.length > 0">
|
<template v-if="expenseBudgets?.length > 0">
|
||||||
<van-swipe-cell v-for="budget in expenseBudgets" :key="budget.id">
|
<van-swipe-cell v-for="budget in expenseBudgets" :key="budget.id">
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</van-tab>
|
</van-tab>
|
||||||
|
|
||||||
<van-tab title="收入" name="income">
|
<van-tab title="收入" :name="BudgetCategory.Income">
|
||||||
<div class="budget-list">
|
<div class="budget-list">
|
||||||
<template v-if="incomeBudgets?.length > 0">
|
<template v-if="incomeBudgets?.length > 0">
|
||||||
<van-swipe-cell v-for="budget in incomeBudgets" :key="budget.id">
|
<van-swipe-cell v-for="budget in incomeBudgets" :key="budget.id">
|
||||||
@@ -146,7 +146,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</van-tab>
|
</van-tab>
|
||||||
|
|
||||||
<van-tab title="存款" name="savings">
|
<van-tab title="存款" :name="BudgetCategory.Savings">
|
||||||
<div class="budget-list">
|
<div class="budget-list">
|
||||||
<template v-if="savingsBudgets?.length > 0">
|
<template v-if="savingsBudgets?.length > 0">
|
||||||
<van-swipe-cell v-for="budget in savingsBudgets" :key="budget.id">
|
<van-swipe-cell v-for="budget in savingsBudgets" :key="budget.id">
|
||||||
@@ -222,7 +222,7 @@
|
|||||||
<!-- 添加预算弹窗 -->
|
<!-- 添加预算弹窗 -->
|
||||||
<PopupContainer v-model="showAddPopup" title="新增预算" height="70%">
|
<PopupContainer v-model="showAddPopup" title="新增预算" height="70%">
|
||||||
<div class="add-budget-form">
|
<div class="add-budget-form">
|
||||||
<van-form @submit="onSubmit">
|
<van-form>
|
||||||
<van-cell-group inset>
|
<van-cell-group inset>
|
||||||
<van-field
|
<van-field
|
||||||
v-model="form.name"
|
v-model="form.name"
|
||||||
@@ -234,10 +234,10 @@
|
|||||||
<van-field name="type" label="统计周期">
|
<van-field name="type" label="统计周期">
|
||||||
<template #input>
|
<template #input>
|
||||||
<van-radio-group v-model="form.type" direction="horizontal">
|
<van-radio-group v-model="form.type" direction="horizontal">
|
||||||
<van-radio name="week">周</van-radio>
|
<van-radio :name="BudgetPeriodType.Week">周</van-radio>
|
||||||
<van-radio name="month">月</van-radio>
|
<van-radio :name="BudgetPeriodType.Month">月</van-radio>
|
||||||
<van-radio name="year">年</van-radio>
|
<van-radio :name="BudgetPeriodType.Year">年</van-radio>
|
||||||
<van-radio name="longterm">长期</van-radio>
|
<van-radio :name="BudgetPeriodType.Longterm">长期</van-radio>
|
||||||
</van-radio-group>
|
</van-radio-group>
|
||||||
</template>
|
</template>
|
||||||
</van-field>
|
</van-field>
|
||||||
@@ -256,9 +256,9 @@
|
|||||||
<van-field name="category" label="类型">
|
<van-field name="category" label="类型">
|
||||||
<template #input>
|
<template #input>
|
||||||
<van-radio-group v-model="form.category" direction="horizontal">
|
<van-radio-group v-model="form.category" direction="horizontal">
|
||||||
<van-radio name="expense">支出</van-radio>
|
<van-radio :name="BudgetCategory.Expense">支出</van-radio>
|
||||||
<van-radio name="income">收入</van-radio>
|
<van-radio :name="BudgetCategory.Income">收入</van-radio>
|
||||||
<van-radio name="savings">存款</van-radio>
|
<van-radio :name="BudgetCategory.Savings">存款</van-radio>
|
||||||
</van-radio-group>
|
</van-radio-group>
|
||||||
</template>
|
</template>
|
||||||
</van-field>
|
</van-field>
|
||||||
@@ -296,137 +296,41 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</van-cell-group>
|
</van-cell-group>
|
||||||
<div style="margin: 32px 16px;">
|
|
||||||
<van-button round block type="primary" native-type="submit">
|
|
||||||
保 存
|
|
||||||
</van-button>
|
|
||||||
</div>
|
|
||||||
</van-form>
|
</van-form>
|
||||||
</div>
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<van-button block type="primary" @click="onSubmit">保存预算</van-button>
|
||||||
|
</template>
|
||||||
</PopupContainer>
|
</PopupContainer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||||
import { showToast, showConfirmDialog } from 'vant'
|
import { showToast, showConfirmDialog } from 'vant'
|
||||||
import PopupContainer from '@/components/PopupContainer.vue'
|
|
||||||
import { getCategoryList } from '@/api/transactionCategory'
|
import { getCategoryList } from '@/api/transactionCategory'
|
||||||
|
import { getBudgetList, createBudget, deleteBudget, toggleStopBudget, syncBudget, getBudgetStatistics } from '@/api/budget'
|
||||||
|
import { BudgetPeriodType, BudgetCategory } from '@/constants/enums'
|
||||||
|
import PopupContainer from '@/components/PopupContainer.vue'
|
||||||
|
|
||||||
const activeTab = ref('expense')
|
const activeTab = ref(BudgetCategory.Expense)
|
||||||
const showAddPopup = ref(false)
|
const showAddPopup = ref(false)
|
||||||
const categories = ref([])
|
const categories = ref([])
|
||||||
|
|
||||||
// 模拟数据 (提前定义以防止模板渲染时 undefined)
|
const expenseBudgets = ref([])
|
||||||
const expenseBudgets = ref([
|
const incomeBudgets = ref([])
|
||||||
{
|
const savingsBudgets = ref([])
|
||||||
id: 1,
|
|
||||||
name: '总支出预算',
|
|
||||||
type: 'month',
|
|
||||||
limit: 5000,
|
|
||||||
current: 3250.5,
|
|
||||||
isStopped: false,
|
|
||||||
startDate: '2026-01-01',
|
|
||||||
period: '2026-01-01 ~ 2026-01-31',
|
|
||||||
lastSync: '2026-01-06 10:00',
|
|
||||||
syncing: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: '餐饮美食',
|
|
||||||
type: 'month',
|
|
||||||
limit: 1500,
|
|
||||||
current: 1420,
|
|
||||||
isStopped: false,
|
|
||||||
startDate: '2026-01-01',
|
|
||||||
period: '2026-01-01 ~ 2026-01-31',
|
|
||||||
lastSync: '2026-01-06 09:30',
|
|
||||||
syncing: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: '年度旅游',
|
|
||||||
type: 'year',
|
|
||||||
limit: 10000,
|
|
||||||
current: 0,
|
|
||||||
isStopped: true,
|
|
||||||
startDate: '2026-01-01',
|
|
||||||
period: '2026-01-01 ~ 2026-12-31',
|
|
||||||
lastSync: '2026-01-01 00:00',
|
|
||||||
syncing: false
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
const incomeBudgets = ref([
|
|
||||||
{
|
|
||||||
id: 101,
|
|
||||||
name: '月度薪资',
|
|
||||||
type: 'month',
|
|
||||||
limit: 10000,
|
|
||||||
current: 10000,
|
|
||||||
isStopped: false,
|
|
||||||
startDate: '2026-01-01',
|
|
||||||
period: '2026-01-01 ~ 2026-01-31',
|
|
||||||
lastSync: '2026-01-06 10:05',
|
|
||||||
syncing: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 102,
|
|
||||||
name: '理财收益',
|
|
||||||
type: 'year',
|
|
||||||
limit: 2000,
|
|
||||||
current: 450.8,
|
|
||||||
isStopped: false,
|
|
||||||
startDate: '2026-01-01',
|
|
||||||
period: '2026-01-01 ~ 2026-12-31',
|
|
||||||
lastSync: '2026-01-06 10:05',
|
|
||||||
syncing: false
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
const savingsBudgets = ref([
|
|
||||||
{
|
|
||||||
id: 201,
|
|
||||||
name: '买房基金',
|
|
||||||
type: 'year',
|
|
||||||
limit: 500000,
|
|
||||||
current: 125000,
|
|
||||||
isStopped: false,
|
|
||||||
startDate: '2025-01-01',
|
|
||||||
period: '2025-01-01 ~ 2030-12-31',
|
|
||||||
lastSync: '2026-01-06 11:00',
|
|
||||||
syncing: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 202,
|
|
||||||
name: '养老金',
|
|
||||||
type: 'year',
|
|
||||||
limit: 1000000,
|
|
||||||
current: 50000,
|
|
||||||
isStopped: false,
|
|
||||||
startDate: '2026-01-01',
|
|
||||||
period: '长期',
|
|
||||||
lastSync: '2026-01-06 11:00',
|
|
||||||
syncing: false
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
type: 'month',
|
type: BudgetPeriodType.Month,
|
||||||
category: 'expense',
|
category: BudgetCategory.Expense,
|
||||||
limit: '',
|
limit: '',
|
||||||
selectedCategories: []
|
selectedCategories: []
|
||||||
})
|
})
|
||||||
|
|
||||||
const categoryTypeMap = {
|
|
||||||
expense: 0,
|
|
||||||
income: 1,
|
|
||||||
savings: 2
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredCategories = computed(() => {
|
const filteredCategories = computed(() => {
|
||||||
const targetType = categoryTypeMap[form.category]
|
const targetType = form.category === BudgetCategory.Expense ? 0 : (form.category === BudgetCategory.Income ? 1 : 2)
|
||||||
return categories.value.filter(c => c.type === targetType)
|
return categories.value.filter(c => c.type === targetType)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -452,14 +356,31 @@ const toggleAll = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
const fetchBudgetList = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await getCategoryList()
|
const res = await getBudgetList()
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
categories.value = res.data || []
|
const data = res.data || []
|
||||||
|
expenseBudgets.value = data.filter(b => b.category === BudgetCategory.Expense)
|
||||||
|
incomeBudgets.value = data.filter(b => b.category === BudgetCategory.Income)
|
||||||
|
savingsBudgets.value = data.filter(b => b.category === BudgetCategory.Savings)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('加载分类失败', err)
|
console.error('加载预算列表失败', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const [catRes] = await Promise.all([
|
||||||
|
getCategoryList(),
|
||||||
|
fetchBudgetList()
|
||||||
|
])
|
||||||
|
if (catRes.success) {
|
||||||
|
categories.value = catRes.data || []
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取初始化数据失败', err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -468,14 +389,15 @@ watch(() => form.category, () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const formatMoney = (val) => {
|
const formatMoney = (val) => {
|
||||||
return parseFloat(val).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
return parseFloat(val || 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPeriodLabel = (type) => {
|
const getPeriodLabel = (type) => {
|
||||||
const map = {
|
const map = {
|
||||||
week: '本周',
|
[BudgetPeriodType.Week]: '本周',
|
||||||
month: '本月',
|
[BudgetPeriodType.Month]: '本月',
|
||||||
year: '本年'
|
[BudgetPeriodType.Year]: '本年',
|
||||||
|
[BudgetPeriodType.Longterm]: '长期'
|
||||||
}
|
}
|
||||||
return map[type] || '周期'
|
return map[type] || '周期'
|
||||||
}
|
}
|
||||||
@@ -493,113 +415,109 @@ const getIncomeProgressColor = (budget) => {
|
|||||||
return '#1989fa' // 蓝色
|
return '#1989fa' // 蓝色
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPeriodRange = (startDate, type) => {
|
const refDateMap = {}
|
||||||
if (!startDate || startDate === '长期') return startDate
|
|
||||||
const start = new Date(startDate)
|
|
||||||
let end = new Date(startDate)
|
|
||||||
|
|
||||||
if (type === 'week') {
|
const handleSwitchPeriod = async (budget, direction) => {
|
||||||
end.setDate(start.getDate() + 6)
|
if (budget.type === BudgetPeriodType.Longterm) {
|
||||||
} else if (type === 'month') {
|
showToast('长期预算不支持切换周期')
|
||||||
end = new Date(start.getFullYear(), start.getMonth() + 1, 0)
|
return
|
||||||
} else if (type === 'year') {
|
|
||||||
end = new Date(start.getFullYear(), 11, 31)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const format = (d) => d.toISOString().split('T')[0]
|
// 获取或初始化该预算的参考日期
|
||||||
return `${format(start)} ~ ${format(end)}`
|
let currentRefDate = refDateMap[budget.id] || new Date()
|
||||||
}
|
const date = new Date(currentRefDate)
|
||||||
|
|
||||||
const handleSwitchPeriod = (budget, direction) => {
|
if (budget.type === BudgetPeriodType.Week) {
|
||||||
if (!budget.startDate || budget.period === '长期') return
|
date.setDate(date.getDate() + direction * 7)
|
||||||
|
} else if (budget.type === BudgetPeriodType.Month) {
|
||||||
const currentStart = new Date(budget.startDate)
|
date.setMonth(date.getMonth() + direction)
|
||||||
if (budget.type === 'week') {
|
} else if (budget.type === BudgetPeriodType.Year) {
|
||||||
currentStart.setDate(currentStart.getDate() + direction * 7)
|
date.setFullYear(date.getFullYear() + direction)
|
||||||
} else if (budget.type === 'month') {
|
|
||||||
currentStart.setMonth(currentStart.getMonth() + direction)
|
|
||||||
currentStart.setDate(1)
|
|
||||||
} else if (budget.type === 'year') {
|
|
||||||
currentStart.setFullYear(currentStart.getFullYear() + direction)
|
|
||||||
currentStart.setMonth(0)
|
|
||||||
currentStart.setDate(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
budget.startDate = currentStart.toISOString().split('T')[0]
|
try {
|
||||||
budget.period = getPeriodRange(budget.startDate, budget.type)
|
const res = await getBudgetStatistics(budget.id, date.toISOString())
|
||||||
|
if (res.success) {
|
||||||
// 模拟数据更新
|
refDateMap[budget.id] = date
|
||||||
budget.current = Math.floor(Math.random() * budget.limit * 1.2 * 100) / 100
|
// 更新当前列表中的预算对象信息
|
||||||
budget.lastSync = new Date().toLocaleString()
|
Object.assign(budget, res.data)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showToast('加载历史统计失败')
|
||||||
|
console.error('加载预算历史统计失败', err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = (budget) => {
|
const handleDelete = (budget) => {
|
||||||
showConfirmDialog({
|
showConfirmDialog({
|
||||||
title: '确认删除',
|
title: '确认删除',
|
||||||
message: `确定要删除预算 "${budget.name}" 吗?`,
|
message: `确定要删除预算 "${budget.name}" 吗?`,
|
||||||
}).then(() => {
|
}).then(async () => {
|
||||||
expenseBudgets.value = expenseBudgets.value.filter(b => b.id !== budget.id)
|
try {
|
||||||
incomeBudgets.value = incomeBudgets.value.filter(b => b.id !== budget.id)
|
const res = await deleteBudget(budget.id)
|
||||||
savingsBudgets.value = savingsBudgets.value.filter(b => b.id !== budget.id)
|
if (res.success) {
|
||||||
showToast('已删除')
|
showToast('已删除')
|
||||||
|
delete refDateMap[budget.id]
|
||||||
|
fetchBudgetList()
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showToast('删除失败')
|
||||||
|
console.error('删除预算失败', err)
|
||||||
|
}
|
||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSync = (budget) => {
|
const handleSync = async (budget) => {
|
||||||
budget.syncing = true
|
budget.syncing = true
|
||||||
setTimeout(() => {
|
try {
|
||||||
|
const refDate = refDateMap[budget.id] ? refDateMap[budget.id].toISOString() : null
|
||||||
|
const res = await syncBudget(budget.id, refDate)
|
||||||
|
if (res.success) {
|
||||||
|
showToast('同步成功')
|
||||||
|
Object.assign(budget, res.data)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showToast('同步失败')
|
||||||
|
console.error('同步预算失败', err)
|
||||||
|
} finally {
|
||||||
budget.syncing = false
|
budget.syncing = false
|
||||||
budget.lastSync = new Date().toLocaleString()
|
}
|
||||||
showToast('同步成功')
|
|
||||||
}, 1000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleToggleStop = (budget) => {
|
const handleToggleStop = async (budget) => {
|
||||||
budget.isStopped = !budget.isStopped
|
try {
|
||||||
showToast(budget.isStopped ? '已停止' : '已恢复')
|
const res = await toggleStopBudget(budget.id)
|
||||||
|
if (res.success) {
|
||||||
|
showToast(budget.isStopped ? '已恢复' : '已停止')
|
||||||
|
// 切换停止状态后刷新列表
|
||||||
|
fetchBudgetList()
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showToast('操作失败')
|
||||||
|
console.error('切换预算状态失败', err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSubmit = () => {
|
const onSubmit = async () => {
|
||||||
const startDate = new Date()
|
try {
|
||||||
if (form.type === 'month') startDate.setDate(1)
|
const res = await createBudget({
|
||||||
if (form.type === 'year') {
|
...form,
|
||||||
startDate.setMonth(0)
|
limit: parseFloat(form.limit),
|
||||||
startDate.setDate(1)
|
categoryNames: form.selectedCategories
|
||||||
|
})
|
||||||
|
if (res.success) {
|
||||||
|
showToast('保存成功')
|
||||||
|
showAddPopup.value = false
|
||||||
|
fetchBudgetList()
|
||||||
|
// 重置表单
|
||||||
|
form.name = ''
|
||||||
|
form.limit = ''
|
||||||
|
form.selectedCategories = []
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showToast('保存失败')
|
||||||
|
console.error('保存预算失败', err)
|
||||||
}
|
}
|
||||||
const startDateStr = startDate.toISOString().split('T')[0]
|
|
||||||
|
|
||||||
const newBudget = {
|
|
||||||
id: Date.now(),
|
|
||||||
name: form.name,
|
|
||||||
type: form.type,
|
|
||||||
limit: parseFloat(form.limit),
|
|
||||||
categories: [...form.selectedCategories],
|
|
||||||
current: 0,
|
|
||||||
isStopped: false,
|
|
||||||
startDate: startDateStr,
|
|
||||||
period: getPeriodRange(startDateStr, form.type),
|
|
||||||
lastSync: '刚刚',
|
|
||||||
syncing: false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (form.category === 'expense') {
|
|
||||||
expenseBudgets.value.unshift(newBudget)
|
|
||||||
activeTab.value = 'expense'
|
|
||||||
} else if (form.category === 'income') {
|
|
||||||
incomeBudgets.value.unshift(newBudget)
|
|
||||||
activeTab.value = 'income'
|
|
||||||
} else {
|
|
||||||
savingsBudgets.value.unshift(newBudget)
|
|
||||||
activeTab.value = 'savings'
|
|
||||||
}
|
|
||||||
|
|
||||||
showAddPopup.value = false
|
|
||||||
showToast('添加成功')
|
|
||||||
|
|
||||||
// 重置表单
|
|
||||||
form.name = ''
|
|
||||||
form.limit = ''
|
|
||||||
form.selectedCategories = []
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
151
WebApi/Controllers/BudgetController.cs
Normal file
151
WebApi/Controllers/BudgetController.cs
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
namespace WebApi.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]/[action]")]
|
||||||
|
public class BudgetController(
|
||||||
|
IBudgetService budgetService,
|
||||||
|
ILogger<BudgetController> logger) : ControllerBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取预算列表
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<BaseResponse<List<BudgetDto>>> GetListAsync([FromQuery] DateTime? referenceDate = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var budgets = await budgetService.GetAllAsync();
|
||||||
|
var dtos = new List<BudgetDto>();
|
||||||
|
|
||||||
|
foreach (var budget in budgets)
|
||||||
|
{
|
||||||
|
var currentAmount = await budgetService.CalculateCurrentAmountAsync(budget, referenceDate);
|
||||||
|
dtos.Add(BudgetDto.FromEntity(budget, currentAmount, referenceDate));
|
||||||
|
}
|
||||||
|
|
||||||
|
return dtos.Ok();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "获取预算列表失败");
|
||||||
|
return $"获取预算列表失败: {ex.Message}".Fail<List<BudgetDto>>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取单个预算统计信息
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<BaseResponse<BudgetDto>> GetStatisticsAsync([FromQuery] long id, [FromQuery] DateTime? referenceDate = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var budget = await budgetService.GetByIdAsync(id);
|
||||||
|
if (budget == null) return "预算不存在".Fail<BudgetDto>();
|
||||||
|
|
||||||
|
var currentAmount = await budgetService.CalculateCurrentAmountAsync(budget, referenceDate);
|
||||||
|
return BudgetDto.FromEntity(budget, currentAmount, referenceDate).Ok();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "获取预算统计失败, Id: {Id}", id);
|
||||||
|
return $"获取预算统计失败: {ex.Message}".Fail<BudgetDto>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建预算
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<BaseResponse<long>> CreateAsync([FromBody] CreateBudgetDto dto)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var budget = new BudgetRecord
|
||||||
|
{
|
||||||
|
Name = dto.Name,
|
||||||
|
Type = dto.Type,
|
||||||
|
Limit = dto.Limit,
|
||||||
|
Category = dto.Category,
|
||||||
|
SelectedCategories = dto.SelectedCategories != null ? string.Join(",", dto.SelectedCategories) : string.Empty,
|
||||||
|
StartDate = dto.StartDate ?? DateTime.Now,
|
||||||
|
LastSync = DateTime.Now
|
||||||
|
};
|
||||||
|
|
||||||
|
var success = await budgetService.AddAsync(budget);
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
return budget.Id.Ok();
|
||||||
|
}
|
||||||
|
return "创建预算失败".Fail<long>();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "创建预算失败");
|
||||||
|
return $"创建预算失败: {ex.Message}".Fail<long>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除预算
|
||||||
|
/// </summary>
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
public async Task<BaseResponse> DeleteByIdAsync(long id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var success = await budgetService.DeleteAsync(id);
|
||||||
|
return success ? BaseResponse.Done() : "删除预算失败".Fail();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "删除预算失败, Id: {Id}", id);
|
||||||
|
return $"删除预算失败: {ex.Message}".Fail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 切换预算暂停状态
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<BaseResponse> ToggleStopAsync([FromQuery] long id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var success = await budgetService.ToggleStopAsync(id);
|
||||||
|
return success ? BaseResponse.Done() : "操作失败".Fail();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "切换预算状态失败, Id: {Id}", id);
|
||||||
|
return $"操作失败: {ex.Message}".Fail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 同步预算数据
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<BaseResponse<BudgetDto>> SyncAsync([FromQuery] long id, [FromQuery] DateTime? referenceDate = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var budget = await budgetService.GetByIdAsync(id);
|
||||||
|
if (budget == null)
|
||||||
|
{
|
||||||
|
return "预算不存在".Fail<BudgetDto>();
|
||||||
|
}
|
||||||
|
|
||||||
|
budget.LastSync = DateTime.Now;
|
||||||
|
await budgetService.UpdateAsync(budget);
|
||||||
|
|
||||||
|
var currentAmount = await budgetService.CalculateCurrentAmountAsync(budget, referenceDate);
|
||||||
|
return BudgetDto.FromEntity(budget, currentAmount, referenceDate).Ok();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "同步预算失败, Id: {Id}", id);
|
||||||
|
return $"同步失败: {ex.Message}".Fail<BudgetDto>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
WebApi/Controllers/Dto/BudgetDto.cs
Normal file
49
WebApi/Controllers/Dto/BudgetDto.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
namespace WebApi.Controllers.Dto;
|
||||||
|
|
||||||
|
public class BudgetDto
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public BudgetPeriodType Type { get; set; }
|
||||||
|
public decimal Limit { get; set; }
|
||||||
|
public decimal Current { get; set; }
|
||||||
|
public BudgetCategory Category { get; set; }
|
||||||
|
public string[] SelectedCategories { get; set; } = Array.Empty<string>();
|
||||||
|
public bool IsStopped { get; set; }
|
||||||
|
public string StartDate { get; set; } = string.Empty;
|
||||||
|
public string Period { get; set; } = string.Empty;
|
||||||
|
public string LastSync { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public static BudgetDto FromEntity(BudgetRecord entity, decimal currentAmount = 0, DateTime? referenceDate = null)
|
||||||
|
{
|
||||||
|
var date = referenceDate ?? DateTime.Now;
|
||||||
|
var (start, end) = BudgetService.GetPeriodRange(entity.StartDate, entity.Type, date);
|
||||||
|
|
||||||
|
return new BudgetDto
|
||||||
|
{
|
||||||
|
Id = entity.Id,
|
||||||
|
Name = entity.Name,
|
||||||
|
Type = entity.Type,
|
||||||
|
Limit = entity.Limit,
|
||||||
|
Current = currentAmount,
|
||||||
|
Category = entity.Category,
|
||||||
|
SelectedCategories = string.IsNullOrEmpty(entity.SelectedCategories)
|
||||||
|
? Array.Empty<string>()
|
||||||
|
: entity.SelectedCategories.Split(','),
|
||||||
|
IsStopped = entity.IsStopped,
|
||||||
|
StartDate = entity.StartDate.ToString("yyyy-MM-dd"),
|
||||||
|
Period = entity.Type == BudgetPeriodType.Longterm ? "长期" : $"{start:yyyy-MM-dd} ~ {end:yyyy-MM-dd}",
|
||||||
|
LastSync = entity.LastSync?.ToString("yyyy-MM-dd HH:mm") ?? "未同步"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CreateBudgetDto
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public BudgetPeriodType Type { get; set; } = BudgetPeriodType.Month;
|
||||||
|
public decimal Limit { get; set; }
|
||||||
|
public BudgetCategory Category { get; set; }
|
||||||
|
public string[] SelectedCategories { get; set; } = Array.Empty<string>();
|
||||||
|
public DateTime? StartDate { get; set; }
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
using FreeSql;
|
using FreeSql;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|||||||
Reference in New Issue
Block a user