diff --git a/accounting/init.py b/accounting/init.py
index a47ac48..74fbbfc 100644
--- a/accounting/init.py
+++ b/accounting/init.py
@@ -7,6 +7,7 @@ from .accounting_config import Accounting
from .bill import write_bill
from .openaccount import openOwnerAccounts, openProviderAccounts, openResellerAccounts, openCustomerAccounts, openRetailRelationshipAccounts
from .getaccount import getAccountBalance, getCustomerBalance, getAccountByName, get_account_total_amount
+from .stats import get_accounting_stats
from .recharge import RechargeBiz, recharge_accounting
from .consume import consume_accounting
@@ -19,16 +20,10 @@ async def all_my_accounts(request):
b.id,
a.name,
b.balance_at,
-case when c.balance is null then 0.00 else c.balance end as balance
+b.balance
from
subject a,
- account b left join
- (
- select a.*
- from acc_balance a,
- (select accountid, max(acc_date) max_date from acc_balance group by accountid) b
- where a.accountid=b.accountid and a.acc_date=b.max_date
- ) c on c.accountid = b.id
+ account b
where b.subjectid = a.id
and b.orgid = ${orgid}$
"""
@@ -77,3 +72,4 @@ def load_accounting():
g.get_accdetail = get_accdetail
g.all_my_accounts = all_my_accounts
g.openRetailRelationshipAccounts = openRetailRelationshipAccounts
+ g.get_accounting_stats = get_accounting_stats
diff --git a/accounting/stats.py b/accounting/stats.py
new file mode 100644
index 0000000..27f5afc
--- /dev/null
+++ b/accounting/stats.py
@@ -0,0 +1,77 @@
+from appPublic.log import debug, exception
+from sqlor.dbpools import get_sor_context
+from datetime import datetime, timedelta
+
+async def get_accounting_stats(request):
+ """Get accounting statistics for the current user's organization"""
+ env = request._run_ns
+ userorgid = await env.get_userorgid()
+
+ now = datetime.now()
+ today_start = now.strftime('%Y-%m-%d')
+ month_start = now.strftime('%Y-%m-01')
+ tomorrow = (now + timedelta(days=1)).strftime('%Y-%m-%d')
+
+ stats = {
+ 'total_balance': 0,
+ 'today_amount': 0,
+ 'month_amount': 0,
+ 'account_count': 0
+ }
+
+ async with get_sor_context(request._run_ns, 'accounting') as sor:
+ # Total balance across all accounts
+ sql_balance = """
+ SELECT COALESCE(SUM(CASE WHEN balance_at = '1' THEN balance ELSE -balance END), 0) as total
+ FROM account
+ WHERE orgid = ${orgid}$
+ """
+ recs = await sor.sqlExe(sql_balance, {'orgid': userorgid})
+ if recs:
+ stats['total_balance'] = float(recs[0].total or 0)
+
+ # Account count
+ sql_count = """
+ SELECT COUNT(*) as cnt FROM account WHERE orgid = ${orgid}$
+ """
+ recs = await sor.sqlExe(sql_count, {'orgid': userorgid})
+ if recs:
+ stats['account_count'] = int(recs[0].cnt or 0)
+
+ # Today's consumption (acc_dir=1 means debit/consumption)
+ sql_today = """
+ SELECT COALESCE(SUM(amount), 0) as total
+ FROM acc_detail a
+ JOIN account b ON a.accountid = b.id
+ WHERE b.orgid = ${orgid}$
+ AND a.acc_dir = 1
+ AND a.acc_date >= ${from_date}$
+ AND a.acc_date < ${to_date}$
+ """
+ recs = await sor.sqlExe(sql_today, {
+ 'orgid': userorgid,
+ 'from_date': today_start,
+ 'to_date': tomorrow
+ })
+ if recs:
+ stats['today_amount'] = float(recs[0].total or 0)
+
+ # This month's consumption
+ sql_month = """
+ SELECT COALESCE(SUM(amount), 0) as total
+ FROM acc_detail a
+ JOIN account b ON a.accountid = b.id
+ WHERE b.orgid = ${orgid}$
+ AND a.acc_dir = 1
+ AND a.acc_date >= ${from_date}$
+ AND a.acc_date < ${to_date}$
+ """
+ recs = await sor.sqlExe(sql_month, {
+ 'orgid': userorgid,
+ 'from_date': month_start,
+ 'to_date': tomorrow
+ })
+ if recs:
+ stats['month_amount'] = float(recs[0].total or 0)
+
+ return stats
diff --git a/models/acc_balance.json b/models/acc_balance.json
new file mode 100644
index 0000000..14d8d0a
--- /dev/null
+++ b/models/acc_balance.json
@@ -0,0 +1,37 @@
+{
+ "summary": [
+ {
+ "name": "acc_balance",
+ "title": "账户余额表",
+ "primary": [
+ "id"
+ ]
+ }
+ ],
+ "fields": [
+ {
+ "name": "id",
+ "title": "id",
+ "type": "str",
+ "length": 32
+ },
+ {
+ "name": "accountid",
+ "title": "账户id",
+ "type": "str",
+ "length": 32
+ },
+ {
+ "name": "acc_date",
+ "title": "记账日期",
+ "type": "date"
+ },
+ {
+ "name": "balance",
+ "title": "账户余额",
+ "type": "float",
+ "length": 20,
+ "dec": 2
+ }
+ ]
+}
\ No newline at end of file
diff --git a/models/acc_detail.json b/models/acc_detail.json
new file mode 100644
index 0000000..b0d687b
--- /dev/null
+++ b/models/acc_detail.json
@@ -0,0 +1,72 @@
+{
+ "summary": [
+ {
+ "name": "acc_detail",
+ "title": "账户明细表",
+ "primary": [
+ "id"
+ ]
+ }
+ ],
+ "fields": [
+ {
+ "name": "id",
+ "title": "id",
+ "type": "str",
+ "length": 32
+ },
+ {
+ "name": "accountid",
+ "title": "账户id",
+ "type": "str",
+ "length": 32
+ },
+ {
+ "name": "acc_no",
+ "title": "明细顺序号",
+ "type": "short"
+ },
+ {
+ "name": "acc_date",
+ "title": "记账日期",
+ "type": "date"
+ },
+ {
+ "name": "acc_timestamp",
+ "title": "记账时间戳",
+ "type": "timestamp"
+ },
+ {
+ "name": "acc_dir",
+ "title": "记账方向",
+ "type": "str",
+ "length": 1
+ },
+ {
+ "name": "summary",
+ "title": "摘要",
+ "type": "str",
+ "length": 255
+ },
+ {
+ "name": "amount",
+ "title": "记账金额",
+ "type": "float",
+ "length": 18,
+ "dec": 2
+ },
+ {
+ "name": "balance",
+ "title": "账户余额",
+ "type": "float",
+ "length": 18,
+ "dec": 2
+ },
+ {
+ "name": "acclogid",
+ "title": "账务流水id",
+ "type": "str",
+ "length": 32
+ }
+ ]
+}
\ No newline at end of file
diff --git a/models/account.json b/models/account.json
new file mode 100644
index 0000000..859b82a
--- /dev/null
+++ b/models/account.json
@@ -0,0 +1,100 @@
+{
+ "summary": [
+ {
+ "name": "account",
+ "title": "机构账户表",
+ "primary": [
+ "id"
+ ]
+ }
+ ],
+ "fields": [
+ {
+ "name": "id",
+ "title": "id",
+ "type": "str",
+ "length": 32
+ },
+ {
+ "name": "accounting_orgid",
+ "title": "账本机构",
+ "type": "str",
+ "length": 32
+ },
+ {
+ "name": "orgid",
+ "title": "主机构id",
+ "type": "str",
+ "length": 32
+ },
+ {
+ "name": "org1id",
+ "title": "从机构id",
+ "type": "str",
+ "length": 32
+ },
+ {
+ "name": "subjectid",
+ "title": "科目号",
+ "type": "str",
+ "length": 21
+ },
+ {
+ "name": "balance_at",
+ "title": "余额方向",
+ "type": "str",
+ "length": 1
+ },
+ {
+ "name": "max_detailno",
+ "title": "最大明细顺序号",
+ "type": "short"
+ },
+ {
+ "name": "balance",
+ "title": "余额",
+ "type": "float",
+ "length": 20,
+ "dec": 2
+ }
+ ],
+ "indexes": [
+ {
+ "name": "idx1",
+ "idxtype": "unique",
+ "idxfields": [
+ "accounting_orgid",
+ "orgid",
+ "subjectid",
+ "org1id"
+ ]
+ }
+ ],
+ "codes": [
+ {
+ "field": "subjectid",
+ "table": "subject",
+ "valuefield": "id",
+ "textfield": "name"
+ },
+ {
+ "field": "orgid",
+ "table": "organization",
+ "valuefield": "id",
+ "textfield": "orgname"
+ },
+ {
+ "field": "balance_at",
+ "table": "appcodes_kv",
+ "valuefield": "k",
+ "textfield": "v",
+ "cond": "parentid='accounting_dir'"
+ },
+ {
+ "field": "org1id",
+ "table": "organization",
+ "valuefield": "id",
+ "textfield": "orgname"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/models/account_config.json b/models/account_config.json
new file mode 100644
index 0000000..3709c3b
--- /dev/null
+++ b/models/account_config.json
@@ -0,0 +1,61 @@
+{
+ "summary": [
+ {
+ "name": "account_config",
+ "title": "账户配置表",
+ "primary": [
+ "id"
+ ]
+ }
+ ],
+ "fields": [
+ {
+ "name": "id",
+ "title": "id",
+ "type": "str",
+ "length": 32
+ },
+ {
+ "name": "subjectid",
+ "title": "科目id",
+ "type": "str",
+ "length": 32
+ },
+ {
+ "name": "partytype",
+ "title": "主参与方类型",
+ "type": "str",
+ "length": 255,
+ "nullable": "no"
+ },
+ {
+ "name": "party1type",
+ "title": "从参与方类型",
+ "type": "str",
+ "length": 255,
+ "nullable": "yes"
+ }
+ ],
+ "codes": [
+ {
+ "field": "party1type",
+ "table": "appcodes_kv",
+ "valuefield": "k",
+ "textfield": "v",
+ "cond": "parentid='partytype'"
+ },
+ {
+ "field": "subjectid",
+ "table": "subject",
+ "valuefield": "id",
+ "textfield": "name"
+ },
+ {
+ "field": "partytype",
+ "table": "appcodes_kv",
+ "valuefield": "k",
+ "textfield": "v",
+ "cond": "parentid='partytype'"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/models/accounting_config.json b/models/accounting_config.json
new file mode 100644
index 0000000..35de6f4
--- /dev/null
+++ b/models/accounting_config.json
@@ -0,0 +1,98 @@
+{
+ "summary": [
+ {
+ "name": "accounting_config",
+ "title": "记账配置表",
+ "primary": [
+ "id"
+ ]
+ }
+ ],
+ "fields": [
+ {
+ "name": "id",
+ "title": "id",
+ "type": "str",
+ "length": 32
+ },
+ {
+ "name": "action",
+ "title": "交易",
+ "type": "str",
+ "length": 255
+ },
+ {
+ "name": "accounting_orgtype",
+ "title": "账务机构",
+ "type": "str",
+ "length": 256
+ },
+ {
+ "name": "accounting_dir",
+ "title": "记账方向",
+ "type": "str",
+ "length": 255
+ },
+ {
+ "name": "orgtype",
+ "title": "机构类型",
+ "type": "str",
+ "length": 32
+ },
+ {
+ "name": "org1type",
+ "title": "从机构类型",
+ "type": "str",
+ "length": 32,
+ "nullable": "yes"
+ },
+ {
+ "name": "subjectid",
+ "title": "科目id",
+ "type": "str",
+ "length": 21
+ },
+ {
+ "name": "amt_pattern",
+ "title": "金额模板",
+ "type": "str",
+ "length": 255
+ }
+ ],
+ "codes": [
+ {
+ "field": "accounting_orgtype",
+ "table": "appcodes_kv",
+ "valuefield": "k",
+ "textfield": "v",
+ "cond": "parentid='partytype'"
+ },
+ {
+ "field": "accounting_dir",
+ "table": "appcodes_kv",
+ "valuefield": "k",
+ "textfield": "v",
+ "cond": "parentid='accounting_dir'"
+ },
+ {
+ "field": "orgtype",
+ "table": "appcodes_kv",
+ "valuefield": "k",
+ "textfield": "v",
+ "cond": "parentid='partytype'"
+ },
+ {
+ "field": "org1type",
+ "table": "appcodes_kv",
+ "valuefield": "k",
+ "textfield": "v",
+ "cond": "parentid='partytype'"
+ },
+ {
+ "field": "subjectid",
+ "table": "subject",
+ "valuefield": "id",
+ "textfield": "name"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/models/accounting_log.json b/models/accounting_log.json
new file mode 100644
index 0000000..a7c5fea
--- /dev/null
+++ b/models/accounting_log.json
@@ -0,0 +1,69 @@
+{
+ "summary": [
+ {
+ "name": "accounting_log",
+ "title": "账务流水表",
+ "primary": [
+ "id"
+ ]
+ }
+ ],
+ "fields": [
+ {
+ "name": "id",
+ "title": "id",
+ "type": "str",
+ "length": 32
+ },
+ {
+ "name": "accountid",
+ "title": "账户id",
+ "type": "str",
+ "length": 32
+ },
+ {
+ "name": "acc_date",
+ "title": "记账日期",
+ "type": "date"
+ },
+ {
+ "name": "acc_timestamp",
+ "title": "记账时间戳",
+ "type": "timestamp"
+ },
+ {
+ "name": "acc_dir",
+ "title": "记账方向",
+ "type": "str",
+ "length": 1
+ },
+ {
+ "name": "summary",
+ "title": "摘要",
+ "type": "str",
+ "length": 255
+ },
+ {
+ "name": "amount",
+ "title": "记账金额",
+ "type": "float",
+ "length": 18,
+ "dec": 2
+ },
+ {
+ "name": "billid",
+ "title": "账单id",
+ "type": "str",
+ "length": 32
+ }
+ ],
+ "codes": [
+ {
+ "field": "acc_dir",
+ "table": "appcodes_kv",
+ "valuefield": "k",
+ "textfield": "v",
+ "cond": "parentid='accounting_dir'"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/models/bill.json b/models/bill.json
new file mode 100644
index 0000000..30f57fd
--- /dev/null
+++ b/models/bill.json
@@ -0,0 +1,78 @@
+{
+ "summary": [
+ {
+ "name": "bill",
+ "title": "账单",
+ "primary": [
+ "id"
+ ]
+ }
+ ],
+ "fields": [
+ {
+ "name": "id",
+ "title": "id",
+ "type": "str",
+ "length": 32
+ },
+ {
+ "name": "customerid",
+ "title": "客户编号",
+ "type": "str",
+ "length": 32
+ },
+ {
+ "name": "resellerid",
+ "title": "商户id",
+ "type": "str",
+ "length": 32
+ },
+ {
+ "name": "productid",
+ "title": "产品id",
+ "type": "str",
+ "length": 32
+ },
+ {
+ "name": "resourceid",
+ "title": "资源id",
+ "type": "str",
+ "length": 32
+ },
+ {
+ "name": "orderid",
+ "title": "订单编号",
+ "type": "str",
+ "length": 32
+ },
+ {
+ "name": "business_op",
+ "title": "业务操作",
+ "type": "str",
+ "length": 255
+ },
+ {
+ "name": "amount",
+ "title": "金额",
+ "type": "float",
+ "length": 18,
+ "dec": 2
+ },
+ {
+ "name": "bill_date",
+ "title": "账单日期",
+ "type": "date"
+ },
+ {
+ "name": "bill_timestamp",
+ "title": "账单时间戳",
+ "type": "timestamp"
+ },
+ {
+ "name": "bill_state",
+ "title": "账单状态",
+ "type": "str",
+ "length": 1
+ }
+ ]
+}
\ No newline at end of file
diff --git a/models/bill_detail.json b/models/bill_detail.json
new file mode 100644
index 0000000..ea799c0
--- /dev/null
+++ b/models/bill_detail.json
@@ -0,0 +1,68 @@
+{
+ "summary": [
+ {
+ "name": "bill_detail",
+ "title": "账单明细",
+ "primary": [
+ "id"
+ ]
+ }
+ ],
+ "fields": [
+ {
+ "name": "id",
+ "title": "id",
+ "type": "str",
+ "length": 32
+ },
+ {
+ "name": "accounting_orgid",
+ "title": "账务机构id",
+ "type": "str",
+ "length": 32
+ },
+ {
+ "name": "billid",
+ "title": "账单ID",
+ "type": "str",
+ "length": 32
+ },
+ {
+ "name": "description",
+ "title": "账务说明",
+ "type": "str",
+ "length": 255
+ },
+ {
+ "name": "participantid",
+ "title": "记账方id",
+ "type": "str",
+ "length": 32
+ },
+ {
+ "name": "participanttype",
+ "title": "记账方类型",
+ "type": "str",
+ "length": 255
+ },
+ {
+ "name": "subjectname",
+ "title": "科目名称",
+ "type": "str",
+ "length": 255
+ },
+ {
+ "name": "accounting_dir",
+ "title": "记账方向",
+ "type": "str",
+ "length": 255
+ },
+ {
+ "name": "amount",
+ "title": "账单金额",
+ "type": "float",
+ "length": 18,
+ "dec": 2
+ }
+ ]
+}
\ No newline at end of file
diff --git a/models/ledger.json b/models/ledger.json
new file mode 100644
index 0000000..f3d3b16
--- /dev/null
+++ b/models/ledger.json
@@ -0,0 +1,50 @@
+{
+ "summary": [
+ {
+ "name": "ledger",
+ "title": "总账表",
+ "primary": [
+ "id"
+ ]
+ }
+ ],
+ "fields": [
+ {
+ "name": "id",
+ "title": "id",
+ "type": "str",
+ "length": 32
+ },
+ {
+ "name": "accounting_orgid",
+ "title": "账本机构",
+ "type": "str",
+ "length": 32
+ },
+ {
+ "name": "subjectid",
+ "title": "科目号",
+ "type": "str",
+ "length": 32
+ },
+ {
+ "name": "acc_date",
+ "title": "账务日期",
+ "type": "date"
+ },
+ {
+ "name": "d_balance",
+ "title": "借方余额",
+ "type": "float",
+ "length": 18,
+ "dec": 2
+ },
+ {
+ "name": "c_balance",
+ "title": "贷方余额",
+ "type": "float",
+ "length": 18,
+ "dec": 2
+ }
+ ]
+}
\ No newline at end of file
diff --git a/models/subject.json b/models/subject.json
new file mode 100644
index 0000000..f660d9c
--- /dev/null
+++ b/models/subject.json
@@ -0,0 +1,53 @@
+{
+ "summary": [
+ {
+ "name": "subject",
+ "title": "科目表",
+ "primary": [
+ "id"
+ ]
+ }
+ ],
+ "fields": [
+ {
+ "name": "id",
+ "title": "id",
+ "type": "str",
+ "length": 32
+ },
+ {
+ "name": "name",
+ "title": "科目名称",
+ "type": "str",
+ "length": 255
+ },
+ {
+ "name": "balance_side",
+ "title": "余额方向",
+ "type": "str",
+ "length": 1
+ },
+ {
+ "name": "subjecttype",
+ "title": "科目类别",
+ "type": "str",
+ "length": 30
+ }
+ ],
+ "codes": [
+ {
+ "field": "balance_side",
+ "table": "appcodes_kv",
+ "valuefield": "k",
+ "textfield": "v",
+ "cond": "parentid='balance_side'"
+ },
+ {
+ "field": "subjecttype",
+ "table": "appcodes_kv",
+ "valuefield": "k",
+ "textfield": "v",
+ "cond": "parentid='subjecttype'"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/scripts/load_path.py b/scripts/load_path.py
new file mode 100644
index 0000000..fdd29a7
--- /dev/null
+++ b/scripts/load_path.py
@@ -0,0 +1,99 @@
+#!/usr/bin/env python3
+"""
+accounting 模块 RBAC 权限管理脚本
+
+使用方法:
+ cd ~/repos/sage
+ ./py3/bin/python ~/accounting/scripts/load_path.py
+
+每次代码变更如有新 path 出现,需同步更新此脚本。
+"""
+
+import subprocess
+import os
+import sys
+
+def find_sage_root():
+ candidates = [
+ os.path.expanduser("~/repos/sage"),
+ os.path.expanduser("~/sage"),
+ os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))),
+ ]
+ for c in candidates:
+ if os.path.isdir(os.path.join(c, "py3")) and os.path.isdir(os.path.join(c, "wwwroot")):
+ return c
+ return None
+
+SAGE_ROOT = find_sage_root()
+if not SAGE_ROOT:
+ print("ERROR: Cannot find Sage root directory")
+ sys.exit(1)
+
+PYTHON = os.path.join(SAGE_ROOT, "py3", "bin", "python")
+SET_PERM_SCRIPT = os.path.join(SAGE_ROOT, "set_role_perm.py")
+
+MOD = "accounting"
+
+# ============================================================
+# 权限路径定义 — 每次新增页面或API时同步更新
+# ============================================================
+
+# any — 无需登录(菜单、登录页等)
+PATHS_ANY = [
+ f"/accounting/usermenu.ui",]
+
+# logined — 需要认证的页面和 API
+PATHS_LOGINED = [
+ f"/accounting",
+ f"/accounting/acc_balance",
+ f"/accounting/acc_detail",
+ f"/accounting/accdetail.dspy",
+ f"/accounting/accdetail.ui",
+ f"/accounting/account",
+ f"/accounting/account_config",
+ f"/accounting/accounting_config",
+ f"/accounting/accounting_log",
+ f"/accounting/get_user_balance.dspy",
+ f"/accounting/index.ui",
+ f"/accounting/myaccounts.dspy",
+ f"/accounting/myaccounts.ui",
+ f"/accounting/mybalance.dspy",
+ f"/accounting/oca.dspy",
+ f"/accounting/open_customer_accounts.dspy",
+ f"/accounting/open_owner_accounts.dspy",
+ f"/accounting/open_provider_accounts.dspy",
+ f"/accounting/open_reseller_accounts.dspy",
+ f"/accounting/open_reseller_provider_accounts.dspy",
+ f"/accounting/stat_account_count.ui",
+ f"/accounting/stat_month_consumption.ui",
+ f"/accounting/stat_today_consumption.ui",
+ f"/accounting/stat_total_balance.ui",
+ f"/accounting/subject",]
+
+# ============================================================
+# 执行注册
+# ============================================================
+
+def run_set_perm(role, path):
+ cmd = [PYTHON, SET_PERM_SCRIPT, role, path]
+ result = subprocess.run(cmd, capture_output=True, text=True)
+ return result.returncode == 0
+
+def register_role_paths(role, paths):
+ count = 0
+ for p in paths:
+ if run_set_perm(role, p):
+ count += 1
+ print(f" {role}: {count}/{len(paths)} paths registered")
+ return count
+
+def main():
+ print(f"Sage root: {SAGE_ROOT}")
+ total = 0
+ total += register_role_paths("any", PATHS_ANY)
+ total += register_role_paths("logined", PATHS_LOGINED)
+ print(f"\nDone. Total {total} permission entries registered.")
+ print("NOTE: Restart Sage after permission changes to reload RBAC cache.")
+
+if __name__ == "__main__":
+ main()
diff --git a/wwwroot/stat_account_count.ui b/wwwroot/stat_account_count.ui
new file mode 100644
index 0000000..d2b49a3
--- /dev/null
+++ b/wwwroot/stat_account_count.ui
@@ -0,0 +1,53 @@
+{% set stats = get_accounting_stats(request) %}
+{
+ "widgettype": "VBox",
+ "options": {
+ "bgcolor": "#1E293B",
+ "padding": "20px",
+ "borderRadius": "12px",
+ "border": "1px solid #334155",
+ "flex": "1",
+ "minHeight": "110px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "HBox",
+ "options": {
+ "alignItems": "center",
+ "marginBottom": "12px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "Svg",
+ "options": {
+ "svg": "",
+ "width": "24px",
+ "height": "24px"
+ }
+ },
+ {
+ "widgettype": "Filler"
+ }
+ ]
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "{{stats.account_count}}",
+ "fontSize": "32px",
+ "fontWeight": "700",
+ "color": "#F1F5F9",
+ "lineHeight": "1.1"
+ }
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "账户数量",
+ "fontSize": "14px",
+ "color": "#94A3B8",
+ "marginTop": "4px"
+ }
+ }
+ ]
+}
diff --git a/wwwroot/stat_month_consumption.ui b/wwwroot/stat_month_consumption.ui
new file mode 100644
index 0000000..8dcb266
--- /dev/null
+++ b/wwwroot/stat_month_consumption.ui
@@ -0,0 +1,53 @@
+{% set stats = get_accounting_stats(request) %}
+{
+ "widgettype": "VBox",
+ "options": {
+ "bgcolor": "#1E293B",
+ "padding": "20px",
+ "borderRadius": "12px",
+ "border": "1px solid #334155",
+ "flex": "1",
+ "minHeight": "110px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "HBox",
+ "options": {
+ "alignItems": "center",
+ "marginBottom": "12px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "Svg",
+ "options": {
+ "svg": "",
+ "width": "24px",
+ "height": "24px"
+ }
+ },
+ {
+ "widgettype": "Filler"
+ }
+ ]
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "¥{{'%.2f' % stats.month_amount}}",
+ "fontSize": "32px",
+ "fontWeight": "700",
+ "color": "#F1F5F9",
+ "lineHeight": "1.1"
+ }
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "本月消费",
+ "fontSize": "14px",
+ "color": "#94A3B8",
+ "marginTop": "4px"
+ }
+ }
+ ]
+}
diff --git a/wwwroot/stat_today_consumption.ui b/wwwroot/stat_today_consumption.ui
new file mode 100644
index 0000000..4f4f5ee
--- /dev/null
+++ b/wwwroot/stat_today_consumption.ui
@@ -0,0 +1,53 @@
+{% set stats = get_accounting_stats(request) %}
+{
+ "widgettype": "VBox",
+ "options": {
+ "bgcolor": "#1E293B",
+ "padding": "20px",
+ "borderRadius": "12px",
+ "border": "1px solid #334155",
+ "flex": "1",
+ "minHeight": "110px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "HBox",
+ "options": {
+ "alignItems": "center",
+ "marginBottom": "12px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "Svg",
+ "options": {
+ "svg": "",
+ "width": "24px",
+ "height": "24px"
+ }
+ },
+ {
+ "widgettype": "Filler"
+ }
+ ]
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "¥{{'%.2f' % stats.today_amount}}",
+ "fontSize": "32px",
+ "fontWeight": "700",
+ "color": "#F1F5F9",
+ "lineHeight": "1.1"
+ }
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "今日消费",
+ "fontSize": "14px",
+ "color": "#94A3B8",
+ "marginTop": "4px"
+ }
+ }
+ ]
+}
diff --git a/wwwroot/stat_total_balance.ui b/wwwroot/stat_total_balance.ui
new file mode 100644
index 0000000..4687c21
--- /dev/null
+++ b/wwwroot/stat_total_balance.ui
@@ -0,0 +1,53 @@
+{% set stats = get_accounting_stats(request) %}
+{
+ "widgettype": "VBox",
+ "options": {
+ "bgcolor": "#1E293B",
+ "padding": "20px",
+ "borderRadius": "12px",
+ "border": "1px solid #334155",
+ "flex": "1",
+ "minHeight": "110px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "HBox",
+ "options": {
+ "alignItems": "center",
+ "marginBottom": "12px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "Svg",
+ "options": {
+ "svg": "",
+ "width": "24px",
+ "height": "24px"
+ }
+ },
+ {
+ "widgettype": "Filler"
+ }
+ ]
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "¥{{'%.2f' % stats.total_balance}}",
+ "fontSize": "32px",
+ "fontWeight": "700",
+ "color": "#F1F5F9",
+ "lineHeight": "1.1"
+ }
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "账户总余额",
+ "fontSize": "14px",
+ "color": "#94A3B8",
+ "marginTop": "4px"
+ }
+ }
+ ]
+}