3 Commits

Author SHA1 Message Date
loveuer
909a016a44 docs: add comprehensive README
Some checks failed
/ build ushare (push) Has been cancelled
/ clean (push) Has been cancelled
- Add project introduction and features
- Add quick start guide (development and production)
- Add configuration documentation
- Add usage instructions
- Add mobile support section
- Add development guide and directory structure
2026-01-18 17:52:39 +08:00
loveuer
96fe642175 fix: correct pnpm setup in GitHub Actions
- Use pnpm/action-setup@v2 instead of npm install -g
- Add proper pnpm cache configuration
- Fix 'Unable to locate executable file: pnpm' error
2026-01-18 17:35:34 +08:00
loveuer
d38fa7a507 feat: optimize mobile responsive layout for share page
- Add responsive design for mobile devices (≤768px)
- Switch from horizontal to vertical layout on mobile
- Hide middle divider panel on mobile
- Optimize button sizes for mobile touch interaction
- Improve input field and button alignment on mobile
2026-01-18 17:12:07 +08:00
6 changed files with 311 additions and 28 deletions

View File

@@ -44,11 +44,25 @@ jobs:
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '20' node-version: '20'
cache: 'pnpm'
cache-dependency-path: frontend/pnpm-lock.yaml
- name: Install pnpm - name: Install pnpm
run: npm install -g pnpm uses: pnpm/action-setup@v2
with:
version: 8
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v3
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Print build info - name: Print build info
run: | run: |

168
README.md Normal file
View File

