chore: initial commit

This commit is contained in:
hailin
2026-05-18 13:52:47 +08:00
commit 0753129afe
148 changed files with 14202 additions and 0 deletions

View File

@@ -0,0 +1,252 @@
---
name: implement-design
description: Translates Figma designs into production-ready code with 1:1 visual fidelity. Use when implementing UI from Figma files, when user mentions "implement design", "generate code", "implement component", "build Figma design", provides Figma URLs, or asks to build components matching Figma specs. Requires Figma MCP server connection.
metadata:
mcp-server: figma, figma-desktop
---
# Implement Design
## Overview
This skill provides a structured workflow for translating Figma designs into production-ready code with pixel-perfect accuracy. It ensures consistent integration with the Figma MCP server, proper use of design tokens, and 1:1 visual parity with designs.
## Prerequisites
- Figma MCP server must be connected and accessible
- User must provide a Figma URL in the format: `https://figma.com/design/:fileKey/:fileName?node-id=1-2`
- `:fileKey` is the file key
- `1-2` is the node ID (the specific component or frame to implement)
- **OR** when using `figma-desktop` MCP: User can select a node directly in the Figma desktop app (no URL required)
- Project should have an established design system or component library (preferred)
## Required Workflow
**Follow these steps in order. Do not skip steps.**
### Step 1: Get Node ID
#### Option A: Parse from Figma URL
When the user provides a Figma URL, extract the file key and node ID to pass as arguments to MCP tools.
**URL format:** `https://figma.com/design/:fileKey/:fileName?node-id=1-2`
**Extract:**
- **File key:** `:fileKey` (the segment after `/design/`)
- **Node ID:** `1-2` (the value of the `node-id` query parameter)
**Note:** When using the local desktop MCP (`figma-desktop`), `fileKey` is not passed as a parameter to tool calls. The server automatically uses the currently open file, so only `nodeId` is needed.
**Example:**
- URL: `https://figma.com/design/kL9xQn2VwM8pYrTb4ZcHjF/DesignSystem?node-id=42-15`
- File key: `kL9xQn2VwM8pYrTb4ZcHjF`
- Node ID: `42-15`
#### Option B: Use Current Selection from Figma Desktop App (figma-desktop MCP only)
When using the `figma-desktop` MCP and the user has NOT provided a URL, the tools automatically use the currently selected node from the open Figma file in the desktop app.
**Note:** Selection-based prompting only works with the `figma-desktop` MCP server. The remote server requires a link to a frame or layer to extract context. The user must have the Figma desktop app open with a node selected.
### Step 2: Fetch Design Context
Run `get_design_context` with the extracted file key and node ID.
```
get_design_context(fileKey=":fileKey", nodeId="1-2")
```
This provides the structured data including:
- Layout properties (Auto Layout, constraints, sizing)
- Typography specifications
- Color values and design tokens
- Component structure and variants
- Spacing and padding values
**If the response is too large or truncated:**
1. Run `get_metadata(fileKey=":fileKey", nodeId="1-2")` to get the high-level node map
2. Identify the specific child nodes needed from the metadata
3. Fetch individual child nodes with `get_design_context(fileKey=":fileKey", nodeId=":childNodeId")`
### Step 3: Capture Visual Reference
Run `get_screenshot` with the same file key and node ID for a visual reference.
```
get_screenshot(fileKey=":fileKey", nodeId="1-2")
```
This screenshot serves as the source of truth for visual validation. Keep it accessible throughout implementation.
### Step 4: Download Required Assets
Download any assets (images, icons, SVGs) returned by the Figma MCP server.
**IMPORTANT:** Follow these asset rules:
- If the Figma MCP server returns a `localhost` source for an image or SVG, use that source directly
- DO NOT import or add new icon packages - all assets should come from the Figma payload
- DO NOT use or create placeholders if a `localhost` source is provided
- Assets are served through the Figma MCP server's built-in assets endpoint
### Step 5: Translate to Project Conventions
Translate the Figma output into this project's framework, styles, and conventions.
**Key principles:**
- Treat the Figma MCP output (typically React + Tailwind) as a representation of design and behavior, not as final code style
- Replace Tailwind utility classes with the project's preferred utilities or design system tokens
- Reuse existing components (buttons, inputs, typography, icon wrappers) instead of duplicating functionality
- Use the project's color system, typography scale, and spacing tokens consistently
- Respect existing routing, state management, and data-fetch patterns
### Step 6: Achieve 1:1 Visual Parity
Strive for pixel-perfect visual parity with the Figma design.
**Guidelines:**
- Prioritize Figma fidelity to match designs exactly
- Avoid hardcoded values - use design tokens from Figma where available
- When conflicts arise between design system tokens and Figma specs, prefer design system tokens but adjust spacing or sizes minimally to match visuals
- Follow WCAG requirements for accessibility
- Add component documentation as needed
### Step 7: Validate Against Figma
Before marking complete, validate the final UI against the Figma screenshot.
**Validation checklist:**
- [ ] Layout matches (spacing, alignment, sizing)
- [ ] Typography matches (font, size, weight, line height)
- [ ] Colors match exactly
- [ ] Interactive states work as designed (hover, active, disabled)
- [ ] Responsive behavior follows Figma constraints
- [ ] Assets render correctly
- [ ] Accessibility standards met
## Implementation Rules
### Component Organization
- Place UI components in the project's designated design system directory
- Follow the project's component naming conventions
- Avoid inline styles unless truly necessary for dynamic values
### Design System Integration
- ALWAYS use components from the project's design system when possible
- Map Figma design tokens to project design tokens
- When a matching component exists, extend it rather than creating a new one
- Document any new components added to the design system
### Code Quality
- Avoid hardcoded values - extract to constants or design tokens
- Keep components composable and reusable
- Add TypeScript types for component props
- Include JSDoc comments for exported components
## Examples
### Example 1: Implementing a Button Component
User says: "Implement this Figma button component: https://figma.com/design/kL9xQn2VwM8pYrTb4ZcHjF/DesignSystem?node-id=42-15"
**Actions:**
1. Parse URL to extract fileKey=`kL9xQn2VwM8pYrTb4ZcHjF` and nodeId=`42-15`
2. Run `get_design_context(fileKey="kL9xQn2VwM8pYrTb4ZcHjF", nodeId="42-15")`
3. Run `get_screenshot(fileKey="kL9xQn2VwM8pYrTb4ZcHjF", nodeId="42-15")` for visual reference
4. Download any button icons from the assets endpoint
5. Check if project has existing button component
6. If yes, extend it with new variant; if no, create new component using project conventions
7. Map Figma colors to project design tokens (e.g., `primary-500`, `primary-hover`)
8. Validate against screenshot for padding, border radius, typography
**Result:** Button component matching Figma design, integrated with project design system.
### Example 2: Building a Dashboard Layout
User says: "Build this dashboard: https://figma.com/design/pR8mNv5KqXzGwY2JtCfL4D/Dashboard?node-id=10-5"
**Actions:**
1. Parse URL to extract fileKey=`pR8mNv5KqXzGwY2JtCfL4D` and nodeId=`10-5`
2. Run `get_metadata(fileKey="pR8mNv5KqXzGwY2JtCfL4D", nodeId="10-5")` to understand the page structure
3. Identify main sections from metadata (header, sidebar, content area, cards) and their child node IDs
4. Run `get_design_context(fileKey="pR8mNv5KqXzGwY2JtCfL4D", nodeId=":childNodeId")` for each major section
5. Run `get_screenshot(fileKey="pR8mNv5KqXzGwY2JtCfL4D", nodeId="10-5")` for the full page
6. Download all assets (logos, icons, charts)
7. Build layout using project's layout primitives
8. Implement each section using existing components where possible
9. Validate responsive behavior against Figma constraints
**Result:** Complete dashboard matching Figma design with responsive layout.
## Best Practices
### Always Start with Context
Never implement based on assumptions. Always fetch `get_design_context` and `get_screenshot` first.
### Incremental Validation
Validate frequently during implementation, not just at the end. This catches issues early.
### Document Deviations
If you must deviate from the Figma design (e.g., for accessibility or technical constraints), document why in code comments.
### Reuse Over Recreation
Always check for existing components before creating new ones. Consistency across the codebase is more important than exact Figma replication.
### Design System First
When in doubt, prefer the project's design system patterns over literal Figma translation.
## Common Issues and Solutions
### Issue: Figma output is truncated
**Cause:** The design is too complex or has too many nested layers to return in a single response.
**Solution:** Use `get_metadata` to get the node structure, then fetch specific nodes individually with `get_design_context`.
### Issue: Design doesn't match after implementation
**Cause:** Visual discrepancies between the implemented code and the original Figma design.
**Solution:** Compare side-by-side with the screenshot from Step 3. Check spacing, colors, and typography values in the design context data.
### Issue: Assets not loading
**Cause:** The Figma MCP server's assets endpoint is not accessible or the URLs are being modified.
**Solution:** Verify the Figma MCP server's assets endpoint is accessible. The server serves assets at `localhost` URLs. Use these directly without modification.
### Issue: Design token values differ from Figma
**Cause:** The project's design system tokens have different values than those specified in the Figma design.
**Solution:** When project tokens differ from Figma values, prefer project tokens for consistency but adjust spacing/sizing to maintain visual fidelity.
## Understanding Design Implementation
The Figma implementation workflow establishes a reliable process for translating designs to code:
**For designers:** Confidence that implementations will match their designs with pixel-perfect accuracy.
**For developers:** A structured approach that eliminates guesswork and reduces back-and-forth revisions.
**For teams:** Consistent, high-quality implementations that maintain design system integrity.
By following this workflow, you ensure that every Figma design is implemented with the same level of care and attention to detail.
## Additional Resources
- [Figma MCP Server Documentation](https://developers.figma.com/docs/figma-mcp-server/)
- [Figma MCP Server Tools and Prompts](https://developers.figma.com/docs/figma-mcp-server/tools-and-prompts/)
- [Figma Variables and Design Tokens](https://help.figma.com/hc/en-us/articles/15339657135383-Guide-to-variables-in-Figma)

5
.claude/settings.json Normal file
View File

@@ -0,0 +1,5 @@
{
"enabledPlugins": {
"andrej-karpathy-skills@karpathy-skills": true
}
}

18
.cursorrules Normal file
View File

@@ -0,0 +1,18 @@
# Role
你是一名精通网页开发的高级工程师拥有20年的前端开发经验。你的任务是帮助一位不太懂技术的初中生用户完成网页的开发。你的工作对用户来说非常重要完成后将获得 10000美元奖励。
# Goal
你的目标是以用户容易理解的方式帮助他们完成网页的设计和开发工作。你应该主动完成所有工作,而不是等待用户多次推动你。
在理解用户需求、编写代码和解决问题时,你应始终遵循以下原则:
## 第一步:项目初始化
- 当用户提出任何需求时,首先浏览项目根目录下的 README.md 文件和所有代码文档,理解项目目标、架构和实现方式。
- 如果还没有 README 文件,创建一个。这个文件将作为项目功能的说明书 和你对项目内容的规划。
- 在README.md 中清晰描述所有页面的用途、布局结构、样式说明等,确保用户可以轻松理解网页的结构和样式。
## 第二步:需求分析和开发
### 理解用户需求时:
- 充分理解用户需求,站在用户角度思考。
- 作为产品经理,分析需求是否存在缺漏,与用户讨论并完善需求。
- 选择最简单的解决方案来满足用户需求。

25
.gitignore vendored Normal file
View File

@@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.claude/settings.local.json
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

236
AGENTS.md Normal file
View File

@@ -0,0 +1,236 @@
# Agent Guidelines for Data Index Report App
## Project Overview
This is a React + TypeScript + Vite application for generating data analysis reports with PDF export functionality. The app includes data visualization components (ECharts), Excel import/export, and PDF generation.
## Build, Lint, and Test Commands
### Development
```bash
npm run dev # Start development server
npm run preview # Preview production build
```
### Build
```bash
npm run build # Build for production (runs TypeScript compilation + Vite build)
```
### Linting
```bash
npm run lint # Run ESLint on all files
```
### Type Checking
```bash
npx tsc --noEmit # Type check without emitting files
```
### Single File Operations
```bash
# Lint specific file
npx eslint src/components/ComponentName.tsx
# Type check specific file
npx tsc --noEmit src/components/ComponentName.tsx
```
## Code Style Guidelines
### Imports Order
1. React and external library imports
2. Internal component imports
3. Type imports
4. Utility/helper imports
5. Style/constant imports
Example:
```typescript
import { useRef, useState } from 'react';
import html2pdf from 'html2pdf.js';
import { Download, Upload } from 'lucide-react';
import { CoverPage } from './components/CoverPage';
import { defaultReportData } from './data/defaultData';
import { parseExcelReportData } from './utils/excelUtils';
import type { ReportData } from './types/data';
```
### TypeScript Configuration
- Strict mode enabled (`"strict": true`)
- No unused locals/parameters enforced
- Use explicit type annotations for function parameters and return types
- Prefer `interface` over `type` for object shapes
- Use `type` imports for type-only imports
### Naming Conventions
- **Components**: PascalCase (e.g., `Page02Industry`, `ChinaMap`)
- **Files**: PascalCase for components, camelCase for utilities
- **Variables/Functions**: camelCase
- **Constants**: UPPER_SNAKE_CASE for true constants, camelCase for exported constants
- **Types/Interfaces**: PascalCase with `I` prefix optional (not used in this codebase)
- **Props Interfaces**: `ComponentNameProps` pattern
### Component Structure
1. Import statements
2. Interface/type definitions
3. Component function with typed props
4. Internal helper functions
5. JSX return with proper className organization
Example component pattern:
```typescript
interface ComponentProps {
data: SomeType[];
onAction?: () => void;
}
export const Component = ({ data, onAction }: ComponentProps) => {
// State and hooks
const [state, setState] = useState<StateType>(initialValue);
// Helper functions
const processData = () => {
// Implementation
};
return (
<div className="container-class">
{/* JSX content */}
</div>
);
};
```
### Error Handling
- Use try/catch for async operations
- Provide user feedback for errors (status messages, UI indicators)
- Log errors to console for debugging
- Use TypeScript's strict null checking
### Styling (Tailwind CSS)
- Use Tailwind utility classes exclusively
- Organize classes logically: layout → typography → colors → effects
- Use responsive prefixes (sm:, md:, lg:) when needed
- Custom colors defined in `tailwind.config.js`
- Use `@apply` sparingly in CSS files only
Example class organization:
```jsx
<div className="
flex flex-col /* Layout */
p-4 space-y-4 /* Spacing */
bg-white rounded-lg /* Appearance */
shadow-md /* Effects */
hover:shadow-lg /* Interactive */
">
```
### File Organization
- `src/components/` - React components
- `src/types/` - TypeScript type definitions
- `src/utils/` - Utility functions
- `src/data/` - Default data and constants
- `src/constants.ts` - Application constants
- `public/` - Static assets
### React Patterns
- Use functional components with hooks
- Prefer explicit prop typing over `any`
- Use `useState` for local state
- Use `useRef` for DOM references
- Extract complex logic into custom hooks when reusable
- Use `React.memo` for performance optimization when needed
### ECharts Integration
- Chart components should accept typed data props
- Use responsive chart configurations
- Include proper error handling for missing data
- Export charts as reusable components
### PDF Generation
- Use `html2pdf.js` for PDF export
- Handle width locking during PDF generation
- Provide proper error feedback
- Use `@ts-expect-error` for library type issues when necessary
### Excel Integration
- Use `xlsx` library for Excel operations
- Provide template generation functions
- Validate imported data structure
- Handle file upload errors gracefully
## Development Workflow
### Adding New Components
1. Create component file in `src/components/`
2. Define TypeScript interfaces for props
3. Implement component with Tailwind styling
4. Export component as named export
5. Add to appropriate parent component
### Adding New Types
1. Add to existing interface in `src/types/data.ts` or create new file
2. Use descriptive property names
3. Include JSDoc comments for complex types
4. Export types for reuse
### Adding Utilities
1. Create file in `src/utils/`
2. Write pure functions with typed parameters
3. Include error handling
4. Export functions as named exports
### Testing Changes
1. Run `npm run lint` to check code style
2. Run `npx tsc --noEmit` for type checking
3. Test component in development server
4. Verify PDF generation still works
5. Test Excel import/export functionality
## Common Patterns in Codebase
### Data Processing
```typescript
// Map data aggregation pattern
const aggregatedData = data.reduce((acc, item) => {
const key = item.name;
acc[key] = (acc[key] || 0) + item.value;
return acc;
}, {} as Record<string, number>);
```
### Component Props
```typescript
// Optional props with defaults
interface Props {
data: DataType[];
title?: string;
className?: string;
}
const Component = ({
data,
title = 'Default Title',
className = ''
}: Props) => {
// Implementation
};
```
### State Management
```typescript
// Status state pattern
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
// Data state with default
const [reportData, setReportData] = useState<ReportData>(defaultReportData);
```
## Notes for Agents
- This is a Chinese-language application (UI text in Chinese)
- Focus on data visualization and report generation
- Maintain print-friendly styling for PDF export
- Ensure all interactive elements have `no-print` class when needed
- Follow existing patterns for consistency
- Test PDF generation after UI changes
- Verify Excel template compatibility after data structure changes

99
FIX_SUMMARY.md Normal file
View File

@@ -0,0 +1,99 @@
# 生成报告按钮失效修复总结
## 问题分析
生成报告按钮失效的可能原因:
1. **图表实例获取失败**`echarts.getInstanceByDom()` 无法正确获取 React 组件中的图表实例
2. **图片加载超时**:图片加载 Promise 可能无法正确解析
3. **html2pdf.js 配置问题**:复杂的 CSS 样式可能导致渲染失败
4. **异步时序问题**:多个异步操作没有正确协调
## 已实施的修复
### 1. 添加详细的调试日志
`App.tsx``handleDownloadPDF` 函数中添加了详细的 console.log 语句,用于跟踪:
- 图片加载状态
- 图表实例获取结果
- PDF 生成过程
### 2. 改进图表实例获取逻辑
- 简化了图表实例获取逻辑
- 添加了错误处理,当无法获取图表实例时使用透明占位图
- 避免了复杂的实例查找逻辑
### 3. 修复图表组件
`ChinaMap.tsx` 中:
- 添加了 `alt` 属性修复 accessibility 警告
- 保留了图表实例到 DOM 元素的引用(备用方案)
### 4. 创建测试文件
创建了多个测试文件来验证功能:
- `test-pdf.html` - 测试 html2pdf.js 基本功能
- `test-button.html` - 测试按钮事件处理
- `simple-test.html` - 最小化 PDF 生成测试
## 测试步骤
### 步骤1测试基本按钮功能
1. 打开 `simple-test.html` 文件
2. 点击"测试生成报告按钮"
3. 检查是否能够下载 PDF 文件
4. 查看浏览器控制台是否有错误
### 步骤2测试应用功能
1. 启动开发服务器:`npm run dev`
2. 访问 http://localhost:5174/
3. 点击"生成报告"按钮
4. 查看浏览器控制台输出
5. 检查是否出现错误信息
### 步骤3诊断具体问题
根据控制台输出诊断问题:
- 如果看到"图表 X 无法获取实例",说明图表实例获取有问题
- 如果看到"PDF导出失败",检查 html2pdf.js 配置
- 如果没有任何输出,按钮事件可能没有触发
## 常见问题解决方案
### 问题1图表实例无法获取
**解决方案**
1. 确保图表组件已正确渲染
2. 检查 `echarts.getInstanceByDom()` 的参数是否正确
3. 考虑使用全局变量保存图表实例
### 问题2图片加载超时
**解决方案**
1. 增加图片加载超时时间
2. 添加图片加载失败的回退机制
3. 使用 base64 内联图片
### 问题3html2pdf.js 渲染问题
**解决方案**
1. 简化 html2canvas 配置
2. 减少复杂的 CSS 样式
3. 使用更简单的测试内容验证功能
### 问题4按钮事件未触发
**解决方案**
1. 检查按钮的 `onClick` 事件绑定
2. 确保没有其他事件阻止了按钮点击
3. 添加简单的 console.log 测试事件绑定
## 下一步调试建议
如果问题仍然存在,建议:
1. **逐步简化测试**:从最简单的 HTML 内容开始测试 PDF 生成
2. **分离问题**分别测试图表导出、图片加载、PDF 生成
3. **检查依赖版本**:确保 html2pdf.js、echarts 等依赖版本兼容
4. **查看浏览器兼容性**:在不同浏览器中测试功能
## 相关文件
- `src/App.tsx` - 主应用组件,包含生成报告逻辑
- `src/components/ChinaMap.tsx` - 图表组件
- `src/index.css` - 导出样式定义
- `test-pdf.html` - PDF 生成测试
- `test-button.html` - 按钮功能测试
- `simple-test.html` - 简单 PDF 测试

73
README.md Normal file
View File

@@ -0,0 +1,73 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

61
README_NETWORK.md Normal file
View File

@@ -0,0 +1,61 @@
# 网络访问配置说明
## 配置变更
已成功配置应用支持网络访问和本地代理:
### 1. Vite 配置 (`vite.config.ts`)
- 设置 `host: '0.0.0.0'` - 允许从网络访问
- 设置代理配置,将 `/api` 请求转发到 `http://localhost:3002`
- 保持端口 `5173`
### 2. 前端代码 (`src/App.tsx`)
- 修改 API 调用从硬编码 `http://localhost:3002/api/generate-pdf` 改为相对路径 `/api/generate-pdf`
- 这样可以通过 Vite 代理访问后端
### 3. 服务器配置 (`server/src/index.js`)
- 设置监听地址为 `'0.0.0.0'`,允许从网络访问
- 更新 CORS 配置,允许来自 `localhost:5173``0.0.0.0:5173` 的请求
## 启动方式
### 方式一:使用启动脚本(推荐)
```bash
./start.sh
```
### 方式二:手动启动
1. 启动 PDF 生成服务器:
```bash
cd server
npm start
# 或直接运行node src/index.js
```
2. 在另一个终端启动前端:
```bash
npm run dev
```
## 访问方式
### 本地访问
- 前端http://localhost:5173
- 后端 APIhttp://localhost:3002/api/generate-pdf
### 网络访问
- 前端http://<你的IP地址>:5173
- 后端 APIhttp://<你的IP地址>:3002/api/generate-pdf
## 工作原理
1. 前端运行在 `0.0.0.0:5173`,可以通过网络访问
2. 当点击"服务器生成"按钮时,前端发送请求到 `/api/generate-pdf`
3. Vite 代理将 `/api` 请求转发到 `localhost:3002`
4. 后端服务器处理请求,生成 PDF 并返回
## 注意事项
1. 确保防火墙允许端口 5173 和 3002 的访问
2. 如果使用 Docker 或虚拟机,确保端口映射正确
3. 后端服务器需要 Puppeteer 支持,确保已安装相关依赖

23
eslint.config.js Normal file
View File

@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

126
images/10@2x.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 723 KiB

57
images/10备份@2x.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 560 KiB

15
images/12@2x.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 484 KiB

35
images/13@2x.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 214 KiB

124
images/3@2x.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 774 KiB

221
images/4@2x.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 338 KiB

BIN
images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

152
images/封面@2x.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 257 KiB

BIN
images/封面背景-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

BIN
images/封面背景-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
images/封面背景.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

BIN
images/封面背景@1x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 29 KiB

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>report-app</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

4272
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

43
package.json Normal file
View File

@@ -0,0 +1,43 @@
{
"name": "report-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@types/html2pdf.js": "^0.10.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"echarts": "^5.4.3",
"echarts-for-react": "^3.0.6",
"html2pdf.js": "^0.14.0",
"lucide-react": "^0.563.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwind-merge": "^3.4.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/node": "^24.10.1",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"autoprefixer": "^10.4.24",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.1",
"typescript": "~5.9.3",
"typescript-eslint": "^8.48.0",
"vite": "^7.3.1",
"vite-plugin-singlefile": "^2.3.0"
}
}

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 723 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 560 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 484 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 214 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 774 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 29 KiB

35
public/images/logo.svg Normal file
View File

@@ -0,0 +1,35 @@
<svg width="105" height="36" viewBox="0 0 105 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.7455 0C11.9231 0 5.03327 3.3306 1.00099 8.38444C-0.752414 10.5816 -0.13011 13.8256 2.30355 15.2314C4.1022 16.2696 6.38266 15.9831 7.84714 14.5099C12.4009 9.9275 19.9194 6.93346 28.4245 6.93346C32.0186 6.93346 35.4349 7.46845 38.5274 8.43207C34.4999 3.35124 27.5926 0 19.7455 0Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.1557 7.85349C28.9291 7.85349 25.8453 8.28529 23.01 9.07032C26.4478 10.3887 29.4053 12.2565 31.6437 14.5099C33.1082 15.9831 35.3887 16.2697 37.1873 15.2315C39.6043 13.836 40.2329 10.6277 38.5256 8.43134C36.4912 8.05431 34.3568 7.85349 32.1557 7.85349Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.7448 36C27.5672 36 34.457 32.6694 38.4893 27.6156C40.2427 25.4184 39.6204 22.1744 37.1868 20.7686C35.3881 19.7304 33.1076 20.0169 31.6432 21.4901C27.0894 26.0725 19.5709 29.0673 11.0658 29.0673C7.47171 29.0673 4.05538 28.5316 0.962912 27.5679C4.99043 32.6488 11.8977 36 19.7448 36Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.33467 28.147C10.5613 28.147 13.645 27.7144 16.4803 26.9301C13.0426 25.6117 10.085 23.7432 7.84664 21.4897C6.38216 20.0165 4.1017 19.73 2.30305 20.769C-0.113939 22.1636 -0.742593 25.3728 0.964775 27.5683C2.99917 27.9454 5.13358 28.147 7.33467 28.147Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M91.3893 6.18185H101.916C101.986 6.18185 102.043 6.12549 102.043 6.05484V4.77849C102.043 4.70864 101.986 4.65228 101.916 4.65228H91.3893C91.3194 4.65228 91.2623 4.70864 91.2623 4.77849V6.05484C91.2623 6.12549 91.3194 6.18185 91.3893 6.18185Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M104.794 11.2916H88.5105C88.4406 11.2916 88.3843 11.348 88.3843 11.4186V12.695C88.3843 12.7649 88.4406 12.8212 88.5105 12.8212H93.1603L90.0035 21.5335C89.8226 22.032 90.1924 22.559 90.7227 22.559H102.718C103.249 22.559 103.618 22.0312 103.436 21.5319L101.684 16.7257C101.659 16.6598 101.587 16.6257 101.521 16.6495L100.322 17.0877C100.256 17.1115 100.222 17.1837 100.246 17.2496L101.625 21.0294H91.8181L94.7883 12.8212H104.794C104.864 12.8212 104.921 12.7649 104.921 12.695V11.4186C104.921 11.348 104.864 11.2916 104.794 11.2916Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M52.1665 5.06447H49.2399C49.1693 5.06447 49.1129 5.12083 49.1129 5.19147V6.46783C49.1129 6.53768 49.1693 6.59404 49.2399 6.59404H52.1665C52.2363 6.59404 52.2935 6.53768 52.2935 6.46783V5.19147C52.2935 5.12083 52.2363 5.06447 52.1665 5.06447Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M52.1667 8.77671H48.2003C48.1304 8.77671 48.0741 8.83307 48.0741 8.90371V10.1801C48.0741 10.2499 48.1304 10.3063 48.2003 10.3063H52.1667C52.2365 10.3063 52.2937 10.2499 52.2937 10.1801V8.90371C52.2937 8.83307 52.2365 8.77671 52.1667 8.77671Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M51.8123 12.1679L50.555 11.9457C50.4867 11.9338 50.4208 11.9798 50.4081 12.0481L48.5992 22.2852C48.5873 22.3542 48.6325 22.4201 48.7016 22.432L49.9589 22.6543C50.0279 22.6662 50.093 22.6201 50.1057 22.5519L51.9147 12.3148C51.9274 12.2457 51.8813 12.1799 51.8123 12.1679Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M53.3347 11.8501C53.3529 11.9184 53.4236 11.9581 53.491 11.9398C55.3317 11.4302 57.105 10.7 58.7743 9.77525C60.4467 10.7016 62.2215 11.4302 64.0607 11.9398C64.1281 11.9581 64.1988 11.9184 64.217 11.8501L64.5512 10.6182C64.5695 10.5508 64.5298 10.4825 64.4631 10.4634C63.0288 10.0666 61.6358 9.52602 60.3054 8.85609C61.5905 8.01709 62.7986 7.05664 63.9099 5.98586C64.1099 5.79219 64.2067 5.50644 64.1401 5.23577C64.0543 4.8881 63.7456 4.65235 63.3971 4.65235H53.5323C53.4625 4.65235 53.4061 4.70871 53.4061 4.77856V6.05571C53.4061 6.12557 53.4625 6.18192 53.5323 6.18192H61.381C58.9402 8.16155 56.1088 9.62682 53.0886 10.4634C53.0219 10.4825 52.9822 10.5508 53.0005 10.6182L53.3347 11.8501Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M64.6548 21.0295H59.5565V13.9754H62.9363C63.0062 13.9754 63.0625 13.9183 63.0625 13.8484V12.5721C63.0625 12.5022 63.0062 12.4458 62.9363 12.4458H54.6471C54.5773 12.4458 54.5209 12.5022 54.5209 12.5721V13.8484C54.5209 13.9183 54.5773 13.9754 54.6471 13.9754H58.0269V21.0295H52.9286C52.8588 21.0295 52.8024 21.0859 52.8024 21.1565V22.4329C52.8024 22.5027 52.8588 22.5591 52.9286 22.5591H64.6548C64.7246 22.5591 64.7818 22.5027 64.7818 22.4329V21.1565C64.7818 21.0859 64.7246 21.0295 64.6548 21.0295Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M72.3222 5.06447H69.3956C69.325 5.06447 69.2686 5.12083 69.2686 5.19147V6.46783C69.2686 6.53768 69.325 6.59404 69.3956 6.59404H72.3222C72.392 6.59404 72.4492 6.53768 72.4492 6.46783V5.19147C72.4492 5.12083 72.392 5.06447 72.3222 5.06447Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M72.3221 8.77671H68.3557C68.2859 8.77671 68.2295 8.83307 68.2295 8.90371V10.1801C68.2295 10.2499 68.2859 10.3063 68.3557 10.3063H72.3221C72.392 10.3063 72.4491 10.2499 72.4491 10.1801V8.90371C72.4491 8.83307 72.392 8.77671 72.3221 8.77671Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M71.9678 12.1679L70.7105 11.9457C70.6422 11.9338 70.5763 11.9798 70.5636 12.0481L68.7546 22.2852C68.7427 22.3542 68.788 22.4201 68.857 22.432L70.1143 22.6543C70.1834 22.6662 70.2485 22.6201 70.2612 22.5519L72.0702 12.3148C72.0829 12.2457 72.0368 12.1799 71.9678 12.1679Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M76.3645 14.6696H82.6756V13.0598H76.3645V14.6696ZM76.3645 17.5072H82.5994V15.8975H76.3645V17.5072ZM83.4408 11.8279H75.6001C75.1778 11.8279 74.8349 12.1708 74.8349 12.5931V15.2839V18.1216V22.4325C74.8349 22.5023 74.8921 22.5595 74.9619 22.5595H76.2383C76.3081 22.5595 76.3645 22.5023 76.3645 22.4325V18.736H82.6756V21.3308H80.4158C80.3452 21.3308 80.2888 21.3879 80.2888 21.4578V22.4325C80.2888 22.5023 80.3452 22.5595 80.4158 22.5595H83.4408C83.8631 22.5595 84.2052 22.2166 84.2052 21.7943V12.5931C84.2052 12.1708 83.8631 11.8279 83.4408 11.8279Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M80.2847 6.95346H83.3343V5.52787H80.2847V6.95346ZM80.2847 9.3625H83.3343V8.1814H80.2847V9.3625ZM75.7055 6.95346H78.7551V5.52787H75.7055V6.95346ZM75.7055 9.3625H78.7551V8.1814H75.7055V9.3625ZM84.0995 4.29914H74.9411C74.5189 4.29914 74.176 4.64204 74.176 5.06432V9.82606C74.176 10.2483 74.5189 10.5912 74.9411 10.5912H84.0995C84.5218 10.5912 84.8639 10.2483 84.8639 9.82606V5.06432C84.8639 4.64204 84.5218 4.29914 84.0995 4.29914Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M51.5705 27.4989C52.0269 27.9149 52.4905 28.2729 52.9778 28.5848H50.1632C50.6505 28.2729 51.1141 27.9149 51.5705 27.4989ZM53.5652 31.0574L52.9588 30.8621C52.8802 31.1042 52.623 31.7773 52.4095 32.1591H51.8896V30.5914H53.9549V29.9548H51.8896V29.2222H53.3295V28.7991C53.6358 28.9769 53.951 29.1388 54.2828 29.2873L54.5423 28.7055C53.5604 28.2681 52.7183 27.7029 51.892 26.9274L51.7999 26.8417H51.3411L51.249 26.9274C50.4227 27.7029 49.5805 28.2681 48.5995 28.7055L48.8582 29.2873C49.19 29.1388 49.5059 28.9769 49.8115 28.7991V29.2222H51.2514V29.9548H49.186V30.5914H51.2514V32.1591H50.7315C50.518 31.7765 50.2608 31.1042 50.183 30.8621L49.5766 31.0574C49.6361 31.2439 49.8187 31.736 50.0179 32.1591H48.7288V32.7965H50.3854H52.7564H54.4121V32.1591H53.1239C53.3382 31.7043 53.5247 31.1812 53.5652 31.0574Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M57.3293 30.0795H57.6119V28.3626H57.3293V30.0795ZM58.181 30.0795H58.4636V28.3626H58.181V30.0795ZM61.0774 28.5412H61.6124V28.1031H61.0774V28.5412ZM61.8735 31.0876H62.7149V30.4502H61.7005H61.2743V30.0208H62.607V29.3834H60.0662V28.1031H60.5083V28.8524C60.5083 28.9945 60.6234 29.1103 60.7662 29.1103H61.9235C62.0656 29.1103 62.1815 28.9945 62.1815 28.8524V28.1031H62.7149V27.4657H62.1815V26.8402H61.6124V27.4657H61.0774V26.8402H60.5083V27.4657H60.0662V26.8402H59.497V27.4657H59.1962V28.1031H59.497V29.7636C59.497 29.9057 59.6121 30.0208 59.755 30.0208H60.6369V30.4502H60.2098H59.1962V31.0876H60.0376C59.8836 31.4686 59.6272 31.8131 59.2676 32.1147L59.0279 30.9042L58.4691 31.0154L58.6199 31.7742L58.181 31.8305V30.6486H58.7755C58.9176 30.6486 59.0327 30.5328 59.0327 30.3907V28.0515C59.0327 27.9086 58.9176 27.7935 58.7755 27.7935H58.181V26.8402H57.6119V27.7935H57.0181C56.8761 27.7935 56.761 27.9086 56.761 28.0515V30.3907C56.761 30.5328 56.8761 30.6486 57.0181 30.6486H57.6119V31.9036L56.5879 32.0345L56.6601 32.5989L59.0644 32.2909L59.4288 32.7965C60.0074 32.3798 60.4114 31.8813 60.6369 31.3106V32.7965H61.2743V31.3106C61.4997 31.8813 61.9037 32.3798 62.4823 32.7965L62.8546 32.2798C62.3831 31.9393 62.0561 31.5392 61.8735 31.0876Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M66.7264 27.0253L66.1319 26.7959C65.8699 27.4746 65.3818 28.0556 64.7563 28.4319L65.0857 28.978C65.8311 28.5295 66.4145 27.8357 66.7264 27.0253Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M66.8489 28.674L66.2544 28.4446C65.9694 29.1844 65.4376 29.8162 64.7566 30.2266L65.086 30.7735C65.2955 30.6457 65.4924 30.5005 65.6765 30.3417V32.796H66.3139V29.6464C66.533 29.3479 66.7147 29.0209 66.8489 28.674Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M69.4271 32.1592V29.9954H70.84V29.358H69.4271V27.7475H70.84V27.1101H66.991V27.7475H68.7897V32.1592H67.9634V28.692H67.326V32.1592H66.8323V32.7966H70.9987V32.1592H69.4271Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M74.8926 26.9943L74.275 26.8355C73.9797 27.9849 73.4344 28.9628 72.7399 29.5922L73.1669 30.0661C73.359 29.8915 73.5392 29.6946 73.7091 29.4803V32.7966H74.3464V28.5103H74.321C74.5576 28.0468 74.7528 27.5388 74.8926 26.9943Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M77.3886 27.5663C77.264 27.3258 77.1211 26.952 77.0775 26.8162L76.471 27.0131C76.4925 27.0782 76.5782 27.3179 76.6853 27.5663H74.7851V28.2037H79.1301V27.5663H77.3886Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M75.3476 29.1778H78.5687V28.6087H75.3476V29.1778Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M75.3469 30.1137H78.5679V29.5445H75.3469V30.1137Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M75.8239 32.2274H78.0916V31.0932H75.8239V32.2274ZM78.456 30.524H75.4595C75.3087 30.524 75.1865 30.6471 75.1865 30.7979V32.5235C75.1865 32.6743 75.3087 32.7966 75.4595 32.7966H78.456C78.6068 32.7966 78.729 32.6743 78.729 32.5235V30.7979C78.729 30.6471 78.6068 30.524 78.456 30.524Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M81.8922 27.7475H86.6404V27.1775H81.8922V27.7475Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M87.2291 29.5771V29.0072H83.1127L83.0683 28.9889L83.0603 29.0072H81.2569V29.5771H82.8206L81.6236 32.4195C81.5474 32.5997 81.68 32.7997 81.8753 32.7997H86.6576C86.8529 32.7997 86.9855 32.5997 86.9093 32.4187L86.2346 30.8312L85.7107 31.0527L86.2107 32.2306H82.3206L83.4382 29.5771H87.2291Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M102.64 30.3802H100.731V29.8897H100.093V30.3802H97.9707V31.0184H99.9035C99.5868 31.5089 98.8843 31.9233 97.8913 32.1797L98.0501 32.7964C99.7622 32.3551 100.392 31.601 100.615 31.0184H102.275V31.4002C102.275 31.8193 101.936 32.159 101.516 32.159H100.783V32.7964H101.517C102.288 32.7964 102.913 32.1717 102.913 31.4002V30.6541C102.913 30.5024 102.79 30.3802 102.64 30.3802Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M99.7461 28.0214H101.619C101.327 28.273 101.013 28.4945 100.683 28.685C100.352 28.4945 100.037 28.273 99.7461 28.0214ZM101.354 29.0191C101.823 28.7096 102.254 28.3397 102.636 27.9158L102.4 27.384H99.716C99.7675 27.3023 99.8176 27.2189 99.8644 27.1324L99.3032 26.8292C98.9452 27.492 98.4086 28.0166 97.7927 28.3048L98.0634 28.8818C98.4102 28.7199 98.7333 28.4945 99.0238 28.223C99.3286 28.5207 99.6588 28.7866 100.01 29.0191C99.354 29.2962 98.6499 29.4581 97.9284 29.4914V30.1264C98.8936 30.0875 99.8326 29.8399 100.683 29.4097C101.532 29.8399 102.471 30.0875 103.436 30.1264V29.4914C102.715 29.4581 102.011 29.2962 101.354 29.0191Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M90.3535 28.4825H91.3124V27.5506H90.3535V28.4825ZM90.3535 30.0097H91.3124V29.0508H90.3535V30.0097ZM91.6077 26.9807H90.0575C89.9067 26.9807 89.7844 27.1037 89.7844 27.2545V28.4825V30.1811C89.7844 31.1908 89.7201 31.7051 89.3613 32.5806L89.8876 32.7965C90.2345 31.9512 90.3297 31.3908 90.3488 30.5788H91.3124V31.8385C91.3124 32.0544 91.137 32.2298 90.9203 32.2298V32.7989C91.4505 32.7989 91.8815 32.3679 91.8815 31.8385V30.5788V28.4825V27.2545C91.8815 27.1037 91.7593 26.9807 91.6077 26.9807Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M94.4314 28.1532C94.4314 28.3175 94.2981 28.4508 94.1338 28.4508H93.3273V29.0199H94.1338C94.6124 29.0199 95.0006 28.6326 95.0006 28.1532V27.2546C95.0006 27.103 94.8783 26.9808 94.7267 26.9808H92.1129V32.7974H92.682V27.5499H94.4314V28.1532Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M93.5292 29.8469H94.4007C94.3515 30.285 94.2015 30.7081 93.9681 31.0883C93.7332 30.7057 93.58 30.2811 93.5292 29.8469ZM94.3777 31.6249C94.8119 31.0073 95.0564 30.2787 95.0564 29.5286V29.2095H92.8727V29.5286C92.8727 30.2676 93.1228 31.001 93.5593 31.6225C93.3331 31.8622 93.068 32.0726 92.7704 32.244L93.0879 32.7965C93.4165 32.6084 93.7117 32.3774 93.9681 32.117C94.2229 32.3798 94.5166 32.6099 94.8413 32.7965L95.1588 32.244C94.8659 32.0765 94.6031 31.8654 94.3777 31.6249Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

3
public/images/path_2.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="2" height="19" viewBox="0 0 2 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.576513" d="M0.578029 0.00184827L0.508818 18.9982" stroke="white"/>
</svg>

After

Width:  |  Height:  |  Size: 185 B

3
public/images/path_3.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="372" height="1" viewBox="0 0 372 1" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0.25H372" stroke="#979797" stroke-width="0.5" stroke-dasharray="3"/>
</svg>

After

Width:  |  Height:  |  Size: 184 B

View File

@@ -0,0 +1,3 @@
<svg width="372" height="1" viewBox="0 0 372 1" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0.25H372" stroke="#979797" stroke-width="0.5" stroke-dasharray="3"/>
</svg>

After

Width:  |  Height:  |  Size: 184 B

View File

@@ -0,0 +1,3 @@
<svg width="344" height="1" viewBox="0 0 344 1" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0.250727H345.502" stroke="#979797" stroke-width="0.5" stroke-dasharray="3"/>
</svg>

After

Width:  |  Height:  |  Size: 192 B

View File

@@ -0,0 +1,3 @@
<svg width="344" height="1" viewBox="0 0 344 1" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0.250727H345.502" stroke="#979797" stroke-width="0.5" stroke-dasharray="3"/>
</svg>

After

Width:  |  Height:  |  Size: 192 B

View File

@@ -0,0 +1,3 @@
<svg width="331" height="1" viewBox="0 0 331 1" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.00024451 0.250359L331 0.579427" stroke="#979797" stroke-width="0.5" stroke-dasharray="3"/>
</svg>

After

Width:  |  Height:  |  Size: 206 B

View File

@@ -0,0 +1,3 @@
<svg width="359" height="1" viewBox="0 0 359 1" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.000244481 0.722434L359 0.250348" stroke="#979797" stroke-width="0.5" stroke-dasharray="3"/>
</svg>

After

Width:  |  Height:  |  Size: 207 B

View File

@@ -0,0 +1,3 @@
<svg width="289" height="1" viewBox="0 0 289 1" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.000489126 0.250446L289 0.69407" stroke="#979797" stroke-width="0.5" stroke-dasharray="3"/>
</svg>

After

Width:  |  Height:  |  Size: 206 B

View File

@@ -0,0 +1,3 @@
<svg width="289" height="1" viewBox="0 0 289 1" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0.250416L289 0.321968" stroke="#979797" stroke-width="0.5" stroke-dasharray="3"/>
</svg>

After

Width:  |  Height:  |  Size: 197 B

View File

@@ -0,0 +1,3 @@
<svg width="302" height="1" viewBox="0 0 302 1" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.000244949 0.586001L303.502 0.249175" stroke="#979797" stroke-width="0.5" stroke-dasharray="3"/>
</svg>

After

Width:  |  Height:  |  Size: 211 B

View File

@@ -0,0 +1,3 @@
<svg width="317" height="1" viewBox="0 0 317 1" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.000244526 0.565176L317 0.250395" stroke="#979797" stroke-width="0.5" stroke-dasharray="3"/>
</svg>

After

Width:  |  Height:  |  Size: 207 B

View File

@@ -0,0 +1,3 @@
<svg width="277" height="1" viewBox="0 0 277 1" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.000490035 0.250908L278.497 0.709693" stroke="#979797" stroke-width="0.5" stroke-dasharray="3"/>
</svg>

After

Width:  |  Height:  |  Size: 211 B

View File

@@ -0,0 +1,3 @@
<svg width="359" height="1" viewBox="0 0 359 1" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.000244481 0.250354L359 0.479243" stroke="#979797" stroke-width="0.5" stroke-dasharray="3"/>
</svg>

After

Width:  |  Height:  |  Size: 207 B

9
public/images/path_5.svg Normal file
View File

@@ -0,0 +1,9 @@
<svg width="222" height="301" viewBox="0 0 222 301" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.2" fill-rule="evenodd" clip-rule="evenodd" d="M54.0853 0L0 300.788H167.269L222 0H54.0853Z" fill="url(#paint0_linear_6_14)"/>
<defs>
<linearGradient id="paint0_linear_6_14" x1="9.09855" y1="52.5665" x2="130.709" y2="229.325" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0.01"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 491 B

9
public/images/path_6.svg Normal file
View File

@@ -0,0 +1,9 @@
<svg width="222" height="481" viewBox="0 0 222 481" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.2" fill-rule="evenodd" clip-rule="evenodd" d="M185.693 0L0 195.389L221.229 481V0H185.693Z" fill="url(#paint0_linear_6_15)"/>
<defs>
<linearGradient id="paint0_linear_6_15" x1="99.432" y1="-72.2036" x2="-99.6703" y2="141.781" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0.01"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 492 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

2272
server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
server/package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "report-server",
"version": "1.0.0",
"scripts": {
"start": "node src/index.js",
"build": "tsc"
},
"dependencies": {
"body-parser": "^1.20.4",
"cors": "^2.8.6",
"express": "^4.22.1",
"puppeteer": "^22.15.0"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/node": "^20.11.0",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
}
}

100
server/src/index.js Normal file
View File

@@ -0,0 +1,100 @@
const express = require('express');
const puppeteer = require('puppeteer');
const cors = require('cors');
const bodyParser = require('body-parser');
const app = express();
const port = process.env.PORT || 3002;
app.use(cors({
origin: ['http://localhost:5173', 'http://0.0.0.0:5173'],
credentials: true
}));
app.use(bodyParser.json({ limit: '50mb' }));
let browser = null;
async function getBrowser() {
if (!browser) {
browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
}
return browser;
}
app.post('/api/generate-pdf', async (req, res) => {
const reportData = req.body;
const targetUrl = process.env.TARGET_URL || 'http://localhost:5173';
console.log('Received PDF generation request for:', reportData.companyName);
let page = null;
try {
const browserInstance = await getBrowser();
page = await browserInstance.newPage();
await page.setViewport({
width: 794,
height: 1123,
deviceScaleFactor: 2,
});
const url = new URL(targetUrl);
url.searchParams.set('print', 'true');
console.log('Navigating to:', url.toString());
await page.goto(url.toString(), { waitUntil: 'networkidle0' });
console.log('Injecting data...');
await page.evaluate((data) => {
if (window.loadServerData) {
window.loadServerData(data);
} else {
console.error('window.loadServerData not found');
}
}, reportData);
console.log('Waiting for report ready signal...');
await page.waitForFunction(() => window.isReportReady === true, {
timeout: 30000,
polling: 100
});
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('Printing PDF...');
const pdfBuffer = await page.pdf({
format: 'A4',
printBackground: true,
margin: {
top: '0px',
right: '0px',
bottom: '0px',
left: '0px'
},
preferCSSPageSize: true,
displayHeaderFooter: false
});
console.log('PDF generated successfully');
res.contentType('application/pdf');
res.send(pdfBuffer);
} catch (error) {
console.error('PDF generation failed:', error);
res.status(500).json({
error: 'Failed to generate PDF',
details: error instanceof Error ? error.message : String(error)
});
} finally {
if (page) {
await page.close();
}
}
});
app.listen(port, '0.0.0.0', () => {
console.log(`PDF Generation Server running at http://0.0.0.0:${port}`);
});

117
simple-test.html Normal file
View File

@@ -0,0 +1,117 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>简单按钮测试</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
}
#test-content {
width: 794px;
min-height: 1123px;
background: white;
border: 1px solid #ccc;
padding: 40px;
margin: 20px auto;
}
button {
display: block;
margin: 20px auto;
padding: 15px 30px;
font-size: 18px;
background: #3b82f6;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
}
button:hover {
background: #2563eb;
}
button:active {
transform: scale(0.98);
}
.status {
text-align: center;
margin: 10px;
padding: 10px;
border-radius: 5px;
}
.success {
background: #d1fae5;
color: #065f46;
}
.error {
background: #fee2e2;
color: #991b1b;
}
.loading {
background: #fef3c7;
color: #92400e;
}
</style>
</head>
<body>
<h1 style="text-align: center;">简单按钮功能测试</h1>
<div id="status" class="status"></div>
<button onclick="testButton()">测试生成报告按钮</button>
<div id="test-content">
<h2>测试报告内容</h2>
<p>这是一个简单的测试报告内容。</p>
<p>如果这个按钮工作正常你应该能够下载一个PDF文件。</p>
<p>当前时间: <span id="current-time"></span></p>
<div style="width: 300px; height: 200px; background: linear-gradient(45deg, #3b82f6, #8b5cf6); margin: 20px auto; border-radius: 10px;"></div>
</div>
<script>
document.getElementById('current-time').textContent = new Date().toLocaleString();
function setStatus(message, type = '') {
const statusEl = document.getElementById('status');
statusEl.textContent = message;
statusEl.className = 'status ' + type;
}
async function testButton() {
console.log('测试按钮被点击');
setStatus('开始生成PDF...', 'loading');
const element = document.getElementById('test-content');
// 简单的html2pdf配置
const opt = {
margin: 10,
filename: 'test-report-' + Date.now() + '.pdf',
image: { type: 'jpeg', quality: 0.95 },
html2canvas: {
scale: 2,
useCORS: true,
logging: true,
},
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
};
try {
console.log('调用html2pdf...');
await html2pdf().set(opt).from(element).save();
console.log('PDF生成成功');
setStatus('PDF生成成功文件已开始下载。', 'success');
} catch (error) {
console.error('PDF生成失败:', error);
setStatus('PDF生成失败: ' + error.message, 'error');
}
}
// 初始状态
setStatus('点击上方按钮测试PDF生成功能', '');
console.log('测试页面加载完成');
</script>
</body>
</html>

276
src/App-fixed.tsx Normal file
View File

@@ -0,0 +1,276 @@
import { useRef, useState } from 'react';
import html2pdf from 'html2pdf.js';
import * as echarts from 'echarts';
import { Download, Upload, RefreshCw, FileSpreadsheet, CheckCircle2, AlertCircle } from 'lucide-react';
import { CoverPage } from './components/CoverPage';
import { Page02Industry } from './components/Page02Industry';
import { Page03Duration } from './components/Page03Duration';
import { Page04Portrait } from './components/Page04Portrait';
import { Page05Risk } from './components/Page05Risk';
import { BackCover } from './components/BackCover';
import { defaultReportData } from './data/defaultData';
import { parseExcelReportData, generateExcelTemplate } from './utils/excelUtils';
import type { ReportData } from './types/data';
function App() {
const reportRef = useRef<HTMLDivElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const [reportData, setReportData] = useState<ReportData>(defaultReportData);
const [showImport, setShowImport] = useState(false);
const [isImporting, setIsImporting] = useState(false);
const [importStatus, setImportStatus] = useState<'idle' | 'success' | 'error'>('idle');
const handleDownloadPDF = async () => {
console.log('开始生成报告...');
const element = reportRef.current;
if (!element) {
console.error('报告元素未找到');
alert('无法找到报告内容,请刷新页面重试');
return;
}
console.log('找到报告元素');
try {
// 1. 切换到导出状态样式
console.log('应用导出样式...');
element.classList.add('is-exporting');
// 强制重绘
element.getBoundingClientRect();
await new Promise(resolve => setTimeout(resolve, 300));
// 2. 处理图表:将 ECharts 转换为图片
console.log('处理图表...');
const chartContainers = element.querySelectorAll('.echarts-canvas-container');
console.log(`找到 ${chartContainers.length} 个图表容器`);
const chartPromises = Array.from(chartContainers).map(async (container, index) => {
const inner = container.querySelector('.echarts-inner');
const exportImg = container.querySelector('.echarts-export-image') as HTMLImageElement;
if (inner && exportImg) {
try {
// 尝试获取图表实例
const instance = echarts.getInstanceByDom(inner as HTMLElement);
if (instance) {
const imgUrl = instance.getDataURL({
pixelRatio: 2,
backgroundColor: '#fff',
type: 'png'
});
exportImg.src = imgUrl;
console.log(`图表 ${index} 转换成功`);
} else {
console.warn(`图表 ${index} 实例未找到,使用透明占位`);
exportImg.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
}
} catch (error) {
console.error(`图表 ${index} 转换失败:`, error);
exportImg.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
}
}
});
await Promise.all(chartPromises);
console.log('图表处理完成');
// 3. 等待短暂时间让图片显示
await new Promise(resolve => setTimeout(resolve, 500));
// 4. 生成 PDF
console.log('开始生成PDF...');
const opt = {
margin: 0,
filename: `${reportData.companyName}_运营报告_${new Date().toLocaleDateString()}.pdf`,
image: { type: 'jpeg', quality: 0.98 },
enableLinks: false,
html2canvas: {
scale: 2,
useCORS: true,
logging: false,
width: 794,
windowWidth: 794,
scrollY: 0,
letterRendering: true,
backgroundColor: '#ffffff',
onclone: (clonedDoc) => {
// 在克隆的文档中确保所有图片都显示
const imgs = clonedDoc.querySelectorAll('img');
imgs.forEach(img => {
if (img.style.display === 'none') {
img.style.display = 'block';
}
});
}
},
jsPDF: { unit: 'px', format: [794, 1123], orientation: 'portrait' },
pagebreak: { mode: ['avoid-all', 'css', 'legacy'] }
};
// 显示加载提示
alert('正在生成PDF请稍候...这可能需要几秒钟时间。');
// @ts-expect-error - html2pdf call signature
await html2pdf().set(opt).from(element).save();
console.log('PDF生成成功');
alert('PDF生成成功文件已开始下载。');
} catch (error) {
console.error('PDF导出失败:', error);
alert(`PDF生成失败: ${error instanceof Error ? error.message : '未知错误'}`);
} finally {
// 5. 恢复原始样式
console.log('恢复原始样式...');
element.classList.remove('is-exporting');
}
};
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setIsImporting(true);
setImportStatus('idle');
try {
const newData = await parseExcelReportData(file);
setReportData(newData);
setImportStatus('success');
setTimeout(() => {
setShowImport(false);
setImportStatus('idle');
}, 1500);
} catch (err) {
console.error('Import failed:', err);
setImportStatus('error');
} finally {
setIsImporting(false);
if (fileInputRef.current) fileInputRef.current.value = '';
}
};
return (
<div className="min-h-screen bg-gray-100 flex font-sans relative overflow-x-hidden">
{/* Control Panel (Sidebar) */}
<div className={`fixed left-0 top-0 h-full bg-white shadow-2xl z-[60] transition-all duration-300 no-print flex flex-col ${showImport ? 'w-[380px]' : 'w-0 overflow-hidden'}`}>
<div className="p-8 flex flex-col h-full w-[380px]">
<div className="flex items-center justify-between mb-8">
<h3 className="text-xl font-bold text-gray-800 flex items-center gap-2">
<Upload size={22} className="text-blue-600" />
</h3>
<button onClick={() => setShowImport(false)} className="text-gray-400 hover:text-gray-600 transition-colors text-2xl">&times;</button>
</div>
<div className="bg-blue-50 border border-blue-100 rounded-xl p-5 mb-8">
<h4 className="text-sm font-bold text-blue-800 mb-2 flex items-center gap-2">
<FileSpreadsheet size={16} />
</h4>
<p className="text-xs text-blue-600 mb-4 leading-relaxed">
使 Excel 7 Sheet
</p>
<button
onClick={generateExcelTemplate}
className="w-full bg-white border border-blue-200 text-blue-600 hover:bg-blue-50 py-2.5 rounded-lg text-xs font-bold flex items-center justify-center gap-2 transition-all shadow-sm"
>
<Download size={14} />
Excel
</button>
</div>
<div className="flex-grow">
<h4 className="text-sm font-bold text-gray-700 mb-4 flex items-center gap-2">
<RefreshCw size={16} className={isImporting ? 'animate-spin' : ''} />
</h4>
<div
onClick={() => fileInputRef.current?.click()}
className={`border-2 border-dashed rounded-xl p-10 flex flex-col items-center justify-center cursor-pointer transition-all ${
importStatus === 'success' ? 'border-green-400 bg-green-50' :
importStatus === 'error' ? 'border-red-400 bg-red-50' :
'border-gray-200 hover:border-blue-400 hover:bg-blue-50'
}`}
>
<input
type="file"
ref={fileInputRef}
onChange={handleFileUpload}
className="hidden"
accept=".xlsx, .xls"
/>
{isImporting ? (
<div className="flex flex-col items-center animate-pulse">
<RefreshCw size={40} className="text-blue-500 animate-spin mb-3" />
<span className="text-sm text-blue-600 font-medium">...</span>
</div>
) : importStatus === 'success' ? (
<div className="flex flex-col items-center text-green-600">
<CheckCircle2 size={40} className="mb-3" />
<span className="text-sm font-bold"></span>
</div>
) : importStatus === 'error' ? (
<div className="flex flex-col items-center text-red-600">
<AlertCircle size={40} className="mb-3" />
<span className="text-sm font-bold"></span>
</div>
) : (
<>
<Upload size={40} className="text-gray-300 mb-3" />
<span className="text-sm text-gray-500 font-medium"> Excel </span>
<span className="text-[10px] text-gray-400 mt-2"> .xlsx, .xls </span>
</>
)}
</div>
</div>
<div className="mt-auto pt-8 border-t border-gray-100 text-center">
<p className="text-[10px] text-gray-400">
v2.0
</p>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="fixed bottom-10 right-10 z-50 flex flex-col gap-3 no-print">
<button
onClick={() => setShowImport(!showImport)}
className="bg-white hover:bg-gray-50 text-blue-600 font-bold py-3 px-8 rounded-full shadow-2xl flex items-center gap-2 transition-all transform hover:scale-105 border border-blue-100 active:scale-95"
>
<Upload size={20} />
</button>
<button
onClick={handleDownloadPDF}
className="bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-8 rounded-full shadow-2xl flex items-center gap-2 transition-all transform hover:scale-105 active:scale-95"
>
<Download size={20} />
</button>
</div>
{/* Main Display Area - Fluid Content */}
<div className={`flex-1 flex flex-col transition-all duration-300 ${showImport ? 'pl-[380px]' : ''}`}>
<div
ref={reportRef}
className="bg-white shadow-2xl overflow-hidden print:shadow-none w-full"
>
<CoverPage data={reportData} />
<Page02Industry mapData={reportData.mapData} tableData={reportData.regionStatistics} />
<Page03Duration durationData={reportData.durationStatistics} funnelData={reportData.funnelData} />
<Page04Portrait data={reportData.sunburstData} />
<Page05Risk riskData={reportData.riskIndicators} />
<BackCover />
</div>
</div>
</div>
);
}
export default App;

