Add admin UI
Some checks failed
Some checks failed
This commit is contained in:
24
frontend/admin/.gitignore
vendored
Normal file
24
frontend/admin/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
13
frontend/admin/index.html
Normal file
13
frontend/admin/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/admin/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>LLM Gateway Admin</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
1698
frontend/admin/package-lock.json
generated
Normal file
1698
frontend/admin/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
frontend/admin/package.json
Normal file
22
frontend/admin/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "llm-gateway-admin",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.2.0",
|
||||
"axios": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"typescript": "^5.3.0",
|
||||
"vite": "^5.0.0",
|
||||
"vue-tsc": "^1.8.0"
|
||||
}
|
||||
}
|
||||
1
frontend/admin/public/vite.svg
Normal file
1
frontend/admin/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
26
frontend/admin/src/App.vue
Normal file
26
frontend/admin/src/App.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
}
|
||||
</style>
|
||||
51
frontend/admin/src/api/client.ts
Normal file
51
frontend/admin/src/api/client.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import axios, { AxiosInstance } from 'axios'
|
||||
import type { APIResponse } from '../types/api'
|
||||
|
||||
class APIClient {
|
||||
private client: AxiosInstance
|
||||
|
||||
constructor() {
|
||||
this.client = axios.create({
|
||||
baseURL: '/admin/api/v1',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
// Request interceptor for auth
|
||||
this.client.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('auth_token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
// Response interceptor for error handling
|
||||
this.client.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
console.error('API Error:', error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async get<T>(url: string): Promise<T> {
|
||||
const response = await this.client.get<APIResponse<T>>(url)
|
||||
if (response.data.success && response.data.data) {
|
||||
return response.data.data
|
||||
}
|
||||
throw new Error(response.data.error?.message || 'Unknown error')
|
||||
}
|
||||
|
||||
async post<T>(url: string, data: any): Promise<T> {
|
||||
const response = await this.client.post<APIResponse<T>>(url, data)
|
||||
if (response.data.success && response.data.data) {
|
||||
return response.data.data
|
||||
}
|
||||
throw new Error(response.data.error?.message || 'Unknown error')
|
||||
}
|
||||
}
|
||||
|
||||
export const apiClient = new APIClient()
|
||||
8
frontend/admin/src/api/config.ts
Normal file
8
frontend/admin/src/api/config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { apiClient } from './client'
|
||||
import type { ConfigResponse } from '../types/api'
|
||||
|
||||
export const configAPI = {
|
||||
async getConfig(): Promise<ConfigResponse> {
|
||||
return apiClient.get<ConfigResponse>('/config')
|
||||
},
|
||||
}
|
||||
8
frontend/admin/src/api/providers.ts
Normal file
8
frontend/admin/src/api/providers.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { apiClient } from './client'
|
||||
import type { ProviderInfo } from '../types/api'
|
||||
|
||||
export const providersAPI = {
|
||||
async getProviders(): Promise<ProviderInfo[]> {
|
||||
return apiClient.get<ProviderInfo[]>('/providers')
|
||||
},
|
||||
}
|
||||
12
frontend/admin/src/api/system.ts
Normal file
12
frontend/admin/src/api/system.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { apiClient } from './client'
|
||||
import type { SystemInfo, HealthCheckResponse } from '../types/api'
|
||||
|
||||
export const systemAPI = {
|
||||
async getInfo(): Promise<SystemInfo> {
|
||||
return apiClient.get<SystemInfo>('/system/info')
|
||||
},
|
||||
|
||||
async getHealth(): Promise<HealthCheckResponse> {
|
||||
return apiClient.get<HealthCheckResponse>('/system/health')
|
||||
},
|
||||
}
|
||||
7
frontend/admin/src/main.ts
Normal file
7
frontend/admin/src/main.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
15
frontend/admin/src/router.ts
Normal file
15
frontend/admin/src/router.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Dashboard from './views/Dashboard.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory('/admin/'),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'dashboard',
|
||||
component: Dashboard
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
export default router
|
||||
82
frontend/admin/src/types/api.ts
Normal file
82
frontend/admin/src/types/api.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
export interface APIResponse<T = any> {
|
||||
success: boolean
|
||||
data?: T
|
||||
error?: APIError
|
||||
}
|
||||
|
||||
export interface APIError {
|
||||
code: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface SystemInfo {
|
||||
version: string
|
||||
build_time: string
|
||||
git_commit: string
|
||||
go_version: string
|
||||
platform: string
|
||||
uptime: string
|
||||
}
|
||||
|
||||
export interface HealthCheck {
|
||||
status: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
export interface HealthCheckResponse {
|
||||
status: string
|
||||
timestamp: string
|
||||
checks: Record<string, HealthCheck>
|
||||
}
|
||||
|
||||
export interface SanitizedProvider {
|
||||
type: string
|
||||
api_key: string
|
||||
endpoint?: string
|
||||
api_version?: string
|
||||
project?: string
|
||||
location?: string
|
||||
}
|
||||
|
||||
export interface ModelEntry {
|
||||
name: string
|
||||
provider: string
|
||||
provider_model_id?: string
|
||||
}
|
||||
|
||||
export interface ConfigResponse {
|
||||
server: {
|
||||
address: string
|
||||
max_request_body_size: number
|
||||
}
|
||||
providers: Record<string, SanitizedProvider>
|
||||
models: ModelEntry[]
|
||||
auth: {
|
||||
enabled: boolean
|
||||
issuer: string
|
||||
audience: string
|
||||
}
|
||||
conversations: {
|
||||
store: string
|
||||
ttl: string
|
||||
dsn: string
|
||||
driver: string
|
||||
}
|
||||
logging: {
|
||||
format: string
|
||||
level: string
|
||||
}
|
||||
rate_limit: {
|
||||
enabled: boolean
|
||||
requests_per_second: number
|
||||
burst: number
|
||||
}
|
||||
observability: any
|
||||
}
|
||||
|
||||
export interface ProviderInfo {
|
||||
name: string
|
||||
type: string
|
||||
models: string[]
|
||||
status: string
|
||||
}
|
||||
385
frontend/admin/src/views/Dashboard.vue
Normal file
385
frontend/admin/src/views/Dashboard.vue
Normal file
@@ -0,0 +1,385 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<header class="header">
|
||||
<h1>LLM Gateway Admin</h1>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div v-if="loading" class="loading">Loading...</div>
|
||||
<div v-else-if="error" class="error">{{ error }}</div>
|
||||
<div v-else class="grid">
|
||||
<!-- System Info Card -->
|
||||
<div class="card">
|
||||
<h2>System Information</h2>
|
||||
<div class="info-grid" v-if="systemInfo">
|
||||
<div class="info-item">
|
||||
<span class="label">Version:</span>
|
||||
<span class="value">{{ systemInfo.version }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">Platform:</span>
|
||||
<span class="value">{{ systemInfo.platform }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">Go Version:</span>
|
||||
<span class="value">{{ systemInfo.go_version }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">Uptime:</span>
|
||||
<span class="value">{{ systemInfo.uptime }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">Build Time:</span>
|
||||
<span class="value">{{ systemInfo.build_time }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">Git Commit:</span>
|
||||
<span class="value code">{{ systemInfo.git_commit }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Health Status Card -->
|
||||
<div class="card">
|
||||
<h2>Health Status</h2>
|
||||
<div v-if="health">
|
||||
<div class="health-overall">
|
||||
<span class="label">Overall Status:</span>
|
||||
<span :class="['badge', health.status]">{{ health.status }}</span>
|
||||
</div>
|
||||
<div class="health-checks">
|
||||
<div v-for="(check, name) in health.checks" :key="name" class="health-check">
|
||||
<span class="check-name">{{ name }}:</span>
|
||||
<span :class="['badge', check.status]">{{ check.status }}</span>
|
||||
<span v-if="check.message" class="check-message">{{ check.message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Providers Card -->
|
||||
<div class="card full-width">
|
||||
<h2>Providers</h2>
|
||||
<div v-if="providers && providers.length > 0" class="providers-grid">
|
||||
<div v-for="provider in providers" :key="provider.name" class="provider-card">
|
||||
<div class="provider-header">
|
||||
<h3>{{ provider.name }}</h3>
|
||||
<span :class="['badge', provider.status]">{{ provider.status }}</span>
|
||||
</div>
|
||||
<div class="provider-info">
|
||||
<div class="info-item">
|
||||
<span class="label">Type:</span>
|
||||
<span class="value">{{ provider.type }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">Models:</span>
|
||||
<span class="value">{{ provider.models.length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="provider.models.length > 0" class="models-list">
|
||||
<span v-for="model in provider.models" :key="model" class="model-tag">
|
||||
{{ model }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-state">No providers configured</div>
|
||||
</div>
|
||||
|
||||
<!-- Config Card -->
|
||||
<div class="card full-width collapsible">
|
||||
<div class="card-header" @click="configExpanded = !configExpanded">
|
||||
<h2>Configuration</h2>
|
||||
<span class="expand-icon">{{ configExpanded ? '−' : '+' }}</span>
|
||||
</div>
|
||||
<div v-if="configExpanded && config" class="config-content">
|
||||
<pre class="config-json">{{ JSON.stringify(config, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { systemAPI } from '../api/system'
|
||||
import { configAPI } from '../api/config'
|
||||
import { providersAPI } from '../api/providers'
|
||||
import type { SystemInfo, HealthCheckResponse, ConfigResponse, ProviderInfo } from '../types/api'
|
||||
|
||||
const loading = ref(true)
|
||||
const error = ref<string | null>(null)
|
||||
const systemInfo = ref<SystemInfo | null>(null)
|
||||
const health = ref<HealthCheckResponse | null>(null)
|
||||
const config = ref<ConfigResponse | null>(null)
|
||||
const providers = ref<ProviderInfo[] | null>(null)
|
||||
const configExpanded = ref(false)
|
||||
|
||||
let refreshInterval: number | null = null
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
const [info, healthData, configData, providersData] = await Promise.all([
|
||||
systemAPI.getInfo(),
|
||||
systemAPI.getHealth(),
|
||||
configAPI.getConfig(),
|
||||
providersAPI.getProviders(),
|
||||
])
|
||||
|
||||
systemInfo.value = info
|
||||
health.value = healthData
|
||||
config.value = configData
|
||||
providers.value = providersData
|
||||
} catch (err: any) {
|
||||
error.value = err.message || 'Failed to load data'
|
||||
console.error('Error loading data:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
// Auto-refresh every 30 seconds
|
||||
refreshInterval = window.setInterval(loadData, 30000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.error {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #e53e3e;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.info-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: 500;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.code {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.badge.healthy {
|
||||
background-color: #c6f6d5;
|
||||
color: #22543d;
|
||||
}
|
||||
|
||||
.badge.unhealthy {
|
||||
background-color: #fed7d7;
|
||||
color: #742a2a;
|
||||
}
|
||||
|
||||
.badge.active {
|
||||
background-color: #bee3f8;
|
||||
color: #2c5282;
|
||||
}
|
||||
|
||||
.health-overall {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background-color: #f7fafc;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.health-checks {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.health-check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.check-name {
|
||||
font-weight: 500;
|
||||
color: #4a5568;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.check-message {
|
||||
color: #718096;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.providers-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.provider-card {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
background-color: #f7fafc;
|
||||
}
|
||||
|
||||
.provider-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.provider-header h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.provider-info {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.models-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.model-tag {
|
||||
background-color: #edf2f7;
|
||||
color: #4a5568;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.collapsible .card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.config-content {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.config-json {
|
||||
background-color: #2d3748;
|
||||
color: #e2e8f0;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
25
frontend/admin/tsconfig.json
Normal file
25
frontend/admin/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
10
frontend/admin/tsconfig.node.json
Normal file
10
frontend/admin/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
20
frontend/admin/vite.config.ts
Normal file
20
frontend/admin/vite.config.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
base: '/admin/',
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/admin/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user