Skip to content
This repository was archived by the owner on Apr 17, 2026. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,609 changes: 2,609 additions & 0 deletions api-1.yaml

Large diffs are not rendered by default.

26 changes: 13 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,25 @@
"type-check": "vue-tsc --build"
},
"dependencies": {
"axios": "^1.13.2",
"axios": "^1.13.6",
"echarts": "^6.0.0",
"neoeasytierweb": "link:",
"tailwindcss": "^4.1.17",
"vue": "^3.5.22",
"tailwindcss": "^4.2.1",
"vue": "^3.5.29",
"vue-echarts": "^8.0.1",
"vue-router": "^4.6.3"
"vue-router": "^4.6.4"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.17",
"@tsconfig/node22": "^22.0.2",
"@types/node": "^22.18.11",
"@vitejs/plugin-vue": "^6.0.1",
"@tailwindcss/vite": "^4.2.1",
"@tsconfig/node22": "^22.0.5",
"@types/node": "^22.19.13",
"@vitejs/plugin-vue": "^6.0.4",
"@vue/tsconfig": "^0.8.1",
"daisyui": "^5.4.7",
"daisyui": "^5.5.19",
"npm-run-all2": "^8.0.4",
"typescript": "~5.9.0",
"vite": "^7.1.11",
"vite-plugin-vue-devtools": "^8.0.3",
"vue-tsc": "^3.1.1"
"typescript": "~5.9.3",
"vite": "^7.3.1",
"vite-plugin-vue-devtools": "^8.0.7",
"vue-tsc": "^3.2.5"
}
}
1,540 changes: 763 additions & 777 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

89 changes: 24 additions & 65 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -1,69 +1,24 @@
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import { ref, onMounted } from 'vue'
import { isAuthenticated, getProfile, logout } from '@/utils/request/api'
import { useAuth } from '@/composables'

const year = new Date().getFullYear()
const isLoggedIn = ref(false)
const userInfo = ref<{ login?: string; avatar_url?: string } | null>(null)

onMounted(async () => {
isLoggedIn.value = await isAuthenticated()

if (isLoggedIn.value) {
try {
const userData = await getProfile();
if(!userData) return;
userInfo.value = userData.data
} catch (error) {
console.error('Failed to fetch user info:', error)
}
}else{
logout(); //防止cookie没了用户数据还在
}

// Re-check auth status on storage changes
window.addEventListener('storage', async () => {
isLoggedIn.value = await isAuthenticated()
if (isLoggedIn.value) {
try {
const userData = await getProfile()
if(!userData) return;
userInfo.value = userData.data
} catch (error) {
console.error('Failed to fetch user info:', error)
}
} else {
logout(); //防止cookie没了用户数据还在
userInfo.value = null
}
})
})

const handleLogout = () => {
logout()
isLoggedIn.value = false
userInfo.value = null
window.location.href = '/login'
}
const { isLoggedIn, userInfo, handleLogout } = useAuth()
</script>

<template>
<div class="layout">
<header class="navbar bg-base-100 shadow-md">
<header class="navbar bg-base-100 shadow-md rounded-b-xl">
<div class="navbar-start">
<div class="flex-1">
<h1 class="btn btn-ghost normal-case text-2xl font-bold">EasyTierMC Uptime</h1>
</div>
<RouterLink to="/" class="btn btn-ghost normal-case text-2xl font-bold">EasyTierMC Uptime</RouterLink>
</div>

