feat(settings): 个人空间重构为紧凑窄版布局,完善移动端适配

- 卡片窄化至 648px、圆角 14px、侧栏 184px,控件行高紧凑化
- 4429 行主文件拆分为 13 个 tab 子组件 + 共享样式表
- 移动端(≤760px)改为 iOS 设置式「列表→子页」导航
- 修复内容区缺 flex:1 导致设置行无法占满
- 修复交流语气行 label 被 grid auto 列挤压成竖排
- 修复移动端弹窗顶栏被主界面顶栏(z-index 1500)遮挡
- 窄屏禁用选项 hover/active 视觉态与 tab 切换过渡动画
- 修复上下文图标 28 viewBox/1.7 描边导致的视觉偏小偏细
This commit is contained in:
JOJO 2026-08-29 21:58:53 +08:00
parent 1ca5e15f8e
commit f5bfab4056
18 changed files with 3776 additions and 3077 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,937 @@
/*
* 个人空间抽屉共享样式骨架 / 设置行 / 控件 / 浮层 / 响应式
* PersonalizationDrawer.vue 抽出scoped 样式无法穿透 tab 子组件故改为全局 CSS
* 所有类名均为 settings-* / personal-* 前缀全局冲突风险低
*/
/* 卡片尺寸/圆角/内边距的唯一事实源在全局 .personal-page-card_overlays.scss
这里只保留 redesign 特有的变量容器查询上下文与内部布局 */
.settings-redesign-card {
--settings-floating-menu-bg: color-mix(in srgb, var(--surface-soft) 90%, var(--surface-raised));
--settings-floating-menu-hover: var(--hover-bg);
--settings-floating-menu-shadow: none;
--settings-tab-hover-bg: var(--theme-tab-active);
--settings-tab-active-bg: var(--theme-tab-active);
position: relative;
/* 容器查询上下文:内部布局按卡片自身宽度响应挤压,与视口脱耦 */
container-type: inline-size;
container-name: settings-card;
}
html[data-theme='dark'] .settings-redesign-card,
body[data-theme='dark'] .settings-redesign-card {
--settings-floating-menu-bg: var(--surface-panel);
--settings-floating-menu-hover: var(--hover-bg);
--settings-floating-menu-shadow: none;
--settings-tab-hover-bg: var(--hover-bg);
--settings-tab-active-bg: var(--hover-bg);
}
/* 桌面端关闭按钮位于侧栏顶部流内(不再是绝对定位大圆钮) */
.settings-nav-head {
flex: 0 0 auto;
padding: 12px 12px 4px;
}
.settings-close-button {
width: 32px;
height: 32px;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--text-secondary);
display: grid;
place-items: center;
cursor: pointer;
}
@media (hover: hover) {
.settings-close-button:hover {
background: var(--hover-bg);
color: var(--text-primary);
}
}
.settings-close-button svg {
width: 17px;
height: 17px;
stroke-width: 2;
}
/* 移动端顶栏:桌面端隐藏 */
.settings-mobile-bar {
display: none;
}
.settings-tab-chevron {
display: none;
}
.settings-redesign-body,
.settings-redesign-form {
height: 100%;
min-height: 0;
}
.settings-redesign-layout {
height: 100%;
display: flex;
min-height: 0;
}
.settings-redesign-nav {
flex: 0 0 184px;
min-width: 0;
min-height: 0;
height: 100%;
display: flex;
flex-direction: column;
border-right: 1px solid var(--theme-control-border);
}
.settings-redesign-tabs {
flex: 1 1 auto;
min-height: 0;
padding: 4px 10px 12px;
display: flex;
flex-direction: column;
gap: 2px;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
}
.settings-redesign-tab {
width: 100%;
height: 36px;
flex: 0 0 auto;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--text-primary);
padding: 0 10px;
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
text-align: left;
font-size: 14px;
letter-spacing: -0.006em;
}
.settings-tab-label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (hover: hover) {
.settings-redesign-tab:hover {
background: var(--settings-tab-hover-bg);
}
}
.settings-redesign-tab.active {
background: var(--settings-tab-active-bg);
font-weight: 640;
}
.settings-tab-icon {
--icon-size: 18px;
flex: 0 0 auto;
}
/* 与其他图标外框统一 18px不再特殊放大图标外框对齐规范 */
.settings-tab-icon--chat {
width: 18px;
height: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.settings-tab-icon--chat svg {
width: 18px;
height: 18px;
}
.settings-redesign-content {
flex: 1 1 0;
min-width: 0;
min-height: 0;
height: 100%;
overflow: visible;
display: flex;
flex-direction: column;
position: relative;
}
.settings-redesign-content-header {
height: 58px;
flex: 0 0 58px;
display: flex;
align-items: center;
padding: 0 20px;
border-bottom: 1px solid var(--theme-control-border);
}
.settings-redesign-content-header h2 {
margin: 0;
color: var(--text-primary);
font-size: 19px;
line-height: 1;
letter-spacing: -0.015em;
font-weight: 650;
}
.settings-redesign-scroll {
min-height: 0;
flex: 1 1 auto;
overflow-y: auto;
overflow-x: hidden;
padding: 2px 20px 96px;
scroll-padding-bottom: 96px;
scrollbar-width: none;
-ms-overflow-style: none;
}
.settings-redesign-tabs::-webkit-scrollbar,
.settings-redesign-scroll::-webkit-scrollbar,
.settings-floating-menu::-webkit-scrollbar,
.role-editor-body::-webkit-scrollbar {
display: none;
}
.settings-floating-menu {
scrollbar-width: none;
-ms-overflow-style: none;
}
.settings-page {
min-height: 0;
}
.settings-select-row,
.settings-input-row,
.settings-list-row,
.settings-action-row,
.settings-toggle-row {
min-height: 0;
border-bottom: 1px solid var(--theme-control-border);
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 18px;
padding: 13px 0;
color: var(--text-primary);
}
.settings-list-row.tall,
.settings-input-row.stackable {
align-items: flex-start;
}
.settings-list-row.tall {
grid-template-columns: minmax(180px, 1fr) minmax(0, 360px);
}
.settings-row-copy {
min-width: 0;
display: block;
}
.settings-row-title {
display: block;
color: var(--text-primary);
font-size: 14px;
font-weight: 550;
letter-spacing: -0.01em;
line-height: 1.32;
}
.settings-row-desc {
display: block;
margin-top: 3px;
color: var(--text-secondary);
font-size: 13px;
line-height: 1.45;
}
.settings-select-wrap {
position: relative;
display: inline-flex;
justify-self: end;
}
.settings-select-button {
width: max-content;
min-width: 0;
height: 32px;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--text-primary);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 0 8px 0 10px;
cursor: pointer;
font-size: 14px;
white-space: nowrap;
}
@media (hover: hover) {
.settings-select-button:hover {
background: var(--hover-bg);
}
}
.settings-select-button .select-chevron {
width: 12px;
height: 12px;
display: inline-flex;
align-items: center;
justify-content: center;
transition: transform 0.15s ease;
}
.settings-select-button .select-chevron::before {
content: '';
width: 7px;
height: 7px;
border-right: 1.5px solid currentColor;
border-bottom: 1.5px solid currentColor;
transform: translateY(-1.5px) rotate(45deg);
}
.settings-select-wrap.open .settings-select-button .select-chevron {
transform: rotate(180deg);
}
.settings-floating-menu {
position: fixed;
right: auto;
top: auto;
width: 300px;
max-height: calc(34px * 4 + 12px);
overflow-y: auto;
overflow-x: hidden;
padding: 5px;
border-radius: 10px;
background: var(--settings-floating-menu-bg);
border: 1px solid var(--theme-control-border);
box-shadow: var(--settings-floating-menu-shadow);
opacity: 1;
backdrop-filter: none;
z-index: 2147483647;
display: none;
}
.settings-select-wrap.open .settings-floating-menu {
display: block;
}
.settings-floating-menu.dark {
background: var(--surface-panel) !important;
box-shadow: none !important;
}
@media (hover: hover) {
.settings-floating-menu.dark .settings-menu-option:hover {
background: var(--hover-bg) !important;
}
}
html[data-theme='dark'] .settings-floating-menu,
body[data-theme='dark'] .settings-floating-menu {
background: var(--surface-panel) !important;
box-shadow: none !important;
}
@media (hover: hover) {
html[data-theme='dark'] .settings-menu-option:hover,
body[data-theme='dark'] .settings-menu-option:hover {
background: var(--hover-bg) !important;
}
}
.settings-menu-option {
width: 100%;
min-height: 34px;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--text-primary);
display: grid;
grid-template-columns: minmax(0, 1fr) 22px;
row-gap: 0;
column-gap: 8px;
align-items: center;
padding: 5px 9px;
text-align: left;
cursor: pointer;
}
@media (hover: hover) {
.settings-menu-option:hover {
background: var(--settings-floating-menu-hover);
}
}
.settings-menu-option:disabled {
opacity: 0.48;
cursor: not-allowed;
}
.settings-menu-option strong {
grid-column: 1;
grid-row: 1;
display: block;
color: var(--text-primary);
font-size: 14px;
font-weight: 550;
letter-spacing: -0.01em;
min-width: 0;
}
.settings-menu-option span {
grid-column: 1;
grid-row: 2;
display: block;
margin-top: 0;
color: var(--text-secondary);
font-size: 11px;
line-height: 1.15;
min-width: 0;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.settings-menu-option svg {
grid-column: 2;
grid-row: 1 / span 2;
align-self: center;
width: 21px;
height: 21px;
fill: none;
stroke: var(--text-primary);
stroke-width: 2.25;
stroke-linecap: round;
stroke-linejoin: round;
opacity: 0;
}
.settings-menu-option.selected svg {
opacity: 1;
}
.settings-input-row input,
.settings-add-row input,
.settings-number-row input,
.settings-number-input,
.settings-compression-grid input {
height: 32px;
border: 1px solid var(--theme-control-border);
border-radius: 8px;
background: transparent;
color: var(--text-primary);
padding: 0 11px;
outline: none;
font-size: 13px;
}
.settings-number-input {
width: 120px;
text-align: right;
}
.settings-section-divider {
display: flex;
align-items: center;
margin: 18px 0 6px;
}
.settings-section-divider__label {
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
letter-spacing: 0.04em;
}
.settings-section-divider::after {
content: '';
flex: 1;
height: 1px;
margin-left: 10px;
background: var(--theme-control-border);
}
.settings-input-row > input {
width: min(220px, 100%);
text-align: left;
}
.settings-input-row input:focus,
.settings-add-row input:focus,
.settings-number-row input:focus,
.settings-number-input:focus,
.settings-compression-grid input:focus {
border-color: var(--text-secondary);
box-shadow: none;
}
/* width 用绝对值min(280px, 100%) 的百分比在 grid auto 列固有尺寸计算中会失效
导致列宽被内容chips 撑到 404pxlabel 列被挤成 0px 竖排 */
.settings-input-stack {
width: 280px;
max-width: 100%;
display: grid;
gap: 8px;
justify-self: end;
}
.settings-input-stack.wide {
width: 240px;
max-width: 100%;
}
.settings-input-stack input {
width: 100%;
}
.settings-chip-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.settings-chip-row.right {
justify-content: flex-end;
}
.settings-chip-row button {
border: 1px solid var(--theme-control-border);
border-radius: 999px;
background: transparent;
color: var(--text-primary);
min-height: 28px;
padding: 0 12px;
font-size: 12.5px;
cursor: pointer;
}
.settings-number-row button,
.settings-add-row button,
.settings-secondary-button,
.settings-primary-button {
border: 1px solid var(--theme-control-border);
border-radius: 8px;
background: transparent;
color: var(--text-primary);
height: 32px;
padding: 0 14px;
font-size: 13px;
cursor: pointer;
}
@media (hover: hover) {
.settings-chip-row button:hover,
.settings-number-row button:hover,
.settings-add-row button:hover,
.settings-secondary-button:hover {
background: var(--hover-bg);
}
}
.settings-chip-row button.active,
.settings-primary-button {
border-color: var(--accent);
background: var(--accent);
color: var(--on-accent);
}
.settings-secondary-button.danger {
color: var(--state-warning);
}
.settings-inline-actions {
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
flex-wrap: wrap;
}
.settings-inline-actions.right {
margin-top: 10px;
}
.settings-mini-status {
color: var(--text-secondary);
font-size: 12px;
}
.settings-mini-status.warning {
color: var(--state-warning);
}
.settings-add-row,
.settings-number-row {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
.settings-add-row input,
.settings-number-row input {
min-width: 0;
flex: 1 1 auto;
}
.settings-textarea-row {
min-height: 0;
border-bottom: 1px solid var(--theme-control-border);
display: flex;
flex-direction: column;
gap: 10px;
padding: 14px 0;
color: var(--text-primary);
}
.settings-textarea-row textarea {
width: 100%;
min-height: 96px;
max-height: 320px;
border: 1px solid var(--theme-control-border);
border-radius: 8px;
background: transparent;
color: var(--text-primary);
padding: 12px;
outline: none;
font-size: 13px;
line-height: 1.55;
resize: vertical;
overflow-y: auto;
font-family: inherit;
}
.settings-textarea-row textarea:focus {
border-color: var(--text-secondary);
box-shadow: none;
}
.settings-textarea-row textarea::placeholder {
color: var(--text-secondary);
opacity: 0.6;
}
.settings-toggle-row {
cursor: pointer;
position: relative;
}
.settings-toggle-row input[type='checkbox'] {
position: absolute;
opacity: 0;
pointer-events: none;
}
.settings-group-block {
border-bottom: 1px solid var(--theme-control-border);
padding: 14px 0 16px;
}
.settings-group-title {
margin-bottom: 6px;
}
.settings-toggle-row.inner {
min-height: 48px;
padding: 7px 0 7px 18px;
border-bottom: 0;
}
.settings-check-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0 26px;
}
.settings-compression-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px 14px;
margin-top: 10px;
}
.settings-compression-grid label {
display: grid;
gap: 5px;
color: var(--text-secondary);
font-size: 12px;
}
.settings-save-bar {
position: absolute;
right: 18px;
bottom: 0;
left: 0;
min-height: 54px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
background: transparent;
pointer-events: none;
}
.settings-save-bar > * {
pointer-events: auto;
}
.personal-status-group {
min-height: 20px;
font-size: 12px;
}
.status.success {
color: var(--state-success);
}
.status.error,
.usage-summary-error {
color: var(--state-warning);
}
/* 卡片常态最大宽度 648px以下断点均在此基础上进一步响应
窗口拖窄520px 卡片侧栏再收窄内容留白再压缩输入框收窄 */
@container settings-card (max-width: 520px) {
.settings-redesign-nav {
flex-basis: 164px;
}
.settings-redesign-content-header {
padding: 0 16px;
}
.settings-redesign-scroll {
padding-left: 16px;
padding-right: 16px;
}
.settings-select-row,
.settings-input-row,
.settings-list-row,
.settings-action-row,
.settings-toggle-row {
gap: 12px;
}
.settings-input-row > input {
width: min(180px, 100%);
}
.settings-input-stack,
.settings-input-stack.wide {
width: 220px;
max-width: 100%;
}
}
/* 极限挤压(手机全屏 / 窗口极窄输入类行上下堆叠控件全宽开关行保持同行iOS 设置式) */
@container settings-card (max-width: 440px) {
.settings-select-row,
.settings-input-row,
.settings-list-row,
.settings-action-row {
grid-template-columns: minmax(0, 1fr);
gap: 9px;
}
.settings-select-wrap,
.settings-input-stack,
.settings-input-stack.wide,
.settings-input-row > input {
justify-self: stretch;
width: 100%;
}
.settings-select-wrap .settings-select-button {
width: 100%;
justify-content: space-between;
}
.settings-check-grid,
.settings-compression-grid,
.usage-summary-grid--tokens,
.usage-summary-grid--counts {
grid-template-columns: 1fr;
}
}
/* ═══ 手机端 / 窄屏≤760px全屏 + iOS 设置式「列表 → 子页」导航 ═══ */
@media (max-width: 760px) {
/* 卡片全屏化由全局 _overlays.scss 同断点媒体查询负责(统一断点 760px */
/* 导航列表与内容子页并排,直接切换(触屏不做平移动画,避免看到上一屏滑走) */
.settings-redesign-layout {
width: 200%;
}
.settings-redesign-card.mobile-in-sub .settings-redesign-layout {
transform: translateX(-50%);
}
.settings-redesign-nav,
.settings-redesign-content {
flex: 0 0 50%;
width: 50%;
}
/* iOS 点击默认灰色高亮闪tap-highlight观感类似 hover 粘滞,禁用 */
.settings-redesign-tab {
-webkit-tap-highlight-color: transparent;
}
/* 窄屏布局下禁用选项 hover 桌面窄窗口 hover:hover 仍匹配
触屏已由 @media (hover: hover) 包裹兑底这里按布局模式再禁一层 */
.settings-redesign-tab:hover,
.settings-menu-option:hover {
background: transparent;
}
/* 移动端列表态不需要「当前选中项」常驻高亮iOS 设置式列表无此概念) */
.settings-redesign-tab.active {
background: transparent;
font-weight: inherit;
}
.settings-floating-menu.dark .settings-menu-option:hover,
html[data-theme='dark'] .settings-menu-option:hover,
body[data-theme='dark'] .settings-menu-option:hover {
background: transparent !important;
}
.settings-redesign-nav {
border-right: 0;
}
/* 桌面端头部隐藏,移动端顶栏接管 */
.settings-nav-head,
.settings-redesign-content-header {
display: none;
}
.settings-mobile-bar {
display: flex;
align-items: center;
gap: 4px;
flex: 0 0 auto;
padding: calc(8px + env(safe-area-inset-top, 0px)) 12px 8px;
border-bottom: 1px solid var(--theme-control-border);
min-height: 52px;
}
.settings-mobile-bar-title {
flex: 1 1 auto;
min-width: 0;
text-align: center;
font-size: 16px;
font-weight: 650;
letter-spacing: -0.01em;
color: var(--text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.settings-mobile-bar-cell {
width: 44px;
flex: 0 0 auto;
}
.settings-mobile-bar-btn {
height: 36px;
min-width: 36px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 2px;
padding: 0 6px;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--text-primary);
font-size: 15px;
cursor: pointer;
}
.settings-mobile-bar-btn:active {
background: var(--hover-bg);
}
.settings-mobile-bar-btn svg {
width: 20px;
height: 20px;
}
.settings-mobile-back {
color: var(--accent);
}
/* 导航变为分组列表样式48px 触控目标 + 右箭头 */
.settings-redesign-tabs {
padding: 8px 12px calc(20px + env(safe-area-inset-bottom, 0px));
gap: 3px;
}
.settings-redesign-tab {
height: 48px;
padding: 0 12px;
font-size: 15px;
border-radius: 10px;
gap: 12px;
}
.settings-tab-icon {
--icon-size: 20px;
}
.settings-tab-icon--chat {
width: 20px;
height: 20px;
}
.settings-tab-icon--chat svg {
width: 20px;
height: 20px;
}
.settings-tab-chevron {
display: block;
width: 16px;
height: 16px;
margin-left: auto;
flex: 0 0 auto;
color: var(--text-secondary);
}
.settings-redesign-scroll {
padding: 4px 16px calc(48px + env(safe-area-inset-bottom, 0px));
}
.settings-save-bar {
right: 16px;
}
.settings-floating-menu {
width: min(340px, calc(100vw - 48px));
}
}
/* ── 语音模型下载进度条 ── */

View File

@ -0,0 +1,82 @@
<script setup lang="ts">
import { inject } from 'vue';
defineOptions({ name: 'AdminTab' });
/**
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入
* 解构出的名称与主文件 script 顶层绑定一致模板可直接引用
*/
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
const {
openAdminPanel,
openApiAdmin,
openCustomTools,
openMcpConfig,
showMcpConfigEntry
} = ctx;
</script>
<template>
<section class="settings-page admin-monitor-page">
<div class="settings-action-row" v-if="showMcpConfigEntry">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.mcpConfigTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.mcpConfigDesc') }}</span
></span
><button
type="button"
class="settings-secondary-button"
@click="openMcpConfig"
>
{{ $t('personalization.open') }}
</button>
</div>
<div class="settings-action-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.adminMonitorTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.adminMonitorDesc') }}</span
></span
><button
type="button"
class="settings-secondary-button"
@click="openAdminPanel"
>
{{ $t('personalization.open') }}
</button>
</div>
<div class="settings-action-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.apiAdminTitle') }}</span
><span class="settings-row-desc">{{ $t('personalization.apiAdminDesc') }}</span></span
><button
type="button"
class="settings-secondary-button"
@click="openApiAdmin"
>
{{ $t('personalization.open') }}
</button>
</div>
<div class="settings-action-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.customToolsAdminTitle') }}</span
><span class="settings-row-desc">{{ $t('personalization.customToolsAdminDesc') }}</span></span
><button
type="button"
class="settings-secondary-button"
@click="openCustomTools"
>
{{ $t('personalization.open') }}
</button>
</div>
</section>
</template>
<style scoped>
.admin-monitor-page {
display: block;
padding: 0;
}
</style>

View File

@ -0,0 +1,375 @@
<script setup lang="ts">
import { inject } from 'vue';
import FancyCheck from '@/components/common/FancyCheck.vue';
defineOptions({ name: 'AppearanceTab' });
/**
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入
* 解构出的名称与主文件 script 顶层绑定一致模板可直接引用
*/
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
const {
activeDropdown,
activeTheme,
blockDisplayLabel,
blockDisplayOptions,
compactMessageDisplayLabel,
compactMessageDisplayOptions,
currentBlockDisplayMode,
currentCompactMessageDisplay,
floatingMenuStyle,
form,
handleMinimalExpandHeightLimitedChange,
handleStackedHideBordersChange,
locale,
localeLabel,
localeOptions,
minimalExpandHeightLimited,
personalization,
selectBlockDisplayMode,
selectCompactMessageDisplay,
selectLocaleOption,
selectThemeOption,
stackedHideBorders,
themeLabel,
themeOptions,
toggleDropdown
} = ctx;
</script>
<template>
<section class="settings-page">
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.themeTitle') }}</span
><span class="settings-row-desc">{{ $t('personalization.themeDesc') }}</span></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'theme' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('theme')"
>
{{ themeLabel }} <span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in themeOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{ selected: activeTheme === option.id }"
@click="selectThemeOption(option.id)"
>
<strong>{{ $t(option.labelKey) }}</strong
><span>{{ $t(option.descKey) }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('common.settingsLanguage') }}</span
><span class="settings-row-desc">{{
$t('common.settingsLanguageDesc')
}}</span></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'language' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('language')"
>
{{ localeLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in localeOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{ selected: locale === option.id }"
@click="selectLocaleOption(option.id)"
>
<strong>{{ $t(option.labelKey) }}</strong
><span>{{ $t(option.descKey) }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.groupByWorkspaceTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.groupByWorkspaceDesc') }}</span
></span
><input
type="checkbox"
:checked="form.group_sidebar_by_workspace"
@change="
personalization.updateField({
key: 'group_sidebar_by_workspace',
value: $event.target.checked
})
" /><FancyCheck :checked="form.group_sidebar_by_workspace" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.newChatBlankTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.newChatBlankDesc') }}</span
></span
><input
type="checkbox"
:checked="form.new_chat_button_behavior === 'route'"
@change="
personalization.updateField({
key: 'new_chat_button_behavior',
value: $event.target.checked ? 'route' : 'blank'
})
" /><FancyCheck :checked="form.new_chat_button_behavior === 'route'" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.useCustomNamesTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.useCustomNamesDesc') }}</span
></span
><input
type="checkbox"
:checked="form.use_custom_names"
@change="
personalization.updateField({
key: 'use_custom_names',
value: $event.target.checked
})
" /><FancyCheck :checked="form.use_custom_names" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.enhancedToolDisplayTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.enhancedToolDisplayDesc') }}</span
></span
><input
type="checkbox"
:checked="form.enhanced_tool_display"
@change="
personalization.updateField({
key: 'enhanced_tool_display',
value: $event.target.checked
})
" /><FancyCheck :checked="form.enhanced_tool_display" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.showStatusAvatarTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.showStatusAvatarDesc') }}</span
></span
><input
type="checkbox"
:checked="form.show_status_avatar"
@change="
personalization.updateField({
key: 'show_status_avatar',
value: $event.target.checked
})
" /><FancyCheck :checked="form.show_status_avatar" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.showGitStatusBarTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.showGitStatusBarDesc') }}</span
></span
><input
type="checkbox"
:checked="form.show_git_status_bar"
@change="
personalization.updateField({
key: 'show_git_status_bar',
value: $event.target.checked
})
" /><FancyCheck :checked="form.show_git_status_bar" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.autoOpenTerminalTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.autoOpenTerminalDesc') }}</span
></span
><input
type="checkbox"
:checked="form.auto_open_terminal_panel"
@change="
personalization.updateField({
key: 'auto_open_terminal_panel',
value: $event.target.checked
})
" /><FancyCheck :checked="form.auto_open_terminal_panel" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.quickDockAutoExpandTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.quickDockAutoExpandDesc') }}</span
></span
><input
type="checkbox"
:checked="form.quick_dock_auto_expand"
@change="
personalization.updateField({
key: 'quick_dock_auto_expand',
value: $event.target.checked
})
" /><FancyCheck :checked="form.quick_dock_auto_expand" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.editSummaryLiveTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.editSummaryLiveDesc') }}</span
></span
><input
type="checkbox"
:checked="form.edit_summary_live_display"
@change="
personalization.updateField({
key: 'edit_summary_live_display',
value: $event.target.checked
})
" /><FancyCheck :checked="form.edit_summary_live_display" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.filePreviewWrapTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.filePreviewWrapDesc') }}</span
></span
><input
type="checkbox"
:checked="form.file_preview_auto_wrap"
@change="
personalization.updateField({
key: 'file_preview_auto_wrap',
value: $event.target.checked
})
" /><FancyCheck :checked="form.file_preview_auto_wrap" /></label>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.blockDisplayModeTitle') }}</span
><span class="settings-row-desc">{{ $t('personalization.blockDisplayModeDesc') }}</span></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'block-display' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('block-display')"
>
{{ blockDisplayLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in blockDisplayOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{ selected: currentBlockDisplayMode === option.value }"
@click="selectBlockDisplayMode(option.value)"
>
<strong>{{ $t(option.labelKey) }}</strong
><span>{{ $t(option.descKey) }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<label
v-if="currentBlockDisplayMode === 'stacked'"
class="settings-toggle-row"
>
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.hideBlockBordersTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.hideBlockBordersDesc') }}</span>
</span>
<input
type="checkbox"
:checked="stackedHideBorders"
@change="handleStackedHideBordersChange($event)"
/>
<FancyCheck :checked="stackedHideBorders" />
</label>
<label
v-if="currentBlockDisplayMode === 'minimal'"
class="settings-toggle-row"
>
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.minimalExpandHeightTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.minimalExpandHeightDesc') }}</span>
</span>
<input
type="checkbox"
:checked="minimalExpandHeightLimited"
@change="handleMinimalExpandHeightLimitedChange($event)"
/>
<FancyCheck :checked="minimalExpandHeightLimited" />
</label>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.compactMessageTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.compactMessageDesc') }}</span
></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'compact-message' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('compact-message')"
>
{{ compactMessageDisplayLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in compactMessageDisplayOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{ selected: currentCompactMessageDisplay === option.value }"
@click="selectCompactMessageDisplay(option.value)"
>
<strong>{{ $t(option.labelKey) }}</strong
><span>{{ $t(option.descKey) }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
</section>
</template>

View File

@ -0,0 +1,203 @@
<script setup lang="ts">
import { inject } from 'vue';
import FancyCheck from '@/components/common/FancyCheck.vue';
defineOptions({ name: 'ContextTab' });
/**
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入
* 解构出的名称与主文件 script 顶层绑定一致模板可直接引用
*/
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
const {
commitProjectMemoryInjectLimitInput,
commitRecentConversationsPromptLimitInput,
form,
handleCompressionNumberInput,
handleProjectMemoryInjectLimitInput,
handleRecentConversationsPromptLimitInput,
personalization,
projectMemoryInjectLimitMin,
recentConversationsPromptLimitRange,
restoreCompressionDefaults,
restoreProjectMemoryInjectLimit,
restoreRecentConversationsPromptLimit
} = ctx;
</script>
<template>
<section class="settings-page">
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.recentConversationsTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.recentConversationsDesc') }}</span
></span
><input
type="checkbox"
:checked="form.recent_conversations_prompt_enabled"
@change="
personalization.updateField({
key: 'recent_conversations_prompt_enabled',
value: $event.target.checked
})
" /><FancyCheck :checked="form.recent_conversations_prompt_enabled" /></label>
<div
class="settings-input-row"
v-if="form.recent_conversations_prompt_enabled"
>
<span class="settings-row-title">{{ $t('personalization.recentConversationsCountTitle') }}</span>
<div class="settings-number-row">
<input
type="number"
:min="recentConversationsPromptLimitRange.min"
:max="recentConversationsPromptLimitRange.max"
:value="form.recent_conversations_prompt_limit"
@input="handleRecentConversationsPromptLimitInput"
@blur="commitRecentConversationsPromptLimitInput"
/><button type="button" @click="restoreRecentConversationsPromptLimit">{{ $t('personalization.restoreDefault') }}
</button>
</div>
</div>
<div class="settings-input-row">
<span class="settings-row-title">{{ $t('personalization.maxMemoryInjectTitle') }}</span>
<div class="settings-number-row">
<input
type="number"
:min="projectMemoryInjectLimitMin"
:value="form.project_memory_inject_limit ?? ''"
::placeholder="$t('personalization.noLimitPlaceholder')"
@input="handleProjectMemoryInjectLimitInput"
@blur="commitProjectMemoryInjectLimitInput"
/><button type="button" @click="restoreProjectMemoryInjectLimit">{{ $t('personalization.restoreDefault') }}
</button>
</div>
</div>
<div class="settings-group-block">
<div class="settings-group-title">
<span class="settings-row-title">{{ $t('personalization.compressionStrategyTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.compressionStrategyDesc') }}</span
>
</div>
<label class="settings-toggle-row inner"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.autoShallowCompressTitle') }}</span></span
><input
type="checkbox"
:checked="form.auto_shallow_compress_enabled"
@change="
personalization.updateField({
key: 'auto_shallow_compress_enabled',
value: $event.target.checked
})
" /><FancyCheck :checked="form.auto_shallow_compress_enabled" /></label>
<label class="settings-toggle-row inner"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.autoDeepCompressTitle') }}</span></span
><input
type="checkbox"
:checked="form.auto_deep_compress_enabled"
@change="
personalization.updateField({
key: 'auto_deep_compress_enabled',
value: $event.target.checked
})
" /><FancyCheck :checked="form.auto_deep_compress_enabled" /></label>
<div class="settings-compression-grid">
<label
><span>{{ $t('personalization.shallowTriggerTokens') }}</span
><input
type="number"
:value="form.shallow_compress_trigger_tokens ?? ''"
@input="
handleCompressionNumberInput(
'shallow_compress_trigger_tokens',
$event
)
"
/></label>
<label
><span>{{ $t('personalization.shallowKeepRecentTools') }}</span
><input
type="number"
:value="form.shallow_compress_keep_recent_tools ?? ''"
@input="
handleCompressionNumberInput(
'shallow_compress_keep_recent_tools',
$event
)
"
/></label>
<label
><span>{{ $t('personalization.shallowKeepTurnTools') }}</span
><input
type="number"
:value="form.shallow_compress_keep_user_turn_tools ?? ''"
@input="
handleCompressionNumberInput(
'shallow_compress_keep_user_turn_tools',
$event
)
"
/></label>
<label
><span>{{ $t('personalization.shallowMaxReplace') }}</span
><input
type="number"
:value="form.shallow_compress_max_replace_per_round ?? ''"
@input="
handleCompressionNumberInput(
'shallow_compress_max_replace_per_round',
$event
)
"
/></label>
<label
><span>{{ $t('personalization.shallowToolInterval') }}</span
><input
type="number"
:value="form.shallow_compress_trigger_tool_calls_interval ?? ''"
@input="
handleCompressionNumberInput(
'shallow_compress_trigger_tool_calls_interval',
$event
)
"
/></label>
<label
><span>{{ $t('personalization.deepTriggerTokens') }}</span
><input
type="number"
:value="form.deep_compress_trigger_tokens ?? ''"
@input="
handleCompressionNumberInput('deep_compress_trigger_tokens', $event)
"
/></label>
</div>
<label class="settings-toggle-row inner"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.deepCompressInjectTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.deepCompressInjectDesc') }}</span
></span
><input
type="checkbox"
:checked="form.deep_compress_form === 'inject'"
@change="
personalization.updateField({
key: 'deep_compress_form',
value: $event.target.checked ? 'inject' : 'file'
})
" /><FancyCheck :checked="form.deep_compress_form === 'inject'" /></label>
<div class="settings-inline-actions right">
<button
type="button"
class="settings-secondary-button"
@click="restoreCompressionDefaults"
>{{ $t('personalization.restoreCompressionDefaults') }}
</button>
</div>
</div>
</section>
</template>

View File

@ -0,0 +1,182 @@
<script setup lang="ts">
import { inject } from 'vue';
defineOptions({ name: 'DataTab' });
/**
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入
* 解构出的名称与主文件 script 顶层绑定一致模板可直接引用
*/
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
const {
personalization,
fetchUsageSummary,
formatTokenCount,
usageError,
usageLoading,
usageSummary,
usageUpdatedText
} = ctx;
</script>
<template>
<section
class="settings-page usage-summary-page"
data-tutorial="personal-page-usage"
>
<div class="usage-summary-card settings-stats-card">
<div class="usage-summary-header">
<div>
<p class="usage-summary-eyebrow">{{ $t('personalization.usageEyebrow') }}</p>
<h3>{{ $t('personalization.usageTitle') }}</h3>
<p class="usage-summary-desc">{{ $t('personalization.usageDesc') }}
</p>
</div>
</div>
<div class="usage-summary-grid usage-summary-grid--tokens">
<div class="usage-summary-item">
<div class="label">{{ $t('personalization.usageInputLabel') }}</div>
<div class="value value--success">
{{ formatTokenCount(usageSummary.total_input_tokens) }}
</div>
</div>
<div class="usage-summary-item">
<div class="label">{{ $t('personalization.usageOutputLabel') }}</div>
<div class="value value--warning">
{{ formatTokenCount(usageSummary.total_output_tokens) }}
</div>
</div>
</div>
<div class="usage-summary-grid usage-summary-grid--counts">
<div class="usage-summary-item">
<div class="label">{{ $t('personalization.usageConversationsLabel') }}</div>
<div class="value">
{{ formatTokenCount(usageSummary.total_conversations) }}
</div>
</div>
<div class="usage-summary-item">
<div class="label">{{ $t('personalization.usageMessagesLabel') }}</div>
<div class="value">
{{ formatTokenCount(usageSummary.total_user_messages) }}
</div>
</div>
<div class="usage-summary-item">
<div class="label">{{ $t('personalization.usageToolsLabel') }}</div>
<div class="value">
{{ formatTokenCount(usageSummary.total_tools) }}
</div>
</div>
</div>
<div class="usage-summary-meta">
<span v-if="usageError" class="usage-summary-error">{{ usageError }}</span
><span v-else-if="usageLoading">{{ $t('personalization.usageSyncing') }}</span
><span v-else>{{ $t('personalization.usageUpdated', { time: usageUpdatedText }) }}</span
><button
type="button"
class="usage-summary-refresh"
@click="fetchUsageSummary"
:disabled="usageLoading"
>
{{ usageLoading ? $t('personalization.usageRefreshing') : $t('personalization.usageRefresh') }}
</button>
</div>
</div>
</section>
</template>
<style scoped>
.usage-summary-page {
display: block;
padding: 0;
}
.settings-stats-card,
.usage-summary-card {
width: 100%;
max-width: 720px;
padding: 0;
min-height: 0;
display: flex;
flex-direction: column;
gap: 14px;
}
.usage-summary-header h3 {
margin: 4px 0 6px;
font-size: 20px;
}
.usage-summary-eyebrow {
margin: 0;
font-size: 11px;
color: var(--accent-strong);
letter-spacing: 0.16em;
text-transform: uppercase;
}
.usage-summary-desc,
.usage-summary-note,
.usage-summary-meta {
margin: 0;
color: var(--text-secondary);
font-size: 12px;
}
.usage-summary-grid {
display: grid;
gap: 10px;
}
.usage-summary-grid--tokens {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.usage-summary-grid--counts {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.usage-summary-item {
padding: 12px 14px;
border-radius: 14px;
border: 1px solid var(--theme-control-border);
background: transparent;
}
.usage-summary-item .label {
font-size: 12px;
color: var(--text-secondary);
}
.usage-summary-item .value {
margin-top: 4px;
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
}
.usage-summary-item .value--success {
color: var(--state-success);
}
.usage-summary-item .value--warning {
color: var(--state-warning);
}
.usage-summary-meta {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.usage-summary-refresh {
border: 1px solid var(--theme-control-border);
border-radius: 999px;
padding: 7px 14px;
background: transparent;
color: var(--text-primary);
font-size: 12px;
cursor: pointer;
}
</style>

View File

@ -0,0 +1,69 @@
<script setup lang="ts">
import { inject } from 'vue';
defineOptions({ name: 'FilesTab' });
/**
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入
* 解构出的名称与主文件 script 顶层绑定一致模板可直接引用
*/
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
const {
personalization,
form,
activeDropdown,
floatingMenuStyle,
imageCompressionLabel,
imageCompressionOptions,
selectImageCompression,
toggleDropdown,
activeTheme
} = ctx;
</script>
<template>
<section
class="settings-page"
data-tutorial="personal-page-image"
>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.imageCompressionTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.imageCompressionDesc') }}</span
></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'image' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('image')"
>
{{ imageCompressionLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in imageCompressionOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{ selected: form.image_compression === option.id }"
@click="selectImageCompression(option.id)"
>
<strong>{{ $t(option.labelKey) }}</strong
><span>{{ $t(option.descKey) }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
</section>
</template>

View File

@ -0,0 +1,101 @@
<script setup lang="ts">
import { inject } from 'vue';
import FancyCheck from '@/components/common/FancyCheck.vue';
defineOptions({ name: 'GeneralTab' });
/**
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入
* 解构出的名称与主文件 script 顶层绑定一致模板可直接引用
*/
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
const {
personalization,
form,
startTutorial,
isAppShell,
appCurrentVersionText,
appUpdateCheckedText,
appHasUpdate,
appUpdateStateText,
appUpdateChecking,
checkAppUpdate,
downloadLatestApp
} = ctx;
</script>
<template>
<section class="settings-page">
<label class="settings-toggle-row">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.autoTitleTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.autoTitleDesc') }}</span>
</span>
<input
type="checkbox"
:checked="form.auto_generate_title"
@change="
personalization.updateField({
key: 'auto_generate_title',
value: ($event.target as HTMLInputElement).checked
})
"
/>
<FancyCheck :checked="form.auto_generate_title" />
</label>
<div class="settings-action-row">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.tutorialTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.tutorialDesc') }}</span>
</span>
<button type="button" class="settings-secondary-button" @click="startTutorial">
{{ $t('personalization.tutorialButton') }}
</button>
</div>
<div class="settings-action-row" v-if="isAppShell">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.appUpdateTitle') }}</span>
<span class="settings-row-desc">{{
$t('personalization.appUpdateDesc', {
version: appCurrentVersionText,
checked: appUpdateCheckedText
})
}}</span>
</span>
<div class="settings-inline-actions">
<span class="settings-mini-status" :class="{ warning: appHasUpdate }">{{
appUpdateStateText
}}</span>
<button
type="button"
class="settings-secondary-button"
:disabled="appUpdateChecking"
@click="checkAppUpdate"
>
{{ appUpdateChecking ? $t('personalization.appChecking') : $t('personalization.appCheckUpdate') }}
</button>
<button
v-if="appHasUpdate"
type="button"
class="settings-primary-button"
@click="downloadLatestApp"
>
{{ $t('common.download') }}
</button>
</div>
</div>
<div class="settings-action-row danger-zone">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.logoutTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.logoutDesc') }}</span>
</span>
<button
type="button"
class="settings-secondary-button danger"
@click="personalization.logout()"
>
{{ $t('personalization.logoutTitle') }}
</button>
</div>
</section>
</template>

View File

@ -0,0 +1,159 @@
<script setup lang="ts">
import { inject } from 'vue';
defineOptions({ name: 'ModelTab' });
/**
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入
* 解构出的名称与主文件 script 顶层绑定一致模板可直接引用
*/
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
const {
personalization,
form,
activeDropdown,
defaultModelLabel,
filteredModelOptions,
floatingMenuStyle,
isEffortActive,
isRunModeActive,
reasoningEffortLabel,
runModeLabel,
selectDefaultModel,
selectDefaultReasoningEffort,
selectDefaultRunMode,
toggleDropdown,
runModeOptions,
reasoningEffortOptions,
activeTheme
} = ctx;
</script>
<template>
<section
class="settings-page"
data-tutorial="personal-page-model"
>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.defaultModelTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.defaultModelDesc') }}</span
></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'model' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('model')"
>
{{ defaultModelLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in filteredModelOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{
selected: form.default_model === option.value,
disabled: option.disabled
}"
:disabled="option.disabled"
@click="selectDefaultModel(option.value)"
>
<strong>{{ option.label }}</strong
><span>{{ option.disabled ? $t('personalization.modelDisabled') : option.desc }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.defaultRunModeTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.defaultRunModeDesc') }}</span
></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'run-mode' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('run-mode')"
>
{{ runModeLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in runModeOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{ selected: isRunModeActive(option.value) }"
@click="selectDefaultRunMode(option.value)"
>
<strong>{{ $t(option.labelKey) }}</strong
><span>{{ $t(option.descKey) }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.reasoningEffortTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.reasoningEffortDesc') }}</span
></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'reasoning-effort' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('reasoning-effort')"
>
{{ reasoningEffortLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in reasoningEffortOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{ selected: isEffortActive(option.value) }"
@click="selectDefaultReasoningEffort(option.value)"
>
<strong>{{ $t(option.labelKey) }}</strong
><span>{{ $t(option.descKey) }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
</section>
</template>

View File

@ -0,0 +1,276 @@
<script setup lang="ts">
import { inject } from 'vue';
import FancyCheck from '@/components/common/FancyCheck.vue';
defineOptions({ name: 'PreferencesTab' });
/**
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入
* 解构出的名称与主文件 script 顶层绑定一致模板可直接引用
*/
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
const {
personalization,
form,
tonePresets,
toggleUpdating,
activeDropdown,
toggleDropdown,
floatingMenuStyle,
activeTheme,
communicationStyleLabel,
conversationContinuityLabel,
currentBlockDisplayMode,
stackedHideBorders,
handleStackedHideBordersChange,
minimalExpandHeightLimited,
handleMinimalExpandHeightLimitedChange,
selectCommunicationStyle,
selectConversationContinuity
} = ctx;
</script>
<template>
<section
class="settings-page"
data-tutorial="personal-page-preferences"
>
<label class="settings-toggle-row">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.preferencesTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.preferencesDesc') }}</span>
</span>
<input
type="checkbox"
:checked="form.enabled"
:disabled="toggleUpdating"
@change="personalization.toggleEnabled()"
/>
<FancyCheck :checked="form.enabled" />
</label>
<label class="settings-input-row">
<span class="settings-row-title">{{ $t('personalization.selfIdentifyTitle') }}</span>
<input
type="text"
:value="form.self_identify"
maxlength="20"
:placeholder="$t('personalization.selfIdentifyPlaceholder')"
@input="
personalization.updateField({
key: 'self_identify',
value: $event.target.value
})
"
@focus="personalization.clearFeedback()"
/>
</label>
<label class="settings-input-row">
<span class="settings-row-title">{{ $t('personalization.userNameTitle') }}</span>
<input
type="text"
:value="form.user_name"
maxlength="20"
:placeholder="$t('personalization.userNamePlaceholder')"
@input="
personalization.updateField({
key: 'user_name',
value: $event.target.value
})
"
@focus="personalization.clearFeedback()"
/>
</label>
<label class="settings-input-row">
<span class="settings-row-title">{{ $t('personalization.professionTitle') }}</span>
<input
type="text"
:value="form.profession"
maxlength="20"
:placeholder="$t('personalization.professionPlaceholder')"
@input="
personalization.updateField({
key: 'profession',
value: $event.target.value
})
"
@focus="personalization.clearFeedback()"
/>
</label>
<div class="settings-input-row stackable">
<span class="settings-row-title">{{ $t('personalization.toneTitle') }}</span>
<div class="settings-input-stack">
<input
type="text"
:value="form.tone"
maxlength="20"
:placeholder="$t('personalization.tonePlaceholder')"
@input="
personalization.updateField({
key: 'tone',
value: $event.target.value
})
"
@focus="personalization.clearFeedback()"
/>
<div class="settings-chip-row">
<button
v-for="preset in tonePresets"
:key="preset"
type="button"
@click.prevent="personalization.applyTonePreset(preset)"
>
{{ preset }}
</button>
</div>
</div>
</div>
<div class="settings-textarea-row">
<div class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.considerationsTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.considerationsDesc') }}</span>
</div>
<textarea
:value="form.considerations"
rows="6"
maxlength="2000"
:placeholder="$t('personalization.considerationsPlaceholder')"
@input="personalization.updateConsiderations($event.target.value)"
@focus="personalization.clearFeedback()"
></textarea>
</div>
<label
v-if="currentBlockDisplayMode === 'stacked'"
class="settings-toggle-row"
>
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.hideBlockBordersTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.hideBlockBordersDesc') }}</span>
</span>
<input
type="checkbox"
:checked="stackedHideBorders"
@change="handleStackedHideBordersChange($event)"
/>
<FancyCheck :checked="stackedHideBorders" />
</label>
<label
v-if="currentBlockDisplayMode === 'minimal'"
class="settings-toggle-row"
>
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.minimalExpandHeightTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.minimalExpandHeightDesc') }}</span>
</span>
<input
type="checkbox"
:checked="minimalExpandHeightLimited"
@change="handleMinimalExpandHeightLimitedChange($event)"
/>
<FancyCheck :checked="minimalExpandHeightLimited" />
</label>
<div class="settings-select-row">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.communicationStyleTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.communicationStyleDesc') }}</span>
</span>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'communication' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('communication')"
>
{{ communicationStyleLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
type="button"
class="settings-menu-option"
:class="{ selected: form.communication_style === 'default' }"
@click="selectCommunicationStyle('default')"
>
<strong>{{ $t('personalization.communicationDefault') }}</strong><span>{{ $t('personalization.communicationDefaultDesc') }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
<button
type="button"
class="settings-menu-option"
:class="{ selected: form.communication_style === 'human_like' }"
@click="selectCommunicationStyle('human_like')"
>
<strong>{{ $t('personalization.communicationHumanLike') }}</strong><span>{{ $t('personalization.communicationHumanLikeDesc') }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
<button
type="button"
class="settings-menu-option"
:class="{ selected: form.communication_style === 'auto' }"
@click="selectCommunicationStyle('auto')"
>
<strong>{{ $t('personalization.communicationAuto') }}</strong><span>{{ $t('personalization.communicationAutoDesc') }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<div class="settings-select-row">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.conversationContinuityTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.conversationContinuityDesc') }}</span>
</span>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'conversation-continuity' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('conversation-continuity')"
>
{{ conversationContinuityLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
type="button"
class="settings-menu-option"
:class="{ selected: form.conversation_continuity === 'high' }"
@click="selectConversationContinuity('high')"
>
<strong>{{ $t('personalization.continuityHigh') }}</strong><span>{{ $t('personalization.continuityHighDesc') }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
<button
type="button"
class="settings-menu-option"
:class="{ selected: form.conversation_continuity === 'medium' }"
@click="selectConversationContinuity('medium')"
>
<strong>{{ $t('personalization.continuityMedium') }}</strong><span>{{ $t('personalization.continuityMediumDesc') }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
<button
type="button"
class="settings-menu-option"
:class="{ selected: form.conversation_continuity === 'low' }"
@click="selectConversationContinuity('low')"
>
<strong>{{ $t('personalization.continuityLow') }}</strong><span>{{ $t('personalization.continuityLowDesc') }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
</section>
</template>

View File

@ -0,0 +1,170 @@
<script setup lang="ts">
import { inject } from 'vue';
defineOptions({ name: 'ReviewAgentsTab' });
/**
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入
* 解构出的名称与主文件 script 顶层绑定一致模板可直接引用
*/
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
const {
activeDropdown,
activeTheme,
closeDropdown,
floatingMenuStyle,
reviewAgentDefs,
reviewAgentOf,
subAgentModels,
toggleDropdown,
updateReviewAgent,
updateReviewAgentInt
} = ctx;
</script>
<template>
<section class="settings-page">
<div class="settings-section-desc" style="margin: 0 0 16px; color: var(--text-secondary); font-size: 13px; line-height: 1.6">{{ $t('personalization.reviewIntro') }}
</div>
<template v-for="agent in reviewAgentDefs" :key="agent.key">
<div class="settings-section-divider">
<span class="settings-section-divider__label">{{ $t(agent.nameKey) }}</span>
</div>
<div style="margin: -4px 0 12px; color: var(--text-secondary); font-size: 12px; line-height: 1.5">
{{ $t(agent.descKey) }}
</div>
<div class="settings-select-row">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.modelTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.reviewModelEmptyDesc') }}</span>
</span>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === `review-model-${agent.key}` }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown(`review-model-${agent.key}`)"
>
{{ reviewAgentOf(agent.key).model || $t('personalization.defaultModelOption') }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown === `review-model-${agent.key}` ? floatingMenuStyle : undefined"
>
<button
type="button"
class="settings-menu-option"
:class="{ selected: !reviewAgentOf(agent.key).model }"
@click="updateReviewAgent(agent.key, { model: '' }); closeDropdown()"
>
<strong>{{ $t('personalization.defaultModelOption') }}</strong><span>{{ $t('personalization.reviewDefaultModelDesc') }}</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
<button
v-for="m in subAgentModels"
:key="m.name"
type="button"
class="settings-menu-option"
:class="{ selected: reviewAgentOf(agent.key).model === m.name }"
@click="updateReviewAgent(agent.key, { model: m.name }); closeDropdown()"
>
<strong>{{ m.name }}</strong><span>{{ m.modes }} · {{ m.multimodal || $t('personalization.textOnly') }}</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<div class="settings-select-row">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.thinkingModeTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.reviewThinkingDesc') }}</span>
</span>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === `review-thinking-${agent.key}` }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown(`review-thinking-${agent.key}`)"
>
{{ reviewAgentOf(agent.key).thinking ? 'thinking' : 'fast' }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown === `review-thinking-${agent.key}` ? floatingMenuStyle : undefined"
>
<button
type="button"
class="settings-menu-option"
:class="{ selected: !reviewAgentOf(agent.key).thinking }"
@click="updateReviewAgent(agent.key, { thinking: false }); closeDropdown()"
>
<strong>fast</strong><span>{{ $t('personalization.fastResponseMode') }}</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
<button
type="button"
class="settings-menu-option"
:class="{ selected: reviewAgentOf(agent.key).thinking }"
@click="updateReviewAgent(agent.key, { thinking: true }); closeDropdown()"
>
<strong>thinking</strong><span>{{ $t('personalization.thinkingReasoningMode') }}</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<div class="settings-select-row">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.timeoutTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.timeoutDesc') }}</span>
</span>
<input
type="number"
class="settings-number-input"
min="5"
max="3600"
:value="reviewAgentOf(agent.key).timeout_seconds"
@change="updateReviewAgentInt(agent.key, 'timeout_seconds', ($event.target as HTMLInputElement).value, 5, 3600)"
/>
</div>
<div class="settings-select-row">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.maxRoundsTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.maxRoundsDesc') }}</span>
</span>
<input
type="number"
class="settings-number-input"
min="1"
max="50"
:value="reviewAgentOf(agent.key).max_rounds"
@change="updateReviewAgentInt(agent.key, 'max_rounds', ($event.target as HTMLInputElement).value, 1, 50)"
/>
</div>
<div class="settings-select-row" style="margin-bottom: 8px">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.commandTimeoutTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.commandTimeoutDesc') }}</span>
</span>
<input
type="number"
class="settings-number-input"
min="1"
max="600"
:value="reviewAgentOf(agent.key).max_command_timeout"
@change="updateReviewAgentInt(agent.key, 'max_command_timeout', ($event.target as HTMLInputElement).value, 1, 600)"
/>
</div>
</template>
</section>
</template>

View File

@ -0,0 +1,442 @@
<script setup lang="ts">
import { inject } from 'vue';
defineOptions({ name: 'SubAgentsTab' });
/**
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入
* 解构出的名称与主文件 script 顶层绑定一致模板可直接引用
*/
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
const {
activeDropdown,
activeTheme,
closeDropdown,
closeRoleEditor,
deleteRole,
editingRole,
handleRoleEditorOverlayPressEnd,
handleRoleEditorOverlayPressStart,
openRoleEditor,
personalization,
roleEditorOpen,
roleForm,
roleSaving,
saveRole,
saveSubAgentSettings,
subAgentCompressThreshold,
subAgentMaxTurns,
subAgentModels,
subAgentRoles,
subAgentRolesLoading,
subAgentSettingsSaving,
toggleDropdown
} = ctx;
</script>
<template>
<section class="settings-page">
<div class="settings-section-desc" style="margin: 0 0 16px; color: var(--text-secondary); font-size: 13px; line-height: 1.6">{{ $t('personalization.subAgentsIntro') }}
</div>
<!-- 压缩阈值配置 -->
<div class="settings-action-row" style="margin-bottom: 16px">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.compressThresholdTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.compressThresholdDesc') }}
</span>
</span>
<div style="display: flex; gap: 6px; align-items: center">
<input
type="number"
class="settings-number-input"
v-model.number="subAgentCompressThreshold"
:min="10000"
:step="10000"
style="width: 120px"
/>
<button
type="button"
class="settings-secondary-button"
:disabled="subAgentSettingsSaving"
@click="saveSubAgentSettings"
>
{{ subAgentSettingsSaving ? $t('personalization.saving') : $t('common.save') }}
</button>
</div>
</div>
<!-- 最大执行轮次配置 -->
<div class="settings-action-row" style="margin-bottom: 16px">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.maxTurnsTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.maxTurnsDesc') }}
</span>
</span>
<div style="display: flex; gap: 6px; align-items: center">
<input
type="number"
class="settings-number-input"
v-model.number="subAgentMaxTurns"
:min="0"
:step="10"
placeholder="50"
style="width: 120px"
/>
<button
type="button"
class="settings-secondary-button"
:disabled="subAgentSettingsSaving"
@click="saveSubAgentSettings"
>
{{ subAgentSettingsSaving ? $t('personalization.saving') : $t('common.save') }}
</button>
</div>
</div>
<!-- 角色列表 -->
<div class="settings-section-header" style="margin-bottom: 8px">
<span class="settings-section-title">{{ $t('personalization.roleListTitle') }}</span>
<button
type="button"
class="settings-secondary-button"
@click="openRoleEditor()"
>
{{ $t('personalization.newRole') }}
</button>
</div>
<div v-if="subAgentRolesLoading" class="settings-empty-hint">{{ $t('common.loading') }}</div>
<div v-else-if="subAgentRoles.length === 0" class="settings-empty-hint">{{ $t('personalization.noRoles') }}
</div>
<div v-else class="sub-agent-role-list">
<div
v-for="role in subAgentRoles"
:key="role.role_id"
class="sub-agent-role-card"
>
<div class="sub-agent-role-info">
<div class="sub-agent-role-name">
{{ role.name }}
<span
class="sub-agent-role-tag"
:class="role.is_custom ? 'tag-custom' : 'tag-preset'"
>
{{ role.is_custom ? $t('personalization.customTag') : $t('personalization.presetTag') }}
</span>
</div>
<div class="sub-agent-role-desc">{{ role.description || $t('personalization.noDescription') }}</div>
<div class="sub-agent-role-meta">
{{ $t('personalization.roleMetaId', { id: role.role_id, mode: role.thinking_mode }) }}<template v-if="role.model_key">{{ $t('personalization.roleMetaModel', { model: role.model_key }) }}</template>
</div>
</div>
<div class="sub-agent-role-actions">
<button
type="button"
class="settings-secondary-button"
@click="openRoleEditor(role)"
>{{ $t('personalization.edit') }}</button>
<button
v-if="role.is_custom"
type="button"
class="settings-secondary-button danger"
@click="deleteRole(role)"
>{{ $t('common.delete') }}</button>
</div>
</div>
</div>
<!-- 角色编辑弹窗与个人空间同尺寸 -->
<transition name="personal-page-fade" appear>
<div
v-if="roleEditorOpen"
class="role-editor-drawer-overlay"
@click="closeDropdown"
@mousedown="handleRoleEditorOverlayPressStart"
@mouseup="handleRoleEditorOverlayPressEnd"
>
<div class="role-editor-drawer-card settings-redesign-card" @click.stop>
<div class="role-editor-header">
<h2>{{ editingRole ? $t('personalization.roleEditorEditTitle') : $t('personalization.roleEditorCreateTitle') }}</h2>
<button type="button" class="settings-secondary-button" @click="closeRoleEditor">{{ $t('common.close') }}</button>
</div>
<div class="role-editor-body" @click="closeDropdown">
<label class="settings-input-row">
<span class="settings-row-title">{{ $t('personalization.roleIdTitle') }}</span>
<input
v-if="!editingRole"
type="text"
:value="roleForm.role_id"
maxlength="40"
::placeholder="$t('personalization.roleIdPlaceholder')"
@input="roleForm.role_id = ($event.target as HTMLInputElement).value"
/>
<span v-else class="settings-row-desc" style="text-align: right; font-size: 14px;">{{ roleForm.role_id }}</span>
</label>
<label class="settings-input-row">
<span class="settings-row-title">{{ $t('personalization.roleNameTitle') }}</span>
<input
type="text"
:value="roleForm.name"
maxlength="40"
::placeholder="$t('personalization.roleNamePlaceholder')"
@input="roleForm.name = ($event.target as HTMLInputElement).value"
/>
</label>
<label class="settings-input-row">
<span class="settings-row-title">{{ $t('personalization.roleDescTitle') }}</span>
<input
type="text"
:value="roleForm.description"
maxlength="100"
::placeholder="$t('personalization.roleDescPlaceholder')"
@input="roleForm.description = ($event.target as HTMLInputElement).value"
/>
</label>
<div class="settings-select-row">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.thinkingModeTitle') }}</span>
<span class="settings-row-desc">{{ $t('personalization.roleThinkingDesc') }}</span>
</span>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'role-thinking' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('role-thinking')"
>
{{ roleForm.thinking_mode === 'thinking' ? 'thinking' : 'fast' }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown === 'role-thinking' ? floatingMenuStyle : undefined"
>
<button
type="button"
class="settings-menu-option"
:class="{ selected: roleForm.thinking_mode === 'fast' }"
@click="roleForm.thinking_mode = 'fast'; closeDropdown()"
>
<strong>fast</strong><span>{{ $t('personalization.fastResponseMode') }}</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
<button
type="button"
class="settings-menu-option"
:class="{ selected: roleForm.thinking_mode === 'thinking' }"
@click="roleForm.thinking_mode = 'thinking'; closeDropdown()"
>
<strong>thinking</strong><span>{{ $t('personalization.thinkingReasoningMode') }}</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<div class="settings-select-row">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.modelTitle') }}</span>>
<span class="settings-row-desc">{{ $t('personalization.roleModelEmptyDesc') }}</span>
</span>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'role-model' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('role-model')"
>
{{ roleForm.model_key || $t('personalization.defaultModelOption') }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown === 'role-model' ? floatingMenuStyle : undefined"
>
<button
type="button"
class="settings-menu-option"
:class="{ selected: !roleForm.model_key }"
@click="roleForm.model_key = ''; closeDropdown()"
>
<strong>{{ $t('personalization.defaultModelOption') }}</strong><span>{{ $t('personalization.roleDefaultModelDesc') }}</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
<button
v-for="m in subAgentModels"
:key="m.name"
type="button"
class="settings-menu-option"
:class="{ selected: roleForm.model_key === m.name }"
@click="roleForm.model_key = m.name; closeDropdown()"
>
<strong>{{ m.name }}</strong><span>{{ m.modes }} · {{ m.multimodal || $t('personalization.textOnly') }}</span><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<div class="settings-textarea-row">
<span class="settings-row-title">{{ $t('personalization.promptBodyTitle') }}</span>
<textarea
:value="roleForm.body_prompt"
::placeholder="$t('personalization.promptBodyPlaceholder')"
@input="roleForm.body_prompt = ($event.target as HTMLTextAreaElement).value"
></textarea>
</div>
</div>
<div class="role-editor-footer">
<button type="button" class="settings-secondary-button" @click="closeRoleEditor">{{ $t('common.cancel') }}</button>
<button
type="button"
class="settings-primary-button"
:disabled="roleSaving || !roleForm.role_id || !roleForm.name || !roleForm.body_prompt"
@click="saveRole"
>
{{ roleSaving ? $t('personalization.saving') : $t('common.save') }}
</button>
</div>
</div>
</div>
</transition>
</section>
</template>
<style scoped>
.sub-agent-role-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.sub-agent-role-card {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 11px 0;
border-bottom: 1px solid var(--theme-control-border);
}
.sub-agent-role-info {
flex: 1;
min-width: 0;
}
.sub-agent-role-name {
font-size: 14px;
font-weight: 550;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 6px;
}
.sub-agent-role-tag {
font-size: 10px;
padding: 1px 8px;
border-radius: 999px;
font-weight: 400;
}
.sub-agent-role-tag.tag-preset {
background: color-mix(in srgb, var(--text-secondary) 15%, transparent);
color: var(--text-secondary);
}
.sub-agent-role-tag.tag-custom {
background: color-mix(in srgb, var(--accent) 15%, transparent);
color: var(--accent);
}
.sub-agent-role-desc {
font-size: 12.5px;
color: var(--text-secondary);
margin-top: 3px;
line-height: 1.38;
}
.sub-agent-role-meta {
font-size: 11px;
color: var(--text-secondary);
opacity: 0.7;
margin-top: 2px;
}
.sub-agent-role-actions {
display: flex;
gap: 6px;
flex-shrink: 0;
}
/* 角色编辑弹窗 — 复用 settings-redesign-card */
.role-editor-drawer-overlay {
position: fixed;
inset: 0;
background: var(--overlay-scrim);
display: flex;
align-items: center;
justify-content: center;
z-index: 2147483647;
}
.role-editor-drawer-card {
display: flex;
flex-direction: column;
overflow: hidden;
width: min(648px, calc(100vw - 40px));
height: min(646px, calc(100vh - 40px));
height: min(646px, calc(100dvh - 40px));
padding: 22px 24px 18px;
border-radius: 14px;
background: var(--theme-surface-soft);
border: 1px solid var(--theme-control-border);
}
@media (max-width: 760px) {
.role-editor-drawer-card {
width: 100%;
height: 100vh;
height: 100dvh;
border-radius: 0;
border: 0;
padding: calc(14px + env(safe-area-inset-top, 0px)) 16px calc(18px + env(safe-area-inset-bottom, 0px));
}
}
.role-editor-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
flex-shrink: 0;
}
.role-editor-header h2 {
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.role-editor-body {
flex: 1;
overflow-y: auto;
overflow-x: visible;
min-height: 0;
scrollbar-width: none;
-ms-overflow-style: none;
}
.role-editor-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding-top: 12px;
flex-shrink: 0;
}
.settings-section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.settings-section-title {
font-size: 14px;
font-weight: 550;
color: var(--text-primary);
}
.settings-empty-hint {
color: var(--text-secondary);
font-size: 13px;
padding: 24px 0;
text-align: center;
}
</style>

View File

@ -0,0 +1,170 @@
<script setup lang="ts">
import { inject } from 'vue';
import FancyCheck from '@/components/common/FancyCheck.vue';
defineOptions({ name: 'ToolsTab' });
/**
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入
* 解构出的名称与主文件 script 顶层绑定一致模板可直接引用
*/
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
const {
form,
personalization,
skillsCatalog,
toggleCategory,
toolCategories
} = ctx;
</script>
<template>
<section class="settings-page">
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.silentToolDisableTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.silentToolDisableDesc') }}</span
></span
><input
type="checkbox"
:checked="form.silent_tool_disable"
@change="
personalization.updateField({
key: 'silent_tool_disable',
value: $event.target.checked
})
" /><FancyCheck :checked="form.silent_tool_disable" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.hideToolApprovalTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.hideToolApprovalDesc') }}</span
></span
><input
type="checkbox"
:checked="form.hide_tool_approval_panel"
@change="
personalization.updateField({
key: 'hide_tool_approval_panel',
value: $event.target.checked
})
" /><FancyCheck :checked="form.hide_tool_approval_panel" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.toolIntentTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.toolIntentDesc') }}</span
></span
><input
type="checkbox"
:checked="form.tool_intent_enabled"
@change="
personalization.updateField({
key: 'tool_intent_enabled',
value: $event.target.checked
})
" /><FancyCheck :checked="form.tool_intent_enabled" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.skillHintsTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.skillHintsDesc') }}</span
></span
><input
type="checkbox"
:checked="form.skill_hints_enabled"
@change="
personalization.updateField({
key: 'skill_hints_enabled',
value: $event.target.checked
})
" /><FancyCheck :checked="form.skill_hints_enabled" /></label>
<div class="settings-group-block">
<div class="settings-group-title">
<span class="settings-row-title">{{ $t('personalization.strictSkillTitle') }}</span
><span class="settings-row-desc">{{ $t('personalization.strictSkillDesc') }}</span>
</div>
<label class="settings-toggle-row inner"
><span class="settings-row-title">{{ $t('personalization.terminalToolsTitle') }}</span
><input
type="checkbox"
:checked="form.skill_strict_terminal_enabled"
@change="
personalization.updateField({
key: 'skill_strict_terminal_enabled',
value: $event.target.checked
})
" /><FancyCheck :checked="form.skill_strict_terminal_enabled" /></label>
<label class="settings-toggle-row inner"
><span class="settings-row-title">{{ $t('personalization.subAgentToolsTitle') }}</span
><input
type="checkbox"
:checked="form.skill_strict_sub_agent_enabled"
@change="
personalization.updateField({
key: 'skill_strict_sub_agent_enabled',
value: $event.target.checked
})
" /><FancyCheck :checked="form.skill_strict_sub_agent_enabled" /></label>
<label class="settings-toggle-row inner"
><span class="settings-row-title">{{ $t('personalization.runCommandFgTitle') }}</span
><input
type="checkbox"
:checked="form.skill_strict_run_command_foreground_enabled"
@change="
personalization.updateField({
key: 'skill_strict_run_command_foreground_enabled',
value: $event.target.checked
})
" /><FancyCheck :checked="form.skill_strict_run_command_foreground_enabled" /></label>
<label class="settings-toggle-row inner"
><span class="settings-row-title">{{ $t('personalization.runCommandBgTitle') }}</span
><input
type="checkbox"
:checked="form.skill_strict_run_command_background_enabled"
@change="
personalization.updateField({
key: 'skill_strict_run_command_background_enabled',
value: $event.target.checked
})
" /><FancyCheck :checked="form.skill_strict_run_command_background_enabled" /></label>
</div>
<div class="settings-group-block" v-if="skillsCatalog.length">
<div class="settings-group-title">
<span class="settings-row-title">{{ $t('personalization.availableSkillsTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.availableSkillsDesc') }}</span
>
</div>
<div class="settings-check-grid">
<label
v-for="skill in skillsCatalog"
:key="skill.id"
class="settings-toggle-row inner"
><span class="settings-row-title">{{ skill.label }}</span
><input
type="checkbox"
:checked="form.enabled_skills.includes(skill.id)"
@change="personalization.toggleSkill(skill.id)" /><FancyCheck :checked="form.enabled_skills.includes(skill.id)" /></label>
</div>
</div>
<div class="settings-group-block" v-if="toolCategories.length">
<div class="settings-group-title">
<span class="settings-row-title">{{ $t('personalization.disabledToolCategoriesTitle') }}</span
><span class="settings-row-desc">{{ $t('personalization.disabledToolCategoriesDesc') }}</span>
</div>
<div class="settings-check-grid">
<label
v-for="category in toolCategories"
:key="category.id"
class="settings-toggle-row inner"
><span class="settings-row-title">{{ category.label }}</span
><input
type="checkbox"
:checked="form.disabled_tool_categories.includes(category.id)"
@change="toggleCategory(category.id)" /><FancyCheck :checked="form.disabled_tool_categories.includes(category.id)" /></label>
</div>
</div>
</section>
</template>

View File

@ -0,0 +1,114 @@
<script setup lang="ts">
import { inject } from 'vue';
defineOptions({ name: 'VoiceTab' });
/**
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入
* 解构出的名称与主文件 script 顶层绑定一致模板可直接引用
*/
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
const {
deleteVoiceModel,
downloadVoiceModel,
voiceDownloadMsg,
voiceDownloadPercent,
voiceDownloading,
voiceModelPartial,
voiceModelReady
} = ctx;
</script>
<template>
<section class="settings-page">
<div class="settings-section" style="margin-bottom: 16px">
<p
class="settings-section-desc"
style="
margin: 0;
color: var(--text-secondary);
font-size: 13px;
line-height: 1.6;
"
>{{ $t('personalization.voiceModelIntro') }}
</p>
</div>
<div class="settings-action-row">
<span class="settings-row-copy">
<span class="settings-row-title">{{ $t('personalization.voiceModelTitle') }}</span>
<span class="settings-row-desc">
<template v-if="voiceModelReady">{{ $t('personalization.voiceModelReady') }}</template>
<template v-else-if="voiceDownloading"
>{{ $t('personalization.voiceModelDownloading', { percent: voiceDownloadPercent, msg: voiceDownloadMsg }) }}</template
>
<template v-else-if="voiceModelPartial"
>{{ $t('personalization.voiceModelPartial') }}</template
>
<template v-else>{{ $t('personalization.voiceModelNotDownloaded') }}</template>
</span>
</span>
<div style="display: flex; gap: 6px">
<button
type="button"
class="settings-secondary-button"
:disabled="voiceDownloading"
@click="downloadVoiceModel"
>
{{
voiceDownloading
? $t('personalization.voiceDownloadingBtn')
: voiceModelReady
? $t('personalization.voiceRedownload')
: $t('personalization.voiceDownloadModel')
}}
</button>
<button
v-if="voiceModelReady || voiceModelPartial"
type="button"
class="settings-secondary-button"
style="color: var(--state-error)"
:disabled="voiceDownloading"
@click="deleteVoiceModel"
>
{{ $t('common.delete') }}
</button>
</div>
</div>
<div
v-if="voiceDownloading"
class="voice-download-bar"
style="margin: 8px 0 0"
>
<div class="voice-download-track">
<div
class="voice-download-fill"
:style="{ width: voiceDownloadPercent + '%' }"
></div>
</div>
<span style="font-size: 11px; color: var(--text-secondary); margin-top: 4px"
>{{ voiceDownloadPercent }}%</span
>
</div>
</section>
</template>
<style scoped>
.voice-download-bar {
display: flex;
flex-direction: column;
gap: 4px;
}
.voice-download-track {
width: 100%;
height: 6px;
background: var(--surface-muted);
border-radius: 3px;
overflow: hidden;
}
.voice-download-fill {
height: 100%;
background: var(--accent-primary);
border-radius: 3px;
transition: width 0.3s ease;
}
</style>

View File

@ -0,0 +1,317 @@
<script setup lang="ts">
import { inject } from 'vue';
import FancyCheck from '@/components/common/FancyCheck.vue';
defineOptions({ name: 'WorkspaceTab' });
/**
* 共享上下文由 PersonalizationDrawer.vue 通过 provide 注入
* 解构出的名称与主文件 script 顶层绑定一致模板可直接引用
*/
const ctx = inject<Record<string, any>>('personalizationDrawer')!;
const {
activeDropdown,
floatingMenuStyle,
form,
goalTokenLimitEnabled,
permissionModeLabel,
personalization,
selectDefaultPermissionMode,
selectDefaultWorkMode,
selectVersioningBackupMode,
toggleDropdown,
toggleGoalTokenLimit,
versioningBackupModeLabel,
workModeLabel,
permissionModeOptions,
workModeOptions,
// activeTheme drawerContext
// clampGoalMaxTurns / clampGoalMaxTokens const drawerContext report.md
activeTheme,
clampGoalMaxTurns,
clampGoalMaxTokens
} = ctx;
</script>
<template>
<section class="settings-page" data-tutorial="personal-page-workspace">
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.defaultPermissionTitle') }}</span
><span class="settings-row-desc">{{ $t('personalization.defaultPermissionDesc') }}</span></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'permission' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('permission')"
>
{{ permissionModeLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in permissionModeOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{ selected: form.default_permission_mode === option.id }"
@click="selectDefaultPermissionMode(option.id)"
>
<strong>{{ $t(option.labelKey) }}</strong
><span>{{ $t(option.descKey) }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.defaultRunModeTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.workRunModeDesc') }}</span
></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'work-mode' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('work-mode')"
>
{{ workModeLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
v-for="option in workModeOptions"
:key="option.id"
type="button"
class="settings-menu-option"
:class="{ selected: form.default_work_mode === option.id }"
@click="selectDefaultWorkMode(option.id)"
>
<strong>{{ $t(option.labelKey) }}</strong
><span>{{ $t(option.descKey) }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.agentsMdInjectTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.agentsMdInjectDesc') }}</span
></span
><input
type="checkbox"
:checked="form.agents_md_auto_inject"
@change="
personalization.updateField({
key: 'agents_md_auto_inject',
value: $event.target.checked
})
" /><FancyCheck :checked="form.agents_md_auto_inject" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.claudeMdInjectTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.claudeMdInjectDesc') }}</span
></span
><input
type="checkbox"
:checked="form.claude_md_auto_inject"
@change="
personalization.updateField({
key: 'claude_md_auto_inject',
value: $event.target.checked
})
" /><FancyCheck :checked="form.claude_md_auto_inject" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.agentsSkillsScanTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.agentsSkillsScanDesc') }}</span
></span
><input
type="checkbox"
:checked="form.agents_skills_scan_enabled"
@change="
personalization.updateField({
key: 'agents_skills_scan_enabled',
value: $event.target.checked
})
" /><FancyCheck :checked="form.agents_skills_scan_enabled" /></label>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.modifyHistoryTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.modifyHistoryDesc') }}</span
></span
><input
type="checkbox"
:checked="form.modify_history_enabled"
@change="
personalization.updateField({
key: 'modify_history_enabled',
value: $event.target.checked
})
" /><FancyCheck :checked="form.modify_history_enabled" /></label>
<div class="settings-section-divider">
<span class="settings-section-divider__label">{{ $t('personalization.versionControlDivider') }}</span>
</div>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.versioningByDefaultTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.versioningByDefaultDesc') }}</span
></span
><input
type="checkbox"
:checked="form.versioning_enabled_by_default"
@change="
personalization.updateField({
key: 'versioning_enabled_by_default',
value: $event.target.checked
})
" /><FancyCheck :checked="form.versioning_enabled_by_default" /></label>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.backupModeTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.backupModeDesc') }}</span
></span
>
<div
class="settings-select-wrap"
:class="{ open: activeDropdown === 'versioning-backup-mode' }"
@click.stop
>
<button
type="button"
class="settings-select-button"
@click="toggleDropdown('versioning-backup-mode')"
>
{{ versioningBackupModeLabel }}
<span class="select-chevron" aria-hidden="true"></span>
</button>
<div
:class="['settings-floating-menu', { dark: activeTheme === 'dark' }]"
:style="activeDropdown ? floatingMenuStyle : undefined"
>
<button
type="button"
class="settings-menu-option"
:class="{ selected: form.versioning_backup_mode === 'shallow' }"
@click="selectVersioningBackupMode('shallow')"
>
<strong>{{ $t('personalization.shallowBackup') }}</strong><span>{{ $t('personalization.shallowBackupDesc') }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
<button
type="button"
class="settings-menu-option"
:class="{ selected: form.versioning_backup_mode === 'full' }"
@click="selectVersioningBackupMode('full')"
>
<strong>{{ $t('personalization.fullBackup') }}</strong><span>{{ $t('personalization.fullBackupDesc') }}</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button>
</div>
</div>
</div>
<div class="settings-section-divider">
<span class="settings-section-divider__label">{{ $t('personalization.goalModeDivider') }}</span>
</div>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.goalReviewActiveTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.goalReviewActiveDesc') }}</span
></span
><input
type="checkbox"
:checked="form.goal_review_mode === 'active'"
@change="
personalization.updateField({
key: 'goal_review_mode',
value: $event.target.checked ? 'active' : 'readonly'
})
" /><FancyCheck :checked="form.goal_review_mode === 'active'" /></label>
<div class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.goalMaxTurnsTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.goalMaxTurnsDesc') }}</span
></span
>
<input
type="number"
class="settings-number-input"
min="1"
max="100"
:value="form.goal_max_turns"
@change="
personalization.updateField({
key: 'goal_max_turns',
value: clampGoalMaxTurns($event.target.value)
})
"
/>
</div>
<label class="settings-toggle-row"
><span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.goalTokenLimitTitle') }}</span
><span class="settings-row-desc"
>{{ $t('personalization.goalTokenLimitDesc') }}</span
></span
><input
type="checkbox"
:checked="goalTokenLimitEnabled"
@change="toggleGoalTokenLimit($event.target.checked)" /><FancyCheck :checked="goalTokenLimitEnabled" /></label>
<div v-if="goalTokenLimitEnabled" class="settings-select-row">
<span class="settings-row-copy"
><span class="settings-row-title">{{ $t('personalization.goalTokenLimitValueTitle') }}</span
><span class="settings-row-desc">{{ $t('personalization.goalTokenLimitValueDesc') }}</span></span
>
<input
type="number"
class="settings-number-input"
min="1000"
step="1000"
:value="form.goal_max_tokens || 100000"
@change="
personalization.updateField({
key: 'goal_max_tokens',
value: clampGoalMaxTokens($event.target.value)
})
"
/>
</div>
</section>
</template>

View File

@ -28,6 +28,8 @@ export default {
tool: 'Tool',
versioning: 'Versioning',
unset: 'Not set',
settings: 'Settings',
back: 'Back',
settingsLanguage: 'Language',
settingsLanguageDesc: 'Choose the display language',

View File

@ -30,6 +30,8 @@ export default {
tool: '工具',
versioning: '版本管理',
unset: '未设置',
settings: '设置',
back: '返回',
settingsLanguage: '界面语言',
settingsLanguageDesc: '选择界面显示语言',

View File

@ -15,14 +15,14 @@
}
.personal-page-card {
width: min(96vw, 1020px);
height: calc(100vh - 40px);
max-height: 760px;
background: var(--theme-surface-card);
border-radius: 24px;
width: min(648px, calc(100vw - 40px));
height: min(646px, calc(100vh - 40px));
height: min(646px, calc(100dvh - 40px));
background: var(--theme-surface-soft);
border-radius: 14px;
border: 1px solid var(--theme-control-border);
box-shadow: var(--theme-shadow-strong);
padding: 40px;
box-shadow: none;
padding: 0;
text-align: left;
color: var(--text-primary);
display: flex;
@ -329,11 +329,13 @@
}
}
/* 手机端完整适配 */
@media (max-width: 768px) {
/* 手机端完整适配(与个人空间组件移动端断点 760px 统一) */
@media (max-width: 760px) {
.personal-page-overlay {
padding: 0;
align-items: stretch;
/* 盖过移动端顶栏 mobile-panel-trigger (1500) 及其下拉菜单 (1600) */
z-index: 1700;
}
.personal-page-card {
@ -341,8 +343,6 @@
height: 100%;
max-height: none;
border-radius: 0;
padding: 16px;
padding-top: 60px;
display: flex;
flex-direction: column;
}
@ -2555,6 +2555,21 @@
transform: translateY(-26px);
}
/* 触屏760px不做内容切换过渡mode=out-in 的离场动画在手机上观感拖沓
必须放在原规则之后同特异性后者胜
离场元素直接 display:none窄屏下 transition 组件为默认同时模式
旧内容瞬间消失新内容瞬间出现不会先闪旧内容 */
@media (max-width: 760px) {
.personal-page-vertical-enter-active,
.personal-page-vertical-leave-active {
transition: none;
}
.personal-page-vertical-leave-active {
display: none;
}
}
.personal-page-fade-enter-active,
.personal-page-fade-leave-active {
transition: