mirror of
https://github.com/wahyd4/links.git
synced 2026-08-08 21:04:53 +10:00
feat(invest): P0 dashboard polish — intcomma amounts, ticker-aggregated risk table, unified mobile grid, tx source badges, drop dead invest/base.html
- Add django.contrib.humanize; apply intcomma to every currency amount across invest templates (dashboard, portfolio detail, transactions)
- get_risk_summary now aggregates top_positions by ticker (MRVL across MOMO+IBKR merges into one row with Accounts column), keeping Top 1/3/5 cards and table on the same basis
- Mobile: metric cards all 2-col (2x2 + 2+1 with Last Snapshot spanning full width), no more 1-col break
- Transactions show source badges: purple 🤖 AI (source ai/ocr), grey 手动, amber ⚠ 低置信 when confidence < 0.9
- Delete unused invest/base.html (dead nav; page extends GoLinks base.html)
- tailwind.config.js: add invest/jbot/routermon template dirs so their classes are scanned (text-[10px] etc. were silently missing)
This commit is contained in:
@@ -13,6 +13,7 @@ INSTALLED_APPS = [
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles', # Make sure this is here only once
|
||||
'django.contrib.humanize', # intcomma etc. for invest dashboard amounts
|
||||
'widget_tweaks',
|
||||
'core.apps.CoreConfig',
|
||||
'links',
|
||||
|
||||
+23
-18
@@ -8,7 +8,6 @@ Design goals:
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from datetime import date as date_cls
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
@@ -518,24 +517,30 @@ def get_risk_summary() -> dict:
|
||||
})
|
||||
|
||||
total_value = sum(h['current_value'] for h in holdings)
|
||||
holdings.sort(key=lambda h: h['current_value'], reverse=True)
|
||||
|
||||
for holding in holdings:
|
||||
holding['weight'] = round(holding['current_value'] / total_value, 6) if total_value else 0
|
||||
# 集中度口径统一:按 ticker 聚合(同一股票跨账户合并),
|
||||
# 与 Top 1/3/5 权重卡片一致,避免 MRVL 等跨账户持仓在表格里重复出现。
|
||||
by_ticker: dict[str, dict] = {}
|
||||
for h in holdings:
|
||||
agg = by_ticker.setdefault(h['stock_code'], {
|
||||
'stock_code': h['stock_code'],
|
||||
'current_value': 0.0,
|
||||
'portfolio_names': [],
|
||||
})
|
||||
agg['current_value'] += h['current_value']
|
||||
if h['portfolio_name'] not in agg['portfolio_names']:
|
||||
agg['portfolio_names'].append(h['portfolio_name'])
|
||||
|
||||
top_1 = holdings[0]['weight'] if holdings else 0
|
||||
top_3 = sum(h['weight'] for h in holdings[:3])
|
||||
top_5 = sum(h['weight'] for h in holdings[:5])
|
||||
top_positions = sorted(by_ticker.values(), key=lambda r: r['current_value'], reverse=True)
|
||||
for row in top_positions:
|
||||
row['weight'] = round(row['current_value'] / total_value, 6) if total_value else 0
|
||||
|
||||
by_ticker = defaultdict(float)
|
||||
for holding in holdings:
|
||||
by_ticker[holding['stock_code']] += holding['current_value']
|
||||
ticker_weights = {
|
||||
ticker: value / total_value for ticker, value in by_ticker.items()
|
||||
} if total_value else {}
|
||||
top_1 = top_positions[0]['weight'] if top_positions else 0
|
||||
top_3 = sum(r['weight'] for r in top_positions[:3])
|
||||
top_5 = sum(r['weight'] for r in top_positions[:5])
|
||||
|
||||
semi_weight = sum(weight for ticker, weight in ticker_weights.items() if ticker in SEMI_TICKERS)
|
||||
ai_cloud_weight = sum(weight for ticker, weight in ticker_weights.items() if ticker in AI_CLOUD_TICKERS)
|
||||
semi_weight = sum(r['weight'] for r in top_positions if r['stock_code'] in SEMI_TICKERS)
|
||||
ai_cloud_weight = sum(r['weight'] for r in top_positions if r['stock_code'] in AI_CLOUD_TICKERS)
|
||||
|
||||
concentration_level = 'LOW'
|
||||
if top_1 >= 0.25 or top_5 >= 0.70:
|
||||
@@ -545,13 +550,13 @@ def get_risk_summary() -> dict:
|
||||
|
||||
return {
|
||||
'total_value': round(total_value, 2),
|
||||
'position_count': len(holdings),
|
||||
'position_count': len(top_positions),
|
||||
'top_1_weight': round(top_1, 6),
|
||||
'top_3_weight': round(top_3, 6),
|
||||
'top_5_weight': round(top_5, 6),
|
||||
'concentration_level': concentration_level,
|
||||
'max_position': holdings[0] if holdings else None,
|
||||
'top_positions': holdings[:10],
|
||||
'max_position': top_positions[0] if top_positions else None,
|
||||
'top_positions': top_positions[:10],
|
||||
'theme_exposure': {
|
||||
'semiconductors': round(semi_weight, 6),
|
||||
'ai_cloud': round(ai_cloud_weight, 6),
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Portfolio{% endblock %} – Invest</title>
|
||||
<link href="{% static 'css/dist/styles.css' %}" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" rel="stylesheet">
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
{% block extra_head %}{% endblock %}
|
||||
</head>
|
||||
<body class="bg-stone-100 min-h-screen">
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="bg-gray-900 text-white px-6 py-4 shadow-lg">
|
||||
<div class="max-w-7xl mx-auto flex items-center justify-between">
|
||||
<div class="flex items-center space-x-6">
|
||||
<a href="{% url 'invest-dashboard' %}" class="text-lg font-bold text-white flex items-center">
|
||||
<i class="fas fa-chart-line mr-2 text-green-400"></i>Invest
|
||||
</a>
|
||||
<a href="{% url 'invest-dashboard' %}" class="text-gray-300 hover:text-white text-sm">Portfolios</a>
|
||||
</div>
|
||||
<a href="{% url 'link_list' %}" class="text-gray-400 hover:text-white text-sm">
|
||||
<i class="fas fa-arrow-left mr-1"></i>GoLinks
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static i18n %}
|
||||
{% load static i18n humanize %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script>
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="bg-white rounded-lg p-5 shadow-sm">
|
||||
<p class="text-xs font-semibold tracking-widest text-stone-400 uppercase mb-1">Total Value</p>
|
||||
{% if overview.this_week_total is not None %}
|
||||
<p class="text-3xl font-bold text-stone-900">${{ overview.this_week_total|floatformat:0 }}</p>
|
||||
<p class="text-3xl font-bold text-stone-900">${{ overview.this_week_total|floatformat:0|intcomma }}</p>
|
||||
{% else %}
|
||||
<p class="text-3xl font-bold text-stone-400">—</p>
|
||||
{% endif %}
|
||||
@@ -26,14 +26,14 @@
|
||||
|
||||
<div class="bg-white rounded-lg p-5 shadow-sm">
|
||||
<p class="text-xs font-semibold tracking-widest text-stone-400 uppercase mb-1">Net Contributions</p>
|
||||
<p class="text-3xl font-bold text-blue-800">${{ net_contributions|floatformat:0 }}</p>
|
||||
<p class="text-3xl font-bold text-stone-900">${{ net_contributions|floatformat:0|intcomma }}</p>
|
||||
<p class="text-sm text-stone-400 mt-1">External deposits minus withdrawals</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg p-5 shadow-sm">
|
||||
<p class="text-xs font-semibold tracking-widest text-stone-400 uppercase mb-1">Investment Gain</p>
|
||||
<p class="text-3xl font-bold {% if performance.cash_adjusted_gain >= 0 %}text-green-800{% else %}text-red-700{% endif %}">
|
||||
{% if performance.cash_adjusted_gain >= 0 %}+{% endif %}${{ performance.cash_adjusted_gain|floatformat:0 }}
|
||||
{% if performance.cash_adjusted_gain >= 0 %}+{% endif %}${{ performance.cash_adjusted_gain|floatformat:0|intcomma }}
|
||||
</p>
|
||||
<p class="text-sm text-stone-400 mt-1">Cash-flow adjusted</p>
|
||||
</div>
|
||||
@@ -47,14 +47,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-3 mb-3">
|
||||
<div class="grid grid-cols-2 lg:grid-cols-3 gap-3 mb-3">
|
||||
<div class="bg-white rounded-lg p-5 shadow-sm">
|
||||
<p class="text-xs font-semibold tracking-widest text-stone-400 uppercase mb-1">This Week</p>
|
||||
{% if overview.week_gain is not None %}
|
||||
<p class="text-2xl font-bold {% if overview.week_gain >= 0 %}text-green-800{% else %}text-red-700{% endif %}">
|
||||
{% if overview.week_gain >= 0 %}+{% endif %}${{ overview.week_gain|floatformat:0 }}
|
||||
{% if overview.week_gain >= 0 %}+{% endif %}${{ overview.week_gain|floatformat:0|intcomma }}
|
||||
</p>
|
||||
<p class="text-sm text-stone-400 mt-1">{% if overview.week_gain >= 0 %}+{% endif %}{{ overview.week_change_pct|floatformat:2 }}% vs last snapshot</p>
|
||||
<p class="text-sm text-stone-400 mt-1">{% if overview.week_change_pct >= 0 %}+{% endif %}{{ overview.week_change_pct|floatformat:2 }}% vs last snapshot</p>
|
||||
{% else %}
|
||||
<p class="text-2xl font-bold text-stone-400">—</p>
|
||||
<p class="text-sm text-stone-400 mt-1">No prior snapshot</p>
|
||||
@@ -71,7 +71,7 @@
|
||||
<p class="text-sm text-stone-400 mt-1">IRR based on cash flows</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg p-5 shadow-sm">
|
||||
<div class="col-span-2 lg:col-span-1 bg-white rounded-lg p-5 shadow-sm">
|
||||
<p class="text-xs font-semibold tracking-widest text-stone-400 uppercase mb-1">Last Snapshot</p>
|
||||
{% if overview.this_week_date %}
|
||||
<p class="text-2xl font-bold text-stone-900">{{ overview.this_week_date|date:"M j" }}</p>
|
||||
@@ -89,12 +89,12 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<p class="text-sm text-stone-400">Actual end value</p>
|
||||
<p class="text-xl font-bold text-stone-900">${{ performance.end_value|floatformat:0 }}</p>
|
||||
<p class="text-xl font-bold text-stone-900">${{ performance.end_value|floatformat:0|intcomma }}</p>
|
||||
</div>
|
||||
{% for ticker, bench in performance.benchmarks.items %}
|
||||
<div>
|
||||
<p class="text-sm text-stone-400">Same cash flows into {{ ticker }}</p>
|
||||
<p class="text-xl font-bold text-stone-900">${{ bench.end_value|floatformat:0 }}</p>
|
||||
<p class="text-xl font-bold text-stone-900">${{ bench.end_value|floatformat:0|intcomma }}</p>
|
||||
<p class="text-xs text-stone-400">Return {% if bench.simple_return is not None %}{% widthratio bench.simple_return 1 100 %}%{% else %}—{% endif %}</p>
|
||||
</div>
|
||||
{% empty %}
|
||||
@@ -137,16 +137,16 @@
|
||||
<th class="py-2 text-left">Ticker</th>
|
||||
<th class="py-2 text-right">Value</th>
|
||||
<th class="py-2 text-right">Weight</th>
|
||||
<th class="py-2 text-left">Portfolio</th>
|
||||
<th class="py-2 text-left">Accounts</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for position in risk.top_positions|slice:":5" %}
|
||||
<tr class="border-b border-stone-50">
|
||||
<td class="py-2 font-medium">{{ position.stock_code }}</td>
|
||||
<td class="py-2 text-right">${{ position.current_value|floatformat:0 }}</td>
|
||||
<td class="py-2 text-right">${{ position.current_value|floatformat:0|intcomma }}</td>
|
||||
<td class="py-2 text-right">{% widthratio position.weight 1 100 %}%</td>
|
||||
<td class="py-2 text-stone-500">{{ position.portfolio_name }}</td>
|
||||
<td class="py-2 text-stone-500">{{ position.portfolio_names|join:", " }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -167,9 +167,9 @@
|
||||
<p class="text-xs text-stone-400 mt-0.5">{{ group.position_count }} position{{ group.position_count|pluralize }}{% if overview.this_week_date %} · Snapshot {{ overview.this_week_date|date:"j M Y" }}{% endif %}</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="font-bold text-stone-900 text-sm">{% if group.this_week_value is not None %}${{ group.this_week_value|floatformat:0 }}{% else %}<span class="text-stone-300">—</span>{% endif %}</p>
|
||||
<p class="font-bold text-stone-900 text-sm">{% if group.this_week_value is not None %}${{ group.this_week_value|floatformat:0|intcomma }}{% else %}<span class="text-stone-300">—</span>{% endif %}</p>
|
||||
{% if group.change is not None %}
|
||||
<p class="text-xs font-medium mt-0.5 {% if group.change >= 0 %}text-green-700{% else %}text-red-600{% endif %}">{% if group.change >= 0 %}+{% endif %}${{ group.change|floatformat:0 }} ({% if group.change_pct >= 0 %}+{% endif %}{{ group.change_pct|floatformat:1 }}%)</p>
|
||||
<p class="text-xs font-medium mt-0.5 {% if group.change >= 0 %}text-green-700{% else %}text-red-600{% endif %}">{% if group.change >= 0 %}+{% endif %}${{ group.change|floatformat:0|intcomma }} ({% if group.change_pct >= 0 %}+{% endif %}{{ group.change_pct|floatformat:1 }}%)</p>
|
||||
{% else %}
|
||||
<p class="text-xs text-stone-300 mt-0.5">No prior snapshot</p>
|
||||
{% endif %}
|
||||
@@ -189,10 +189,10 @@
|
||||
{% for stock in group.holdings %}
|
||||
<tr class="border-b border-stone-50 hover:bg-stone-50 {{ group.colors.row }}">
|
||||
<td class="px-6 py-3 font-medium text-stone-900"><span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium {{ group.colors.badge }}">{{ stock.stock_code }}</span></td>
|
||||
<td class="px-6 py-3 text-right text-stone-500">{{ stock.quantity|floatformat:0 }}</td>
|
||||
<td class="px-6 py-3 text-right text-stone-500">{% if stock.current_price %}${{ stock.current_price|floatformat:2 }}{% else %}<span class="text-stone-300">—</span>{% endif %}</td>
|
||||
<td class="px-6 py-3 text-right {% if stock.value_change >= 0 %}text-green-700{% elif stock.value_change < 0 %}text-red-600{% else %}text-stone-300{% endif %}">{% if stock.value_change is not None %}{% if stock.value_change >= 0 %}+{% endif %}${{ stock.value_change|floatformat:0 }} ({% if stock.price_change_pct >= 0 %}+{% endif %}{{ stock.price_change_pct|floatformat:1 }}%){% else %}—{% endif %}</td>
|
||||
<td class="px-6 py-3 text-right font-semibold text-stone-800">{% if stock.current_value %}${{ stock.current_value|floatformat:0 }}{% else %}<span class="text-stone-300">—</span>{% endif %}</td>
|
||||
<td class="px-6 py-3 text-right text-stone-500">{{ stock.quantity|floatformat:0|intcomma }}</td>
|
||||
<td class="px-6 py-3 text-right text-stone-500">{% if stock.current_price %}${{ stock.current_price|floatformat:2|intcomma }}{% else %}<span class="text-stone-300">—</span>{% endif %}</td>
|
||||
<td class="px-6 py-3 text-right {% if stock.value_change >= 0 %}text-green-700{% elif stock.value_change < 0 %}text-red-600{% else %}text-stone-300{% endif %}">{% if stock.value_change is not None %}{% if stock.value_change >= 0 %}+{% endif %}${{ stock.value_change|floatformat:0|intcomma }} ({% if stock.price_change_pct >= 0 %}+{% endif %}{{ stock.price_change_pct|floatformat:1 }}%){% else %}—{% endif %}</td>
|
||||
<td class="px-6 py-3 text-right font-semibold text-stone-800">{% if stock.current_value %}${{ stock.current_value|floatformat:0|intcomma }}{% else %}<span class="text-stone-300">—</span>{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -245,6 +245,7 @@
|
||||
<th class="py-2 text-right">Qty</th>
|
||||
<th class="py-2 text-right">Price</th>
|
||||
<th class="py-2 text-right">Fee</th>
|
||||
<th class="py-2 text-left">Source</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -254,9 +255,19 @@
|
||||
<td class="py-2">{{ tx.portfolio.name }}</td>
|
||||
<td class="py-2">{{ tx.action }}</td>
|
||||
<td class="py-2 font-medium">{{ tx.stock_code }}</td>
|
||||
<td class="py-2 text-right">{{ tx.quantity|floatformat:0 }}</td>
|
||||
<td class="py-2 text-right">{% if tx.price_per_share %}${{ tx.price_per_share|floatformat:2 }}{% else %}<span class="text-stone-300">optional</span>{% endif %}</td>
|
||||
<td class="py-2 text-right">{% if tx.fee %}${{ tx.fee|floatformat:2 }}{% else %}<span class="text-stone-300">—</span>{% endif %}</td>
|
||||
<td class="py-2 text-right">{{ tx.quantity|floatformat:0|intcomma }}</td>
|
||||
<td class="py-2 text-right">{% if tx.price_per_share %}${{ tx.price_per_share|floatformat:2|intcomma }}{% else %}<span class="text-stone-300">optional</span>{% endif %}</td>
|
||||
<td class="py-2 text-right">{% if tx.fee %}${{ tx.fee|floatformat:2|intcomma }}{% else %}<span class="text-stone-300">—</span>{% endif %}</td>
|
||||
<td class="py-2">
|
||||
{% if tx.source %}
|
||||
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium {% if tx.source|lower == 'ai' or tx.source|lower == 'ocr' %}bg-purple-100 text-purple-700{% else %}bg-stone-100 text-stone-500{% endif %}">
|
||||
{% if tx.source|lower == 'ai' or tx.source|lower == 'ocr' %}🤖 AI{% else %}手动{% endif %}
|
||||
</span>
|
||||
{% if tx.confidence is not None and tx.confidence < 0.9 %}
|
||||
<span class="ml-1 inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-amber-100 text-amber-700" title="AI 识别置信度 {{ tx.confidence|floatformat:2 }}">⚠ 低置信</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static i18n %}
|
||||
{% load static i18n humanize %}
|
||||
|
||||
{% block title %}{{ portfolio.name }} - Portfolio{% endblock %}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-4">
|
||||
<div class="bg-white rounded-lg p-5 shadow-sm">
|
||||
<p class="text-xs font-semibold tracking-widest text-stone-400 uppercase mb-1">Live Total Value</p>
|
||||
<p class="text-3xl font-bold text-stone-900">${{ summary.total_value|floatformat:0 }}</p>
|
||||
<p class="text-3xl font-bold text-stone-900">${{ summary.total_value|floatformat:0|intcomma }}</p>
|
||||
<p class="text-sm text-stone-400 mt-1">{{ summary.holdings|length }} position{{ summary.holdings|length|pluralize }}</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg p-5 shadow-sm">
|
||||
@@ -47,18 +47,18 @@
|
||||
{% for stock in summary.holdings %}
|
||||
<tr class="hover:bg-stone-50">
|
||||
<td class="px-6 py-3 font-semibold text-stone-900">{{ stock.stock_code }}</td>
|
||||
<td class="px-6 py-3 text-right text-stone-600">{{ stock.quantity|floatformat:2 }}</td>
|
||||
<td class="px-6 py-3 text-right text-stone-600">{{ stock.quantity|floatformat:2|intcomma }}</td>
|
||||
<td class="px-6 py-3 text-right text-stone-600">
|
||||
{% if stock.current_price > 0 %}${{ stock.current_price|floatformat:2 }}{% else %}<span class="text-stone-300">—</span>{% endif %}
|
||||
{% if stock.current_price > 0 %}${{ stock.current_price|floatformat:2|intcomma }}{% else %}<span class="text-stone-300">—</span>{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-3 text-right font-medium text-stone-900">${{ stock.current_value|floatformat:0 }}</td>
|
||||
<td class="px-6 py-3 text-right font-medium text-stone-900">${{ stock.current_value|floatformat:0|intcomma }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="border-t-2 border-stone-200 bg-stone-50">
|
||||
<td class="px-6 py-3 font-semibold text-stone-500 text-xs uppercase tracking-wide" colspan="3">Total</td>
|
||||
<td class="px-6 py-3 text-right font-bold text-stone-900">${{ summary.total_value|floatformat:0 }}</td>
|
||||
<td class="px-6 py-3 text-right font-bold text-stone-900">${{ summary.total_value|floatformat:0|intcomma }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static i18n %}
|
||||
{% load static i18n humanize %}
|
||||
|
||||
{% block title %}{{ portfolio.name }} - Transactions{% endblock %}
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
<th class="pb-3 text-left">Action</th>
|
||||
<th class="pb-3 text-left">Stock</th>
|
||||
<th class="pb-3 text-right">Quantity</th>
|
||||
<th class="pb-3 text-left">Source</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-stone-50">
|
||||
@@ -43,7 +44,17 @@
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-3 font-semibold text-stone-900">{{ tx.stock_code }}</td>
|
||||
<td class="py-3 text-right text-stone-600">{{ tx.quantity|floatformat:2 }}</td>
|
||||
<td class="py-3 text-right text-stone-600">{{ tx.quantity|floatformat:2|intcomma }}</td>
|
||||
<td class="py-3">
|
||||
{% if tx.source %}
|
||||
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium {% if tx.source|lower == 'ai' or tx.source|lower == 'ocr' %}bg-purple-100 text-purple-700{% else %}bg-stone-100 text-stone-500{% endif %}">
|
||||
{% if tx.source|lower == 'ai' or tx.source|lower == 'ocr' %}🤖 AI{% else %}手动{% endif %}
|
||||
</span>
|
||||
{% if tx.confidence is not None and tx.confidence < 0.9 %}
|
||||
<span class="ml-1 inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-amber-100 text-amber-700" title="AI 识别置信度 {{ tx.confidence|floatformat:2 }}">⚠ 低置信</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -5,6 +5,9 @@ module.exports = {
|
||||
'./new_theme/templates/**/*.html',
|
||||
'./netscan/templates/**/*.html',
|
||||
'./nginxmon/templates/**/*.html',
|
||||
'./routermon/templates/**/*.html',
|
||||
'./invest/templates/**/*.html',
|
||||
'./jbot/templates/**/*.html',
|
||||
'./static_src/**/*.{js,jsx}',
|
||||
'./**/*.js',
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user