|
<!DOCTYPE html>
|
|
<html lang="zh">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>API密钥管理系统</title>
|
|
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
|
|
<script>
|
|
tailwind.config = {
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
primary: {
|
|
50: '#f0f9ff',
|
|
100: '#e0f2fe',
|
|
200: '#bae6fd',
|
|
300: '#7dd3fc',
|
|
400: '#38bdf8',
|
|
500: '#0ea5e9',
|
|
600: '#0284c7',
|
|
700: '#0369a1',
|
|
800: '#075985',
|
|
900: '#0c4a6e',
|
|
950: '#082f49',
|
|
},
|
|
},
|
|
animation: {
|
|
'fade-in': 'fadeIn 0.3s ease-in-out',
|
|
'fade-out': 'fadeOut 0.3s ease-in-out',
|
|
'slide-down': 'slideDown 0.3s ease-in-out',
|
|
'slide-up': 'slideUp 0.3s ease-in-out',
|
|
'expand': 'expand 0.3s ease-in-out',
|
|
'collapse': 'collapse 0.3s ease-in-out',
|
|
},
|
|
keyframes: {
|
|
fadeIn: {
|
|
'0%': { opacity: '0' },
|
|
'100%': { opacity: '1' },
|
|
},
|
|
fadeOut: {
|
|
'0%': { opacity: '1' },
|
|
'100%': { opacity: '0' },
|
|
},
|
|
slideDown: {
|
|
'0%': { maxHeight: '0', opacity: '0', transform: 'translateY(-10px)' },
|
|
'100%': { maxHeight: '1000px', opacity: '1', transform: 'translateY(0)' }
|
|
},
|
|
slideUp: {
|
|
'0%': { maxHeight: '1000px', opacity: '1', transform: 'translateY(0)' },
|
|
'100%': { maxHeight: '0', opacity: '0', transform: 'translateY(-10px)' }
|
|
},
|
|
expand: {
|
|
'0%': { transform: 'scaleY(0)', transformOrigin: 'top', opacity: '0' },
|
|
'100%': { transform: 'scaleY(1)', transformOrigin: 'top', opacity: '1' }
|
|
},
|
|
collapse: {
|
|
'0%': { transform: 'scaleY(1)', transformOrigin: 'top', opacity: '1' },
|
|
'100%': { transform: 'scaleY(0)', transformOrigin: 'top', opacity: '0' }
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
</script>
|
|
|
|
<script defer src="https://unpkg.com/alpinejs@3.14.8/dist/cdn.min.js"></script>
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.11/clipboard.min.js"></script>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
|
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
|
<link rel="icon" href="{{ url_for('static', filename='img/favicon.ico') }}">
|
|
{% block head %}{% endblock %}
|
|
</head>
|
|
<body class="bg-gray-50 text-gray-900 min-h-screen" x-data="apiKeyManager()">
|
|
|
|
|
|
<header class="bg-white shadow">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
|
|
<div class="grid grid-cols-3 items-center">
|
|
|
|
<div class="flex items-center justify-start">
|
|
<h1 class="text-2xl font-bold text-primary-700">
|
|
API密钥管理系统
|
|
</h1>
|
|
</div>
|
|
|
|
|
|
<div class="flex justify-center">
|
|
{% block header_tabs %}{% endblock %}
|
|
</div>
|
|
|
|
|
|
<div class="flex items-center justify-end">
|
|
<a href="{{ url_for('web.logout') }}" class="flex items-center px-3 py-2 text-sm font-medium text-primary-700 hover:text-primary-900 hover:bg-primary-50 rounded-md transition-colors">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" 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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
|
|
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
|
|
|
|
<div id="backToTop" class="back-to-top" title="回到顶部">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18" />
|
|
</svg>
|
|
</div>
|
|
|
|
|
|
<div x-data="notificationSystem()"
|
|
x-init="init()"
|
|
@show-notification.window="add($event.detail.message, $event.detail.type)"
|
|
class="fixed bottom-4 right-4 z-50 space-y-4"
|
|
x-cloak>
|
|
<template x-for="notification in notifications" :key="notification.id">
|
|
<div x-show="notification.visible"
|
|
x-transition:enter="transition ease-out duration-300"
|
|
x-transition:enter-start="transform translate-y-2 opacity-0"
|
|
x-transition:enter-end="transform translate-y-0 opacity-100"
|
|
x-transition:leave="transition ease-in duration-200"
|
|
x-transition:leave-start="transform translate-y-0 opacity-100"
|
|
x-transition:leave-end="transform translate-y-2 opacity-0"
|
|
:class="{
|
|
'bg-green-50 text-green-800 border-green-400': notification.type === 'success',
|
|
'bg-red-50 text-red-800 border-red-400': notification.type === 'error',
|
|
'bg-blue-50 text-blue-800 border-blue-400': notification.type === 'info'
|
|
}"
|
|
class="p-4 border-l-4 shadow-md rounded-r-lg flex justify-between items-center min-w-[300px]">
|
|
<div x-text="notification.message"></div>
|
|
<button @click="remove(notification.id)" class="ml-4 text-gray-500 hover:text-gray-700">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
|
|
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
|
{% block scripts %}{% endblock %}
|
|
|
|
<script>
|
|
|
|
function notificationSystem() {
|
|
return {
|
|
notifications: [],
|
|
nextId: 1,
|
|
init() {
|
|
|
|
},
|
|
add(message, type = 'info') {
|
|
const id = this.nextId++;
|
|
const notification = {
|
|
id,
|
|
message,
|
|
type,
|
|
visible: true
|
|
};
|
|
this.notifications.push(notification);
|
|
|
|
|
|
setTimeout(() => {
|
|
this.remove(id);
|
|
}, 3000);
|
|
},
|
|
remove(id) {
|
|
const index = this.notifications.findIndex(n => n.id === id);
|
|
if (index !== -1) {
|
|
|
|
this.notifications[index].visible = false;
|
|
|
|
|
|
setTimeout(() => {
|
|
this.notifications = this.notifications.filter(n => n.id !== id);
|
|
}, 300);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|