From 4403f6bb1dff035cb88b5b0aab2841443e2de391 Mon Sep 17 00:00:00 2001 From: kx_guo Date: Mon, 26 Oct 2026 15:47:56 +0800 Subject: [PATCH] feat: redesign product AI center portal --- README.md | 97 ++++++++++++++-------------------- index.html | 62 +++++++++------------- package.json | 2 +- scripts/serve.mjs | 2 +- src/apps.mjs | 64 +++++------------------ src/main.mjs | 125 ++++++++++---------------------------------- src/styles.css | 10 +--- tests/apps.test.mjs | 46 +++++++--------- 8 files changed, 130 insertions(+), 278 deletions(-) diff --git a/README.md b/README.md index c091319..2c665f7 100644 --- a/README.md +++ b/README.md @@ -1,77 +1,60 @@ -# 碟雅产品AI中心 +# 蝶雅产品AI中心 -独立的统一应用门户项目,已配置提供的 8 个业务地址。采用原生 HTML、CSS、JavaScript 模块,无第三方依赖、无外部字体或 CDN;不需要 npm install。可独立部署到 Nginx、静态网站托管或现有 Web 服务器。 - -## 启动与构建 - -需要 Node.js 18 或更高版本。在项目目录打开终端: - -```sh -npm run dev -``` - -浏览器访问 http://127.0.0.1:5173 。指定端口:`npm run dev -- --port 8080`。开发预览服务默认仅监听本机;生产环境请部署构建出的静态文件。 - -```sh -npm test -npm run build -``` - -将 `dist/` **里面的文件**上传到网站目录即可。无需运行 Node.js 生产服务。文件使用相对路径,支持部署在域名根目录或带结尾 `/` 的子目录。必须通过 HTTP/HTTPS 访问,不支持直接双击 HTML。服务器需为 `.mjs` 配置 JavaScript MIME 类型,参考 `deploy/nginx.conf.example`。 +面向产品部的对外统一工作台,当前只展示两个正式产品工具:风险预警、成本识别。项目采用原生 HTML、CSS、JavaScript 模块,无第三方依赖,可直接部署到宝塔 Nginx 静态网站。 ## 已集成入口 | 应用 | 地址 | 环境 | | --- | --- | --- | -| 碟雅运营AI中心 | https://aiagent.dieya.net.cn/ | 正式 | -| 碟雅AI中心测试版 | https://aiagent1.dieya.net.cn/ | 测试 | -| 需求收集 | http://demand.dieya.net.cn/ | 正式 | -| 财务AI助手测试 | http://finance-test.dieya.net.cn/ | 测试 | -| 成本识别 | http://cost.dieya.net.cn/ | 正式 | -| 违禁词助手 | http://prohibite.dieya.net.cn/ | 正式 | -| 风险预警测试 | http://risk.dieya.net.cn/ | 测试 | | 风险预警 | http://risk-prod.dieya.net.cn/ | 正式 | +| 成本识别 | http://cost.dieya.net.cn/ | 正式 | -不包含制度小助手。仅加入本次给出地址的 8 个应用,没有猜测截图中其他项目的地址。 +其他业务入口、测试版应用和制度小助手不在本门户展示。 -## 页面功能 +## 本地启动 -- 应用卡片点击后在当前窗口进入配置的完整 URL,浏览器或钉钉容器的返回操作可回到门户。 -- 按应用名称、简介或域名搜索;按分类、正式/测试环境筛选。 -- 星标收藏与“我的常用”,保存在当前浏览器的 localStorage,不跨设备同步,也不作为权限依据。 -- 自适应桌面和手机,支持键盘访问;按 `/` 聚焦搜索,按 Escape 清空搜索。 +需要 Node.js 18 或更高版本。项目没有第三方依赖,不需要 `npm install`: -## 维护应用 +```sh +npm test +npm run dev +``` -修改 `src/apps.mjs` 中的 `applications`,运行测试和构建后重新发布。 +默认访问 http://127.0.0.1:5173,也可以指定端口:`npm run dev -- --port 8080`。 -```js -{ - id: 'unique-id', - name: '应用名称', - shortName: '简称', - description: '应用说明', - url: 'http://your-app.example.com/', - category: 'tools', // ai / tools / risk - environment: 'production', // production / test - color: 'blue', // blue / violet / teal / amber - icon: 'clipboard' // sparkles / clipboard / wallet / scan / text / shield +## 宝塔部署 + +生产环境只部署 `dist/` 目录中的静态文件: + +```sh +npm run build +``` + +将 `dist/` 里面的文件上传到网站目录,或在宝塔网站设置中将网站根目录指向: + +```text +/www/wwwroot/dieya-product-ai-center/dist +``` + +不需要创建 Node 项目,不需要 PHP,不需要反向代理。网站默认文档使用 `index.html`。服务器需为 `.mjs` 文件返回 JavaScript 类型,Nginx 可在站点配置中加入: + +```nginx +location ~ \.mjs$ { + types { application/javascript mjs; } + add_header Cache-Control "no-cache"; + try_files $uri =404; } ``` -## 接入钉钉 +## 修改入口 -1. 确定新门户的正式部署地址。此项目尚未绑定域名或发布到服务器。 -2. 在钉钉开发者后台创建或配置企业内部网页应用,把门户的 URL 配为对应的移动端和电脑端首页,并配置需要的应用可见范围。具体后台字段以企业应用类型为准。 -3. 从钉钉手机端与电脑端打开门户,逐个验证进入目标应用及返回门户的操作。 -4. 目标系统各自维护免登、可信域名、回调地址和服务端权限校验。本门户没有模拟登录、没有采集凭据,也不会把 token 或免登码拼到目标链接。 -5. 特别确认原应用是否依赖从工作台传入的 corpId、应用上下文或特定入口路径。普通域名跳转不会自动恢复这些参数;若依赖,需按目标应用的实际认证方式修改完整入口或做应用启动适配。 -6. 验证全部入口可用后,再由企业管理员决定原工作台入口如何调整。制度小助手保留原独立入口。 +编辑 `src/apps.mjs` 中的 `applications` 数组,然后重新构建并部署 `dist`。当前配置会校验应用必须是产品部正式入口,不允许测试环境配置混入对外门户。 -运营 AI 中心和 AI 中心测试版已按确认的访问地址使用 HTTPS,其余 6 个应用保留原提供的 HTTP 地址。正式发布时请使用实际可用的协议;如果其他目标系统启用 HTTPS,应在配置中更新并重新验证。 +## 页面功能 -## 集成范围 +- 参考多智能体工作台的简洁卡片风格,支持浅色和深色主题按钮切换。 +- 顶部品牌导航、产品部工具卡片和响应式布局,避免复杂的筛选与侧栏层级。 +- 每张卡片展示产品部标签、正式应用状态和打开工作台按钮。 +- 主题选择保存在当前浏览器中,不影响应用权限。 -本项目完成的是应用入口统一,未合并原业务代码、数据库、登录会话或数据权限。所有卡片对能访问门户的人可见;本项目不是访问控制系统。如果后续需要按部门/角色展示卡片,需要增加门户后端或接入现有身份服务,且目标系统仍应自行鉴权。 - -已验证静态构建、入口映射与筛选逻辑;目标系统的线上可用性和钉钉内免登需在企业实际环境联调。 +目标业务系统仍需自行负责登录、钉钉免登、可信域名和权限校验。本门户只负责统一展示和跳转。 diff --git a/index.html b/index.html index 57103a9..0e4d824 100644 --- a/index.html +++ b/index.html @@ -1,52 +1,42 @@ - + - - - 碟雅产品AI中心 + + + 蝶雅产品AI中心 - - -
-
- -
统一应用门户
+ +
+ +
+ + + 蝶雅产品AI中心PRODUCT AI WORKSPACE + +
-
-
-
DIEYA WORKSPACE
-

