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