fix UI & token copy
This commit is contained in:
119
frontend/src/components/QRCodeCard.vue
Normal file
119
frontend/src/components/QRCodeCard.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center space-y-4 p-6 bg-base-100 rounded-xl shadow-lg max-w-sm mx-auto backdrop-blur-xl glass">
|
||||
|
||||
<div class="p-3 bg-white rounded-lg shadow-inner cursor-pointer" @click="toggleQRCode">
|
||||
<qrcode-vue :value="currentValue" :size="size" level="H" />
|
||||
</div>
|
||||
|
||||
<div class="relative w-full p-4 ">
|
||||
<p
|
||||
class="text-sm break-all whitespace-pre-wrap m-1 p-1 pr-5 text-base-content border border-dashed border-base-content rounded-lg">
|
||||
{{ currentValue }}
|
||||
</p>
|
||||
<button @click="copyValue" class="absolute top-2 right-2 btn btn-xs btn-ghost bg-gray-100">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2M16 8h2a2 2 0 012 2v8a2 2 0 01-2-2h-8a2 2 0 01-2-2v-2" />
|
||||
</svg>
|
||||
</button>
|
||||
<div v-if="showCopied"
|
||||
class="absolute -top-5 -right-2 bg-neutral text-neutral-content px-2 py-1 rounded text-xs opacity-100 transition-opacity duration-300">
|
||||
Copied
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2 w-full">
|
||||
<button v-for="app in applist" :key="app.name"
|
||||
class="btn btn-sm btn-outline btn-ghost flex items-center justify-center"
|
||||
@click="applyPrefix(app.name)">
|
||||
<img :src="app.url" alt="" class="w-5 h-5 mr-1">
|
||||
<span>{{ app.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<button class="btn btn-outline btn-sm w-full" @click="resetValue">
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import QrcodeVue from 'qrcode.vue';
|
||||
|
||||
// 定义组件接收的 props
|
||||
const props = defineProps({
|
||||
value: { // 二维码的原始值
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
size: { // 二维码的尺寸 (像素)
|
||||
type: Number,
|
||||
default: 160 // 默认大小
|
||||
}
|
||||
});
|
||||
|
||||
// 使用 ref 创建一个响应式变量,用于存储当前显示的二维码值
|
||||
// 初始值是来自 props 的 value
|
||||
const currentValue = ref(props.value);
|
||||
|
||||
// 监听 props.value 的变化,如果外部传入的 value 改变,更新 currentValue
|
||||
watch(() => props.value, (newValue) => {
|
||||
currentValue.value = newValue;
|
||||
});
|
||||
|
||||
|
||||
const showCopied = ref(false);
|
||||
|
||||
const copyValue = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(currentValue.value);
|
||||
|
||||
showCopied.value = true;
|
||||
setTimeout(() => {
|
||||
showCopied.value = false;
|
||||
}, 1500); // 1.5 秒后恢复
|
||||
} catch (err) {
|
||||
console.error('Failed to copy: ', err);
|
||||
// 可以在这里添加错误提示
|
||||
}
|
||||
};
|
||||
|
||||
const applist = reactive([
|
||||
// { name: 'openteam', url: '/assets/logo.svg' },
|
||||
{ name: 'botgem', url: 'https://botgem.com/favicon.ico' },
|
||||
{ name: 'opencat', url: 'https://opencat.app/favicon.ico' },
|
||||
])
|
||||
|
||||
const applyPrefix = (name) => {
|
||||
let origin = window.location.origin;
|
||||
|
||||
switch (name) {
|
||||
case 'botgem':
|
||||
currentValue.value = `ama://set-api-key?server=${origin}&key=${props.value}`;
|
||||
break;
|
||||
case 'opencat':
|
||||
currentValue.value = `opencat://team/join?domain=${origin}&token=${props.value}`;
|
||||
break;
|
||||
default:
|
||||
currentValue.value = name + props.value;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleQRCode = () => {
|
||||
if (currentValue.value.startsWith('sk-')) {
|
||||
return
|
||||
} else {
|
||||
window.open(currentValue.value, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
// 清除前缀,恢复到原始值
|
||||
const resetValue = () => {
|
||||
currentValue.value = props.value;
|
||||
};
|
||||
</script>
|
||||
@@ -69,10 +69,10 @@
|
||||
<template v-else-if="newApiKey.type === 'gemini'">
|
||||
<img src="../../assets/gemini.svg" class="w-5 h-5" alt="">
|
||||
</template>
|
||||
<template v-else="newApiKey.type ==='azure'">
|
||||
<template v-else-if="newApiKey.type ==='azure'">
|
||||
<img src="../../assets/azure.svg" class="w-5 h-5" alt="">
|
||||
</template>
|
||||
<template v-else="newApiKey.type ==='github'">
|
||||
<template v-else-if="newApiKey.type ==='github'">
|
||||
<img src="../../assets/github.svg" class="w-5 h-5" alt="">
|
||||
</template>
|
||||
<template v-else="newApiKey.type">
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
<div class="min-h-screen bg-base-100 p-4">
|
||||
<!-- Breadcrumb and Title -->
|
||||
<BreadcrumbHeader />
|
||||
|
||||
|
||||
<!-- <div v-if="keyStore.loading" class="loading loading-spinner loading-lg"></div> -->
|
||||
<div class="flex flex-wrap gap-2 mb-4" v-if="keys">
|
||||
<div class="flex flex-1 items-center space-x-2">
|
||||
<input
|
||||
class="flex rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 h-8 w-[150px] lg:w-[250px]"
|
||||
class="flex rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 h-8 w-[120px] lg:w-[250px]"
|
||||
placeholder="Filter" value="">
|
||||
|
||||
<div class="dropdown">
|
||||
@@ -33,7 +33,8 @@
|
||||
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline btn-success btn-sm gap-1" onclick="myModal.showModal()">
|
||||
<div class="flex flex-1 items-center justify-end space-x-2">
|
||||
<button class="btn btn-outline btn-success btn-sm gap-1" onclick="myModal.showModal()">
|
||||
<PlusIcon class="w-3.5 h-3.5" />New
|
||||
</button>
|
||||
<dialog id="myModal" class="modal" ref="modalRef">
|
||||
@@ -48,6 +49,7 @@
|
||||
<button>关闭</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</div>
|
||||
|
||||
<div class="dropdown dropdown-end dropdown-hover">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost btn-sm p-1 h-8 w-8">
|
||||
@@ -85,13 +87,15 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<input type="checkbox" class="checkbox checkbox-xs" v-model="selectAll" @change="toggleSelectAll" />
|
||||
<div class="flex gap-1 items-center">
|
||||
<input type="checkbox" class="checkbox checkbox-xs" v-model="selectAll" @change="toggleSelectAll" />
|
||||
</div>
|
||||
</th>
|
||||
<th>Type</th>
|
||||
<th>Name</th>
|
||||
<th>Active</th>
|
||||
<th>Key</th>
|
||||
<th>Endpoint</th>
|
||||
<!-- <th>Key</th> -->
|
||||
<!-- <th>Endpoint</th> -->
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -100,27 +104,38 @@
|
||||
<tr v-for="key in keys" :key="key.id"
|
||||
class="hover:bg-gray-500/50 dark:hover:bg-neutral-600 transition-colors">
|
||||
<td>
|
||||
<input type="checkbox" class="checkbox checkbox-xs" v-model="key.selected"
|
||||
@change="toggleUserSelection(key)" />
|
||||
<div class="flex gap-1 items-center">
|
||||
<input type="checkbox" class="checkbox checkbox-xs" v-model="key.selected"
|
||||
@change="toggleUserSelection(key)" />
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-xs dark:text-white">
|
||||
<div class="flex gap-1 items-center">
|
||||
<span class="backdrop-blur-lg glass rounded-full border-none"> <img :src="displayIcon(key.type)"
|
||||
class="w-5 h-5" alt=""></span>
|
||||
{{ key.type }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-xs dark:text-white">{{ key.type }}</td>
|
||||
<td class="text-xs dark:text-white">{{ key.name }}</td>
|
||||
<td>
|
||||
<input type="checkbox" class="toggle toggle-xs" :class="key.active ? 'toggle-success' : 'toggle-error'"
|
||||
v-model="key.active" @change="updateStatus(key)" />
|
||||
<div class="flex gap-1">
|
||||
<input type="checkbox" class="toggle toggle-xs" :class="key.active ? 'toggle-success' : 'toggle-error'"
|
||||
v-model="key.active" @change="updateStatus(key)" />
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-xs dark:text-white">{{ key.apikey }}</td>
|
||||
<td class="text-xs dark:text-white">{{ key.endpoint }}</td>
|
||||
<!-- <td class="text-xs dark:text-white">{{ key.apikey }}</td> -->
|
||||
<!-- <td class="text-xs dark:text-white">{{ key.endpoint }}</td> -->
|
||||
<td>
|
||||
<div class="flex gap-1">
|
||||
<div class="lg:tooltip lg:tooltip-top lg:tooltip-open" data-tip="预览">
|
||||
<button class="btn btn-ghost btn-xs btn-square " @click="viewKey(key)">
|
||||
<EyeIcon class="w-3.5 h-3.5 dark:text-white" />
|
||||
<EyeIcon class="w-4 h-4 dark:text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="lg:tooltip lg:tooltip-top lg:tooltip-open" data-tip="删除">
|
||||
<button class="btn btn-ghost btn-xs btn-square text-error hover:bg-error/30" @click="confirmDeleteKey(key)">
|
||||
<TrashIcon class="w-3.5 h-3.5 dark:text-white" />
|
||||
<button class="btn btn-ghost btn-xs btn-square text-error hover:bg-error/30"
|
||||
@click="confirmDeleteKey(key)">
|
||||
<TrashIcon class="w-4 h-4 dark:text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -133,7 +148,7 @@
|
||||
|
||||
<!-- Pagination -->
|
||||
<Pagination :currentPage="currentPage" :totalItems="totalItems" :pageSize="pageSize"
|
||||
:pageSizeOptions="[ 10, 20, 50, 100]" @changePage="changePage" @changePageSize="changePageSize" />
|
||||
:pageSizeOptions="[10, 20, 50, 100]" @changePage="changePage" @changePageSize="changePageSize" />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
@@ -305,6 +320,24 @@ const deleteKey = async (key) => {
|
||||
}
|
||||
};
|
||||
|
||||
const displayIcon = (apitype) => {
|
||||
switch (apitype) {
|
||||
case 'openai':
|
||||
return '/assets/openai.svg';
|
||||
case 'claude':
|
||||
return '/assets/claude.svg';
|
||||
case 'gemini':
|
||||
return '/assets/gemini.svg'
|
||||
case 'azure':
|
||||
return '/assets/azure.svg';
|
||||
case 'github':
|
||||
return '/assets/github.svg';
|
||||
default:
|
||||
return '/assets/logo.svg';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 关闭模态框
|
||||
const modalRef = ref(null);
|
||||
const closeModal = async () => {
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
class="absolute inset-y-0 right-0 px-3 flex items-center text-base-content/60 hover:text-base-content/80 focus:outline-none focus:ring-0 rounded-r-md"
|
||||
id="token-visibility-toggle">
|
||||
|
||||
<template v-if="!isPasswordVisible">
|
||||
<template v-if="!isTokenVisible">
|
||||
<EyeOff class="w-5 h-5" />
|
||||
</template>
|
||||
<template v-else>
|
||||
|
||||
@@ -28,65 +28,91 @@
|
||||
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-lg">Tokens</h3>
|
||||
<div v-if="user.tokens && user.tokens.length" class="overflow-x-auto -mx-6">
|
||||
<table class="table table-sm w-full">
|
||||
<thead>
|
||||
<tr class="text-xs text-base-content/70 uppercase bg-base-200">
|
||||
<th class="px-2 py-3">Token Name</th>
|
||||
<th class="px-2 py-3">Status</th>
|
||||
<th class="px-2 py-3">Key</th>
|
||||
<th class="px-2 py-3">Expired At</th>
|
||||
<th class="px-2 py-3">Quota</th>
|
||||
<th class="px-2 py-3">Used Quota</th>
|
||||
<th class="text-right px-2 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="token in user.tokens" :key="token.id" class="hover">
|
||||
<td class="font-mono text-xs px-2 py-3">{{ token.name }}</td>
|
||||
<td>
|
||||
<input type="checkbox" class="toggle toggle-xs"
|
||||
:class="token.active ? 'toggle-success' : 'toggle-error'" v-model="token.active"
|
||||
@change="updateStatus(token)" />
|
||||
</td>
|
||||
<td class="font-mono text-xs px-2 py-3">{{ token.key }}</td>
|
||||
<td class="px-2 py-3">{{ token.expired_at == 0 ? 'Never' : unixToDate(token.expired_at) }}</td>
|
||||
<td class="px-2 py-3">
|
||||
<template v-if="token.unlimited_quota">
|
||||
<Infinity />
|
||||
</template>
|
||||
<template v-else>{{ token.quota }}</template>
|
||||
</td>
|
||||
<td class="px-2 py-3">{{ token.used_quota }}</td>
|
||||
<td class="text-right px-2 py-3 flex justify-between items-center gap-1">
|
||||
<div class="md:tooltip" data-tip="clean usedquota">
|
||||
<button class="btn btn-ghost btn-xs btn-square text-sky-300" @click="cleanUsedToken(token)"
|
||||
aria-label="Revoke token">
|
||||
<Eraser class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="user.tokens">
|
||||
|
||||
<button v-if="token.name !== 'default'" class="btn btn-ghost btn-xs btn-square text-error"
|
||||
@click="confirmRevokeToken(token)" aria-label="Revoke token">
|
||||
<TrashIcon class="w-4 h-4" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p v-else class="text-center text-base-content/70 py-4">No tokens found</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow-xs overflow-x-auto dark:bg-base-200" v-if="user">
|
||||
<table class="table table-sm w-full">
|
||||
<thead>
|
||||
<tr class="text-xs text-base-content/70 uppercase bg-base-200">
|
||||
<th class="px-2 py-3">Token</th>
|
||||
<th class="px-2 py-3">Status</th>
|
||||
<!-- <th class="px-2 py-3">Key</th> -->
|
||||
<th class="px-2 py-3">Expired</th>
|
||||
<th class="px-2 py-3">Quota</th>
|
||||
<th class="px-2 py-3">Used</th>
|
||||
<th class="text-right px-2 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="token in user.tokens" :key="token.id" class="hover">
|
||||
<td class="font-mono text-xs px-2 py-3">{{ token.name }}</td>
|
||||
<td>
|
||||
<input type="checkbox" class="toggle toggle-xs" :class="token.active ? 'toggle-success' : 'toggle-error'"
|
||||
v-model="token.active" @change="updateStatus(token)" />
|
||||
</td>
|
||||
<!-- <td class="font-mono text-xs px-2 py-3">{{ token.key }}</td> -->
|
||||
<td class="px-2 py-3">{{ token.expired_at == 0 ? 'Never' : unixToDate(token.expired_at) }}</td>
|
||||
<td class="px-2 py-3">
|
||||
<template v-if="token.unlimited_quota">
|
||||
<Infinity />
|
||||
</template>
|
||||
<template v-else>{{ token.quota }}</template>
|
||||
</td>
|
||||
<td class="px-2 py-3">{{ token.used_quota }}</td>
|
||||
<td class="text-right px-1 py-3">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="lg:tooltip lg:tooltip-top lg:tooltip-open pt-1" data-tip="预览">
|
||||
<button class="btn btn-ghost btn-xs btn-square" onclick="" @click="viewToken(token)">
|
||||
<EyeIcon class="w-5 h-5 dark:text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<br />
|
||||
<div class="md:tooltip" data-tip="clean used">
|
||||
<button class="btn btn-ghost btn-xs btn-square text-sky-300 mt-1" @click="cleanUsedToken(token)"
|
||||
aria-label="Revoke token">
|
||||
<Eraser class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button v-if="token.name !== 'default'" class="btn btn-ghost btn-xs btn-square text-error items-center"
|
||||
@click="confirmRevokeToken(token)" aria-label="Revoke token">
|
||||
<TrashIcon class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<dialog id="myToken" class="modal" ref="tokenRef">
|
||||
<div class="modal-box px-0 sm:px-8">
|
||||
<form method="dialog">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||
</form>
|
||||
token
|
||||
<QRCodeCard
|
||||
:value="qrCodeValue"
|
||||
:size="120" />
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>关闭</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, inject, computed,watch } from 'vue';
|
||||
import { ref, onMounted, inject, computed, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import BreadcrumbHeader from '@/components/dashboard/BreadcrumbHeader.vue';
|
||||
import QRCodeCard from '@/components/QRCodeCard.vue';
|
||||
import TokenNew from '@/views/dashboard/TokenNew.vue';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import {
|
||||
@@ -104,7 +130,7 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
watch(() => authStore.user, async (newUser) => {
|
||||
if (newUser.expired_at>0) {
|
||||
if (newUser.expired_at > 0) {
|
||||
newUser.format_expired_at = unixToDate(newUser.expired_at);
|
||||
}
|
||||
})
|
||||
@@ -160,6 +186,40 @@ const cleanUsedToken = async (token) => {
|
||||
}
|
||||
}
|
||||
|
||||
const showTokenModel = ref(false);
|
||||
const tokenRef = ref(null);
|
||||
const viewToken = (token) => {
|
||||
const dialog = tokenRef.value;
|
||||
if (dialog) {
|
||||
if (!dialog.hasAttribute('open')) {
|
||||
qrCodeValue.value = token.key;
|
||||
dialog.showModal();
|
||||
} else {
|
||||
if (dialog.hasAttribute('open')) {
|
||||
dialog.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
showTokenModel.value = !showTokenModel.value
|
||||
}
|
||||
|
||||
const qrCodeValue = ref('');
|
||||
|
||||
// 监听showTokenModel的变化,控制模态框的显示和隐藏
|
||||
// watch(showTokenModel, (newValue) => {
|
||||
// const dialog = tokenRef.value;
|
||||
// if (dialog) {
|
||||
// if (newValue) {
|
||||
// if (!dialog.hasAttribute('open')) {
|
||||
// dialog.showModal();
|
||||
// }
|
||||
// } else {
|
||||
// if (dialog.hasAttribute('open')) {
|
||||
// dialog.close();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
|
||||
// 关闭模态框
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
<div class="flex flex-1 items-center space-x-2">
|
||||
<input
|
||||
class="flex rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 h-8 w-[150px] lg:w-[250px]"
|
||||
class="flex rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 h-8 w-[120px] lg:w-[250px]"
|
||||
placeholder="Filter" value="">
|
||||
|
||||
<div class="dropdown">
|
||||
@@ -34,21 +34,23 @@
|
||||
|
||||
</div>
|
||||
|
||||
<button class="btn btn-outline btn-success btn-sm gap-1" onclick="myModal.showModal()">
|
||||
<PlusIcon class="w-3.5 h-3.5" />New
|
||||
</button>
|
||||
<dialog id="myModal" class="modal" ref="modalRef">
|
||||
<div class="modal-box w-11/12 max-w-5xl h-screen px-0 sm:px-8">
|
||||
<form method="dialog">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||
</form>
|
||||
<UserNew @closeModal="closeModal" />
|
||||
<div class="flex flex-1 items-center justify-end space-x-1">
|
||||
<button class="btn btn-outline btn-success btn-sm gap-1" onclick="myModal.showModal()">
|
||||
<PlusIcon class="w-3.5 h-3.5" />New
|
||||
</button>
|
||||
<dialog id="myModal" class="modal" ref="modalRef">
|
||||
<div class="modal-box w-11/12 max-w-5xl h-screen px-0 sm:px-8">
|
||||
<form method="dialog">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||
</form>
|
||||
<UserNew @closeModal="closeModal" />
|
||||
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>关闭</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>关闭</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</div>
|
||||
|
||||
<div class="dropdown dropdown-end dropdown-hover">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost btn-sm p-1 h-8 w-8">
|
||||
@@ -92,7 +94,7 @@
|
||||
<th>Name</th>
|
||||
<th>Active</th>
|
||||
<th>Quota</th>
|
||||
<th>UsedQuota</th>
|
||||
<th>Used</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -107,8 +109,10 @@
|
||||
<td class="text-xs dark:text-white">{{ user.id }}</td>
|
||||
<td class="text-xs dark:text-white">{{ user.username }}</td>
|
||||
<td>
|
||||
<input type="checkbox" class="toggle toggle-xs" :class="user.active ? 'toggle-success' : 'toggle-error'"
|
||||
v-model="user.active" @change="updateStatus(user)" />
|
||||
<div class="flex items-center gap-1">
|
||||
<input type="checkbox" class="toggle toggle-xs" :class="user.active ? 'toggle-success' : 'toggle-error'"
|
||||
v-model="user.active" @change="updateStatus(user)" />
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-xs dark:text-white">
|
||||
<template v-if="user.unlimited_quota">
|
||||
@@ -124,7 +128,7 @@
|
||||
<EyeIcon class="w-3.5 h-3.5 dark:text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="lg:tooltip lg:tooltip-top lg:tooltip-open" data-tip="删除" v-if="user.role<20">
|
||||
<div class="lg:tooltip lg:tooltip-top lg:tooltip-open" data-tip="删除" v-if="user.role < 20">
|
||||
<button class="btn btn-ghost btn-xs btn-square text-error hover:bg-error/30"
|
||||
@click="confirmDeleteUser(user)">
|
||||
<TrashIcon class="w-3.5 h-3.5 dark:text-white" />
|
||||
@@ -168,7 +172,7 @@ const pageSize = ref(10);
|
||||
const totalItems = computed(() => userStore.totalUsers);
|
||||
|
||||
// 封装公共的用户列表获取方法
|
||||
const listUsers = async (size = pageSize.value, page = currentPage.value, active=selectedStatuses.map(status => status.value)) => {
|
||||
const listUsers = async (size = pageSize.value, page = currentPage.value, active = selectedStatuses.map(status => status.value)) => {
|
||||
currentPage.value = page || currentPage.value;
|
||||
// console.log('pagesize', pageSize.value, 'page', currentPage.value, 'active', selectedStatuses.map(status => status.value));
|
||||
await userStore.listUser(size, page, active);
|
||||
@@ -231,7 +235,7 @@ const toggleStatusFilter = async (status) => {
|
||||
selectedStatuses.push({ status, value: statusValue });
|
||||
}
|
||||
|
||||
await listUsers(undefined,1,undefined);
|
||||
await listUsers(undefined, 1, undefined);
|
||||
};
|
||||
|
||||
// 处理批量操作
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
<th class="px-2 py-3">Key</th>
|
||||
<th class="px-2 py-3">Expired At</th>
|
||||
<th class="px-2 py-3">Quota</th>
|
||||
<th class="px-2 py-3">Used Quota</th>
|
||||
<th class="px-2 py-3">Used</th>
|
||||
<th class="text-right px-2 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
Reference in New Issue
Block a user