feat: redesign personal space layout

This commit is contained in:
JOJO 2025-11-30 02:21:46 +08:00
parent 93c53eed32
commit 9bfc6f3903
2 changed files with 612 additions and 146 deletions

View File

@ -22,6 +22,37 @@
</div>
</div>
<div class="personalization-body" v-if="!loading">
<form class="personal-form" @submit.prevent="personalization.save()">
<div class="personalization-layout">
<nav class="personal-page-tabs" aria-label="个人空间分组切换">
<button
type="button"
class="personal-tab-button"
:class="{ active: activeTab === 'preferences' }"
:aria-pressed="activeTab === 'preferences'"
@click.prevent="setActiveTab('preferences')"
>
<span>个性化设置</span>
</button>
<button
type="button"
class="personal-tab-button"
:class="{ active: activeTab === 'behavior' }"
:aria-pressed="activeTab === 'behavior'"
@click.prevent="setActiveTab('behavior')"
>
<span>模型行为</span>
</button>
</nav>
<div class="personalization-content-shell">
<div
class="personalization-content"
@touchstart.passive="handleSwipeStart"
@touchend.passive="handleSwipeEnd"
>
<transition name="personal-page-vertical" mode="out-in">
<section v-if="activeTab === 'preferences'" key="preferences" class="personal-page personal-page-pref">
<div class="personal-toggle-row">
<label class="personal-toggle">
<span class="toggle-text">
<span class="toggle-title">启用个性化提示</span>
@ -37,8 +68,9 @@
<span class="switch-slider"></span>
</span>
</label>
<form class="personal-form" @submit.prevent="personalization.save()">
</div>
<div class="personalization-sections">
<div class="personal-left-column">
<div class="personal-section personal-info">
<label class="personal-field">
<span>您希望AI智能体怎么自称</span>
@ -95,6 +127,7 @@
</div>
</div>
</div>
</div>
<div class="personal-right-column">
<div class="personal-section personal-considerations">
<div class="personal-field">
@ -139,7 +172,7 @@
<p class="consideration-limit">最多 {{ maxConsiderations }} 可拖动排序</p>
</div>
</div>
<div class="personal-form-actions">
<div class="personal-form-actions card-aligned">
<div class="personal-status-group">
<transition name="personal-status-fade">
<span class="status success" v-if="status">{{ status }}</span>
@ -148,12 +181,93 @@
<span class="status error" v-if="error">{{ error }}</span>
</transition>
</div>
<button type="button" class="primary" :disabled="saving" @click="personalization.save()">
<button type="submit" class="primary" :disabled="saving">
{{ saving ? '保存中...' : '保存设置' }}
</button>
</div>
</div>
</div>
</section>
<section v-else key="behavior" class="personal-page behavior-page">
<div class="behavior-section">
<div class="behavior-field">
<div class="behavior-field-header">
<span class="field-title">思考频率</span>
<p class="field-desc">控制连续快速模式运行多少轮后系统会强制切换到思考模型</p>
</div>
<div class="thinking-presets">
<button
v-for="preset in thinkingPresets"
:key="preset.id"
type="button"
:class="{ active: isPresetActive(preset.value) }"
@click.prevent="applyThinkingPreset(preset.value)"
>
{{ preset.label }}
<small>{{ preset.value }} </small>
</button>
</div>
<div class="thinking-input-row">
<label>
<span>自定义轮数</span>
<input
type="number"
:min="thinkingIntervalRange.min"
:max="thinkingIntervalRange.max"
:placeholder="`默认 ${thinkingIntervalDefault} 轮`"
:value="form.thinking_interval ?? ''"
@input="handleThinkingInput"
@focus="personalization.clearFeedback()"
/>
</label>
<span class="thinking-hint">范围 {{ thinkingIntervalRange.min }}~{{ thinkingIntervalRange.max }} </span>
<button type="button" class="link-button" @click.prevent="restoreThinkingInterval">
恢复默认
</button>
</div>
</div>
<div class="behavior-field">
<div class="behavior-field-header">
<span class="field-title">默认禁用工具类别</span>
<p class="field-desc">选择后这些类别在新任务中会保持关闭也可随时通过快捷菜单临时开启</p>
</div>
<div class="tool-category-grid" v-if="toolCategories.length">
<label
v-for="category in toolCategories"
:key="category.id"
class="tool-category-chip"
>
<input
type="checkbox"
:checked="form.disabled_tool_categories.includes(category.id)"
@change="toggleCategory(category.id)"
/>
<span>{{ category.label }}</span>
</label>
</div>
<p class="behavior-hint" v-else>暂无可配置的工具类别</p>
</div>
</div>
<div class="personal-actions-row">
<div class="personal-form-actions card-aligned">
<div class="personal-status-group">
<transition name="personal-status-fade">
<span class="status success" v-if="status">{{ status }}</span>
</transition>
<transition name="personal-status-fade">
<span class="status error" v-if="error">{{ error }}</span>
</transition>
</div>
<button type="submit" class="primary" :disabled="saving">
{{ saving ? '保存中...' : '保存设置' }}
</button>
</div>
</div>
</section>
</transition>
</div>
</div>
</div>
</form>
</div>
<div class="personalization-loading" v-else>正在加载个性化配置...</div>
@ -163,6 +277,7 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { storeToRefs } from 'pinia';
import { usePersonalizationStore } from '@/stores/personalization';
@ -179,7 +294,74 @@ const {
status,
error,
saving,
toggleUpdating
toggleUpdating,
toolCategories,
thinkingIntervalDefault,
thinkingIntervalRange
} = storeToRefs(personalization);
const activeTab = ref<'preferences' | 'behavior'>('preferences');
const swipeState = ref<{ startY: number; active: boolean }>({ startY: 0, active: false });
const thinkingPresets = [
{ id: 'low', label: '低', value: 10 },
{ id: 'medium', label: '中', value: 5 },
{ id: 'high', label: '高', value: 3 }
];
const setActiveTab = (tab: 'preferences' | 'behavior') => {
activeTab.value = tab;
};
const handleSwipeStart = (event: TouchEvent) => {
if (!event.touches.length) {
return;
}
swipeState.value = { startY: event.touches[0].clientY, active: true };
};
const handleSwipeEnd = (event: TouchEvent) => {
if (!swipeState.value.active || !event.changedTouches.length) {
return;
}
const deltaY = event.changedTouches[0].clientY - swipeState.value.startY;
swipeState.value.active = false;
if (Math.abs(deltaY) < 60) {
return;
}
if (deltaY < 0) {
setActiveTab('behavior');
} else {
setActiveTab('preferences');
}
};
const applyThinkingPreset = (value: number) => {
personalization.setThinkingInterval(value);
};
const handleThinkingInput = (event: Event) => {
const target = event.target as HTMLInputElement;
if (!target.value) {
personalization.setThinkingInterval(null);
return;
}
const parsed = Number(target.value);
personalization.setThinkingInterval(Number.isNaN(parsed) ? null : parsed);
};
const restoreThinkingInterval = () => {
personalization.setThinkingInterval(null);
};
const isPresetActive = (value: number) => {
if (form.value.thinking_interval === null || typeof form.value.thinking_interval === 'undefined') {
return value === thinkingIntervalDefault.value;
}
return form.value.thinking_interval === value;
};
const toggleCategory = (categoryId: string) => {
personalization.toggleDefaultToolCategory(categoryId);
};
</script>

