Browse Source

更新代码:修复预创建时间与自动下架时间冲突,调整定时任务间隔为1分钟

Boss
Default User 4 weeks ago
commit
8b0da19554
  1. 128
      .gitignore
  2. 294
      DEPLOYMENT.md
  3. 23
      Dockerfile
  4. 2621
      Management.html
  5. 2438
      Reject.html
  6. 2590
      Reject.js
  7. 3169
      SupplierReview.html
  8. 111
      deploy.sh
  9. 22
      docker-compose.yml
  10. 106
      health-check.sh
  11. 124
      image-processor.js
  12. 12
      index.html
  13. 14
      js/chart.js
  14. 265
      login.html
  15. 8
      oss-config.js
  16. 323
      oss-uploader.js
  17. 5370
      package-lock.json
  18. 19
      package.json
  19. 9436
      supply.html
  20. 410
      tatus
  21. BIN
      watermark-test-output.png

128
.gitignore

@ -0,0 +1,128 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Build outputs
dist/
build/
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
Thumbs.db
.DS_Store
# Log files
logs
*.log
# Temporary files
tmp/
temp/
# Database files
*.sqlite
*.sqlite3
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# nyc test coverage
.nyc_output
# Dependency directories
jspm_packages/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# Test scripts
*.test.js
test-*.js
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

294
DEPLOYMENT.md

