|
|
@@ -0,0 +1,692 @@
|
|
|
+<!-- 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 === 'index')?.show"
|
|
|
+ fixed
|
|
|
+ label="序号"
|
|
|
+ min-width="60"
|
|
|
+ >
|
|
|
+ <template #default="scope">
|
|
|
+ {{ (pagination.currentPage - 1) * pagination.pageSize + scope.$index + 1 }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <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 === '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>
|
|
|
+
|
|
|
+ <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>
|
|
|
+
|
|
|
+ <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="docs">
|
|
|
+ <div class="docs-container">
|
|
|
+ <el-card>
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span>API Key认证使用说明</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div class="docs-content">
|
|
|
+ <h2>1. 认证方式</h2>
|
|
|
+ <p>使用API Key进行认证时,需要在请求头中添加以下信息:</p>
|
|
|
+ <pre><code>Authorization: ApiKey {api_key}:{signature}</code></pre>
|
|
|
+ <p>其中:</p>
|
|
|
+ <ul>
|
|
|
+ <li><strong>api_key</strong>:从管理界面获取的API Key</li>
|
|
|
+ <li><strong>signature</strong>:可选,请求签名(详见签名验证部分)</li>
|
|
|
+ </ul>
|
|
|
+
|
|
|
+ <h2>2. 签名验证</h2>
|
|
|
+ <p>为了增强安全性,建议在请求中添加签名。签名生成步骤:</p>
|
|
|
+ <ol>
|
|
|
+ <li>将请求数据(JSON格式)按参数名升序排序</li>
|
|
|
+ <li>将排序后的参数拼接为字符串:<code>key1=value1&key2=value2</code></li>
|
|
|
+ <li>使用API Secret作为密钥,通过HMAC-SHA256算法生成签名</li>
|
|
|
+ <li>将签名添加到Authorization头中</li>
|
|
|
+ </ol>
|
|
|
+
|
|
|
+ <h2>3. 注意事项</h2>
|
|
|
+ <ul>
|
|
|
+ <li>API Key和Secret请妥善保管,不要泄露给他人</li>
|
|
|
+ <li>定期更新API Key,建议每3-6个月更换一次</li>
|
|
|
+ <li>如发现API Key泄露,请立即禁用并重新生成</li>
|
|
|
+ <li>签名验证可选,但建议在生产环境中使用</li>
|
|
|
+ <li>API Key有过期时间,请在过期前及时更新</li>
|
|
|
+ </ul>
|
|
|
+
|
|
|
+ <h2>4. cURL示例</h2>
|
|
|
+ <h3>4.1 租户转账接口</h3>
|
|
|
+ <pre><code># 基础认证(不带签名)
|
|
|
+curl -X POST 'https://api.example.com/payment/openapi/account/transfer' \
|
|
|
+ -H 'Authorization: ApiKey your_api_key' \
|
|
|
+ -H 'Content-Type: application/json' \
|
|
|
+ -d '{
|
|
|
+ "account_book_id": "资金账号",
|
|
|
+ "amount": 100.00,
|
|
|
+ "order_title": "转账标题",
|
|
|
+ "payee_info": {
|
|
|
+ "identity_type": "ALIPAY_ACCOUNT",
|
|
|
+ "name": "收款人姓名",
|
|
|
+ "identity": "收款人支付宝账号"
|
|
|
+ }
|
|
|
+ }'
|
|
|
+
|
|
|
+# 带签名认证
|
|
|
+curl -X POST 'https://api.example.com/payment/openapi/account/transfer' \
|
|
|
+ -H 'Authorization: ApiKey your_api_key:your_signature' \
|
|
|
+ -H 'Content-Type: application/json' \
|
|
|
+ -d '{
|
|
|
+ "account_book_id": "资金账号",
|
|
|
+ "amount": 100.00,
|
|
|
+ "order_title": "转账标题",
|
|
|
+ "payee_info": {
|
|
|
+ "identity_type": "ALIPAY_ACCOUNT",
|
|
|
+ "name": "收款人姓名",
|
|
|
+ "identity": "收款人支付宝账号"
|
|
|
+ }
|
|
|
+ }'</code></pre>
|
|
|
+
|
|
|
+ <h3>4.2 签名计算方式</h3>
|
|
|
+ <p>签名是对请求体对象按参数名升序排序后拼接成字符串,再进行HMAC-SHA256计算:</p>
|
|
|
+ <pre><code># Python签名示例
|
|
|
+import hashlib
|
|
|
+import hmac
|
|
|
+
|
|
|
+def calculate_signature(api_secret, request_data):
|
|
|
+ \"\"\"
|
|
|
+ 对请求体字典进行签名
|
|
|
+ request_data: dict 请求体数据
|
|
|
+ \"\"\"
|
|
|
+ # 按参数名升序排序
|
|
|
+ sorted_data = sorted(request_data.items(), key=lambda x: x[0])
|
|
|
+ # 拼接为 key1=value1&key2=value2 格式
|
|
|
+ sign_str = "&".join([f"{k}={v}" for k, v in sorted_data])
|
|
|
+ # HMAC-SHA256签名
|
|
|
+ signature = hmac.new(
|
|
|
+ api_secret.encode('utf-8'),
|
|
|
+ sign_str.encode('utf-8'),
|
|
|
+ hashlib.sha256
|
|
|
+ ).hexdigest()
|
|
|
+ return signature
|
|
|
+
|
|
|
+# 示例
|
|
|
+api_secret = "your_api_secret"
|
|
|
+request_data = {
|
|
|
+ "account_book_id": "资金账号",
|
|
|
+ "amount": 100.00
|
|
|
+}
|
|
|
+signature = calculate_signature(api_secret, request_data)
|
|
|
+# Authorization: ApiKey your_api_key:signature</code></pre>
|
|
|
+
|
|
|
+ <h3>4.3 注意事项</h3>
|
|
|
+ <ul>
|
|
|
+ <li>签名使用HMAC-SHA256算法,密钥为API Secret</li>
|
|
|
+ <li>签名对象是请求体字典排序后的键值对字符串(key1=value1&key2=value2)</li>
|
|
|
+ <li>注意是字典排序后拼接,不是JSON字符串</li>
|
|
|
+ </ul>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+ </el-tab-pane>
|
|
|
+ </el-tabs>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, reactive } from "vue";
|
|
|
+import ApiKeyAPI, {
|
|
|
+ ApiKeyCreateForm,
|
|
|
+ ApiKeyUpdateForm,
|
|
|
+ ApiKeyPageQuery,
|
|
|
+ ApiKeyResponse,
|
|
|
+ ApiKeyTable,
|
|
|
+} from "@/api/module_payment/apikey";
|
|
|
+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";
|
|
|
+
|
|
|
+defineOptions({
|
|
|
+ name: "ApiKey",
|
|
|
+ inheritAttrs: false,
|
|
|
+});
|
|
|
+
|
|
|
+const activeTab = ref("management");
|
|
|
+const { searchRef, contentRef, handleQueryClick, handleResetClick, refreshList } = useCrudList();
|
|
|
+const dataFormRef = ref();
|
|
|
+const submitLoading = ref(false);
|
|
|
+const apiKeyDetailVisible = ref(false);
|
|
|
+
|
|
|
+const apiKeyDetail = reactive<ApiKeyResponse>({
|
|
|
+ id: 0,
|
|
|
+ api_key: "",
|
|
|
+ api_secret: "",
|
|
|
+ status: "0",
|
|
|
+ expired_at: "",
|
|
|
+ created_time: "",
|
|
|
+});
|
|
|
+
|
|
|
+const searchConfig = reactive<ISearchConfig>({
|
|
|
+ permPrefix: "module_system:tenant:api-key",
|
|
|
+ colon: true,
|
|
|
+ isExpandable: true,
|
|
|
+ showNumber: 2,
|
|
|
+ form: { labelWidth: "auto" },
|
|
|
+ formItems: [
|
|
|
+ // {
|
|
|
+ // prop: "tenant_id",
|
|
|
+ // label: "租户ID",
|
|
|
+ // type: "input",
|
|
|
+ // attrs: { placeholder: "请输入租户ID", clearable: true },
|
|
|
+ // },
|
|
|
+ {
|
|
|
+ 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: "index", label: "序号", show: true },
|
|
|
+ { prop: "api_key", label: "API Key", show: true },
|
|
|
+ { prop: "status", 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: "",
|
|
|
+});
|
|
|
+
|
|
|
+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: "",
|
|
|
+};
|
|
|
+
|
|
|
+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.id || 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;
|
|
|
+}
|
|
|
+
|
|
|
+.docs-container {
|
|
|
+ padding: 20px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.card-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+}
|
|
|
+
|
|
|
+.docs-content {
|
|
|
+ line-height: 1.6;
|
|
|
+}
|
|
|
+
|
|
|
+.docs-content h2 {
|
|
|
+ margin-top: 30px;
|
|
|
+ margin-bottom: 15px;
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+.docs-content h3 {
|
|
|
+ margin-top: 20px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+.docs-content p {
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.docs-content ul,
|
|
|
+.docs-content ol {
|
|
|
+ margin-left: 20px;
|
|
|
+ margin-bottom: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.docs-content li {
|
|
|
+ margin-bottom: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.docs-content pre {
|
|
|
+ background-color: #f5f5f5;
|
|
|
+ padding: 15px;
|
|
|
+ border-radius: 4px;
|
|
|
+ overflow-x: auto;
|
|
|
+ margin-bottom: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.docs-content code {
|
|
|
+ font-family: 'Courier New', Courier, monospace;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+</style>
|