feat: add url prefix path

This commit is contained in:
engigu
2026-01-13 18:18:50 +08:00
parent b07e742550
commit ff6faa229c
15 changed files with 275 additions and 53 deletions
+15 -12
View File
@@ -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 }) =>
+38 -5
View File
@@ -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);
+3 -1
View File
@@ -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)
+1 -1
View File
@@ -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,
)
"
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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>
+4 -1
View File
@@ -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',
+9
View File
@@ -0,0 +1,9 @@
// 扩展 Window 接口,添加后端注入的全局变量
declare global {
interface Window {
__BASE_URL__?: string
__API_VERSION__?: string
}
}
export {}
+31 -22
View File
@@ -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>