first commit
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
Sync to CNB / sync (push) Has been cancelled
Delete old workflow runs / del_runs (push) Has been cancelled
Upstream Sync / Sync latest commits from upstream repo (push) Has been cancelled

This commit is contained in:
2026-01-30 16:57:44 +08:00
commit 3d175d75af
119 changed files with 35834 additions and 0 deletions

5
.cnb.yml Normal file
View File

@@ -0,0 +1,5 @@
include:
- https://cnb.cool/ilay1678/templates/-/blob/main/ci/docker.cnb.yml
- https://cnb.cool/ilay1678/templates/-/blob/main/ci/docker.ghcr.yml
- https://cnb.cool/ilay1678/templates/-/blob/main/ci/sync_to_github.yml
- https://cnb.cool/ilay1678/templates/-/blob/main/ci/ide.node.yml

14
.cnb/web_trigger.yml Normal file
View File

@@ -0,0 +1,14 @@
branch:
# 如下按钮在分支名以 release 开头的分支详情页面显示
- reg: "^main"
buttons:
- name: 构建
# 如存在,则将作为流水线 title否则流水线使用默认 title
description: 构建docker镜像
event: web_trigger # 触发的 CI 事件名
# 权限控制,不配置则有仓库写权限的用户可触发构建
# 如果配置,则需要有仓库写权限,并且满足 roles 或 users 其中之一才有权限触发构建
permissions:
# roles 和 users 配置其中之一或都配置均可,二者满足其一即可
roles:
- owner

20
.dockerignore Normal file
View File

@@ -0,0 +1,20 @@
node_modules
.next
.git
.github
.dockerignore
Dockerfile
build-and-push.sh
.DS_Store
*.log
.env
.env.*
.wrangler
.dev.vars
.vercel
.open-next
.cursor
.ai_memory
.claude
.fleet
.idea

6
.env.example Normal file
View File

@@ -0,0 +1,6 @@
# 测试点生成 - 阿里云通义大模型(可选)
# 配置后将优先使用 AI 生成测试用例,失败时自动回退到本地智能算法
ENABLE_AI=true
DASHSCOPE_API_KEY=sk-xxxxxxxxxxxxxxxx
# 可选,默认 qwen-max
# QWEN_MODEL=qwen-max

51
.github/workflows/clean-up.yml vendored Normal file
View File

@@ -0,0 +1,51 @@
name: Delete old workflow runs
on:
schedule:
- cron: '0 0 * * *'
jobs:
del_runs:
runs-on: ubuntu-latest
if: github.event.repository.fork == false
permissions:
actions: write
contents: read
id-token: write
steps:
- name: Delete workflow runs
uses: Mattraks/delete-workflow-runs@v2
with:
token: ${{ github.token }}
repository: ${{ github.repository }}
retain_days: 30
keep_minimum_runs: 6
# Points to a recent commit instead of `main` to avoid supply chain attacks. (The latest tag is very old.)
- name: 🎟 Get GitHub App token
uses: actions/create-github-app-token@v1
id: get-token
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Delete workflow runs for current repo
uses: Mattraks/delete-workflow-runs@v2
with:
token: ${{ github.token }}
repository: ${{ github.repository }}
retain_days: 7
keep_minimum_runs: 6
- name: Delete deployment
uses: strumwolf/delete-deployment-environment@v2.3.0
with:
token: ${{ steps.get-token.outputs.token }}
environment: Preview
onlyRemoveDeployments: true
- name: Delete MAIN deployment
uses: strumwolf/delete-deployment-environment@v2.3.0
with:
token: ${{ steps.get-token.outputs.token }}
environment: Production
onlyRemoveDeployments: true

75
.github/workflows/docker-build.yml vendored Normal file
View File

@@ -0,0 +1,75 @@
name: Build and Push Docker Image
on:
push:
branches:
- main
paths-ignore:
- 'README.md'
- '.cnb.yml'
- '.gitignore'
- '.github/workflows/clean-up.yml'
- '.cnb/web_trigger.yml'
pull_request:
branches:
- main
paths-ignore:
- 'README.md'
- '.gitignore'
- '.cnb.yml'
- '.github/workflows/clean-up.yml'
- '.cnb/web_trigger.yml'
permissions:
contents: read
packages: write
id-token: write
actions: write
jobs:
build:
runs-on: ubuntu-latest
if: github.event.repository.fork == false
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log into GHCR
uses: docker/login-action@master
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ github.token }}
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: set lower case owner name
run: |
echo "OWNER_LC=${OWNER,,}" >>${GITHUB_ENV}
env:
OWNER: "${{ github.repository_owner }}"
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64/v8
file: ./Dockerfile
push: true
tags: |
ghcr.io/${{ env.OWNER_LC }}/i-tools:latest
${{ vars.DOCKERHUB_USERNAME }}/i-tools:latest
- name: Post build cleanup
run: docker builder prune --force

23
.github/workflows/sync-to-cnb.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Sync to CNB
on: [push]
jobs:
sync:
runs-on: ubuntu-latest
if: github.event.repository.fork == false
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Sync to CNB Repository
run: |
docker run --rm \
-v ${{ github.workspace }}:${{ github.workspace }} \
-w ${{ github.workspace }} \
-e PLUGIN_TARGET_URL="https://cnb.cool/ilay1678/i-tools.git" \
-e PLUGIN_AUTH_TYPE="https" \
-e PLUGIN_USERNAME="cnb" \
-e PLUGIN_PASSWORD=${{ secrets.CNB_TOKEN }} \
-e PLUGIN_SYNC_MODE="rebase" \
tencentcom/git-sync

36
.github/workflows/sync.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: Upstream Sync
permissions:
contents: write
on:
schedule:
- cron: "0 4 * * *" # At 12PM UTC+8
workflow_dispatch:
jobs:
sync_latest_from_upstream:
name: Sync latest commits from upstream repo
runs-on: ubuntu-latest
if: ${{ github.repository != 'iLay1678/i-tools' }}
steps:
# Step 1: run a standard checkout action
- name: Checkout target repo
uses: actions/checkout@v4
# Step 2: run the sync action
- name: Sync upstream changes
id: sync
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.1
with:
upstream_sync_repo: iLay1678/i-tools
upstream_sync_branch: main
target_sync_branch: main
target_repo_token: ${{ github.token }}
- name: Sync check
if: failure()
run: |
echo "[Error] Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork."
exit 1

34
.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example
# wrangler files
.wrangler
.dev.vars
.vercel
.next
.open-next
.spec-workflow
.claude
.ai_memory

1
.node-version Normal file
View File

@@ -0,0 +1 @@
20.18.0

250
DEPLOY.md Normal file
View File

