You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
trip-planner/src/views/SettingsPage.vue

533 lines
12 KiB

<template>
<div class="settings-page">
<div class="settings-header">
<button class="back-btn" @click="goBack"> </button>
<h1> AI 模型配置</h1>
<div class="header-spacer"></div>
</div>
<div class="settings-content">
<!-- Provider Selection -->
<div class="settings-section">
<h2>🤖 服务商</h2>
<div class="provider-selector">
<div class="provider-card active">
<span class="provider-icon"></span>
<div class="provider-info">
<span class="provider-name">Aliyun DashScope</span>
<span class="provider-desc">通义千问系列模型</span>
</div>
<span class="check-mark"></span>
</div>
</div>
</div>
<!-- API Key -->
<div class="settings-section">
<h2>🔑 API Key</h2>
<div class="input-group">
<div class="input-wrapper">
<input
:type="showKey ? 'text' : 'password'"
v-model="localConfig.apiKey"
placeholder="请输入 DashScope API Key"
class="api-input"
@input="debounceSave"
/>
<button class="toggle-key-btn" @click="showKey = !showKey">
{{ showKey ? '🙈' : '👁️' }}
</button>
</div>
<p class="input-hint">
获取地址<a href="https://bailian.console.aliyun.com/" target="_blank">阿里云百炼控制台 API-KEY 管理</a>
</p>
</div>
</div>
<!-- Model Selection -->
<div class="settings-section">
<h2>📦 模型选择</h2>
<div class="model-grid">
<div
v-for="model in ALIYUN_MODELS"
:key="model.value"
class="model-card"
:class="{ selected: localConfig.model === model.value }"
@click="selectModel(model.value)"
>
<div class="model-header">
<span class="model-radio" :class="{ checked: localConfig.model === model.value }"></span>
<span class="model-name">{{ model.label }}</span>
</div>
<span class="model-desc">{{ model.desc }}</span>
</div>
</div>
<!-- Custom Model Input -->
<div class="custom-model-input">
<label>自定义模型</label>
<input
v-model="localConfig.model"
placeholder="输入自定义模型名称,如 qwen3.6-plus"
class="text-input"
@input="onModelInput"
/>
<p class="input-hint">当前选择{{ localConfig.model }}</p>
</div>
</div>
<!-- Base URL -->
<div class="settings-section">
<h2>🌐 Base URL</h2>
<div class="input-group">
<input
v-model="localConfig.baseUrl"
placeholder="API 基础地址"
class="text-input"
@input="debounceSave"
/>
<p class="input-hint">默认https://coding.dashscope.aliyuncs.com/v1</p>
</div>
</div>
<!-- Advanced Settings -->
<div class="settings-section">
<h2>🔧 高级设置</h2>
<div class="advanced-grid">
<div class="advanced-item">
<label>Temperature</label>
<div class="slider-group">
<input
type="range"
min="0"
max="1"
step="0.1"
v-model.number="localConfig.temperature"
@input="debounceSave"
/>
<span class="slider-value">{{ localConfig.temperature }}</span>
</div>
<span class="item-desc">创造性 (0=精确, 1=随机)</span>
</div>
<div class="advanced-item">
<label>Max Tokens</label>
<div class="slider-group">
<input
type="range"
min="500"
max="8000"
step="500"
v-model.number="localConfig.maxTokens"
@input="debounceSave"
/>
<span class="slider-value">{{ localConfig.maxTokens }}</span>
</div>
<span class="item-desc">最大输出长度</span>
</div>
</div>
</div>
<!-- Actions -->
<div class="settings-actions">
<button class="test-btn" @click="handleTest" :disabled="isTesting">
<span v-if="isTesting" class="spinner"></span>
{{ isTesting ? '测试中...' : '🔗 测试连接' }}
</button>
<button class="save-btn" @click="handleSave">💾 </button>
<button class="reset-btn" @click="handleReset">🔄 </button>
</div>
<!-- Test Result -->
<transition name="fade">
<div v-if="testResult" class="test-result" :class="{ success: testResult.success, error: !testResult.success }">
<span class="result-icon">{{ testResult.success ? '✅' : '❌' }}</span>
<span class="result-msg">{{ testResult.message }}</span>
<button class="close-result" @click="testResult = null">×</button>
</div>
</transition>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useSettingsStore } from '../stores/settings'
const router = useRouter()
const store = useSettingsStore()
const localConfig = ref({ ...store.config })
const showKey = ref(false)
const testResult = ref(null)
const ALIYUN_MODELS = computed(() => store.ALIYUN_MODELS)
const isTesting = computed(() => store.isTesting)
let saveTimer = null
const debounceSave = () => {
if (saveTimer) clearTimeout(saveTimer)
saveTimer = setTimeout(() => {
store.updateConfig(localConfig.value)
}, 500)
}
const selectModel = (model) => {
localConfig.value.model = model
store.updateConfig({ model })
}
const onModelInput = () => {
debounceSave()
}
const goBack = () => router.push('/')
const handleTest = async () => {
await store.saveConfig()
const success = await store.testConnection()
testResult.value = store.testResult
}
const handleSave = async () => {
await store.saveConfig()
testResult.value = { success: true, message: '配置已保存到服务器' }
setTimeout(() => { testResult.value = null }, 2000)
}
const handleReset = () => {
if (confirm('确定要重置所有配置吗?')) {
store.resetConfig()
localConfig.value = { ...store.config }
testResult.value = { success: true, message: '🔄 已重置为默认配置' }
}
}
onMounted(async () => {
await store.loadConfig()
localConfig.value = { ...store.config }
})
</script>
<style scoped>
.settings-page {
height: 100vh;
display: flex;
flex-direction: column;
background: #f5f5f5;
}
.settings-header {
display: flex;
align-items: center;
padding: 12px 20px;
background: #fff;
color: #2d3436;
gap: 16px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
.back-btn {
background: #f5f7fa;
border: none;
color: #636e72;
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
}
.back-btn:hover { background: #e8e8e8; }
.settings-header h1 {
margin: 0;
font-size: 18px;
}
.header-spacer { flex: 1; }
.settings-content {
flex: 1;
overflow-y: auto;
padding: 20px;
max-width: 800px;
margin: 0 auto;
width: 100%;
}
.settings-section {
background: #fff;
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.settings-section h2 {
margin: 0 0 16px;
font-size: 15px;
color: #1b4332;
}
/* Provider */
.provider-selector {
display: flex;
gap: 12px;
}
.provider-card {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
border: 2px solid #6c5ce7;
border-radius: 10px;
background: #f0f0f5;
flex: 1;
}
.provider-icon { font-size: 28px; }
.provider-info { flex: 1; }
.provider-name { display: block; font-weight: 600; color: #333; }
.provider-desc { display: block; font-size: 12px; color: #888; }
.check-mark { color: #6c5ce7; font-size: 20px; font-weight: bold; }
/* Input */
.input-group { display: flex; flex-direction: column; gap: 8px; }
.input-wrapper {
display: flex;
align-items: center;
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
transition: border-color 0.2s;
}
.input-wrapper:focus-within { border-color: #6c5ce7; }
.api-input, .text-input {
flex: 1;
padding: 12px 16px;
border: none;
font-size: 14px;
outline: none;
}
.toggle-key-btn {
background: none;
border: none;
padding: 8px 12px;
cursor: pointer;
font-size: 16px;
color: #888;
}
.input-hint {
margin: 0;
font-size: 12px;
color: #888;
}
.input-hint a {
color: #6c5ce7;
text-decoration: none;
}
.input-hint a:hover { text-decoration: underline; }
/* Model Grid */
.model-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 12px;
}
.model-card {
padding: 14px;
border: 2px solid #e0e0e0;
border-radius: 10px;
cursor: pointer;
transition: all 0.2s;
}
.model-card:hover { border-color: #b8b0f0; }
.model-card.selected {
border-color: #6c5ce7;
background: #f0f0f5;
}
.model-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.model-radio {
width: 18px;
height: 18px;
border-radius: 50%;
border: 2px solid #ccc;
display: flex;
align-items: center;
justify-content: center;
}
.model-radio.checked {
border-color: #6c5ce7;
}
.model-radio.checked::after {
content: '';
width: 10px;
height: 10px;
border-radius: 50%;
background: #6c5ce7;
}
.model-name { font-weight: 500; color: #333; }
.model-desc { font-size: 12px; color: #888; }
/* Custom Model Input */
.custom-model-input {
margin-top: 16px;
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
}
.custom-model-input label {
display: block;
font-size: 13px;
color: #666;
font-weight: 500;
margin-bottom: 8px;
}
/* Advanced */
.advanced-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.advanced-item { display: flex; flex-direction: column; gap: 8px; }
.advanced-item label { font-size: 13px; color: #666; font-weight: 500; }
.slider-group {
display: flex;
align-items: center;
gap: 12px;
}
.slider-group input[type="range"] {
flex: 1;
height: 4px;
-webkit-appearance: none;
background: #e0e0e0;
border-radius: 2px;
outline: none;
}
.slider-group input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: #6c5ce7;
cursor: pointer;
}
.slider-value {
min-width: 40px;
text-align: right;
font-weight: 600;
color: #6c5ce7;
}
.item-desc { font-size: 11px; color: #aaa; }
/* Actions */
.settings-actions {
display: flex;
gap: 12px;
padding: 20px 0;
}
.test-btn, .save-btn, .reset-btn {
padding: 12px 24px;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s;
}
.test-btn { background: #e3f2fd; color: #1976d2; }
.test-btn:hover:not(:disabled) { background: #bbdefb; }
.test-btn:disabled { opacity: 0.6; cursor: not-allowed; }
.save-btn { background: #6c5ce7; color: #fff; }
.save-btn:hover { background: #5b4cdb; }
.reset-btn { background: #f5f5f5; color: #666; }
.reset-btn:hover { background: #e0e0e0; }
.spinner {
width: 16px;
height: 16px;
border: 2px solid rgba(25,118,210,0.3);
border-top-color: #1976d2;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Test Result */
.test-result {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
padding: 14px 20px;
border-radius: 12px;
display: flex;
align-items: center;
gap: 10px;
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
z-index: 1000;
}
.test-result.success { background: #e8f5e9; color: #2e7d32; }
.test-result.error { background: #ffebee; color: #c62828; }
.result-icon { font-size: 18px; }
.result-msg { font-size: 14px; }
.close-result {
background: none;
border: none;
font-size: 18px;
cursor: pointer;
color: inherit;
opacity: 0.6;
margin-left: 8px;
}
.close-result:hover { opacity: 1; }
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
@media (max-width: 600px) {
.advanced-grid { grid-template-columns: 1fr; }
.model-grid { grid-template-columns: 1fr; }
.settings-actions { flex-direction: column; }
}
</style>