feat: JSON 配置、质量分与仪表盘,及设置与爬取流程
- 后端改为 config/app.json;pytest 使用 config/app.test.json 与 set_config_file,不再依赖环境变量;移除 pydantic-settings。 - 前端 API/WebSocket 由 config/webui.json 经 Vite define 注入。 - 代理分数按延迟与随机取用次数计算,新增 use_count 与 proxy_scoring;保存设置时同步调度器启停。 - 仪表盘双饼图(可用/待验证协议);设置页去掉调度器启停按钮并移动立即验证;爬取全部结束后自动提交全量验证。 - 删除 script/settings_maintain.py(此前已标记删除)。 Made-with: Cursor
This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
import axios from 'axios'
|
||||
import { showError } from '../utils/message'
|
||||
|
||||
/** @type {string} 默认 API 基础 URL */
|
||||
export const DEFAULT_API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:18080'
|
||||
/** @type {string} 由项目根目录 config/webui.json 注入(见 vite.config.js) */
|
||||
export const DEFAULT_API_BASE_URL =
|
||||
typeof __WEBUI_API_BASE_URL__ !== 'undefined' ? __WEBUI_API_BASE_URL__ : 'http://127.0.0.1:18080'
|
||||
|
||||
/** @type {number} 请求超时时间(毫秒) */
|
||||
export const REQUEST_TIMEOUT = 120000
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || DEFAULT_API_BASE_URL,
|
||||
baseURL: DEFAULT_API_BASE_URL,
|
||||
timeout: REQUEST_TIMEOUT
|
||||
})
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<template>
|
||||
<el-card class="chart-card" shadow="hover">
|
||||
<el-card class="chart-card" :class="{ 'chart-card--compact': compact }" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">
|
||||
<el-icon class="header-icon"><PieChart /></el-icon>
|
||||
协议分布
|
||||
{{ titleText }}
|
||||
</span>
|
||||
<el-tooltip content="显示各协议类型的代理数量分布">
|
||||
<el-tooltip :content="helpText">
|
||||
<el-icon class="help-icon"><InfoFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="chartRef" class="chart-container" v-loading="!hasData">
|
||||
<el-empty v-if="!hasData" description="暂无数据" :image-size="80" />
|
||||
<div ref="chartRef" class="chart-container">
|
||||
<el-empty v-if="!hasData" :description="emptyText" :image-size="72" />
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
@@ -27,29 +27,73 @@ const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
/** available:仅已验证可用;pending:仅待验证池 */
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'available',
|
||||
validator: (v) => ['available', 'pending'].includes(v)
|
||||
},
|
||||
/** 并排展示时略压低高度 */
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const titleText = computed(() =>
|
||||
props.variant === 'pending' ? '待验证 · 协议分布' : '可用代理 · 协议分布'
|
||||
)
|
||||
|
||||
const helpText = computed(() =>
|
||||
props.variant === 'pending'
|
||||
? '仅统计 validated=0 的待验证代理,与各协议在队列中的占比'
|
||||
: '仅统计已验证且分数大于 0 的可用代理,不含待验证与低分条目'
|
||||
)
|
||||
|
||||
const emptyText = computed(() =>
|
||||
props.variant === 'pending' ? '暂无待验证代理' : '暂无可用代理'
|
||||
)
|
||||
|
||||
const chartRef = ref(null)
|
||||
let chartInstance = null
|
||||
let resizeTimer = null
|
||||
const cachedColors = ref(null)
|
||||
|
||||
// ==================== 计算属性 ====================
|
||||
const counts = computed(() => {
|
||||
const d = props.data || {}
|
||||
if (props.variant === 'pending') {
|
||||
return {
|
||||
http: d.pending_http_count || 0,
|
||||
https: d.pending_https_count || 0,
|
||||
socks4: d.pending_socks4_count || 0,
|
||||
socks5: d.pending_socks5_count || 0
|
||||
}
|
||||
}
|
||||
return {
|
||||
http: d.http_count || 0,
|
||||
https: d.https_count || 0,
|
||||
socks4: d.socks4_count || 0,
|
||||
socks5: d.socks5_count || 0
|
||||
}
|
||||
})
|
||||
|
||||
const hasData = computed(() => {
|
||||
const { http_count, https_count, socks4_count, socks5_count } = props.data
|
||||
return (http_count || 0) + (https_count || 0) + (socks4_count || 0) + (socks5_count || 0) > 0
|
||||
const c = counts.value
|
||||
return c.http + c.https + c.socks4 + c.socks5 > 0
|
||||
})
|
||||
|
||||
const chartData = computed(() => {
|
||||
if (!cachedColors.value) return []
|
||||
const colors = cachedColors.value
|
||||
const c = counts.value
|
||||
return [
|
||||
{ value: props.data.http_count || 0, name: 'HTTP', itemStyle: { color: colors.info } },
|
||||
{ value: props.data.https_count || 0, name: 'HTTPS', itemStyle: { color: colors.success } },
|
||||
{ value: props.data.socks4_count || 0, name: 'SOCKS4', itemStyle: { color: colors.primary } },
|
||||
{ value: props.data.socks5_count || 0, name: 'SOCKS5', itemStyle: { color: colors.warning } }
|
||||
].filter(item => item.value > 0)
|
||||
{ value: c.http, name: 'HTTP', itemStyle: { color: colors.info } },
|
||||
{ value: c.https, name: 'HTTPS', itemStyle: { color: colors.success } },
|
||||
{ value: c.socks4, name: 'SOCKS4', itemStyle: { color: colors.primary } },
|
||||
{ value: c.socks5, name: 'SOCKS5', itemStyle: { color: colors.warning } }
|
||||
].filter((item) => item.value > 0)
|
||||
})
|
||||
|
||||
const total = computed(() =>
|
||||
@@ -141,11 +185,16 @@ function getChartOption() {
|
||||
|
||||
function initChart() {
|
||||
if (!chartRef.value || !hasData.value) return
|
||||
|
||||
|
||||
loadColors()
|
||||
if (chartInstance) {
|
||||
updateChart()
|
||||
return
|
||||
}
|
||||
|
||||
chartInstance = echarts.init(chartRef.value)
|
||||
updateChart()
|
||||
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
}
|
||||
|
||||
@@ -172,13 +221,21 @@ function destroyChart() {
|
||||
}
|
||||
|
||||
// ==================== 监听 ====================
|
||||
watch(() => props.data, () => {
|
||||
if (!chartInstance && hasData.value) {
|
||||
initChart()
|
||||
} else {
|
||||
updateChart()
|
||||
}
|
||||
}, { deep: true })
|
||||
watch(
|
||||
() => [props.data, props.variant, props.compact],
|
||||
() => {
|
||||
if (!hasData.value) {
|
||||
destroyChart()
|
||||
return
|
||||
}
|
||||
if (!chartInstance) {
|
||||
initChart()
|
||||
} else {
|
||||
updateChart()
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// ==================== 生命周期 ====================
|
||||
onMounted(() => {
|
||||
@@ -200,6 +257,14 @@ onUnmounted(() => {
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.chart-card--compact {
|
||||
min-height: 340px;
|
||||
}
|
||||
|
||||
.chart-card--compact .chart-container {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.chart-card:hover {
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,16 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator: (value) =>
|
||||
['default', 'total', 'pending', 'available', 'new', 'score'].includes(value)
|
||||
[
|
||||
'default',
|
||||
'total',
|
||||
'pending',
|
||||
'available',
|
||||
'new',
|
||||
'score',
|
||||
'invalid',
|
||||
'latency'
|
||||
].includes(value)
|
||||
},
|
||||
/** 图标组件 */
|
||||
icon: {
|
||||
@@ -45,6 +54,9 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const displayValue = computed(() => {
|
||||
if (props.value === '—' || props.value === '-') {
|
||||
return props.value
|
||||
}
|
||||
const num = Number(props.value)
|
||||
if (!isNaN(num) && num > 9999) {
|
||||
return (num / 10000).toFixed(1) + 'w'
|
||||
@@ -95,6 +107,16 @@ const displayValue = computed(() => {
|
||||
filter: drop-shadow(0 0 8px rgba(146, 124, 255, 0.4));
|
||||
}
|
||||
|
||||
.stat-card.invalid .stat-icon {
|
||||
color: var(--danger, #f56c6c);
|
||||
filter: drop-shadow(0 0 8px rgba(245, 108, 108, 0.35));
|
||||
}
|
||||
|
||||
.stat-card.latency .stat-icon {
|
||||
color: var(--info);
|
||||
filter: drop-shadow(0 0 8px rgba(56, 189, 248, 0.35));
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -9,12 +9,16 @@ const INITIAL_DELAY_MS = 1000
|
||||
* @returns {string}
|
||||
*/
|
||||
export function resolveWebSocketStatsUrl() {
|
||||
const explicit = import.meta.env.VITE_WS_URL
|
||||
const explicit =
|
||||
typeof __WEBUI_WS_URL__ !== 'undefined' ? String(__WEBUI_WS_URL__).trim() : ''
|
||||
if (explicit) {
|
||||
const t = String(explicit).trim().replace(/\/$/, '')
|
||||
const t = explicit.replace(/\/$/, '')
|
||||
return t.endsWith('/api/ws') ? t : `${t}/api/ws`
|
||||
}
|
||||
const api = import.meta.env.VITE_API_BASE_URL || 'http://localhost:18080'
|
||||
const api =
|
||||
typeof __WEBUI_API_BASE_URL__ !== 'undefined'
|
||||
? __WEBUI_API_BASE_URL__
|
||||
: 'http://127.0.0.1:18080'
|
||||
const u = new URL(api)
|
||||
u.protocol = u.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
u.pathname = '/api/ws'
|
||||
|
||||
@@ -31,13 +31,32 @@
|
||||
type="score"
|
||||
:icon="StarFilled"
|
||||
:value="avgScore"
|
||||
label="平均分数"
|
||||
label="平均分数(可用)"
|
||||
/>
|
||||
<StatCard
|
||||
type="latency"
|
||||
:icon="Odometer"
|
||||
:value="latencyLabel"
|
||||
label="平均延迟(可用)"
|
||||
/>
|
||||
<StatCard
|
||||
type="invalid"
|
||||
:icon="WarningFilled"
|
||||
:value="stats.invalid_count || 0"
|
||||
label="低分待清理"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="20" class="charts-row">
|
||||
<el-col :xs="24" :lg="16">
|
||||
<ProtocolChart :data="stats" />
|
||||
<el-row :gutter="16" class="charts-inner">
|
||||
<el-col :xs="24" :md="12">
|
||||
<ProtocolChart :data="stats" variant="available" compact />
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="12">
|
||||
<ProtocolChart :data="stats" variant="pending" compact />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
<el-col :xs="24" :lg="8">
|
||||
<QuickActions
|
||||
@@ -67,17 +86,21 @@
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">HTTP 代理</span>
|
||||
<span class="status-label">HTTP(可用)</span>
|
||||
<span class="status-value">{{ stats.http_count || 0 }}</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">HTTPS 代理</span>
|
||||
<span class="status-label">HTTPS(可用)</span>
|
||||
<span class="status-value">{{ stats.https_count || 0 }}</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">SOCKS 代理</span>
|
||||
<span class="status-label">SOCKS(可用)</span>
|
||||
<span class="status-value">{{ (stats.socks4_count || 0) + (stats.socks5_count || 0) }}</span>
|
||||
</div>
|
||||
<div class="status-item" v-if="(stats.invalid_count || 0) > 0">
|
||||
<span class="status-label">低分(可清理)</span>
|
||||
<span class="status-value warn">{{ stats.invalid_count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
@@ -95,7 +118,9 @@ import {
|
||||
Timer,
|
||||
StarFilled,
|
||||
InfoFilled,
|
||||
Clock
|
||||
Clock,
|
||||
Odometer,
|
||||
WarningFilled
|
||||
} from '@element-plus/icons-vue'
|
||||
import { useProxyStore } from '../stores/proxy'
|
||||
import { formatNumber } from '../utils/format'
|
||||
@@ -113,6 +138,14 @@ const { start: startStatsWs } = useStatsWebSocket()
|
||||
const stats = computed(() => proxyStore.stats)
|
||||
const avgScore = computed(() => formatNumber(stats.value.avg_score || 0, 1))
|
||||
|
||||
const latencyLabel = computed(() => {
|
||||
const ms = stats.value.avg_response_ms
|
||||
if (ms == null || ms === '' || Number(ms) <= 0) {
|
||||
return '—'
|
||||
}
|
||||
return `${formatNumber(Number(ms), 1)} ms`
|
||||
})
|
||||
|
||||
async function refreshData() {
|
||||
await proxyStore.fetchStats()
|
||||
}
|
||||
@@ -170,6 +203,10 @@ onMounted(async () => {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.charts-inner {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.status-row {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@@ -214,6 +251,10 @@ onMounted(async () => {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.status-value.warn {
|
||||
color: var(--danger, #f56c6c);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.status-list {
|
||||
flex-direction: column;
|
||||
|
||||
@@ -359,7 +359,7 @@ async function handleCrawlAll() {
|
||||
}
|
||||
|
||||
await ElMessageBox.confirm(
|
||||
`确定要运行所有 ${enabledPlugins.length} 个启用的插件吗?代理将先以「待验证」入库,需再执行「全部验证」后才会变为可用(除非已开启「爬取后立即验证」)。`,
|
||||
`确定要运行所有 ${enabledPlugins.length} 个启用的插件吗?代理将先以「待验证」入库;全部插件爬取结束后会自动执行一次「全部验证」(若已开启「爬取后立即验证」,新入库条目也会在爬取时提前排队验证)。`,
|
||||
'批量爬取确认',
|
||||
{
|
||||
confirmButtonText: '开始爬取',
|
||||
@@ -405,7 +405,11 @@ async function handleCrawlAll() {
|
||||
crawlResults.value = merged
|
||||
}
|
||||
if (!data.cancelled) {
|
||||
ElMessage.success('批量爬取完成')
|
||||
ElMessage.success(
|
||||
data.validate_all_task_id
|
||||
? '批量爬取完成,已自动启动全部验证'
|
||||
: '批量爬取完成'
|
||||
)
|
||||
}
|
||||
await pluginsStore.fetchPlugins()
|
||||
} else {
|
||||
|
||||
@@ -113,9 +113,21 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="use_count" label="取用次数" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.use_count ?? 0 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="score" label="分数" width="100">
|
||||
<template #default="{ row }">
|
||||
<span class="score-value" :class="{ 'score-high': row.score >= 8, 'score-medium': row.score >= 5 && row.score < 8, 'score-low': row.score < 5 }">
|
||||
<span
|
||||
class="score-value"
|
||||
:class="{
|
||||
'score-high': row.score >= 70,
|
||||
'score-medium': row.score >= 40 && row.score < 70,
|
||||
'score-low': row.score < 40
|
||||
}"
|
||||
>
|
||||
{{ row.score || 0 }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="page-container">
|
||||
<PageHeader title="系统设置" :icon="Setting" />
|
||||
|
||||
<!-- 验证调度器控制 -->
|
||||
<!-- 验证调度器状态(启停由下方「启用自动验证」+ 保存配置) -->
|
||||
<el-card class="settings-card scheduler-card" shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
@@ -17,37 +17,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="scheduler-actions">
|
||||
<el-button
|
||||
type="success"
|
||||
@click="handleStartScheduler"
|
||||
:disabled="schedulerRunning"
|
||||
:loading="schedulerLoading"
|
||||
>
|
||||
<el-icon class="btn-icon"><VideoPlay /></el-icon>
|
||||
启动自动验证
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
type="danger"
|
||||
@click="handleStopScheduler"
|
||||
:disabled="!schedulerRunning"
|
||||
:loading="schedulerLoading"
|
||||
>
|
||||
<el-icon class="btn-icon"><VideoPause /></el-icon>
|
||||
停止自动验证
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleValidateNow"
|
||||
:loading="validating"
|
||||
>
|
||||
<el-icon class="btn-icon"><Refresh /></el-icon>
|
||||
立即验证全部
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="scheduler-info">
|
||||
<el-alert
|
||||
:title="schedulerInfo"
|
||||
@@ -66,15 +35,25 @@
|
||||
<el-icon class="header-icon"><Tools /></el-icon>
|
||||
基础配置
|
||||
</span>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSave"
|
||||
size="large"
|
||||
:loading="saving"
|
||||
>
|
||||
<el-icon class="btn-icon"><DocumentChecked /></el-icon>
|
||||
保存配置
|
||||
</el-button>
|
||||
<div class="header-actions">
|
||||
<el-button
|
||||
size="large"
|
||||
@click="handleValidateNow"
|
||||
:loading="validating"
|
||||
>
|
||||
<el-icon class="btn-icon"><Refresh /></el-icon>
|
||||
立即验证全部
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSave"
|
||||
size="large"
|
||||
:loading="saving"
|
||||
>
|
||||
<el-icon class="btn-icon"><DocumentChecked /></el-icon>
|
||||
保存配置
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -199,8 +178,6 @@ import {
|
||||
DocumentChecked,
|
||||
Tools,
|
||||
Timer,
|
||||
VideoPlay,
|
||||
VideoPause,
|
||||
Refresh
|
||||
} from '@element-plus/icons-vue'
|
||||
import { settingService } from '../services/settingService'
|
||||
@@ -210,11 +187,8 @@ import PageHeader from '../components/PageHeader.vue'
|
||||
// ==================== Composables ====================
|
||||
const {
|
||||
schedulerRunning,
|
||||
schedulerLoading,
|
||||
validating,
|
||||
fetchStatus,
|
||||
startScheduler,
|
||||
stopScheduler,
|
||||
validateNow
|
||||
} = useScheduler()
|
||||
|
||||
@@ -248,7 +222,7 @@ const schedulerInfo = computed(() => {
|
||||
if (schedulerRunning.value) {
|
||||
return `验证调度器正在运行,每 ${settings.validate_interval_minutes} 分钟执行一次:优先验证待验证代理,再按检查时间复检已入库代理`
|
||||
}
|
||||
return '验证调度器已停止,待验证代理不会自动检查;可在下方开启自动验证或点击「立即验证全部」'
|
||||
return '验证调度器当前未运行。请在下方打开「启用自动验证」并保存配置以恢复定时任务;需要时可使用「基础配置」标题栏中的「立即验证全部」手动执行一轮全量验证。'
|
||||
})
|
||||
|
||||
// ==================== 表单验证规则 ====================
|
||||
@@ -276,21 +250,6 @@ async function fetchSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 调度器控制 ====================
|
||||
async function handleStartScheduler() {
|
||||
await startScheduler(
|
||||
(msg) => ElMessage.success(msg),
|
||||
(msg) => ElMessage.error(msg)
|
||||
)
|
||||
}
|
||||
|
||||
async function handleStopScheduler() {
|
||||
await stopScheduler(
|
||||
(msg) => ElMessage.success(msg),
|
||||
(msg) => ElMessage.error(msg)
|
||||
)
|
||||
}
|
||||
|
||||
async function handleValidateNow() {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
@@ -372,6 +331,13 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
@@ -403,15 +369,8 @@ onMounted(() => {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.scheduler-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.scheduler-info {
|
||||
margin-top: 8px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.settings-form {
|
||||
|
||||
3
WebUI/src/vite-globals.d.ts
vendored
Normal file
3
WebUI/src/vite-globals.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/** 由 vite.config.js define 注入(值来自项目根目录 config/webui.json) */
|
||||
declare const __WEBUI_API_BASE_URL__: string
|
||||
declare const __WEBUI_WS_URL__: string
|
||||
@@ -1,9 +1,25 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const webuiConfigPath = path.resolve(__dirname, '../config/webui.json')
|
||||
let webui = { api_base_url: 'http://127.0.0.1:18080', ws_url: '' }
|
||||
try {
|
||||
webui = { ...webui, ...JSON.parse(fs.readFileSync(webuiConfigPath, 'utf-8')) }
|
||||
} catch {
|
||||
console.warn('[vite] 未读取 config/webui.json,使用默认 API 地址')
|
||||
}
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
define: {
|
||||
__WEBUI_API_BASE_URL__: JSON.stringify(String(webui.api_base_url || '').trim() || 'http://127.0.0.1:18080'),
|
||||
__WEBUI_WS_URL__: JSON.stringify(webui.ws_url != null ? String(webui.ws_url) : ''),
|
||||
},
|
||||
server: {
|
||||
port: 18081,
|
||||
// 支持 Vue Router 的 history 模式
|
||||
|
||||
Reference in New Issue
Block a user