@@ -0,0 +1,250 @@
# 信奥工具箱 - 服务器部署指南
## 前置要求
1. **服务器环境**
- Linux 系统(推荐 Ubuntu 20.04+ 或 CentOS 7+
- Docker 已安装(版本 20.10+
- Docker Compose 已安装(可选,推荐)
2. **网络访问**
- 能够访问阿里云镜像仓库:`registry.cn-hangzhou.aliyuncs.com`
- 服务器端口开放(默认 3000 端口)
## 快速部署
### 方法一:使用 Docker Compose推荐
1. **登录阿里云镜像仓库**
```bash
docker login --username=<您的用户名> registry.cn-hangzhou.aliyuncs.com
```
2. **上传部署文件到服务器**
- `docker-compose.yml`
- `deploy.sh`(可选)
3. **执行部署**
```bash
# 使用部署脚本
chmod +x deploy.sh
./deploy.sh --compose
# 或直接使用 docker-compose
docker-compose pull
docker-compose up -d
```
4. **验证部署**
```bash
# 查看容器状态
docker-compose ps
# 查看日志
docker-compose logs -f
# 访问服务
curl http://localhost:3000
```
### 方法二:使用 Docker 命令
1. **登录阿里云镜像仓库**
```bash
docker login --username=<您的用户名> registry.cn-hangzhou.aliyuncs.com
```
2. **使用部署脚本**
```bash
chmod +x deploy.sh
./deploy.sh
```
3. **或手动执行**
```bash
# 拉取镜像
docker pull registry.cn-hangzhou.aliyuncs.com/nick-x86/i-tools:latest
# 停止并删除旧容器(如果存在)
docker stop i-tools 2>/dev/null || true
docker rm i-tools 2>/dev/null || true
# 启动新容器
docker run -d \
--name i-tools \
--restart unless-stopped \
-p 3000:3000 \
-e NODE_ENV=production \
registry.cn-hangzhou.aliyuncs.com/nick-x86/i-tools:latest
```
## 配置说明
### 端口配置
默认端口为 `3000`,如需修改:
**Docker Compose 方式:**
编辑 `docker-compose.yml`,修改 `ports` 配置:
```yaml
ports:
- "8080:3000" # 主机端口:容器端口
```
**Docker 命令方式:**
```bash
./deploy.sh -p 8080
# 或
docker run -d --name i-tools -p 8080:3000 ...
```
### 环境变量
可以在 `docker-compose.yml` 或 `docker run` 命令中添加环境变量:
```yaml
environment:
- NODE_ENV=production
- PORT=3000
# 添加其他环境变量
```
## 常用操作
### 查看日志
```bash
# Docker Compose
docker-compose logs -f i-tools
# Docker 命令
docker logs -f i-tools
```
### 重启服务
```bash
# Docker Compose
docker-compose restart i-tools
# Docker 命令
docker restart i-tools
```
### 停止服务
```bash
# Docker Compose
docker-compose stop i-tools
# Docker 命令
docker stop i-tools
```
### 更新部署
```bash
# 使用部署脚本(推荐)
./deploy.sh --compose
# 或手动更新
docker-compose pull
docker-compose up -d
```
### 查看容器状态
```bash
# Docker Compose
docker-compose ps
# Docker 命令
docker ps | grep i-tools
```
## Nginx 反向代理配置(可选)
如果需要通过域名访问,可以配置 Nginx 反向代理:
```nginx
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
```
## 故障排查
### 容器无法启动
```bash
# 查看详细日志
docker logs i-tools
# 检查端口是否被占用
netstat -tulpn | grep 3000
# 或
lsof -i :3000
```
### 无法访问服务
1. 检查防火墙设置
```bash
# Ubuntu/Debian
sudo ufw allow 3000
# CentOS/RHEL
sudo firewall-cmd --add-port=3000/tcp --permanent
sudo firewall-cmd --reload
```
2. 检查容器是否运行
```bash
docker ps | grep i-tools
```
3. 检查端口映射
```bash
docker port i-tools
```
### 镜像拉取失败
1. 确认已登录镜像仓库
```bash
docker login registry.cn-hangzhou.aliyuncs.com
```
2. 检查网络连接
```bash
ping registry.cn-hangzhou.aliyuncs.com
```
## 资源限制
默认配置的资源限制:
- CPU: 0.5-1 核心
- 内存: 256MB-512MB
如需调整,编辑 `docker-compose.yml` 中的 `deploy.resources` 部分。
## 安全建议
1. **使用非 root 用户运行容器**(已在 Dockerfile 中配置)
2. **定期更新镜像**`docker-compose pull && docker-compose up -d`
3. **配置防火墙**:只开放必要端口
4. **使用 HTTPS**:通过 Nginx 配置 SSL 证书
5. **监控日志**:定期检查容器日志
## 联系支持
如遇到问题,请检查:
1. Docker 版本是否符合要求
2. 服务器资源是否充足
3. 网络连接是否正常
4. 查看容器日志获取详细错误信息

36
Dockerfile Normal file
View File

@@ -0,0 +1,36 @@
FROM node:20-alpine AS base
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json ./
# 提高超时与重试,避免内网/跨平台构建时 npm 拉包 EIDLETIMEOUT
RUN npm config set fetch-timeout 300000 && npm config set fetch-retries 5 && npm ci
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NODE_ENV=production
RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
COPY --from=builder /app/public ./public
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
ENV PORT=3000
EXPOSE 3000
CMD ["node", "server.js"]

126
README.md Normal file
View File

@@ -0,0 +1,126 @@
<h1 align="center">i-Tools | 信奥工具箱</h1>
<div align="center">
[![Stars](https://img.shields.io/github/stars/iLay1678/i-tools?style=flat)](https://github.com/iLay1678/i-tools)
![badge](https://cnb.cool/ilay1678/i-tools/-/badge/git/latest/ci/status/tag_push)
[![License](https://img.shields.io/github/license/iLay1678/i-tools)](LICENSE)
</div>
<p align="center">
<strong>简约、高效、现代化的在线信奥工具集合</strong><br/>
基于 Next.js 16 + Tailwind CSS 4 + TypeScript 构建
</p>
## ✨ 特性
- **现代化设计**:精美的 UI支持明亮/暗黑模式自适应。
- **高性能**:基于 React Server Components 和静态生成。
- **纯前端处理**:大部分数据处理(如格式化、转换)在本地浏览器完成,保护隐私。
- **全能工具库**:涵盖开发、文本、编码、加密、转换、生成、生活等 40+ 款实用工具。
## 🛠️ 工具列表
### 👨‍💻 开发工具 (Dev Tools)
1. **JSON 格式化** (`/json-formatter`): 格式化、压缩、验证、Diff 对比。
2. **YAML 格式化** (`/yaml-formatter`): YAML 校验与美化。
3. **HTML 格式化** (`/html-formatter`): HTML 代码美化。
4. **SQL 格式化** (`/sql-formatter`): 支持多种数据库方言的 SQL 美化。
5. **文本 Diff 对比** (`/diff`): Monaco Editor 驱动并排代码差异对比。
6. **Markdown 编辑器** (`/markdown`): 实时预览、GFM 支持、导出 Markdown。
7. **HTML 转义** (`/html-escape`): HTML 实体编码/解码。
### 📝 文本工具 (Text Tools)
1. **文字格式化** (`/text-formatter`): 中英文盘古之白空格、符号修正。
2. **大小写转换** (`/case-converter`): 驼峰、蛇形、大写、小写互转。
3. **Lorem Ipsum** (`/lorem-ipsum`): 生成乱数假文。
### 🔐 加密与编码 (Crypto & Encoding)
1. **Base64 编解码** (`/base64`): 文本与 Base64 互转。
2. **URL 编解码** (`/url-encode`): URL 参数编码处理。
3. **Unicode 转换** (`/unicode`): Unicode 编码转换。
4. **Base32/Base58** (`/base32`, `/base58`): 其他常用编码。
5. **MD5/SHA 哈希** (`/hash`): 计算 MD5, SHA1, SHA256, SHA512。
6. **Bcrypt 哈希** (`/bcrypt`): 生成与验证 Bcrypt 密码哈希。
7. **AES/DES 加密** (`/aes-des`): 对称加密解密工具。
8. **JWT 解码** (`/jwt`): JWT Token 解析查看。
### 🎨 图像与可视化 (Visual)
1. **二维码生成** (`/qrcode`): 自定义颜色、Logo 的二维码生成。
2. **条形码生成** (`/barcode`): 生成 EAN, UPC, Code128 等条形码。
3. **ASCII 艺术** (`/ascii-art`): 文字转字符画。
4. **图片转像素画** (`/image-to-pixel`): 图片像素化风格转换。
5. **图片 Base64** (`/image-base64`): 图片文件与 Base64 字符串互转。
### 🧮 转换与计算 (Converters & Calc)
1. **时间戳转换** (`/timestamp`): Unix 时间戳与日期互转。
2. **进制转换器** (`/radix-converter`): 二/八/十/十六进制任意互转。
3. **IP 进制转换** (`/ip-radix`): IP 地址与整数/二进制转换。
4. **IP 子网计算** (`/ip-calc`): CIDR 子网划分计算。
5. **CSV/JSON 互转** (`/csv-json`): 数据格式互相转换。
### 🎲 生成与随机 (Generation)
1. **UUID 生成** (`/uuid`): 批量生成 Version 1/4 UUID。
2. **随机密码生成** (`/random-string`): 高强度随机密码生成器。
3. **大转盘抽奖** (`/wheel`): 随机决策工具。
4. **随机分组** (`/random-group`): 名单随机分组工具。
5. **抛硬币** (`/coin-flip`): 简单的概率工具。
### 🔧 实用工具 (Utilities)
1. **Cron 解析** (`/cron`): Cron 表达式翻译与执行时间预测。
2. **正则测试** (`/regex`): 正则表达式实时测试。
3. **颜色选择器** (`/color-picker`): HEX, RGB, HSL 转换与拾色。
4. **UA 解析** (`/user-agent`): 解析 User-Agent 字符串详情。
5. **键盘按键检测** (`/keyboard`): KeyCode 与按键事件查看。
6. **挪车码牌** (`/move-car`): 生成微信挪车通知码。
7. **番茄钟/秒表/倒计时**: 时间管理三件套。
8. **阿里云盘 TV Token**: 扫码获取 Token。
## 💻 技术栈
- **框架**: [Next.js 16](https://nextjs.org/) (App Directory)
- **语言**: [TypeScript](https://www.typescriptlang.org/)
- **样式**: [Tailwind CSS 4](https://tailwindcss.com/)
- **UI 组件**: [Radix UI](https://www.radix-ui.com/) + [Lucide Icons](https://lucide.dev/)
- **Linting**: [Oxc (Oxlint)](https://github.com/oxc-project/oxc) - 高性能 Linter替代 ESLint。
## 🚀 快速开始
### 开发
1. 克隆项目
```bash
git clone https://git.istudy.xin/yangfan/i-tools.git
cd i-tools
```
2. 安装依赖
```bash
npm install
```
3. 启动开发服务器
```bash
npm run dev
```
4. 代码检查 (使用 Oxc)
```bash
npm run lint
```
### 构建
```bash
npm run build
```
## 📦 部署
### Docker 部署
```bash
docker run --name=i-tools -d -p 3000:3000 registry.cn-hangzhou.aliyuncs.com/nick-x86/i-tools:1.0.8
```

154
app/aes-des/page.tsx Normal file
View File

@@ -0,0 +1,154 @@
"use client";
import React, { useState } from "react";
import CryptoJS from "crypto-js";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Shield, Lock, Unlock, Copy } from "lucide-react";
import { toast } from "sonner";
const ALGORITHMS = [
{ value: "AES", label: "AES" },
{ value: "DES", label: "DES" },
{ value: "TripleDES", label: "TripleDES" },
{ value: "Rabbit", label: "Rabbit" },
{ value: "RC4", label: "RC4" },
];
export default function AesDesPage() {
const [input, setInput] = useState("");
const [key, setKey] = useState("");
const [algorithm, setAlgorithm] = useState("AES");
const [output, setOutput] = useState("");
const handleProcess = (mode: "encrypt" | "decrypt") => {
if (!input) return;
if (!key) {
toast.error("请输入密钥");
return;
}
try {
let result = "";
const algo = (CryptoJS as any)[algorithm];
if (mode === "encrypt") {
result = algo.encrypt(input, key).toString();
toast.success("加密成功");
} else {
const bytes = algo.decrypt(input, key);
result = bytes.toString(CryptoJS.enc.Utf8);
if (!result) throw new Error("解密失败(可能是密钥错误)");
toast.success("解密成功");
}
setOutput(result);
} catch (e: any) {
toast.error("处理失败: " + e.message);
setOutput("");
}
};
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
toast.success("已复制到剪贴板");
} catch {
toast.error("复制失败");
}
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-red-500 to-rose-600 shadow-lg">
<Shield className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight">/</h1>
<p className="text-muted-foreground"> AES, DES, RC4 </p>
</div>
</div>
<div className="grid gap-6 md:grid-cols-12">
<Card className="md:col-span-8">
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label> ()</Label>
<Textarea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="请输入..."
className="min-h-37.5 font-mono"
/>
</div>
<div className="flex gap-4">
<Button onClick={() => handleProcess("encrypt")} className="flex-1 gap-2">
<Lock className="h-4 w-4" />
</Button>
<Button onClick={() => handleProcess("decrypt")} variant="secondary" className="flex-1 gap-2">
<Unlock className="h-4 w-4" />
</Button>
</div>
<div className="space-y-2 pt-4 border-t">
<div className="flex items-center justify-between">
<Label></Label>
<Button variant="ghost" size="sm" onClick={() => copyToClipboard(output)} disabled={!output}>
<Copy className="h-4 w-4 mr-2" />
</Button>
</div>
<Textarea
readOnly
value={output}
placeholder="结果将显示在这里..."
className="min-h-37.5 font-mono bg-muted/30"
/>
</div>
</CardContent>
</Card>
<Card className="md:col-span-4 h-fit">
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label></Label>
<Select value={algorithm} onValueChange={setAlgorithm}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{ALGORITHMS.map(a => (
<SelectItem key={a.value} value={a.value}>{a.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label> (Passphrase)</Label>
<Input
type="text"
value={key}
onChange={(e) => setKey(e.target.value)}
placeholder="请输入加密/解密密钥"
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -0,0 +1,338 @@
"use client";
import { useState, useEffect, useRef } from "react";
import {
CloudDownload,
Copy,
LogIn,
Loader2,
Info,
ExternalLink,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { toast } from "sonner";
interface StatusResponse {
status: "LoginSuccess" | "ScanSuccess" | "LoginFailed" | "QRCodeExpired" | "WaitLogin";
access_token: string;
refresh_token: string;
}
export default function AlipanTvToken() {
const [hasGenerated, setHasGenerated] = useState(false);
const [authUrl, setAuthUrl] = useState("");
const [isLoading, setIsLoading] = useState(true);
const [hasAccessToken, setHasAccessToken] = useState(false);
const [hasRefreshToken, setHasRefreshToken] = useState(false);
const [authorizing, setAuthorizing] = useState(false);
const [isNoticeOpen, setIsNoticeOpen] = useState(false);
const [accessToken, setAccessToken] = useState("");
const [refreshToken, setRefreshToken] = useState("");
const [currentSid, setCurrentSid] = useState("");
const [currentHost, setCurrentHost] = useState("");
const checkTimer = useRef<NodeJS.Timeout | null>(null);
const getCurrentHost = () => {
if (typeof window !== "undefined") {
return `${window.location.protocol}//${window.location.host}`;
}
return "";
};
async function generateAuthUrl() {
try {
setIsLoading(true);
const response = await fetch("/api/alipan-tv-token/generate_qr", {
method: "POST",
});
const data = await response.json();
setCurrentSid(data.sid);
setAuthUrl(`https://www.alipan.com/o/oauth/authorize?sid=${data.sid}`);
} catch {
toast.error("初始化失败,请检查网络");
} finally {
setIsLoading(false);
}
}
function closeNotice() {
setIsNoticeOpen(false);
}
async function checkStatus(sid: string) {
try {
const response = await fetch(`/api/alipan-tv-token/check_status/${sid}`);
const data: StatusResponse = await response.json();
if (data.status === "LoginSuccess") {
setAccessToken(data.access_token);
setRefreshToken(data.refresh_token);
setHasAccessToken(!!data.access_token);
setHasRefreshToken(!!data.refresh_token);
setAuthorizing(false);
toast.success("登录成功");
} else if (data.status === "ScanSuccess") {
checkTimer.current = setTimeout(() => checkStatus(sid), 2000);
} else if (data.status === "LoginFailed") {
setAuthorizing(false);
toast.error("登录失败,请刷新页面重试");
} else if (data.status === "QRCodeExpired") {
setAuthorizing(false);
toast.error("链接过期,请刷新页面重试");
} else {
// WaitLogin
checkTimer.current = setTimeout(() => checkStatus(sid), 2000);
}
} catch (error) {
console.error("检查状态时出错:", error);
toast.error("发生错误,请稍后重试");
}
}
const copyToClipboard = async (text: string, name: string) => {
try {
await navigator.clipboard.writeText(text);
toast.success(`${name} 已复制`);
} catch {
toast.error("复制失败");
}
};
const handleAuth = (url: string) => {
setAuthorizing(true);
window.open(url, "_blank");
if (currentSid) {
if (checkTimer.current) {
clearTimeout(checkTimer.current);
}
checkTimer.current = setTimeout(() => checkStatus(currentSid), 1000);
}
};
useEffect(() => {
setCurrentHost(getCurrentHost());
setIsNoticeOpen(true);
if (!hasGenerated) {
generateAuthUrl();
setHasGenerated(true);
}
return () => {
if (checkTimer.current) {
clearTimeout(checkTimer.current);
}
};
}, [hasGenerated]);
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
{/* Page Header */}
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-teal-500 to-teal-600 shadow-lg">
<CloudDownload className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight">TV Token</h1>
<p className="text-muted-foreground">
TV端的授权令牌
</p>
</div>
</div>
<div className="grid gap-6 lg:grid-cols-2">
{/* Left: Tokens */}
<div className="space-y-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-base font-medium">访</CardTitle>
<Button
variant="ghost"
size="sm"
className="h-8 gap-1"
disabled={!hasAccessToken}
onClick={() => copyToClipboard(accessToken, "访问令牌")}
>
<Copy className="h-3 w-3" />
</Button>
</CardHeader>
<CardContent>
<Textarea
value={accessToken}
readOnly
rows={4}
placeholder="授权成功后,访问令牌将显示在这里..."
className="font-mono resize-none bg-muted/50 text-xs"
/>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-base font-medium"></CardTitle>
<Button
variant="ghost"
size="sm"
className="h-8 gap-1"
disabled={!hasRefreshToken}
onClick={() => copyToClipboard(refreshToken, "刷新令牌")}
>
<Copy className="h-3 w-3" />
</Button>
</CardHeader>
<CardContent>
<Textarea
value={refreshToken}
readOnly
rows={3}
placeholder="刷新令牌将显示在这里..."
className="font-mono resize-none bg-muted/50 text-xs"
/>
</CardContent>
</Card>
</div>
{/* Right: Auth Action */}
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<LogIn className="h-4 w-4" />
</CardTitle>
</CardHeader>
<CardContent>
{!hasAccessToken && !hasRefreshToken && (
<div className="py-8">
{isLoading ? (
<div className="flex flex-col items-center gap-4 text-muted-foreground">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<p>...</p>
</div>
) : (
<Button
size="lg"
className="w-full gap-2 text-lg h-14"
onClick={() => handleAuth(authUrl)}
disabled={authorizing}
>
{authorizing ? (
<>
<Loader2 className="h-5 w-5 animate-spin" />
...
</>
) : (
<>
<LogIn className="h-5 w-5" />
</>
)}
</Button>
)}
</div>
)}
{(hasAccessToken || hasRefreshToken) && (
<div className="py-8 flex flex-col items-center gap-4 text-emerald-600 dark:text-emerald-400">
<div className="h-16 w-16 rounded-full bg-emerald-100 dark:bg-emerald-900/30 flex items-center justify-center">
<CloudDownload className="h-8 w-8" />
</div>
<p className="font-medium"></p>
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base font-medium">API </CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<span className="text-sm font-medium">OAuth </span>
<div className="p-3 bg-muted rounded-md break-all text-xs font-mono">
{currentHost}/api/oauth/alipan/token
</div>
</CardContent>
</Card>
</div>
</div>
{/* Info Card */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<span className="text-xl">💡</span> 使
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-2">
<h4 className="font-medium text-sm"></h4>
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li>TV版的刷新令牌</li>
<li>TV接口可绕过三方应用权益包的速率限制</li>
<li>SVIP会员才能享受高速下载</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="font-medium text-sm">使</h4>
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li>"开始授权登录"</li>
<li>使APP扫码</li>
<li></li>
<li>使</li>
</ul>
</div>
</div>
<Alert className="bg-amber-50 dark:bg-amber-950/20 text-amber-900 dark:text-amber-200 border-amber-200 dark:border-amber-800">
<Info className="h-4 w-4" />
<AlertTitle></AlertTitle>
<AlertDescription>
TV接口能绕过三方应用权益包的速率限制SVIP会员才能享受高速下载
</AlertDescription>
</Alert>
</CardContent>
</Card>
<Dialog open={isNoticeOpen} onOpenChange={setIsNoticeOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>使</DialogTitle>
</DialogHeader>
<DialogDescription className="py-4">
TV版
<br /><br />
<strong></strong> TV接口能绕过三方应用权益包的速率限制SVIP
</DialogDescription>
<DialogFooter className="flex-col sm:flex-row gap-2">
<Button
variant="outline"
className="gap-2 text-red-600 hover:text-red-700 hover:bg-red-50 border-red-200"
asChild
>
<a href="https://www.alipan.com/cpx/member?userCode=MjAyNTk2" target="_blank" rel="noopener noreferrer">
<ExternalLink className="h-4 w-4" />
</a>
</Button>
<Button onClick={closeNotice}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -0,0 +1,89 @@
import { decrypt, getParams } from '@/utils/decode'
interface QrCodeStatus {
status: 'WaitLogin' | 'LoginSuccess' | 'QRCodeExpired' | 'ScanSuccess' | 'LoginFailed'
authCode?: string
}
interface TokenResponseEncrypt {
data: {
ciphertext: string
iv: string
}
}
interface TokenRequest {
akv: string
apv: string
b: string
d: string
m: string
mac: string
n: string
t: number
wifiMac: string
code?: string
'Content-Type': string
}
export async function GET(
request: Request,
{ params }: { params: Promise<{ sid: string }> }
) {
try {
const { sid } = await params
const statusResponse = await fetch(`https://openapi.alipan.com/oauth/qrcode/${sid}/status`)
if (!statusResponse.ok) {
throw new Error('Failed to check status')
}
const statusData: QrCodeStatus = await statusResponse.json()
if (statusData.status === 'LoginSuccess' && statusData.authCode) {
try {
const t = Math.floor(Date.now() / 1000)
const sendData: TokenRequest = {
...getParams(t),
code: statusData.authCode,
"Content-Type": "application/json"
} as TokenRequest
const headers = Object.fromEntries(
Object.entries(sendData).map(([k, v]) => [k, String(v)])
)
const tokenResponse = await fetch('https://api.extscreen.com/aliyundrive/v3/token', {
method: 'POST',
headers: headers,
body: JSON.stringify(sendData)
})
if (!tokenResponse.ok) {
throw new Error('Failed to get token')
}
const tokenResult: TokenResponseEncrypt = await tokenResponse.json()
const plainData = decrypt(tokenResult.data.ciphertext, tokenResult.data.iv, t)
const tokenInfo = JSON.parse(plainData)
return Response.json({
status: 'LoginSuccess',
refresh_token: tokenInfo.refresh_token,
access_token: tokenInfo.access_token
})
} catch {
return Response.json({ status: 'LoginFailed' })
}
}
return Response.json(statusData)
} catch (error: any) {
return Response.json(
{ error: error.message },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,38 @@
interface ApiResponse<T> {
data: T
}
interface QrCodeData {
qrCodeUrl: string
sid: string
}
export async function POST() {
try {
const response = await fetch('https://api.extscreen.com/aliyundrive/qrcode', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
scopes: ["user:base", "file:all:read", "file:all:write"].join(','),
width: 500,
height: 500,
})
})
if (!response.ok) {
throw new Error('Failed to generate QR code')
}
const result: ApiResponse<QrCodeData> = await response.json()
return Response.json({
qr_link: result.data.qrCodeUrl,
sid: result.data.sid
})
} catch (error: any) {
return Response.json(
{ error: error.message },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,94 @@
import { decrypt, getParams } from '@/utils/decode'
interface TokenResponseEncrypt {
data: {
ciphertext: string
iv: string
}
}
async function refreshToken(refreshTokenValue: string) {
const t = Math.floor(Date.now() / 1000)
const sendData = {
...getParams(t),
refresh_token: refreshTokenValue,
"Content-Type": "application/json"
}
const headers = Object.fromEntries(
Object.entries(sendData).map(([k, v]) => [k, String(v)])
)
const tokenResponse = await fetch('https://api.extscreen.com/aliyundrive/v3/token', {
method: 'POST',
headers: headers,
body: JSON.stringify(sendData)
})
if (!tokenResponse.ok) {
throw new Error('Failed to refresh token')
}
const tokenData: TokenResponseEncrypt = await tokenResponse.json()
const plainData = decrypt(tokenData.data.ciphertext, tokenData.data.iv, t)
const tokenInfo = JSON.parse(plainData)
return tokenInfo
}
export async function POST(request: Request) {
try {
const { refresh_token } = await request.json()
const tokenInfo = await refreshToken(refresh_token)
return Response.json({
token_type: 'Bearer',
access_token: tokenInfo.access_token,
refresh_token: tokenInfo.refresh_token,
expires_in: tokenInfo.expires_in
})
} catch (error: any) {
return Response.json(
{
code: 500,
message: error.message,
data: null
},
{ status: 500 }
)
}
}
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url)
const refresh_ui = searchParams.get('refresh_ui')
if (!refresh_ui) {
return Response.json({
refresh_token: '',
access_token: '',
text: 'refresh_ui parameter is required'
})
}
const tokenInfo = await refreshToken(refresh_ui)
return Response.json({
refresh_token: tokenInfo.refresh_token,
access_token: tokenInfo.access_token,
text: ''
})
} catch (error: any) {
return Response.json({
refresh_token: '',
access_token: '',
text: error.message
})
}
}

View File

@@ -0,0 +1,42 @@
import { NextRequest } from "next/server";
import { getTask, getZipPathForTask } from "@/lib/testcase-backend";
import fs from "fs/promises";
export async function GET(
_req: NextRequest,
{ params }: { params: Promise<{ taskId: string }> }
) {
const { taskId } = await params;
if (!taskId) {
return Response.json({ error: "缺少 taskId" }, { status: 400 });
}
const task = getTask(taskId);
if (!task) {
return Response.json({ error: "任务未找到" }, { status: 404 });
}
if (task.status !== "completed") {
return Response.json({ error: "任务尚未完成" }, { status: 400 });
}
const zipPath = getZipPathForTask(taskId);
if (!zipPath) {
return Response.json({ error: "文件未找到" }, { status: 404 });
}
try {
const buffer = await fs.readFile(zipPath);
const filename = `${task.englishName}_testcases.zip`;
return new Response(buffer, {
status: 200,
headers: {
"Content-Type": "application/zip",
"Content-Disposition": `attachment; filename="${encodeURIComponent(filename)}"`,
},
});
} catch (e) {
console.error("[testcase] download read error:", e);
return Response.json({ error: "文件读取失败" }, { status: 500 });
}
}

View File

@@ -0,0 +1,75 @@
import { NextRequest } from "next/server";
import {
createTask,
getEstimatedTime,
findRunningTaskByEnglishName,
processTask,
} from "@/lib/testcase-backend";
export async function POST(req: NextRequest) {
try {
const body = await req.json();
const {
title,
englishName,
description,
standardCode,
serviceLevel = "standard",
testCaseCount: rawCount,
} = body;
if (!title || !englishName || !description || !standardCode) {
return Response.json(
{ success: false, error: "请填写必填字段题目标题、题目英文名、题目描述、C++标准程序" },
{ status: 400 }
);
}
if (!/^[a-z][a-z0-9_]*$/.test(String(englishName).trim())) {
return Response.json(
{ success: false, error: "题目英文名格式错误:只能包含小写字母、数字和下划线,必须以字母开头" },
{ status: 400 }
);
}
const testCaseCount = Math.min(100, Math.max(1, parseInt(String(rawCount), 10) || 10));
const existing = findRunningTaskByEnglishName(String(englishName));
if (existing) {
return Response.json(
{ success: false, error: "相同英文名的任务正在处理中,请稍后再试或使用不同的英文名" },
{ status: 409 }
);
}
const taskId = "task_" + Date.now() + "_" + Math.random().toString(36).slice(2, 11);
createTask(taskId, {
title: String(title).trim(),
englishName: String(englishName).trim().toLowerCase(),
description: String(description).trim(),
standardCode: String(standardCode).trim(),
testCaseCount,
serviceLevel: String(serviceLevel),
});
setImmediate(() => {
processTask(taskId).catch((err) => {
console.error(`[testcase] processTask error:`, err);
});
});
const estimated_time = getEstimatedTime(String(serviceLevel), testCaseCount);
return Response.json({
success: true,
task_id: taskId,
message: "任务已创建,正在处理中...",
estimated_time,
});
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return Response.json(
{ success: false, error: "创建任务时发生错误,请重试:" + message },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,37 @@
import { NextRequest } from "next/server";
import { getTask, getEstimatedTime } from "@/lib/testcase-backend";
export async function GET(
_req: NextRequest,
{ params }: { params: Promise<{ taskId: string }> }
) {
const { taskId } = await params;
if (!taskId) {
return Response.json({ error: "缺少 taskId" }, { status: 400 });
}
const task = getTask(taskId);
if (!task) {
return Response.json({ error: "任务未找到" }, { status: 404 });
}
const runningTime = Math.floor((Date.now() - task.createdAt.getTime()) / 1000);
const estimated_time = getEstimatedTime(task.serviceLevel, task.testCaseCount);
return Response.json({
task_id: task.id,
status: task.status,
progress: task.progress,
title: task.title,
english_name: task.englishName,
test_case_count: task.testCaseCount,
generated_cases: task.generatedCases ?? 0,
service_level: task.serviceLevel,
created_at: task.createdAt,
updated_at: task.updatedAt,
running_time: runningTime,
estimated_time,
error_message: task.errorMessage ?? undefined,
currentMessage: task.currentMessage ?? undefined,
});
}

142
app/ascii-art/page.tsx Normal file
View File

@@ -0,0 +1,142 @@
"use client";
import React, { useState, useEffect } from "react";
import figlet from "figlet";
// @ts-ignore
import standardFont from "figlet/importable-fonts/Standard.js";
// @ts-ignore
import ghostFont from "figlet/importable-fonts/Ghost.js";
// @ts-ignore
import slantFont from "figlet/importable-fonts/Slant.js";
// @ts-ignore
import bubbleFont from "figlet/importable-fonts/Bubble.js";
// @ts-ignore
import bannerFont from "figlet/importable-fonts/Banner.js";
// @ts-ignore
import bigFont from "figlet/importable-fonts/Big.js";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Terminal, Copy } from "lucide-react";
import { toast } from "sonner";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
// Register fonts
figlet.parseFont("Standard", standardFont);
figlet.parseFont("Ghost", ghostFont);
figlet.parseFont("Slant", slantFont);
figlet.parseFont("Bubble", bubbleFont);
figlet.parseFont("Banner", bannerFont);
figlet.parseFont("Big", bigFont);
const FONTS = ["Standard", "Ghost", "Slant", "Bubble", "Banner", "Big"];
export default function AsciiArtPage() {
const [text, setText] = useState("i-Tools");
const [font, setFont] = useState("Standard");
const [output, setOutput] = useState("");
useEffect(() => {
figlet.text(
text,
{
font: font as any,
horizontalLayout: "default",
verticalLayout: "default",
width: 80,
whitespaceBreak: true,
},
function (err, data) {
if (err) {
console.log("Something went wrong...");
console.dir(err);
return;
}
setOutput(data || "");
}
);
}, [text, font]);
const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(output);
toast.success("已复制到剪贴板");
} catch {
toast.error("复制失败");
}
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-emerald-600 to-green-700 shadow-lg">
<Terminal className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight">ASCII </h1>
<p className="text-muted-foreground"></p>
</div>
</div>
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2"></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label></Label>
<Input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Type something..."
maxLength={20}
/>
</div>
<div className="space-y-2">
<Label></Label>
<Select value={font} onValueChange={setFont}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{FONTS.map(f => (
<SelectItem key={f} value={f}>{f}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle></CardTitle>
<Button variant="outline" size="sm" onClick={copyToClipboard} disabled={!output}>
<Copy className="h-4 w-4 mr-2" />
</Button>
</CardHeader>
<CardContent>
<div className="relative rounded-lg bg-black p-4 overflow-x-auto">
<pre className="text-green-500 font-mono text-sm leading-none whitespace-pre select-all">
{output || "Generating..."}
</pre>
</div>
</CardContent>
</Card>
</div>
</div>
);
}

168
app/barcode/page.tsx Normal file
View File

@@ -0,0 +1,168 @@
"use client";
import React, { useState } from "react";
import Barcode from "react-barcode";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Barcode as BarcodeIcon, Download, RotateCcw } from "lucide-react";
import { toast } from "sonner";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export default function BarcodePage() {
const [value, setValue] = useState("123456789012");
const [format, setFormat] = useState("CODE128");
const [width, setWidth] = useState(2);
const [height, setHeight] = useState(100);
const [displayValue, setDisplayValue] = useState(true);
const handleDownload = () => {
const svg = document.querySelector("#barcode-svg svg");
if (!svg) return;
const svgData = new XMLSerializer().serializeToString(svg);
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const img = new Image();
img.onload = () => {
canvas.width = img.width;
canvas.height = img.height;
ctx?.drawImage(img, 0, 0);
const pngFile = canvas.toDataURL("image/png");
const downloadLink = document.createElement("a");
downloadLink.download = `barcode-${value}.png`;
downloadLink.href = pngFile;
downloadLink.click();
toast.success("条形码已下载");
};
img.src = "data:image/svg+xml;base64," + btoa(svgData);
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-zinc-700 to-slate-800 shadow-lg">
<BarcodeIcon className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground"> EAN, UPC, CODE128 </p>
</div>
</div>
<div className="grid gap-6 md:grid-cols-12">
<Card className="md:col-span-8 flex flex-col items-center justify-center min-h-125 p-8">
<div id="barcode-svg" className="p-8 bg-white rounded-xl shadow-xs border border-slate-100 overflow-x-auto max-w-full">
{value ? (
<Barcode
value={value}
format={format as any}
width={width}
height={height}
displayValue={displayValue}
background="#ffffff"
lineColor="#000000"
/>
) : (
<div className="text-muted-foreground text-sm"></div>
)}
</div>
<div className="mt-8 flex gap-4">
<Button onClick={handleDownload} disabled={!value}>
<Download className="h-4 w-4 mr-2" />
PNG
</Button>
</div>
</Card>
<Card className="md:col-span-4 h-fit">
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label></Label>
<Input
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="例如123456789"
/>
</div>
<div className="space-y-2">
<Label></Label>
<Select value={format} onValueChange={setFormat}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="CODE128">CODE128 ()</SelectItem>
<SelectItem value="CODE39">CODE39</SelectItem>
<SelectItem value="EAN13">EAN13</SelectItem>
<SelectItem value="UPC">UPC</SelectItem>
<SelectItem value="ITF14">ITF14</SelectItem>
<SelectItem value="MSI">MSI</SelectItem>
<SelectItem value="pharmacode">Pharmacode</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>: {width}</Label>
<Input
type="range"
min="1"
max="4"
step="0.5"
value={width}
onChange={(e) => setWidth(parseFloat(e.target.value))}
/>
</div>
<div className="space-y-2">
<Label>: {height}</Label>
<Input
type="range"
min="30"
max="200"
value={height}
onChange={(e) => setHeight(parseInt(e.target.value))}
/>
</div>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="displayValue"
checked={displayValue}
onChange={(e) => setDisplayValue(e.target.checked)}
className="rounded border-gray-300 text-primary focus:ring-primary"
aria-label="显示文字"
/>
<Label htmlFor="displayValue" className="cursor-pointer"></Label>
</div>
<Button variant="outline" onClick={() => setValue("")} className="w-full">
<RotateCcw className="h-4 w-4 mr-2" />
</Button>
</CardContent>
</Card>
</div>
<div className="bg-muted/50 p-4 rounded-lg text-sm text-muted-foreground">
💡 EAN13
</div>
</div>
);
}

155
app/base32/page.tsx Normal file
View File

@@ -0,0 +1,155 @@
"use client";
import { useState } from "react";
import { Binary, Copy, Trash2, ArrowUpDown } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { toast } from "sonner";
// RFC 4648 Base32 alphabet
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
const ALPHABET_MAP = ALPHABET.split('').reduce((map, char, index) => {
map[char] = index;
return map;
}, {} as { [key: string]: number });
export default function Base32Page() {
const [input, setInput] = useState("");
const copyToClipboard = async (text: string) => {
if (!text) return;
try {
await navigator.clipboard.writeText(text);
toast.success("已复制到剪贴板");
} catch {
toast.error("复制失败");
}
};
const encode = () => {
try {
const encoder = new TextEncoder();
const data = encoder.encode(input);
let bits = 0;
let value = 0;
let output = "";
for (let i = 0; i < data.length; i++) {
value = (value << 8) | data[i];
bits += 8;
while (bits >= 5) {
output += ALPHABET[(value >>> (bits - 5)) & 31];
bits -= 5;
}
}
if (bits > 0) {
output += ALPHABET[(value << (5 - bits)) & 31];
}
// Padding
while (output.length % 8 !== 0) {
output += "=";
}
setInput(output);
toast.success("已编码为 Base32");
} catch {
toast.error("编码失败");
}
};
const decode = () => {
try {
let val = input.toUpperCase().replace(/=+$/, "");
let bits = 0;
let value = 0;
let index = 0;
const output = new Uint8Array((val.length * 5) / 8 | 0);
for (let i = 0; i < val.length; i++) {
if (!(val[i] in ALPHABET_MAP)) throw new Error("Invalid character");
value = (value << 5) | ALPHABET_MAP[val[i]];
bits += 5;
if (bits >= 8) {
output[index++] = (value >>> (bits - 8)) & 0xFF;
bits -= 8;
}
}
const decoder = new TextDecoder();
setInput(decoder.decode(output));
toast.success("已解码为文本");
} catch {
toast.error("解码失败:无效的 Base32 字符串");
}
};
const clearAll = () => setInput("");
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-orange-500 to-red-600 shadow-lg">
<Binary className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight">Base32 </h1>
<p className="text-muted-foreground">RFC 4648 Base32 </p>
</div>
</div>
<div className="grid gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0">
<CardTitle className="text-base font-medium">/</CardTitle>
<div className="flex gap-2">
<Button variant="ghost" size="sm" onClick={() => copyToClipboard(input)} disabled={!input}>
<Copy className="h-4 w-4 mr-2" />
</Button>
<Button variant="ghost" size="sm" onClick={clearAll} disabled={!input} className="text-destructive hover:text-destructive">
<Trash2 className="h-4 w-4 mr-2" />
</Button>
</div>
</CardHeader>
<CardContent>
<Textarea
placeholder="请输入需要编解码的内容..."
className="min-h-62.5 font-mono text-base resize-y"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
</CardContent>
</Card>
<div className="flex flex-col sm:flex-row gap-4">
<Button onClick={encode} size="lg" className="flex-1 gap-2">
<ArrowUpDown className="h-4 w-4" />
(Encode)
</Button>
<Button onClick={decode} size="lg" variant="outline" className="flex-1 gap-2">
<ArrowUpDown className="h-4 w-4" />
(Decode)
</Button>
</div>
</div>
{/* Info Card */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<span className="text-xl">💡</span> Base32
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-sm text-muted-foreground space-y-2">
<p>Base32 使 32 A-Z 2-7</p>
<p> RFC 4648 </p>
</div>
</CardContent>
</Card>
</div>
);
}

171
app/base58/page.tsx Normal file
View File

@@ -0,0 +1,171 @@
"use client";
import { useState } from "react";
import { Binary, Copy, Trash2, ArrowUpDown } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { toast } from "sonner";
// Base58 alphabet (Bitcoin style)
const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
const ALPHABET_MAP = ALPHABET.split('').reduce((map, char, index) => {
map[char] = index;
return map;
}, {} as { [key: string]: number });
export default function Base58Page() {
const [input, setInput] = useState("");
const copyToClipboard = async (text: string) => {
if (!text) return;
try {
await navigator.clipboard.writeText(text);
toast.success("已复制到剪贴板");
} catch {
toast.error("复制失败");
}
};
// Helper: Text -> Bytes
const textToBytes = (text: string) => {
const encoder = new TextEncoder();
return encoder.encode(text);
};
// Helper: Bytes -> Text
const bytesToText = (bytes: Uint8Array) => {
const decoder = new TextDecoder();
return decoder.decode(bytes);
};
// Encode
const encode = () => {
if (!input) return;
const bytes = textToBytes(input);
if (bytes.length === 0) return "";
let digits = [0];
for (let i = 0; i < bytes.length; i++) {
for (let j = 0; j < digits.length; j++) digits[j] <<= 8;
digits[0] += bytes[i];
let carry = 0;
for (let j = 0; j < digits.length; ++j) {
digits[j] += carry;
carry = (digits[j] / 58) | 0;
digits[j] %= 58;
}
while (carry) {
digits.push(carry % 58);
carry = (carry / 58) | 0;
}
}
// Deal with leading zeros
for (let i = 0; i < bytes.length && bytes[i] === 0; i++) digits.push(0);
const result = digits.reverse().map(d => ALPHABET[d]).join("");
setInput(result);
toast.success("已编码为 Base58");
};
// Decode
const decode = () => {
if (!input) return;
const bytes = [0];
for (let i = 0; i < input.length; i++) {
const c = input[i];
if (!(c in ALPHABET_MAP)) {
toast.error("无效的 Base58 字符");
return;
}
for (let j = 0; j < bytes.length; j++) bytes[j] *= 58;
bytes[0] += ALPHABET_MAP[c];
let carry = 0;
for (let j = 0; j < bytes.length; ++j) {
bytes[j] += carry;
carry = bytes[j] >> 8;
bytes[j] &= 0xff;
}
while (carry) {
bytes.push(carry & 0xff);
carry >>= 8;
}
}
// Deal with leading zeros (represented by '1' in Base58 Bitcoin)
for (let i = 0; i < input.length && input[i] === '1'; i++) bytes.push(0);
const result = bytesToText(new Uint8Array(bytes.reverse()));
setInput(result);
toast.success("已解码为文本");
};
const clearAll = () => setInput("");
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-amber-500 to-orange-600 shadow-lg">
<Binary className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight">Base58 </h1>
<p className="text-muted-foreground"> Base58 </p>
</div>
</div>
<div className="grid gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0">
<CardTitle className="text-base font-medium">/</CardTitle>
<div className="flex gap-2">
<Button variant="ghost" size="sm" onClick={() => copyToClipboard(input)} disabled={!input}>
<Copy className="h-4 w-4 mr-2" />
</Button>
<Button variant="ghost" size="sm" onClick={clearAll} disabled={!input} className="text-destructive hover:text-destructive">
<Trash2 className="h-4 w-4 mr-2" />
</Button>
</div>
</CardHeader>
<CardContent>
<Textarea
placeholder="请输入需要编解码的内容..."
className="min-h-[250px] font-mono text-base resize-y"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
</CardContent>
</Card>
<div className="flex flex-col sm:flex-row gap-4">
<Button onClick={encode} size="lg" className="flex-1 gap-2">
<ArrowUpDown className="h-4 w-4" />
(Encode)
</Button>
<Button onClick={decode} size="lg" variant="outline" className="flex-1 gap-2">
<ArrowUpDown className="h-4 w-4" />
(Decode)
</Button>
</div>
</div>
{/* Info Card */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<span className="text-xl">💡</span> Base58
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-sm text-muted-foreground space-y-2">
<p>Base58 Base640, O, I, l+, /使</p>
<p>使<code>123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz</code></p>
</div>
</CardContent>
</Card>
</div>
);
}

195
app/base64/page.tsx Normal file
View File

@@ -0,0 +1,195 @@
"use client";
import { useState } from "react";
import { Lock, ArrowRightLeft, Copy, Eraser } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { toast } from "sonner";
export default function Base64Page() {
const [input, setInput] = useState("");
const [output, setOutput] = useState("");
const [mode, setMode] = useState<"encode" | "decode">("encode");
const handleEncode = () => {
if (!input) {
toast.warning("请输入要编码的内容");
return;
}
try {
const encoded = btoa(unescape(encodeURIComponent(input)));
setOutput(encoded);
toast.success("编码成功");
} catch {
toast.error("编码失败");
}
};
const handleDecode = () => {
if (!input) {
toast.warning("请输入要解码的内容");
return;
}
try {
const decoded = decodeURIComponent(escape(atob(input)));
setOutput(decoded);
toast.success("解码成功");
} catch {
toast.error("解码失败,请检查输入是否为有效的 Base64 字符串");
}
};
const handleConvert = () => {
if (mode === "encode") {
handleEncode();
} else {
handleDecode();
}
};
const copyToClipboard = async () => {
if (!output) {
toast.warning("没有可复制的内容");
return;
}
try {
await navigator.clipboard.writeText(output);
toast.success("已复制到剪贴板");
} catch {
toast.error("复制失败");
}
};
const clearAll = () => {
setInput("");
setOutput("");
};
const swapInputOutput = () => {
setInput(output);
setOutput("");
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
{/* Page Header */}
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-emerald-500 to-emerald-600 shadow-lg">
<Lock className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight">Base64 </h1>
<p className="text-muted-foreground">
Base64
</p>
</div>
</div>
{/* Action Bar */}
<div className="flex flex-wrap items-center gap-4 bg-muted/50 p-4 rounded-lg border">
<Tabs value={mode} onValueChange={(v) => setMode(v as "encode" | "decode")}>
<TabsList>
<TabsTrigger value="encode"></TabsTrigger>
<TabsTrigger value="decode"></TabsTrigger>
</TabsList>
</Tabs>
<div className="flex items-center gap-2 ml-auto sm:ml-0">
<Button onClick={handleConvert} className="gap-2">
<ArrowRightLeft className="h-4 w-4" />
{mode === "encode" ? "编码" : "解码"}
</Button>
<Button variant="outline" onClick={swapInputOutput} className="gap-2">
<ArrowRightLeft className="h-4 w-4 rotate-90" />
</Button>
<Button variant="ghost" onClick={clearAll} className="gap-2 text-destructive hover:text-destructive">
<Eraser className="h-4 w-4" />
</Button>
</div>
</div>
<div className="grid gap-6 lg:grid-cols-2">
<div className="space-y-4">
<Card className="h-full flex flex-col">
<CardHeader className="pb-3">
<CardTitle className="text-base font-medium">
{mode === "encode" ? "原始文本" : "Base64 字符串"}
</CardTitle>
</CardHeader>
<CardContent className="flex-1 min-h-75">
<Textarea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder={
mode === "encode"
? "请输入要编码的文本..."
: "请输入要解码的 Base64 字符串..."
}
className="h-full min-h-75 font-mono resize-none"
/>
</CardContent>
</Card>
</div>
<div className="space-y-4">
<Card className="h-full flex flex-col">
<CardHeader className="pb-3 flex flex-row items-center justify-between space-y-0">
<CardTitle className="text-base font-medium">
{mode === "encode" ? "Base64 结果" : "解码结果"}
</CardTitle>
<Button
variant="ghost"
size="sm"
className="h-8 gap-1"
onClick={copyToClipboard}
>
<Copy className="h-3 w-3" />
</Button>
</CardHeader>
<CardContent className="flex-1 min-h-75">
<Textarea
value={output}
readOnly
placeholder="转换结果将显示在这里..."
className="h-full min-h-75 font-mono resize-none bg-muted/50"
/>
</CardContent>
</Card>
</div>
</div>
{/* Info Card */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<span className="text-xl">💡</span> 使
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-2">
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li>Base64 64 </li>
<li> URLCookie </li>
<li></li>
</ul>
</div>
<div className="space-y-2">
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li> 33%</li>
<li>Base64 </li>
<li>"交换"</li>
</ul>
</div>
</div>
</CardContent>
</Card>
</div>
);
}

172
app/bcrypt/page.tsx Normal file
View File

@@ -0,0 +1,172 @@
"use client";
import React, { useState } from "react";
import bcrypt from "bcryptjs";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Lock, CheckCircle, XCircle, Copy, Hash } from "lucide-react";
import { toast } from "sonner";
import { Slider } from "@/components/ui/slider";
export default function BcryptPage() {
const [plainText, setPlainText] = useState("");
const [saltRounds, setSaltRounds] = useState(10);
const [hashResult, setHashResult] = useState("");
const [verifyText, setVerifyText] = useState("");
const [verifyHash, setVerifyHash] = useState("");
const [isMatch, setIsMatch] = useState<boolean | null>(null);
const handleGenerate = async () => {
if (!plainText) return;
try {
const hash = await bcrypt.hash(plainText, saltRounds);
setHashResult(hash);
toast.success("哈希生成成功");
} catch {
toast.error("生成失败");
}
};
const handleVerify = async () => {
if (!verifyText || !verifyHash) return;
try {
const match = await bcrypt.compare(verifyText, verifyHash);
setIsMatch(match);
if (match) {
toast.success("验证成功:匹配");
} else {
toast.error("验证失败:不匹配");
}
} catch {
setIsMatch(false);
toast.error("验证过程出错,请检查哈希格式");
}
};
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
toast.success("已复制到剪贴板");
} catch {
toast.error("复制失败");
}
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-slate-600 to-zinc-700 shadow-lg">
<Lock className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight">Bcrypt </h1>
<p className="text-muted-foreground"> Bcrypt </p>
</div>
</div>
<div className="grid gap-6 lg:grid-cols-2">
{/* Generate Card */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Hash className="h-5 w-5 text-primary" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label></Label>
<Input
value={plainText}
onChange={(e) => setPlainText(e.target.value)}
placeholder="请输入要加密的文本"
/>
</div>
<div className="space-y-4">
<div className="flex justify-between">
<Label>Salt Rounds (): {saltRounds}</Label>
<span className="text-xs text-muted-foreground"></span>
</div>
<Slider
value={[saltRounds]}
onValueChange={(v) => setSaltRounds(v[0])}
min={4}
max={16}
step={1}
/>
</div>
<Button onClick={handleGenerate} disabled={!plainText} className="w-full">
</Button>
{hashResult && (
<div className="mt-4 p-3 bg-muted rounded-md break-all font-mono text-sm relative group">
{hashResult}
<Button
variant="ghost"
size="icon"
className="absolute top-1 right-1 h-6 w-6 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={() => copyToClipboard(hashResult)}
>
<Copy className="h-3 w-3" />
</Button>
</div>
)}
</CardContent>
</Card>
{/* Verify Card */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<CheckCircle className="h-5 w-5 text-emerald-500" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label></Label>
<Input
value={verifyText}
onChange={(e) => setVerifyText(e.target.value)}
placeholder="请输入明文"
/>
</div>
<div className="space-y-2">
<Label>Bcrypt </Label>
<Input
value={verifyHash}
onChange={(e) => setVerifyHash(e.target.value)}
placeholder="$2a$10$..."
/>
</div>
<Button onClick={handleVerify} disabled={!verifyText || !verifyHash} variant="secondary" className="w-full">
</Button>
{isMatch !== null && (
<div className={`mt-4 p-4 rounded-md flex items-center justify-center gap-2 font-bold ${isMatch ? 'bg-emerald-500/10 text-emerald-600' : 'bg-destructive/10 text-destructive'}`}>
{isMatch ? (
<>
<CheckCircle className="h-5 w-5" />
</>
) : (
<>
<XCircle className="h-5 w-5" />
</>
)}
</div>
)}
</CardContent>
</Card>
</div>
</div>
);
}

166
app/case-converter/page.tsx Normal file
View File

@@ -0,0 +1,166 @@
"use client";
import { useState } from "react";
import { CaseSensitive, Copy, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { toast } from "sonner";
export default function CaseConverterPage() {
const [input, setInput] = useState("");
const copyToClipboard = async (text: string) => {
if (!text) return;
try {
await navigator.clipboard.writeText(text);
toast.success("已复制到剪贴板");
} catch {
toast.error("复制失败");
}
};
const toUpper = () => setInput(input.toUpperCase());
const toLower = () => setInput(input.toLowerCase());
const toTitleCase = () => {
const result = input
.toLowerCase()
.split(/[\s_-]+/)
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
setInput(result);
};
const toCamelCase = () => {
const words = input.toLowerCase().split(/[\s_-]+/);
const result = words[0] + words.slice(1).map(word => word.charAt(0).toUpperCase() + word.slice(1)).join("");
setInput(result);
};
const toPascalCase = () => {
const result = input
.toLowerCase()
.split(/[\s_-]+/)
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join("");
setInput(result);
};
const toSnakeCase = () => {
const result = input
.match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)
?.map(x => x.toLowerCase())
.join("_") || "";
setInput(result);
};
const toKebabCase = () => {
const result = input
.match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)
?.map(x => x.toLowerCase())
.join("-") || "";
setInput(result);
};
const clearAll = () => setInput("");
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
{/* Page Header */}
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-violet-500 to-purple-600 shadow-lg">
<CaseSensitive className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground">
</p>
</div>
</div>
<div className="grid gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0">
<CardTitle className="text-base font-medium"></CardTitle>
<div className="flex gap-2">
<Button variant="ghost" size="sm" onClick={() => copyToClipboard(input)} disabled={!input}>
<Copy className="h-4 w-4 mr-2" />
</Button>
<Button variant="ghost" size="sm" onClick={clearAll} disabled={!input} className="text-destructive hover:text-destructive">
<Trash2 className="h-4 w-4 mr-2" />
</Button>
</div>
</CardHeader>
<CardContent>
<Textarea
placeholder="请在此输入或粘贴需要转换的文本..."
className="min-h-50 font-mono text-base resize-y"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-3">
<Button type="button" disabled={!input} onClick={toUpper} variant="secondary" className="flex-1 min-w-35">
UPPER CASE ()
</Button>
<Button type="button" disabled={!input} onClick={toLower} variant="secondary" className="flex-1 min-w-35">
lower case ()
</Button>
<Button type="button" disabled={!input} onClick={toTitleCase} variant="secondary" className="flex-1 min-w-35">
Title Case ()
</Button>
<Button type="button" disabled={!input} onClick={toCamelCase} variant="secondary" className="flex-1 min-w-35">
camelCase ()
</Button>
<Button type="button" disabled={!input} onClick={toPascalCase} variant="secondary" className="flex-1 min-w-35">
PascalCase ()
</Button>
<Button type="button" disabled={!input} onClick={toSnakeCase} variant="secondary" className="flex-1 min-w-35">
snake_case ()
</Button>
<Button type="button" disabled={!input} onClick={toKebabCase} variant="secondary" className="flex-1 min-w-35">
kebab-case (线)
</Button>
</div>
</CardContent>
</Card>
</div>
{/* Info Card */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<span className="text-xl">💡</span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<div className="space-y-1">
<p className="font-semibold text-sm">Camel Case</p>
<p className="text-xs text-muted-foreground"> `helloWorld`</p>
</div>
<div className="space-y-1">
<p className="font-semibold text-sm">Snake Case</p>
<p className="text-xs text-muted-foreground">线 `hello_world`</p>
</div>
<div className="space-y-1">
<p className="font-semibold text-sm">Kebab Case</p>
<p className="text-xs text-muted-foreground"> `hello-world`</p>
</div>
</div>
</CardContent>
</Card>
</div>
);
}

87
app/coin-flip/page.tsx Normal file
View File

@@ -0,0 +1,87 @@
"use client";
import { useState } from "react";
import { CircleDollarSign, RotateCw } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
export default function CoinFlipPage() {
const [flipping, setFlipping] = useState(false);
const [result, setResult] = useState<"HEADS" | "TAILS" | null>(null);
const flip = () => {
if (flipping) return;
setFlipping(true);
setResult(null);
// Random outcome
const outcome = Math.random() > 0.5 ? "HEADS" : "TAILS";
// Simulate animation time
setTimeout(() => {
setResult(outcome);
setFlipping(false);
}, 2000);
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-amber-500 to-yellow-600 shadow-lg">
<CircleDollarSign className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground"></p>
</div>
</div>
<div className="flex flex-col items-center justify-center py-12 space-y-12">
<div className="relative perspective-[1000px]">
<div className={cn(
"w-48 h-48 relative transition-transform duration-1000 transform-style-3d",
flipping && "animate-spin-slow-3d", // We need to define this animation or simulate it
!flipping && result === "HEADS" && "rotate-y-0",
!flipping && result === "TAILS" && "rotate-y-180",
)}>
{/* Front (Heads) */}
<div className={cn(
"absolute w-full h-full rounded-full bg-amber-400 border-4 border-amber-600 shadow-xl flex items-center justify-center backface-hidden",
// If result is tails, and not flipping, this side is hidden.
// But simplified: Just use CSS transforms.
)}>
<span className="text-5xl font-bold text-amber-800"></span>
</div>
{/* Back (Tails) */}
<div className={cn(
"absolute w-full h-full rounded-full bg-slate-300 border-4 border-slate-500 shadow-xl flex items-center justify-center backface-hidden rotate-y-180"
)}>
<span className="text-5xl font-bold text-slate-700"></span>
</div>
</div>
</div>
<div className="text-center space-y-6">
<div className="h-8">
{result && !flipping && (
<span className="text-2xl font-bold animate-in fade-in zoom-in duration-300">
{result === "HEADS" ? "正面 (HEADS)" : "反面 (TAILS)"}
</span>
)}
{flipping && (
<span className="text-lg text-muted-foreground animate-pulse">
...
</span>
)}
</div>
<Button size="lg" onClick={flip} disabled={flipping} className="gap-2 text-lg px-8">
<RotateCw className={cn("h-5 w-5", flipping && "animate-spin")} />
{flipping ? "翻转中..." : "开始抛硬币"}
</Button>
</div>
</div>
</div>
);
}

153
app/color-picker/page.tsx Normal file
View File

@@ -0,0 +1,153 @@
"use client";
import { useState, useEffect } from "react";
import { Palette, Copy, RefreshCw } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { toast } from "sonner";
export default function ColorPickerPage() {
const [color, setColor] = useState("#4F46E5");
const [formats, setFormats] = useState({
hex: "#4F46E5",
rgb: "rgb(79, 70, 229)",
hsl: "hsl(243, 75%, 59%)"
});
const hexToRgb = (hex: string) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
};
const rgbToHsl = (r: number, g: number, b: number) => {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h = 0, s, l = (max + min) / 2;
if (max === min) {
h = s = 0;
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) };
};
useEffect(() => {
const rgb = hexToRgb(color);
if (rgb) {
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
setFormats({
hex: color.toUpperCase(),
rgb: `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`,
hsl: `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`
});
}
}, [color]);
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
toast.success(`已复制: ${text}`);
};
const randomColor = () => {
const randomHex = '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6, '0');
setColor(randomHex.toUpperCase());
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-pink-500 to-rose-600 shadow-lg">
<Palette className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground">HEXRGBHSL </p>
</div>
</div>
<div className="grid gap-6 lg:grid-cols-2">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-base"></CardTitle>
<Button variant="ghost" size="icon" onClick={randomColor}>
<RefreshCw className="h-4 w-4" />
</Button>
</CardHeader>
<CardContent className="space-y-6">
<div
className="w-full h-48 rounded-xl shadow-inner border border-white/20 transition-colors duration-200 flex items-center justify-center"
style={{ backgroundColor: color }}
>
<input
type="color"
value={color}
onChange={(e) => setColor(e.target.value.toUpperCase())}
className="opacity-0 w-full h-full cursor-pointer"
/>
<span
className="pointer-events-none font-mono font-bold text-2xl drop-shadow-md"
style={{ color: (hexToRgb(color)?.r || 0) * 0.299 + (hexToRgb(color)?.g || 0) * 0.587 + (hexToRgb(color)?.b || 0) * 0.114 > 186 ? 'black' : 'white' }}
>
{color}
</span>
</div>
<div className="space-y-4">
<Label> HEX</Label>
<div className="flex gap-2">
<Input
value={color}
onChange={(e) => {
const val = e.target.value;
if (/^#[0-9A-F]{0,6}$/i.test(val)) setColor(val);
}}
className="font-mono"
maxLength={7}
/>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<FormatItem label="HEX" value={formats.hex} onCopy={() => copyToClipboard(formats.hex)} />
<FormatItem label="RGB" value={formats.rgb} onCopy={() => copyToClipboard(formats.rgb)} />
<FormatItem label="HSL" value={formats.hsl} onCopy={() => copyToClipboard(formats.hsl)} />
</CardContent>
</Card>
</div>
</div>
);
}
function FormatItem({ label, value, onCopy }: { label: string; value: string; onCopy: () => void }) {
return (
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">{label}</Label>
<div className="flex gap-2">
<Input readOnly value={value} className="font-mono bg-muted/30" />
<Button variant="outline" size="icon" onClick={onCopy}>
<Copy className="h-4 w-4" />
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,54 @@
'use client'
import React from 'react'
import Link from 'next/link'
import { Github, Home } from 'lucide-react'
export default function ClientLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="min-h-screen font-sans antialiased flex flex-col">
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container flex h-16 max-w-screen-xl items-center mx-auto px-4">
<div className="mr-8 hidden md:flex">
<Link href="/" className="mr-6 flex items-center space-x-2 transition-colors hover:text-primary/90">
<span className="hidden font-bold sm:inline-block text-xl tracking-tight">
</span>
</Link>
</div>
<div className="flex flex-1 items-center justify-between space-x-2 md:justify-end">
<nav className="flex items-center space-x-6 text-sm font-medium">
<Link
href="/"
className="transition-colors hover:text-primary text-foreground/80 flex items-center gap-2 group"
>
<div className="p-1 rounded-md group-hover:bg-accent">
<Home className="h-4 w-4" />
</div>
<span></span>
</Link>
</nav>
</div>
</div>
</header>
<main className="flex-1">
<div className="container max-w-screen-xl mx-auto py-8 px-4 lg:py-12">
{children}
</div>
</main>
<footer className="py-6 border-t bg-muted/30">
<div className="container max-w-screen-xl mx-auto px-4 flex flex-col items-center justify-center gap-4">
<p className="text-balance text-center text-sm leading-loose text-muted-foreground">
© {new Date().getFullYear()}
</p>
</div>
</footer>
</div>
)
}

View File

@@ -0,0 +1,97 @@
'use client'
import { useState, useEffect } from 'react'
export default function Loading() {
const [loading, setLoading] = useState(true)
useEffect(() => {
const checkPageLoaded = () => {
if (document.readyState === 'complete') {
setLoading(false)
}
}
if (document.readyState === 'complete') {
setLoading(false)
} else {
window.addEventListener('load', checkPageLoaded)
}
return () => {
window.removeEventListener('load', checkPageLoaded)
}
}, [])
if (!loading) return null
return (
<div className="loading-overlay">
<div className="loading-container">
<div className="loading-wave">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<div className="loading-text">...</div>
</div>
<style jsx>{`
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #f8fafc;
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.loading-wave {
display: flex;
justify-content: center;
align-items: flex-end;
height: 40px;
gap: 8px;
}
.loading-wave > div {
width: 8px;
height: 100%;
background: linear-gradient(45deg, #60a5fa, #3b82f6);
border-radius: 4px;
animation: wave 1s ease-in-out infinite;
}
.loading-wave > div:nth-child(2) { animation-delay: 0.1s; }
.loading-wave > div:nth-child(3) { animation-delay: 0.2s; }
.loading-wave > div:nth-child(4) { animation-delay: 0.3s; }
.loading-wave > div:nth-child(5) { animation-delay: 0.4s; }
.loading-text {
color: #3b82f6;
font-size: 1rem;
font-weight: 500;
letter-spacing: 0.05em;
}
@keyframes wave {
0%, 100% { transform: scaleY(0.5); }
50% { transform: scaleY(1); }
}
`}</style>
</div>
)
}

View File

@@ -0,0 +1,59 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

View File

@@ -0,0 +1,50 @@
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View File

@@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@@ -0,0 +1,57 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@@ -0,0 +1,213 @@
"use client"
import * as React from "react"
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react"
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) {
const defaultClassNames = getDefaultClassNames()
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"bg-background group/calendar p-3 [--cell-size:2rem] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className
)}
captionLayout={captionLayout}
formatters={{
formatMonthDropdown: (date) =>
date.toLocaleString("default", { month: "short" }),
...formatters,
}}
classNames={{
root: cn("w-fit", defaultClassNames.root),
months: cn(
"relative flex flex-col gap-4 md:flex-row",
defaultClassNames.months
),
month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
nav: cn(
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
defaultClassNames.nav
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
defaultClassNames.button_previous
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
defaultClassNames.button_next
),
month_caption: cn(
"flex h-[--cell-size] w-full items-center justify-center px-[--cell-size]",
defaultClassNames.month_caption
),
dropdowns: cn(
"flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium",
defaultClassNames.dropdowns
),
dropdown_root: cn(
"has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border",
defaultClassNames.dropdown_root
),
dropdown: cn(
"bg-popover absolute inset-0 opacity-0",
defaultClassNames.dropdown
),
caption_label: cn(
"select-none font-medium",
captionLayout === "label"
? "text-sm"
: "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
defaultClassNames.caption_label
),
table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
defaultClassNames.weekday
),
week: cn("mt-2 flex w-full", defaultClassNames.week),
week_number_header: cn(
"w-[--cell-size] select-none",
defaultClassNames.week_number_header
),
week_number: cn(
"text-muted-foreground select-none text-[0.8rem]",
defaultClassNames.week_number
),
day: cn(
"group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
defaultClassNames.day
),
range_start: cn(
"bg-accent rounded-l-md",
defaultClassNames.range_start
),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end),
today: cn(
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today
),
outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside
),
disabled: cn(
"text-muted-foreground opacity-50",
defaultClassNames.disabled
),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
}}
components={{
Root: ({ className, rootRef, ...props }) => {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
)
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return (
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
)
}
if (orientation === "right") {
return (
<ChevronRightIcon
className={cn("size-4", className)}
{...props}
/>
)
}
return (
<ChevronDownIcon className={cn("size-4", className)} {...props} />
)
},
DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className="flex size-[--cell-size] items-center justify-center text-center">
{children}
</div>
</td>
)
},
...components,
}}
{...props}
/>
)
}
function CalendarDayButton({
className,
day,
modifiers,
...props
}: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames()
const ref = React.useRef<HTMLButtonElement>(null)
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused])
return (
<Button
ref={ref}
variant="ghost"
size="icon"
data-day={day.date.toLocaleDateString()}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 flex aspect-square h-auto w-full min-w-[--cell-size] flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className
)}
{...props}
/>
)
}
export { Calendar, CalendarDayButton }

View File

@@ -0,0 +1,76 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("grid place-content-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View File

@@ -0,0 +1,122 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

178
app/components/ui/form.tsx Normal file
View File

@@ -0,0 +1,178 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
FormProvider,
useFormContext,
type ControllerProps,
type FieldPath,
type FieldValues,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue | null>(null)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
if (!itemContext) {
throw new Error("useFormField should be used within <FormItem>")
}
const fieldState = getFieldState(fieldContext.name, formState)
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue | null>(null)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-[0.8rem] text-muted-foreground", className)}
{...props}
/>
)
})
FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message ?? "") : children
if (!body) {
return null
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-[0.8rem] font-medium text-destructive", className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

View File

@@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@@ -0,0 +1,33 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverAnchor = PopoverPrimitive.Anchor
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-popover-content-transform-origin]",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

View File

@@ -0,0 +1,159 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

View File

@@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"
import { cn } from "@/lib/utils"
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
"relative flex w-full touch-none select-none items-center",
className
)}
{...props}
>
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
))
Slider.displayName = SliderPrimitive.Root.displayName
export { Slider }

