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

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<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>