@ -0,0 +1,294 @@
# 应用部署文档
## 1. 环境要求
### 服务器要求
- 操作系统:Linux(推荐 Ubuntu 18.04+ 或 CentOS 7+)
- 内存:至少 2GB RAM
- 存储空间:至少 10GB 可用空间
- 网络:可访问 Gitea 仓库(http://8.137.125.67:4000)和互联网
### 软件依赖
- Docker:用于容器化部署
- Git:用于代码拉取
## 2. 部署前准备
### 2.1 安装 Docker
#### Ubuntu/Debian
```bash
# 更新系统包
apt-get update
# 安装 Docker
curl -fsSL https://get.docker.com | sh
# 启动 Docker 服务
systemctl start docker
# 设置 Docker 开机自启
systemctl enable docker
```
#### CentOS/RHEL
```bash
# 更新系统包
yum update
# 安装 Docker
yum install -y docker
# 启动 Docker 服务
systemctl start docker
# 设置 Docker 开机自启
systemctl enable docker
```
### 2.2 配置防火墙和安全组
#### 2.2.1 系统防火墙设置
确保服务器开放以下端口:
- 3000:应用访问端口
- 4000:Gitea 仓库访问端口(仅在需要访问仓库时)
##### Ubuntu/Debian (ufw)
```bash
# 允许 3000 端口
ufw allow 3000/tcp
# 允许 4000 端口(如果需要)
ufw allow 4000/tcp
# 启用防火墙
ufw enable
# 查看防火墙规则
ufw status verbose
```
##### CentOS/RHEL (firewalld)
```bash
# 允许 3000 端口
firewall-cmd --zone=public --add-port=3000/tcp --permanent
# 允许 4000 端口(如果需要)
firewall-cmd --zone=public --add-port=4000/tcp --permanent
# 重新加载防火墙
firewall-cmd --reload
# 查看防火墙规则
firewall-cmd --list-ports --zone=public
```
#### 2.2.2 云服务商安全组设置
如果您使用的是云服务器(如阿里云、腾讯云、华为云等),还需要在云服务商控制台配置安全组规则:
1. 登录云服务器控制台
2. 找到对应服务器的安全组配置
3. 添加入站规则:
- 端口范围:3000/3000
- 协议:TCP
- 授权对象:0.0.0.0/0(允许所有IP访问,或根据需要设置特定IP)
- 描述:应用访问端口
### 2.3 配置 Git 凭证(可选)
如果 Gitea 仓库需要认证,可以配置 Git 凭证缓存:
```bash
# 设置凭证缓存时间(1小时)
git config --global credential.helper cache
# 设置凭证永久保存
git config --global credential.helper store
# 首次拉取时会要求输入用户名和密码,之后会自动保存
```
## 3. 部署步骤
### 3.1 下载部署脚本
`deploy.sh` 脚本上传到服务器的任意目录,例如 `/root` 目录。
### 3.2 设置执行权限
```bash
chmod +x deploy.sh
```
### 3.3 运行部署脚本
```bash
./deploy.sh
```
### 3.4 部署过程说明
脚本将执行以下步骤:
1. **检查应用目录**:如果不存在则克隆仓库,存在则拉取最新代码
2. **切换分支**:确保使用正确的 `Ly` 分支
3. **拉取代码**:从 Gitea 仓库拉取最新代码
4. **停止旧容器**:停止并删除旧的 Docker 容器
5. **检查端口**:确保 3000 端口未被占用
6. **构建镜像**:重新构建 Docker 镜像
7. **启动容器**:启动新的 Docker 容器
8. **清理镜像**:清理无用的 Docker 镜像
## 4. 验证部署
### 4.1 检查容器状态
```bash
docker ps | grep reject-app
```
正常输出示例:
```
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
abcdef123456 reject-app "node Reject.js" 1 minute ago Up 1 minute 0.0.0.0:3000->3000/tcp reject-app
```
### 4.2 检查端口映射
```bash
docker port reject-app
```
正常输出示例:
```
3000/tcp -> 0.0.0.0:3000
```
### 4.3 访问应用
在浏览器中访问:http://8.137.125.67:3000
## 5. 常见问题解决
### 5.1 拒绝连接错误
**错误现象**:访问 http://8.137.125.67:3000 时显示"拒绝连接"
**可能原因及解决方法**:
1. **防火墙未开放端口**
- 检查防火墙规则是否允许 3000 端口
- 按照 2.2 节重新配置防火墙
2. **容器未正常启动**
- 检查容器状态:`docker ps -a | grep reject-app`
- 查看容器日志:`docker logs reject-app`
3. **端口被占用**
- 检查 3000 端口占用情况:`lsof -i :3000` 或 `netstat -tulpn | grep :3000`
- 停止占用端口的进程或容器
### 5.2 数据库连接错误
**错误现象**:应用启动后显示数据库连接失败
**可能原因及解决方法**:
1. **数据库配置错误**
- 检查 `Reject.js` 中的数据库连接配置
- 确保数据库地址、用户名、密码正确
2. **数据库服务器未运行**
- 检查数据库服务器状态
- 确保数据库服务器允许远程连接
### 5.3 代码拉取失败
**错误现象**:脚本执行时显示"代码拉取失败"
**可能原因及解决方法**:
1. **Gitea 仓库不可访问**
- 检查 Gitea 服务器状态:`curl -I http://8.137.125.67:4000`
- 确保服务器可以访问 Gitea 仓库
2. **分支名称错误**
- 检查 `deploy.sh` 中的 `BRANCH` 变量是否正确
- 确保仓库中存在指定的分支
## 6. 应用维护
### 6.1 查看应用日志
```bash
docker logs reject-app
# 实时查看日志
docker logs -f reject-app
```
### 6.2 重启应用
```bash
docker restart reject-app
```
### 6.3 更新应用
再次运行部署脚本即可更新应用:
```bash
./deploy.sh
```
### 6.4 停止应用
```bash
docker stop reject-app
```
### 6.5 删除应用
```bash
# 停止并删除容器
docker stop reject-app
docker rm reject-app
# 删除镜像
docker rmi reject-app
# 删除应用目录
rm -rf /app
```
## 7. 脚本说明
### 7.1 配置参数
脚本中的主要配置参数:
- `REPO_URL`:Gitea 仓库地址
- `APP_DIR`:应用部署目录
- `BRANCH`:使用的代码分支
### 7.2 脚本功能
- **自动代码拉取**:从 Gitea 仓库拉取最新代码
- **容器管理**:自动停止旧容器并启动新容器
- **端口管理**:自动检查并清理占用 3000 端口的进程
- **错误检查**:提供详细的错误信息和处理建议
- **日志记录**:记录部署过程中的关键步骤
## 8. 注意事项
1. **第一次部署**:脚本会自动克隆仓库并启动应用
2. **后续部署**:脚本会自动拉取最新代码并重启应用
3. **分支切换**:如需切换分支,请修改 `deploy.sh` 中的 `BRANCH` 变量
4. **数据持久化**:应用数据通过 Docker 卷映射到 `/app` 目录,确保该目录安全
5. **定期备份**:建议定期备份 `/app` 目录和数据库
## 9. 联系方式
如有部署问题,请联系:
- 技术支持:[技术支持邮箱]
- 管理员:[管理员联系方式]

23
Dockerfile

@ -0,0 +1,23 @@
# 使用Node.js作为基础镜像
FROM node:18-alpine
# 设置工作目录
WORKDIR /app
# 设置npm镜像源为国内源加速依赖安装
RUN npm config set registry https://registry.npmmirror.com
# 复制package.json和package-lock.json
COPY package*.json ./
# 安装项目依赖
RUN npm install
# 复制应用代码
COPY . .
# 暴露端口
EXPOSE 3000
# 直接使用node启动应用
CMD ["node", "Reject.js"]

2621
Management.html

File diff suppressed because it is too large

2438
Reject.html

File diff suppressed because it is too large

2590
Reject.js

File diff suppressed because it is too large

3169
SupplierReview.html

File diff suppressed because it is too large

111
deploy.sh

@ -0,0 +1,111 @@
#!/bin/bash
# 部署脚本:当Gitea仓库有更新时,一键部署新代码到云服务器
# 配置参数
GITEA_USER="SwtTt29"
GITEA_PASSWORD="qazswt123"
REPO_URL="http://${GITEA_USER}:${GITEA_PASSWORD}@8.137.125.67:4000/SwtTt29/Review.git"
APP_DIR="/app"
DOCKER_COMPOSE_FILE="docker-compose.yml"
BRANCH="sh"
IMAGE_NAME="reject-app" # 改为与docker-compose.yml中一致的镜像名称
CONTAINER_NAME="reject-app"
# 输出日志函数
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
# 关闭错误立即停止,改用手动错误检查
source /etc/profile
log "开始部署应用..."
# 确保脚本具有执行权限
chmod +x "$0"
# 1. 检查应用目录是否存在
if [ ! -d "$APP_DIR" ]; then
log "应用目录不存在,开始克隆仓库..."
git clone "$REPO_URL" "$APP_DIR"
if [ $? -ne 0 ]; then
log "错误:克隆仓库失败!请检查网络连接和仓库地址。"
exit 1
fi
cd "$APP_DIR"
git checkout "$BRANCH"
if [ $? -ne 0 ]; then
log "错误:切换分支失败!请检查分支名称是否正确。"
exit 1
fi
else
log "应用目录已存在,开始拉取最新代码..."
cd "$APP_DIR"
# 处理分支冲突问题
if ! git pull origin "$BRANCH" --ff-only; then
log "快进合并失败,尝试先重置本地分支再拉取..."
git fetch origin
git reset --hard origin/"$BRANCH"
if [ $? -ne 0 ]; then
log "错误:重置本地分支失败!"
exit 1
fi
fi
fi
# 2. 检查docker-compose.yml文件是否存在
if [ ! -f "$DOCKER_COMPOSE_FILE" ]; then
log "错误:$DOCKER_COMPOSE_FILE 文件不存在!"
exit 1
fi
# 3. 停止并删除旧的Docker容器
log "停止并删除旧的Docker容器..."
docker-compose -f "$DOCKER_COMPOSE_FILE" down
if [ $? -ne 0 ]; then
log "警告:docker-compose down失败,尝试直接删除容器..."
fi
# 强制删除可能残留的容器
if docker ps -a | grep -q "$CONTAINER_NAME"; then
log "强制删除残留的容器 $CONTAINER_NAME..."
docker rm -f "$CONTAINER_NAME"
if [ $? -ne 0 ]; then
log "错误:强制删除容器失败!"
exit 1
fi
fi
# 4. 直接使用docker build命令构建镜像,绕过docker-compose的buildx依赖
log "重新构建Docker镜像..."
docker build -t "$IMAGE_NAME" .
if [ $? -ne 0 ]; then
log "错误:构建镜像失败!请检查Dockerfile和依赖。"
exit 1
fi
# 5. 启动新的Docker容器(不使用build参数)
log "启动新的Docker容器..."
# 使用--no-build参数避免docker-compose尝试重新构建
docker-compose -f "$DOCKER_COMPOSE_FILE" up -d --no-build
if [ $? -ne 0 ]; then
log "错误:启动容器失败!请检查配置文件。"
exit 1
fi
# 6. 验证容器是否正常启动
log "验证容器运行状态..."
sleep 10
if docker-compose -f "$DOCKER_COMPOSE_FILE" ps | grep -q "Up"; then
log "容器启动成功!"
else
log "警告:容器可能未正常启动,正在检查日志..."
docker-compose -f "$DOCKER_COMPOSE_FILE" logs | tail -50
fi
# 7. 清理无用的Docker镜像
log "清理无用的Docker镜像..."
docker image prune -f
log "应用部署完成!"

22
docker-compose.yml

@ -0,0 +1,22 @@
# 移除version声明以解决兼容性警告
services:
app:
build: .
image: reject-app
container_name: reject-app
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DB_HOST=1.95.162.61
- DB_USER=root
- DB_PASSWORD=schl@2025
- DB_NAME=wechat_app
- USER_LOGIN_DB_NAME=userlogin
restart: always
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"

106
health-check.sh

@ -0,0 +1,106 @@
#!/bin/bash
# 健康检查脚本:定期检查应用是否正常运行,如果不正常则自动重启
# 配置参数
APP_DIR="/app"
DOCKER_COMPOSE_FILE="docker-compose.yml"
CHECK_INTERVAL=300 # 检查间隔(秒)
MAX_RESTARTS=3 # 最大重启次数
RESTART_INTERVAL=3600 # 重启间隔(秒)
# 带颜色的日志函数
colored_log() {
local color=$1
local message=$2
local reset="\033[0m"
local colors=(
["red"]="\033[31m"
["green"]="\033[32m"
["yellow"]="\033[33m"
["blue"]="\033[34m"
["purple"]="\033[35m"
)
echo -e "${colors[$color]}[$(date '+%Y-%m-%d %H:%M:%S')] $message$reset"
}
log() {
colored_log "blue" "$1"
}
success() {
colored_log "green" "$1"
}
error() {
colored_log "red" "$1"
}
warning() {
colored_log "yellow" "$1"
}
log "启动健康检查服务..."
# 初始化重启计数器
restart_count=0
last_restart_time=$(date +%s)
while true; do
log "开始健康检查..."
cd "$APP_DIR"
# 检查容器是否在运行
if docker-compose -f "$DOCKER_COMPOSE_FILE" ps | grep -q "Up"; then
# 检查应用是否能正常响应
if curl -s -o /dev/null -w "%{http_code}" http://localhost:3000 > /dev/null; then
success "应用运行正常"
else
warning "应用容器在运行,但无法正常响应请求"
# 查看应用日志
log "查看应用日志:"
docker-compose -f "$DOCKER_COMPOSE_FILE" logs -n 10
# 检查重启次数
current_time=$(date +%s)
time_since_last_restart=$((current_time - last_restart_time))
if [ $restart_count -lt $MAX_RESTARTS ] || [ $time_since_last_restart -gt $RESTART_INTERVAL ]; then
log "尝试重启应用..."
if docker-compose -f "$DOCKER_COMPOSE_FILE" restart; then
success "应用已重启"
restart_count=$((restart_count + 1))
last_restart_time=$current_time
else
error "应用重启失败"
fi
else
error "已达到最大重启次数,在$RESTART_INTERVAL秒内不再尝试重启"
fi
fi
else
warning "应用容器未运行"
# 检查重启次数
current_time=$(date +%s)
time_since_last_restart=$((current_time - last_restart_time))
if [ $restart_count -lt $MAX_RESTARTS ] || [ $time_since_last_restart -gt $RESTART_INTERVAL ]; then
log "尝试启动应用..."
if docker-compose -f "$DOCKER_COMPOSE_FILE" up -d; then
success "应用已启动"
restart_count=$((restart_count + 1))
last_restart_time=$current_time
else
error "应用启动失败"
fi
else
error "已达到最大重启次数,在$RESTART_INTERVAL秒内不再尝试启动"
fi
fi
log "健康检查完成,等待$CHECK_INTERVAL秒后再次检查..."
sleep $CHECK_INTERVAL
done

124
image-processor.js

@ -0,0 +1,124 @@
const sharp = require('sharp');
class ImageProcessor {
/**
* 为图片添加文字水印
* @param {Buffer} imageBuffer - 原始图片的Buffer数据
* @param {String} text - 水印文字内容
* @param {Object} options - 水印配置选项
* @returns {Promise<Buffer>} - 添加水印后的图片Buffer
*/
static async addWatermark(imageBuffer, text = '又鸟蛋平台', options = {}) {
try {
console.log('【图片处理】开始添加水印');
// 设置默认配置
const defaultOptions = {
fontSize: 20, // 字体大小 - 减小以确保完整显示
color: 'rgba(0,0,0,0.5)', // 文字颜色(加深以便更清晰)
position: 'bottom-right', // 水印位置
marginX: -50, // X轴边距 - 调整使水印居中在红色框中
marginY: 10 // Y轴边距 - 减小使水印靠下,放入红色框中
};
// 强制使用'bottom-right'位置
options.position = 'bottom-right';
const config = { ...defaultOptions, ...options };
// 使用sharp处理图片
const image = sharp(imageBuffer);
// 获取图片信息以确定水印位置
const metadata = await image.metadata();
const width = metadata.width || 800;
const height = metadata.height || 600;
// 确定水印位置
let x = config.marginX;
let y = config.marginY;
if (config.position === 'bottom-right') {
// 右下角位置,需要计算文字宽度(这里简化处理,实际应该根据字体计算)
// 这里使用一个简单的估算:每个字符约占字体大小的0.6倍宽度
const estimatedTextWidth = text.length * config.fontSize * 0.6;
x = width - estimatedTextWidth - config.marginX;
y = height - config.fontSize - config.marginY;
} else if (config.position === 'center') {
x = (width / 2) - (text.length * config.fontSize * 0.3);
y = height / 2;
} else if (config.position === 'top-left') {
// 左上角,使用默认的margin值
}
// 确保位置不会超出图片边界
x = Math.max(0, Math.min(x, width - 1));
y = Math.max(config.fontSize, Math.min(y, height - 1));
// 添加文字水印
const watermarkedBuffer = await image
.composite([{
input: Buffer.from(`<svg width="${width}" height="${height}">
<text x="${x}" y="${y}" font-family="Arial" font-size="${config.fontSize}" fill="${config.color}">${text}</text>
</svg>`),
gravity: 'southeast'
}])
.toBuffer();
console.log('【图片处理】水印添加成功');
return watermarkedBuffer;
} catch (error) {
console.error('【图片处理】添加水印失败:', error.message);
console.error('【图片处理】错误详情:', error);
// 如果水印添加失败,返回原始图片
return imageBuffer;
}
}
/**
* 批量为图片添加水印
* @param {Array<Buffer>} imageBuffers - 图片Buffer数组
* @param {String} text - 水印文字内容
* @param {Object} options - 水印配置选项
* @returns {Promise<Array<Buffer>>} - 添加水印后的图片Buffer数组
*/
static async addWatermarkToMultiple(imageBuffers, text = '又鸟蛋平台', options = {}) {
try {
console.log(`【图片处理】开始批量添加水印,共${imageBuffers.length}张图片`);
const watermarkedPromises = imageBuffers.map(buffer =>
this.addWatermark(buffer, text, options)
);
const results = await Promise.all(watermarkedPromises);
console.log('【图片处理】批量水印添加完成');
return results;
} catch (error) {
console.error('【图片处理】批量添加水印失败:', error.message);
throw error;
}
}
/**
* 为Base64编码的图片添加水印
* @param {String} base64Image - Base64编码的图片
* @param {String} text - 水印文字内容
* @param {Object} options - 水印配置选项
* @returns {Promise<Buffer>} - 添加水印后的图片Buffer
*/
static async addWatermarkToBase64(base64Image, text = '又鸟蛋平台', options = {}) {
try {
// 移除Base64前缀
const base64Data = base64Image.replace(/^data:image\/(png|jpeg|jpg|gif);base64,/, '');
// 转换为Buffer
const buffer = Buffer.from(base64Data, 'base64');
// 添加水印
return await this.addWatermark(buffer, text, options);
} catch (error) {
console.error('【图片处理】为Base64图片添加水印失败:', error.message);
throw error;
}
}
}
module.exports = ImageProcessor;

12
index.html

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>审核系统</title>
<meta http-equiv="refresh" content="0;url=SupplierReview.html">
</head>
<body>
<p>正在跳转到审核系统...</p>
</body>
</html>

14
js/chart.js

File diff suppressed because one or more lines are too long

265
login.html

@ -0,0 +1,265 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录页面</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f5f5f5;
color: #333;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.login-container {
background-color: white;
width: 90%;
max-width: 400px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
padding: 40px;
}
.login-title {
text-align: center;
font-size: 24px;
font-weight: 600;
margin-bottom: 30px;
color: #333;
}
.form-group {
margin-bottom: 24px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
color: #666;
}
.form-input {
width: 100%;
padding: 12px 16px;
border: 1px solid #d9d9d9;
border-radius: 8px;
font-size: 14px;
box-sizing: border-box;
transition: all 0.3s;
background-color: #fff;
}
.form-input:hover {
border-color: #1677ff;
}
.form-input:focus {
outline: none;
border-color: #1677ff;
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.2);
}
.login-btn {
width: 100%;
padding: 14px;
background-color: #1677ff;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
margin-top: 10px;
}
.login-btn:hover {
background-color: #4096ff;
box-shadow: 0 4px 12px rgba(22, 119, 255, 0.3);
}
.login-btn:disabled {
background-color: #d9d9d9;
cursor: not-allowed;
box-shadow: none;
}
.error-message {
color: #f5222d;
font-size: 12px;
margin-top: 8px;
display: none;
}
.error-message.show {
display: block;
}
.loading {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 0.8s linear infinite;
margin-right: 8px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="login-container">
<h1 class="login-title">审核系统登录</h1>
<form id="loginForm">
<div class="form-group">
<label class="form-label" for="projectName">职位名称</label>
<input type="text" class="form-input" id="projectName" name="projectName" placeholder="请输入职位名称" required>
<div class="error-message" id="projectNameError"></div>
</div>
<div class="form-group">
<label class="form-label" for="userName">用户名</label>
<input type="text" class="form-input" id="userName" name="userName" placeholder="请输入用户名" required>
<div class="error-message" id="userNameError"></div>
</div>
<div class="form-group">
<label class="form-label" for="password">密码</label>
<input type="password" class="form-input" id="password" name="password" placeholder="请输入密码" required>
<div class="error-message" id="passwordError"></div>
</div>
<div class="error-message" id="loginError"></div>
<button type="submit" class="login-btn" id="loginBtn">
登录
</button>
</form>
</div>
<script>
// 登录表单提交事件
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
// 获取表单数据
const projectName = document.getElementById('projectName').value.trim();
const userName = document.getElementById('userName').value.trim();
const password = document.getElementById('password').value.trim();
// 验证表单
let isValid = true;
// 职位名称验证
const projectNameError = document.getElementById('projectNameError');
if (!projectName) {
projectNameError.textContent = '请输入职位名称';
projectNameError.classList.add('show');
isValid = false;
} else {
projectNameError.classList.remove('show');
}
// 用户名验证
const userNameError = document.getElementById('userNameError');
if (!userName) {
userNameError.textContent = '请输入用户名';
userNameError.classList.add('show');
isValid = false;
} else {
userNameError.classList.remove('show');
}
// 密码验证
const passwordError = document.getElementById('passwordError');
if (!password) {
passwordError.textContent = '请输入密码';
passwordError.classList.add('show');
isValid = false;
} else {
passwordError.classList.remove('show');
}
if (!isValid) {
return;
}
// 显示加载状态
const loginBtn = document.getElementById('loginBtn');
const originalText = loginBtn.innerHTML;
loginBtn.innerHTML = '<span class="loading"></span>登录中...';
loginBtn.disabled = true;
const loginError = document.getElementById('loginError');
loginError.classList.remove('show');
try {
// 发送登录请求
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
projectName,
userName,
password
})
});
const result = await response.json();
if (result.success) {
// 登录成功,保存登录信息到localStorage
localStorage.setItem('userInfo', JSON.stringify(result.data.userInfo));
localStorage.setItem('token', result.data.token);
// 根据角色跳转到不同页面
const userInfo = result.data.userInfo;
if (userInfo.projectName === '管理员') {
// 管理员跳转到管理页面
window.location.href = 'Management.html';
} else if (userInfo.projectName === '采购员') {
// 采购员跳转到供应页面
window.location.href = 'supply.html';
} else {
// 其他角色默认跳转到审核页面
window.location.href = 'SupplierReview.html';
}
} else {
// 登录失败
loginError.textContent = result.message || '登录失败,请检查用户名和密码';
loginError.classList.add('show');
}
} catch (error) {
console.error('登录失败:', error);
loginError.textContent = '登录失败,请检查网络连接';
loginError.classList.add('show');
} finally {
// 恢复按钮状态
loginBtn.innerHTML = originalText;
loginBtn.disabled = false;
}
});
// 检查是否已登录
if (localStorage.getItem('userInfo') && localStorage.getItem('token')) {
// 如果已经登录,直接跳转到审核页面
window.location.href = 'SupplierReview.html';
}
</script>
</body>
</html>