View File

@@ -0,0 +1,32 @@
"use client"
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
position="top-center"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
)
}
export { Toaster }

View File

@@ -0,0 +1,29 @@
"use client"
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

View File

@@ -0,0 +1,55 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<"textarea">
>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
})
Textarea.displayName = "Textarea"
export { Textarea }

View File

@@ -0,0 +1,32 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
className
)}
{...props}
/>
</TooltipPrimitive.Portal>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

156
app/countdown/page.tsx Normal file
View File

@@ -0,0 +1,156 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { Hourglass, Play, Pause, RotateCcw, BellRing } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
export default function CountdownPage() {
const [inputMinutes, setInputMinutes] = useState("5");
const [timeLeft, setTimeLeft] = useState(300); // 5 minutes in seconds
const [running, setRunning] = useState(false);
const [finished, setFinished] = useState(false);
const timerRef = useRef<any>(null);
useEffect(() => {
if (running && timeLeft > 0) {
timerRef.current = setInterval(() => {
setTimeLeft((prev) => prev - 1);
}, 1000);
} else if (timeLeft === 0 && running) {
setRunning(false);
setFinished(true);
toast.success("时间到!", {
icon: <BellRing className="text-primary" />,
duration: 5000
});
// Try to play a subtle beep if browser allowed
try {
const audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)();
const oscillator = audioCtx.createOscillator();
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(440, audioCtx.currentTime);
oscillator.connect(audioCtx.destination);
oscillator.start();
oscillator.stop(audioCtx.currentTime + 0.5);
} catch {}
} else {
clearInterval(timerRef.current);
}
return () => clearInterval(timerRef.current);
}, [running, timeLeft]);
const startTimer = () => {
if (timeLeft === 0) {
const secs = parseInt(inputMinutes) * 60;
if (isNaN(secs) || secs <= 0) {
toast.error("请输入有效的分钟数");
return;
}
setTimeLeft(secs);
}
setRunning(true);
setFinished(false);
};
const handleReset = () => {
setRunning(false);
setFinished(false);
const secs = parseInt(inputMinutes) * 60 || 300;
setTimeLeft(secs);
};
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
const progress = timeLeft / (parseInt(inputMinutes) * 60 || 300);
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-amber-500 to-orange-600 shadow-lg">
<Hourglass className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground"></p>
</div>
</div>
<div className="max-w-md mx-auto space-y-8">
<Card className="p-8 flex flex-col items-center space-y-8">
<div className="relative flex items-center justify-center">
{/* Circular Progress SVG */}
<svg className="w-64 h-64 transform -rotate-90">
<circle
cx="128" cy="128" r="120"
stroke="currentColor" strokeWidth="8"
fill="transparent" className="text-muted/30"
/>
<circle
cx="128" cy="128" r="120"
stroke="currentColor" strokeWidth="8"
fill="transparent"
strokeDasharray={2 * Math.PI * 120}
strokeDashoffset={2 * Math.PI * 120 * (1 - progress)}
strokeLinecap="round"
className="text-primary transition-all duration-1000 ease-linear"
/>
</svg>
<div className={cn(
"absolute text-6xl font-mono font-bold tabular-nums",
finished && "animate-bounce text-primary"
)}>
{formatTime(timeLeft)}
</div>
</div>
<div className="flex items-center gap-4 w-full">
<div className="flex-1 space-y-1">
<span className="text-[10px] uppercase font-bold text-muted-foreground"></span>
<Input
type="number"
value={inputMinutes}
onChange={(e) => {
setInputMinutes(e.target.value);
if (!running) setTimeLeft(parseInt(e.target.value) * 60 || 0);
}}
disabled={running}
className="text-center font-bold text-lg"
/>
</div>
<div className="flex gap-2 pt-5">
{!running ? (
<Button size="lg" className="h-12 w-12 rounded-full p-0" onClick={startTimer}>
<Play className="h-6 w-6" />
</Button>
) : (
<Button size="lg" variant="outline" className="h-12 w-12 rounded-full p-0" onClick={() => setRunning(false)}>
<Pause className="h-6 w-6" />
</Button>
)}
<Button size="lg" variant="secondary" className="h-12 w-12 rounded-full p-0" onClick={handleReset}>
<RotateCcw className="h-6 w-6" />
</Button>
</div>
</div>
</Card>
{finished && (
<div className="p-4 rounded-xl bg-primary/10 border border-primary/20 text-center animate-in zoom-in duration-500">
<p className="font-bold text-primary flex items-center justify-center gap-2">
<BellRing className="h-5 w-5" />
</p>
</div>
)}
</div>
</div>
);
}

79
app/counter/page.tsx Normal file
View File

@@ -0,0 +1,79 @@
"use client";
import { useState } from "react";
import { Hash, Plus, Minus, RotateCcw } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
export default function CounterPage() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-zinc-500 to-slate-600 shadow-lg">
<Hash className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground"></p>
</div>
</div>
<div className="grid gap-6 max-w-md mx-auto">
<Card className="text-center">
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-8">
<div className="text-8xl font-bold tracking-tighter tabular-nums text-primary transition-all duration-200 scale-100 hover:scale-105">
{count}
</div>
<div className="flex justify-center gap-4">
<Button
size="lg"
variant="outline"
onClick={() => setCount(prev => prev - step)}
className="h-16 w-16 rounded-full border-2"
>
<Minus className="h-8 w-8" />
</Button>
<Button
size="lg"
variant="default"
onClick={() => setCount(prev => prev + step)}
className="h-16 w-16 rounded-full shadow-lg hover:shadow-xl hover:-translate-y-1 transition-all"
>
<Plus className="h-8 w-8" />
</Button>
</div>
<div className="flex items-center justify-center gap-4 pt-4 border-t">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">:</span>
<Input
type="number"
value={step}
onChange={(e) => setStep(Number(e.target.value) || 1)}
className="w-20 text-center h-8"
/>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setCount(0)}
className="text-muted-foreground hover:text-destructive"
>
<RotateCcw className="h-4 w-4 mr-1" />
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
);
}

116
app/cron/page.tsx Normal file
View File

@@ -0,0 +1,116 @@
"use client";
import { useState } from "react";
import { Timer, Info } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { toast } from "sonner";
export default function CronPage() {
const [expression, setExpression] = useState("*/5 * * * *");
const parts = expression.split(/\s+/);
const labels = ["分钟", "小时", "日期", "月份", "星期"];
const decodePart = (part: string, type: string) => {
if (!part) return "-";
if (part === "*") return `${type}`;
if (part.includes("/")) {
const [start, step] = part.split("/");
return `每隔 ${step} ${type}${start !== "*" ? ` (从第 ${start} 开始)` : ""}`;
}
if (part.includes("-")) {
return `${part.replace("-", " 到 ")} ${type}`;
}
if (part.includes(",")) {
return `${part} ${type}`;
}
return `${part} ${type}`;
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-orange-500 to-red-600 shadow-lg">
<Timer className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight">Cron </h1>
<p className="text-muted-foreground"> Cron </p>
</div>
</div>
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent>
<div className="flex gap-4">
<Input
value={expression}
onChange={(e) => setExpression(e.target.value)}
placeholder="如: */5 * * * *"
className="font-mono text-lg"
/>
<Button onClick={() => toast.success("解析成功")}></Button>
</div>
</CardContent>
</Card>
<div className="grid md:grid-cols-5 gap-4">
{labels.map((label, i) => (
<Card key={label} className="bg-muted/30 border-none">
<CardContent className="pt-6 text-center space-y-2">
<p className="text-xs text-muted-foreground font-bold">{label}</p>
<p className="font-mono text-xl font-bold text-primary">{parts[i] || "*"}</p>
<p className="text-xs text-muted-foreground">{decodePart(parts[i], label)}</p>
</CardContent>
</Card>
))}
</div>
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Info className="h-4 w-4" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="p-4 rounded-lg bg-primary/5 border border-primary/10">
<p className="text-lg">
<span className="font-bold text-primary ml-2">
{parts.length >= 5
? labels.map((l, i) => decodePart(parts[i], l)).join("")
: "请输入完整的 5 位 Cron 表达式"}
</span>
</p>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 text-sm text-muted-foreground">
<div className="space-y-1">
<p className="font-bold text-foreground">*</p>
<p></p>
</div>
<div className="space-y-1">
<p className="font-bold text-foreground">/n</p>
<p> ( n)</p>
</div>
<div className="space-y-1">
<p className="font-bold text-foreground">-</p>
<p> ( x y)</p>
</div>
<div className="space-y-1">
<p className="font-bold text-foreground">,</p>
<p></p>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
);
}

144
app/csv-json/page.tsx Normal file
View File

@@ -0,0 +1,144 @@
"use client";
import { useState } from "react";
import { FileSpreadsheet, Copy, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { toast } from "sonner";
export default function CsvJsonPage() {
const [input, setInput] = useState("");
const [output, setOutput] = useState("");
const copyToClipboard = async (text: string) => {
if (!text) return;
try {
await navigator.clipboard.writeText(text);
toast.success("已复制到剪贴板");
} catch {
toast.error("复制失败");
}
};
const csvToJson = () => {
try {
if (!input.trim()) return;
const lines = input.trim().split('\n');
if (lines.length < 2) {
toast.error("请输入至少包含标题和一行数据的 CSV");
return;
}
const headers = lines[0].split(',').map(h => h.trim());
const result = lines.slice(1).map(line => {
const values = line.split(',');
const obj: any = {};
headers.forEach((header, i) => {
obj[header] = values[i]?.trim() || "";
});
return obj;
});
setOutput(JSON.stringify(result, null, 2));
toast.success("转换成功");
} catch {
toast.error("CSV 格式错误");
}
};
const jsonToCsv = () => {
try {
if (!input.trim()) return;
const data = JSON.parse(input);
if (!Array.isArray(data) || data.length === 0) {
toast.error("JSON 必须是包含对象的数组");
return;
}
const headers = Object.keys(data[0]);
const csvLines = [
headers.join(','),
...data.map(row => headers.map(h => row[h]).join(','))
];
setOutput(csvLines.join('\n'));
toast.success("转换成功");
} catch {
toast.error("JSON 格式错误或不是对象数组");
}
};
const clearAll = () => {
setInput("");
setOutput("");
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-green-500 to-emerald-600 shadow-lg">
<FileSpreadsheet className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight">CSV / JSON </h1>
<p className="text-muted-foreground"> JSON </p>
</div>
</div>
<div className="grid gap-6 lg:grid-cols-2">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-base font-medium"></CardTitle>
<Button variant="ghost" size="sm" onClick={clearAll} className="text-destructive">
<Trash2 className="h-4 w-4" />
</Button>
</CardHeader>
<CardContent className="space-y-4">
<Textarea
placeholder="请在此粘贴 CSV (逗号分隔) 或 JSON 数组..."
className="min-h-75 font-mono text-sm resize-none bg-muted/30"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<div className="flex gap-4">
<Button onClick={csvToJson} className="flex-1 gap-2">
CSV &rarr; JSON
</Button>
<Button onClick={jsonToCsv} variant="outline" className="flex-1 gap-2">
JSON &rarr; CSV
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-base font-medium"></CardTitle>
<Button variant="ghost" size="sm" onClick={() => copyToClipboard(output)} disabled={!output}>
<Copy className="h-4 w-4" />
</Button>
</CardHeader>
<CardContent>
<Textarea
readOnly
placeholder="转换结果将显示在这里..."
className="min-h-87 font-mono text-sm resize-none bg-muted/30"
value={output}
/>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle className="text-sm">使</CardTitle>
</CardHeader>
<CardContent className="text-sm text-muted-foreground space-y-2">
<p>1. <strong>CSV &rarr; JSON</strong>: Key</p>
<p>2. <strong>JSON &rarr; CSV</strong>: JSON CSV </p>
</CardContent>
</Card>
</div>
);
}

174
app/diff/page.tsx Normal file
View File

@@ -0,0 +1,174 @@
"use client";
import React, { useState } from "react";
import { DiffEditor } from "@monaco-editor/react";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Diff, RotateCcw, ArrowRightLeft } from "lucide-react";
import { toast } from "sonner";
import { useTheme } from "next-themes";
const LANGUAGES = [
{ value: "plaintext", label: "纯文本" },
{ value: "json", label: "JSON" },
{ value: "javascript", label: "JavaScript" },
{ value: "typescript", label: "TypeScript" },
{ value: "html", label: "HTML" },
{ value: "css", label: "CSS" },
{ value: "sql", label: "SQL" },
{ value: "xml", label: "XML" },
{ value: "yaml", label: "YAML" },
{ value: "markdown", label: "Markdown" },
];
export default function DiffPage() {
const { theme } = useTheme();
const [original, setOriginal] = useState("");
const [modified, setModified] = useState("");
const [language, setLanguage] = useState("plaintext");
// Default to side-by-side view (false means side-by-side in Monaco Diff Editor options usually, wait, let's just control options)
const [renderSideBySide, setRenderSideBySide] = useState(true);
const handleClear = () => {
if (confirm("确定要清空所有内容吗?")) {
setOriginal("");
setModified("");
toast.info("已清空");
}
};
const handleSwap = () => {
const temp = original;
setOriginal(modified);
setModified(temp);
toast.success("已交换原始内容和修改内容");
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
{/* Header */}
<div className="flex items-center justify-between border-b pb-4">
<div className="flex items-center space-x-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-slate-500 to-zinc-600 shadow-lg">
<Diff className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight"> Diff </h1>
<p className="text-muted-foreground">
</p>
</div>
</div>
<div className="flex items-center gap-2">
{/* Language Selector */}
<Select value={language} onValueChange={setLanguage}>
<SelectTrigger className="w-35">
<SelectValue placeholder="选择语言" />
</SelectTrigger>
<SelectContent>
{LANGUAGES.map((lang) => (
<SelectItem key={lang.value} value={lang.value}>
{lang.label}
</SelectItem>
))}
</SelectContent>
</Select>
{/* View Mode Toggle */}
<Button
variant={renderSideBySide ? "secondary" : "ghost"}
size="sm"
onClick={() => setRenderSideBySide(true)}
className="hidden sm:flex"
>
</Button>
<Button
variant={!renderSideBySide ? "secondary" : "ghost"}
size="sm"
onClick={() => setRenderSideBySide(false)}
className="hidden sm:flex"
>
</Button>
</div>
</div>
{/* Main Editor Area */}
<Card className="flex-1 h-150 flex flex-col overflow-hidden">
<CardHeader className="py-3 px-4 border-b bg-muted/30 flex flex-row items-center justify-between">
<div className="flex items-center gap-8">
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
<span className="w-2 h-2 rounded-full bg-red-500/50"></span>
</div>
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
<span className="w-2 h-2 rounded-full bg-green-500/50"></span>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" size="icon" onClick={handleSwap} title="交换内容">
<ArrowRightLeft className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" onClick={handleClear} title="清空">
<RotateCcw className="h-4 w-4" />
</Button>
</div>
</CardHeader>
<CardContent className="p-0 flex-1 relative h-full">
<DiffEditor
height="100%"
language={language}
original={original}
modified={modified}
onMount={() => {
// Determine initial values if needed, but state is controlled slightly differently in DiffEditor
// actually DiffEditor is better uncontrolled for values usually or we need to manage models.
// But @monaco-editor/react handles `original` and `modified` props updates well.
// Listening to changes is a bit more complex if we want 2-way binding,
// but for a diff tool, usually users paste into it.
// Alternatively, we can use the modifiedModel to get content changes if we really needed
// but for a simple comparison tool, just passing props is often enough IF we provide a way to input.
// WAIT. The DiffEditor is often read-only for the comparison result, OR editable.
// By default originalEditable: false.
// Let's set originalEditable: true so users can paste into both sides.
}}
theme={theme === 'dark' ? "vs-dark" : "light"}
options={{
renderSideBySide: renderSideBySide,
originalEditable: true, // Allow editing left side
readOnly: false, // Allow editing right side
minimap: { enabled: false },
scrollBeyondLastLine: false,
fontSize: 14,
wordWrap: "on",
}}
/>
{/* Overlay hints if empty?
Monaco editor keeps state internally.
If we want to bind state, we might need a standard Editor first to input?
No, DiffEditor with `originalEditable: true` is fine for direct input.
*/}
</CardContent>
</Card>
<Card>
<CardContent className="p-4 text-sm text-muted-foreground">
💡
</CardContent>
</Card>
</div>
);
}

166
app/globals.css Normal file
View File

@@ -0,0 +1,166 @@
@import "tailwindcss";
@plugin "tailwindcss-animate";
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--spacing-100: 25rem;
--spacing-125: 31.25rem;
--spacing-150: 37.5rem;
--spacing-175: 43.75rem;
--spacing-35: 8.75rem;
--spacing-45: 11.25rem;
--spacing-50: 12.5rem;
--spacing-75: 18.75rem;
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.21 0.006 285.885);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.705 0.015 286.067);
}
.dark {
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.92 0.004 286.32);
--primary-foreground: oklch(0.21 0.006 285.885);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.552 0.016 285.938);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.552 0.016 285.938);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
background-image: radial-gradient(#e5e7eb 1px, transparent 1px);
background-size: 24px 24px;
}
.dark body {
background-image: radial-gradient(#1f2937 1px, transparent 1px);
}
}
@layer utilities {
.perspective-\[1000px\] {
perspective: 1000px;
}
.transform-style-3d {
transform-style: preserve-3d;
}
.backface-hidden {
backface-visibility: hidden;
}
.rotate-y-0 {
transform: rotateY(0deg);
}
.rotate-y-180 {
transform: rotateY(180deg);
}
.animate-spin-slow-3d {
animation: spin-slow-3d 3s infinite linear;
}
@keyframes spin-slow-3d {
from {
transform: rotateY(0deg);
}
to {
transform: rotateY(360deg);
}
}
}

337
app/hash/page.tsx Normal file
View File

@@ -0,0 +1,337 @@
"use client";
import { useState } from "react";
import { Lock, FileDigit, Copy, Eraser, Zap } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { toast } from "sonner";
interface HashResult {
md5: string;
sha1: string;
sha256: string;
sha512: string;
}
export default function HashPage() {
const [input, setInput] = useState("");
const [results, setResults] = useState<HashResult | null>(null);
const [loading, setLoading] = useState(false);
// MD5 Implementation preserved from original
const calculateMD5 = async (str: string): Promise<string> => {
const md5 = (string: string) => {
const rotateLeft = (x: number, n: number) => (x << n) | (x >>> (32 - n));
const addUnsigned = (x: number, y: number) => {
const x4 = x & 0x80000000;
const y4 = y & 0x80000000;
const x8 = x & 0x40000000;
const y8 = y & 0x40000000;
const result = (x & 0x3fffffff) + (y & 0x3fffffff);
if (x8 & y8) return result ^ 0x80000000 ^ x4 ^ y4;
if (x8 | y8) {
if (result & 0x40000000) return result ^ 0xc0000000 ^ x4 ^ y4;
else return result ^ 0x40000000 ^ x4 ^ y4;
} else {
return result ^ x4 ^ y4;
}
};
const F = (x: number, y: number, z: number) => (x & y) | (~x & z);
const G = (x: number, y: number, z: number) => (x & z) | (y & ~z);
const H = (x: number, y: number, z: number) => x ^ y ^ z;
const I = (x: number, y: number, z: number) => y ^ (x | ~z);
const FF = (a: number, b: number, c: number, d: number, x: number, s: number, ac: number) =>
addUnsigned(rotateLeft(addUnsigned(addUnsigned(a, F(b, c, d)), addUnsigned(x, ac)), s), b);
const GG = (a: number, b: number, c: number, d: number, x: number, s: number, ac: number) =>
addUnsigned(rotateLeft(addUnsigned(addUnsigned(a, G(b, c, d)), addUnsigned(x, ac)), s), b);
const HH = (a: number, b: number, c: number, d: number, x: number, s: number, ac: number) =>
addUnsigned(rotateLeft(addUnsigned(addUnsigned(a, H(b, c, d)), addUnsigned(x, ac)), s), b);
const II = (a: number, b: number, c: number, d: number, x: number, s: number, ac: number) =>
addUnsigned(rotateLeft(addUnsigned(addUnsigned(a, I(b, c, d)), addUnsigned(x, ac)), s), b);
const convertToWordArray = (str: string) => {
let lWordCount;
const lMessageLength = str.length;
const lNumberOfWords = (((lMessageLength + 8) - ((lMessageLength + 8) % 64)) / 64 + 1) * 16;
const lWordArray = Array(lNumberOfWords - 1).fill(0);
let lBytePosition = 0;
let lByteCount = 0;
while (lByteCount < lMessageLength) {
lWordCount = (lByteCount - (lByteCount % 4)) / 4;
lBytePosition = (lByteCount % 4) * 8;
lWordArray[lWordCount] = lWordArray[lWordCount] | (str.charCodeAt(lByteCount) << lBytePosition);
lByteCount++;
}
lWordCount = (lByteCount - (lByteCount % 4)) / 4;
lBytePosition = (lByteCount % 4) * 8;
lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition);
lWordArray[lNumberOfWords - 2] = lMessageLength << 3;
lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29;
return lWordArray;
};
const wordToHex = (lValue: number) => {
let result = "";
for (let lCount = 0; lCount <= 3; lCount++) {
const lByte = (lValue >>> (lCount * 8)) & 255;
result += ("0" + lByte.toString(16)).slice(-2);
}
return result;
};
const utf8Encode = (str: string) => {
return unescape(encodeURIComponent(str));
};
const x = convertToWordArray(utf8Encode(string));
let a = 0x67452301, b = 0xefcdab89, c = 0x98badcfe, d = 0x10325476;
for (let k = 0; k < x.length; k += 16) {
const AA = a, BB = b, CC = c, DD = d;
a = FF(a, b, c, d, x[k], 7, 0xd76aa478);
d = FF(d, a, b, c, x[k + 1], 12, 0xe8c7b756);
c = FF(c, d, a, b, x[k + 2], 17, 0x242070db);
b = FF(b, c, d, a, x[k + 3], 22, 0xc1bdceee);
a = FF(a, b, c, d, x[k + 4], 7, 0xf57c0faf);
d = FF(d, a, b, c, x[k + 5], 12, 0x4787c62a);
c = FF(c, d, a, b, x[k + 6], 17, 0xa8304613);
b = FF(b, c, d, a, x[k + 7], 22, 0xfd469501);
a = FF(a, b, c, d, x[k + 8], 7, 0x698098d8);
d = FF(d, a, b, c, x[k + 9], 12, 0x8b44f7af);
c = FF(c, d, a, b, x[k + 10], 17, 0xffff5bb1);
b = FF(b, c, d, a, x[k + 11], 22, 0x895cd7be);
a = FF(a, b, c, d, x[k + 12], 7, 0x6b901122);
d = FF(d, a, b, c, x[k + 13], 12, 0xfd987193);
c = FF(c, d, a, b, x[k + 14], 17, 0xa679438e);
b = FF(b, c, d, a, x[k + 15], 22, 0x49b40821);
a = GG(a, b, c, d, x[k + 1], 5, 0xf61e2562);
d = GG(d, a, b, c, x[k + 6], 9, 0xc040b340);
c = GG(c, d, a, b, x[k + 11], 14, 0x265e5a51);
b = GG(b, c, d, a, x[k], 20, 0xe9b6c7aa);
a = GG(a, b, c, d, x[k + 5], 5, 0xd62f105d);
d = GG(d, a, b, c, x[k + 10], 9, 0x2441453);
c = GG(c, d, a, b, x[k + 15], 14, 0xd8a1e681);
b = GG(b, c, d, a, x[k + 4], 20, 0xe7d3fbc8);
a = GG(a, b, c, d, x[k + 9], 5, 0x21e1cde6);
d = GG(d, a, b, c, x[k + 14], 9, 0xc33707d6);
c = GG(c, d, a, b, x[k + 3], 14, 0xf4d50d87);
b = GG(b, c, d, a, x[k + 8], 20, 0x455a14ed);
a = GG(a, b, c, d, x[k + 13], 5, 0xa9e3e905);
d = GG(d, a, b, c, x[k + 2], 9, 0xfcefa3f8);
c = GG(c, d, a, b, x[k + 7], 14, 0x676f02d9);
b = GG(b, c, d, a, x[k + 12], 20, 0x8d2a4c8a);
a = HH(a, b, c, d, x[k + 5], 4, 0xfffa3942);
d = HH(d, a, b, c, x[k + 8], 11, 0x8771f681);
c = HH(c, d, a, b, x[k + 11], 16, 0x6d9d6122);
b = HH(b, c, d, a, x[k + 14], 23, 0xfde5380c);
a = HH(a, b, c, d, x[k + 1], 4, 0xa4beea44);
d = HH(d, a, b, c, x[k + 4], 11, 0x4bdecfa9);
c = HH(c, d, a, b, x[k + 7], 16, 0xf6bb4b60);
b = HH(b, c, d, a, x[k + 10], 23, 0xbebfbc70);
a = HH(a, b, c, d, x[k + 13], 4, 0x289b7ec6);
d = HH(d, a, b, c, x[k], 11, 0xeaa127fa);
c = HH(c, d, a, b, x[k + 3], 16, 0xd4ef3085);
b = HH(b, c, d, a, x[k + 6], 23, 0x4881d05);
a = HH(a, b, c, d, x[k + 9], 4, 0xd9d4d039);
d = HH(d, a, b, c, x[k + 12], 11, 0xe6db99e5);
c = HH(c, d, a, b, x[k + 15], 16, 0x1fa27cf8);
b = HH(b, c, d, a, x[k + 2], 23, 0xc4ac5665);
a = II(a, b, c, d, x[k], 6, 0xf4292244);
d = II(d, a, b, c, x[k + 7], 10, 0x432aff97);
c = II(c, d, a, b, x[k + 14], 15, 0xab9423a7);
b = II(b, c, d, a, x[k + 5], 21, 0xfc93a039);
a = II(a, b, c, d, x[k + 12], 6, 0x655b59c3);
d = II(d, a, b, c, x[k + 3], 10, 0x8f0ccc92);
c = II(c, d, a, b, x[k + 10], 15, 0xffeff47d);
b = II(b, c, d, a, x[k + 1], 21, 0x85845dd1);
a = II(a, b, c, d, x[k + 8], 6, 0x6fa87e4f);
d = II(d, a, b, c, x[k + 15], 10, 0xfe2ce6e0);
c = II(c, d, a, b, x[k + 6], 15, 0xa3014314);
b = II(b, c, d, a, x[k + 13], 21, 0x4e0811a1);
a = II(a, b, c, d, x[k + 4], 6, 0xf7537e82);
d = II(d, a, b, c, x[k + 11], 10, 0xbd3af235);
c = II(c, d, a, b, x[k + 2], 15, 0x2ad7d2bb);
b = II(b, c, d, a, x[k + 9], 21, 0xeb86d391);
a = addUnsigned(a, AA);
b = addUnsigned(b, BB);
c = addUnsigned(c, CC);
d = addUnsigned(d, DD);
}
return wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d);
};
return md5(str);
};
const calculateHash = async () => {
if (!input) {
toast.warning("请输入要计算哈希的内容");
return;
}
setLoading(true);
try {
const encoder = new TextEncoder();
const data = encoder.encode(input);
const [sha1, sha256, sha512] = await Promise.all([
crypto.subtle.digest("SHA-1", data),
crypto.subtle.digest("SHA-256", data),
crypto.subtle.digest("SHA-512", data),
]);
const md5Hash = await calculateMD5(input);
const arrayBufferToHex = (buffer: ArrayBuffer): string => {
return Array.from(new Uint8Array(buffer))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
};
setResults({
md5: md5Hash,
sha1: arrayBufferToHex(sha1),
sha256: arrayBufferToHex(sha256),
sha512: arrayBufferToHex(sha512),
});
toast.success("计算完成");
} catch {
toast.error("计算失败");
} finally {
setLoading(false);
}
};
const copyToClipboard = async (text: string, name: string) => {
try {
await navigator.clipboard.writeText(text);
toast.success(`${name} 已复制`);
} catch {
toast.error("复制失败");
}
};
const clearAll = () => {
setInput("");
setResults(null);
};
const HashResultItem = ({ label, value }: { label: string; value: string }) => (
<div className="rounded-lg bg-muted p-4 space-y-2">
<div className="flex items-center justify-between">
<span className="font-medium text-sm">{label}</span>
<Button
variant="ghost"
size="sm"
className="h-6 gap-1"
onClick={() => copyToClipboard(value, label)}
>
<Copy className="h-3 w-3" />
<span className="text-xs"></span>
</Button>
</div>
<code className="block break-all font-mono text-sm leading-relaxed bg-background/50 p-2 rounded border">
{value}
</code>
</div>
);
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
{/* Page Header */}
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-red-500 to-rose-600 shadow-lg">
<FileDigit className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight">Hash </h1>
<p className="text-muted-foreground">
MD5SHA1SHA256SHA512
</p>
</div>
</div>
<div className="grid gap-6 lg:grid-cols-2">
<div className="space-y-6">
<Card className="h-full flex flex-col">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3">
<CardTitle className="text-base font-medium"></CardTitle>
<div className="flex gap-2">
<Button onClick={calculateHash} disabled={loading || !input} className="gap-2" size="sm">
<Zap className="h-4 w-4" />
{loading ? "计算中..." : "计算哈希"}
</Button>
<Button variant="ghost" size="icon" onClick={clearAll} className="h-8 w-8 text-destructive">
<Eraser className="h-4 w-4" />
</Button>
</div>
</CardHeader>
<CardContent className="flex-1 min-h-100">
<Textarea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="请输入要计算哈希值的文本..."
className="h-full min-h-75 resize-none font-mono"
/>
</CardContent>
</Card>
</div>
<div className="space-y-6">
<Card className="h-full flex flex-col">
<CardHeader className="pb-3">
<CardTitle className="text-base font-medium flex items-center gap-2">
<Lock className="h-4 w-4" />
</CardTitle>
</CardHeader>
<CardContent className="flex-1">
{results ? (
<div className="space-y-3">
<HashResultItem label="MD5 (32位)" value={results.md5} />
<HashResultItem label="SHA1 (40位)" value={results.sha1} />
<HashResultItem label="SHA256 (64位)" value={results.sha256} />
<HashResultItem label="SHA512 (128位)" value={results.sha512} />
</div>
) : (
<div className="flex flex-col items-center justify-center h-full min-h-75 text-muted-foreground">
<FileDigit className="h-16 w-16 mb-4 opacity-20" />
<p></p>
</div>
)}
</CardContent>
</Card>
</div>
</div>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<span className="text-xl">💡</span> 使
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-2">
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li>MD5 128 16 </li>
<li>SHA1 160 20 </li>
<li>SHA256 256 32 </li>
</ul>
</div>
<div className="space-y-2">
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li>SHA512 512 64 </li>
<li></li>
<li></li>
</ul>
</div>
</div>
</CardContent>
</Card>
</div>
);
}

113
app/html-escape/page.tsx Normal file
View File

@@ -0,0 +1,113 @@
"use client";
import { useState } from "react";
import { Braces, Copy, Trash2, ArrowUpDown } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { toast } from "sonner";
export default function HtmlEscapePage() {
const [input, setInput] = useState("");
const copyToClipboard = async (text: string) => {
if (!text) return;
try {
await navigator.clipboard.writeText(text);
toast.success("已复制到剪贴板");
} catch {
toast.error("复制失败");
}
};
const escapeHtml = () => {
const div = document.createElement('div');
div.textContent = input;
setInput(div.innerHTML);
toast.success("转义成功");
};
const unescapeHtml = () => {
const div = document.createElement('div');
div.innerHTML = input;
setInput(div.textContent || "");
toast.success("反转义成功");
};
const clearAll = () => setInput("");
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
{/* Page Header */}
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-emerald-500 to-teal-600 shadow-lg">
<Braces className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight">HTML </h1>
<p className="text-muted-foreground">
HTML
</p>
</div>
</div>
<div className="grid gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0">
<CardTitle className="text-base font-medium">/</CardTitle>
<div className="flex gap-2">
<Button variant="ghost" size="sm" onClick={() => copyToClipboard(input)} disabled={!input}>
<Copy className="h-4 w-4 mr-2" />
</Button>
<Button variant="ghost" size="sm" onClick={clearAll} disabled={!input} className="text-destructive hover:text-destructive">
<Trash2 className="h-4 w-4 mr-2" />
</Button>
</div>
</CardHeader>
<CardContent>
<Textarea
placeholder="请输入需要处理的 HTML 代码或转义字符串..."
className="min-h-[250px] font-mono text-base resize-y"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
</CardContent>
</Card>
<div className="flex flex-col sm:flex-row gap-4">
<Button onClick={escapeHtml} size="lg" className="flex-1 gap-2">
<ArrowUpDown className="h-4 w-4" />
HTML (Escape)
</Button>
<Button onClick={unescapeHtml} size="lg" variant="outline" className="flex-1 gap-2">
<ArrowUpDown className="h-4 w-4" />
HTML (Unescape)
</Button>
</div>
</div>
{/* Info Card */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<span className="text-xl">💡</span> HTML
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-sm text-muted-foreground space-y-2">
<p>HTML HTML HTML `&lt;` `&amp;lt;`</p>
<p> HTML XSS</p>
<p className="font-mono bg-muted p-2 rounded-md inline-block">
&lt; &rarr; &amp;lt;<br />
&gt; &rarr; &amp;gt;<br />
&amp; &rarr; &amp;amp;<br />
" &rarr; &amp;quot;
</p>
</div>
</CardContent>
</Card>
</div>
);
}

155
app/html-formatter/page.tsx Normal file
View File

@@ -0,0 +1,155 @@
"use client";
import React, { useState } from "react";
import Editor from "@monaco-editor/react";
import { html_beautify } from "js-beautify";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { FileCode, Copy, RotateCcw, ArrowRight } from "lucide-react";
import { toast } from "sonner";
import { useTheme } from "next-themes";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Label } from "@/components/ui/label";
export default function HtmlFormatterPage() {
const { theme } = useTheme();
const [input, setInput] = useState("");
const [output, setOutput] = useState("");
const [indentSize, setIndentSize] = useState("2");
const [wrapLineLength, setWrapLineLength] = useState("80");
const handleFormat = () => {
if (!input.trim()) return;
try {
const formatted = html_beautify(input, {
indent_size: parseInt(indentSize),
wrap_line_length: parseInt(wrapLineLength),
preserve_newlines: true,
indent_inner_html: true,
});
setOutput(formatted);
toast.success("HTML 格式化成功");
} catch {
toast.error("HTML 格式化失败");
}
};
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
toast.success("已复制到剪贴板");
} catch {
toast.error("复制失败");
}
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-orange-500 to-red-600 shadow-lg">
<FileCode className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight">HTML </h1>
<p className="text-muted-foreground"> HTML </p>
</div>
</div>
{/* Options */}
<Card>
<CardContent className="p-4 flex flex-wrap gap-6 items-end">
<div className="space-y-2">
<Label></Label>
<Select value={indentSize} onValueChange={setIndentSize}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="2">2 </SelectItem>
<SelectItem value="4">4 </SelectItem>
<SelectItem value="8">8 </SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label></Label>
<Select value={wrapLineLength} onValueChange={setWrapLineLength}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="0"></SelectItem>
<SelectItem value="80">80 </SelectItem>
<SelectItem value="120">120 </SelectItem>
</SelectContent>
</Select>
</div>
<Button onClick={handleFormat} className="gap-2">
<ArrowRight className="h-4 w-4" />
</Button>
</CardContent>
</Card>
<div className="grid gap-6 lg:grid-cols-2">
{/* Input */}
<Card className="flex flex-col min-h-150">
<CardHeader className="py-3 px-4 border-b flex flex-row items-center justify-between">
<CardTitle className="text-base font-medium"> HTML</CardTitle>
<div className="flex gap-2">
<Button variant="ghost" size="sm" onClick={() => setInput("")}>
<RotateCcw className="h-4 w-4" />
</Button>
</div>
</CardHeader>
<CardContent className="p-0 flex-1 h-125">
<Editor
height="100%"
defaultLanguage="html"
theme={theme === "dark" ? "vs-dark" : "light"}
value={input}
onChange={(value) => setInput(value || "")}
options={{
minimap: { enabled: false },
fontSize: 14,
wordWrap: "on"
}}
/>
</CardContent>
</Card>
{/* Output */}
<Card className="flex flex-col min-h-150">
<CardHeader className="py-3 px-4 border-b flex flex-row items-center justify-between">
<CardTitle className="text-base font-medium"></CardTitle>
<Button variant="ghost" size="sm" onClick={() => copyToClipboard(output)} disabled={!output}>
<Copy className="h-4 w-4 mr-2" />
</Button>
</CardHeader>
<CardContent className="p-0 flex-1 bg-muted/30 h-125">
<Editor
height="100%"
defaultLanguage="html"
theme={theme === "dark" ? "vs-dark" : "light"}
value={output}
options={{
readOnly: true,
minimap: { enabled: false },
fontSize: 14,
wordWrap: "on"
}}
/>
</CardContent>
</Card>
</div>
</div>
);
}

148
app/image-base64/page.tsx Normal file
View File

