diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 9b50e3b424..4a1af30b8e 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -7,11 +7,13 @@ import ManualKnowledgeEditor from '@/components/manual-knowledge-editor.vue' import UploadConfirmHost from '@/components/UploadConfirmHost.vue' import { useAuthStore } from '@/stores/auth' import { useSettingsStore } from '@/stores/settings' +import { useUIStore } from '@/stores/ui' import { getCurrentUser, userInfoFromApi } from '@/api/auth' import { consumePendingTenantSwitchToast } from '@/utils/tenantSwitch' import { useRoleLabel } from '@/composables/useRoleLabel' import { notifyLoginSuccess } from '@/utils/loginNotify' import { renderWorkspaceNotifyContent } from '@/utils/workspaceNotifyContent' +import { evalBreakpoint, startListening, stopListening, isMobile } from '@/composables/useBreakpoint' // TDesign locale configs import enUSConfig from 'tdesign-vue-next/esm/locale/en_US' @@ -24,6 +26,7 @@ const { formatRole, roleIcon } = useRoleLabel() const router = useRouter() const authStore = useAuthStore() const settingsStore = useSettingsStore() +const uiStore = useUIStore() const tdLocaleMap: Record = { 'en-US': enUSConfig, @@ -194,6 +197,16 @@ watch( { immediate: true }, ) +// 同步断点响应式状态到 UI Store。immediate 让 store 在首帧就与视口一致, +// 否则手机上要等 onMounted 才切形态,会先闪一下桌面侧栏。 +watch(isMobile, (val) => { + uiStore.setMobile(val) + // 从移动端切换到桌面端时,关闭移动端菜单 + if (!val) { + uiStore.closeMobileMenu() + } +}, { immediate: true }) + // 切换空间后会 hard reload;切换前 stash 的 toast 这里 consume 并弹出, // 这样 toast 显示在新页面上,duration 才真正生效。 const showPendingTenantSwitchToast = () => { @@ -223,6 +236,10 @@ const showPendingTenantSwitchToast = () => { } onMounted(() => { + // 初始化全局响应式断点监听,同步到 UI Store + startListening() + evalBreakpoint() + handleGlobalOIDCCallback() showPendingTenantSwitchToast() @@ -250,6 +267,7 @@ onMounted(() => { }) onUnmounted(() => { + stopListening() if (updateCheckTimer) { clearInterval(updateCheckTimer) } diff --git a/frontend/src/assets/responsive.less b/frontend/src/assets/responsive.less new file mode 100644 index 0000000000..e2a20acbb1 --- /dev/null +++ b/frontend/src/assets/responsive.less @@ -0,0 +1,84 @@ +// ============================================================ +// WeKnora 响应式断点 Less 混入 +// ============================================================ +// 使用移动端优先 (mobile-first) 策略: +// 基础样式 = 移动端, +// .tablet-up() → 平板及以上增强, +// .desktop() → 桌面端增强。 +// ============================================================ + +// 断点值(与 useBreakpoint.ts 保持一致) +@bp-mobile-max: 767px; +@bp-tablet-min: 768px; +@bp-tablet-max: 1023px; +@bp-desktop-min: 1024px; + +// --- 设备类型混入 --- + +// 仅手机 (< 768px) +.mobile(@rules) { + @media screen and (max-width: @bp-mobile-max) { + @rules(); + } +} + +// 仅平板 (768px - 1023px) +.tablet(@rules) { + @media screen and (min-width: @bp-tablet-min) and (max-width: @bp-tablet-max) { + @rules(); + } +} + +// 平板及以上 (≥ 768px) — mobile-first 增强用 +.tablet-up(@rules) { + @media screen and (min-width: @bp-tablet-min) { + @rules(); + } +} + +// 仅桌面 (≥ 1024px) +.desktop(@rules) { + @media screen and (min-width: @bp-desktop-min) { + @rules(); + } +} + +// 非桌面 (< 1024px) — 即手机 + 平板 +.not-desktop(@rules) { + @media screen and (max-width: @bp-tablet-max) { + @rules(); + } +} + +// --- 自定义断点混入(使用 min-width) --- +.min-width(@width, @rules) { + @media screen and (min-width: @width) { + @rules(); + } +} + +.max-width(@width, @rules) { + @media screen and (max-width: @width) { + @rules(); + } +} + +// --- 触摸设备 --- +.touch(@rules) { + @media (hover: none) { + @rules(); + } +} + +.hover-capable(@rules) { + @media (hover: hover) { + @rules(); + } +} + +// --- 减少动效 --- +.reduced-motion(@rules) { + @media (prefers-reduced-motion: reduce) { + @rules(); + } +} diff --git a/frontend/src/assets/theme/theme.css b/frontend/src/assets/theme/theme.css index e62ec6f171..f15e622d03 100644 --- a/frontend/src/assets/theme/theme.css +++ b/frontend/src/assets/theme/theme.css @@ -97,6 +97,74 @@ } +/* ============================================================ + 响应式断点 CSS 变量(与 useBreakpoint.ts / responsive.less 保持一致) + ============================================================ */ +:root { + --breakpoint-mobile-max: 767px; + --breakpoint-tablet-min: 768px; + --breakpoint-tablet-max: 1023px; + --breakpoint-desktop-min: 1024px; + --app-min-touch-target: 44px; +} + +/* ============================================================ + 移动端全局样式增强 + ============================================================ */ + +/* 触摸设备:放大 TDesign 交互元素的点击区域。 + 会话行的常显操作按钮改在 menu.vue 的 scoped 块里覆写, + 因为默认的 opacity: 0 也带着 scoped 属性选择器的权重。 */ +@media (hover: none) { + /* 下拉菜单项最小触摸区域 */ + .t-dropdown__item { + min-height: 44px; + display: flex; + align-items: center; + } + + /* 表单按钮 */ + .t-button { + min-height: 40px; + } + + /* 小按钮可以小一些 */ + .t-button.t-size-s { + min-height: 32px; + } +} + +/* ============================================================ + 移动端全局排版调整 + ============================================================ */ +@media screen and (max-width: 767px) { + /* 移动端基础字号略大,补偿手持距离 */ + body, + html { + font-size: 15px; + } + + /* TDesign 表格/列表在移动端的水平滚动 */ + .t-table { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + + /* 标签在小屏幕上换行 */ + .t-tag { + white-space: normal; + word-break: break-word; + } +} + +/* 平板端微调 */ +@media screen and (min-width: 768px) and (max-width: 1023px) { + body, + html { + font-size: 14px; + } +} + /* 全局深色模式滚动条样式 */ :root[theme-mode="dark"] *::-webkit-scrollbar-thumb { background-color: #4b4b4b !important; diff --git a/frontend/src/components/Input-field.vue b/frontend/src/components/Input-field.vue index e1cfef13ce..813c7b135f 100644 --- a/frontend/src/components/Input-field.vue +++ b/frontend/src/components/Input-field.vue @@ -2738,6 +2738,7 @@ const getImgSrc = (url: string) => { } diff --git a/frontend/src/components/ShareKnowledgeBaseDialog.vue b/frontend/src/components/ShareKnowledgeBaseDialog.vue index c31eb13400..a9766e3b77 100644 --- a/frontend/src/components/ShareKnowledgeBaseDialog.vue +++ b/frontend/src/components/ShareKnowledgeBaseDialog.vue @@ -461,6 +461,31 @@ function handleGoToOrgSettings(orgId: string) { color: var(--td-text-color-secondary); opacity: 0.75; } + // 移动端:弹窗全宽 + @media screen and (max-width: 767px) { + :deep(.t-dialog) { + width: 100vw !important; + max-width: 100vw !important; + margin: 0 !important; + border-radius: 0 !important; + } + + :deep(.t-dialog__header) { + padding: 14px 16px; + } + + :deep(.t-dialog__body) { + padding: 16px; + } + + :deep(.t-dialog__footer) { + padding: 12px 16px; + } + + .share-form { + padding: 0; + } + } } diff --git a/frontend/src/components/css/suggested-questions.less b/frontend/src/components/css/suggested-questions.less index e768d9c4f1..e5154462c2 100644 --- a/frontend/src/components/css/suggested-questions.less +++ b/frontend/src/components/css/suggested-questions.less @@ -202,3 +202,32 @@ color: var(--td-brand-color); } +// 移动端:建议问题全宽单列,间距缩小 +@media screen and (max-width: 767px) { + .suggested-questions-container { + padding: 8px 4px 8px; + max-width: 100%; + } + + .suggested-questions-grid { + gap: 8px; + } + + .suggested-question-card { + width: 100%; + max-width: 100%; + padding: 10px 14px; + + &.sq-card-skeleton, + &.sq-card-skeleton:nth-child(2n), + &.sq-card-skeleton:nth-child(3n), + &.sq-card-skeleton:nth-child(5n) { + width: 100%; + } + } + + .suggested-question-text { + font-size: 14px; + } +} + diff --git a/frontend/src/components/doc-content.vue b/frontend/src/components/doc-content.vue index 0b2652dc2b..c173b07461 100644 --- a/frontend/src/components/doc-content.vue +++ b/frontend/src/components/doc-content.vue @@ -3285,4 +3285,97 @@ body:has(.t-drawer.kp-secondary-drawer--resizing) .trace-drawer-resize-line { .t-drawer.kp-secondary-drawer--resizing .t-drawer__content { transition: none !important; } + +// ============================================================ +// 移动端适配:抽屉全屏,隐藏拖拽手柄 +// ============================================================ +@media screen and (max-width: 767px) { + // 主抽屉全屏 + .t-drawer.doc-main-drawer { + .t-drawer__content-wrapper, + .t-drawer__content { + width: 100vw !important; + max-width: 100vw !important; + } + + .t-drawer__header { + padding: 12px 14px; + } + + .t-drawer__body { + padding: 14px; + } + } + + // 追踪抽屉全屏 + .t-drawer.kp-secondary-drawer { + .t-drawer__content-wrapper, + .t-drawer__content { + width: 100vw !important; + max-width: 100vw !important; + } + } + + // 隐藏拖拽手柄 + .doc-drawer-resize-handle, + .trace-drawer-resize-handle { + display: none !important; + } + + // 元数据行纵向堆叠 + .doc-detail-rows { + flex-direction: column; + gap: 8px; + } + + .doc-detail-row { + flex: none; + width: 100%; + } + + .doc-detail-label { + flex: 0 0 auto; + min-width: 0; + } + + // 标签 inline 换行 + .doc-tag-chip { + max-width: none; + } + + // 头部操作按钮紧凑 + .header-actions { + gap: 4px; + } + + .header-action-btn { + width: 32px; + height: 32px; + } + + // 内容区全宽 + .md-content { + max-width: 100%; + padding: 0; + } +} + +// ============================================================ +// 平板适配 +// ============================================================ +@media screen and (min-width: 768px) and (max-width: 1023px) { + .t-drawer.doc-main-drawer { + .t-drawer__content-wrapper, + .t-drawer__content { + max-width: 85vw !important; + } + } + + .t-drawer.kp-secondary-drawer { + .t-drawer__content-wrapper, + .t-drawer__content { + max-width: 90vw !important; + } + } +} diff --git a/frontend/src/components/menu.vue b/frontend/src/components/menu.vue index 2e3082b131..e316bd0f49 100644 --- a/frontend/src/components/menu.vue +++ b/frontend/src/components/menu.vue @@ -1,7 +1,7 @@