diff --git a/.cursor/plans/多端响应式适配方案_de37db33.plan.md b/.cursor/plans/多端响应式适配方案_de37db33.plan.md index 2a395847..91e4e5f0 100644 --- a/.cursor/plans/多端响应式适配方案_de37db33.plan.md +++ b/.cursor/plans/多端响应式适配方案_de37db33.plan.md @@ -4,31 +4,31 @@ overview: 系统性地对Aitu白板应用进行手机端、平板端适配,解 todos: - id: responsive-infra content: 创建响应式基础设施(_responsive.scss、useDeviceType hook) - status: pending + status: completed - id: unified-toolbar content: 适配统一工具栏 - 移动端横向布局/可收起 - status: pending + status: completed - id: ai-input-bar content: 适配AI输入栏 - 移动端全宽、安全区域 - status: pending + status: completed - id: view-navigation content: 适配视图导航 - 移动端缩小/右上角 - status: pending + status: completed - id: chat-drawer content: 适配聊天抽屉 - 移动端全屏显示 - status: pending + status: completed - id: popup-toolbar content: 适配弹出工具栏 - 增加触控区域 - status: pending + status: completed - id: dialogs content: 适配对话框组件 - 移动端全屏/安全区域 - status: pending + status: completed - id: touch-fix content: 修复触控问题 - 优化双指缩放阻止逻辑 - status: pending + status: completed - id: e2e-test content: E2E测试验证 - 多视口尺寸测试 - status: pending + status: completed isProject: false --- diff --git a/apps/web-e2e/playwright.config.ts b/apps/web-e2e/playwright.config.ts index a6d990db..8ab677cf 100644 --- a/apps/web-e2e/playwright.config.ts +++ b/apps/web-e2e/playwright.config.ts @@ -109,15 +109,31 @@ export default defineConfig({ }, }, - // Uncomment for mobile browsers support - /* { - name: 'Mobile Chrome', + // 响应式测试 - 多视口尺寸 + { + name: 'responsive', + testMatch: '**/visual/responsive-visual.spec.ts', + use: { ...devices['Desktop Chrome'] }, + }, + + // 移动端浏览器测试 + { + name: 'mobile-chrome', + testMatch: '**/visual/responsive-visual.spec.ts', use: { ...devices['Pixel 5'] }, }, { - name: 'Mobile Safari', + name: 'mobile-safari', + testMatch: '**/visual/responsive-visual.spec.ts', use: { ...devices['iPhone 12'] }, - }, */ + }, + + // 平板测试 + { + name: 'tablet', + testMatch: '**/visual/responsive-visual.spec.ts', + use: { ...devices['iPad Pro 11'] }, + }, // Uncomment for branded browsers /* { diff --git a/apps/web-e2e/src/visual/responsive-visual.spec.ts b/apps/web-e2e/src/visual/responsive-visual.spec.ts new file mode 100644 index 00000000..a6a6e640 --- /dev/null +++ b/apps/web-e2e/src/visual/responsive-visual.spec.ts @@ -0,0 +1,283 @@ +/** + * 响应式视觉测试 + * + * 测试不同视口尺寸下的UI适配效果 + * - 桌面端 (1920x1080, 1280x720) + * - 平板横屏 (1024x768) + * - 平板竖屏 (768x1024) + * - 移动端横屏 (640x360) + * - 移动端竖屏 (375x667, 360x640) + */ + +import { test, expect, Page } from '@playwright/test'; + +// 视口尺寸配置 +const VIEWPORTS = { + // 桌面端 + desktopLarge: { width: 1920, height: 1080, name: 'desktop-large' }, + desktopMedium: { width: 1280, height: 720, name: 'desktop-medium' }, + + // 平板 + tabletLandscape: { width: 1024, height: 768, name: 'tablet-landscape' }, + tabletPortrait: { width: 768, height: 1024, name: 'tablet-portrait' }, + + // 移动端 + mobileLandscape: { width: 640, height: 360, name: 'mobile-landscape' }, + mobilePortrait: { width: 375, height: 667, name: 'mobile-portrait-iphone' }, + mobileSmall: { width: 360, height: 640, name: 'mobile-portrait-android' }, +}; + +// 等待页面加载完成 +async function waitForPageReady(page: Page): Promise { + // 等待主要组件加载 + await page.waitForSelector('.drawnix', { timeout: 30000 }); + // 等待工具栏加载 + await page.waitForSelector('.unified-toolbar', { timeout: 10000 }); + // 给一些额外时间让动画完成 + await page.waitForTimeout(1000); +} + +// 隐藏动态内容以获得稳定截图 +async function prepareForScreenshot(page: Page): Promise { + await page.evaluate(() => { + // 隐藏可能变化的动态内容 + const dynamicElements = document.querySelectorAll( + '.task-queue-panel, .minimap__preview, .loading-indicator' + ); + dynamicElements.forEach(el => { + (el as HTMLElement).style.visibility = 'hidden'; + }); + }); +} + +test.describe('响应式布局测试', () => { + test.beforeEach(async ({ page }) => { + // 设置较长的超时时间 + test.setTimeout(60000); + }); + + // 桌面端测试 + test.describe('桌面端', () => { + test('大屏幕桌面端布局 (1920x1080)', async ({ page }) => { + await page.setViewportSize(VIEWPORTS.desktopLarge); + await page.goto('/'); + await waitForPageReady(page); + await prepareForScreenshot(page); + + // 验证工具栏可见 + await expect(page.locator('.unified-toolbar')).toBeVisible(); + // 验证AI输入栏可见 + await expect(page.locator('.ai-input-bar')).toBeVisible(); + // 验证视图导航可见 + await expect(page.locator('.view-navigation')).toBeVisible(); + + // 截图对比 + await expect(page).toHaveScreenshot(`responsive-${VIEWPORTS.desktopLarge.name}.png`, { + fullPage: false, + maxDiffPixelRatio: 0.1, + }); + }); + + test('中等屏幕桌面端布局 (1280x720)', async ({ page }) => { + await page.setViewportSize(VIEWPORTS.desktopMedium); + await page.goto('/'); + await waitForPageReady(page); + await prepareForScreenshot(page); + + await expect(page.locator('.unified-toolbar')).toBeVisible(); + await expect(page.locator('.ai-input-bar')).toBeVisible(); + + await expect(page).toHaveScreenshot(`responsive-${VIEWPORTS.desktopMedium.name}.png`, { + fullPage: false, + maxDiffPixelRatio: 0.1, + }); + }); + }); + + // 平板测试 + test.describe('平板', () => { + test('平板横屏布局 (1024x768)', async ({ page }) => { + await page.setViewportSize(VIEWPORTS.tabletLandscape); + await page.goto('/'); + await waitForPageReady(page); + await prepareForScreenshot(page); + + // 验证工具栏使用图标模式 + await expect(page.locator('.unified-toolbar')).toBeVisible(); + await expect(page.locator('.ai-input-bar')).toBeVisible(); + + await expect(page).toHaveScreenshot(`responsive-${VIEWPORTS.tabletLandscape.name}.png`, { + fullPage: false, + maxDiffPixelRatio: 0.1, + }); + }); + + test('平板竖屏布局 (768x1024)', async ({ page }) => { + await page.setViewportSize(VIEWPORTS.tabletPortrait); + await page.goto('/'); + await waitForPageReady(page); + await prepareForScreenshot(page); + + await expect(page.locator('.unified-toolbar')).toBeVisible(); + await expect(page.locator('.ai-input-bar')).toBeVisible(); + + await expect(page).toHaveScreenshot(`responsive-${VIEWPORTS.tabletPortrait.name}.png`, { + fullPage: false, + maxDiffPixelRatio: 0.1, + }); + }); + }); + + // 移动端测试 + test.describe('移动端', () => { + test('移动端横屏布局 (640x360)', async ({ page }) => { + await page.setViewportSize(VIEWPORTS.mobileLandscape); + await page.goto('/'); + await waitForPageReady(page); + await prepareForScreenshot(page); + + // 验证组件可见且不遮挡 + await expect(page.locator('.unified-toolbar')).toBeVisible(); + await expect(page.locator('.ai-input-bar')).toBeVisible(); + + await expect(page).toHaveScreenshot(`responsive-${VIEWPORTS.mobileLandscape.name}.png`, { + fullPage: false, + maxDiffPixelRatio: 0.1, + }); + }); + + test('iPhone 竖屏布局 (375x667)', async ({ page }) => { + await page.setViewportSize(VIEWPORTS.mobilePortrait); + await page.goto('/'); + await waitForPageReady(page); + await prepareForScreenshot(page); + + await expect(page.locator('.unified-toolbar')).toBeVisible(); + await expect(page.locator('.ai-input-bar')).toBeVisible(); + + await expect(page).toHaveScreenshot(`responsive-${VIEWPORTS.mobilePortrait.name}.png`, { + fullPage: false, + maxDiffPixelRatio: 0.1, + }); + }); + + test('Android 竖屏布局 (360x640)', async ({ page }) => { + await page.setViewportSize(VIEWPORTS.mobileSmall); + await page.goto('/'); + await waitForPageReady(page); + await prepareForScreenshot(page); + + await expect(page.locator('.unified-toolbar')).toBeVisible(); + await expect(page.locator('.ai-input-bar')).toBeVisible(); + + await expect(page).toHaveScreenshot(`responsive-${VIEWPORTS.mobileSmall.name}.png`, { + fullPage: false, + maxDiffPixelRatio: 0.1, + }); + }); + }); +}); + +test.describe('组件响应式测试', () => { + // 工具栏响应式测试 + test.describe('统一工具栏', () => { + test('移动端工具栏可展开/收起', async ({ page }) => { + await page.setViewportSize(VIEWPORTS.mobilePortrait); + await page.goto('/'); + await waitForPageReady(page); + + const toolbar = page.locator('.unified-toolbar'); + await expect(toolbar).toBeVisible(); + + // 检查移动端切换按钮 + const toggleBtn = page.locator('.unified-toolbar__mobile-toggle'); + // 如果存在切换按钮,测试展开/收起功能 + const toggleExists = await toggleBtn.count() > 0; + if (toggleExists) { + // 初始应该是收起状态 + await expect(toolbar).toHaveClass(/mobile-collapsed/); + + // 点击展开 + await toggleBtn.click(); + await page.waitForTimeout(300); + + // 验证已展开 + await expect(toolbar).not.toHaveClass(/mobile-collapsed/); + } + }); + }); + + // AI输入栏响应式测试 + test.describe('AI输入栏', () => { + test('移动端AI输入栏布局正确', async ({ page }) => { + await page.setViewportSize(VIEWPORTS.mobilePortrait); + await page.goto('/'); + await waitForPageReady(page); + + const inputBar = page.locator('.ai-input-bar'); + await expect(inputBar).toBeVisible(); + + // 验证输入栏不与工具栏重叠 + const toolbar = page.locator('.unified-toolbar'); + const inputBarBox = await inputBar.boundingBox(); + const toolbarBox = await toolbar.boundingBox(); + + if (inputBarBox && toolbarBox) { + // 输入栏和工具栏不应该重叠(允许少量重叠) + const overlap = Math.max(0, + Math.min(inputBarBox.x + inputBarBox.width, toolbarBox.x + toolbarBox.width) - + Math.max(inputBarBox.x, toolbarBox.x) + ) * Math.max(0, + Math.min(inputBarBox.y + inputBarBox.height, toolbarBox.y + toolbarBox.height) - + Math.max(inputBarBox.y, toolbarBox.y) + ); + + expect(overlap).toBeLessThan(100); // 允许最多100px重叠 + } + }); + }); + + // 视图导航响应式测试 + test.describe('视图导航', () => { + test('视图导航在不同视口尺寸下正确显示', async ({ page }) => { + // 桌面端 + await page.setViewportSize(VIEWPORTS.desktopMedium); + await page.goto('/'); + await waitForPageReady(page); + + const viewNav = page.locator('.view-navigation'); + await expect(viewNav).toBeVisible(); + + // 移动端 + await page.setViewportSize(VIEWPORTS.mobilePortrait); + await page.waitForTimeout(500); + + await expect(viewNav).toBeVisible(); + + // 验证缩放控件可见 + await expect(page.locator('.view-navigation__zoom')).toBeVisible(); + }); + }); +}); + +test.describe('触控交互测试', () => { + test('移动端触控区域足够大', async ({ page }) => { + await page.setViewportSize(VIEWPORTS.mobilePortrait); + await page.goto('/'); + await waitForPageReady(page); + + // 检查主要按钮的尺寸 + const buttons = page.locator('.unified-toolbar button, .ai-input-bar button'); + const count = await buttons.count(); + + for (let i = 0; i < Math.min(count, 10); i++) { + const button = buttons.nth(i); + const box = await button.boundingBox(); + if (box) { + // 触控目标应该至少 32x32 像素 + expect(box.width).toBeGreaterThanOrEqual(28); + expect(box.height).toBeGreaterThanOrEqual(28); + } + } + }); +}); diff --git a/apps/web/public/version.json b/apps/web/public/version.json index 5204fe89..8f0be4e5 100644 --- a/apps/web/public/version.json +++ b/apps/web/public/version.json @@ -1,6 +1,6 @@ { "version": "0.5.50", - "buildTime": "2026-01-25T05:01:00.301Z", + "buildTime": "2026-01-25T05:22:53.411Z", "gitCommit": "unknown", "changelog": [ "对比模式下视频联动同步播放和重置" diff --git a/packages/drawnix/src/components/ai-input-bar/ai-input-bar.scss b/packages/drawnix/src/components/ai-input-bar/ai-input-bar.scss index 13dd8289..2e882f71 100644 --- a/packages/drawnix/src/components/ai-input-bar/ai-input-bar.scss +++ b/packages/drawnix/src/components/ai-input-bar/ai-input-bar.scss @@ -1368,54 +1368,93 @@ $vip-color: #E91E63; } } -// Mobile responsive -@media (max-width: 640px) { +// ============================================================================ +// Mobile Responsive Styles +// ============================================================================ + +// 平板横屏及以下 (max-width: 1024px) - 稍微缩窄 +@media (max-width: 1024px) { .ai-input-bar { - bottom: 16px; + max-width: 600px; + } +} + +// 平板及以下 (max-width: 768px) - 调整布局 +@media (max-width: 768px) { + .ai-input-bar { + bottom: calc(env(safe-area-inset-bottom, 0px) + 16px); padding: 0 12px; - // 移动端考虑左侧工具栏宽度,避免遮挡 - left: calc(50% + 24px); // 工具栏宽度48px的一半,使输入框在工具栏右侧区域居中 - max-width: calc(100% - 60px); // 左侧工具栏48px + 右侧边距12px + max-width: calc(100% - 80px); // 给左侧工具栏留空间 + left: calc(50% + 30px); // 稍微右偏以避开工具栏 + + &__container { + padding: 6px 8px; + border-radius: 20px; + } &__content-preview { gap: 6px; } - &__content-item { - - &--text { - height: auto; // 移动端也使用自动高度 - min-width: 56px; - max-width: 140px; - } + &__bottom-bar { + gap: 2px; } + // 隐藏 hover 预览(移动端不需要) &__hover-preview { - max-width: 250px; - max-height: 250px; + display: none; + } + } +} - &--text { - max-width: 280px; - } +// 移动端竖屏 (max-width: 640px) - 全宽布局 +@media (max-width: 640px) { + .ai-input-bar { + bottom: calc(env(safe-area-inset-bottom, 0px) + 12px); + padding: 0 8px; + padding-left: calc(env(safe-area-inset-left, 0px) + 8px); + padding-right: calc(env(safe-area-inset-right, 0px) + 8px); + // 移动端居中显示,工具栏在左下角不会遮挡 + left: 50%; + transform: translateX(-50%); + max-width: calc(100% - 16px); + width: 100%; - &--video { - max-width: 280px; - } + &__content-preview { + gap: 4px; + padding-left: 8px; } - &__hover-video video { - max-width: 280px; - max-height: 250px; + &__content-item { + width: 32px; + height: 32px; + + &--text { + height: auto; + min-width: 48px; + max-width: 120px; + padding: 6px 8px; + } } &__container { - padding: 4px 6px 4px 4px; + padding: 4px 6px; + border-radius: 16px; gap: 4px; } + &__bottom-bar { + gap: 2px; + } + + &__bottom-spacer { + min-width: 4px; + } + &__type-btn { - width: 32px; - height: 32px; + width: 36px; + height: 36px; + min-width: 36px; // 确保触控友好 } &__model-btn { @@ -1423,29 +1462,93 @@ $vip-color: #E91E63; font-size: 13px; } - &__upload-btn { - width: 32px; - height: 32px; + &__upload-btn, + &__library-btn { + width: 36px; + height: 36px; + min-width: 36px; } &__highlight-layer { font-size: 14px; - padding: 4px 6px; + padding: 4px 8px; } &__input { font-size: 14px; - padding: 4px 6px; + padding: 4px 8px; + min-height: 20px; } &__send-btn { width: 36px; height: 36px; + min-width: 36px; } - &__send-logo { - width: 18px; - height: 18px; + // 下拉菜单调整 + .generation-type-dropdown, + .count-dropdown { + &__trigger { + padding: 4px 6px; + font-size: 12px; + } + + &__menu { + max-width: calc(100vw - 32px); + } + } + } + + // Prompt suggestion panel 移动端适配 + .prompt-suggestion-panel { + max-height: 240px; + border-radius: 12px; + + &__item { + padding: 8px 10px; + } + + &__item-text { + font-size: 13px; + } + } +} + +// 小屏移动端 (max-width: 480px) - 更紧凑 +@media (max-width: 480px) { + .ai-input-bar { + bottom: calc(env(safe-area-inset-bottom, 0px) + 8px); + padding: 0 6px; + padding-left: calc(env(safe-area-inset-left, 0px) + 6px); + padding-right: calc(env(safe-area-inset-right, 0px) + 6px); + + &__container { + padding: 4px; + } + + &__type-btn, + &__upload-btn, + &__library-btn, + &__send-btn { + width: 34px; + height: 34px; + min-width: 34px; + } + + &__model-btn { + padding: 4px 6px; + font-size: 12px; + } + + &__input { + font-size: 13px; + } + + // 隐藏一些次要元素节省空间 + .generation-type-dropdown__label, + .count-dropdown__label { + display: none; } } } diff --git a/packages/drawnix/src/components/chat-drawer/chat-drawer.scss b/packages/drawnix/src/components/chat-drawer/chat-drawer.scss index 89c1b5c4..dd390071 100644 --- a/packages/drawnix/src/components/chat-drawer/chat-drawer.scss +++ b/packages/drawnix/src/components/chat-drawer/chat-drawer.scss @@ -724,7 +724,246 @@ $drawer-input-height: 160px; // Mobile Styles // ============================================================================ +// 平板及以下 (max-width: 768px) - 全屏抽屉 +@media (max-width: 768px) { + .chat-drawer { + width: 100vw !important; + max-width: 100vw !important; + border-left: none; + border-radius: 0; + // 添加安全区域支持 + padding-top: env(safe-area-inset-top, 0px); + padding-bottom: env(safe-area-inset-bottom, 0px); + + &__resize-handle { + display: none; // 移动端不需要调整宽度 + } + + &__header { + padding: $spacing-sm $spacing-md; + padding-top: calc(env(safe-area-inset-top, 0px) + #{$spacing-sm}); + } + + &__header-top { + gap: $spacing-xs; + } + + &__title { + font-size: 15px; + } + + &__close-btn { + width: 36px; + height: 36px; + min-width: 36px; // 触控友好 + } + + &__content { + height: calc(100vh - #{$drawer-header-height} - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px)); + } + } + + .chat-drawer-trigger { + // 移动端隐藏侧边触发器,使用其他方式打开 + display: none; + } + + // 消息列表调整 + .chat-messages-list { + padding: $spacing-md; + } + + .chat-message { + max-width: 95%; + } + + // 输入区域调整 + .enhanced-chat-input { + padding: $spacing-sm $spacing-md; + padding-bottom: calc(env(safe-area-inset-bottom, 0px) + #{$spacing-sm}); + + &__form { + padding: 6px; + } + + &__textarea { + min-height: 60px; + font-size: 15px; + } + + &__send { + width: 36px; + height: 36px; + min-width: 36px; + } + } +} + +// 移动端竖屏 (max-width: 640px) +@media (max-width: 640px) { + .chat-drawer { + &__header { + padding: $spacing-xs $spacing-sm; + gap: $spacing-xs; + } + + &__header-bottom { + padding: 4px 8px; + gap: $spacing-xs; + } + + &__title { + font-size: 14px; + } + + &__session-actions { + gap: 2px; + } + } + + // 会话列表调整 + .session-list { + top: 80px; + max-height: 50vh; + + &__header { + padding: $spacing-xs $spacing-md; + } + + &__new-btn { + padding: 6px 10px; + font-size: 11px; + } + } + + .session-item { + padding: $spacing-xs $spacing-sm; + + &__title { + font-size: 12px; + } + } + + // 消息样式调整 + .chat-messages-list { + padding: $spacing-sm; + gap: $spacing-md; + } + + .chat-message-content { + padding: $spacing-sm $spacing-md; + font-size: 13px; + } + + .chat-message-avatar { + width: 28px; + height: 28px; + + svg { + width: 14px; + height: 14px; + } + } + + // 输入区域更紧凑 + .enhanced-chat-input { + padding: $spacing-xs $spacing-sm; + max-height: 140px; + + &__form { + padding: 4px; + border-radius: 12px; + } + + &__textarea { + min-height: 44px; + max-height: 80px; + padding: 8px 10px; + font-size: 14px; + } + + &__selection { + gap: $spacing-xs; + padding-bottom: $spacing-xs; + margin-bottom: $spacing-xs; + } + + &__selection-item { + width: 32px; + height: 32px; + + &--text { + min-width: 48px; + max-width: 100px; + padding: 4px 8px; + } + } + } + + // 模型选择器调整 + .model-selector { + &__trigger { + padding: 3px 8px; + font-size: 11px; + } + + &__dropdown { + min-width: 280px; + max-width: calc(100vw - 32px); + } + + &__item { + padding: 8px 10px; + } + + &__item-name { + font-size: 13px; + } + + &__item-desc { + font-size: 11px; + } + } +} + +// 小屏移动端 (max-width: 480px) +@media (max-width: 480px) { + .chat-drawer { + &__header-bottom { + // 隐藏会话操作按钮,节省空间 + .chat-drawer__session-actions { + display: none; + } + } + } + + .enhanced-chat-input { + &__selection-item { + width: 28px; + height: 28px; + } + + &__send { + width: 32px; + height: 32px; + min-width: 32px; + } + } + + .model-selector { + &--variant-capsule { + .model-selector__trigger { + padding: 2px 6px; + font-size: 10px; + } + } + + &__trigger-text { + max-width: 60px; + } + } +} // ============================================================================ // @llamaindex/chat-ui Component Styles diff --git a/packages/drawnix/src/components/media-library/MediaLibraryGrid.scss b/packages/drawnix/src/components/media-library/MediaLibraryGrid.scss index 6c268817..cb4ca8f4 100644 --- a/packages/drawnix/src/components/media-library/MediaLibraryGrid.scss +++ b/packages/drawnix/src/components/media-library/MediaLibraryGrid.scss @@ -326,25 +326,30 @@ $brand-orange-dark: #E67E22; &__search { flex: 1; max-width: 480px; + min-width: 0; // 防止 flex 子元素溢出 .t-input { - border-radius: 6px; - border-color: rgba(0, 0, 0, 0.1); - background: #f8fafc; + border-radius: 8px !important; + border: 1px solid rgba(0, 0, 0, 0.1) !important; + background: #f8fafc !important; transition: all 0.15s ease; - height: 32px !important; - min-height: 32px !important; + height: 36px !important; + min-height: 36px !important; &:hover { - border-color: rgba(0, 0, 0, 0.2); - background: #ffffff; + border-color: rgba(0, 0, 0, 0.2) !important; + background: #ffffff !important; } &:focus, &.t-is-focused { - border-color: #000; - background: #ffffff; - box-shadow: 0 0 0 1px #000; + border-color: #000 !important; + background: #ffffff !important; + box-shadow: 0 0 0 1px #000 !important; + } + + .t-input__inner { + font-size: 14px; } } } @@ -552,14 +557,111 @@ $brand-orange-dark: #E67E22; } } + // ========== 移动端选中素材简介栏 ========== + &__mobile-inspector { + display: none; // 默认隐藏,仅在移动端媒体查询中显示 + } + // ========== 移动端优化 ========== @media (max-width: 768px) { + // 移动端选中素材简介栏 + &__mobile-inspector { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 12px; + background: #ffffff; + border-top: 1px solid rgba(0, 0, 0, 0.06); + flex-shrink: 0; + + &-info { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; + } + + &-type { + display: flex; + align-items: center; + gap: 4px; + font-size: 11px; + color: #64748b; + + svg { + width: 14px; + height: 14px; + } + } + + &-ai { + background: rgba(0, 0, 0, 0.6); + color: #fff; + font-size: 9px; + font-weight: 600; + padding: 2px 5px; + border-radius: 3px; + margin-left: 4px; + } + + &-name { + font-size: 13px; + font-weight: 500; + color: #1e293b; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &-actions { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; + } + + &-btn { + width: 36px; + height: 36px; + border: none; + border-radius: 8px; + background: #f1f5f9; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.15s ease; + color: #64748b; + + &:hover, &:active { + background: #e2e8f0; + color: #1e293b; + } + + &--primary { + background: #F39C12; + color: #fff; + + &:hover, &:active { + background: #E67E22; + color: #fff; + } + } + } + } &__header { padding: 10px 12px; gap: 8px; flex-wrap: wrap; } + &__header-top { + flex-wrap: nowrap; + gap: 8px; + } + &__header-left { width: 100%; flex-wrap: wrap; @@ -567,13 +669,41 @@ $brand-orange-dark: #E67E22; } &__header-right { - width: 100%; + flex: 1; justify-content: flex-end; + flex-wrap: nowrap; + gap: 6px; + + // 移动端按钮只显示图标 + .t-button { + flex-shrink: 0; + padding: 0 8px !important; + min-width: 36px !important; + height: 36px !important; + + // 隐藏按钮文字,只显示图标 + .t-button__text { + display: none; + } + } } &__search { - width: 100%; flex: 1; + min-width: 0; + max-width: none !important; + + .t-input { + min-height: 36px !important; + height: 36px !important; + border-radius: 8px !important; + border: 1px solid rgba(0, 0, 0, 0.1) !important; + background: #f8fafc !important; + + .t-input__inner { + font-size: 16px; // 防止 iOS 自动缩放 + } + } } &__filter-select { @@ -581,9 +711,95 @@ $brand-orange-dark: #E67E22; } &__footer { - padding: 0 12px; + padding: 8px 12px; font-size: 11px; + height: auto; + min-height: 40px; + flex-wrap: nowrap; + gap: 12px; + } + + &__footer-left { + flex: 1; + gap: 8px; + flex-wrap: nowrap; + min-width: 0; + overflow: hidden; + } + + &__footer-right { + flex-shrink: 0; + } + + &__footer-storage { + flex-shrink: 0; + } + + &__footer-count { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + &::after { + display: none; + } + } + + &__footer-hint { + display: none; // 移动端隐藏提示 + } + + &__zoom-control { + // 移动端保留缩放控制,但更紧凑 + gap: 6px; + + svg { + width: 16px; + height: 16px; + } + } + + &__zoom-slider { + width: 80px !important; // 移动端滑块更短 + } + + // 筛选按钮组更紧凑 - 只显示图标 + &__filter-option { + padding: 0 6px !important; + min-width: 32px; height: 32px; + + // 隐藏数字 + .media-library-grid__filter-count { + display: none; + } + } + + // header-bottom 支持水平滚动(作为备用) + &__header-bottom { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + gap: 8px; + padding: 4px 8px; + + &::-webkit-scrollbar { + display: none; + } + } + + // 筛选岛更紧凑 + &__filter-island { + flex-shrink: 0; + padding: 2px; + gap: 2px; + } + + // 排序选项更紧凑 + &__sort-options { + flex-shrink: 0; + padding: 2px; + gap: 2px; } &__drop-message { diff --git a/packages/drawnix/src/components/media-library/MediaLibraryGrid.tsx b/packages/drawnix/src/components/media-library/MediaLibraryGrid.tsx index 502d1d04..f7a451a6 100644 --- a/packages/drawnix/src/components/media-library/MediaLibraryGrid.tsx +++ b/packages/drawnix/src/components/media-library/MediaLibraryGrid.tsx @@ -12,6 +12,7 @@ import { XSquare, HardDrive, Video as VideoIcon, + Image as ImageIcon, Globe, User, Sparkles, @@ -23,7 +24,9 @@ import { ArrowUpNarrowWide, Minus, Plus, - Download + Download, + Eye, + PlusCircle, } from 'lucide-react'; import JSZip from 'jszip'; import { @@ -31,7 +34,9 @@ import { MediaLibraryIcon, } from '../icons'; import { useAssets } from '../../contexts/AssetContext'; -import { filterAssets, formatFileSize } from '../../utils/asset-utils'; +import { filterAssets, formatFileSize, formatDate } from '../../utils/asset-utils'; +import { useDeviceType } from '../../hooks/useDeviceType'; +import { downloadFile } from '../../utils/download-utils'; import { VirtualAssetGrid } from './VirtualAssetGrid'; import { MediaLibraryEmpty } from './MediaLibraryEmpty'; import { ViewModeToggle } from './ViewModeToggle'; @@ -130,8 +135,9 @@ export function MediaLibraryGrid({ onUploadClick, storageStatus, }: MediaLibraryGridProps) { - const { assets, filters, loading, setFilters, removeAssets } = useAssets(); + const { assets, filters, loading, setFilters, removeAssets, removeAsset } = useAssets(); const { board } = useDrawnix(); + const { isMobile } = useDeviceType(); const [isDragging, setIsDragging] = useState(false); const [isSelectionMode, setIsSelectionMode] = useState(false); const [selectedAssetIds, setSelectedAssetIds] = useState>(new Set()); @@ -164,6 +170,12 @@ export function MediaLibraryGrid({ }; }, [assets]); + // 获取选中的素材(用于移动端底部简介) + const selectedAsset = useMemo(() => { + if (!selectedAssetId) return null; + return assets.find(a => a.id === selectedAssetId) || null; + }, [assets, selectedAssetId]); + // 监听网格尺寸变化并缓存 useEffect(() => { localStorage.setItem(GRID_SIZE_STORAGE_KEY, gridSize.toString()); @@ -644,15 +656,16 @@ export function MediaLibraryGrid({ >
- setFilters({ searchQuery: value as string })} - placeholder="搜索素材..." - prefixIcon={} - clearable - data-track="grid_search_input" - className="media-library-grid__search" - /> +
+ setFilters({ searchQuery: value as string })} + placeholder="搜索素材..." + prefixIcon={} + clearable + data-track="grid_search_input" + /> +
{isSelectionMode ? ( @@ -880,7 +893,7 @@ export function MediaLibraryGrid({ assets={filteredResult.assets} viewMode={viewMode} gridSize={gridSize} - selectedAssetId={selectedAssetId} + selectedAssetId={selectedAssetId ?? undefined} selectedAssetIds={selectedAssetIds} isSelectionMode={isSelectionMode} onSelectAsset={isSelectionMode ? toggleAssetSelection : onSelectAsset} @@ -889,6 +902,56 @@ export function MediaLibraryGrid({ />
+ {/* 移动端:选中素材时显示简介和操作 */} + {isMobile && selectedAsset && !isSelectionMode && ( +
+
+
+ {selectedAsset.type === AssetType.IMAGE ? : } + {selectedAsset.type === AssetType.IMAGE ? '图片' : '视频'} + {selectedAsset.source === AssetSource.AI_GENERATED && ( + AI + )} +
+
+ {selectedAsset.name} +
+
+
+ + + + {onDoubleClick && ( + + + + )} + + + +
+
+ )} + + {/* 底部工具栏(始终显示) */}
{storageStatus ? ( diff --git a/packages/drawnix/src/components/media-library/MediaLibraryModal.tsx b/packages/drawnix/src/components/media-library/MediaLibraryModal.tsx index ba27e9ae..ce3aef51 100644 --- a/packages/drawnix/src/components/media-library/MediaLibraryModal.tsx +++ b/packages/drawnix/src/components/media-library/MediaLibraryModal.tsx @@ -87,18 +87,14 @@ export function MediaLibraryModal({ return () => window.removeEventListener('resize', checkMobile); }, []); - // 处理资产选择 + // 处理资产选择 - 移动端单击只选中,不弹出详情 const handleSelectAsset = useCallback( (id: string) => { setLocalSelectedAssetId(id); setSelectedAssetId(id); - - // 在移动端,点击素材时打开抽屉 - if (isMobile) { - setShowMobileInspector(true); - } + // 移动端不再自动弹出详情抽屉,用户可以通过双击预览或点击详情按钮 }, - [setSelectedAssetId, isMobile], + [setSelectedAssetId], ); // 关闭移动端检查器 diff --git a/packages/drawnix/src/components/menu/menu.scss b/packages/drawnix/src/components/menu/menu.scss index 193d0d0f..5c364c06 100644 --- a/packages/drawnix/src/components/menu/menu.scss +++ b/packages/drawnix/src/components/menu/menu.scss @@ -86,7 +86,6 @@ &:active { background-color: var(--color-surface-primary-container); - border-color: var(--color-brand-active); } svg { diff --git a/packages/drawnix/src/components/shared/media-preview/MediaViewport.scss b/packages/drawnix/src/components/shared/media-preview/MediaViewport.scss index 382a3ffe..3532cd1c 100644 --- a/packages/drawnix/src/components/shared/media-preview/MediaViewport.scss +++ b/packages/drawnix/src/components/shared/media-preview/MediaViewport.scss @@ -210,6 +210,8 @@ cursor: grab; border-radius: 4px; transition: color 0.2s ease, background 0.2s ease; + touch-action: none; // 防止触摸手势被浏览器拦截 + user-select: none; &:hover { color: rgba(255, 255, 255, 0.8); @@ -288,3 +290,78 @@ opacity: 1; } } + +/* 移动端响应式适配 */ +@media (max-width: 768px) { + .media-viewport { + // 启用触摸操作 + touch-action: pan-x pan-y pinch-zoom; + + &__toolbar { + // 限制最大宽度,避免超出屏幕 + max-width: calc(100vw - 32px); + overflow-x: auto; + overflow-y: hidden; + padding: 4px 8px; + gap: 2px; + + // 隐藏滚动条 + scrollbar-width: none; + -ms-overflow-style: none; + &::-webkit-scrollbar { + display: none; + } + + button { + width: 28px; + height: 28px; + flex-shrink: 0; + } + } + + &__toolbar-handle { + width: 32px; + height: 32px; + flex-shrink: 0; + } + + &__toolbar-divider { + height: 16px; + margin: 0 4px; + flex-shrink: 0; + } + + &__zoom-level { + min-width: 40px; + font-size: 11px; + flex-shrink: 0; + } + + &__toolbar-group { + flex-shrink: 0; + } + } +} + +@media (max-width: 480px) { + .media-viewport { + &__toolbar { + padding: 3px 6px; + gap: 1px; + + button { + width: 26px; + height: 26px; + } + } + + &__toolbar-divider { + margin: 0 2px; + } + + &__zoom-level { + min-width: 36px; + font-size: 10px; + } + } +} diff --git a/packages/drawnix/src/components/shared/media-preview/MediaViewport.tsx b/packages/drawnix/src/components/shared/media-preview/MediaViewport.tsx index 8bda5655..1245ad68 100644 --- a/packages/drawnix/src/components/shared/media-preview/MediaViewport.tsx +++ b/packages/drawnix/src/components/shared/media-preview/MediaViewport.tsx @@ -251,7 +251,7 @@ export const MediaViewport = forwardRef(({ ); }, []); - // 工具栏拖拽开始 + // 工具栏拖拽开始 - 鼠标事件 const handleToolbarDragStart = useCallback( (e: React.MouseEvent) => { e.preventDefault(); @@ -287,6 +287,49 @@ export const MediaViewport = forwardRef(({ [toolbarPosition] ); + // 工具栏拖拽开始 - 触摸事件 + const handleToolbarTouchStart = useCallback( + (e: React.TouchEvent) => { + e.stopPropagation(); + if (e.touches.length !== 1) return; + + const touch = e.touches[0]; + setIsToolbarDragging(true); + + const currentPos = toolbarPosition || { x: 0, y: 0 }; + toolbarDragStartRef.current = { + x: touch.clientX, + y: touch.clientY, + posX: currentPos.x, + posY: currentPos.y, + }; + + const handleTouchMove = (moveEvent: TouchEvent) => { + if (moveEvent.touches.length !== 1) return; + moveEvent.preventDefault(); + const moveTouch = moveEvent.touches[0]; + const deltaX = moveTouch.clientX - toolbarDragStartRef.current.x; + const deltaY = moveTouch.clientY - toolbarDragStartRef.current.y; + setToolbarPosition({ + x: toolbarDragStartRef.current.posX + deltaX, + y: toolbarDragStartRef.current.posY + deltaY, + }); + }; + + const handleTouchEnd = () => { + setIsToolbarDragging(false); + document.removeEventListener('touchmove', handleTouchMove); + document.removeEventListener('touchend', handleTouchEnd); + document.removeEventListener('touchcancel', handleTouchEnd); + }; + + document.addEventListener('touchmove', handleTouchMove, { passive: false }); + document.addEventListener('touchend', handleTouchEnd); + document.addEventListener('touchcancel', handleTouchEnd); + }, + [toolbarPosition] + ); + // 重置工具栏位置 const resetToolbarPosition = useCallback(() => { setToolbarPosition(null); @@ -382,6 +425,7 @@ export const MediaViewport = forwardRef(({
{ e.stopPropagation(); resetToolbarPosition(); diff --git a/packages/drawnix/src/components/side-drawer/SideDrawer.tsx b/packages/drawnix/src/components/side-drawer/SideDrawer.tsx index 4843e257..d76623d6 100644 --- a/packages/drawnix/src/components/side-drawer/SideDrawer.tsx +++ b/packages/drawnix/src/components/side-drawer/SideDrawer.tsx @@ -128,39 +128,51 @@ export const SideDrawer: React.FC = ({ } }, [closeOnBackdropClick, onClose]); - // 开始拖拽 - const handleResizeStart = useCallback((e: React.MouseEvent) => { + // 开始拖拽 - 支持鼠标和触摸 + const handleResizeStart = useCallback((e: React.MouseEvent | React.TouchEvent) => { e.preventDefault(); if (!drawerRef.current) return; setIsDragging(true); - startXRef.current = e.clientX; + // 获取起始 X 坐标(支持鼠标和触摸) + const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX; + startXRef.current = clientX; startWidthRef.current = drawerRef.current.offsetWidth; }, []); - // 拖拽中 + // 拖拽中 - 支持鼠标和触摸 useEffect(() => { if (!isDragging) return; - const handleMouseMove = (e: MouseEvent) => { - const deltaX = e.clientX - startXRef.current; + const handleMove = (e: MouseEvent | TouchEvent) => { + // 获取当前 X 坐标(支持鼠标和触摸) + const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX; + const deltaX = clientX - startXRef.current; const newWidth = Math.min(maxWidth, Math.max(minWidth, startWidthRef.current + deltaX)); setDraggingWidth(newWidth); }; - const handleMouseUp = () => { + const handleEnd = () => { setIsDragging(false); if (draggingWidth !== null) { onWidthChange?.(draggingWidth); } }; - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); + // 鼠标事件 + document.addEventListener('mousemove', handleMove); + document.addEventListener('mouseup', handleEnd); + // 触摸事件 + document.addEventListener('touchmove', handleMove, { passive: false }); + document.addEventListener('touchend', handleEnd); + document.addEventListener('touchcancel', handleEnd); return () => { - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); + document.removeEventListener('mousemove', handleMove); + document.removeEventListener('mouseup', handleEnd); + document.removeEventListener('touchmove', handleMove); + document.removeEventListener('touchend', handleEnd); + document.removeEventListener('touchcancel', handleEnd); }; }, [isDragging, draggingWidth, minWidth, maxWidth, onWidthChange]); @@ -231,6 +243,7 @@ export const SideDrawer: React.FC = ({
)}
diff --git a/packages/drawnix/src/components/side-drawer/side-drawer.scss b/packages/drawnix/src/components/side-drawer/side-drawer.scss index 73749021..30ab0c70 100644 --- a/packages/drawnix/src/components/side-drawer/side-drawer.scss +++ b/packages/drawnix/src/components/side-drawer/side-drawer.scss @@ -47,6 +47,16 @@ $drawer-width-wide: min(1024px, 50vw); &.side-drawer--open { transform: translateX(0); } + + // 移动端适配:抽屉从屏幕左边缘开始 + @media (max-width: 768px) { + left: 0; + transform: translateX(-100%); + + &.side-drawer--open { + transform: translateX(0); + } + } } // 屏幕左边缘(TaskQueuePanel) @@ -81,7 +91,10 @@ $drawer-width-wide: min(1024px, 50vw); width: clamp(320px, 50vw, 1024px); @media (max-width: 768px) { - width: 100%; + width: 100vw; + max-width: 100vw; + border-radius: 0; + left: 0 !important; // 覆盖 toolbar-right 的 left 设置 } } @@ -236,6 +249,28 @@ $drawer-width-wide: min(1024px, 50vw); // Resize Handle // ======================================== + // ======================================== + // Mobile Responsive + // ======================================== + + @media (max-width: 768px) { + &__header { + padding: $spacing-md $spacing-sm; + } + + &__title { + font-size: 16px; + } + + &__filter { + padding: 0 $spacing-sm $spacing-sm; + } + + &__content { + padding-left: $spacing-sm; + } + } + &__resize-handle { position: absolute; top: 0; @@ -279,3 +314,29 @@ $drawer-width-wide: min(1024px, 50vw); background: #F39C12; } } + +// ============================================================================ +// Mobile Responsive Styles for Side Drawer +// ============================================================================ + +@media (max-width: 768px) { + .side-drawer { + // 移动端增加 resize handle 触摸区域 + &__resize-handle { + width: 20px; // 增加触摸区域宽度 + right: -10px; + + // 移动端始终显示拖动指示条 + &::after { + opacity: 0.5; + width: 6px; + height: 50px; + } + + &:active::after { + opacity: 1; + background: #F39C12; + } + } + } +} diff --git a/packages/drawnix/src/components/task-queue/VirtualTaskList.tsx b/packages/drawnix/src/components/task-queue/VirtualTaskList.tsx index acc5fb98..09e93593 100644 --- a/packages/drawnix/src/components/task-queue/VirtualTaskList.tsx +++ b/packages/drawnix/src/components/task-queue/VirtualTaskList.tsx @@ -19,7 +19,7 @@ const EMPTY_SET = new Set(); const TASK_ITEM_HEIGHT = 140; const TASK_ITEM_GAP = 0; const OVERSCAN_COUNT = 3; -const COMPACT_LAYOUT_THRESHOLD = 500; +const COMPACT_LAYOUT_THRESHOLD = 375; export interface VirtualTaskListProps { tasks: Task[]; @@ -66,7 +66,13 @@ export const VirtualTaskList: React.FC = ({ const parentRef = useRef(null); const containerRef = useRef(null); const scrollContainerRef = useRef(null); - const [internalIsCompact, setInternalIsCompact] = useState(false); + // 惰性初始化:小屏幕默认使用紧凑布局(用屏幕宽度作为初始估计) + const [internalIsCompact, setInternalIsCompact] = useState(() => { + if (typeof window !== 'undefined' && window.innerWidth < COMPACT_LAYOUT_THRESHOLD) { + return true; + } + return false; + }); const [showBackToTop, setShowBackToTop] = useState(false); // 统一的布局检测模式 @@ -136,32 +142,36 @@ export const VirtualTaskList: React.FC = ({ // 使用一个全局的 ResizeObserver 监听容器宽度,批量同步给所有 TaskItem // 支持抽屉(.side-drawer__content)和弹窗(.t-dialog__body)等多种容器 + // 根据容器宽度判断是否使用紧凑布局 useEffect(() => { if (forcedIsCompact !== undefined) return; const container = parentRef.current; if (!container) return; - // 查找合适的父容器来监听宽度(优先级:抽屉 > WinBox 内容区 > 弹窗任务列表 > 弹窗主体 > 自身) - const drawerContent = container.closest('.side-drawer__content') as HTMLElement; - const winboxContent = container.closest('.winbox-content-wrapper') as HTMLElement; - const dialogTaskList = container.closest('.dialog-task-list') as HTMLElement; - const dialogBody = container.closest('.t-dialog__body') as HTMLElement; - const targetElement = drawerContent || winboxContent || dialogTaskList || dialogBody || container; + // 查找合适的父容器来监听宽度 + // 注意:监听 .side-drawer 而不是 .side-drawer__content,因为宽度变化发生在外层 + const targetElement = container; + // 统一的布局检测函数 - 根据容器宽度判断 + const updateLayout = (containerWidth?: number) => { + const width = containerWidth ?? targetElement.getBoundingClientRect().width; + setInternalIsCompact(width < COMPACT_LAYOUT_THRESHOLD); + }; + + // 监听容器宽度变化(处理抽屉拖动、窗口缩放等) const resizeObserver = new ResizeObserver((entries) => { - const width = entries[0].contentRect.width; - const shouldBeCompact = width < COMPACT_LAYOUT_THRESHOLD; - setInternalIsCompact(shouldBeCompact); + updateLayout(entries[0].contentRect.width); }); - resizeObserver.observe(targetElement); - // 初始检查一次 - const initialWidth = targetElement.getBoundingClientRect().width; - setInternalIsCompact(initialWidth < COMPACT_LAYOUT_THRESHOLD); - - return () => resizeObserver.disconnect(); - }, [forcedIsCompact]); + + // 初始检查 + updateLayout(); + + return () => { + resizeObserver.disconnect(); + }; + }, [forcedIsCompact, parentRef.current]); // Only use virtualization for large lists const useVirtualization = tasks.length > VIRTUALIZATION_THRESHOLD; diff --git a/packages/drawnix/src/components/task-queue/task-queue.scss b/packages/drawnix/src/components/task-queue/task-queue.scss index f09dbe3c..0e5a8695 100644 --- a/packages/drawnix/src/components/task-queue/task-queue.scss +++ b/packages/drawnix/src/components/task-queue/task-queue.scss @@ -614,6 +614,188 @@ } } +// Mobile specific styles +@media (max-width: 768px) { + // 任务队列抽屉的标题 + .side-drawer__title { + font-size: 16px !important; + } + + .side-drawer__header { + padding: 12px !important; + } + + .side-drawer__filter { + padding: 0 12px 12px !important; + } + + .task-queue-panel { + &__filters-container { + gap: 6px; + + // TDesign Tabs 移动端样式 - 更紧凑 + .t-tabs { + .t-tabs__nav-container { + padding: 0 !important; + } + + .t-tabs__nav-wrap { + padding: 0 !important; + } + + .t-tabs__nav { + gap: 0 !important; // 移除 tab 间距 + } + + .t-tabs__nav-item { + font-size: 11px !important; + padding: 4px 2px !important; // 最小 padding + min-width: auto !important; + white-space: nowrap; + margin: 0 !important; + } + + // 缩小滚动箭头 + .t-tabs__nav-scroll-btn { + width: 14px !important; + min-width: 14px !important; + padding: 0 !important; + } + + // 底部指示条 + .t-tabs__bar { + height: 2px !important; + } + } + } + + &__filters { + gap: 6px; + } + + &__type-filters { + padding: 2px; + border-radius: 8px; + + .t-button { + height: 30px !important; + min-width: 30px !important; + } + } + + &__search-input { + flex: 1; + min-width: 80px !important; + + .t-input { + min-height: 28px !important; + height: 28px !important; + font-size: 13px !important; + border-width: 1px !important; + } + + .t-input__inner { + font-size: 13px !important; + } + } + + &__filter-actions { + .t-button { + min-height: 28px !important; + height: 28px !important; + padding: 0 10px !important; + font-size: 12px !important; + } + } + + &__content { + padding: 8px 0 16px 12px; + } + + &__list { + gap: 8px; + } + + &__empty { + padding: 32px 16px; + + &-icon { + font-size: 36px; + margin-bottom: 12px; + } + + &-text { + font-size: 13px; + } + } + } + + // Task Item mobile styles + .task-item { + padding: 12px 16px 12px 0; + gap: 3px 8px; + + // 覆盖全局按钮移动端样式,保持紧凑 + &__primary-action { + min-height: 26px !important; + height: 26px !important; + min-width: auto !important; + padding: 0 10px !important; + } + + &--wide { + grid-template-columns: 80px 1fr; + + &.task-item--selection-mode { + grid-template-columns: auto 80px 1fr; + } + } + + &--compact { + grid-template-columns: 70px 1fr; + grid-template-areas: + "preview prompt" + "info info"; + + &.task-item--selection-mode { + grid-template-columns: auto 70px 1fr; + grid-template-areas: + "checkbox preview prompt" + "info info info"; + } + + .task-item__preview-wrapper { + grid-area: preview; + width: 70px; + height: 52px; + } + + .task-item__prompt-area { + grid-area: prompt; + } + + .task-item__info-area { + grid-area: info; + margin-top: 4px; + } + } + + &__preview-wrapper { + width: 80px; + height: 60px; + } + + &__prompt { + font-size: 13px; + -webkit-line-clamp: 2; + } + + &__checkbox { + padding-left: 12px; + } + } +} + // Animations @keyframes slideUp { from { diff --git a/packages/drawnix/src/components/tool-icon.scss b/packages/drawnix/src/components/tool-icon.scss index 2267f168..5162f3f0 100644 --- a/packages/drawnix/src/components/tool-icon.scss +++ b/packages/drawnix/src/components/tool-icon.scss @@ -77,7 +77,7 @@ background-color: initial; &:focus-visible { - box-shadow: 0 0 0 2px var(--color-primary); + box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1); outline: none; } diff --git a/packages/drawnix/src/components/toolbar/pen-settings-toolbar/pen-settings-toolbar.scss b/packages/drawnix/src/components/toolbar/pen-settings-toolbar/pen-settings-toolbar.scss index bee443a8..0c12d147 100644 --- a/packages/drawnix/src/components/toolbar/pen-settings-toolbar/pen-settings-toolbar.scss +++ b/packages/drawnix/src/components/toolbar/pen-settings-toolbar/pen-settings-toolbar.scss @@ -161,3 +161,86 @@ } } } + +// ============================================================================ +// Mobile Responsive Styles +// ============================================================================ + +// 平板及以下 (max-width: 768px) +@media (max-width: 768px) { + .pen-settings-toolbar { + // 限制最大宽度,给右侧缩放控制留出更多空间 + max-width: calc(100vw - 250px); + + .island { + // 支持水平滚动 + overflow-x: auto; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + flex-wrap: nowrap; + padding: 3px 4px !important; + gap: 2px; + + &::-webkit-scrollbar { + display: none; + } + } + + // 按钮不收缩,尺寸更紧凑 + .pen-color-button, + .pen-width-button { + flex-shrink: 0; + height: 26px; + width: 26px; + min-width: 26px; + } + + // 选择器不收缩 + .pen-stroke-style-picker, + .pen-anchor-type-picker { + flex-shrink: 0; + padding: 1px; + gap: 1px; + + .pen-stroke-style-button, + .pen-anchor-type-button { + height: 24px; + width: 24px; + min-width: 24px; + } + } + + // 隐藏输入框,使用按钮弹出选择 + .pen-width-input-wrapper { + display: none; + } + + // 紧凑的分隔线 + .pen-settings-divider { + flex-shrink: 0; + height: 14px; + margin: 0 2px; + } + } +} + +// 移动端竖屏 (max-width: 640px) +@media (max-width: 640px) { + .pen-settings-toolbar { + // 进一步限制最大宽度 + max-width: calc(100vw - 200px); + } +} + +// 小屏移动端 (max-width: 480px) +@media (max-width: 480px) { + .pen-settings-toolbar { + max-width: calc(100vw - 180px); + + // 进一步紧凑化 + .island { + padding: 2px 3px !important; + } + } +} diff --git a/packages/drawnix/src/components/toolbar/pencil-settings-toolbar/pencil-settings-toolbar.scss b/packages/drawnix/src/components/toolbar/pencil-settings-toolbar/pencil-settings-toolbar.scss index c7dc2479..a6b1e0cc 100644 --- a/packages/drawnix/src/components/toolbar/pencil-settings-toolbar/pencil-settings-toolbar.scss +++ b/packages/drawnix/src/components/toolbar/pencil-settings-toolbar/pencil-settings-toolbar.scss @@ -277,3 +277,110 @@ padding: 0 4px !important; } } + +// ============================================================================ +// Mobile Responsive Styles +// ============================================================================ + +// 平板及以下 (max-width: 768px) +@media (max-width: 768px) { + .pencil-settings-toolbar { + // 限制最大宽度,给右侧缩放控制留出更多空间 + max-width: calc(100vw - 260px); + + .island { + // 支持水平滚动 + overflow-x: auto; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + flex-wrap: nowrap; + padding: 3px 4px !important; + gap: 2px; + + &::-webkit-scrollbar { + display: none; + } + } + + // 隐藏光标预览点(节省空间) + .cursor-preview-dot { + display: none; + } + + // 按钮不收缩,尺寸更紧凑 + .pencil-color-button, + .pencil-width-button, + .size-picker-button { + flex-shrink: 0; + height: 26px; + width: 26px; + min-width: 26px; + } + + + // 描边样式选择器不收缩 + .pencil-stroke-style-picker, + .eraser-shape-picker { + flex-shrink: 0; + padding: 1px; + gap: 1px; + + .pencil-stroke-style-button, + .eraser-shape-button { + height: 24px; + width: 24px; + min-width: 24px; + } + } + + // 隐藏输入框,使用按钮弹出选择 + .pencil-width-input-wrapper, + .size-picker-input-wrapper { + display: none; + } + + // 紧凑的分隔线 + .toolbar-divider { + height: 14px; + margin: 0 1px; + flex-shrink: 0; + } + + // 精确擦除开关更紧凑,隐藏文字 + .precise-eraser-switch { + flex-shrink: 0; + padding: 2px; + + .switch-label { + display: none; + } + } + + // 压力感应开关不收缩,确保显示 + .pencil-pressure-switch { + flex-shrink: 0; + margin-left: 4px; + } + } +} + +// 移动端竖屏 (max-width: 640px) +@media (max-width: 640px) { + .pencil-settings-toolbar { + // 进一步限制最大宽度 + max-width: calc(100vw - 200px); + } +} + +// 小屏移动端 (max-width: 480px) +@media (max-width: 480px) { + .pencil-settings-toolbar { + max-width: calc(100vw - 220px); + + // 进一步紧凑化 + .island { + padding: 2px 3px !important; + } + } +} diff --git a/packages/drawnix/src/components/toolbar/popup-toolbar/popup-toolbar.scss b/packages/drawnix/src/components/toolbar/popup-toolbar/popup-toolbar.scss index b302e9d1..947d2648 100644 --- a/packages/drawnix/src/components/toolbar/popup-toolbar/popup-toolbar.scss +++ b/packages/drawnix/src/components/toolbar/popup-toolbar/popup-toolbar.scss @@ -224,4 +224,179 @@ flex-shrink: 0; margin-left: -2px; } +} + +// ============================================================================ +// Mobile Responsive Styles +// ============================================================================ + +// 平板及以下 (max-width: 768px) +@media (max-width: 768px) { + .popup-toolbar { + // 限制最大宽度,防止超出屏幕 + max-width: calc(100vw - 32px); + + // 内部 stack 支持水平滚动 + .stack_horizontal { + // 将 grid 改为 flex 以支持更好的滚动 + display: flex !important; + flex-wrap: nowrap; + overflow-x: auto; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; // Firefox + gap: 4px; // 保持按钮间距 + + &::-webkit-scrollbar { + display: none; // Chrome, Safari, Edge + } + } + + // 确保弹出工具栏在移动端有足够的触控区域 + .property-button { + min-width: 40px; + min-height: 40px; + flex-shrink: 0; + } + + // 调整按钮内部元素 + .tool-icon__icon { + svg { + width: 20px; + height: 20px; + } + } + + // 所有工具按钮不收缩 + .tool-button { + flex-shrink: 0; + } + } + + // 颜色/描边设置面板 + .stroke-setting, + .fill-setting { + // 增加滑块触控区域 + .t-slider__button { + width: 20px; + height: 20px; + } + + // 增加输入框尺寸 + .stroke-width-input { + width: 40px; + height: 28px; + font-size: 14px; + } + + // 拾色器宽度适配屏幕 + .unified-color-picker { + width: min(280px, calc(100vw - 48px)); + } + } + + // 圆角设置面板 + .corner-radius-setting-compact { + .corner-radius-row { + height: 32px; + } + + .t-slider__button { + width: 18px; + height: 18px; + } + + .corner-radius-input { + width: 44px; + height: 28px; + font-size: 14px; + } + } +} + +// 移动端竖屏 (max-width: 640px) +@media (max-width: 640px) { + .popup-toolbar { + // 进一步限制最大宽度 + max-width: calc(100vw - 24px); + + // 更大的触控目标 + .property-button { + min-width: 44px; + min-height: 44px; + } + + .fill-label, + .stroke-label { + width: 20px !important; + height: 20px !important; + } + + // 减小按钮间距以容纳更多按钮 + .stack_horizontal { + gap: 2px; + } + } + + .stroke-setting, + .fill-setting { + // 移动端垂直布局 + .stroke-width-section { + flex-direction: column; + align-items: stretch; + gap: 8px; + + .t-slider { + width: 100%; + } + } + + .stroke-style-section { + flex-wrap: wrap; + } + + // 滑块增大触控区域 + .t-slider__button { + width: 24px; + height: 24px; + } + + .t-slider__rail, + .t-slider__track { + height: 6px; + border-radius: 3px; + } + } + + // 拾色器移动端适配 + .unified-color-picker { + width: min(260px, calc(100vw - 32px)) !important; + } +} + +// 小屏移动端 (max-width: 480px) +@media (max-width: 480px) { + .popup-toolbar { + // 简化工具栏,只显示最重要的按钮 + .property-button { + padding: 8px; + } + } + + .stroke-setting, + .fill-setting { + padding: 8px !important; + + .stroke-style-label { + font-size: 11px; + } + } + + .corner-radius-setting-compact { + padding: 6px !important; + + .corner-radius-label { + font-size: 11px; + } + } } \ No newline at end of file diff --git a/packages/drawnix/src/components/toolbar/popup-toolbar/size-input.scss b/packages/drawnix/src/components/toolbar/popup-toolbar/size-input.scss index 4b83b2b2..4ed0d237 100644 --- a/packages/drawnix/src/components/toolbar/popup-toolbar/size-input.scss +++ b/packages/drawnix/src/components/toolbar/popup-toolbar/size-input.scss @@ -80,3 +80,15 @@ height: 12px; } } + +// ============================================================================ +// Mobile Responsive Styles +// ============================================================================ + +// 移动端竖屏 (max-width: 640px) +@media (max-width: 640px) { + .size-input-container { + // 在移动端隐藏宽高输入,节省空间 + display: none; + } +} diff --git a/packages/drawnix/src/components/toolbar/popup-toolbar/text-property-panel.scss b/packages/drawnix/src/components/toolbar/popup-toolbar/text-property-panel.scss index 80fbee7d..81376324 100644 --- a/packages/drawnix/src/components/toolbar/popup-toolbar/text-property-panel.scss +++ b/packages/drawnix/src/components/toolbar/popup-toolbar/text-property-panel.scss @@ -1322,3 +1322,226 @@ background: #ffffff; border-radius: 8px; } + +// ============================================================================ +// Mobile Responsive Styles +// ============================================================================ + +// 平板及以下 (max-width: 768px) +@media (max-width: 768px) { + .text-property-panel { + width: min(320px, calc(100vw - 32px)); + max-height: 70vh; + transform: translateX(0) translateY(10px); + + &.is-open { + transform: translateX(0) translateY(0); + } + + &__header { + padding: 10px 14px; + + &-close { + width: 32px; + height: 32px; + min-width: 32px; // 触控友好 + } + } + + &__content { + max-height: calc(70vh - 60px); + } + + &__section { + padding: 0 12px; + } + + // 触控友好的按钮尺寸 + .font-size-selector__preset { + min-width: 36px; + height: 32px; + padding: 0 10px; + } + + .align-buttons__btn, + .style-buttons__btn, + .case-buttons__btn, + .decoration-style-buttons__btn { + min-width: 36px; + min-height: 36px; + } + + .highlight-colors__btn { + width: 32px; + height: 32px; + } + + // 滑块增大触控区域 + .slider-control__input, + .inline-control__slider { + height: 8px; + + &::-webkit-slider-thumb { + width: 20px; + height: 20px; + } + } + } + + .color-picker-popover { + width: min(320px, calc(100vw - 32px)); + } +} + +// 移动端竖屏 (max-width: 640px) +@media (max-width: 640px) { + .text-property-panel { + // 移动端底部全宽显示 + position: fixed !important; + left: 0 !important; + right: 0 !important; + bottom: 0 !important; + top: auto !important; + width: 100% !important; + max-width: 100% !important; + max-height: 60vh; + border-radius: 16px 16px 0 0; + transform: translateY(100%); + + &.is-open { + transform: translateY(0); + } + + // 添加顶部拖拽指示条 + &::before { + content: ''; + position: absolute; + top: 8px; + left: 50%; + transform: translateX(-50%); + width: 36px; + height: 4px; + background: #e5e7eb; + border-radius: 2px; + } + + &__header { + padding: 16px 16px 12px; + padding-top: 20px; // 为拖拽指示条留空间 + } + + &__content { + max-height: calc(60vh - 80px); + padding-bottom: env(safe-area-inset-bottom, 16px); + } + + &__section { + padding: 0 16px; + margin-bottom: 12px; + + &-content { + padding: 12px; + } + } + + // 按钮组使用网格布局 + .style-buttons, + .case-buttons { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(40px, 1fr)); + gap: 8px; + + &__btn { + width: 100%; + height: 40px; + } + } + + .align-buttons { + justify-content: space-between; + + &__btn { + flex: 1; + max-width: 60px; + height: 40px; + } + } + + // 字体大小预设网格 + .font-size-selector__presets { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(44px, 1fr)); + gap: 6px; + } + + .font-size-selector__preset { + width: 100%; + height: 36px; + } + + // 高亮颜色等距排列 + .highlight-colors { + justify-content: space-between; + + &__btn { + width: 36px; + height: 36px; + } + } + + // 效果预设网格 + .effect-presets__list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(44px, 1fr)); + gap: 8px; + } + + .effect-presets__item { + width: 100%; + height: 44px; + } + } + + // 颜色选择器也底部显示 + .color-picker-popover { + position: fixed !important; + left: 0 !important; + right: 0 !important; + bottom: 0 !important; + top: auto !important; + width: 100% !important; + max-width: 100% !important; + border-radius: 16px 16px 0 0; + padding-bottom: env(safe-area-inset-bottom, 16px); + } +} + +// 小屏移动端 (max-width: 480px) +@media (max-width: 480px) { + .text-property-panel { + max-height: 70vh; + + &__section { + &-title { + font-size: 11px; + } + } + + .font-size-selector__input { + height: 36px; + font-size: 14px; + } + + .inline-control { + &__label { + width: 36px; + font-size: 11px; + } + + &__value { + font-size: 10px; + min-width: 28px; + } + } + } +} diff --git a/packages/drawnix/src/components/toolbar/unified-toolbar.tsx b/packages/drawnix/src/components/toolbar/unified-toolbar.tsx index 4dc60894..9800f2c7 100644 --- a/packages/drawnix/src/components/toolbar/unified-toolbar.tsx +++ b/packages/drawnix/src/components/toolbar/unified-toolbar.tsx @@ -1,6 +1,7 @@ import React, { useState, useCallback, useEffect, useRef } from 'react'; import classNames from 'classnames'; import { ATTACHED_ELEMENT_CLASS_NAME } from '@plait/core'; +import { ChevronUp, ChevronDown } from 'lucide-react'; import { AppToolbar } from './app-toolbar/app-toolbar'; import { CreationToolbar } from './creation-toolbar'; import { UnifiedToolbarProps } from './toolbar.types'; @@ -8,6 +9,9 @@ import { Island } from '../island'; import { BottomActionsSection } from './bottom-actions-section'; import { TaskQueuePanel } from '../task-queue/TaskQueuePanel'; import { useViewportScale } from '../../hooks/useViewportScale'; +import { useDeviceType } from '../../hooks/useDeviceType'; +import { AIImageIcon, AIVideoIcon } from '../icons'; +import { DialogType, useDrawnix } from '../../hooks/use-drawnix'; // 工具栏高度阈值: 当容器高度小于此值时切换到图标模式 // 基于四个分区的最小高度 + 分割线 + padding 计算得出 @@ -37,10 +41,15 @@ export const UnifiedToolbar: React.FC = React.memo(({ onOpenBackupRestore, }) => { const [isIconMode, setIsIconMode] = useState(false); + const [isMobileCollapsed, setIsMobileCollapsed] = useState(true); // 移动端默认收起 const hasEverExpanded = useRef(false); const containerRef = useRef(null); const scrollableRef = useRef(null); const hasScrolledToAI = useRef(false); + + // 检测设备类型 + const { isMobile: isSmallScreen, isTablet } = useDeviceType(); + const isMobileOrTablet = isSmallScreen || isTablet; // 使用 viewport scale hook 确保缩放时工具栏保持在视口左上角且大小不变 useViewportScale(containerRef, { @@ -135,6 +144,23 @@ export const UnifiedToolbar: React.FC = React.memo(({ } }, [taskPanelExpanded, onTaskPanelToggle]); + // 移动端工具栏切换 + const handleMobileToggle = useCallback(() => { + setIsMobileCollapsed(prev => !prev); + }, []); + + // 获取对话框控制 + const { openDialog } = useDrawnix(); + + // AI 按钮点击处理 + const handleAIImageClick = useCallback(() => { + openDialog(DialogType.aiImageGeneration); + }, [openDialog]); + + const handleAIVideoClick = useCallback(() => { + openDialog(DialogType.aiVideoGeneration); + }, [openDialog]); + return ( <> {/* 任务队列面板 - 只在首次展开后才渲染 */} @@ -148,23 +174,65 @@ export const UnifiedToolbar: React.FC = React.memo(({ 'unified-toolbar', ATTACHED_ELEMENT_CLASS_NAME, { - 'unified-toolbar--icon-only': isIconMode, + 'unified-toolbar--icon-only': isIconMode || isMobileOrTablet, + 'unified-toolbar--mobile-collapsed': isMobileOrTablet && isMobileCollapsed, }, className )} padding={0} data-testid="unified-toolbar" > + {/* 移动端收起状态的快捷按钮区域 */} + {isMobileOrTablet && isMobileCollapsed && ( +
+ {/* 展开按钮 */} + + {/* AI 图片生成 */} + + {/* AI 视频生成 */} + +
+ )} + + {/* 移动端展开状态的收起按钮 */} + {isMobileOrTablet && !isMobileCollapsed && ( + + )} + {/* 顶部固定区域 - 应用工具分区(菜单、撤销、重做) */}
- +
{/* 可滚动的工具栏内容区 */}
{/* 创作工具分区 - 手型、选择、思维导图、文本、画笔、箭头、形状、图片、AI工具、缩放 */}
- +
diff --git a/packages/drawnix/src/components/toolbox-drawer/ToolWinBoxManager.tsx b/packages/drawnix/src/components/toolbox-drawer/ToolWinBoxManager.tsx index 378cb909..4bc379ce 100644 --- a/packages/drawnix/src/components/toolbox-drawer/ToolWinBoxManager.tsx +++ b/packages/drawnix/src/components/toolbox-drawer/ToolWinBoxManager.tsx @@ -5,7 +5,7 @@ * 支持最小化、常驻工具栏等功能 */ -import React, { useEffect, useState, Suspense, useCallback } from 'react'; +import React, { useEffect, useState, Suspense, useCallback, useMemo } from 'react'; import { PlaitBoard, getViewportOrigination } from '@plait/core'; import { WinBoxWindow } from '../winbox'; import { toolWindowService } from '../../services/tool-window-service'; @@ -16,6 +16,7 @@ import { useDrawnix } from '../../hooks/use-drawnix'; import { ToolTransforms } from '../../plugins/with-tool'; import { DEFAULT_TOOL_CONFIG } from '../../constants/built-in-tools'; import { processToolUrl } from '../../utils/url-template'; +import { useDeviceType } from '../../hooks/useDeviceType'; /** * 工具弹窗管理器组件 @@ -24,6 +25,7 @@ export const ToolWinBoxManager: React.FC = () => { const [toolStates, setToolStates] = useState([]); const { language } = useI18n(); const { board } = useDrawnix(); + const { isMobile, isTablet, viewportWidth, viewportHeight } = useDeviceType(); useEffect(() => { const subscription = toolWindowService.observeToolStates().subscribe(states => { @@ -33,6 +35,26 @@ export const ToolWinBoxManager: React.FC = () => { return () => subscription.unsubscribe(); }, []); + /** + * 计算窗口尺寸,确保不超出视口 + */ + const getWindowSize = useCallback((tool: ToolDefinition, savedSize?: { width: number; height: number }) => { + const defaultWidth = savedSize?.width || tool.defaultWidth || 800; + const defaultHeight = savedSize?.height || tool.defaultHeight || 600; + + // 移动端/平板端限制窗口尺寸 + if (isMobile || isTablet) { + const maxWidth = viewportWidth - 16; // 留出边距 + const maxHeight = viewportHeight - 60; // 留出标题栏和边距 + return { + width: Math.min(defaultWidth, maxWidth), + height: Math.min(defaultHeight, maxHeight), + }; + } + + return { width: defaultWidth, height: defaultHeight }; + }, [isMobile, isTablet, viewportWidth, viewportHeight]); + /** * 处理工具最小化 */ @@ -138,6 +160,9 @@ export const ToolWinBoxManager: React.FC = () => { // 确定窗口是否可见 const isVisible = status === 'open'; + // 计算窗口尺寸(移动端限制不超出屏幕) + const windowSize = getWindowSize(tool, size); + return ( { visible={isVisible} keepAlive={true} title={tool.name} - width={size?.width || tool.defaultWidth || 800} - height={size?.height || tool.defaultHeight || 600} + width={windowSize.width} + height={windowSize.height} x={position?.x} y={position?.y} onClose={() => toolWindowService.closeTool(tool.id)} diff --git a/packages/drawnix/src/components/ttd-dialog/ai-image-generation.scss b/packages/drawnix/src/components/ttd-dialog/ai-image-generation.scss index 13313113..a0a2c490 100644 --- a/packages/drawnix/src/components/ttd-dialog/ai-image-generation.scss +++ b/packages/drawnix/src/components/ttd-dialog/ai-image-generation.scss @@ -1849,62 +1849,78 @@ // 响应式适配 - 移动端统一使用垂直布局 @media (max-width: 768px) { - // 移动端容器布局 + // 移动端容器布局 - 不滚动,由 wb-body 处理滚动 .ai-image-generation-container { - height: auto; - min-height: 100%; - overflow-y: auto; - display: flex; - flex-direction: column; - } - - .main-content { - flex-direction: column; - gap: 12px; - min-height: auto; - height: auto; - flex: 1; - overflow-y: auto; - } - - .ai-image-generation-section { - max-height: none; - flex: none; - order: 1; // 表单区域在上面 + height: auto !important; + max-height: none !important; + overflow: visible !important; + display: flex !important; + flex-direction: column !important; + } + + // 主内容区域 - 使用后代选择器确保匹配 + .ai-image-generation-container .main-content { + display: flex !important; + flex-direction: column !important; + gap: 12px !important; + min-height: auto !important; + height: auto !important; + flex: none !important; + overflow: visible !important; + } + + // 表单区域 - 确保显示 + .ai-image-generation-container .ai-image-generation-section { + display: flex !important; + flex-direction: column !important; + width: 100% !important; + min-width: 0 !important; + max-height: none !important; + height: auto !important; + flex: none !important; + overflow: visible !important; + padding-right: 0 !important; + order: 1 !important; + } + + // 任务侧栏 - 全宽显示 + .ai-image-generation-container .task-sidebar { + display: block !important; + flex: none !important; + width: 100% !important; + height: auto !important; + min-height: auto !important; + max-height: none !important; + overflow: visible !important; + padding: 8px !important; + order: 2 !important; + margin-top: 8px !important; } + // 预览区域 - 不滚动 .preview-section { - max-height: none; + max-height: none !important; + height: auto !important; + flex: none !important; + overflow: visible !important; } - // 移动端任务侧栏样式 - 放在表单下方 - .task-sidebar { - flex: none; - height: auto; - min-height: 100px; - max-height: 180px; // 小窗默认高度限制,更紧凑 - overflow-y: auto; - padding: 8px; - order: 2; // 任务列表在下面 - margin-top: 8px; + // 任务列表内容也不滚动 + .task-list { + max-height: none !important; + overflow: visible !important; } - // 全屏/最大化时,任务列表可以更高 - .winbox.max, - .winbox.full { - .task-sidebar { - max-height: 40vh; // 全屏时允许更大的任务列表区域 - } - } - - // 操作按钮区域 - 确保可见 + // 操作按钮区域 .section-actions { flex-wrap: wrap; gap: 8px; padding: 8px 0; - position: relative; + position: sticky; // 固定在底部 + bottom: 0; z-index: 10; background: #fff; + margin-top: auto; &.unified-action-bar { gap: 8px; @@ -1927,10 +1943,11 @@ } @media (max-width: 480px) { - // 更小屏幕下任务列表进一步缩小 + .ai-image-generation-container { + max-height: calc(100vh - 50px); + } + .task-sidebar { - min-height: 80px; - max-height: 150px; padding: 6px; } diff --git a/packages/drawnix/src/components/ttd-dialog/ai-video-generation.scss b/packages/drawnix/src/components/ttd-dialog/ai-video-generation.scss index c3a0cc36..7d1fcf54 100644 --- a/packages/drawnix/src/components/ttd-dialog/ai-video-generation.scss +++ b/packages/drawnix/src/components/ttd-dialog/ai-video-generation.scss @@ -126,70 +126,78 @@ } -/* Responsive adjustments - 移动端统一使用垂直布局 */ +/* Responsive adjustments - 移动端统一使用垂直布局,单一滚动条 */ @media (max-width: 768px) { - /* 移动端容器布局 */ + /* 移动端容器布局 - 不滚动,由 wb-body 处理滚动 */ .ai-video-generation-container { - height: auto; - min-height: 100%; - overflow-y: auto; - display: flex; - flex-direction: column; - - .main-content { - flex-direction: column; - gap: 12px; - min-height: auto; - height: auto; - flex: 1; - overflow-y: auto; - } - - .ai-image-generation-section { - max-height: none; - flex: none; - order: 1; - } - - /* 移动端任务侧栏样式 - 放在表单下方 */ - .task-sidebar { - flex: none; - height: auto; - min-height: 100px; - max-height: 180px; /* 小窗默认高度限制,更紧凑 */ - overflow-y: auto; - padding: 8px; - order: 2; - margin-top: 8px; - } - - /* 操作按钮区域 - 确保可见 */ - .section-actions { - flex-wrap: wrap; - gap: 8px; - padding: 8px 0; - position: relative; - z-index: 10; - background: #fff; - } + height: auto !important; + max-height: none !important; + overflow: visible !important; + display: flex !important; + flex-direction: column !important; + } + + /* 主内容区域 - 使用后代选择器确保匹配 */ + .ai-video-generation-container .main-content { + display: flex !important; + flex-direction: column !important; + gap: 12px !important; + min-height: auto !important; + height: auto !important; + flex: none !important; + overflow: visible !important; + } + + /* 表单区域 - 确保显示 */ + .ai-video-generation-container .ai-image-generation-section { + display: flex !important; + flex-direction: column !important; + width: 100% !important; + min-width: 0 !important; + max-height: none !important; + height: auto !important; + flex: none !important; + overflow: visible !important; + order: 1 !important; + } + + /* 任务侧栏 - 全宽显示 */ + .ai-video-generation-container .task-sidebar { + display: block !important; + flex: none !important; + width: 100% !important; + height: auto !important; + min-height: auto !important; + max-height: none !important; + overflow: visible !important; + padding: 8px !important; + order: 2 !important; + margin-top: 8px !important; } - /* 全屏/最大化时,任务列表可以更高 */ - .winbox.max, - .winbox.full { - .ai-video-generation-container .task-sidebar { - max-height: 40vh; /* 全屏时允许更大的任务列表区域 */ - } + /* 操作按钮区域 */ + .ai-video-generation-container .section-actions { + flex-wrap: wrap; + gap: 8px; + padding: 8px 0; + position: sticky; + bottom: 0; + z-index: 10; + background: #fff; + } + + .task-list { + max-height: none !important; + overflow: visible !important; } } @media (max-width: 480px) { .ai-video-generation-container { - /* 更小屏幕下任务列表进一步缩小 */ + max-height: calc(100vh - 50px); + .task-sidebar { - min-height: 80px; - max-height: 150px; padding: 6px; } diff --git a/packages/drawnix/src/components/ttd-dialog/batch-image-generation.scss b/packages/drawnix/src/components/ttd-dialog/batch-image-generation.scss index 254570d5..ededb1d1 100644 --- a/packages/drawnix/src/components/ttd-dialog/batch-image-generation.scss +++ b/packages/drawnix/src/components/ttd-dialog/batch-image-generation.scss @@ -597,6 +597,23 @@ gap: 10px; } + .empty-library { + grid-column: 1 / -1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 32px 16px; + text-align: center; + color: #999; + font-size: 13px; + line-height: 1.5; + background: #fafafa; + border-radius: 8px; + border: 1px dashed #e0e0e0; + white-space: nowrap; + } + .library-image { aspect-ratio: 1; border-radius: 8px; @@ -732,3 +749,217 @@ background: rgba(243, 156, 18, 0.1) !important; border-color: rgba(243, 156, 18, 0.3) !important; } + +// 移动端响应式适配 +@media (max-width: 768px) { + .batch-image-generation { + flex-direction: column; + padding: 8px; + gap: 8px; + + .batch-main-content { + padding: 12px; + border-radius: 8px; + } + + // 工具栏移动端优化 + .batch-toolbar { + flex-direction: column; + align-items: stretch; + gap: 8px; + margin-bottom: 8px; + padding-bottom: 8px; + + .toolbar-left, + .toolbar-right { + justify-content: flex-start; + flex-wrap: wrap; + gap: 6px; + } + + .toolbar-divider { + display: none; + } + } + + .batch-generate-btn { + padding: 0 12px !important; + font-size: 13px !important; + } + + // 表格容器移动端优化 + .excel-table-container { + border-radius: 6px; + } + + .excel-table { + th, td { + font-size: 11px; + } + + th { + height: 36px; + } + + .row-number { + width: 32px; + min-width: 32px; + padding: 4px; + font-size: 10px; + } + + .col-checkbox { + width: 32px; + min-width: 32px; + } + + // 移动端列宽调整 + .col-prompt { width: 150px; min-width: 150px; } + .col-size { width: 70px; min-width: 70px; } + .col-images { width: 100px; min-width: 100px; } + .col-count { width: 50px; min-width: 50px; } + .col-preview { width: 100px; min-width: 100px; } + + .th-content { + padding: 0 4px; + font-size: 10px; + } + + .column-fill-btn { + width: 16px !important; + height: 16px !important; + min-height: 16px !important; + margin-left: 4px; + } + } + + .excel-cell { + min-height: 36px; + padding: 6px 8px; + font-size: 12px; + + &.cell-prompt { + .cell-text, + .cell-textarea { + padding: 8px !important; + font-size: 12px; + } + } + + input[type="number"] { + height: 28px; + font-size: 12px; + } + + .custom-size-dropdown { + .dropdown-trigger { + height: 28px; + font-size: 11px; + padding: 0 6px; + } + } + } + + // 图片缩略图缩小 + .cell-image-thumb, + .preview-thumb { + width: 28px; + height: 28px; + border-radius: 4px; + } + + .add-image-btn { + width: 28px !important; + height: 28px !important; + } + + // 图片库侧栏移动端隐藏或全屏 + .image-library-sidebar { + width: 100%; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1000; + border-radius: 0; + border: none; + + &.hidden { + display: none; + } + + .library-header { + padding: 12px; + } + + .library-content { + padding: 12px; + } + + .library-grid { + grid-template-columns: repeat(4, 1fr); + gap: 8px; + } + } + + // 添加行区域 + .add-rows-section { + padding: 8px 0; + gap: 8px; + flex-wrap: wrap; + } + + .model-selector-wrapper { + min-width: 140px; + } + + .hint-text { + margin-top: 8px; + font-size: 10px; + } + } +} + +// 更小屏幕适配 +@media (max-width: 480px) { + .batch-image-generation { + padding: 4px; + + .batch-main-content { + padding: 8px; + } + + .batch-toolbar { + .toolbar-left, + .toolbar-right { + gap: 4px; + } + } + + .excel-table { + .row-number, + .col-checkbox { + width: 28px; + min-width: 28px; + } + + .col-prompt { width: 120px; min-width: 120px; } + .col-size { width: 60px; min-width: 60px; } + .col-images { width: 80px; min-width: 80px; } + .col-count { width: 40px; min-width: 40px; } + .col-preview { width: 80px; min-width: 80px; } + } + + .cell-image-thumb, + .preview-thumb { + width: 24px; + height: 24px; + } + + .add-image-btn { + width: 24px !important; + height: 24px !important; + } + } +} diff --git a/packages/drawnix/src/components/ttd-dialog/batch-image-generation.tsx b/packages/drawnix/src/components/ttd-dialog/batch-image-generation.tsx index 3ec731f6..ca90a997 100644 --- a/packages/drawnix/src/components/ttd-dialog/batch-image-generation.tsx +++ b/packages/drawnix/src/components/ttd-dialog/batch-image-generation.tsx @@ -38,6 +38,7 @@ import type { Asset } from '../../types/asset.types'; import { MediaLibraryModal } from '../media-library/MediaLibraryModal'; import { LS_KEYS_TO_MIGRATE } from '../../constants/storage-keys'; import { kvStorageService } from '../../services/kv-storage-service'; +import { useDeviceType } from '../../hooks/useDeviceType'; import './batch-image-generation.scss'; import { trackMemory } from '../../utils/common'; @@ -106,6 +107,10 @@ const BatchImageGeneration: React.FC = ({ onSwitchToS const { language } = useI18n(); const { createTask, tasks: queueTasks } = useTaskQueue(); const { addAsset, assets: libraryAssets, loadAssets } = useAssets(); + const { isMobile, isTablet } = useDeviceType(); + + // 移动端/平板端隐藏素材库侧栏 + const hideLibrarySidebar = isMobile || isTablet; // 过滤出图片类型的素材 const imageAssets = libraryAssets.filter(asset => asset.type === AssetType.IMAGE); @@ -2283,7 +2288,7 @@ const BatchImageGeneration: React.FC = ({ onSwitchToS - {!showLibrary && ( + {!hideLibrarySidebar && !showLibrary && (
- {/* 素材库侧栏 */} + {/* 素材库侧栏 - 移动端/平板端隐藏 */} + {!hideLibrarySidebar && (

{language === 'zh' ? '素材库' : 'Library'}

@@ -2538,6 +2544,7 @@ const BatchImageGeneration: React.FC = ({ onSwitchToS
+ )} {/* 批量导入弹窗 */} {showBatchImportModal && ( diff --git a/packages/drawnix/src/components/ttd-dialog/ttd-dialog.tsx b/packages/drawnix/src/components/ttd-dialog/ttd-dialog.tsx index 66d7fdca..be6c01d5 100644 --- a/packages/drawnix/src/components/ttd-dialog/ttd-dialog.tsx +++ b/packages/drawnix/src/components/ttd-dialog/ttd-dialog.tsx @@ -8,6 +8,7 @@ import type { ReferenceImage } from './shared/ReferenceImageUpload'; import { useI18n } from '../../i18n'; import { useBoard } from '@plait-board/react-board'; import React, { useState, useEffect, useRef, memo, useCallback, lazy, Suspense } from 'react'; +import { useDeviceType } from '../../hooks/useDeviceType'; import { processSelectedContentForAI, extractSelectedContent } from '../../utils/selection-utils'; import { AI_IMAGE_GENERATION_PREVIEW_CACHE_KEY, @@ -28,6 +29,10 @@ const TTDDialogComponent = ({ container }: { container: HTMLElement | null }) => const { appState, setAppState, openDialog, closeDialog } = useDrawnix(); const { language } = useI18n(); const board = useBoard(); + const { isMobile, isTablet } = useDeviceType(); + + // 移动端和平板端不显示批量出图 + const showBatchTab = !isMobile && !isTablet; // 使用ref来防止多次并发处理 const isProcessingRef = useRef(false); @@ -120,6 +125,13 @@ const TTDDialogComponent = ({ container }: { container: HTMLElement | null }) => } }); + // 移动端/平板端自动切换回单图模式 + useEffect(() => { + if (!showBatchTab && imageGenerationMode === 'batch') { + setImageGenerationMode('single'); + } + }, [showBatchTab, imageGenerationMode]); + // 处理图片生成模式变化 const handleImageModeChange = useCallback((mode: ImageGenerationMode) => { setImageGenerationMode(mode); @@ -457,20 +469,37 @@ const TTDDialogComponent = ({ container }: { container: HTMLElement | null }) => : (language === 'zh' ? 'AI 图片生成' : 'AI Image Generation') } headerContent={ -
- - -
+ + +
+ ) : undefined } onClose={handleImageDialogClose} width="80%" diff --git a/packages/drawnix/src/components/view-navigation/view-navigation.scss b/packages/drawnix/src/components/view-navigation/view-navigation.scss index 11e1dd30..c5b71a87 100644 --- a/packages/drawnix/src/components/view-navigation/view-navigation.scss +++ b/packages/drawnix/src/components/view-navigation/view-navigation.scss @@ -202,16 +202,76 @@ } } -// 响应式调整 +// ============================================================================ +// Responsive Styles +// ============================================================================ + +// 平板及以下 (max-width: 768px) @media (max-width: 768px) { .view-navigation { + top: calc(env(safe-area-inset-top, 0px) + 8px); + z-index: 4005; + &__zoom { padding: 2px 6px; + border-radius: 16px; + } + + // 确保触控友好的按钮尺寸 (最小44px) + &__zoom-btn { + width: 36px; + height: 36px; + min-width: 36px; + + svg { + width: 16px; + height: 16px; + } + } + + &__zoom-percent { + min-width: 44px; + height: 36px; + font-size: 12px; + padding: 0 6px; + } + + &__minimap-toggle { + width: 32px; + height: 32px; + min-width: 32px; + + svg { + width: 14px; + height: 14px; + } + } + + &__minimap { + .minimap__content { + transform: scale(0.8); + transform-origin: top right; + } + } + } +} + +// 移动端竖屏 (max-width: 640px) +@media (max-width: 640px) { + .view-navigation { + top: calc(env(safe-area-inset-top, 0px) + 6px); + gap: 4px; + + &__zoom { + padding: 2px 4px; + border-radius: 14px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } &__zoom-btn { - width: 24px; - height: 24px; + width: 32px; + height: 32px; + min-width: 32px; svg { width: 14px; @@ -220,14 +280,17 @@ } &__zoom-percent { - min-width: 38px; - height: 24px; - font-size: 12px; + min-width: 40px; + height: 32px; + font-size: 11px; + padding: 0 4px; } + // 移动端隐藏 minimap toggle 按钮,节省空间 &__minimap-toggle { - width: 20px; - height: 20px; + width: 28px; + height: 28px; + min-width: 28px; svg { width: 12px; @@ -235,11 +298,75 @@ } } + // 移动端 minimap 缩小更多 &__minimap { .minimap__content { - transform: scale(0.85); + transform: scale(0.7); transform-origin: top right; } } } } + +// 小屏移动端 (max-width: 480px) +@media (max-width: 480px) { + .view-navigation { + &__zoom { + padding: 1px 3px; + } + + &__zoom-btn { + width: 28px; + height: 28px; + min-width: 28px; + + svg { + width: 12px; + height: 12px; + } + } + + &__zoom-percent { + min-width: 36px; + height: 28px; + font-size: 10px; + } + + // 小屏幕 minimap toggle 更紧凑 + &__minimap-toggle { + width: 24px; + height: 24px; + min-width: 24px; + + svg { + width: 10px; + height: 10px; + } + } + + // 小屏幕 minimap 缩小 + &__minimap { + .minimap__content { + transform: scale(0.6); + transform-origin: top right; + } + } + } +} + +// 缩放菜单响应式 +@media (max-width: 640px) { + .view-navigation-zoom-menu { + min-width: 120px; + padding: 2px; + + .zoom-menu-item { + padding: 10px 10px; // 增加触控区域 + font-size: 13px; + + .zoom-menu-item__shortcut { + display: none; // 移动端隐藏快捷键提示 + } + } + } +} diff --git a/packages/drawnix/src/components/winbox/WinBoxWindow.tsx b/packages/drawnix/src/components/winbox/WinBoxWindow.tsx index c0a494e0..cda7115b 100644 --- a/packages/drawnix/src/components/winbox/WinBoxWindow.tsx +++ b/packages/drawnix/src/components/winbox/WinBoxWindow.tsx @@ -694,6 +694,14 @@ export const WinBoxWindow: React.FC = ({ const portalContainer = document.createElement('div'); portalContainer.className = 'wb-header-custom'; + // 阻止触摸和鼠标事件冒泡到拖拽区域,确保按钮可点击 + const stopPropagation = (e: Event) => { + e.stopPropagation(); + }; + portalContainer.addEventListener('mousedown', stopPropagation, true); + portalContainer.addEventListener('touchstart', stopPropagation, true); + portalContainer.addEventListener('pointerdown', stopPropagation, true); + // 插入到拖拽区域末尾(标题后面) drag.appendChild(portalContainer); setHeaderPortalContainer(portalContainer); diff --git a/packages/drawnix/src/components/winbox/winbox-custom.scss b/packages/drawnix/src/components/winbox/winbox-custom.scss index def44ebd..c6736007 100644 --- a/packages/drawnix/src/components/winbox/winbox-custom.scss +++ b/packages/drawnix/src/components/winbox/winbox-custom.scss @@ -76,6 +76,16 @@ * { pointer-events: auto; } + + // 确保模式切换按钮可点击 + .image-generation-mode-tabs { + pointer-events: auto; + + .mode-tab { + pointer-events: auto; + touch-action: manipulation; + } + } } } @@ -405,6 +415,9 @@ gap: 4px; border-radius: 8px; margin-left: 8px; + position: relative; + z-index: 100; // 确保在拖拽区域之上 + pointer-events: auto; // 确保可点击 .mode-tab { padding: 6px 16px; @@ -421,12 +434,18 @@ display: flex; align-items: center; justify-content: center; + pointer-events: auto; // 确保可点击 + touch-action: manipulation; // 优化移动端触摸 &:hover:not(.active) { color: #333333; background: rgba(255, 255, 255, 0.5); } + &:active { + transform: scale(0.98); + } + &.active { color: #F39C12; font-weight: 600; @@ -437,11 +456,18 @@ } } -// 图片生成弹窗 - 有模式切换时隐藏标题 +// 图片生成弹窗 - PC端有模式切换tabs时隐藏标题,移动端保留标题 .winbox-ai-image-generation { .wb-title { display: none !important; } + + // 移动端显示标题(因为没有模式切换tabs) + @media (max-width: 768px) { + .wb-title { + display: block !important; + } + } } // 视频生成弹窗 - 保留标题显示 @@ -544,7 +570,6 @@ .wb-min, .wb-max, .wb-full, - .wb-split, .wb-insert-canvas, .wb-close { width: 30px; @@ -553,6 +578,11 @@ border-radius: 6px; } + // 移动端隐藏分屏按钮 + .wb-split { + display: none !important; + } + .wb-body { border-radius: 0 0 12px 12px; } @@ -579,6 +609,39 @@ } } + // 移动端 header-custom 启用触摸 + .wb-header-custom { + pointer-events: auto !important; + touch-action: auto !important; + } + + // 移动端模式切换按钮确保可点击 + .image-generation-mode-tabs { + pointer-events: auto !important; + touch-action: auto !important; + position: relative; + z-index: 100; + + .mode-tab { + pointer-events: auto !important; + touch-action: manipulation !important; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0.1); + min-height: 40px; + padding: 10px 14px; + cursor: pointer; + + // 确保触摸区域 + &::before { + content: ''; + position: absolute; + top: -5px; + left: -5px; + right: -5px; + bottom: -5px; + } + } + } + &.max, &.full { .wb-body { top: 44px !important; @@ -590,7 +653,44 @@ padding: 12px; } - .winbox-ai-generation .winbox-content-wrapper { - padding: 12px 16px; + // AI 生成弹窗移动端特殊处理 - 单一滚动条在最外层 + .winbox-ai-generation { + .wb-body { + overflow-y: auto !important; // wb-body 负责滚动 + overflow-x: hidden !important; + + // 滚动条样式 + scrollbar-width: thin; + scrollbar-color: rgba(0, 0, 0, 0.2) transparent; + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.2); + border-radius: 3px; + } + } + + .winbox-content-wrapper { + padding: 12px 16px; + overflow: visible !important; + height: auto !important; + flex: none !important; + } + + // AI 容器不滚动 + .ai-image-generation-container, + .ai-video-generation-container { + flex: none !important; + height: auto !important; + max-height: none !important; + overflow: visible !important; + } } } diff --git a/packages/drawnix/src/hooks/useDeviceType.ts b/packages/drawnix/src/hooks/useDeviceType.ts new file mode 100644 index 00000000..303749c8 --- /dev/null +++ b/packages/drawnix/src/hooks/useDeviceType.ts @@ -0,0 +1,176 @@ +/** + * useDeviceType Hook + * + * 检测当前设备类型(移动端、平板、桌面端) + * 响应窗口大小变化实时更新 + */ + +import { useState, useEffect, useCallback } from 'react'; + +export type DeviceType = 'mobile' | 'tablet' | 'desktop'; + +export interface DeviceInfo { + /** 设备类型 */ + type: DeviceType; + /** 是否为移动端 (包括平板) */ + isMobile: boolean; + /** 是否为平板 */ + isTablet: boolean; + /** 是否为桌面端 */ + isDesktop: boolean; + /** 是否为触控设备 */ + isTouchDevice: boolean; + /** 是否为竖屏 */ + isPortrait: boolean; + /** 是否为横屏 */ + isLandscape: boolean; + /** 视口宽度 */ + viewportWidth: number; + /** 视口高度 */ + viewportHeight: number; +} + +// 断点定义(与 _responsive.scss 保持一致) +const BREAKPOINTS = { + mobile: 640, + tablet: 768, + tabletLandscape: 1024, +} as const; + +/** + * 检测是否为触控设备 + */ +function detectTouchDevice(): boolean { + if (typeof window === 'undefined') return false; + + return ( + 'ontouchstart' in window || + navigator.maxTouchPoints > 0 || + // @ts-expect-error - msMaxTouchPoints is IE-specific + navigator.msMaxTouchPoints > 0 + ); +} + +/** + * 根据视口宽度获取设备类型 + */ +function getDeviceType(width: number): DeviceType { + if (width <= BREAKPOINTS.mobile) { + return 'mobile'; + } + if (width <= BREAKPOINTS.tabletLandscape) { + return 'tablet'; + } + return 'desktop'; +} + +/** + * 获取完整的设备信息 + */ +function getDeviceInfo(): DeviceInfo { + if (typeof window === 'undefined') { + // SSR 默认值 + return { + type: 'desktop', + isMobile: false, + isTablet: false, + isDesktop: true, + isTouchDevice: false, + isPortrait: false, + isLandscape: true, + viewportWidth: 1920, + viewportHeight: 1080, + }; + } + + const width = window.innerWidth; + const height = window.innerHeight; + const type = getDeviceType(width); + const isTouchDevice = detectTouchDevice(); + const isPortrait = height > width; + + return { + type, + isMobile: type === 'mobile' || type === 'tablet', + isTablet: type === 'tablet', + isDesktop: type === 'desktop', + isTouchDevice, + isPortrait, + isLandscape: !isPortrait, + viewportWidth: width, + viewportHeight: height, + }; +} + +/** + * useDeviceType Hook + * + * @example + * ```tsx + * const { type, isMobile, isPortrait } = useDeviceType(); + * + * if (isMobile) { + * return ; + * } + * return ; + * ``` + */ +export function useDeviceType(): DeviceInfo { + const [deviceInfo, setDeviceInfo] = useState(getDeviceInfo); + + const updateDeviceInfo = useCallback(() => { + setDeviceInfo(getDeviceInfo()); + }, []); + + useEffect(() => { + // 初始化时更新(处理 SSR hydration) + updateDeviceInfo(); + + // 监听窗口大小变化 + window.addEventListener('resize', updateDeviceInfo); + + // 监听屏幕方向变化 + window.addEventListener('orientationchange', updateDeviceInfo); + + return () => { + window.removeEventListener('resize', updateDeviceInfo); + window.removeEventListener('orientationchange', updateDeviceInfo); + }; + }, [updateDeviceInfo]); + + return deviceInfo; +} + +/** + * 简化版 hook,仅返回设备类型 + */ +export function useDeviceTypeSimple(): DeviceType { + const { type } = useDeviceType(); + return type; +} + +/** + * 检测是否为移动端(包括平板) + */ +export function useIsMobile(): boolean { + const { isMobile } = useDeviceType(); + return isMobile; +} + +/** + * 检测是否为触控设备 + */ +export function useIsTouchDevice(): boolean { + const { isTouchDevice } = useDeviceType(); + return isTouchDevice; +} + +/** + * 检测屏幕方向 + */ +export function useOrientation(): 'portrait' | 'landscape' { + const { isPortrait } = useDeviceType(); + return isPortrait ? 'portrait' : 'landscape'; +} + +export default useDeviceType; diff --git a/packages/drawnix/src/libs/image-viewer.ts b/packages/drawnix/src/libs/image-viewer.ts index 0da001df..ef35abec 100644 --- a/packages/drawnix/src/libs/image-viewer.ts +++ b/packages/drawnix/src/libs/image-viewer.ts @@ -22,6 +22,12 @@ export interface ImageViewerOptions { enableKeyboard?: boolean; } +// 检测是否为移动设备 +const isMobileDevice = (): boolean => { + return window.innerWidth <= 768 || + /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); +}; + export class ImageViewer { private options: Required; private viewer: Viewer | null = null; @@ -55,26 +61,42 @@ export class ImageViewer { this.container.appendChild(img); document.body.appendChild(this.container); + // 移动端使用精简的 toolbar + const isMobile = isMobileDevice(); + const toolbar = isMobile ? { + zoomIn: 1, + zoomOut: 1, + oneToOne: 0, + reset: 1, + prev: 0, + play: 0, + next: 0, + rotateLeft: 1, + rotateRight: 1, + flipHorizontal: 0, + flipVertical: 0, + } : { + zoomIn: 1, + zoomOut: 1, + oneToOne: 1, + reset: 1, + prev: 0, + play: 0, + next: 0, + rotateLeft: 1, + rotateRight: 1, + flipHorizontal: 1, + flipVertical: 1, + }; + // 创建 ViewerJS 实例 this.viewer = new Viewer(this.container, { inline: false, button: true, navbar: false, title: !!alt, - toolbar: { - zoomIn: 1, - zoomOut: 1, - oneToOne: 1, - reset: 1, - prev: 0, - play: 0, - next: 0, - rotateLeft: 1, - rotateRight: 1, - flipHorizontal: 1, - flipVertical: 1, - }, - fullscreen: true, + toolbar, + fullscreen: !isMobile, // 移动端禁用全屏按钮 keyboard: this.options.enableKeyboard, backdrop: true, loading: true, diff --git a/packages/drawnix/src/services/prevent-pinch-zoom-service.ts b/packages/drawnix/src/services/prevent-pinch-zoom-service.ts index 85922aa0..aacb6076 100644 --- a/packages/drawnix/src/services/prevent-pinch-zoom-service.ts +++ b/packages/drawnix/src/services/prevent-pinch-zoom-service.ts @@ -2,8 +2,121 @@ * 防止双指缩放服务 * * 通过监听触摸事件来阻止页面缩放 + * + * 优化:仅在画布区域阻止缩放,允许 UI 元素的正常触控交互 */ +/** + * 检查元素是否为可交互的 UI 元素 + * 这些元素应该允许正常的触控事件 + */ +function isInteractiveElement(target: EventTarget | null): boolean { + if (!target || !(target instanceof Element)) { + return false; + } + + // 检查元素本身或其祖先是否为可交互元素 + const element = target as HTMLElement; + + // 可交互元素的选择器列表 + const interactiveSelectors = [ + // 表单元素 + 'input', + 'textarea', + 'select', + 'button', + // 工具栏和抽屉 + '.unified-toolbar', + '.chat-drawer', + '.side-drawer', + '.toolbox-drawer', + '.project-drawer', + '.popup-toolbar', + // 对话框和弹窗 + '.t-dialog', + '.t-drawer', + '.t-popup', + '.t-popover', + '.Dialog', + '.winbox', + // AI 输入栏 + '.ai-input-bar', + // 视图导航 + '.view-navigation', + // 小地图 + '.minimap', + // 任务队列 + '.task-queue-panel', + // 滑块和颜色选择器 + '.t-slider', + '.unified-color-picker', + '.color-picker', + // 媒体库 + '.media-library', + // 图片预览器 (ViewerJS) + '.viewer-container', + '.viewer-canvas', + '.viewer-footer', + '.viewer-toolbar', + // 媒体预览器 (UnifiedMediaViewer) + '.unified-viewer', + '.media-viewport', + '.thumbnail-queue', + // 其他交互元素 + '[role="button"]', + '[role="slider"]', + '[role="menu"]', + '[role="menuitem"]', + '[role="dialog"]', + '[data-allow-touch]', // 自定义属性,允许触控 + ]; + + // 检查元素或其祖先是否匹配任一选择器 + for (const selector of interactiveSelectors) { + if (element.matches(selector) || element.closest(selector)) { + return true; + } + } + + // 检查是否有可滚动内容 + if (element.scrollHeight > element.clientHeight || element.scrollWidth > element.clientWidth) { + const style = window.getComputedStyle(element); + if (style.overflowY === 'auto' || style.overflowY === 'scroll' || + style.overflowX === 'auto' || style.overflowX === 'scroll') { + return true; + } + } + + return false; +} + +/** + * 检查是否在画布区域内 + */ +function isInCanvasArea(target: EventTarget | null): boolean { + if (!target || !(target instanceof Element)) { + return false; + } + + const element = target as HTMLElement; + + // 画布区域的选择器 + const canvasSelectors = [ + '.plait-board-container', + '.plait-board', + '[plait-board]', + 'svg.rough-svg', + ]; + + for (const selector of canvasSelectors) { + if (element.matches(selector) || element.closest(selector)) { + return true; + } + } + + return false; +} + /** * 初始化防止双指缩放 */ @@ -14,21 +127,41 @@ export function initPreventPinchZoom(): () => void { // console.log('[PreventPinchZoom] Initializing'); - // 阻止多点触摸 + // 阻止多点触摸(仅在画布区域) const handleTouch = (event: TouchEvent) => { if (event.touches.length > 1) { + const target = event.target; + + // 如果是可交互的 UI 元素,允许触控 + if (isInteractiveElement(target)) { + return; + } + + // 仅在画布区域阻止缩放 + if (isInCanvasArea(target)) { + event.preventDefault(); + event.stopPropagation(); + } + } + }; + + // 阻止手势事件(iOS Safari)- 仅在画布区域 + const handleGesture = (event: Event) => { + const target = event.target; + + // 如果是可交互的 UI 元素,允许手势 + if (isInteractiveElement(target)) { + return; + } + + // 仅在画布区域阻止手势 + if (isInCanvasArea(target)) { event.preventDefault(); event.stopPropagation(); } }; - // 阻止手势事件(iOS Safari) - const handleGesture = (event: Event) => { - event.preventDefault(); - event.stopPropagation(); - }; - - // 阻止 Ctrl/Cmd + 滚轮缩放 + // 阻止 Ctrl/Cmd + 滚轮缩放(全局阻止,因为这是桌面端行为) const handleWheel = (event: WheelEvent) => { if (event.ctrlKey || event.metaKey) { event.preventDefault(); @@ -36,6 +169,8 @@ export function initPreventPinchZoom(): () => void { }; // 添加事件监听器 + // 使用 passive: false 确保可以调用 preventDefault + // 使用 capture: true 在捕获阶段处理事件 document.addEventListener('touchstart', handleTouch, { passive: false, capture: true }); document.addEventListener('touchmove', handleTouch, { passive: false, capture: true }); document.addEventListener('touchend', handleTouch, { passive: false, capture: true }); diff --git a/packages/drawnix/src/styles/_responsive.scss b/packages/drawnix/src/styles/_responsive.scss new file mode 100644 index 00000000..bf6ab699 --- /dev/null +++ b/packages/drawnix/src/styles/_responsive.scss @@ -0,0 +1,225 @@ +/** + * Responsive Design Variables and Mixins + * + * 响应式设计变量和混合宏 + * 用于多端适配(移动端、平板、桌面端) + */ + +// ============================================================================ +// Device Breakpoints (设备断点) +// ============================================================================ + +$breakpoint-mobile: 640px; // 移动端竖屏 +$breakpoint-tablet: 768px; // 移动端横屏/平板竖屏 +$breakpoint-tablet-landscape: 1024px; // 平板横屏 +$breakpoint-desktop: 1280px; // 桌面端 + +// ============================================================================ +// Safe Area Insets (安全区域) +// ============================================================================ +// 用于适配 iPhone X 及以上设备的刘海屏和底部指示条 + +$safe-area-top: env(safe-area-inset-top, 0px); +$safe-area-bottom: env(safe-area-inset-bottom, 0px); +$safe-area-left: env(safe-area-inset-left, 0px); +$safe-area-right: env(safe-area-inset-right, 0px); + +// ============================================================================ +// Mobile-specific Variables (移动端专用变量) +// ============================================================================ + +$mobile-toolbar-height: 48px; // 移动端工具栏高度 +$mobile-input-bar-height: 56px; // 移动端输入栏高度 +$mobile-button-min-size: 44px; // 移动端最小触控目标尺寸 (Apple HIG) +$mobile-spacing: 8px; // 移动端基础间距 +$mobile-border-radius: 12px; // 移动端圆角 + +// ============================================================================ +// Responsive Mixins (响应式混合宏) +// ============================================================================ + +/// 仅移动端竖屏 (max-width: 640px) +@mixin mobile { + @media (max-width: #{$breakpoint-mobile}) { + @content; + } +} + +/// 仅移动端横屏/平板竖屏 (641px - 768px) +@mixin tablet-portrait { + @media (min-width: #{$breakpoint-mobile + 1}) and (max-width: #{$breakpoint-tablet}) { + @content; + } +} + +/// 平板及以下 (max-width: 768px) +@mixin tablet-and-below { + @media (max-width: #{$breakpoint-tablet}) { + @content; + } +} + +/// 仅平板横屏 (769px - 1024px) +@mixin tablet-landscape { + @media (min-width: #{$breakpoint-tablet + 1}) and (max-width: #{$breakpoint-tablet-landscape}) { + @content; + } +} + +/// 平板横屏及以下 (max-width: 1024px) +@mixin tablet-landscape-and-below { + @media (max-width: #{$breakpoint-tablet-landscape}) { + @content; + } +} + +/// 仅桌面端 (min-width: 1025px) +@mixin desktop { + @media (min-width: #{$breakpoint-tablet-landscape + 1}) { + @content; + } +} + +/// 大桌面端 (min-width: 1281px) +@mixin desktop-large { + @media (min-width: #{$breakpoint-desktop + 1}) { + @content; + } +} + +// ============================================================================ +// Touch-friendly Mixins (触控友好混合宏) +// ============================================================================ + +/// 触控设备检测 +@mixin touch-device { + @media (hover: none) and (pointer: coarse) { + @content; + } +} + +/// 非触控设备检测 (鼠标/触控板) +@mixin pointer-device { + @media (hover: hover) and (pointer: fine) { + @content; + } +} + +/// 确保最小触控目标尺寸 +@mixin touch-target($size: $mobile-button-min-size) { + min-width: $size; + min-height: $size; +} + +/// 扩展触控区域(通过伪元素) +@mixin extend-touch-area($extend: 8px) { + position: relative; + + &::before { + content: ''; + position: absolute; + top: -$extend; + right: -$extend; + bottom: -$extend; + left: -$extend; + } +} + +// ============================================================================ +// Safe Area Helpers (安全区域辅助) +// ============================================================================ + +/// 添加底部安全区域 padding +@mixin safe-area-bottom($additional: 0px) { + padding-bottom: calc(#{$safe-area-bottom} + #{$additional}); +} + +/// 添加顶部安全区域 padding +@mixin safe-area-top($additional: 0px) { + padding-top: calc(#{$safe-area-top} + #{$additional}); +} + +/// 添加左侧安全区域 padding +@mixin safe-area-left($additional: 0px) { + padding-left: calc(#{$safe-area-left} + #{$additional}); +} + +/// 添加右侧安全区域 padding +@mixin safe-area-right($additional: 0px) { + padding-right: calc(#{$safe-area-right} + #{$additional}); +} + +/// 添加所有安全区域 padding +@mixin safe-area-all($additional: 0px) { + padding-top: calc(#{$safe-area-top} + #{$additional}); + padding-right: calc(#{$safe-area-right} + #{$additional}); + padding-bottom: calc(#{$safe-area-bottom} + #{$additional}); + padding-left: calc(#{$safe-area-left} + #{$additional}); +} + +// ============================================================================ +// Layout Helpers (布局辅助) +// ============================================================================ + +/// 移动端全屏抽屉/对话框 +@mixin mobile-fullscreen { + @include mobile { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + width: 100vw; + height: 100vh; + max-width: none; + max-height: none; + border-radius: 0; + @include safe-area-all(); + } +} + +/// 移动端底部固定元素 +@mixin mobile-fixed-bottom($height: auto) { + @include mobile { + position: fixed; + left: 0; + right: 0; + bottom: 0; + height: $height; + @include safe-area-bottom(); + @include safe-area-left(); + @include safe-area-right(); + } +} + +/// 隐藏在移动端 +@mixin hide-on-mobile { + @include mobile { + display: none !important; + } +} + +/// 隐藏在平板及以下 +@mixin hide-on-tablet-and-below { + @include tablet-and-below { + display: none !important; + } +} + +/// 仅在移动端显示 +@mixin show-on-mobile-only { + display: none !important; + + @include mobile { + display: block !important; + } +} + +/// 仅在平板及以下显示 +@mixin show-on-tablet-and-below-only { + display: none !important; + + @include tablet-and-below { + display: block !important; + } +} diff --git a/packages/drawnix/src/styles/index.scss b/packages/drawnix/src/styles/index.scss index f051c774..77f9a47e 100644 --- a/packages/drawnix/src/styles/index.scss +++ b/packages/drawnix/src/styles/index.scss @@ -5,6 +5,7 @@ @import './tdesign-theme.scss'; @import './theme.scss'; @import './z-index.scss'; // Z-Index层级管理变量 +@import './responsive'; // 响应式设计变量和混合宏 @import './toolbox-theme.scss'; // 工具箱主题变量 @import '../components/side-drawer/side-drawer.scss'; // 通用侧边抽屉 @import '../components/chat-drawer/chat-drawer.scss'; @@ -54,6 +55,13 @@ background: var(--color-surface-high); border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + + // 移动端调整定位,位于工具栏右侧避免重叠 + @media (max-width: 768px) { + left: 70px; // 工具栏宽度(~50px) + 间距 + transform: none; + top: calc(env(safe-area-inset-top, 0px) + 8px); + } } .pen-settings-toolbar { position: absolute; @@ -64,6 +72,13 @@ background: var(--color-surface-high); border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + + // 移动端调整定位,位于工具栏右侧避免重叠 + @media (max-width: 768px) { + left: 70px; // 工具栏宽度(~50px) + 间距 + transform: none; + top: calc(env(safe-area-inset-top, 0px) + 8px); + } } .draw-toolbar { cursor: default; @@ -170,7 +185,10 @@ // 可滚动内容区域 &__scrollable { flex: 1; + min-height: 0; // 关键:允许 flex 子元素收缩以启用滚动 overflow-y: auto; + -webkit-overflow-scrolling: touch; // iOS 惯性滚动 + touch-action: pan-y; // 允许垂直滚动触摸 display: flex; flex-direction: column; gap: 4px; // 增加间距 @@ -234,6 +252,181 @@ display: none; } } + + // ============================================================================ + // 移动端响应式样式 (Mobile Responsive Styles) + // ============================================================================ + + // 平板及以下 (max-width: 768px) - 简化工具栏 + @include tablet-and-below { + left: $mobile-spacing; + top: auto; + bottom: calc(#{$safe-area-bottom} + 70px); // 给 AI 输入栏留出空间 + height: auto; + max-height: calc(100vh - 160px); // 限制最大高度 + width: auto; + max-width: none; + border-radius: $mobile-border-radius; + + // 移动端强制使用图标模式 + .tool-icon__label { + display: none; + } + + // 调整 section padding + .unified-toolbar__section { + padding: 0 4px; + + &--fixed-top, + &--fixed-bottom { + padding: 8px 4px; + } + } + + // 调整按钮尺寸确保触控友好 + .tool-button, + .tool-icon { + @include touch-target($mobile-button-min-size); + } + } + + // 移动端竖屏 (max-width: 640px) - 最小化工具栏 + @include mobile { + left: $mobile-spacing; + bottom: calc(#{$safe-area-bottom} + 80px); // 更多空间给 AI 输入栏 + max-height: calc(100vh - 180px); + + // 更紧凑的布局 + &__scrollable { + padding: 4px 0; + gap: 2px; + } + + // 更紧凑的 section + .unified-toolbar__section { + padding: 0 2px; + + &:not(:first-child):not(.unified-toolbar__section--fixed-top):not(.unified-toolbar__section--fixed-bottom) { + margin-top: 4px; + padding-top: 4px; + } + + &--fixed-top, + &--fixed-bottom { + padding: 6px 2px; + } + } + } + + // 移动端收起状态修饰符 + &--mobile-collapsed { + @include tablet-and-below { + // 保持位置不变,只调整高度为自适应 + height: auto !important; + min-height: 0 !important; + max-height: none !important; + bottom: auto !important; + overflow: visible !important; + + // 隐藏所有常规内容 + .unified-toolbar__scrollable, + .unified-toolbar__section, + .unified-toolbar__mobile-toggle { + display: none !important; + } + } + } + + // 移动端收起状态快捷按钮区域 + &__collapsed-shortcuts { + display: none; // 默认隐藏 + + @include tablet-and-below { + display: flex; + flex-direction: column; + align-items: center; + gap: 0; + padding: 4px 8px 8px; + width: 100%; // 填满容器宽度 + } + } + + // 收起状态快捷按钮 + &__collapsed-btn { + display: flex; + align-items: center; + justify-content: center; + width: 100%; // 填满容器宽度 + height: 40px; + min-height: 40px; + background: transparent; + border: none; + border-radius: 8px; + color: var(--color-on-surface); + cursor: pointer; + transition: all 0.15s ease; + outline: none; // 移除焦点边框 + + &:hover, &:active { + background: var(--color-surface-high, rgba(0, 0, 0, 0.05)); + } + + &:focus { + outline: none; // 移除焦点边框 + } + + svg { + width: 22px; + height: 22px; + } + + // 展开按钮特殊样式 - 更紧凑 + &--toggle { + height: 28px; + min-height: 28px; + margin-bottom: 2px; + + svg { + width: 16px; + height: 16px; + color: var(--color-text-secondary, #999); + } + } + } + + // 移动端展开按钮(展开状态时显示) + &__mobile-toggle { + display: none; + + @include tablet-and-below { + display: flex; + align-items: center; + justify-content: center; + width: $mobile-button-min-size; + height: $mobile-button-min-size; + background: transparent; + border: none; + border-radius: 8px; + color: var(--color-on-surface); + cursor: pointer; + transition: background-color 0.2s ease; + flex-shrink: 0; + + &:hover, &:active { + background: var(--color-surface-high); + } + + svg { + width: 20px; + height: 20px; + transition: transform 0.2s ease; + } + + &--expanded svg { + transform: rotate(180deg); + } + } + } } // Embedded mode for individual toolbars (适配80px垂直布局) diff --git a/packages/drawnix/src/styles/tdesign-theme.scss b/packages/drawnix/src/styles/tdesign-theme.scss index 87d6e43f..62c80ad2 100644 --- a/packages/drawnix/src/styles/tdesign-theme.scss +++ b/packages/drawnix/src/styles/tdesign-theme.scss @@ -289,4 +289,298 @@ --td-border-level-1-color: #404040; --td-border-level-2-color: #595959; } +} + +/* ============================================================================ + * 移动端响应式适配 (Mobile Responsive Styles) + * ============================================================================ */ + +/* 平板及以下 (max-width: 768px) */ +@media (max-width: 768px) { + /* Dialog 组件移动端适配 */ + .t-dialog { + max-width: calc(100vw - 32px) !important; + margin: 16px !important; + + &.t-dialog--mode-modal { + /* 模态对话框几乎全屏 */ + width: calc(100vw - 32px) !important; + max-height: calc(100vh - 100px) !important; + } + + /* Header 紧凑布局 */ + .t-dialog__header { + padding: 12px 16px; + + .t-dialog__header-content { + font-size: 16px; + } + } + + /* Body 滚动支持 */ + .t-dialog__body { + padding: 12px 16px; + max-height: calc(100vh - 200px); + overflow-y: auto; + } + + /* Footer 固定在底部 */ + .t-dialog__footer { + padding: 12px 16px; + padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 12px); + } + + /* 按钮触控友好 */ + .t-button { + min-height: 40px; + min-width: 80px; + } + } + + /* Drawer 组件移动端适配 */ + .t-drawer { + &.t-drawer--left, + &.t-drawer--right { + max-width: calc(100vw - 48px) !important; + } + + .t-drawer__header { + padding: 12px 16px; + } + + .t-drawer__body { + padding: 12px 16px; + } + + .t-drawer__footer { + padding: 12px 16px; + padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 12px); + } + } + + /* Popup/Popover 移动端适配 */ + .t-popup, + .t-popover { + max-width: calc(100vw - 32px) !important; + } + + /* Select 下拉菜单适配 */ + .t-select__dropdown { + max-width: calc(100vw - 32px); + max-height: 50vh; + } + + /* Input 组件触控优化 */ + .t-input { + min-height: 40px; + + .t-input__inner { + font-size: 16px; /* 防止 iOS 自动缩放 */ + } + } + + /* Textarea 组件触控优化 */ + .t-textarea { + .t-textarea__inner { + font-size: 16px; /* 防止 iOS 自动缩放 */ + } + } + + /* Button 组件触控区域 */ + .t-button { + min-height: 36px; + padding: 8px 16px; + } + + /* Slider 组件触控优化 */ + .t-slider { + .t-slider__button { + width: 20px !important; + height: 20px !important; + } + + .t-slider__rail, + .t-slider__track { + height: 6px !important; + } + } +} + +/* 移动端竖屏 (max-width: 640px) */ +@media (max-width: 640px) { + /* Dialog 全屏显示 */ + .t-dialog { + &.t-dialog--mode-modal { + width: 100vw !important; + max-width: 100vw !important; + height: 100vh !important; + max-height: 100vh !important; + margin: 0 !important; + border-radius: 0 !important; + } + + .t-dialog__header { + padding-top: calc(env(safe-area-inset-top, 0px) + 12px); + } + + .t-dialog__body { + flex: 1; + max-height: none; + } + } + + /* Drawer 全宽显示 */ + .t-drawer { + &.t-drawer--left, + &.t-drawer--right { + width: 100vw !important; + max-width: 100vw !important; + } + + .t-drawer__header { + padding-top: calc(env(safe-area-inset-top, 0px) + 12px); + } + } + + /* Message 提示位置调整 */ + .t-message { + top: calc(env(safe-area-inset-top, 0px) + 16px) !important; + max-width: calc(100vw - 32px); + } + + /* Notification 提示位置调整 */ + .t-notification { + max-width: calc(100vw - 32px); + right: 16px !important; + left: 16px !important; + } +} + +/* 小屏移动端 (max-width: 480px) */ +@media (max-width: 480px) { + .t-dialog { + .t-dialog__header { + padding: 10px 12px; + padding-top: calc(env(safe-area-inset-top, 0px) + 10px); + + .t-dialog__header-content { + font-size: 15px; + } + } + + .t-dialog__body { + padding: 10px 12px; + } + + .t-dialog__footer { + padding: 10px 12px; + flex-wrap: wrap; + gap: 8px; + + .t-button { + flex: 1; + min-width: calc(50% - 4px); + } + } + } + + /* Select 组件紧凑 */ + .t-select { + .t-input__inner { + font-size: 14px; + } + } + + /* Tabs 组件紧凑 */ + .t-tabs { + .t-tabs__nav-item { + padding: 8px 12px; + font-size: 13px; + } + } +} + +/* ViewerJS 图片预览器移动端适配 */ +@media (max-width: 768px) { + .viewer-container { + // 底部区域允许滚动 + .viewer-footer { + overflow-x: auto !important; + overflow-y: hidden !important; + } + + // 工具条容器 + .viewer-toolbar { + overflow-x: auto !important; + overflow-y: hidden !important; + max-width: 100vw !important; + + // 隐藏滚动条但保持滚动功能 + scrollbar-width: none; + -ms-overflow-style: none; + &::-webkit-scrollbar { + display: none; + } + + > ul { + display: flex !important; + flex-wrap: nowrap !important; + white-space: nowrap !important; + padding: 6px 8px !important; + margin: 0 auto 5px !important; + + > li { + flex-shrink: 0 !important; + float: none !important; + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + } + } + } + + // 底部导航栏适配 + .viewer-navbar { + overflow-x: auto !important; + overflow-y: hidden !important; + + scrollbar-width: none; + -ms-overflow-style: none; + &::-webkit-scrollbar { + display: none; + } + } + + .viewer-list { + display: flex !important; + flex-wrap: nowrap !important; + overflow-x: auto !important; + width: auto !important; + } + + // 缩小关闭按钮 + .viewer-button { + width: 60px !important; + height: 60px !important; + } + } +} + +@media (max-width: 480px) { + .viewer-container { + .viewer-toolbar > ul > li { + width: 22px !important; + height: 22px !important; + margin: 0 1px !important; + + &::before { + margin: 1px !important; + } + } + + .viewer-button { + width: 50px !important; + height: 50px !important; + } + } } \ No newline at end of file diff --git a/packages/drawnix/src/styles/variables.module.scss b/packages/drawnix/src/styles/variables.module.scss index b47e2f08..4ee31512 100644 --- a/packages/drawnix/src/styles/variables.module.scss +++ b/packages/drawnix/src/styles/variables.module.scss @@ -45,7 +45,6 @@ &:active { background-color: var(--color-surface-primary-container); - border: 1px solid var(--button-active-border); svg { color: var(--color-on-primary-container); @@ -95,7 +94,6 @@ &:active { background-color: var(--button-active-bg, var(--island-bg-color)); - border-color: var(--button-active-border, var(--color-primary-darkest)); } &.active {