初始化提交

This commit is contained in:
2025-12-14 15:11:17 +08:00
commit 1123d6aef2
39 changed files with 5213 additions and 0 deletions

41
.dockerignore Normal file
View File

@@ -0,0 +1,41 @@
# Docker 相关文件
.dockerignore
# Node
node_modules/
npm-debug.log
yarn-error.log
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
*.egg-info/
# 构建产物
dist/
build/
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Git
.git/
.gitignore
# 文档
README.md
*.md
# 配置
.env
.env.local

56
.gitattributes vendored Normal file
View File

@@ -0,0 +1,56 @@
# Auto detect text files and perform LF normalization
* text=auto
# Explicitly declare text files you want to always be normalized and converted
# to native line endings on checkout.
*.c text
*.h text
*.c++ text
*.h++ text
*.hpp text
*.cpp text
*.hxx text
*.css text
*.scss text
*.html text
*.java text
*.js text
*.json text
*.jsx text
*.less text
*.lua text
*.md text
*.php text
*.pl text
*.py text
*.rb text
*.rss text
*.sh text
*.sql text
*.swift text
*.ts text
*.tsx text
*.txt text
*.xml text
*.yml text
# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.mov binary
*.mp4 binary
*.mp3 binary
*.flv binary
*.fla binary
*.swf binary
*.gz binary
*.zip binary
*.7z binary
*.ttf binary
*.eot binary
*.woff binary
*.pyc binary
*.pdf binary

52
.gitignore vendored Normal file
View File

@@ -0,0 +1,52 @@
# General
.DS_Store
Thumbs.db
*.log
*.tmp
# IDE
.idea/
.vscode/
*.swp
*.swo
*.swn
# Python / Backend (mengyalinkfly-backend)
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
ENV/
env.bak/
venv.bak/
*.egg-info/
.coverage
.tox/
.pytest_cache/
# Flask specific
instance/
.webassets-cache
# Node / Frontend (mengyalinkfly-frontend)
node_modules/
dist/
dist-ssr/
*.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
.npm/
.eslintcache
# Environment Variables
.env
.env.local
.env.*.local
*.env
# Docker
docker-compose.override.yml

43
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,43 @@
# 贡献指南
感谢你对萌芽短链接项目的关注!我们欢迎任何形式的贡献。
## 🤝 如何参与
1. **Fork 本仓库**:点击右上角的 Fork 按钮,将仓库复刻到你的 GitHub 账号下。
2. **Clone 仓库**:将 Fork 后的仓库克隆到本地。
```bash
git clone https://github.com/your-username/mengyalinkfly.git
```
3. **创建分支**:为你的修改创建一个新的分支。
```bash
git checkout -b feature/AmazingFeature
```
4. **提交修改**
- 请确保代码风格统一。
- 提交信息请清晰描述修改内容。
```bash
git commit -m 'Add some AmazingFeature'
```
5. **推送到远程**
```bash
git push origin feature/AmazingFeature
```
6. **提交 Pull Request**:在 GitHub 上发起 Pull Request我们将尽快审核。
## 🐛 提交 Issue
如果你发现了 Bug 或有功能建议,欢迎提交 Issue。请包含以下信息
- 问题描述
- 复现步骤
- 预期行为
- 截图(如果有)
- 环境信息OS, Browser, etc.
## 📄 代码规范
- **前端**:遵循 React 最佳实践,使用 ESLint 进行检查。
- **后端**:遵循 PEP 8 规范。
再次感谢你的贡献!

50
Dockerfile Normal file
View File

@@ -0,0 +1,50 @@
# 使用多阶段构建
# 阶段1: 构建前端
FROM node:18-alpine AS frontend-builder
WORKDIR /app/frontend
# 复制前端文件
COPY mengyalinkfly-frontend/package*.json ./
RUN npm install
COPY mengyalinkfly-frontend/ ./
RUN npm run build
# 阶段2: 最终镜像
FROM python:3.11-slim
WORKDIR /app
# 安装 nginx 和必要的工具
RUN apt-get update && \
apt-get install -y nginx && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# 复制后端文件
COPY mengyalinkfly-backend/ ./backend/
# 安装 Python 依赖
RUN pip install --no-cache-dir -r ./backend/requirements.txt && \
pip install --no-cache-dir gunicorn
# 从前端构建阶段复制构建产物
COPY --from=frontend-builder /app/frontend/dist ./frontend/dist
# 创建持久化数据目录
RUN mkdir -p /shumengya/docker/storage/mengyalinkfly
# 复制 nginx 配置和启动脚本
COPY docker/nginx.conf /etc/nginx/sites-available/default
COPY docker/start.sh /app/start.sh
RUN chmod +x /app/start.sh
# 暴露端口
EXPOSE 7878
# 设置工作目录
WORKDIR /app/backend
# 启动服务
CMD ["/app/start.sh"]

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 萌芽短链接
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