<!-- 用户信息显示在正中心 -->
<div class="navbar-center" v-if="isLoggedIn && userInfo">
<div class="flex items-center gap-3 px-4 py-2">
<div class="avatar">
<div class="w-8 h-8 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2">
<img
:src="userInfo.avatar_url || `https://ui-avatars.com/api/?name=${encodeURIComponent(userInfo.login || 'User')}&background=random`"
<img
:src="userInfo.avatar_url || `https://ui-avatars.com/api/?name=${encodeURIComponent(userInfo.login || 'User')}&background=random`"
:alt="userInfo.login || 'User'"
/>
</div>
Expand All @@ -76,19 +31,23 @@ const handleLogout = () => {
</svg>
</label>
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-52">
<li><RouterLink to="/dashboard" class="flex items-center gap-2">
<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="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
管理控制台
</RouterLink></li>
<li><button @click="handleLogout" class="flex items-center gap-2 text-error">
<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="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
退出登录
</button></li>
<li>
<RouterLink to="/dashboard" class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
控制台
</RouterLink>
</li>
<li>
<a @click="handleLogout" class="flex items-center gap-2 text-error! hover:text-error! cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
退出登录
</a>
</li>
</ul>
</div>
</div>
Expand All @@ -106,7 +65,7 @@ const handleLogout = () => {
<main class="flex-1">
<RouterView />
</main>
<footer class="footer footer-center bg-base-200 text-base-content p-4 border-t">
<footer class="footer footer-center bg-base-200 text-base-content p-4 border-t rounded-t-xl">
<div>
<small>© {{ year }} EasyTierMC Uptime</small>
</div>
Expand Down
129 changes: 129 additions & 0 deletions src/components/admin/ApiKeysTab.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { listAdminApiKeys, deleteAdminApiKey, updateApiKeyType, type ApiKeyData } from '@/utils/request/api'
import { formatDate, useConfirm } from '@/composables'
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'
import EmptyState from '@/components/common/EmptyState.vue'
import Pagination from '@/components/common/Pagination.vue'

const { confirmAction } = useConfirm()

const loading = ref(false)
const apiKeys = ref<ApiKeyData[]>([])
const total = ref(0)
const page = ref(0)
const pageSize = ref(20)
const typeFilter = ref<'active' | 'pending' | undefined>(undefined)

const totalPages = computed(() => Math.ceil(total.value / pageSize.value))

async function fetchApiKeys() {
loading.value = true
try {
const data = await listAdminApiKeys(pageSize.value, page.value * pageSize.value, typeFilter.value)
apiKeys.value = data.items || []
total.value = data.total || 0
} finally {
loading.value = false
}
}

async function handleDelete(id: number) {
if (!confirmAction('确定删除此 API Key?')) return
await deleteAdminApiKey(id)
fetchApiKeys()
}

async function handleUpdateType(id: number, type: 'active' | 'pending') {
await updateApiKeyType(id, type)
fetchApiKeys()
}

watch([page, typeFilter], fetchApiKeys)
onMounted(fetchApiKeys)
</script>

<template>
<div class="card bg-base-100 shadow flex-1 flex flex-col">
<div class="card-body p-4 flex-1 flex flex-col overflow-hidden">
<div class="flex justify-between items-center mb-3 shrink-0">
<h2 class="card-title text-base">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" 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 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg>
API Key 列表
</h2>
<select v-model="typeFilter" class="select select-sm select-bordered w-32">
<option :value="undefined">全部状态</option>
<option value="active">已激活</option>
<option value="pending">待审核</option>
</select>
</div>

<div class="flex-1 overflow-auto">
<table class="table table-sm table-pin-rows">
<thead>
<tr>
<th>ID</th>
<th>名称</th>
<th>描述</th>
<th>User-Agent</th>
<th>提供者</th>
<th>状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-if="loading">
<td colspan="8" class="text-center py-8"><LoadingSpinner /></td>
</tr>
<EmptyState v-else-if="apiKeys.length === 0" :colspan="8" message="暂无 API Key" />
<tr v-else v-for="key in apiKeys" :key="key.id">
<td class="font-mono text-xs">{{ key.id }}</td>
<td class="font-medium">{{ key.name }}</td>
<td class="text-sm max-w-32 truncate">{{ key.description || '-' }}</td>
<td>
<code class="bg-base-200 px-1.5 py-0.5 rounded text-xs">{{ key.userAgent || '-' }}</code>
</td>
<td>
<div v-if="key.provider" class="flex items-center gap-1">
<div class="avatar">
<div class="w-6 h-6 rounded-full">
<img :src="`https://avatars.githubusercontent.com/u/${key.provider.githubId}`" />
</div>
</div>
<span class="text-sm">{{ key.provider.username }}</span>
</div>
<span v-else class="text-base-content/50">-</span>
</td>
<td>
<div class="badge" :class="key.type === 'active' ? 'badge-success' : 'badge-warning'">
{{ key.type === 'active' ? '已激活' : '待审核' }}
</div>
</td>
<td class="text-sm">{{ formatDate(key.createdAt) }}</td>
<td>
<div class="flex gap-1">
<button
v-if="key.type === 'pending'"
class="btn btn-xs btn-ghost text-success"
@click="handleUpdateType(key.id, 'active')"
>激活</button>
<button
v-else
class="btn btn-xs btn-ghost text-warning"
@click="handleUpdateType(key.id, 'pending')"
>禁用</button>
<button class="btn btn-xs btn-ghost text-error" @click="handleDelete(key.id)">删除</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>

<Pagination :current-page="page" :total-pages="totalPages" @update:current-page="page = $event" />
</div>
</div>
</template>
Loading