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中心
- 统一应用门户碟
+ 跳转到智能体列表
+
+
+
-
-
- DIEYA WORKSPACE
-
每一份效率,都有入口。
-
连接 AI 与业务,让常用应用触手可及。
-
8 个应用一个入口,开启工作
-
-
+
+
+ PRODUCT AI CENTER
+ 选择智能体
+ 选择需要使用的产品工具,立即开始工作。
-
-
-
-
- 没有找到匹配的应用
试试其他关键词,或调整筛选条件。
-
-
+
+
+
-
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.description)}
- ${escapeHtml(categoryName)}${app.environment === 'test' ? '测试' : '正式'}
- ${escapeHtml(new URL(app.url).hostname)}${svg('arrow')}
- `;
- }).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')}
+ ${svg('external')}
+ `).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' }]));
});