8
oss-config.js

@ -0,0 +1,8 @@
// 阿里云OSS配置
module.exports = {
region: 'oss-cn-chengdu', // OSS区域,例如 'oss-cn-hangzhou'
accessKeyId: 'LTAI5tRT6ReeHUdmqFpmLZi7', // 访问密钥ID
accessKeySecret: 'zTnK27IAphwgCDMmyJzMUsHYxGsDBE', // 访问密钥Secret
bucket: 'my-supplier-photos', // OSS存储桶名称
endpoint: 'oss-cn-chengdu.aliyuncs.com' // 注意:不要在endpoint中包含bucket名称
};

323
oss-uploader.js

@ -0,0 +1,323 @@
const fs = require('fs');
const path = require('path');
const { createHash } = require('crypto');
const OSSClient = require('ali-oss');
const ossConfig = require('./oss-config');
// 创建OSS客户端 - ali-oss 6.23.0版本的正确配置
let client = null;
// 初始化OSS客户端的函数
function initOSSClient() {
try {
console.log('初始化OSS客户端配置:', {
region: ossConfig.region,
accessKeyId: ossConfig.accessKeyId ? '已配置' : '未配置',
accessKeySecret: ossConfig.accessKeySecret ? '已配置' : '未配置',
bucket: ossConfig.bucket,
endpoint: `https://${ossConfig.endpoint}`
});
client = new OSSClient({
region: ossConfig.region,
accessKeyId: ossConfig.accessKeyId,
accessKeySecret: ossConfig.accessKeySecret,
bucket: ossConfig.bucket,
endpoint: ossConfig.endpoint, // 直接使用配置的endpoint,不添加前缀
secure: true, // 启用HTTPS
cname: false, // 对于标准OSS域名,不需要启用cname模式
timeout: 600000, // 设置超时时间为10分钟,适应大文件上传
connectTimeout: 60000, // 连接超时时间1分钟
socketTimeout: 600000 // socket超时时间10分钟
});
return client;
} catch (error) {
console.error('初始化OSS客户端失败:', error);
throw error;
}
}
// 延迟初始化,避免应用启动时就连接OSS
function getOSSClient() {
if (!client) {
return initOSSClient();
}
return client;
}
class OssUploader {
/**
* 上传文件到OSS
* @param {String} filePath - 本地文件路径
* @param {String} folder - OSS上的文件夹路径
* @param {String} fileType - 文件类型默认为'image'
* @returns {Promise<String>} - 上传后的文件URL
*/
/**
* 计算文件的MD5哈希值
* @param {String} filePath - 文件路径
* @returns {Promise<String>} - MD5哈希值
*/
static async getFileHash(filePath) {
return new Promise((resolve, reject) => {
const hash = createHash('md5');
const stream = fs.createReadStream(filePath);
stream.on('error', reject);
stream.on('data', chunk => hash.update(chunk));
stream.on('end', () => resolve(hash.digest('hex')));
});
}
/**
* 计算缓冲区的MD5哈希值
* @param {Buffer} buffer - 数据缓冲区
* @returns {String} - MD5哈希值
*/
static getBufferHash(buffer) {
return createHash('md5').update(buffer).digest('hex');
}
static async uploadFile(filePath, folder = 'images', fileType = 'image') {
try {
console.log('【OSS上传】开始上传文件:', filePath, '到目录:', folder);
// 确保文件存在
const fileExists = await fs.promises.access(filePath).then(() => true).catch(() => false);
if (!fileExists) {
throw new Error(`文件不存在: ${filePath}`);
}
// 获取文件扩展名
const extname = path.extname(filePath).toLowerCase();
if (!extname) {
throw new Error(`无法获取文件扩展名: ${filePath}`);
}
// 基于文件内容计算MD5哈希值,实现文件级去重
console.log('【文件去重】开始计算文件哈希值...');
const fileHash = await this.getFileHash(filePath);
console.log(`【文件去重】文件哈希计算完成: ${fileHash}`);
// 使用哈希值作为文件名,确保相同内容的文件生成相同的文件名
const uniqueFilename = `${fileHash}${extname}`;
const ossFilePath = `${folder}/${fileType}/${uniqueFilename}`;
console.log(`【文件去重】使用基于内容的文件名: ${uniqueFilename}`);
// 获取OSS客户端,延迟初始化
const ossClient = getOSSClient();
// 测试OSS连接
try {
await ossClient.list({ max: 1 });
console.log('OSS连接测试成功');
} catch (connError) {
console.error('OSS连接测试失败,尝试重新初始化客户端:', connError.message);
// 尝试重新初始化客户端
initOSSClient();
}
// 检查OSS客户端配置
console.log('【OSS上传】OSS配置检查 - region:', ossClient.options.region, 'bucket:', ossClient.options.bucket);
// 上传文件,明确设置为公共读权限
console.log(`开始上传文件到OSS: ${filePath} -> ${ossFilePath}`);
const result = await ossClient.put(ossFilePath, filePath, {
headers: {
'x-oss-object-acl': 'public-read' // 确保文件可以公开访问
},
acl: 'public-read' // 额外设置ACL参数,确保文件公开可读
});
console.log(`文件上传成功: ${result.url}`);
console.log('已设置文件为公共读权限');
// 返回完整的文件URL
return result.url;
} catch (error) {
console.error('【OSS上传】上传文件失败:', error);
console.error('【OSS上传】错误详情:', error.message);
console.error('【OSS上传】错误栈:', error.stack);
throw error;
}
}
/**
* 从缓冲区上传文件到OSS
* @param {Buffer} buffer - 文件数据缓冲区
* @param {String} filename - 文件名
* @param {String} folder - OSS上的文件夹路径
* @param {String} fileType - 文件类型默认为'image'
* @returns {Promise<String>} - 上传后的文件URL
*/
static async uploadBuffer(buffer, filename, folder = 'images', fileType = 'image') {
try {
// 获取文件扩展名
const extname = path.extname(filename).toLowerCase();
if (!extname) {
throw new Error(`无法获取文件扩展名: ${filename}`);
}
// 基于文件内容计算MD5哈希值,实现文件级去重
console.log('【文件去重】开始计算缓冲区哈希值...');
const bufferHash = this.getBufferHash(buffer);
console.log(`【文件去重】缓冲区哈希计算完成: ${bufferHash}`);
// 使用哈希值作为文件名,确保相同内容的文件生成相同的文件名
const uniqueFilename = `${bufferHash}${extname}`;
const ossFilePath = `${folder}/${fileType}/${uniqueFilename}`;
console.log(`【文件去重】使用基于内容的文件名: ${uniqueFilename}`);
// 获取OSS客户端,延迟初始化
const ossClient = getOSSClient();
// 上传缓冲区,明确设置为公共读权限
console.log(`开始上传缓冲区到OSS: ${ossFilePath}`);
const result = await ossClient.put(ossFilePath, buffer, {
headers: {
'x-oss-object-acl': 'public-read' // 确保文件可以公开访问
},
acl: 'public-read' // 额外设置ACL参数,确保文件公开可读
});
console.log(`缓冲区上传成功: ${result.url}`);
console.log('已设置文件为公共读权限');
// 返回完整的文件URL
return result.url;
} catch (error) {
console.error('OSS缓冲区上传失败:', error);
console.error('OSS缓冲区上传错误详情:', error.message);
console.error('OSS缓冲区上传错误栈:', error.stack);
throw error;
}
}
/**
* 批量上传文件到OSS
* @param {Array<String>} filePaths - 本地文件路径数组
* @param {String} folder - OSS上的文件夹路径
* @param {String} fileType - 文件类型默认为'image'
* @returns {Promise<Array<String>>} - 上传后的文件URL数组
*/
static async uploadFiles(filePaths, folder = 'images', fileType = 'image') {
try {
const uploadPromises = filePaths.map(filePath =>
this.uploadFile(filePath, folder, fileType)
);
const urls = await Promise.all(uploadPromises);
console.log(`批量上传完成,成功上传${urls.length}个文件`);
return urls;
} catch (error) {
console.error('OSS批量上传失败:', error);
throw error;
}
}
/**
* 删除OSS上的文件
* @param {String} ossFilePath - OSS上的文件路径
* @returns {Promise<Boolean>} - 删除是否成功
*/
static async deleteFile(ossFilePath) {
try {
console.log(`【OSS删除】开始删除OSS文件: ${ossFilePath}`);
const ossClient = getOSSClient();
// 【新增】记录OSS客户端配置信息(隐藏敏感信息)
console.log(`【OSS删除】OSS客户端配置 - region: ${ossClient.options.region}, bucket: ${ossClient.options.bucket}`);
const result = await ossClient.delete(ossFilePath);
console.log(`【OSS删除】OSS文件删除成功: ${ossFilePath}`, result);
return true;
} catch (error) {
console.error('【OSS删除】OSS文件删除失败:', ossFilePath, '错误:', error.message);
console.error('【OSS删除】错误详情:', error);
// 【增强日志】详细分析错误类型
console.log('【OSS删除】=== 错误详细分析开始 ===');
console.log('【OSS删除】错误名称:', error.name);
console.log('【OSS删除】错误代码:', error.code);
console.log('【OSS删除】HTTP状态码:', error.status);
console.log('【OSS删除】请求ID:', error.requestId);
console.log('【OSS删除】主机ID:', error.hostId);
// 【关键检查】判断是否为权限不足错误
const isPermissionError =
error.code === 'AccessDenied' ||
error.status === 403 ||
error.message.includes('permission') ||
error.message.includes('AccessDenied') ||
error.message.includes('do not have write permission');
if (isPermissionError) {
console.error('【OSS删除】❌ 确认是权限不足错误!');
console.error('【OSS删除】❌ 当前AccessKey缺少删除文件的权限');
console.error('【OSS删除】❌ 请检查RAM策略是否包含 oss:DeleteObject 权限');
console.error('【OSS删除】❌ 建议在RAM中授予 AliyunOSSFullAccess 或自定义删除权限');
}
console.log('【OSS删除】=== 错误详细分析结束 ===');
// 如果文件不存在,也算删除成功
if (error.code === 'NoSuchKey' || error.status === 404) {
console.log(`【OSS删除】文件不存在,视为删除成功: ${ossFilePath}`);
return true;
}
// 【新增】对于权限错误,提供更友好的错误信息
if (isPermissionError) {
const permissionError = new Error(`OSS删除权限不足: ${error.message}`);
permissionError.code = 'OSS_ACCESS_DENIED';
permissionError.originalError = error;
throw permissionError;
}
throw error;
}
}
/**
* 获取OSS配置信息
* @returns {Object} - OSS配置信息
*/
static getConfig() {
return {
region: ossConfig.region,
bucket: ossConfig.bucket,
endpoint: ossConfig.endpoint
};
}
/**
* 测试OSS连接
* @returns {Promise<Object>} - 连接测试结果
*/
static async testConnection() {
try {
console.log('【OSS连接测试】开始测试OSS连接...');
const ossClient = getOSSClient();
// 执行简单的list操作来验证连接
const result = await ossClient.list({ max: 1 });
console.log('【OSS连接测试】连接成功,存储桶中有', result.objects.length, '个对象');
return {
success: true,
message: 'OSS连接成功',
region: ossClient.options.region,
bucket: ossClient.options.bucket
};
} catch (error) {
console.error('【OSS连接测试】连接失败:', error.message);
return {
success: false,
message: `OSS连接失败: ${error.message}`,
error: error.message
};
}
}
}
module.exports = OssUploader;