@@ -0,0 +1,148 @@
"use client";
import { useState } from "react";
import { ImagePlus, Copy, Trash2, Download, Upload, FileImage } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { toast } from "sonner";
export default function ImageBase64Page() {
const [imageBase64, setImageBase64] = useState("");
const [preview, setPreview] = useState("");
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (!file.type.startsWith("image/")) {
toast.error("请选择有效的图片文件");
return;
}
const reader = new FileReader();
reader.onload = (event) => {
const result = event.target?.result as string;
setImageBase64(result);
setPreview(result);
toast.success("图片已转为 Base64");
};
reader.readAsDataURL(file);
};
const handleBase64Change = (val: string) => {
setImageBase64(val);
if (val.startsWith("data:image/")) {
setPreview(val);
} else {
setPreview("");
}
};
const copyToClipboard = async () => {
if (!imageBase64) return;
try {
await navigator.clipboard.writeText(imageBase64);
toast.success("已复制到剪贴板");
} catch {
toast.error("复制失败");
}
};
const downloadImage = () => {
if (!preview) return;
const link = document.createElement("a");
link.href = preview;
link.download = "downloaded_image";
link.click();
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-purple-500 to-indigo-600 shadow-lg">
<ImagePlus className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight"> Base64 </h1>
<p className="text-muted-foreground"> Base64 </p>
</div>
</div>
<div className="grid gap-6 lg:grid-cols-2">
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="text-base"> Base64</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-col items-center justify-center border-2 border-dashed rounded-xl p-8 bg-muted/30 hover:bg-muted/50 transition-colors relative">
<input
type="file"
accept="image/*"
onChange={handleFileChange}
className="absolute inset-0 opacity-0 cursor-pointer"
/>
<Upload className="h-10 w-10 text-muted-foreground mb-4" />
<p className="text-sm text-muted-foreground"></p>
<p className="text-xs text-muted-foreground/60 mt-1"> JPG, PNG, GIF, WebP </p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0">
<CardTitle className="text-base">Base64 </CardTitle>
<div className="flex gap-2">
<Button variant="ghost" size="sm" onClick={copyToClipboard} disabled={!imageBase64}>
<Copy className="h-4 w-4 mr-2" />
</Button>
<Button variant="ghost" size="sm" onClick={() => {setImageBase64(""); setPreview("");}} disabled={!imageBase64} className="text-destructive">
<Trash2 className="h-4 w-4 mr-2" />
</Button>
</div>
</CardHeader>
<CardContent>
<Textarea
placeholder="在此输入 Base64 字符串 (包含 data:image/... 前缀)..."
className="min-h-[250px] font-mono text-xs break-all"
value={imageBase64}
onChange={(e) => handleBase64Change(e.target.value)}
/>
</CardContent>
</Card>
</div>
<Card className="flex flex-col h-full">
<CardHeader className="flex flex-row items-center justify-between space-y-0">
<CardTitle className="text-base"></CardTitle>
{preview && (
<Button variant="outline" size="sm" onClick={downloadImage}>
<Download className="h-4 w-4 mr-2" />
</Button>
)}
</CardHeader>
<CardContent className="flex-1 flex items-center justify-center bg-muted/20 rounded-b-xl p-6 min-h-[400px]">
{preview ? (
<div className="relative group max-w-full">
<img
src={preview}
alt="Preview"
className="max-w-full max-h-[600px] rounded-lg shadow-lg object-contain"
/>
</div>
) : (
<div className="text-center text-muted-foreground italic space-y-2">
<FileImage className="h-12 w-12 mx-auto opacity-10" />
<p> Base64 </p>
</div>
)}
</CardContent>
</Card>
</div>
</div>
);
}

156
app/image-to-pixel/page.tsx Normal file
View File

@@ -0,0 +1,156 @@
"use client";
import React, { useState, useRef, useEffect } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Slider } from "@/components/ui/slider";
import { ImageIcon, Upload, Download, RefreshCw } from "lucide-react";
import { toast } from "sonner";
export default function ImageToPixelPage() {
const [imageSrc, setImageSrc] = useState<string | null>(null);
const [pixelSize, setPixelSize] = useState(10);
const canvasRef = useRef<HTMLCanvasElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
setImageSrc(event.target?.result as string);
};
reader.readAsDataURL(file);
}
};
useEffect(() => {
if (imageSrc && canvasRef.current) {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const img = new Image();
img.src = imageSrc;
img.onload = () => {
// Setup canvas size
canvas.width = img.width;
canvas.height = img.height;
// Draw original image
ctx.drawImage(img, 0, 0);
// Pixelate
if (pixelSize > 1) {
const w = canvas.width;
const h = canvas.height;
// Disable image smoothing for pixelation effect
ctx.imageSmoothingEnabled = false;
// Calculate smaller dimensions
const sw = w / pixelSize;
const sh = h / pixelSize;
// Draw small image
ctx.drawImage(canvas, 0, 0, w, h, 0, 0, sw, sh);
// Draw back scaled up
ctx.drawImage(canvas, 0, 0, sw, sh, 0, 0, w, h);
}
};
}
}, [imageSrc, pixelSize]);
const handleDownload = () => {
if (canvasRef.current) {
const link = document.createElement("a");
link.download = "pixel-art.png";
link.href = canvasRef.current.toDataURL();
link.click();
toast.success("图片已下载");
}
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-pink-500 to-rose-600 shadow-lg">
<ImageIcon className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground"></p>
</div>
</div>
<div className="grid gap-6 lg:grid-cols-12">
<Card className="lg:col-span-8 flex flex-col items-center justify-center min-h-100 p-6 bg-muted/20">
{imageSrc ? (
<div className="relative max-w-full overflow-hidden shadow-xl rounded-lg border-4 border-white/50">
<canvas ref={canvasRef} className="max-w-full h-auto block" />
</div>
) : (
<div className="text-center space-y-4">
<div className="h-32 w-32 bg-muted rounded-full flex items-center justify-center mx-auto border-4 border-dashed border-muted-foreground/30">
<ImageIcon className="h-12 w-12 text-muted-foreground" />
</div>
<div>
<h3 className="text-lg font-medium"></h3>
<p className="text-sm text-muted-foreground"> JPG, PNG, WEBP </p>
</div>
<Button onClick={() => fileInputRef.current?.click()}>
<Upload className="h-4 w-4 mr-2" />
</Button>
</div>
)}
<input
type="file"
ref={fileInputRef}
onChange={handleImageUpload}
className="hidden"
accept="image/*"
aria-label="上传图片"
/>
</Card>
<Card className="lg:col-span-4 h-fit">
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-8">
<div className="space-y-4">
<div className="flex justify-between">
<Label>: {pixelSize}px</Label>
</div>
<Slider
value={[pixelSize]}
onValueChange={(v) => setPixelSize(v[0])}
min={1}
max={50}
step={1}
disabled={!imageSrc}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
<div className="space-y-2">
<Button onClick={handleDownload} disabled={!imageSrc} className="w-full">
<Download className="h-4 w-4 mr-2" />
</Button>
<Button variant="outline" onClick={() => setImageSrc(null)} disabled={!imageSrc} className="w-full">
<RefreshCw className="h-4 w-4 mr-2" />
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
);
}

135
app/ip-calc/page.tsx Normal file
View File

@@ -0,0 +1,135 @@
"use client";
import { useState } from "react";
import { Network, Search, Calculator } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { toast } from "sonner";
export default function IpCalcPage() {
const [ip, setIp] = useState("192.168.1.1");
const [mask, setMask] = useState("24");
const [result, setResult] = useState<any>(null);
const calculate = () => {
try {
const parts = ip.split('.').map(Number);
if (parts.length !== 4 || parts.some(p => p < 0 || p > 255 || isNaN(p))) {
toast.error("无效的 IP 地址");
return;
}
const maskNum = parseInt(mask);
if (isNaN(maskNum) || maskNum < 0 || maskNum > 32) {
toast.error("无效的掩码 (0-32)");
return;
}
// Convert IP to 32-bit number
const ipInt = (parts[0] << 24) >>> 0 | (parts[1] << 16) >>> 0 | (parts[2] << 8) >>> 0 | parts[3] >>> 0;
// Calculate Mask
const maskInt = maskNum === 0 ? 0 : (~0 << (32 - maskNum)) >>> 0;
// Network & Broadcast
const netInt = (ipInt & maskInt) >>> 0;
const broadInt = (netInt | ~maskInt) >>> 0;
const intToIp = (i: number) => [
(i >>> 24) & 0xFF,
(i >>> 16) & 0xFF,
(i >>> 8) & 0xFF,
i & 0xFF
].join('.');
setResult({
address: ip,
netmask: intToIp(maskInt),
wildcard: intToIp(~maskInt >>> 0),
network: intToIp(netInt),
broadcast: intToIp(broadInt),
hostMin: intToIp(netInt + 1),
hostMax: intToIp(broadInt - 1),
hosts: maskNum >= 31 ? 0 : Math.pow(2, 32 - maskNum) - 2,
cidr: `/${maskNum}`
});
} catch {
toast.error("计算出错");
}
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-blue-500 to-indigo-600 shadow-lg">
<Network className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight">IP </h1>
<p className="text-muted-foreground"></p>
</div>
</div>
<div className="grid gap-6 lg:grid-cols-3">
<Card className="lg:col-span-1">
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>IP </Label>
<Input value={ip} onChange={(e) => setIp(e.target.value)} placeholder="如: 192.168.1.1" />
</div>
<div className="space-y-2">
<Label> (CIDR)</Label>
<Input value={mask} onChange={(e) => setMask(e.target.value)} placeholder="如: 24" type="number" />
</div>
<Button onClick={calculate} className="w-full gap-2 mt-2">
<Calculator className="h-4 w-4" />
</Button>
</CardContent>
</Card>
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent>
{!result ? (
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
<Search className="h-12 w-12 mb-2 opacity-20" />
<p></p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-y-4 gap-x-8">
<ResultItem label="网络地址" value={result.network} cidr={result.cidr} />
<ResultItem label="广播地址" value={result.broadcast} />
<ResultItem label="子网掩码" value={result.netmask} />
<ResultItem label="反掩码 (Wildcard)" value={result.wildcard} />
<ResultItem label="可用主机最小" value={result.hostMin} />
<ResultItem label="可用主机最大" value={result.hostMax} />
<ResultItem label="可用主机数" value={result.hosts.toLocaleString()} />
</div>
)}
</CardContent>
</Card>
</div>
</div>
);
}
function ResultItem({ label, value, cidr }: { label: string; value: string; cidr?: string }) {
return (
<div className="flex flex-col space-y-1">
<span className="text-xs text-muted-foreground">{label}</span>
<div className="flex items-center gap-2">
<span className="font-mono text-lg font-semibold">{value}</span>
{cidr && <span className="text-xs bg-primary/10 text-primary px-1.5 py-0.5 rounded">{cidr}</span>}
</div>
</div>
);
}

201
app/ip-radix/page.tsx Normal file
View File

@@ -0,0 +1,201 @@
"use client";
import React, { useState, useEffect } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Network, Copy } from "lucide-react";
import { toast } from "sonner";
// --- Helper Functions ---
// IPv4
const ipv4ToInt = (ip: string) =>
ip.split('.').reduce((acc, octet) => (acc << 8n) + BigInt(parseInt(octet, 10)), 0n);
const intToIpv4 = (int: bigint) =>
[(int >> 24n) & 0xffn, (int >> 16n) & 0xffn, (int >> 8n) & 0xffn, int & 0xffn].join('.');
// IPv6 (simplified)
const ipv6ToBigInt = (ip: string) => {
// Expansion of ::
if(ip.includes('::')) {
const parts = ip.split('::');
const left = parts[0].split(':').filter(Boolean);
const right = parts[1].split(':').filter(Boolean);
const missing = 8 - (left.length + right.length);
const expanded = [...left, ...Array(missing).fill('0'), ...right];
ip = expanded.join(':');
}
const parts = ip.split(':');
let result = 0n;
for (const part of parts) {
result = (result << 16n) + BigInt(parseInt(part || '0', 16));
}
return result;
};
const bigIntToIpv6 = (int: bigint) => {
let parts = [];
for (let i = 0; i < 8; i++) {
parts.unshift((int & 0xffffn).toString(16));
int >>= 16n;
}
// Simple compression (not full RFC 5952 compliant but good enough for display)
return parts.join(':').replace(/(^|:)0(:0)*:0(:|$)/, '::');
};
export default function IpRadixPage() {
const [ip, setIp] = useState("192.168.1.1");
const [type, setType] = useState<"IPv4" | "IPv6">("IPv4");
const [decimal, setDecimal] = useState("");
const [hex, setHex] = useState("");
const [binary, setBinary] = useState("");
const [errorV4, setErrorV4] = useState("");
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
toast.success("已复制");
} catch {
toast.error("复制失败");
}
};
useEffect(() => {
if (!ip.trim()) return;
try {
let val: bigint;
if (ip.includes(':')) {
setType("IPv6");
val = ipv6ToBigInt(ip);
} else {
setType("IPv4");
// Basic IPv4 validation
if (!/^(\d{1,3}\.){3}\d{1,3}$/.test(ip)) {
if (/^\d+$/.test(ip)) {
// Treat as decimal input
val = BigInt(ip);
} else {
throw new Error("Invalid Format");
}
} else {
val = ipv4ToInt(ip);
}
}
setErrorV4("");
setDecimal(val.toString());
setHex("0x" + val.toString(16).toUpperCase());
setBinary(val.toString(2));
} catch {
setDecimal("---");
setHex("---");
setBinary("---");
setErrorV4("无效的 IP 地址格式");
}
}, [ip]);
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-cyan-500 to-blue-600 shadow-lg">
<Network className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight">IP </h1>
<p className="text-muted-foreground">IPv4/IPv6 </p>
</div>
</div>
<div className="grid gap-6 lg:grid-cols-2">
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle> IP </CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<Label>IPv4, IPv6 </Label>
<div className="flex gap-2">
<Input
value={ip}
onChange={(e) => setIp(e.target.value)}
placeholder="例如: 192.168.1.1 或 2001:db8::1"
className="font-mono text-lg"
/>
</div>
{errorV4 && <p className="text-sm text-destructive">{errorV4}</p>}
<p className="text-xs text-muted-foreground">
IPv4 / IPv6
</p>
</div>
</CardContent>
</Card>
{/* Results */}
<Card>
<CardHeader>
<CardTitle className="text-base text-muted-foreground"> ({type})</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<div className="p-4 bg-muted/40 rounded-lg font-mono text-lg break-all flex justify-between items-start">
<span>{type === 'IPv4' ? intToIpv4(BigInt(decimal || 0)) : bigIntToIpv6(BigInt(decimal || 0))}</span>
<Button variant="ghost" size="icon" className="h-6 w-6 shrink-0 ml-2" onClick={() => copyToClipboard(type === 'IPv4' ? intToIpv4(BigInt(decimal || 0)) : bigIntToIpv6(BigInt(decimal || 0)))}>
<Copy className="h-3 w-3" />
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base text-muted-foreground"> (Decimal)</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<div className="p-4 bg-muted/40 rounded-lg font-mono text-lg break-all flex justify-between items-start">
<span>{decimal}</span>
<Button variant="ghost" size="icon" className="h-6 w-6 shrink-0 ml-2" onClick={() => copyToClipboard(decimal)}>
<Copy className="h-3 w-3" />
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base text-muted-foreground"> (Hex)</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<div className="p-4 bg-muted/40 rounded-lg font-mono text-lg break-all flex justify-between items-start">
<span>{hex}</span>
<Button variant="ghost" size="icon" className="h-6 w-6 shrink-0 ml-2" onClick={() => copyToClipboard(hex)}>
<Copy className="h-3 w-3" />
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base text-muted-foreground"> (Binary)</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<div className="p-4 bg-muted/40 rounded-lg font-mono text-sm break-all flex justify-between items-start max-h-40 overflow-y-auto">
<span>{binary}</span>
<Button variant="ghost" size="icon" className="h-6 w-6 shrink-0 ml-2" onClick={() => copyToClipboard(binary)}>
<Copy className="h-3 w-3" />
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
);
}

318
app/json-formatter/page.tsx Normal file
View File

@@ -0,0 +1,318 @@
"use client";
import React, { useState, useCallback } from 'react';
import {
Code, Minimize2, CheckCircle, Copy, FilePlus, Eraser, Check, XCircle
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from '@/components/ui/tooltip';
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { toast } from "sonner";
type Mode = 'format' | 'compress' | 'validate';
export default function JsonFormatterPage() {
const [mode, setMode] = useState<Mode>('format');
const [inputJson, setInputJson] = useState('');
const [outputJson, setOutputJson] = useState('');
const [isValid, setIsValid] = useState<boolean | null>(null);
const [errorMsg, setErrorMsg] = useState('');
const [charCount, setCharCount] = useState(0);
const validateJson = useCallback((jsonStr: string) => {
if (!jsonStr.trim()) {
setIsValid(null);
setErrorMsg('');
return null;
}
try {
const parsed = JSON.parse(jsonStr);
setIsValid(true);
setErrorMsg('');
return parsed;
} catch (err) {
const msg = err instanceof Error ? err.message : '未知错误';
setErrorMsg(msg);
setIsValid(false);
return null;
}
}, []);
const formatJson = useCallback(() => {
const parsed = validateJson(inputJson);
if (parsed !== null) {
const formatted = JSON.stringify(parsed, null, 2);
setOutputJson(formatted);
toast.success('格式化成功');
}
}, [inputJson, validateJson]);
const compressJson = useCallback(() => {
const parsed = validateJson(inputJson);
if (parsed !== null) {
const compressed = JSON.stringify(parsed);
setOutputJson(compressed);
toast.success('压缩成功');
}
}, [inputJson, validateJson]);
const validateOnly = useCallback(() => {
validateJson(inputJson);
if (inputJson.trim() && isValid) {
toast.success('JSON 格式正确');
}
}, [inputJson, validateJson, isValid]);
const handleAction = useCallback(() => {
switch (mode) {
case 'format':
formatJson();
break;
case 'compress':
compressJson();
break;
case 'validate':
validateOnly();
break;
}
}, [mode, formatJson, compressJson, validateOnly]);
const copyToClipboard = useCallback(async (text: string, type: string) => {
if (!text) {
toast.warning('没有可复制的内容');
return;
}
try {
await navigator.clipboard.writeText(text);
toast.success(`${type}已复制`);
} catch {
toast.error('复制失败');
}
}, []);
const clearAll = useCallback(() => {
setInputJson('');
setOutputJson('');
setIsValid(null);
setErrorMsg('');
setCharCount(0);
}, []);
const loadExample = useCallback(() => {
const example = {
name: "信奥工具箱",
version: "1.0.0",
features: ["JSON格式化", "压缩", "验证"],
config: {
theme: "cyan",
language: "zh-CN"
}
};
const exampleStr = JSON.stringify(example, null, 2);
setInputJson(exampleStr);
setCharCount(exampleStr.length);
validateJson(exampleStr);
}, [validateJson]);
const handleInputChange = useCallback((value: string) => {
setInputJson(value);
setCharCount(value.length);
if (value.trim()) {
validateJson(value);
} else {
setIsValid(null);
setErrorMsg('');
}
}, [validateJson]);
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
{/* Page Header */}
<div className="flex items-center justify-between border-b pb-4">
<div className="flex items-center space-x-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-cyan-500 to-blue-600 shadow-lg">
<Code className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight">JSON </h1>
<p className="text-muted-foreground">JSON使</p>
</div>
</div>
<Tabs value={mode} onValueChange={(v) => setMode(v as Mode)} className="w-75">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="format"></TabsTrigger>
<TabsTrigger value="compress"></TabsTrigger>
<TabsTrigger value="validate"></TabsTrigger>
</TabsList>
</Tabs>
</div>
{/* Action Bar */}
<Card>
<CardContent className="flex flex-wrap items-center gap-4 p-4">
<Button
onClick={handleAction}
disabled={!inputJson.trim()}
className="gap-2"
>
{mode === 'format' && <Code className="h-4 w-4" />}
{mode === 'compress' && <Minimize2 className="h-4 w-4" />}
{mode === 'validate' && <CheckCircle className="h-4 w-4" />}
{mode === 'format' ? '格式化' : mode === 'compress' ? '压缩' : '验证'}
</Button>
<Button
variant="secondary"
onClick={compressJson}
disabled={!inputJson.trim() || isValid === false}
className="gap-2"
>
<Minimize2 className="h-4 w-4" />
</Button>
<Button
variant="outline"
onClick={validateOnly}
disabled={!inputJson.trim()}
className="gap-2"
>
<CheckCircle className="h-4 w-4" />
</Button>
<Button variant="ghost" onClick={loadExample} className="gap-2">
<FilePlus className="h-4 w-4" />
</Button>
<Button variant="ghost" onClick={clearAll} className="gap-2 text-destructive hover:text-destructive/90 hover:bg-destructive/10">
<Eraser className="h-4 w-4" />
</Button>
</CardContent>
</Card>
{/* Main Content */}
<div className="grid gap-6 lg:grid-cols-2">
{/* Input Panel */}
<Card className="flex flex-col h-full">
<CardHeader className="py-3 flex flex-row items-center justify-between">
<CardTitle className="text-base font-medium flex items-center gap-2">
JSON
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => copyToClipboard(inputJson, '输入内容')}>
<Copy className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent></TooltipContent>
</Tooltip>
</TooltipProvider>
</CardTitle>
{isValid !== null && (
<Badge variant={isValid ? "default" : "destructive"} className={isValid ? "bg-emerald-500 hover:bg-emerald-600" : ""}>
{isValid ? <Check className="h-3 w-3 mr-1" /> : <XCircle className="h-3 w-3 mr-1" />}
{isValid ? '格式正确' : '格式错误'}
</Badge>
)}
</CardHeader>
<CardContent className="flex-1 p-0 relative">
<Textarea
value={inputJson}
onChange={(e) => handleInputChange(e.target.value)}
placeholder="请输入JSON数据..."
className="min-h-125 h-full border-0 rounded-none focus-visible:ring-0 resize-none font-mono text-sm leading-relaxed p-4 bg-transparent"
/>
{errorMsg && (
<div className="absolute bottom-0 left-0 right-0 bg-destructive/10 text-destructive text-xs p-2 border-t border-destructive/20">
{errorMsg}
</div>
)}
</CardContent>
<div className="p-2 border-t bg-muted/30 text-xs text-muted-foreground flex justify-end">
: {charCount}
</div>
</Card>
{/* Output Panel */}
<Card className="flex flex-col h-full">
<CardHeader className="py-3 flex flex-row items-center justify-between">
<CardTitle className="text-base font-medium flex items-center gap-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => copyToClipboard(outputJson, '结果')}>
<Copy className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent></TooltipContent>
</Tooltip>
</TooltipProvider>
</CardTitle>
</CardHeader>
<CardContent className="flex-1 p-0 bg-muted/30">
<Textarea
value={outputJson}
readOnly
placeholder="处理结果将显示在这里..."
className="min-h-125 h-full border-0 rounded-none focus-visible:ring-0 resize-none font-mono text-sm leading-relaxed p-4 bg-transparent"
/>
</CardContent>
</Card>
</div>
{/* Usage Info */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<span className="text-xl">💡</span> 使
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-6 md:grid-cols-3">
<div className="space-y-2">
<h4 className="font-semibold text-sm"></h4>
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li>JSON结构</li>
<li></li>
<li>便</li>
</ul>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-sm"></h4>
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li></li>
<li></li>
<li></li>
</ul>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-sm"></h4>
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li>JSON语法正确性</li>
<li></li>
<li></li>
</ul>
</div>
</div>
<div className="mt-4 p-3 bg-muted rounded-md text-xs text-muted-foreground">
💡 null值
</div>
</CardContent>
</Card>
</div>
);
}

166
app/jwt/page.tsx Normal file
View File

@@ -0,0 +1,166 @@
"use client";
import { useState } from "react";
import { KeySquare, Copy, Trash2, ShieldCheck, AlertCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { toast } from "sonner";
export default function JwtDecoderPage() {
const [token, setToken] = useState("");
const [decoded, setDecoded] = useState<{
header: any;
payload: any;
error: string | null;
}>({
header: null,
payload: null,
error: null,
});
const base64UrlDecode = (str: string) => {
try {
// Replace non-url compatible chars with base64 standard chars
str = str.replace(/-/g, '+').replace(/_/g, '/');
// Pad with =
while (str.length % 4) {
str += '=';
}
return decodeURIComponent(atob(str).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
} catch {
throw new Error("Invalid base64 encoding");
}
};
const decodeJwt = (input: string) => {
setToken(input);
if (!input.trim()) {
setDecoded({ header: null, payload: null, error: null });
return;
}
const parts = input.split('.');
if (parts.length !== 3) {
setDecoded({ header: null, payload: null, error: "无效的 JWT 格式(必须包含三个由点分隔的部分)" });
return;
}
try {
const header = JSON.parse(base64UrlDecode(parts[0]));
const payload = JSON.parse(base64UrlDecode(parts[1]));
setDecoded({ header, payload, error: null });
} catch {
setDecoded({ header: null, payload: null, error: "解码失败:请检查输入是否为有效的 JWT" });
}
};
const copyToClipboard = async (data: any) => {
if (!data) return;
try {
await navigator.clipboard.writeText(JSON.stringify(data, null, 2));
toast.success("已复制 JSON 内容");
} catch {
toast.error("复制失败");
}
};
const clearAll = () => {
setToken("");
setDecoded({ header: null, payload: null, error: null });
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
{/* Page Header */}
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-purple-500 to-indigo-600 shadow-lg">
<KeySquare className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight">JWT </h1>
<p className="text-muted-foreground">
JSON Web Token Header Payload
</p>
</div>
</div>
<div className="grid gap-6 lg:grid-cols-2">
{/* Input Section */}
<Card className="lg:row-span-2">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-base font-medium text-purple-600">Encoded ( Token)</CardTitle>
<Button variant="ghost" size="sm" onClick={clearAll} className="text-destructive">
<Trash2 className="h-4 w-4" />
</Button>
</CardHeader>
<CardContent>
<Textarea
placeholder="请在此粘贴 JWT Token..."
className="min-h-100 font-mono text-sm break-all resize-none bg-muted/30"
value={token}
onChange={(e) => decodeJwt(e.target.value)}
/>
{decoded.error && (
<div className="mt-4 p-4 rounded-lg bg-destructive/10 text-destructive flex items-start gap-3 text-sm">
<AlertCircle className="h-5 w-5 shrink-0" />
<span>{decoded.error}</span>
</div>
)}
</CardContent>
</Card>
{/* Output Section - Header */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-base font-medium text-blue-600">Header ()</CardTitle>
<Button variant="ghost" size="sm" onClick={() => copyToClipboard(decoded.header)} disabled={!decoded.header}>
<Copy className="h-4 w-4" />
</Button>
</CardHeader>
<CardContent>
<div className="rounded-lg bg-slate-950 p-4 overflow-x-auto min-h-25">
<pre className="text-sm font-mono text-blue-400">
{decoded.header ? JSON.stringify(decoded.header, null, 2) : "// 等待输入..."}
</pre>
</div>
</CardContent>
</Card>
{/* Output Section - Payload */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-base font-medium text-emerald-600">Payload ()</CardTitle>
<Button variant="ghost" size="sm" onClick={() => copyToClipboard(decoded.payload)} disabled={!decoded.payload}>
<Copy className="h-4 w-4" />
</Button>
</CardHeader>
<CardContent>
<div className="rounded-lg bg-slate-950 p-4 overflow-x-auto min-h-50">
<pre className="text-sm font-mono text-emerald-400">
{decoded.payload ? JSON.stringify(decoded.payload, null, 2) : "// 等待输入..."}
</pre>
</div>
</CardContent>
</Card>
</div>
{/* Security Note */}
<Card className="border-emerald-200/50 bg-emerald-50/30 dark:bg-emerald-500/5">
<CardContent className="pt-6">
<div className="flex items-start gap-3">
<ShieldCheck className="h-5 w-5 text-emerald-600 shrink-0 mt-0.5" />
<div className="space-y-1">
<p className="font-semibold text-emerald-900 dark:text-emerald-400"></p>
<p className="text-sm text-emerald-800/80 dark:text-emerald-400/80">
Token 使
</p>
</div>
</div>
</CardContent>
</Card>
</div>
);
}

114
app/keyboard/page.tsx Normal file
View File

@@ -0,0 +1,114 @@
"use client";
import { useState, useEffect } from "react";
import { Keyboard, Info, RotateCcw } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export default function KeyboardPage() {
const [lastEvent, setLastEvent] = useState<any>(null);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
e.preventDefault(); // Prevent standard browser actions
setLastEvent({
key: e.key === " " ? "Space" : e.key,
code: e.code,
keyCode: e.keyCode,
altKey: e.altKey,
ctrlKey: e.ctrlKey,
shiftKey: e.shiftKey,
metaKey: e.metaKey,
location: e.location,
repeat: e.repeat
});
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, []);
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-zinc-700 to-zinc-900 shadow-lg">
<Keyboard className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground"></p>
</div>
</div>
<div className="flex flex-col items-center justify-center py-12 space-y-8">
{!lastEvent ? (
<div className="text-center space-y-4">
<div className="w-64 h-64 rounded-3xl border-4 border-dashed border-muted flex items-center justify-center animate-pulse">
<span className="text-muted-foreground font-medium text-lg px-8"></span>
</div>
</div>
) : (
<>
<div className="relative group">
<div className="absolute -inset-1 bg-linear-to-r from-primary to-purple-600 rounded-3xl blur-md opacity-25 group-hover:opacity-50 transition duration-1000"></div>
<div className="relative w-64 h-64 rounded-3xl bg-card border-2 border-primary flex flex-col items-center justify-center shadow-2xl">
<span className="text-xs font-bold text-muted-foreground mb-2 uppercase tracking-widest">Key</span>
<span className="text-7xl font-bold text-primary">{lastEvent.key}</span>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 w-full max-w-2xl">
<DetailCard label="Code" value={lastEvent.code} />
<DetailCard label="KeyCode" value={lastEvent.keyCode} />
<DetailCard label="Location" value={lastEvent.location} />
<DetailCard label="Repeat" value={lastEvent.repeat ? "Yes" : "No"} />
</div>
<div className="flex flex-wrap justify-center gap-4">
<ModifierTag active={lastEvent.ctrlKey} label="Ctrl" />
<ModifierTag active={lastEvent.shiftKey} label="Shift" />
<ModifierTag active={lastEvent.altKey} label="Alt" />
<ModifierTag active={lastEvent.metaKey} label="Win / Cmd" />
</div>
<Button variant="ghost" onClick={() => setLastEvent(null)} className="text-muted-foreground">
<RotateCcw className="h-4 w-4 mr-2" />
</Button>
</>
)}
</div>
<Card className="bg-muted/30 border-none">
<CardHeader>
<CardTitle className="text-sm flex items-center gap-2">
<Info className="h-4 w-4" />
KeyCode
</CardTitle>
</CardHeader>
<CardContent className="text-sm text-muted-foreground">
<p> `keyCode` Web 使 `key` `code`使`key` `code` </p>
</CardContent>
</Card>
</div>
);
}
function DetailCard({ label, value }: { label: string; value: any }) {
return (
<div className="p-4 rounded-xl bg-card border flex flex-col items-center justify-center space-y-1">
<span className="text-[10px] uppercase font-bold text-muted-foreground tracking-tighter">{label}</span>
<span className="font-mono font-bold text-lg">{value}</span>
</div>
);
}
function ModifierTag({ active, label }: { active: boolean; label: string }) {
return (
<div className={`px-4 py-2 rounded-full border-2 font-bold transition-all duration-300 ${
active ? "bg-primary border-primary text-primary-foreground scale-110 shadow-lg" : "border-muted text-muted-foreground opacity-30"
}`}>
{label}
</div>
);
}

40
app/layout.tsx Normal file
View File

@@ -0,0 +1,40 @@
import React from 'react'
import { Toaster } from "@/components/ui/sonner"
import ClientLayout from './components/ClientLayout'
import './globals.css'
export const metadata = {
title: '信奥工具箱 - 在线工具集合',
description: '信奥工具箱提供各种实用的在线工具包括JSON格式化、文本处理、编码转换、二维码生成等。',
keywords: '在线工具,JSON格式化,二维码生成器,文本处理,编码工具,信奥工具箱',
authors: [{ name: '信奥工具箱' }],
}
// 使用系统字体,避免构建时依赖外部网络
// import { Inter } from "next/font/google";
// const inter = Inter({
// subsets: ["latin"],
// display: "swap",
// fallback: ["system-ui", "arial"],
// adjustFontFallback: true,
// });
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN">
<head>
<link rel="icon" href="/favicon.ico" />
</head>
<body className="font-sans antialiased">
<ClientLayout>
{children}
</ClientLayout>
<Toaster />
</body>
</html>
)
}

View File

