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.
 
 
 
 

379 lines
8.5 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 reviewApprovalMode = ref<string>("manual");
const loading = ref(false);
const saving = ref(false);
const savingApproval = 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;
const approvalVal = res.data?.config?.reviewApprovalMode;
reviewApprovalMode.value = typeof approvalVal === "string" && ["manual", "all-pass"].includes(approvalVal) ? approvalVal : "manual";
} 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;
}
}
async function handleSaveApproval() {
savingApproval.value = true;
try {
await $fetch("/api/config/me", {
method: "PUT",
body: { key: "reviewApprovalMode", value: reviewApprovalMode.value },
});
const { $toast } = useNuxtApp();
$toast?.success?.("审批设置已保存");
} catch {
const { $toast } = useNuxtApp();
$toast?.error?.("保存失败,请稍后重试");
} finally {
savingApproval.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 class="settings-card">
<div class="card-header">
<h2 class="card-title">审批设置</h2>
<p class="card-desc">Agent 调用工具时的审批策略</p>
</div>
<div class="card-body">
<div class="approval-options">
<label
class="approval-option"
:class="{ active: reviewApprovalMode === 'manual' }"
>
<input
v-model="reviewApprovalMode"
type="radio"
value="manual"
class="approval-radio"
/>
<div class="approval-content">
<span class="approval-label">手动审批</span>
<span class="approval-desc">每次工具调用需要您手动确认后才会执行</span>
</div>
</label>
<label
class="approval-option"
:class="{ active: reviewApprovalMode === 'all-pass' }"
>
<input
v-model="reviewApprovalMode"
type="radio"
value="all-pass"
class="approval-radio"
/>
<div class="approval-content">
<span class="approval-label">全部通过</span>
<span class="approval-desc">所有工具调用自动批准,无需手动确认</span>
</div>
</label>
</div>
<div class="form-actions">
<button
type="button"
class="btn-primary"
:disabled="savingApproval"
@click="handleSaveApproval"
>
{{ savingApproval ? "保存中..." : "保存" }}
</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;
}
}
.approval-options {
display: flex;
flex-direction: column;
gap: 12px;
}
.approval-option {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 16px;
border: 1px solid var(--color-hairline);
border-radius: 8px;
cursor: pointer;
transition: all 0.15s ease;
}
.approval-option:hover {
border-color: var(--color-primary);
}
.approval-option.active {
border-color: var(--color-primary);
background: rgba(204, 120, 92, 0.05);
}
.approval-radio {
margin-top: 2px;
accent-color: var(--color-primary);
cursor: pointer;
}
.approval-content {
display: flex;
flex-direction: column;
gap: 4px;
}
.approval-label {
font-size: 14px;
font-weight: 500;
color: var(--color-ink);
}
.approval-desc {
font-size: 13px;
color: var(--color-muted);
line-height: 1.5;
}
.settings-card + .settings-card {
margin-top: 24px;
}
</style>