Files
svn-log-tool/frontend-vue/src/views/SettingsView.vue
T
liumangmang 1b182c2930 feat: v2 Vue3 frontend + multiple optimizations
- New Vue 3 + Vite frontend at /v2/ (OLED dark theme, Fira Sans/Code)
- Date selector: support day/week/month range (backend unchanged)
- SSE auto-reconnect (up to 3 retries)
- Visibility polling pause (dashboard pauses when tab hidden)
- Friendly Chinese HTTP error messages
- Cancel task with confirmation in Dashboard
- Split AiWorkflowService (1700->845 lines):
  - AiApiService: AI API calls + streaming
  - ExcelExportService: POI Excel generation
- Dockerfile: 3-stage build (Node frontend -> Maven -> JRE)
- WebApplication.java: System.out -> Logger
- .gitignore: v2 build output, backup dirs

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 15:12:52 +08:00

160 lines
7.9 KiB
Vue

<template>
<div class="settings">
<div class="card">
<div class="card-title">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
系统配置
</div>
<form @submit.prevent="onSave">
<div class="form-grid span-all" style="gap:var(--space-lg);">
<div class="form-group span-all">
<label for="provider">AI 提供商</label>
<select id="provider" class="form-select" v-model="form.provider" style="max-width:300px;">
<option value="deepseek">DeepSeek</option>
<option value="openai-compatible">OpenAI 兼容</option>
</select>
</div>
<template v-if="form.provider === 'deepseek'">
<div class="form-group span-all">
<label for="apiKey">DeepSeek API Key</label>
<input id="apiKey" class="form-input" type="password" v-model="form.apiKey" placeholder="保存后写入本地 settings.json" autocomplete="off" />
</div>
</template>
<template v-if="form.provider === 'openai-compatible'">
<div class="form-group span-all">
<label for="openaiBaseUrl">OpenAI 兼容 Base URL</label>
<input id="openaiBaseUrl" class="form-input" v-model="form.openaiBaseUrl" placeholder="例如 http://127.0.0.1:5001/v1" autocomplete="off" />
</div>
<div class="form-group span-all">
<label for="openaiApiKey">OpenAI 兼容 API Key</label>
<input id="openaiApiKey" class="form-input" type="password" v-model="form.openaiApiKey" placeholder="保存后写入本地 settings.json" autocomplete="off" />
</div>
<div class="form-group">
<label for="stageOneModel">第一阶段模型</label>
<select id="stageOneModel" class="form-select" v-model="form.openaiStageOneModel">
<option value="deepseek-v4-flash">deepseek-v4-flash</option>
<option value="deepseek-v4-pro">deepseek-v4-pro</option>
</select>
</div>
<div class="form-group">
<label for="stageTwoModel">第二阶段模型</label>
<select id="stageTwoModel" class="form-select" v-model="form.openaiStageTwoModel">
<option value="deepseek-v4-pro">deepseek-v4-pro</option>
<option value="deepseek-v4-flash">deepseek-v4-flash</option>
</select>
</div>
</template>
<div class="form-group">
<label for="svnUsername">SVN 用户名</label>
<input id="svnUsername" class="form-input" v-model="form.svnUsername" placeholder="留空则继续使用已保存值" autocomplete="username" spellcheck="false" />
</div>
<div class="form-group">
<label for="svnPassword">SVN 密码</label>
<input id="svnPassword" class="form-input" type="password" v-model="form.svnPassword" placeholder="留空则不覆盖已保存密码" autocomplete="current-password" />
</div>
<div class="form-group span-all">
<label for="defaultPreset">默认 SVN 项目</label>
<select id="defaultPreset" class="form-select" v-model="form.defaultSvnPresetId" style="max-width:400px;">
<option v-for="p in presets" :key="p.id" :value="p.id">{{ p.name }}</option>
</select>
</div>
<div class="form-group span-all">
<label for="outputDir">输出目录</label>
<input id="outputDir" class="form-input" v-model="form.outputDir" placeholder="默认 outputs" autocomplete="off" />
</div>
</div>
<div class="btn-group" style="margin-top:var(--space-lg);">
<button type="submit" class="btn btn-primary" :disabled="saving">
<span v-if="saving" class="spinner"></span>
保存系统设置
</button>
</div>
</form>
<div v-if="savedState" style="margin-top:var(--space-lg);padding-top:var(--space-md);border-top:1px solid var(--c-border-light);font-size:13px;color:var(--c-text-secondary);line-height:1.8;">
<div v-if="savedState.provider === 'openai-compatible'">
当前提供商: <strong style="color:var(--c-text);">OpenAI 兼容</strong><br>
Base URL: {{ savedState.openaiBaseUrl || '(未配置)' }}<br>
API Key: <span :style="{ color: savedState.openaiApiKeyConfigured ? 'var(--c-success)' : 'var(--c-warning)' }">{{ savedState.openaiApiKeyConfigured ? '已配置' : '未配置' }}</span><br>
Stage1: {{ savedState.openaiStageOneModel || '-' }}<br>
Stage2: {{ savedState.openaiStageTwoModel || '-' }}<br>
SVN: {{ renderSvnState(savedState) }}
</div>
<div v-else>
当前提供商: <strong style="color:var(--c-text);">DeepSeek</strong><br>
API Key: <span :style="{ color: savedState.apiKeyConfigured ? 'var(--c-success)' : 'var(--c-warning)' }">{{ savedState.apiKeyConfigured ? '已配置' : '未配置' }}</span> (来源: {{ savedState.apiKeySource }})<br>
SVN: {{ renderSvnState(savedState) }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useApi, useToast } from '../composables/useApi'
const { apiFetch } = useApi()
const { toast } = useToast()
const form = ref({
provider: 'deepseek',
apiKey: '',
openaiBaseUrl: '',
openaiApiKey: '',
openaiStageOneModel: 'deepseek-v4-flash',
openaiStageTwoModel: 'deepseek-v4-pro',
svnUsername: '',
svnPassword: '',
outputDir: '',
defaultSvnPresetId: '',
})
const presets = ref([])
const savedState = ref(null)
const saving = ref(false)
onMounted(async () => {
try {
const [settingsData, presetsData] = await Promise.all([
apiFetch('/api/settings'),
apiFetch('/api/svn/presets'),
])
presets.value = presetsData.presets || []
form.value.provider = settingsData.provider || 'deepseek'
form.value.openaiBaseUrl = settingsData.openaiBaseUrl || ''
form.value.openaiStageOneModel = settingsData.openaiStageOneModel || 'deepseek-v4-flash'
form.value.openaiStageTwoModel = settingsData.openaiStageTwoModel || 'deepseek-v4-pro'
form.value.svnUsername = settingsData.svnUsername || ''
form.value.outputDir = settingsData.outputDir || ''
form.value.defaultSvnPresetId = settingsData.defaultSvnPresetId || (presets.value[0]?.id || '')
savedState.value = settingsData
} catch (err) { toast(err.message, true) }
})
async function onSave() {
saving.value = true
try {
const data = await apiFetch('/api/settings', { method: 'PUT', body: JSON.stringify(form.value) })
savedState.value = data
form.value.apiKey = ''
form.value.openaiApiKey = ''
form.value.svnPassword = ''
toast('设置保存成功')
} catch (err) { toast(err.message, true) }
finally { saving.value = false }
}
function renderSvnState(d) {
const user = d.svnUsername || '(未配置)'
const configured = d.svnCredentialsConfigured ? '已配置' : '未配置'
return `${user} / ${configured}`
}
</script>