feat: add url prefix path
This commit is contained in:
+15
-12
@@ -1,4 +1,7 @@
|
||||
const BASE_URL = '/api'
|
||||
// 获取 base URL(从后端注入的全局变量)
|
||||
const BASE_URL = (window as any).__BASE_URL__ || ''
|
||||
const API_VERSION = (window as any).__API_VERSION__ || '/api/v1'
|
||||
const API_BASE_URL = BASE_URL + API_VERSION
|
||||
|
||||
interface ApiResponse<T> {
|
||||
code: number
|
||||
@@ -7,7 +10,7 @@ interface ApiResponse<T> {
|
||||
}
|
||||
|
||||
async function request<T>(url: string, options?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${BASE_URL}${url}`, {
|
||||
const res = await fetch(`${API_BASE_URL}${url}`, {
|
||||
...options,
|
||||
credentials: 'include', // 携带 Cookie
|
||||
headers: {
|
||||
@@ -20,7 +23,7 @@ async function request<T>(url: string, options?: RequestInit): Promise<T> {
|
||||
|
||||
if (json.code === 401) {
|
||||
// 未登录或登录过期,跳转到登录页
|
||||
window.location.href = '/login'
|
||||
window.location.href = BASE_URL + '/login'
|
||||
throw new Error(json.msg || '请先登录')
|
||||
}
|
||||
|
||||
@@ -34,7 +37,7 @@ async function request<T>(url: string, options?: RequestInit): Promise<T> {
|
||||
// 检查登录状态(不触发自动跳转)
|
||||
export async function checkAuth(): Promise<boolean> {
|
||||
try {
|
||||
const res = await fetch(`${BASE_URL}/auth/me`, {
|
||||
const res = await fetch(`${API_BASE_URL}/auth/me`, {
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
@@ -128,18 +131,18 @@ export const api = {
|
||||
},
|
||||
createBackup: () => request('/settings/backup', { method: 'POST' }),
|
||||
getBackupStatus: () => request<{ has_backup: boolean; backup_time: string }>('/settings/backup/status'),
|
||||
downloadBackup: () => `${BASE_URL}/settings/backup/download`,
|
||||
downloadBackup: () => `${API_BASE_URL}/settings/backup/download`,
|
||||
restoreBackup: async (file: File) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
const res = await fetch(`${BASE_URL}/settings/restore`, {
|
||||
const res = await fetch(`${API_BASE_URL}/settings/restore`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: formData
|
||||
})
|
||||
const json: ApiResponse<null> = await res.json()
|
||||
if (json.code === 401) {
|
||||
window.location.href = '/login'
|
||||
window.location.href = BASE_URL + '/login'
|
||||
throw new Error('请先登录')
|
||||
}
|
||||
if (json.code !== 200) throw new Error(json.msg || '恢复失败')
|
||||
@@ -157,14 +160,14 @@ export const api = {
|
||||
formData.append('file', file)
|
||||
if (targetPath) formData.append('path', targetPath)
|
||||
|
||||
const res = await fetch(`${BASE_URL}/files/upload`, {
|
||||
const res = await fetch(`${API_BASE_URL}/files/upload`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: formData
|
||||
})
|
||||
const json: ApiResponse<null> = await res.json()
|
||||
if (json.code === 401) {
|
||||
window.location.href = '/login'
|
||||
window.location.href = BASE_URL + '/login'
|
||||
throw new Error('请先登录')
|
||||
}
|
||||
if (json.code !== 200) throw new Error(json.msg || '上传失败')
|
||||
@@ -180,14 +183,14 @@ export const api = {
|
||||
}
|
||||
if (targetPath) formData.append('path', targetPath)
|
||||
|
||||
const res = await fetch(`${BASE_URL}/files/uploadfiles`, {
|
||||
const res = await fetch(`${API_BASE_URL}/files/uploadfiles`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: formData
|
||||
})
|
||||
const json: ApiResponse<null> = await res.json()
|
||||
if (json.code === 401) {
|
||||
window.location.href = '/login'
|
||||
window.location.href = BASE_URL + '/login'
|
||||
throw new Error('请先登录')
|
||||
}
|
||||
if (json.code !== 200) throw new Error(json.msg || '上传失败')
|
||||
@@ -215,7 +218,7 @@ export const api = {
|
||||
request('/agents/' + id, { method: 'PUT', body: JSON.stringify(data) }),
|
||||
delete: (id: number) => request('/agents/' + id, { method: 'DELETE' }),
|
||||
forceUpdate: (id: number) => request('/agents/' + id + '/update', { method: 'POST' }),
|
||||
downloadUrl: (os: string, arch: string) => `${BASE_URL}/agent/download?os=${os}&arch=${arch}`,
|
||||
downloadUrl: (os: string, arch: string) => `${API_BASE_URL}/agent/download?os=${os}&arch=${arch}`,
|
||||
// 令牌管理
|
||||
listTokens: () => request<AgentToken[]>('/agents/tokens'),
|
||||
createToken: (data: { remark?: string; max_uses?: number; expires_at?: string }) =>
|
||||
|
||||
@@ -134,13 +134,25 @@
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
@apply outline-ring/50;
|
||||
}
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
border-color: transparent;
|
||||
}
|
||||
.border,
|
||||
[class*="border-"] {
|
||||
border-color: var(--border);
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground antialiased;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,20 +167,20 @@ body {
|
||||
}
|
||||
|
||||
/* 卡片增强 - 添加微妙阴影 */
|
||||
[class*="card"] {
|
||||
[data-slot="card"] {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.05), 0 1px 2px -1px rgb(0 0 0 / 0.05);
|
||||
}
|
||||
|
||||
[class*="card"]:hover {
|
||||
[data-slot="card"]:hover {
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.08), 0 2px 4px -2px rgb(0 0 0 / 0.08);
|
||||
}
|
||||
|
||||
.dark [class*="card"] {
|
||||
.dark [data-slot="card"] {
|
||||
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.3), 0 1px 2px -1px rgb(0 0 0 / 0.3);
|
||||
}
|
||||
|
||||
.dark [class*="card"]:hover {
|
||||
.dark [data-slot="card"]:hover {
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.4);
|
||||
}
|
||||
|
||||
@@ -230,6 +242,27 @@ pre code {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Windows 小字体优化 */
|
||||
.text-xs, .text-sm, [class*="text-xs"], [class*="text-sm"] {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
/* 表格和列表中的小字体优化 */
|
||||
table, [role="table"] {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* 命令和路径文本优化 */
|
||||
code, pre, [class*="font-mono"] {
|
||||
-webkit-font-smoothing: auto;
|
||||
-moz-osx-font-smoothing: auto;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* 徽章和标签增强 */
|
||||
[class*="badge"], [class*="tag"] {
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
@@ -128,7 +128,9 @@ function initTerminal(forceConnect = false) {
|
||||
|
||||
function connectWebSocket() {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const wsUrl = `${protocol}//${window.location.host}/api/terminal/ws`
|
||||
const baseUrl = (window as any).__BASE_URL__ || ''
|
||||
const apiVersion = (window as any).__API_VERSION__ || '/api/v1'
|
||||
const wsUrl = `${protocol}//${window.location.host}${baseUrl}${apiVersion}/terminal/ws`
|
||||
|
||||
ws = new WebSocket(wsUrl)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ const props = defineProps<{
|
||||
data-slot="card"
|
||||
:class="
|
||||
cn(
|
||||
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
|
||||
'bg-card text-card-foreground flex flex-col rounded-xl border border-border py-6 shadow-sm',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
|
||||
@@ -10,7 +10,7 @@ const props = defineProps<{
|
||||
<template>
|
||||
<div
|
||||
data-slot="card-content"
|
||||
:class="cn('px-6', props.class)"
|
||||
:class="cn('px-6 border-0', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ const props = defineProps<{
|
||||
<template>
|
||||
<div
|
||||
data-slot="card-header"
|
||||
:class="cn('@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6', props.class)"
|
||||
:class="cn('@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6 [&]:border-none [&]:outline-none', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ const props = defineProps<{
|
||||
<template>
|
||||
<h3
|
||||
data-slot="card-title"
|
||||
:class="cn('leading-none font-semibold', props.class)"
|
||||
:class="cn('leading-none font-semibold border-0', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</h3>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { checkAuth } from '@/api'
|
||||
|
||||
// 获取 base URL(从后端注入的全局变量)
|
||||
const BASE_URL = (window as any).__BASE_URL__ || ''
|
||||
|
||||
// 缓存认证状态,避免每次路由跳转都请求
|
||||
let authChecked = false
|
||||
let isAuth = false
|
||||
@@ -21,7 +24,7 @@ export function resetAuthCache() {
|
||||
}
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
history: createWebHistory(BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/login',
|
||||
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
// 扩展 Window 接口,添加后端注入的全局变量
|
||||
declare global {
|
||||
interface Window {
|
||||
__BASE_URL__?: string
|
||||
__API_VERSION__?: string
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, computed, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ListTodo, Variable, Clock, Play, ScrollText } from 'lucide-vue-next'
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
|
||||
@@ -77,6 +77,8 @@ async function reloadCharts() {
|
||||
pieChart = null
|
||||
}
|
||||
|
||||
chartsLoaded.value = false
|
||||
|
||||
// 重新获取数据
|
||||
const [sendStatsData, taskStatsData] = await Promise.all([
|
||||
api.dashboard.sendStats(chartDays.value),
|
||||
@@ -85,22 +87,31 @@ async function reloadCharts() {
|
||||
sendStats.value = sendStatsData
|
||||
taskStats.value = taskStatsData
|
||||
|
||||
setTimeout(() => {
|
||||
// 等待 Vue 更新 DOM
|
||||
await nextTick()
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
|
||||
// 检查容器是否存在再渲染
|
||||
const statsChart = document.querySelector("#stats-chart")
|
||||
const pieChartEl = document.querySelector("#pie-chart")
|
||||
|
||||
if (statsChart && pieChartEl && statsChart.parentElement && pieChartEl.parentElement) {
|
||||
renderLineChart()
|
||||
renderPieChart()
|
||||
await nextTick()
|
||||
chartsLoaded.value = true
|
||||
}, 50)
|
||||
}
|
||||
}
|
||||
|
||||
const renderLineChart = () => {
|
||||
const container = document.querySelector("#stats-chart")
|
||||
if (!container || !container.parentElement) return
|
||||
|
||||
if (lineChart) {
|
||||
lineChart.destroy()
|
||||
lineChart = null
|
||||
}
|
||||
|
||||
const container = document.querySelector("#stats-chart")
|
||||
if (!container) return
|
||||
|
||||
// 清空容器
|
||||
container.innerHTML = ''
|
||||
|
||||
@@ -234,14 +245,14 @@ const renderLineChart = () => {
|
||||
const renderPieChart = () => {
|
||||
if (taskStats.value.length === 0) return
|
||||
|
||||
const container = document.querySelector("#pie-chart")
|
||||
if (!container || !container.parentElement) return
|
||||
|
||||
if (pieChart) {
|
||||
pieChart.destroy()
|
||||
pieChart = null
|
||||
}
|
||||
|
||||
const container = document.querySelector("#pie-chart")
|
||||
if (!container) return
|
||||
|
||||
// 清空容器
|
||||
container.innerHTML = ''
|
||||
|
||||
@@ -416,11 +427,10 @@ onUnmounted(() => {
|
||||
<CardTitle class="text-base sm:text-lg">执行统计</CardTitle>
|
||||
<CardDescription class="text-xs sm:text-sm">最近{{ chartDays }}天任务执行情况</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="pb-8">
|
||||
<div id="stats-chart" class="w-full h-[300px] sm:h-[300px]">
|
||||
<div v-if="!chartsLoaded" class="h-full flex items-center justify-center text-muted-foreground text-sm">
|
||||
加载中...
|
||||
</div>
|
||||
<CardContent class="pb-8 relative">
|
||||
<div id="stats-chart" class="w-full h-[300px] sm:h-[300px]"></div>
|
||||
<div v-if="!chartsLoaded" class="absolute inset-0 flex items-center justify-center text-muted-foreground text-sm bg-card">
|
||||
加载中...
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -430,14 +440,13 @@ onUnmounted(() => {
|
||||
<CardTitle class="text-base sm:text-lg">任务占比</CardTitle>
|
||||
<CardDescription class="text-xs sm:text-sm">最近{{ chartDays }}天任务执行分布</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="pb-8">
|
||||
<div id="pie-chart" class="w-full h-[300px] sm:h-[300px]">
|
||||
<div v-if="!chartsLoaded" class="h-full flex items-center justify-center text-muted-foreground text-sm">
|
||||
加载中...
|
||||
</div>
|
||||
<div v-else-if="taskStats.length === 0" class="h-full flex items-center justify-center text-muted-foreground text-sm">
|
||||
暂无数据
|
||||
</div>
|
||||
<CardContent class="pb-8 relative">
|
||||
<div id="pie-chart" class="w-full h-[300px] sm:h-[300px]"></div>
|
||||
<div v-if="!chartsLoaded" class="absolute inset-0 flex items-center justify-center text-muted-foreground text-sm bg-card">
|
||||
加载中...
|
||||
</div>
|
||||
<div v-else-if="taskStats.length === 0" class="absolute inset-0 flex items-center justify-center text-muted-foreground text-sm bg-card">
|
||||
暂无数据
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
+3
-1
@@ -18,5 +18,7 @@ export default defineConfig({
|
||||
ws: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// 支持通过环境变量设置 base URL(开发时测试用)
|
||||
base: process.env.VITE_BASE_URL || '/'
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user