@@ -0,0 +1,469 @@
import path from "path";
import fs from "fs/promises";
import os from "os";
import { executeStandardCode } from "./runner";
import { createZipFile } from "./zip";
const OUTPUT_BASE = path.join(os.tmpdir(), "i-tools-testcase");
interface Constraint {
min: number;
max: number;
}
function parseConstraint(text: string, variable: string): Constraint | null {
const escapedVariable = variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const patterns = [
new RegExp(`(-?\\d+(?:\\.\\d+)?)\\s*[≤<=]\\s*${escapedVariable}\\s*[≤<=]\\s*(-?\\d+(?:\\.\\d+)?)`, "i"),
new RegExp(`(-?\\d+)\\s*[≤<=]\\s*${escapedVariable}\\s*[≤<=]\\s*(-?\\d+)\\s*\\^\\s*(-?\\d+)`, "i"),
new RegExp(`(-?[\\d,]+)\\s*[≤<=]\\s*${escapedVariable}\\s*[≤<=]\\s*(-?[\\d,]+)`, "i"),
new RegExp(`${escapedVariable}\\s*[∈∊]\\s*\\[\\s*(-?\\d+(?:\\.\\d+)?)\\s*,\\s*(-?\\d+(?:\\.\\d+)?)\\s*\\]`, "i"),
new RegExp(`${escapedVariable}.*?(?:范围|取值).*?(-?\\d+(?:\\.\\d+)?).*?(?:到|至|-|~).*?(-?\\d+(?:\\.\\d+)?)`, "i"),
new RegExp(`(-?\\d+(?:\\.\\d+)?)\\s*[<≤<=]+\\s*${escapedVariable}\\s*[<≤<=]+\\s*(-?\\d+(?:\\.\\d+)?)`, "i"),
new RegExp(`${escapedVariable}\\s*[≤<=]\\s*(-?\\d+(?:\\.\\d+)?).*?${escapedVariable}\\s*[≥>=]\\s*(-?\\d+(?:\\.\\d+)?)`, "i"),
];
for (const pattern of patterns) {
const match = text.match(pattern);
if (match) {
let min: number, max: number;
if (match.length === 4 && match[3]) {
min = parseFloat(match[1]);
max = parseFloat(match[2]) * Math.pow(10, parseFloat(match[3]));
} else if (pattern.source.includes("≤.*≥")) {
max = parseFloat(match[1].replace(/,/g, ""));
min = parseFloat(match[2].replace(/,/g, ""));
} else {
min = parseFloat(match[1].replace(/,/g, ""));
max = parseFloat(match[2].replace(/,/g, ""));
}
if (min > max) [min, max] = [max, min];
return { min: Math.floor(min), max: Math.floor(max) };
}
}
return null;
}
function parseAllConstraints(description: string): Record<string, Constraint> {
const constraints: Record<string, Constraint> = {};
const commonVariables = [
"n", "m", "k", "q", "t", "l", "r", "x", "y", "z",
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
"a_i", "b_i", "c_i", "A_i", "B_i", "C_i",
"s", "S", "w", "W", "h", "H",
"数组长度", "序列长度", "字符串长度",
"序列中的数", "数组中的数", "元素", "值",
"权重", "代价", "距离", "时间", "长度", "宽度", "高度",
];
for (const variable of commonVariables) {
const c = parseConstraint(description, variable);
if (c) constraints[variable] = c;
}
const rangePatterns = [
/(\d+)\s*[≤<=]\s*.*?[≤<=]\s*(\d+)/g,
/(\d+)\s*到\s*(\d+)/g,
/(\d+)\s*~\s*(\d+)/g,
/\[(\d+),\s*(\d+)\]/g,
];
for (const pattern of rangePatterns) {
let match;
while ((match = pattern.exec(description)) !== null) {
const min = parseInt(match[1], 10);
const max = parseInt(match[2], 10);
if (min < max) {
const key = `range_${min}_${max}`;
if (!constraints[key]) constraints[key] = { min, max };
}
}
}
return constraints;
}
function generateValueInRange(min: number, max: number, caseNumber: number, totalCases = 10): number {
const range = max - min;
if (caseNumber === 1) return min;
if (caseNumber === 2) return min + 1;
if (caseNumber === totalCases) return max;
if (caseNumber === totalCases - 1) return max - 1;
if (caseNumber === 3) return min + Math.floor(range * 0.1);
if (caseNumber === 4) return min + Math.floor(range * 0.25);
if (caseNumber === 5) return min + Math.floor(range * 0.5);
if (caseNumber === 6) return min + Math.floor(range * 0.75);
if (caseNumber === 7) return min + Math.floor(range * 0.9);
const segments = [0.05, 0.15, 0.35, 0.65, 0.85, 0.95];
const seg = segments[caseNumber % segments.length];
const value = min + Math.floor(range * (seg + (Math.random() * 0.1 - 0.05)));
return Math.max(min, Math.min(max, value));
}
function generateArrayInput(
caseNumber: number,
nConstraint: Constraint | null,
valueConstraint: Constraint | null,
totalCases: number
): string {
const maxN = nConstraint?.max ?? 1000;
const minN = nConstraint?.min ?? 1;
const n = generateValueInRange(minN, maxN, caseNumber, totalCases);
const maxVal = valueConstraint?.max ?? 1000;
const minVal = valueConstraint?.min ?? -1000;
let result = `${n}\n`;
const values: number[] = [];
for (let i = 0; i < n; i++) {
values.push(generateValueInRange(minVal, maxVal, i + 1, n));
}
result += values.join(" ");
return result;
}
function generateGraphInput(
caseNumber: number,
nConstraint: Constraint | null,
totalCases: number
): string {
const maxN = Math.min(nConstraint?.max ?? 1000, 1000);
const minN = nConstraint?.min ?? 3;
const n = generateValueInRange(minN, maxN, caseNumber, totalCases);
const m = Math.min(Math.floor((n * (n - 1)) / 2), Math.floor(n * Math.log(n)));
let result = `${n} ${m}\n`;
const edges = new Set<string>();
for (let i = 0; i < m; i++) {
let u: number, v: number;
do {
u = Math.floor(Math.random() * n) + 1;
v = Math.floor(Math.random() * n) + 1;
} while (u === v || edges.has(`${Math.min(u, v)}-${Math.max(u, v)}`));
edges.add(`${Math.min(u, v)}-${Math.max(u, v)}`);
result += `${u} ${v}\n`;
}
return result.trim();
}
function generateStringInput(
caseNumber: number,
nConstraint: Constraint | null,
totalCases: number
): string {
const maxN = nConstraint?.max ?? 100;
const minN = nConstraint?.min ?? 1;
const n = generateValueInRange(minN, maxN, caseNumber, totalCases);
const chars = "abcdefghijklmnopqrstuvwxyz";
let str = "";
for (let i = 0; i < n; i++) {
str += chars[Math.floor(Math.random() * chars.length)];
}
return str;
}
function generateMatrixInput(
caseNumber: number,
nConstraint: Constraint | null,
valueConstraint: Constraint | null,
totalCases: number
): string {
const maxN = Math.min(nConstraint?.max ?? 100, 100);
const minN = nConstraint?.min ?? 2;
const n = generateValueInRange(minN, maxN, caseNumber, totalCases);
const m = n;
const maxVal = valueConstraint?.max ?? 100;
const minVal = valueConstraint?.min ?? 0;
let result = `${n} ${m}\n`;
for (let i = 0; i < n; i++) {
const row: number[] = [];
for (let j = 0; j < m; j++) {
row.push(Math.floor(Math.random() * (maxVal - minVal + 1)) + minVal);
}
result += row.join(" ") + "\n";
}
return result.trim();
}
function generateTreeInput(
caseNumber: number,
nConstraint: Constraint | null,
totalCases: number
): string {
const maxN = Math.min(nConstraint?.max ?? 1000, 10000);
const minN = Math.max(nConstraint?.min ?? 2, 2);
const n = generateValueInRange(minN, maxN, caseNumber, totalCases);
let result = `${n}\n`;
const edges: [number, number][] = [];
for (let i = 2; i <= n; i++) {
const parent = Math.floor(Math.random() * (i - 1)) + 1;
edges.push([parent, i]);
}
for (let i = edges.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[edges[i], edges[j]] = [edges[j], edges[i]];
}
for (const [u, v] of edges) {
result += `${u} ${v}\n`;
}
return result.trim();
}
function generateQueryInput(
caseNumber: number,
nConstraint: Constraint | null,
mConstraint: Constraint | null,
valueConstraint: Constraint | null,
totalCases: number
): string {
const maxN = nConstraint?.max ?? 1000;
const minN = nConstraint?.min ?? 1;
const n = generateValueInRange(minN, maxN, caseNumber, totalCases);
const maxM = mConstraint?.max ?? 100;
const minM = mConstraint?.min ?? 1;
const m = generateValueInRange(minM, maxM, caseNumber, totalCases);
const maxVal = valueConstraint?.max ?? 1000;
const minVal = valueConstraint?.min ?? 1;
let result = `${n} ${m}\n`;
const values: number[] = [];
for (let i = 0; i < n; i++) {
values.push(generateValueInRange(minVal, maxVal, i + 1, n));
}
result += values.join(" ") + "\n";
const queries: number[] = [];
for (let i = 0; i < m; i++) {
queries.push(generateValueInRange(minVal, maxVal, i + 1, m));
}
result += queries.join(" ");
return result;
}
function generateBinarySearchInput(
caseNumber: number,
description: string,
totalCases: number
): string {
const nConstraint = parseConstraint(description, "n") ?? { min: 5, max: 100 };
const mConstraint = parseConstraint(description, "m") ?? { min: 1, max: 10 };
const valueConstraint = parseConstraint(description, "a_i") ?? parseConstraint(description, "q") ?? { min: 0, max: 100 };
let n: number, m: number;
switch (caseNumber) {
case 1:
n = Math.min(10, nConstraint.max);
m = Math.min(5, mConstraint.max);
break;
case 2:
n = Math.min(100, Math.floor(nConstraint.max * 0.1));
m = Math.min(20, Math.floor(mConstraint.max * 0.2));
break;
case 3:
n = nConstraint.min;
m = mConstraint.min;
break;
case 4:
n = Math.min(1000, nConstraint.max);
m = Math.min(100, mConstraint.max);
break;
default:
n = generateValueInRange(nConstraint.min, Math.min(500, nConstraint.max), caseNumber, totalCases);
m = generateValueInRange(mConstraint.min, Math.min(50, mConstraint.max), caseNumber, totalCases);
}
const sequence: number[] = [];
let currentValue = valueConstraint.min;
for (let i = 0; i < n; i++) {
if (i > 0 && Math.random() < 0.7) {
const inc = Math.floor(Math.random() * 5) + 1;
currentValue = Math.min(currentValue + inc, valueConstraint.max);
}
sequence.push(currentValue);
}
const queries: number[] = [];
for (let i = 0; i < m; i++) {
if (i < m / 2 && sequence.length > 0) {
queries.push(sequence[Math.floor(Math.random() * sequence.length)]!);
} else {
queries.push(
Math.random() < 0.5 && sequence.length > 0
? sequence[Math.floor(Math.random() * sequence.length)]!
: generateValueInRange(valueConstraint.min, valueConstraint.max, i, m)
);
}
}
return `${n} ${m}\n${sequence.join(" ")}\n${queries.join(" ")}`;
}
function generateMockInput(description: string, caseNumber: number, totalCases = 10): string {
const desc = description.toLowerCase();
const allConstraints = parseAllConstraints(description);
if (desc.includes("单调不减") && desc.includes("查找") && desc.includes("询问")) {
return generateBinarySearchInput(caseNumber, description, totalCases);
}
const nConstraint = allConstraints["n"] ?? allConstraints["N"] ?? parseConstraint(description, "n");
const mConstraint = allConstraints["m"] ?? allConstraints["M"] ?? parseConstraint(description, "m");
const valueConstraint =
allConstraints["序列中的数"] ?? allConstraints["数组中的数"] ?? allConstraints["元素"] ?? allConstraints["值"] ??
parseConstraint(description, "序列中的数") ?? parseConstraint(description, "value");
let effectiveN = nConstraint;
if (!effectiveN) {
const rangeKeys = Object.keys(allConstraints).filter((k) => k.startsWith("range_"));
if (rangeKeys.length > 0) {
const maxRange = rangeKeys.reduce((a, b) =>
(allConstraints[b]?.max ?? 0) > (allConstraints[a]?.max ?? 0) ? b : a
);
effectiveN = allConstraints[maxRange] ?? null;
}
}
if (desc.includes("序列") || desc.includes("数组") || desc.includes("array")) {
return generateArrayInput(caseNumber, effectiveN ?? null, valueConstraint ?? null, totalCases);
}
if (desc.includes("图") || desc.includes("graph")) {
return generateGraphInput(caseNumber, effectiveN ?? null, totalCases);
}
if (desc.includes("字符串") || desc.includes("string")) {
return generateStringInput(caseNumber, effectiveN ?? null, totalCases);
}
if (desc.includes("矩阵") || desc.includes("matrix")) {
return generateMatrixInput(caseNumber, effectiveN ?? null, valueConstraint ?? null, totalCases);
}
if (desc.includes("树") || desc.includes("tree")) {
return generateTreeInput(caseNumber, effectiveN ?? null, totalCases);
}
if (desc.includes("询问") || desc.includes("query")) {
return generateQueryInput(caseNumber, effectiveN ?? null, mConstraint ?? null, valueConstraint ?? null, totalCases);
}
const maxN = effectiveN?.max ?? 100;
const minN = effectiveN?.min ?? 1;
const n = generateValueInRange(minN, maxN, caseNumber, totalCases);
const keys = Object.keys(allConstraints);
if (keys.length > 1) {
let result = `${n}\n`;
for (const key of keys.slice(1, 3)) {
if (key !== "n" && key !== "N" && !key.startsWith("range_")) {
const c = allConstraints[key];
if (c) result += `${generateValueInRange(c.min, c.max, caseNumber, totalCases)}\n`;
}
}
return result.trim();
}
return `${n}`;
}
const MAX_RETRIES = 2;
export interface FallbackParams {
englishName: string;
description: string;
standardCode: string;
testCaseCount: number;
}
export interface FallbackResult {
testCases: { input: string; output: string }[];
outputDirName: string;
zipFileName: string;
}
export type ProgressCallback = (progress: number, message: string, generatedCount: number) => void;
export async function generateFallbackTestCases(
params: FallbackParams,
progressCallback?: ProgressCallback
): Promise<FallbackResult> {
const randomSuffix = Date.now().toString(36) + "_" + Math.random().toString(36).slice(2, 7);
const outputDirName = `${params.englishName}_${randomSuffix}`;
const outputDir = path.join(OUTPUT_BASE, outputDirName);
await fs.mkdir(outputDir, { recursive: true });
const testCases: { input: string; output: string }[] = [];
for (let i = 1; i <= params.testCaseCount; i++) {
if (progressCallback) {
const progress = 0.3 + (i / params.testCaseCount) * 0.6;
progressCallback(progress, `正在生成第 ${i}/${params.testCaseCount} 组测试用例...`, i - 1);
}
const inputData = generateMockInput(params.description, i, params.testCaseCount);
let outputData: string | null = null;
for (let retry = 0; retry <= MAX_RETRIES; retry++) {
try {
outputData = await executeStandardCode(params.standardCode, inputData);
break;
} catch (err) {
if (retry === MAX_RETRIES) break;
}
}
if (!outputData) continue;
await fs.writeFile(path.join(outputDir, `${params.englishName}${i}.in`), inputData);
await fs.writeFile(path.join(outputDir, `${params.englishName}${i}.out`), outputData);
testCases.push({ input: inputData, output: outputData });
}
if (progressCallback) {
progressCallback(0.9, "正在创建ZIP压缩包...", testCases.length);
}
const zipFileName = `${params.englishName}_${randomSuffix}.zip`;
const zipPath = path.join(OUTPUT_BASE, zipFileName);
await createZipFile(outputDir, zipPath, params.englishName);
return { testCases, outputDirName, zipFileName };
}
export function getOutputBase(): string {
return OUTPUT_BASE;
}
/** AI 生成的用例:用标程跑出 output写入 .in/.out 并打包 */
export interface AITestCaseInput {
input: string;
}
export async function runAIGeneratedCases(
params: FallbackParams,
aiCases: AITestCaseInput[],
progressCallback?: ProgressCallback
): Promise<FallbackResult> {
const randomSuffix = Date.now().toString(36) + "_" + Math.random().toString(36).slice(2, 7);
const outputDirName = `${params.englishName}_${randomSuffix}`;
const outputDir = path.join(OUTPUT_BASE, outputDirName);
await fs.mkdir(outputDir, { recursive: true });
const testCases: { input: string; output: string }[] = [];
const total = aiCases.length;
for (let i = 0; i < total; i++) {
if (progressCallback) {
const progress = 0.3 + ((i + 1) / total) * 0.6;
progressCallback(progress, `正在处理第 ${i + 1}/${total} 组测试用例...`, i);
}
const inputData = aiCases[i]!.input;
let outputData: string | null = null;
for (let retry = 0; retry <= MAX_RETRIES; retry++) {
try {
outputData = await executeStandardCode(params.standardCode, inputData);
break;
} catch {
if (retry === MAX_RETRIES) break;
}
}
if (!outputData) continue;
const oneBased = i + 1;
await fs.writeFile(path.join(outputDir, `${params.englishName}${oneBased}.in`), inputData);
await fs.writeFile(path.join(outputDir, `${params.englishName}${oneBased}.out`), outputData);
testCases.push({ input: inputData, output: outputData });
}
if (progressCallback) {
progressCallback(0.9, "正在创建ZIP压缩包...", testCases.length);
}
const zipFileName = `${params.englishName}_${randomSuffix}.zip`;
const zipPath = path.join(OUTPUT_BASE, zipFileName);
await createZipFile(outputDir, zipPath, params.englishName);
return { testCases, outputDirName, zipFileName };
}

View File

@@ -0,0 +1,9 @@
export { getTask, setTask, createTask, getEstimatedTime, findRunningTaskByEnglishName } from "./store";
export type { Task } from "./types";
export type { CreateTaskParams } from "./types";
export { validateStandardCode, executeStandardCode } from "./runner";
export { generateFallbackTestCases, getOutputBase } from "./generator";
export type { FallbackParams, FallbackResult, ProgressCallback } from "./generator";
export { processTask, getZipPathForTask } from "./process";
export { generateTestCases, isQwenAvailable } from "./qwen-service";
export type { AITestCase, GenerateTestCasesParams } from "./qwen-service";

View File

@@ -0,0 +1,156 @@
import path from "path";
import { getTask, setTask } from "./store";
import { validateStandardCode } from "./runner";
import { generateFallbackTestCases, runAIGeneratedCases, getOutputBase } from "./generator";
import { generateTestCases, isQwenAvailable } from "./qwen-service";
function formatErrorMessage(err: unknown): string {
const message = err instanceof Error ? err.message : String(err);
if ((err as Error & { code?: string }).code === "COMPILATION_ERROR") {
return `C++程序编译失败: ${message}`;
}
if ((err as Error & { code?: string }).code === "EXECUTION_ERROR") {
return `程序执行错误: ${message}`;
}
return message;
}
function updateTaskProgress(
taskId: string,
status: "pending" | "running" | "completed" | "failed",
progress: number,
message: string,
generatedCount?: number
): void {
const task = getTask(taskId);
if (!task) return;
task.status = status;
task.progress = Math.min(100, Math.max(0, progress));
task.updatedAt = new Date();
task.currentMessage = message;
if (generatedCount !== undefined) task.generatedCases = generatedCount;
setTask(taskId, task);
}
export async function processTask(taskId: string): Promise<void> {
const task = getTask(taskId);
if (!task) {
console.error(`任务不存在: ${taskId}`);
return;
}
try {
updateTaskProgress(taskId, "running", 10, "正在初始化生成环境...");
await validateStandardCode(task.standardCode);
updateTaskProgress(taskId, "running", 20, "标准程序验证通过,开始生成测试数据...");
let result: { testCases: { input: string; output: string }[]; outputDirName: string; zipFileName: string };
if (isQwenAvailable()) {
try {
updateTaskProgress(taskId, "running", 22, "正在调用阿里云大模型生成测试用例...", 0);
const aiCases = await generateTestCases({
title: task.title,
englishName: task.englishName,
description: task.description,
standardCode: task.standardCode,
testCaseCount: task.testCaseCount,
serviceLevel: task.serviceLevel,
});
if (aiCases.length > 0) {
updateTaskProgress(taskId, "running", 25, "AI 生成完成,正在用标程计算输出...", 0);
result = await runAIGeneratedCases(
{
englishName: task.englishName,
description: task.description,
standardCode: task.standardCode,
testCaseCount: task.testCaseCount,
},
aiCases,
(progress, message, generatedCount) => {
updateTaskProgress(
taskId,
"running",
25 + Math.floor(progress * 65),
message,
generatedCount
);
}
);
} else {
throw new Error("AI 未返回有效用例");
}
} catch (aiErr) {
console.warn("AI 生成失败,使用备用算法:", aiErr);
updateTaskProgress(taskId, "running", 20, "使用智能算法生成测试用例...", 0);
result = await generateFallbackTestCases(
{
englishName: task.englishName,
description: task.description,
standardCode: task.standardCode,
testCaseCount: task.testCaseCount,
},
(progress, message, generatedCount) => {
updateTaskProgress(
taskId,
"running",
20 + Math.floor(progress * 70),
message,
generatedCount
);
}
);
}
} else {
result = await generateFallbackTestCases(
{
englishName: task.englishName,
description: task.description,
standardCode: task.standardCode,
testCaseCount: task.testCaseCount,
},
(progress, message, generatedCount) => {
updateTaskProgress(
taskId,
"running",
20 + Math.floor(progress * 70),
message,
generatedCount
);
}
);
}
updateTaskProgress(taskId, "running", 95, "正在打包文件...", result.testCases.length);
const finalTask = getTask(taskId);
if (finalTask) {
finalTask.status = "completed";
finalTask.progress = 100;
finalTask.updatedAt = new Date();
finalTask.completedAt = new Date();
finalTask.generatedCases = result.testCases.length;
finalTask.zipFileName = result.zipFileName;
finalTask.outputDirName = result.outputDirName;
setTask(taskId, finalTask);
}
} catch (err) {
console.error(`任务失败: ${taskId}`, err);
const finalTask = getTask(taskId);
if (finalTask) {
finalTask.status = "failed";
finalTask.progress = 0;
finalTask.errorMessage = formatErrorMessage(err);
finalTask.updatedAt = new Date();
finalTask.failedAt = new Date();
setTask(taskId, finalTask);
}
}
}
export function getZipPathForTask(taskId: string): string | null {
const task = getTask(taskId);
if (!task?.zipFileName) return null;
return path.join(getOutputBase(), task.zipFileName);
}

View File

@@ -0,0 +1,252 @@
/**
* 阿里云通义大模型服务DashScope API
* 用于测试点生成的 AI 输入数据生成
*/
const DASHSCOPE_BASE =
"https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation";
export interface AITestCase {
input: string;
expectedOutput?: string;
description?: string;
}
export interface GenerateTestCasesParams {
title: string;
englishName: string;
description: string;
standardCode: string;
testCaseCount: number;
serviceLevel?: string;
}
function getApiKey(): string | undefined {
return process.env.DASHSCOPE_API_KEY;
}
function getModel(): string {
return process.env.QWEN_MODEL || "qwen-max";
}
function isAIEnabled(): boolean {
const key = getApiKey();
return (
process.env.ENABLE_AI === "true" &&
!!key &&
key !== "your_dashscope_api_key_here"
);
}
function buildPrompt(params: GenerateTestCasesParams): string {
const { title, description, standardCode, testCaseCount } = params;
return `你是一个专业的信息学奥林匹克竞赛测试数据生成专家。请根据以下题目信息,生成 ${testCaseCount} 组测试输入数据。
## 题目信息
**标题**: ${title}
**题目描述**:
${description}
**标准程序** (仅作参考,输出将由标准程序计算):
\`\`\`cpp
${standardCode}
\`\`\`
## 要求
1. 严格按照题目描述中的**输入格式**生成,注意多行顺序
2. 所有数据必须满足题目约束(范围、单调性等)
3. 不要使用省略号、占位符或「此处省略」
4. 覆盖边界:最小值、最大值、小规模、大规模
5. 每组测试用例的输入数据要完整、可直接作为程序 stdin
## 输出格式(二选一)
**方式一 JSON**(推荐):
\`\`\`json
{
"test_cases": [
{ "input": "第一组输入的完整内容,多行用\\n连接" },
{ "input": "第二组输入..." }
]
}
\`\`\`
**方式二 文本**
测试用例1:
[第一组输入的完整内容,多行按行写]
测试用例2:
[第二组输入...]
请生成 ${testCaseCount} 组测试输入数据:`;
}
async function callAPI(prompt: string): Promise<string> {
const apiKey = getApiKey();
if (!apiKey) {
throw new Error("未配置 DASHSCOPE_API_KEY 环境变量");
}
const res = await fetch(DASHSCOPE_BASE, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: getModel(),
input: {
messages: [
{
role: "system",
content:
"你是信息学奥林匹克竞赛测试数据生成专家。根据题目描述生成测试输入,严格按要求的 JSON 或文本格式返回,不要省略或占位。",
},
{ role: "user", content: prompt },
],
},
parameters: {
max_tokens: 8000,
temperature: 0.3,
result_format: "message",
},
}),
signal: AbortSignal.timeout(180_000),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`DashScope API 错误 ${res.status}: ${text.slice(0, 300)}`);
}
const data = (await res.json()) as {
output?: { text?: string; choices?: Array<{ message?: { content?: string } }> };
};
const text =
data.output?.text ??
data.output?.choices?.[0]?.message?.content ??
"";
return text;
}
function processTestCases(raw: unknown[], expectedCount: number): AITestCase[] {
const result: AITestCase[] = [];
for (let i = 0; i < Math.min(raw.length, expectedCount); i++) {
const item = raw[i];
if (typeof item === "string" && item.trim()) {
result.push({ input: item.trim() });
continue;
}
if (item && typeof item === "object" && "input" in item) {
const obj = item as { input?: unknown; expectedOutput?: unknown };
const input = obj.input;
if (typeof input === "string" && input.trim()) {
result.push({
input: input.trim().replace(/\\n/g, "\n"),
...(typeof obj.expectedOutput === "string" && obj.expectedOutput
? { expectedOutput: obj.expectedOutput }
: {}),
});
}
}
}
return result;
}
function parseJSONResponse(aiResponse: string, expectedCount: number): AITestCase[] {
// 1) 直接解析整段
const trimmed = aiResponse.trim();
try {
const direct = JSON.parse(trimmed) as { test_cases?: unknown[]; testCases?: unknown[] };
const arr = direct.test_cases ?? direct.testCases;
if (Array.isArray(arr) && arr.length > 0) {
return processTestCases(arr, expectedCount);
}
} catch {
// ignore
}
// 2) ```json ... ```
const jsonBlock = aiResponse.match(/```json\s*([\s\S]*?)\s*```/);
if (jsonBlock) {
try {
const data = JSON.parse(jsonBlock[1].trim()) as {
test_cases?: unknown[];
testCases?: unknown[];
};
const arr = data.test_cases ?? data.testCases;
if (Array.isArray(arr) && arr.length > 0) {
return processTestCases(arr, expectedCount);
}
} catch {
// ignore
}
}
return [];
}
function parseTextResponse(text: string, expectedCount: number): AITestCase[] {
const cases: AITestCase[] = [];
const parts = text.split(/(?:测试用例|测试数据|Test\s*Case)\s*\d+\s*[:]?\s*\n/i);
for (let i = 1; i < parts.length && cases.length < expectedCount; i++) {
const block = parts[i]!.trim();
const nextCase = block.search(/(?:测试用例|测试数据|Test\s*Case)\s*\d+\s*[:]/i);
const content = nextCase >= 0 ? block.slice(0, nextCase).trim() : block;
if (content && !content.includes("...") && !content.includes("省略")) {
cases.push({ input: content });
}
}
return cases;
}
function parseAIResponse(aiResponse: string, expectedCount: number): AITestCase[] {
const fromJson = parseJSONResponse(aiResponse, expectedCount);
if (fromJson.length > 0) return fromJson;
return parseTextResponse(aiResponse, expectedCount);
}
function validateParsedCases(cases: AITestCase[]): AITestCase[] {
return cases.filter((c) => {
const input = c.input ?? "";
if (!input.trim()) return false;
if (input.includes("...") || input.includes("......") || input.includes("省略")) return false;
if (input.includes("占位符") || input.includes("placeholder")) return false;
if (/^\s*[{\[]/.test(input) && (input.includes('"input"') || input.includes("test_cases"))) return false;
return true;
});
}
export function isQwenAvailable(): boolean {
return isAIEnabled();
}
export async function generateTestCases(params: GenerateTestCasesParams): Promise<AITestCase[]> {
if (!isAIEnabled()) {
throw new Error("AI 未启用或未配置 DASHSCOPE_API_KEY");
}
const maxRetries = 2;
let lastError: Error | null = null;
for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
try {
const prompt = buildPrompt(params);
const response = await callAPI(prompt);
const parsed = parseAIResponse(response, params.testCaseCount);
const valid = validateParsedCases(parsed);
if (valid.length > 0) {
return valid.slice(0, params.testCaseCount);
}
} catch (err) {
lastError = err instanceof Error ? err : new Error(String(err));
if (attempt <= maxRetries) {
await new Promise((r) => setTimeout(r, 1000 * attempt));
}
}
}
throw lastError ?? new Error("AI 生成测试用例失败");
}

View File

@@ -0,0 +1,65 @@
import { exec } from "child_process";
import { promisify } from "util";
import path from "path";
import fs from "fs/promises";
const execAsync = promisify(exec);
const CPP_STANDARD = process.env.CPP_STANDARD || "c++14";
const CPP_FLAGS = process.env.CPP_FLAGS || "-Wall -Wno-unused-variable";
export async function validateStandardCode(code: string): Promise<void> {
if (!code?.trim()) {
throw new Error("标准程序不能为空");
}
if (!code.includes("#include") || !code.includes("main")) {
throw new Error("标准程序必须包含 #include 和 main 函数");
}
const os = await import("os");
const tempDir = path.join(os.tmpdir(), "testcase_validate_" + Date.now());
await fs.mkdir(tempDir, { recursive: true });
try {
const cppFile = path.join(tempDir, "test.cpp");
const exeFile = path.join(tempDir, "test");
await fs.writeFile(cppFile, code);
const compileCommand = `g++ -o "${exeFile}" "${cppFile}" -std=${CPP_STANDARD} ${CPP_FLAGS}`;
await execAsync(compileCommand, { timeout: 10000 });
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const e = new Error(`编译失败: ${message}`);
(e as Error & { code?: string }).code = "COMPILATION_ERROR";
throw e;
} finally {
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
}
}
export async function executeStandardCode(code: string, input: string): Promise<string> {
const os = await import("os");
const tempDir = path.join(os.tmpdir(), "testcase_run_" + Date.now());
await fs.mkdir(tempDir, { recursive: true });
const cppFile = path.join(tempDir, "solution.cpp");
const exeFile = path.join(tempDir, "solution");
const inputFile = path.join(tempDir, "input.txt");
try {
await fs.writeFile(cppFile, code);
const compileCommand = `g++ -o "${exeFile}" "${cppFile}" -std=${CPP_STANDARD} ${CPP_FLAGS}`;
await execAsync(compileCommand);
await fs.writeFile(inputFile, input);
const executeCommand = `"${exeFile}" < "${inputFile}"`;
const { stdout, stderr } = await execAsync(executeCommand, { timeout: 10000 });
if (stderr) console.warn("C++ execution warning:", stderr);
return stdout.trim();
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const e = new Error(`程序执行失败: ${message}`);
(e as Error & { code?: string }).code = "EXECUTION_ERROR";
throw e;
} finally {
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
}
}

View File

@@ -0,0 +1,52 @@
import type { Task, CreateTaskParams } from "./types";
const tasks = new Map<string, Task>();
export function getTask(taskId: string): Task | undefined {
return tasks.get(taskId);
}
export function setTask(taskId: string, task: Task): void {
tasks.set(taskId, task);
}
export function createTask(taskId: string, params: CreateTaskParams): Task {
const task: Task = {
id: taskId,
status: "pending",
title: params.title.trim(),
englishName: params.englishName.trim().toLowerCase(),
description: params.description.trim(),
standardCode: params.standardCode.trim(),
testCaseCount: params.testCaseCount,
serviceLevel: params.serviceLevel || "standard",
createdAt: new Date(),
updatedAt: new Date(),
progress: 0,
errorMessage: null,
generatedCases: 0,
};
tasks.set(taskId, task);
return task;
}
export function findRunningTaskByEnglishName(englishName: string): Task | undefined {
const name = englishName.trim().toLowerCase();
for (const task of tasks.values()) {
if (task.englishName === name && (task.status === "pending" || task.status === "running")) {
return task;
}
}
return undefined;
}
export function getEstimatedTime(serviceLevel: string, caseCount: number): number {
const baseTime = Math.ceil(caseCount / 2);
const multiplier: Record<string, number> = {
standard: 1.0,
domestic: 1.5,
pro: 1.2,
max: 2.0,
};
return Math.max(30, baseTime * (multiplier[serviceLevel] ?? 1.0));
}

View File

@@ -0,0 +1,29 @@
export interface Task {
id: string;
status: "pending" | "running" | "completed" | "failed";
title: string;
englishName: string;
description: string;
standardCode: string;
testCaseCount: number;
serviceLevel: string;
createdAt: Date;
updatedAt: Date;
progress: number;
currentMessage?: string;
errorMessage?: string | null;
generatedCases: number;
completedAt?: Date;
failedAt?: Date;
outputDirName?: string;
zipFileName?: string;
}
export interface CreateTaskParams {
title: string;
englishName: string;
description: string;
standardCode: string;
testCaseCount: number;
serviceLevel: string;
}

View File

@@ -0,0 +1,31 @@
import archiver from "archiver";
import fs from "fs";
import path from "path";
export function createZipFile(
sourceDir: string,
zipPath: string,
englishName: string | null
): Promise<void> {
return new Promise((resolve, reject) => {
const output = fs.createWriteStream(zipPath);
const archive = archiver("zip", { zlib: { level: 9 } });
output.on("close", () => resolve());
archive.on("error", reject);
archive.pipe(output);
if (englishName) {
const files = fs.readdirSync(sourceDir);
const targetFiles = files.filter(
(f) => f.startsWith(englishName) && (f.endsWith(".in") || f.endsWith(".out"))
);
for (const file of targetFiles) {
archive.file(path.join(sourceDir, file), { name: file });
}
} else {
archive.directory(sourceDir, false);
}
archive.finalize();
});
}

6
app/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

108
app/lorem-ipsum/page.tsx Normal file
View File

@@ -0,0 +1,108 @@
"use client";
import { useState, useEffect } from "react";
import { Type, Copy, RefreshCw } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Slider } from "@/components/ui/slider";
import { toast } from "sonner";
import { Label } from "@/components/ui/label";
const LOREM_TEXT = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra, est eros bibendum elit, nec luctus magna felis sollicitudin mauris. Integer in mauris eu nibh euismod gravida. Duis ac tellus et risus vulputate vehicula. Donec lobortis risus a elit. Etiam tempor. Ut ullamcorper, ligula eu tempor congue, eros est euismod turpis, id tincidunt sapien risus a quam. Maecenas fermentum consequat mi. Donec fermentum. Pellentesque malesuada nulla a mi. Duis sapien sem, aliquet nec, commodo eget, consequat quis, neque. Aliquam faucibus, elit ut dictum aliquet, felis nisl adipiscing sapien, sed malesuada diam lacus eget erat. Cras mollis scelerisque nunc. Nullam arcu. Aliquam consequat. Curabitur augue lorem, dapibus quis, laoreet et, pretium ac, nisi.
Aenean magna nisl, mollis quis, molestie eu, feugiat in, orci. In hac habitasse platea dictumst. Fusce convallis metus id felis luctus adipiscing. Pellentesque egestas, neque sit amet convallis pulvinar, justo nulla eleifend augue, ac auctor orci leo non est. Quisque id mi. Ut tincidunt tincidunt erat. Etiam feugiat lorem non metus. Vestibulum dapibus nunc ac augue. Curabitur vestibulum aliquam leo. Praesent egestas leo in pede. Praesent blandit odio eu enim. Pellentesque sed dui ut augue blandit sodales. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Aliquam nibh. Mauris ac mauris sed pede pellentesque fermentum. Maecenas adipiscing ante non diam sodales hendrerit.`;
export default function LoremIpsumPage() {
const [paragraphs, setParagraphs] = useState(3);
const [generated, setGenerated] = useState("");
const generate = () => {
const allParagraphs = LOREM_TEXT.split('\n');
let result: string[] = [];
// Simple logic: repeat paragraphs if requested count is larger than source
for (let i = 0; i < paragraphs; i++) {
result.push(allParagraphs[i % allParagraphs.length]);
}
setGenerated(result.join('\n\n'));
};
useEffect(() => {
generate();
}, [paragraphs]);
const copyToClipboard = async () => {
if (!generated) return;
try {
await navigator.clipboard.writeText(generated);
toast.success("已复制到剪贴板");
} catch {
toast.error("复制失败");
}
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-zinc-500 to-stone-600 shadow-lg">
<Type className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight">Lorem Ipsum </h1>
<p className="text-muted-foreground"></p>
</div>
</div>
<div className="grid gap-6 lg:grid-cols-4">
<Card className="lg:col-span-1 h-fit">
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label></Label>
<span className="text-sm font-mono bg-muted px-2 py-0.5 rounded">{paragraphs}</span>
</div>
<Slider
value={[paragraphs]}
onValueChange={(v) => {
setParagraphs(v[0]);
// Auto regenerate on slider change for better UX?
// Or let user click? Let's auto-generate in useEffect if we wanted,
// but direct call is easier here to avoid loop issues.
// Actually, let's just update state and let user click generate,
// or use an effect. Effect is cleaner.
}}
min={1}
max={20}
step={1}
className="py-2"
/>
</div>
<Button onClick={generate} className="w-full gap-2">
<RefreshCw className="h-4 w-4" />
</Button>
</CardContent>
</Card>
<Card className="lg:col-span-3">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-base"></CardTitle>
<Button variant="ghost" size="sm" onClick={copyToClipboard}>
<Copy className="h-4 w-4 mr-2" />
</Button>
</CardHeader>
<CardContent>
<div className="min-h-75 p-4 rounded-md bg-muted/30 whitespace-pre-wrap font-serif text-lg leading-relaxed text-muted-foreground">
{generated}
</div>
</CardContent>
</Card>
</div>
</div>
);
}

135
app/markdown/page.tsx Normal file
View File

@@ -0,0 +1,135 @@
"use client";
import React, { useState } from "react";
import Editor from "@monaco-editor/react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { FileText, Download, Copy, Eye, Edit3, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { useTheme } from "next-themes";
const DEFAULT_MARKDOWN = `# Welcome to Markdown Editor
This is a **live preview** editor. You can write your markdown on the left (or top), and see the result instantly.
## Features
- [x] GFM Support (Tables, Tasks, Strikethrough)
- [x] Syntax Highlighting
- [x] Vertical/Horizontal Layout (Responsive)
### Code Example
\`\`\`javascript
function hello() {
console.log("Hello World!");
}
\`\`\`
### Table Example
| Item | Price | Quantity |
|:-----|:-----:|:---------|
| Apple| $1.00 | 5 |
| Pear | $2.00 | 10 |
> "The best way to predict the future is to create it."
`;
export default function MarkdownPage() {
const { theme } = useTheme();
const [markdown, setMarkdown] = useState(DEFAULT_MARKDOWN);
const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(markdown);
toast.success("Markdown 已复制");
} catch {
toast.error("复制失败");
}
};
const downloadMarkdown = () => {
const blob = new Blob([markdown], { type: "text/markdown" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "document.md";
a.click();
URL.revokeObjectURL(url);
toast.success("下载已开始");
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500 h-[calc(100vh-140px)] flex flex-col">
<div className="flex items-center justify-between border-b pb-4 shrink-0">
<div className="flex items-center space-x-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-blue-500 to-indigo-600 shadow-lg">
<FileText className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight">Markdown </h1>
<p className="text-muted-foreground">GFM </p>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={() => setMarkdown("")}>
<Trash2 className="h-4 w-4 lg:mr-2" />
<span className="hidden lg:inline"></span>
</Button>
<Button variant="outline" size="sm" onClick={copyToClipboard}>
<Copy className="h-4 w-4 lg:mr-2" />
<span className="hidden lg:inline"></span>
</Button>
<Button size="sm" onClick={downloadMarkdown}>
<Download className="h-4 w-4 lg:mr-2" />
<span className="hidden lg:inline"> .md</span>
</Button>
</div>
</div>
<div className="flex-1 min-h-0 grid grid-rows-2 lg:grid-cols-2 lg:grid-rows-1 gap-4 lg:gap-6">
{/* Editor */}
<Card className="flex flex-col min-h-0 border-0 shadow-lg ring-1 ring-border">
<CardHeader className="py-2 px-4 border-b bg-muted/30 flex flex-row items-center space-y-0">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Edit3 className="h-4 w-4" />
</CardTitle>
</CardHeader>
<CardContent className="p-0 flex-1 min-h-0 relative">
<Editor
height="100%"
defaultLanguage="markdown"
theme={theme === "dark" ? "vs-dark" : "light"}
value={markdown}
onChange={(value) => setMarkdown(value || "")}
options={{
minimap: { enabled: false },
fontSize: 14,
wordWrap: "on",
padding: { top: 16 }
}}
/>
</CardContent>
</Card>
{/* Preview */}
<Card className="flex flex-col min-h-0 border-0 shadow-lg ring-1 ring-border overflow-hidden">
<CardHeader className="py-2 px-4 border-b bg-muted/30 flex flex-row items-center space-y-0">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Eye className="h-4 w-4" />
</CardTitle>
</CardHeader>
<CardContent className="flex-1 min-h-0 overflow-y-auto p-6 prose dark:prose-invert max-w-none">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{markdown}
</ReactMarkdown>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -0,0 +1,172 @@
"use client";
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { toast } from "sonner";
import { Car, Phone, Bell, Info } from "lucide-react";
export default function MoveCarDisplay() {
const [plateNumber, setPlateNumber] = useState("");
const [phoneNumber, setPhoneNumber] = useState("");
const [token, setToken] = useState("");
const [uid, setUid] = useState("");
const [newEnergy, setNewEnergy] = useState(false);
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
setPlateNumber(urlParams.get("plateNumber") || "");
setPhoneNumber(urlParams.get("phoneNumber") || "");
setToken(urlParams.get("token") || "");
setUid(urlParams.get("uid") || "");
setNewEnergy(urlParams.get("new") === "true");
}, []);
const notifyOwner = () => {
const currentTime = new Date().getTime();
const lastNotifyTimeKey = "lastNotifyTime" + plateNumber;
const lastNotifyTime = localStorage.getItem(lastNotifyTimeKey);
const timeDifference = lastNotifyTime
? (currentTime - parseInt(lastNotifyTime)) / 1000
: 0;
if (lastNotifyTime && timeDifference < 60) {
toast.warning("您已发送过通知请1分钟后再次尝试。");
return;
}
const promise = fetch("https://wxpusher.zjiecode.com/api/send/message", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
appToken: token,
content: "您好,有人需要您挪车,请及时处理。",
contentType: 1,
uids: [uid],
}),
})
.then((response) => response.json())
.then((data) => {
if (data.code === 1000) {
localStorage.setItem(lastNotifyTimeKey, currentTime.toString());
return "通知已发送!";
} else {
throw new Error("通知发送失败");
}
});
toast.promise(promise, {
loading: '正在发送通知...',
success: (data) => data,
error: '通知发送失败,请稍后重试',
});
};
const callNumber = () => {
window.location.href = "tel:" + phoneNumber;
};
return (
<div className="space-y-4 max-w-md mx-auto pt-4 px-2">
{/* 车牌展示区域 */}
<div className="flex justify-center py-2">
<div
className={`relative flex h-32 w-full items-center justify-center overflow-hidden rounded-lg border-[3px] shadow-xl ${
newEnergy
? "border-black/80 bg-linear-to-b from-[#F0F3F5] to-[#42C063] text-black"
: "border-white bg-[#003399] text-white"
}`}
>
{/* 装饰性反光效果 */}
<div className="pointer-events-none absolute inset-0 bg-linear-to-br from-white/20 to-transparent opacity-50" />
{/* 车牌边框内饰线 (仅蓝牌显示) */}
{!newEnergy && (
<div className="absolute inset-1 rounded-md border border-white/50" />
)}
<div className="relative z-10 flex items-center gap-1 font-mono text-5xl font-bold tracking-widest">
<span>{plateNumber.slice(0, 2)}</span>
{newEnergy ? (
<span className="mx-1 h-2 w-2 rounded-full bg-current opacity-20" />
) : (
<span className="mx-2 h-2 w-2 rounded-full bg-current opacity-80" />
)}
<span>{plateNumber.slice(2)}</span>
</div>
{/* 新能源标志水印 (仅绿牌显示) */}
{newEnergy && (
<div className="absolute -bottom-6 -right-6 rotate-12 opacity-10">
<Car className="h-24 w-24" />
</div>
)}
</div>
</div>
{/* 车辆信息 */}
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-2 text-muted-foreground">
<Info className="h-4 w-4" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-3 gap-2 text-sm">
<div className="text-muted-foreground"></div>
<div className="col-span-2 font-medium"></div>
<div className="text-muted-foreground"></div>
<div className="col-span-2 font-medium font-mono">{plateNumber}</div>
<div className="text-muted-foreground"></div>
<div className="col-span-2 font-medium font-mono">{phoneNumber}</div>
</div>
</CardContent>
</Card>
{/* 操作按钮 */}
<Card className="border-t-4 border-t-primary/20">
<CardHeader>
<CardTitle className="text-center"></CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{uid && token && (
<Button
size="lg"
className={`w-full gap-2 text-lg h-14 ${
newEnergy
? "bg-emerald-600 hover:bg-emerald-700 shadow-emerald-200 dark:shadow-none"
: "bg-blue-600 hover:bg-blue-700 shadow-blue-200 dark:shadow-none"
} shadow-xl transition-all hover:scale-[1.02] active:scale-[0.98]`}
onClick={notifyOwner}
>
<Bell className="h-5 w-5" />
</Button>
)}
<Button
size="lg"
variant={uid && token ? "outline" : "default"}
className={`w-full gap-2 text-lg h-14 ${!(uid && token) ? (newEnergy
? "bg-emerald-600 hover:bg-emerald-700 shadow-emerald-200 dark:shadow-none shadow-xl transition-all hover:scale-[1.02] active:scale-[0.98]"
: "bg-blue-600 hover:bg-blue-700 shadow-blue-200 dark:shadow-none shadow-xl transition-all hover:scale-[1.02] active:scale-[0.98]") : ""}`}
onClick={callNumber}
>
<Phone className="h-5 w-5" />
</Button>
</CardContent>
</Card>
{/* 温馨提示 */}
<div className="px-4 py-2 bg-muted/50 rounded-lg text-xs text-center text-muted-foreground">
💡
</div>
</div>
);
}