42
src/App.css Normal file
View File

@@ -0,0 +1,42 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

358
src/App.tsx Normal file
View File

@@ -0,0 +1,358 @@
import { useRef, useState, useEffect } from 'react';
import html2pdf from 'html2pdf.js';
import * as echarts from 'echarts';
import { Download, Upload, RefreshCw, FileSpreadsheet, CheckCircle2, AlertCircle, Server } from 'lucide-react';
import { CoverPage } from './components/CoverPage';
import { Page02Industry } from './components/Page02Industry';
import { Page03Duration } from './components/Page03Duration';
import { Page04Portrait } from './components/Page04Portrait';
import { Page05Risk } from './components/Page05Risk';
import { Page06ExportTax } from './components/Page06ExportTax';
import { Page07Association } from './components/Page07Association';
import { BackCover } from './components/BackCover';
import { defaultReportData } from './data/defaultData';
import { parseExcelReportData, generateExcelTemplate } from './utils/excelUtils';
import type { ReportData } from './types/data';
function App() {
const reportRef = useRef<HTMLDivElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const [reportData, setReportData] = useState<ReportData>(defaultReportData);
const [showImport, setShowImport] = useState(false);
const [isImporting, setIsImporting] = useState(false);
const [importStatus, setImportStatus] = useState<'idle' | 'success' | 'error'>('idle');
const [isServerGenerating, setIsServerGenerating] = useState(false);
// 暴露给 Puppeteer 的接口
useEffect(() => {
window.loadServerData = (data: ReportData) => {
console.log('Received data from server injection');
setReportData(data);
};
// 标记页面已加载,但还没准备好(需要等待图表渲染)
window.isReportReady = false;
// 检测是否是打印模式
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('print')) {
document.body.classList.add('print-mode');
}
return () => {
delete window.loadServerData;
};
}, []);
// 监听数据变化,设置渲染完成信号
useEffect(() => {
// 给 ECharts 一点渲染时间
const timer = setTimeout(() => {
window.isReportReady = true;
console.log('Report is ready for capture');
}, 2000);
return () => {
clearTimeout(timer);
window.isReportReady = false;
};
}, [reportData]);
const handleDownloadPDF = async () => {
console.log('开始生成报告...');
const element = reportRef.current;
if (!element) {
console.error('报告元素未找到');
alert('无法找到报告内容,请刷新页面重试');
return;
}
console.log('找到报告元素');
try {
// 1. 切换到导出状态样式
console.log('应用导出样式...');
element.classList.add('is-exporting');
// 强制重绘
element.getBoundingClientRect();
await new Promise(resolve => setTimeout(resolve, 300));
// 2. 处理图表:将 ECharts 转换为图片
console.log('处理图表...');
const chartContainers = element.querySelectorAll('.echarts-canvas-container');
console.log(`找到 ${chartContainers.length} 个图表容器`);
const chartPromises = Array.from(chartContainers).map(async (container, index) => {
const inner = container.querySelector('.echarts-inner');
const exportImg = container.querySelector('.echarts-export-image') as HTMLImageElement;
if (inner && exportImg) {
try {
// 尝试获取图表实例
const instance = echarts.getInstanceByDom(inner as HTMLElement);
if (instance) {
const imgUrl = instance.getDataURL({
pixelRatio: 2,
backgroundColor: '#fff',
type: 'png'
});
exportImg.src = imgUrl;
console.log(`图表 ${index} 转换成功`);
} else {
console.warn(`图表 ${index} 实例未找到,使用透明占位`);
exportImg.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
}
} catch (error) {
console.error(`图表 ${index} 转换失败:`, error);
exportImg.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
}
}
});
await Promise.all(chartPromises);
console.log('图表处理完成');
// 3. 等待短暂时间让图片显示
await new Promise(resolve => setTimeout(resolve, 500));
// 4. 生成 PDF
console.log('开始生成PDF...');
const opt = {
margin: 0,
filename: `${reportData.companyName}_运营报告_${new Date().toLocaleDateString()}.pdf`,
image: { type: 'jpeg', quality: 0.98 },
enableLinks: false,
html2canvas: {
scale: 2,
useCORS: true,
logging: false,
width: 794,
windowWidth: 794,
scrollY: 0,
letterRendering: true,
backgroundColor: '#ffffff',
onclone: (clonedDoc: Document) => {
// 在克隆的文档中确保所有图片都显示
const imgs = clonedDoc.querySelectorAll('img');
imgs.forEach((img: HTMLImageElement) => {
if (img.style.display === 'none') {
img.style.display = 'block';
}
});
}
},
jsPDF: { unit: 'px', format: [794, 1123], orientation: 'portrait' },
pagebreak: { mode: ['avoid-all', 'css', 'legacy'] }
};
// 显示加载提示
alert('正在生成PDF请稍候...这可能需要几秒钟时间。');
// @ts-expect-error - html2pdf call signature
await html2pdf().set(opt).from(element).save();
console.log('PDF生成成功');
alert('PDF生成成功文件已开始下载。');
} catch (error) {
console.error('PDF导出失败:', error);
alert(`PDF生成失败: ${error instanceof Error ? error.message : '未知错误'}`);
} finally {
// 5. 恢复原始样式
console.log('恢复原始样式...');
element.classList.remove('is-exporting');
}
};
const handleServerGenerate = async () => {
setIsServerGenerating(true);
try {
const response = await fetch('/api/generate-pdf', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(reportData),
});
if (!response.ok) {
throw new Error(`Server responded with ${response.status}`);
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${reportData.companyName}_运营报告_服务端_${new Date().toLocaleDateString()}.pdf`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
alert('服务端生成 PDF 成功!');
} catch (error) {
console.error('Server PDF generation failed:', error);
alert(`服务端生成失败: ${error instanceof Error ? error.message : '未知错误'}`);
} finally {
setIsServerGenerating(false);
}
};
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setIsImporting(true);
setImportStatus('idle');
try {
const newData = await parseExcelReportData(file);
setReportData(newData);
setImportStatus('success');
setTimeout(() => {
setShowImport(false);
setImportStatus('idle');
}, 1500);
} catch (err) {
console.error('Import failed:', err);
setImportStatus('error');
} finally {
setIsImporting(false);
if (fileInputRef.current) fileInputRef.current.value = '';
}
};
return (
<div className="min-h-screen bg-gray-100 flex font-sans relative overflow-x-hidden">
{/* Control Panel (Sidebar) */}
<div className={`fixed left-0 top-0 h-full bg-white shadow-2xl z-[60] transition-all duration-300 no-print flex flex-col ${showImport ? 'w-[380px]' : 'w-0 overflow-hidden'}`}>
<div className="p-8 flex flex-col h-full w-[380px]">
<div className="flex items-center justify-between mb-8">
<h3 className="text-xl font-bold text-gray-800 flex items-center gap-2">
<Upload size={22} className="text-blue-600" />
</h3>
<button onClick={() => setShowImport(false)} className="text-gray-400 hover:text-gray-600 transition-colors text-2xl">&times;</button>
</div>
<div className="bg-blue-50 border border-blue-100 rounded-xl p-5 mb-8">
<h4 className="text-sm font-bold text-blue-800 mb-2 flex items-center gap-2">
<FileSpreadsheet size={16} />
</h4>
<p className="text-xs text-blue-600 mb-4 leading-relaxed">
使 Excel 13 Sheet
</p>
<button
onClick={generateExcelTemplate}
className="w-full bg-white border border-blue-200 text-blue-600 hover:bg-blue-50 py-2.5 rounded-lg text-xs font-bold flex items-center justify-center gap-2 transition-all shadow-sm"
>
<Download size={14} />
Excel
</button>
</div>
<div className="flex-grow">
<h4 className="text-sm font-bold text-gray-700 mb-4 flex items-center gap-2">
<RefreshCw size={16} className={isImporting ? 'animate-spin' : ''} />
</h4>
<div
onClick={() => fileInputRef.current?.click()}
className={`border-2 border-dashed rounded-xl p-10 flex flex-col items-center justify-center cursor-pointer transition-all ${
importStatus === 'success' ? 'border-green-400 bg-green-50' :
importStatus === 'error' ? 'border-red-400 bg-red-50' :
'border-gray-200 hover:border-blue-400 hover:bg-blue-50'
}`}
>
<input
type="file"
ref={fileInputRef}
onChange={handleFileUpload}
className="hidden"
accept=".xlsx, .xls"
/>
{isImporting ? (
<div className="flex flex-col items-center animate-pulse">
<RefreshCw size={40} className="text-blue-500 animate-spin mb-3" />
<span className="text-sm text-blue-600 font-medium">...</span>
</div>
) : importStatus === 'success' ? (
<div className="flex flex-col items-center text-green-600">
<CheckCircle2 size={40} className="mb-3" />
<span className="text-sm font-bold"></span>
</div>
) : importStatus === 'error' ? (
<div className="flex flex-col items-center text-red-600">
<AlertCircle size={40} className="mb-3" />
<span className="text-sm font-bold"></span>
</div>
) : (
<>
<Upload size={40} className="text-gray-300 mb-3" />
<span className="text-sm text-gray-500 font-medium"> Excel </span>
<span className="text-[10px] text-gray-400 mt-2"> .xlsx, .xls </span>
</>
)}
</div>
</div>
<div className="mt-auto pt-8 border-t border-gray-100 text-center">
<p className="text-[10px] text-gray-400">
v2.0
</p>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="fixed bottom-10 right-10 z-50 flex flex-col gap-3 no-print">
<button
onClick={() => setShowImport(!showImport)}
className="bg-white hover:bg-gray-50 text-blue-600 font-bold py-3 px-8 rounded-full shadow-2xl flex items-center gap-2 transition-all transform hover:scale-105 border border-blue-100 active:scale-95"
>
<Upload size={20} />
</button>
{/* <button
onClick={handleDownloadPDF}
className="bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-8 rounded-full shadow-2xl flex items-center gap-2 transition-all transform hover:scale-105 active:scale-95"
>
<Download size={20} />
浏览器生成
</button> */}
<button
onClick={handleServerGenerate}
disabled={isServerGenerating}
className="bg-indigo-600 hover:bg-indigo-700 disabled:bg-indigo-400 text-white font-bold py-3 px-8 rounded-full shadow-2xl flex items-center gap-2 transition-all transform hover:scale-105 active:scale-95"
>
{isServerGenerating ? <RefreshCw size={20} className="animate-spin" /> : <Server size={20} />}
PDF
</button>
</div>
{/* Main Display Area - Fluid Content */}
<div className={`flex-1 flex flex-col transition-all duration-300 ${showImport ? 'pl-[380px]' : ''}`}>
<div
ref={reportRef}
className="bg-white shadow-2xl overflow-hidden print:overflow-visible print:shadow-none w-full"
>
<CoverPage data={reportData} />
<Page02Industry mapData={reportData.mapData} tableData={reportData.regionStatistics} />
<Page03Duration durationData={reportData.durationStatistics} funnelData={reportData.funnelData} />
<Page04Portrait data={reportData.sunburstData} poorCustomerData={reportData.poorCustomerData} goodCustomerData={reportData.goodCustomerData} allDescription={reportData.portraitDescriptions?.all} poorDescription={reportData.portraitDescriptions?.poor} goodDescription={reportData.portraitDescriptions?.good} />
<Page05Risk riskData={reportData.riskIndicators} />
<Page06ExportTax exportFreeTaxRebate={reportData.exportFreeTaxRebate} exportTaxRebate={reportData.exportTaxRebate} chartDescriptions={reportData.chartDescriptions} />
<Page07Association loginPersonAssociation={reportData.loginPersonAssociation} legalPersonAssociation={reportData.legalPersonAssociation} chartDescriptions={reportData.chartDescriptions} />
<BackCover riskScanning={reportData.riskScanning} riskScanningDescription={reportData.chartDescriptions?.riskScanning} />
</div>
</div>
</div>
);
}
export default App;

1
src/assets/china.json Normal file

File diff suppressed because one or more lines are too long

126
src/assets/images/10@2x.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 723 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 560 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 484 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 214 KiB

124
src/assets/images/3@2x.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 774 KiB

221
src/assets/images/4@2x.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 338 KiB

BIN
src/assets/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,35 @@
<svg width="105" height="36" viewBox="0 0 105 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.7455 0C11.9231 0 5.03327 3.3306 1.00099 8.38444C-0.752414 10.5816 -0.13011 13.8256 2.30355 15.2314C4.1022 16.2696 6.38266 15.9831 7.84714 14.5099C12.4009 9.9275 19.9194 6.93346 28.4245 6.93346C32.0186 6.93346 35.4349 7.46845 38.5274 8.43207C34.4999 3.35124 27.5926 0 19.7455 0Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.1557 7.85349C28.9291 7.85349 25.8453 8.28529 23.01 9.07032C26.4478 10.3887 29.4053 12.2565 31.6437 14.5099C33.1082 15.9831 35.3887 16.2697 37.1873 15.2315C39.6043 13.836 40.2329 10.6277 38.5256 8.43134C36.4912 8.05431 34.3568 7.85349 32.1557 7.85349Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.7448 36C27.5672 36 34.457 32.6694 38.4893 27.6156C40.2427 25.4184 39.6204 22.1744 37.1868 20.7686C35.3881 19.7304 33.1076 20.0169 31.6432 21.4901C27.0894 26.0725 19.5709 29.0673 11.0658 29.0673C7.47171 29.0673 4.05538 28.5316 0.962912 27.5679C4.99043 32.6488 11.8977 36 19.7448 36Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.33467 28.147C10.5613 28.147 13.645 27.7144 16.4803 26.9301C13.0426 25.6117 10.085 23.7432 7.84664 21.4897C6.38216 20.0165 4.1017 19.73 2.30305 20.769C-0.113939 22.1636 -0.742593 25.3728 0.964775 27.5683C2.99917 27.9454 5.13358 28.147 7.33467 28.147Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M91.3893 6.18185H101.916C101.986 6.18185 102.043 6.12549 102.043 6.05484V4.77849C102.043 4.70864 101.986 4.65228 101.916 4.65228H91.3893C91.3194 4.65228 91.2623 4.70864 91.2623 4.77849V6.05484C91.2623 6.12549 91.3194 6.18185 91.3893 6.18185Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M104.794 11.2916H88.5105C88.4406 11.2916 88.3843 11.348 88.3843 11.4186V12.695C88.3843 12.7649 88.4406 12.8212 88.5105 12.8212H93.1603L90.0035 21.5335C89.8226 22.032 90.1924 22.559 90.7227 22.559H102.718C103.249 22.559 103.618 22.0312 103.436 21.5319L101.684 16.7257C101.659 16.6598 101.587 16.6257 101.521 16.6495L100.322 17.0877C100.256 17.1115 100.222 17.1837 100.246 17.2496L101.625 21.0294H91.8181L94.7883 12.8212H104.794C104.864 12.8212 104.921 12.7649 104.921 12.695V11.4186C104.921 11.348 104.864 11.2916 104.794 11.2916Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M52.1665 5.06447H49.2399C49.1693 5.06447 49.1129 5.12083 49.1129 5.19147V6.46783C49.1129 6.53768 49.1693 6.59404 49.2399 6.59404H52.1665C52.2363 6.59404 52.2935 6.53768 52.2935 6.46783V5.19147C52.2935 5.12083 52.2363 5.06447 52.1665 5.06447Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M52.1667 8.77671H48.2003C48.1304 8.77671 48.0741 8.83307 48.0741 8.90371V10.1801C48.0741 10.2499 48.1304 10.3063 48.2003 10.3063H52.1667C52.2365 10.3063 52.2937 10.2499 52.2937 10.1801V8.90371C52.2937 8.83307 52.2365 8.77671 52.1667 8.77671Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M51.8123 12.1679L50.555 11.9457C50.4867 11.9338 50.4208 11.9798 50.4081 12.0481L48.5992 22.2852C48.5873 22.3542 48.6325 22.4201 48.7016 22.432L49.9589 22.6543C50.0279 22.6662 50.093 22.6201 50.1057 22.5519L51.9147 12.3148C51.9274 12.2457 51.8813 12.1799 51.8123 12.1679Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M53.3347 11.8501C53.3529 11.9184 53.4236 11.9581 53.491 11.9398C55.3317 11.4302 57.105 10.7 58.7743 9.77525C60.4467 10.7016 62.2215 11.4302 64.0607 11.9398C64.1281 11.9581 64.1988 11.9184 64.217 11.8501L64.5512 10.6182C64.5695 10.5508 64.5298 10.4825 64.4631 10.4634C63.0288 10.0666 61.6358 9.52602 60.3054 8.85609C61.5905 8.01709 62.7986 7.05664 63.9099 5.98586C64.1099 5.79219 64.2067 5.50644 64.1401 5.23577C64.0543 4.8881 63.7456 4.65235 63.3971 4.65235H53.5323C53.4625 4.65235 53.4061 4.70871 53.4061 4.77856V6.05571C53.4061 6.12557 53.4625 6.18192 53.5323 6.18192H61.381C58.9402 8.16155 56.1088 9.62682 53.0886 10.4634C53.0219 10.4825 52.9822 10.5508 53.0005 10.6182L53.3347 11.8501Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M64.6548 21.0295H59.5565V13.9754H62.9363C63.0062 13.9754 63.0625 13.9183 63.0625 13.8484V12.5721C63.0625 12.5022 63.0062 12.4458 62.9363 12.4458H54.6471C54.5773 12.4458 54.5209 12.5022 54.5209 12.5721V13.8484C54.5209 13.9183 54.5773 13.9754 54.6471 13.9754H58.0269V21.0295H52.9286C52.8588 21.0295 52.8024 21.0859 52.8024 21.1565V22.4329C52.8024 22.5027 52.8588 22.5591 52.9286 22.5591H64.6548C64.7246 22.5591 64.7818 22.5027 64.7818 22.4329V21.1565C64.7818 21.0859 64.7246 21.0295 64.6548 21.0295Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M72.3222 5.06447H69.3956C69.325 5.06447 69.2686 5.12083 69.2686 5.19147V6.46783C69.2686 6.53768 69.325 6.59404 69.3956 6.59404H72.3222C72.392 6.59404 72.4492 6.53768 72.4492 6.46783V5.19147C72.4492 5.12083 72.392 5.06447 72.3222 5.06447Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M72.3221 8.77671H68.3557C68.2859 8.77671 68.2295 8.83307 68.2295 8.90371V10.1801C68.2295 10.2499 68.2859 10.3063 68.3557 10.3063H72.3221C72.392 10.3063 72.4491 10.2499 72.4491 10.1801V8.90371C72.4491 8.83307 72.392 8.77671 72.3221 8.77671Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M71.9678 12.1679L70.7105 11.9457C70.6422 11.9338 70.5763 11.9798 70.5636 12.0481L68.7546 22.2852C68.7427 22.3542 68.788 22.4201 68.857 22.432L70.1143 22.6543C70.1834 22.6662 70.2485 22.6201 70.2612 22.5519L72.0702 12.3148C72.0829 12.2457 72.0368 12.1799 71.9678 12.1679Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M76.3645 14.6696H82.6756V13.0598H76.3645V14.6696ZM76.3645 17.5072H82.5994V15.8975H76.3645V17.5072ZM83.4408 11.8279H75.6001C75.1778 11.8279 74.8349 12.1708 74.8349 12.5931V15.2839V18.1216V22.4325C74.8349 22.5023 74.8921 22.5595 74.9619 22.5595H76.2383C76.3081 22.5595 76.3645 22.5023 76.3645 22.4325V18.736H82.6756V21.3308H80.4158C80.3452 21.3308 80.2888 21.3879 80.2888 21.4578V22.4325C80.2888 22.5023 80.3452 22.5595 80.4158 22.5595H83.4408C83.8631 22.5595 84.2052 22.2166 84.2052 21.7943V12.5931C84.2052 12.1708 83.8631 11.8279 83.4408 11.8279Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M80.2847 6.95346H83.3343V5.52787H80.2847V6.95346ZM80.2847 9.3625H83.3343V8.1814H80.2847V9.3625ZM75.7055 6.95346H78.7551V5.52787H75.7055V6.95346ZM75.7055 9.3625H78.7551V8.1814H75.7055V9.3625ZM84.0995 4.29914H74.9411C74.5189 4.29914 74.176 4.64204 74.176 5.06432V9.82606C74.176 10.2483 74.5189 10.5912 74.9411 10.5912H84.0995C84.5218 10.5912 84.8639 10.2483 84.8639 9.82606V5.06432C84.8639 4.64204 84.5218 4.29914 84.0995 4.29914Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M51.5705 27.4989C52.0269 27.9149 52.4905 28.2729 52.9778 28.5848H50.1632C50.6505 28.2729 51.1141 27.9149 51.5705 27.4989ZM53.5652 31.0574L52.9588 30.8621C52.8802 31.1042 52.623 31.7773 52.4095 32.1591H51.8896V30.5914H53.9549V29.9548H51.8896V29.2222H53.3295V28.7991C53.6358 28.9769 53.951 29.1388 54.2828 29.2873L54.5423 28.7055C53.5604 28.2681 52.7183 27.7029 51.892 26.9274L51.7999 26.8417H51.3411L51.249 26.9274C50.4227 27.7029 49.5805 28.2681 48.5995 28.7055L48.8582 29.2873C49.19 29.1388 49.5059 28.9769 49.8115 28.7991V29.2222H51.2514V29.9548H49.186V30.5914H51.2514V32.1591H50.7315C50.518 31.7765 50.2608 31.1042 50.183 30.8621L49.5766 31.0574C49.6361 31.2439 49.8187 31.736 50.0179 32.1591H48.7288V32.7965H50.3854H52.7564H54.4121V32.1591H53.1239C53.3382 31.7043 53.5247 31.1812 53.5652 31.0574Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M57.3293 30.0795H57.6119V28.3626H57.3293V30.0795ZM58.181 30.0795H58.4636V28.3626H58.181V30.0795ZM61.0774 28.5412H61.6124V28.1031H61.0774V28.5412ZM61.8735 31.0876H62.7149V30.4502H61.7005H61.2743V30.0208H62.607V29.3834H60.0662V28.1031H60.5083V28.8524C60.5083 28.9945 60.6234 29.1103 60.7662 29.1103H61.9235C62.0656 29.1103 62.1815 28.9945 62.1815 28.8524V28.1031H62.7149V27.4657H62.1815V26.8402H61.6124V27.4657H61.0774V26.8402H60.5083V27.4657H60.0662V26.8402H59.497V27.4657H59.1962V28.1031H59.497V29.7636C59.497 29.9057 59.6121 30.0208 59.755 30.0208H60.6369V30.4502H60.2098H59.1962V31.0876H60.0376C59.8836 31.4686 59.6272 31.8131 59.2676 32.1147L59.0279 30.9042L58.4691 31.0154L58.6199 31.7742L58.181 31.8305V30.6486H58.7755C58.9176 30.6486 59.0327 30.5328 59.0327 30.3907V28.0515C59.0327 27.9086 58.9176 27.7935 58.7755 27.7935H58.181V26.8402H57.6119V27.7935H57.0181C56.8761 27.7935 56.761 27.9086 56.761 28.0515V30.3907C56.761 30.5328 56.8761 30.6486 57.0181 30.6486H57.6119V31.9036L56.5879 32.0345L56.6601 32.5989L59.0644 32.2909L59.4288 32.7965C60.0074 32.3798 60.4114 31.8813 60.6369 31.3106V32.7965H61.2743V31.3106C61.4997 31.8813 61.9037 32.3798 62.4823 32.7965L62.8546 32.2798C62.3831 31.9393 62.0561 31.5392 61.8735 31.0876Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M66.7264 27.0253L66.1319 26.7959C65.8699 27.4746 65.3818 28.0556 64.7563 28.4319L65.0857 28.978C65.8311 28.5295 66.4145 27.8357 66.7264 27.0253Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M66.8489 28.674L66.2544 28.4446C65.9694 29.1844 65.4376 29.8162 64.7566 30.2266L65.086 30.7735C65.2955 30.6457 65.4924 30.5005 65.6765 30.3417V32.796H66.3139V29.6464C66.533 29.3479 66.7147 29.0209 66.8489 28.674Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M69.4271 32.1592V29.9954H70.84V29.358H69.4271V27.7475H70.84V27.1101H66.991V27.7475H68.7897V32.1592H67.9634V28.692H67.326V32.1592H66.8323V32.7966H70.9987V32.1592H69.4271Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M74.8926 26.9943L74.275 26.8355C73.9797 27.9849 73.4344 28.9628 72.7399 29.5922L73.1669 30.0661C73.359 29.8915 73.5392 29.6946 73.7091 29.4803V32.7966H74.3464V28.5103H74.321C74.5576 28.0468 74.7528 27.5388 74.8926 26.9943Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M77.3886 27.5663C77.264 27.3258 77.1211 26.952 77.0775 26.8162L76.471 27.0131C76.4925 27.0782 76.5782 27.3179 76.6853 27.5663H74.7851V28.2037H79.1301V27.5663H77.3886Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M75.3476 29.1778H78.5687V28.6087H75.3476V29.1778Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M75.3469 30.1137H78.5679V29.5445H75.3469V30.1137Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M75.8239 32.2274H78.0916V31.0932H75.8239V32.2274ZM78.456 30.524H75.4595C75.3087 30.524 75.1865 30.6471 75.1865 30.7979V32.5235C75.1865 32.6743 75.3087 32.7966 75.4595 32.7966H78.456C78.6068 32.7966 78.729 32.6743 78.729 32.5235V30.7979C78.729 30.6471 78.6068 30.524 78.456 30.524Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M81.8922 27.7475H86.6404V27.1775H81.8922V27.7475Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M87.2291 29.5771V29.0072H83.1127L83.0683 28.9889L83.0603 29.0072H81.2569V29.5771H82.8206L81.6236 32.4195C81.5474 32.5997 81.68 32.7997 81.8753 32.7997H86.6576C86.8529 32.7997 86.9855 32.5997 86.9093 32.4187L86.2346 30.8312L85.7107 31.0527L86.2107 32.2306H82.3206L83.4382 29.5771H87.2291Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M102.64 30.3802H100.731V29.8897H100.093V30.3802H97.9707V31.0184H99.9035C99.5868 31.5089 98.8843 31.9233 97.8913 32.1797L98.0501 32.7964C99.7622 32.3551 100.392 31.601 100.615 31.0184H102.275V31.4002C102.275 31.8193 101.936 32.159 101.516 32.159H100.783V32.7964H101.517C102.288 32.7964 102.913 32.1717 102.913 31.4002V30.6541C102.913 30.5024 102.79 30.3802 102.64 30.3802Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M99.7461 28.0214H101.619C101.327 28.273 101.013 28.4945 100.683 28.685C100.352 28.4945 100.037 28.273 99.7461 28.0214ZM101.354 29.0191C101.823 28.7096 102.254 28.3397 102.636 27.9158L102.4 27.384H99.716C99.7675 27.3023 99.8176 27.2189 99.8644 27.1324L99.3032 26.8292C98.9452 27.492 98.4086 28.0166 97.7927 28.3048L98.0634 28.8818C98.4102 28.7199 98.7333 28.4945 99.0238 28.223C99.3286 28.5207 99.6588 28.7866 100.01 29.0191C99.354 29.2962 98.6499 29.4581 97.9284 29.4914V30.1264C98.8936 30.0875 99.8326 29.8399 100.683 29.4097C101.532 29.8399 102.471 30.0875 103.436 30.1264V29.4914C102.715 29.4581 102.011 29.2962 101.354 29.0191Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M90.3535 28.4825H91.3124V27.5506H90.3535V28.4825ZM90.3535 30.0097H91.3124V29.0508H90.3535V30.0097ZM91.6077 26.9807H90.0575C89.9067 26.9807 89.7844 27.1037 89.7844 27.2545V28.4825V30.1811C89.7844 31.1908 89.7201 31.7051 89.3613 32.5806L89.8876 32.7965C90.2345 31.9512 90.3297 31.3908 90.3488 30.5788H91.3124V31.8385C91.3124 32.0544 91.137 32.2298 90.9203 32.2298V32.7989C91.4505 32.7989 91.8815 32.3679 91.8815 31.8385V30.5788V28.4825V27.2545C91.8815 27.1037 91.7593 26.9807 91.6077 26.9807Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M94.4314 28.1532C94.4314 28.3175 94.2981 28.4508 94.1338 28.4508H93.3273V29.0199H94.1338C94.6124 29.0199 95.0006 28.6326 95.0006 28.1532V27.2546C95.0006 27.103 94.8783 26.9808 94.7267 26.9808H92.1129V32.7974H92.682V27.5499H94.4314V28.1532Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M93.5292 29.8469H94.4007C94.3515 30.285 94.2015 30.7081 93.9681 31.0883C93.7332 30.7057 93.58 30.2811 93.5292 29.8469ZM94.3777 31.6249C94.8119 31.0073 95.0564 30.2787 95.0564 29.5286V29.2095H92.8727V29.5286C92.8727 30.2676 93.1228 31.001 93.5593 31.6225C93.3331 31.8622 93.068 32.0726 92.7704 32.244L93.0879 32.7965C93.4165 32.6084 93.7117 32.3774 93.9681 32.117C94.2229 32.3798 94.5166 32.6099 94.8413 32.7965L95.1588 32.244C94.8659 32.0765 94.6031 31.8654 94.3777 31.6249Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,3 @@
<svg width="2" height="19" viewBox="0 0 2 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.576513" d="M0.578029 0.00184827L0.508818 18.9982" stroke="white"/>
</svg>

After

Width:  |  Height:  |  Size: 185 B

View File

@@ -0,0 +1,3 @@
<svg width="372" height="1" viewBox="0 0 372 1" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0.25H372" stroke="#979797" stroke-width="0.5" stroke-dasharray="3"/>
</svg>

After

Width:  |  Height:  |  Size: 184 B

View File

@@ -0,0 +1,3 @@
<svg width="372" height="1" viewBox="0 0 372 1" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0.25H372" stroke="#979797" stroke-width="0.5" stroke-dasharray="3"/>
</svg>

After

Width:  |  Height:  |  Size: 184 B

View File

@@ -0,0 +1,3 @@
<svg width="344" height="1" viewBox="0 0 344 1" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0.250727H345.502" stroke="#979797" stroke-width="0.5" stroke-dasharray="3"/>
</svg>

After

Width:  |  Height:  |  Size: 192 B

View File

@@ -0,0 +1,3 @@
<svg width="344" height="1" viewBox="0 0 344 1" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0.250727H345.502" stroke="#979797" stroke-width="0.5" stroke-dasharray="3"/>
</svg>

After

Width:  |  Height:  |  Size: 192 B

View File

@@ -0,0 +1,3 @@
<svg width="331" height="1" viewBox="0 0 331 1" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.00024451 0.250359L331 0.579427" stroke="#979797" stroke-width="0.5" stroke-dasharray="3"/>
</svg>

After

Width:  |  Height:  |  Size: 206 B

Some files were not shown because too many files have changed in this diff Show More