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:
parent
1ca5e15f8e
commit
f5bfab4056
File diff suppressed because it is too large
Load Diff
937
static/src/components/personalization/styles/settings-shared.css
Normal file
937
static/src/components/personalization/styles/settings-shared.css
Normal 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 排)撑到 404px、label 列被挤成 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 语音模型下载进度条 ── */
|
||||||
82
static/src/components/personalization/tabs/AdminTab.vue
Normal file
82
static/src/components/personalization/tabs/AdminTab.vue
Normal 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>
|
||||||
375
static/src/components/personalization/tabs/AppearanceTab.vue
Normal file
375
static/src/components/personalization/tabs/AppearanceTab.vue
Normal 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>
|
||||||
203
static/src/components/personalization/tabs/ContextTab.vue
Normal file
203
static/src/components/personalization/tabs/ContextTab.vue
Normal 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>
|
||||||
182
static/src/components/personalization/tabs/DataTab.vue
Normal file
182
static/src/components/personalization/tabs/DataTab.vue
Normal 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>
|
||||||
69
static/src/components/personalization/tabs/FilesTab.vue
Normal file
69
static/src/components/personalization/tabs/FilesTab.vue
Normal 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>
|
||||||
101
static/src/components/personalization/tabs/GeneralTab.vue
Normal file
101
static/src/components/personalization/tabs/GeneralTab.vue
Normal 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>
|
||||||
159
static/src/components/personalization/tabs/ModelTab.vue
Normal file
159
static/src/components/personalization/tabs/ModelTab.vue
Normal 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>
|
||||||
276
static/src/components/personalization/tabs/PreferencesTab.vue
Normal file
276
static/src/components/personalization/tabs/PreferencesTab.vue
Normal 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>
|
||||||
170
static/src/components/personalization/tabs/ReviewAgentsTab.vue
Normal file
170
static/src/components/personalization/tabs/ReviewAgentsTab.vue
Normal 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>
|
||||||
442
static/src/components/personalization/tabs/SubAgentsTab.vue
Normal file
442
static/src/components/personalization/tabs/SubAgentsTab.vue
Normal 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>
|
||||||
170
static/src/components/personalization/tabs/ToolsTab.vue
Normal file
170
static/src/components/personalization/tabs/ToolsTab.vue
Normal 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>
|
||||||
114
static/src/components/personalization/tabs/VoiceTab.vue
Normal file
114
static/src/components/personalization/tabs/VoiceTab.vue
Normal 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>
|
||||||
317
static/src/components/personalization/tabs/WorkspaceTab.vue
Normal file
317
static/src/components/personalization/tabs/WorkspaceTab.vue
Normal 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>
|
||||||
@ -28,6 +28,8 @@ export default {
|
|||||||
tool: 'Tool',
|
tool: 'Tool',
|
||||||
versioning: 'Versioning',
|
versioning: 'Versioning',
|
||||||
unset: 'Not set',
|
unset: 'Not set',
|
||||||
|
settings: 'Settings',
|
||||||
|
back: 'Back',
|
||||||
settingsLanguage: 'Language',
|
settingsLanguage: 'Language',
|
||||||
settingsLanguageDesc: 'Choose the display language',
|
settingsLanguageDesc: 'Choose the display language',
|
||||||
|
|
||||||
|
|||||||
@ -30,6 +30,8 @@ export default {
|
|||||||
tool: '工具',
|
tool: '工具',
|
||||||
versioning: '版本管理',
|
versioning: '版本管理',
|
||||||
unset: '未设置',
|
unset: '未设置',
|
||||||
|
settings: '设置',
|
||||||
|
back: '返回',
|
||||||
settingsLanguage: '界面语言',
|
settingsLanguage: '界面语言',
|
||||||
settingsLanguageDesc: '选择界面显示语言',
|
settingsLanguageDesc: '选择界面显示语言',
|
||||||
|
|
||||||
|
|||||||
@ -15,14 +15,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.personal-page-card {
|
.personal-page-card {
|
||||||
width: min(96vw, 1020px);
|
width: min(648px, calc(100vw - 40px));
|
||||||
height: calc(100vh - 40px);
|
height: min(646px, calc(100vh - 40px));
|
||||||
max-height: 760px;
|
height: min(646px, calc(100dvh - 40px));
|
||||||
background: var(--theme-surface-card);
|
background: var(--theme-surface-soft);
|
||||||
border-radius: 24px;
|
border-radius: 14px;
|
||||||
border: 1px solid var(--theme-control-border);
|
border: 1px solid var(--theme-control-border);
|
||||||
box-shadow: var(--theme-shadow-strong);
|
box-shadow: none;
|
||||||
padding: 40px;
|
padding: 0;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -329,11 +329,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 手机端完整适配 */
|
/* 手机端完整适配(与个人空间组件移动端断点 760px 统一) */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 760px) {
|
||||||
.personal-page-overlay {
|
.personal-page-overlay {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
/* 盖过移动端顶栏 mobile-panel-trigger (1500) 及其下拉菜单 (1600) */
|
||||||
|
z-index: 1700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.personal-page-card {
|
.personal-page-card {
|
||||||
@ -341,8 +343,6 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
padding: 16px;
|
|
||||||
padding-top: 60px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@ -2555,6 +2555,21 @@
|
|||||||
transform: translateY(-26px);
|
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-enter-active,
|
||||||
.personal-page-fade-leave-active {
|
.personal-page-fade-leave-active {
|
||||||
transition:
|
transition:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user