feat(config, i18n): add integration guide documentation page with multi-language support

Added new integration guide page accessible from config hub, providing SSO and wallet integration documentation for client engineering teams. Includes translations in English, Nepali, and Chinese. Refactored percentage conversion utilities across agent management modules to use consistent `percentValueToUi` function, removing deprecated `ratioToPercentUi` and `percentUiToRatio` helpers.
This commit is contained in:
2026-06-11 09:23:41 +08:00
parent 13ae574aad
commit 44ad51698f
10 changed files with 468 additions and 16 deletions

View File

@@ -21,7 +21,7 @@ import { AgentsPlayersPanel } from "@/modules/agents/agents-players-panel";
import { AgentProfileFields, type AgentProfileFieldsProps } from "@/modules/agents/agent-profile-fields";
import { formatCredit } from "@/modules/agents/agent-line-sidebar";
import { Button } from "@/components/ui/button";
import { ratioToPercentUi } from "@/lib/admin-rate-percent";
import { percentValueToUi } from "@/lib/admin-rate-percent";
import { resolveRoleStatusTone } from "@/lib/admin-status-tone";
import { cn } from "@/lib/utils";
import type { AgentNodeProfileSummary, AgentNodeRow, AgentProfileRow } from "@/types/api/admin-agent";
@@ -366,7 +366,7 @@ function OverviewTab({
const { t } = useTranslation(["agents", "common"]);
const rebateCap =
profile && !profileLoading ? ratioToPercentUi(profile.rebate_limit ?? 0) : null;
profile && !profileLoading ? percentValueToUi(profile.rebate_limit ?? 0) : null;
return (
<div className="mx-auto max-w-5xl space-y-6">
@@ -421,8 +421,8 @@ function OverviewTab({
<p className="text-xs text-muted-foreground">
{t("lineUi.profileFootnote", {
defaultValue: "回水上限 {{rebate}}% · 默认回水 {{defaultRebate}}% · {{cycle}}",
rebate: ratioToPercentUi(profile.rebate_limit ?? 0),
defaultRebate: ratioToPercentUi(profile.default_player_rebate ?? 0),
rebate: percentValueToUi(profile.rebate_limit ?? 0),
defaultRebate: percentValueToUi(profile.default_player_rebate ?? 0),
cycle: cycleLabel,
})}
{(profile.risk_tags?.length ?? 0) > 0

View File

@@ -20,7 +20,7 @@ import {
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { useAsyncEffect } from "@/hooks/use-async-effect";
import { percentUiToRatio } from "@/lib/admin-rate-percent";
import { percentValueToUi } from "@/lib/admin-rate-percent";
import { adminSiteCodeLabel } from "@/lib/admin-select-display";
import { LotteryApiBizError } from "@/types/api/errors";
import type { AdminIntegrationSiteRow } from "@/types/api/admin-integration-site";
@@ -73,8 +73,8 @@ export function AgentLineProvisionWizard(): React.ReactElement {
password: form.password,
total_share_rate: Number.parseFloat(form.total_share_rate) || 0,
credit_limit: Number.parseInt(form.credit_limit, 10) || 0,
rebate_limit: percentUiToRatio(form.rebate_limit),
default_player_rebate: percentUiToRatio(form.default_player_rebate),
rebate_limit: Number.parseFloat(form.rebate_limit) || 0,
default_player_rebate: Number.parseFloat(form.default_player_rebate) || 0,
settlement_cycle: form.settlement_cycle,
can_grant_extra_rebate: form.can_grant_extra_rebate,
});

View File

@@ -34,10 +34,8 @@ import { useAsyncEffect } from "@/hooks/use-async-effect";
import { useConfirmAction } from "@/hooks/use-confirm-action";
import { useTranslationRef } from "@/hooks/use-translation-ref";
import {
percentUiToRatio,
percentValueToUi,
parsePercentUi,
ratioToPercentUi,
} from "@/lib/admin-rate-percent";
import { adminHasAnyPermission } from "@/lib/admin-permissions";
import {
@@ -168,8 +166,8 @@ export function AgentsConsole(): React.ReactElement {
setProfileShareRate(percentValueToUi(row.total_share_rate ?? 0));
}
setProfileCreditLimit(String(row.credit_limit ?? 0));
setProfileRebateLimit(ratioToPercentUi(row.rebate_limit ?? 0));
setProfileDefaultRebate(ratioToPercentUi(row.default_player_rebate ?? 0));
setProfileRebateLimit(percentValueToUi(row.rebate_limit ?? 0));
setProfileDefaultRebate(percentValueToUi(row.default_player_rebate ?? 0));
setProfileSettlementCycle(normalizeAgentSettlementCycle(row.settlement_cycle));
setProfileExtraRebate(Boolean(row.can_grant_extra_rebate));
setProfileCanCreateChild(Boolean(row.can_create_child_agent));
@@ -183,8 +181,8 @@ export function AgentsConsole(): React.ReactElement {
const shareRate = Number.parseFloat(profileShareRate) || 0;
const base = {
credit_limit: Number.parseInt(profileCreditLimit, 10) || 0,
rebate_limit: percentUiToRatio(profileRebateLimit),
default_player_rebate: percentUiToRatio(profileDefaultRebate),
rebate_limit: Number.parseFloat(profileRebateLimit) || 0,
default_player_rebate: Number.parseFloat(profileDefaultRebate) || 0,
settlement_cycle: normalizeAgentSettlementCycle(profileSettlementCycle),
can_grant_extra_rebate: profileExtraRebate,
can_create_child_agent: profileCanCreateChild,

View File

@@ -48,6 +48,12 @@ const HUB_CARDS: HubCard[] = [
descKey: "hub.integrationDesc",
requiredAny: PRD_INTEGRATION_ACCESS_ANY,
},
{
href: "/admin/docs/integration-guide",
titleKey: "hub.integrationGuideTitle",
descKey: "hub.integrationGuideDesc",
requiredAny: PRD_INTEGRATION_ACCESS_ANY,
},
];
export function ConfigHubScreen() {

View File

@@ -0,0 +1,414 @@
import { Card, CardContent } from "@/components/ui/card";
const sections = [
{ id: "overview", title: "1. 文档说明" },
{ id: "preparation", title: "2. 接入前准备" },
{ id: "sso", title: "3. SSO 登录接入" },
{ id: "wallet", title: "4. 钱包接口" },
{ id: "errors", title: "5. 错误码与幂等" },
{ id: "testing", title: "6. 联调与上线" },
{ id: "appendix", title: "7. 附录" },
] as const;
const ssoFields = [
["site_code", "string", "是", "站点编码,由我方提供。"],
["user_id", "string", "是", "客户侧用户唯一标识。"],
["username", "string", "是", "客户侧用户名或展示名。"],
["timestamp", "number", "是", "发起时间戳,单位秒。"],
["nonce", "string", "是", "随机串,用于防重放。"],
["currency", "string", "否", "币种,如 CNY。"],
["device", "string", "否", "终端类型,如 h5、web。"],
] as const;
const walletCommonFields = [
["site_code", "string", "站点编码"],
["user_id", "string", "客户侧用户唯一标识"],
["transaction_id", "string", "唯一交易号,幂等主键"],
["timestamp", "number", "请求时间戳,单位秒"],
["sign", "string", "签名结果"],
] as const;
const errorRows = [
["0", "成功", "按成功结果落账或继续业务。"],
["1001", "参数错误", "检查字段完整性、类型、必填项。"],
["1002", "签名错误", "检查签名原串、密钥、时间戳。"],
["1003", "用户不存在", "确认用户标识是否正确。"],
["1004", "余额不足", "前端提示余额不足,不继续下注。"],
["1006", "重复交易", "返回首次结果,不允许重复记账。"],
["1099", "系统异常", "可按约定策略重试,并保留日志。"],
] as const;
function DocTable({
headers,
rows,
}: {
headers: readonly string[];
rows: readonly (readonly string[])[];
}): React.ReactElement {
return (
<div className="overflow-x-auto rounded-lg border border-border">
<table className="w-full min-w-[640px] border-collapse text-sm">
<thead className="bg-muted/50 text-left">
<tr>
{headers.map((header) => (
<th key={header} className="border-b border-border px-4 py-3 font-medium text-foreground">
{header}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, rowIndex) => (
<tr key={`${row[0]}-${rowIndex}`} className="align-top">
{row.map((cell, cellIndex) => (
<td
key={`${row[0]}-${cellIndex}`}
className="border-b border-border px-4 py-3 leading-6 text-muted-foreground"
>
{cell}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}
export function IntegrationGuideScreen(): React.ReactElement {
return (
<div className="mx-auto flex w-full max-w-[1280px] min-w-0 flex-col px-4 py-6 sm:px-6 lg:px-8 lg:py-8">
<div className="grid gap-10 xl:grid-cols-[220px_minmax(0,1fr)]">
<aside className="hidden xl:block">
<div className="sticky top-6 space-y-4 text-sm">
<div>
<div className="text-xs text-muted-foreground"></div>
<div className="mt-2 font-medium text-foreground"></div>
</div>
<nav className="space-y-1 border-l border-border pl-4">
{sections.map((section) => (
<a
key={section.id}
href={`#${section.id}`}
className="block py-1 text-muted-foreground transition hover:text-foreground"
>
{section.title}
</a>
))}
</nav>
</div>
</aside>
<main className="min-w-0">
<header className="border-b border-border pb-8">
<h1 className="text-3xl font-semibold tracking-tight text-foreground"></h1>
<p className="mt-4 max-w-3xl text-sm leading-7 text-muted-foreground">
线
</p>
</header>
<div className="mt-8 space-y-10">
<section id="overview" className="scroll-mt-24 space-y-5 border-b border-border pb-10">
<div>
<h2 className="text-2xl font-semibold tracking-tight">1. </h2>
<p className="mt-3 text-sm leading-7 text-muted-foreground">
SSO
</p>
</div>
<div className="grid gap-4 md:grid-cols-3">
<div className="rounded-lg border border-border px-5 py-5">
<div className="text-sm font-medium text-foreground"></div>
<div className="mt-2 text-sm leading-6 text-muted-foreground">
H5WebApp WebView
</div>
</div>
<div className="rounded-lg border border-border px-5 py-5">
<div className="text-sm font-medium text-foreground"></div>
<div className="mt-2 text-sm leading-6 text-muted-foreground">
</div>
</div>
<div className="rounded-lg border border-border px-5 py-5">
<div className="text-sm font-medium text-foreground"></div>
<div className="mt-2 text-sm leading-6 text-muted-foreground">
</div>
</div>
</div>
<div className="rounded-lg border border-border px-5 py-5">
<div className="text-sm font-medium text-foreground"></div>
<ol className="mt-3 space-y-2 text-sm leading-6 text-muted-foreground">
<li>1. </li>
<li>2. SSO token</li>
<li>3. </li>
<li>4. </li>
<li>5. </li>
<li>6. </li>
</ol>
</div>
</section>
<section id="preparation" className="scroll-mt-24 space-y-5 border-b border-border pb-10">
<div>
<h2 className="text-2xl font-semibold tracking-tight">2. </h2>
<p className="mt-3 text-sm leading-7 text-muted-foreground">
</p>
</div>
<div className="grid gap-4 lg:grid-cols-2">
<div className="rounded-lg border border-border px-5 py-5">
<div className="text-base font-medium text-foreground"></div>
<ul className="mt-4 space-y-2 text-sm leading-6 text-muted-foreground">
<li> </li>
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
<div className="rounded-lg border border-border px-5 py-5">
<div className="text-base font-medium text-foreground"></div>
<ul className="mt-4 space-y-2 text-sm leading-6 text-muted-foreground">
<li> SSO JWT </li>
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
</div>
</section>
<section id="sso" className="scroll-mt-24 space-y-5 border-b border-border pb-10">
<div>
<h2 className="text-2xl font-semibold tracking-tight">3. SSO </h2>
<p className="mt-3 text-sm leading-7 text-muted-foreground">
</p>
</div>
<div className="rounded-lg border border-border px-5 py-5">
<div className="text-base font-medium text-foreground"></div>
<ol className="mt-4 space-y-2 text-sm leading-6 text-muted-foreground">
<li>1. </li>
<li>2. SSO </li>
<li>3. 使</li>
<li>4. </li>
<li>5. </li>
</ol>
</div>
<div>
<div className="mb-3 text-base font-medium text-foreground">SSO </div>
<DocTable headers={["字段", "类型", "必填", "说明"]} rows={ssoFields} />
</div>
<div className="grid gap-4 lg:grid-cols-2">
<div className="rounded-lg border border-border px-5 py-5">
<div className="text-base font-medium text-foreground"></div>
<ul className="mt-4 space-y-2 text-sm leading-6 text-muted-foreground">
<li> token 60 300 </li>
<li> <code className="rounded bg-muted px-1.5 py-0.5 text-[12px]">user_id</code> </li>
<li> </li>
<li> </li>
</ul>
</div>
<Card className="rounded-lg border-border shadow-none">
<CardContent className="p-5">
<div className="text-sm font-medium text-foreground">SSO </div>
<pre className="mt-4 overflow-x-auto whitespace-pre-wrap break-words rounded-md bg-muted px-4 py-4 text-sm leading-7 text-muted-foreground">{`{
"site_code": "demo",
"user_id": "100001",
"username": "demo_user",
"timestamp": 1718000000,
"nonce": "N8F2X9Q1",
"currency": "CNY",
"device": "h5"
}`}</pre>
</CardContent>
</Card>
</div>
</section>
<section id="wallet" className="scroll-mt-24 space-y-5 border-b border-border pb-10">
<div>
<h2 className="text-2xl font-semibold tracking-tight">4. </h2>
<p className="mt-3 text-sm leading-7 text-muted-foreground">
</p>
</div>
<div className="grid gap-4 xl:grid-cols-3">
<div className="rounded-lg border border-border px-5 py-5">
<div className="text-base font-medium text-foreground"></div>
<div className="mt-2 text-sm leading-6 text-muted-foreground">
</div>
<div className="mt-3 rounded-md bg-muted px-3 py-2 text-xs text-foreground">POST /wallet/balance</div>
</div>
<div className="rounded-lg border border-border px-5 py-5">
<div className="text-base font-medium text-foreground"></div>
<div className="mt-2 text-sm leading-6 text-muted-foreground">
</div>
<div className="mt-3 rounded-md bg-muted px-3 py-2 text-xs text-foreground">POST /wallet/debit</div>
</div>
<div className="rounded-lg border border-border px-5 py-5">
<div className="text-base font-medium text-foreground"></div>
<div className="mt-2 text-sm leading-6 text-muted-foreground">
退
</div>
<div className="mt-3 rounded-md bg-muted px-3 py-2 text-xs text-foreground">POST /wallet/credit</div>
</div>
</div>
<div>
<div className="mb-3 text-base font-medium text-foreground"></div>
<DocTable headers={["字段", "类型", "说明"]} rows={walletCommonFields} />
</div>
<div className="grid gap-4 lg:grid-cols-2">
<Card className="rounded-lg border-border shadow-none">
<CardContent className="p-5">
<div className="text-sm font-medium text-foreground"></div>
<pre className="mt-4 overflow-x-auto whitespace-pre-wrap break-words rounded-md bg-muted px-4 py-4 text-sm leading-7 text-muted-foreground">{`{
"site_code": "demo",
"user_id": "100001",
"transaction_id": "BET202606100001",
"order_id": "TICKET202606100001",
"amount": "20.00",
"timestamp": 1718000001,
"sign": "xxxxxx"
}`}</pre>
</CardContent>
</Card>
<Card className="rounded-lg border-border shadow-none">
<CardContent className="p-5">
<div className="text-sm font-medium text-foreground"></div>
<pre className="mt-4 overflow-x-auto whitespace-pre-wrap break-words rounded-md bg-muted px-4 py-4 text-sm leading-7 text-muted-foreground">{`{
"code": 0,
"message": "success",
"data": {
"transaction_id": "BET202606100001",
"balance": "980.00"
}
}`}</pre>
</CardContent>
</Card>
</div>
<div className="grid gap-4 lg:grid-cols-2">
<div className="rounded-lg border border-border px-5 py-5">
<div className="text-base font-medium text-foreground"></div>
<ol className="mt-4 space-y-2 text-sm leading-6 text-muted-foreground">
<li>1. </li>
<li>2. <code className="rounded bg-muted px-1.5 py-0.5 text-[12px]">key=value</code> </li>
<li>3. </li>
<li>4. 使 <code className="rounded bg-muted px-1.5 py-0.5 text-[12px]">HMAC-SHA256</code> </li>
</ol>
</div>
<div className="rounded-lg border border-border px-5 py-5">
<div className="text-base font-medium text-foreground"></div>
<ul className="mt-4 space-y-2 text-sm leading-6 text-muted-foreground">
<li> 使 HTTPS</li>
<li> 使 <code className="rounded bg-muted px-1.5 py-0.5 text-[12px]">1000.00</code></li>
<li> <code className="rounded bg-muted px-1.5 py-0.5 text-[12px]">application/json</code></li>
<li> </li>
</ul>
</div>
</div>
</section>
<section id="errors" className="scroll-mt-24 space-y-5 border-b border-border pb-10">
<div>
<h2 className="text-2xl font-semibold tracking-tight">5. </h2>
<p className="mt-3 text-sm leading-7 text-muted-foreground">
</p>
</div>
<div>
<div className="mb-3 text-base font-medium text-foreground"></div>
<DocTable headers={["错误码", "含义", "处理方式"]} rows={errorRows} />
</div>
<div className="rounded-lg border border-border px-5 py-5">
<div className="text-base font-medium text-foreground"></div>
<ul className="mt-4 space-y-2 text-sm leading-6 text-muted-foreground">
<li> <code className="rounded bg-muted px-1.5 py-0.5 text-[12px]">transaction_id</code> </li>
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
</section>
<section id="testing" className="scroll-mt-24 space-y-5 border-b border-border pb-10">
<div>
<h2 className="text-2xl font-semibold tracking-tight">6. 线</h2>
<p className="mt-3 text-sm leading-7 text-muted-foreground">
线
</p>
</div>
<div className="grid gap-4 lg:grid-cols-2">
<div className="rounded-lg border border-border px-5 py-5">
<div className="text-base font-medium text-foreground"></div>
<ol className="mt-4 space-y-2 text-sm leading-6 text-muted-foreground">
<li>1. </li>
<li>2. SSO </li>
<li>3. </li>
<li>4. </li>
<li>5. </li>
</ol>
</div>
<div className="rounded-lg border border-border px-5 py-5">
<div className="text-base font-medium text-foreground">线</div>
<ul className="mt-4 space-y-2 text-sm leading-6 text-muted-foreground">
<li> </li>
<li> </li>
<li> </li>
<li> </li>
<li> 线</li>
</ul>
</div>
</div>
</section>
<section id="appendix" className="scroll-mt-24 space-y-5 pb-2">
<div>
<h2 className="text-2xl font-semibold tracking-tight">7. </h2>
<p className="mt-3 text-sm leading-7 text-muted-foreground">
</p>
</div>
<div className="grid gap-4 lg:grid-cols-3">
<div className="rounded-lg border border-border px-5 py-5">
<div className="text-base font-medium text-foreground"></div>
<div className="mt-3 text-sm leading-6 text-muted-foreground">
使 <code className="rounded bg-muted px-1.5 py-0.5 text-[12px]">1000.00</code>
</div>
</div>
<div className="rounded-lg border border-border px-5 py-5">
<div className="text-base font-medium text-foreground"></div>
<div className="mt-3 text-sm leading-6 text-muted-foreground">
使 Unix ISO8601
</div>
</div>
<div className="rounded-lg border border-border px-5 py-5">
<div className="text-base font-medium text-foreground"></div>
<div className="mt-3 text-sm leading-6 text-muted-foreground">
UTF-8 JSON
</div>
</div>
</div>
</section>
</div>
</main>
</div>
</div>
);
}