f0142f7019
## 关联 Issue - Close #213:补充本地资源注册类型 - Close #215:后台表查询添加字段查询 ## 背景 本 PR 合并处理两个规模较小、已完成统一 review 的客户端与后台能力改动,整体已基于远端最新 master。 ## 本地资源注册(#213) - AGC 本地项目资源支持图片、字体、音频、视频、文档和代码类型。 - 支持 PNG/JPEG/WEBP/GIF/SVG/AVIF/BMP、TTF/OTF/WOFF/WOFF2、MP3/WAV/OGG/FLAC/M4A/AAC/OPUS、MP4/WEBM/MOV,以及常见文本、配置和源码扩展名。 - 同步更新 Direct Tool Bridge、MCP schema、Runtime/Provider Prompt、AGC Skill 文档和技术方案。 - 保留项目相对路径、敏感目录、符号链接、文件大小、字体解析、UTF-8、项目锁、manifest/revision 和幂等语义。 - 修复从 game 等目录复制到 assets/uploads 时扩展名退化为 .bin 的问题。 ## 后台表查询(#215) - 使用结构化筛选条件替换手写筛选 JSON。 - 支持多条件、比较、包含、前后缀、列表、空值和旧 JSON object 兼容。 - 优化预览表格、详情复制、Toast、JSON 预览和排序交互。 - 修复数字范围筛选,包括数据库字符串化数字的数值比较。 - 补充前后端回归测试与文档。 ## 验证 - AGC 本地资源导入测试:6/6 - AGC Direct/MCP 工具测试:8/8 - 本地资源扩展名与 Prompt 映射回归测试通过 - 后台 admin 数据库测试:17/17 - 后台页面 Vitest:16/16 - `npm run admin-web:typecheck` - `npm run admin-web:build` - `npm run check:encoding` - `git diff --check` - `rustfmt --edition 2024 --check server-rs/crates/api-server/src/admin.rs` ## 限制 - 未执行真实生产 SpacetimeDB、OSS 或其它付费服务验证。 - Native shell CI 及其它 PR CI 以最新 head 的远端结果为准。 - 当前 PR 保持 WIP,待 reviewer 基于最新 head 重新 review。 Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/232 Co-authored-by: 董羽秦 <suzmii@qq.com> Co-committed-by: 董羽秦 <suzmii@qq.com>
78 lines
3.0 KiB
TypeScript
78 lines
3.0 KiB
TypeScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
import { describe, expect, test } from 'vitest';
|
|
|
|
const stylesheet = fs.readFileSync(
|
|
path.resolve(process.cwd(), 'apps/admin-web/src/styles/admin.css'),
|
|
'utf8',
|
|
);
|
|
|
|
describe('admin shell scrolling contract', () => {
|
|
test('desktop shell keeps sidebar separate from the scrollable content', () => {
|
|
expect(ruleFor('.admin-shell')).toContain('\n height: 100dvh;');
|
|
expect(ruleFor('.admin-shell')).toContain('overflow: hidden');
|
|
expect(ruleFor('.admin-sidebar')).toContain('min-height: 0');
|
|
expect(ruleFor('.admin-sidebar')).toContain('overflow-y: auto');
|
|
expect(ruleFor('.admin-main')).toContain('min-height: 0');
|
|
expect(ruleFor('.admin-main')).toContain('overflow: hidden');
|
|
expect(ruleFor('.admin-content')).toContain('min-height: 0');
|
|
expect(ruleFor('.admin-content')).toContain('overflow: auto');
|
|
});
|
|
|
|
test('mobile navigation stays in one horizontally scrollable row', () => {
|
|
const mobileStyles = stylesheet.slice(
|
|
stylesheet.indexOf('@media (max-width: 980px)'),
|
|
);
|
|
expect(mobileStyles).toContain('.admin-bottom-nav {');
|
|
expect(mobileStyles).toContain('display: flex');
|
|
expect(mobileStyles).toContain('overflow-x: auto');
|
|
expect(mobileStyles).not.toContain(
|
|
'grid-template-columns: repeat(auto-fit, minmax(64px, 1fr))',
|
|
);
|
|
expect(mobileStyles).toContain('flex: 0 0 76px');
|
|
});
|
|
|
|
test('database preview headers stay on one line and expose ellipsis', () => {
|
|
const sortButton = ruleFor('.admin-table-sort-button');
|
|
const sortLabel = ruleFor('.admin-table-sort-button span');
|
|
expect(sortButton).toContain('white-space: nowrap');
|
|
expect(sortButton).toContain('overflow: hidden');
|
|
expect(sortLabel).toContain('text-overflow: ellipsis');
|
|
expect(sortLabel).toContain('white-space: nowrap');
|
|
});
|
|
|
|
test('copy feedback uses an auto-dismissing toast surface', () => {
|
|
const toast = ruleFor('.admin-toast');
|
|
expect(toast).toContain('position: fixed');
|
|
expect(toast).toContain('z-index: 120');
|
|
expect(toast).toContain('pointer-events: none');
|
|
expect(toast).toContain('animation: admin-toast-in');
|
|
expect(stylesheet).toContain(".admin-toast[data-tone='error']");
|
|
});
|
|
|
|
test('detail JSON keeps only a slim scrollbar thumb', () => {
|
|
const json = ruleFor('.admin-json-preview');
|
|
expect(json).toContain('scrollbar-width: thin');
|
|
expect(json).toContain('scrollbar-color: #d1b09a transparent');
|
|
expect(stylesheet).toContain(
|
|
'.admin-json-preview::-webkit-scrollbar-track',
|
|
);
|
|
expect(stylesheet).toContain('background: transparent');
|
|
expect(stylesheet).toContain(
|
|
'.admin-json-preview::-webkit-scrollbar-thumb',
|
|
);
|
|
});
|
|
});
|
|
|
|
function ruleFor(selector: string) {
|
|
const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
const match = stylesheet.match(
|
|
new RegExp(`${escapedSelector}\\s*\\{([^}]*)\\}`),
|
|
);
|
|
if (!match) {
|
|
throw new Error(`Missing CSS rule for ${selector}`);
|
|
}
|
|
return match[1];
|
|
}
|