diff --git a/accounting/creditlimit.py b/accounting/creditlimit.py
index a7c2845..269acf2 100644
--- a/accounting/creditlimit.py
+++ b/accounting/creditlimit.py
@@ -1,6 +1,103 @@
from appPublic.log import debug, exception
+from appPublic.uniqueID import getID
from datetime import datetime
+
+async def get_credit_stats(sor, orgid):
+ """
+ Get credit summary statistics for an organization.
+ Returns total_credit, total_used, total_available, usage_pct, customer_count.
+ """
+ sql = """
+ SELECT
+ COALESCE(SUM(credit_limit), 0) as total_credit,
+ COALESCE(SUM(used_credit), 0) as total_used,
+ COALESCE(SUM(available_credit), 0) as total_available,
+ COUNT(*) as customer_count,
+ COUNT(CASE WHEN status = 'active' THEN 1 END) as active_count,
+ COUNT(CASE WHEN status = 'expired' THEN 1 END) as expired_count
+ FROM credit_limit
+ WHERE orgid = ${orgid}$
+ """
+ recs = await sor.sqlExe(sql, {'orgid': orgid})
+ if recs and len(recs) > 0:
+ r = recs[0]
+ total_credit = float(r.total_credit or 0)
+ total_used = float(r.total_used or 0)
+ total_available = float(r.total_available or 0)
+ usage_pct = round((total_used / total_credit * 100), 1) if total_credit > 0 else 0
+ return {
+ 'total_credit': total_credit,
+ 'total_used': total_used,
+ 'total_available': total_available,
+ 'usage_pct': usage_pct,
+ 'customer_count': int(r.customer_count or 0),
+ 'active_count': int(r.active_count or 0),
+ 'expired_count': int(r.expired_count or 0)
+ }
+ return {
+ 'total_credit': 0, 'total_used': 0, 'total_available': 0,
+ 'usage_pct': 0, 'customer_count': 0, 'active_count': 0, 'expired_count': 0
+ }
+
+
+async def get_my_credit_list(sor, orgid):
+ """
+ Get all credit limit records for the current user's organization,
+ with organization name and account info for display.
+ """
+ sql = """
+ SELECT
+ cl.*,
+ org.orgname as orgname_text,
+ sub.name as subject_name,
+ CASE
+ WHEN cl.credit_limit > 0 THEN ROUND(cl.used_credit / cl.credit_limit * 100, 1)
+ ELSE 0
+ END as usage_pct
+ FROM credit_limit cl
+ LEFT JOIN organization org ON cl.orgid = org.id
+ LEFT JOIN account acc ON cl.accountid = acc.id
+ LEFT JOIN subject sub ON acc.subjectid = sub.id
+ WHERE cl.orgid = ${orgid}$
+ ORDER BY cl.created_at DESC
+ """
+ recs = await sor.sqlExe(sql, {'orgid': orgid})
+ return recs
+
+
+async def get_all_customer_credits(sor, orgid, status_filter=None):
+ """
+ Get all customer credit limits for management view.
+ For distributor sales to see all their customers' credit status.
+ """
+ where_clause = "WHERE cl.orgid = ${orgid}$"
+ params = {'orgid': orgid}
+ if status_filter and status_filter != 'all':
+ where_clause += " AND cl.status = ${status}$"
+ params['status'] = status_filter
+
+ sql = f"""
+ SELECT
+ cl.*,
+ org.orgname as orgname_text,
+ sub.name as subject_name,
+ acc.balance as account_balance,
+ CASE
+ WHEN cl.credit_limit > 0 THEN ROUND(cl.used_credit / cl.credit_limit * 100, 1)
+ ELSE 0
+ END as usage_pct
+ FROM credit_limit cl
+ LEFT JOIN organization org ON cl.orgid = org.id
+ LEFT JOIN account acc ON cl.accountid = acc.id
+ LEFT JOIN subject sub ON acc.subjectid = sub.id
+ {where_clause}
+ ORDER BY cl.updated_at DESC
+ """
+ recs = await sor.sqlExe(sql, params)
+ return recs
+
+
async def get_credit_limit_for_account(sor, accid):
"""
Get active credit limit for an account.
@@ -52,8 +149,6 @@ async def set_credit_limit(sor, accountid, orgid, credit_limit_amount,
Set or update credit limit for an account.
If a credit limit already exists, update it; otherwise create new.
"""
- from appPublic.uniqueID import getID
-
# Check if credit limit exists
existing = await get_credit_limit_for_account(sor, accountid)
diff --git a/accounting/init.py b/accounting/init.py
index 08b069a..e9ae4b8 100644
--- a/accounting/init.py
+++ b/accounting/init.py
@@ -10,7 +10,7 @@ from .getaccount import getAccountBalance, getCustomerBalance, getAccountByName,
from .stats import get_accounting_stats
from .recharge import RechargeBiz, recharge_accounting
from .consume import consume_accounting
-from .creditlimit import get_credit_limit_for_account, update_used_credit, set_credit_limit
+from .creditlimit import get_credit_limit_for_account, update_used_credit, set_credit_limit, get_credit_stats, get_my_credit_list, get_all_customer_credits
async def all_my_accounts(request):
env = request._run_ns
@@ -77,3 +77,34 @@ def load_accounting():
g.get_credit_limit_for_account = get_credit_limit_for_account
g.update_used_credit = update_used_credit
g.set_credit_limit = set_credit_limit
+ g.get_credit_stats = get_credit_stats
+ g.get_my_credit_list = get_my_credit_list
+ g.get_all_customer_credits = get_all_customer_credits
+ g.get_credit_stats_web = get_credit_stats_web
+ g.get_my_credits_web = get_my_credits_web
+ g.get_all_credits_web = get_all_credits_web
+
+
+async def get_credit_stats_web(request):
+ """Web wrapper for get_credit_stats - used in Jinja2 .ui templates"""
+ env = request._run_ns
+ userorgid = await env.get_userorgid()
+ async with get_sor_context(env, 'accounting') as sor:
+ return await get_credit_stats(sor, userorgid)
+
+
+async def get_my_credits_web(request):
+ """Web wrapper for get_my_credit_list - used in Jinja2 .ui templates"""
+ env = request._run_ns
+ userorgid = await env.get_userorgid()
+ async with get_sor_context(env, 'accounting') as sor:
+ return await get_my_credit_list(sor, userorgid)
+
+
+async def get_all_credits_web(request):
+ """Web wrapper for get_all_customer_credits - used in Jinja2 .ui templates"""
+ env = request._run_ns
+ userorgid = await env.get_userorgid()
+ status_filter = getattr(request, '_params_kw', {}).get('status', None) if hasattr(request, '_params_kw') else None
+ async with get_sor_context(env, 'accounting') as sor:
+ return await get_all_customer_credits(sor, userorgid, status_filter)
diff --git a/wwwroot/credit_limit/api/credit_summary.dspy b/wwwroot/credit_limit/api/credit_summary.dspy
new file mode 100644
index 0000000..5c859a7
--- /dev/null
+++ b/wwwroot/credit_limit/api/credit_summary.dspy
@@ -0,0 +1,43 @@
+
+orgid = await get_userorgid()
+
+db = DBPools()
+dbname = get_module_dbname('accounting')
+async with db.sqlorContext(dbname) as sor:
+ sql = """
+ SELECT
+ COALESCE(SUM(credit_limit), 0) as total_credit,
+ COALESCE(SUM(used_credit), 0) as total_used,
+ COALESCE(SUM(available_credit), 0) as total_available,
+ COUNT(*) as customer_count,
+ COUNT(CASE WHEN status = 'active' THEN 1 END) as active_count,
+ COUNT(CASE WHEN status = 'expired' THEN 1 END) as expired_count
+ FROM credit_limit
+ WHERE orgid = ${orgid}$
+ """
+ recs = await sor.sqlExe(sql, {'orgid': orgid})
+ if recs and len(recs) > 0:
+ r = recs[0]
+ total_credit = float(r.total_credit or 0)
+ total_used = float(r.total_used or 0)
+ total_available = float(r.total_available or 0)
+ usage_pct = round((total_used / total_credit * 100), 1) if total_credit > 0 else 0
+ return json.dumps({
+ "status": "ok",
+ "data": {
+ "total_credit": total_credit,
+ "total_used": total_used,
+ "total_available": total_available,
+ "usage_pct": usage_pct,
+ "customer_count": int(r.customer_count or 0),
+ "active_count": int(r.active_count or 0),
+ "expired_count": int(r.expired_count or 0)
+ }
+ })
+ return json.dumps({
+ "status": "ok",
+ "data": {
+ "total_credit": 0, "total_used": 0, "total_available": 0,
+ "usage_pct": 0, "customer_count": 0, "active_count": 0, "expired_count": 0
+ }
+ })
diff --git a/wwwroot/credit_limit/api/set_credit_form.ui b/wwwroot/credit_limit/api/set_credit_form.ui
new file mode 100644
index 0000000..4d2d739
--- /dev/null
+++ b/wwwroot/credit_limit/api/set_credit_form.ui
@@ -0,0 +1,63 @@
+{
+ "widgettype": "Form",
+ "options": {
+ "width": "100%",
+ "padding": "16px",
+ "url": "{{entire_url('/accounting/credit_limit/api/set_customer_credit.dspy')}}",
+ "method": "POST",
+ "fields": [
+ {
+ "name": "id",
+ "label": "id",
+ "uitype": "hidden",
+ "value": "{{params_kw.get('id', '')}}"
+ },
+ {
+ "name": "accountid",
+ "label": "账户ID",
+ "uitype": "str",
+ "required": true,
+ "value": "{{params_kw.get('accountid', '')}}"
+ },
+ {
+ "name": "credit_limit",
+ "label": "授信额度",
+ "uitype": "float",
+ "required": true,
+ "value": "{{params_kw.get('credit_limit', '0')}}"
+ },
+ {
+ "name": "valid_from",
+ "label": "生效日期",
+ "uitype": "date",
+ "value": "{{params_kw.get('valid_from', '')}}"
+ },
+ {
+ "name": "valid_to",
+ "label": "失效日期",
+ "uitype": "date",
+ "value": "{{params_kw.get('valid_to', '')}}"
+ },
+ {
+ "name": "remark",
+ "label": "备注",
+ "uitype": "str",
+ "value": "{{params_kw.get('remark', '')}}"
+ }
+ ],
+ "buttons": [
+ {
+ "label": "保存",
+ "actiontype": "submit",
+ "bgcolor": "#3B82F6",
+ "color": "#FFFFFF"
+ },
+ {
+ "label": "取消",
+ "actiontype": "close",
+ "bgcolor": "#475569",
+ "color": "#FFFFFF"
+ }
+ ]
+ }
+}
diff --git a/wwwroot/credit_limit/api/set_customer_credit.dspy b/wwwroot/credit_limit/api/set_customer_credit.dspy
new file mode 100644
index 0000000..7c295ad
--- /dev/null
+++ b/wwwroot/credit_limit/api/set_customer_credit.dspy
@@ -0,0 +1,105 @@
+
+ns = params_kw.copy()
+for k, v in ns.items():
+ if v == 'NaN' or v == 'null' or v == '':
+ ns[k] = None
+
+accountid = ns.get('accountid')
+if not accountid:
+ return {
+ "widgettype": "Error",
+ "options": {
+ "title": "参数错误",
+ "cwidth": 16,
+ "cheight": 9,
+ "timeout": 3,
+ "message": "账户ID不能为空"
+ }
+ }
+
+credit_limit_amount = float(ns.get('credit_limit', 0) or 0)
+if credit_limit_amount <= 0:
+ return {
+ "widgettype": "Error",
+ "options": {
+ "title": "参数错误",
+ "cwidth": 16,
+ "cheight": 9,
+ "timeout": 3,
+ "message": "授信额度必须大于0"
+ }
+ }
+
+valid_from = ns.get('valid_from')
+valid_to = ns.get('valid_to')
+remark = ns.get('remark')
+record_id = ns.get('id')
+
+user_id = await get_user()
+orgid = await get_userorgid()
+
+db = DBPools()
+dbname = get_module_dbname('accounting')
+async with db.sqlorContext(dbname) as sor:
+ if record_id:
+ sql = """
+ UPDATE credit_limit
+ SET credit_limit = ${credit_limit}$,
+ available_credit = ${credit_limit}$ - used_credit,
+ valid_from = ${valid_from}$,
+ valid_to = ${valid_to}$,
+ remark = ${remark}$,
+ updated_at = CURRENT_TIMESTAMP
+ WHERE id = ${id}$ AND orgid = ${orgid}$
+ """
+ await sor.sqlExe(sql, {
+ 'credit_limit': credit_limit_amount,
+ 'valid_from': valid_from,
+ 'valid_to': valid_to,
+ 'remark': remark,
+ 'id': record_id,
+ 'orgid': orgid
+ })
+ debug(f'Updated credit limit {record_id} to {credit_limit_amount}')
+ else:
+ new_id = uuid()
+ now = datetime.now()
+ data = {
+ 'id': new_id,
+ 'accountid': accountid,
+ 'orgid': orgid,
+ 'credit_limit': credit_limit_amount,
+ 'used_credit': 0,
+ 'available_credit': credit_limit_amount,
+ 'valid_from': valid_from,
+ 'valid_to': valid_to,
+ 'status': 'active',
+ 'created_at': now,
+ 'updated_at': now,
+ 'created_by': user_id,
+ 'remark': remark
+ }
+ await sor.C('credit_limit', data)
+ debug(f'Created credit limit for {accountid}: {credit_limit_amount}')
+
+ return {
+ "widgettype": "Message",
+ "options": {
+ "cwidth": 16,
+ "cheight": 9,
+ "title": "授信额度设置成功",
+ "timeout": 3,
+ "message": "ok"
+ }
+ }
+
+return {
+ "widgettype": "Error",
+ "options": {
+ "title": "设置失败",
+ "cwidth": 16,
+ "cheight": 9,
+ "timeout": 3,
+ "message": "failed"
+ }
+}
diff --git a/wwwroot/credit_limit/credit_manage.ui b/wwwroot/credit_limit/credit_manage.ui
new file mode 100644
index 0000000..aaaabde
--- /dev/null
+++ b/wwwroot/credit_limit/credit_manage.ui
@@ -0,0 +1,276 @@
+{% set credits = get_all_credits_web(request) %}
+{
+ "widgettype": "VBox",
+ "options": {
+ "width": "100%",
+ "gap": "12px",
+ "padding": "4px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "HBox",
+ "options": {
+ "alignItems": "center",
+ "justifyContent": "space-between"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "客户额度管理 (共{{credits|length}}条)",
+ "fontSize": "16px",
+ "fontWeight": "600",
+ "color": "#F1F5F9"
+ }
+ },
+ {
+ "widgettype": "Button",
+ "options": {
+ "label": "新增授信",
+ "bgcolor": "#3B82F6",
+ "color": "#FFFFFF",
+ "borderRadius": "6px",
+ "padding": "6px 14px"
+ },
+ "binds": [{
+ "wid": "self",
+ "event": "click",
+ "actiontype": "urlwidget",
+ "target": "PopupWindow",
+ "popup_options": {
+ "title": "新增客户授信",
+ "width": "480px",
+ "height": "520px"
+ },
+ "options": {
+ "url": "{{entire_url('/accounting/credit_limit/api/set_credit_form.ui')}}"
+ }
+ }]
+ }
+ ]
+ },
+ {
+ "widgettype": "VBox",
+ "options": {
+ "bgcolor": "#1E293B",
+ "borderRadius": "10px",
+ "border": "1px solid #334155",
+ "width": "100%"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "HBox",
+ "options": {
+ "bgcolor": "#0F172A",
+ "padding": "10px 16px",
+ "borderRadius": "10px 10px 0 0",
+ "alignItems": "center"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "Text",
+ "options": {"text": "客户名称", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8", "cwidth": 10}
+ },
+ {
+ "widgettype": "Text",
+ "options": {"text": "授信额度", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8", "cwidth": 8}
+ },
+ {
+ "widgettype": "Text",
+ "options": {"text": "已用/剩余", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8", "cwidth": 10}
+ },
+ {
+ "widgettype": "Text",
+ "options": {"text": "使用率", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8", "cwidth": 6}
+ },
+ {
+ "widgettype": "Text",
+ "options": {"text": "状态", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8", "cwidth": 4}
+ },
+ {
+ "widgettype": "Text",
+ "options": {"text": "操作", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8", "cwidth": 6}
+ }
+ ]
+ },
+{% if credits|length == 0 %}
+ {
+ "widgettype": "VBox",
+ "options": {
+ "padding": "30px",
+ "alignItems": "center"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "暂无客户授信记录",
+ "fontSize": "14px",
+ "color": "#64748B"
+ }
+ }
+ ]
+ }
+{% else %}
+{% for c in credits %}
+ {
+ "widgettype": "HBox",
+ "options": {
+ "padding": "12px 16px",
+ "alignItems": "center",
+ "border": "0 0 1px 0",
+ "borderColor": "#334155"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "VBox",
+ "options": {"cwidth": 10, "gap": "2px"},
+ "subwidgets": [
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "{{c.orgname_text or c.accountid}}",
+ "fontSize": "13px",
+ "fontWeight": "500",
+ "color": "#F1F5F9"
+ }
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "{{c.subject_name or ''}}",
+ "fontSize": "11px",
+ "color": "#64748B"
+ }
+ }
+ ]
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "¥{{'%.2f' % c.credit_limit}}",
+ "fontSize": "13px",
+ "fontWeight": "600",
+ "color": "#3B82F6",
+ "cwidth": 8
+ }
+ },
+ {
+ "widgettype": "VBox",
+ "options": {"cwidth": 10, "gap": "2px"},
+ "subwidgets": [
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "已用 ¥{{'%.2f' % c.used_credit}}",
+ "fontSize": "12px",
+ "color": "#F59E0B"
+ }
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "剩余 ¥{{'%.2f' % c.available_credit}}",
+ "fontSize": "12px",
+ "color": "#22C55E"
+ }
+ }
+ ]
+ },
+ {
+ "widgettype": "HBox",
+ "options": {"cwidth": 6, "alignItems": "center", "gap": "4px"},
+ "subwidgets": [
+ {
+ "widgettype": "HBox",
+ "options": {
+ "bgcolor": "#334155",
+ "borderRadius": "3px",
+ "height": "6px",
+ "width": "50px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "HBox",
+ "options": {
+ "bgcolor": "{{'#22C55E' if c.usage_pct < 60 else ('#F59E0B' if c.usage_pct < 85 else '#EF4444')}}",
+ "borderRadius": "3px",
+ "height": "6px",
+ "width": "{{c.usage_pct}}%"
+ },
+ "subwidgets": []
+ }
+ ]
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "{{c.usage_pct}}%",
+ "fontSize": "11px",
+ "color": "{{'#22C55E' if c.usage_pct < 60 else ('#F59E0B' if c.usage_pct < 85 else '#EF4444')}}"
+ }
+ }
+ ]
+ },
+ {
+ "widgettype": "VBox",
+ "options": {"cwidth": 4},
+ "subwidgets": [
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "{{'生效' if c.status == 'active' else ('停用' if c.status == 'inactive' else '过期')}}",
+ "fontSize": "11px",
+ "fontWeight": "600",
+ "color": "{{'#22C55E' if c.status == 'active' else '#EF4444'}}"
+ }
+ }
+ ]
+ },
+ {
+ "widgettype": "HBox",
+ "options": {"cwidth": 6, "gap": "4px"},
+ "subwidgets": [
+ {
+ "widgettype": "Button",
+ "options": {
+ "label": "调整",
+ "bgcolor": "#475569",
+ "color": "#FFFFFF",
+ "borderRadius": "4px",
+ "padding": "4px 8px",
+ "fontSize": "11px"
+ },
+ "binds": [{
+ "wid": "self",
+ "event": "click",
+ "actiontype": "urlwidget",
+ "target": "PopupWindow",
+ "popup_options": {
+ "title": "调整授信额度",
+ "width": "480px",
+ "height": "520px"
+ },
+ "options": {
+ "url": "{{entire_url('/accounting/credit_limit/api/set_credit_form.ui')}}",
+ "params_kw": {
+ "id": "{{c.id}}",
+ "accountid": "{{c.accountid}}",
+ "credit_limit": "{{c.credit_limit}}",
+ "valid_from": "{{c.valid_from or ''}}",
+ "valid_to": "{{c.valid_to or ''}}",
+ "remark": "{{c.remark or ''}}"
+ }
+ }
+ }]
+ }
+ ]
+ }
+ ]
+ }{% if not loop.last %},{% endif %}
+{% endfor %}
+{% endif %}
+ ]
+ }
+ ]
+}
diff --git a/wwwroot/credit_limit/credit_overview.ui b/wwwroot/credit_limit/credit_overview.ui
new file mode 100644
index 0000000..b6cfd30
--- /dev/null
+++ b/wwwroot/credit_limit/credit_overview.ui
@@ -0,0 +1,289 @@
+{% set credits = get_my_credits_web(request) %}
+{
+ "widgettype": "VBox",
+ "options": {
+ "width": "100%",
+ "gap": "12px",
+ "padding": "4px"
+ },
+ "subwidgets": [
+{% if credits|length == 0 %}
+ {
+ "widgettype": "VBox",
+ "options": {
+ "bgcolor": "#1E293B",
+ "padding": "40px",
+ "borderRadius": "10px",
+ "border": "1px solid #334155",
+ "alignItems": "center"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "暂无信用额度记录",
+ "fontSize": "16px",
+ "color": "#94A3B8"
+ }
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "请联系您的分销商销售人员为您设置信用额度",
+ "fontSize": "13px",
+ "color": "#64748B",
+ "marginTop": "8px"
+ }
+ }
+ ]
+ }
+{% else %}
+{% for c in credits %}
+ {
+ "widgettype": "VBox",
+ "options": {
+ "bgcolor": "#1E293B",
+ "padding": "16px",
+ "borderRadius": "10px",
+ "border": "1px solid #334155",
+ "gap": "12px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "HBox",
+ "options": {
+ "alignItems": "center",
+ "justifyContent": "space-between"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "VBox",
+ "options": {"gap": "2px"},
+ "subwidgets": [
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "{{c.orgname_text or '未知客户'}}",
+ "fontSize": "16px",
+ "fontWeight": "600",
+ "color": "#F1F5F9"
+ }
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "{{c.subject_name or ''}} | 账户: {{c.accountid}}",
+ "fontSize": "12px",
+ "color": "#64748B"
+ }
+ }
+ ]
+ },
+ {
+ "widgettype": "VBox",
+ "options": {
+ "bgcolor": "{{'#16A34A22' if c.status == 'active' else '#EF444422'}}",
+ "padding": "4px 12px",
+ "borderRadius": "12px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "{{'生效' if c.status == 'active' else ('停用' if c.status == 'inactive' else '已过期')}}",
+ "fontSize": "12px",
+ "fontWeight": "600",
+ "color": "{{'#22C55E' if c.status == 'active' else '#EF4444'}}"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "widgettype": "HBox",
+ "options": {
+ "gap": "12px",
+ "alignItems": "center"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "VBox",
+ "options": {"flex": "1", "gap": "4px"},
+ "subwidgets": [
+ {
+ "widgettype": "HBox",
+ "options": {"justifyContent": "space-between"},
+ "subwidgets": [
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "额度使用",
+ "fontSize": "12px",
+ "color": "#94A3B8"
+ }
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "{{c.usage_pct}}%",
+ "fontSize": "12px",
+ "fontWeight": "600",
+ "color": "{{'#22C55E' if c.usage_pct < 60 else ('#F59E0B' if c.usage_pct < 85 else '#EF4444')}}"
+ }
+ }
+ ]
+ },
+ {
+ "widgettype": "HBox",
+ "options": {
+ "bgcolor": "#334155",
+ "borderRadius": "4px",
+ "height": "8px",
+ "width": "100%"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "HBox",
+ "options": {
+ "bgcolor": "{{'#22C55E' if c.usage_pct < 60 else ('#F59E0B' if c.usage_pct < 85 else '#EF4444')}}",
+ "borderRadius": "4px",
+ "height": "8px",
+ "width": "{{c.usage_pct}}%"
+ },
+ "subwidgets": []
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "widgettype": "HBox",
+ "options": {
+ "gap": "8px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "VBox",
+ "options": {
+ "flex": "1",
+ "bgcolor": "#0F172A",
+ "padding": "10px",
+ "borderRadius": "6px",
+ "alignItems": "center"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "¥{{'%.2f' % c.credit_limit}}",
+ "fontSize": "16px",
+ "fontWeight": "700",
+ "color": "#3B82F6"
+ }
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "授信额度",
+ "fontSize": "11px",
+ "color": "#64748B",
+ "marginTop": "2px"
+ }
+ }
+ ]
+ },
+ {
+ "widgettype": "VBox",
+ "options": {
+ "flex": "1",
+ "bgcolor": "#0F172A",
+ "padding": "10px",
+ "borderRadius": "6px",
+ "alignItems": "center"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "¥{{'%.2f' % c.used_credit}}",
+ "fontSize": "16px",
+ "fontWeight": "700",
+ "color": "#F59E0B"
+ }
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "已用额度",
+ "fontSize": "11px",
+ "color": "#64748B",
+ "marginTop": "2px"
+ }
+ }
+ ]
+ },
+ {
+ "widgettype": "VBox",
+ "options": {
+ "flex": "1",
+ "bgcolor": "#0F172A",
+ "padding": "10px",
+ "borderRadius": "6px",
+ "alignItems": "center"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "¥{{'%.2f' % c.available_credit}}",
+ "fontSize": "16px",
+ "fontWeight": "700",
+ "color": "#22C55E"
+ }
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "剩余额度",
+ "fontSize": "11px",
+ "color": "#64748B",
+ "marginTop": "2px"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "widgettype": "HBox",
+ "options": {
+ "justifyContent": "space-between"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "有效期: {{c.valid_from or '不限'}} ~ {{c.valid_to or '不限'}}",
+ "fontSize": "11px",
+ "color": "#64748B"
+ }
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "更新于 {{c.updated_at}}",
+ "fontSize": "11px",
+ "color": "#475569"
+ }
+ }
+ ]
+ }
+ ]
+ }{% if not loop.last %},{% endif %}
+{% endfor %}
+{% endif %}
+ ]
+}
diff --git a/wwwroot/credit_limit/hub.ui b/wwwroot/credit_limit/hub.ui
new file mode 100644
index 0000000..e1af8d7
--- /dev/null
+++ b/wwwroot/credit_limit/hub.ui
@@ -0,0 +1,301 @@
+{% set cstats = get_credit_stats_web(request) %}
+{
+ "widgettype": "VBox",
+ "options": {
+ "width": "100%",
+ "height": "100%",
+ "padding": "16px",
+ "gap": "16px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "信用额度管理",
+ "fontSize": "22px",
+ "fontWeight": "700",
+ "color": "#F1F5F9"
+ }
+ },
+ {
+ "widgettype": "ResponsableBox",
+ "options": {
+ "gap": "12px",
+ "minWidth": "200px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "VBox",
+ "options": {
+ "bgcolor": "#1E293B",
+ "padding": "16px",
+ "borderRadius": "10px",
+ "border": "1px solid #334155",
+ "flex": "1",
+ "minHeight": "100px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "HBox",
+ "options": {
+ "alignItems": "center",
+ "marginBottom": "8px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "Svg",
+ "options": {
+ "svg": "",
+ "width": "20px",
+ "height": "20px"
+ }
+ },
+ {"widgettype": "Filler"}
+ ]
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "¥{{'%.2f' % cstats.total_credit}}",
+ "fontSize": "26px",
+ "fontWeight": "700",
+ "color": "#3B82F6",
+ "lineHeight": "1.2"
+ }
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "授信总额度",
+ "fontSize": "13px",
+ "color": "#94A3B8",
+ "marginTop": "4px"
+ }
+ }
+ ]
+ },
+ {
+ "widgettype": "VBox",
+ "options": {
+ "bgcolor": "#1E293B",
+ "padding": "16px",
+ "borderRadius": "10px",
+ "border": "1px solid #334155",
+ "flex": "1",
+ "minHeight": "100px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "HBox",
+ "options": {
+ "alignItems": "center",
+ "marginBottom": "8px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "Svg",
+ "options": {
+ "svg": "",
+ "width": "20px",
+ "height": "20px"
+ }
+ },
+ {"widgettype": "Filler"}
+ ]
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "¥{{'%.2f' % cstats.total_used}}",
+ "fontSize": "26px",
+ "fontWeight": "700",
+ "color": "#F59E0B",
+ "lineHeight": "1.2"
+ }
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "已用额度",
+ "fontSize": "13px",
+ "color": "#94A3B8",
+ "marginTop": "4px"
+ }
+ }
+ ]
+ },
+ {
+ "widgettype": "VBox",
+ "options": {
+ "bgcolor": "#1E293B",
+ "padding": "16px",
+ "borderRadius": "10px",
+ "border": "1px solid #334155",
+ "flex": "1",
+ "minHeight": "100px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "HBox",
+ "options": {
+ "alignItems": "center",
+ "marginBottom": "8px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "Svg",
+ "options": {
+ "svg": "",
+ "width": "20px",
+ "height": "20px"
+ }
+ },
+ {"widgettype": "Filler"}
+ ]
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "¥{{'%.2f' % cstats.total_available}}",
+ "fontSize": "26px",
+ "fontWeight": "700",
+ "color": "#22C55E",
+ "lineHeight": "1.2"
+ }
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "剩余额度",
+ "fontSize": "13px",
+ "color": "#94A3B8",
+ "marginTop": "4px"
+ }
+ }
+ ]
+ },
+ {
+ "widgettype": "VBox",
+ "options": {
+ "bgcolor": "#1E293B",
+ "padding": "16px",
+ "borderRadius": "10px",
+ "border": "1px solid #334155",
+ "flex": "1",
+ "minHeight": "100px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "HBox",
+ "options": {
+ "alignItems": "center",
+ "marginBottom": "8px"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "Svg",
+ "options": {
+ "svg": "",
+ "width": "20px",
+ "height": "20px"
+ }
+ },
+ {"widgettype": "Filler"}
+ ]
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "{{cstats.usage_pct}}%",
+ "fontSize": "26px",
+ "fontWeight": "700",
+ "color": "#A78BFA",
+ "lineHeight": "1.2"
+ }
+ },
+ {
+ "widgettype": "Text",
+ "options": {
+ "text": "额度使用率 ({{cstats.active_count}}/{{cstats.customer_count}}户)",
+ "fontSize": "13px",
+ "color": "#94A3B8",
+ "marginTop": "4px"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "widgettype": "HBox",
+ "options": {
+ "gap": "8px",
+ "alignItems": "center"
+ },
+ "subwidgets": [
+ {
+ "widgettype": "Button",
+ "options": {
+ "label": "我的额度",
+ "bgcolor": "#3B82F6",
+ "color": "#FFFFFF",
+ "borderRadius": "6px",
+ "padding": "8px 16px"
+ },
+ "binds": [{
+ "wid": "self",
+ "event": "click",
+ "actiontype": "urlwidget",
+ "target": "app.credit_content",
+ "options": {"url": "{{entire_url('/accounting/credit_limit/credit_overview.ui')}}"},
+ "mode": "replace"
+ }]
+ },
+ {
+ "widgettype": "Button",
+ "options": {
+ "label": "客户额度管理",
+ "bgcolor": "#475569",
+ "color": "#FFFFFF",
+ "borderRadius": "6px",
+ "padding": "8px 16px"
+ },
+ "binds": [{
+ "wid": "self",
+ "event": "click",
+ "actiontype": "urlwidget",
+ "target": "app.credit_content",
+ "options": {"url": "{{entire_url('/accounting/credit_limit/credit_manage.ui')}}"},
+ "mode": "replace"
+ }]
+ },
+ {
+ "widgettype": "Button",
+ "options": {
+ "label": "全部客户查询",
+ "bgcolor": "#475569",
+ "color": "#FFFFFF",
+ "borderRadius": "6px",
+ "padding": "8px 16px"
+ },
+ "binds": [{
+ "wid": "self",
+ "event": "click",
+ "actiontype": "urlwidget",
+ "target": "app.credit_content",
+ "options": {"url": "{{entire_url('/accounting/credit_limit/index.ui')}}"},
+ "mode": "replace"
+ }]
+ }
+ ]
+ },
+ {
+ "widgettype": "VBox",
+ "id": "credit_content",
+ "options": {
+ "width": "100%",
+ "flex": "1"
+ }
+ }
+ ]
+}