每一份效率,都有入口。

-

连接 AI 与业务,让常用应用触手可及。

-
8 个应用一个入口,开启工作
-
- +
+
+

PRODUCT AI CENTER

+

选择智能体

+

选择需要使用的产品工具,立即开始工作。

-
-

全部应用

你的业务工具与 AI 助手,都在这里。

- -
-
-
- -
-
碟雅产品AI中心连接业务 · 提升效率
+
+ +
蝶雅产品AI中心连接产品 · 提升效率
- diff --git a/package.json b/package.json index 7a43660..acd604a 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "private": true, "type": "module", - "description": "碟雅产品AI中心", + "description": "蝶雅产品AI中心", "engines": { "node": ">=18" }, "scripts": { "dev": "node scripts/serve.mjs", diff --git a/scripts/serve.mjs b/scripts/serve.mjs index ee0ce27..4664021 100644 --- a/scripts/serve.mjs +++ b/scripts/serve.mjs @@ -23,4 +23,4 @@ const server = http.createServer(async (request, response) => { } catch { response.writeHead(404); response.end('Not found'); } }); server.on('error', error => { console.error(`启动失败:${error.message}`); process.exitCode = 1; }); -server.listen(port, host, () => console.log(`碟雅产品AI中心:http://${host}:${port}`)); +server.listen(port, host, () => console.log(`蝶雅产品AI中心:http://${host}:${port}`)); diff --git a/src/apps.mjs b/src/apps.mjs index bb6271b..7e95139 100644 --- a/src/apps.mjs +++ b/src/apps.mjs @@ -1,68 +1,29 @@ -// 新增或修改应用只需维护此配置。各入口使用用户确认的域名与协议。 +// 对外门户只维护产品部正式应用。新增入口时在此数组追加配置。 export const applications = [ { - id: 'operations-ai', name: '碟雅运营AI中心', shortName: '运营', - description: '运营 AI 应用统一入口', - url: 'https://aiagent.dieya.net.cn/', - category: 'ai', environment: 'production', color: 'blue', icon: 'sparkles' - }, - { - id: 'ai-center-test', name: '碟雅AI中心测试版', shortName: 'AI', - description: 'AI 中心测试应用入口', - url: 'https://aiagent1.dieya.net.cn/', - category: 'ai', environment: 'test', color: 'violet', icon: 'sparkles' - }, - { - id: 'demand', name: '需求收集', shortName: '需求', - description: '业务需求收集与协作入口', - url: 'http://demand.dieya.net.cn/', - category: 'tools', environment: 'production', color: 'teal', icon: 'clipboard' - }, - { - id: 'finance-test', name: '财务AI助手测试', shortName: '财务', - description: '财务 AI 助手测试应用入口', - url: 'http://finance-test.dieya.net.cn/', - category: 'ai', environment: 'test', color: 'amber', icon: 'wallet' + id: 'risk', name: '风险预警', shortName: '风控', + description: '识别业务风险,快速查看预警信息与处理建议。', + url: 'http://risk-prod.dieya.net.cn/', + category: 'product', environment: 'production', color: 'violet', icon: 'shield' }, { id: 'cost', name: '成本识别', shortName: '成本', - description: '成本识别业务应用入口', + description: '辅助产品团队识别成本结构,提升评估与决策效率。', url: 'http://cost.dieya.net.cn/', - category: 'tools', environment: 'production', color: 'blue', icon: 'scan' - }, - { - id: 'prohibite', name: '违禁词助手', shortName: '文本', - description: '违禁词检查应用入口', - url: 'http://prohibite.dieya.net.cn/', - category: 'tools', environment: 'production', color: 'violet', icon: 'text' - }, - { - id: 'risk-test', name: '风险预警测试', shortName: '风控', - description: '风险预警测试应用入口', - url: 'http://risk.dieya.net.cn/', - category: 'risk', environment: 'test', color: 'amber', icon: 'shield' - }, - { - id: 'risk', name: '风险预警', shortName: '风控', - description: '风险预警正式应用入口', - url: 'http://risk-prod.dieya.net.cn/', - category: 'risk', environment: 'production', color: 'teal', icon: 'shield' + category: 'product', environment: 'production', color: 'blue', icon: 'scan' } ]; export const categories = [ - { id: 'all', name: '全部应用', icon: 'grid' }, - { id: 'ai', name: 'AI 助手', icon: 'sparkles' }, - { id: 'tools', name: '业务工具', icon: 'clipboard' }, - { id: 'risk', name: '风险管理', icon: 'shield' }, + { id: 'all', name: '全部工具', icon: 'grid' }, + { id: 'product', name: '产品部工作台', icon: 'layers' }, { id: 'favorites', name: '我的常用', icon: 'star' } ]; -export function filterApplications(apps, { query = '', category = 'all', environment = 'all', favorites = [] } = {}) { +export function filterApplications(apps, { query = '', category = 'all', favorites = [] } = {}) { const term = query.trim().toLocaleLowerCase(); return apps.filter(app => (category === 'all' || (category === 'favorites' ? favorites.includes(app.id) : app.category === category)) && - (environment === 'all' || app.environment === environment) && (!term || [app.name, app.description, app.url].some(value => value.toLocaleLowerCase().includes(term))) ); } @@ -76,8 +37,9 @@ export function validateApplications(apps) { if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) { throw new Error(`应用地址无效:${app.name}`); } - if (!app.name || !app.description || !['ai', 'tools', 'risk'].includes(app.category) || - !['production', 'test'].includes(app.environment)) throw new Error(`应用配置不完整:${app.id}`); + if (!app.name || !app.description || app.category !== 'product' || app.environment !== 'production') { + throw new Error(`应用配置不完整:${app.id}`); + } } return true; } diff --git a/src/main.mjs b/src/main.mjs index f007d4d..1903d77 100644 --- a/src/main.mjs +++ b/src/main.mjs @@ -1,110 +1,43 @@ -import { applications, categories, filterApplications, validateApplications } from './apps.mjs'; +import { applications, validateApplications } from './apps.mjs'; validateApplications(applications); const icons = { - grid: '', - sparkles: '', - clipboard: '', - wallet: '', - scan: '', - text: '', shield: '', - star: '', + scan: '', arrow: '', - search: '' + external: '' }; -const svg = name => ``; +const svg = name => ``; const escapeHtml = value => String(value).replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char])); -const storageKey = 'dieya-portal:favorites:v1'; -function readFavorites() { - try { - const stored = JSON.parse(localStorage.getItem(storageKey) || '[]'); - return Array.isArray(stored) ? stored.filter(id => applications.some(app => app.id === id)) : []; - } catch { return []; } -} -const state = { query: '', category: 'all', environment: 'all', favorites: readFavorites() }; -const grid = document.querySelector('#app-grid'); -const search = document.querySelector('#search'); -const nav = document.querySelector('#category-nav'); -const filters = document.querySelector('#environment-filters'); -document.querySelector('#search-icon').innerHTML = svg('search'); -document.querySelector('#empty-icon').innerHTML = svg('search'); -document.querySelector('#total-count').textContent = applications.length; -const environmentLabels = [{ id: 'all', label: '全部' }, { id: 'production', label: '正式应用' }, { id: 'test', label: '测试应用' }]; -let toastTimer; +const grid = document.querySelector('#agent-grid'); +const root = document.documentElement; +const themeKey = 'dieya-portal:theme:v1'; +const storedTheme = localStorage.getItem(themeKey); +const preferredTheme = storedTheme || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); -function renderNav() { - nav.innerHTML = categories.map(category => { - const count = category.id === 'favorites' ? state.favorites.length : category.id === 'all' ? applications.length : applications.filter(app => app.category === category.id).length; - return ``; - }).join(''); +function applyTheme(theme) { + root.dataset.theme = theme; + document.querySelectorAll('[data-theme]').forEach(button => { + const active = button.dataset.theme === theme; + button.classList.toggle('active', active); + button.setAttribute('aria-pressed', String(active)); + }); + document.querySelector('meta[name="theme-color"]').setAttribute('content', theme === 'dark' ? '#070b17' : '#f5f8fc'); + try { localStorage.setItem(themeKey, theme); } catch { /* 当前浏览器不支持持久化时仍可切换 */ } } function render() { - renderNav(); - filters.innerHTML = environmentLabels.map(item => ``).join(''); - document.querySelector('#section-title').textContent = categories.find(category => category.id === state.category).name; - document.querySelector('#section-description').textContent = state.category === 'favorites' ? '收藏你常用的应用,下次更快找到。' : '你的业务工具与 AI 助手,都在这里。'; - const matches = filterApplications(applications, state); - document.querySelector('#result-count').textContent = `共 ${matches.length} 个应用`; - grid.innerHTML = matches.map(app => { - const favorite = state.favorites.includes(app.id); - const categoryName = categories.find(category => category.id === app.category).name; - return `
-
${svg(app.icon)}
-

