| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938 |
- <!-- 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>转账状态:SUCCESS(成功)、FAIL(失败)</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 v-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--
- 转账成功示例
- POST /商户回调地址 HTTP/1.1
- Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
- ------WebKitFormBoundary
- Content-Disposition: form-data; name="notify_id"
- n12535554089713704963
- ------WebKitFormBoundary
- Content-Disposition: form-data; name="timestamp"
- 1779037365774
- ------WebKitFormBoundary
- Content-Disposition: form-data; name="content"
- {
- "status": "SUCCESS",
- "order_no": "2026042711122334455",
- "third_biz_no": "商户订单号202604270001",
- "amount": "100.00",
- "out_biz_no": "12535474352010076162",
- "enterprise_id": "2088480767913636",
- "account_book_id": "2088480770941200",
- "order_title": "转账标题",
- "created_time": "2026-04-27 11:22:33"
- }
- ------WebKitFormBoundary--
- 转账失败示例
- POST /your/callback/url HTTP/1.1
- Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
- ------WebKitFormBoundary
- Content-Disposition: form-data; name="notify_id"
- n12535554089713704963
- ------WebKitFormBoundary
- Content-Disposition: form-data; name="timestamp"
- 1779037365774
- ------WebKitFormBoundary
- Content-Disposition: form-data; name="content"
- {
- "status": "FAIL",
- "third_biz_no": "商户订单号202604270001",
- "amount": "1.00",
- "error_msg": "收款账号不存在或姓名有误,建议核实账号和姓名是否准确"
- }
- ------WebKitFormBoundary--
- ------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>
|