feat: 新增多token功能

This commit is contained in:
Gsh
2025-11-29 16:43:08 +08:00
parent 2d0ca08314
commit ddb00879f4
5 changed files with 1292 additions and 509 deletions

View File

@@ -1,5 +1,5 @@
import type { GetSessionListVO } from './types';
import { get, post } from '@/utils/request';
import { del, get, post, put } from '@/utils/request';
// 获取当前用户的模型列表
export function getModelList() {
@@ -28,3 +28,77 @@ export function getLast7DaysTokenUsage() {
export function getModelTokenUsage() {
return get<any>('/usage-statistics/model-token-usage').json();
}
// 以下为新增接口
// 获取当前用户得token列表
export function getTokenList() {
return get<any>('/token/list').json();
}
/*
返回数据:
[
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"apiKey": "string",
"expireTime": "2025-11-29T07:34:23.850Z",
"premiumQuotaLimit": 0,
"premiumUsedQuota": 0,
"isDisabled": true,
"creationTime": "2025-11-29T07:34:23.850Z"
}
] */
// 创建token
export function createToken(data: any) {
return post<any>('/token', data).json();
}
/*
data:
{
"name": "string",
"expireTime": "2025-11-29T07:35:10.458Z",
"premiumQuotaLimit": 0
} */
/*
返回:
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"apiKey": "string",
"expireTime": "2025-11-29T07:35:10.459Z",
"premiumQuotaLimit": 0,
"premiumUsedQuota": 0,
"isDisabled": true,
"creationTime": "2025-11-29T07:35:10.459Z"
} */
// 编辑token
export function editToken(data: any) {
return put('/token', data).json();
}
/*
data:
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"expireTime": "2025-11-29T07:36:49.589Z",
"premiumQuotaLimit": 0
}
*/
// 删除token
export function deleteToken(id: string) {
return del(`/token/${id}`).json();
}
// 启用token
export function enableToken(id: string) {
return post(`/token/${id}/enable`).json();
}
// 禁用token
export function disableToken(id: string) {
return post(`/token/${id}/disable`).json();
}

View File

@@ -0,0 +1,352 @@
<script lang="ts" setup>
import { ref, watch, computed } from 'vue';
import { ElMessage } from 'element-plus';
interface TokenFormData {
id?: string;
name: string;
expireTime: string;
premiumQuotaLimit: number;
quotaUnit: string;
}
interface Props {
visible: boolean;
mode: 'create' | 'edit';
formData?: TokenFormData;
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
mode: 'create',
formData: () => ({
name: '',
expireTime: '',
premiumQuotaLimit: 0,
quotaUnit: '万'
})
});
const emit = defineEmits<{
'update:visible': [value: boolean];
'confirm': [data: TokenFormData];
}>();
const localFormData = ref<TokenFormData>({
name: '',
expireTime: '',
premiumQuotaLimit: 0,
quotaUnit: '万'
});
const submitting = ref(false);
const neverExpire = ref(false); // 永不过期开关
const unlimitedQuota = ref(false); // 无限制额度开关
const quotaUnitOptions = [
{ label: '个', value: '个', multiplier: 1 },
{ label: '十', value: '十', multiplier: 10 },
{ label: '百', value: '百', multiplier: 100 },
{ label: '千', value: '千', multiplier: 1000 },
{ label: '万', value: '万', multiplier: 10000 },
{ label: '亿', value: '亿', multiplier: 100000000 }
];
// 监听visible变化重置表单
watch(() => props.visible, (newVal) => {
if (newVal) {
if (props.mode === 'edit' && props.formData) {
// 编辑模式:转换后端数据为展示数据
const quota = props.formData.premiumQuotaLimit || 0;
let displayValue = quota;
let unit = '个';
// 判断是否无限制
unlimitedQuota.value = quota === 0;
if (!unlimitedQuota.value) {
// 自动选择合适的单位
if (quota >= 100000000 && quota % 100000000 === 0) {
displayValue = quota / 100000000;
unit = '亿';
} else if (quota >= 10000 && quota % 10000 === 0) {
displayValue = quota / 10000;
unit = '万';
} else if (quota >= 1000 && quota % 1000 === 0) {
displayValue = quota / 1000;
unit = '千';
} else if (quota >= 100 && quota % 100 === 0) {
displayValue = quota / 100;
unit = '百';
} else if (quota >= 10 && quota % 10 === 0) {
displayValue = quota / 10;
unit = '十';
}
}
// 判断是否永不过期
neverExpire.value = !props.formData.expireTime;
localFormData.value = {
...props.formData,
premiumQuotaLimit: displayValue,
quotaUnit: unit
};
} else {
// 新增模式:重置表单
localFormData.value = {
name: '',
expireTime: '',
premiumQuotaLimit: 1,
quotaUnit: '万'
};
neverExpire.value = false;
unlimitedQuota.value = false;
}
submitting.value = false;
}
});
// 监听永不过期开关
watch(neverExpire, (newVal) => {
if (newVal) {
localFormData.value.expireTime = '';
}
});
// 监听无限制开关
watch(unlimitedQuota, (newVal) => {
if (newVal) {
localFormData.value.premiumQuotaLimit = 0;
}
});
// 关闭对话框
function handleClose() {
if (submitting.value) return;
emit('update:visible', false);
}
// 确认提交
async function handleConfirm() {
if (!localFormData.value.name.trim()) {
ElMessage.warning('请输入Token名称');
return;
}
if (!neverExpire.value && !localFormData.value.expireTime) {
ElMessage.warning('请选择过期时间');
return;
}
if (!unlimitedQuota.value && localFormData.value.premiumQuotaLimit <= 0) {
ElMessage.warning('请输入有效的配额限制');
return;
}
submitting.value = true;
try {
// 将展示值转换为实际值
let actualQuota = 0;
if (!unlimitedQuota.value) {
const unit = quotaUnitOptions.find(u => u.value === localFormData.value.quotaUnit);
actualQuota = localFormData.value.premiumQuotaLimit * (unit?.multiplier || 1);
}
const submitData: TokenFormData = {
...localFormData.value,
expireTime: neverExpire.value ? '' : localFormData.value.expireTime,
premiumQuotaLimit: actualQuota
};
emit('confirm', submitData);
} finally {
// 注意:这里不设置 submitting.value = false
// 因为父组件会关闭对话框watch会重置状态
}
}
const dialogTitle = computed(() => props.mode === 'create' ? '新增 Token' : '编辑 Token');
</script>
<template>
<el-dialog
:model-value="visible"
:title="dialogTitle"
width="540px"
:close-on-click-modal="false"
:show-close="!submitting"
@close="handleClose"
>
<el-form :model="localFormData" label-width="110px" label-position="right">
<el-form-item label="Token名称" required>
<el-input
v-model="localFormData.name"
placeholder="例如:生产环境、测试环境、开发环境"
maxlength="50"
show-word-limit
clearable
:disabled="submitting"
>
<template #prefix>
<el-icon><i-ep-collection-tag /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="过期时间">
<div class="form-item-with-switch">
<el-switch
v-model="neverExpire"
active-text="永不过期"
:disabled="submitting"
class="expire-switch"
/>
<el-date-picker
v-if="!neverExpire"
v-model="localFormData.expireTime"
type="datetime"
placeholder="选择过期时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DDTHH:mm:ss"
style="width: 100%"
clearable
:disabled="submitting"
:disabled-date="(time: Date) => time.getTime() < Date.now()"
>
<template #prefix>
<el-icon><i-ep-clock /></el-icon>
</template>
</el-date-picker>
</div>
<div v-if="!neverExpire" class="form-hint">
<el-icon><i-ep-warning /></el-icon>
Token将在过期时间后自动失效
</div>
</el-form-item>
<el-form-item label="配额限制">
<div class="form-item-with-switch">
<el-switch
v-model="unlimitedQuota"
active-text="无限制"
:disabled="submitting"
class="quota-switch"
/>
<div v-if="!unlimitedQuota" class="quota-input-group">
<el-input-number
v-model="localFormData.premiumQuotaLimit"
:min="1"
:precision="0"
:controls="true"
controls-position="right"
placeholder="请输入配额"
class="quota-number"
:disabled="submitting"
/>
<el-select
v-model="localFormData.quotaUnit"
class="quota-unit"
placeholder="单位"
:disabled="submitting"
>
<el-option
v-for="option in quotaUnitOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</div>
</div>
<div v-if="!unlimitedQuota" class="form-hint">
<el-icon><i-ep-info-filled /></el-icon>
超出配额后Token将无法继续使用
</div>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose" :disabled="submitting">
取消
</el-button>
<el-button
type="primary"
@click="handleConfirm"
:loading="submitting"
:disabled="submitting"
>
{{ mode === 'create' ? '创建' : '保存' }}
</el-button>
</div>
</template>
</el-dialog>
</template>
<style scoped lang="scss">
.form-item-with-switch {
width: 100%;
display: flex;
flex-direction: column;
gap: 12px;
}
.expire-switch,
.quota-switch {
--el-switch-on-color: #67c23a;
}
.quota-input-group {
display: flex;
gap: 10px;
width: 100%;
}
.quota-number {
flex: 1;
:deep(.el-input__wrapper) {
width: 100%;
}
}
.quota-unit {
width: 100px;
}
.form-hint {
display: flex;
align-items: center;
gap: 6px;
margin-top: 8px;
padding: 8px 12px;
font-size: 13px;
color: #606266;
background: #f4f4f5;
border-radius: 6px;
border-left: 3px solid #409eff;
.el-icon {
font-size: 14px;
color: #409eff;
flex-shrink: 0;
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
:deep(.el-form-item__label) {
font-weight: 600;
color: #303133;
}
:deep(.el-input__prefix) {
color: #909399;
}
</style>

View File

@@ -34,6 +34,7 @@ declare module 'vue' {
ElIcon: typeof import('element-plus/es')['ElIcon']
ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElMain: typeof import('element-plus/es')['ElMain']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
@@ -41,6 +42,7 @@ declare module 'vue' {
ElPagination: typeof import('element-plus/es')['ElPagination']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
@@ -69,6 +71,7 @@ declare module 'vue' {
SupportModelList: typeof import('./../src/components/userPersonalCenter/components/SupportModelList.vue')['default']
SvgIcon: typeof import('./../src/components/SvgIcon/index.vue')['default']
SystemAnnouncementDialog: typeof import('./../src/components/SystemAnnouncementDialog/index.vue')['default']
TokenFormDialog: typeof import('./../src/components/userPersonalCenter/components/TokenFormDialog.vue')['default']
UsageStatistics: typeof import('./../src/components/userPersonalCenter/components/UsageStatistics.vue')['default']
UserManagement: typeof import('./../src/components/userPersonalCenter/components/UserManagement.vue')['default']
VerificationCode: typeof import('./../src/components/LoginDialog/components/FormLogin/VerificationCode.vue')['default']

View File

@@ -6,7 +6,6 @@ interface ImportMetaEnv {
readonly VITE_WEB_ENV: string;
readonly VITE_WEB_BASE_API: string;
readonly VITE_API_URL: string;
readonly VITE_BUILD_COMPRESS: string;
readonly VITE_SSO_SEVER_URL: string;
readonly VITE_APP_VERSION: string;
}