124
README.md Normal file
View File

@@ -0,0 +1,124 @@
# 萌芽短链接分发网站
一个简单、快速的短链接生成工具,使用 React + Flask 构建。
## 🌟 功能特点
- ✨ 生成 4 位字母数字组合的短链接(支持大小写)
- 🎲 随机生成可用的短链接代码
- ✅ 实时检查链接代码是否可用
- 📋 一键复制生成的短链接
- 🎨 淡绿色渐变清新柔和风格
- 📱 完美适配移动端
## 🛠️ 技术栈
### 前端
- React 19
- Axios (HTTP 请求)
- Vite (构建工具)
### 后端
- Python 3
- Flask (Web 框架)
- Flask-CORS (跨域支持)
## 📦 项目结构
```
萌芽短链分发/
├── mengyalinkfly-backend/ # 后端
│ ├── app.py # Flask 应用主文件
│ ├── requirements.txt # Python 依赖
│ ├── link_data.json # 链接数据存储
│ └── README.md # 后端说明
└── mengyalinkfly-frontend/ # 前端
├── src/
│ ├── App.jsx # 主应用组件
│ ├── App.css # 样式文件
│ └── main.jsx # 入口文件
├── package.json # 前端依赖
├── vite.config.js # Vite 配置
└── README.md # 前端说明
```
## 🚀 快速开始
### 1. 启动后端
```bash
cd mengyalinkfly-backend
# 安装依赖
pip install -r requirements.txt
# 运行后端服务
python app.py
```
后端将在 `http://localhost:5000` 启动
### 2. 启动前端
```bash
cd mengyalinkfly-frontend
# 安装依赖
npm install
# 启动开发服务器
npm run dev
```
前端将在 `http://localhost:5173` 启动
## 📝 API 接口
### 检查短链接是否可用
```
GET /api/check/<code>
```
### 生成随机短链接代码
```
GET /api/generate
```
### 创建短链接
```
POST /api/create
Content-Type: application/json
{
"code": "Aaa3",
"target_url": "https://example.com"
}
```
### 跳转到目标链接
```
GET /<code>
```
## 🎨 设计特色
- **配色方案**:淡绿色渐变 (#a8e6cf#dcedc1#c9e4ca)
- **交互动画**:平滑过渡、悬浮效果、弹跳动画
- **响应式设计**:完美适配各种屏幕尺寸
- **用户体验**:清晰的视觉反馈、友好的错误提示
## 🌐 域名配置
项目使用域名:`short.shumengya.top`
短链接格式:`short.shumengya.top/{4位代码}`
例如:`short.shumengya.top/Aaa3`
## 📄 许可证
MIT License
---
© 2025 萌芽短链接 - 让分享更简单 🌱

11
docker-compose.yml Normal file
View File

@@ -0,0 +1,11 @@
services:
mengyalinkfly:
build: .
container_name: mengyalinkfly
ports:
- "7878:7878"
volumes:
- /shumengya/docker/storage/mengyalinkfly:/shumengya/docker/storage/mengyalinkfly
restart: unless-stopped
environment:
- TZ=Asia/Shanghai

36
docker/nginx.conf Normal file
View File

@@ -0,0 +1,36 @@
server {
listen 7878;
server_name _;
root /app/frontend/dist;
index index.html;
# 静态内容优先走前端
location / {
try_files $uri $uri/ /index.html @backend;
add_header Cache-Control "no-cache, must-revalidate";
}
# 后端 API 代理
location /api {
proxy_pass http://127.0.0.1:5000;
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;
}
# 兜底转发到后端(短链跳转)
location @backend {
proxy_pass http://127.0.0.1:5000;
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;
}
# 静态资源缓存策略
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}

20
docker/start.sh Normal file
View File

@@ -0,0 +1,20 @@
#!/bin/bash
# 如果持久化目录中有数据文件,使用它;否则创建新的
if [ -f /shumengya/docker/storage/mengyalinkfly/link_data.json ]; then
echo "使用持久化的数据文件..."
ln -sf /shumengya/docker/storage/mengyalinkfly/link_data.json /app/backend/link_data.json
else
echo "创建新的数据文件..."
echo '{}' > /shumengya/docker/storage/mengyalinkfly/link_data.json
ln -sf /shumengya/docker/storage/mengyalinkfly/link_data.json /app/backend/link_data.json
fi
# 启动 nginx
echo "启动 Nginx..."
nginx
# 启动 Flask 后端(使用 gunicorn
echo "启动 Flask 后端..."
cd /app/backend
exec gunicorn -w 4 -b 0.0.0.0:5000 app:app

View File

@@ -0,0 +1,30 @@
# 萌芽短链接分发系统 - 后端
## 功能说明
- 生成4位随机字母数字组合的短链接支持大小写
- 检查短链接是否已被使用
- 创建和保存短链接
- 重定向到目标URL
## 安装依赖
```bash
pip install -r requirements.txt
```
## 运行
```bash
python app.py
```
服务将在 `http://localhost:5000` 启动
## API 接口
- `GET /api/check/<code>` - 检查短链接是否可用
- `GET /api/generate` - 生成随机可用短链接
- `POST /api/create` - 创建短链接
- `GET /<code>` - 跳转到目标链接
- `GET /api/links` - 获取所有链接列表

View File

@@ -0,0 +1,407 @@
from flask import Flask, request, jsonify, redirect
from flask_cors import CORS
import json
import os
import random
import string
from datetime import datetime, timedelta
app = Flask(__name__)
CORS(app)
DATA_FILE = 'link_data.json'
def load_links():
"""加载链接数据"""
if not os.path.exists(DATA_FILE):
return {}
try:
with open(DATA_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except:
return {}
def save_links(links):
"""保存链接数据"""
with open(DATA_FILE, 'w', encoding='utf-8') as f:
json.dump(links, f, ensure_ascii=False, indent=2)
def clean_expired_links():
"""清理过期的链接"""
links = load_links()
current_time = datetime.now()
expired_codes = []
for code, data in links.items():
expires_at = data.get('expires_at')
if expires_at and expires_at != 'never':
expire_time = datetime.fromisoformat(expires_at)
if current_time > expire_time:
expired_codes.append(code)
for code in expired_codes:
del links[code]
if expired_codes:
save_links(links)
return len(expired_codes)
def is_link_expired(link_data):
"""检查链接是否过期"""
expires_at = link_data.get('expires_at')
if not expires_at or expires_at == 'never':
return False
expire_time = datetime.fromisoformat(expires_at)
return datetime.now() > expire_time
def calculate_expire_time(expire_option):
"""计算过期时间"""
if expire_option == 'never':
return 'never'
expire_map = {
'10min': timedelta(minutes=10),
'1hour': timedelta(hours=1),
'1day': timedelta(days=1),
'3days': timedelta(days=3),
'1month': timedelta(days=30),
'1year': timedelta(days=365)
}
delta = expire_map.get(expire_option)
if delta:
return (datetime.now() + delta).isoformat()
return 'never'
def generate_random_code():
"""生成随机4位字母数字组合包含大小写"""
chars = string.ascii_letters + string.digits # a-zA-Z0-9
return ''.join(random.choices(chars, k=4))
#==========================后端API接口==========================#
@app.route('/')
def index():
"""首页"""
return jsonify({
'message': '萌芽短链接分发系统',
"author": "树萌芽",
"API":
{
"/api/check/XXXX":"检查短链接是否已被使用",
"/api/generate":"生成随机可用的短链接代码",
"/api/create":"创建短链接",
"/XXXX":"跳转到目标链接",
"/api/links":"获取所有链接(可选功能)",
"/api/redirect/XXXX":"API方式获取跳转链接供前端路由使用",
"":"",
}
})
@app.route('/api/check/<code>', methods=['GET'])
def check_code(code):
"""检查短链接是否已被使用"""
if len(code) != 4:
return jsonify({'valid': False, 'message': '链接代码必须是4位字符'}), 400
links = load_links()
exists = code in links
return jsonify({
'exists': exists,
'available': not exists
})
@app.route('/api/generate', methods=['GET'])
def generate_code():
"""生成随机可用的短链接代码"""
links = load_links()
max_attempts = 100
for _ in range(max_attempts):
code = generate_random_code()
if code not in links:
return jsonify({
'code': code,
'success': True
})
return jsonify({
'success': False,
'message': '生成失败,请重试'
}), 500
@app.route('/api/create', methods=['POST'])
def create_link():
"""创建短链接"""
data = request.json
code = data.get('code', '').strip()
target_url = data.get('target_url', '').strip()
expire_option = data.get('expire_option', 'never')
# 验证输入
if not code or not target_url:
return jsonify({
'success': False,
'message': '链接代码和目标地址不能为空'
}), 400
if len(code) != 4:
return jsonify({
'success': False,
'message': '链接代码必须是4位字符'
}), 400
# 清理过期链接
clean_expired_links()
# 检查是否已存在
links = load_links()
if code in links:
return jsonify({
'success': False,
'message': '该链接代码已被使用,请选择其他代码'
}), 409
# 计算过期时间
expires_at = calculate_expire_time(expire_option)
# 保存链接
links[code] = {
'target_url': target_url,
'created_at': datetime.now().isoformat(),
'expires_at': expires_at,
'expire_option': expire_option,
'visit_count': 0
}
save_links(links)
return jsonify({
'success': True,
'code': code,
'short_url': f'short.shumengya.top/{code}',
'expires_at': expires_at,
'message': '短链接创建成功'
})
@app.route('/<code>', methods=['GET'])
def redirect_link(code):
"""跳转到目标链接"""
if len(code) != 4:
return jsonify({
'error': '无效的短链接',
'message': '链接代码必须是4位字符'
}), 404
# 清理过期链接
clean_expired_links()
links = load_links()
if code not in links:
return jsonify({
'error': '短链接不存在',
'message': f'未找到代码为 {code} 的短链接'
}), 404
link_data = links[code]
# 检查是否过期
if is_link_expired(link_data):
del links[code]
save_links(links)
return jsonify({
'error': '短链接已过期',
'message': f'代码为 {code} 的短链接已过期'
}), 410
# 增加访问次数
link_data['visit_count'] = link_data.get('visit_count', 0) + 1
links[code] = link_data
save_links(links)
target_url = link_data['target_url']
# 确保URL包含协议
if not target_url.startswith(('http://', 'https://')):
target_url = 'http://' + target_url
return redirect(target_url, code=302)
@app.route('/api/links', methods=['GET'])
def get_all_links():
"""获取所有链接(可选功能)"""
# 清理过期链接
clean_expired_links()
links = load_links()
return jsonify({
'success': True,
'count': len(links),
'links': links
})
@app.route('/api/redirect/<code>', methods=['GET'])
def api_redirect(code):
"""API方式获取跳转链接供前端路由使用"""
if len(code) != 4:
return jsonify({
'success': False,
'message': '链接代码必须是4位字符'
}), 404
# 清理过期链接
clean_expired_links()
links = load_links()
if code not in links:
return jsonify({
'success': False,
'message': f'未找到代码为 {code} 的短链接'
}), 404
link_data = links[code]
# 检查是否过期
if is_link_expired(link_data):
del links[code]
save_links(links)
return jsonify({
'success': False,
'message': f'代码为 {code} 的短链接已过期'
}), 410
# 增加访问次数
link_data['visit_count'] = link_data.get('visit_count', 0) + 1
links[code] = link_data
save_links(links)
target_url = link_data['target_url']
# 确保URL包含协议
if not target_url.startswith(('http://', 'https://')):
target_url = 'http://' + target_url
return jsonify({
'success': True,
'code': code,
'target_url': target_url
})
@app.route('/api/stats/<code>', methods=['GET'])
def get_link_stats(code):
"""获取链接统计信息"""
if len(code) != 4:
return jsonify({
'success': False,
'message': '链接代码必须是4位字符'
}), 404
links = load_links()
if code not in links:
return jsonify({
'success': False,
'message': f'未找到代码为 {code} 的短链接'
}), 404
link_data = links[code]
# 检查是否过期
if is_link_expired(link_data):
return jsonify({
'success': False,
'message': f'代码为 {code} 的短链接已过期',
'expired': True
}), 410
return jsonify({
'success': True,
'code': code,
'target_url': link_data['target_url'],
'visit_count': link_data.get('visit_count', 0),
'created_at': link_data.get('created_at'),
'expires_at': link_data.get('expires_at'),
'expire_option': link_data.get('expire_option')
})
@app.route('/api/admin/delete/<code>', methods=['DELETE'])
def admin_delete_link(code):
"""管理员删除指定链接"""
if len(code) != 4:
return jsonify({
'success': False,
'message': '链接代码必须是4位字符'
}), 400
links = load_links()
if code not in links:
return jsonify({
'success': False,
'message': f'未找到代码为 {code} 的短链接'
}), 404
# 删除链接
del links[code]
save_links(links)
return jsonify({
'success': True,
'message': f'成功删除短链接 {code}'
})
@app.route('/api/admin/delete-batch', methods=['POST'])
def admin_delete_batch():
"""管理员批量删除链接"""
data = request.json
codes = data.get('codes', [])
if not codes or not isinstance(codes, list):
return jsonify({
'success': False,
'message': '请提供要删除的链接代码列表'
}), 400
links = load_links()
deleted = []
not_found = []
for code in codes:
if len(code) == 4 and code in links:
del links[code]
deleted.append(code)
else:
not_found.append(code)
save_links(links)
return jsonify({
'success': True,
'deleted': deleted,
'deleted_count': len(deleted),
'not_found': not_found,
'message': f'成功删除 {len(deleted)} 个短链接'
})
@app.route('/api/admin/clear-expired', methods=['POST'])
def admin_clear_expired():
"""管理员清理所有过期链接"""
expired_count = clean_expired_links()
return jsonify({
'success': True,
'cleared_count': expired_count,
'message': f'成功清理 {expired_count} 个过期链接'
})
#==========================后端API接口==========================#
if __name__ == '__main__':
# 确保数据文件存在
if not os.path.exists(DATA_FILE):
save_links({})
app.run(debug=True, host='0.0.0.0', port=5000)

View File

@@ -0,0 +1,8 @@
{
"wadw": {
"target_url": "https://pan.shumengya.top",
"created_at": "2025-11-16T13:33:19.613949",
"expires_at": "never",
"expire_option": "never"
}
}

View File

@@ -0,0 +1,2 @@
Flask==3.0.0
flask-cors==4.0.0

24
mengyalinkfly-frontend/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,43 @@
# 萌芽短链接 - 前端
基于 React + Vite 构建的短链接生成前端界面。
## 功能
- 创建自定义或随机生成的 4 位短链接代码
- 实时检查链接代码可用性
- 一键复制生成的短链接
- 淡绿色渐变清新界面设计
- 移动端友好的响应式布局
## 安装和运行
```bash
# 安装依赖
npm install
# 启动开发服务器
npm run dev
# 构建生产版本
npm run build
# 预览生产构建
npm run preview
```
## 技术栈
- React 19
- Vite
- Axios
- CSS3 (渐变、动画、响应式)
## 代理配置
开发环境下,所有 `/api` 请求会被代理到 `http://localhost:5000`(后端服务)
## 浏览器支持
支持所有现代浏览器的最新版本。

View File

@@ -0,0 +1,29 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
rules: {
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
])

View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>萌芽短链</title>
<meta name="description" content="萌芽短链接,简单、快速的短链接生成工具" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

3204
mengyalinkfly-frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
{
"name": "mengyalinkfly-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.6.2",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^6.30.2"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^5.1.0",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"vite": "^7.2.2"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 642 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

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

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,495 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.app {
min-height: 100vh;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.app::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
backdrop-filter: blur(3.5px);
-webkit-backdrop-filter: blur(3.5px);
z-index: 0;
}
.container {
width: 100%;
max-width: 500px;
background: rgba(255, 255, 255, 0.95);
border-radius: 24px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
padding: 40px 30px;
backdrop-filter: blur(10px);
position: relative;
z-index: 1;
}
/* 头部 */
.header {
text-align: center;
margin-bottom: 30px;
}
.logo {
width: 80px;
height: 80px;
margin-bottom: 10px;
border-radius: 20px;
animation: float 3s ease-in-out infinite;
object-fit: cover;
}
@keyframes float {
0%, 100% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
}
.title {
font-size: 32px;
color: #2d6a4f;
margin-bottom: 8px;
font-weight: 700;
}
.subtitle {
font-size: 14px;
color: #52b788;
font-weight: 400;
}
/* 消息提示 */
.message {
padding: 12px 20px;
border-radius: 12px;
margin-bottom: 20px;
font-size: 14px;
text-align: center;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message-success {
background: #d8f3dc;
color: #1b4332;
border: 1px solid #95d5b2;
}
.message-error {
background: #ffe5e5;
color: #c92a2a;
border: 1px solid #ffc9c9;
}
.message-info {
background: #e7f5ff;
color: #1864ab;
border: 1px solid #a5d8ff;
}
/* 表单 */
.form {
display: flex;
flex-direction: column;
gap: 20px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 10px;
}
.label {
font-size: 15px;
font-weight: 900;
color: #000000;
display: flex;
align-items: center;
gap: 8px;
}
.label-hint {
font-size: 12px;
font-weight: 400;
color: #74c69d;
}
.input-group {
display: flex;
align-items: center;
background: #f8f9fa;
border: 2px solid #d8f3dc;
border-radius: 12px;
overflow: hidden;
transition: all 0.3s ease;
}
.input-group:focus-within {
border-color: #52b788;
box-shadow: 0 0 0 3px rgba(82, 183, 136, 0.1);
}
.input-prefix {
padding: 0 12px;
font-size: 14px;
color: #74c69d;
font-weight: 500;
white-space: nowrap;
}
.input {
width: 100%;
padding: 14px 16px;
font-size: 15px;
border: 2px solid #d8f3dc;
border-radius: 12px;
background: #f8f9fa;
color: #2d6a4f;
transition: all 0.3s ease;
font-family: inherit;
}
.code-input {
border: none;
background: transparent;
padding: 14px 12px;
font-weight: 600;
letter-spacing: 2px;
text-transform: uppercase;
}
.input:focus {
outline: none;
border-color: #52b788;
background: #fff;
box-shadow: 0 0 0 3px rgba(82, 183, 136, 0.1);
}
.input::placeholder {
color: #95d5b2;
}
.select {
cursor: pointer;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2352b788' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 16px center;
padding-right: 40px;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
}
.select::-ms-expand {
display: none;
}
/* 按钮 */
.btn {
padding: 14px 24px;
font-size: 16px;
font-weight: 600;
border: none;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
font-family: inherit;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-primary {
background: linear-gradient(135deg, #40916c 0%, #52b788 100%);
color: white;
box-shadow: 0 4px 15px rgba(64, 145, 108, 0.3);
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(64, 145, 108, 0.4);
}
.btn-primary:active:not(:disabled) {
transform: translateY(0);
}
.btn-secondary {
background: #fff;
color: #52b788;
border: 2px solid #d8f3dc;
}
.btn-secondary:hover:not(:disabled) {
background: #f1faee;
border-color: #95d5b2;
}
.btn-copy {
background: #e9f5f0;
color: #2d6a4f;
border: 2px solid #b7e4c7;
padding: 10px 20px;
font-size: 14px;
}
.btn-copy:hover:not(:disabled) {
background: #d8f3dc;
}
/* 结果页面 */
.result {
text-align: center;
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
.success-icon {
font-size: 64px;
margin-bottom: 20px;
animation: bounce 0.6s ease-out;
}
@keyframes bounce {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.2);
}
}
.result-title {
font-size: 24px;
color: #2d6a4f;
margin-bottom: 24px;
font-weight: 700;
}
.result-box {
background: linear-gradient(135deg, #d8f3dc 0%, #e9f5f0 100%);
border: 2px solid #95d5b2;
border-radius: 16px;
padding: 24px;
margin-bottom: 24px;
display: flex;
flex-direction: column;
gap: 16px;
}
.result-url {
font-size: 18px;
font-weight: 600;
color: #1b4332;
word-break: break-all;
padding: 12px;
background: rgba(255, 255, 255, 0.7);
border-radius: 10px;
}
.result-info {
background: #f8f9fa;
border-radius: 12px;
padding: 16px;
margin-bottom: 20px;
text-align: left;
}
.result-info p {
font-size: 14px;
color: #2d6a4f;
margin: 8px 0;
word-break: break-all;
}
.result-info strong {
color: #1b4332;
}
/* 页脚 */
.footer {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #d8f3dc;
text-align: center;
}
.footer p {
font-size: 13px;
color: #74c69d;
}
/* 加载页面 */
.loading-page {
text-align: center;
padding: 60px 20px;
}
.loading-icon {
font-size: 80px;
margin-bottom: 20px;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
}
.loading-title {
font-size: 24px;
color: #2d6a4f;
font-weight: 600;
}
/* 错误页面 */
.error-page {
text-align: center;
padding: 60px 20px;
}
.error-icon {
font-size: 80px;
margin-bottom: 20px;
}
.error-title {
font-size: 24px;
color: #c92a2a;
font-weight: 600;
margin-bottom: 12px;
}
.error-text {
font-size: 16px;
color: #74c69d;
}
/* 移动端适配 */
@media (max-width: 600px) {
.app {
padding: 10px;
}
.container {
padding: 30px 20px;
border-radius: 20px;
}
.logo {
width: 70px;
height: 70px;
border-radius: 16px;
}
.title {
font-size: 28px;
}
.subtitle {
font-size: 13px;
}
.input-prefix {
font-size: 12px;
padding: 0 8px;
}
.input {
font-size: 14px;
padding: 12px 14px;
}
.btn {
font-size: 15px;
padding: 12px 20px;
}
.result-url {
font-size: 16px;
}
.result-title {
font-size: 22px;
}
.success-icon {
font-size: 56px;
}
}
@media (max-width: 400px) {
.container {
padding: 24px 16px;
}
.input-prefix {
font-size: 11px;
}
.result-url {
font-size: 14px;
}
}

View File

@@ -0,0 +1,354 @@
import { useState, useEffect } from 'react';
import { Routes, Route, useParams, useNavigate } from 'react-router-dom';
import axios from 'axios';
import './App.css';
// 短链接跳转组件
function RedirectPage() {
const { code } = useParams();
const navigate = useNavigate();
const [error, setError] = useState('');
useEffect(() => {
const redirect = async () => {
try {
// 调用后端API获取目标URL
const response = await axios.get(`/api/redirect/${code}`);
if (response.data.success) {
// 直接跳转到目标URL使用replace替换当前历史记录
window.location.replace(response.data.target_url);
}
} catch (err) {
setError(err.response?.data?.message || '短链接不存在');
// 3秒后返回首页
setTimeout(() => {
navigate('/');
}, 3000);
}
};
if (code && code.length === 4) {
redirect();
} else {
navigate('/');
}
}, [code, navigate]);
if (error) {
return (
<div className="app">
<div className="container">
<div className="error-page">
<div className="error-icon"></div>
<h2 className="error-title">{error}</h2>
<p className="error-text">3秒后自动返回首页...</p>
</div>
</div>
</div>
);
}
// 跳转中不显示任何内容
return null;
}
// 主页面组件
function HomePage() {
const [code, setCode] = useState('');
const [targetUrl, setTargetUrl] = useState('');
const [message, setMessage] = useState('');
const [messageType, setMessageType] = useState(''); // success, error, info
const [shortUrl, setShortUrl] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [expireOption, setExpireOption] = useState('never');
const [backgroundImage, setBackgroundImage] = useState('');
// 背景图片列表
const backgroundImages = [
'/background/image1.png',
'/background/image2.png',
'/background/image3.png',
'/background/image4.png',
'/background/image5.png',
'/background/image6.png',
'/background/image7.png',
'/background/image8.png',
];
// 随机选择背景图片(确保与上一张不同)
useEffect(() => {
const lastBackground = sessionStorage.getItem('lastBackground');
let randomIndex;
let selectedImage;
do {
randomIndex = Math.floor(Math.random() * backgroundImages.length);
selectedImage = backgroundImages[randomIndex];
} while (selectedImage === lastBackground && backgroundImages.length > 1);
setBackgroundImage(selectedImage);
sessionStorage.setItem('lastBackground', selectedImage);
}, []);
// 过期时间选项
const expireOptions = [
{ value: '10min', label: '10分钟' },
{ value: '1hour', label: '1小时' },
{ value: '1day', label: '1天' },
{ value: '3days', label: '3天' },
{ value: '1month', label: '1个月' },
{ value: '1year', label: '1年' },
{ value: 'never', label: '永久' }
];
// 显示消息
const showMessage = (text, type = 'info') => {
setMessage(text);
setMessageType(type);
setTimeout(() => {
setMessage('');
setMessageType('');
}, 5000);
};
// 检查链接是否可用
const checkCode = async (codeToCheck) => {
if (codeToCheck.length !== 4) return;
try {
const response = await axios.get(`/api/check/${codeToCheck}`);
if (response.data.exists) {
showMessage('该链接代码已被使用', 'error');
}
} catch (error) {
console.error('检查失败:', error);
}
};
// 生成随机链接
const generateRandomCode = async () => {
setIsLoading(true);
try {
const response = await axios.get('/api/generate');
if (response.data.success) {
setCode(response.data.code);
} else {
showMessage('生成失败,请重试', 'error');
}
} catch (error) {
showMessage('生成失败,请重试', 'error');
} finally {
setIsLoading(false);
}
};
// 创建短链接
const createShortLink = async (e) => {
e.preventDefault();
if (!code || code.length !== 4) {
showMessage('请输入4位链接代码', 'error');
return;
}
if (!targetUrl) {
showMessage('请输入目标地址', 'error');
return;
}
setIsLoading(true);
try {
const response = await axios.post('/api/create', {
code: code,
target_url: targetUrl,
expire_option: expireOption
});
if (response.data.success) {
setShortUrl(`${window.location.origin}/${code}`);
}
} catch (error) {
if (error.response?.status === 409) {
showMessage('该链接代码已被使用,请选择其他代码或使用随机生成', 'error');
} else {
showMessage(error.response?.data?.message || '创建失败,请重试', 'error');
}
} finally {
setIsLoading(false);
}
};
// 重置表单
const resetForm = () => {
setCode('');
setTargetUrl('');
setShortUrl('');
setMessage('');
setMessageType('');
setExpireOption('never');
};
// 复制短链接
const copyToClipboard = () => {
// 兼容性更好的复制方法
if (navigator.clipboard && window.isSecureContext) {
// 使用现代 Clipboard API
navigator.clipboard.writeText(shortUrl)
.then(() => {
})
.catch(() => {
// 降级方案
fallbackCopy();
});
} else {
// 使用传统方法
fallbackCopy();
}
};
// 降级复制方案
const fallbackCopy = () => {
try {
const textArea = document.createElement('textarea');
textArea.value = shortUrl;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
const successful = document.execCommand('copy');
document.body.removeChild(textArea);
if (successful) {
showMessage('链接已复制到剪贴板', 'success');
} else {
showMessage('复制失败,请手动复制', 'error');
}
} catch (err) {
showMessage('复制失败,请手动复制', 'error');
}
};
return (
<div className="app" style={{ backgroundImage: `url(${backgroundImage})` }}>
<div className="container">
<header className="header">
<img src="/logo.png" alt="萌芽短链" className="logo" />
<h1 className="title">萌芽短链</h1>
<p className="subtitle">简单快速的短链接生成工具</p>
</header>
{message && (
<div className={`message message-${messageType}`}>
{message}
</div>
)}
{!shortUrl ? (
<form onSubmit={createShortLink} className="form">
<div className="form-group">
<label className="label">
短链接代码
<span className="label-hint">4位字母或数字区分大小写</span>
</label>
<div className="input-group">
<span className="input-prefix">{window.location.host}/</span>
<input
type="text"
className="input code-input"
value={code}
onChange={(e) => {
const value = e.target.value.slice(0, 4);
setCode(value);
}}
onBlur={() => code.length === 4 && checkCode(code)}
placeholder="Aaa3"
maxLength={4}
required
/>
</div>
<button
type="button"
className="btn btn-secondary"
onClick={generateRandomCode}
disabled={isLoading}
>
🎲 随机生成
</button>
</div>
<div className="form-group">
<label className="label">目标地址</label>
<input
type="url"
className="input"
value={targetUrl}
onChange={(e) => setTargetUrl(e.target.value)}
placeholder="https://example.com"
required
/>
</div>
<div className="form-group">
<label className="label">过期时间</label>
<select
className="input select"
value={expireOption}
onChange={(e) => setExpireOption(e.target.value)}
>
{expireOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<button
type="submit"
className="btn btn-primary"
disabled={isLoading}
>
{isLoading ? '创建中...' : '创建短链接'}
</button>
</form>
) : (
<div className="result">
<div className="success-icon"></div>
<h2 className="result-title">短链接创建成功</h2>
<div className="result-box">
<div className="result-url">{shortUrl}</div>
<button className="btn btn-copy" onClick={copyToClipboard}>
📋 复制链接
</button>
</div>
<div className="result-info">
<p><strong>目标地址</strong>{targetUrl}</p>
</div>
<button className="btn btn-secondary" onClick={resetForm}>
创建新的短链接
</button>
</div>
)}
<footer className="footer">
<p>© 2025 萌芽短链 - 蜀ICP备2025151694号</p>
</footer>
</div>
</div>
);
}
// 主应用组件
function App() {
return (
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/:code" element={<RedirectPage />} />
</Routes>
);
}
export default App;

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,15 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@@ -0,0 +1,13 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>,
)

View File

@@ -0,0 +1,17 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
server: {
host: '0.0.0.0',
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true
}
}
}
})

27
start-backend.bat Normal file
View File

@@ -0,0 +1,27 @@
@echo off
chcp 65001 >nul
title 萌芽短链接 - 后端服务
echo.
echo ========================================
echo 萌芽短链接 - 后端服务启动中...
echo ========================================
echo.
cd mengyalinkfly-backend
echo [1/2] 检查依赖...
pip show Flask >nul 2>&1
if errorlevel 1 (
echo 正在安装依赖...
pip install -r requirements.txt
)
echo [2/2] 启动后端服务...
echo.
echo 后端服务地址: http://localhost:5000
echo.
python app.py
pause

26
start-frontend.bat Normal file
View File

@@ -0,0 +1,26 @@
@echo off
chcp 65001 >nul
title 萌芽短链接 - 前端服务
echo.
echo ========================================
echo 萌芽短链接 - 前端服务启动中...
echo ========================================
echo.
cd mengyalinkfly-frontend
echo [1/2] 检查依赖...
if not exist "node_modules" (
echo 正在安装依赖,请稍候...
call npm install
)
echo [2/2] 启动前端服务...
echo.
echo 前端服务地址: http://localhost:5173
echo.
call npm run dev
pause

20
启动.bat Normal file
View File

@@ -0,0 +1,20 @@
@echo off
chcp 65001 >nul
title 萌芽短链接 - 一键启动
echo.
echo ========================================
echo 🌱 萌芽短链接 - 一键启动
echo ========================================
echo.
echo 正在启动前后端服务...
echo.
start "萌芽短链接-后端" cmd /k "cd /d "%~dp0" && start-backend.bat"
timeout /t 2 /nobreak >nul
start "萌芽短链接-前端" cmd /k "cd /d "%~dp0" && start-frontend.bat"
echo 按任意键退出此窗口...
pause >nul