350 lines
12 KiB
Vue
350 lines
12 KiB
Vue
<template>
|
|
<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-[120px] lg:w-[250px]"
|
|
placeholder="Filter" value="">
|
|
|
|
<div class="dropdown">
|
|
<label tabindex="0"
|
|
class="inline-flex items-center justify-center flex gap-2 whitespace-nowrap font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground rounded-md px-3 text-xs h-8 border-dashed">
|
|
<svg viewBox="0 0 15 15" width="1.2em" height="1.2em" class="mr-2 h-4 w-4">
|
|
<path fill="currentColor" fill-rule="evenodd"
|
|
d="M7.5.877a6.623 6.623 0 1 0 0 13.246A6.623 6.623 0 0 0 7.5.877M1.827 7.5a5.673 5.673 0 1 1 11.346 0a5.673 5.673 0 0 1-11.346 0M7.5 4a.5.5 0 0 1 .5.5V7h2.5a.5.5 0 1 1 0 1H8v2.5a.5.5 0 0 1-1 0V8H4.5a.5.5 0 0 1 0-1H7V4.5a.5.5 0 0 1 .5-.5"
|
|
clip-rule="evenodd"></path>
|
|
</svg>
|
|
Status
|
|
</label>
|
|
<ul tabindex="0" class="dropdown-content z-[1] menu shadow-lg bg-base-100 rounded-none w-24 px-0">
|
|
<li v-for="status in statusOptions" :key="status" class="px-0 mx-0">
|
|
<a class="px-2 mx-0 hover:rounded-none">
|
|
<input type="checkbox" :checked="selectedStatuses.some(item => item.status === status)"
|
|
@change="toggleStatusFilter(status)" class="checkbox checkbox-xs" />
|
|
{{ status }}
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<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">
|
|
<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>
|
|
<KeyNew @closeModal="closeModal" />
|
|
|
|
</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">
|
|
<Settings2Icon class="w-6 h-6" />
|
|
</div>
|
|
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box z-[1] w-20 text-sm">
|
|
<li>
|
|
<div class="btn btn-xs p-1 text-xs hover:bg-rose-100 hover:text-rose-600 transition-colors"
|
|
@click="handleBatchAction('delete')">
|
|
<TrashIcon class="w-4 h-4" />删除
|
|
</div>
|
|
</li>
|
|
<hr class="-mx-2 my-1 border-base-content/10">
|
|
<li>
|
|
<div class="btn btn-xs p-1 text-xs hover:bg-rose-100 hover:text-rose-600 transition-colors"
|
|
@click="handleBatchAction('disable')">
|
|
<BadgeXIcon class="w-4 h-4" />禁用
|
|
</div>
|
|
</li>
|
|
<li>
|
|
<div class="btn btn-xs p-1 text-xs hover:bg-green-100 hover:text-green-600 transition-colors"
|
|
@click="handleBatchAction('enable')">
|
|
<BadgeCheckIcon class="w-4 h-4" />启用
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Table -->
|
|
<div class="card bg-base-100 shadow-xs overflow-x-auto dark:bg-base-200">
|
|
<table class="table table-sm">
|
|
<!-- Table Header -->
|
|
<thead>
|
|
<tr>
|
|
<th>
|
|
<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></th>
|
|
</tr>
|
|
</thead>
|
|
<!-- Table Body -->
|
|
<tbody>
|
|
<tr v-for="key in keys" :key="key.id"
|
|
class="hover:bg-gray-500/50 dark:hover:bg-neutral-600 transition-colors">
|
|
<td>
|
|
<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.name }}</td>
|
|
<td>
|
|
<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>
|
|
<div class="flex gap-2">
|
|
<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-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-4 h-4 dark:text-white" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
|
|
<!-- Pagination -->
|
|
<Pagination :currentPage="currentPage" :totalItems="totalItems" :pageSize="pageSize"
|
|
:pageSizeOptions="[10, 20, 50, 100]" @changePage="changePage" @changePageSize="changePageSize" />
|
|
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, reactive, onMounted, inject, computed } from 'vue';
|
|
import { useRouter } from 'vue-router';
|
|
import BreadcrumbHeader from '@/components/dashboard/BreadcrumbHeader.vue';
|
|
import Pagination from '@/components/Pagination.vue';
|
|
import KeyNew from '@/views/dashboard/KeyNew.vue';
|
|
import { useKeyStore } from '@/stores/key';
|
|
|
|
import {
|
|
BadgeXIcon, BadgeCheckIcon, EyeIcon, PlusIcon, Settings2Icon,
|
|
TrashIcon, Infinity
|
|
} from 'lucide-vue-next';
|
|
|
|
const router = useRouter();
|
|
const keyStore = useKeyStore();
|
|
const { setToast } = inject('toast');
|
|
|
|
onMounted(async () => {
|
|
await keyStore.fetchKeys();
|
|
})
|
|
|
|
const keys = computed(() => keyStore.keys);
|
|
|
|
// 用户数据
|
|
const currentPage = ref(1);
|
|
const pageSize = ref(10);
|
|
const totalItems = computed(() => keyStore.totalKeys);
|
|
|
|
|
|
|
|
// 封装公共的用户列表获取方法
|
|
const fetchKeys = 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 keyStore.fetchKeys(size, page, active);
|
|
};
|
|
|
|
// 组件挂载时加载用户数据
|
|
// onMounted(async () => {
|
|
// await fetchKeys();
|
|
// });
|
|
|
|
// 分页与页面大小变化
|
|
const changePage = async (page, size) => {
|
|
if (page == currentPage.value && size == pageSize.value) {
|
|
return
|
|
}
|
|
currentPage.value = page;
|
|
pageSize.value = size;
|
|
await fetchKeys();
|
|
};
|
|
|
|
const changePageSize = changePage;
|
|
|
|
// 复选框选择状态
|
|
const selectAll = ref(false)
|
|
const selectedKeys = ref([])
|
|
|
|
const toggleSelectAll = () => {
|
|
if (keys.value.length === 0) {
|
|
return
|
|
}
|
|
keys.value.forEach(key => key.selected = selectAll.value)
|
|
|
|
if (selectAll.value) {
|
|
// Select all on the current page
|
|
selectedKeys.value = users.value.map(user => user)
|
|
} else {
|
|
// Clear all selections
|
|
selectedKeys.value = []
|
|
}
|
|
|
|
}
|
|
|
|
const toggleUserSelection = (key) => {
|
|
if (selectedKeys.value.includes(key)) {
|
|
selectedKeys.value = selectedKeys.value.filter(selected => selected !== key);
|
|
} else {
|
|
selectedKeys.value.push(key);
|
|
}
|
|
selectAll.value = selectedKeys.value.length === keys.value.length;
|
|
};
|
|
|
|
// 状态筛选
|
|
const statusOptions = ['Active', 'Inactive'];
|
|
const selectedStatuses = reactive([]);
|
|
|
|
const toggleStatusFilter = async (status) => {
|
|
const statusValue = status === 'Active';
|
|
const index = selectedStatuses.findIndex(item => item.status === status);
|
|
|
|
if (index > -1) {
|
|
selectedStatuses.splice(index, 1);
|
|
} else {
|
|
selectedStatuses.push({ status, value: statusValue });
|
|
}
|
|
|
|
await fetchKeys(undefined, 1, undefined);
|
|
};
|
|
|
|
// 处理批量操作
|
|
const handleBatchAction = async (action) => {
|
|
if (selectedKeys.value.length === 0) {
|
|
return setToast('请选择数据', 'error');
|
|
}
|
|
if (!['enable', 'disable', 'delete'].includes(action)) {
|
|
return setToast('无效的操作 ${action}', 'error');
|
|
}
|
|
|
|
try {
|
|
const res = await keyStore.keyOption(action, selectedKeys.value.map(item => item.id));
|
|
if (res.data?.code === 200) {
|
|
setToast(`Key ${action} Success`, 'success');
|
|
} else {
|
|
setToast(res.data.error || `${action} Failed`, 'error');
|
|
}
|
|
selectedKeys.value = [];
|
|
selectAll.value = false;
|
|
await fetchKeys();
|
|
|
|
} catch (error) {
|
|
console.error(`批量操作 ${action} 失败:`, error);
|
|
setToast('批量操作失败', 'error');
|
|
}
|
|
};
|
|
|
|
// 更新用户状态
|
|
const updateStatus = async (key) => {
|
|
try {
|
|
const action = key.active ? 'enable' : 'disable';
|
|
const res = await keyStore.keyOption(action, [key.id]);
|
|
|
|
if (res.data?.code === 200) {
|
|
setToast(`Key ${key.name} has been ${action}`, 'success');
|
|
}
|
|
await fetchKeys();
|
|
} catch (error) {
|
|
console.error('状态更新失败:', error);
|
|
setToast('状态更新失败', 'error');
|
|
}
|
|
};
|
|
|
|
const viewKey = (key) => {
|
|
router.push({ name: 'ApiKeyView', query: { id: key.id } });
|
|
}
|
|
|
|
// 删除用户
|
|
const confirmDeleteKey = async (key) => {
|
|
if (confirm(`确认删除 ${key.name}?`)) {
|
|
await deleteKey(key);
|
|
}
|
|
};
|
|
|
|
const deleteKey = async (key) => {
|
|
try {
|
|
const res = await keyStore.keyOption('delete', [key.id]);
|
|
if (res.data?.code === 200) {
|
|
setToast('删除成功', 'success');
|
|
}
|
|
|
|
await fetchKeys();
|
|
} catch (error) {
|
|
console.error('删除失败:', error);
|
|
setToast('删除失败', 'error');
|
|
}
|
|
};
|
|
|
|
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 () => {
|
|
if (modalRef.value) {
|
|
modalRef.value.close();
|
|
}
|
|
await fetchKeys();
|
|
};
|
|
</script>
|