feat(frontend): 内容图片全站接入灯箱预览,关闭按钮统一为 CloseButton 组件
- 新增 document 级事件委托(bootstrap.ts setupImagePreviewDelegation), show_image 卡片 / Markdown 正文图 / view_image / ocr_image 工具结果 统一支持点击灯箱预览;可预览图片加 zoom-in 光标 - show_file 图片卡片由打开新标签页改为灯箱预览,并解除 Android 端禁用 - CitationPopover 引用缩略图支持点击预览 - renderShowFileCard 独立 app 实例补挂 i18n 插件(修复 $t 不可用隐患) - 新建 common/CloseButton.vue(boxed 圆角方 hover 底 / bare 裸字形双变体), 替换全站 25 处形态各异的关闭按钮(圆形/圆角方/裸字符/macOS 红点等 8 种风格), 修复 ImagePicker/VideoPicker/路径授权弹窗原本无 hover 反馈的问题 - showHtmlFullscreen 顶栏按钮 CSS 对齐 boxed 形态(动态 DOM 无法复用组件) - 清理 10+ 份散落重复的关闭按钮旧样式与 .mobile-overlay-close 死代码 Co-authored-by: Astrion powered by Kimi-K3 <astrion-agent@users.noreply.github.com>
This commit is contained in:
parent
e6d8f194d2
commit
b3158913ef
@ -49,7 +49,7 @@
|
|||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<div v-if="banner.message" class="banner" :class="banner.type">
|
<div v-if="banner.message" class="banner" :class="banner.type">
|
||||||
<span>{{ banner.message }}</span>
|
<span>{{ banner.message }}</span>
|
||||||
<button type="button" class="banner-close" @click="banner.message = ''">×</button>
|
<CloseButton :label="$t('common.close')" @click="banner.message = ''" />
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
@ -300,6 +300,7 @@ import { computed, reactive, ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
|||||||
import { useSecondaryPass } from './useSecondaryPass';
|
import { useSecondaryPass } from './useSecondaryPass';
|
||||||
import SecondaryGate from './SecondaryGate.vue';
|
import SecondaryGate from './SecondaryGate.vue';
|
||||||
import FancyCheck from '@/components/common/FancyCheck.vue';
|
import FancyCheck from '@/components/common/FancyCheck.vue';
|
||||||
|
import CloseButton from '@/components/common/CloseButton.vue';
|
||||||
import { t, currentLocale } from '@/locales';
|
import { t, currentLocale } from '@/locales';
|
||||||
|
|
||||||
type TargetType = 'global' | 'role' | 'user' | 'invite';
|
type TargetType = 'global' | 'role' | 'user' | 'invite';
|
||||||
@ -1061,14 +1062,6 @@ onBeforeUnmount(() => {
|
|||||||
border-color: rgba(189, 93, 58, 0.35);
|
border-color: rgba(189, 93, 58, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner-close {
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: rgba(255, 255, 255, 0.9);
|
||||||
border: 1px solid rgba(118, 103, 84, 0.2);
|
border: 1px solid rgba(118, 103, 84, 0.2);
|
||||||
|
|||||||
@ -2,8 +2,9 @@
|
|||||||
import katex from 'katex';
|
import katex from 'katex';
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import { createApp } from 'vue';
|
import { createApp } from 'vue';
|
||||||
import { t } from '@/locales';
|
import { t, i18n } from '@/locales';
|
||||||
import ShowFileCard from '../components/chat/ShowFileCard.vue';
|
import ShowFileCard from '../components/chat/ShowFileCard.vue';
|
||||||
|
import { useUiStore } from '@/stores/ui';
|
||||||
import { buildShowHtmlIframeSrcdoc } from '../utils/showHtmlSandbox';
|
import { buildShowHtmlIframeSrcdoc } from '../utils/showHtmlSandbox';
|
||||||
import {
|
import {
|
||||||
openShowHtmlFullscreen,
|
openShowHtmlFullscreen,
|
||||||
@ -174,11 +175,16 @@ function normalizeShowImageSrc(src: string) {
|
|||||||
trimmed = trimmed.replace(/^\/(?=[A-Za-z]:\/)/, '');
|
trimmed = trimmed.replace(/^\/(?=[A-Za-z]:\/)/, '');
|
||||||
}
|
}
|
||||||
if (trimmed.startsWith('/user_upload/')) return trimmed;
|
if (trimmed.startsWith('/user_upload/')) return trimmed;
|
||||||
// 兼容容器内部路径:/workspace/.../user_upload/xxx.png 或 /workspace/user_upload/xxx
|
// 兼容容器内部绝对路径:/workspace/.../user_upload/xxx.png 或 /workspace/user_upload/xxx
|
||||||
|
// 注意:.astrion/user_upload/xxx 是工作区相对路径,不在此特判,
|
||||||
|
// 让它落到下方通用分支走 /api/file/content(与 _validate_path 同源解析,
|
||||||
|
// 避免依赖 /user_upload/ 后端路由在宿主机免登录模式下的工作区解析)
|
||||||
|
if (trimmed.startsWith('/')) {
|
||||||
const idx = trimmed.toLowerCase().indexOf('/user_upload/');
|
const idx = trimmed.toLowerCase().indexOf('/user_upload/');
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
return '/user_upload/' + trimmed.slice(idx + '/user_upload/'.length);
|
return '/user_upload/' + trimmed.slice(idx + '/user_upload/'.length);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// /workspace 前缀是容器内绝对路径写法,剥掉后按工作区相对路径处理
|
// /workspace 前缀是容器内绝对路径写法,剥掉后按工作区相对路径处理
|
||||||
if (trimmed === '/workspace') return '';
|
if (trimmed === '/workspace') return '';
|
||||||
if (trimmed.startsWith('/workspace/')) {
|
if (trimmed.startsWith('/workspace/')) {
|
||||||
@ -1256,6 +1262,36 @@ function buildNodePathKey(node: Element) {
|
|||||||
return parts.reverse().join('/');
|
return parts.reverse().join('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 内容图片点击预览(document 级事件委托) =====
|
||||||
|
// 覆盖三类无 Vue 事件绑定的注入图片:
|
||||||
|
// 1. <show_image> 卡片(figure.chat-inline-image,renderShowImages 动态创建)
|
||||||
|
// 2. Markdown ![]() 正文图片(.text-content 内的 img)
|
||||||
|
// 3. 工具结果图片(.tool-result-image,如 view_image / ocr_image)
|
||||||
|
// 不会误伤:消息气泡图片与 CitationPopover 缩略图已用 @click.stop 阻断冒泡;
|
||||||
|
// ShowFileCard 卡片自带 @click,在此显式排除;头像/图标为 SVG 或不在命中容器内。
|
||||||
|
let imagePreviewDelegationBound = false;
|
||||||
|
|
||||||
|
export function setupImagePreviewDelegation() {
|
||||||
|
if (imagePreviewDelegationBound) return;
|
||||||
|
imagePreviewDelegationBound = true;
|
||||||
|
document.addEventListener('click', (event) => {
|
||||||
|
const target = event.target as HTMLElement | null;
|
||||||
|
if (!target || typeof target.closest !== 'function') return;
|
||||||
|
const img = target.closest('img') as HTMLImageElement | null;
|
||||||
|
if (!img) return;
|
||||||
|
// ShowFileCard 内部已自行打开灯箱预览,避免双重触发
|
||||||
|
if (img.closest('.show-file-card')) return;
|
||||||
|
const hit =
|
||||||
|
img.closest('figure.chat-inline-image') ||
|
||||||
|
img.closest('.tool-result-image') ||
|
||||||
|
img.closest('.text-content');
|
||||||
|
if (!hit) return;
|
||||||
|
const url = img.currentSrc || img.src;
|
||||||
|
if (!url) return;
|
||||||
|
useUiStore().openImagePreview({ url, name: img.alt || '' });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function setupShowImageObserver() {
|
export function setupShowImageObserver() {
|
||||||
if (showImageObserver || showContainerObserver) return;
|
if (showImageObserver || showContainerObserver) return;
|
||||||
setupLayoutDebugObservers();
|
setupLayoutDebugObservers();
|
||||||
@ -1471,6 +1507,9 @@ function renderShowFileCard(node: Element) {
|
|||||||
node.replaceChildren(mountEl);
|
node.replaceChildren(mountEl);
|
||||||
|
|
||||||
const app = createApp(ShowFileCard, { path });
|
const app = createApp(ShowFileCard, { path });
|
||||||
|
// 独立 createApp 实例不继承主应用的插件,必须显式安装 i18n,
|
||||||
|
// 否则组件模板里的 $t 不可用(报 "j.$t is not a function")
|
||||||
|
app.use(i18n);
|
||||||
app.mount(mountEl);
|
app.mount(mountEl);
|
||||||
showFileAppMap.set(node, app);
|
showFileAppMap.set(node, app);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
import { useChatActionStore } from '../stores/chatActions';
|
import { useChatActionStore } from '../stores/chatActions';
|
||||||
import { useSandboxSetupStore } from '../stores/sandboxSetup';
|
import { useSandboxSetupStore } from '../stores/sandboxSetup';
|
||||||
import { normalizeScrollLock } from '../composables/useScrollControl';
|
import { normalizeScrollLock } from '../composables/useScrollControl';
|
||||||
import { setupShowImageObserver, teardownShowImageObserver } from './bootstrap';
|
import { setupShowImageObserver, teardownShowImageObserver, setupImagePreviewDelegation } from './bootstrap';
|
||||||
import { debugLog } from './methods/common';
|
import { debugLog } from './methods/common';
|
||||||
|
|
||||||
export function created() {
|
export function created() {
|
||||||
@ -41,6 +41,7 @@ export async function mounted() {
|
|||||||
normalizeScrollLock(this);
|
normalizeScrollLock(this);
|
||||||
});
|
});
|
||||||
setupShowImageObserver();
|
setupShowImageObserver();
|
||||||
|
setupImagePreviewDelegation();
|
||||||
|
|
||||||
// 立即加载初始数据(并行获取状态,优先同步运行模式)
|
// 立即加载初始数据(并行获取状态,优先同步运行模式)
|
||||||
const initialDataPromise = this.loadInitialData();
|
const initialDataPromise = this.loadInitialData();
|
||||||
|
|||||||
@ -27,7 +27,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="single.url" class="pop-url">{{ single.url }}</div>
|
<div v-if="single.url" class="pop-url">{{ single.url }}</div>
|
||||||
<div v-if="isImageFile(single)" class="pop-image">
|
<div v-if="isImageFile(single)" class="pop-image">
|
||||||
<img :src="fileContentUrl(single)" :alt="single.file_name || ''" loading="lazy" />
|
<img
|
||||||
|
:src="fileContentUrl(single)"
|
||||||
|
:alt="single.file_name || ''"
|
||||||
|
loading="lazy"
|
||||||
|
@click.stop="openCitationPreview(single)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="displaySnippet(single)" class="pop-snippet">“{{ displaySnippet(single) }}”</div>
|
<div v-else-if="displaySnippet(single)" class="pop-snippet">“{{ displaySnippet(single) }}”</div>
|
||||||
</div>
|
</div>
|
||||||
@ -132,6 +137,14 @@ function fileContentUrl(ann: CitationAnnotation): string {
|
|||||||
return `/api/file/content?path=${encodeURIComponent(ann.file_path || '')}`;
|
return `/api/file/content?path=${encodeURIComponent(ann.file_path || '')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 点击引用图片缩略图:打开全局灯箱预览 */
|
||||||
|
function openCitationPreview(ann: CitationAnnotation) {
|
||||||
|
uiStore.openImagePreview({
|
||||||
|
url: fileContentUrl(ann),
|
||||||
|
name: ann.file_name || ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function shortDomain(domain?: string) {
|
function shortDomain(domain?: string) {
|
||||||
return (domain || '').replace(/^www\./, '');
|
return (domain || '').replace(/^www\./, '');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
||||||
import { useUiStore } from '@/stores/ui';
|
import { useUiStore } from '@/stores/ui';
|
||||||
|
import CloseButton from '@/components/common/CloseButton.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
summary: any;
|
summary: any;
|
||||||
@ -342,15 +343,12 @@ function lineNumber(line: any): string {
|
|||||||
<span class="edit-summary-plus">+{{ activeFile.added || 0 }}</span>
|
<span class="edit-summary-plus">+{{ activeFile.added || 0 }}</span>
|
||||||
<span class="edit-summary-minus">-{{ activeFile.removed || 0 }}</span>
|
<span class="edit-summary-minus">-{{ activeFile.removed || 0 }}</span>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<CloseButton
|
||||||
v-if="pinned || isMobile"
|
v-if="pinned || isMobile"
|
||||||
type="button"
|
:label="$t('common.close')"
|
||||||
class="es-modal__close"
|
size="sm"
|
||||||
:aria-label="$t('common.close')"
|
|
||||||
@click.stop="close"
|
@click.stop="close"
|
||||||
>
|
/>
|
||||||
<span class="icon icon-sm" :style="props.iconStyle('x')" aria-hidden="true"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="es-modal__body">
|
<div class="es-modal__body">
|
||||||
<template v-if="(activeFile.lines || []).length">
|
<template v-if="(activeFile.lines || []).length">
|
||||||
@ -539,26 +537,6 @@ function lineNumber(line: any): string {
|
|||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
.es-modal__close {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
flex: none;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.es-modal__close:hover {
|
|
||||||
background: var(--surface-muted);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.es-modal__body {
|
.es-modal__body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|||||||
@ -62,7 +62,7 @@
|
|||||||
:class="{ 'sfc-image--android': isAndroidApp }"
|
:class="{ 'sfc-image--android': isAndroidApp }"
|
||||||
:src="contentUrl"
|
:src="contentUrl"
|
||||||
:alt="displayName"
|
:alt="displayName"
|
||||||
@click="!isAndroidApp && openFullImage()"
|
@click="openFullImage()"
|
||||||
@error="onImageError"
|
@error="onImageError"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@ -82,6 +82,7 @@ import { buildShowHtmlIframeSrcdoc } from '@/utils/showHtmlSandbox';
|
|||||||
import { openShowHtmlFullscreen } from '@/utils/showHtmlFullscreen';
|
import { openShowHtmlFullscreen } from '@/utils/showHtmlFullscreen';
|
||||||
import PdfPreview from './PdfPreview.vue';
|
import PdfPreview from './PdfPreview.vue';
|
||||||
import { t } from '@/locales';
|
import { t } from '@/locales';
|
||||||
|
import { useUiStore } from '@/stores/ui';
|
||||||
|
|
||||||
// 文本类文件内容缓存,避免父组件反复重建卡片时重复 fetch 导致闪烁
|
// 文本类文件内容缓存,避免父组件反复重建卡片时重复 fetch 导致闪烁
|
||||||
const SHOW_FILE_CONTENT_CACHE = new Map<string, { content: string; ts: number }>();
|
const SHOW_FILE_CONTENT_CACHE = new Map<string, { content: string; ts: number }>();
|
||||||
@ -430,7 +431,9 @@ async function copyContent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openFullImage() {
|
function openFullImage() {
|
||||||
if (contentUrl.value) window.open(contentUrl.value, '_blank');
|
if (!contentUrl.value) return;
|
||||||
|
// 全站统一走全局灯箱预览(Android WebView 同样适用,不再新开标签页)
|
||||||
|
useUiStore().openImagePreview({ url: contentUrl.value, name: displayName.value || '' });
|
||||||
}
|
}
|
||||||
|
|
||||||
function onImageError() {
|
function onImageError() {
|
||||||
|
|||||||
@ -768,7 +768,9 @@ function renderViewImage(result: any, args: any): string {
|
|||||||
|
|
||||||
if (imageUrl) {
|
if (imageUrl) {
|
||||||
const safeUrl = escapeHtml(imageUrl);
|
const safeUrl = escapeHtml(imageUrl);
|
||||||
html += `<div class="tool-result-image"><a href="${safeUrl}" target="_blank" rel="noopener" title="${escapeHtml(t('toolResults.media.viewImgTitle'))}"><img src="${safeUrl}" alt="${escapeHtml(t('toolResults.media.viewImgAlt'))}" loading="lazy" /></a></div>`;
|
// 不再用 <a target="_blank"> 打开新标签页,直接渲染 <img>,
|
||||||
|
// 由聊天消息区的事件委托统一承接点击灯箱预览。
|
||||||
|
html += `<div class="tool-result-image"><img src="${safeUrl}" alt="${escapeHtml(t('toolResults.media.viewImgAlt'))}" loading="lazy" /></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
|
|||||||
@ -25,16 +25,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<span class="qd-preview__name" :title="previewPath">{{ fileName }}</span>
|
<span class="qd-preview__name" :title="previewPath">{{ fileName }}</span>
|
||||||
<span class="qd-preview__path" :title="previewPath">{{ dirName }}</span>
|
<span class="qd-preview__path" :title="previewPath">{{ dirName }}</span>
|
||||||
<button class="qd-preview__close" :title="$t('common.close')" @click="close">
|
<CloseButton :label="$t('common.close')" :title="$t('common.close')" @click="close" />
|
||||||
<svg viewBox="0 0 16 16" fill="none">
|
|
||||||
<path
|
|
||||||
d="M4 4l8 8M12 4l-8 8"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="1.4"
|
|
||||||
stroke-linecap="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</header>
|
</header>
|
||||||
<div class="qd-preview__body">
|
<div class="qd-preview__body">
|
||||||
<div v-if="loading" class="qd-preview__loading">{{ $t('common.loading') }}</div>
|
<div v-if="loading" class="qd-preview__loading">{{ $t('common.loading') }}</div>
|
||||||
@ -70,6 +61,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
|
import CloseButton from '@/components/common/CloseButton.vue';
|
||||||
import { t } from '@/locales';
|
import { t } from '@/locales';
|
||||||
import { useQuickDockStore } from '@/stores/quickDock';
|
import { useQuickDockStore } from '@/stores/quickDock';
|
||||||
import { usePersonalizationStore } from '@/stores/personalization';
|
import { usePersonalizationStore } from '@/stores/personalization';
|
||||||
|
|||||||
@ -9,16 +9,9 @@
|
|||||||
<span class="qd-detail__title" :title="title">{{ title }}</span>
|
<span class="qd-detail__title" :title="title">{{ title }}</span>
|
||||||
<span class="qd-detail__badge" :class="`is-${stateClass}`">{{ statusText }}</span>
|
<span class="qd-detail__badge" :class="`is-${stateClass}`">{{ statusText }}</span>
|
||||||
<span v-if="tokensText" class="qd-detail__tokens" :title="tokensTitle">{{ tokensText }}</span>
|
<span v-if="tokensText" class="qd-detail__tokens" :title="tokensTitle">{{ tokensText }}</span>
|
||||||
<button class="qd-detail__close" :title="$t('common.close')" @click="close">
|
<span class="qd-detail__close">
|
||||||
<svg viewBox="0 0 16 16" fill="none">
|
<CloseButton :label="$t('common.close')" :title="$t('common.close')" @click="close" />
|
||||||
<path
|
</span>
|
||||||
d="M4 4l8 8M12 4l-8 8"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="1.4"
|
|
||||||
stroke-linecap="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</header>
|
</header>
|
||||||
<div ref="bodyRef" class="qd-detail__body" :class="{ 'body-fade': bodyFading }">
|
<div ref="bodyRef" class="qd-detail__body" :class="{ 'body-fade': bodyFading }">
|
||||||
<!-- 子智能体:工具调用 + 文本输出时间线 -->
|
<!-- 子智能体:工具调用 + 文本输出时间线 -->
|
||||||
|
|||||||
@ -581,30 +581,8 @@
|
|||||||
.qd-detail__close {
|
.qd-detail__close {
|
||||||
flex: none;
|
flex: none;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
width: 26px;
|
|
||||||
height: 26px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
|
||||||
padding: 0;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
cursor: pointer;
|
|
||||||
transition:
|
|
||||||
background 0.12s ease,
|
|
||||||
color 0.12s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qd-detail__close svg {
|
|
||||||
width: 13px;
|
|
||||||
height: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qd-detail__close:hover {
|
|
||||||
background: var(--tab-active);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.qd-detail__body {
|
.qd-detail__body {
|
||||||
@ -879,34 +857,6 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qd-preview__close {
|
|
||||||
flex: none;
|
|
||||||
width: 26px;
|
|
||||||
height: 26px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
cursor: pointer;
|
|
||||||
transition:
|
|
||||||
background 0.12s ease,
|
|
||||||
color 0.12s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qd-preview__close svg {
|
|
||||||
width: 13px;
|
|
||||||
height: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qd-preview__close:hover {
|
|
||||||
background: var(--tab-active);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.qd-preview__body {
|
.qd-preview__body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|||||||
131
static/src/components/common/CloseButton.vue
Normal file
131
static/src/components/common/CloseButton.vue
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 全局统一关闭按钮(X)——两种变体,全站所有「关闭」语义按钮统一引用。
|
||||||
|
*
|
||||||
|
* 变体:
|
||||||
|
* - boxed(默认):用于一切「有容器承载」场景(窗口/弹窗/抽屉/侧边栏/面板/通知卡片)。
|
||||||
|
* 28×28px(sm=24×24px),圆角 7px(sm=6px),透明底,hover/active 出现 --hover-bg 底色块。
|
||||||
|
* - bare:用于「悬空无容器」场景(当前仅 ImageLightbox 灯箱),无背景/无圆角/无底色块,
|
||||||
|
* 仅字形变色;颜色通过 `--close-btn-color` / `--close-btn-color-hover` CSS 变量由父容器覆盖。
|
||||||
|
*
|
||||||
|
* 接口:
|
||||||
|
* - variant: 'boxed' | 'bare'(默认 boxed)
|
||||||
|
* - size: 'sm' | 'md'(默认 md=28px)
|
||||||
|
* - label: aria-label 文案(必传,调用方给 i18n 文案)
|
||||||
|
* - emit: click
|
||||||
|
*
|
||||||
|
* 字形复用项目图标机制 iconStyle('x')(等价 VersioningDialog 中的
|
||||||
|
* `<span class="icon icon-sm" :style="iconStyle('x')" />`),通过在组件内直接由 ICONS
|
||||||
|
* 构造 `--icon-src` 的 style 对象实现,无需依赖父组件透传 iconStyle prop。
|
||||||
|
*/
|
||||||
|
import { ICONS } from '../../utils/icons';
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
variant?: 'boxed' | 'bare';
|
||||||
|
size?: 'sm' | 'md';
|
||||||
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
variant: 'boxed',
|
||||||
|
size: 'md',
|
||||||
|
disabled: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(e: 'click', evt: MouseEvent): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 等价 iconStyle('x'):mask 使用 --icon-src 指向 x.svg,字形即图标
|
||||||
|
const iconStyle = { '--icon-src': `url(${ICONS.x})` };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="close-btn"
|
||||||
|
:class="[`close-btn--${variant}`, `close-btn--${size}`]"
|
||||||
|
:aria-label="label"
|
||||||
|
:disabled="disabled"
|
||||||
|
v-bind="$attrs"
|
||||||
|
@click="$emit('click', $event)"
|
||||||
|
>
|
||||||
|
<span class="icon icon-sm" :style="iconStyle" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.close-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: none;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== boxed:有容器承载 ===== */
|
||||||
|
.close-btn--boxed {
|
||||||
|
--_size: 28px;
|
||||||
|
--_radius: 7px;
|
||||||
|
width: var(--_size);
|
||||||
|
height: var(--_size);
|
||||||
|
border-radius: var(--_radius);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
transition:
|
||||||
|
background 140ms ease,
|
||||||
|
color 140ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn--boxed.close-btn--sm {
|
||||||
|
--_size: 24px;
|
||||||
|
--_radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
.close-btn--boxed:hover {
|
||||||
|
background: var(--hover-bg);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn--boxed:active {
|
||||||
|
background: var(--hover-bg);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== bare:悬空无容器 ===== */
|
||||||
|
.close-btn--bare {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
color: var(--close-btn-color);
|
||||||
|
transition: color 140ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 默认配色用 :where() 零特异性声明:父容器(如 ImageLightbox)在同元素上覆盖
|
||||||
|
--close-btn-color / --close-btn-color-hover 时始终能赢过默认值。
|
||||||
|
注意:禁止写成 var(--x, 兜底) 形式,会被 stylelint 颜色规则拦截。 */
|
||||||
|
:where(.close-btn--bare) {
|
||||||
|
--close-btn-color: var(--text-secondary);
|
||||||
|
--close-btn-color-hover: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn--bare .icon {
|
||||||
|
--icon-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
.close-btn--bare:hover {
|
||||||
|
color: var(--close-btn-color-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -6,7 +6,7 @@
|
|||||||
<div class="subagent-activity-title">
|
<div class="subagent-activity-title">
|
||||||
{{ $t('overlay.bgCommandTitle', { id: activeCommand.command_id }) }}
|
{{ $t('overlay.bgCommandTitle', { id: activeCommand.command_id }) }}
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="subagent-activity-close" @click="close">×</button>
|
<CloseButton :label="$t('common.close')" @click="close" />
|
||||||
</div>
|
</div>
|
||||||
<div class="subagent-activity-meta">
|
<div class="subagent-activity-meta">
|
||||||
<span
|
<span
|
||||||
@ -49,6 +49,7 @@
|
|||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { t } from '@/locales';
|
import { t } from '@/locales';
|
||||||
|
import CloseButton from '@/components/common/CloseButton.vue';
|
||||||
import { useBackgroundCommandStore } from '@/stores/backgroundCommand';
|
import { useBackgroundCommandStore } from '@/stores/backgroundCommand';
|
||||||
|
|
||||||
const commandStore = useBackgroundCommandStore();
|
const commandStore = useBackgroundCommandStore();
|
||||||
|
|||||||
@ -10,16 +10,12 @@
|
|||||||
<div v-if="generatedPath" class="hint" :title="generatedPath">
|
<div v-if="generatedPath" class="hint" :title="generatedPath">
|
||||||
{{ $t('overlay.generatedHint', { path: generatedPath }) }}
|
{{ $t('overlay.generatedHint', { path: generatedPath }) }}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<CloseButton
|
||||||
type="button"
|
:label="$t('common.close')"
|
||||||
class="icon-close-btn"
|
|
||||||
:aria-label="$t('common.close')"
|
|
||||||
:title="$t('common.close')"
|
:title="$t('common.close')"
|
||||||
@click="$emit('close')"
|
|
||||||
:disabled="submitting"
|
:disabled="submitting"
|
||||||
>
|
@click="$emit('close')"
|
||||||
<span class="icon icon-sm" :style="iconStyle('x')" aria-hidden="true"></span>
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -127,6 +123,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import CloseButton from '@/components/common/CloseButton.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'ConversationReviewDialog' });
|
defineOptions({ name: 'ConversationReviewDialog' });
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@ -256,32 +254,6 @@ const formatUpdatedAt = (value: string | number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 自定义关闭按钮,固定尺寸、视觉居中 */
|
/* 自定义关闭按钮,固定尺寸、视觉居中 */
|
||||||
.icon-close-btn {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 9px;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
cursor: pointer;
|
|
||||||
transition:
|
|
||||||
background 140ms ease,
|
|
||||||
color 140ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-close-btn:hover:not(:disabled) {
|
|
||||||
background: var(--theme-tab-active);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-close-btn:disabled {
|
|
||||||
opacity: 0.45;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 主体:左右两栏,靠竖线分隔 ===== */
|
/* ===== 主体:左右两栏,靠竖线分隔 ===== */
|
||||||
.review-body {
|
.review-body {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
<div class="subagent-activity-title">
|
<div class="subagent-activity-title">
|
||||||
{{ isDone ? $t('overlay.goalDoneTitle') : isStopped ? $t('overlay.goalStoppedTitle') : $t('overlay.goalRunningTitle') }}
|
{{ isDone ? $t('overlay.goalDoneTitle') : isStopped ? $t('overlay.goalStoppedTitle') : $t('overlay.goalRunningTitle') }}
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="subagent-activity-close" @click="$emit('close')">×</button>
|
<CloseButton :label="$t('common.close')" @click="$emit('close')" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="goal-progress-meta">
|
<div class="goal-progress-meta">
|
||||||
@ -49,6 +49,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
||||||
import { t, currentLocale } from '@/locales';
|
import { t, currentLocale } from '@/locales';
|
||||||
|
import CloseButton from '@/components/common/CloseButton.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'GoalProgressDialog' });
|
defineOptions({ name: 'GoalProgressDialog' });
|
||||||
|
|
||||||
|
|||||||
@ -9,14 +9,12 @@
|
|||||||
:aria-label="preview.name || $t('overlay.imagePreview')"
|
:aria-label="preview.name || $t('overlay.imagePreview')"
|
||||||
@click.self="close"
|
@click.self="close"
|
||||||
>
|
>
|
||||||
<button
|
<CloseButton
|
||||||
type="button"
|
variant="bare"
|
||||||
class="image-lightbox__close"
|
class="image-lightbox__close"
|
||||||
:aria-label="$t('overlay.closePreview')"
|
:label="$t('overlay.closePreview')"
|
||||||
@click.stop="close"
|
@click="close"
|
||||||
>
|
/>
|
||||||
×
|
|
||||||
</button>
|
|
||||||
<div class="image-lightbox__stage" @click.self="close">
|
<div class="image-lightbox__stage" @click.self="close">
|
||||||
<img
|
<img
|
||||||
class="image-lightbox__img"
|
class="image-lightbox__img"
|
||||||
@ -34,6 +32,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, watch } from 'vue';
|
import { computed, onBeforeUnmount, watch } from 'vue';
|
||||||
import { useUiStore } from '@/stores/ui';
|
import { useUiStore } from '@/stores/ui';
|
||||||
|
import CloseButton from '@/components/common/CloseButton.vue';
|
||||||
|
|
||||||
const uiStore = useUiStore();
|
const uiStore = useUiStore();
|
||||||
const preview = computed(() => uiStore.imagePreview);
|
const preview = computed(() => uiStore.imagePreview);
|
||||||
@ -125,28 +124,15 @@ onBeforeUnmount(() => syncLock(false));
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* bare 变体:按钮本体样式在 CloseButton 组件内,此处只负责灯箱场景的定位与配色覆盖。
|
||||||
|
灯箱遮罩永远为深色,颜色不随主题变量;默认约 72% 透明度的白,hover 全亮。 */
|
||||||
.image-lightbox__close {
|
.image-lightbox__close {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(10px + env(safe-area-inset-top, 0px));
|
top: calc(10px + env(safe-area-inset-top, 0px));
|
||||||
right: calc(10px + env(safe-area-inset-right, 0px));
|
right: calc(10px + env(safe-area-inset-right, 0px));
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
width: 36px;
|
--close-btn-color: color-mix(in srgb, var(--lightbox-text) 72%, transparent);
|
||||||
height: 36px;
|
--close-btn-color-hover: var(--lightbox-text);
|
||||||
border-radius: 50%;
|
|
||||||
border: none;
|
|
||||||
background: var(--lightbox-btn-bg);
|
|
||||||
color: var(--lightbox-text);
|
|
||||||
font-size: 20px;
|
|
||||||
line-height: 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-lightbox__close:hover {
|
|
||||||
background: var(--lightbox-btn-bg-hover);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.lightbox-fade-enter-active,
|
.lightbox-fade-enter-active,
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
@change="onLocalChange"
|
@change="onLocalChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button class="close-btn" @click="close">×</button>
|
<CloseButton :label="$t('common.close')" @click="close" />
|
||||||
</div>
|
</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div v-if="loading" class="loading">{{ $t('common.loading') }}</div>
|
<div v-if="loading" class="loading">{{ $t('common.loading') }}</div>
|
||||||
@ -57,6 +57,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch, onMounted, withDefaults } from 'vue';
|
import { computed, ref, watch, onMounted, withDefaults } from 'vue';
|
||||||
|
import CloseButton from '@/components/common/CloseButton.vue';
|
||||||
|
|
||||||
interface ImageEntry {
|
interface ImageEntry {
|
||||||
name: string;
|
name: string;
|
||||||
@ -195,13 +196,6 @@ onMounted(() => {
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
.close-btn {
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
border: none;
|
|
||||||
font-size: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.body {
|
.body {
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<div class="overlay-card">
|
<div class="overlay-card">
|
||||||
<div class="overlay-header">
|
<div class="overlay-header">
|
||||||
<h3>{{ $t('overlay.pathAuthTitle') }}</h3>
|
<h3>{{ $t('overlay.pathAuthTitle') }}</h3>
|
||||||
<button type="button" @click="$emit('close')">×</button>
|
<CloseButton :label="$t('common.close')" @click="$emit('close')" />
|
||||||
</div>
|
</div>
|
||||||
<div class="mode-switch">
|
<div class="mode-switch">
|
||||||
<button
|
<button
|
||||||
@ -51,6 +51,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import CloseButton from '@/components/common/CloseButton.vue';
|
||||||
|
|
||||||
defineProps<{ open: boolean; value: string; mode: 'writable' | 'readable'; saving?: boolean }>();
|
defineProps<{ open: boolean; value: string; mode: 'writable' | 'readable'; saving?: boolean }>();
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
(e: 'close'): void;
|
(e: 'close'): void;
|
||||||
@ -65,7 +67,6 @@ defineEmits<{
|
|||||||
.overlay-card { width: min(680px, 92vw); background: var(--theme-surface-soft); border: 1px solid var(--theme-control-border); border-radius: 12px; padding: 12px; }
|
.overlay-card { width: min(680px, 92vw); background: var(--theme-surface-soft); border: 1px solid var(--theme-control-border); border-radius: 12px; padding: 12px; }
|
||||||
.overlay-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
.overlay-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
||||||
.overlay-header h3 { margin: 0; font-size: 16px; }
|
.overlay-header h3 { margin: 0; font-size: 16px; }
|
||||||
.overlay-header button { border: none; background: transparent; font-size: 20px; cursor: pointer; }
|
|
||||||
.hint { font-size: 12px; color: var(--text-secondary); margin: 0 0 8px 0; }
|
.hint { font-size: 12px; color: var(--text-secondary); margin: 0 0 8px 0; }
|
||||||
.mode-switch { display: inline-flex; border: 1px solid var(--theme-control-border); border-radius: 10px; overflow: hidden; margin-bottom: 8px; }
|
.mode-switch { display: inline-flex; border: 1px solid var(--theme-control-border); border-radius: 10px; overflow: hidden; margin-bottom: 8px; }
|
||||||
.mode-btn { border: none; background: transparent; padding: 6px 10px; cursor: pointer; font-size: 12px; }
|
.mode-btn { border: none; background: transparent; padding: 6px 10px; cursor: pointer; font-size: 12px; }
|
||||||
|
|||||||
@ -4,17 +4,13 @@
|
|||||||
<section class="plan-approval-card" role="dialog" aria-modal="true" :aria-label="$t('overlay.planApprovalAriaLabel')">
|
<section class="plan-approval-card" role="dialog" aria-modal="true" :aria-label="$t('overlay.planApprovalAriaLabel')">
|
||||||
<header class="plan-approval-windowbar">
|
<header class="plan-approval-windowbar">
|
||||||
<div class="plan-approval-window-title">{{ $t('overlay.planApprovalTitle') }}</div>
|
<div class="plan-approval-window-title">{{ $t('overlay.planApprovalTitle') }}</div>
|
||||||
<button
|
<span class="plan-approval-close">
|
||||||
type="button"
|
<CloseButton
|
||||||
class="plan-approval-close"
|
:label="$t('overlay.planApprovalMinimize')"
|
||||||
:title="$t('overlay.planApprovalMinimize')"
|
:title="$t('overlay.planApprovalMinimize')"
|
||||||
:aria-label="$t('overlay.planApprovalMinimize')"
|
|
||||||
@click="emit('minimize')"
|
@click="emit('minimize')"
|
||||||
>
|
/>
|
||||||
<svg viewBox="0 0 20 20" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
</span>
|
||||||
<path d="M5 5l10 10M15 5L5 15" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<header class="plan-approval-header">
|
<header class="plan-approval-header">
|
||||||
@ -77,6 +73,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import MarkdownRenderer from '../chat/MarkdownRenderer.vue';
|
import MarkdownRenderer from '../chat/MarkdownRenderer.vue';
|
||||||
|
import CloseButton from '@/components/common/CloseButton.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@ -165,22 +162,9 @@ function reject() {
|
|||||||
right: 10px;
|
right: 10px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
width: 26px;
|
|
||||||
height: 26px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0;
|
|
||||||
border: none;
|
|
||||||
border-radius: 7px;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plan-approval-close:hover {
|
|
||||||
background: var(--hover-bg);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.plan-approval-window-title {
|
.plan-approval-window-title {
|
||||||
|
|||||||
@ -9,17 +9,9 @@
|
|||||||
>
|
>
|
||||||
<header class="sandbox-setup-windowbar">
|
<header class="sandbox-setup-windowbar">
|
||||||
<div class="sandbox-setup-window-title">{{ $t('sandbox.setupTitle') }}</div>
|
<div class="sandbox-setup-window-title">{{ $t('sandbox.setupTitle') }}</div>
|
||||||
<button
|
<span class="sandbox-setup-close">
|
||||||
type="button"
|
<CloseButton :label="$t('common.close')" :title="$t('common.close')" @click="onClose" />
|
||||||
class="sandbox-setup-close"
|
</span>
|
||||||
:title="$t('common.close')"
|
|
||||||
:aria-label="$t('common.close')"
|
|
||||||
@click="onClose"
|
|
||||||
>
|
|
||||||
<svg viewBox="0 0 20 20" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
||||||
<path d="M5 5l10 10M15 5L5 15" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="sandbox-setup-body">
|
<div class="sandbox-setup-body">
|
||||||
@ -144,6 +136,7 @@
|
|||||||
import { computed, nextTick, ref, watch } from 'vue';
|
import { computed, nextTick, ref, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import FancyCheck from '@/components/common/FancyCheck.vue';
|
import FancyCheck from '@/components/common/FancyCheck.vue';
|
||||||
|
import CloseButton from '@/components/common/CloseButton.vue';
|
||||||
import { useSandboxSetupStore } from '@/stores/sandboxSetup';
|
import { useSandboxSetupStore } from '@/stores/sandboxSetup';
|
||||||
|
|
||||||
defineOptions({ name: 'SandboxSetupDialog' });
|
defineOptions({ name: 'SandboxSetupDialog' });
|
||||||
@ -312,22 +305,9 @@ function onClose() {
|
|||||||
right: 10px;
|
right: 10px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
width: 26px;
|
|
||||||
height: 26px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0;
|
|
||||||
border: none;
|
|
||||||
border-radius: 7px;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sandbox-setup-close:hover {
|
|
||||||
background: var(--hover-bg);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sandbox-setup-body {
|
.sandbox-setup-body {
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
<div class="subagent-activity-title">
|
<div class="subagent-activity-title">
|
||||||
{{ $t('overlay.subAgentProgressTitle', { id: activeAgent.agent_id || activeAgent.task_id }) }}
|
{{ $t('overlay.subAgentProgressTitle', { id: activeAgent.agent_id || activeAgent.task_id }) }}
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="subagent-activity-close" @click="close">×</button>
|
<CloseButton :label="$t('common.close')" @click="close" />
|
||||||
</div>
|
</div>
|
||||||
<div class="subagent-activity-meta">
|
<div class="subagent-activity-meta">
|
||||||
<span class="subagent-activity-status" :class="activeAgent.status || ''">{{
|
<span class="subagent-activity-status" :class="activeAgent.status || ''">{{
|
||||||
@ -59,6 +59,7 @@
|
|||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { t, currentLocale } from '@/locales';
|
import { t, currentLocale } from '@/locales';
|
||||||
|
import CloseButton from '@/components/common/CloseButton.vue';
|
||||||
import { useSubAgentStore } from '@/stores/subAgent';
|
import { useSubAgentStore } from '@/stores/subAgent';
|
||||||
|
|
||||||
type ActivityEntry = {
|
type ActivityEntry = {
|
||||||
|
|||||||
@ -3,8 +3,8 @@
|
|||||||
<div v-if="visible && questions.length" class="user-question-overlay" @click.self="emit('minimize')">
|
<div v-if="visible && questions.length" class="user-question-overlay" @click.self="emit('minimize')">
|
||||||
<section class="user-question-card" role="dialog" aria-modal="true" :aria-label="$t('overlay.userQuestionAriaLabel')">
|
<section class="user-question-card" role="dialog" aria-modal="true" :aria-label="$t('overlay.userQuestionAriaLabel')">
|
||||||
<header class="user-question-windowbar">
|
<header class="user-question-windowbar">
|
||||||
<div class="user-question-traffic" :aria-label="$t('overlay.windowControlAria')">
|
<div class="user-question-traffic">
|
||||||
<button type="button" class="traffic-dot traffic-dot--close" :aria-label="$t('overlay.minimizeAria')" @click="emit('minimize')"></button>
|
<CloseButton :label="$t('overlay.minimizeAria')" @click="emit('minimize')" />
|
||||||
</div>
|
</div>
|
||||||
<div class="user-question-window-title">{{ $t('overlay.userQuestionWindowTitle') }}</div>
|
<div class="user-question-window-title">{{ $t('overlay.userQuestionWindowTitle') }}</div>
|
||||||
</header>
|
</header>
|
||||||
@ -80,6 +80,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive, watch } from 'vue';
|
import { computed, reactive, watch } from 'vue';
|
||||||
|
import CloseButton from '@/components/common/CloseButton.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@ -208,16 +209,7 @@ function dismiss() {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.traffic-dot {
|
/* 关闭按钮本体样式在 common/CloseButton.vue(boxed 变体),traffic-dot 红点样式已随统一下线 */
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 1px solid var(--border-strong);
|
|
||||||
padding: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.traffic-dot--close { background: var(--mac-close); }
|
|
||||||
|
|
||||||
.user-question-window-title {
|
.user-question-window-title {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@ -20,9 +20,7 @@
|
|||||||
<span class="icon icon-sm" :style="iconStyle('refreshCw')" aria-hidden="true"></span>
|
<span class="icon icon-sm" :style="iconStyle('refreshCw')" aria-hidden="true"></span>
|
||||||
<span>{{ $t('overlay.refresh') }}</span>
|
<span>{{ $t('overlay.refresh') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="icon-close-btn" :aria-label="$t('common.close')" @click="$emit('close')">
|
<CloseButton :label="$t('common.close')" @click="$emit('close')" />
|
||||||
<span class="icon icon-sm" :style="iconStyle('x')" aria-hidden="true"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -155,6 +153,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import CloseButton from '@/components/common/CloseButton.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
hostMode: boolean;
|
hostMode: boolean;
|
||||||
@ -498,26 +497,6 @@ const splitDiffContent = (content: any): string[] => {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-close-btn {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 9px;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
cursor: pointer;
|
|
||||||
transition:
|
|
||||||
background 140ms ease,
|
|
||||||
color 140ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-close-btn:hover {
|
|
||||||
background: var(--theme-tab-active);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===== 主体:左右分栏靠竖线 ===== */
|
/* ===== 主体:左右分栏靠竖线 ===== */
|
||||||
.versioning-body {
|
.versioning-body {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
@change="onLocalChange"
|
@change="onLocalChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button class="close-btn" @click="close">×</button>
|
<CloseButton :label="$t('common.close')" @click="close" />
|
||||||
</div>
|
</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div v-if="loading" class="loading">{{ $t('common.loading') }}</div>
|
<div v-if="loading" class="loading">{{ $t('common.loading') }}</div>
|
||||||
@ -59,6 +59,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch, onMounted, withDefaults } from 'vue';
|
import { computed, ref, watch, onMounted, withDefaults } from 'vue';
|
||||||
|
import CloseButton from '@/components/common/CloseButton.vue';
|
||||||
|
|
||||||
interface VideoEntry {
|
interface VideoEntry {
|
||||||
name: string;
|
name: string;
|
||||||
@ -197,13 +198,6 @@ onMounted(() => {
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
.close-btn {
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
border: none;
|
|
||||||
font-size: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.body {
|
.body {
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|||||||
@ -9,15 +9,11 @@
|
|||||||
<span class="icon icon-sm" :style="iconStyle('eye')" aria-hidden="true"></span>
|
<span class="icon icon-sm" :style="iconStyle('eye')" aria-hidden="true"></span>
|
||||||
<span>{{ $t('shell.focusFilesCount', { n: focusedCount }) }}</span>
|
<span>{{ $t('shell.focusFilesCount', { n: focusedCount }) }}</span>
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<CloseButton
|
||||||
v-if="showCloseButton"
|
v-if="showCloseButton"
|
||||||
type="button"
|
:label="$t('shell.closeFocusPanel')"
|
||||||
class="focus-close-btn"
|
|
||||||
:aria-label="$t('shell.closeFocusPanel')"
|
|
||||||
@click="$emit('close')"
|
@click="$emit('close')"
|
||||||
>
|
/>
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="focused-files" v-if="!collapsed">
|
<div class="focused-files" v-if="!collapsed">
|
||||||
<div v-if="!focusedCount" class="no-files">{{ $t('shell.noFocusFiles') }}</div>
|
<div v-if="!focusedCount" class="no-files">{{ $t('shell.noFocusFiles') }}</div>
|
||||||
@ -40,6 +36,7 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useFocusStore } from '@/stores/focus';
|
import { useFocusStore } from '@/stores/focus';
|
||||||
|
import CloseButton from '@/components/common/CloseButton.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'FocusPanel' });
|
defineOptions({ name: 'FocusPanel' });
|
||||||
|
|
||||||
|
|||||||
@ -9,9 +9,7 @@
|
|||||||
<div class="git-changes-panel__summary">
|
<div class="git-changes-panel__summary">
|
||||||
<span class="git-changes-panel__add">+{{ additions }}</span>
|
<span class="git-changes-panel__add">+{{ additions }}</span>
|
||||||
<span class="git-changes-panel__del">-{{ deletions }}</span>
|
<span class="git-changes-panel__del">-{{ deletions }}</span>
|
||||||
<button type="button" class="git-changes-panel__close" :aria-label="$t('shell.closeGitPanel')" @click="$emit('close')">
|
<CloseButton :label="$t('shell.closeGitPanel')" @click="$emit('close')" />
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -135,6 +133,7 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { t, currentLocale } from '@/locales';
|
import { t, currentLocale } from '@/locales';
|
||||||
|
import CloseButton from '@/components/common/CloseButton.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'GitChangesPanel' });
|
defineOptions({ name: 'GitChangesPanel' });
|
||||||
|
|
||||||
|
|||||||
@ -18,12 +18,7 @@
|
|||||||
<span class="terminal-panel__tab-name">{{ name }}</span>
|
<span class="terminal-panel__tab-name">{{ name }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<CloseButton :label="$t('shell.closeTerminalPanel')" @click="$emit('close')" />
|
||||||
type="button"
|
|
||||||
class="terminal-panel__close"
|
|
||||||
:aria-label="$t('shell.closeTerminalPanel')"
|
|
||||||
@click="$emit('close')"
|
|
||||||
>×</button>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- 终端容器 -->
|
<!-- 终端容器 -->
|
||||||
@ -44,6 +39,7 @@ import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
|||||||
import { Terminal } from 'xterm';
|
import { Terminal } from 'xterm';
|
||||||
import { io as createSocketClient } from 'socket.io-client';
|
import { io as createSocketClient } from 'socket.io-client';
|
||||||
import 'xterm/css/xterm.css';
|
import 'xterm/css/xterm.css';
|
||||||
|
import CloseButton from '@/components/common/CloseButton.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'TerminalPanel' });
|
defineOptions({ name: 'TerminalPanel' });
|
||||||
|
|
||||||
@ -541,29 +537,6 @@ onBeforeUnmount(() => {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-panel__close {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 14px;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 18px;
|
|
||||||
line-height: 1;
|
|
||||||
cursor: pointer;
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-left: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-panel__close:hover {
|
|
||||||
background: var(--theme-tab-active);
|
|
||||||
color: var(--text-primary);
|
|
||||||
box-shadow: 0 1px 2px var(--shadow-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-panel__body {
|
.terminal-panel__body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@ -6,9 +6,7 @@
|
|||||||
>
|
>
|
||||||
<div class="sidebar-header" :class="{ 'mobile-header': isMobileViewport }">
|
<div class="sidebar-header" :class="{ 'mobile-header': isMobileViewport }">
|
||||||
<h3 class="icon-label">{{ panelTitle }}</h3>
|
<h3 class="icon-label">{{ panelTitle }}</h3>
|
||||||
<button type="button" class="approval-close-btn" :aria-label="$t('shell.closeApprovalPanel')" @click="handleCloseClick">
|
<CloseButton :label="$t('shell.closeApprovalPanel')" @click="handleCloseClick" />
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="approval-panel-body" v-if="!collapsed">
|
<div class="approval-panel-body" v-if="!collapsed">
|
||||||
<div v-if="!approvals.length && !isGoalApprovalMode" class="no-files">{{ $t('shell.noPendingApprovals') }}</div>
|
<div v-if="!approvals.length && !isGoalApprovalMode" class="no-files">{{ $t('shell.noPendingApprovals') }}</div>
|
||||||
@ -132,6 +130,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, computed } from 'vue';
|
import { onMounted, computed } from 'vue';
|
||||||
import { t, currentLocale } from '@/locales';
|
import { t, currentLocale } from '@/locales';
|
||||||
|
import CloseButton from '@/components/common/CloseButton.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'ToolApprovalPanel' });
|
defineOptions({ name: 'ToolApprovalPanel' });
|
||||||
|
|
||||||
|
|||||||
@ -23,29 +23,19 @@
|
|||||||
<div class="settings-mobile-bar">
|
<div class="settings-mobile-bar">
|
||||||
<span class="settings-mobile-bar-cell" aria-hidden="true"></span>
|
<span class="settings-mobile-bar-cell" aria-hidden="true"></span>
|
||||||
<span class="settings-mobile-bar-title">{{ $t('common.settings') }}</span>
|
<span class="settings-mobile-bar-title">{{ $t('common.settings') }}</span>
|
||||||
<button
|
<span class="settings-mobile-bar-btn">
|
||||||
type="button"
|
<CloseButton
|
||||||
class="settings-mobile-bar-btn"
|
:label="$t('personalization.closePersonalSpaceAriaLabel')"
|
||||||
:aria-label="$t('personalization.closePersonalSpaceAriaLabel')"
|
|
||||||
@click="personalization.closeDrawer()"
|
@click="personalization.closeDrawer()"
|
||||||
>
|
/>
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
</span>
|
||||||
<path d="M6 6l12 12M18 6 6 18" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-nav-head">
|
<div class="settings-nav-head">
|
||||||
<button
|
<CloseButton
|
||||||
type="button"
|
|
||||||
class="settings-close-button"
|
|
||||||
data-tutorial="personal-close"
|
data-tutorial="personal-close"
|
||||||
:aria-label="$t('personalization.closePersonalSpaceAriaLabel')"
|
:label="$t('personalization.closePersonalSpaceAriaLabel')"
|
||||||
@click="personalization.closeDrawer()"
|
@click="personalization.closeDrawer()"
|
||||||
>
|
/>
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round">
|
|
||||||
<path d="M6 6l12 12M18 6 6 18" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-redesign-tabs">
|
<div class="settings-redesign-tabs">
|
||||||
<button
|
<button
|
||||||
@ -164,6 +154,7 @@
|
|||||||
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick, provide } from 'vue';
|
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick, provide } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import FancyCheck from '@/components/common/FancyCheck.vue';
|
import FancyCheck from '@/components/common/FancyCheck.vue';
|
||||||
|
import CloseButton from '@/components/common/CloseButton.vue';
|
||||||
import GeneralTab from './tabs/GeneralTab.vue';
|
import GeneralTab from './tabs/GeneralTab.vue';
|
||||||
import PreferencesTab from './tabs/PreferencesTab.vue';
|
import PreferencesTab from './tabs/PreferencesTab.vue';
|
||||||
import ModelTab from './tabs/ModelTab.vue';
|
import ModelTab from './tabs/ModelTab.vue';
|
||||||
|
|||||||
@ -33,31 +33,6 @@ body[data-theme='dark'] .settings-redesign-card {
|
|||||||
padding: 12px 12px 4px;
|
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 {
|
.settings-mobile-bar {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
<transition name="quota-toast-fade">
|
<transition name="quota-toast-fade">
|
||||||
<div class="quota-toast" v-if="quotaToast">
|
<div class="quota-toast" v-if="quotaToast">
|
||||||
<span class="quota-toast-label">{{ quotaToast.message }}</span>
|
<span class="quota-toast-label">{{ quotaToast.message }}</span>
|
||||||
<button type="button" class="toast-close" :aria-label="$t('shell.closeNotification')" @click="dismiss">×</button>
|
<CloseButton size="sm" :label="$t('shell.closeNotification')" @click="dismiss" />
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
@ -10,6 +10,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useUiStore } from '@/stores/ui';
|
import { useUiStore } from '@/stores/ui';
|
||||||
|
import CloseButton from '@/components/common/CloseButton.vue';
|
||||||
|
|
||||||
const uiStore = useUiStore();
|
const uiStore = useUiStore();
|
||||||
const { quotaToast } = storeToRefs(uiStore);
|
const { quotaToast } = storeToRefs(uiStore);
|
||||||
|
|||||||
@ -10,15 +10,12 @@
|
|||||||
<div v-if="toast.title" class="app-toast-title">{{ toast.title }}</div>
|
<div v-if="toast.title" class="app-toast-title">{{ toast.title }}</div>
|
||||||
<div class="app-toast-message">{{ toast.message }}</div>
|
<div class="app-toast-message">{{ toast.message }}</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<CloseButton
|
||||||
v-if="toast.closable !== false"
|
v-if="toast.closable !== false"
|
||||||
type="button"
|
size="sm"
|
||||||
class="toast-close"
|
:label="$t('shell.closeNotification')"
|
||||||
:aria-label="$t('shell.closeNotification')"
|
|
||||||
@click="dismiss(toast.id)"
|
@click="dismiss(toast.id)"
|
||||||
>
|
/>
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
@ -27,6 +24,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useUiStore } from '@/stores/ui';
|
import { useUiStore } from '@/stores/ui';
|
||||||
|
import CloseButton from '@/components/common/CloseButton.vue';
|
||||||
|
|
||||||
const uiStore = useUiStore();
|
const uiStore = useUiStore();
|
||||||
const { toastQueue: toasts } = storeToRefs(uiStore);
|
const { toastQueue: toasts } = storeToRefs(uiStore);
|
||||||
|
|||||||
@ -1,15 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="token-drawer" v-if="visible" :class="{ collapsed }" data-tutorial="token-drawer">
|
<div class="token-drawer" v-if="visible" :class="{ collapsed }" data-tutorial="token-drawer">
|
||||||
<div class="token-display-panel">
|
<div class="token-display-panel">
|
||||||
<button
|
<CloseButton
|
||||||
class="token-close-btn"
|
class="token-close-btn"
|
||||||
type="button"
|
|
||||||
data-tutorial="token-close"
|
data-tutorial="token-close"
|
||||||
|
:label="$t('sidebar.collapseUsage')"
|
||||||
@click="emit('toggle')"
|
@click="emit('toggle')"
|
||||||
:aria-label="$t('sidebar.collapseUsage')"
|
/>
|
||||||
>
|
|
||||||
<span class="sr-only">{{ $t('common.close') }}</span>
|
|
||||||
</button>
|
|
||||||
<div class="token-panel-content">
|
<div class="token-panel-content">
|
||||||
<div class="usage-dashboard">
|
<div class="usage-dashboard">
|
||||||
<div class="usage-cell usage-cell--left usage-cell--token panel-card">
|
<div class="usage-cell usage-cell--left usage-cell--token panel-card">
|
||||||
@ -127,6 +124,7 @@ defineOptions({ name: 'TokenDrawer' });
|
|||||||
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { t, currentLocale } from '@/locales';
|
import { t, currentLocale } from '@/locales';
|
||||||
|
import CloseButton from '@/components/common/CloseButton.vue';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'toggle'): void;
|
(e: 'toggle'): void;
|
||||||
|
|||||||
@ -18,7 +18,7 @@
|
|||||||
<div v-else class="wf-shell__loading">{{ $t('common.loading') }}</div>
|
<div v-else class="wf-shell__loading">{{ $t('common.loading') }}</div>
|
||||||
<div v-if="errorMessage" class="wf-shell__error" role="alert">
|
<div v-if="errorMessage" class="wf-shell__error" role="alert">
|
||||||
<span>{{ errorMessage }}</span>
|
<span>{{ errorMessage }}</span>
|
||||||
<button type="button" class="wf-shell__error-close" :aria-label="$t('common.close')" @click="errorMessage = ''">×</button>
|
<CloseButton :label="$t('common.close')" @click="errorMessage = ''" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -26,6 +26,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { t } from '@/locales';
|
import { t } from '@/locales';
|
||||||
|
import CloseButton from '@/components/common/CloseButton.vue';
|
||||||
import WorkflowLibraryView from './WorkflowLibraryView.vue';
|
import WorkflowLibraryView from './WorkflowLibraryView.vue';
|
||||||
import WorkflowEditorView from './WorkflowEditorView.vue';
|
import WorkflowEditorView from './WorkflowEditorView.vue';
|
||||||
import { createEmptyWorkflow, type WorkflowDef } from './workflowModel';
|
import { createEmptyWorkflow, type WorkflowDef } from './workflowModel';
|
||||||
|
|||||||
@ -2007,11 +2007,13 @@ html.show-html-fullscreen-open body {
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 与卡片 ⋯ 按钮一致的纯文字风格:无边框无背景,hover 仅变色 */
|
/* 全屏预览顶栏按钮:与全站统一关闭按钮(common/CloseButton.vue boxed 变体)同一形态——
|
||||||
|
圆角方透明底,hover/active 出现 --hover-bg 底色块(动态 DOM 无法复用组件,样式在此对齐) */
|
||||||
.show-html-fullscreen__btn {
|
.show-html-fullscreen__btn {
|
||||||
width: 32px;
|
width: 28px;
|
||||||
height: 32px;
|
height: 28px;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
border-radius: 7px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@ -2020,10 +2022,18 @@ html.show-html-fullscreen-open body {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 0.18s ease;
|
transition: background 140ms ease, color 140ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.show-html-fullscreen__btn:hover {
|
@media (hover: hover) {
|
||||||
|
.show-html-fullscreen__btn:hover {
|
||||||
|
background: var(--hover-bg);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-html-fullscreen__btn:active {
|
||||||
|
background: var(--hover-bg);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2058,6 +2068,14 @@ html.show-html-fullscreen-open body {
|
|||||||
@extend .chat-inline-card__caption;
|
@extend .chat-inline-card__caption;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 可点击预览的内容图片统一给放大镜光标提示
|
||||||
|
(show_image 卡片 / Markdown 正文图 / 工具结果图,点击由 bootstrap.ts 事件委托承接) */
|
||||||
|
.chat-inline-image img,
|
||||||
|
.text-content img,
|
||||||
|
.tool-result-image img {
|
||||||
|
cursor: zoom-in;
|
||||||
|
}
|
||||||
|
|
||||||
/* show_html 在 streaming 重建期间会短暂回到原始标签,需提供稳定占位避免 scrollHeight 抖动 */
|
/* show_html 在 streaming 重建期间会短暂回到原始标签,需提供稳定占位避免 scrollHeight 抖动 */
|
||||||
show_html:not([data-rendered='1']),
|
show_html:not([data-rendered='1']),
|
||||||
show-html:not([data-rendered='1']) {
|
show-html:not([data-rendered='1']) {
|
||||||
|
|||||||
@ -1479,15 +1479,6 @@ body[data-theme='light'] {
|
|||||||
padding: 0 16px !important;
|
padding: 0 16px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 移动端审批面板关闭按钮 - 移除圆形外框 */
|
|
||||||
.mobile-panel-sheet--approval .approval-close-btn {
|
|
||||||
background: transparent !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
width: auto !important;
|
|
||||||
height: auto !important;
|
|
||||||
padding: 4px !important;
|
|
||||||
font-size: 24px !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-question-mini-dot {
|
.user-question-mini-dot {
|
||||||
|
|||||||
@ -1811,20 +1811,7 @@
|
|||||||
background: var(--surface-panel);
|
background: var(--surface-panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-overlay-close {
|
/* .mobile-overlay-close 为历史遗留死代码(全站无模板引用),统一关闭按钮改造时删除 */
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border-radius: 16px;
|
|
||||||
border: none;
|
|
||||||
background: var(--border-default);
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-size: 18px;
|
|
||||||
cursor: pointer;
|
|
||||||
z-index: 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-overlay-content {
|
.mobile-overlay-content {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
@ -2085,20 +2072,6 @@
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.subagent-activity-close {
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-size: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subagent-activity-close:hover {
|
|
||||||
background: var(--theme-control-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.subagent-activity-meta {
|
.subagent-activity-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -172,29 +172,11 @@ body[data-theme='dark'] {
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 按钮本体样式在 common/CloseButton.vue(boxed 变体),此处仅保留抽屉左上角定位 */
|
||||||
.token-close-btn {
|
.token-close-btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 12px;
|
top: 12px;
|
||||||
left: 12px;
|
left: 12px;
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
border: 1px solid var(--mac-close-border);
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--mac-close);
|
|
||||||
display: block;
|
|
||||||
padding: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s ease, transform 0.2s ease;
|
|
||||||
box-shadow: 0 2px 4px var(--shadow-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-close-btn:hover {
|
|
||||||
background: var(--mac-close-hover);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-close-btn:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sr-only {
|
.sr-only {
|
||||||
@ -437,19 +419,7 @@ body[data-theme='dark'] {
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast-close {
|
/* .toast-close 已移除:Toast 关闭按钮统一改用 common/CloseButton.vue(boxed sm) */
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast-close:hover {
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.status-pill--running {
|
.status-pill--running {
|
||||||
background: color-mix(in srgb, var(--state-success) 22%, transparent);
|
background: color-mix(in srgb, var(--state-success) 22%, transparent);
|
||||||
|
|||||||
@ -40,28 +40,6 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.focus-close-btn {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 16px;
|
|
||||||
background: var(--hover-bg);
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-size: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.approval-close-btn {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 16px;
|
|
||||||
background: var(--hover-bg);
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-size: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.git-changes-panel {
|
.git-changes-panel {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
min-width: 340px;
|
min-width: 340px;
|
||||||
@ -126,24 +104,6 @@
|
|||||||
color: var(--git-diff-del-text);
|
color: var(--git-diff-del-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.git-changes-panel__close {
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 14px;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 18px;
|
|
||||||
line-height: 1;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.git-changes-panel__close:hover {
|
|
||||||
background: var(--theme-tab-active);
|
|
||||||
color: var(--text-primary);
|
|
||||||
box-shadow: 0 1px 2px var(--shadow-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.git-changes-panel__body {
|
.git-changes-panel__body {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@ -843,19 +803,4 @@ body[data-theme='dark'] {
|
|||||||
background: var(--surface-base);
|
background: var(--surface-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.focus-close-btn {
|
|
||||||
background: var(--badge-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.focus-close-btn:hover {
|
|
||||||
background: var(--surface-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.approval-close-btn {
|
|
||||||
background: var(--badge-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.approval-close-btn:hover {
|
|
||||||
background: var(--surface-muted);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user