View File

@ -13,7 +13,9 @@
}
.personal-page-card {
width: min(95vw, 860px);
width: min(96vw, 1020px);
height: calc(100vh - 40px);
max-height: 760px;
background: #fffaf4;
border-radius: 24px;
border: 1px solid rgba(118, 103, 84, 0.25);
@ -21,7 +23,8 @@
padding: 40px;
text-align: left;
color: var(--claude-text);
max-height: calc(100vh - 40px);
display: flex;
flex-direction: column;
overflow: hidden;
}
@ -91,15 +94,10 @@
display: flex;
flex-direction: column;
gap: 20px;
overflow-y: auto;
max-height: calc(100vh - 180px);
padding-right: 6px;
scrollbar-width: none;
-ms-overflow-style: none;
}
.personalization-body::-webkit-scrollbar {
display: none;
flex: 1 1 auto;
min-height: 0;
height: 100%;
overflow: hidden;
}
.personal-toggle {
@ -168,6 +166,233 @@
transform: translateX(20px);
}
.personal-toggle-row {
margin-bottom: 18px;
}
.personalization-layout {
display: grid;
grid-template-columns: 200px minmax(0, 1fr);
gap: 20px;
align-items: stretch;
min-height: 0;
flex: 1 1 auto;
height: 100%;
}
.personal-page-tabs {
display: flex;
flex-direction: column;
gap: 12px;
padding: 20px;
border-radius: 20px;
border: 1px solid rgba(118, 103, 84, 0.2);
background: rgba(255, 255, 255, 0.92);
align-self: flex-start;
height: auto;
max-height: 220px;
overflow: hidden;
}
.personal-tab-button {
border: none;
border-radius: 16px;
padding: 14px 18px;
text-align: left;
font-weight: 600;
background: transparent;
color: var(--claude-text-secondary);
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
}
.personal-tab-button.active {
background: rgba(189, 93, 58, 0.12);
color: var(--claude-accent);
box-shadow: 0 10px 24px rgba(189, 93, 58, 0.15);
}
.personal-tab-button:focus-visible {
outline: 2px solid rgba(189, 93, 58, 0.4);
outline-offset: 2px;
}
.personalization-content-shell {
display: flex;
flex-direction: column;
min-height: 0;
flex: 1 1 auto;
height: 100%;
}
.personalization-content {
position: relative;
overflow-y: auto;
border-radius: 24px;
border: 1px solid rgba(118, 103, 84, 0.18);
background: rgba(255, 255, 255, 0.98);
min-height: 0;
flex: 1 1 auto;
height: 100%;
scrollbar-width: none;
-ms-overflow-style: none;
}
.personalization-content::-webkit-scrollbar {
display: none;
}
.personalization-content .personal-page {
width: 100%;
padding: 16px 26px 26px;
}
@media (max-width: 1024px) {
.personalization-layout {
grid-template-columns: 1fr;
}
.personal-page-tabs {
flex-direction: row;
padding: 14px;
height: auto;
}
.personal-tab-button {
flex: 1;
text-align: center;
}
}
.behavior-section {
display: flex;
flex-direction: column;
gap: 24px;
background: rgba(255, 255, 255, 0.85);
border-radius: 20px;
padding: 24px;
border: 1px solid rgba(118, 103, 84, 0.18);
}
.behavior-field {
display: flex;
flex-direction: column;
gap: 12px;
}
.behavior-field-header {
display: flex;
flex-direction: column;
gap: 4px;
}
.behavior-field-header .field-title {
font-weight: 600;
}
.behavior-field-header .field-desc {
color: var(--claude-text-secondary);
font-size: 13px;
}
.thinking-presets {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.thinking-presets button {
flex: 1;
min-width: 100px;
border: 1px solid rgba(118, 103, 84, 0.2);
border-radius: 12px;
padding: 10px 12px;
background: #fff;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
font-weight: 600;
color: var(--claude-text-secondary);
}
.thinking-presets button.active {
border-color: var(--claude-accent);
color: var(--claude-accent);
background: rgba(118, 103, 84, 0.08);
}
.thinking-presets button small {
font-size: 12px;
font-weight: 500;
}
.thinking-input-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
}
.thinking-input-row label {
display: flex;
flex-direction: column;
gap: 6px;
font-weight: 600;
}
.thinking-input-row input {
width: 120px;
padding: 8px 10px;
border-radius: 8px;
border: 1px solid rgba(118, 103, 84, 0.3);
}
.thinking-hint {
color: var(--claude-text-secondary);
font-size: 13px;
}
.behavior-hint {
color: var(--claude-text-secondary);
font-size: 13px;
}
.link-button {
border: none;
background: none;
color: var(--claude-accent);
font-weight: 600;
cursor: pointer;
}
.tool-category-grid {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.tool-category-chip {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
border-radius: 12px;
background: rgba(118, 103, 84, 0.08);
border: 1px solid rgba(118, 103, 84, 0.2);
font-size: 13px;
}
.tool-category-chip input {
accent-color: var(--claude-accent);
}
.global-actions {
justify-content: flex-start;
align-items: center;
gap: 16px;
}
/* ========================================= */
/* 移动端面板入口 */
/* ========================================= */
@ -507,13 +732,25 @@
flex-direction: column;
gap: 18px;
flex: 1;
min-height: 0;
height: 100%;
}
.personalization-sections {
display: flex;
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(280px, 360px);
gap: 18px;
align-items: flex-start;
flex-wrap: nowrap;
align-items: stretch;
}
.personal-left-column {
display: flex;
flex-direction: column;
min-width: 0;
}
.personal-left-column .personal-section {
height: 100%;
}
.personal-section {
@ -532,13 +769,13 @@
display: flex;
flex-direction: column;
gap: 14px;
flex: 1 1 0;
max-width: none;
min-width: 0;
align-self: stretch;
height: 100%;
}
.personal-right-column .personal-section {
flex: 1 1 auto;
.personal-right-column > .personal-form-actions {
margin-top: auto;
}
.personal-section.personal-considerations .personal-field {
@ -566,11 +803,13 @@
@media (max-width: 1024px) {
.personalization-sections {
display: flex;
flex-direction: column;
flex-wrap: wrap;
gap: 18px;
}
.personal-right-column {
.personal-right-column,
.personal-left-column {
width: 100%;
max-width: none;
}
@ -709,6 +948,36 @@
justify-content: flex-end;
}
.personal-form-actions.card-aligned {
padding: 0;
margin-top: auto;
justify-content: flex-end;
}
@media (max-width: 1024px) {
.personal-form-actions.card-aligned {
margin-top: 12px;
}
}
.personal-actions-row {
display: flex;
justify-content: flex-end;
margin-top: 18px;
}
.personal-actions-row .personal-form-actions {
width: 100%;
max-width: 420px;
padding: 0;
}
@media (min-width: 1025px) {
.personal-actions-row .personal-form-actions {
max-width: calc(50% - 9px);
}
}
.personal-status-group {
flex: 0 0 auto;
display: flex;
@ -761,6 +1030,21 @@
font-size: 14px;
}
.personal-page-vertical-enter-active,
.personal-page-vertical-leave-active {
transition: opacity 0.35s ease, transform 0.35s ease;
}
.personal-page-vertical-enter-from {
opacity: 0;
transform: translateY(26px);
}
.personal-page-vertical-leave-to {
opacity: 0;
transform: translateY(-26px);
}
.personal-page-fade-enter-active,
.personal-page-fade-leave-active {
transition: opacity 0.25s ease, backdrop-filter 0.25s ease;