fix: lint errors

This commit is contained in:
Feng Yue
2025-08-13 13:10:31 +08:00
parent eb150b4937
commit f193db926d
9 changed files with 1214 additions and 541 deletions

View File

@@ -2,13 +2,22 @@
<div class="min-h-screen bg-gray-50">
<!-- 导航栏 -->
<nav class="bg-white shadow">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="flex h-16 justify-between">
<div class="flex items-center">
<div class="flex-shrink-0 flex items-center">
<svg class="h-8 w-8 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
<div class="flex flex-shrink-0 items-center">
<svg
class="h-8 w-8 text-blue-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg>
<span class="ml-2 text-xl font-bold text-gray-900">Claude Relay</span>
</div>
@@ -16,9 +25,9 @@
<div class="flex items-baseline space-x-4">
<button
:class="[
'px-3 py-2 rounded-md text-sm font-medium',
activeTab === 'overview'
? 'bg-blue-100 text-blue-700'
'rounded-md px-3 py-2 text-sm font-medium',
activeTab === 'overview'
? 'bg-blue-100 text-blue-700'
: 'text-gray-500 hover:text-gray-700'
]"
@click="activeTab = 'overview'"
@@ -27,9 +36,9 @@
</button>
<button
:class="[
'px-3 py-2 rounded-md text-sm font-medium',
activeTab === 'api-keys'
? 'bg-blue-100 text-blue-700'
'rounded-md px-3 py-2 text-sm font-medium',
activeTab === 'api-keys'
? 'bg-blue-100 text-blue-700'
: 'text-gray-500 hover:text-gray-700'
]"
@click="activeTab = 'api-keys'"
@@ -38,9 +47,9 @@
</button>
<button
:class="[
'px-3 py-2 rounded-md text-sm font-medium',
activeTab === 'usage'
? 'bg-blue-100 text-blue-700'
'rounded-md px-3 py-2 text-sm font-medium',
activeTab === 'usage'
? 'bg-blue-100 text-blue-700'
: 'text-gray-500 hover:text-gray-700'
]"
@click="activeTab = 'usage'"
@@ -55,8 +64,8 @@
Welcome, <span class="font-medium">{{ userStore.userName }}</span>
</div>
<button
class="rounded-md px-3 py-2 text-sm font-medium text-gray-500 hover:text-gray-700"
@click="handleLogout"
class="text-gray-500 hover:text-gray-700 px-3 py-2 rounded-md text-sm font-medium"
>
Logout
</button>
@@ -66,7 +75,7 @@
</nav>
<!-- 主内容 -->
<main class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
<main class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
<!-- Overview Tab -->
<div v-if="activeTab === 'overview'" class="space-y-6">
<div>
@@ -75,73 +84,121 @@
</div>
<!-- Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
<div class="overflow-hidden rounded-lg bg-white shadow">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m0 0a2 2 0 012 2m-2-2h-6m6 0v6a2 2 0 01-2 2H9a2 2 0 01-2-2V9a2 2 0 012-2h6z" />
<svg
class="h-6 w-6 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M15 7a2 2 0 012 2m0 0a2 2 0 012 2m-2-2h-6m6 0v6a2 2 0 01-2 2H9a2 2 0 01-2-2V9a2 2 0 012-2h6z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">API Keys</dt>
<dd class="text-lg font-medium text-gray-900">{{ userProfile?.apiKeyCount || 0 }}</dd>
<dt class="truncate text-sm font-medium text-gray-500">API Keys</dt>
<dd class="text-lg font-medium text-gray-900">
{{ userProfile?.apiKeyCount || 0 }}
</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="overflow-hidden rounded-lg bg-white shadow">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
<svg
class="h-6 w-6 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M13 10V3L4 14h7v7l9-11h-7z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Total Requests</dt>
<dd class="text-lg font-medium text-gray-900">{{ formatNumber(userProfile?.totalUsage?.requests || 0) }}</dd>
<dt class="truncate text-sm font-medium text-gray-500">Total Requests</dt>
<dd class="text-lg font-medium text-gray-900">
{{ formatNumber(userProfile?.totalUsage?.requests || 0) }}
</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="overflow-hidden rounded-lg bg-white shadow">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
<svg
class="h-6 w-6 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Input Tokens</dt>
<dd class="text-lg font-medium text-gray-900">{{ formatNumber(userProfile?.totalUsage?.inputTokens || 0) }}</dd>
<dt class="truncate text-sm font-medium text-gray-500">Input Tokens</dt>
<dd class="text-lg font-medium text-gray-900">
{{ formatNumber(userProfile?.totalUsage?.inputTokens || 0) }}
</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="overflow-hidden rounded-lg bg-white shadow">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1" />
<svg
class="h-6 w-6 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Total Cost</dt>
<dd class="text-lg font-medium text-gray-900">${{ (userProfile?.totalUsage?.totalCost || 0).toFixed(4) }}</dd>
<dt class="truncate text-sm font-medium text-gray-500">Total Cost</dt>
<dd class="text-lg font-medium text-gray-900">
${{ (userProfile?.totalUsage?.totalCost || 0).toFixed(4) }}
</dd>
</dl>
</div>
</div>
@@ -150,38 +207,50 @@
</div>
<!-- User Info -->
<div class="bg-white shadow rounded-lg">
<div class="rounded-lg bg-white shadow">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">Account Information</h3>
<h3 class="text-lg font-medium leading-6 text-gray-900">Account Information</h3>
<div class="mt-5 border-t border-gray-200">
<dl class="divide-y divide-gray-200">
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<dt class="text-sm font-medium text-gray-500">Username</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userProfile?.username }}</dd>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ userProfile?.username }}
</dd>
</div>
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<dt class="text-sm font-medium text-gray-500">Display Name</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userProfile?.displayName || 'N/A' }}</dd>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ userProfile?.displayName || 'N/A' }}
</dd>
</div>
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<dt class="text-sm font-medium text-gray-500">Email</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ userProfile?.email || 'N/A' }}</dd>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ userProfile?.email || 'N/A' }}
</dd>
</div>
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<dt class="text-sm font-medium text-gray-500">Role</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<span
class="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800"
>
{{ userProfile?.role || 'user' }}
</span>
</dd>
</div>
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<dt class="text-sm font-medium text-gray-500">Member Since</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ formatDate(userProfile?.createdAt) }}</dd>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ formatDate(userProfile?.createdAt) }}
</dd>
</div>
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5">
<dt class="text-sm font-medium text-gray-500">Last Login</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ formatDate(userProfile?.lastLoginAt) || 'N/A' }}</dd>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
{{ formatDate(userProfile?.lastLoginAt) || 'N/A' }}
</dd>
</div>
</dl>
</div>
@@ -263,4 +332,4 @@ onMounted(() => {
<style scoped>
/* 组件特定样式 */
</style>
</style>

View File

@@ -1,65 +1,67 @@
<template>
<div class="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div class="max-w-md w-full space-y-8">
<div class="flex min-h-screen items-center justify-center bg-gray-50 px-4 py-12 sm:px-6 lg:px-8">
<div class="w-full max-w-md space-y-8">
<div>
<div class="mx-auto h-12 w-auto flex items-center justify-center">
<svg class="h-8 w-8 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
<div class="mx-auto flex h-12 w-auto items-center justify-center">
<svg class="h-8 w-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg>
<span class="ml-2 text-xl font-bold text-gray-900">Claude Relay</span>
</div>
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
User Sign In
</h2>
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">User Sign In</h2>
<p class="mt-2 text-center text-sm text-gray-600">
Sign in to your account to manage your API keys
</p>
</div>
<div class="bg-white rounded-lg shadow px-6 py-8">
<div class="rounded-lg bg-white px-6 py-8 shadow">
<form class="space-y-6" @submit.prevent="handleLogin">
<div>
<label for="username" class="block text-sm font-medium text-gray-700">
Username
</label>
<label class="block text-sm font-medium text-gray-700" for="username"> Username </label>
<div class="mt-1">
<input
id="username"
name="username"
type="text"
required
v-model="form.username"
class="relative block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-blue-500 focus:outline-none focus:ring-blue-500 sm:text-sm"
:disabled="loading"
class="appearance-none relative block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
name="username"
placeholder="Enter your username"
required
type="text"
/>
</div>
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-700">
Password
</label>
<label class="block text-sm font-medium text-gray-700" for="password"> Password </label>
<div class="mt-1">
<input
id="password"
name="password"
type="password"
required
v-model="form.password"
class="relative block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-blue-500 focus:outline-none focus:ring-blue-500 sm:text-sm"
:disabled="loading"
class="appearance-none relative block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
name="password"
placeholder="Enter your password"
required
type="password"
/>
</div>
</div>
<div v-if="error" class="bg-red-50 border border-red-200 rounded-md p-4">
<div v-if="error" class="rounded-md border border-red-200 bg-red-50 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
<svg class="h-5 w-5 text-red-400" fill="currentColor" viewBox="0 0 20 20">
<path
clip-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
fill-rule="evenodd"
/>
</svg>
</div>
<div class="ml-3">
@@ -70,14 +72,30 @@
<div>
<button
type="submit"
class="group relative flex w-full justify-center rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="loading || !form.username || !form.password"
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
type="submit"
>
<span v-if="loading" class="absolute left-0 inset-y-0 flex items-center pl-3">
<svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
<span v-if="loading" class="absolute inset-y-0 left-0 flex items-center pl-3">
<svg
class="h-5 w-5 animate-spin text-white"
fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
fill="currentColor"
></path>
</svg>
</span>
{{ loading ? 'Signing In...' : 'Sign In' }}
@@ -85,10 +103,7 @@
</div>
<div class="text-center">
<router-link
to="/admin-login"
class="text-sm text-blue-600 hover:text-blue-500"
>
<router-link class="text-sm text-blue-600 hover:text-blue-500" to="/admin-login">
Admin Login
</router-link>
</div>
@@ -143,4 +158,4 @@ const handleLogin = async () => {
<style scoped>
/* 组件特定样式 */
</style>
</style>

View File

@@ -8,14 +8,19 @@
Manage users, their API keys, and view usage statistics
</p>
</div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
<button
@click="loadUsers"
:disabled="loading"
class="inline-flex items-center justify-center rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 sm:w-auto"
:disabled="loading"
@click="loadUsers"
>
<svg class="-ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
<svg class="-ml-1 mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg>
Refresh
</button>
@@ -23,18 +28,28 @@
</div>
<!-- Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
<div class="overflow-hidden rounded-lg bg-white shadow">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
<svg
class="h-6 w-6 text-blue-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Total Users</dt>
<dt class="truncate text-sm font-medium text-gray-500">Total Users</dt>
<dd class="text-lg font-medium text-gray-900">{{ userStats?.totalUsers || 0 }}</dd>
</dl>
</div>
@@ -42,17 +57,27 @@
</div>
</div>
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="overflow-hidden rounded-lg bg-white shadow">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
<svg
class="h-6 w-6 text-green-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Active Users</dt>
<dt class="truncate text-sm font-medium text-gray-500">Active Users</dt>
<dd class="text-lg font-medium text-gray-900">{{ userStats?.activeUsers || 0 }}</dd>
</dl>
</div>
@@ -60,36 +85,60 @@
</div>
</div>
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="overflow-hidden rounded-lg bg-white shadow">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-purple-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m0 0a2 2 0 012 2m-2-2h-6m6 0v6a2 2 0 01-2 2H9a2 2 0 01-2-2V9a2 2 0 012-2h6z" />
<svg
class="h-6 w-6 text-purple-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M15 7a2 2 0 012 2m0 0a2 2 0 012 2m-2-2h-6m6 0v6a2 2 0 01-2 2H9a2 2 0 01-2-2V9a2 2 0 012-2h6z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Total API Keys</dt>
<dd class="text-lg font-medium text-gray-900">{{ userStats?.totalApiKeys || 0 }}</dd>
<dt class="truncate text-sm font-medium text-gray-500">Total API Keys</dt>
<dd class="text-lg font-medium text-gray-900">
{{ userStats?.totalApiKeys || 0 }}
</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="overflow-hidden rounded-lg bg-white shadow">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-yellow-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1" />
<svg
class="h-6 w-6 text-yellow-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Total Cost</dt>
<dd class="text-lg font-medium text-gray-900">${{ (userStats?.totalUsage?.totalCost || 0).toFixed(4) }}</dd>
<dt class="truncate text-sm font-medium text-gray-500">Total Cost</dt>
<dd class="text-lg font-medium text-gray-900">
${{ (userStats?.totalUsage?.totalCost || 0).toFixed(4) }}
</dd>
</dl>
</div>
</div>
@@ -98,24 +147,34 @@
</div>
<!-- Search and Filters -->
<div class="bg-white shadow rounded-lg">
<div class="rounded-lg bg-white shadow">
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-center sm:justify-between">
<div class="sm:flex sm:items-center space-y-4 sm:space-y-0 sm:space-x-4">
<div class="space-y-4 sm:flex sm:items-center sm:space-x-4 sm:space-y-0">
<!-- Search -->
<div class="flex-1 min-w-0">
<div class="min-w-0 flex-1">
<div class="relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<svg
class="h-5 w-5 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg>
</div>
<input
v-model="searchQuery"
@input="debouncedSearch"
type="search"
class="focus:ring-blue-500 focus:border-blue-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md"
class="block w-full rounded-md border-gray-300 pl-10 focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
placeholder="Search users..."
type="search"
@input="debouncedSearch"
/>
</div>
</div>
@@ -124,8 +183,8 @@
<div>
<select
v-model="selectedRole"
@change="loadUsers"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
@change="loadUsers"
>
<option value="">All Roles</option>
<option value="user">User</option>
@@ -137,8 +196,8 @@
<div>
<select
v-model="selectedStatus"
@change="loadUsers"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
@change="loadUsers"
>
<option value="">All Status</option>
<option value="true">Active</option>
@@ -151,55 +210,85 @@
</div>
<!-- Users Table -->
<div class="bg-white shadow overflow-hidden sm:rounded-md">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200">
<h3 class="text-lg leading-6 font-medium text-gray-900">
<div class="overflow-hidden bg-white shadow sm:rounded-md">
<div class="border-b border-gray-200 px-4 py-5 sm:px-6">
<h3 class="text-lg font-medium leading-6 text-gray-900">
Users
<span v-if="!loading" class="text-sm text-gray-500">({{ filteredUsers.length }} of {{ users.length }})</span>
<span v-if="!loading" class="text-sm text-gray-500"
>({{ filteredUsers.length }} of {{ users.length }})</span
>
</h3>
</div>
<!-- Loading State -->
<div v-if="loading" class="text-center py-12">
<svg class="animate-spin h-8 w-8 text-blue-600 mx-auto" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
<div v-if="loading" class="py-12 text-center">
<svg
class="mx-auto h-8 w-8 animate-spin text-blue-600"
fill="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
fill="currentColor"
></path>
</svg>
<p class="mt-2 text-sm text-gray-500">Loading users...</p>
</div>
<!-- Users List -->
<ul v-else-if="filteredUsers.length > 0" role="list" class="divide-y divide-gray-200">
<ul v-else-if="filteredUsers.length > 0" class="divide-y divide-gray-200" role="list">
<li v-for="user in filteredUsers" :key="user.id" class="px-6 py-4">
<div class="flex items-center justify-between">
<div class="flex items-center min-w-0 flex-1">
<div class="flex min-w-0 flex-1 items-center">
<div class="flex-shrink-0">
<div class="h-10 w-10 rounded-full bg-gray-300 flex items-center justify-center">
<svg class="h-6 w-6 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-gray-300">
<svg
class="h-6 w-6 text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg>
</div>
</div>
<div class="ml-4 flex-1 min-w-0">
<div class="ml-4 min-w-0 flex-1">
<div class="flex items-center">
<p class="text-sm font-medium text-gray-900 truncate">
<p class="truncate text-sm font-medium text-gray-900">
{{ user.displayName || user.username }}
</p>
<div class="ml-2 flex items-center space-x-2">
<span :class="[
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
user.isActive
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
]">
<span
:class="[
'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium',
user.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
]"
>
{{ user.isActive ? 'Active' : 'Disabled' }}
</span>
<span :class="[
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
user.role === 'admin'
? 'bg-purple-100 text-purple-800'
: 'bg-blue-100 text-blue-800'
]">
<span
:class="[
'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium',
user.role === 'admin'
? 'bg-purple-100 text-purple-800'
: 'bg-blue-100 text-blue-800'
]"
>
{{ user.role }}
</span>
</div>
@@ -208,10 +297,15 @@
<span>@{{ user.username }}</span>
<span v-if="user.email">{{ user.email }}</span>
<span>{{ user.apiKeyCount || 0 }} API keys</span>
<span v-if="user.lastLoginAt">Last login: {{ formatDate(user.lastLoginAt) }}</span>
<span v-if="user.lastLoginAt"
>Last login: {{ formatDate(user.lastLoginAt) }}</span
>
<span v-else>Never logged in</span>
</div>
<div v-if="user.totalUsage" class="mt-1 flex items-center space-x-4 text-xs text-gray-400">
<div
v-if="user.totalUsage"
class="mt-1 flex items-center space-x-4 text-xs text-gray-400"
>
<span>{{ formatNumber(user.totalUsage.requests || 0) }} requests</span>
<span>${{ (user.totalUsage.totalCost || 0).toFixed(4) }} total cost</span>
</div>
@@ -220,54 +314,85 @@
<div class="flex items-center space-x-2">
<!-- View Usage Stats -->
<button
@click="viewUserStats(user)"
class="inline-flex items-center p-1 border border-transparent rounded text-gray-400 hover:text-blue-600"
class="inline-flex items-center rounded border border-transparent p-1 text-gray-400 hover:text-blue-600"
title="View Usage Stats"
@click="viewUserStats(user)"
>
<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="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg>
</button>
<!-- Disable User API Keys -->
<button
@click="disableUserApiKeys(user)"
class="inline-flex items-center rounded border border-transparent p-1 text-gray-400 hover:text-red-600 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="user.apiKeyCount === 0"
class="inline-flex items-center p-1 border border-transparent rounded text-gray-400 hover:text-red-600 disabled:opacity-50 disabled:cursor-not-allowed"
title="Disable All API Keys"
@click="disableUserApiKeys(user)"
>
<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="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L18 12M6 6l12 12" />
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L18 12M6 6l12 12"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg>
</button>
<!-- Toggle User Status -->
<button
@click="toggleUserStatus(user)"
:class="[
'inline-flex items-center p-1 border border-transparent rounded',
user.isActive
? 'text-gray-400 hover:text-red-600'
'inline-flex items-center rounded border border-transparent p-1',
user.isActive
? 'text-gray-400 hover:text-red-600'
: 'text-gray-400 hover:text-green-600'
]"
:title="user.isActive ? 'Disable User' : 'Enable User'"
@click="toggleUserStatus(user)"
>
<svg v-if="user.isActive" 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="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L18 12M6 6l12 12" />
<svg
v-if="user.isActive"
class="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L18 12M6 6l12 12"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg>
<svg v-else 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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
<svg v-else class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg>
</button>
<!-- Change Role -->
<button
@click="changeUserRole(user)"
class="inline-flex items-center p-1 border border-transparent rounded text-gray-400 hover:text-purple-600"
class="inline-flex items-center rounded border border-transparent p-1 text-gray-400 hover:text-purple-600"
title="Change Role"
@click="changeUserRole(user)"
>
<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="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4" />
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg>
</button>
</div>
@@ -276,13 +401,25 @@
</ul>
<!-- Empty State -->
<div v-else class="text-center py-12">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
<div v-else class="py-12 text-center">
<svg
class="mx-auto h-12 w-12 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">No users found</h3>
<p class="mt-1 text-sm text-gray-500">
{{ searchQuery ? 'No users match your search criteria.' : 'No users have been created yet.' }}
{{
searchQuery ? 'No users match your search criteria.' : 'No users have been created yet.'
}}
</p>
</div>
</div>
@@ -296,13 +433,13 @@
<!-- Confirm Modals -->
<ConfirmModal
:confirm-class="confirmAction.confirmClass"
:confirm-text="confirmAction.confirmText"
:message="confirmAction.message"
:show="showConfirmModal"
:title="confirmAction.title"
:message="confirmAction.message"
:confirmText="confirmAction.confirmText"
:confirmClass="confirmAction.confirmClass"
@confirm="handleConfirmAction"
@cancel="showConfirmModal = false"
@confirm="handleConfirmAction"
/>
<!-- Change Role Modal -->
@@ -350,22 +487,23 @@ const filteredUsers = computed(() => {
// Apply search filter
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
filtered = filtered.filter(user =>
user.username.toLowerCase().includes(query) ||
user.displayName?.toLowerCase().includes(query) ||
user.email?.toLowerCase().includes(query)
filtered = filtered.filter(
(user) =>
user.username.toLowerCase().includes(query) ||
user.displayName?.toLowerCase().includes(query) ||
user.email?.toLowerCase().includes(query)
)
}
// Apply role filter
if (selectedRole.value) {
filtered = filtered.filter(user => user.role === selectedRole.value)
filtered = filtered.filter((user) => user.role === selectedRole.value)
}
// Apply status filter
if (selectedStatus.value !== '') {
const isActive = selectedStatus.value === 'true'
filtered = filtered.filter(user => user.isActive === isActive)
filtered = filtered.filter((user) => user.isActive === isActive)
}
return filtered
@@ -432,7 +570,7 @@ const toggleUserStatus = (user) => {
selectedUser.value = user
confirmAction.value = {
title: user.isActive ? 'Disable User' : 'Enable User',
message: user.isActive
message: user.isActive
? `Are you sure you want to disable user "${user.username}"? This will prevent them from logging in.`
: `Are you sure you want to enable user "${user.username}"?`,
confirmText: user.isActive ? 'Disable' : 'Enable',
@@ -444,7 +582,7 @@ const toggleUserStatus = (user) => {
const disableUserApiKeys = (user) => {
if (user.apiKeyCount === 0) return
selectedUser.value = user
confirmAction.value = {
title: 'Disable All API Keys',
@@ -472,7 +610,7 @@ const handleConfirmAction = async () => {
})
if (response.data.success) {
const userIndex = users.value.findIndex(u => u.id === user.id)
const userIndex = users.value.findIndex((u) => u.id === user.id)
if (userIndex !== -1) {
users.value[userIndex].isActive = !user.isActive
}
@@ -508,4 +646,4 @@ onMounted(() => {
<style scoped>
/* 组件特定样式 */
</style>
</style>