main #134

Merged
charles merged 49 commits from main into prod 2026-07-10 17:41:19 +08:00
Showing only changes of commit bd716b467d - Show all commits

View File

@ -0,0 +1,800 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>财务结算中心</title>
<style>
:root {
--primary: #246bfe;
--primary-soft: #e9f0ff;
--cyan: #00a9d6;
--green: #0aa66a;
--orange: #f59e0b;
--red: #ef4444;
--text: #1f2937;
--muted: #667085;
--line: #dbe7ff;
--panel: rgba(255, 255, 255, 0.88);
--bg: #f5f9ff;
--shadow: 0 18px 50px rgba(36, 107, 254, 0.12);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
color: var(--text);
font-family: "Inter", "PingFang SC", "Microsoft YaHei", Arial, sans-serif;
background:
radial-gradient(circle at 12% 10%, rgba(36, 107, 254, 0.14), transparent 30%),
radial-gradient(circle at 85% 8%, rgba(0, 169, 214, 0.13), transparent 28%),
linear-gradient(135deg, #f7fbff 0%, #eef6ff 48%, #ffffff 100%);
}
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
opacity: 0.58;
background-image:
linear-gradient(rgba(36, 107, 254, 0.08) 1px, transparent 1px),
linear-gradient(90deg, rgba(36, 107, 254, 0.08) 1px, transparent 1px);
background-size: 28px 28px;
mask-image: linear-gradient(to bottom, black, transparent 75%);
}
.page {
position: relative;
width: min(1440px, calc(100% - 40px));
margin: 0 auto;
padding: 28px 0 44px;
}
.hero {
display: grid;
grid-template-columns: 1.4fr 0.8fr;
gap: 20px;
align-items: stretch;
margin-bottom: 20px;
}
.hero-card,
.panel,
.metric-card {
border: 1px solid rgba(145, 178, 255, 0.46);
border-radius: 22px;
background: var(--panel);
box-shadow: var(--shadow);
backdrop-filter: blur(16px);
}
.hero-card {
position: relative;
overflow: hidden;
padding: 30px;
}
.hero-card::after {
content: "";
position: absolute;
right: -80px;
top: -80px;
width: 240px;
height: 240px;
border-radius: 50%;
background: radial-gradient(circle, rgba(36, 107, 254, 0.18), transparent 68%);
}
.eyebrow {
display: inline-flex;
gap: 8px;
align-items: center;
padding: 7px 12px;
border: 1px solid rgba(36, 107, 254, 0.18);
border-radius: 999px;
color: var(--primary);
background: rgba(36, 107, 254, 0.08);
font-size: 13px;
font-weight: 700;
}
.pulse {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--green);
box-shadow: 0 0 0 6px rgba(10, 166, 106, 0.14);
}
h1 {
margin: 18px 0 10px;
font-size: clamp(30px, 4vw, 48px);
letter-spacing: -1px;
}
.hero p {
margin: 0;
max-width: 760px;
color: var(--muted);
line-height: 1.8;
}
.hero-side {
padding: 24px;
display: grid;
gap: 14px;
}
.status-chip {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px;
border-radius: 16px;
background: linear-gradient(135deg, rgba(36, 107, 254, 0.08), rgba(255, 255, 255, 0.88));
border: 1px solid rgba(36, 107, 254, 0.12);
}
.status-chip strong {
font-size: 20px;
color: var(--primary);
}
.metrics {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 20px;
}
.metric-card {
padding: 18px;
min-height: 116px;
}
.metric-label {
color: var(--muted);
font-size: 13px;
}
.metric-value {
margin-top: 12px;
font-size: 28px;
font-weight: 800;
color: var(--primary);
word-break: break-all;
}
.metric-foot {
margin-top: 8px;
color: var(--muted);
font-size: 12px;
}
.layout {
display: grid;
grid-template-columns: 380px 1fr;
gap: 20px;
}
.panel {
padding: 20px;
}
.panel-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 16px;
}
.panel-title h2 {
margin: 0;
font-size: 18px;
}
.badge {
padding: 5px 10px;
border-radius: 999px;
color: var(--primary);
background: var(--primary-soft);
font-size: 12px;
font-weight: 700;
}
.form-grid {
display: grid;
gap: 14px;
}
.field {
display: grid;
gap: 7px;
}
label {
color: #344054;
font-size: 13px;
font-weight: 700;
}
input,
select {
width: 100%;
height: 42px;
border: 1px solid var(--line);
border-radius: 12px;
outline: none;
padding: 0 12px;
color: var(--text);
background: rgba(255, 255, 255, 0.92);
transition: 0.18s ease;
}
input:focus,
select:focus {
border-color: var(--primary);
box-shadow: 0 0 0 4px rgba(36, 107, 254, 0.12);
}
.two {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 6px;
}
button {
height: 42px;
border: 0;
border-radius: 12px;
cursor: pointer;
color: #fff;
background: linear-gradient(135deg, var(--primary), #4f8cff);
font-weight: 800;
box-shadow: 0 10px 24px rgba(36, 107, 254, 0.22);
transition: transform 0.18s ease, box-shadow 0.18s ease;
}
button:hover {
transform: translateY(-1px);
box-shadow: 0 14px 30px rgba(36, 107, 254, 0.28);
}
button.secondary {
color: var(--primary);
border: 1px solid rgba(36, 107, 254, 0.22);
background: #fff;
box-shadow: none;
}
button.green {
background: linear-gradient(135deg, var(--green), #32c28a);
}
button.orange {
background: linear-gradient(135deg, var(--orange), #fbbf24);
}
.tabs {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.tab {
padding: 10px 14px;
border: 1px solid var(--line);
border-radius: 999px;
color: var(--muted);
background: #fff;
cursor: pointer;
font-size: 13px;
font-weight: 800;
}
.tab.active {
color: #fff;
background: linear-gradient(135deg, var(--primary), var(--cyan));
border-color: transparent;
}
.table-wrap {
overflow: auto;
border: 1px solid rgba(145, 178, 255, 0.38);
border-radius: 16px;
background: #fff;
}
table {
width: 100%;
min-width: 980px;
border-collapse: collapse;
}
th,
td {
padding: 13px 14px;
text-align: left;
border-bottom: 1px solid #edf2ff;
font-size: 13px;
white-space: nowrap;
}
th {
position: sticky;
top: 0;
z-index: 1;
color: #31507a;
background: linear-gradient(180deg, #f6f9ff, #ffffff);
font-weight: 900;
}
tr:hover td {
background: #f8fbff;
}
.tag {
display: inline-flex;
padding: 4px 9px;
border-radius: 999px;
color: var(--primary);
background: var(--primary-soft);
font-size: 12px;
font-weight: 800;
}
.log {
min-height: 170px;
max-height: 340px;
overflow: auto;
padding: 14px;
border: 1px solid rgba(145, 178, 255, 0.38);
border-radius: 16px;
background: #fbfdff;
color: #344054;
font-family: Consolas, Monaco, monospace;
font-size: 12px;
line-height: 1.7;
white-space: pre-wrap;
}
.hint {
margin-top: 12px;
color: var(--muted);
font-size: 12px;
line-height: 1.7;
}
@media (max-width: 1100px) {
.hero,
.layout,
.metrics {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<main class="page">
<section class="hero">
<div class="hero-card">
<span class="eyebrow"><span class="pulse"></span> Finance Settlement Console</span>
<h1>财务结算中心</h1>
<p>
面向供应商与分销商的日结、月结费用查询和结算处理页面。支持汇总查询、账单预览、结算单创建、审批提交、审批回调和结算单追踪。
</p>
</div>
<div class="hero-card hero-side">
<div class="status-chip">
<span>接口状态</span>
<strong id="apiStatus">Ready</strong>
</div>
<div class="status-chip">
<span>当前模式</span>
<strong id="modeText">供应商 / 日结</strong>
</div>
</div>
</section>
<section class="metrics">
<div class="metric-card">
<div class="metric-label">销售金额</div>
<div class="metric-value" id="salesAmount">0.00000000</div>
<div class="metric-foot">sales_amount</div>
</div>
<div class="metric-card">
<div class="metric-label">结算金额</div>
<div class="metric-value" id="settlementAmount">0.00000000</div>
<div class="metric-foot">settlement_amount</div>
</div>
<div class="metric-card">
<div class="metric-label">平台收入</div>
<div class="metric-value" id="platformIncomeAmount">0.00000000</div>
<div class="metric-foot">platform_income_amount</div>
</div>
<div class="metric-card">
<div class="metric-label">账单数量</div>
<div class="metric-value" id="billCount">0</div>
<div class="metric-foot">bill_count</div>
</div>
</section>
<section class="layout">
<aside class="panel">
<div class="panel-title">
<h2>查询条件</h2>
<span class="badge">Light Tech UI</span>
</div>
<div class="form-grid">
<div class="field">
<label>账本机构 accounting_orgid</label>
<input id="accountingOrgid" placeholder="请输入账本机构ID" />
</div>
<div class="two">
<div class="field">
<label>对手方类型</label>
<select id="counterpartyType">
<option value="supplier">供应商 supplier</option>
<option value="reseller">分销商 reseller</option>
</select>
</div>
<div class="field">
<label>账期类型</label>
<select id="periodType">
<option value="day">日结 day</option>
<option value="month">月结 month</option>
</select>
</div>
</div>
<div class="field">
<label>对手方机构 counterparty_orgid</label>
<input id="counterpartyOrgid" placeholder="汇总可不填;预览/创建必填" />
</div>
<div class="two">
<div class="field">
<label>开始日期</label>
<input id="startDate" type="date" />
</div>
<div class="field">
<label>结束日期</label>
<input id="endDate" type="date" />
</div>
</div>
<div class="two">
<div class="field">
<label>页码</label>
<input id="currentPage" type="number" min="1" value="1" />
</div>
<div class="field">
<label>每页数量</label>
<input id="pageSize" type="number" min="1" value="20" />
</div>
</div>
<div class="field">
<label>结算单 ID settlement_id</label>
<input id="settlementId" placeholder="提交审批/详情时填写" />
</div>
<div class="field">
<label>用户 ID userid</label>
<input id="userid" placeholder="创建/提交审批时填写" />
</div>
<div class="field">
<label>审批 ID apv_id / approval_id</label>
<input id="approvalId" placeholder="审批回调时填写" />
</div>
<div class="field">
<label>审批状态</label>
<select id="approvalStatus">
<option value="agree">agree 审批通过</option>
<option value="start">start 审批中</option>
<option value="refuse">refuse 拒绝</option>
<option value="terminate">terminate 撤销</option>
</select>
</div>
<div class="actions">
<button onclick="querySummary()">汇总查询</button>
<button class="secondary" onclick="previewSettlement()">预览明细</button>
<button class="green" onclick="createSettlement()">创建结算单</button>
<button class="orange" onclick="submitSettlement()">提交审批</button>
<button class="secondary" onclick="queryList()">结算单列表</button>
<button class="secondary" onclick="queryDetail()">结算单详情</button>
<button onclick="approvalCallback()">审批回调</button>
<button class="secondary" onclick="clearResult()">清空结果</button>
</div>
<div class="hint">
推荐流程:汇总查询 -> 预览明细 -> 创建结算单 -> 提交审批 -> 审批回调。接口路径默认按 `/bill/*.dspy` 调用。
</div>
</div>
</aside>
<section class="panel">
<div class="panel-title">
<h2>结算数据</h2>
<span class="badge" id="resultBadge">等待查询</span>
</div>
<div class="tabs">
<button class="tab active" onclick="switchView('table')">数据表格</button>
<button class="tab" onclick="switchView('raw')">接口返回</button>
</div>
<div id="tableView" class="table-wrap">
<table>
<thead id="tableHead">
<tr>
<th>提示</th>
</tr>
</thead>
<tbody id="tableBody">
<tr>
<td>请先执行查询</td>
</tr>
</tbody>
</table>
</div>
<pre id="rawView" class="log" style="display: none;">暂无数据</pre>
</section>
</section>
</main>
<script>
const API = {
summary: "/bill/finance_settlement_summary.dspy",
preview: "/bill/finance_settlement_preview.dspy",
create: "/bill/finance_settlement_create.dspy",
list: "/bill/finance_settlement_list.dspy",
detail: "/bill/finance_settlement_detail.dspy",
submit: "/bill/finance_settlement_submit.dspy",
callback: "/bill/finance_settlement_apv_callback.dspy"
};
const moneyFields = ["sales_amount", "settlement_amount", "platform_income_amount"];
let lastResponse = null;
function $(id) {
return document.getElementById(id);
}
function value(id) {
return $(id).value.trim();
}
function basePayload() {
return {
accounting_orgid: value("accountingOrgid"),
counterparty_type: value("counterpartyType"),
period_type: value("periodType"),
start_date: value("startDate"),
end_date: value("endDate"),
period_start: value("startDate"),
period_end: value("endDate"),
counterparty_orgid: value("counterpartyOrgid"),
current_page: Number(value("currentPage") || 1),
page_size: Number(value("pageSize") || 20)
};
}
function compact(obj) {
return Object.fromEntries(
Object.entries(obj).filter(([, v]) => v !== "" && v !== null && v !== undefined)
);
}
async function postJson(url, payload) {
$("apiStatus").textContent = "Loading";
$("resultBadge").textContent = "请求中";
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(compact(payload))
});
const text = await response.text();
try {
return JSON.parse(text);
} catch (error) {
return { status: false, msg: "接口未返回 JSON", raw: text };
}
}
function setModeText() {
const typeText = value("counterpartyType") === "supplier" ? "供应商" : "分销商";
const periodText = value("periodType") === "day" ? "日结" : "月结";
$("modeText").textContent = `${typeText} / ${periodText}`;
}
function updateMetrics(data) {
const summary = data?.summary || data?.data?.summary || {};
$("salesAmount").textContent = formatMoney(summary.sales_amount);
$("settlementAmount").textContent = formatMoney(summary.settlement_amount);
$("platformIncomeAmount").textContent = formatMoney(summary.platform_income_amount);
$("billCount").textContent = summary.bill_count ?? 0;
}
function formatMoney(value) {
const number = Number(value || 0);
return Number.isFinite(number) ? number.toFixed(8) : "0.00000000";
}
function normalizeRows(resp) {
const data = resp?.data || {};
if (Array.isArray(data.items)) return data.items;
if (Array.isArray(data)) return data;
if (data.settlement) return [data.settlement, ...(data.items || [])];
return [];
}
function render(resp) {
lastResponse = resp;
$("rawView").textContent = JSON.stringify(resp, null, 2);
$("apiStatus").textContent = resp?.status ? "Success" : "Failed";
$("resultBadge").textContent = resp?.status ? "请求成功" : "请求失败";
updateMetrics(resp?.data || {});
const rows = normalizeRows(resp);
if (!rows.length) {
$("tableHead").innerHTML = "<tr><th>提示</th></tr>";
$("tableBody").innerHTML = `<tr><td>${escapeHtml(resp?.msg || "暂无数据")}</td></tr>`;
return;
}
const columns = Array.from(
rows.reduce((set, row) => {
Object.keys(flatten(row)).forEach(key => set.add(key));
return set;
}, new Set())
).slice(0, 14);
$("tableHead").innerHTML = `<tr>${columns.map(col => `<th>${escapeHtml(col)}</th>`).join("")}</tr>`;
$("tableBody").innerHTML = rows.map(row => {
const flat = flatten(row);
return `<tr>${columns.map(col => `<td>${formatCell(col, flat[col])}</td>`).join("")}</tr>`;
}).join("");
}
function flatten(obj, prefix = "", output = {}) {
Object.entries(obj || {}).forEach(([key, val]) => {
const path = prefix ? `${prefix}.${key}` : key;
if (val && typeof val === "object" && !Array.isArray(val)) {
flatten(val, path, output);
} else {
output[path] = Array.isArray(val) ? JSON.stringify(val) : val;
}
});
return output;
}
function formatCell(key, val) {
if (moneyFields.some(field => key.endsWith(field))) {
return `<strong>${formatMoney(val)}</strong>`;
}
if (key.endsWith("status") && val) {
return `<span class="tag">${escapeHtml(val)}</span>`;
}
return escapeHtml(val ?? "");
}
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
function requireFields(payload, fields) {
const missing = fields.filter(field => !payload[field]);
if (missing.length) {
const resp = { status: false, msg: `缺少参数: ${missing.join(", ")}` };
render(resp);
return false;
}
return true;
}
async function querySummary() {
setModeText();
const payload = basePayload();
if (!requireFields(payload, ["accounting_orgid", "counterparty_type", "period_type", "start_date", "end_date"])) return;
render(await postJson(API.summary, payload));
}
async function previewSettlement() {
setModeText();
const payload = basePayload();
if (!requireFields(payload, ["accounting_orgid", "counterparty_type", "counterparty_orgid", "period_start", "period_end"])) return;
render(await postJson(API.preview, payload));
}
async function createSettlement() {
setModeText();
const payload = { ...basePayload(), userid: value("userid") };
if (!requireFields(payload, ["accounting_orgid", "counterparty_type", "counterparty_orgid", "period_start", "period_end"])) return;
render(await postJson(API.create, payload));
}
async function queryList() {
setModeText();
const payload = basePayload();
if (!requireFields(payload, ["accounting_orgid"])) return;
render(await postJson(API.list, payload));
}
async function queryDetail() {
const payload = {
settlement_id: value("settlementId"),
current_page: Number(value("currentPage") || 1),
page_size: Number(value("pageSize") || 100)
};
if (!requireFields(payload, ["settlement_id"])) return;
render(await postJson(API.detail, payload));
}
async function submitSettlement() {
const payload = {
settlement_id: value("settlementId"),
userid: value("userid"),
business_name: "财务结算"
};
if (!requireFields(payload, ["settlement_id", "userid"])) return;
render(await postJson(API.submit, payload));
}
async function approvalCallback() {
const payload = {
apv_id: value("approvalId"),
status: value("approvalStatus")
};
if (!requireFields(payload, ["apv_id", "status"])) return;
render(await postJson(API.callback, payload));
}
function switchView(view) {
document.querySelectorAll(".tab").forEach(tab => tab.classList.remove("active"));
event.target.classList.add("active");
$("tableView").style.display = view === "table" ? "block" : "none";
$("rawView").style.display = view === "raw" ? "block" : "none";
}
function clearResult() {
lastResponse = null;
$("rawView").textContent = "暂无数据";
$("tableHead").innerHTML = "<tr><th>提示</th></tr>";
$("tableBody").innerHTML = "<tr><td>请先执行查询</td></tr>";
$("apiStatus").textContent = "Ready";
$("resultBadge").textContent = "等待查询";
updateMetrics({});
}
setModeText();
$("counterpartyType").addEventListener("change", setModeText);
$("periodType").addEventListener("change", setModeText);
</script>
</body>
</html>