| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712 |
- <!-- API Key管理 -->
- <template>
- <div class="app-container">
- <el-tabs v-model="activeTab" type="card">
- <!-- API Key管理 -->
- <el-tab-pane label="API Key管理" name="management">
- <PageSearch
- ref="searchRef"
- :search-config="searchConfig"
- @query-click="handleQueryClick"
- @reset-click="handleResetClick"
- />
- <PageContent ref="contentRef" :content-config="contentConfig">
- <template #toolbar="{ toolbarRight, onToolbar, removeIds, cols }">
- <CrudToolbarLeft
- :remove-ids="removeIds"
- :perm-create="['module_system:tenant:api-key:create']"
- :perm-delete="['module_system:tenant:api-key:delete']"
- @add="handleOpenDialog('create')"
- @delete="onToolbar('delete')"
- />
- <div class="data-table__toolbar--right">
- <CrudToolbarRight :buttons="toolbarRight" :cols="cols" :on-toolbar="onToolbar" />
- </div>
- </template>
- <template #table="{ data, loading, tableRef, onSelectionChange, pagination }">
- <div class="data-table__content">
- <el-table
- :ref="tableRef as any"
- v-loading="loading"
- row-key="id"
- :data="data"
- height="100%"
- border
- stripe
- @selection-change="onSelectionChange"
- >
- <template #empty>
- <el-empty :image-size="80" description="暂无数据" />
- </template>
- <el-table-column
- v-if="contentCols.find((col) => col.prop === 'selection')?.show"
- type="selection"
- min-width="55"
- align="center"
- />
- <el-table-column
- v-if="contentCols.find((col) => col.prop === 'api_key')?.show"
- key="api_key"
- label="API Key"
- prop="api_key"
- min-width="300"
- show-overflow-tooltip
- >
- <template #default="scope">
- <div class="api-key-container">
- <span>{{ scope.row.api_key }}</span>
- <el-button
- type="text"
- size="small"
- @click="copyToClipboard(scope.row.api_key)"
- >
- 复制
- </el-button>
- </div>
- </template>
- </el-table-column>
- <el-table-column
- v-if="contentCols.find((col) => col.prop === 'status')?.show"
- key="status"
- label="状态"
- prop="status"
- min-width="80"
- align="center"
- >
- <template #default="scope">
- <el-tag :type="scope.row.status === '0' ? 'success' : 'danger'">
- {{ scope.row.status === "0" ? "正常" : "禁用" }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column
- v-if="contentCols.find((col) => col.prop === 'expired_at')?.show"
- key="expired_at"
- label="过期时间"
- prop="expired_at"
- min-width="180"
- show-overflow-tooltip
- />
- <el-table-column
- v-if="contentCols.find((col) => col.prop === 'return_url')?.show"
- key="return_url"
- label="回调地址"
- prop="return_url"
- min-width="200"
- show-overflow-tooltip
- />
- <!-- <el-table-column
- v-if="contentCols.find((col) => col.prop === 'last_used_at')?.show"
- key="last_used_at"
- label="最后使用时间"
- prop="last_used_at"
- min-width="180"
- show-overflow-tooltip
- >
- <template #default="scope">
- {{ scope.row.last_used_at || "未使用" }}
- </template>
- </el-table-column> -->
- <el-table-column
- v-if="contentCols.find((col) => col.prop === 'description')?.show"
- key="description"
- label="描述"
- prop="description"
- min-width="150"
- show-overflow-tooltip
- />
- <el-table-column
- v-if="contentCols.find((col) => col.prop === 'created_time')?.show"
- key="created_time"
- label="创建时间"
- prop="created_time"
- min-width="180"
- show-overflow-tooltip
- />
- <el-table-column
- v-if="contentCols.find((col) => col.prop === 'operation')?.show"
- fixed="right"
- label="操作"
- align="center"
- min-width="200"
- >
- <template #default="scope">
- <el-button
- v-hasPerm="['module_system:tenant:api-key:update']"
- type="primary"
- size="small"
- link
- @click="handleUpdateStatus(scope.row.id, scope.row.status === '0' ? '1' : '0')"
- >
- {{ scope.row.status === '0' ? '禁用' : '启用' }}
- </el-button>
- <el-button
- v-hasPerm="['module_system:tenant:api-key:delete']"
- type="danger"
- size="small"
- link
- @click="handleRowDelete(scope.row.id)"
- >
- 删除
- </el-button>
- </template>
- </el-table-column>
- </el-table>
- </div>
- </template>
- </PageContent>
- <EnhancedDialog
- v-model="dialogVisible.visible"
- :title="dialogVisible.title"
- @close="handleCloseDialog"
- width="500"
- >
- <el-form
- ref="dataFormRef"
- :model="formData"
- :rules="rules"
- label-suffix=":"
- label-width="auto"
- label-position="right"
- >
- <!-- <el-form-item label="租户ID" prop="tenant_id">
- <el-input v-model="formData.tenant_id" placeholder="可选,默认使用当前租户" :maxlength="20" />
- </el-form-item> -->
- <el-form-item label="过期天数" prop="expired_days">
- <el-input v-model.number="formData.expired_days" type="number" placeholder="请输入过期天数" min="1" :maxlength="4" />
- </el-form-item>
- <el-form-item label="描述" prop="description">
- <el-input
- v-model="formData.description"
- type="textarea"
- :rows="3"
- placeholder="请输入描述(可选)"
- :maxlength="255"
- />
- </el-form-item>
- <el-form-item label="回调地址" prop="return_url">
- <el-input
- v-model="formData.return_url"
- type="textarea"
- :rows="2"
- placeholder="请输入回调地址(可选)"
- :maxlength="255"
- />
- </el-form-item>
- </el-form>
- <template #footer>
- <div class="dialog-footer">
- <el-button @click="handleCloseDialog">取消</el-button>
- <el-button
- v-hasPerm="['module_system:tenant:api-key:create']"
- type="primary"
- :loading="submitLoading"
- @click="handleSubmit"
- >
- 确定
- </el-button>
- </div>
- </template>
- </EnhancedDialog>
- <EnhancedDialog
- v-model="apiKeyDetailVisible"
- title="API Key详情"
- width="600"
- :close-on-press-escape="false"
- :close-on-click-modal="false"
- @close="handleCloseApiKeyDetail"
- >
- <el-alert
- title="请及时保存API Key和Secret,关闭后无法再次查看"
- type="warning"
- :closable="false"
- show-icon
- style="margin-bottom: 16px"
- />
- <el-descriptions :column="1" border>
- <el-descriptions-item label="API Key">
- <div class="api-key-detail">
- <span>{{ apiKeyDetail.api_key }}</span>
- <el-button type="text" size="small" @click="copyToClipboard(apiKeyDetail.api_key)">
- 复制
- </el-button>
- </div>
- </el-descriptions-item>
- <el-descriptions-item label="API Secret">
- <div class="api-key-detail">
- <span>{{ apiKeyDetail.api_secret }}</span>
- <el-button type="text" size="small" @click="copyToClipboard(apiKeyDetail.api_secret || '')">
- 复制
- </el-button>
- </div>
- </el-descriptions-item>
- <el-descriptions-item label="状态">
- <el-tag :type="apiKeyDetail.status === '0' ? 'success' : 'danger'">
- {{ apiKeyDetail.status === "0" ? "正常" : "禁用" }}
- </el-tag>
- </el-descriptions-item>
- <el-descriptions-item label="过期时间">
- {{ apiKeyDetail.expired_at }}
- </el-descriptions-item>
- <el-descriptions-item label="创建时间">
- {{ apiKeyDetail.created_time }}
- </el-descriptions-item>
- <el-descriptions-item label="描述">
- {{ apiKeyDetail.description || "无" }}
- </el-descriptions-item>
- <el-descriptions-item label="回调地址">
- {{ apiKeyDetail.return_url || "无" }}
- </el-descriptions-item>
- </el-descriptions>
- <template #footer>
- <div class="dialog-footer">
- <el-button @click="downloadApiKeyCsv">下载CSV</el-button>
- <el-button type="primary" @click="handleConfirmApiKeyDetail">确认已保存</el-button>
- </div>
- </template>
- </EnhancedDialog>
- </el-tab-pane>
- <!-- 应用配置 -->
- <el-tab-pane label="应用配置" name="config">
- <div class="config-layout">
- <!-- 查询中显示loading -->
- <el-card v-if="configLoading" v-loading="configLoading" class="config-card">
- <template #header>
- <div class="card-header">
- <span>开放平台配置</span>
- </div>
- </template>
- </el-card>
- <!-- 配置不存在时显示创建按钮 -->
- <el-card v-else-if="!configExists" class="config-card">
- <template #header>
- <div class="card-header">
- <span>开放平台配置</span>
- </div>
- </template>
- <div class="config-empty">
- <el-empty description="暂无开放平台配置" />
- <p style="margin: 16px 0; color: #606266;">
- 请先创建开放平台配置,才能收到平台回调通知。
- </p>
- <el-button
- type="primary"
- :loading="configLoading"
- @click="handleCreateConfig"
- >
- 创建配置
- </el-button>
- </div>
- </el-card>
- <!-- 配置存在时显示编辑表单 -->
- <el-card v-else class="config-card">
- <template #header>
- <div class="card-header">
- <span>开放平台配置</span>
- </div>
- </template>
- <el-form
- ref="configFormRef"
- :model="configForm"
- label-width="140px"
- label-position="right"
- style="max-width: 600px"
- >
- <el-form-item label="应用ID">
- <el-input v-model="configForm.app_id" disabled>
- <template #append>
- <el-button @click="copyToClipboard(configForm.app_id)">
- <el-icon><CopyDocument /></el-icon>
- </el-button>
- </template>
- </el-input>
- </el-form-item>
- <el-form-item label="网关地址">
- <el-input v-model="configForm.gateway_url" disabled>
- <template #append>
- <el-button @click="copyToClipboard(configForm.gateway_url)">
- <el-icon><CopyDocument /></el-icon>
- </el-button>
- </template>
- </el-input>
- </el-form-item>
- <!-- <el-form-item label="异步通知地址">
- <el-input
- v-model="configForm.notify_url"
- placeholder="请输入异步通知地址"
- clearable
- />
- </el-form-item> -->
- <el-form-item label="回调地址">
- <el-input
- v-model="configForm.return_url"
- placeholder="请输入回调地址"
- clearable
- />
- </el-form-item>
- <el-form-item label="状态">
- <el-tag :type="configForm.status === 'ENABLED' ? 'success' : 'info'">
- {{ configForm.status === 'ENABLED' ? '启用' : '禁用' }}
- </el-tag>
- </el-form-item>
- <!-- <el-form-item label="描述">
- <el-input
- v-model="configForm.description"
- type="textarea"
- :rows="2"
- disabled
- />
- </el-form-item> -->
- <el-form-item>
- <el-button
- type="primary"
- :loading="configLoading"
- @click="handleSaveConfig"
- >
- 保存配置
- </el-button>
- </el-form-item>
- </el-form>
- </el-card>
- </div>
- </el-tab-pane>
- <!-- 接入文档 -->
- <el-tab-pane label="接入文档" name="docs">
- <div class="docs-layout">
- <!-- 左侧导航 -->
- <div class="docs-sidebar">
- <el-scrollbar>
- <div class="sidebar-section">
- <h3>认证</h3>
- <ul>
- <li @click="activeSection = 'auth'" :class="{ active: activeSection === 'auth' }">认证方式</li>
- <li @click="activeSection = 'signature'" :class="{ active: activeSection === 'signature' }">签名验证</li>
- <li @click="activeSection = 'notes'" :class="{ active: activeSection === 'notes' }">注意事项</li>
- </ul>
- </div>
- <div class="sidebar-section">
- <h3>账户接口</h3>
- <ul>
- <li @click="activeSection = 'transfer'" :class="{ active: activeSection === 'transfer' }">发起转账</li>
- <li @click="activeSection = 'transfer_query'" :class="{ active: activeSection === 'transfer_query' }">查询转账</li>
- <li @click="activeSection = 'balance'" :class="{ active: activeSection === 'balance' }">账户余额</li>
- </ul>
- </div>
- <div class="sidebar-section">
- <h3>回调通知</h3>
- <ul>
- <li @click="activeSection = 'callback'" :class="{ active: activeSection === 'callback' }">通知接口</li>
- </ul>
- </div>
- <div class="sidebar-section">
- <h3>其他</h3>
- <ul>
- <li @click="activeSection = 'errors'" :class="{ active: activeSection === 'errors' }">常见错误</li>
- <li @click="activeSection = 'php'" :class="{ active: activeSection === 'php' }">PHP示例代码</li>
- </ul> identity_type
- </div>
- </el-scrollbar>
- </div>
- <!-- 右侧内容 -->
- <div class="docs-content">
- <el-card>
- <template #header>
- <div class="card-header">
- <span>{{ getSectionTitle() }}</span>
- </div>
- </template>
- <div v-if="activeSection === 'auth'" class="section-content">
- <h2>1. 认证方式</h2>
- <p>使用API Key进行认证时,需要在请求头中添加以下信息:</p>
- <pre><code>Authorization: ApiKey {api_key}
- Signature: {signature}</code></pre>
- <p>其中:</p>
- <ul>
- <li><strong>Authorization</strong>:API Key认证头,格式为 <code>ApiKey {api_key}</code></li>
- <li><strong>Signature</strong>:请求签名(必填),用于验证请求数据的完整性</li>
- </ul>
- </div>
- <div v-else-if="activeSection === 'signature'" class="section-content">
- <h2>2. 签名验证</h2>
- <p>签名用于验证请求数据的完整性,防止数据被篡改。签名生成步骤:</p>
- <ol>
- <li>过滤请求参数:排除 <code>sign</code> 参数、<code>null</code> 值、空字符串、空数组、空对象</li>
- <li>将过滤后的参数按参数名ASCII码升序排序</li>
- <li>对字典或列表类型的值进行JSON序列化(<code>sort_keys=true</code>,<code>separators=(',', ':')</code>)</li>
- <li>对每个参数值进行URL编码(<code>UTF-8</code>编码)</li>
- <li>将排序后的参数拼接为字符串:<code>key1=value1&key2=value2</code></li>
- <li>使用API Secret作为密钥,通过HMAC-SHA256算法生成签名</li>
- <li>将签名添加到请求头 <code>Signature</code> 中</li>
- </ol>
- <h3>2.1 签名计算示例</h3>
- <pre><code># 原始请求数据
- {
- "account_book_id": "123456",
- "amount": "100.00",
- "payee_info": {
- "identity_type": "ALIPAY_ACCOUNT",
- "name": "张三",
- "identity": "zhangsan@example.com"
- },
- "sign": "不需要参与签名",
- "empty_param": "",
- "null_param": null
- }
- # 1. 过滤后(排除sign、空字符串、null)
- {
- "account_book_id": "123456",
- "amount": "100.00",
- "payee_info": {
- "identity_type": "ALIPAY_ACCOUNT",
- "name": "张三",
- "identity": "zhangsan@example.com"
- }
- }
- # 2. 按参数名升序排序
- account_book_id, amount, payee_info
- # 3. JSON序列化嵌套对象
- payee_info={"identity":"zhangsan@example.com","identity_type":"ALIPAY_ACCOUNT","name":"张三"}
- # 4. URL编码(处理中文)
- name=%E5%BC%A0%E4%B8%89
- # 5. 拼接字符串
- account_book_id=123456&amount=100.0&payee_info=%7B%22identity%22%3A%22zhangsan%40example.com%22%2C%22identity_type%22%3A%22ALIPAY_ACCOUNT%22%2C%22name%22%3A%22%E5%BC%A0%E4%B8%89%22%7D
- # 6. HMAC-SHA256签名(密钥为API Secret)
- signature = HMAC-SHA256(api_secret, sign_str)
- # 7. 请求头中添加签名
- Signature: {signature}</code></pre>
- </div>
- <div v-else-if="activeSection === 'notes'" class="section-content">
- <h2>3. 注意事项</h2>
- <ul>
- <li>API Key和Secret请妥善保管,不要泄露给他人</li>
- <li>签名验证是<strong>必填</strong>的,未带签名或签名错误将返回401</li>
- <li>签名使用HMAC-SHA256算法,密钥为API Secret</li>
- <li>签名计算前会自动过滤:<code>sign</code>参数、<code>null</code>值、空字符串、空数组、空对象</li>
- <li>嵌套对象(如<code>payee_info</code>)会先进行JSON序列化再参与签名</li>
- <li>参数值会进行URL编码(UTF-8),确保中文字符正确处理</li>
- <li>定期更新API Key,建议每3-6个月更换一次</li>
- <li>如发现API Key泄露,请立即禁用并重新生成</li>
- <li>API Key有过期时间,请在过期前及时更新</li>
- </ul>
- </div>
- <div v-else-if="activeSection === 'transfer'" class="section-content">
- <h2>4. 发起转账</h2>
- <h3>4.1 接口说明</h3>
- <p>从资金账户转账到支付宝账户/银行卡</p>
- <h4>API接口地址</h4>
- <p><code>POST https://api.qcsj88888.com/payment/openapi/account/transfer</code></p>
- <h4>请求参数</h4>
- <table class="api-table">
- <thead>
- <tr>
- <th>参数名</th>
- <th>类型</th>
- <th>是否必填</th>
- <th>描述</th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td>account_book_id</td>
- <td>string</td>
- <td>是</td>
- <td>付款方资金账户号</td>
- </tr>
- <tr>
- <td>amount</td>
- <td>string</td>
- <td>是</td>
- <td>转账金额,单位为元,精确到小数点后两位,大于0.02元</td>
- </tr>
- <tr>
- <td>order_title</td>
- <td>string</td>
- <td>否</td>
- <td>转账标题</td>
- </tr>
- <tr>
- <td>remark</td>
- <td>string</td>
- <td>否</td>
- <td>转账备注</td>
- </tr>
- <tr>
- <td>third_biz_no</td>
- <td>string</td>
- <td>是</td>
- <td>三方订单号(商户侧唯一标识,不可重复)</td>
- </tr>
- <tr>
- <td>payee_info</td>
- <td>object</td>
- <td>是</td>
- <td>
- <div class="expandable-section">
- <span @click="toggleExpand('payee_info')" class="expand-btn">
- {{ expandedSections.payee_info ? '▼' : '▶' }} 收款方信息
- </span>
- <div v-if="expandedSections.payee_info" class="expandable-content">
- <table class="api-table nested-table">
- <thead>
- <tr>
- <th>参数名</th>
- <th>类型</th>
- <th>是否必填</th>
- <th>描述</th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td>identity_type</td>
- <td>string</td>
- <td>是</td>
- <td>收款方类型:alipay(支付宝账户)/ bank(银行卡)</td>
- </tr>
- <tr>
- <td>name</td>
- <td>string</td>
- <td>是</td>
- <td>收款方真实姓名</td>
- </tr>
- <tr>
- <td>identity</td>
- <td>string</td>
- <td>是</td>
- <td>收款方唯一标识(支付宝账号/银行卡号)</td>
- </tr>
- <tr>
- <td>bankcard_ext_info</td>
- <td>object</td>
- <td>否</td>
- <td>
- <div class="expandable-section">
- <span @click="toggleExpand('bankcard_ext_info')" class="expand-btn">
- {{ expandedSections.bankcard_ext_info ? '▼' : '▶' }} 银行卡信息(当 identity_type 为 bank 时必填)
- </span>
- <div v-if="expandedSections.bankcard_ext_info" class="expandable-content">
- <table class="api-table nested-table">
- <thead>
- <tr>
- <th>参数名</th>
- <th>类型</th>
- <th>是否必填</th>
- <th>描述</th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td>account_type</td>
- <td>string</td>
- <td>是</td>
- <td>收款账户类型: 1/2。对公: 1,对私: 2</td>
- </tr>
- <tr>
- <td>inst_name</td>
- <td>string</td>
- <td>否</td>
- <td>机构名称(当 account_type 是 1 时必填)</td>
- </tr>
- <tr>
- <td>inst_province</td>
- <td>string</td>
- <td>否</td>
- <td>银行所在省份</td>
- </tr>
- <tr>
- <td>inst_city</td>
- <td>string</td>
- <td>否</td>
- <td>收款银行所在市</td>
- </tr>
- <tr>
- <td>inst_branch_name</td>
- <td>string</td>
- <td>否</td>
- <td>收款银行所属支行</td>
- </tr>
- <tr>
- <td>bank_code</td>
- <td>string</td>
- <td>否</td>
- <td>银行支行联行号</td>
- </tr>
- </tbody>
- </table>
- </div>
- </div>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- </div>
- </td>
- </tr>
- </tbody>
- </table>
- <h4>请求示例</h4>
- <pre><code>curl -X POST 'https://api.qcsj88888.com/payment/openapi/account/transfer' \
- -H 'Authorization: ApiKey your_api_key' \
- -H 'Signature: your_signature' \
- -H 'Content-Type: application/json' \
- -d '{
- "account_book_id": "资金账号",
- "amount": "100.00",
- "order_title": "转账标题",
- "third_biz_no": "商户订单号202604270001",
- "payee_info": {
- "identity_type": "ALIPAY_ACCOUNT",
- "name": "收款人姓名",
- "identity": "收款人支付宝账号"
- }
- }'</code></pre>
- <h4>响应示例</h4>
- <pre><code>{"code": 200, "message": "转账申请已提交", "data": {"status": "DEALING", "order_no": "2026042711122334455", "third_biz_no": "商户订单号202604270001"}}</code></pre>
- </div>
- <div v-else-if="activeSection === 'transfer_query'" class="section-content">
- <h2>5. 查询转账</h2>
- <h3>5.1 接口说明</h3>
- <p>根据三方订单号查询转账状态和详情</p>
- <h4>API接口地址</h4>
- <p><code>POST https://api.qcsj88888.com/payment/openapi/account/transfer/query</code></p>
- <h4>请求参数</h4>
- <table class="api-table">
- <thead>
- <tr>
- <th>参数名</th>
- <th>类型</th>
- <th>是否必填</th>
- <th>描述</th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td>third_biz_no</td>
- <td>string</td>
- <td>是</td>
- <td>三方订单号(发起转账时传入的商户侧唯一标识)</td>
- </tr>
- </tbody>
- </table>
- <h4>请求示例</h4>
- <pre><code>curl -X POST 'https://api.qcsj88888.com/payment/openapi/account/transfer/query' \
- -H 'Authorization: ApiKey your_api_key' \
- -H 'Signature: your_signature' \
- -H 'Content-Type: application/json' \
- -d '{
- "third_biz_no": "商户订单号202604270001"
- }'</code></pre>
- <h4>响应示例</h4>
- <pre><code>{
- "code": 200,
- "message": "查询成功",
- "data": {
- "status": "SUCCESS",
- "order_no": "2026042711122334455",
- "amount": "100.00",
- "payee_info": {
- "identity_type": "ALIPAY_ACCOUNT",
- "name": "张*",
- "identity": "z****@example.com"
- },
- "created_time": "2026-04-27 11:22:33",
- "updated_time": "2026-04-27 11:25:45"
- }
- }</code></pre>
- <h4>状态说明</h4>
- <table class="api-table">
- <thead>
- <tr>
- <th>状态码</th>
- <th>描述</th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td>DEALING</td>
- <td>处理中</td>
- </tr>
- <tr>
- <td>SUCCESS</td>
- <td>成功</td>
- </tr>
- <tr>
- <td>FAIL</td>
- <td>失败</td>
- </tr>
- <tr>
- <td>REFUND</td>
- <td>已退款</td>
- </tr>
- </tbody>
- </table>
- </div>
- <div v-else-if="activeSection === 'balance'" class="section-content">
- <h2>6. 账户余额</h2>
- <h3>6.1 接口说明</h3>
- <p>查询指定企业资金专户的余额信息</p>
- <h4>API接口地址</h4>
- <p><code>POST https://api.qcsj88888.com/payment/openapi/account/balance/query</code></p>
- <h4>请求参数</h4>
- <table class="api-table">
- <thead>
- <tr>
- <th>参数名</th>
- <th>类型</th>
- <th>是否必填</th>
- <th>描述</th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td>enterprise_id</td>
- <td>string</td>
- <td>是</td>
- <td>企业ID(在支付宝企业码平台注册的企业唯一标识)</td>
- </tr>
- </tbody>
- </table>
- <h4>请求示例</h4>
- <pre><code>curl -X POST 'https://api.qcsj88888.com/payment/openapi/account/balance/query' \
- -H 'Authorization: ApiKey your_api_key' \
- -H 'Signature: your_signature' \
- -H 'Content-Type: application/json' \
- -d '{
- "enterprise_id": "2088480777900000"
- }'</code></pre>
- <h4>响应示例</h4>
- <pre><code>{
- "code": 200,
- "message": "查询成功",
- "data": [
- {
- "account_book_id": "2088480770900000",
- "available_amount": "50000.00",
- "enable_status": "ENABLE",
- "scene": "B2B_TRANS",
- "account_card_info": {
- "card_no": "xxxx",
- "bank_name": "招商银行"
- }
- }
- ]
- }</code></pre>
- <h4>响应字段说明</h4>
- <table class="api-table">
- <thead>
- <tr>
- <th>字段名</th>
- <th>类型</th>
- <th>描述</th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td>account_book_id</td>
- <td>string</td>
- <td>资金专户号</td>
- </tr>
- <tr>
- <td>available_amount</td>
- <td>string</td>
- <td>可用余额(单位:元,精确到小数点后两位)</td>
- </tr>
- <tr>
- <td>enable_status</td>
- <td>string</td>
- <td>启用状态:ENABLE(启用)/ DISABLE(禁用)</td>
- </tr>
- <tr>
- <td>scene</td>
- <td>string</td>
- <td>场景类型:B2B_TRANS(B2B转账)</td>
- </tr>
- <tr>
- <td>account_card_info</td>
- <td>object</td>
- <td>账户卡信息(银行卡号、银行名称等)</td>
- </tr>
- </tbody>
- </table>
- <h4>注意事项</h4>
- <ul>
- <li>返回结果为<strong>数组</strong>,一个企业可能有多个资金专户</li>
- <li>余额单位为<strong>元</strong>,精确到小数点后两位</li>
- <li>仅返回 <code>scene</code> 为 <code>B2B_TRANS</code> 的资金专户</li>
- </ul>
- </div>
- <div v-else-if="activeSection === 'callback'" class="section-content">
- <h2>7. 回调通知</h2>
- <h3>7.1 接口说明</h3>
- <p>当转账状态发生变化时,系统会主动向商户配置的回调地址发送通知。</p>
- <h4>回调地址配置</h4>
- <p>系统按照以下优先级获取回调地址:</p>
- <ol style="margin-left: 20px;">
- <li><strong>API Key 级别</strong>:在创建/编辑 API Key 时配置回调地址(优先级最高)</li>
- <li><strong>开放平台配置</strong>:在 <strong>应用配置</strong> 页面设置默认回调地址</li>
- </ol>
- <p style="color: #909399; margin-top: 8px;">说明:如果 API Key 已配置回调地址,则优先使用;否则使用开放平台配置中的回调地址。</p>
- <h4>通知方式</h4>
- <ul>
- <li><strong>POST 请求</strong>:系统通过 HTTP POST 方式将通知数据发送到商户的回调地址</li>
- <li><strong>表单形式</strong>:通知参数以表单形式提交(Content-Type: multipart/form-data)</li>
- <li><strong>重试机制</strong>:如果通知失败,系统会自动重试(最多2次,间隔1秒、2秒)</li>
- </ul>
- <h4>请求参数</h4>
- <table class="api-table">
- <thead>
- <tr>
- <th>参数名</th>
- <th>类型</th>
- <th>描述</th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td>notify_id</td>
- <td>string</td>
- <td>通知ID,唯一标识</td>
- </tr>
- <tr>
- <td>timestamp</td>
- <td>int</td>
- <td>通知时间戳(毫秒)</td>
- </tr>
- <tr>
- <td>content</td>
- <td>string</td>
- <td>JSON格式的通知内容</td>
- </tr>
- </tbody>
- </table>
- <h4>content 字段说明</h4>
- <table class="api-table">
- <thead>
- <tr>
- <th>参数名</th>
- <th>类型</th>
- <th>描述</th>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td>status</td>
- <td>string</td>
- <td>转账状态:DEALING(处理中)、SUCCESS(成功)、FAIL(失败)、REFUND(已退款)</td>
- </tr>
- <tr>
- <td>order_no</td>
- <td>string</td>
- <td>平台订单号</td>
- </tr>
- <tr>
- <td>third_biz_no</td>
- <td>string</td>
- <td>商户订单号(发起转账时传入的三方订单号)</td>
- </tr>
- <tr>
- <td>amount</td>
- <td>number</td>
- <td>转账金额(元)</td>
- </tr>
- <tr>
- <td>created_time</td>
- <td>string</td>
- <td>创建时间</td>
- </tr>
- <tr>
- <td>updated_time</td>
- <td>string</td>
- <td>更新时间</td>
- </tr>
- </tbody>
- </table>
- <h4>通知示例</h4>
- <pre><code>POST /your/callback/url HTTP/1.1
- Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
- ------WebKitFormBoundary
- Content-Disposition: form-data; name="notify_id"
- n1234567890123456789
- ------WebKitFormBoundary
- Content-Disposition: form-data; name="timestamp"
- 1715767200000
- ------WebKitFormBoundary
- Content-Disposition: form-data; name="content"
- {
- "status": "SUCCESS",
- "order_no": "2026042711122334455",
- "third_biz_no": "商户订单号202604270001",
- "amount": "100.00",
- "created_time": "2026-04-27 11:22:33",
- "updated_time": "2026-04-27 11:25:45"
- }
- ------WebKitFormBoundary--</code></pre>
- <h4>响应要求</h4>
- <p>商户服务端收到通知后,需要返回 HTTP 200 状态码表示成功接收。如果返回非 200 状态码或超时,系统会进行重试。</p>
- <h4>注意事项</h4>
- <ul>
- <li>回调地址需要支持 <strong>HTTPS</strong> 协议</li>
- <li>确保回调接口能够在 <strong>5秒内</strong> 返回响应</li>
- <li><span style="color: #f56c6c;"><strong>重要</strong>:回调通知不包含签名验证,收到通知后请主动调用查询接口确认订单状态,以确保数据真实性</span></li>
- <li>通知可能会重复发送,请确保业务逻辑支持 <strong>幂等性</strong></li>
- <li>系统最多重试 <strong>2次</strong>,重试间隔为 1 秒和 2 秒</li>
- </ul>
- </div>
- <div v-else-if="activeSection === 'errors'" class="section-content">
- <h2>8. 常见错误</h2>
- <ul>
- <li><strong>401 Invalid API Key</strong>:API Key无效或已过期</li>
- <li><strong>401 Signature header required</strong>:未提供Signature请求头</li>
- <li><strong>401 Invalid Signature</strong>:签名验证失败,请检查签名计算方式</li>
- <li><strong>400 Bad Request</strong>:请求参数错误</li>
- <li><strong>403 Forbidden</strong>:无权限访问</li>
- <li><strong>500 Internal Server Error</strong>:服务器内部错误</li>
- </ul>
- </div>
- <div v-else-if="activeSection === 'php'" class="section-content">
- <h2>9. PHP 示例代码</h2>
- <h3>9.1 以下是签名生成的 PHP 示例代码:</h3>
- <pre class="code-block"><code class="language-php"><?php
- class SignatureGenerator
- {
- private static function ksortRecursive(&$array) {
- if (!is_array($array)) return;
- ksort($array, SORT_STRING);
- foreach ($array as &$value) {
- self::ksortRecursive($value);
- }
- }
- public static function generateSignature(
- string $apiSecret,
- array $requestData,
- array $excludeParams = ['sign']
- ): string {
- $filteredData = [];
- foreach ($requestData as $key => $value) {
- if (in_array($key, $excludeParams, true)) {
- continue;
- }
- if ($value === null || $value === '') {
- continue;
- }
- if (is_array($value) && empty($value)) {
- continue;
- }
- $filteredData[$key] = $value;
- }
- self::ksortRecursive($filteredData);
- $collect = [];
- foreach ($filteredData as $key => $value) {
- if (is_array($value)) {
- $value = json_encode($value, JSON_UNESCAPED_SLASHES);
- }
- $encodedValue = rawurlencode((string)$value);
- $collect[] = "{$key}={$encodedValue}";
- }
- $signStr = implode('&', $collect);
- return hash_hmac('sha256', $signStr, $apiSecret);
- }
- public static function verifySignature(
- string $apiSecret,
- array $requestData,
- string $signature
- ): bool {
- $expectedSignature = self::generateSignature($apiSecret, $requestData);
- return hash_equals($expectedSignature, $signature);
- }
- }
- // ================= 测试调用 =================
- $apiSecret = 'your_api_secret_here';
- $requestData = [
- "account_book_id" => "2088480770900000",
- "amount" => "1.00",
- "order_title" => "Apikey转账",
- "third_biz_no" => "1234242026042700111",
- "payee_info" => [
- "identity_type" => "ALIPAY_ACCOUNT",
- "name" => "钱先生",
- "identity" => "1xx9xx9xxxxx"
- ]
- ];
- // 生成签名
- $signature = SignatureGenerator::generateSignature($apiSecret, $requestData);
- echo "生成的签名: {$signature}\n";
- // 验证签名
- $isValid = SignatureGenerator::verifySignature($apiSecret, $requestData, $signature);
- echo "签名验证结果: " . ($isValid ? '有效' : '无效') . "\n";
- ?></code></pre>
- </div>
- </el-card>
- </div>
- </div>
- </el-tab-pane>
- </el-tabs>
- </div>
- </template>
- <script setup lang="ts">
- import { ref, reactive, onMounted } from "vue";
- import ApiKeyAPI, {
- ApiKeyCreateForm,
- ApiKeyPageQuery,
- ApiKeyResponse,
- } from "@/api/module_payment/apikey";
- import OpenAPI from "@/api/module_payment/openapi";
- import CrudToolbarLeft from "@/components/CURD/CrudToolbarLeft.vue";
- import CrudToolbarRight from "@/components/CURD/CrudToolbarRight.vue";
- import PageSearch from "@/components/CURD/PageSearch.vue";
- import PageContent from "@/components/CURD/PageContent.vue";
- import EnhancedDialog from "@/components/CURD/EnhancedDialog.vue";
- import { useCrudList } from "@/components/CURD/useCrudList";
- import type { IContentConfig, ISearchConfig } from "@/components/CURD/types";
- import { ElMessage } from "element-plus";
- import { Loading, CopyDocument } from "@element-plus/icons-vue";
- import { watch } from "vue";
- defineOptions({
- name: "ApiKey",
- inheritAttrs: false,
- });
- const activeTab = ref("management");
- const activeSection = ref("auth");
- const { searchRef, contentRef, handleQueryClick, handleResetClick, refreshList } = useCrudList();
- const dataFormRef = ref();
- const submitLoading = ref(false);
- const apiKeyDetailVisible = ref(false);
- const configFormRef = ref();
- const configLoading = ref(true);
- const expandedSections = ref({
- payee_info: false,
- bankcard_ext_info: false
- });
- const configExists = ref(false);
- const configForm = reactive({
- app_id: "",
- gateway_url: "",
- notify_url: "",
- return_url: "",
- status: "ENABLED",
- description: "",
- });
- watch(activeTab, (newTab) => {
- if (newTab === "config") {
- loadOpenConf();
- }
- });
- async function loadOpenConf() {
- configLoading.value = true;
- configExists.value = false;
- try {
- const res = await OpenAPI.getOpenConf();
- if (res.data.code === 0 && res.data.data) {
- Object.assign(configForm, res.data.data);
- configExists.value = true;
- } else {
- configExists.value = false;
- }
- } catch (error: any) {
- console.error("加载配置失败:", error);
- // 如果返回"配置不存在"错误,则显示创建按钮
- if (error?.response?.data?.msg?.includes("配置不存在")) {
- configExists.value = false;
- }
- } finally {
- configLoading.value = false;
- }
- }
- async function handleCreateConfig() {
- configLoading.value = true;
- try {
- const res = await OpenAPI.saveOpenConf({
- notify_url: "",
- return_url: "",
- });
- if (res.data.code === 0 && res.data.data) {
- Object.assign(configForm, res.data.data);
- configExists.value = true;
- ElMessage.success("创建成功");
- }
- } catch (error) {
- console.error(error);
- ElMessage.error("创建失败");
- } finally {
- configLoading.value = false;
- }
- }
- async function handleSaveConfig() {
- configLoading.value = true;
- try {
- const res = await OpenAPI.saveOpenConf({
- notify_url: configForm.notify_url,
- return_url: configForm.return_url,
- });
- if (res.data.code === 0 && res.data.data) {
- Object.assign(configForm, res.data.data);
- }
- } catch (error) {
- console.error(error);
- } finally {
- configLoading.value = false;
- }
- }
- function toggleExpand(section: string) {
- expandedSections.value[section] = !expandedSections.value[section];
- }
- function getSectionTitle() {
- const titles = {
- auth: "认证方式",
- signature: "签名验证",
- notes: "注意事项",
- transfer: "发起转账",
- transfer_query: "查询转账",
- balance: "账户余额",
- callback: "回调通知",
- errors: "常见错误",
- php: "PHP示例代码"
- };
- return titles[activeSection.value] || "API文档";
- }
- const apiKeyDetail = reactive<ApiKeyResponse>({
- id: 0,
- api_key: "",
- api_secret: "",
- status: "0",
- expired_at: "",
- created_time: "",
- return_url: "",
- });
- const searchConfig = reactive<ISearchConfig>({
- permPrefix: "module_system:tenant:api-key",
- colon: true,
- isExpandable: true,
- showNumber: 2,
- form: { labelWidth: "auto" },
- formItems: [
- {
- prop: "status",
- label: "状态",
- type: "select",
- attrs: {
- placeholder: "请选择状态",
- clearable: true,
- options: [
- { label: "正常", value: "0" },
- { label: "禁用", value: "1" },
- ],
- style: { width: "167.5px" },
- },
- },
- ],
- });
- const contentCols = reactive<
- Array<{
- prop?: string;
- label?: string;
- show?: boolean;
- }>
- >([
- { prop: "selection", label: "选择框", show: true },
- { prop: "id", label: "ID", show: false },
- { prop: "api_key", label: "API Key", show: true },
- { prop: "status", label: "状态", show: true },
- { prop: "return_url", label: "回调地址", show: true },
- { prop: "expired_at", label: "过期时间", show: true },
- { prop: "last_used_at", label: "最后使用时间", show: true },
- { prop: "description", label: "描述", show: true },
- { prop: "created_time", label: "创建时间", show: true },
- { prop: "operation", label: "操作", show: true },
- ]);
- const contentConfig = reactive<IContentConfig<ApiKeyPageQuery>>({
- permPrefix: "module_system:tenant:api-key",
- pk: "id",
- cols: contentCols as IContentConfig["cols"],
- hideColumnFilter: false,
- toolbar: [],
- defaultToolbar: [{ name: "refresh", perm: "refresh" }, "filter"],
- pagination: {
- pageSize: 10,
- pageSizes: [10, 20, 30, 50],
- },
- request: { page_no: "page", page_size: "page_size" },
- indexAction: async (params) => {
- const res = await ApiKeyAPI.listApiKey(params as ApiKeyPageQuery);
- return {
- total: res.data.data.total,
- list: res.data.data.items,
- };
- },
- deleteAction: async (ids) => {
- const idList = ids
- .split(",")
- .map((s) => Number(s.trim()))
- .filter((n) => !Number.isNaN(n));
- for (const id of idList) {
- await ApiKeyAPI.deleteApiKey(id);
- }
- },
- deleteConfirm: {
- title: "警告",
- message: "确认删除该项数据?",
- type: "warning",
- },
- });
- const formData = reactive<ApiKeyCreateForm>({
- tenant_id: undefined,
- expired_days: 365,
- description: "",
- return_url: "",
- });
- const dialogVisible = reactive({
- title: "",
- visible: false,
- type: "create" as "create",
- });
- const rules = reactive({
- expired_days: [{ required: true, message: "请输入过期天数", trigger: "blur" }],
- });
- const initialFormData: ApiKeyCreateForm = {
- tenant_id: undefined,
- expired_days: 365,
- description: "",
- };
- async function handleRowDelete(id: number) {
- contentRef.value?.handleDelete(id);
- }
- async function resetForm() {
- if (dataFormRef.value) {
- dataFormRef.value.resetFields();
- dataFormRef.value.clearValidate();
- }
- Object.assign(formData, initialFormData);
- }
- async function handleCloseDialog() {
- dialogVisible.visible = false;
- await resetForm();
- }
- async function handleOpenDialog(type: "create") {
- dialogVisible.type = type;
- dialogVisible.title = "创建API Key";
- dialogVisible.visible = true;
- }
- async function handleSubmit() {
- dataFormRef.value.validate(async (valid: boolean) => {
- if (valid) {
- submitLoading.value = true;
- try {
- const response = await ApiKeyAPI.createApiKey(formData);
- Object.assign(apiKeyDetail, response.data.data);
- apiKeyDetailVisible.value = true;
- dialogVisible.visible = false;
- await resetForm();
- refreshList();
- } catch (error: unknown) {
- console.error(error);
- } finally {
- submitLoading.value = false;
- }
- }
- });
- }
- async function handleUpdateStatus(id: number, status: string) {
- try {
- await ApiKeyAPI.updateApiKeyStatus(id, { status });
- ElMessage.success(`API Key已${status === '0' ? '启用' : '禁用'}`);
- refreshList();
- } catch (error: unknown) {
- console.error(error);
- ElMessage.error('操作失败');
- }
- }
- function copyToClipboard(text: string) {
- navigator.clipboard.writeText(text).then(() => {
- ElMessage.success('复制成功');
- }).catch(() => {
- ElMessage.error('复制失败');
- });
- }
- function handleCloseApiKeyDetail() {
- ElMessage.warning('请务必已保存API Key和Secret,关闭后将无法再次查看');
- apiKeyDetailVisible.value = false;
- }
- function handleConfirmApiKeyDetail() {
- ElMessage.success('已确认保存');
- apiKeyDetailVisible.value = false;
- }
- function downloadApiKeyCsv() {
- const csvContent = `API Key,API Secret,状态,过期时间,创建时间,描述\n"${apiKeyDetail.api_key}","${apiKeyDetail.api_secret}","${apiKeyDetail.status === '0' ? '正常' : '禁用'}","${apiKeyDetail.expired_at}","${apiKeyDetail.created_time}","${apiKeyDetail.description || ''}"`;
- const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
- const link = document.createElement('a');
- const url = URL.createObjectURL(blob);
- link.setAttribute('href', url);
- link.setAttribute('download', `api-key-${apiKeyDetail.api_key || Date.now()}.csv`);
- link.style.visibility = 'hidden';
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- ElMessage.success('CSV已下载');
- }
- </script>
- <style scoped>
- .app-container {
- padding: 20px;
- }
- .api-key-container {
- display: flex;
- align-items: center;
- justify-content: space-between;
- word-break: break-all;
- }
- .api-key-detail {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- flex-wrap: wrap;
- word-break: break-all;
- gap: 8px;
- }
- .api-key-detail span {
- word-break: break-all;
- flex: 1;
- min-width: 0;
- }
- .config-loading-content {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 60px 0;
- color: #909399;
- }
- .config-loading-content .loading-icon {
- font-size: 32px;
- margin-bottom: 16px;
- animation: rotate 1s linear infinite;
- }
- @keyframes rotate {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
- }
- .config-empty {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 40px 0;
- }
- .docs-layout {
- display: flex;
- height: calc(100vh - 120px);
- gap: 20px;
- }
- .docs-sidebar {
- width: 280px;
- background-color: #f5f7fa;
- border-radius: 8px;
- padding: 20px 0;
- overflow: hidden;
- }
- .sidebar-section {
- margin-bottom: 24px;
- padding: 0 20px;
- }
- .sidebar-section h3 {
- font-size: 14px;
- font-weight: bold;
- color: #606266;
- margin-bottom: 12px;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- }
- .sidebar-section ul {
- list-style: none;
- padding: 0;
- margin: 0;
- }
- .sidebar-section li {
- margin-bottom: 8px;
- padding: 8px 12px;
- border-radius: 4px;
- cursor: pointer;
- transition: all 0.3s ease;
- font-size: 14px;
- color: #606266;
- }
- .sidebar-section li:hover {
- background-color: #ecf5ff;
- color: #409eff;
- }
- .sidebar-section li.active {
- background-color: #ecf5ff;
- color: #409eff;
- font-weight: 500;
- border-left: 3px solid #409eff;
- }
- .docs-content {
- flex: 1;
- overflow-y: auto;
- padding: 0 20px;
- }
- .config-layout {
- padding: 20px;
- }
- .config-card {
- max-width: 700px;
- }
- .section-content {
- line-height: 1.6;
- padding: 20px 0;
- }
- .section-content h2 {
- margin-top: 0;
- margin-bottom: 20px;
- font-size: 20px;
- font-weight: bold;
- color: #303133;
- }
- .section-content h3 {
- margin-top: 24px;
- margin-bottom: 12px;
- font-size: 16px;
- font-weight: bold;
- color: #404145;
- }
- .section-content h4 {
- margin-top: 16px;
- margin-bottom: 8px;
- font-size: 14px;
- font-weight: bold;
- color: #606266;
- }
- .section-content p {
- margin-bottom: 12px;
- color: #606266;
- }
- .section-content ul,
- .section-content ol {
- margin-left: 20px;
- margin-bottom: 16px;
- color: #606266;
- }
- .section-content li {
- margin-bottom: 6px;
- }
- .section-content pre {
- background-color: #f5f5f5;
- padding: 16px;
- border-radius: 4px;
- overflow-x: auto;
- margin-bottom: 16px;
- border: 1px solid #e4e7ed;
- }
- .section-content code {
- font-family: 'Courier New', Courier, monospace;
- font-size: 14px;
- color: #303133;
- }
- .api-table {
- width: 100%;
- border-collapse: collapse;
- margin-bottom: 20px;
- border: 1px solid #e4e7ed;
- border-radius: 4px;
- overflow: hidden;
- }
- .api-table th,
- .api-table td {
- border: 1px solid #e4e7ed;
- padding: 12px;
- text-align: left;
- }
- .api-table th {
- background-color: #f5f7fa;
- font-weight: bold;
- color: #303133;
- }
- .api-table tr:nth-child(even) {
- background-color: #fafafa;
- }
- .api-table tr:hover {
- background-color: #f5f7fa;
- }
- .nested-table {
- margin: 10px 0 0 20px;
- font-size: 13px;
- }
- .expandable-section {
- width: 100%;
- }
- .expand-btn {
- cursor: pointer;
- display: inline-block;
- font-weight: 500;
- color: #409eff;
- transition: all 0.3s ease;
- }
- .expand-btn:hover {
- color: #66b1ff;
- }
- .expandable-content {
- margin-top: 8px;
- padding-left: 20px;
- animation: slideDown 0.3s ease;
- }
- @keyframes slideDown {
- from {
- opacity: 0;
- transform: translateY(-10px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
- }
- .card-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 12px 20px;
- /* border-bottom: 1px solid #e4e7ed; */
- margin: -20px -20px 20px -20px;
- /* background-color: #fafafa; */
- }
- .card-header span {
- font-size: 16px;
- font-weight: bold;
- color: #303133;
- }
- </style>
|