5370
package-lock.json

File diff suppressed because it is too large

19
package.json

@ -0,0 +1,19 @@
{
"dependencies": {
"ali-oss": "^6.23.0",
"axios": "^1.13.2",
"body-parser": "^2.2.1",
"cors": "^2.8.5",
"express": "^5.1.0",
"mysql2": "^3.15.3",
"sharp": "^0.34.5",
"ws": "^8.19.0"
},
"devDependencies": {
"chai": "^6.2.1",
"mocha": "^11.7.5",
"nyc": "^17.1.0",
"sinon": "^21.0.0",
"supertest": "^7.1.4"
}
}

9436
supply.html

File diff suppressed because it is too large

410
tatus

@ -0,0 +1,410 @@
SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
Commands marked with * may be preceded by a number, _N.
Notes in parentheses indicate the behavior if _N is given.
A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
h H Display this help.
q :q Q :Q ZZ Exit.
---------------------------------------------------------------------------
MMOOVVIINNGG
e ^E j ^N CR * Forward one line (or _N lines).
y ^Y k ^K ^P * Backward one line (or _N lines).
ESC-j * Forward one file line (or _N file lines).
ESC-k * Backward one file line (or _N file lines).
f ^F ^V SPACE * Forward one window (or _N lines).
b ^B ESC-v * Backward one window (or _N lines).
z * Forward one window (and set window to _N).
w * Backward one window (and set window to _N).
ESC-SPACE * Forward one window, but don't stop at end-of-file.
ESC-b * Backward one window, but don't stop at beginning-of-file.
d ^D * Forward one half-window (and set half-window to _N).
u ^U * Backward one half-window (and set half-window to _N).
ESC-) RightArrow * Right one half screen width (or _N positions).
ESC-( LeftArrow * Left one half screen width (or _N positions).
ESC-} ^RightArrow Right to last column displayed.
ESC-{ ^LeftArrow Left to first column.
F Forward forever; like "tail -f".
ESC-F Like F but stop when search pattern is found.
r ^R ^L Repaint screen.
R Repaint screen, discarding buffered input.
---------------------------------------------------
Default "window" is the screen height.
Default "half-window" is half of the screen height.
---------------------------------------------------------------------------
SSEEAARRCCHHIINNGG
/_p_a_t_t_e_r_n * Search forward for (_N-th) matching line.
?_p_a_t_t_e_r_n * Search backward for (_N-th) matching line.
n * Repeat previous search (for _N-th occurrence).
N * Repeat previous search in reverse direction.
ESC-n * Repeat previous search, spanning files.
ESC-N * Repeat previous search, reverse dir. & spanning files.
^O^N ^On * Search forward for (_N-th) OSC8 hyperlink.
^O^P ^Op * Search backward for (_N-th) OSC8 hyperlink.
^O^L ^Ol Jump to the currently selected OSC8 hyperlink.
ESC-u Undo (toggle) search highlighting.
ESC-U Clear search highlighting.
&_p_a_t_t_e_r_n * Display only matching lines.
---------------------------------------------------
Search is case-sensitive unless changed with -i or -I.
A search pattern may begin with one or more of:
^N or ! Search for NON-matching lines.
^E or * Search multiple files (pass thru END OF FILE).
^F or @ Start search at FIRST file (for /) or last file (for ?).
^K Highlight matches, but don't move (KEEP position).
^R Don't use REGULAR EXPRESSIONS.
^S _n Search for match in _n-th parenthesized subpattern.
^W WRAP search if no match found.
^L Enter next character literally into pattern.
---------------------------------------------------------------------------
JJUUMMPPIINNGG
g < ESC-< * Go to first line in file (or line _N).
G > ESC-> * Go to last line in file (or line _N).
p % * Go to beginning of file (or _N percent into file).
t * Go to the (_N-th) next tag.
T * Go to the (_N-th) previous tag.
{ ( [ * Find close bracket } ) ].
} ) ] * Find open bracket { ( [.
ESC-^F _<_c_1_> _<_c_2_> * Find close bracket _<_c_2_>.
ESC-^B _<_c_1_> _<_c_2_> * Find open bracket _<_c_1_>.
---------------------------------------------------
Each "find close bracket" command goes forward to the close bracket
matching the (_N-th) open bracket in the top line.
Each "find open bracket" command goes backward to the open bracket
matching the (_N-th) close bracket in the bottom line.
m_<_l_e_t_t_e_r_> Mark the current top line with <letter>.
M_<_l_e_t_t_e_r_> Mark the current bottom line with <letter>.
'_<_l_e_t_t_e_r_> Go to a previously marked position.
'' Go to the previous position.
^X^X Same as '.
ESC-m_<_l_e_t_t_e_r_> Clear a mark.
---------------------------------------------------
A mark is any upper-case or lower-case letter.
Certain marks are predefined:
^ means beginning of the file
$ means end of the file
---------------------------------------------------------------------------
CCHHAANNGGIINNGG FFIILLEESS
:e [_f_i_l_e] Examine a new file.
^X^V Same as :e.
:n * Examine the (_N-th) next file from the command line.
:p * Examine the (_N-th) previous file from the command line.
:x * Examine the first (or _N-th) file from the command line.
^O^O Open the currently selected OSC8 hyperlink.
:d Delete the current file from the command line list.
= ^G :f Print current file name.
---------------------------------------------------------------------------
MMIISSCCEELLLLAANNEEOOUUSS CCOOMMMMAANNDDSS
-_<_f_l_a_g_> Toggle a command line option [see OPTIONS below].
--_<_n_a_m_e_> Toggle a command line option, by name.
__<_f_l_a_g_> Display the setting of a command line option.
___<_n_a_m_e_> Display the setting of an option, by name.
+_c_m_d Execute the less cmd each time a new file is examined.
!_c_o_m_m_a_n_d Execute the shell command with $SHELL.
#_c_o_m_m_a_n_d Execute the shell command, expanded like a prompt.
|XX_c_o_m_m_a_n_d Pipe file between current pos & mark XX to shell command.
s _f_i_l_e Save input to a file.
v Edit the current file with $VISUAL or $EDITOR.
V Print version number of "less".
---------------------------------------------------------------------------
OOPPTTIIOONNSS
Most options may be changed either on the command line,
or from within less by using the - or -- command.
Options may be given in one of two forms: either a single
character preceded by a -, or a name preceded by --.
-? ........ --help
Display help (from command line).
-a ........ --search-skip-screen
Search skips current screen.
-A ........ --SEARCH-SKIP-SCREEN
Search starts just after target line.
-b [_N] .... --buffers=[_N]
Number of buffers.
-B ........ --auto-buffers
Don't automatically allocate buffers for pipes.
-c ........ --clear-screen
Repaint by clearing rather than scrolling.
-d ........ --dumb
Dumb terminal.
-D xx_c_o_l_o_r . --color=xx_c_o_l_o_r
Set screen colors.
-e -E .... --quit-at-eof --QUIT-AT-EOF
Quit at end of file.
-f ........ --force
Force open non-regular files.
-F ........ --quit-if-one-screen
Quit if entire file fits on first screen.
-g ........ --hilite-search
Highlight only last match for searches.
-G ........ --HILITE-SEARCH
Don't highlight any matches for searches.
-h [_N] .... --max-back-scroll=[_N]
Backward scroll limit.
-i ........ --ignore-case
Ignore case in searches that do not contain uppercase.
-I ........ --IGNORE-CASE
Ignore case in all searches.
-j [_N] .... --jump-target=[_N]
Screen position of target lines.
-J ........ --status-column
Display a status column at left edge of screen.
-k _f_i_l_e ... --lesskey-file=_f_i_l_e
Use a compiled lesskey file.
-K ........ --quit-on-intr
Exit less in response to ctrl-C.
-L ........ --no-lessopen
Ignore the LESSOPEN environment variable.
-m -M .... --long-prompt --LONG-PROMPT
Set prompt style.
-n ......... --line-numbers
Suppress line numbers in prompts and messages.
-N ......... --LINE-NUMBERS
Display line number at start of each line.
-o [_f_i_l_e] .. --log-file=[_f_i_l_e]
Copy to log file (standard input only).
-O [_f_i_l_e] .. --LOG-FILE=[_f_i_l_e]
Copy to log file (unconditionally overwrite).
-p _p_a_t_t_e_r_n . --pattern=[_p_a_t_t_e_r_n]
Start at pattern (from command line).
-P [_p_r_o_m_p_t] --prompt=[_p_r_o_m_p_t]
Define new prompt.
-q -Q .... --quiet --QUIET --silent --SILENT
Quiet the terminal bell.
-r -R .... --raw-control-chars --RAW-CONTROL-CHARS
Output "raw" control characters.
-s ........ --squeeze-blank-lines
Squeeze multiple blank lines.
-S ........ --chop-long-lines
Chop (truncate) long lines rather than wrapping.
-t _t_a_g .... --tag=[_t_a_g]
Find a tag.
-T [_t_a_g_s_f_i_l_e] --tag-file=[_t_a_g_s_f_i_l_e]
Use an alternate tags file.
-u -U .... --underline-special --UNDERLINE-SPECIAL
Change handling of backspaces, tabs and carriage returns.
-V ........ --version
Display the version number of "less".
-w ........ --hilite-unread
Highlight first new line after forward-screen.
-W ........ --HILITE-UNREAD
Highlight first new line after any forward movement.
-x [_N[,...]] --tabs=[_N[,...]]
Set tab stops.
-X ........ --no-init
Don't use termcap init/deinit strings.
-y [_N] .... --max-forw-scroll=[_N]
Forward scroll limit.
-z [_N] .... --window=[_N]
Set size of window.
-" [_c[_c]] . --quotes=[_c[_c]]
Set shell quote characters.
-~ ........ --tilde
Don't display tildes after end of file.
-# [_N] .... --shift=[_N]
Set horizontal scroll amount (0 = one half screen width).
--exit-follow-on-close
Exit F command on a pipe when writer closes pipe.
--file-size
Automatically determine the size of the input file.
--follow-name
The F command changes files if the input file is renamed.
--form-feed
Stop scrolling when a form feed character is reached.
--header=[_L[,_C[,_N]]]
Use _L lines (starting at line _N) and _C columns as headers.
--incsearch
Search file as each pattern character is typed in.
--intr=[_C]
Use _C instead of ^X to interrupt a read.
--lesskey-context=_t_e_x_t
Use lesskey source file contents.
--lesskey-src=_f_i_l_e
Use a lesskey source file.
--line-num-width=[_N]
Set the width of the -N line number field to _N characters.
--match-shift=[_N]
Show at least _N characters to the left of a search match.
--modelines=[_N]
Read _N lines from the input file and look for vim modelines.
--mouse
Enable mouse input.
--no-edit-warn
Don't warn when using v command on a file opened via LESSOPEN.
--no-keypad
Don't send termcap keypad init/deinit strings.
--no-histdups
Remove duplicates from command history.
--no-number-headers
Don't give line numbers to header lines.
--no-paste
Ignore pasted input.
--no-search-header-lines
Searches do not include header lines.
--no-search-header-columns
Searches do not include header columns.
--no-search-headers
Searches do not include header lines or columns.
--no-vbell
Disable the terminal's visual bell.
--redraw-on-quit
Redraw final screen when quitting.
--rscroll=[_C]
Set the character used to mark truncated lines.
--save-marks
Retain marks across invocations of less.
--search-options=[EFKNRW-]
Set default options for every search.
--show-preproc-errors
Display a message if preprocessor exits with an error status.
--proc-backspace
Process backspaces for bold/underline.
--PROC-BACKSPACE
Treat backspaces as control characters.
--proc-return
Delete carriage returns before newline.
--PROC-RETURN
Treat carriage returns as control characters.
--proc-tab
Expand tabs to spaces.
--PROC-TAB
Treat tabs as control characters.
--status-col-width=[_N]
Set the width of the -J status column to _N characters.
--status-line
Highlight or color the entire line containing a mark.
--use-backslash
Subsequent options use backslash as escape char.
--use-color
Enables colored text.
--wheel-lines=[_N]
Each click of the mouse wheel moves _N lines.
--wordwrap
Wrap lines at spaces.
---------------------------------------------------------------------------
LLIINNEE EEDDIITTIINNGG
These keys can be used to edit text being entered
on the "command line" at the bottom of the screen.
RightArrow ..................... ESC-l ... Move cursor right one character.
LeftArrow ...................... ESC-h ... Move cursor left one character.
ctrl-RightArrow ESC-RightArrow ESC-w ... Move cursor right one word.
ctrl-LeftArrow ESC-LeftArrow ESC-b ... Move cursor left one word.
HOME ........................... ESC-0 ... Move cursor to start of line.
END ............................ ESC-$ ... Move cursor to end of line.
BACKSPACE ................................ Delete char to left of cursor.
DELETE ......................... ESC-x ... Delete char under cursor.
ctrl-BACKSPACE ESC-BACKSPACE ........... Delete word to left of cursor.
ctrl-DELETE .... ESC-DELETE .... ESC-X ... Delete word under cursor.
ctrl-U ......... ESC (MS-DOS only) ....... Delete entire line.
UpArrow ........................ ESC-k ... Retrieve previous command line.
DownArrow ...................... ESC-j ... Retrieve next command line.
TAB ...................................... Complete filename & cycle.
SHIFT-TAB ...................... ESC-TAB Complete filename & reverse cycle.
ctrl-L ................................... Complete filename, list all.
SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS
Commands marked with * may be preceded by a number, _N.
Notes in parentheses indicate the behavior if _N is given.
A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K.
h H Display this help.
q :q Q :Q ZZ Exit.
---------------------------------------------------------------------------
MMOOVVIINNGG
e ^E j ^N CR * Forward one line (or _N lines).
y ^Y k ^K ^P * Backward one line (or _N lines).
ESC-j * Forward one file line (or _N file lines).
ESC-k * Backward one file line (or _N file lines).
f ^F ^V SPACE * Forward one window (or _N lines).
b ^B ESC-v * Backward one window (or _N lines).
z * Forward one window (and set window to _N).
w * Backward one window (and set window to _N).
ESC-SPACE * Forward one window, but don't stop at end-of-file.
ESC-b * Backward one window, but don't stop at beginning-of-file.
d ^D * Forward one half-window (and set half-window to _N).
u ^U * Backward one half-window (and set half-window to _N).
ESC-) RightArrow * Right one half screen width (or _N positions).
ESC-( LeftArrow * Left one half screen width (or _N positions).
ESC-} ^RightArrow Right to last column displayed.
ESC-{ ^LeftArrow Left to first column.
F Forward forever; like "tail -f".
ESC-F Like F but stop when search pattern is found.
r ^R ^L Repaint screen.
R Repaint screen, discarding buffered input.
---------------------------------------------------
Default "window" is the screen height.
Default "half-window" is half of the screen height.
---------------------------------------------------------------------------
SSEEAARRCCHHIINNGG
/_p_a_t_t_e_r_n * Search forward for (_N-th) matching line.
?_p_a_t_t_e_r_n * Search backward for (_N-th) matching line.
n * Repeat previous search (for _N-th occurrence).
N * Repeat previous search in reverse direction.
ESC-n * Repeat previous search, spanning files.
ESC-N * Repeat previous search, reverse dir. & spanning files.
^O^N ^On * Search forward for (_N-th) OSC8 hyperlink.
^O^P ^Op * Search backward for (_N-th) OSC8 hyperlink.
^O^L ^Ol Jump to the currently selected OSC8 hyperlink.
ESC-u Undo (toggle) search highlighting.
ESC-U Clear search highlighting.
&_p_a_t_t_e_r_n * Display only matching lines.
---------------------------------------------------
Search is case-sensitive unless changed with -i or -I.
A search pattern may begin with one or more of:
^N or ! Search for NON-matching lines.
^E or * Search multiple files (pass thru END OF FILE).
^F or @ Start search at FIRST file (for /) or last file (for ?).
^K Highlight matches, but don't move (KEEP position).
^R Don't use REGULAR EXPRESSIONS.
^S _n Search for match in _n-th parenthesized subpattern.
^W WRAP search if no match found.
^L Enter next character literally into pattern.
---------------------------------------------------------------------------
JJUUMMPPIINNGG
g < ESC-< * Go to first line in file (or line _N).
G > ESC-> * Go to last line in file (or line _N).
p % * Go to beginning of file (or _N percent into file).
t * Go to the (_N-th) next tag.
T * Go to the (_N-th) previous tag.
{ ( [ * Find close bracket } ) ].
} ) ] * Find open bracket { ( [.
ESC-^F _<_c_1_> _<_c_2_> * Find close bracket _<_c_2_>.
ESC-^B _<_c_1_> _<_c_2_> * Find open bracket _<_c_1_>.
---------------------------------------------------
Each "find close bracket" command goes forward to the close bracket
matching the (_N-th) open bracket in the top line.
Each "find open bracket" command goes backward to the open bracket
matching the (_N-th) close bracket in the bottom line.
m_<_l_e_t_t_e_r_> Mark the current top line with <letter>.
M_<_l_e_t_t_e_r_> Mark the current bottom line with <letter>.
'_<_l_e_t_t_e_r_> Go to a previously marked position.
'' Go to the previous position.

BIN
watermark-test-output.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 B

Loading…
Cancel
Save