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.
250 lines
5.3 KiB
250 lines
5.3 KiB
<script setup lang="ts">
|
|
definePageMeta({
|
|
layout: false,
|
|
});
|
|
|
|
interface ModelOption {
|
|
id: number;
|
|
name: string;
|
|
modelId: string;
|
|
providerName: string;
|
|
}
|
|
|
|
const models = ref<ModelOption[]>([]);
|
|
const preferredModelId = ref<number | null>(null);
|
|
const loading = ref(false);
|
|
const saving = ref(false);
|
|
|
|
async function loadModels() {
|
|
try {
|
|
const res = await $fetch<{ code: number; data: { list: any[] } }>("/api/llm/models");
|
|
const list = res.data?.list ?? [];
|
|
models.value = list.map((m) => ({
|
|
id: m.id,
|
|
name: m.name,
|
|
modelId: m.modelId,
|
|
providerName: m.providerName ?? "—",
|
|
}));
|
|
} catch {
|
|
models.value = [];
|
|
}
|
|
}
|
|
|
|
async function loadUserConfig() {
|
|
try {
|
|
const res = await $fetch<{ code: number; data: { config: Record<string, unknown> } }>(
|
|
"/api/config/me",
|
|
);
|
|
const val = res.data?.config?.preferredLlmModelId;
|
|
preferredModelId.value = typeof val === "number" && val > 0 ? val : null;
|
|
} catch {
|
|
preferredModelId.value = null;
|
|
}
|
|
}
|
|
|
|
async function handleSave() {
|
|
if (preferredModelId.value === null) {
|
|
const { $toast } = useNuxtApp();
|
|
$toast?.error?.("请选择一个模型");
|
|
return;
|
|
}
|
|
saving.value = true;
|
|
try {
|
|
await $fetch("/api/config/me", {
|
|
method: "PUT",
|
|
body: { key: "preferredLlmModelId", value: preferredModelId.value },
|
|
});
|
|
const { $toast } = useNuxtApp();
|
|
$toast?.success?.("默认模型已保存");
|
|
} catch {
|
|
const { $toast } = useNuxtApp();
|
|
$toast?.error?.("保存失败,请稍后重试");
|
|
} finally {
|
|
saving.value = false;
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
loading.value = true;
|
|
await Promise.all([loadModels(), loadUserConfig()]);
|
|
loading.value = false;
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="preferences-page">
|
|
<header class="page-header">
|
|
<h1 class="page-title">偏好设置</h1>
|
|
<p class="page-subtitle">管理您的个人偏好配置</p>
|
|
</header>
|
|
|
|
<div v-if="loading" class="loading-state">加载中...</div>
|
|
|
|
<div v-else class="settings-card">
|
|
<div class="card-header">
|
|
<h2 class="card-title">默认模型</h2>
|
|
<p class="card-desc">新建对话时将自动选中此模型</p>
|
|
</div>
|
|
|
|
<div class="card-body">
|
|
<div class="form-group">
|
|
<label class="form-label">偏好模型</label>
|
|
<select
|
|
v-model="preferredModelId"
|
|
class="form-select"
|
|
>
|
|
<option :value="null" disabled>请选择模型</option>
|
|
<option
|
|
v-for="m in models"
|
|
:key="m.id"
|
|
:value="m.id"
|
|
>
|
|
{{ m.name }} ({{ m.providerName }})
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<button
|
|
type="button"
|
|
class="btn-primary"
|
|
:disabled="saving || preferredModelId === null"
|
|
@click="handleSave"
|
|
>
|
|
{{ saving ? "保存中..." : "保存" }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.preferences-page {
|
|
padding: 40px;
|
|
max-width: 800px;
|
|
}
|
|
|
|
.page-header {
|
|
margin-bottom: 32px;
|
|
}
|
|
|
|
.page-title {
|
|
font-family: var(--font-display);
|
|
font-size: 32px;
|
|
font-weight: 400;
|
|
color: var(--color-ink);
|
|
letter-spacing: -0.3px;
|
|
margin: 0 0 8px 0;
|
|
}
|
|
|
|
.page-subtitle {
|
|
font-size: 15px;
|
|
color: var(--color-muted);
|
|
margin: 0;
|
|
}
|
|
|
|
.loading-state {
|
|
padding: 40px;
|
|
text-align: center;
|
|
color: var(--color-muted);
|
|
font-size: 14px;
|
|
}
|
|
|
|
.settings-card {
|
|
background: var(--color-surface-card);
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.card-header {
|
|
padding: 20px 24px;
|
|
border-bottom: 1px solid var(--color-hairline);
|
|
background: var(--color-surface-soft);
|
|
}
|
|
|
|
.card-title {
|
|
font-size: 16px;
|
|
font-weight: 500;
|
|
color: var(--color-ink);
|
|
margin: 0 0 4px 0;
|
|
}
|
|
|
|
.card-desc {
|
|
font-size: 13px;
|
|
color: var(--color-muted);
|
|
margin: 0;
|
|
}
|
|
|
|
.card-body {
|
|
padding: 24px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
}
|
|
|
|
.form-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.form-label {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: var(--color-body-strong);
|
|
}
|
|
|
|
.form-select {
|
|
width: 100%;
|
|
padding: 12px 16px;
|
|
font-size: 14px;
|
|
color: var(--color-ink);
|
|
background: var(--color-canvas);
|
|
border: 1px solid var(--color-hairline);
|
|
border-radius: 8px;
|
|
transition: all 0.15s ease;
|
|
appearance: none;
|
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
|
|
background-repeat: no-repeat;
|
|
background-position: right 12px center;
|
|
padding-right: 40px;
|
|
}
|
|
|
|
.form-select:focus {
|
|
outline: none;
|
|
border-color: var(--color-primary);
|
|
box-shadow: 0 0 0 3px rgba(204, 120, 92, 0.15);
|
|
}
|
|
|
|
.form-actions {
|
|
padding-top: 8px;
|
|
}
|
|
|
|
.btn-primary {
|
|
padding: 12px 24px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: var(--color-on-primary);
|
|
background: var(--color-primary);
|
|
border: none;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: background 0.15s ease;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background: var(--color-primary-active);
|
|
}
|
|
|
|
.btn-primary:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.preferences-page {
|
|
padding: 24px;
|
|
}
|
|
}
|
|
</style>
|
|
|