feat: improve application access and money precision

This commit is contained in:
hectorzhao
2026-07-16 17:54:05 +08:00
parent 9d5c507007
commit faa716b8d0
49 changed files with 1699 additions and 489 deletions
+18 -3
View File
@@ -2,6 +2,8 @@ import { useEffect, useState } from 'react';
import { BookOpen, Copy, KeyRound, RefreshCw, Webhook } from 'lucide-react';
import { clientApi, type ClientSmsApplication, type HttpApiConfigResponse, type HttpApiCredential, type HttpApiRequestLog, type HttpWebhookDelivery, type HttpWebhookEndpoint } from '@/api/adminApi';
import { Button, Input, Select, Tabs, Tag } from '@/components/ui';
import { copyText } from '@/utils/clipboard';
import { formatHttpApiParams } from '@/utils/interfaceParams';
export function ClientHttpApiPage() {
const [applications, setApplications] = useState<ClientSmsApplication[]>([]);
@@ -16,6 +18,19 @@ export function ClientHttpApiPage() {
const [revealedSecret, setRevealedSecret] = useState<{ title: string; accessKey?: string; secret: string } | null>(null);
const [error, setError] = useState('');
const [loading, setLoading] = useState(true);
const [paramsCopied, setParamsCopied] = useState(false);
async function copyHttpParams() {
if (!config) return;
try {
await copyText(formatHttpApiParams(config, window.location.origin));
setParamsCopied(true);
setError('');
window.setTimeout(() => setParamsCopied(false), 1600);
} catch (failure) {
setError(failure instanceof Error ? failure.message : 'HTTP接口参数复制失败');
}
}
useEffect(() => {
clientApi.listApplications().then((items) => {
@@ -67,7 +82,7 @@ export function ClientHttpApiPage() {
const api = config?.config;
const overview = <div className="page-stack">
{!api?.enabled ? <p className="form-error"> HTTP </p> : null}
<div className="surface" style={{ padding: 18 }}><h3>{config?.applicationName ?? '企业应用'}</h3><p className="muted">{window.location.origin}/api/openapi/v1</p><div className="table-actions">
<div className="surface" style={{ padding: 18 }}><div className="section-heading"><div><h3>{config?.applicationName ?? '企业应用'}</h3><p className="muted">{window.location.origin}/api/openapi/v1</p></div><Button disabled={!api?.enabled} icon={<Copy size={14} />} onClick={() => void copyHttpParams()} size="sm">{paramsCopied ? '已复制' : '复制HTTP参数'}</Button></div><div className="table-actions">
<Tag tone={api?.sendEnabled ? 'success' : 'info'}> {api?.sendEnabled ? '已开通' : '未开通'}</Tag>
<Tag tone={api?.messageQueryEnabled ? 'success' : 'info'}> {api?.messageQueryEnabled ? '已开通' : '未开通'}</Tag>
<Tag tone={api?.uplinkQueryEnabled ? 'success' : 'info'}> {api?.uplinkQueryEnabled ? '已开通' : '未开通'}</Tag>
@@ -78,7 +93,7 @@ export function ClientHttpApiPage() {
</div>;
const credentialPanel = <div className="page-stack"><div className="section-heading"><div><h3><KeyRound size={17} />访</h3><p className="muted"></p></div><Button disabled={!api?.credentialSelfServiceEnabled} onClick={() => void createCredential()}></Button></div>
{credentials.map((item) => <div className="surface" key={item.id} style={{ display: 'grid', gridTemplateColumns: '1fr 1.5fr 100px 1fr auto', gap: 12, padding: 14, alignItems: 'center' }}><strong>{item.name}</strong><code>{item.accessKey}</code><span>****{item.secretLast4}</span><span className="muted">使{item.lastUsedAt ? new Date(item.lastUsedAt).toLocaleString('zh-CN') : '从未'}</span>{item.status === 'active' ? <Button disabled={!api?.credentialSelfServiceEnabled} onClick={() => void clientApi.revokeHttpApiCredential(applicationId, item.id).then(() => loadApplication(applicationId))} size="sm" variant="danger"></Button> : <Tag tone="info"></Tag>}</div>)}
{credentials.map((item) => <div className="surface client-http-credential-row" key={item.id}><strong>{item.name}</strong><code>{item.accessKey}</code><span>****{item.secretLast4}</span><span className="muted">使{item.lastUsedAt ? new Date(item.lastUsedAt).toLocaleString('zh-CN') : '从未'}</span>{item.status === 'active' ? <Button disabled={!api?.credentialSelfServiceEnabled} onClick={() => void clientApi.revokeHttpApiCredential(applicationId, item.id).then(() => loadApplication(applicationId))} size="sm" variant="danger"></Button> : <Tag tone="info"></Tag>}</div>)}
{credentials.length === 0 ? <p className="muted">访</p> : null}
</div>;
@@ -104,7 +119,7 @@ SHA256(rawBody)`}</pre><p>使用访问密钥执行 HMAC-SHA256,输出小写十
return <section className="page-stack"><div className="page-heading"><div><h1></h1><p> HTTP 访</p></div><Select label="企业应用" onChange={(event) => setApplicationId(event.target.value)} options={applications.map((item) => ({ label: item.name, value: item.id }))} value={applicationId} /></div>
{loading ? <p className="muted">...</p> : null}{error ? <p className="form-error">{error}</p> : null}
{revealedSecret ? <div className="surface" style={{ border: '1px solid #f59e0b', padding: 16 }}><strong>{revealedSecret.title}</strong>{revealedSecret.accessKey ? <p>Access Key<code>{revealedSecret.accessKey}</code></p> : null}<p>Secret<code>{revealedSecret.secret}</code></p><Button icon={<Copy size={14} />} onClick={() => void navigator.clipboard.writeText([revealedSecret.accessKey, revealedSecret.secret].filter(Boolean).join('\n'))} size="sm"></Button></div> : null}
{revealedSecret ? <div className="surface" style={{ border: '1px solid #f59e0b', padding: 16 }}><strong>{revealedSecret.title}</strong>{revealedSecret.accessKey ? <p>Access Key<code>{revealedSecret.accessKey}</code></p> : null}<p>Secret<code>{revealedSecret.secret}</code></p><Button icon={<Copy size={14} />} onClick={() => void copyText([revealedSecret.accessKey, revealedSecret.secret].filter(Boolean).join('\n')).catch((failure: Error) => setError(failure.message))} size="sm"></Button></div> : null}
{!loading && applicationId ? <Tabs items={tabs} /> : null}
</section>;
}