fix: polish client templates docs dashboard and receipt display
CSS quality / css-quality (push) Has been cancelled

This commit is contained in:
hectorzhao
2026-09-14 22:53:34 +08:00
parent 7f9abe3da0
commit 4665079ca3
22 changed files with 1305 additions and 762 deletions
+178 -43
View File
@@ -1,47 +1,182 @@
.http-developer-docs { margin: 0; color: #1f2937; background: #f6f7f9; font: 14px/1.6 system-ui, sans-serif; }
.http-developer-docs * { box-sizing: border-box; }
.http-developer-docs .http-doc-header { padding: 24px; border-bottom: 1px solid #e5e7eb; background: #fff; display: flex; gap: 20px; justify-content: space-between; align-items: center; }
.http-developer-docs h1 { font-size: 24px; margin: 8px 0; }
.http-developer-docs h2 { font-size: 20px; margin: 0 0 16px; }
.http-developer-docs h3 { font-size: 16px; margin: 20px 0 12px; }
.http-developer-docs p { overflow-wrap: anywhere; }
.http-developer-docs a { color: #2563eb; text-decoration: none; overflow-wrap: anywhere; }
.http-developer-docs a:hover { text-decoration: underline; }
.http-developer-docs .http-doc-actions { display: flex; gap: 16px; flex-wrap: wrap; }
.http-developer-docs .http-doc-layout { display: grid; grid-template-columns: 210px minmax(0, 1fr); }
.http-developer-docs nav { padding: 20px; position: sticky; top: 0; align-self: start; max-height: 100vh; overflow-y: auto; }
.http-developer-docs nav a { display: block; padding: 7px 0; font-size: 13px; }
.http-developer-docs nav label { display: block; margin-top: 20px; }
.http-developer-docs input { width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 6px; font: inherit; }
.http-developer-docs main { min-width: 0; }
.http-developer-docs section { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 0.9fr); border-bottom: 1px solid #e5e7eb; scroll-margin-top: 20px; }
.http-developer-docs section[hidden] { display: none; }
.http-developer-docs .http-doc-body { padding: 24px; background: #fff; min-width: 0; }
.http-developer-docs aside { min-width: 0; padding: 24px 16px; }
.http-developer-docs .http-doc-sample { margin-bottom: 16px; border: 1px solid #d1d5db; border-radius: 8px; overflow: hidden; background: #fff; }
.http-developer-docs .http-doc-sample-bar { padding: 10px; display: flex; gap: 12px; justify-content: space-between; align-items: center; font-size: 12px; color: #6b7280; }
.http-developer-docs button { padding: 5px 12px; border: 1px solid #d1d5db; border-radius: 6px; color: #1f2937; background: #fff; cursor: pointer; flex-shrink: 0; }
.http-developer-docs button:focus-visible, .http-developer-docs a:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
.http-developer-docs pre { margin: 0; padding: 16px; overflow-x: auto; font-size: 13px; background: #f4f6f8; }
.http-developer-docs code { font-family: ui-monospace, monospace; overflow-wrap: anywhere; }
.http-developer-docs .http-doc-table { overflow-x: auto; }
.http-developer-docs table { border-collapse: collapse; min-width: 100%; }
.http-developer-docs td { padding: 9px; border: 1px solid #e5e7eb; min-width: 100px; overflow-wrap: anywhere; }
.http-developer-docs tr:first-child { font-weight: 600; background: #f4f6f8; }
.http-developer-docs .http-doc-copy-status { position: fixed; bottom: 12px; right: 12px; max-width: 80vw; background: #fff; border-radius: 6px; padding: 8px; box-shadow: 0 2px 12px #0002; }
.http-developer-docs .http-doc-copy-status:empty { display: none; }
@media (width <= 1400px) {
.http-developer-docs section { grid-template-columns: minmax(0, 1fr); }
.http-developer-docs aside { padding: 16px 24px; }
.http-developer-docs aside:empty { display: none; }
.http-developer-docs {
margin: 0;
color: #1f2937;
background: #f6f7f9;
font:
14px/1.6 system-ui,
sans-serif;
}
.http-developer-docs * {
box-sizing: border-box;
}
.http-developer-docs .http-doc-header {
padding: 24px;
border-bottom: 1px solid #e5e7eb;
background: #fff;
display: flex;
gap: 20px;
justify-content: space-between;
align-items: center;
}
.http-developer-docs h1 {
font-size: 24px;
margin: 8px 0;
}
.http-developer-docs h2 {
font-size: 20px;
margin: 0 0 16px;
}
.http-developer-docs h3 {
font-size: 16px;
margin: 20px 0 12px;
}
.http-developer-docs p {
overflow-wrap: anywhere;
}
.http-developer-docs a {
color: #2563eb;
text-decoration: none;
overflow-wrap: anywhere;
}
.http-developer-docs a:hover {
text-decoration: underline;
}
.http-developer-docs .http-doc-actions {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.http-developer-docs .http-doc-layout {
display: grid;
grid-template-columns: 210px minmax(0, 1fr);
}
.http-developer-docs nav {
padding: 20px;
position: sticky;
top: 0;
align-self: start;
max-height: 100vh;
overflow-y: auto;
}
.http-developer-docs nav a {
display: block;
padding: 7px 0;
font-size: 13px;
}
.http-developer-docs nav label {
display: block;
margin-top: 20px;
}
.http-developer-docs input {
width: 100%;
padding: 8px;
border: 1px solid #d1d5db;
border-radius: 6px;
font: inherit;
}
.http-developer-docs main {
min-width: 0;
}
.http-developer-docs section {
display: block;
border-bottom: 1px solid #e5e7eb;
scroll-margin-top: 20px;
}
.http-developer-docs section[hidden] {
display: none;
}
.http-developer-docs .http-doc-body {
padding: 24px;
background: #fff;
min-width: 0;
}
.http-developer-docs .http-doc-sample {
margin-bottom: 16px;
border: 1px solid #d1d5db;
border-radius: 8px;
overflow: hidden;
background: #fff;
}
.http-developer-docs .http-doc-sample-bar {
padding: 10px;
display: flex;
gap: 12px;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #6b7280;
}
.http-developer-docs button {
padding: 5px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
color: #1f2937;
background: #fff;
cursor: pointer;
flex-shrink: 0;
}
.http-developer-docs button:focus-visible,
.http-developer-docs a:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
.http-developer-docs pre {
margin: 0;
padding: 16px;
overflow: auto;
max-height: 560px;
font-size: 13px;
background: #f4f6f8;
}
.http-developer-docs code {
font-family: ui-monospace, monospace;
overflow-wrap: anywhere;
}
.http-developer-docs .http-doc-table {
overflow-x: auto;
}
.http-developer-docs table {
border-collapse: collapse;
min-width: 100%;
}
.http-developer-docs td {
padding: 9px;
border: 1px solid #e5e7eb;
min-width: 100px;
overflow-wrap: anywhere;
}
.http-developer-docs tr:first-child {
font-weight: 600;
background: #f4f6f8;
}
.http-developer-docs .http-doc-copy-status {
position: fixed;
bottom: 12px;
right: 12px;
max-width: 80vw;
background: #fff;
border-radius: 6px;
padding: 8px;
box-shadow: 0 2px 12px #0002;
}
.http-developer-docs .http-doc-copy-status:empty {
display: none;
}
@media (width <= 700px) {
.http-developer-docs .http-doc-header { padding: 16px; display: block; }
.http-developer-docs .http-doc-layout { grid-template-columns: minmax(0, 1fr); }
.http-developer-docs nav { position: static; max-height: none; padding: 16px; }
.http-developer-docs .http-doc-body, .http-developer-docs aside { padding: 16px; }
.http-developer-docs .http-doc-header {
padding: 16px;
display: block;
}
.http-developer-docs .http-doc-layout {
grid-template-columns: minmax(0, 1fr);
}
.http-developer-docs nav {
position: static;
max-height: none;
padding: 16px;
}
.http-developer-docs .http-doc-body {
padding: 16px;
}
}
.http-developer-docs .http-doc-sample-tabs { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
.http-developer-docs .http-doc-sample-tabs button[aria-pressed="true"] { background: #eff6ff; color: #2563eb; border-color: #2563eb; }
-7
View File
@@ -1,13 +1,6 @@
/* global document, navigator, window, Event */
const copyStatus = document.getElementById('copy-status');
document.addEventListener('click', async (event) => {
const tab = event.target.closest('button[data-show-sample]');
if (tab) {
const aside = tab.closest('aside');
aside.querySelectorAll('.http-doc-sample').forEach((sample, index) => { sample.hidden = index !== Number(tab.dataset.showSample); });
aside.querySelectorAll('button[data-show-sample]').forEach((button) => button.setAttribute('aria-pressed', String(button === tab)));
return;
}
const button = event.target.closest('button[data-copy]');
if (!button) return;
const content = document.getElementById(button.dataset.copy);
+6 -5
View File
@@ -23,8 +23,8 @@ function inline(value: string): string {
}
export function renderHttpGuide(markdown: string, origin: string) {
const lines = markdown.replace(/\r\n/g, '\n').split('\n');
const sections: Array<{ id: string; title: string; body: string[]; samples: string[] }> = [];
let current = { id: 'introduction', title: '接入指南', body: [] as string[], samples: [] as string[] };
const sections: Array<{ id: string; title: string; body: string[] }> = [];
let current = { id: 'introduction', title: '接入指南', body: [] as string[] };
sections.push(current);
let code: string[] | null = null;
let language = '';
@@ -35,7 +35,7 @@ export function renderHttpGuide(markdown: string, origin: string) {
for (const line of lines) {
if (code) {
if (/^```/.test(line)) {
current.samples.push(`<div class="http-doc-sample"><div class="http-doc-sample-bar"><span>${escapeHtml(sampleTitle)} · ${escapeHtml(language || '示例')} · 仅供阅读,不执行请求</span><button type="button" data-copy="sample-${++sampleCount}">复制</button></div><pre id="sample-${sampleCount}" tabindex="0"><code>${escapeHtml(code.join('\n'))}</code></pre></div>`);
current.body.push(`<div class="http-doc-sample"><div class="http-doc-sample-bar"><span>${escapeHtml(sampleTitle)} · ${escapeHtml(language || '示例')} · 仅供阅读,不执行请求</span><button type="button" data-copy="sample-${++sampleCount}">复制代码</button></div><pre id="sample-${sampleCount}" tabindex="0"><code>${escapeHtml(code.join('\n'))}</code></pre></div>`);
code = null;
} else code.push(line);
continue;
@@ -46,7 +46,7 @@ export function renderHttpGuide(markdown: string, origin: string) {
closeTable();
sampleTitle = heading[2];
if (heading[1].length === 2) {
current = { id: 'section-' + sections.length, title: heading[2], body: [], samples: [] };
current = { id: 'section-' + sections.length, title: heading[2], body: [] };
sections.push(current);
} else if (heading[1].length > 2) current.body.push(`<h3>${inline(heading[2])}</h3>`);
continue;
@@ -58,8 +58,9 @@ export function renderHttpGuide(markdown: string, origin: string) {
continue;
}
closeTable();
if (line.trim().endsWith('') && line.trim().length < 70) sampleTitle = line.trim().replace(/$/, '');
if (line.trim() && !/^---+$/.test(line)) current.body.push(`<p>${inline(line.replace(/^>\s?/, '').replace(/^- /, '• '))}</p>`);
}
closeTable();
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>聆界短信 · HTTP 接入文档</title><style>${asset('reader.css')}</style></head><body class="http-developer-docs"><header class="http-doc-header"><div><strong>聆界短信 · 开发者文档</strong><h1>HTTP 接口接入文档</h1><p>${escapeHtml(httpDocVersion(markdown))} · 基础地址 ${escapeHtml(origin || '当前环境')}/api/openapi/v1</p></div><div class="http-doc-actions"><a href="/api/client-docs?format=md" download="client-http-api-guide.md">下载 MD</a><a href="/api/client-docs-json" target="_blank" rel="noreferrer">OpenAPI JSON</a></div></header><div class="http-doc-layout"><nav aria-label="文档目录"><details open><summary>目录</summary>${sections.map((section) => `<a href="#${section.id}">${inline(section.title)}</a>`).join('')}</details><label for="doc-search">错误码 / 文档检索</label><input id="doc-search" type="search" placeholder="输入错误码或关键词"><p id="search-status" role="status"></p></nav><main>${sections.map((section) => `<section id="${section.id}" data-doc-section><div class="http-doc-body"><h2>${inline(section.title)}</h2>${section.body.join('')}</div><aside aria-label="${escapeHtml(section.title)} 示例">${section.samples.length > 1 ? '<div class="http-doc-sample-tabs" role="group" aria-label="切换示例">' + section.samples.map((_sample, index) => '<button type="button" data-show-sample="' + index + '" aria-pressed="' + (index === 0) + '">示例 ' + (index + 1) + '</button>').join('') + '</div>' : ''}${section.samples.map((sample, index) => sample.replace('class="http-doc-sample"', 'class="http-doc-sample"' + (index ? ' hidden' : ''))).join('')}</aside></section>`).join('')}<p id="no-results" hidden>没有匹配的文档内容,请更换关键词。</p></main></div><p class="http-doc-copy-status" role="status" id="copy-status"></p><script>${asset('reader.js')}</script></body></html>`;
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>聆界短信 · HTTP 接入文档</title><style>${asset('reader.css')}</style></head><body class="http-developer-docs"><header class="http-doc-header"><div><strong>聆界短信 · 开发者文档</strong><h1>HTTP 接口接入文档</h1><p>${escapeHtml(httpDocVersion(markdown))} · 基础地址 ${escapeHtml(origin || '当前环境')}/api/openapi/v1</p></div><div class="http-doc-actions"><a href="/api/client-docs?format=md" download="client-http-api-guide.md">下载 MD</a><a href="/api/client-docs-json" target="_blank" rel="noreferrer">OpenAPI JSON</a></div></header><div class="http-doc-layout"><nav aria-label="文档目录"><details open><summary>目录</summary>${sections.map((section) => `<a href="#${section.id}">${inline(section.title)}</a>`).join('')}</details><label for="doc-search">错误码 / 文档检索</label><input id="doc-search" type="search" placeholder="输入错误码或关键词"><p id="search-status" role="status"></p></nav><main>${sections.map((section) => `<section id="${section.id}" data-doc-section><div class="http-doc-body"><h2>${inline(section.title)}</h2>${section.body.join('')}</div></section>`).join('')}<p id="no-results" hidden>没有匹配的文档内容,请更换关键词。</p></main></div><p class="http-doc-copy-status" role="status" id="copy-status"></p><script>${asset('reader.js')}</script></body></html>`;
}
@@ -167,6 +167,17 @@ describe('HTTP API remediation boundaries', () => {
).rejects.toMatchObject({ response: expect.objectContaining({ code: 'REQUEST_REQUIRES_REVIEW' }) });
expect(send).not.toHaveBeenCalled();
});
it('keeps named code examples beside their source paragraphs', () => {
const html = renderHttpGuide(
'**接口版本:v1 · 2026-09-14**\n## 鉴权\n### 签名原文\n五行原文:\n```text\nMETHOD\nPATH\n```\n后续说明\n### 回执\n```json\n{}\n```',
'',
);
expect(html.indexOf('sample-1')).toBeLessThan(html.indexOf('后续说明'));
expect(html).toContain('五行原文 · text');
expect(html).not.toContain('data-show-sample');
expect(html).not.toContain('<aside');
expect(html).not.toMatch(/>示例 \d+</);
});
it('renders escaped MD and code, without executable document HTML or unsafe links', () => {
const html = renderHttpGuide(
'**接口版本:v1 · 2026-09-14**\n## 接入\n<script>alert(1)</script>\n[bad](javascript:alert)\n```html\n<img src=x onerror=alert(1)>\n```',
@@ -372,6 +372,8 @@ describe('OperationsService', () => {
id: true,
content: true,
hasDrainageContent: true,
receiptStatus: true,
deliveredAt: true,
tenant: { select: { id: true, name: true } },
}),
}),
@@ -90,6 +90,8 @@ export class OperationsMessageQueries {
amountCents: true,
status: true,
submitStatus: true,
receiptStatus: true,
deliveredAt: true,
queuedAt: true,
tenant: { select: { id: true, name: true } },
application: { select: { id: true, name: true } },