feat: improve application access and money precision
This commit is contained in:
@@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { clientApi, type ApplicationCmppParams, type ClientSmsApplication } from '@/api/adminApi';
|
||||
import { formatCents } from '@/utils/currency';
|
||||
import { Button, Modal, Pagination, Tag } from '@/components/ui';
|
||||
import { copyText } from '@/utils/clipboard';
|
||||
|
||||
type LinkStatus = 'connected' | 'degraded' | 'disconnected' | 'inactive';
|
||||
|
||||
@@ -67,6 +68,7 @@ export function ClientApplicationsPage() {
|
||||
const [error, setError] = useState('');
|
||||
const [paramsError, setParamsError] = useState('');
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [copyError, setCopyError] = useState('');
|
||||
|
||||
function loadApplications() {
|
||||
setLoading(true);
|
||||
@@ -84,6 +86,7 @@ export function ClientApplicationsPage() {
|
||||
}, []);
|
||||
|
||||
function openParams(application: ClientSmsApplication) {
|
||||
if (application.interfaceEnabled === false) return;
|
||||
setSelectedApp(application);
|
||||
setParams(null);
|
||||
setParamsError('');
|
||||
@@ -113,7 +116,9 @@ export function ClientApplicationsPage() {
|
||||
return;
|
||||
}
|
||||
const text = selectedRows.map((item) => `${item.label}: ${item.value}`).join('\n');
|
||||
void navigator.clipboard.writeText(text).then(() => setCopied(true));
|
||||
void copyText(text)
|
||||
.then(() => { setCopied(true); setCopyError(''); })
|
||||
.catch((failure: Error) => setCopyError(failure.message || '复制失败'));
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -162,7 +167,7 @@ export function ClientApplicationsPage() {
|
||||
</div>
|
||||
<div><dt>HTTP接口</dt><dd><Tag tone={application.httpConfig?.enabled ? 'success' : 'info'}>{application.httpConfig?.enabled ? '已开通' : '未开通'}</Tag></dd></div>
|
||||
</dl>
|
||||
<div className="table-actions"><Button onClick={() => openParams(application)} variant="ghost">CMPP参数</Button><Button disabled={!application.httpConfig?.enabled} onClick={() => navigate('/client/http-api')} variant="ghost">HTTP接口对接</Button></div>
|
||||
<div className="table-actions"><Button disabled={application.interfaceEnabled === false} onClick={() => openParams(application)} variant="ghost">{application.interfaceEnabled === false ? 'CMPP未开通' : 'CMPP参数'}</Button><Button disabled={!application.httpConfig?.enabled} onClick={() => navigate('/client/http-api')} variant="ghost">HTTP接口对接</Button></div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
@@ -197,6 +202,7 @@ export function ClientApplicationsPage() {
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{copyError ? <p className="form-error">{copyError}</p> : null}
|
||||
</Modal>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ import { Button, Chart, Table, Tag, type TableColumn } from '@/components/ui';
|
||||
import { clientApi, type DashboardResponse } from '@/api/adminApi';
|
||||
import { createLineOption, createPieOption } from '@/theme/chartOptions';
|
||||
import { formatDateTime } from '@/utils/dateTime';
|
||||
import { formatAmount, formatCents } from '@/utils/currency';
|
||||
import { formatAmount, formatCents, moneyUnitsToYuan } from '@/utils/currency';
|
||||
|
||||
type RecentTaskRow = {
|
||||
id: string;
|
||||
@@ -50,10 +50,10 @@ export function ClientHome() {
|
||||
}, []);
|
||||
|
||||
const account = dashboard?.accounts[0];
|
||||
const availableBalance = ((account?.balanceCents ?? 0) + (account?.creditCents ?? 0)) / 100;
|
||||
const todaySpend = (dashboard?.today.spendCents ?? 0) / 100;
|
||||
const availableBalance = moneyUnitsToYuan((account?.balanceCents ?? 0) + (account?.creditCents ?? 0));
|
||||
const todaySpend = moneyUnitsToYuan(dashboard?.today.spendCents);
|
||||
const todayRefundCents = Math.max(0, dashboard?.today.returnedCents ?? 0);
|
||||
const todayRefund = todayRefundCents / 100;
|
||||
const todayRefund = moneyUnitsToYuan(todayRefundCents);
|
||||
const balanceBaseline = Math.max(availableBalance + todaySpend - todayRefund, availableBalance, 1);
|
||||
const balancePercent = Math.min(100, Math.round((availableBalance / balanceBaseline) * 100));
|
||||
const recentMessages = useMemo<RecentTaskRow[]>(() => (dashboard?.recentTasks ?? []).map((task) => ({
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user