| <!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>
|
|
|