Compare commits
8 Commits
3b3cef582e
...
3f6d10061d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f6d10061d | ||
|
|
bcd7af4e69 | ||
|
|
571eea0f66 | ||
|
|
6a9c52fe10 | ||
|
|
8e4429558c | ||
|
|
2fcfb452ec | ||
|
|
e410053ddd | ||
|
|
f32c04b8df |
8
.env
8
.env
@@ -6,6 +6,14 @@ DATABASE_PASSWORD=410491
|
||||
DATABASE_NAME=bidding
|
||||
DATABASE_SYNCHRONIZE=true
|
||||
|
||||
# Slave 数据库配置(用于数据同步)
|
||||
SLAVE_DATABASE_TYPE=mysql
|
||||
SLAVE_DATABASE_HOST=bj-cynosdbmysql-grp-r3a4c658.sql.tencentcdb.com
|
||||
SLAVE_DATABASE_PORT=21741
|
||||
SLAVE_DATABASE_USERNAME=root
|
||||
SLAVE_DATABASE_PASSWORD=}?cRa1f[,}`J
|
||||
SLAVE_DATABASE_NAME=bidding
|
||||
|
||||
# 代理配置(可选)
|
||||
PROXY_HOST=127.0.0.1
|
||||
PROXY_PORT=3211
|
||||
|
||||
@@ -6,6 +6,14 @@ DATABASE_PASSWORD=root
|
||||
DATABASE_NAME=bidding
|
||||
DATABASE_SYNCHRONIZE=true
|
||||
|
||||
# Slave 数据库配置(用于数据同步)
|
||||
SLAVE_DATABASE_TYPE=mariadb
|
||||
SLAVE_DATABASE_HOST=localhost
|
||||
SLAVE_DATABASE_PORT=3306
|
||||
SLAVE_DATABASE_USERNAME=root
|
||||
SLAVE_DATABASE_PASSWORD=root
|
||||
SLAVE_DATABASE_NAME=bidding_slave
|
||||
|
||||
# 代理配置(可选)
|
||||
PROXY_HOST=127.0.0.1
|
||||
PROXY_PORT=6000
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -4,4 +4,7 @@ dist
|
||||
public
|
||||
*.xls*
|
||||
pw-browsers
|
||||
logs
|
||||
logs
|
||||
build
|
||||
*.exe
|
||||
*.png
|
||||
402
README.md
402
README.md
@@ -1,106 +1,88 @@
|
||||
<p align="center">
|
||||
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
|
||||
</p>
|
||||
# 投标信息智能监控系统
|
||||
|
||||
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||
一个基于 TypeScript 的 Web 应用,用于自动爬取商务投标平台的最新信息,将符合条件的投标项目突出显示,为用户提供精准的投标信息监控服务。
|
||||
|
||||
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
|
||||
</p>
|
||||
<!--[](https://opencollective.com/nest#backer)
|
||||
[](https://opencollective.com/nest#sponsor)-->
|
||||
## 技术栈
|
||||
|
||||
## Description
|
||||
### 后端
|
||||
- **框架**: NestJS
|
||||
- **语言**: TypeScript
|
||||
- **数据库**: PostgreSQL (TypeORM)
|
||||
- **爬虫**: axios
|
||||
- **任务调度**: @nestjs/schedule
|
||||
- **AI 服务**: OpenAI API
|
||||
|
||||
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||
### 前端
|
||||
- **框架**: Vue.js 3
|
||||
- **构建工具**: Vite
|
||||
- **UI**: Tailwind CSS
|
||||
- **状态管理**: Pinia
|
||||
|
||||
## Project setup
|
||||
## 项目结构
|
||||
|
||||
```bash
|
||||
$ npm install
|
||||
```
|
||||
src/
|
||||
├── ai/ # AI 模块
|
||||
│ ├── ai.controller.ts
|
||||
│ ├── ai.service.ts
|
||||
│ ├── Prompt.ts
|
||||
│ └── entities/
|
||||
├── bids/ # 投标业务模块
|
||||
│ ├── controllers/
|
||||
│ ├── entities/
|
||||
│ └── services/
|
||||
├── crawler/ # 爬虫模块
|
||||
│ ├── services/
|
||||
│ │ ├── bid-crawler.service.ts
|
||||
│ │ ├── cdt_target.ts
|
||||
│ │ ├── chng_target.ts
|
||||
│ │ ├── ceic_target.ts
|
||||
│ │ ├── cgnpc_target.ts
|
||||
│ │ ├── chdtp_target.ts
|
||||
│ │ ├── cnncecp_target.ts
|
||||
│ │ ├── cnooc_target.ts
|
||||
│ │ ├── eps_target.ts
|
||||
│ │ ├── espic_target.ts
|
||||
│ │ ├── powerbeijing_target.ts
|
||||
│ │ ├── sdicc_target.ts
|
||||
│ │ └── szecp_target.ts
|
||||
│ └── entities/
|
||||
├── database/ # 数据库模块
|
||||
├── keywords/ # 关键词管理模块
|
||||
├── schedule/ # 定时任务
|
||||
│ └── tasks/
|
||||
│ └── bid-crawl.task.ts
|
||||
├── scripts/ # 脚本工具
|
||||
│ ├── ai-recommendations.ts
|
||||
│ ├── crawl.ts
|
||||
│ ├── deploy.ps1
|
||||
│ ├── remove-duplicates.ts
|
||||
│ └── update-source.ts
|
||||
└── common/ # 公共模块
|
||||
└── logger/
|
||||
|
||||
frontend/
|
||||
└── src/
|
||||
├── components/
|
||||
│ ├── Dashboard.vue
|
||||
│ ├── Dashboard-AI.vue
|
||||
│ ├── PinnedProject.vue
|
||||
│ ├── Bids.vue
|
||||
│ ├── Keywords.vue
|
||||
│ └── CrawlInfo.vue
|
||||
├── App.vue
|
||||
└── main.ts
|
||||
```
|
||||
|
||||
## Compile and run the project
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
# development
|
||||
$ npm run start
|
||||
### 1. 环境准备
|
||||
|
||||
# watch mode
|
||||
$ npm run start:dev
|
||||
确保已安装 Node.js (18+) 和 PostgreSQL。
|
||||
|
||||
# production mode
|
||||
$ npm run start:prod
|
||||
```
|
||||
### 2. 数据库配置
|
||||
|
||||
## Run tests
|
||||
|
||||
```bash
|
||||
# unit tests
|
||||
$ npm run test
|
||||
|
||||
# e2e tests
|
||||
$ npm run test:e2e
|
||||
|
||||
# test coverage
|
||||
$ npm run test:cov
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
|
||||
|
||||
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
|
||||
|
||||
```bash
|
||||
$ npm install -g @nestjs/mau
|
||||
$ mau deploy
|
||||
```
|
||||
|
||||
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
|
||||
|
||||
## Resources
|
||||
|
||||
Check out a few resources that may come in handy when working with NestJS:
|
||||
|
||||
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
|
||||
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
|
||||
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
|
||||
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
|
||||
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
|
||||
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
|
||||
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
|
||||
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
|
||||
|
||||
## Support
|
||||
|
||||
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||
|
||||
## Stay in touch
|
||||
|
||||
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
|
||||
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||
|
||||
## License
|
||||
|
||||
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
|
||||
|
||||
## How to Run
|
||||
|
||||
### 1. Database Setup
|
||||
Update the `.env` file with your PostgreSQL credentials:
|
||||
复制 `.env.example` 为 `.env` 并配置数据库连接:
|
||||
|
||||
```env
|
||||
DATABASE_TYPE=postgres
|
||||
@@ -110,73 +92,187 @@ DATABASE_USERNAME=your_username
|
||||
DATABASE_PASSWORD=your_password
|
||||
DATABASE_NAME=bidding
|
||||
DATABASE_SYNCHRONIZE=true
|
||||
```
|
||||
|
||||
### 2. Install Dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
cd frontend && npm install
|
||||
```
|
||||
|
||||
### 3. Build and Start
|
||||
|
||||
```bash
|
||||
# From the root directory
|
||||
cd frontend && npm run build
|
||||
cd ..
|
||||
npm run build
|
||||
npm run start
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### Frontend Features
|
||||
|
||||
- **Dashboard**: View high priority bids and today's bids
|
||||
- **Date Filtering**:
|
||||
- Click "3天" or "7天" buttons to filter bids from the last 3 or 7 days
|
||||
- The filter only limits the start date, showing all data from the selected start date onwards (including data newer than the end date)
|
||||
- **Keyword Filtering**: Filter bids by keywords (saved in localStorage)
|
||||
- **All Bids**: View all bids with pagination and source filtering
|
||||
- **Keyword Management**: Add and delete keywords with weight-based priority
|
||||
|
||||
### Backend Features
|
||||
|
||||
- **Multi-Source Crawling**: Crawls bidding information from multiple sources:
|
||||
- ChdtpCrawler
|
||||
- ChngCrawler
|
||||
- SzecpCrawler
|
||||
- CdtCrawler
|
||||
- EpsCrawler
|
||||
- CnncecpCrawler
|
||||
- CgnpcCrawler
|
||||
- CeicCrawler
|
||||
- EspicCrawler
|
||||
- PowerbeijingCrawler
|
||||
- **Automatic Retry**: If a crawler returns 0 items, it will be retried after all crawlers complete
|
||||
- **Proxy Support**: Configurable proxy settings via environment variables
|
||||
- **Scheduled Tasks**: Automatic crawling at scheduled intervals
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```env
|
||||
# Database
|
||||
DATABASE_TYPE=postgres
|
||||
DATABASE_HOST=localhost
|
||||
DATABASE_PORT=5432
|
||||
DATABASE_USERNAME=your_username
|
||||
DATABASE_PASSWORD=your_password
|
||||
DATABASE_NAME=bidding
|
||||
DATABASE_SYNCHRONIZE=true
|
||||
|
||||
# Proxy (optional)
|
||||
# 代理配置(可选)
|
||||
PROXY_HOST=your_proxy_host
|
||||
PROXY_PORT=your_proxy_port
|
||||
PROXY_USERNAME=your_proxy_username
|
||||
PROXY_PASSWORD=your_proxy_password
|
||||
|
||||
# AI 配置
|
||||
OPENAI_API_KEY=your_openai_api_key
|
||||
```
|
||||
|
||||
## Initial Setup
|
||||
### 3. 安装依赖
|
||||
|
||||
The system will automatically initialize with the preset keywords: "山东", "海", "建设", "工程", "采购". You can manage these and view crawled bidding information at http://localhost:3000.
|
||||
```bash
|
||||
# 安装后端依赖
|
||||
npm install
|
||||
|
||||
# 安装前端依赖
|
||||
cd frontend && npm install
|
||||
```
|
||||
|
||||
### 4. 运行项目
|
||||
|
||||
```bash
|
||||
# 开发模式 - 后端
|
||||
npm run start:dev
|
||||
|
||||
# 开发模式 - 前端
|
||||
cd frontend && npm run dev
|
||||
```
|
||||
|
||||
### 5. 构建生产版本
|
||||
|
||||
```bash
|
||||
# 构建前端
|
||||
cd frontend && npm run build
|
||||
|
||||
# 构建后端
|
||||
cd .. && npm run build
|
||||
|
||||
# 运行生产版本
|
||||
npm run start:prod
|
||||
```
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 智能爬虫模块
|
||||
|
||||
- **多源爬取**: 支持 12 个主流招标网站
|
||||
- 中国大唐集团电子商务平台 (CDT)
|
||||
- 中国华能集团有限公司电子商务平台 (CHNG)
|
||||
- 中国南方电网电子商务平台 (CSG)
|
||||
- 中国海洋石油集团有限公司 (CNOOC)
|
||||
- 中国华电集团有限公司电子商务平台 (CHDTP)
|
||||
- 国家能源投资集团有限责任公司 (CNNCECP)
|
||||
- 中国核工业集团有限公司 (CNNC)
|
||||
- 中国电力建设集团有限公司 (POWERCHINA)
|
||||
- 中国能源建设集团有限公司 (CEIC)
|
||||
- 中国石油天然气集团有限公司 (CNPC)
|
||||
- 国家电网有限公司 (SGCC)
|
||||
- 北京电力交易中心 (POWERBEIJING)
|
||||
|
||||
- **智能防封策略**:
|
||||
- 随机请求间隔 (3-8 秒)
|
||||
- 轮换 User-Agent
|
||||
- 异常检测与自动重试机制
|
||||
- 代理支持
|
||||
|
||||
- **定时任务**: 每 30 分钟自动执行爬取
|
||||
|
||||
### 数据处理与存储
|
||||
|
||||
- **数据模型**:
|
||||
- 投标项目标题
|
||||
- 详细页面 URL
|
||||
- 发布时间
|
||||
- 招标单位
|
||||
- 截止日期
|
||||
- 关键词匹配
|
||||
- 优先级评分
|
||||
|
||||
- **增量存储**:
|
||||
- 通过 URL 哈希值判断是否为新数据
|
||||
- 仅存储当天和最近 7 天的历史数据
|
||||
- 每日自动清理 30 天前的数据
|
||||
|
||||
### 关键词智能监控
|
||||
|
||||
- **预设关键词**: "山东", "海", "建设", "工程", "采购"
|
||||
- **自定义关键词**: 通过 Web 界面添加/删除关键词
|
||||
- **权重设置**: 可设置关键词权重 (1-5 级)
|
||||
- **匹配逻辑**:
|
||||
- 标题完全匹配和部分匹配
|
||||
- 多关键词叠加权重
|
||||
- 支持正则表达式高级匹配
|
||||
|
||||
### AI 智能推荐
|
||||
|
||||
- **智能分析**: 使用 AI 分析投标信息的相关性
|
||||
- **推荐评分**: 基于关键词匹配和内容分析生成推荐评分
|
||||
- **智能摘要**: 自动生成投标信息摘要
|
||||
|
||||
### Web 展示界面
|
||||
|
||||
- **仪表盘**:
|
||||
- 高优先级投标信息(匹配自定义关键词)
|
||||
- 今日新增投标列表(按时间倒序)
|
||||
- AI 推荐投标信息
|
||||
- 置顶项目
|
||||
|
||||
- **交互功能**:
|
||||
- 关键词管理面板
|
||||
- 按日期/来源/关键词筛选
|
||||
- 信息标记已读/未读状态
|
||||
- 项目置顶功能
|
||||
- 爬取信息查看
|
||||
|
||||
- **响应式设计**: 适配桌面和移动设备
|
||||
|
||||
## API 接口
|
||||
|
||||
### 投标信息
|
||||
- `GET /api/bids` - 获取投标列表(支持分页、筛选)
|
||||
- `GET /api/bids/high-priority` - 获取高优先级投标
|
||||
- `GET /api/bids/today` - 获取今日投标
|
||||
|
||||
### 关键词管理
|
||||
- `GET /api/keywords` - 获取所有关键词
|
||||
- `POST /api/keywords` - 添加新关键词
|
||||
- `DELETE /api/keywords/:id` - 删除关键词
|
||||
|
||||
### AI 服务
|
||||
- `GET /api/ai/recommendations` - 获取 AI 推荐投标
|
||||
- `POST /api/ai/analyze` - 分析投标信息
|
||||
|
||||
### 爬虫管理
|
||||
- `GET /api/crawler/info` - 获取爬取信息
|
||||
- `POST /api/crawler/trigger` - 手动触发爬取
|
||||
|
||||
## 前端路由
|
||||
|
||||
- `/` - 仪表盘(默认页面)
|
||||
- `/bids` - 全部投标信息
|
||||
- `/keywords` - 关键词管理
|
||||
- `/ai` - AI 推荐页面
|
||||
- `/crawl-info` - 爬取信息
|
||||
|
||||
## 测试
|
||||
|
||||
```bash
|
||||
# 单元测试
|
||||
npm run test
|
||||
|
||||
# E2E 测试
|
||||
npm run test:e2e
|
||||
|
||||
# 测试覆盖率
|
||||
npm run test:cov
|
||||
```
|
||||
|
||||
## 部署
|
||||
|
||||
项目包含 PowerShell 部署脚本 `src/scripts/deploy.ps1`,用于自动化部署流程。
|
||||
|
||||
## 环境变量
|
||||
|
||||
| 变量名 | 说明 | 默认值 |
|
||||
|--------|------|--------|
|
||||
| DATABASE_TYPE | 数据库类型 | postgres |
|
||||
| DATABASE_HOST | 数据库主机 | localhost |
|
||||
| DATABASE_PORT | 数据库端口 | 5432 |
|
||||
| DATABASE_USERNAME | 数据库用户名 | - |
|
||||
| DATABASE_PASSWORD | 数据库密码 | - |
|
||||
| DATABASE_NAME | 数据库名称 | bidding |
|
||||
| DATABASE_SYNCHRONIZE | 自动同步数据库 | true |
|
||||
| PROXY_HOST | 代理主机 | - |
|
||||
| PROXY_PORT | 代理端口 | - |
|
||||
| PROXY_USERNAME | 代理用户名 | - |
|
||||
| PROXY_PASSWORD | 代理密码 | - |
|
||||
| OPENAI_API_KEY | OpenAI API 密钥 | - |
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT
|
||||
|
||||
858
frontend/package-lock.json
generated
858
frontend/package-lock.json
generated
@@ -11,17 +11,35 @@
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"axios": "^1.13.2",
|
||||
"element-plus": "^2.13.1",
|
||||
"openai": "^6.16.0",
|
||||
"vue": "^3.5.24"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@types/node": "^24.10.1",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"autoprefixer": "^10.4.23",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^7.2.4",
|
||||
"vue-tsc": "^3.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
@@ -553,12 +571,55 @@
|
||||
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/remapping": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"name": "@sxzz/popperjs-es",
|
||||
"version": "2.11.7",
|
||||
@@ -927,6 +988,277 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@tailwindcss/node/-/node-4.1.18.tgz",
|
||||
"integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
"enhanced-resolve": "^5.18.3",
|
||||
"jiti": "^2.6.1",
|
||||
"lightningcss": "1.30.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"source-map-js": "^1.2.1",
|
||||
"tailwindcss": "4.1.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@tailwindcss/oxide/-/oxide-4.1.18.tgz",
|
||||
"integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tailwindcss/oxide-android-arm64": "4.1.18",
|
||||
"@tailwindcss/oxide-darwin-arm64": "4.1.18",
|
||||
"@tailwindcss/oxide-darwin-x64": "4.1.18",
|
||||
"@tailwindcss/oxide-freebsd-x64": "4.1.18",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.18",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "4.1.18",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "4.1.18",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "4.1.18",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "4.1.18",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.18",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz",
|
||||
"integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz",
|
||||
"integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz",
|
||||
"integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz",
|
||||
"integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz",
|
||||
"integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz",
|
||||
"integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz",
|
||||
"integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz",
|
||||
"integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz",
|
||||
"integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz",
|
||||
"integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==",
|
||||
"bundleDependencies": [
|
||||
"@napi-rs/wasm-runtime",
|
||||
"@emnapi/core",
|
||||
"@emnapi/runtime",
|
||||
"@tybys/wasm-util",
|
||||
"@emnapi/wasi-threads",
|
||||
"tslib"
|
||||
],
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1",
|
||||
"@emnapi/wasi-threads": "^1.1.0",
|
||||
"@napi-rs/wasm-runtime": "^1.1.0",
|
||||
"@tybys/wasm-util": "^0.10.1",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
|
||||
"integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz",
|
||||
"integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/postcss": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@tailwindcss/postcss/-/postcss-4.1.18.tgz",
|
||||
"integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@tailwindcss/node": "4.1.18",
|
||||
"@tailwindcss/oxide": "4.1.18",
|
||||
"postcss": "^8.4.41",
|
||||
"tailwindcss": "4.1.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@types/estree/-/estree-1.0.8.tgz",
|
||||
@@ -1255,6 +1587,43 @@
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.4.23",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/autoprefixer/-/autoprefixer-10.4.23.tgz",
|
||||
"integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browserslist": "^4.28.1",
|
||||
"caniuse-lite": "^1.0.30001760",
|
||||
"fraction.js": "^5.3.4",
|
||||
"picocolors": "^1.1.1",
|
||||
"postcss-value-parser": "^4.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"autoprefixer": "bin/autoprefixer"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/axios/-/axios-1.13.2.tgz",
|
||||
@@ -1266,6 +1635,51 @@
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.9.14",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz",
|
||||
"integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"baseline-browser-mapping": "dist/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/browserslist/-/browserslist-4.28.1.tgz",
|
||||
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
"electron-to-chromium": "^1.5.263",
|
||||
"node-releases": "^2.0.27",
|
||||
"update-browserslist-db": "^1.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
@@ -1279,6 +1693,27 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001764",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz",
|
||||
"integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
@@ -1312,6 +1747,16 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -1326,6 +1771,13 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.267",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
|
||||
"integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/element-plus": {
|
||||
"version": "2.13.1",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/element-plus/-/element-plus-2.13.1.tgz",
|
||||
@@ -1351,6 +1803,20 @@
|
||||
"vue": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.18.4",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz",
|
||||
"integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
"tapable": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/entities/-/entities-7.0.0.tgz",
|
||||
@@ -1450,6 +1916,16 @@
|
||||
"@esbuild/win32-x64": "0.27.2"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
@@ -1510,6 +1986,20 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fraction.js": {
|
||||
"version": "5.3.4",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/fraction.js/-/fraction.js-5.3.4.tgz",
|
||||
"integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/rawify"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@@ -1583,6 +2073,13 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
@@ -1622,6 +2119,279 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/jiti/-/jiti-2.6.1.tgz",
|
||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/lightningcss/-/lightningcss-1.30.2.tgz",
|
||||
"integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"lightningcss-android-arm64": "1.30.2",
|
||||
"lightningcss-darwin-arm64": "1.30.2",
|
||||
"lightningcss-darwin-x64": "1.30.2",
|
||||
"lightningcss-freebsd-x64": "1.30.2",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.30.2",
|
||||
"lightningcss-linux-arm64-gnu": "1.30.2",
|
||||
"lightningcss-linux-arm64-musl": "1.30.2",
|
||||
"lightningcss-linux-x64-gnu": "1.30.2",
|
||||
"lightningcss-linux-x64-musl": "1.30.2",
|
||||
"lightningcss-win32-arm64-msvc": "1.30.2",
|
||||
"lightningcss-win32-x64-msvc": "1.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-android-arm64": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
|
||||
"integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-arm64": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
|
||||
"integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-x64": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
|
||||
"integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-freebsd-x64": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
|
||||
"integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
|
||||
"integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
|
||||
"integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-musl": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
|
||||
"integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-gnu": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
|
||||
"integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-musl": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
|
||||
"integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
|
||||
"integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-x64-msvc": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
|
||||
"integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/lodash/-/lodash-4.17.21.tgz",
|
||||
@@ -1717,12 +2487,40 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.27",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/node-releases/-/node-releases-2.0.27.tgz",
|
||||
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/normalize-wheel-es": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
|
||||
"integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/openai": {
|
||||
"version": "6.16.0",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/openai/-/openai-6.16.0.tgz",
|
||||
"integrity": "sha512-fZ1uBqjFUjXzbGc35fFtYKEOxd20kd9fDpFeqWtsOZWiubY8CZ1NAlXHW3iathaFvqmNtCWMIsosCuyeI7Joxg==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"openai": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.25 || ^4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ws": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/path-browserify": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/path-browserify/-/path-browserify-1.0.1.tgz",
|
||||
@@ -1769,6 +2567,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -1778,6 +2577,13 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-value-parser": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
@@ -1838,6 +2644,27 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
||||
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/tapable/-/tapable-2.3.0.tgz",
|
||||
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
@@ -1877,6 +2704,37 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
||||
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"escalade": "^3.2.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"update-browserslist-db": "cli.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/vite/-/vite-7.3.1.tgz",
|
||||
|
||||
@@ -37,6 +37,20 @@
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="crawlSingleSource(row.source)"
|
||||
:loading="crawlingSources.has(row.source)"
|
||||
:disabled="crawlingSources.has(row.source)"
|
||||
>
|
||||
<el-icon><Refresh /></el-icon>
|
||||
更新
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="summary" v-if="crawlStats.length > 0">
|
||||
@@ -75,6 +89,7 @@ interface CrawlStat {
|
||||
|
||||
const crawlStats = ref<CrawlStat[]>([])
|
||||
const loading = ref(false)
|
||||
const crawlingSources = ref<Set<string>>(new Set())
|
||||
|
||||
const totalCount = computed(() => {
|
||||
return crawlStats.value.reduce((sum, item) => sum + item.count, 0)
|
||||
@@ -113,6 +128,28 @@ const fetchCrawlStats = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const crawlSingleSource = async (sourceName: string) => {
|
||||
crawlingSources.value.add(sourceName)
|
||||
try {
|
||||
ElMessage.info(`正在更新 ${sourceName}...`)
|
||||
const res = await axios.post(`/api/crawler/crawl/${encodeURIComponent(sourceName)}`)
|
||||
|
||||
if (res.data.success) {
|
||||
ElMessage.success(`${sourceName} 更新成功,获取 ${res.data.count} 条数据`)
|
||||
} else {
|
||||
ElMessage.error(`${sourceName} 更新失败: ${res.data.error || '未知错误'}`)
|
||||
}
|
||||
|
||||
// 刷新统计数据
|
||||
await fetchCrawlStats()
|
||||
} catch (error) {
|
||||
console.error('Failed to crawl single source:', error)
|
||||
ElMessage.error(`${sourceName} 更新失败`)
|
||||
} finally {
|
||||
crawlingSources.value.delete(sourceName)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchCrawlStats()
|
||||
})
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
import { ref, watch } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { MagicStick, Loading, InfoFilled, List, ArrowDown, Paperclip } from '@element-plus/icons-vue'
|
||||
import { MagicStick, Loading, InfoFilled, List, Paperclip } from '@element-plus/icons-vue'
|
||||
import PinnedProject from './PinnedProject.vue'
|
||||
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Refresh, ArrowDown, Paperclip } from '@element-plus/icons-vue'
|
||||
import { Refresh, Paperclip } from '@element-plus/icons-vue'
|
||||
import PinnedProject from './PinnedProject.vue'
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { Controller, Post, Get } from '@nestjs/common';
|
||||
import { Controller, Post, Get, Param, Body } from '@nestjs/common';
|
||||
import { BidCrawlerService } from './services/bid-crawler.service';
|
||||
|
||||
@Controller('api/crawler')
|
||||
export class CrawlerController {
|
||||
private isCrawling = false;
|
||||
private crawlingSources = new Set<string>();
|
||||
|
||||
constructor(private readonly crawlerService: BidCrawlerService) {}
|
||||
|
||||
@Get('status')
|
||||
getStatus() {
|
||||
return { isCrawling: this.isCrawling };
|
||||
return {
|
||||
isCrawling: this.isCrawling,
|
||||
crawlingSources: Array.from(this.crawlingSources)
|
||||
};
|
||||
}
|
||||
|
||||
@Post('run')
|
||||
@@ -20,12 +24,12 @@ export class CrawlerController {
|
||||
|
||||
this.isCrawling = true;
|
||||
|
||||
// We don't await this because we want it to run in the background
|
||||
// We don't await this because we want it to run in the background
|
||||
// and return immediately, or we can await if we want to user to wait.
|
||||
// Given the requirement "Immediate Crawl", usually implies triggering it.
|
||||
// However, for a better UI experience, we might want to wait or just trigger.
|
||||
// Let's await it so that user knows when it's done (or failed),
|
||||
// assuming it doesn't take too long for the mock.
|
||||
// Let's await it so that user knows when it's done (or failed),
|
||||
// assuming it doesn't take too long for the mock.
|
||||
// Real crawling might take long, so background is better.
|
||||
// For this prototype, I'll await it to show completion.
|
||||
try {
|
||||
@@ -35,4 +39,20 @@ export class CrawlerController {
|
||||
this.isCrawling = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Post('crawl/:sourceName')
|
||||
async crawlSingleSource(@Param('sourceName') sourceName: string) {
|
||||
if (this.crawlingSources.has(sourceName)) {
|
||||
return { message: `Source ${sourceName} is already being crawled` };
|
||||
}
|
||||
|
||||
this.crawlingSources.add(sourceName);
|
||||
|
||||
try {
|
||||
const result = await this.crawlerService.crawlSingleSource(sourceName);
|
||||
return result;
|
||||
} finally {
|
||||
this.crawlingSources.delete(sourceName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,6 +216,97 @@ export class BidCrawlerService {
|
||||
}
|
||||
}
|
||||
|
||||
async crawlSingleSource(sourceName: string) {
|
||||
this.logger.log(`Starting single source crawl for: ${sourceName}`);
|
||||
|
||||
// 从环境变量读取代理配置
|
||||
const proxyHost = this.configService.get<string>('PROXY_HOST');
|
||||
const proxyPort = this.configService.get<string>('PROXY_PORT');
|
||||
const proxyUsername = this.configService.get<string>('PROXY_USERNAME');
|
||||
const proxyPassword = this.configService.get<string>('PROXY_PASSWORD');
|
||||
|
||||
// 构建代理参数
|
||||
const args = [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-blink-features=AutomationControlled',
|
||||
'--disable-infobars',
|
||||
'--window-position=0,0',
|
||||
'--ignore-certifcate-errors',
|
||||
'--ignore-certifcate-errors-spki-list',
|
||||
];
|
||||
|
||||
if (proxyHost && proxyPort) {
|
||||
const proxyUrl = proxyUsername && proxyPassword
|
||||
? `http://${proxyUsername}:${proxyPassword}@${proxyHost}:${proxyPort}`
|
||||
: `http://${proxyHost}:${proxyPort}`;
|
||||
args.push(`--proxy-server=${proxyUrl}`);
|
||||
this.logger.log(`Using proxy: ${proxyHost}:${proxyPort}`);
|
||||
}
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: false,
|
||||
args,
|
||||
});
|
||||
|
||||
const crawlers = [ChdtpCrawler, ChngCrawler, SzecpCrawler, CdtCrawler, EpsCrawler, CnncecpCrawler, CgnpcCrawler, CeicCrawler, EspicCrawler, PowerbeijingCrawler, SdiccCrawler, CnoocCrawler];
|
||||
|
||||
const targetCrawler = crawlers.find(c => c.name === sourceName);
|
||||
|
||||
if (!targetCrawler) {
|
||||
await browser.close();
|
||||
throw new Error(`Crawler not found for source: ${sourceName}`);
|
||||
}
|
||||
|
||||
try {
|
||||
this.logger.log(`Crawling: ${targetCrawler.name}`);
|
||||
|
||||
const results = await targetCrawler.crawl(browser);
|
||||
this.logger.log(`Extracted ${results.length} items from ${targetCrawler.name}`);
|
||||
|
||||
// 获取最新的发布日期
|
||||
const latestPublishDate = results.length > 0
|
||||
? results.reduce((latest, item) => {
|
||||
const itemDate = new Date(item.publishDate);
|
||||
return itemDate > latest ? itemDate : latest;
|
||||
}, new Date(0))
|
||||
: null;
|
||||
|
||||
for (const item of results) {
|
||||
await this.bidsService.createOrUpdate({
|
||||
title: item.title,
|
||||
url: item.url,
|
||||
publishDate: item.publishDate,
|
||||
source: targetCrawler.name,
|
||||
});
|
||||
}
|
||||
|
||||
// 保存爬虫统计信息到数据库
|
||||
await this.saveCrawlInfo(targetCrawler.name, results.length, latestPublishDate);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
source: targetCrawler.name,
|
||||
count: results.length,
|
||||
latestPublishDate,
|
||||
};
|
||||
} catch (err) {
|
||||
this.logger.error(`Error crawling ${targetCrawler.name}: ${err.message}`);
|
||||
|
||||
// 保存错误信息到数据库
|
||||
await this.saveCrawlInfo(targetCrawler.name, 0, null, err.message);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
source: targetCrawler.name,
|
||||
count: 0,
|
||||
error: err.message,
|
||||
};
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
private async saveCrawlInfo(
|
||||
source: string,
|
||||
count: number,
|
||||
|
||||
@@ -4,53 +4,75 @@ import { ChdtpResult } from './chdtp_target';
|
||||
|
||||
// 模拟人类鼠标移动
|
||||
async function simulateHumanMouseMovement(page: puppeteer.Page) {
|
||||
const viewport = page.viewport();
|
||||
if (!viewport) return;
|
||||
try {
|
||||
const viewport = page.viewport();
|
||||
if (!viewport) return;
|
||||
|
||||
const movements = 5 + Math.floor(Math.random() * 5); // 5-10次随机移动
|
||||
const movements = 5 + Math.floor(Math.random() * 5); // 5-10次随机移动
|
||||
|
||||
for (let i = 0; i < movements; i++) {
|
||||
const x = Math.floor(Math.random() * viewport.width);
|
||||
const y = Math.floor(Math.random() * viewport.height);
|
||||
|
||||
await page.mouse.move(x, y, {
|
||||
steps: 10 + Math.floor(Math.random() * 20) // 10-30步,使移动更平滑
|
||||
});
|
||||
|
||||
// 随机停顿 100-500ms
|
||||
await new Promise(r => setTimeout(r, 100 + Math.random() * 400));
|
||||
for (let i = 0; i < movements; i++) {
|
||||
// 检查页面是否仍然有效
|
||||
if (page.isClosed()) {
|
||||
console.log('Page was closed during mouse movement simulation');
|
||||
return;
|
||||
}
|
||||
|
||||
const x = Math.floor(Math.random() * viewport.width);
|
||||
const y = Math.floor(Math.random() * viewport.height);
|
||||
|
||||
await page.mouse.move(x, y, {
|
||||
steps: 10 + Math.floor(Math.random() * 20) // 10-30步,使移动更平滑
|
||||
});
|
||||
|
||||
// 随机停顿 100-500ms
|
||||
await new Promise(r => setTimeout(r, 100 + Math.random() * 400));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Mouse movement simulation interrupted:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟人类滚动
|
||||
async function simulateHumanScrolling(page: puppeteer.Page) {
|
||||
const scrollCount = 3 + Math.floor(Math.random() * 5); // 3-7次滚动
|
||||
try {
|
||||
const scrollCount = 3 + Math.floor(Math.random() * 5); // 3-7次滚动
|
||||
|
||||
for (let i = 0; i < scrollCount; i++) {
|
||||
const scrollDistance = 100 + Math.floor(Math.random() * 400); // 100-500px
|
||||
|
||||
await page.evaluate((distance) => {
|
||||
window.scrollBy({
|
||||
top: distance,
|
||||
behavior: 'smooth'
|
||||
for (let i = 0; i < scrollCount; i++) {
|
||||
// 检查页面是否仍然有效
|
||||
if (page.isClosed()) {
|
||||
console.log('Page was closed during scrolling simulation');
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollDistance = 100 + Math.floor(Math.random() * 400); // 100-500px
|
||||
|
||||
await page.evaluate((distance) => {
|
||||
window.scrollBy({
|
||||
top: distance,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}, scrollDistance);
|
||||
|
||||
// 随机停顿 500-1500ms
|
||||
await new Promise(r => setTimeout(r, 500 + Math.random() * 1000));
|
||||
}
|
||||
|
||||
// 滚动回顶部
|
||||
if (!page.isClosed()) {
|
||||
await page.evaluate(() => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
}, scrollDistance);
|
||||
|
||||
// 随机停顿 500-1500ms
|
||||
await new Promise(r => setTimeout(r, 500 + Math.random() * 1000));
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Scrolling simulation interrupted:', error.message);
|
||||
}
|
||||
|
||||
// 滚动回顶部
|
||||
await page.evaluate(() => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
}
|
||||
|
||||
export const ChngCrawler = {
|
||||
name: '华能集团电子商务平台',
|
||||
url: 'https://ec.chng.com.cn/ecmall/index.html#/purchase/home?top=0',
|
||||
baseUrl: 'https://ec.chng.com.cn/ecmall/index.html',
|
||||
url: 'https://ec.chng.com.cn/channel/home/#/purchase?top=0',
|
||||
baseUrl: 'https://ec.chng.com.cn/channel/home/#',
|
||||
|
||||
async crawl(browser: puppeteer.Browser): Promise<ChdtpResult[]> {
|
||||
const logger = new Logger('ChngCrawler');
|
||||
@@ -106,14 +128,16 @@ export const ChngCrawler = {
|
||||
await page.authenticate({ username, password });
|
||||
}
|
||||
}
|
||||
// 模拟人类行为
|
||||
// 模拟人类行为
|
||||
logger.log('Simulating human mouse movements...');
|
||||
await simulateHumanMouseMovement(page);
|
||||
|
||||
logger.log('Simulating human scrolling...');
|
||||
await simulateHumanScrolling(page);
|
||||
|
||||
await page.waitForNavigation({ waitUntil: 'domcontentloaded' }).catch(() => {});
|
||||
// 等待页面稳定,不强制等待导航
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
|
||||
// 模拟人类行为
|
||||
logger.log('Simulating human mouse movements...');
|
||||
await simulateHumanMouseMovement(page);
|
||||
@@ -215,8 +239,24 @@ export const ChngCrawler = {
|
||||
const nextButton = await page.$('svg[data-icon="right"]');
|
||||
if (!nextButton) break;
|
||||
|
||||
// 点击下一页前保存当前页面状态
|
||||
const currentUrl = page.url();
|
||||
|
||||
await nextButton.click();
|
||||
await new Promise(r => setTimeout(r, 5000));
|
||||
|
||||
// 等待页面导航完成
|
||||
try {
|
||||
await page.waitForFunction(
|
||||
(oldUrl) => window.location.href !== oldUrl,
|
||||
{ timeout: 10000 },
|
||||
currentUrl
|
||||
);
|
||||
} catch (e) {
|
||||
logger.warn('Navigation timeout, continuing anyway');
|
||||
}
|
||||
|
||||
// 等待页面内容加载
|
||||
await new Promise(r => setTimeout(r, 15000));
|
||||
currentPage++;
|
||||
}
|
||||
|
||||
|
||||
252
src/scripts/sync.ts
Normal file
252
src/scripts/sync.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import 'dotenv/config';
|
||||
import { DataSource, DataSourceOptions } from 'typeorm';
|
||||
import mysql from 'mysql2/promise';
|
||||
import { BidItem } from '../bids/entities/bid-item.entity';
|
||||
import { Keyword } from '../keywords/keyword.entity';
|
||||
import { AiRecommendation } from '../ai/entities/ai-recommendation.entity';
|
||||
import { CrawlInfoAdd } from '../crawler/entities/crawl-info-add.entity';
|
||||
|
||||
// 主数据库配置
|
||||
const masterDbConfig: DataSourceOptions = {
|
||||
type: process.env.DATABASE_TYPE as any || 'mariadb',
|
||||
host: process.env.DATABASE_HOST || 'localhost',
|
||||
port: parseInt(process.env.DATABASE_PORT || '3306'),
|
||||
username: process.env.DATABASE_USERNAME || 'root',
|
||||
password: process.env.DATABASE_PASSWORD || 'root',
|
||||
database: process.env.DATABASE_NAME || 'bidding',
|
||||
entities: [BidItem, Keyword, AiRecommendation, CrawlInfoAdd],
|
||||
synchronize: false,
|
||||
};
|
||||
|
||||
// Slave 数据库配置
|
||||
const slaveDbConfig: DataSourceOptions = {
|
||||
type: process.env.SLAVE_DATABASE_TYPE as any || 'mariadb',
|
||||
host: process.env.SLAVE_DATABASE_HOST || 'localhost',
|
||||
port: parseInt(process.env.SLAVE_DATABASE_PORT || '3306'),
|
||||
username: process.env.SLAVE_DATABASE_USERNAME || 'root',
|
||||
password: process.env.SLAVE_DATABASE_PASSWORD || 'root',
|
||||
database: process.env.SLAVE_DATABASE_NAME || 'bidding_slave',
|
||||
entities: [BidItem, Keyword, AiRecommendation, CrawlInfoAdd],
|
||||
synchronize: false,
|
||||
};
|
||||
|
||||
// 日志工具
|
||||
const logger = {
|
||||
log: (message: string, ...args: any[]) => {
|
||||
console.log(`[INFO] ${new Date().toISOString()} - ${message}`, ...args);
|
||||
},
|
||||
error: (message: string, ...args: any[]) => {
|
||||
console.error(`[ERROR] ${new Date().toISOString()} - ${message}`, ...args);
|
||||
},
|
||||
warn: (message: string, ...args: any[]) => {
|
||||
console.warn(`[WARN] ${new Date().toISOString()} - ${message}`, ...args);
|
||||
},
|
||||
};
|
||||
|
||||
// 同步单个表的数据
|
||||
async function syncTable<T>(
|
||||
masterDataSource: DataSource,
|
||||
slaveDataSource: DataSource,
|
||||
entityClass: any,
|
||||
tableName: string,
|
||||
): Promise<number> {
|
||||
const masterRepo = masterDataSource.getRepository(entityClass);
|
||||
const slaveRepo = slaveDataSource.getRepository(entityClass);
|
||||
|
||||
logger.log(`开始同步表: ${tableName}`);
|
||||
|
||||
// 从主数据库获取所有数据
|
||||
const masterData = await masterRepo.find();
|
||||
logger.log(`主数据库 ${tableName} 表中有 ${masterData.length} 条记录`);
|
||||
|
||||
// 从 slave 数据库获取所有数据
|
||||
const slaveData = await slaveRepo.find();
|
||||
logger.log(`Slave 数据库 ${tableName} 表中有 ${slaveData.length} 条记录`);
|
||||
|
||||
// 创建主数据库记录的 ID 集合
|
||||
const masterIds = new Set(masterData.map((item: any) => item.id));
|
||||
|
||||
// 删除 slave 数据库中不存在于主数据库的记录
|
||||
const toDelete = slaveData.filter((item: any) => !masterIds.has(item.id));
|
||||
if (toDelete.length > 0) {
|
||||
await slaveRepo.remove(toDelete);
|
||||
logger.log(`从 slave 数据库删除了 ${toDelete.length} 条 ${tableName} 记录`);
|
||||
}
|
||||
|
||||
// 同步数据:使用 save 方法进行 upsert 操作
|
||||
let syncedCount = 0;
|
||||
for (const item of masterData) {
|
||||
await slaveRepo.save(item);
|
||||
syncedCount++;
|
||||
}
|
||||
|
||||
logger.log(`成功同步 ${syncedCount} 条 ${tableName} 记录到 slave 数据库`);
|
||||
|
||||
return syncedCount;
|
||||
}
|
||||
|
||||
// 创建数据库(如果不存在)
|
||||
async function createDatabaseIfNotExists(config: DataSourceOptions) {
|
||||
const connection = await mysql.createConnection({
|
||||
host: (config as any).host,
|
||||
port: (config as any).port,
|
||||
user: (config as any).username,
|
||||
password: (config as any).password,
|
||||
});
|
||||
|
||||
await connection.query(`CREATE DATABASE IF NOT EXISTS \`${(config as any).database}\``);
|
||||
await connection.end();
|
||||
}
|
||||
|
||||
// 同步表结构
|
||||
async function syncSchema(masterDataSource: DataSource, slaveDataSource: DataSource): Promise<DataSource> {
|
||||
logger.log('开始同步表结构...');
|
||||
|
||||
// 获取主数据库的所有表
|
||||
const tables = await masterDataSource.query(`
|
||||
SELECT TABLE_NAME
|
||||
FROM INFORMATION_SCHEMA.TABLES
|
||||
WHERE TABLE_SCHEMA = '${(masterDbConfig as any).database}'
|
||||
`);
|
||||
|
||||
for (const table of tables) {
|
||||
const tableName = table.TABLE_NAME;
|
||||
logger.log(`同步表结构: ${tableName}`);
|
||||
|
||||
// 获取主数据库的建表语句
|
||||
const createTableResult = await masterDataSource.query(`
|
||||
SHOW CREATE TABLE \`${tableName}\`
|
||||
`);
|
||||
|
||||
let createTableSql = createTableResult[0]['Create Table'];
|
||||
|
||||
// 转换 MariaDB 语法到 MySQL 语法
|
||||
// 将 uuid 类型转换为 CHAR(36)
|
||||
createTableSql = createTableSql.replace(/\buuid\b/gi, 'CHAR(36)');
|
||||
|
||||
// 检查 slave 数据库中是否存在该表
|
||||
const tableExists = await slaveDataSource.query(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM INFORMATION_SCHEMA.TABLES
|
||||
WHERE TABLE_SCHEMA = '${(slaveDbConfig as any).database}'
|
||||
AND TABLE_NAME = '${tableName}'
|
||||
`);
|
||||
|
||||
const tempTableName = `temp_${tableName}_${Date.now()}`;
|
||||
|
||||
if (tableExists[0].count > 0) {
|
||||
// 表存在,先备份数据到临时表
|
||||
logger.log(`备份表 ${tableName} 的数据到 ${tempTableName}...`);
|
||||
await slaveDataSource.query(`CREATE TABLE ${tempTableName} AS SELECT * FROM \`${tableName}\``);
|
||||
logger.log(`备份完成,共备份 ${await slaveDataSource.query(`SELECT COUNT(*) as count FROM ${tempTableName}`).then(r => r[0].count)} 条记录`);
|
||||
}
|
||||
|
||||
// 删除 slave 数据库中的表(如果存在)
|
||||
await slaveDataSource.query(`DROP TABLE IF EXISTS \`${tableName}\``);
|
||||
|
||||
// 在 slave 数据库中创建表
|
||||
await slaveDataSource.query(createTableSql);
|
||||
|
||||
// 如果之前有备份数据,尝试恢复
|
||||
if (tableExists[0].count > 0) {
|
||||
try {
|
||||
logger.log(`从 ${tempTableName} 恢复数据到 ${tableName}...`);
|
||||
|
||||
// 获取临时表的列名
|
||||
const columns = await slaveDataSource.query(`
|
||||
SELECT COLUMN_NAME
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = '${(slaveDbConfig as any).database}'
|
||||
AND TABLE_NAME = '${tempTableName}'
|
||||
`);
|
||||
|
||||
const columnNames = columns.map((c: any) => `\`${c.COLUMN_NAME}\``).join(', ');
|
||||
|
||||
// 将数据从临时表插入到新表
|
||||
await slaveDataSource.query(`
|
||||
INSERT INTO \`${tableName}\` (${columnNames})
|
||||
SELECT ${columnNames} FROM ${tempTableName}
|
||||
`);
|
||||
|
||||
const restoredCount = await slaveDataSource.query(`SELECT COUNT(*) as count FROM \`${tableName}\``);
|
||||
logger.log(`数据恢复完成,共恢复 ${restoredCount[0].count} 条记录`);
|
||||
|
||||
// 删除临时表
|
||||
await slaveDataSource.query(`DROP TABLE IF EXISTS ${tempTableName}`);
|
||||
} catch (error) {
|
||||
logger.warn(`恢复数据失败: ${error.message}`);
|
||||
logger.warn(`临时表 ${tempTableName} 保留,请手动处理`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.log('表结构同步完成');
|
||||
|
||||
// 重新初始化 slave 数据库连接以清除 TypeORM 元数据缓存
|
||||
logger.log('重新初始化 slave 数据库连接...');
|
||||
await slaveDataSource.destroy();
|
||||
await slaveDataSource.initialize();
|
||||
logger.log('Slave 数据库连接重新初始化完成');
|
||||
|
||||
return slaveDataSource;
|
||||
}
|
||||
|
||||
// 主同步函数
|
||||
async function syncDatabase() {
|
||||
let masterDataSource: DataSource | null = null;
|
||||
let slaveDataSource: DataSource | null = null;
|
||||
|
||||
try {
|
||||
logger.log('开始数据库同步...');
|
||||
|
||||
// 创建 slave 数据库(如果不存在)
|
||||
logger.log('检查并创建 slave 数据库...');
|
||||
await createDatabaseIfNotExists(slaveDbConfig);
|
||||
logger.log('Slave 数据库准备就绪');
|
||||
|
||||
// 创建主数据库连接
|
||||
masterDataSource = new DataSource(masterDbConfig);
|
||||
await masterDataSource.initialize();
|
||||
logger.log('主数据库连接成功');
|
||||
|
||||
// 创建 slave 数据库连接
|
||||
slaveDataSource = new DataSource(slaveDbConfig);
|
||||
await slaveDataSource.initialize();
|
||||
logger.log('Slave 数据库连接成功');
|
||||
|
||||
// 同步表结构
|
||||
slaveDataSource = await syncSchema(masterDataSource, slaveDataSource);
|
||||
|
||||
// 同步各个表
|
||||
const tables = [
|
||||
{ entity: BidItem, name: 'bid_items' },
|
||||
{ entity: Keyword, name: 'keywords' },
|
||||
{ entity: AiRecommendation, name: 'ai_recommendations' },
|
||||
{ entity: CrawlInfoAdd, name: 'crawl_info_add' },
|
||||
];
|
||||
|
||||
let totalSynced = 0;
|
||||
for (const table of tables) {
|
||||
const count = await syncTable(masterDataSource, slaveDataSource, table.entity, table.name);
|
||||
totalSynced += count;
|
||||
}
|
||||
|
||||
logger.log(`数据库同步完成,共同步 ${totalSynced} 条记录`);
|
||||
|
||||
await masterDataSource.destroy();
|
||||
await slaveDataSource.destroy();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
logger.error('数据库同步失败:', error);
|
||||
if (masterDataSource && masterDataSource.isInitialized) {
|
||||
await masterDataSource.destroy();
|
||||
}
|
||||
if (slaveDataSource && slaveDataSource.isInitialized) {
|
||||
await slaveDataSource.destroy();
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行同步
|
||||
syncDatabase();
|
||||
3
widget/looker/.gitignore
vendored
Normal file
3
widget/looker/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
build/bin
|
||||
node_modules
|
||||
frontend/dist
|
||||
19
widget/looker/README.md
Normal file
19
widget/looker/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# README
|
||||
|
||||
## About
|
||||
|
||||
This is the official Wails Vue-TS template.
|
||||
|
||||
You can configure the project by editing `wails.json`. More information about the project settings can be found
|
||||
here: https://wails.io/docs/reference/project-config
|
||||
|
||||
## Live Development
|
||||
|
||||
To run in live development mode, run `wails dev` in the project directory. This will run a Vite development
|
||||
server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser
|
||||
and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect
|
||||
to this in your browser, and you can call your Go code from devtools.
|
||||
|
||||
## Building
|
||||
|
||||
To build a redistributable, production mode package, use `wails build`.
|
||||
371
widget/looker/app.go
Normal file
371
widget/looker/app.go
Normal file
@@ -0,0 +1,371 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
// DatabaseConfig 数据库配置
|
||||
type DatabaseConfig struct {
|
||||
Type string
|
||||
Host string
|
||||
Port string
|
||||
Username string
|
||||
Password string
|
||||
Name string
|
||||
}
|
||||
|
||||
// BidItem 投标项目结构体
|
||||
type BidItem struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
PublishDate string `json:"publishDate"`
|
||||
Source string `json:"source"`
|
||||
Pin bool `json:"pin"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// AiRecommendation AI推荐结构体
|
||||
type AiRecommendation struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Confidence int `json:"confidence"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
|
||||
// CrawlInfoStat 爬虫统计信息结构体
|
||||
type CrawlInfoStat struct {
|
||||
Source string `json:"source"`
|
||||
Count int `json:"count"`
|
||||
LatestUpdate string `json:"latestUpdate"`
|
||||
LatestPublishDate string `json:"latestPublishDate"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
dbConfig *DatabaseConfig
|
||||
projectRootDir string
|
||||
}
|
||||
|
||||
// NewApp creates a new App application struct
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
}
|
||||
|
||||
// startup is called when the app starts. The context is saved
|
||||
// so we can call the runtime methods
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
a.loadEnv()
|
||||
}
|
||||
|
||||
// loadEnv 加载 .env 文件
|
||||
func (a *App) loadEnv() {
|
||||
// 获取项目根目录(从当前工作目录向上查找 .env 文件)
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Printf("获取工作目录失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 查找 .env 文件
|
||||
envPath := a.findEnvFile(wd)
|
||||
if envPath == "" {
|
||||
fmt.Println("未找到 .env 文件")
|
||||
return
|
||||
}
|
||||
|
||||
// 加载 .env 文件
|
||||
if err := godotenv.Load(envPath); err != nil {
|
||||
fmt.Printf("加载 .env 文件失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 保存项目根目录
|
||||
a.projectRootDir = filepath.Dir(envPath)
|
||||
|
||||
// 读取数据库配置
|
||||
a.dbConfig = &DatabaseConfig{
|
||||
Type: os.Getenv("DATABASE_TYPE"),
|
||||
Host: os.Getenv("DATABASE_HOST"),
|
||||
Port: os.Getenv("DATABASE_PORT"),
|
||||
Username: os.Getenv("DATABASE_USERNAME"),
|
||||
Password: os.Getenv("DATABASE_PASSWORD"),
|
||||
Name: os.Getenv("DATABASE_NAME"),
|
||||
}
|
||||
|
||||
fmt.Printf("数据库配置已加载: %s@%s:%s/%s\n", a.dbConfig.Username, a.dbConfig.Host, a.dbConfig.Port, a.dbConfig.Name)
|
||||
}
|
||||
|
||||
// findEnvFile 查找 .env 文件
|
||||
func (a *App) findEnvFile(startDir string) string {
|
||||
dir := startDir
|
||||
for {
|
||||
envPath := filepath.Join(dir, ".env")
|
||||
if _, err := os.Stat(envPath); err == nil {
|
||||
return envPath
|
||||
}
|
||||
|
||||
// 检查是否到达根目录
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
break
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetDatabaseConfig 获取数据库配置
|
||||
func (a *App) GetDatabaseConfig() *DatabaseConfig {
|
||||
return a.dbConfig
|
||||
}
|
||||
|
||||
// GetDatabaseDSN 获取数据库连接字符串
|
||||
func (a *App) GetDatabaseDSN() string {
|
||||
if a.dbConfig == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 根据数据库类型生成 DSN
|
||||
switch strings.ToLower(a.dbConfig.Type) {
|
||||
case "mariadb", "mysql":
|
||||
return fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
a.dbConfig.Username,
|
||||
a.dbConfig.Password,
|
||||
a.dbConfig.Host,
|
||||
a.dbConfig.Port,
|
||||
a.dbConfig.Name,
|
||||
)
|
||||
case "postgres", "postgresql":
|
||||
return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
||||
a.dbConfig.Host,
|
||||
a.dbConfig.Port,
|
||||
a.dbConfig.Username,
|
||||
a.dbConfig.Password,
|
||||
a.dbConfig.Name,
|
||||
)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Greet returns a greeting for the given name
|
||||
func (a *App) Greet(name string) string {
|
||||
return fmt.Sprintf("Hello %s, It's show time!", name)
|
||||
}
|
||||
|
||||
// GetPinnedBidItems 获取 pin=true 的投标项目
|
||||
func (a *App) GetPinnedBidItems() ([]BidItem, error) {
|
||||
dsn := a.GetDatabaseDSN()
|
||||
if dsn == "" {
|
||||
return nil, fmt.Errorf("数据库配置未加载")
|
||||
}
|
||||
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("连接数据库失败: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// 测试连接
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, fmt.Errorf("数据库连接测试失败: %v", err)
|
||||
}
|
||||
|
||||
// 查询 pin=true 的记录
|
||||
query := `SELECT id, title, url, publishDate, source, pin, createdAt, updatedAt
|
||||
FROM bid_items
|
||||
WHERE pin = true
|
||||
ORDER BY createdAt DESC`
|
||||
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询失败: %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []BidItem
|
||||
for rows.Next() {
|
||||
var item BidItem
|
||||
var publishDate, createdAt, updatedAt time.Time
|
||||
|
||||
err := rows.Scan(
|
||||
&item.ID,
|
||||
&item.Title,
|
||||
&item.URL,
|
||||
&publishDate,
|
||||
&item.Source,
|
||||
&item.Pin,
|
||||
&createdAt,
|
||||
&updatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("扫描行失败: %v", err)
|
||||
}
|
||||
|
||||
item.PublishDate = publishDate.Format("2006-01-02")
|
||||
item.CreatedAt = createdAt.Format("2006-01-02")
|
||||
item.UpdatedAt = updatedAt.Format("2006-01-02")
|
||||
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("遍历行失败: %v", err)
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// GetAiRecommendations 获取 AI 推荐数据
|
||||
func (a *App) GetAiRecommendations() ([]AiRecommendation, error) {
|
||||
dsn := a.GetDatabaseDSN()
|
||||
if dsn == "" {
|
||||
return nil, fmt.Errorf("数据库配置未加载")
|
||||
}
|
||||
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("连接数据库失败: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// 测试连接
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, fmt.Errorf("数据库连接测试失败: %v", err)
|
||||
}
|
||||
|
||||
// 查询 ai_recommendations 表
|
||||
query := `SELECT id, title, confidence, createdAt
|
||||
FROM ai_recommendations
|
||||
ORDER BY createdAt DESC`
|
||||
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询失败: %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []AiRecommendation
|
||||
for rows.Next() {
|
||||
var item AiRecommendation
|
||||
var createdAt time.Time
|
||||
|
||||
err := rows.Scan(
|
||||
&item.ID,
|
||||
&item.Title,
|
||||
&item.Confidence,
|
||||
&createdAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("扫描行失败: %v", err)
|
||||
}
|
||||
|
||||
item.CreatedAt = createdAt.Format("2006-01-02")
|
||||
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("遍历行失败: %v", err)
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// GetCrawlInfoStats 获取爬虫统计信息
|
||||
func (a *App) GetCrawlInfoStats() ([]CrawlInfoStat, error) {
|
||||
dsn := a.GetDatabaseDSN()
|
||||
if dsn == "" {
|
||||
return nil, fmt.Errorf("数据库配置未加载")
|
||||
}
|
||||
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("连接数据库失败: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// 测试连接
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, fmt.Errorf("数据库连接测试失败: %v", err)
|
||||
}
|
||||
|
||||
// 查询 crawl_info_add 表,按 source 分组获取最新记录
|
||||
query := `SELECT
|
||||
c1.source,
|
||||
c1.count,
|
||||
c1.createdAt as latestUpdate,
|
||||
c1.latestPublishDate,
|
||||
c1.error
|
||||
FROM crawl_info_add c1
|
||||
WHERE c1.createdAt = (
|
||||
SELECT MAX(c2.createdAt)
|
||||
FROM crawl_info_add c2
|
||||
WHERE c2.source = c1.source
|
||||
)
|
||||
ORDER BY c1.source`
|
||||
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询失败: %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var stats []CrawlInfoStat
|
||||
for rows.Next() {
|
||||
var stat CrawlInfoStat
|
||||
var latestUpdate, latestPublishDate sql.NullTime
|
||||
var errorStr sql.NullString
|
||||
|
||||
err := rows.Scan(
|
||||
&stat.Source,
|
||||
&stat.Count,
|
||||
&latestUpdate,
|
||||
&latestPublishDate,
|
||||
&errorStr,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("扫描行失败: %v", err)
|
||||
}
|
||||
|
||||
if latestUpdate.Valid {
|
||||
stat.LatestUpdate = latestUpdate.Time.Format("2006-01-02 15:04:05")
|
||||
} else {
|
||||
stat.LatestUpdate = ""
|
||||
}
|
||||
|
||||
if latestPublishDate.Valid {
|
||||
stat.LatestPublishDate = latestPublishDate.Time.Format("2006-01-02 15:04:05")
|
||||
} else {
|
||||
stat.LatestPublishDate = ""
|
||||
}
|
||||
|
||||
if errorStr.Valid {
|
||||
stat.Error = errorStr.String
|
||||
} else {
|
||||
stat.Error = ""
|
||||
}
|
||||
|
||||
stats = append(stats, stat)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("遍历行失败: %v", err)
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
23
widget/looker/frontend/README.md
Normal file
23
widget/looker/frontend/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue
|
||||
3 `<script setup>` SFCs, check out
|
||||
the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
|
||||
|
||||
## Type Support For `.vue` Imports in TS
|
||||
|
||||
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type
|
||||
by default. In most cases this is fine if you don't really care about component prop types outside of templates.
|
||||
However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using
|
||||
manual `h(...)` calls), you can enable Volar's Take Over mode by following these steps:
|
||||
|
||||
1. Run `Extensions: Show Built-in Extensions` from VS Code's command palette, look
|
||||
for `TypeScript and JavaScript Language Features`, then right click and select `Disable (Workspace)`. By default,
|
||||
Take Over mode will enable itself if the default TypeScript extension is disabled.
|
||||
2. Reload the VS Code window by running `Developer: Reload Window` from the command palette.
|
||||
|
||||
You can learn more about Take Over mode [here](https://github.com/johnsoncodehk/volar/discussions/471).
|
||||
13
widget/looker/frontend/index.html
Normal file
13
widget/looker/frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>looker</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="./src/main.ts" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
21
widget/looker/frontend/package.json
Normal file
21
widget/looker/frontend/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.2.37"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^3.0.3",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^3.0.7",
|
||||
"vue-tsc": "^1.8.27",
|
||||
"@babel/types": "^7.18.10"
|
||||
}
|
||||
}
|
||||
1
widget/looker/frontend/package.json.md5
Normal file
1
widget/looker/frontend/package.json.md5
Normal file
@@ -0,0 +1 @@
|
||||
bb7ffb87329c9ad4990374471d4ce9a4
|
||||
195
widget/looker/frontend/src/App.vue
Normal file
195
widget/looker/frontend/src/App.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import PinnedBids from './components/PinnedBids.vue'
|
||||
import AiRecommendations from './components/AiRecommendations.vue'
|
||||
import WidgetCrawlInfo from './components/WidgetCrawlInfo.vue'
|
||||
|
||||
const activeTab = ref('pinned')
|
||||
const pinnedBidsRef = ref<InstanceType<typeof PinnedBids>>()
|
||||
const aiRecommendationsRef = ref<InstanceType<typeof AiRecommendations>>()
|
||||
const widgetCrawlInfoRef = ref<InstanceType<typeof WidgetCrawlInfo>>()
|
||||
let refreshTimer: number | null = null
|
||||
const showToast = ref(false)
|
||||
const toastMessage = ref('')
|
||||
|
||||
const tabs = [
|
||||
{ id: 'pinned', label: '置顶项目' },
|
||||
{ id: 'ai', label: 'AI 推荐' },
|
||||
{ id: 'status', label: '状态' }
|
||||
]
|
||||
|
||||
const handleRefresh = () => {
|
||||
if (activeTab.value === 'pinned' && pinnedBidsRef.value) {
|
||||
pinnedBidsRef.value.loadPinnedBids()
|
||||
showToastMessage('置顶项目已刷新')
|
||||
} else if (activeTab.value === 'ai' && aiRecommendationsRef.value) {
|
||||
aiRecommendationsRef.value.loadRecommendations()
|
||||
showToastMessage('AI 推荐已刷新')
|
||||
} else if (activeTab.value === 'status' && widgetCrawlInfoRef.value) {
|
||||
widgetCrawlInfoRef.value.loadCrawlStats()
|
||||
showToastMessage('状态已刷新')
|
||||
}
|
||||
}
|
||||
|
||||
const showToastMessage = (message: string) => {
|
||||
toastMessage.value = message
|
||||
showToast.value = true
|
||||
setTimeout(() => {
|
||||
showToast.value = false
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
const startAutoRefresh = () => {
|
||||
// 每5分钟(300000毫秒)自动刷新
|
||||
refreshTimer = window.setInterval(() => {
|
||||
handleRefresh()
|
||||
}, 300000)
|
||||
}
|
||||
|
||||
const stopAutoRefresh = () => {
|
||||
if (refreshTimer !== null) {
|
||||
clearInterval(refreshTimer)
|
||||
refreshTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
startAutoRefresh()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAutoRefresh()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div class="tabs">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
:class="['tab-button', { active: activeTab === tab.id }]"
|
||||
@click="activeTab = tab.id"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
<button class="refresh-button" @click="handleRefresh">
|
||||
🔄 刷新
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-content">
|
||||
<PinnedBids v-if="activeTab === 'pinned'" ref="pinnedBidsRef"/>
|
||||
<AiRecommendations v-else-if="activeTab === 'ai'" ref="aiRecommendationsRef"/>
|
||||
<WidgetCrawlInfo v-else-if="activeTab === 'status'" ref="widgetCrawlInfoRef"/>
|
||||
</div>
|
||||
|
||||
<div v-if="showToast" class="toast">
|
||||
{{ toastMessage }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #f5f5f5;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
padding: 8px 12px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.tab-button:hover {
|
||||
color: #333;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
color: #3498db;
|
||||
border-bottom-color: #3498db;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.refresh-button {
|
||||
margin-left: auto;
|
||||
padding: 4px 10px;
|
||||
background: #3498db;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.refresh-button:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
|
||||
.refresh-button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 200px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: #fff;
|
||||
padding: 12px 24px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
z-index: 1000;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
159
widget/looker/frontend/src/components/AiRecommendations.vue
Normal file
159
widget/looker/frontend/src/components/AiRecommendations.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { GetAiRecommendations } from '../../wailsjs/go/main/App'
|
||||
|
||||
interface AiRecommendation {
|
||||
id: string
|
||||
title: string
|
||||
confidence: number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
const recommendations = ref<AiRecommendation[]>([])
|
||||
const loading = ref(true)
|
||||
const error = ref('')
|
||||
|
||||
const loadRecommendations = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
const items = await GetAiRecommendations()
|
||||
recommendations.value = items
|
||||
} catch (err) {
|
||||
error.value = `加载失败: ${err}`
|
||||
console.error('加载 AI 推荐失败:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getConfidenceColor = (confidence: number) => {
|
||||
if (confidence >= 80) return '#27ae60'
|
||||
if (confidence >= 60) return '#f39c12'
|
||||
return '#e74c3c'
|
||||
}
|
||||
|
||||
const getConfidenceLabel = (confidence: number) => {
|
||||
if (confidence >= 80) return '高'
|
||||
if (confidence >= 60) return '中'
|
||||
return '低'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadRecommendations()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
loadRecommendations
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ai-recommendations-container">
|
||||
<!-- <h2 class="title">AI 推荐项目</h2> -->
|
||||
|
||||
<div v-if="loading" class="loading">
|
||||
加载中...
|
||||
</div>
|
||||
|
||||
<div v-else-if="error" class="error">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="recommendations.length === 0" class="empty">
|
||||
暂无推荐项目
|
||||
</div>
|
||||
|
||||
<div v-else class="recommendation-list">
|
||||
<div
|
||||
v-for="item in recommendations"
|
||||
:key="item.id"
|
||||
class="recommendation-item"
|
||||
>
|
||||
<div class="recommendation-header">
|
||||
<span
|
||||
class="confidence-badge"
|
||||
:style="{ backgroundColor: getConfidenceColor(item.confidence) }"
|
||||
>
|
||||
{{ getConfidenceLabel(item.confidence) }} ({{ item.confidence }}%)
|
||||
</span>
|
||||
<span class="date">{{ item.createdAt }}</span>
|
||||
</div>
|
||||
<h3 class="recommendation-title">{{ item.title }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ai-recommendations-container {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.error,
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.recommendation-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.recommendation-item {
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.recommendation-item:hover {
|
||||
border-color: #3498db;
|
||||
box-shadow: 0 1px 4px rgba(52, 152, 219, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.recommendation-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.confidence-badge {
|
||||
color: #fff;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 10px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.recommendation-title {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
168
widget/looker/frontend/src/components/PinnedBids.vue
Normal file
168
widget/looker/frontend/src/components/PinnedBids.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { GetPinnedBidItems } from '../../wailsjs/go/main/App'
|
||||
|
||||
interface BidItem {
|
||||
id: string
|
||||
title: string
|
||||
url: string
|
||||
publishDate: string
|
||||
source: string
|
||||
pin: boolean
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
const bidItems = ref<BidItem[]>([])
|
||||
const loading = ref(true)
|
||||
const error = ref('')
|
||||
|
||||
const loadPinnedBids = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
const items = await GetPinnedBidItems()
|
||||
bidItems.value = items
|
||||
} catch (err) {
|
||||
error.value = `加载失败: ${err}`
|
||||
console.error('加载置顶投标项目失败:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openUrl = (url: string) => {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadPinnedBids()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
loadPinnedBids
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pinned-bids-container">
|
||||
<!-- <h2 class="title">置顶投标项目</h2> -->
|
||||
|
||||
<div v-if="loading" class="loading">
|
||||
加载中...
|
||||
</div>
|
||||
|
||||
<div v-else-if="error" class="error">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="bidItems.length === 0" class="empty">
|
||||
暂无置顶项目
|
||||
</div>
|
||||
|
||||
<div v-else class="bid-list">
|
||||
<div
|
||||
v-for="item in bidItems"
|
||||
:key="item.id"
|
||||
class="bid-item"
|
||||
@click="openUrl(item.url)"
|
||||
>
|
||||
<div class="bid-header">
|
||||
<span class="source">{{ item.source }}</span>
|
||||
<span class="date">{{ item.publishDate }}</span>
|
||||
</div>
|
||||
<h3 class="bid-title">{{ item.title }}</h3>
|
||||
<!-- <div class="bid-footer">
|
||||
<span class="pin-badge">📌 已置顶</span>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pinned-bids-container {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.error,
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.bid-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bid-item {
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.bid-item:hover {
|
||||
border-color: #3498db;
|
||||
box-shadow: 0 1px 4px rgba(52, 152, 219, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.bid-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.source {
|
||||
background: #f0f0f0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 10px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.bid-title {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin: 0 0 6px 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.bid-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.pin-badge {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
}
|
||||
</style>
|
||||
155
widget/looker/frontend/src/components/WidgetCrawlInfo.vue
Normal file
155
widget/looker/frontend/src/components/WidgetCrawlInfo.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { GetCrawlInfoStats } from '../../wailsjs/go/main/App'
|
||||
|
||||
interface CrawlInfoStat {
|
||||
source: string
|
||||
count: number
|
||||
latestUpdate: string
|
||||
latestPublishDate: string
|
||||
error: string
|
||||
}
|
||||
|
||||
const crawlStats = ref<CrawlInfoStat[]>([])
|
||||
const loading = ref(true)
|
||||
const error = ref('')
|
||||
|
||||
const loadCrawlStats = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
const stats = await GetCrawlInfoStats()
|
||||
crawlStats.value = stats
|
||||
} catch (err) {
|
||||
error.value = `加载失败: ${err}`
|
||||
console.error('加载爬虫统计信息失败:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusText = (stat: CrawlInfoStat) => {
|
||||
if (stat.error) {
|
||||
return '出错'
|
||||
}
|
||||
if (stat.count > 0) {
|
||||
return '正常'
|
||||
}
|
||||
return '无数据'
|
||||
}
|
||||
|
||||
const getStatusClass = (stat: CrawlInfoStat) => {
|
||||
if (stat.error) {
|
||||
return 'status-error'
|
||||
}
|
||||
if (stat.count > 0) {
|
||||
return 'status-success'
|
||||
}
|
||||
return 'status-info'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadCrawlStats()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
loadCrawlStats
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="crawl-info-container">
|
||||
<div v-if="loading" class="loading">
|
||||
加载中...
|
||||
</div>
|
||||
|
||||
<div v-else-if="error" class="error">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="crawlStats.length === 0" class="empty">
|
||||
暂无爬虫统计信息
|
||||
</div>
|
||||
|
||||
<div v-else class="crawl-list">
|
||||
<div
|
||||
v-for="stat in crawlStats"
|
||||
:key="stat.source"
|
||||
class="crawl-item"
|
||||
>
|
||||
<span class="source">{{ stat.source }}</span>
|
||||
<span :class="['status', getStatusClass(stat)]">
|
||||
{{ getStatusText(stat) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.crawl-info-container {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.error,
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.crawl-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.crawl-item {
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.crawl-item:hover {
|
||||
border-color: #3498db;
|
||||
box-shadow: 0 1px 4px rgba(52, 152, 219, 0.15);
|
||||
}
|
||||
|
||||
.source {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
background: #e2e3e5;
|
||||
color: #383d41;
|
||||
}
|
||||
</style>
|
||||
5
widget/looker/frontend/src/main.ts
Normal file
5
widget/looker/frontend/src/main.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import {createApp} from 'vue'
|
||||
import App from './App.vue'
|
||||
import './style.css';
|
||||
|
||||
createApp(App).mount('#app')
|
||||
26
widget/looker/frontend/src/style.css
Normal file
26
widget/looker/frontend/src/style.css
Normal file
@@ -0,0 +1,26 @@
|
||||
html {
|
||||
background-color: rgba(27, 38, 54, 1);
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: white;
|
||||
font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Nunito";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local(""),
|
||||
url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
text-align: center;
|
||||
}
|
||||
7
widget/looker/frontend/src/vite-env.d.ts
vendored
Normal file
7
widget/looker/frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type {DefineComponent} from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
30
widget/looker/frontend/tsconfig.json
Normal file
30
widget/looker/frontend/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": [
|
||||
"ESNext",
|
||||
"DOM"
|
||||
],
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
11
widget/looker/frontend/tsconfig.node.json
Normal file
11
widget/looker/frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts"
|
||||
]
|
||||
}
|
||||
7
widget/looker/frontend/vite.config.ts
Normal file
7
widget/looker/frontend/vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import {defineConfig} from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()]
|
||||
})
|
||||
15
widget/looker/frontend/wailsjs/go/main/App.d.ts
vendored
Normal file
15
widget/looker/frontend/wailsjs/go/main/App.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {main} from '../models';
|
||||
|
||||
export function GetAiRecommendations():Promise<Array<main.AiRecommendation>>;
|
||||
|
||||
export function GetCrawlInfoStats():Promise<Array<main.CrawlInfoStat>>;
|
||||
|
||||
export function GetDatabaseConfig():Promise<main.DatabaseConfig>;
|
||||
|
||||
export function GetDatabaseDSN():Promise<string>;
|
||||
|
||||
export function GetPinnedBidItems():Promise<Array<main.BidItem>>;
|
||||
|
||||
export function Greet(arg1:string):Promise<string>;
|
||||
27
widget/looker/frontend/wailsjs/go/main/App.js
Normal file
27
widget/looker/frontend/wailsjs/go/main/App.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function GetAiRecommendations() {
|
||||
return window['go']['main']['App']['GetAiRecommendations']();
|
||||
}
|
||||
|
||||
export function GetCrawlInfoStats() {
|
||||
return window['go']['main']['App']['GetCrawlInfoStats']();
|
||||
}
|
||||
|
||||
export function GetDatabaseConfig() {
|
||||
return window['go']['main']['App']['GetDatabaseConfig']();
|
||||
}
|
||||
|
||||
export function GetDatabaseDSN() {
|
||||
return window['go']['main']['App']['GetDatabaseDSN']();
|
||||
}
|
||||
|
||||
export function GetPinnedBidItems() {
|
||||
return window['go']['main']['App']['GetPinnedBidItems']();
|
||||
}
|
||||
|
||||
export function Greet(arg1) {
|
||||
return window['go']['main']['App']['Greet'](arg1);
|
||||
}
|
||||
91
widget/looker/frontend/wailsjs/go/models.ts
Normal file
91
widget/looker/frontend/wailsjs/go/models.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
export namespace main {
|
||||
|
||||
export class AiRecommendation {
|
||||
id: string;
|
||||
title: string;
|
||||
confidence: number;
|
||||
createdAt: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new AiRecommendation(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.id = source["id"];
|
||||
this.title = source["title"];
|
||||
this.confidence = source["confidence"];
|
||||
this.createdAt = source["createdAt"];
|
||||
}
|
||||
}
|
||||
export class BidItem {
|
||||
id: string;
|
||||
title: string;
|
||||
url: string;
|
||||
publishDate: string;
|
||||
source: string;
|
||||
pin: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new BidItem(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.id = source["id"];
|
||||
this.title = source["title"];
|
||||
this.url = source["url"];
|
||||
this.publishDate = source["publishDate"];
|
||||
this.source = source["source"];
|
||||
this.pin = source["pin"];
|
||||
this.createdAt = source["createdAt"];
|
||||
this.updatedAt = source["updatedAt"];
|
||||
}
|
||||
}
|
||||
export class CrawlInfoStat {
|
||||
source: string;
|
||||
count: number;
|
||||
latestUpdate: string;
|
||||
latestPublishDate: string;
|
||||
error: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new CrawlInfoStat(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.source = source["source"];
|
||||
this.count = source["count"];
|
||||
this.latestUpdate = source["latestUpdate"];
|
||||
this.latestPublishDate = source["latestPublishDate"];
|
||||
this.error = source["error"];
|
||||
}
|
||||
}
|
||||
export class DatabaseConfig {
|
||||
Type: string;
|
||||
Host: string;
|
||||
Port: string;
|
||||
Username: string;
|
||||
Password: string;
|
||||
Name: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new DatabaseConfig(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.Type = source["Type"];
|
||||
this.Host = source["Host"];
|
||||
this.Port = source["Port"];
|
||||
this.Username = source["Username"];
|
||||
this.Password = source["Password"];
|
||||
this.Name = source["Name"];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
24
widget/looker/frontend/wailsjs/runtime/package.json
Normal file
24
widget/looker/frontend/wailsjs/runtime/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@wailsapp/runtime",
|
||||
"version": "2.0.0",
|
||||
"description": "Wails Javascript runtime library",
|
||||
"main": "runtime.js",
|
||||
"types": "runtime.d.ts",
|
||||
"scripts": {
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wailsapp/wails.git"
|
||||
},
|
||||
"keywords": [
|
||||
"Wails",
|
||||
"Javascript",
|
||||
"Go"
|
||||
],
|
||||
"author": "Lea Anthony <lea.anthony@gmail.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/wailsapp/wails/issues"
|
||||
},
|
||||
"homepage": "https://github.com/wailsapp/wails#readme"
|
||||
}
|
||||
249
widget/looker/frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
249
widget/looker/frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
@@ -0,0 +1,249 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Size {
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
export interface Screen {
|
||||
isCurrent: boolean;
|
||||
isPrimary: boolean;
|
||||
width : number
|
||||
height : number
|
||||
}
|
||||
|
||||
// Environment information such as platform, buildtype, ...
|
||||
export interface EnvironmentInfo {
|
||||
buildType: string;
|
||||
platform: string;
|
||||
arch: string;
|
||||
}
|
||||
|
||||
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
|
||||
// emits the given event. Optional data may be passed with the event.
|
||||
// This will trigger any event listeners.
|
||||
export function EventsEmit(eventName: string, ...data: any): void;
|
||||
|
||||
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
|
||||
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
|
||||
// sets up a listener for the given event name, but will only trigger a given number times.
|
||||
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
|
||||
|
||||
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
|
||||
// sets up a listener for the given event name, but will only trigger once.
|
||||
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
|
||||
// unregisters the listener for the given event name.
|
||||
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
|
||||
|
||||
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
|
||||
// unregisters all listeners.
|
||||
export function EventsOffAll(): void;
|
||||
|
||||
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
|
||||
// logs the given message as a raw message
|
||||
export function LogPrint(message: string): void;
|
||||
|
||||
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
|
||||
// logs the given message at the `trace` log level.
|
||||
export function LogTrace(message: string): void;
|
||||
|
||||
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
|
||||
// logs the given message at the `debug` log level.
|
||||
export function LogDebug(message: string): void;
|
||||
|
||||
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
|
||||
// logs the given message at the `error` log level.
|
||||
export function LogError(message: string): void;
|
||||
|
||||
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
|
||||
// logs the given message at the `fatal` log level.
|
||||
// The application will quit after calling this method.
|
||||
export function LogFatal(message: string): void;
|
||||
|
||||
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
|
||||
// logs the given message at the `info` log level.
|
||||
export function LogInfo(message: string): void;
|
||||
|
||||
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
|
||||
// logs the given message at the `warning` log level.
|
||||
export function LogWarning(message: string): void;
|
||||
|
||||
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
|
||||
// Forces a reload by the main application as well as connected browsers.
|
||||
export function WindowReload(): void;
|
||||
|
||||
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
|
||||
// Reloads the application frontend.
|
||||
export function WindowReloadApp(): void;
|
||||
|
||||
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
|
||||
// Sets the window AlwaysOnTop or not on top.
|
||||
export function WindowSetAlwaysOnTop(b: boolean): void;
|
||||
|
||||
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
|
||||
// *Windows only*
|
||||
// Sets window theme to system default (dark/light).
|
||||
export function WindowSetSystemDefaultTheme(): void;
|
||||
|
||||
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
|
||||
// *Windows only*
|
||||
// Sets window to light theme.
|
||||
export function WindowSetLightTheme(): void;
|
||||
|
||||
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
|
||||
// *Windows only*
|
||||
// Sets window to dark theme.
|
||||
export function WindowSetDarkTheme(): void;
|
||||
|
||||
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
|
||||
// Centers the window on the monitor the window is currently on.
|
||||
export function WindowCenter(): void;
|
||||
|
||||
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
|
||||
// Sets the text in the window title bar.
|
||||
export function WindowSetTitle(title: string): void;
|
||||
|
||||
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
|
||||
// Makes the window full screen.
|
||||
export function WindowFullscreen(): void;
|
||||
|
||||
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
|
||||
// Restores the previous window dimensions and position prior to full screen.
|
||||
export function WindowUnfullscreen(): void;
|
||||
|
||||
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
|
||||
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
|
||||
export function WindowIsFullscreen(): Promise<boolean>;
|
||||
|
||||
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
|
||||
// Sets the width and height of the window.
|
||||
export function WindowSetSize(width: number, height: number): void;
|
||||
|
||||
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
|
||||
// Gets the width and height of the window.
|
||||
export function WindowGetSize(): Promise<Size>;
|
||||
|
||||
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
|
||||
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMaxSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
|
||||
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMinSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
|
||||
// Sets the window position relative to the monitor the window is currently on.
|
||||
export function WindowSetPosition(x: number, y: number): void;
|
||||
|
||||
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
|
||||
// Gets the window position relative to the monitor the window is currently on.
|
||||
export function WindowGetPosition(): Promise<Position>;
|
||||
|
||||
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
|
||||
// Hides the window.
|
||||
export function WindowHide(): void;
|
||||
|
||||
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
|
||||
// Shows the window, if it is currently hidden.
|
||||
export function WindowShow(): void;
|
||||
|
||||
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
|
||||
// Maximises the window to fill the screen.
|
||||
export function WindowMaximise(): void;
|
||||
|
||||
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
|
||||
// Toggles between Maximised and UnMaximised.
|
||||
export function WindowToggleMaximise(): void;
|
||||
|
||||
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
|
||||
// Restores the window to the dimensions and position prior to maximising.
|
||||
export function WindowUnmaximise(): void;
|
||||
|
||||
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
|
||||
// Returns the state of the window, i.e. whether the window is maximised or not.
|
||||
export function WindowIsMaximised(): Promise<boolean>;
|
||||
|
||||
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
|
||||
// Minimises the window.
|
||||
export function WindowMinimise(): void;
|
||||
|
||||
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
|
||||
// Restores the window to the dimensions and position prior to minimising.
|
||||
export function WindowUnminimise(): void;
|
||||
|
||||
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
|
||||
// Returns the state of the window, i.e. whether the window is minimised or not.
|
||||
export function WindowIsMinimised(): Promise<boolean>;
|
||||
|
||||
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
|
||||
// Returns the state of the window, i.e. whether the window is normal or not.
|
||||
export function WindowIsNormal(): Promise<boolean>;
|
||||
|
||||
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
|
||||
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
|
||||
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
|
||||
|
||||
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
|
||||
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
|
||||
export function ScreenGetAll(): Promise<Screen[]>;
|
||||
|
||||
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
|
||||
// Opens the given URL in the system browser.
|
||||
export function BrowserOpenURL(url: string): void;
|
||||
|
||||
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
|
||||
// Returns information about the environment
|
||||
export function Environment(): Promise<EnvironmentInfo>;
|
||||
|
||||
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
|
||||
// Quits the application.
|
||||
export function Quit(): void;
|
||||
|
||||
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
|
||||
// Hides the application.
|
||||
export function Hide(): void;
|
||||
|
||||
// [Show](https://wails.io/docs/reference/runtime/intro#show)
|
||||
// Shows the application.
|
||||
export function Show(): void;
|
||||
|
||||
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
|
||||
// Returns the current text stored on clipboard
|
||||
export function ClipboardGetText(): Promise<string>;
|
||||
|
||||
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
|
||||
// Sets a text on the clipboard
|
||||
export function ClipboardSetText(text: string): Promise<boolean>;
|
||||
|
||||
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
|
||||
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
|
||||
|
||||
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
|
||||
// OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
export function OnFileDropOff() :void
|
||||
|
||||
// Check if the file path resolver is available
|
||||
export function CanResolveFilePaths(): boolean;
|
||||
|
||||
// Resolves file paths for an array of files
|
||||
export function ResolveFilePaths(files: File[]): void
|
||||
242
widget/looker/frontend/wailsjs/runtime/runtime.js
Normal file
242
widget/looker/frontend/wailsjs/runtime/runtime.js
Normal file
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export function LogPrint(message) {
|
||||
window.runtime.LogPrint(message);
|
||||
}
|
||||
|
||||
export function LogTrace(message) {
|
||||
window.runtime.LogTrace(message);
|
||||
}
|
||||
|
||||
export function LogDebug(message) {
|
||||
window.runtime.LogDebug(message);
|
||||
}
|
||||
|
||||
export function LogInfo(message) {
|
||||
window.runtime.LogInfo(message);
|
||||
}
|
||||
|
||||
export function LogWarning(message) {
|
||||
window.runtime.LogWarning(message);
|
||||
}
|
||||
|
||||
export function LogError(message) {
|
||||
window.runtime.LogError(message);
|
||||
}
|
||||
|
||||
export function LogFatal(message) {
|
||||
window.runtime.LogFatal(message);
|
||||
}
|
||||
|
||||
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
|
||||
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
|
||||
}
|
||||
|
||||
export function EventsOn(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, -1);
|
||||
}
|
||||
|
||||
export function EventsOff(eventName, ...additionalEventNames) {
|
||||
return window.runtime.EventsOff(eventName, ...additionalEventNames);
|
||||
}
|
||||
|
||||
export function EventsOffAll() {
|
||||
return window.runtime.EventsOffAll();
|
||||
}
|
||||
|
||||
export function EventsOnce(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, 1);
|
||||
}
|
||||
|
||||
export function EventsEmit(eventName) {
|
||||
let args = [eventName].slice.call(arguments);
|
||||
return window.runtime.EventsEmit.apply(null, args);
|
||||
}
|
||||
|
||||
export function WindowReload() {
|
||||
window.runtime.WindowReload();
|
||||
}
|
||||
|
||||
export function WindowReloadApp() {
|
||||
window.runtime.WindowReloadApp();
|
||||
}
|
||||
|
||||
export function WindowSetAlwaysOnTop(b) {
|
||||
window.runtime.WindowSetAlwaysOnTop(b);
|
||||
}
|
||||
|
||||
export function WindowSetSystemDefaultTheme() {
|
||||
window.runtime.WindowSetSystemDefaultTheme();
|
||||
}
|
||||
|
||||
export function WindowSetLightTheme() {
|
||||
window.runtime.WindowSetLightTheme();
|
||||
}
|
||||
|
||||
export function WindowSetDarkTheme() {
|
||||
window.runtime.WindowSetDarkTheme();
|
||||
}
|
||||
|
||||
export function WindowCenter() {
|
||||
window.runtime.WindowCenter();
|
||||
}
|
||||
|
||||
export function WindowSetTitle(title) {
|
||||
window.runtime.WindowSetTitle(title);
|
||||
}
|
||||
|
||||
export function WindowFullscreen() {
|
||||
window.runtime.WindowFullscreen();
|
||||
}
|
||||
|
||||
export function WindowUnfullscreen() {
|
||||
window.runtime.WindowUnfullscreen();
|
||||
}
|
||||
|
||||
export function WindowIsFullscreen() {
|
||||
return window.runtime.WindowIsFullscreen();
|
||||
}
|
||||
|
||||
export function WindowGetSize() {
|
||||
return window.runtime.WindowGetSize();
|
||||
}
|
||||
|
||||
export function WindowSetSize(width, height) {
|
||||
window.runtime.WindowSetSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMaxSize(width, height) {
|
||||
window.runtime.WindowSetMaxSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMinSize(width, height) {
|
||||
window.runtime.WindowSetMinSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetPosition(x, y) {
|
||||
window.runtime.WindowSetPosition(x, y);
|
||||
}
|
||||
|
||||
export function WindowGetPosition() {
|
||||
return window.runtime.WindowGetPosition();
|
||||
}
|
||||
|
||||
export function WindowHide() {
|
||||
window.runtime.WindowHide();
|
||||
}
|
||||
|
||||
export function WindowShow() {
|
||||
window.runtime.WindowShow();
|
||||
}
|
||||
|
||||
export function WindowMaximise() {
|
||||
window.runtime.WindowMaximise();
|
||||
}
|
||||
|
||||
export function WindowToggleMaximise() {
|
||||
window.runtime.WindowToggleMaximise();
|
||||
}
|
||||
|
||||
export function WindowUnmaximise() {
|
||||
window.runtime.WindowUnmaximise();
|
||||
}
|
||||
|
||||
export function WindowIsMaximised() {
|
||||
return window.runtime.WindowIsMaximised();
|
||||
}
|
||||
|
||||
export function WindowMinimise() {
|
||||
window.runtime.WindowMinimise();
|
||||
}
|
||||
|
||||
export function WindowUnminimise() {
|
||||
window.runtime.WindowUnminimise();
|
||||
}
|
||||
|
||||
export function WindowSetBackgroundColour(R, G, B, A) {
|
||||
window.runtime.WindowSetBackgroundColour(R, G, B, A);
|
||||
}
|
||||
|
||||
export function ScreenGetAll() {
|
||||
return window.runtime.ScreenGetAll();
|
||||
}
|
||||
|
||||
export function WindowIsMinimised() {
|
||||
return window.runtime.WindowIsMinimised();
|
||||
}
|
||||
|
||||
export function WindowIsNormal() {
|
||||
return window.runtime.WindowIsNormal();
|
||||
}
|
||||
|
||||
export function BrowserOpenURL(url) {
|
||||
window.runtime.BrowserOpenURL(url);
|
||||
}
|
||||
|
||||
export function Environment() {
|
||||
return window.runtime.Environment();
|
||||
}
|
||||
|
||||
export function Quit() {
|
||||
window.runtime.Quit();
|
||||
}
|
||||
|
||||
export function Hide() {
|
||||
window.runtime.Hide();
|
||||
}
|
||||
|
||||
export function Show() {
|
||||
window.runtime.Show();
|
||||
}
|
||||
|
||||
export function ClipboardGetText() {
|
||||
return window.runtime.ClipboardGetText();
|
||||
}
|
||||
|
||||
export function ClipboardSetText(text) {
|
||||
return window.runtime.ClipboardSetText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
*
|
||||
* @export
|
||||
* @callback OnFileDropCallback
|
||||
* @param {number} x - x coordinate of the drop
|
||||
* @param {number} y - y coordinate of the drop
|
||||
* @param {string[]} paths - A list of file paths.
|
||||
*/
|
||||
|
||||
/**
|
||||
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
*
|
||||
* @export
|
||||
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
|
||||
*/
|
||||
export function OnFileDrop(callback, useDropTarget) {
|
||||
return window.runtime.OnFileDrop(callback, useDropTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
*/
|
||||
export function OnFileDropOff() {
|
||||
return window.runtime.OnFileDropOff();
|
||||
}
|
||||
|
||||
export function CanResolveFilePaths() {
|
||||
return window.runtime.CanResolveFilePaths();
|
||||
}
|
||||
|
||||
export function ResolveFilePaths(files) {
|
||||
return window.runtime.ResolveFilePaths(files);
|
||||
}
|
||||
42
widget/looker/go.mod
Normal file
42
widget/looker/go.mod
Normal file
@@ -0,0 +1,42 @@
|
||||
module looker
|
||||
|
||||
go 1.23
|
||||
|
||||
require (
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/wailsapp/wails/v2 v2.11.0
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||
github.com/leaanthony/gosod v1.0.4 // indirect
|
||||
github.com/leaanthony/slicer v1.6.0 // indirect
|
||||
github.com/leaanthony/u v1.1.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/samber/lo v1.49.1 // indirect
|
||||
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.22 // indirect
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
)
|
||||
|
||||
// replace github.com/wailsapp/wails/v2 v2.11.0 => C:\Users\3040\go\pkg\mod
|
||||
87
widget/looker/go.sum
Normal file
87
widget/looker/go.sum
Normal file
@@ -0,0 +1,87 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
|
||||
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
|
||||
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
|
||||
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
|
||||
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
|
||||
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
|
||||
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58=
|
||||
github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||
github.com/wailsapp/wails/v2 v2.11.0 h1:seLacV8pqupq32IjS4Y7V8ucab0WZwtK6VvUVxSBtqQ=
|
||||
github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
36
widget/looker/main.go
Normal file
36
widget/looker/main.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||
)
|
||||
|
||||
//go:embed all:frontend/dist
|
||||
var assets embed.FS
|
||||
|
||||
func main() {
|
||||
// Create an instance of the app structure
|
||||
app := NewApp()
|
||||
|
||||
// Create application with options
|
||||
err := wails.Run(&options.App{
|
||||
Title: "looker",
|
||||
Width: 400,
|
||||
Height: 500,
|
||||
AssetServer: &assetserver.Options{
|
||||
Assets: assets,
|
||||
},
|
||||
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
|
||||
OnStartup: app.startup,
|
||||
Bind: []interface{}{
|
||||
app,
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
println("Error:", err.Error())
|
||||
}
|
||||
}
|
||||
194
widget/looker/sys_run/systray_run.go
Normal file
194
widget/looker/sys_run/systray_run.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/getlantern/systray"
|
||||
)
|
||||
|
||||
var (
|
||||
sendPipe *os.File
|
||||
receivePipe *os.File
|
||||
logFile *os.File
|
||||
)
|
||||
|
||||
func initLog() {
|
||||
var err error
|
||||
logFile, err = os.OpenFile("systray_debug.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
// failsafe
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func logMsg(format string, args ...interface{}) {
|
||||
if logFile != nil {
|
||||
fmt.Fprintf(logFile, format+"\n", args...)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
initLog()
|
||||
logMsg("Systray process started")
|
||||
|
||||
// 使用 Stdin 和 Stdout 与主进程通信
|
||||
// 主进程将 cmd.Stdin 连接到 pipes,将 cmd.Stdout 连接到 pipes
|
||||
// 所以这里我们直接使用 os.Stdin 读取,os.Stdout 发送
|
||||
|
||||
sendPipe = os.Stdout
|
||||
receivePipe = os.Stdin
|
||||
|
||||
// 启动系统托盘
|
||||
logMsg("Calling systray.Run")
|
||||
systray.Run(onReady, onExit)
|
||||
}
|
||||
|
||||
func onReady() {
|
||||
logMsg("systray onReady called")
|
||||
// 设置托盘图标 - 使用简单的图标
|
||||
systray.SetIcon(getIcon())
|
||||
systray.SetTitle("Bidding Looker")
|
||||
systray.SetTooltip("Bidding Looker - 招标信息查看器")
|
||||
|
||||
// 显示窗口菜单项
|
||||
mShow := systray.AddMenuItem("显示窗口", "显示主窗口")
|
||||
mQuit := systray.AddMenuItem("退出", "退出程序")
|
||||
|
||||
// 监听来自主进程的消息
|
||||
go monitorPipe()
|
||||
|
||||
// 处理菜单点击
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-mShow.ClickedCh:
|
||||
logMsg("Show menu clicked")
|
||||
// 发送显示窗口消息到主进程
|
||||
sendMessage("SHOW")
|
||||
case <-mQuit.ClickedCh:
|
||||
logMsg("Quit menu clicked")
|
||||
// 发送退出消息到主进程
|
||||
sendMessage("QUIT")
|
||||
systray.Quit()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// 监听系统信号
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-sigCh
|
||||
logMsg("Received signal, quitting")
|
||||
systray.Quit()
|
||||
}()
|
||||
}
|
||||
|
||||
func onExit() {
|
||||
logMsg("systray onExit called")
|
||||
// 清理资源
|
||||
}
|
||||
|
||||
func monitorPipe() {
|
||||
logMsg("Starting monitorPipe")
|
||||
reader := bufio.NewReader(receivePipe)
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
logMsg("Pipe EOF, quitting")
|
||||
// 主进程关闭了管道(可能退出了),我们也退出
|
||||
systray.Quit()
|
||||
return
|
||||
}
|
||||
logMsg("Pipe read error: %v", err)
|
||||
fmt.Fprintf(os.Stderr, "Pipe read error: %v\n", err)
|
||||
return
|
||||
}
|
||||
// 处理来自主进程的消息 (日志输出到 Stderr,避免污染 Stdout)
|
||||
logMsg("Received from main: %s", line)
|
||||
fmt.Fprintf(os.Stderr, "Received from main: %s", line)
|
||||
}
|
||||
}
|
||||
|
||||
func sendMessage(message string) {
|
||||
logMsg("Sending message: %s", message)
|
||||
_, err := sendPipe.WriteString(message + "\n")
|
||||
if err != nil {
|
||||
logMsg("Failed to send message: %v", err)
|
||||
fmt.Fprintf(os.Stderr, "Failed to send message: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 简单的图标数据(16x16 像素的简单图标)
|
||||
// 使用 PNG 格式,这是 systray 库在 Windows 上的推荐格式
|
||||
func getIcon() []byte {
|
||||
// PNG 文件签名
|
||||
// IHDR chunk (图像头): 16x16, 8-bit RGBA
|
||||
// IDAT chunk (图像数据): 简单的蓝色方块图标
|
||||
// IEND chunk (文件结束)
|
||||
return []byte{
|
||||
// PNG 签名 (8 bytes)
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
|
||||
|
||||
// IHDR chunk
|
||||
0x00, 0x00, 0x00, 0x0D, // Length: 13 bytes
|
||||
0x49, 0x48, 0x44, 0x52, // Type: IHDR
|
||||
0x00, 0x00, 0x00, 0x10, // Width: 16
|
||||
0x00, 0x00, 0x00, 0x10, // Height: 16
|
||||
0x08, // Bit depth: 8
|
||||
0x06, // Color type: RGBA
|
||||
0x00, 0x00, 0x00, // Compression, Filter, Interlace
|
||||
0x5D, 0x8A, 0x7F, 0xD4, // CRC
|
||||
|
||||
// IDAT chunk (使用最简单的 PNG 编码)
|
||||
0x00, 0x00, 0x01, 0x6D, // Length: 365 bytes
|
||||
0x49, 0x44, 0x41, 0x54, // Type: IDAT
|
||||
// 压缩数据 (zlib 格式)
|
||||
0x78, 0x9C, // zlib header (default compression)
|
||||
// Deflate 压缩块
|
||||
0x63, 0x18, 0x19, 0x60, 0x28, 0x55, 0xF6, 0x7F, 0x00, 0x00, 0x00,
|
||||
0x00, 0x05, 0x00, 0x01, 0x0A, 0x8C, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x52, 0x4B, 0x8C, 0x36, // CRC
|
||||
|
||||
// IEND chunk
|
||||
0x00, 0x00, 0x00, 0x00, // Length: 0 bytes
|
||||
0x49, 0x45, 0x4E, 0x44, // Type: IEND
|
||||
0xAE, 0x42, 0x60, 0x82, // CRC
|
||||
}
|
||||
}
|
||||
13
widget/looker/wails.json
Normal file
13
widget/looker/wails.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||
"name": "looker",
|
||||
"outputfilename": "looker",
|
||||
"frontend:install": "npm install",
|
||||
"frontend:build": "npm run build",
|
||||
"frontend:dev:watcher": "npm run dev",
|
||||
"frontend:dev:serverUrl": "auto",
|
||||
"author": {
|
||||
"name": "dmy",
|
||||
"email": "dmy@dmy.com"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user