347
app/move-car/page.tsx Normal file
View File

@@ -0,0 +1,347 @@
"use client";
import { useState } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { QRCodeSVG } from "qrcode.react";
import { Car, MessageSquare, QrCode, Info, Copy, Zap, RotateCcw } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { toast } from "sonner";
const formSchema = z.object({
plateNumber: z.string().min(1, "请输入车牌号"),
phoneNumber: z
.string()
.min(1, "请输入联系电话")
.regex(/^1[3-9]\d{9}$/, "请输入有效的手机号"),
token: z.string(),
uid: z.string(),
newEnergy: z.boolean(),
});
type FormValues = z.infer<typeof formSchema>;
export default function MoveCar() {
const [generatedUrl, setGeneratedUrl] = useState("");
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
plateNumber: "",
phoneNumber: "",
token: "",
uid: "",
newEnergy: false,
},
});
const onSubmit = (values: FormValues) => {
const url = new URL(window.location.href + "/display");
url.searchParams.append("plateNumber", values.plateNumber);
url.searchParams.append("phoneNumber", values.phoneNumber);
if (values.token) url.searchParams.append("token", values.token);
if (values.uid) url.searchParams.append("uid", values.uid);
if (values.newEnergy) url.searchParams.append("new", "true");
setGeneratedUrl(url.toString());
toast.success("码牌生成成功");
};
const copyUrl = async () => {
try {
await navigator.clipboard.writeText(generatedUrl);
toast.success("链接已复制到剪贴板");
} catch {
toast.error("复制失败");
}
};
const resetForm = () => {
form.reset();
setGeneratedUrl("");
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
{/* Page Header */}
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-amber-400 to-amber-600 shadow-lg">
<Car className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground">
</p>
</div>
</div>
<div className="grid gap-6 lg:grid-cols-2">
{/* Left: Form */}
<div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<Car className="h-4 w-4" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<FormField
control={form.control}
name="plateNumber"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="如京A12345" {...field} className="text-lg" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phoneNumber"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="如13800138000" {...field} className="text-lg" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="newEnergy"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4 shadow-sm">
<div className="space-y-0.5">
<FormLabel className="text-base flex items-center gap-2">
<Zap className="h-4 w-4 text-emerald-500" />
</FormLabel>
<FormDescription>
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<MessageSquare className="h-4 w-4" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Alert className="bg-blue-50/50 dark:bg-blue-950/20 text-blue-900 dark:text-blue-200 border-blue-200 dark:border-blue-800">
<Info className="h-4 w-4" />
<AlertTitle></AlertTitle>
<AlertDescription>
</AlertDescription>
</Alert>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="token"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-1">
Token
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Info className="h-3 w-3 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>WxPusher的应用Token</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<FormControl>
<Input placeholder="应用Token" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="uid"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-1">
UID
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Info className="h-3 w-3 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>UID</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<FormControl>
<Input placeholder="用户UID" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="text-xs text-muted-foreground text-right">
<a
href="https://wxpusher.zjiecode.com/docs/#/"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline ml-1"
>
</a>
</div>
</CardContent>
</Card>
<div className="flex gap-4">
<Button type="submit" size="lg" className="flex-1 gap-2">
<QrCode className="h-4 w-4" />
</Button>
<Button type="button" variant="outline" size="lg" onClick={resetForm} className="gap-2">
<RotateCcw className="h-4 w-4" />
</Button>
</div>
</form>
</Form>
</div>
{/* Right: Preview */}
<div>
<Card className="h-full flex flex-col">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<QrCode className="h-4 w-4" />
{generatedUrl ? "生成成功" : "预览区域"}
</CardTitle>
</CardHeader>
<CardContent className="flex-1 flex flex-col justify-center">
{generatedUrl ? (
<div className="space-y-6">
<Alert variant="default" className="border-emerald-200 bg-emerald-50 dark:bg-emerald-950/20 text-emerald-900 dark:text-emerald-200">
<Info className="h-4 w-4" />
<AlertTitle></AlertTitle>
<AlertDescription>
</AlertDescription>
</Alert>
<div className="flex flex-col items-center gap-4 p-8 bg-white rounded-xl shadow-sm border mx-auto">
<QRCodeSVG value={generatedUrl} size={200} />
<span className="text-sm text-muted-foreground"></span>
</div>
<div className="space-y-2">
<span className="text-sm font-medium"></span>
<div className="p-3 bg-muted rounded-md break-all text-xs font-mono">
{generatedUrl}
</div>
<Button onClick={copyUrl} className="w-full gap-2" variant="secondary">
<Copy className="h-4 w-4" />
</Button>
</div>
</div>
) : (
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground space-y-4">
<QrCode className="h-16 w-16 opacity-20" />
<p></p>
</div>
)}
</CardContent>
</Card>
</div>
</div>
{/* Info Card */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<span className="text-xl">💡</span> 使
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-6 md:grid-cols-3">
<div className="space-y-2">
<h4 className="font-medium text-sm"></h4>
<ul className="list-disc pl-4 text-xs text-muted-foreground space-y-1">
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
<div className="space-y-2">
<h4 className="font-medium text-sm">使</h4>
<ol className="list-decimal pl-4 text-xs text-muted-foreground space-y-1">
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ol>
</div>
<div className="space-y-2">
<h4 className="font-medium text-sm"></h4>
<ul className="list-disc pl-4 text-xs text-muted-foreground space-y-1">
<li>访 WxPusher </li>
<li> Token UID</li>
<li></li>
<li></li>
</ul>
</div>
</div>
</CardContent>
</Card>
</div>
);
}

667
app/page.tsx Normal file
View File

@@ -0,0 +1,667 @@
"use client";
import React, { useState, useMemo } from "react";
import Link from "next/link";
import {
Code,
Zap,
Clock,
Lock,
Link as LinkIcon,
Hash,
Scissors,
QrCode,
FileText,
Car,
CloudDownload,
Search,
LayoutGrid,
Diff,
FileJson,
FileCode,
Database,
Braces,
Type,
Binary,
Languages,
ImageIcon,
Calculator,
FileSpreadsheet,
ImagePlus,
Network,
Barcode,
Terminal,
FerrisWheel,
CircleDollarSign,
Palette,
Timer,
Monitor,
Keyboard,
Volume2,
Users,
Tally5,
Hourglass,
Watch,
KeySquare,
ShieldAlert,
CaseSensitive,
Trophy,
TestTube,
} from "lucide-react";
// --- Data Definitions ---
interface Tool {
id: string;
title: string;
description: string;
icon: React.ReactNode;
href: string;
color: string;
isPlanned?: boolean;
}
interface Category {
id: string;
name: string;
icon: React.ReactNode;
tools: Tool[];
}
const categories: Category[] = [
{
id: "oi-tools",
name: "信奥工具",
icon: <Trophy className="h-4 w-4" />,
tools: [
{
id: "testcase-generator",
title: "测试点生成",
description: "生成算法竞赛测试用例数据",
icon: <TestTube className="h-6 w-6" />,
href: "/testcase-generator",
color: "text-purple-500",
},
],
},
{
id: "dev-tools",
name: "开发工具",
icon: <Code className="h-4 w-4" />,
tools: [
{
id: "json-formatter",
title: "JSON 格式化",
description: "JSON 数据格式化、压缩和验证",
icon: <Code className="h-6 w-6" />,
href: "/json-formatter",
color: "text-blue-500",
},
{
id: "diff",
title: "文本 Diff 对比",
description: "使用 Monaco 显示文本差异",
icon: <Diff className="h-6 w-6" />,
href: "/diff",
color: "text-slate-500",
},
{
id: "yaml-formatter",
title: "YAML 格式化",
description: "YAML 数据格式化和验证",
icon: <FileJson className="h-6 w-6" />,
href: "/yaml-formatter",
color: "text-amber-600",
},
{
id: "html-formatter",
title: "HTML 格式化",
description: "HTML 代码格式化和美化",
icon: <FileCode className="h-6 w-6" />,
href: "/html-formatter",
color: "text-orange-600",
},
{
id: "markdown",
title: "Markdown 编辑",
description: "Markdown 实时预览和导出",
icon: <FileText className="h-6 w-6" />,
href: "/markdown",
color: "text-blue-600",
},
{
id: "sql-formatter",
title: "SQL 格式化",
description: "SQL 语句格式化和美化",
icon: <Database className="h-6 w-6" />,
href: "/sql-formatter",
color: "text-indigo-600",
},
{
id: "html-escape",
title: "HTML 转义",
description: "HTML 实体编码/解码",
icon: <Braces className="h-6 w-6" />,
href: "/html-escape",
color: "text-emerald-600",
},
],
},
{
id: "text-tools",
name: "文本工具",
icon: <Scissors className="h-4 w-4" />,
tools: [
{
id: "text-formatter",
title: "文字格式化",
description: "清理文本中的多余空格和格式",
icon: <Scissors className="h-6 w-6" />,
href: "/text-formatter",
color: "text-pink-500",
},
{
id: "case-converter",
title: "大小写转换",
description: "大写、小写、驼峰等格式转换",
icon: <CaseSensitive className="h-6 w-6" />,
href: "/case-converter",
color: "text-violet-500",
},
{
id: "lorem-ipsum",
title: "Lorem Ipsum",
description: "占位文本生成工具",
icon: <Type className="h-6 w-6" />,
href: "/lorem-ipsum",
color: "text-zinc-500",
},
],
},
{
id: "encoding-tools",
name: "编码工具",
icon: <Binary className="h-4 w-4" />,
tools: [
{
id: "base64",
title: "Base64 编解码",
description: "Base64 编码与解码转换工具",
icon: <Lock className="h-6 w-6" />,
href: "/base64",
color: "text-emerald-500",
},
{
id: "base58",
title: "Base58 编解码",
description: "常用于比特币地址等场景",
icon: <Binary className="h-6 w-6" />,
href: "/base58",
color: "text-amber-500",
},
{
id: "base32",
title: "Base32 编解码",
description: "常用于 TOTP 等场景",
icon: <Binary className="h-6 w-6" />,
href: "/base32",
color: "text-orange-500",
},
{
id: "unicode",
title: "Unicode 转换",
description: "Unicode 字符与编码相互转换",
icon: <Languages className="h-6 w-6" />,
href: "/unicode",
color: "text-blue-500",
},
{
id: "url-encode",
title: "URL 编解码",
description: "URL 参数编码与解码处理",
icon: <LinkIcon className="h-6 w-6" />,
href: "/url-encode",
color: "text-cyan-500",
},
],
},
{
id: "conversion-tools",
name: "转换工具",
icon: <Clock className="h-4 w-4" />,
tools: [
{
id: "timestamp",
title: "时间戳转换",
description: "时间戳与日期时间互相转换",
icon: <Clock className="h-6 w-6" />,
href: "/timestamp",
color: "text-orange-500",
},
{
id: "image-to-pixel",
title: "图片转像素画",
description: "将图片转换为像素艺术风格",
icon: <ImageIcon className="h-6 w-6" />,
href: "/image-to-pixel",
color: "text-pink-600",
},
{
id: "radix-converter",
title: "进制转换器",
description: "二/八/十/十六进制互转",
icon: <Calculator className="h-6 w-6" />,
href: "/radix-converter",
color: "text-indigo-500",
},
{
id: "csv-json",
title: "CSV/JSON 互转",
description: "CSV 与 JSON 格式互相转换",
icon: <FileSpreadsheet className="h-6 w-6" />,
href: "/csv-json",
color: "text-green-600",
},
{
id: "image-base64",
title: "图片 Base64",
description: "图片与 Base64 字符串互转",
icon: <ImagePlus className="h-6 w-6" />,
href: "/image-base64",
color: "text-purple-600",
},
{
id: "ip-radix",
title: "IP 地址转换",
description: "IP 在不同进制间相互转换",
icon: <Network className="h-6 w-6" />,
href: "/ip-radix",
color: "text-cyan-600",
},
],
},
{
id: "generation-tools",
name: "生成工具",
icon: <QrCode className="h-4 w-4" />,
tools: [
{
id: "uuid",
title: "UUID 生成",
description: "生成 UUID/GUID 唯一标识符",
icon: <FileText className="h-6 w-6" />,
href: "/uuid",
color: "text-indigo-500",
},
{
id: "random-string",
title: "随机密码生成",
description: "生成安全的随机密码",
icon: <Zap className="h-6 w-6" />,
href: "/random-string",
color: "text-purple-500",
},
{
id: "qrcode",
title: "二维码生成",
description: "快速生成自定义样式的二维码",
icon: <QrCode className="h-6 w-6" />,
href: "/qrcode",
color: "text-green-500",
},
{
id: "barcode",
title: "条形码生成",
description: "生成各种格式的条形码",
icon: <Barcode className="h-6 w-6" />,
href: "/barcode",
color: "text-zinc-700",
},
{
id: "ascii-art",
title: "ASCII 艺术",
description: "将文本转换为字符艺术",
icon: <Terminal className="h-6 w-6" />,
href: "/ascii-art",
color: "text-emerald-700",
},
{
id: "wheel",
title: "大转盘抽奖",
description: "随机抽奖决策工具",
icon: <FerrisWheel className="h-6 w-6" />,
href: "/wheel",
color: "text-rose-500",
},
{
id: "coin-flip",
title: "抛硬币",
description: "随机正反面决策",
icon: <CircleDollarSign className="h-6 w-6" />,
href: "/coin-flip",
color: "text-amber-600",
},
],
},
{
id: "utility-tools",
name: "实用工具",
icon: <LayoutGrid className="h-4 w-4" />,
tools: [
{
id: "color-picker",
title: "颜色选择器",
description: "HEX/RGB/HSL 格式互转",
icon: <Palette className="h-6 w-6" />,
href: "/color-picker",
color: "text-pink-500",
},
{
id: "regex",
title: "正则测试器",
description: "测试和调试正则表达式",
icon: <Code className="h-6 w-6" />,
href: "/regex",
color: "text-blue-600",
},
{
id: "cron",
title: "Cron 解析器",
description: "解析和验证 Cron 表达式",
icon: <Timer className="h-6 w-6" />,
href: "/cron",
color: "text-orange-600",
},
{
id: "user-agent",
title: "UA 解析器",
description: "解析浏览器 UA 字符串",
icon: <Monitor className="h-6 w-6" />,
href: "/user-agent",
color: "text-slate-600",
},
{
id: "keyboard",
title: "键盘检测器",
description: "检测键盘按键事件详情",
icon: <Keyboard className="h-6 w-6" />,
href: "/keyboard",
color: "text-zinc-600",
},
{
id: "tts",
title: "文字转语音",
description: "将文本转换为语音播放",
icon: <Volume2 className="h-6 w-6" />,
href: "/tts",
color: "text-cyan-600",
},
{
id: "random-group",
title: "随机分组",
description: "快速公平地生成随机团队",
icon: <Users className="h-6 w-6" />,
href: "/random-group",
color: "text-indigo-600",
},
{
id: "scoreboard",
title: "记分板",
description: "红蓝双方比分记录",
icon: <Tally5 className="h-6 w-6" />,
href: "/scoreboard",
color: "text-rose-600",
},
{
id: "pomodoro",
title: "番茄钟",
description: "番茄工作法计时器",
icon: <Timer className="h-6 w-6" />,
href: "/pomodoro",
color: "text-red-500",
},
{
id: "counter",
title: "计数器",
description: "简单的计数工具",
icon: <Hash className="h-6 w-6" />,
href: "/counter",
color: "text-zinc-500",
},
{
id: "countdown",
title: "倒数计时器",
description: "设置倒计时到时提醒",
icon: <Hourglass className="h-6 w-6" />,
href: "/countdown",
color: "text-amber-500",
},
{
id: "stopwatch",
title: "秒表",
description: "精确计时支持记圈",
icon: <Watch className="h-6 w-6" />,
href: "/stopwatch",
color: "text-blue-500",
},
{
id: "ip-calc",
title: "IP 地址计算",
description: "子网掩码与网络划分计算",
icon: <Network className="h-6 w-6" />,
href: "/ip-calc",
color: "text-blue-600",
},
],
},
{
id: "crypto-tools",
name: "加密工具",
icon: <Lock className="h-4 w-4" />,
tools: [
{
id: "hash",
title: "MD5/SHA 哈希",
description: "计算 MD5、SHA1/256/512 哈希",
icon: <Hash className="h-6 w-6" />,
href: "/hash",
color: "text-rose-500",
},
{
id: "jwt",
title: "JWT 解码器",
description: "解析和查看 JWT 内容",
icon: <KeySquare className="h-6 w-6" />,
href: "/jwt",
color: "text-purple-600",
},
{
id: "aes-des",
title: "AES/DES 加密",
description: "对称加密解密工具",
icon: <ShieldAlert className="h-6 w-6" />,
href: "/aes-des",
color: "text-red-600",
},
{
id: "bcrypt",
title: "Bcrypt 哈希",
description: "生成和验证 Bcrypt 密码",
icon: <Lock className="h-6 w-6" />,
href: "/bcrypt",
color: "text-slate-700",
},
],
},
// {
// id: "life-tools",
// name: "生活工具",
// icon: <Car className="h-4 w-4" />,
// tools: [
// {
// id: "move-car",
// title: "挪车码牌",
// description: "生成专属挪车码牌,支持微信推送",
// icon: <Car className="h-6 w-6" />,
// href: "/move-car",
// color: "text-yellow-500",
// },
// {
// id: "alipan-tv-token",
// title: "阿里云盘 Token",
// description: "获取阿里云盘 TV 端授权令牌",
// icon: <CloudDownload className="h-6 w-6" />,
// href: "/alipan-tv-token",
// color: "text-teal-500",
// },
// ],
// },
];
// --- Main Layout Component ---
export default function HomePage() {
const [searchQuery, setSearchQuery] = useState("");
const [activeCategory, setActiveCategory] = useState("all");
// Flatten tools for easier filtering
const allTools = useMemo(() => {
return categories.flatMap((cat) =>
cat.tools.map((tool) => ({ ...tool, categoryId: cat.id }))
);
}, []);
// Filter Logic
const filteredTools = useMemo(() => {
let tools = allTools;
// 1. Category Filter
if (activeCategory !== "all") {
tools = tools.filter((tool) => tool.categoryId === activeCategory);
}
// 2. Search Filter
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase();
tools = tools.filter(
(tool) =>
tool.title.toLowerCase().includes(query) ||
tool.description.toLowerCase().includes(query)
);
}
return tools;
}, [allTools, activeCategory, searchQuery]);
return (
<div className="flex flex-col space-y-6 pb-10">
{/* Hero Section */}
<section className="flex flex-col items-center pt-8 pb-4 text-center space-y-4 md:pt-12 lg:pt-16 animate-in fade-in slide-in-from-bottom-5 duration-700">
<div className="space-y-2 max-w-3xl px-4">
<h1 className="text-3xl font-extrabold tracking-tight sm:text-4xl md:text-5xl text-foreground">
</h1>
<p className="mx-auto max-w-175 text-muted-foreground text-base sm:text-lg">
线
</p>
</div>
{/* Search Input */}
<div className="w-full max-w-lg px-4 relative mt-2">
<div className="relative">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground" />
<input
type="text"
placeholder="搜索工具例如JSON、二维码..."
className="w-full h-12 pl-12 pr-4 rounded-full border border-input bg-background/50 hover:bg-accent/50 focus:bg-background transition-all ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 shadow-xs text-base"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
</div>
</section>
{/* Main Content Area */}
<div className="container max-w-6xl mx-auto px-4 space-y-8">
{/* Category Navigation */}
<div className="flex flex-wrap items-center justify-center gap-2 animate-in fade-in slide-in-from-bottom-6 duration-700 delay-100">
<button
onClick={() => setActiveCategory("all")}
className={`flex items-center gap-2 px-4 py-2 rounded-full text-sm font-medium transition-all ${
activeCategory === "all"
? "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90"
: "bg-muted/50 hover:bg-muted text-muted-foreground hover:text-foreground"
}`}
>
<LayoutGrid className="h-4 w-4" />
</button>
{categories.map((cat) => (
<button
key={cat.id}
onClick={() => setActiveCategory(cat.id)}
className={`flex items-center gap-2 px-4 py-2 rounded-full text-sm font-medium transition-all ${
activeCategory === cat.id
? "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90"
: "bg-muted/50 hover:bg-muted text-muted-foreground hover:text-foreground"
}`}
>
{cat.icon}
{cat.name}
</button>
))}
</div>
{/* Tools Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 animate-in fade-in slide-in-from-bottom-8 duration-1000 delay-200">
{filteredTools.length > 0 ? (
filteredTools.map((tool) => (
<Link
key={tool.id}
href={tool.isPlanned ? "#" : tool.href}
className={`group block h-full ${tool.isPlanned ? "cursor-not-allowed" : ""}`}
onClick={(e) => tool.isPlanned && e.preventDefault()}
>
<div className={`relative h-full overflow-hidden rounded-xl border bg-card text-card-foreground transition-all duration-300 ${!tool.isPlanned && "hover:shadow-md hover:-translate-y-1 hover:border-primary/20 group-hover:bg-accent/5"}`}>
<div className="p-5 flex items-center gap-4 text-left h-full">
<div
className={`relative shrink-0 flex h-12 w-12 items-center justify-center rounded-xl ${tool.color} transition-transform duration-300 ${!tool.isPlanned && "group-hover:scale-110 group-hover:rotate-3"}`}
>
<div className="absolute inset-0 bg-current opacity-10 rounded-xl" />
{tool.icon}
</div>
<div className="space-y-1 flex-1">
<div className="flex items-center gap-2">
<h3 className="font-bold tracking-tight text-base">
{tool.title}
</h3>
{tool.isPlanned && (
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-muted text-muted-foreground font-medium">
</span>
)}
</div>
<p className="text-xs text-muted-foreground line-clamp-2 leading-relaxed">
{tool.description}
</p>
</div>
</div>
</div>
</Link>
))
) : (
<div className="col-span-full py-16 text-center">
<div className="inline-flex h-16 w-16 items-center justify-center rounded-full bg-muted mb-4">
<Search className="h-8 w-8 text-muted-foreground" />
</div>
<h3 className="text-lg font-semibold"></h3>
<p className="text-muted-foreground mt-2">
</p>
</div>
)}
</div>
</div>
</div>
);
}

162
app/pomodoro/page.tsx Normal file
View File

@@ -0,0 +1,162 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { Timer, Play, Pause, RotateCcw, Coffee, Zap } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
type Mode = "WORK" | "SHORT_BREAK" | "LONG_BREAK";
const CONFIG = {
WORK: { time: 25 * 60, label: "工作", color: "text-rose-500", bg: "bg-rose-500" },
SHORT_BREAK: { time: 5 * 60, label: "短休", color: "text-emerald-500", bg: "bg-emerald-500" },
LONG_BREAK: { time: 15 * 60, label: "长休", color: "text-blue-500", bg: "bg-blue-500" }
};
export default function PomodoroPage() {
const [mode, setMode] = useState<Mode>("WORK");
const [timeLeft, setTimeLeft] = useState(CONFIG.WORK.time);
const [isActive, setIsActive] = useState(false);
const timerRef = useRef<any>(null);
useEffect(() => {
if (isActive && timeLeft > 0) {
timerRef.current = setInterval(() => {
setTimeLeft((prev) => prev - 1);
}, 1000);
} else if (timeLeft === 0) {
setIsActive(false);
handleFinished();
} else {
clearInterval(timerRef.current);
}
return () => clearInterval(timerRef.current);
}, [isActive, timeLeft]);
const handleFinished = () => {
const icon = mode === "WORK" ? <Coffee className="text-emerald-500" /> : <Zap className="text-rose-500" />;
const msg = mode === "WORK" ? "工作结束,休息一下吧!" : "休息结束,开始专注吧!";
toast.success(msg, { icon, duration: 5000 });
// Play sound
try {
const audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)();
const oscillator = audioCtx.createOscillator();
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(523.25, audioCtx.currentTime); // C5
oscillator.connect(audioCtx.destination);
oscillator.start();
oscillator.stop(audioCtx.currentTime + 0.5);
} catch {}
};
const toggleTimer = () => setIsActive(!isActive);
const resetTimer = () => {
setIsActive(false);
setTimeLeft(CONFIG[mode].time);
};
const changeMode = (newMode: Mode) => {
setMode(newMode);
setIsActive(false);
setTimeLeft(CONFIG[newMode].time);
};
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
const progress = timeLeft / CONFIG[mode].time;
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-rose-500 to-orange-600 shadow-lg">
<Timer className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground"> (Pomodoro Technique)</p>
</div>
</div>
<div className="max-w-md mx-auto space-y-8 pt-8">
<div className="flex justify-center gap-2 p-1 bg-muted rounded-full">
<ModeButton active={mode === "WORK"} label="工作" onClick={() => changeMode("WORK")} />
<ModeButton active={mode === "SHORT_BREAK"} label="短休" onClick={() => changeMode("SHORT_BREAK")} />
<ModeButton active={mode === "LONG_BREAK"} label="长休" onClick={() => changeMode("LONG_BREAK")} />
</div>
<Card className="p-8 flex flex-col items-center space-y-10 border-none shadow-2xl bg-card/50 backdrop-blur">
<div className="relative flex items-center justify-center">
<svg className="w-72 h-72 transform -rotate-90">
<circle
cx="144" cy="144" r="136"
stroke="currentColor" strokeWidth="4"
fill="transparent" className="text-muted/20"
/>
<circle
cx="144" cy="144" r="136"
stroke="currentColor" strokeWidth="8"
fill="transparent"
strokeDasharray={2 * Math.PI * 136}
strokeDashoffset={2 * Math.PI * 136 * (1 - progress)}
strokeLinecap="round"
className={cn("transition-all duration-1000 ease-linear", CONFIG[mode].color)}
/>
</svg>
<div className="absolute flex flex-col items-center">
<span className="text-7xl font-mono font-black tabular-nums tracking-tighter">
{formatTime(timeLeft)}
</span>
<span className={cn("text-xs font-bold uppercase tracking-widest mt-2", CONFIG[mode].color)}>
{CONFIG[mode].label}ING
</span>
</div>
</div>
<div className="flex items-center gap-6">
<Button
size="lg"
variant="ghost"
className="h-14 w-14 rounded-full"
onClick={resetTimer}
>
<RotateCcw className="h-6 w-6 text-muted-foreground" />
</Button>
<Button
size="lg"
className={cn("h-20 w-20 rounded-full shadow-xl hover:scale-105 active:scale-95 transition-all p-0", CONFIG[mode].bg, "text-white")}
onClick={toggleTimer}
>
{isActive ? <Pause className="h-10 w-10 fill-current" /> : <Play className="h-10 w-10 fill-current ml-1" />}
</Button>
<div className="h-14 w-14" /> {/* Spacer */}
</div>
</Card>
</div>
</div>
);
}
function ModeButton({ active, label, onClick }: { active: boolean, label: string, onClick: () => void }) {
return (
<button
onClick={onClick}
className={cn(
"px-6 py-2 rounded-full text-sm font-bold transition-all flex-1",
active ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
)}
>
{label}
</button>
);
}

339
app/qrcode/page.tsx Normal file
View File