${escapeHtml(app.name)}

-

${escapeHtml(app.description)}

-
${escapeHtml(categoryName)}${app.environment === 'test' ? '测试' : '正式'}
-
${escapeHtml(new URL(app.url).hostname)}
-
`; - }).join(''); - document.querySelector('#empty-state').hidden = matches.length > 0; - const emptyFavorites = state.category === 'favorites' && state.favorites.length === 0; - document.querySelector('#empty-title').textContent = emptyFavorites ? '把常用应用放在这里' : '没有找到匹配的应用'; - document.querySelector('#empty-description').textContent = emptyFavorites ? '点击应用卡片右上角的星标,即可加入我的常用。' : '试试其他关键词,或调整筛选条件。'; + grid.innerHTML = applications.map((app, index) => `
+
${svg(app.icon)}产品部
+

${escapeHtml(app.name)} 正式应用

+

${escapeHtml(app.description)}

+
就绪0${index + 1}
+ 打开工作台 ${svg('arrow')} + +
`).join(''); + document.querySelector('#empty-state').hidden = applications.length > 0; } -nav.addEventListener('click', event => { - const button = event.target.closest('[data-category]'); - if (!button) return; - state.category = button.dataset.category; - render(); -}); -filters.addEventListener('click', event => { - const button = event.target.closest('[data-environment]'); - if (!button) return; - state.environment = button.dataset.environment; - render(); -}); -search.addEventListener('input', () => { state.query = search.value; render(); }); -grid.addEventListener('click', event => { - const button = event.target.closest('[data-favorite]'); - if (!button) return; - const id = button.dataset.favorite; - const adding = !state.favorites.includes(id); - state.favorites = adding ? [...state.favorites, id] : state.favorites.filter(value => value !== id); - let persisted = true; - try { localStorage.setItem(storageKey, JSON.stringify(state.favorites)); } catch { persisted = false; } - render(); - grid.querySelector(`[data-favorite="${id}"]`)?.focus({ preventScroll: true }); - const toast = document.querySelector('#toast'); - toast.textContent = `${adding ? '已加入我的常用' : '已取消常用'}${persisted ? '' : '(当前浏览器无法保存,仅本次有效)'}`; - toast.classList.add('visible'); - clearTimeout(toastTimer); - toastTimer = setTimeout(() => toast.classList.remove('visible'), 2400); -}); -document.querySelector('#reset-filters').addEventListener('click', () => { - state.category = 'all'; state.environment = 'all'; state.query = ''; search.value = ''; render(); -}); -document.addEventListener('keydown', event => { - if (event.key === '/' && !event.ctrlKey && !event.metaKey && !event.altKey && !event.target.closest('input,textarea,[contenteditable="true"]')) { - event.preventDefault(); search.focus(); - } - if (event.key === 'Escape' && document.activeElement === search) { - search.value = ''; state.query = ''; render(); search.blur(); - } -}); -window.addEventListener('storage', event => { - if (event.key === storageKey || event.key === null) { state.favorites = readFavorites(); render(); } -}); +document.querySelectorAll('[data-theme]').forEach(button => button.addEventListener('click', () => applyTheme(button.dataset.theme))); +applyTheme(preferredTheme); render(); diff --git a/src/styles.css b/src/styles.css index 389d69a..3a4b7d0 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,9 +1 @@ -:root{font-family:Inter,"Microsoft YaHei","PingFang SC",system-ui,sans-serif;color:#202a3f;background:#f6f8fc;font-synthesis:none;--blue:#215ae8;--muted:#8b94a6;--line:#e9edf4} -*{box-sizing:border-box}body{margin:0}button,input,a{-webkit-tap-highlight-color:transparent}button,input{font:inherit}button,a{touch-action:manipulation}button{cursor:pointer}a{color:inherit;text-decoration:none}button{color:inherit}svg{width:22px;height:22px;display:block}button:focus-visible,a:focus-visible,input:focus-visible{outline:3px solid #7297ff;outline-offset:4px}button{border:0}button:hover{transition:background .15s,color .15s} [hidden]{display:none!important} -.skip-link{position:fixed;top:-100px;left:20px;z-index:20;background:white;padding:12px;border-radius:8px}.skip-link:focus{top:10px}.sidebar{width:224px;position:fixed;inset:0 auto 0 0;background:#fff;border-right:1px solid var(--line);display:flex;flex-direction:column;padding:32px 20px 22px}.brand{display:flex;align-items:center;gap:12px;margin:0 9px 48px}.brand-mark{display:grid;place-items:center;width:44px;height:44px;border-radius:13px;background:var(--blue);color:white;font-size:25px;font-weight:600;box-shadow:0 5px 12px #215ae826}.brand strong{font-size:21px;letter-spacing:3px;display:block;line-height:1.3}.brand small{display:block;font-size:11px;color:#8a94a8;letter-spacing:2px;margin-top:3px}.workspace-label{font-size:10px;letter-spacing:1.3px;color:#a3acbc;padding:0 13px;margin-bottom:18px}.workspace-label span{display:block;font-size:11px;letter-spacing:1px;margin-top:6px;color:#8c96a9}.nav-item{background:transparent;display:flex;width:100%;align-items:center;gap:12px;padding:14px 13px;margin-bottom:8px;border-radius:9px;color:#737e94;text-align:left;font-size:13px;min-height:48px}.nav-item svg{width:19px;height:19px;flex-shrink:0}.nav-item span{flex:1}.nav-item small{font-size:11px;color:#a6aebd}.nav-item:hover{background:#f5f7fc}.nav-item.active{color:var(--blue);background:#edf3ff;font-weight:600}.nav-item.active small{color:var(--blue)}.sidebar-bottom{border-top:1px solid var(--line);margin-top:auto;padding:22px 9px 0;display:flex;gap:10px;align-items:center}.small-mark{display:grid;place-items:center;background:#f2f5fa;color:#8594b0;border-radius:9px;width:33px;height:33px;font-weight:600;font-size:15px}.sidebar-bottom strong{font-size:11px;font-weight:500;display:block}.sidebar-bottom small{font-size:10px;color:#98a1b2;display:block;margin-top:5px}.page{margin-left:224px}.topbar{height:80px;border-bottom:1px solid var(--line);background:#ffffffd9;display:flex;align-items:center;justify-content:space-between;padding:0 44px}.breadcrumb{font-size:12px;color:#9aa3b3;display:flex;gap:16px}.breadcrumb span{color:#d3d9e3}.breadcrumb strong{font-weight:500;color:#4f5d77}.topbar-right{display:flex;align-items:center;gap:23px}.portal-indicator{font-size:11px;color:#8894a9}.avatar{width:32px;height:32px;display:grid;place-items:center;color:#5a73b0;background:#eef2fc;border-radius:50%;font-size:14px}main{max-width:1490px;margin:auto;padding:36px 44px 0;outline:0}.welcome{position:relative;overflow:hidden;min-height:224px;border:1px solid #dfe8fc;border-radius:16px;background:linear-gradient(105deg,#edf3ff 0%,#f0f5ff 58%,#e6edff 100%);display:flex;align-items:center;padding:32px 35px}.welcome-copy{position:relative;z-index:1}.eyebrow{font-size:10px;letter-spacing:2.7px;font-weight:600;color:#5477be;display:flex;align-items:center;gap:9px}.eyebrow>span{width:6px;height:6px;border-radius:50%;background:#5485ee}h1{font-size:29px;line-height:1.6;letter-spacing:1px;margin:12px 0 7px;font-weight:600;color:#203967}.welcome p{font-size:12px;color:#7a8ba8;margin:0}.hero-meta{display:flex;align-items:center;gap:14px;color:#8392ae;font-size:10px;margin-top:24px}.hero-meta strong{font-size:14px;font-weight:600;color:#476eb5;margin-right:3px}.hero-meta>i{height:10px;width:1px;background:#c8d5ee}.hero-art{position:absolute;right:12px;top:0;width:340px;height:240px;pointer-events:none}.orbit{position:absolute;border:1px solid #a6c0fb45;border-radius:50%;transform:rotate(-25deg)}.orbit-one{width:282px;height:172px;top:37px;left:25px}.orbit-two{width:330px;height:210px;top:19px;left:1px}.art-tile{position:absolute;display:grid;place-items:center;box-shadow:0 16px 24px #5c7cd421}.tile-one{width:88px;height:88px;border:1px solid #fff;background:linear-gradient(145deg,#fff,#e9f0ff);border-radius:23px;left:46px;top:69px;transform:rotate(-13deg);color:#376deb;font-size:28px;font-weight:600}.tile-one span{position:absolute;font-size:17px;right:12px;top:7px}.tile-two{width:98px;height:98px;background:linear-gradient(135deg,#568dff,#2457dc);border:1px solid #739aff;border-radius:26px;left:141px;top:89px;transform:rotate(12deg);color:#fff;font-size:51px;font-weight:600}.tile-three{width:52px;height:52px;background:#ffffffbc;border:1px solid #fff;border-radius:15px;left:211px;top:29px;transform:rotate(14deg);color:#6387e5;font-size:27px}.art-dot{position:absolute;border-radius:50%;background:#8caafa}.dot-one{width:7px;height:7px;top:42px;left:104px}.dot-two{width:5px;height:5px;top:181px;left:273px;background:#b7c9f3}.application-section{margin-top:36px}.section-heading{display:flex;align-items:center;justify-content:space-between;gap:20px}.section-heading h2{font-size:19px;margin:0 0 8px;font-weight:600;letter-spacing:.5px}.section-heading p{font-size:11px;color:#929cae;margin:0}.search-box{border:1px solid #e4e9f2;border-radius:8px;background:#fff;display:flex;align-items:center;height:40px;width:280px;padding:0 12px;gap:9px;flex-shrink:0}.search-box:focus-within{border-color:#799cf5;box-shadow:0 0 0 3px #215ae80c}.search-box svg{height:17px;width:17px;color:#9ba6ba}.search-box input{border:0;background:transparent;outline:0;width:100%;min-width:0;font-size:11px;color:#344362}.search-box input:focus-visible{outline:0}.search-box input::placeholder{color:#a6aebc}.search-box kbd{border:1px solid #e6eaf1;border-radius:4px;font-size:11px;width:20px;text-align:center;color:#abb3c1;line-height:20px}.filter-row{display:flex;justify-content:space-between;align-items:center;margin:24px 0 20px;gap:16px}.environment-filters{display:flex;gap:6px}.filter-button{border-radius:6px;font-size:11px;padding:8px 14px;background:transparent;color:#8b95a8;min-height:34px}.filter-button:hover{background:#eaf0fb}.filter-button.active{background:#e8efff;color:#3160cf;font-weight:600}.result-count{font-size:11px;color:#9aa3b3}.app-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:18px}.app-card{position:relative;min-width:0;border:1px solid #e7ebf3;background:#fff;border-radius:12px;padding:22px 22px 0;transition:transform .18s,box-shadow .18s,border-color .18s}.app-card:hover{transform:translateY(-3px);border-color:#c5d6fc;box-shadow:0 10px 25px #1d4f9c0b}.card-top{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:18px}.app-icon{width:44px;height:44px;display:grid;place-items:center;border-radius:12px}.app-icon svg{width:25px;height:25px;stroke-width:1.55}.blue{background:#edf3ff;color:#3976f6}.violet{background:#f2edff;color:#8a66dc}.teal{background:#e9f8f5;color:#31a996}.amber{background:#fff5e6;color:#d9a147}.favorite-button{position:relative;z-index:2;padding:9px;margin:-7px -7px 0 0;background:transparent;border-radius:7px;color:#cbd2df;line-height:0}.favorite-button svg{width:17px;height:17px}.favorite-button:hover{color:#d5a442;background:#fff8eb}.favorite-button.selected{color:#dda945}.favorite-button.selected svg{fill:#f6d99c}.app-card h3{font-size:14px;font-weight:600;color:#2c3953;margin:0 0 8px;line-height:1.5}.app-link::after{content:"";position:absolute;inset:0;border-radius:12px;z-index:1}.app-link:focus-visible{outline:0}.app-card:has(.app-link:focus-visible){outline:3px solid #7297ff;outline-offset:3px}.app-description{font-size:11px;color:#9aa3b4;margin:0;line-height:1.8}.card-tags{display:flex;align-items:center;gap:8px;margin-top:17px;margin-bottom:19px;font-size:9px;height:20px}.category-tag{background:#f5f7fa;color:#8b96a9;border-radius:4px;padding:4px 7px}.test-tag{color:#c4953f;background:#fff8e9;border-radius:4px;padding:4px 7px}.production-tag{display:flex;align-items:center;gap:4px;color:#96a3b4;font-size:9px}.production-tag i{height:4px;width:4px;background:#68bfa3;border-radius:50%}.card-bottom{border-top:1px solid #f0f2f7;display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:44px}.app-domain{font-size:10px;letter-spacing:.1px;color:#a2abbc;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.launch-icon{color:#a9b6ce;flex-shrink:0}.launch-icon svg{width:17px;height:17px}.app-card:hover .launch-icon{color:var(--blue)}.footer{padding:30px 0 25px;display:flex;justify-content:space-between;color:#aeb6c4;font-size:10px;letter-spacing:.4px}.empty-state{text-align:center;padding:65px 20px 80px;background:#fff;border:1px dashed #dfe5f0;border-radius:12px}.empty-state>div{background:#f1f5fe;border-radius:50%;width:52px;height:52px;display:grid;place-items:center;color:#8fa6da;margin:0 auto 18px}.empty-state h3{font-size:15px;font-weight:500}.empty-state p{font-size:12px;color:#929caf;line-height:1.8}.reset-button{font-size:12px;margin-top:12px;padding:10px 18px;background:#edf3ff;color:var(--blue);border-radius:7px}.toast{position:fixed;bottom:30px;left:calc(50% + 112px);transform:translate(-50%,12px);padding:12px 20px;border-radius:10px;background:#25334beb;color:#fff;font-size:12px;box-shadow:0 5px 20px #111a3026;opacity:0;pointer-events:none;transition:opacity .2s,transform .2s;z-index:10;max-width:90vw;text-align:center}.toast.visible{opacity:1;transform:translate(-50%,0)}.noscript{position:fixed;bottom:0;background:#fff1d4;padding:20px;width:100%;z-index:20;text-align:center} -.skip-link{top:10px;clip-path:inset(50%);width:1px;height:1px;overflow:hidden;padding:0}.skip-link:focus{clip-path:none;width:auto;height:auto;overflow:visible;padding:12px} -@media(min-width:1600px){.app-grid{grid-template-columns:repeat(4,minmax(0,1fr))}.hero-art{right:60px}} -@media(max-width:1150px){.sidebar{width:194px;padding-left:14px;padding-right:14px}.page{margin-left:194px}.topbar{padding:0 28px}main{padding:28px 28px 0}.app-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.hero-art{opacity:.7;right:-32px}h1{font-size:25px}.toast{left:calc(50% + 97px)}} -@media(max-width:760px){.sidebar{position:static;width:auto;padding:18px 20px 0;border-right:0;border-bottom:1px solid var(--line)}.brand{margin:0 0 18px;gap:9px}.brand-mark{width:34px;height:34px;border-radius:10px;font-size:20px}.brand strong{font-size:18px}.brand small{font-size:9px;letter-spacing:1.5px}.workspace-label,.sidebar-bottom{display:none}#category-nav{display:flex;gap:5px;overflow-x:auto;padding:0 0 10px;scrollbar-width:none}#category-nav::-webkit-scrollbar{display:none}.nav-item{width:auto;white-space:nowrap;font-size:12px;padding:10px 12px;min-height:40px;margin:0;gap:7px;flex-shrink:0}.nav-item small{font-size:10px}.nav-item svg{height:16px;width:16px}.page{margin-left:0}.topbar{height:49px;padding:0 22px}.breadcrumb{font-size:10px;gap:10px}.portal-indicator{font-size:10px}.avatar{display:none}.topbar-right{gap:0}main{padding:20px 20px 0}.welcome{padding:25px 23px;min-height:196px;border-radius:12px}.eyebrow{font-size:8px;letter-spacing:2px}h1{font-size:23px;letter-spacing:0;margin-top:14px}.welcome p{font-size:11px}.hero-meta{font-size:9px;margin-top:21px;gap:10px}.hero-art{opacity:.25;right:-112px;transform:scale(.85);transform-origin:center}.welcome-copy{z-index:1}.section-heading{display:block}.application-section{margin-top:27px}.section-heading h2{font-size:17px}.section-heading p{font-size:10px}.search-box{margin-top:20px;width:100%;height:42px}.search-box input{font-size:12px}.search-box kbd{display:none}.filter-row{margin:17px 0 15px;gap:8px}.filter-button{font-size:10px;padding:8px 10px}.environment-filters{gap:3px}.result-count{font-size:10px}.app-grid{gap:12px}.app-card{padding:16px 15px 0;border-radius:10px}.card-top{margin-bottom:14px}.app-icon{width:37px;height:37px;border-radius:10px}.app-icon svg{width:22px;height:22px}.app-card h3{font-size:12px;min-height:36px;margin-bottom:4px}.app-description{font-size:10px;line-height:1.7;min-height:34px}.card-tags{margin:13px 0 15px;gap:5px}.app-domain{font-size:8px;letter-spacing:0}.card-bottom{min-height:38px;gap:3px}.launch-icon svg{width:14px;height:14px}.footer{font-size:9px;padding-top:25px}.toast{left:50%}.favorite-button{padding:10px}.favorite-button svg{width:16px;height:16px}} -@media(max-width:360px){main{padding-left:14px;padding-right:14px}.sidebar{padding-left:14px;padding-right:14px}.welcome{padding-left:18px}h1{font-size:21px}.app-grid{grid-template-columns:1fr}.app-card h3,.app-description{min-height:0}.app-domain{font-size:10px}} -@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}} +:root{font-family:"Segoe UI","PingFang SC","Microsoft YaHei",system-ui,sans-serif;color:#23395e;background:#f5f8fc;font-synthesis:none;--page:#f5f8fc;--header:#fff;--card:rgba(255,255,255,.92);--text:#223a60;--muted:#70809d;--line:#d7def1;--blue:#3978f6;--purple:#8057e8;--soft:#edf2ff;--shadow:0 14px 34px rgba(75,99,160,.1)}:root[data-theme=dark]{--page:#070b17;--header:#080c18;--card:rgba(13,20,37,.94);--text:#eef2ff;--muted:#91a0bf;--line:#2b3962;--soft:#18234a;--shadow:0 16px 40px rgba(0,0,0,.25)}*{box-sizing:border-box}html,body{min-height:100%;margin:0}body{background:var(--page);color:var(--text);-webkit-font-smoothing:antialiased;transition:background .25s,color .25s}a{color:inherit;text-decoration:none}button{font:inherit;cursor:pointer}button:focus-visible,a:focus-visible{outline:2px solid #7395ff;outline-offset:3px}.skip-link{position:fixed;z-index:10;top:10px;left:12px;clip-path:inset(50%);width:1px;height:1px;overflow:hidden;background:var(--card);padding:10px;border-radius:8px}.skip-link:focus{clip-path:none;width:auto;height:auto;overflow:visible}.app-shell{position:relative;min-height:100vh;background:var(--page);overflow:hidden}.background-grid{position:fixed;inset:86px 0 0;pointer-events:none;background-image:linear-gradient(rgba(108,139,218,.08) 1px,transparent 1px),linear-gradient(90deg,rgba(108,139,218,.08) 1px,transparent 1px);background-size:48px 48px;opacity:.65;mask-image:linear-gradient(180deg,#000,transparent 88%)}:root[data-theme=dark] .background-grid{opacity:.42}.header{position:relative;z-index:2;height:86px;display:flex;align-items:center;justify-content:space-between;padding:0 32px;border-bottom:1px solid var(--line);background:var(--header);transition:background .25s,border-color .25s}.brand{display:flex;align-items:center;gap:13px}.brand-logo{display:grid;place-items:center;width:44px;height:44px;border-radius:12px;border:1px solid #7a8dff;background:linear-gradient(135deg,#4a80fb,#8058e8);color:#fff;font-size:25px;box-shadow:0 7px 20px rgba(75,111,240,.24)}.brand strong{display:block;font-size:17px;letter-spacing:.4px;color:var(--text)}.brand small{display:block;margin-top:4px;color:var(--muted);font-size:10px}.nav{display:flex;align-items:center;gap:24px}.nav-icon{width:32px;height:32px;border:0;background:transparent;color:var(--muted);font-size:20px;line-height:1}.nav-icon:hover{color:var(--text)}.nav-link{padding:10px 16px;border:1px solid transparent;border-radius:999px;color:var(--muted);font-size:13px;white-space:nowrap}.nav-link:hover{color:var(--text)}.nav-link.active{border-color:#8b9dff;background:var(--soft);color:var(--text);box-shadow:0 8px 20px rgba(73,95,210,.1)}.theme-switch{display:flex;align-items:center;gap:2px;padding:3px;border:1px solid var(--line);border-radius:999px;background:var(--soft)}.theme-button{display:grid;place-items:center;width:28px;height:26px;padding:0;border:0;border-radius:999px;background:transparent;color:var(--muted);font-size:16px;line-height:1}.theme-button.active{background:var(--card);color:var(--text);box-shadow:0 2px 7px rgba(50,70,130,.14)}.main{position:relative;z-index:1;max-width:1380px;margin:0 auto;padding:0 40px 20px}.intro{padding:42px 0 34px}.eyebrow{margin:0 0 12px;color:#5c68d8;font-size:12px;font-weight:700;letter-spacing:1.2px}.intro h1{margin:0;color:var(--text);font-size:32px;line-height:1.25;letter-spacing:.2px}.intro-description{margin:10px 0 0;color:var(--muted);font-size:14px}.agent-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:20px}.agent-card{position:relative;min-height:274px;display:flex;flex-direction:column;padding:22px 20px 19px;border:1px solid var(--line);border-radius:14px;background:var(--card);box-shadow:var(--shadow);transition:transform .2s,border-color .2s,box-shadow .2s,background .25s}.agent-card:hover{transform:translateY(-3px);border-color:#899eff;box-shadow:0 16px 34px rgba(62,91,170,.17)}.agent-card--violet{border-top:3px solid #8d67eb}.agent-card--blue{border-top:3px solid #4d80f5}.agent-card__top{display:flex;align-items:center;justify-content:space-between}.agent-card__icon{display:grid;place-items:center;width:43px;height:43px;border:1px solid #b3c2f1;border-radius:11px;background:#eef2ff;color:#3978f6}.agent-card--violet .agent-card__icon{border-color:#c5b9f0;background:#f1edff;color:#8057e8}.agent-card__icon svg{width:24px;height:24px}.agent-card__category{padding:5px 9px;border-radius:999px;background:var(--soft);color:#656bd2;font-size:10px}.agent-card__title{margin:22px 0 10px;color:var(--text);font-size:19px;line-height:1.35}.agent-card__guide{display:inline-flex;align-items:center;margin-left:6px;padding:3px 6px;border:1px solid #8aa1e3;border-radius:999px;color:#5070bb;background:var(--soft);font-size:9px;vertical-align:middle;white-space:nowrap}.agent-card__description{min-height:45px;margin:0;color:var(--muted);font-size:12px;line-height:1.75}.agent-card__meta{display:flex;align-items:center;gap:7px;margin-top:18px;color:var(--muted);font-size:11px}.ready-dot{width:7px;height:7px;border-radius:50%;background:#31b97a;box-shadow:0 0 7px rgba(49,185,122,.55)}.agent-card__index{margin-left:auto;color:#8b97b4;font-size:10px;letter-spacing:1px}.agent-card__button{display:flex;align-items:center;justify-content:center;gap:8px;min-height:42px;margin-top:17px;border-radius:10px;background:linear-gradient(100deg,#3978f6,#8057e8);color:#fff;font-size:13px;font-weight:600;box-shadow:0 7px 16px rgba(91,95,232,.2);transition:filter .2s,transform .2s}.agent-card__button svg{width:17px;height:17px;transition:transform .2s}.agent-card__button:hover{filter:brightness(1.07)}.agent-card__button:hover svg{transform:translateX(4px)}.agent-card__external{position:absolute;right:20px;top:78px;color:#a1acc4}.agent-card__external svg{width:15px;height:15px}.empty-state{padding:58px 20px;border:1px dashed var(--line);border-radius:14px;color:var(--muted);text-align:center}.footer{display:flex;justify-content:space-between;padding:42px 0 12px;color:var(--muted);font-size:11px;letter-spacing:.3px}.toast{position:fixed;z-index:10;left:50%;bottom:26px;padding:11px 18px;border:1px solid #8195de;border-radius:9px;background:var(--card);color:var(--text);box-shadow:var(--shadow);opacity:0;transform:translate(-50%,10px);pointer-events:none;transition:opacity .2s,transform .2s}.toast.visible{opacity:1;transform:translate(-50%,0)}@media(max-width:1100px){.agent-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.nav{gap:10px}.nav-link{padding:9px 11px}.main{padding:0 28px 20px}}@media(max-width:700px){.header{height:auto;min-height:76px;align-items:flex-start;padding:16px 16px;gap:14px}.brand-logo{width:37px;height:37px}.brand strong{font-size:14px}.brand small{font-size:9px}.nav{flex-wrap:wrap;justify-content:flex-end;gap:3px}.nav-icon{display:none}.nav-link{padding:6px 8px;font-size:10px}.nav-link[href="#stats"],.nav-link[href="#feedback"]{display:none}.theme-switch{margin-left:4px}.theme-button{width:25px;height:23px;font-size:14px}.main{padding:0 16px 16px}.intro{padding:31px 0 25px}.eyebrow{font-size:10px}.intro h1{font-size:27px}.intro-description{font-size:12px}.agent-grid{grid-template-columns:1fr;gap:14px}.agent-card{min-height:252px;padding:20px 18px 17px}.agent-card__title{font-size:18px}.footer{padding-top:28px;font-size:10px}}@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important}} diff --git a/tests/apps.test.mjs b/tests/apps.test.mjs index 1cd773a..c3818d8 100644 --- a/tests/apps.test.mjs +++ b/tests/apps.test.mjs @@ -1,37 +1,29 @@ import test from 'node:test'; import assert from 'node:assert/strict'; -import { applications, filterApplications, validateApplications } from '../src/apps.mjs'; +import { applications, categories, filterApplications, validateApplications } from '../src/apps.mjs'; -test('八个业务地址与用户给定映射一致,正式和测试风险入口不混淆', () => { - const expected = { - '碟雅运营AI中心': 'https://aiagent.dieya.net.cn/', - '碟雅AI中心测试版': 'https://aiagent1.dieya.net.cn/', - '需求收集': 'http://demand.dieya.net.cn/', - '财务AI助手测试': 'http://finance-test.dieya.net.cn/', - '成本识别': 'http://cost.dieya.net.cn/', - '违禁词助手': 'http://prohibite.dieya.net.cn/', - '风险预警测试': 'http://risk.dieya.net.cn/', - '风险预警': 'http://risk-prod.dieya.net.cn/' - }; - assert.deepEqual(Object.fromEntries(applications.map(app => [app.name, app.url])), expected); +test('门户只展示产品部两个正式应用', () => { + assert.deepEqual(Object.fromEntries(applications.map(app => [app.name, app.url])), { + '风险预警': 'http://risk-prod.dieya.net.cn/', + '成本识别': 'http://cost.dieya.net.cn/' + }); + assert.equal(applications.length, 2); + assert.equal(applications.every(app => app.category === 'product' && app.environment === 'production'), true); + assert.equal(applications.some(app => /测试|运营|需求|财务|违禁|制度/.test(app.name)), false); + assert.deepEqual(categories.map(category => category.id), ['all', 'product', 'favorites']); assert.equal(validateApplications(applications), true); - assert.equal(filterApplications(applications, { environment: 'production' }).length, 5); - assert.equal(filterApplications(applications, { environment: 'test' }).length, 3); }); -test('分类、中文或域名搜索和环境条件同时生效', () => { - assert.deepEqual(filterApplications(applications, { category: 'risk', environment: 'production' }).map(app => app.id), ['risk']); - assert.equal(filterApplications(applications, { query: ' 风险预警 ' }).length, 2); - assert.deepEqual(filterApplications(applications, { query: 'AIAGENT1.DIEYA' }).map(app => app.id), ['ai-center-test']); - assert.equal(filterApplications(applications, { query: '不存在的应用' }).length, 0); -}); -test('常用列表与环境和关键词组合筛选', () => { - const options = { category: 'favorites', favorites: ['cost', 'finance-test'] }; - assert.equal(filterApplications(applications, options).length, 2); - assert.deepEqual(filterApplications(applications, { ...options, environment: 'test', query: '财务' }).map(app => app.id), ['finance-test']); + +test('分类、关键词和常用筛选同时生效', () => { + assert.deepEqual(filterApplications(applications, { category: 'product' }).map(app => app.id), ['risk', 'cost']); + assert.deepEqual(filterApplications(applications, { query: '成本' }).map(app => app.id), ['cost']); + assert.deepEqual(filterApplications(applications, { query: 'RISK-PROD' }).map(app => app.id), ['risk']); + assert.deepEqual(filterApplications(applications, { category: 'favorites', favorites: ['cost'] }).map(app => app.id), ['cost']); assert.equal(filterApplications(applications, { category: 'favorites', favorites: [] }).length, 0); }); -test('拒绝非网页链接、重复标识和不完整配置', () => { + +test('拒绝不安全地址、重复标识和测试入口配置', () => { assert.throws(() => validateApplications([{ ...applications[0], url: 'javascript:alert(1)' }])); assert.throws(() => validateApplications([applications[0], applications[0]])); - assert.throws(() => validateApplications([{ ...applications[0], environment: 'unknown' }])); + assert.throws(() => validateApplications([{ ...applications[0], environment: 'test' }])); });