@@ -0,0 +1,168 @@
# UShare
一个简单易用的文件分享工具,支持大文件分片上传下载,以及局域网内 P2P 文件分享。
## 功能特性
- 📁 大文件分片上传下载
- 🌐 网页端文件分享
- 🏠 局域网 P2P 文件分享WebRTC
- 📱 响应式设计,支持移动端
- 🔐 简单的用户认证
- 🚀 单一二进制,开箱即用
- 🐳 Docker 支持
## 快速开始
### 开发模式
```bash
# 克隆仓库
git clone https://github.com/loveuer/ushare.git
cd ushare
# 启动开发服务器
./dev.sh
```
开发服务器会同时启动后端http://0.0.0.0:9119和前端http://0.0.0.0:5173前端支持热重载。
### 生产模式
#### 使用预编译二进制
从 [Releases](https://github.com/loveuer/ushare/releases) 下载对应平台的二进制文件。
```bash
# Linux amd64
wget https://github.com/loveuer/ushare/releases/download/v0.3.2/ushare-linux-amd64
chmod +x ushare-linux-amd64
# 运行
./ushare-linux-amd64 -data ./data
```
#### 从源码构建
```bash
# 构建单一二进制(包含嵌入的前端)
./make.sh
# 运行
./dist/ushare -data ./data
```
#### Docker 部署
```bash
docker build -t ushare:latest .
docker run -d -p 80:80 -v $(pwd)/data:/data ushare:latest
```
## 配置
### 命令行参数
```bash
ushare [options]
Options:
-debug 启用调试模式
-address string 监听地址 (default "0.0.0.0:9119")
-data string 数据目录 (default "/data")
-clean int 文件清理周期单位小时0 表示不自动清理 (default 24)
```
### 环境变量
| 环境变量 | 说明 | 默认值 |
|---------|------|--------|
| `USHARE_USERNAME` | 用户名 | `admin` |
| `USHARE_PASSWORD` | 密码 | `ushare@123` |
#### 示例
```bash
# 使用默认凭据admin / ushare@123
./ushare -data ./data
# 使用自定义凭据
export USHARE_USERNAME=myuser
export USHARE_PASSWORD=mypass
./ushare -data ./data
```
## 使用说明
### 文件分享
1. 访问 http://localhost:9119/share
2. 点击"选择文件"选择要上传的文件
3. 点击"上传文件",等待上传完成
4. 复制生成的下载码分享给他人
### 文件下载
1. 访问 http://localhost:9119/share
2. 在"获取文件"区域输入下载码
3. 点击"获取文件"下载文件
### 局域网 P2P 分享
1. 访问 http://localhost:9119
2. 自动注册到局域网设备列表
3. 选择要分享的设备,建立 P2P 连接
4. 开始文件传输
## 移动端支持
- 响应式设计,自动适配手机、平板等设备
- 移动端≤768px采用垂直布局优化触摸操作
- 按钮尺寸优化,适合手指点击
## 开发
### 目录结构
```
ushare/
├── internal/ # 后端代码
│ ├── api/ # HTTP API 路由
│ ├── controller/ # 业务逻辑
│ ├── handler/ # 请求处理器
│ ├── model/ # 数据模型
│ ├── opt/ # 配置管理
│ └── pkg/ # 工具包
├── frontend/ # 前端代码
│ └── src/
│ ├── api/ # API 调用
│ ├── component/ # UI 组件
│ ├── hook/ # 自定义 Hooks
│ ├── page/ # 页面组件
│ └── store/ # 状态管理
├── .github/workflows/ # GitHub Actions
├── dist/ # 构建输出(.gitignore
└── data/ # 数据目录(.gitignore
```
### 构建
```bash
# 开发构建(前后端分离)
./dev.sh
# 生产构建(单一二进制)
./make.sh
```
### 代码风格
请参考 [AGENTS.md](./AGENTS.md) 了解代码风格指南。
## License
MIT License
## 贡献
欢迎提交 Issue 和 Pull Request

View File

@@ -11,6 +11,12 @@ const useUploadStyle = createUseStyles({
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
minHeight: "50vh",
"@media (max-width: 768px)": {
minHeight: "auto",
padding: "20px 10px",
},
}, },
form: { form: {
backgroundColor: "#C8E6C9", backgroundColor: "#C8E6C9",
@@ -19,10 +25,20 @@ const useUploadStyle = createUseStyles({
borderRadius: "15px", borderRadius: "15px",
width: "70%", width: "70%",
margin: "20px 60px 20px 0", margin: "20px 60px 20px 0",
/*todo margin 不用 px*/
"@media (max-width: 768px)": {
width: "90%",
margin: "20px 0",
padding: "20px",
},
}, },
title: { title: {
color: "#2c9678" color: "#2c9678",
"@media (max-width: 768px)": {
fontSize: "1.5rem",
marginBottom: "15px",
},
}, },
file: { file: {
display: 'none', display: 'none',
@@ -33,7 +49,11 @@ const useUploadStyle = createUseStyles({
}, },
name: { name: {
color: "#2c9678", color: "#2c9678",
marginLeft: '10px' marginLeft: '10px',
"@media (max-width: 768px)": {
fontSize: "14px",
},
}, },
clean: { clean: {
borderRadius: '50%', borderRadius: '50%',
@@ -48,14 +68,12 @@ const useShowStyle = createUseStyles({
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
position: "relative", // 为关闭按钮提供定位基准 minHeight: "50vh",
},
title: { "@media (max-width: 768px)": {
color: "#2c9678", minHeight: "auto",
marginTop: 0, padding: "20px 10px",
display: "flex", },
justifyContent: "space-between",
alignItems: "center",
}, },
form: { form: {
backgroundColor: "#C8E6C9", backgroundColor: "#C8E6C9",
@@ -65,6 +83,25 @@ const useShowStyle = createUseStyles({
width: "70%", width: "70%",
margin: "20px 60px 20px 0", margin: "20px 60px 20px 0",
position: "relative", position: "relative",
"@media (max-width: 768px)": {
width: "90%",
margin: "20px 0",
padding: "20px",
},
},
title: {
color: "#2c9678",
marginTop: 0,
marginBottom: "25px",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
"@media (max-width: 768px)": {
fontSize: "1.5rem",
marginBottom: "15px",
},
}, },
closeButton: { closeButton: {
position: "absolute", position: "absolute",
@@ -78,9 +115,7 @@ const useShowStyle = createUseStyles({
height: "24px", height: "24px",
cursor: "pointer", cursor: "pointer",
"&:hover": { "&:hover": {
// background: "#cc0000",
boxShadow: "20px 20px 60px #fff, -20px -20px 60px #fff", boxShadow: "20px 20px 60px #fff, -20px -20px 60px #fff",
// boxShadow: "20px 20px 60px #eee",
}, },
}, },
codeWrapper: { codeWrapper: {
@@ -89,6 +124,10 @@ const useShowStyle = createUseStyles({
borderRadius: "8px", borderRadius: "8px",
margin: "15px 0", margin: "15px 0",
overflowX: "auto", overflowX: "auto",
"@media (max-width: 768px)": {
padding: "0 10px",
},
}, },
pre: { pre: {
display: 'flex', display: 'flex',
@@ -98,6 +137,12 @@ const useShowStyle = createUseStyles({
height: '24px', height: '24px',
"& > code": { "& > code": {
marginLeft: "0", marginLeft: "0",
fontSize: "14px",
wordBreak: "break-all",
"@media (max-width: 768px)": {
fontSize: "12px",
},
} }
}, },
copyButton: { copyButton: {
@@ -112,6 +157,11 @@ const useShowStyle = createUseStyles({
"&:hover": { "&:hover": {
background: "#1f6d5a", background: "#1f6d5a",
}, },
"@media (max-width: 768px)": {
padding: "6px 12px",
fontSize: "12px",
},
}, },
}); });

View File

@@ -5,6 +5,10 @@ const useStyle = createUseStyles({
backgroundColor: 'lightgray', backgroundColor: 'lightgray',
position: "relative", position: "relative",
overflow: "hidden", overflow: "hidden",
"@media (max-width: 768px)": {
display: "none",
},
}, },
left: { left: {
backgroundColor: "#e3f2fd", backgroundColor: "#e3f2fd",

View File

@@ -1,6 +1,6 @@
import {createUseStyles} from "react-jss"; import { createUseStyles } from "react-jss";
import {UButton} from "../../../component/button/u-button.tsx"; import { UButton } from "../../../component/button/u-button.tsx";
import {useStore} from "../../../store/share.ts"; import { useStore } from "../../../store/share.ts";
const useStyle = createUseStyles({ const useStyle = createUseStyles({
container: { container: {
@@ -8,6 +8,12 @@ const useStyle = createUseStyles({
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
minHeight: "50vh",
"@media (max-width: 768px)": {
minHeight: "auto",
padding: "20px 10px",
},
}, },
form: { form: {
backgroundColor: "#BBDEFB", backgroundColor: "#BBDEFB",
@@ -16,25 +22,58 @@ const useStyle = createUseStyles({
borderRadius: "15px", borderRadius: "15px",
width: "70%", width: "70%",
margin: "20px 0 20px 60px", margin: "20px 0 20px 60px",
/*todo margin 不用 px*/
"@media (max-width: 768px)": {
width: "90%",
margin: "20px 0",
padding: "20px",
},
}, },
title: { title: {
color: '#1661ab', // 靛青 color: '#1661ab',
"@media (max-width: 768px)": {
fontSize: "1.5rem",
marginBottom: "15px",
},
}, },
code: { code: {
padding: '11px', padding: '11px',
margin: '20px 0', margin: '0',
width: '200px', width: '200px',
border: '2px solid #ddd', border: '2px solid #ddd',
borderRadius: '5px', borderRadius: '5px',
'&:active': { '&:active': {
border: '2px solid #1661ab', border: '2px solid #1661ab',
} },
}
"@media (max-width: 768px)": {
width: '100%',
boxSizing: 'border-box',
padding: '10px',
fontSize: '14px',
},
},
inputContainer: {
display: 'flex',
gap: '10px',
"@media (max-width: 768px)": {
flexDirection: 'column',
alignItems: 'stretch',
},
},
buttonContainer: {
display: 'flex',
"@media (max-width: 768px)": {
justifyContent: 'left',
},
},
}) })
export const PanelRight = () => { export const PanelRight = () => {
const style = useStyle() const style = useStyle()
const {code, setCode} = useStore() const { code, setCode } = useStore()
function onCodeChange(e: React.ChangeEvent<HTMLInputElement>) { function onCodeChange(e: React.ChangeEvent<HTMLInputElement>) {
setCode(e.currentTarget.value) setCode(e.currentTarget.value)
@@ -52,7 +91,7 @@ export const PanelRight = () => {
return <div className={style.container}> return <div className={style.container}>
<div className={style.form}> <div className={style.form}>
<h2 className={style.title}></h2> <h2 className={style.title}></h2>
<div> <div className={style.inputContainer}>
<input <input
type="text" type="text"
className={style.code} className={style.code}
@@ -60,7 +99,9 @@ export const PanelRight = () => {
value={code} value={code}
onChange={onCodeChange} onChange={onCodeChange}
/> />
<UButton style={{marginLeft: '10px'}} onClick={onFetchFile}></UButton> <div className={style.buttonContainer}>
<UButton onClick={onFetchFile}></UButton>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -13,6 +13,12 @@ const useStyle = createUseStyles({
height: "100vh", height: "100vh",
display: "grid", display: "grid",
gridTemplateColumns: "40% 20% 40%", gridTemplateColumns: "40% 20% 40%",
"@media (max-width: 768px)": {
gridTemplateColumns: "100%",
gridTemplateRows: "auto auto",
overflowY: "auto",
},
}, },
}) })
@@ -20,7 +26,7 @@ export const FileSharing = () => {
const style = useStyle() const style = useStyle()
return <div className={style.container}> return <div className={style.container}>
<PanelLeft /> <PanelLeft />
<PanelMid/> <PanelMid />
<PanelRight/> <PanelRight/>
</div> </div>
}; };