diff --git a/src/agentkit/server/frontend/package-lock.json b/src/agentkit/server/frontend/package-lock.json index 3f609c9..2b3ae00 100644 --- a/src/agentkit/server/frontend/package-lock.json +++ b/src/agentkit/server/frontend/package-lock.json @@ -24,7 +24,8 @@ "markdown-it": "^14.2.0", "pinia": "^2.2.0", "vue": "^3.5.0", - "vue-router": "^4.4.0" + "vue-router": "^4.4.0", + "vxe-table": "^4.19.19" }, "devDependencies": { "@playwright/test": "^1.59.0", @@ -1699,6 +1700,19 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/@vxe-ui/core": { + "version": "4.4.15", + "resolved": "https://registry.npmmirror.com/@vxe-ui/core/-/core-4.4.15.tgz", + "integrity": "sha512-DPNPnjSnypg8XO44ApcHw/nJNNJUYF85k1IVFJU8nyKj3qvYBFP96m4ZLOhc6tlMmMJWmhbzXCWZOb37dUNctQ==", + "license": "MIT", + "dependencies": { + "dom-zindex": "^1.0.7", + "xe-utils": "^4.0.10" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, "node_modules/acorn": { "version": "8.17.0", "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.17.0.tgz", @@ -2045,6 +2059,12 @@ "integrity": "sha512-bvVTQe1lfaUr1oFzZX80ce9KLDlZ3iU+XGNE/bz9HnGdklTieqsbmsLHe+rT2XWqopvL0PckkYqN7ksmm5pe3w==", "license": "MIT" }, + "node_modules/dom-zindex": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/dom-zindex/-/dom-zindex-1.0.7.tgz", + "integrity": "sha512-cKU/h8v8IPBgdZOTPbPmq3Ib+Ac5C+kKoh9I4LbGR9BM3GwbmB16KYWKJcj5M2BavnA66EbgYzxYDLd1IytnlQ==", + "license": "MIT" + }, "node_modules/dompurify": { "version": "3.4.10", "resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.4.10.tgz", @@ -3320,6 +3340,24 @@ "vue": "^3.0.0" } }, + "node_modules/vxe-pc-ui": { + "version": "4.15.21", + "resolved": "https://registry.npmmirror.com/vxe-pc-ui/-/vxe-pc-ui-4.15.21.tgz", + "integrity": "sha512-8uCUelYE2OyhKOUMCZ9DffRQvIuTpEYm3LsiwQ/XiKNq4qOdFY8RlBEzGY8MnZnpm+kKDXvynFFS59inQT42Rg==", + "license": "MIT", + "dependencies": { + "@vxe-ui/core": "^4.4.15" + } + }, + "node_modules/vxe-table": { + "version": "4.19.23", + "resolved": "https://registry.npmmirror.com/vxe-table/-/vxe-table-4.19.23.tgz", + "integrity": "sha512-kg6nzIkGCea4otjoxzWKVdcshUa00eswSlVZgo0cNIxpmmOMMIdmcY34IzJXl+UyYd/rvFGVjRidy/R97yaqVA==", + "license": "MIT", + "dependencies": { + "vxe-pc-ui": "^4.14.0" + } + }, "node_modules/warning": { "version": "4.0.3", "resolved": "https://registry.npmmirror.com/warning/-/warning-4.0.3.tgz", @@ -3373,6 +3411,12 @@ "node": ">=8" } }, + "node_modules/xe-utils": { + "version": "4.0.10", + "resolved": "https://registry.npmmirror.com/xe-utils/-/xe-utils-4.0.10.tgz", + "integrity": "sha512-HLj9r+EjCh6e0J1vfZhDKPwaTtONMl0GNK0OYR6b3KU4QHHpOXNFzEhgwe35KFy8dqsK2sm2WFRzHRafqkYgMA==", + "license": "MIT" + }, "node_modules/zrender": { "version": "6.1.0", "resolved": "https://registry.npmmirror.com/zrender/-/zrender-6.1.0.tgz", diff --git a/src/agentkit/server/frontend/package.json b/src/agentkit/server/frontend/package.json index 49e0492..32d076a 100644 --- a/src/agentkit/server/frontend/package.json +++ b/src/agentkit/server/frontend/package.json @@ -33,7 +33,8 @@ "markdown-it": "^14.2.0", "pinia": "^2.2.0", "vue": "^3.5.0", - "vue-router": "^4.4.0" + "vue-router": "^4.4.0", + "vxe-table": "^4.19.19" }, "devDependencies": { "@playwright/test": "^1.59.0", diff --git a/src/agentkit/server/frontend/src/components/bitable/AttachmentCell.vue b/src/agentkit/server/frontend/src/components/bitable/AttachmentCell.vue index a5b82cf..2d8c84f 100644 --- a/src/agentkit/server/frontend/src/components/bitable/AttachmentCell.vue +++ b/src/agentkit/server/frontend/src/components/bitable/AttachmentCell.vue @@ -45,10 +45,10 @@ function formatSize(bytes: number): string { .attachment-cell__link { display: inline-flex; align-items: center; - gap: 4px; - color: var(--color-primary, #1a1a1a); + gap: var(--bitable-spacing-xs); + color: var(--bitable-color-primary); text-decoration: none; - font-size: 12px; + font-size: var(--bitable-font-xs); } .attachment-cell__link:hover { @@ -63,11 +63,11 @@ function formatSize(bytes: number): string { } .attachment-cell__size { - color: var(--text-secondary, #8c8c8c); + color: var(--bitable-color-text-secondary); font-size: 11px; } .attachment-cell__empty { - color: var(--text-placeholder, #bfbfbf); + color: var(--bitable-color-text-placeholder); } diff --git a/src/agentkit/server/frontend/src/components/bitable/BitableGrid.vue b/src/agentkit/server/frontend/src/components/bitable/BitableGrid.vue index b8d4e77..c72fb85 100644 --- a/src/agentkit/server/frontend/src/components/bitable/BitableGrid.vue +++ b/src/agentkit/server/frontend/src/components/bitable/BitableGrid.vue @@ -292,35 +292,35 @@ defineExpose({ /* KTD10: CSS isolation — all vxe-table style overrides scoped to .bitable-grid-scope. Use :deep() to reach vxe-table internals. */ .bitable-grid-scope :deep(.vxe-table) { - font-size: 13px; + font-size: var(--bitable-font-sm); } .bitable-grid-scope :deep(.vxe-header--column) { - background: var(--bg-secondary, #fafafa); + background: var(--bitable-color-bg-secondary); font-weight: 600; } .bitable-grid-scope :deep(.vxe-body--column.is--dirty) { - background: var(--bg-tertiary, #f3f4f6); + background: var(--bitable-color-bg-tertiary); } .bitable-grid-scope :deep(.vxe-cell--dirty) { - color: var(--color-primary, #1a1a1a); + color: var(--bitable-color-primary); } .bitable-grid-scope__add-col { display: flex; align-items: center; - gap: 4px; + gap: var(--bitable-spacing-xs); cursor: pointer; - color: var(--text-secondary, #8c8c8c); - font-size: 12px; - padding: 0 8px; + color: var(--bitable-color-text-secondary); + font-size: var(--bitable-font-xs); + padding: 0 var(--bitable-spacing-sm); height: 100%; user-select: none; } .bitable-grid-scope__add-col:hover { - color: var(--color-primary, #1a1a1a); + color: var(--bitable-color-primary); } diff --git a/src/agentkit/server/frontend/src/components/bitable/ColumnHeaderMenu.vue b/src/agentkit/server/frontend/src/components/bitable/ColumnHeaderMenu.vue index 69ef205..83bd1d3 100644 --- a/src/agentkit/server/frontend/src/components/bitable/ColumnHeaderMenu.vue +++ b/src/agentkit/server/frontend/src/components/bitable/ColumnHeaderMenu.vue @@ -59,7 +59,7 @@ function handleMenuClick({ key }: { key: string }): void { .column-header-menu { display: flex; align-items: center; - gap: 4px; + gap: var(--bitable-spacing-xs); cursor: pointer; width: 100%; height: 100%; @@ -72,12 +72,12 @@ function handleMenuClick({ key }: { key: string }): void { text-overflow: ellipsis; white-space: nowrap; font-weight: 600; - font-size: 13px; + font-size: var(--bitable-font-sm); } .column-header-menu__arrow { font-size: 10px; - color: var(--text-secondary, #8c8c8c); + color: var(--bitable-color-text-secondary); flex-shrink: 0; opacity: 0.6; transition: opacity 0.15s; diff --git a/src/agentkit/server/frontend/src/components/bitable/ErrorState.vue b/src/agentkit/server/frontend/src/components/bitable/ErrorState.vue new file mode 100644 index 0000000..6e5e782 --- /dev/null +++ b/src/agentkit/server/frontend/src/components/bitable/ErrorState.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/src/agentkit/server/frontend/src/components/bitable/FieldTypeIcon.vue b/src/agentkit/server/frontend/src/components/bitable/FieldTypeIcon.vue new file mode 100644 index 0000000..ec7e593 --- /dev/null +++ b/src/agentkit/server/frontend/src/components/bitable/FieldTypeIcon.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/src/agentkit/server/frontend/src/components/bitable/FileCard.vue b/src/agentkit/server/frontend/src/components/bitable/FileCard.vue index 88bf9d7..0eab058 100644 --- a/src/agentkit/server/frontend/src/components/bitable/FileCard.vue +++ b/src/agentkit/server/frontend/src/components/bitable/FileCard.vue @@ -52,18 +52,18 @@ function formatDate(iso: string): string { diff --git a/src/agentkit/server/frontend/src/components/bitable/ImageCell.vue b/src/agentkit/server/frontend/src/components/bitable/ImageCell.vue index 5d8e425..beb23d6 100644 --- a/src/agentkit/server/frontend/src/components/bitable/ImageCell.vue +++ b/src/agentkit/server/frontend/src/components/bitable/ImageCell.vue @@ -83,21 +83,21 @@ onUnmounted(() => { .image-cell { display: flex; flex-wrap: wrap; - gap: 4px; + gap: var(--bitable-spacing-xs); padding: 2px 0; } .image-cell__thumb { width: 40px; height: 40px; - border-radius: 4px; + border-radius: var(--bitable-radius-sm); overflow: hidden; cursor: pointer; - background: var(--bg-secondary, #f5f5f5); + background: var(--bitable-color-bg-secondary); display: flex; align-items: center; justify-content: center; - border: 1px solid var(--border-color, #f0f0f0); + border: 1px solid var(--bitable-color-border); } .image-cell__img { @@ -107,11 +107,11 @@ onUnmounted(() => { } .image-cell__placeholder { - color: var(--text-placeholder, #bfbfbf); - font-size: 16px; + color: var(--bitable-color-text-placeholder); + font-size: var(--bitable-font-lg); } .image-cell__empty { - color: var(--text-placeholder, #bfbfbf); + color: var(--bitable-color-text-placeholder); } diff --git a/src/agentkit/server/frontend/src/components/bitable/LoadingState.vue b/src/agentkit/server/frontend/src/components/bitable/LoadingState.vue new file mode 100644 index 0000000..15700cb --- /dev/null +++ b/src/agentkit/server/frontend/src/components/bitable/LoadingState.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/src/agentkit/server/frontend/src/components/bitable/SelectDisplay.vue b/src/agentkit/server/frontend/src/components/bitable/SelectDisplay.vue index 5b95996..9871daa 100644 --- a/src/agentkit/server/frontend/src/components/bitable/SelectDisplay.vue +++ b/src/agentkit/server/frontend/src/components/bitable/SelectDisplay.vue @@ -1,28 +1,27 @@ + + diff --git a/src/agentkit/server/frontend/src/composables/useResponsiveBreakpoint.ts b/src/agentkit/server/frontend/src/composables/useResponsiveBreakpoint.ts new file mode 100644 index 0000000..7f56a8d --- /dev/null +++ b/src/agentkit/server/frontend/src/composables/useResponsiveBreakpoint.ts @@ -0,0 +1,87 @@ +/** + * useResponsiveBreakpoint — 响应式断点 composable + * + * 断点(移动优先,与 plan U1 Open Questions 一致): + * - isMobile: viewport < 768px + * - isTablet: 768px ≤ viewport < 1024px + * - isDesktop: viewport ≥ 1024px + * - isWide: viewport ≥ 1440px(供宽屏布局可选使用) + * + * 消费点:U3 RecordDetailDrawer(isMobile 时全屏覆盖)、 + * U5 ViewConfigPanel(isMobile 时改底部抽屉)。U1 仅暴露 API。 + * + * 实现:window.matchMedia + change 事件监听,onUnmounted 清理。 + * SSR 安全:未定义 window 时返回全 false 默认值( ponytail: 不引入 + * 专门的 SSR 检测库,matchMedia 不存在即视为非移动端默认布局)。 + */ +import { ref, onMounted, onUnmounted, type Ref } from 'vue' + +export interface ResponsiveBreakpoint { + isMobile: Ref + isTablet: Ref + isDesktop: Ref + isWide: Ref +} + +const MOBILE_MAX = 767 // < 768 +const TABLET_MAX = 1023 // < 1024 +const WIDE_MIN = 1440 // ≥ 1440 + +export function useResponsiveBreakpoint(): ResponsiveBreakpoint { + const isMobile = ref(false) + const isTablet = ref(false) + const isDesktop = ref(false) + const isWide = ref(false) + + // matchMedia 不支持 change 事件的旧浏览器回退(ponytail: 不做 + // resize 节流兜底——现代浏览器均支持 matchMedia change,避免额外 + // 依赖 lodash/throttle;已知 ceiling: IE11 不可用,本项目目标现代浏览器) + const mobileMql: MediaQueryList | null = + typeof window !== 'undefined' && window.matchMedia + ? window.matchMedia(`(max-width: ${MOBILE_MAX}px)`) + : null + const tabletMql: MediaQueryList | null = + typeof window !== 'undefined' && window.matchMedia + ? window.matchMedia(`(min-width: ${MOBILE_MAX + 1}px) and (max-width: ${TABLET_MAX}px)`) + : null + const wideMql: MediaQueryList | null = + typeof window !== 'undefined' && window.matchMedia + ? window.matchMedia(`(min-width: ${WIDE_MIN}px)`) + : null + + function sync(): void { + isMobile.value = mobileMql?.matches ?? false + isTablet.value = tabletMql?.matches ?? false + isWide.value = wideMql?.matches ?? false + // desktop = 既非 mobile 也非 tablet(≥ 1024) + isDesktop.value = !isMobile.value && !isTablet.value + } + + function createHandler(): () => void { + return () => sync() + } + + const mobileHandler = createHandler() + const tabletHandler = createHandler() + const wideHandler = createHandler() + + onMounted(() => { + sync() + // addEventListener('change', ...) 是现代标准 API + mobileMql?.addEventListener('change', mobileHandler) + tabletMql?.addEventListener('change', tabletHandler) + wideMql?.addEventListener('change', wideHandler) + }) + + onUnmounted(() => { + mobileMql?.removeEventListener('change', mobileHandler) + tabletMql?.removeEventListener('change', tabletHandler) + wideMql?.removeEventListener('change', wideHandler) + }) + + return { isMobile, isTablet, isDesktop, isWide } +} + +// ponytail self-check: 断点边界互斥性。运行时调用 sync() 后, +// isMobile/isTablet/isDesktop 三者有且仅有一个为 true(基于互斥的 +// matchMedia 查询)。trivial 互斥逻辑,不另立测试文件。 diff --git a/src/agentkit/server/frontend/src/main.ts b/src/agentkit/server/frontend/src/main.ts index 36f3be5..f1ce076 100644 --- a/src/agentkit/server/frontend/src/main.ts +++ b/src/agentkit/server/frontend/src/main.ts @@ -2,6 +2,7 @@ import { createApp } from 'vue' import { createPinia } from 'pinia' import 'ant-design-vue/dist/reset.css' import './styles' +import './styles/bitable-tokens.css' import App from './App.vue' import router from './router' import { useAuthStore } from './stores/auth' diff --git a/src/agentkit/server/frontend/src/styles/bitable-tokens.css b/src/agentkit/server/frontend/src/styles/bitable-tokens.css new file mode 100644 index 0000000..2a50902 --- /dev/null +++ b/src/agentkit/server/frontend/src/styles/bitable-tokens.css @@ -0,0 +1,77 @@ +/** + * Bitable Design Tokens — CSS Custom Properties + * + * 用 `--bitable-*` 前缀避免与 Ant Design Vue 4 全局 token 冲突。 + * 颜色/间距/圆角/字号 4 类 token + 抽屉宽度。Phase 2 (U2-U5) 所有 + * bitable 组件必须引用本文件的 var(),禁止硬编码 hex。 + * + * 颜色 token 映射到全局 tokens.css(已支持暗色主题),bitable 组件 + * 不直接维护暗色覆写,跟随全局主题切换。 + * + * 条件格式 8 色(--bitable-cf-*):每色提供 -bg(浅底)+ -fg(深字)配对。 + * -bg 用于单元格/chip 背景(配深色文本,对比度 ~12:1) + * -fg 用于文本/边框/图标(在白底上对比度 ≥4.5:1,WCAG AA) + * 颜色语义 key: red | orange | yellow | green | blue | purple | gray | neutral + */ +:root { + /* ── 颜色:基础语义(映射全局 token,跟随暗色主题) ── */ + --bitable-color-primary: var(--color-primary, #1a1a1a); + --bitable-color-bg: var(--bg-primary, #ffffff); + --bitable-color-bg-secondary: var(--bg-secondary, #fbfbfa); + --bitable-color-bg-tertiary: var(--bg-tertiary, #f7f7f5); + --bitable-color-text: var(--text-primary, #1a1a1a); + --bitable-color-text-secondary: var(--text-secondary, #4a4a4a); + --bitable-color-text-tertiary: var(--text-tertiary, #6b6b6a); + --bitable-color-text-placeholder: var(--text-placeholder, #9b9b9a); + --bitable-color-border: var(--border-color, #ededec); + --bitable-color-border-hover: var(--border-color-hover, #dfdfde); + --bitable-color-border-split: var(--border-color-split, #f2f2f0); + + /* ── 颜色:条件格式 8 色(语义 key → bg/fg 配对) ── */ + /* red */ + --bitable-cf-red-bg: #fee2e2; + --bitable-cf-red-fg: #b91c1c; + /* orange */ + --bitable-cf-orange-bg: #ffedd5; + --bitable-cf-orange-fg: #9a3412; + /* yellow(深金以保证 AA) */ + --bitable-cf-yellow-bg: #fef9c3; + --bitable-cf-yellow-fg: #a16207; + /* green */ + --bitable-cf-green-bg: #dcfce7; + --bitable-cf-green-fg: #15803d; + /* blue */ + --bitable-cf-blue-bg: #dbeafe; + --bitable-cf-blue-fg: #1d4ed8; + /* purple */ + --bitable-cf-purple-bg: #f3e8ff; + --bitable-cf-purple-fg: #7e22ce; + /* gray */ + --bitable-cf-gray-bg: #f3f4f6; + --bitable-cf-gray-fg: #374151; + /* neutral(与 gray 区分:更纯的中性灰) */ + --bitable-cf-neutral-bg: #f5f5f4; + --bitable-cf-neutral-fg: #404040; + + /* ── 间距 ── */ + --bitable-spacing-xs: 4px; + --bitable-spacing-sm: 8px; + --bitable-spacing-md: 12px; + --bitable-spacing-lg: 16px; + --bitable-spacing-xl: 24px; + + /* ── 圆角 ── */ + --bitable-radius-sm: 4px; + --bitable-radius-md: 6px; + --bitable-radius-lg: 8px; + + /* ── 字号 ── */ + --bitable-font-xs: 12px; + --bitable-font-sm: 13px; + --bitable-font-md: 14px; + --bitable-font-lg: 16px; + + /* ── 抽屉宽度(KTD4: ≤10 字段 480px,>10 字段 640px) ── */ + --bitable-drawer-width: 480px; + --bitable-drawer-width-wide: 640px; +} diff --git a/src/agentkit/server/frontend/src/views/BitableFileDetailView.vue b/src/agentkit/server/frontend/src/views/BitableFileDetailView.vue index 6c3b845..8185e96 100644 --- a/src/agentkit/server/frontend/src/views/BitableFileDetailView.vue +++ b/src/agentkit/server/frontend/src/views/BitableFileDetailView.vue @@ -45,7 +45,7 @@
- +

请选择左侧的数据表,或点击 + 新建数据表

@@ -280,40 +280,40 @@ async function handleDeleteField(field: IBitableField): Promise { height: 100vh; width: 100vw; overflow: hidden; - background: var(--bg-primary, #fff); + background: var(--bitable-color-bg); } .bitable-file-detail-view__topbar { display: flex; align-items: center; justify-content: space-between; - padding: 8px 16px; - border-bottom: 1px solid var(--border-color, #f0f0f0); + padding: var(--bitable-spacing-sm) var(--bitable-spacing-lg); + border-bottom: 1px solid var(--bitable-color-border); flex-shrink: 0; } .bitable-file-detail-view__topbar-left { display: flex; align-items: center; - gap: 8px; + gap: var(--bitable-spacing-sm); } .bitable-file-detail-view__topbar-right { display: flex; align-items: center; - gap: 8px; + gap: var(--bitable-spacing-sm); } .bitable-file-detail-view__icon { font-size: 20px; - color: var(--color-primary, #1a1a1a); + color: var(--bitable-color-primary); display: inline-flex; align-items: center; } .bitable-file-detail-view__title { font-weight: 600; - font-size: 16px; + font-size: var(--bitable-font-lg); } .bitable-file-detail-view__body { @@ -342,28 +342,28 @@ async function handleDeleteField(field: IBitableField): Promise { align-items: center; justify-content: center; height: 100%; - gap: 16px; - color: var(--text-placeholder, #bfbfbf); + gap: var(--bitable-spacing-lg); + color: var(--bitable-color-text-placeholder); } .bitable-file-detail-view__grid-header { display: flex; align-items: baseline; - gap: 12px; - padding: 12px 16px; - border-bottom: 1px solid var(--border-color, #f0f0f0); + gap: var(--bitable-spacing-md); + padding: var(--bitable-spacing-md) var(--bitable-spacing-lg); + border-bottom: 1px solid var(--bitable-color-border); flex-shrink: 0; } .bitable-file-detail-view__table-name { margin: 0; - font-size: 16px; + font-size: var(--bitable-font-lg); font-weight: 600; } .bitable-file-detail-view__field-count { - font-size: 12px; - color: var(--text-secondary, #8c8c8c); + font-size: var(--bitable-font-xs); + color: var(--bitable-color-text-secondary); } .bitable-file-detail-view__grid-container { @@ -375,8 +375,8 @@ async function handleDeleteField(field: IBitableField): Promise { .bitable-file-detail-view__load-more { display: flex; justify-content: center; - padding: 8px; - border-top: 1px solid var(--border-color, #f0f0f0); + padding: var(--bitable-spacing-sm); + border-top: 1px solid var(--bitable-color-border); flex-shrink: 0; } diff --git a/src/agentkit/server/frontend/src/views/BitableFileListView.vue b/src/agentkit/server/frontend/src/views/BitableFileListView.vue index 55bfd10..8151fe6 100644 --- a/src/agentkit/server/frontend/src/views/BitableFileListView.vue +++ b/src/agentkit/server/frontend/src/views/BitableFileListView.vue @@ -189,16 +189,16 @@ function handleDelete(file: IBitableFile): void { height: 100vh; width: 100vw; overflow: hidden; - background: var(--bg-secondary, #fafafa); + background: var(--bitable-color-bg-secondary); } .bitable-file-list-view__topbar { display: flex; align-items: center; justify-content: space-between; - padding: 8px 16px; - background: var(--bg-primary, #fff); - border-bottom: 1px solid var(--border-color, #f0f0f0); + padding: var(--bitable-spacing-sm) var(--bitable-spacing-lg); + background: var(--bitable-color-bg); + border-bottom: 1px solid var(--bitable-color-border); flex-shrink: 0; } @@ -248,7 +248,7 @@ function handleDelete(file: IBitableFile): void { } .bitable-file-list-view__option-icon { - font-size: 16px; - color: var(--color-primary, #1a1a1a); + font-size: var(--bitable-font-lg); + color: var(--bitable-color-primary); }