外观
接入准备
约 212 字小于 1 分钟
指南快速开始
2025-03-13
在开始编码之前,需先了解接口调用时的一些注意事项:
appId、公私钥在接入之前向需我方申请获取。- 请求方式:
POST,Content-Type=application/json;charset=UTF-8。 - 公共参数:
body中需要传的appId、sign、data参数,sign计算方式见【签名规则】。 - 业务参数:统一放在公共参数的
data中。
接口域名
测试环境:https://gw-test.senins.cn
生产环境:https://gw.senins.cn
公共参数
| 参数 | 名称 | 说明 | 类型 | 必须 |
|---|---|---|---|---|
| appId | 系统标识 | 由平台提供 | string | 是 |
| sign | 签名 | 对data参数按照签名规则得到的结果 | string | 是 |
| data | 业务数据 | 具体的业务参数 | T | 是 |
产品查询
HTML
<div id="app">
<main class="shell" role="main">
<section class="search" aria-label="搜索区域">
<div class="input-row">
<i class="icon" aria-hidden="true"></i>
<input id="appId" placeholder="输入 AppId,检索关联产品信息" inputmode="text" autocomplete="off"/>
<button id="btn" type="button">搜索</button>
</div>
</section>
<div class="divider" aria-hidden="true"></div>
<section class="content" id="content" aria-live="polite">
<div class="state" id="placeholder">👋 开始于一次检索。输入 AppId 后点击"搜索"。</div>
</section>
</main>
</div>Javascript
const API_ENDPOINT = "https://gw-gray.senins.cn/wy-cel-open/api/test/app/product/search?appId=";
const $ = (s) => document.querySelector(s);
const content = $("#content");
const btn = $("#btn");
const input = $("#appId");
btn.addEventListener('click', (e) => {
searchApp();
});
// 回车触发搜索
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
searchApp();
}
});
async function searchApp() {
const appId = input.value.trim();
if (!appId) {
flashError("请输入有效的 AppId");
input.focus();
return;
}
setLoading(true);
try {
const res = await fetch(API_ENDPOINT + encodeURIComponent(appId), {
headers: {
'accept': 'application/json',
}
});
if (!res.ok) throw new Error(`请求失败:${res.status}`);
const data = await res.json();
renderData(data);
} catch (err) {
renderError(err.message || '未知错误');
} finally {
setLoading(false);
}
}
function renderData(resp) {
const ok = resp?.code === 0;
const rows = resp?.data || [];
if (!ok) {
renderError(resp?.message || '查询失败');
return;
}
if (!Array.isArray(rows) || rows.length === 0) {
content.innerHTML = `<div class="state">未查询到与 <b>${escapeHtml(input.value)}</b> 相关的数据。</div>`;
return;
}
// 动态生成表头(保持字段顺序友好)
const columns = inferColumns(rows);
const columnNames = {
'code': '产品编码',
'name': '产品名称',
'statusText': '状态',
'serviceWayText': '方式',
'createAt': '创建时间',
};
const thead = `<thead><tr>${columns.map(c => `<th>${escapeHtml(columnNames[c] || c)}</th>`).join('')}</tr></thead>`;
const tbody = `<tbody>${rows.map(r => {
return `<tr>${columns.map(c => {
const v = r[c];
if (c === 'statusText' || c === 'serviceWayText') {
return `<td><span class="badge">${escapeHtml(String(v ?? ''))}</span></td>`;
}
return `<td>${escapeHtml(formatValue(v))}</td>`;
}).join('')
}</tr>`;
}).join('')
}</tbody>`;
content.innerHTML = `<div class="table-wrap"><table>${thead}${tbody}</table></div>`;
}
function renderError(msg) {
content.innerHTML = `<div class="error">⚠️ ${escapeHtml(msg)}</div>`;
}
function setLoading(isLoading) {
btn.disabled = isLoading;
if (isLoading) {
content.innerHTML = `<div class="state"><span class="spinner"></span>正在检索,请稍候...</div>`;
}
}
function flashError(msg) {
// 轻量提示:顶部卡片内展示
content.innerHTML = `<div class="error">⚠️ ${escapeHtml(msg)}</div>`;
setTimeout(() => {
const el = document.activeElement;
if (el && el.id !== 'appId') input.focus();
}, 0);
}
function inferColumns(rows) {
// 合并所有键,保持第一行的主序;常见字段优先排序
const pref = ['code', 'name', 'statusText', 'serviceWayText', 'createAt'];
const set = new Set();
rows.forEach(r => Object.keys(r || {}).forEach(k => set.add(k)));
const keys = Array.from(set);
return [...pref.filter(p => keys.includes(p)), ...keys.filter(k => !pref.includes(k))];
}
function formatValue(v) {
if (v == null) return '';
if (typeof v === 'object') return JSON.stringify(v);
return String(v);
}
function escapeHtml(str) {
return String(str)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
}CSS
:root {
--bg: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 50%, #e2e8f0 100%);
--glass: rgba(255, 255, 255, .85);
--border: rgba(0, 0, 0, .06);
--text: #1e293b;
--muted: #64748b;
--primary: #3b82f6;
--primary-ink: #ffffff;
--ring: 0 0 0 3px rgba(59, 130, 246, .1);
--shadow: 0 20px 50px rgba(0, 0, 0, .08), 0 8px 20px rgba(0, 0, 0, .04);
--radius: 24px;
--spacing-xs: 8px;
--spacing-sm: 16px;
--spacing-md: 24px;
--spacing-lg: 32px;
--spacing-xl: 48px;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #334155 100%);
--glass: rgba(15, 23, 42, .8);
--border: rgba(255, 255, 255, .08);
--text: #f1f5f9;
--muted: #94a3b8;
--primary: #60a5fa;
--primary-ink: #0f172a;
--ring: 0 0 0 3px rgba(96, 165, 250, .15);
--shadow: 0 20px 50px rgba(0, 0, 0, .3), 0 8px 20px rgba(0, 0, 0, .2);
}
}
* {
box-sizing: border-box
}
body {
margin: 0;
padding: 0;
font: 16px/1.7 ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Apple Color Emoji", "Segoe UI Emoji";
color: var(--text);
background: transparent;
}
.shell {
width: 100%;
max-width: none;
background: transparent;
border: none;
border-radius: 0;
box-shadow: none;
overflow: visible;
}
.search {
padding: var(--spacing-sm);
}
.input-row {
display: flex;
gap: var(--spacing-xs);
align-items: center;
flex-wrap: nowrap;
background: rgba(255, 255, 255, .8);
border: 2px solid var(--border);
border-radius: 12px;
padding: 8px 12px;
transition: all 0.3s ease;
position: relative;
min-width: 0;
}
@media (prefers-color-scheme: dark) {
.input-row {
background: rgba(255, 255, 255, .08)
}
}
.input-row:focus-within {
box-shadow: var(--ring);
border-color: var(--primary);
background: rgba(255, 255, 255, .95);
}
@media (prefers-color-scheme: dark) {
.input-row:focus-within {
background: rgba(255, 255, 255, .12)
}
}
.icon {
width: 20px;
height: 20px;
flex: 0 0 20px;
opacity: .6;
background: currentColor;
-webkit-mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>') center/contain no-repeat;
mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>') center/contain no-repeat;
color: var(--muted);
}
input {
flex: 1 1 auto;
min-width: 200px;
border: 0;
background: transparent;
outline: none;
padding: var(--spacing-xs) 0;
font-size: 16px;
color: var(--text);
font-weight: 400;
}
input::placeholder {
color: var(--muted);
font-weight: 400;
}
button {
flex: 0 0 auto;
border: 0;
border-radius: 12px;
padding: 8px 16px;
background: var(--primary);
color: var(--primary-ink);
font-weight: 600;
font-size: 15px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(59, 130, 246, .2);
min-width: 80px;
white-space: nowrap;
}
button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, .3);
}
button:disabled {
opacity: .6;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.content {
padding: var(--spacing-sm)
}
.state,
.error {
padding: var(--spacing-sm);
border: 1px dashed var(--border);
border-radius: 8px;
color: var(--muted);
text-align: center;
font-size: 15px;
line-height: 1.6;
}
.error {
border-style: solid;
color: #dc2626;
background: rgba(220, 38, 38, .05);
border-color: rgba(220, 38, 38, .2);
}
.table-wrap {
border-radius: 16px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, .05);
}
table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
background: rgba(255, 255, 255, .8);
border: 1px solid var(--border);
}
@media (prefers-color-scheme: dark) {
table {
background: rgba(255, 255, 255, .06)
}
}
thead th {
position: sticky;
top: 0;
background: rgba(248, 250, 252, .9);
backdrop-filter: blur(8px);
text-align: left;
font-size: 13px;
color: var(--muted);
letter-spacing: 0.5px;
font-weight: 600;
text-transform: uppercase;
}
@media (prefers-color-scheme: dark) {
thead th {
background: rgba(15, 23, 42, .8)
}
}
th,
td {
padding: 8px 12px;
border-bottom: 1px solid var(--border);
white-space: nowrap;
}
tbody tr:last-child td {
border-bottom: 0
}
tbody tr {
transition: all 0.2s ease;
}
tbody tr:hover {
background: rgba(59, 130, 246, .03);
transform: translateY(-1px);
}
.badge {
display: inline-block;
padding: 2px 6px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
background: rgba(59, 130, 246, .1);
color: #1d4ed8;
border: 1px solid rgba(59, 130, 246, .2);
}
@media (prefers-color-scheme: dark) {
.badge {
background: rgba(96, 165, 250, .15);
color: #93c5fd;
border-color: rgba(96, 165, 250, .3);
}
}
.spinner {
width: 18px;
height: 18px;
border: 2px solid transparent;
border-top-color: currentColor;
border-right-color: currentColor;
border-radius: 50%;
display: inline-block;
vertical-align: -4px;
animation: spin .7s linear infinite;
margin-right: 8px
}
@keyframes spin {
to {
transform: rotate(360deg)
}
}
/* 响应式设计优化 */
@media (max-width: 768px) {
body {
padding: var(--spacing-xs);
}
.search {
padding: var(--spacing-xs);
}
.content {
padding: var(--spacing-xs);
}
.table-wrap {
overflow-x: auto;
}
table {
min-width: 600px;
}
th,
td {
padding: 6px 8px;
}
}
@media (max-width: 480px) {
input {
font-size: 16px;
}
button {
font-size: 14px;
}
}