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',
|
||||
versioning: 'Versioning',
|
||||
unset: 'Not set',
|
||||
settings: 'Settings',
|
||||
back: 'Back',
|
||||
settingsLanguage: 'Language',
|
||||
settingsLanguageDesc: 'Choose the display language',
|
||||
|
||||
|
||||
@ -30,6 +30,8 @@ export default {
|
||||
tool: '工具',
|
||||
versioning: '版本管理',
|
||||
unset: '未设置',
|
||||
settings: '设置',
|
||||
back: '返回',
|
||||
settingsLanguage: '界面语言',
|
||||
settingsLanguageDesc: '选择界面显示语言',
|
||||
|
||||
|
||||
@ -15,14 +15,14 @@
|
||||
}
|
||||
|
||||
.personal-page-card {
|
||||
width: min(96vw, 1020px);
|
||||
height: calc(100vh - 40px);
|
||||
max-height: 760px;
|
||||
background: var(--theme-surface-card);
|
||||
border-radius: 24px;
|
||||
width: min(648px, calc(100vw - 40px));
|
||||
height: min(646px, calc(100vh - 40px));
|
||||
height: min(646px, calc(100dvh - 40px));
|
||||
background: var(--theme-surface-soft);
|
||||
border-radius: 14px;
|
||||
border: 1px solid var(--theme-control-border);
|
||||
box-shadow: var(--theme-shadow-strong);
|
||||
padding: 40px;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
@ -329,11 +329,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* 手机端完整适配 */
|
||||
@media (max-width: 768px) {
|
||||
/* 手机端完整适配(与个人空间组件移动端断点 760px 统一) */
|
||||
@media (max-width: 760px) {
|
||||
.personal-page-overlay {
|
||||
padding: 0;
|
||||
align-items: stretch;
|
||||
/* 盖过移动端顶栏 mobile-panel-trigger (1500) 及其下拉菜单 (1600) */
|
||||
z-index: 1700;
|
||||
}
|
||||
|
||||
.personal-page-card {
|
||||
@ -341,8 +343,6 @@
|
||||
height: 100%;
|
||||
max-height: none;
|
||||
border-radius: 0;
|
||||
padding: 16px;
|
||||
padding-top: 60px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@ -2555,6 +2555,21 @@
|
||||
transform: translateY(-26px);
|
||||
}
|
||||
|
||||
/* 触屏(≤760px)不做内容切换过渡:mode=out-in 的离场动画在手机上观感拖沓。
|
||||
必须放在原规则之后(同特异性后者胜)。
|
||||
离场元素直接 display:none:窄屏下 transition 组件为默认同时模式,
|
||||
旧内容瞬间消失、新内容瞬间出现,不会先闪旧内容。 */
|
||||
@media (max-width: 760px) {
|
||||
.personal-page-vertical-enter-active,
|
||||
.personal-page-vertical-leave-active {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.personal-page-vertical-leave-active {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.personal-page-fade-enter-active,
|
||||
.personal-page-fade-leave-active {
|
||||
transition:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user