mirror of
https://gitee.com/ccnetcore/Yi
synced 2026-03-02 15:50:54 +08:00
fix: 批量创建apikey
This commit is contained in:
@@ -46,6 +46,9 @@ const currentFormData = ref<TokenFormData>({
|
|||||||
});
|
});
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
// TokenFormDialog 组件引用
|
||||||
|
const tokenFormDialogRef = ref<InstanceType<typeof TokenFormDialog> | null>(null);
|
||||||
|
|
||||||
// 移动端检测
|
// 移动端检测
|
||||||
const isMobile = ref(false);
|
const isMobile = ref(false);
|
||||||
|
|
||||||
@@ -165,6 +168,8 @@ async function handleFormSubmit(data: TokenFormData) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
|
// 单个创建或编辑
|
||||||
const submitData = {
|
const submitData = {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -193,6 +198,37 @@ async function handleFormSubmit(data: TokenFormData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理批量创建
|
||||||
|
async function handleBatchCreate(
|
||||||
|
items: TokenFormData[],
|
||||||
|
onProgress: (index: number, status: 'pending' | 'creating' | 'success' | 'failed', error?: string) => void
|
||||||
|
) {
|
||||||
|
// 并发创建所有 token
|
||||||
|
const promises = items.map(async (item, index) => {
|
||||||
|
try {
|
||||||
|
onProgress(index, 'creating');
|
||||||
|
await createToken({
|
||||||
|
name: item.name,
|
||||||
|
expireTime: item.expireTime || null,
|
||||||
|
premiumQuotaLimit: item.premiumQuotaLimit || null,
|
||||||
|
});
|
||||||
|
onProgress(index, 'success');
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`创建 "${item.name}" 失败:`, error);
|
||||||
|
onProgress(index, 'failed', error?.message || '创建失败');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.allSettled(promises);
|
||||||
|
|
||||||
|
// 创建完成后刷新列表
|
||||||
|
await fetchTokenList();
|
||||||
|
|
||||||
|
// 调用子组件的完成方法
|
||||||
|
tokenFormDialogRef.value?.completeBatchCreate();
|
||||||
|
}
|
||||||
|
|
||||||
// 删除Token(带防抖)
|
// 删除Token(带防抖)
|
||||||
async function handleDelete(row: TokenItem) {
|
async function handleDelete(row: TokenItem) {
|
||||||
if (operatingTokenId.value === row.id)
|
if (operatingTokenId.value === row.id)
|
||||||
@@ -752,10 +788,12 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<!-- Token表单对话框 -->
|
<!-- Token表单对话框 -->
|
||||||
<TokenFormDialog
|
<TokenFormDialog
|
||||||
|
ref="tokenFormDialogRef"
|
||||||
v-model:visible="showFormDialog"
|
v-model:visible="showFormDialog"
|
||||||
:mode="formMode"
|
:mode="formMode"
|
||||||
:form-data="currentFormData"
|
:form-data="currentFormData"
|
||||||
@confirm="handleFormSubmit"
|
@confirm="handleFormSubmit"
|
||||||
|
@batch-create="handleBatchCreate"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,18 @@
|
|||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
// 最大批量创建数量
|
||||||
|
const MAX_BATCH_CREATE_COUNT = 10;
|
||||||
|
|
||||||
|
// 创建状态类型
|
||||||
|
type CreateStatus = 'pending' | 'creating' | 'success' | 'failed';
|
||||||
|
|
||||||
|
interface CreateItemStatus {
|
||||||
|
name: string;
|
||||||
|
status: CreateStatus;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface TokenFormData {
|
interface TokenFormData {
|
||||||
id?: string;
|
id?: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -30,7 +42,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:visible': [value: boolean];
|
'update:visible': [value: boolean];
|
||||||
'confirm': [data: TokenFormData];
|
'confirm': [data: TokenFormData | TokenFormData[]];
|
||||||
|
'batchCreate': [items: TokenFormData[], onProgress: (index: number, status: CreateStatus, error?: string) => void];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const localFormData = ref<TokenFormData>({
|
const localFormData = ref<TokenFormData>({
|
||||||
@@ -45,6 +58,84 @@ const neverExpire = ref(false); // 永不过期开关
|
|||||||
const unlimitedQuota = ref(false); // 无限制额度开关
|
const unlimitedQuota = ref(false); // 无限制额度开关
|
||||||
const isEnableLog = ref(false); // 是否启用请求日志(只读)
|
const isEnableLog = ref(false); // 是否启用请求日志(只读)
|
||||||
|
|
||||||
|
// 批量创建相关
|
||||||
|
const batchCount = ref(1); // 批量创建数量,默认1
|
||||||
|
const batchNamesText = ref(''); // 批量创建的名称文本(换行分隔)
|
||||||
|
const showPreviewDialog = ref(false); // 是否显示预览对话框
|
||||||
|
const previewNames = ref<string[]>([]); // 预览的名称列表
|
||||||
|
const createStatusList = ref<CreateItemStatus[]>([]); // 创建状态列表
|
||||||
|
const isCreating = ref(false); // 是否正在创建
|
||||||
|
|
||||||
|
// 解析批量名称文本,返回有效的名称数组
|
||||||
|
function parseBatchNames(): string[] {
|
||||||
|
return batchNamesText.value
|
||||||
|
.split('\n')
|
||||||
|
.map(line => line.trim())
|
||||||
|
.filter(name => name.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成默认名称列表
|
||||||
|
function generateDefaultNames(count: number): string[] {
|
||||||
|
const baseName = localFormData.value.name.trim() || 'API密钥';
|
||||||
|
const names: string[] = [];
|
||||||
|
for (let i = 1; i <= count; i++) {
|
||||||
|
names.push(`${baseName}${i}`);
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建最终的名称列表(以用户选择的数量为准)
|
||||||
|
function buildFinalNames(): string[] {
|
||||||
|
const targetCount = batchCount.value;
|
||||||
|
const parsedNames = parseBatchNames();
|
||||||
|
const result: string[] = [];
|
||||||
|
|
||||||
|
// 先使用用户粘贴的名称
|
||||||
|
for (let i = 0; i < Math.min(parsedNames.length, targetCount); i++) {
|
||||||
|
result.push(parsedNames[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不够,用默认规则补齐
|
||||||
|
if (result.length < targetCount) {
|
||||||
|
const baseName = localFormData.value.name.trim() || 'API密钥';
|
||||||
|
for (let i = result.length + 1; i <= targetCount; i++) {
|
||||||
|
result.push(`${baseName}${i}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 batchCount 变化,自动生成名称
|
||||||
|
watch(batchCount, (count) => {
|
||||||
|
if (count > 1 && props.mode === 'create') {
|
||||||
|
// 如果当前文本框的名称数量小于新数量,自动补齐
|
||||||
|
const parsedNames = parseBatchNames();
|
||||||
|
if (parsedNames.length < count) {
|
||||||
|
const baseName = localFormData.value.name.trim() || 'API密钥';
|
||||||
|
const names: string[] = [...parsedNames];
|
||||||
|
for (let i = names.length + 1; i <= count; i++) {
|
||||||
|
names.push(`${baseName}${i}`);
|
||||||
|
}
|
||||||
|
batchNamesText.value = names.join('\n');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
batchNamesText.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听 baseName 变化,当批量创建时自动更新名称
|
||||||
|
watch(() => localFormData.value.name, (newName) => {
|
||||||
|
if (batchCount.value > 1 && props.mode === 'create') {
|
||||||
|
const baseName = newName.trim() || 'API密钥';
|
||||||
|
const names: string[] = [];
|
||||||
|
for (let i = 1; i <= batchCount.value; i++) {
|
||||||
|
names.push(`${baseName}${i}`);
|
||||||
|
}
|
||||||
|
batchNamesText.value = names.join('\n');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 移动端检测
|
// 移动端检测
|
||||||
const isMobile = ref(false);
|
const isMobile = ref(false);
|
||||||
|
|
||||||
@@ -117,6 +208,8 @@ watch(() => props.visible, (newVal) => {
|
|||||||
premiumQuotaLimit: displayValue,
|
premiumQuotaLimit: displayValue,
|
||||||
quotaUnit: unit,
|
quotaUnit: unit,
|
||||||
};
|
};
|
||||||
|
// 编辑模式禁用批量创建
|
||||||
|
batchCount.value = 1;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// 新增模式:重置表单
|
// 新增模式:重置表单
|
||||||
@@ -128,6 +221,7 @@ watch(() => props.visible, (newVal) => {
|
|||||||
};
|
};
|
||||||
neverExpire.value = false;
|
neverExpire.value = false;
|
||||||
unlimitedQuota.value = false;
|
unlimitedQuota.value = false;
|
||||||
|
batchCount.value = 1;
|
||||||
}
|
}
|
||||||
submitting.value = false;
|
submitting.value = false;
|
||||||
}
|
}
|
||||||
@@ -156,6 +250,40 @@ function handleClose() {
|
|||||||
|
|
||||||
// 确认提交
|
// 确认提交
|
||||||
async function handleConfirm() {
|
async function handleConfirm() {
|
||||||
|
// 批量创建模式
|
||||||
|
if (props.mode === 'create' && batchCount.value > 1) {
|
||||||
|
if (!neverExpire.value && !localFormData.value.expireTime) {
|
||||||
|
ElMessage.warning('请选择过期时间');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!unlimitedQuota.value && localFormData.value.premiumQuotaLimit <= 0) {
|
||||||
|
ElMessage.warning('请输入有效的配额限制');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建最终的名称列表
|
||||||
|
const finalNames = buildFinalNames();
|
||||||
|
|
||||||
|
// 检查是否有重复名称
|
||||||
|
const uniqueNames = new Set(finalNames);
|
||||||
|
if (uniqueNames.size !== finalNames.length) {
|
||||||
|
ElMessage.warning('存在重复的API密钥名称,请检查');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示预览对话框
|
||||||
|
previewNames.value = finalNames;
|
||||||
|
// 初始化状态列表为 pending,显示在预览对话框中
|
||||||
|
createStatusList.value = finalNames.map(name => ({
|
||||||
|
name,
|
||||||
|
status: 'pending' as CreateStatus,
|
||||||
|
}));
|
||||||
|
showPreviewDialog.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单个创建或编辑模式
|
||||||
if (!localFormData.value.name.trim()) {
|
if (!localFormData.value.name.trim()) {
|
||||||
ElMessage.warning('请输入API密钥名称');
|
ElMessage.warning('请输入API密钥名称');
|
||||||
return;
|
return;
|
||||||
@@ -194,7 +322,115 @@ async function handleConfirm() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 预览对话框确认提交
|
||||||
|
async function handlePreviewConfirm() {
|
||||||
|
if (!neverExpire.value && !localFormData.value.expireTime) {
|
||||||
|
ElMessage.warning('请选择过期时间');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!unlimitedQuota.value && localFormData.value.premiumQuotaLimit <= 0) {
|
||||||
|
ElMessage.warning('请输入有效的配额限制');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先初始化创建状态为 pending
|
||||||
|
createStatusList.value = previewNames.value.map(name => ({
|
||||||
|
name,
|
||||||
|
status: 'pending' as CreateStatus,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 然后设置为创建中状态,触发 UI 更新显示状态列
|
||||||
|
isCreating.value = true;
|
||||||
|
|
||||||
|
// 将展示值转换为实际值
|
||||||
|
let actualQuota = null;
|
||||||
|
if (!unlimitedQuota.value) {
|
||||||
|
const unit = quotaUnitOptions.find(u => u.value === localFormData.value.quotaUnit);
|
||||||
|
actualQuota = localFormData.value.premiumQuotaLimit * (unit?.multiplier || 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成多个提交数据
|
||||||
|
const submitDataList: TokenFormData[] = previewNames.value.map(name => ({
|
||||||
|
...localFormData.value,
|
||||||
|
name,
|
||||||
|
expireTime: neverExpire.value ? '' : localFormData.value.expireTime,
|
||||||
|
premiumQuotaLimit: actualQuota,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 进度回调函数
|
||||||
|
const onProgress = (index: number, status: CreateStatus, error?: string) => {
|
||||||
|
if (createStatusList.value[index]) {
|
||||||
|
createStatusList.value[index].status = status;
|
||||||
|
if (error) {
|
||||||
|
createStatusList.value[index].error = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
emit('batchCreate', submitDataList, onProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预览对话框取消
|
||||||
|
function handlePreviewCancel() {
|
||||||
|
if (isCreating.value) return;
|
||||||
|
showPreviewDialog.value = false;
|
||||||
|
previewNames.value = [];
|
||||||
|
createStatusList.value = [];
|
||||||
|
// 同时关闭主表单对话框
|
||||||
|
emit('update:visible', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完成批量创建(由父组件调用)
|
||||||
|
function completeBatchCreate() {
|
||||||
|
isCreating.value = false;
|
||||||
|
showPreviewDialog.value = false;
|
||||||
|
previewNames.value = [];
|
||||||
|
createStatusList.value = [];
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露给父组件的方法
|
||||||
|
defineExpose({
|
||||||
|
completeBatchCreate,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取创建进度统计
|
||||||
|
const createProgress = computed(() => {
|
||||||
|
const total = createStatusList.value.length;
|
||||||
|
const success = createStatusList.value.filter(s => s.status === 'success').length;
|
||||||
|
const failed = createStatusList.value.filter(s => s.status === 'failed').length;
|
||||||
|
const pending = createStatusList.value.filter(s => s.status === 'pending' || s.status === 'creating').length;
|
||||||
|
return { total, success, failed, pending };
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取预览对话框标题
|
||||||
|
const previewDialogTitle = computed(() => {
|
||||||
|
if (isCreating.value) {
|
||||||
|
const { success, failed, pending } = createProgress.value;
|
||||||
|
if (pending > 0) {
|
||||||
|
return `创建中... (${success}/${createProgress.value.total})`;
|
||||||
|
}
|
||||||
|
return `创建完成`;
|
||||||
|
}
|
||||||
|
return '批量创建预览';
|
||||||
|
});
|
||||||
|
|
||||||
const dialogTitle = computed(() => props.mode === 'create' ? '新增 API密钥' : '编辑 API密钥');
|
const dialogTitle = computed(() => props.mode === 'create' ? '新增 API密钥' : '编辑 API密钥');
|
||||||
|
|
||||||
|
// 格式化日期时间
|
||||||
|
function formatDateTime(dateStr: string | null | undefined) {
|
||||||
|
if (!dateStr)
|
||||||
|
return '-';
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -223,7 +459,42 @@ const dialogTitle = computed(() => props.mode === 'create' ? '新增 API密钥'
|
|||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="过期时间">
|
<!-- 批量创建功能(仅新增模式) -->
|
||||||
|
<el-form-item v-if="mode === 'create'" label="批量创建">
|
||||||
|
<el-input-number
|
||||||
|
v-model="batchCount"
|
||||||
|
:min="1"
|
||||||
|
:max="MAX_BATCH_CREATE_COUNT"
|
||||||
|
:precision="0"
|
||||||
|
controls-position="right"
|
||||||
|
placeholder="数量"
|
||||||
|
:disabled="submitting"
|
||||||
|
/>
|
||||||
|
<div class="form-hint">
|
||||||
|
<el-icon><i-ep-info-filled /></el-icon>
|
||||||
|
选择创建数量,最多支持 {{ MAX_BATCH_CREATE_COUNT }} 个
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 批量名称输入(当数量 > 1 时显示) -->
|
||||||
|
<el-form-item v-if="mode === 'create' && batchCount > 1" label="批量名称列表" required>
|
||||||
|
<el-input
|
||||||
|
v-model="batchNamesText"
|
||||||
|
type="textarea"
|
||||||
|
:rows="Math.min(batchCount + 1, 10)"
|
||||||
|
placeholder="每行一个API密钥名称,以换行分隔"
|
||||||
|
maxlength="1000"
|
||||||
|
show-word-limit
|
||||||
|
clearable
|
||||||
|
:disabled="submitting"
|
||||||
|
/>
|
||||||
|
<div class="form-hint">
|
||||||
|
<el-icon><i-ep-info-filled /></el-icon>
|
||||||
|
<span>按换行符识别名称,自动去除空行。以"创建数量"为准,不足时自动补齐,多余时取前 {{ batchCount }} 个</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="过期时间" :required="!neverExpire">
|
||||||
<div class="form-item-with-switch">
|
<div class="form-item-with-switch">
|
||||||
<el-switch
|
<el-switch
|
||||||
v-model="neverExpire"
|
v-model="neverExpire"
|
||||||
@@ -326,6 +597,136 @@ const dialogTitle = computed(() => props.mode === 'create' ? '新增 API密钥'
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 批量创建预览对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="showPreviewDialog"
|
||||||
|
:title="previewDialogTitle"
|
||||||
|
:width="isMobile ? '95%' : '600px'"
|
||||||
|
:fullscreen="isMobile"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:show-close="!isCreating"
|
||||||
|
@close="handlePreviewCancel"
|
||||||
|
>
|
||||||
|
<div class="preview-container">
|
||||||
|
<!-- 创建进度提示 -->
|
||||||
|
<div v-if="isCreating" class="preview-progress-header">
|
||||||
|
<div class="progress-summary">
|
||||||
|
<el-icon v-if="createProgress.pending > 0" class="is-loading"><i-ep-loading /></el-icon>
|
||||||
|
<el-icon v-else-if="createProgress.failed > 0"><i-ep-circle-close /></el-icon>
|
||||||
|
<el-icon v-else><i-ep-circle-check /></el-icon>
|
||||||
|
<span>
|
||||||
|
<template v-if="createProgress.pending > 0">正在创建中...</template>
|
||||||
|
<template v-else-if="createProgress.failed > 0">部分创建失败</template>
|
||||||
|
<template v-else>全部创建成功</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<el-progress
|
||||||
|
:percentage="createProgress.total > 0 ? Math.round((createProgress.success + createProgress.failed) / createProgress.total * 100) : 0"
|
||||||
|
:status="createProgress.pending > 0 ? undefined : (createProgress.failed > 0 ? 'exception' : 'success')"
|
||||||
|
:stroke-width="8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 创建中时的简化显示 -->
|
||||||
|
<div v-if="isCreating" class="preview-names-list preview-names-list-compact">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in createStatusList"
|
||||||
|
:key="index"
|
||||||
|
class="preview-name-item preview-name-item-compact"
|
||||||
|
:class="`status-${item.status}`"
|
||||||
|
>
|
||||||
|
<span class="preview-name-index">{{ index + 1 }}</span>
|
||||||
|
<span class="preview-name-text">{{ item.name }}</span>
|
||||||
|
<span class="preview-name-status">
|
||||||
|
<el-icon v-if="item.status === 'creating'" class="is-loading"><i-ep-loading /></el-icon>
|
||||||
|
<el-icon v-else-if="item.status === 'success'"><i-ep-circle-check /></el-icon>
|
||||||
|
<el-icon v-else-if="item.status === 'failed'"><i-ep-circle-close /></el-icon>
|
||||||
|
<el-icon v-else><i-ep-clock /></el-icon>
|
||||||
|
<span class="preview-name-status-text">
|
||||||
|
<template v-if="item.status === 'pending'">待创建</template>
|
||||||
|
<template v-else-if="item.status === 'creating'">创建中</template>
|
||||||
|
<template v-else-if="item.status === 'success'">成功</template>
|
||||||
|
<template v-else-if="item.status === 'failed'">失败</template>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 预览确认时的完整显示 -->
|
||||||
|
<template v-else>
|
||||||
|
<div class="preview-header">
|
||||||
|
<el-icon><i-ep-warning /></el-icon>
|
||||||
|
<span>即将创建 {{ previewNames.length }} 个 API 密钥,请确认以下信息:</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 配额信息 -->
|
||||||
|
<div class="preview-section">
|
||||||
|
<div class="preview-section-title">配额设置</div>
|
||||||
|
<div class="preview-section-content">
|
||||||
|
<span v-if="unlimitedQuota" class="preview-tag preview-tag-success">无限制</span>
|
||||||
|
<span v-else class="preview-tag preview-tag-info">
|
||||||
|
{{ localFormData.premiumQuotaLimit }} {{ localFormData.quotaUnit }}
|
||||||
|
</span>
|
||||||
|
<span v-if="neverExpire" class="preview-tag preview-tag-success">永不过期</span>
|
||||||
|
<span v-else class="preview-tag preview-tag-warning">
|
||||||
|
过期时间:{{ formatDateTime(localFormData.expireTime) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 名称列表 -->
|
||||||
|
<div class="preview-section">
|
||||||
|
<div class="preview-section-title">API 密钥名称列表</div>
|
||||||
|
<div class="preview-names-list">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in createStatusList"
|
||||||
|
:key="index"
|
||||||
|
class="preview-name-item"
|
||||||
|
:class="`status-${item.status}`"
|
||||||
|
>
|
||||||
|
<span class="preview-name-index">{{ index + 1 }}</span>
|
||||||
|
<span class="preview-name-text">{{ item.name }}</span>
|
||||||
|
<span class="preview-name-status">
|
||||||
|
<el-icon v-if="item.status === 'creating'" class="is-loading"><i-ep-loading /></el-icon>
|
||||||
|
<el-icon v-else-if="item.status === 'success'"><i-ep-circle-check /></el-icon>
|
||||||
|
<el-icon v-else-if="item.status === 'failed'"><i-ep-circle-close /></el-icon>
|
||||||
|
<el-icon v-else><i-ep-clock /></el-icon>
|
||||||
|
<span class="preview-name-status-text">
|
||||||
|
<template v-if="item.status === 'pending'">待创建</template>
|
||||||
|
<template v-else-if="item.status === 'creating'">创建中</template>
|
||||||
|
<template v-else-if="item.status === 'success'">成功</template>
|
||||||
|
<template v-else-if="item.status === 'failed'">失败</template>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button :disabled="isCreating" @click="handlePreviewCancel">
|
||||||
|
取消
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="!isCreating"
|
||||||
|
type="primary"
|
||||||
|
@click="handlePreviewConfirm"
|
||||||
|
>
|
||||||
|
确认创建
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-else
|
||||||
|
type="success"
|
||||||
|
@click="handlePreviewCancel"
|
||||||
|
>
|
||||||
|
完成
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -413,4 +814,254 @@ const dialogTitle = computed(() => props.mode === 'create' ? '新增 API密钥'
|
|||||||
:deep(.el-input__prefix) {
|
:deep(.el-input__prefix) {
|
||||||
color: #909399;
|
color: #909399;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 预览对话框样式
|
||||||
|
.preview-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff7e6;
|
||||||
|
border: 1px solid #ffd591;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fa8c16;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-section {
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-section-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #262626;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-section-content {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.preview-tag-success {
|
||||||
|
background: #f6ffed;
|
||||||
|
color: #52c41a;
|
||||||
|
border: 1px solid #b7eb8f;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.preview-tag-info {
|
||||||
|
background: #e6f7ff;
|
||||||
|
color: #1890ff;
|
||||||
|
border: 1px solid #91d5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.preview-tag-warning {
|
||||||
|
background: #fff7e6;
|
||||||
|
color: #fa8c16;
|
||||||
|
border: 1px solid #ffd591;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-names-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-name-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #1890ff;
|
||||||
|
background: #f0f7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态样式
|
||||||
|
&.status-pending {
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
background: #fafafa;
|
||||||
|
|
||||||
|
.preview-name-status .el-icon {
|
||||||
|
color: #8c8c8c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-creating {
|
||||||
|
border-color: #1890ff;
|
||||||
|
background: #e6f7ff;
|
||||||
|
|
||||||
|
.preview-name-status .el-icon {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-success {
|
||||||
|
border-color: #52c41a;
|
||||||
|
background: #f6ffed;
|
||||||
|
|
||||||
|
.preview-name-status .el-icon {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-failed {
|
||||||
|
border-color: #ff4d4f;
|
||||||
|
background: #fff1f0;
|
||||||
|
|
||||||
|
.preview-name-status .el-icon {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-name-index {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background: #1890ff;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-name-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #262626;
|
||||||
|
font-weight: 500;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进度相关样式
|
||||||
|
.preview-progress-header {
|
||||||
|
padding: 16px;
|
||||||
|
background: #f6ffed;
|
||||||
|
border: 1px solid #b7eb8f;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #52c41a;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-names-list-compact {
|
||||||
|
max-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-name-item-compact {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
|
||||||
|
&.status-creating {
|
||||||
|
border-color: #1890ff;
|
||||||
|
background: #e6f7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-success {
|
||||||
|
border-color: #52c41a;
|
||||||
|
background: #f6ffed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-failed {
|
||||||
|
border-color: #ff4d4f;
|
||||||
|
background: #fff1f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-name-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
color: #8c8c8c;
|
||||||
|
|
||||||
|
&:is(.is-loading) {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-creating & .el-icon {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-success & .el-icon {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-failed & .el-icon {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-name-status-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
.status-pending & {
|
||||||
|
color: #8c8c8c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-creating & {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-success & {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-failed & {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user