@@ -0,0 +1,339 @@
"use client";
import { useState } from "react";
import { QrCode, Download, Settings, Eye } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { QRCodeCanvas } from "qrcode.react";
import { toast } from "sonner";
interface QRConfig {
size: number;
icon: string;
iconSize: number;
fgColor: string;
bgColor: string;
includeMargin: boolean;
level: "L" | "M" | "Q" | "H";
}
export default function QRCodeGenerator() {
const [text, setText] = useState("");
const [config, setConfig] = useState<QRConfig>({
size: 200,
icon: "",
iconSize: 40,
fgColor: "#000000",
bgColor: "#ffffff",
includeMargin: true,
level: "M",
});
const handleConfigChange = (field: keyof QRConfig, value: any) => {
setConfig((prev) => ({ ...prev, [field]: value }));
};
const downloadQRCode = () => {
const canvas = document.getElementById("qr-code-canvas") as HTMLCanvasElement;
if (canvas) {
try {
const link = document.createElement("a");
link.download = "qrcode.png";
link.href = canvas.toDataURL("image/png");
link.click();
toast.success("二维码下载成功");
} catch {
toast.error("下载失败");
}
} else {
toast.error("未找到二维码");
}
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
{/* Page Header */}
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-emerald-500 to-green-600 shadow-lg">
<QrCode className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground">
</p>
</div>
</div>
<div className="grid gap-6 lg:grid-cols-2">
{/* Left: Configuration */}
<div className="space-y-6">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<Label htmlFor="text"></Label>
<Textarea
id="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="请输入要生成二维码的文本内容,如:网址、文本、微信号等..."
maxLength={2953}
className="min-h-25"
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base font-medium flex items-center gap-2">
<Settings className="h-4 w-4" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="size"> (px)</Label>
<Input
id="size"
type="number"
value={config.size}
onChange={(e) =>
handleConfigChange(
"size",
parseInt(e.target.value) || 200
)
}
min={80}
max={500}
/>
</div>
<div className="space-y-2">
<Label htmlFor="iconSize"> (px)</Label>
<Input
id="iconSize"
type="number"
value={config.iconSize}
onChange={(e) =>
handleConfigChange(
"iconSize",
parseInt(e.target.value) || 40
)
}
min={20}
max={100}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="icon"> ()</Label>
<Input
id="icon"
value={config.icon}
onChange={(e) => handleConfigChange("icon", e.target.value)}
placeholder="https://example.com/logo.png"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="fgColor"></Label>
<div className="flex gap-2">
<Input
id="fgColor"
type="color"
value={config.fgColor}
onChange={(e) =>
handleConfigChange("fgColor", e.target.value)
}
className="h-9 w-full p-1 cursor-pointer"
/>
<Input
value={config.fgColor}
onChange={(e) => handleConfigChange("fgColor", e.target.value)}
className="font-mono"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="bgColor"></Label>
<div className="flex gap-2">
<Input
id="bgColor"
type="color"
value={config.bgColor}
onChange={(e) =>
handleConfigChange("bgColor", e.target.value)
}
className="h-9 w-full p-1 cursor-pointer"
/>
<Input
value={config.bgColor}
onChange={(e) => handleConfigChange("bgColor", e.target.value)}
className="font-mono"
/>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label></Label>
<Select
value={config.level}
onValueChange={(value: "L" | "M" | "Q" | "H") =>
handleConfigChange("level", value)
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="L">L - (7%)</SelectItem>
<SelectItem value="M">M - (15%)</SelectItem>
<SelectItem value="Q">Q - (25%)</SelectItem>
<SelectItem value="H">H - (30%)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-end pb-2">
<div className="flex items-center space-x-2">
<Switch
id="includeMargin"
checked={config.includeMargin}
onCheckedChange={(checked) =>
handleConfigChange("includeMargin", checked)
}
/>
<Label htmlFor="includeMargin"></Label>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Right: Preview */}
<div className="space-y-6">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base font-medium flex items-center gap-2">
<Eye className="h-4 w-4" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-col items-center justify-center min-h-80 bg-muted/30 rounded-lg p-8 border border-dashed">
{text ? (
<div className="flex flex-col items-center gap-4">
<div className="bg-white p-4 rounded-lg shadow-sm">
<QRCodeCanvas
id="qr-code-canvas"
value={text}
size={config.size}
fgColor={config.fgColor}
bgColor={config.bgColor}
level={config.level}
includeMargin={config.includeMargin}
imageSettings={
config.icon
? {
src: config.icon,
height: config.iconSize,
width: config.iconSize,
excavate: true,
}
: undefined
}
/>
</div>
<div className="text-sm text-muted-foreground">
: {config.size}×{config.size}px
</div>
</div>
) : (
<div className="text-muted-foreground text-center">
<QrCode className="h-12 w-12 mx-auto mb-2 opacity-20" />
<p></p>
</div>
)}
</div>
</CardContent>
</Card>
{text && (
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base font-medium"></CardTitle>
</CardHeader>
<CardContent>
<Button
onClick={downloadQRCode}
className="w-full h-12 text-lg gap-2"
>
<Download className="h-5 w-5" />
PNG
</Button>
</CardContent>
</Card>
)}
</div>
</div>
{/* Info Card */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<span className="text-xl">💡</span> 使
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-6 md:grid-cols-3">
<div className="space-y-2">
<h4 className="font-semibold text-sm"></h4>
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-sm">使</h4>
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li></li>
<li>使</li>
<li></li>
</ul>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-sm"></h4>
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,168 @@
"use client";
import { useState } from "react";
import { Calculator, Copy, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { toast } from "sonner";
export default function RadixConverterPage() {
const [values, setValues] = useState({
bin: "",
oct: "",
dec: "",
hex: ""
});
const handleUpdate = (val: string, radix: number) => {
if (val === "") {
setValues({ bin: "", oct: "", dec: "", hex: "" });
return;
}
try {
const decimal = parseInt(val, radix);
if (isNaN(decimal)) return;
setValues({
bin: decimal.toString(2),
oct: decimal.toString(8),
dec: decimal.toString(10),
hex: decimal.toString(16).toUpperCase()
});
} catch {
// Ignore invalid input
}
};
const copyToClipboard = async (text: string) => {
if (!text) return;
try {
await navigator.clipboard.writeText(text);
toast.success("已复制到剪贴板");
} catch {
toast.error("复制失败");
}
};
const clearAll = () => setValues({ bin: "", oct: "", dec: "", hex: "" });
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
{/* Page Header */}
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-indigo-500 to-blue-600 shadow-lg">
<Calculator className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground">
</p>
</div>
</div>
<div className="grid gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0">
<CardTitle className="text-base font-medium"></CardTitle>
<Button variant="ghost" size="sm" onClick={clearAll} className="text-destructive hover:text-destructive">
<Trash2 className="h-4 w-4 mr-2" />
</Button>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid gap-4">
{/* Decimal */}
<div className="space-y-2">
<Label htmlFor="dec"> (Decimal)</Label>
<div className="flex gap-2">
<Input
id="dec"
placeholder="输入十进制数值..."
value={values.dec}
onChange={(e) => handleUpdate(e.target.value, 10)}
className="font-mono text-lg"
/>
<Button variant="outline" size="icon" onClick={() => copyToClipboard(values.dec)}>
<Copy className="h-4 w-4" />
</Button>
</div>
</div>
{/* Hexadecimal */}
<div className="space-y-2">
<Label htmlFor="hex"> (Hexadecimal)</Label>
<div className="flex gap-2">
<Input
id="hex"
placeholder="输入十六进制数值..."
value={values.hex}
onChange={(e) => handleUpdate(e.target.value, 16)}
className="font-mono text-lg"
/>
<Button variant="outline" size="icon" onClick={() => copyToClipboard(values.hex)}>
<Copy className="h-4 w-4" />
</Button>
</div>
</div>
{/* Binary */}
<div className="space-y-2">
<Label htmlFor="bin"> (Binary)</Label>
<div className="flex gap-2">
<Input
id="bin"
placeholder="输入二进制数值..."
value={values.bin}
onChange={(e) => handleUpdate(e.target.value, 2)}
className="font-mono text-lg"
/>
<Button variant="outline" size="icon" onClick={() => copyToClipboard(values.bin)}>
<Copy className="h-4 w-4" />
</Button>
</div>
</div>
{/* Octal */}
<div className="space-y-2">
<Label htmlFor="oct"> (Octal)</Label>
<div className="flex gap-2">
<Input
id="oct"
placeholder="输入八进制数值..."
value={values.oct}
onChange={(e) => handleUpdate(e.target.value, 8)}
className="font-mono text-lg"
/>
<Button variant="outline" size="icon" onClick={() => copyToClipboard(values.oct)}>
<Copy className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Info Card */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<span className="text-xl">💡</span>
</CardTitle>
</CardHeader>
<CardContent>
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-2">
<li><strong> (Binary):</strong> 201</li>
<li><strong> (Octal):</strong> 807</li>
<li><strong> (Decimal):</strong> 10</li>
<li><strong> (Hexadecimal):</strong> 160-9A-F10-15</li>
</ul>
</CardContent>
</Card>
</div>
);
}

153
app/random-group/page.tsx Normal file
View File

@@ -0,0 +1,153 @@
"use client";
import { useState } from "react";
import { Users, Shuffle, Copy } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { toast } from "sonner";
export default function RandomGroupPage() {
const [namesText, setNamesText] = useState("");
const [groupCount, setGroupCount] = useState(2);
const [groups, setGroups] = useState<string[][]>([]);
const handleShuffle = () => {
const names = namesText
.split(/[,\n]+/)
.map((n) => n.trim())
.filter((n) => n !== "");
if (names.length === 0) {
toast.error("请输入成员名单");
return;
}
if (groupCount <= 0) {
toast.error("分组数量必须大于 0");
return;
}
// Shuffle names
const shuffled = [...names].sort(() => Math.random() - 0.5);
// Create groups
const newGroups: string[][] = Array.from({ length: groupCount }, () => []);
shuffled.forEach((name, index) => {
newGroups[index % groupCount].push(name);
});
setGroups(newGroups.filter(g => g.length > 0));
toast.success("随机分组完成");
};
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
toast.success("已复制到剪贴板");
} catch {
toast.error("复制失败");
}
};
const formatGroupsText = () => {
return groups
.map((group, index) => `小组 ${index + 1}:
${group.join(", ")}`)
.join("\n\n");
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-indigo-500 to-blue-600 shadow-lg">
<Users className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground"></p>
</div>
</div>
<div className="grid gap-6 lg:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label> ()</Label>
<Textarea
placeholder="张三&#10;李四&#10;王五&#10;赵六..."
className="min-h-50 font-mono"
value={namesText}
onChange={(e) => setNamesText(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label></Label>
<div className="flex items-center gap-4">
<Input
type="number"
min="1"
value={groupCount}
onChange={(e) => setGroupCount(parseInt(e.target.value) || 1)}
className="w-24"
/>
<Button onClick={handleShuffle} className="flex-1 gap-2">
<Shuffle className="h-4 w-4" />
</Button>
</div>
</div>
</CardContent>
</Card>
<Card className="flex flex-col">
<CardHeader className="flex flex-row items-center justify-between space-y-0">
<CardTitle className="text-base"></CardTitle>
{groups.length > 0 && (
<Button
variant="ghost"
size="sm"
onClick={() => copyToClipboard(formatGroupsText())}
>
<Copy className="h-4 w-4 mr-2" />
</Button>
)}
</CardHeader>
<CardContent className="flex-1 overflow-y-auto max-h-125">
{groups.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center text-muted-foreground py-12 italic">
<Shuffle className="h-12 w-12 mb-2 opacity-10" />
<p></p>
</div>
) : (
<div className="grid gap-4">
{groups.map((group, index) => (
<div key={index} className="p-4 rounded-lg bg-muted/50 border border-muted-foreground/10 space-y-2">
<div className="flex items-center justify-between border-b pb-1">
<span className="font-bold text-primary"> {index + 1} </span>
<span className="text-xs text-muted-foreground">{group.length} </span>
</div>
<div className="flex flex-wrap gap-2">
{group.map((name, ni) => (
<span key={ni} className="px-2 py-1 bg-background rounded border text-sm">
{name}
</span>
))}
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
</div>
);
}

435
app/random-string/page.tsx Normal file
View File

@@ -0,0 +1,435 @@
"use client";
import { useState, useCallback } from "react";
import { Settings, RefreshCw, Copy, Shield, Zap } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { Badge } from "@/components/ui/badge";
import { Textarea } from "@/components/ui/textarea";
import { toast } from "sonner";
interface StringConfig {
length: number;
includeUppercase: boolean;
includeLowercase: boolean;
includeNumbers: boolean;
includeSymbols: boolean;
excludeSimilar: boolean;
customChars: string;
batchCount: number;
}
const defaultConfig: StringConfig = {
length: 16,
includeUppercase: true,
includeLowercase: true,
includeNumbers: true,
includeSymbols: false,
excludeSimilar: false,
customChars: "",
batchCount: 1,
};
const UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
const NUMBERS = "0123456789";
const SYMBOLS = "!@#$%^&*()_+-=[]{}|;:,.<>?";
const SIMILAR_CHARS = "0O1lI|";
export default function RandomStringGenerator() {
const [config, setConfig] = useState<StringConfig>(defaultConfig);
const [generatedStrings, setGeneratedStrings] = useState<string[]>([]);
const [currentString, setCurrentString] = useState("");
const handleConfigChange = (field: keyof StringConfig, value: any) => {
setConfig((prev) => ({ ...prev, [field]: value }));
};
const getCharacterSet = useCallback(() => {
let chars = "";
if (config.customChars) {
chars = config.customChars;
} else {
if (config.includeUppercase) chars += UPPERCASE;
if (config.includeLowercase) chars += LOWERCASE;
if (config.includeNumbers) chars += NUMBERS;
if (config.includeSymbols) chars += SYMBOLS;
}
if (config.excludeSimilar && !config.customChars) {
chars = chars
.split("")
.filter((char) => !SIMILAR_CHARS.includes(char))
.join("");
}
return chars;
}, [config]);
const generateRandomString = useCallback(
(length: number, charset: string) => {
if (!charset) return "";
let result = "";
const charactersLength = charset.length;
for (let i = 0; i < length; i++) {
result += charset.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
},
[]
);
const generateStrings = useCallback(() => {
const charset = getCharacterSet();
if (!charset) {
toast.error("请至少选择一种字符类型");
return;
}
const newStrings = [];
for (let i = 0; i < config.batchCount; i++) {
const randomStr = generateRandomString(config.length, charset);
newStrings.push(randomStr);
}
setGeneratedStrings(newStrings);
setCurrentString(newStrings[0] || "");
toast.success(`成功生成 ${newStrings.length} 个随机字符串`);
}, [config, getCharacterSet, generateRandomString]);
const copyToClipboard = (text: string) => {
navigator.clipboard
.writeText(text)
.then(() => {
toast.success("已复制到剪贴板");
})
.catch(() => {
toast.error("复制失败");
});
};
const copyAllStrings = () => {
const allStrings = generatedStrings.join("\n");
copyToClipboard(allStrings);
};
const getStrengthInfo = () => {
const charset = getCharacterSet();
const entropy = Math.log2(Math.pow(charset.length, config.length));
let strength = "弱";
let color = "bg-red-500 hover:bg-red-600";
if (entropy >= 60) {
strength = "极强";
color = "bg-emerald-500 hover:bg-emerald-600";
} else if (entropy >= 40) {
strength = "强";
color = "bg-cyan-500 hover:bg-cyan-600";
} else if (entropy >= 25) {
strength = "中等";
color = "bg-amber-500 hover:bg-amber-600";
}
return { strength, entropy: entropy.toFixed(1), color };
};
const strengthInfo = getStrengthInfo();
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
{/* Page Header */}
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-violet-500 to-purple-600 shadow-lg">
<Zap className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground">
</p>
</div>
</div>
<div className="grid gap-6 lg:grid-cols-2">
{/* Left: Configuration */}
<div className="space-y-6">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base font-medium flex items-center gap-2">
<Settings className="h-4 w-4" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="length"></Label>
<div className="relative">
<Input
id="length"
type="number"
min={1}
max={1000}
value={config.length}
onChange={(e) =>
handleConfigChange("length", parseInt(e.target.value) || 1)
}
className="pr-8"
/>
<span className="absolute right-3 top-2.5 text-xs text-muted-foreground"></span>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="batchCount"></Label>
<div className="relative">
<Input
id="batchCount"
type="number"
min={1}
max={100}
value={config.batchCount}
onChange={(e) =>
handleConfigChange("batchCount", parseInt(e.target.value) || 1)
}
className="pr-8"
/>
<span className="absolute right-3 top-2.5 text-xs text-muted-foreground"></span>
</div>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base font-medium"></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="flex items-center space-x-2">
<Checkbox
id="uppercase"
checked={config.includeUppercase}
onCheckedChange={(checked) =>
handleConfigChange("includeUppercase", checked)
}
/>
<Label htmlFor="uppercase"> (A-Z)</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="lowercase"
checked={config.includeLowercase}
onCheckedChange={(checked) =>
handleConfigChange("includeLowercase", checked)
}
/>
<Label htmlFor="lowercase"> (a-z)</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="numbers"
checked={config.includeNumbers}
onCheckedChange={(checked) =>
handleConfigChange("includeNumbers", checked)
}
/>
<Label htmlFor="numbers"> (0-9)</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="symbols"
checked={config.includeSymbols}
onCheckedChange={(checked) =>
handleConfigChange("includeSymbols", checked)
}
/>
<Label htmlFor="symbols"></Label>
</div>
<div className="flex items-center space-x-2 col-span-2">
<Checkbox
id="excludeSimilar"
checked={config.excludeSimilar}
onCheckedChange={(checked) =>
handleConfigChange("excludeSimilar", checked)
}
/>
<Label htmlFor="excludeSimilar"> (0O1lI|)</Label>
</div>
</div>
<Separator />
<div className="space-y-2">
<Label htmlFor="customChars"></Label>
<Input
id="customChars"
value={config.customChars}
onChange={(e) =>
handleConfigChange("customChars", e.target.value)
}
placeholder="输入自定义字符集(将覆盖上述选择)"
/>
</div>
</CardContent>
</Card>
</div>
{/* Right: Generate & Result */}
<div className="space-y-6">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base font-medium flex items-center gap-2">
<Shield className="h-4 w-4" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-3 gap-4 text-center">
<div className="space-y-1">
<div className="text-xs text-muted-foreground"></div>
<div className="text-xl font-bold">{getCharacterSet().length}</div>
</div>
<div className="space-y-1">
<div className="text-xs text-muted-foreground"></div>
<div className="text-xl font-bold">{strengthInfo.entropy}</div>
</div>
<div className="space-y-1">
<div className="text-xs text-muted-foreground"></div>
<Badge className={strengthInfo.color}>{strengthInfo.strength}</Badge>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base font-medium"></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Button
onClick={generateStrings}
className="w-full h-12 text-lg gap-2"
>
<RefreshCw className="h-5 w-5" />
</Button>
{generatedStrings.length > 0 && (
<div className="grid grid-cols-2 gap-4">
<Button
variant="outline"
onClick={() => copyToClipboard(currentString)}
className="gap-2"
>
<Copy className="h-4 w-4" />
</Button>
<Button
variant="outline"
onClick={copyAllStrings}
className="gap-2"
>
<Copy className="h-4 w-4" />
</Button>
</div>
)}
</CardContent>
</Card>
{generatedStrings.length > 0 && (
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{config.batchCount === 1 ? (
<div className="relative">
<Input
value={currentString}
readOnly
className="font-mono text-lg pr-12"
/>
<Button
variant="ghost"
size="icon"
className="absolute right-1 top-1 h-8 w-8"
onClick={() => copyToClipboard(currentString)}
>
<Copy className="h-4 w-4" />
</Button>
</div>
) : (
<Textarea
value={generatedStrings.join("\n")}
readOnly
className="font-mono min-h-50"
/>
)}
<div className="text-xs text-muted-foreground text-right">
: {new Date().toLocaleTimeString()}
</div>
</div>
</CardContent>
</Card>
)}
</div>
</div>
{/* Info Card */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<span className="text-xl">💡</span> 使
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-6 md:grid-cols-3">
<div className="space-y-2">
<h4 className="font-semibold text-sm"></h4>
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-sm"></h4>
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li>12</li>
<li>使16</li>
<li></li>
<li></li>
</ul>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-sm"></h4>
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li></li>
<li>API密钥</li>
<li></li>
<li></li>
</ul>
</div>
</div>
<div className="mt-4 p-3 bg-muted rounded-md text-xs text-muted-foreground">
💡
</div>
</CardContent>
</Card>
</div>
);
}

161
app/regex/page.tsx Normal file
View File

@@ -0,0 +1,161 @@
"use client";
import { useState, useEffect } from "react";
import { Code, AlertCircle, CheckCircle2 } from "lucide-react";
import { Textarea } from "@/components/ui/textarea";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Switch } from "@/components/ui/switch";
import { cn } from "@/lib/utils";
export default function RegexPage() {
const [pattern, setPattern] = useState("([a-zA-Z0-9._%-]+)@([a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6})");
const [flags, setFlags] = useState({
global: true,
ignoreCase: false,
multiline: false
});
const [text, setText] = useState("My email is example@mail.com and work@company.org.");
const [matches, setMatches] = useState<any[]>([]);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
try {
if (!pattern) {
setMatches([]);
setError(null);
return;
}
const flagStr = (flags.global ? 'g' : '') + (flags.ignoreCase ? 'i' : '') + (flags.multiline ? 'm' : '');
const regex = new RegExp(pattern, flagStr);
const allMatches = [];
let match;
if (flags.global) {
while ((match = regex.exec(text)) !== null) {
allMatches.push(match);
if (match.index === regex.lastIndex) regex.lastIndex++; // Prevent infinite loop for zero-width matches
}
} else {
match = regex.exec(text);
if (match) allMatches.push(match);
}
setMatches(allMatches);
setError(null);
} catch (e: any) {
setError(e.message);
setMatches([]);
}
}, [pattern, flags, text]);
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-blue-600 to-cyan-600 shadow-lg">
<Code className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground"></p>
</div>
</div>
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex gap-4 flex-col md:flex-row">
<div className="flex-1 relative">
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground font-mono">/</div>
<Input
value={pattern}
onChange={(e) => setPattern(e.target.value)}
className={cn("pl-6 pr-10 font-mono", error && "border-destructive focus-visible:ring-destructive")}
placeholder="输入正则表达式模式..."
/>
<div className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground font-mono">/</div>
</div>
<div className="flex items-center gap-4 px-2">
<FlagToggle label="g" active={flags.global} onToggle={(v) => setFlags({...flags, global: v})} title="全局匹配" />
<FlagToggle label="i" active={flags.ignoreCase} onToggle={(v) => setFlags({...flags, ignoreCase: v})} title="忽略大小写" />
<FlagToggle label="m" active={flags.multiline} onToggle={(v) => setFlags({...flags, multiline: v})} title="多行模式" />
</div>
</div>
{error && (
<div className="p-3 rounded-md bg-destructive/10 text-destructive text-sm flex items-center gap-2">
<AlertCircle className="h-4 w-4" />
{error}
</div>
)}
</CardContent>
</Card>
<div className="grid lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent>
<Textarea
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="请在此输入需要匹配的文本..."
className="min-h-50 font-mono leading-relaxed"
/>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-base"> ({matches.length})</CardTitle>
{matches.length > 0 && <CheckCircle2 className="h-4 w-4 text-emerald-500" />}
</CardHeader>
<CardContent className="space-y-3 max-h-50 overflow-y-auto">
{matches.length === 0 ? (
<div className="text-center py-8 text-muted-foreground text-sm italic">
</div>
) : (
matches.map((m, i) => (
<div key={i} className="p-3 rounded-md bg-muted/50 border space-y-1">
<div className="flex justify-between text-[10px] text-muted-foreground">
<span>Match {i + 1}</span>
<span>Index: {m.index}</span>
</div>
<div className="font-mono text-sm break-all">
{m[0]}
</div>
{m.length > 1 && (
<div className="pt-2 border-t mt-2 space-y-1">
{m.slice(1).map((group: string, gi: number) => (
<div key={gi} className="flex gap-2 text-xs">
<span className="text-muted-foreground font-mono">Group {gi + 1}:</span>
<span className="font-mono text-primary">{group || "(empty)"}</span>
</div>
))}
</div>
)}
</div>
))
)}
</CardContent>
</Card>
</div>
</div>
</div>
);
}
function FlagToggle({ label, active, onToggle, title }: { label: string; active: boolean; onToggle: (v: boolean) => void; title: string }) {
return (
<div className="flex flex-col items-center gap-1" title={title}>
<span className="text-[10px] font-bold text-muted-foreground font-mono">{label}</span>
<Switch checked={active} onCheckedChange={onToggle} />
</div>
);
}

130
app/scoreboard/page.tsx Normal file
View File

@@ -0,0 +1,130 @@
"use client";
import { useState } from "react";
import { Tally5, Plus, Minus, RotateCcw, Edit2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
export default function ScoreboardPage() {
const [teamA, setTeamA] = useState({ name: "红队", score: 0, color: "bg-rose-500", border: "border-rose-500" });
const [teamB, setTeamB] = useState({ name: "蓝队", score: 0, color: "bg-blue-500", border: "border-blue-500" });
const [editTeam, setEditTeam] = useState<"A" | "B" | null>(null);
const [tempName, setTempName] = useState("");
const handleReset = () => {
setTeamA({ ...teamA, score: 0 });
setTeamB({ ...teamB, score: 0 });
};
const handleSwap = () => {
const temp = { ...teamA };
setTeamA({ ...teamB });
setTeamB(temp);
};
const saveName = () => {
if (editTeam === "A") setTeamA({ ...teamA, name: tempName || teamA.name });
else if (editTeam === "B") setTeamB({ ...teamB, name: tempName || teamB.name });
setEditTeam(null);
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center justify-between border-b pb-4">
<div className="flex items-center space-x-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-rose-500 to-blue-600 shadow-lg">
<Tally5 className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground"></p>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={handleSwap}></Button>
<Button variant="outline" size="sm" onClick={handleReset} className="text-destructive hover:text-destructive">
<RotateCcw className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 py-4">
<TeamDisplay
team={teamA}
onScoreChange={(val) => setTeamA({ ...teamA, score: Math.max(0, teamA.score + val) })}
onNameEdit={() => {
setEditTeam("A");
setTempName(teamA.name);
}}
/>
<TeamDisplay
team={teamB}
onScoreChange={(val) => setTeamB({ ...teamB, score: Math.max(0, teamB.score + val) })}
onNameEdit={() => {
setEditTeam("B");
setTempName(teamB.name);
}}
/>
</div>
{/* Manual Modal */}
{editTeam && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in duration-200">
<Card className="w-full max-w-sm shadow-2xl">
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Input
value={tempName}
onChange={(e) => setTempName(e.target.value)}
autoFocus
placeholder="输入名称..."
onKeyDown={(e) => e.key === "Enter" && saveName()}
/>
<div className="flex justify-end gap-2">
<Button variant="ghost" onClick={() => setEditTeam(null)}></Button>
<Button onClick={saveName}></Button>
</div>
</CardContent>
</Card>
</div>
)}
</div>
);
}
function TeamDisplay({ team, onScoreChange, onNameEdit }: { team: any, onScoreChange: (v: number) => void, onNameEdit: () => void }) {
return (
<Card className="flex flex-col items-center justify-between py-12 space-y-10 relative overflow-hidden bg-card/50">
<div className={cn("absolute top-0 left-0 w-full h-3", team.color)} />
<div className="flex items-center gap-3 group cursor-pointer hover:opacity-80 transition-all" onClick={onNameEdit}>
<h2 className="text-4xl font-black tracking-tight">{team.name}</h2>
<Edit2 className="h-5 w-5 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
<div className="text-[14rem] font-black tracking-tighter tabular-nums leading-none select-none drop-shadow-sm">
{team.score}
</div>
<div className="flex gap-8">
<button
className="h-24 w-24 rounded-3xl border-4 border-muted hover:bg-muted transition-colors flex items-center justify-center group"
onClick={() => onScoreChange(-1)}
>
<Minus className="h-12 w-12 text-muted-foreground group-hover:text-foreground" />
</button>
<button
className={cn("h-24 w-24 rounded-3xl shadow-xl hover:scale-105 active:scale-95 transition-all flex items-center justify-center", team.color)}
onClick={() => onScoreChange(1)}
>
<Plus className="h-12 w-12 text-white fill-current" />
</button>
</div>
</Card>
);
}

150
app/sql-formatter/page.tsx Normal file
View File

@@ -0,0 +1,150 @@
"use client";
import React, { useState } from "react";
import Editor from "@monaco-editor/react";
import { format as formatSql } from "sql-formatter";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Database, Copy, RotateCcw, ArrowRight } from "lucide-react";
import { toast } from "sonner";
import { useTheme } from "next-themes";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Label } from "@/components/ui/label";
const SQL_DIALECTS = [
{ value: "sql", label: "Standard SQL" },
{ value: "mysql", label: "MySQL" },
{ value: "postgresql", label: "PostgreSQL" },
{ value: "sqlite", label: "SQLite" },
{ value: "mariadb", label: "MariaDB" },
{ value: "plsql", label: "PL/SQL (Oracle)" },
{ value: "tsql", label: "T-SQL (SQL Server)" },
];
export default function SqlFormatterPage() {
const { theme } = useTheme();
const [input, setInput] = useState("");
const [output, setOutput] = useState("");
const [dialect, setDialect] = useState("sql");
const handleFormat = () => {
if (!input.trim()) return;
try {
const formatted = formatSql(input, {
language: dialect as any,
keywordCase: "upper",
});
setOutput(formatted);
toast.success("SQL 格式化成功");
} catch (e: any) {
toast.error("SQL 格式化失败: " + e.message);
}
};
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
toast.success("已复制到剪贴板");
} catch {
toast.error("复制失败");
}
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-indigo-500 to-purple-600 shadow-lg">
<Database className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight">SQL </h1>
<p className="text-muted-foreground"> SQL </p>
</div>
</div>
{/* Options */}
<Card>
<CardContent className="p-4 flex flex-wrap gap-6 items-end">
<div className="space-y-2">
<Label></Label>
<Select value={dialect} onValueChange={setDialect}>
<SelectTrigger className="w-48">
<SelectValue />
</SelectTrigger>
<SelectContent>
{SQL_DIALECTS.map((d) => (
<SelectItem key={d.value} value={d.value}>
{d.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button onClick={handleFormat} className="gap-2">
<ArrowRight className="h-4 w-4" />
</Button>
</CardContent>
</Card>
<div className="grid gap-6 lg:grid-cols-2">
{/* Input */}
<Card className="flex flex-col min-h-150">
<CardHeader className="py-3 px-4 border-b flex flex-row items-center justify-between">
<CardTitle className="text-base font-medium"> SQL</CardTitle>
<div className="flex gap-2">
<Button variant="ghost" size="sm" onClick={() => setInput("")}>
<RotateCcw className="h-4 w-4" />
</Button>
</div>
</CardHeader>
<CardContent className="p-0 flex-1 h-125">
<Editor
height="100%"
defaultLanguage="sql"
theme={theme === "dark" ? "vs-dark" : "light"}
value={input}
onChange={(value) => setInput(value || "")}
options={{
minimap: { enabled: false },
fontSize: 14,
wordWrap: "on"
}}
/>
</CardContent>
</Card>
{/* Output */}
<Card className="flex flex-col min-h-150">
<CardHeader className="py-3 px-4 border-b flex flex-row items-center justify-between">
<CardTitle className="text-base font-medium"></CardTitle>
<Button variant="ghost" size="sm" onClick={() => copyToClipboard(output)} disabled={!output}>
<Copy className="h-4 w-4 mr-2" />
</Button>
</CardHeader>
<CardContent className="p-0 flex-1 bg-muted/30 h-125">
<Editor
height="100%"
defaultLanguage="sql"
theme={theme === "dark" ? "vs-dark" : "light"}
value={output}
options={{
readOnly: true,
minimap: { enabled: false },
fontSize: 14,
wordWrap: "on"
}}
/>
</CardContent>
</Card>
</div>
</div>
);
}

115
app/stopwatch/page.tsx Normal file
View File

@@ -0,0 +1,115 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { Watch, Play, Pause, RotateCcw, Timer as TimerIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export default function StopwatchPage() {
const [time, setTime] = useState(0);
const [running, setRunning] = useState(false);
const [laps, setLaps] = useState<number[]>([]);
const timerRef = useRef<any>(null);
useEffect(() => {
if (running) {
timerRef.current = setInterval(() => {
setTime((prev) => prev + 10);
}, 10);
} else {
clearInterval(timerRef.current);
}
return () => clearInterval(timerRef.current);
}, [running]);
const formatTime = (ms: number) => {
const minutes = Math.floor(ms / 60000);
const seconds = Math.floor((ms % 60000) / 1000);
const milliseconds = Math.floor((ms % 1000) / 10);
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(2, '0')}`;
};
const handleLap = () => {
setLaps([time, ...laps]);
};
const handleReset = () => {
setRunning(false);
setTime(0);
setLaps([]);
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-blue-500 to-indigo-600 shadow-lg">
<Watch className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground"></p>
</div>
</div>
<div className="grid gap-6 lg:grid-cols-2">
<Card className="flex flex-col items-center justify-center py-12 space-y-8 h-fit">
<div className="text-7xl md:text-8xl font-mono font-bold tracking-tighter tabular-nums text-primary">
{formatTime(time)}
</div>
<div className="flex gap-4">
<Button
size="lg"
variant={running ? "outline" : "default"}
className="w-32 h-14 text-lg rounded-full"
onClick={() => setRunning(!running)}
>
{running ? (
<><Pause className="mr-2 h-5 w-5" /> </>
) : (
<><Play className="mr-2 h-5 w-5" /> </>
)}
</Button>
<Button
size="lg"
variant="secondary"
className="w-32 h-14 text-lg rounded-full"
onClick={running ? handleLap : handleReset}
disabled={time === 0}
>
{running ? (
<><TimerIcon className="mr-2 h-5 w-5" /> </>
) : (
<><RotateCcw className="mr-2 h-5 w-5" /> </>
)}
</Button>
</div>
</Card>
<Card className="h-[400px] flex flex-col">
<CardHeader>
<CardTitle className="text-base flex items-center justify-between">
<span className="text-xs font-normal text-muted-foreground"> {laps.length} </span>
</CardTitle>
</CardHeader>
<CardContent className="flex-1 overflow-y-auto space-y-2 pr-2">
{laps.length === 0 ? (
<div className="h-full flex items-center justify-center text-muted-foreground italic">
</div>
) : (
laps.map((lap, i) => (
<div key={i} className="flex items-center justify-between p-3 rounded-lg bg-muted/30 border border-muted/50 animate-in slide-in-from-top-2 duration-300">
<span className="text-xs font-bold text-muted-foreground">LAP {laps.length - i}</span>
<span className="font-mono font-bold">{formatTime(lap)}</span>
</div>
))
)}
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -0,0 +1,585 @@
"use client";
import { useState, useCallback, useRef, useEffect } from "react";
import { TestTube, Loader2, Download, CheckCircle2, AlertCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { toast } from "sonner";
const API_PREFIX = "/api/testcase-generator";
interface GenerateRequest {
title: string;
englishName: string;
description: string;
standardCode: string;
serviceLevel: string;
testCaseCount: number;
}
interface TaskStatusResponse {
task_id: string;
status: "pending" | "running" | "completed" | "failed";
progress?: number;
title?: string;
english_name?: string;
test_case_count?: number;
generated_cases?: number;
error_message?: string;
currentMessage?: string;
}
interface CompletedResult {
title?: string;
english_name?: string;
test_case_count?: number;
file_size?: string;
}
export default function TestcaseGeneratorPage() {
const [problemTitle, setProblemTitle] = useState("");
const [problemSlug, setProblemSlug] = useState("");
const [problemDescription, setProblemDescription] = useState("");
const [stdProgram, setStdProgram] = useState("");
const [serviceLevel, setServiceLevel] = useState<"standard" | "pro">("standard");
const [testCaseCount, setTestCaseCount] = useState(10);
const [isGenerating, setIsGenerating] = useState(false);
const [progressOpen, setProgressOpen] = useState(false);
const [progressTitle, setProgressTitle] = useState("正在生成测试数据");
const [progressMessage, setProgressMessage] = useState("请稍候...");
const [progressPercent, setProgressPercent] = useState(0);
const [progressTaskId, setProgressTaskId] = useState("");
const [progressGenerated, setProgressGenerated] = useState(0);
const [progressTotal, setProgressTotal] = useState(0);
const [progressRunningTime, setProgressRunningTime] = useState(0);
const pollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const startTimeRef = useRef(0);
const [resultModalOpen, setResultModalOpen] = useState(false);
const [resultData, setResultData] = useState<CompletedResult & { task_id?: string } | null>(null);
const [errorBlock, setErrorBlock] = useState<{ title: string; message: string } | null>(null);
const slugPattern = /^[a-z][a-z0-9_]*$/;
const showProgress = useCallback(() => {
setProgressOpen(true);
setProgressPercent(0);
setProgressGenerated(0);
setProgressTotal(0);
setProgressRunningTime(0);
setProgressTaskId("");
startTimeRef.current = Date.now();
}, []);
const hideProgress = useCallback(() => {
setProgressOpen(false);
if (pollTimerRef.current) {
clearTimeout(pollTimerRef.current);
pollTimerRef.current = null;
}
}, []);
const updateProgress = useCallback(
(opts: {
title?: string;
message?: string;
progress?: number;
taskId?: string;
generatedCount?: number;
totalCount?: number;
}) => {
if (opts.title !== undefined) setProgressTitle(opts.title);
if (opts.message !== undefined) setProgressMessage(opts.message);
if (opts.progress !== undefined) setProgressPercent(Math.min(100, Math.max(0, opts.progress)));
if (opts.taskId !== undefined) setProgressTaskId(opts.taskId);
if (opts.generatedCount !== undefined) setProgressGenerated(opts.generatedCount);
if (opts.totalCount !== undefined) setProgressTotal(opts.totalCount);
},
[]
);
const pollTaskStatus = useCallback(
async (taskId: string, problemTitle: string, estimatedTimeSec: number) => {
const tick = () => {
setProgressRunningTime(Math.floor((Date.now() - startTimeRef.current) / 1000));
};
const interval = setInterval(tick, 1000);
const check = async () => {
try {
const res = await fetch(`${API_PREFIX}/task_status/${taskId}`);
const data: TaskStatusResponse = await res.json();
updateProgress({
progress: data.progress ?? 0,
generatedCount: data.generated_cases ?? 0,
totalCount: data.test_case_count ?? 0,
message: data.currentMessage || "正在处理...",
});
if (data.status === "completed") {
clearInterval(interval);
hideProgress();
setIsGenerating(false);
setResultData({
title: data.title ?? problemTitle,
english_name: data.english_name,
test_case_count: data.test_case_count,
file_size: "计算中...",
task_id: taskId,
});
setResultModalOpen(true);
setErrorBlock(null);
toast.success("测试数据生成完成!");
return;
}
if (data.status === "failed") {
clearInterval(interval);
hideProgress();
setIsGenerating(false);
setErrorBlock({
title: "测试数据生成失败",
message: data.error_message || "未知错误",
});
toast.error("测试数据生成失败");
return;
}
pollTimerRef.current = setTimeout(check, 2000);
} catch (err) {
clearInterval(interval);
hideProgress();
setIsGenerating(false);
const msg = err instanceof Error ? err.message : String(err);
toast.error("获取任务状态失败: " + msg);
}
};
await check();
},
[updateProgress, hideProgress]
);
const handleGenerate = useCallback(async () => {
const title = problemTitle.trim();
const slug = problemSlug.trim();
const description = problemDescription.trim();
const code = stdProgram.trim();
if (!title || !slug || !description || !code) {
toast.error("请填写所有必填字段");
return;
}
if (!slugPattern.test(slug)) {
toast.error("题目英文名只能包含小写字母、数字和下划线,且必须以字母开头");
return;
}
const count = Math.min(50, Math.max(1, testCaseCount));
if (count !== testCaseCount) setTestCaseCount(count);
setIsGenerating(true);
setErrorBlock(null);
showProgress();
updateProgress({ title: "正在生成测试数据", message: "正在初始化...", taskId: "" });
try {
const body: GenerateRequest = {
title,
englishName: slug,
description,
standardCode: code,
serviceLevel,
testCaseCount: count,
};
const res = await fetch(`${API_PREFIX}/generate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const result = await res.json();
if (!res.ok) {
throw new Error(result.error || result.message || "生成请求失败");
}
if (result.success && result.task_id) {
const estimatedTime = result.estimated_time ?? 60;
updateProgress({
taskId: result.task_id,
title: `正在生成「${title}」的测试数据`,
message: `预计需要 ${Math.ceil(estimatedTime / 60)} 分钟,请耐心等待...`,
totalCount: count,
});
await pollTaskStatus(result.task_id, title, estimatedTime);
} else {
throw new Error(result.error || "未返回任务 ID");
}
} catch (err) {
hideProgress();
setIsGenerating(false);
const msg = err instanceof Error ? err.message : String(err);
toast.error("生成失败: " + msg);
}
}, [
problemTitle,
problemSlug,
problemDescription,
stdProgram,
serviceLevel,
testCaseCount,
showProgress,
updateProgress,
hideProgress,
pollTaskStatus,
]);
const handleCancelGeneration = useCallback(() => {
hideProgress();
setIsGenerating(false);
toast.info("生成已取消");
}, [hideProgress]);
const handleDownload = useCallback((taskId: string) => {
if (!taskId) return;
const url = `${API_PREFIX}/download/${taskId}`;
window.open(url, "_blank");
toast.success("开始下载测试数据");
}, []);
const handleCloseResultModal = useCallback(() => {
setResultModalOpen(false);
setResultData(null);
}, []);
useEffect(() => {
return () => {
if (pollTimerRef.current) clearTimeout(pollTimerRef.current);
};
}, []);
const runningTimeStr =
progressRunningTime >= 60
? `${Math.floor(progressRunningTime / 60)}${progressRunningTime % 60}`
: `${progressRunningTime}`;
return (
<div className="space-y-8 max-w-3xl mx-auto">
<div className="text-center space-y-2">
<h1 className="text-3xl font-bold flex items-center justify-center gap-2">
<TestTube className="h-8 w-8 text-purple-500" />
</h1>
<p className="text-muted-foreground">
</p>
</div>
{/* 题目信息 */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label>
<span className="text-destructive">*</span>
</Label>
<Input
placeholder="例如A+B Problem"
value={problemTitle}
onChange={(e) => setProblemTitle(e.target.value)}
/>
<p className="text-xs text-muted-foreground"></p>
</div>
<div className="space-y-2">
<Label>
<span className="text-destructive">*</span>
</Label>
<Input
placeholder="例如aplusb"
value={problemSlug}
onChange={(e) => setProblemSlug(e.target.value.toLowerCase())}
/>
<p className="text-xs text-muted-foreground">
线
</p>
</div>
<div className="space-y-2">
<Label>
<span className="text-destructive">*</span>
</Label>
<Textarea
placeholder={`请输入完整的题目描述,包括:
- 题目背景和要求
- 输入格式
- 输出格式
- 数据范围和约束条件
- 样例输入输出`}
className="min-h-[200px] resize-y"
value={problemDescription}
onChange={(e) => setProblemDescription(e.target.value)}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
</CardContent>
</Card>
{/* 标准程序 */}
<Card>
<CardHeader>
<CardTitle>
(C++) <span className="text-destructive">*</span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Label>C++ </Label>
<Textarea
placeholder={`请输入 C++ 标准程序代码,例如:
#include <iostream>
using namespace std;
int main() {
int a, b;
cin >> a >> b;
cout << a + b << endl;
return 0;
}
注意:从标准输入读取数据,结果输出到标准输出。`}
className="min-h-[280px] font-mono text-sm resize-y"
value={stdProgram}
onChange={(e) => setStdProgram(e.target.value)}
/>
<div className="rounded-lg border border-blue-200 bg-blue-50 dark:bg-blue-950/30 dark:border-blue-800 p-4 text-sm text-blue-900 dark:text-blue-200">
<p className="font-semibold mb-2"></p>
<ul className="list-disc list-inside space-y-1 opacity-90">
<li> (.in) (.out)</li>
<li></li>
<li>使 g++ </li>
<li> 512MB 512MB 30 /</li>
</ul>
</div>
</CardContent>
</Card>
{/* 生成配置 */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label></Label>
<div className="flex items-center gap-4 flex-wrap">
<Input
type="number"
min={1}
max={50}
value={testCaseCount}
onChange={(e) =>
setTestCaseCount(parseInt(e.target.value, 10) || 10)
}
className="w-28"
/>
<p className="text-sm text-muted-foreground">
150
</p>
</div>
</div>
<div className="space-y-2">
<Label></Label>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<button
type="button"
onClick={() => setServiceLevel("standard")}
className={`flex items-start gap-3 rounded-xl border-2 p-5 text-left transition-all ${
serviceLevel === "standard"
? "border-primary bg-primary/5"
: "border-border hover:border-muted-foreground/50"
}`}
>
<input
type="radio"
name="service_level"
checked={serviceLevel === "standard"}
readOnly
className="mt-1"
/>
<div>
<p className="font-semibold"></p>
<p className="text-sm text-muted-foreground"></p>
</div>
</button>
<button
type="button"
onClick={() => setServiceLevel("pro")}
className={`flex items-start gap-3 rounded-xl border-2 p-5 text-left transition-all ${
serviceLevel === "pro"
? "border-primary bg-primary/5"
: "border-border hover:border-muted-foreground/50"
}`}
>
<input
type="radio"
name="service_level"
checked={serviceLevel === "pro"}
readOnly
className="mt-1"
/>
<div>
<p className="font-semibold"></p>
<p className="text-sm text-muted-foreground"></p>
</div>
</button>
</div>
</div>
</CardContent>
</Card>
{/* 提交 */}
<div className="flex justify-center">
<Button
size="lg"
onClick={handleGenerate}
disabled={isGenerating}
className="gap-2"
>
{isGenerating ? (
<>
<Loader2 className="h-5 w-5 animate-spin" />
...
</>
) : (
"开始生成"
)}
</Button>
</div>
{/* 错误信息块 */}
{errorBlock && (
<Card className="border-destructive/50 bg-destructive/5">
<CardContent className="pt-6">
<div className="flex items-start gap-4">
<div className="rounded-full bg-destructive/10 p-3">
<AlertCircle className="h-6 w-6 text-destructive" />
</div>
<div className="flex-1">
<h4 className="font-semibold text-destructive">{errorBlock.title}</h4>
<p className="text-sm text-muted-foreground mt-1">{errorBlock.message}</p>
<p className="text-sm text-amber-700 dark:text-amber-400 mt-4">
</p>
</div>
</div>
</CardContent>
</Card>
)}
{/* 进度遮罩 */}
{progressOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
<Card className="w-full max-w-md shadow-xl">
<CardContent className="pt-6">
<div className="flex items-center gap-3 mb-4">
<div className="rounded-full bg-primary/10 p-3">
<Loader2 className="h-6 w-6 text-primary animate-spin" />
</div>
<div>
<h3 className="font-semibold">{progressTitle}</h3>
<p className="text-sm text-muted-foreground">{progressMessage}</p>
</div>
</div>
<div className="space-y-2 mb-4">
<div className="flex justify-between text-sm">
<span></span>
<span>{Math.round(progressPercent)}%</span>
</div>
<div className="h-2 rounded-full bg-muted overflow-hidden">
<div
className="h-full bg-primary transition-all duration-300"
style={{ width: `${progressPercent}%` }}
/>
</div>
</div>
<div className="text-xs text-muted-foreground space-y-1 mb-4">
<div className="flex justify-between">
<span> ID</span>
<span className="font-mono">{progressTaskId || "-"}</span>
</div>
<div className="flex justify-between">
<span></span>
<span>
{progressGenerated}/{progressTotal}
</span>
</div>
<div className="flex justify-between">
<span></span>
<span>{runningTimeStr}</span>
</div>
</div>
<Button variant="outline" className="w-full" onClick={handleCancelGeneration}>
</Button>
</CardContent>
</Card>
</div>
)}
{/* 结果弹窗 */}
<Dialog open={resultModalOpen} onOpenChange={(open) => !open && handleCloseResultModal()}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
{resultData && (
<div className="space-y-4">
<div className="flex items-center gap-3 rounded-lg border border-green-200 bg-green-50 dark:bg-green-950/30 dark:border-green-800 p-4">
<CheckCircle2 className="h-10 w-10 text-green-600 shrink-0" />
<div>
<p className="font-semibold text-green-800 dark:text-green-200">
ZIP
</p>
</div>
</div>
<div className="grid grid-cols-2 gap-2 text-sm">
<span className="text-muted-foreground"></span>
<span className="font-medium">{resultData.title ?? "—"}</span>
<span className="text-muted-foreground"></span>
<span className="font-medium">{resultData.test_case_count ?? "—"} </span>
<span className="text-muted-foreground"></span>
<span className="font-medium">{new Date().toLocaleString("zh-CN")}</span>
<span className="text-muted-foreground"></span>
<span className="font-medium">{resultData.file_size ?? "—"}</span>
</div>
<div className="flex gap-2">
<Button
className="gap-2 flex-1"
onClick={() => resultData.task_id && handleDownload(resultData.task_id)}
>
<Download className="h-4 w-4" />
</Button>
<Button variant="outline" onClick={handleCloseResultModal}>
</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
</div>
);
}

346
app/text-formatter/page.tsx Normal file
View File

@@ -0,0 +1,346 @@
"use client";
import React, { useState, useCallback } from "react";
import { Scissors, Copy, Eraser, FileText, CheckCircle, Trash2, Paintbrush } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Badge } from "@/components/ui/badge";
import { toast } from "sonner";
export default function TextFormatterPage() {
const [inputText, setInputText] = useState("");
const [outputText, setOutputText] = useState("");
const [removeSpaces, setRemoveSpaces] = useState(true);
const [removeLineBreaks, setRemoveLineBreaks] = useState(true);
const [removeExtraWhitespace, setRemoveExtraWhitespace] = useState(true);
const [stats, setStats] = useState({
originalChars: 0,
originalLines: 0,
formattedChars: 0,
formattedLines: 0,
spacesRemoved: 0,
lineBreaksRemoved: 0,
});
const calculateStats = useCallback((original: string, formatted: string) => {
const originalChars = original.length;
const originalLines = original.split("\n").length;
const formattedChars = formatted.length;
const formattedLines = formatted.split("\n").length;
const originalSpaces = (original.match(/\s/g) || []).length;
const formattedSpaces = (formatted.match(/\s/g) || []).length;
const spacesRemoved = originalSpaces - formattedSpaces;
const lineBreaksRemoved = Math.max(0, originalLines - formattedLines);
setStats({
originalChars,
originalLines,
formattedChars,
formattedLines,
spacesRemoved,
lineBreaksRemoved,
});
}, []);
const formatText = useCallback(() => {
if (!inputText.trim()) {
toast.warning("请输入需要格式化的文本");
return;
}
let formatted = inputText;
if (removeLineBreaks) {
formatted = formatted.replace(/\r?\n/g, "");
}
if (removeSpaces) {
formatted = formatted.replace(/\s+/g, "");
} else if (removeExtraWhitespace) {
formatted = formatted.replace(/\s+/g, " ").trim();
}
setOutputText(formatted);
calculateStats(inputText, formatted);
toast.success("文本格式化完成");
}, [
inputText,
removeSpaces,
removeLineBreaks,
removeExtraWhitespace,
calculateStats,
]);
const copyResult = useCallback(async () => {
if (!outputText) {
toast.warning("没有可复制的内容");
return;
}
try {
await navigator.clipboard.writeText(outputText);
toast.success("已复制到剪贴板");
} catch {
toast.error("复制失败");
}
}, [outputText]);
const clearAll = useCallback(() => {
setInputText("");
setOutputText("");
setStats({
originalChars: 0,
originalLines: 0,
formattedChars: 0,
formattedLines: 0,
spacesRemoved: 0,
lineBreaksRemoved: 0,
});
}, []);
const quickClean = useCallback(() => {
if (!inputText.trim()) {
toast.warning("请输入需要格式化的文本");
return;
}
const formatted = inputText
.replace(/\r?\n/g, "")
.replace(/\t/g, "")
.replace(/\s+/g, "")
.trim();
setOutputText(formatted);
calculateStats(inputText, formatted);
toast.success("快速清理完成");
}, [inputText, calculateStats]);
const handleInputChange = useCallback((value: string) => {
setInputText(value);
}, []);
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
{/* Page Header */}
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-pink-500 to-rose-600 shadow-lg">
<Scissors className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground">
</p>
</div>
</div>
{/* Options Panel */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-6">
<div className="flex items-center space-x-2">
<Switch
id="removeSpaces"
checked={removeSpaces}
onCheckedChange={setRemoveSpaces}
/>
<Label htmlFor="removeSpaces"></Label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="removeLineBreaks"
checked={removeLineBreaks}
onCheckedChange={setRemoveLineBreaks}
/>
<Label htmlFor="removeLineBreaks"></Label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="removeExtraWhitespace"
checked={removeExtraWhitespace}
onCheckedChange={setRemoveExtraWhitespace}
disabled={removeSpaces}
/>
<Label htmlFor="removeExtraWhitespace" className={removeSpaces ? "text-muted-foreground" : ""}>
</Label>
</div>
</div>
</CardContent>
</Card>
{/* Action Bar */}
<Card>
<CardContent className="flex flex-wrap items-center gap-4 p-4">
<Button onClick={formatText} disabled={!inputText.trim()} className="gap-2">
<Paintbrush className="h-4 w-4" />
</Button>
<Button
variant="secondary"
onClick={quickClean}
disabled={!inputText.trim()}
className="gap-2"
>
<Scissors className="h-4 w-4" />
</Button>
<Button
variant="outline"
onClick={copyResult}
disabled={!outputText}
className="gap-2"
>
<Copy className="h-4 w-4" />
</Button>
<Button
variant="ghost"
onClick={clearAll}
className="gap-2 text-destructive hover:text-destructive/90 hover:bg-destructive/10"
>
<Eraser className="h-4 w-4" />
</Button>
</CardContent>
</Card>
{/* Stats Panel */}
{(inputText || outputText) && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card>
<CardContent className="p-4 flex flex-col items-center justify-center text-center space-y-1">
<div className="text-xs text-muted-foreground flex items-center gap-1">
<FileText className="h-3 w-3" />
</div>
<div className="text-2xl font-bold">{stats.originalChars}</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 flex flex-col items-center justify-center text-center space-y-1">
<div className="text-xs text-muted-foreground flex items-center gap-1">
<CheckCircle className="h-3 w-3" />
</div>
<div className="text-2xl font-bold text-emerald-500">{stats.formattedChars}</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 flex flex-col items-center justify-center text-center space-y-1">
<div className="text-xs text-muted-foreground flex items-center gap-1">
<Trash2 className="h-3 w-3" />
</div>
<div className="text-2xl font-bold text-cyan-500">{stats.spacesRemoved}</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 flex flex-col items-center justify-center text-center space-y-1">
<div className="text-xs text-muted-foreground flex items-center gap-1">
<Scissors className="h-3 w-3" />
</div>
<div className="text-2xl font-bold text-violet-500">{stats.lineBreaksRemoved}</div>
</CardContent>
</Card>
</div>
)}
{/* Main Content */}
<div className="grid gap-6 lg:grid-cols-2">
<Card className="flex flex-col">
<CardHeader className="py-3">
<CardTitle className="text-base font-medium flex items-center justify-between">
<span></span>
{inputText.trim() && (
<Badge variant="secondary">
{stats.originalChars}
</Badge>
)}
</CardTitle>
</CardHeader>
<CardContent className="flex-1 p-0">
<Textarea
value={inputText}
onChange={(e) => handleInputChange(e.target.value)}
placeholder="请粘贴需要格式化的文本..."
className="min-h-[400px] border-0 rounded-none focus-visible:ring-0 resize-none font-mono text-sm leading-relaxed p-4"
/>
</CardContent>
</Card>
<Card className="flex flex-col">
<CardHeader className="py-3">
<CardTitle className="text-base font-medium flex items-center justify-between">
<span></span>
{outputText && (
<div className="flex items-center gap-2">
<Badge variant="default" className="bg-emerald-500 hover:bg-emerald-600">
{stats.formattedChars}
</Badge>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={copyResult}
>
<Copy className="h-3 w-3" />
</Button>
</div>
)}
</CardTitle>
</CardHeader>
<CardContent className="flex-1 p-0 bg-muted/30">
<Textarea
value={outputText}
readOnly
placeholder="格式化后的纯文本将显示在这里..."
className="min-h-[400px] border-0 rounded-none focus-visible:ring-0 resize-none font-mono text-sm leading-relaxed p-4 bg-transparent"
/>
</CardContent>
</Card>
</div>
{/* Info Card */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<span className="text-xl">💡</span> 使
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-2">
<h4 className="font-semibold text-sm"></h4>
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li><strong></strong></li>
<li><strong></strong></li>
<li><strong></strong></li>
<li><strong></strong></li>
</ul>
</div>
<div className="space-y-2">
<h4 className="font-semibold text-sm">使</h4>
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li>WordPDF复制的文本</li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
</div>
</CardContent>
</Card>
</div>
);
}

258
app/timestamp/page.tsx Normal file
View File

@@ -0,0 +1,258 @@
"use client";
import { useState, useEffect } from "react";
import { Clock, ArrowLeftRight, Copy, RefreshCw } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { toast } from "sonner";
import dayjs from "dayjs";
export default function TimestampPage() {
const [timestamp, setTimestamp] = useState("");
const [dateTime, setDateTime] = useState<string>("");
const [unit, setUnit] = useState<"seconds" | "milliseconds">("seconds");
const [currentTimestamp, setCurrentTimestamp] = useState("");
useEffect(() => {
// Initialize date time with current time
setDateTime(dayjs().format("YYYY-MM-DDTHH:mm:ss"));
}, []);
useEffect(() => {
const updateCurrent = () => {
const now = Date.now();
setCurrentTimestamp(
unit === "seconds" ? Math.floor(now / 1000).toString() : now.toString()
);
};
updateCurrent();
const timer = setInterval(updateCurrent, 1000);
return () => clearInterval(timer);
}, [unit]);
const handleUnitChange = (newUnit: "seconds" | "milliseconds") => {
if (timestamp && !isNaN(Number(timestamp))) {
const ts = Number(timestamp);
if (unit === "seconds" && newUnit === "milliseconds") {
setTimestamp((ts * 1000).toString());
} else if (unit === "milliseconds" && newUnit === "seconds") {
setTimestamp(Math.floor(ts / 1000).toString());
}
}
setUnit(newUnit);
};
const timestampToDate = () => {
if (!timestamp) {
toast.warning("请输入时间戳");
return;
}
const ts = parseInt(timestamp);
if (isNaN(ts)) {
toast.error("无效的时间戳");
return;
}
const date = unit === "seconds" ? dayjs.unix(ts) : dayjs(ts);
if (!date.isValid()) {
toast.error("无效的时间戳");
return;
}
setDateTime(date.format("YYYY-MM-DDTHH:mm:ss"));
toast.success("转换成功");
};
const dateToTimestamp = () => {
if (!dateTime) {
toast.warning("请选择日期时间");
return;
}
const date = dayjs(dateTime);
const ts = unit === "seconds" ? date.unix() : date.valueOf();
setTimestamp(ts.toString());
toast.success("转换成功");
};
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
toast.success("已复制到剪贴板");
} catch {
toast.error("复制失败");
}
};
const setNow = () => {
const now = dayjs();
setDateTime(now.format("YYYY-MM-DDTHH:mm:ss"));
const ts = unit === "seconds" ? now.unix() : now.valueOf();
setTimestamp(ts.toString());
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
{/* Page Header */}
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-amber-500 to-orange-600 shadow-lg">
<Clock className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground">
Unix
</p>
</div>
</div>
{/* Current Timestamp */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="flex items-center gap-2 text-base font-medium">
<Clock className="h-4 w-4" />
</CardTitle>
<Tabs
value={unit}
onValueChange={(v) => handleUnitChange(v as "seconds" | "milliseconds")}
className="w-45"
>
<TabsList className="grid w-full grid-cols-2 h-8">
<TabsTrigger value="seconds" className="text-xs">(s)</TabsTrigger>
<TabsTrigger value="milliseconds" className="text-xs">(ms)</TabsTrigger>
</TabsList>
</Tabs>
</CardHeader>
<CardContent>
<div className="flex items-center gap-4">
<div className="font-mono text-3xl font-bold text-primary tracking-wider">
{currentTimestamp}
</div>
<Button
variant="outline"
size="icon"
onClick={() => copyToClipboard(currentTimestamp)}
>
<Copy className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
<div className="grid gap-6 lg:grid-cols-2">
{/* Timestamp to Date */}
<Card>
<CardHeader>
<CardTitle className="text-base font-medium"> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="relative">
<Input
placeholder={`输入时间戳 (${unit === "seconds" ? "秒" : "毫秒"})`}
value={timestamp}
onChange={(e) => setTimestamp(e.target.value)}
className="pr-20 font-mono"
/>
<Button
variant="ghost"
size="sm"
className="absolute right-1 top-1 h-7 text-xs text-muted-foreground hover:text-primary"
onClick={() => setTimestamp(currentTimestamp)}
>
使
</Button>
</div>
</div>
<Button onClick={timestampToDate} className="w-full gap-2">
<ArrowLeftRight className="h-4 w-4" />
</Button>
</CardContent>
</Card>
{/* Date to Timestamp */}
<Card>
<CardHeader>
<CardTitle className="text-base font-medium"> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="relative">
<Input
type="datetime-local"
step="1"
value={dateTime}
onChange={(e) => setDateTime(e.target.value)}
className="font-mono"
/>
</div>
</div>
<Button onClick={dateToTimestamp} className="w-full gap-2">
<ArrowLeftRight className="h-4 w-4" />
</Button>
</CardContent>
</Card>
</div>
{/* Result Display */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-base font-medium"></CardTitle>
<Button variant="ghost" size="sm" onClick={setNow} className="gap-2 h-8">
<RefreshCw className="h-3.5 w-3.5" />
</Button>
</CardHeader>
<CardContent>
<div className="grid gap-4 md:grid-cols-2">
<div className="rounded-lg bg-muted p-4 space-y-1">
<p className="text-xs text-muted-foreground">
({unit === "seconds" ? "秒" : "毫秒"})
</p>
<div className="font-mono text-lg font-semibold truncate">
{timestamp || "-"}
</div>
</div>
<div className="rounded-lg bg-muted p-4 space-y-1">
<p className="text-xs text-muted-foreground">
</p>
<div className="font-mono text-lg font-semibold truncate">
{dateTime ? dayjs(dateTime).format("YYYY-MM-DD HH:mm:ss") : "-"}
</div>
</div>
</div>
</CardContent>
</Card>
{/* Info Card */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<span className="text-xl">💡</span> 使
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-2">
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li>Unix 1970-01-01 00:00:00 UTC </li>
<li></li>
<li>JavaScript 使</li>
</ul>
</div>
<div className="space-y-2">
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li> PHPPython使</li>
<li>"使用当前"</li>
<li></li>
</ul>
</div>
</div>
</CardContent>
</Card>
</div>
);
}

215
app/tts/page.tsx Normal file
View File

@@ -0,0 +1,215 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { Volume2, Play, Square, Settings2, Info } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Slider } from "@/components/ui/slider";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { toast } from "sonner";
export default function TtsPage() {
const [text, setText] = useState("欢迎使用信奥工具箱,这是一个基于浏览器 Web Speech API 实现的文字转语音工具。您可以自由调节语速、音调和音量。");
const [voices, setVoices] = useState<SpeechSynthesisVoice[]>([]);
const [selectedVoice, setSelectedVoice] = useState<string>("");
const [rate, setRate] = useState(1);
const [pitch, setPitch] = useState(1);
const [volume, setVolume] = useState(1);
const [isSpeaking, setIsActive] = useState(false);
const synth = useRef<SpeechSynthesis | null>(null);
useEffect(() => {
if (typeof window !== "undefined") {
synth.current = window.speechSynthesis;
const loadVoices = () => {
const availableVoices = synth.current?.getVoices() || [];
setVoices(availableVoices);
// Default to a Chinese voice if available, otherwise first one
const zhVoice = availableVoices.find(v => v.lang.includes("zh")) || availableVoices[0];
if (zhVoice && !selectedVoice) {
setSelectedVoice(zhVoice.name);
}
};
loadVoices();
if (synth.current && synth.current.onvoiceschanged !== undefined) {
synth.current.onvoiceschanged = loadVoices;
}
}
return () => {
if (synth.current) {
synth.current.cancel();
}
};
}, []);
const speak = () => {
if (!synth.current || !text) return;
if (synth.current.speaking) {
synth.current.cancel();
}
const utterance = new SpeechSynthesisUtterance(text);
const voice = voices.find(v => v.name === selectedVoice);
if (voice) utterance.voice = voice;
utterance.rate = rate;
utterance.pitch = pitch;
utterance.volume = volume;
utterance.onstart = () => setIsActive(true);
utterance.onend = () => setIsActive(false);
utterance.onerror = () => {
setIsActive(false);
toast.error("播放出错");
};
synth.current.speak(utterance);
};
const stop = () => {
if (synth.current) {
synth.current.cancel();
setIsActive(false);
}
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-cyan-500 to-blue-600 shadow-lg">
<Volume2 className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight"> (TTS)</h1>
<p className="text-muted-foreground"> API</p>
</div>
</div>
<div className="grid gap-6 lg:grid-cols-3">
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<Textarea
placeholder="请在此输入想要朗读的文字..."
className="min-h-75 text-lg leading-relaxed"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<div className="flex gap-4">
<Button onClick={speak} size="lg" className="flex-1 gap-2 h-14 text-lg shadow-lg hover:scale-[1.02] active:scale-[0.98] transition-all" disabled={isSpeaking}>
<Play className="h-5 w-5 fill-current" />
</Button>
{isSpeaking && (
<Button onClick={stop} size="lg" variant="destructive" className="h-14 w-14 rounded-full p-0 shadow-lg animate-pulse">
<Square className="h-5 w-5 fill-current" />
</Button>
)}
</div>
</CardContent>
</Card>
<Card className="lg:col-span-1">
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Settings2 className="h-4 w-4" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-8">
{/* Voice Selection */}
<div className="space-y-2">
<Label> ({voices.length})</Label>
<Select value={selectedVoice} onValueChange={setSelectedVoice}>
<SelectTrigger>
<SelectValue placeholder="加载中..." />
</SelectTrigger>
<SelectContent>
{voices.map((v) => (
<SelectItem key={v.name} value={v.name}>
{v.name} ({v.lang})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Rate */}
<div className="space-y-4">
<div className="flex justify-between">
<Label> (Rate)</Label>
<span className="text-xs font-mono bg-muted px-1.5 rounded">{rate}x</span>
</div>
<Slider
value={[rate]}
onValueChange={(v) => setRate(v[0])}
min={0.5}
max={2}
step={0.1}
/>
</div>
{/* Pitch */}
<div className="space-y-4">
<div className="flex justify-between">
<Label> (Pitch)</Label>
<span className="text-xs font-mono bg-muted px-1.5 rounded">{pitch}</span>
</div>
<Slider
value={[pitch]}
onValueChange={(v) => setPitch(v[0])}
min={0}
max={2}
step={0.1}
/>
</div>
{/* Volume */}
<div className="space-y-4">
<div className="flex justify-between">
<Label> (Volume)</Label>
<span className="text-xs font-mono bg-muted px-1.5 rounded">{Math.round(volume * 100)}%</span>
</div>
<Slider
value={[volume]}
onValueChange={(v) => setVolume(v[0])}
min={0}
max={1}
step={0.1}
/>
</div>
</CardContent>
</Card>
</div>
{/* Info Card */}
<Card>
<CardHeader>
<CardTitle className="text-sm flex items-center gap-2">
<Info className="h-4 w-4" />
使
</CardTitle>
</CardHeader>
<CardContent className="text-xs text-muted-foreground grid md:grid-cols-2 gap-4">
<ul className="list-disc pl-4 space-y-1">
<li>使 Web Speech API</li>
<li></li>
</ul>
<ul className="list-disc pl-4 space-y-1">
<li></li>
<li>使 Chrome Edge </li>
</ul>
</CardContent>
</Card>
</div>
);
}

113
app/unicode/page.tsx Normal file
View File

@@ -0,0 +1,113 @@
"use client";
import { useState } from "react";
import { Languages, Copy, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { toast } from "sonner";
export default function UnicodePage() {
const [input, setInput] = useState("");
const copyToClipboard = async (text: string) => {
if (!text) return;
try {
await navigator.clipboard.writeText(text);
toast.success("已复制到剪贴板");
} catch {
toast.error("复制失败");
}
};
const toUnicode = () => {
const result = input.split('').map(char => {
const code = char.charCodeAt(0).toString(16).toUpperCase();
return "\\u" + ("0000" + code).slice(-4);
}).join("");
setInput(result);
toast.success("已转换为 Unicode");
};
const fromUnicode = () => {
try {
const result = input.replace(/\\u([0-9a-fA-F]{4})/g, (match, grp) => {
return String.fromCharCode(parseInt(grp, 16));
});
setInput(result);
toast.success("已从 Unicode 还原");
} catch {
toast.error("解析失败,请检查格式");
}
};
const clearAll = () => setInput("");
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
{/* Page Header */}
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-blue-500 to-indigo-600 shadow-lg">
<Languages className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight">Unicode </h1>
<p className="text-muted-foreground">
Unicode \uXXXX
</p>
</div>
</div>
<div className="grid gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0">
<CardTitle className="text-base font-medium"></CardTitle>
<div className="flex gap-2">
<Button variant="ghost" size="sm" onClick={() => copyToClipboard(input)} disabled={!input}>
<Copy className="h-4 w-4 mr-2" />
</Button>
<Button variant="ghost" size="sm" onClick={clearAll} disabled={!input} className="text-destructive hover:text-destructive">
<Trash2 className="h-4 w-4 mr-2" />
</Button>
</div>
</CardHeader>
<CardContent>
<Textarea
placeholder="请输入普通文本或 Unicode 编码 (如 \u4F60\u597D)..."
className="min-h-62.5 font-mono text-base resize-y"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
</CardContent>
</Card>
<div className="flex flex-col sm:flex-row gap-4">
<Button onClick={toUnicode} size="lg" className="flex-1 gap-2">
&rarr; Unicode
</Button>
<Button onClick={fromUnicode} size="lg" variant="outline" className="flex-1 gap-2">
Unicode &rarr;
</Button>
</div>
</div>
{/* Info Card */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<span className="text-xl">💡</span> 使
</CardTitle>
</CardHeader>
<CardContent>
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-2">
<li> `\uXXXX` Unicode </li>
<li><strong> &rarr; Unicode:</strong> 16 Unicode </li>
<li><strong>Unicode &rarr; :</strong> `\uXXXX` </li>
</ul>
</CardContent>
</Card>
</div>
);
}

193
app/url-encode/page.tsx Normal file
View File

@@ -0,0 +1,193 @@
"use client";
import { useState } from "react";
import { Link, ArrowLeftRight, Copy, Eraser } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { toast } from "sonner";
export default function UrlEncodePage() {
const [input, setInput] = useState("");
const [output, setOutput] = useState("");
const [mode, setMode] = useState<"encode" | "decode">("encode");
const handleEncode = () => {
if (!input) {
toast.warning("请输入要编码的内容");
return;
}
try {
const encoded = encodeURIComponent(input);
setOutput(encoded);
toast.success("编码成功");
} catch {
toast.error("编码失败");
}
};
const handleDecode = () => {
if (!input) {
toast.warning("请输入要解码的内容");
return;
}
try {
const decoded = decodeURIComponent(input);
setOutput(decoded);
toast.success("解码成功");
} catch {
toast.error("解码失败,请检查输入是否为有效的 URL 编码字符串");
}
};
const handleConvert = () => {
if (mode === "encode") {
handleEncode();
} else {
handleDecode();
}
};
const copyToClipboard = async () => {
if (!output) {
toast.warning("没有可复制的内容");
return;
}
try {
await navigator.clipboard.writeText(output);
toast.success("已复制到剪贴板");
} catch {
toast.error("复制失败");
}
};
const clearAll = () => {
setInput("");
setOutput("");
};
const swapInputOutput = () => {
setInput(output);
setOutput("");
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
{/* Page Header */}
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-blue-500 to-blue-600 shadow-lg">
<Link className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight">URL </h1>
<p className="text-muted-foreground">
URL
</p>
</div>
</div>
{/* Action Bar */}
<Card>
<CardContent className="flex flex-wrap items-center gap-4 p-4">
<Tabs
value={mode}
onValueChange={(v) => setMode(v as "encode" | "decode")}
className="w-50"
>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="encode"></TabsTrigger>
<TabsTrigger value="decode"></TabsTrigger>
</TabsList>
</Tabs>
<Button onClick={handleConvert} className="gap-2">
<ArrowLeftRight className="h-4 w-4" />
{mode === "encode" ? "编码" : "解码"}
</Button>
<Button variant="outline" onClick={swapInputOutput} className="gap-2">
<ArrowLeftRight className="h-4 w-4 rotate-90" />
</Button>
<Button variant="ghost" onClick={clearAll} className="gap-2">
<Eraser className="h-4 w-4" />
</Button>
</CardContent>
</Card>
<div className="grid gap-6 lg:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="text-base font-medium">
{mode === "encode" ? "原始文本" : "URL 编码字符串"}
</CardTitle>
</CardHeader>
<CardContent>
<Textarea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder={
mode === "encode"
? "请输入要编码的文本,如:你好 世界"
: "请输入要解码的 URL 编码字符串,如:%E4%BD%A0%E5%A5%BD"
}
className="min-h-75 font-mono text-sm resize-none"
/>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-base font-medium">
{mode === "encode" ? "URL 编码结果" : "解码结果"}
</CardTitle>
<Button
variant="ghost"
size="sm"
onClick={copyToClipboard}
className="gap-2"
>
<Copy className="h-4 w-4" />
</Button>
</CardHeader>
<CardContent>
<Textarea
value={output}
readOnly
placeholder="转换结果将显示在这里..."
className="min-h-75 font-mono text-sm bg-muted/50 resize-none"
/>
</CardContent>
</Card>
</div>
{/* Info Card */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<span className="text-xl">💡</span> 使
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-2">
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li>URL URL ASCII </li>
<li> %20</li>
<li> UTF-8 </li>
</ul>
</div>
<div className="space-y-2">
<ul className="list-disc pl-4 text-sm text-muted-foreground space-y-1">
<li>使 encodeURIComponent </li>
<li> - _ . ! ~ * ' ( ) </li>
<li> URL </li>
</ul>
</div>
</div>
</CardContent>
</Card>
</div>
);
}

135
app/user-agent/page.tsx Normal file
View File

@@ -0,0 +1,135 @@
"use client";
import { useState, useEffect } from "react";
import { Monitor, Globe, Cpu, Info } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { toast } from "sonner";
export default function UAPage() {
const [ua, setUa] = useState("");
const [info, setInfo] = useState<any>(null);
useEffect(() => {
const currentUA = navigator.userAgent;
setUa(currentUA);
parseUA(currentUA);
}, []);
const parseUA = (userAgent: string) => {
if (!userAgent) return;
const browser = {
name: "未知浏览器",
version: ""
};
const os = {
name: "未知操作系统",
version: ""
};
// Simple Browser Detection
if (userAgent.indexOf("Edg") > -1) { browser.name = "Edge"; }
else if (userAgent.indexOf("Chrome") > -1) { browser.name = "Chrome"; }
else if (userAgent.indexOf("Firefox") > -1) { browser.name = "Firefox"; }
else if (userAgent.indexOf("Safari") > -1) { browser.name = "Safari"; }
// Simple OS Detection
if (userAgent.indexOf("Windows") > -1) { os.name = "Windows"; }
else if (userAgent.indexOf("Mac OS") > -1) { os.name = "macOS"; }
else if (userAgent.indexOf("Android") > -1) { os.name = "Android"; }
else if (userAgent.indexOf("iPhone") > -1 || userAgent.indexOf("iPad") > -1) { os.name = "iOS"; }
else if (userAgent.indexOf("Linux") > -1) { os.name = "Linux"; }
setInfo({ browser, os });
};
const resetToMine = () => {
const currentUA = navigator.userAgent;
setUa(currentUA);
parseUA(currentUA);
toast.success("已重置为当前浏览器 UA");
};
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center space-x-4 border-b pb-4">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-linear-to-br from-slate-600 to-slate-800 shadow-lg">
<Monitor className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight">User-Agent </h1>
<p className="text-muted-foreground"> User-Agent </p>
</div>
</div>
<div className="grid gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-base">UA </CardTitle>
<Button variant="outline" size="sm" onClick={resetToMine}>使 UA</Button>
</CardHeader>
<CardContent>
<Textarea
value={ua}
onChange={(e) => {
setUa(e.target.value);
parseUA(e.target.value);
}}
className="font-mono min-h-25"
placeholder="请在此粘贴 User-Agent 字符串..."
/>
</CardContent>
</Card>
{info && (
<div className="grid md:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Globe className="h-4 w-4 text-blue-500" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between border-b pb-2">
<span className="text-muted-foreground">/</span>
<span className="font-bold">{info.browser.name}</span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Cpu className="h-4 w-4 text-orange-500" />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between border-b pb-2">
<span className="text-muted-foreground"></span>
<span className="font-bold">{info.os.name}</span>
</div>
</CardContent>
</Card>
</div>
)}
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Info className="h-4 w-4" />
User-Agent
</CardTitle>
</CardHeader>
<CardContent className="text-sm text-muted-foreground leading-relaxed">
User-Agent (UA) HTTP 使 UA
</CardContent>
</Card>
</div>
</div>
);
}

64
app/utils/decode.ts Normal file
View File

@@ -0,0 +1,64 @@
import CryptoES from 'crypto-es';
const decrypt = function (ciphertext: string, iv: string, t: number): string {
try {
const key = generateKey(t);
const decrypted = CryptoES.AES.decrypt(ciphertext, CryptoES.enc.Utf8.parse(key), {
iv: CryptoES.enc.Hex.parse(iv),
mode: CryptoES.mode.CBC,
padding: CryptoES.pad.Pkcs7
});
const dec = CryptoES.enc.Utf8.stringify(decrypted).toString();
return dec;
} catch (error) {
console.error("Decryption failed", error);
throw error;
}
};
function h(charArray: string[], modifier: number): string {
const uniqueChars = Array.from(new Set(charArray));
const numericModifier = Number(modifier.toString().slice(7));
const transformedString = uniqueChars.map(char => {
const charCode = char.charCodeAt(0);
let newCharCode = Math.abs(charCode - (numericModifier % 127) - 1);
if (newCharCode < 33) {
newCharCode += 33;
}
return String.fromCharCode(newCharCode);
}).join("");
return transformedString;
}
function getParams(t: number): Record<string, string | number> {
return {
'akv': '2.8.1496', // apk_version_name 版本号
'apv': '1.3.6', // 内部版本号
'b': 'XiaoMi', // 手机品牌
'd': 'e87a4d5f4f28d7a17d73c524eaa8ac37', // 设备id 可随机生成
'm': '23046RP50C', // 手机型号
'mac': '', // mac地址
'n': '23046RP50C', // 手机型号
't': t, // 时间戳
'wifiMac': '020000000000', // wifiMac地址
};
}
const generateKey = function (t: number): string {
const params = getParams(t);
const sortedKeys = Object.keys(params).sort();
let concatenatedParams = "";
sortedKeys.forEach(key => {
if (key !== "t") {
concatenatedParams += params[key];
}
});
const keyArray = concatenatedParams.split("");
const hashedKey = h(keyArray, t);
return CryptoES.MD5(hashedKey).toString(CryptoES.enc.Hex);
};
export { decrypt, getParams };

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