Procházet zdrojové kódy

first commit

master
lizhuang před 2 týdny
revize
7b5abae69f
57 změnil soubory, kde provedl 1185 přidání a 0 odebrání
  1. 27
    0
      .gitignore
  2. 93
    0
      README.md
  3. 203
    0
      app.js
  4. 39
    0
      config/oss.js
  5. binární
      downloads/edited_1737080856624.xlsx
  6. binární
      downloads/edited_1739340265117.xlsx
  7. binární
      downloads/edited_1739512520399.xlsx
  8. binární
      downloads/edited_1742457844022.xlsx
  9. binární
      downloads/edited_1742458401902.xlsx
  10. binární
      downloads/edited_1742462419471.xlsx
  11. 19
    0
      package.json
  12. 710
    0
      public/index.html
  13. 94
    0
      routes/upload.js
  14. binární
      uploads/021026947b8e2765cff3c159b4575c8a
  15. binární
      uploads/0f98de8a83aeb547d554dd69affd264c
  16. binární
      uploads/1714d8f99fd82197eb3a6b3578c580fb
  17. binární
      uploads/1c3bdaf4a6052c238f4c8f14a1481142
  18. binární
      uploads/2c3e49f94f0598133d8c28212c52c676
  19. binární
      uploads/32c0b3ed6261e53ebc9c3c655c6f0034
  20. binární
      uploads/398c8464f79caee71670907ab5b33363
  21. binární
      uploads/422ff7314608a05de33738bb25506186
  22. binární
      uploads/439f254fc7015b04c360a717ae8455f1
  23. binární
      uploads/44baab9e90d03f5b1bddb5a7fd6727b7
  24. binární
      uploads/47ce5f78e785832427a4cb820181a3d7
  25. binární
      uploads/4b5ec563e1da9169678c36ac3e6ef55f
  26. binární
      uploads/568caef389174b75123b84f84bde41f7
  27. binární
      uploads/5bef95156eaeac6bf6a254e806006900
  28. binární
      uploads/5e05c813b3244bd4be8ffdd9d5dba2b5
  29. binární
      uploads/65aa04d414afa7992ab79937a7756951
  30. binární
      uploads/6b9401f11b16d109a9f4b3013814284e
  31. binární
      uploads/70a35e1947a209ceac6a4c8a3211cb60
  32. binární
      uploads/7bcbe690fcd02456f6ca06f0122dd739
  33. binární
      uploads/7d3b961ebd1960e10aae86db9af91036
  34. binární
      uploads/7eff93203beecd1578548f2032b032dd
  35. binární
      uploads/7f640b4e3250431d9d06ec4ddfdb049b
  36. binární
      uploads/81eac194e46108db3a17ea0df63c7207
  37. binární
      uploads/82df0237f7f5685fbc7b21cd6af1b389
  38. binární
      uploads/843f27324cff13c805e593d3b2b49c5b
  39. binární
      uploads/86848f33e40139c4f0e047ac2e0ef490
  40. binární
      uploads/880b956b7feef99f4fd4e9feeb2a1ba9
  41. binární
      uploads/8dae43608bf898c96a68bd6b219b3e88
  42. binární
      uploads/97be1001e3379decc8348023af08af88
  43. binární
      uploads/983344a3b07c6c038f889f803cfa2ce5
  44. binární
      uploads/a8d856d46190fd283a6b8ce8b30d593e
  45. binární
      uploads/abd043c11fb2d7312ada0ce5420796fe
  46. binární
      uploads/b2e8d9f5bf0fcf64c463816b447181e9
  47. binární
      uploads/be664b230914444e441c2e595db1607c
  48. binární
      uploads/c19f72b1bd4675e99ecc54dece34b2ba
  49. binární
      uploads/c260ce1326761d968d154419e11f3a67
  50. binární
      uploads/c7a2102d732a4ce2c0a6f9e725ba046e
  51. binární
      uploads/d1be6b3a36d1c957813a6519f4cc13b9
  52. binární
      uploads/d29606a92390f4d15dc664c5de0c1709
  53. binární
      uploads/d6177affe2c9278cd8f025669515c2ea
  54. binární
      uploads/da66242941096c2f7d770dddd55a4d00
  55. binární
      uploads/e3ab0b96c8f1c48d89fd7932f5064203
  56. binární
      uploads/f0d3e0272d5fa39becfd5a24ad1b369a
  57. binární
      uploads/f6c5326074c6c84efefa1bd3b200abb2

+ 27
- 0
.gitignore Zobrazit soubor

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Dependencies
node_modules/
package-lock.json
yarn.lock

# Build output
dist/
out/
build/

# Electron
.DS_Store
Thumbs.db

# Environment
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

+ 93
- 0
README.md Zobrazit soubor

# Excel商品数据处理工具

## 项目简介

Excel商品数据处理工具是一个基于Node.js开发的Web应用,用于上传、编辑和下载Excel文件中的商品数据。该工具提供了友好的Web界面,允许用户对Excel数据进行在线编辑,并支持将修改后的数据下载为新的Excel文件。

## 主要功能

- Excel文件上传与解析
- 在线数据编辑
- Excel文件导出
- 图片上传至阿里云OSS存储
- 自动检查数据格式和长度限制

## 技术栈

- **后端**: Node.js, Express
- **前端**: HTML, JavaScript, CSS
- **数据处理**: xlsx库
- **文件上传**: multer
- **云存储**: 阿里云OSS

## 安装与运行

### 前置条件

- Node.js (v12.0.0或更高版本)
- npm包管理器

### 安装步骤

1. 克隆项目到本地
```
git clone <项目仓库地址>
cd plugin/goods-excel
```

2. 安装依赖
```
npm install
```

3. 配置环境变量
创建`.env`文件并设置以下变量:
```
OSS_ACCESS_KEY_ID=<阿里云OSS访问密钥ID>
OSS_ACCESS_KEY_SECRET=<阿里云OSS访问密钥Secret>
OSS_BUCKET=<阿里云OSS存储桶名称>
OSS_ENDPOINT=<阿里云OSS终端节点>
```

4. 启动服务器
```
node app.js
```

5. 在浏览器中访问
```
http://localhost:3100
```

## 使用说明

1. 在网页界面上传Excel文件(支持.xlsx格式)
2. 系统会解析并显示Excel中的数据
3. 可以在线编辑数据内容
4. 编辑完成后,点击保存按钮导出为新的Excel文件
5. 对于需要上传的图片,会自动上传至阿里云OSS并生成链接

## 注意事项

- Excel单元格内容长度不能超过32767个字符,系统会自动检查并提示
- 上传的文件会暂存在服务器,下载完成后会自动清理
- 图片上传需要正确配置阿里云OSS的访问凭证

## 项目结构

```
plugin/goods-excel/
├── app.js # 应用主入口
├── .env # 环境变量配置
├── public/ # 静态资源目录
│ └── index.html # Web界面
├── routes/ # 路由文件
│ └── upload.js # 上传相关路由
├── downloads/ # 下载文件临时目录
├── uploads/ # 上传文件临时目录
└── package.json # 项目依赖配置
```

## 许可证

ISC

+ 203
- 0
app.js Zobrazit soubor

const express = require('express');
const multer = require('multer');
const XLSX = require('xlsx');
const path = require('path');
const fs = require('fs');
const uploadRouter = require('./routes/upload');
const dotenv = require('dotenv');

// 确保在其他代码之前加载环境变量
const result = dotenv.config({
path: path.resolve(__dirname, '.env')
});

if (result.error) {
console.error('Error loading .env file:', result.error);
}

const app = express();
const upload = multer({ dest: 'uploads/' });

// 用于存储原始工作簿格式
let originalWorkbook = null;

// 配置中间件
app.use(express.json({limit: '50mb'}));
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
app.use(express.static(path.join(__dirname, 'public')));

// 注册图片上传路由
app.use('/', uploadRouter);

// Excel文件上传处理
app.post('/upload', upload.single('excelFile'), (req, res) => {
if (!req.file) {
return res.status(400).send('No file uploaded.');
}

// 读取并保存原始工作簿
originalWorkbook = XLSX.readFile(req.file.path, {
cellStyles: true,
cellNF: true,
cellFormula: true
});
const firstSheet = originalWorkbook.Sheets[originalWorkbook.SheetNames[0]];
// 读取标题 (A1 到 AR1)
const headers = [];
for (let i = 0; i < 44; i++) {
const cellRef = XLSX.utils.encode_cell({c: i, r: 0});
const cell = firstSheet[cellRef];
headers.push(cell ? cell.v : '');
}

// 读取数据并保留单元格格式信息
const range = XLSX.utils.decode_range(firstSheet['!ref']);
const data = [];
for(let R = 1; R <= range.e.r; ++R) {
const row = {};
for(let C = 0; C <= range.e.c; ++C) {
const cellRef = XLSX.utils.encode_cell({c: C, r: R});
const cell = firstSheet[cellRef];
if(cell) {
row[headers[C]] = cell.v;
}
}
data.push(row);
}

// 保存原始文件路径
originalWorkbook.filePath = req.file.path;

res.json({headers, data});
});

// 保存编辑后的数据
app.post('/save', (req, res) => {
try {
const { headers, data } = req.body;
// 检查超长内容
const exceedingCells = [];
data.forEach((row, rowIndex) => {
headers.forEach((header, colIndex) => {
const content = row[header];
if (content && content.length > 32767) {
exceedingCells.push({
row: rowIndex + 2,
column: header,
length: content.length
});
}
});
});

if (exceedingCells.length > 0) {
const errorMessage = exceedingCells.map(cell =>
`第 ${cell.row} 行的 "${cell.column}" 列内容长度为 ${cell.length},超出了Excel单元格32767个字符的限制`
).join('\n');
return res.status(400).json({
error: '存在超出Excel字符限制的单元格',
details: exceedingCells,
message: errorMessage
});
}

if (!originalWorkbook) {
return res.status(400).json({ error: 'No original workbook found. Please upload file first.' });
}

const originalSheet = originalWorkbook.Sheets[originalWorkbook.SheetNames[0]];
data.forEach((row, rowIndex) => {
headers.forEach((header, colIndex) => {
const cellRef = XLSX.utils.encode_cell({ r: rowIndex + 1, c: colIndex });
const value = row[header];
if (value === undefined || value === null) {
// 如果单元格值为空,则删除该单元格
delete originalSheet[cellRef];
} else {
// 创建或更新单元格
originalSheet[cellRef] = {
t: 's', // 设置类型为字符串
v: value.toString(), // 值
w: value.toString(), // 格式化显示值
h: value.toString(), // HTML显示值
};
}
});
});

// 创建downloads目录(如果不存在)
const downloadsDir = path.join(__dirname, 'downloads');
if (!fs.existsSync(downloadsDir)) {
fs.mkdirSync(downloadsDir);
}

// 保存文件
const fileName = `edited_${Date.now()}.xlsx`;
const filePath = path.join(downloadsDir, fileName);
XLSX.writeFile(originalWorkbook, filePath, {
bookType: 'xlsx',
bookSST: false,
type: 'file',
cellStyles: true,
cellNF: true,
cellFormula: true
});

res.json({ downloadUrl: `/download/${fileName}` });
} catch (error) {
console.error('Save error:', error);
res.status(500).json({ error: error.message });
}
});

// 下载文件
app.get('/download/:filename', (req, res) => {
const filePath = path.join(__dirname, 'downloads', req.params.filename);
res.download(filePath, req.params.filename, (err) => {
if (!err) {
// 下载完成后删除文件
fs.unlinkSync(filePath);
}
});
});

// 错误处理中间件
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: err.message || '服务器内部错误' });
});

// 清理函数:当服务器关闭时删除临时文件
function cleanup() {
if (originalWorkbook && originalWorkbook.filePath) {
try {
fs.unlinkSync(originalWorkbook.filePath);
} catch (err) {
console.error('Error cleaning up:', err);
}
}
}

// 监听进程退出事件
process.on('SIGINT', () => {
cleanup();
process.exit();
});

process.on('SIGTERM', () => {
cleanup();
process.exit();
});

const port = process.env.PORT || 3100;
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});

+ 39
- 0
config/oss.js Zobrazit soubor

const dotenv = require('dotenv');
const path = require('path');

// 加载 .env 文件
const result = dotenv.config({
path: path.resolve(__dirname, '../.env')
});

if (result.error) {
console.error('Error loading .env file:', result.error);
}

const config = {
accessKeyId: process.env.OSS_ACCESS_KEY_ID,
accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
bucket: process.env.OSS_BUCKET,
endpoint: process.env.OSS_ENDPOINT
};

// 验证配置
console.log('Current environment variables:', {
NODE_ENV: process.env.NODE_ENV,
PWD: process.env.PWD
});

console.log('OSS Config:', {
accessKeyId: config.accessKeyId ? '***' : undefined,
accessKeySecret: config.accessKeySecret ? '***' : undefined,
bucket: config.bucket,
endpoint: config.endpoint
});

Object.entries(config).forEach(([key, value]) => {
if (!value) {
console.error(`Missing OSS configuration: ${key}`);
}
});

module.exports = config;

binární
downloads/edited_1737080856624.xlsx Zobrazit soubor


binární
downloads/edited_1739340265117.xlsx Zobrazit soubor


binární
downloads/edited_1739512520399.xlsx Zobrazit soubor


binární
downloads/edited_1742457844022.xlsx Zobrazit soubor


binární
downloads/edited_1742458401902.xlsx Zobrazit soubor


binární
downloads/edited_1742462419471.xlsx Zobrazit soubor


+ 19
- 0
package.json Zobrazit soubor

{
"name": "goods-excel",
"version": "1.0.0",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"ali-oss": "^6.17.1",
"dotenv": "^16.0.3",
"express": "^4.17.1",
"multer": "^1.4.5-lts.1",
"xlsx": "^0.18.5"
}
}

+ 710
- 0
public/index.html Zobrazit soubor

<!DOCTYPE html>
<html>

<head>
<title>Excel 编辑器</title>
<style>
body {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
font-size: 12px;
background-color: #f0f0f0;
color: #333;

}

.container {
max-width: 100%;
}

.toolbar {
position: sticky;
top: 0;
background: #fff;
padding: 10px 0;
margin-bottom: 15px;
border-bottom: 1px solid #ddd;
z-index: 100;
display: flex;
gap: 10px;
align-items: center;
}

.file-controls {
display: flex;
gap: 10px;
}

.column-controls {
margin-left: 20px;
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
}

.column-checkbox {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background: #f5f5f5;
border-radius: 4px;
cursor: pointer;
}

.column-checkbox:hover {
background: #e8e8e8;
}

button {
padding: 6px 12px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}

button:hover {
background: #45a049;
}

input[type="file"] {
padding: 6px;
border: 1px solid #ddd;
border-radius: 4px;
}

table {
border-collapse: collapse;
width: 100%;
background: white;
font-size: 12px;
}

th,
td {
border: 1px solid #ddd;
padding: 0;
vertical-align: top;
}

th {
background: #f5f5f5;
position: sticky;
top: 60px;
z-index: 90;
}

textarea {
width: 100%;
min-height: 100px;
resize: vertical;
padding: 0;
border: 0;
border-radius: 4px;
font-size: 12px;
box-sizing: border-box;
}

.index-column {
width: 60px;
text-align: center;
background-color: #f5f5f5;
position: sticky;
left: 0;
z-index: 80;
}

th.index-column {
z-index: 91;
}

.image-upload-container {
margin-top: 5px;
}

.preview-button {
margin-top: 5px;
background: #2196F3;
}

.preview-button:hover {
background: #1976D2;
}

.preview-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 1000;
}

.preview-content {
position: relative;
width: 80%;
height: 80%;
margin: 5% auto;
background: white;
padding: 20px;
overflow: auto;
border-radius: 8px;
}

.close-preview {
position: absolute;
right: 20px;
top: 10px;
font-size: 24px;
cursor: pointer;
}

.h5-preview-content {
max-width: 375px;
margin: 0 auto;
background: #fff;
}

.h5-preview-content img {
max-width: 100%;
height: auto;
}

.hidden-column {
display: none;
}

.quick-actions {
margin-left: 20px;
display: flex;
gap: 10px;
}

.help-button {
background: #607D8B;
color: white;
border: none;
border-radius: 50%;
width: 24px;
height: 24px;
cursor: pointer;
font-weight: bold;
}

.help-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 1000;
}

.help-content {
position: relative;
width: 60%;
max-height: 80%;
margin: 5% auto;
background: white;
padding: 20px;
border-radius: 8px;
overflow-y: auto;
}

.shortcut-key {
background: #f0f0f0;
padding: 2px 6px;
border-radius: 3px;
font-family: monospace;
}

.status-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #f5f5f5;
padding: 8px 20px;
border-top: 1px solid #ddd;
display: flex;
justify-content: space-between;
z-index: 100;
}

.unsaved-changes {
color: #f44336;
font-weight: bold;
}

.save-reminder {
position: fixed;
bottom: 40px;
right: 20px;
background: #ff9800;
color: white;
padding: 10px 20px;
border-radius: 4px;
display: none;
animation: bounce 1s infinite;
}

@keyframes bounce {

0%,
100% {
transform: translateY(0);
}

50% {
transform: translateY(-5px);
}
}

.search-box {
padding: 6px 12px;
border: 1px solid #ddd;
border-radius: 4px;
margin-right: 10px;
}

.highlight {
background-color: #fff176;
}

.column-preset {
padding: 4px 8px;
background: #e3f2fd;
border: 1px solid #90caf9;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
}
</style>
</head>

<body>
<div class="container">
<div class="toolbar">
<div class="file-controls">
<input type="file" id="fileInput" accept=".xlsx">
<button onclick="uploadFile()">上传</button>
<button onclick="saveChanges()" id="saveButton">保存</button>
<button class="help-button" onclick="showHelp()">?</button>
</div>
<div class="quick-actions">
<input type="text" class="search-box" placeholder="搜索内容..." onkeyup="searchContent(this.value)">
<button onclick="toggleCommonColumns()">常用列</button>
<button onclick="toggleAllColumns()">显示/隐藏所有列</button>
</div>

</div>
<div class="column-controls" id="columnControls">
<!-- 列控制复选框 -->
</div>
<div id="tableContainer"></div>
</div>

<!-- 帮助模态框 -->
<div id="helpModal" class="help-modal">
<div class="help-content">
<span class="close-preview" onclick="closeHelp()">&times;</span>
<h2>使用帮助</h2>
<h3>快捷键</h3>
<ul>
<li><span class="shortcut-key">Ctrl + S</span> - 保存更改</li>
<li><span class="shortcut-key">Ctrl + F</span> - 搜索内容</li>
<li><span class="shortcut-key">Tab</span> - 切换到下一个单元格</li>
<li><span class="shortcut-key">Shift + Tab</span> - 切换到上一个单元格</li>
</ul>
<h3>常用功能</h3>
<ul>
<li>点击"常用列"可以快速显示常用编辑的列</li>
<li>使用搜索框可以快速定位内容</li>
<li>双击单元格可以快速编辑</li>
<li>图片上传支持多选</li>
</ul>
<h3>注意事项</h3>
<ul>
<li>有未保存的更改时会有提醒</li>
<li>建议定期保存更改</li>
<li>图片上传完成后请等待提示再继续操作</li>
</ul>
</div>
</div>

<div class="status-bar">
<span id="statusText">就绪</span>
<span id="unsavedChanges" class="unsaved-changes" style="display: none;">有未保存的更改</span>
</div>

<div id="saveReminder" class="save-reminder">
请记得保存更改!
</div>

<div id="previewModal" class="preview-modal">
<div class="preview-content">
<span class="close-preview" onclick="closePreview()">&times;</span>
<div id="previewContent"></div>
</div>
</div>

<script src="https://gosspublic.alicdn.com/aliyun-oss-sdk-6.16.0.min.js"></script>
<script>
let headers = [];
let data = [];
let columnVisibility = {};
let hasUnsavedChanges = false;
const commonColumns = ['SKU商品描述(PC)', 'SKU商品描述(h5)', 'SKU商品名称'];

function initColumnControls() {
const container = document.getElementById('columnControls');
container.innerHTML = headers.map(header => `
<label class="column-checkbox">
<input type="checkbox"
checked
onchange="toggleColumn('${header}')"
data-column="${header}">
${header}
</label>
`).join('');

// 初始化列可见性状态
headers.forEach(header => {
columnVisibility[header] = true;
});
}

function toggleColumn(header) {
columnVisibility[header] = !columnVisibility[header];
const cells = document.querySelectorAll(`[data-column="${header}"]`);
cells.forEach(cell => {
if (cell.tagName === 'INPUT') {
// 复选框状态
cell.checked = columnVisibility[header];
} else {
// 表格单元格
cell.classList.toggle('hidden-column');
}
});
}

function renderTable() {
const container = document.getElementById('tableContainer');
let html = '<table><tr>';

html += '<th class="index-column">序号</th>';
headers.forEach(header => {
html += `<th data-column="${header}" ${!columnVisibility[header] ? 'class="hidden-column"' : ''}>${header}</th>`;
});
html += '</tr>';

data.forEach((row, rowIndex) => {
html += '<tr>';
html += `<td class="index-column">${rowIndex + 1}</td>`;
headers.forEach(header => {
html += `<td data-column="${header}" ${!columnVisibility[header] ? 'class="hidden-column"' : ''}>
<textarea
data-row="${rowIndex}"
data-header="${header}"
onchange="updateData(${rowIndex}, '${header}', this.value)"
>${row[header] || ''}</textarea>
${header === 'SKU商品描述(PC)' ? `
<div class="image-upload-container">
<input type="file" accept="image/*"
onchange="uploadImage(this, ${rowIndex}, '${header}')" multiple>
</div>
<button class="preview-button" onclick="previewContent(${rowIndex}, '${header}', 'pc')">
预览
</button>
` : ''}
${header === 'SKU商品描述(h5)' ? `
<button class="preview-button" onclick="previewContent(${rowIndex}, '${header}', 'h5')">
预览
</button>
` : ''}
</td>`;
});
html += '</tr>';
});

html += '</table>';
container.innerHTML = html;
}

async function uploadFile() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) return;

const formData = new FormData();
formData.append('excelFile', file);

const response = await fetch('/upload', {
method: 'POST',
body: formData
});
const result = await response.json();

headers = result.headers;
data = result.data;

// 初始化列控制
initColumnControls();
renderTable();
}

function updateData(rowIndex, header, value) {
data[rowIndex][header] = value;
hasUnsavedChanges = true;
document.getElementById('unsavedChanges').style.display = 'block';
document.getElementById('saveReminder').style.display = 'block';
updateStatus('有未保存的更改');
}

async function saveChanges() {
updateStatus('正在保存...');
try {
const response = await fetch('/save', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ headers, data })
});

const result = await response.json();

if (!response.ok) {
if (result.message) {
alert(result.message);
} else {
alert('保存失败:' + (result.error || '未知错误'));
}
return;
}

window.location.href = result.downloadUrl;
hasUnsavedChanges = false;
document.getElementById('unsavedChanges').style.display = 'none';
document.getElementById('saveReminder').style.display = 'none';
updateStatus('保存成功');
} catch (error) {
updateStatus('保存失败:' + error.message);
}
}

async function uploadImage(input, rowIndex, header) {
const files = input.files;
if (!files.length) return;

try {
updateStatus('正在上传图片...');

const formData = new FormData();
Array.from(files).forEach(file => {
formData.append('images', file);
});

const response = await fetch('/upload-images', {
method: 'POST',
body: formData
});

if (!response.ok) {
throw new Error('上传失败');
}

const results = await response.json();

// 生成PC描述HTML
const pcImageHtml = results.map(result =>
`<p><img src="${result.url}" title="${result.name}"></p>`
).join('\n');

// 生成H5描述JSON
const h5ImageJson = results.map(result => ({
type: "image",
content: result.url,
checked: false,
edit_checked: false
}));

// 更新PC描述
const pcTextarea = input.parentElement.previousElementSibling;
const currentPcValue = pcTextarea.value || '';
pcTextarea.value = currentPcValue + (currentPcValue ? '\n' : '') + pcImageHtml;
updateData(rowIndex, header, pcTextarea.value);

// 更新H5描述
const h5Header = 'SKU商品描述(h5)';
const currentH5Value = data[rowIndex][h5Header] || '[]';
let h5Data;
try {
h5Data = JSON.parse(currentH5Value);
if (!Array.isArray(h5Data)) h5Data = [];
} catch {
h5Data = [];
}

// 合并新的图片数据
h5Data.push(...h5ImageJson);

// 更新H5数据
const h5Value = JSON.stringify(h5Data);
updateData(rowIndex, h5Header, h5Value);

// 更新表格显示
const h5Textarea = document.querySelector(`textarea[data-row="${rowIndex}"][data-header="${h5Header}"]`);
if (h5Textarea) {
h5Textarea.value = h5Value;
}

// 清空文件输入框
input.value = '';

updateStatus('图片上传成功');

} catch (error) {
console.error('Upload error:', error);
alert('上传图片失败:' + error.message);
updateStatus('图片上传失败');
}
}

function previewContent(rowIndex, header, type) {
const content = data[rowIndex][header] || '';
const modal = document.getElementById('previewModal');
const previewContent = document.getElementById('previewContent');

if (type === 'pc') {
// PC预览直接显示HTML
previewContent.innerHTML = content;
} else if (type === 'h5') {
// H5预览需要解析JSON并生成预览内容
try {
const h5Data = JSON.parse(content);
const h5Html = `
<div class="h5-preview-content">
${h5Data.map(item => {
if (item.type === 'image') {
return `<img src="${item.content}" alt="">`;
}
return '';
}).join('')}
</div>
`;
previewContent.innerHTML = h5Html;
} catch (error) {
previewContent.innerHTML = '预览失败:无效的JSON格式';
}
}

modal.style.display = 'block';
}

function closePreview() {
document.getElementById('previewModal').style.display = 'none';
}

// 点击模态框背景关闭预览
document.getElementById('previewModal').addEventListener('click', function (e) {
if (e.target === this) {
closePreview();
}
});

// 添加快捷键支持
document.addEventListener('keydown', function (e) {
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
saveChanges();
}
if (e.ctrlKey && e.key === 'f') {
e.preventDefault();
document.querySelector('.search-box').focus();
}
});

// 搜索功能
function searchContent(searchText) {
if (!searchText) {
clearHighlights();
return;
}

const textareas = document.querySelectorAll('textarea');
textareas.forEach(textarea => {
const content = textarea.value.toLowerCase();
if (content.includes(searchText.toLowerCase())) {
textarea.classList.add('highlight');
textarea.parentElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
} else {
textarea.classList.remove('highlight');
}
});
}

function clearHighlights() {
document.querySelectorAll('.highlight').forEach(el => {
el.classList.remove('highlight');
});
}

// 常用列切换
function toggleCommonColumns() {
headers.forEach(header => {
const isCommon = commonColumns.includes(header);
columnVisibility[header] = isCommon;
const checkbox = document.querySelector(`input[data-column="${header}"]`);
if (checkbox) {
checkbox.checked = isCommon;
}
});
renderTable();
}

// 全部列切换
function toggleAllColumns() {
const allVisible = headers.every(header => columnVisibility[header]);
headers.forEach(header => {
columnVisibility[header] = !allVisible;
const checkbox = document.querySelector(`input[data-column="${header}"]`);
if (checkbox) {
checkbox.checked = !allVisible;
}
});
renderTable();
}

function updateStatus(text) {
document.getElementById('statusText').textContent = text;
}

// 帮助功能
function showHelp() {
document.getElementById('helpModal').style.display = 'block';
}

function closeHelp() {
document.getElementById('helpModal').style.display = 'none';
}

// 离开页面提醒
window.addEventListener('beforeunload', function (e) {
if (hasUnsavedChanges) {
e.preventDefault();
e.returnValue = '有未保存的更改,确定要离开吗?';
}
});
</script>
</body>

</html>

+ 94
- 0
routes/upload.js Zobrazit soubor

const express = require('express');
const router = express.Router();
const multer = require('multer');
const OSS = require('ali-oss');
const config = require('../config/oss');
const path = require('path');
const fs = require('fs');

// 确保上传目录存在
const uploadsDir = path.join(__dirname, '../uploads');
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir, { recursive: true });
}

const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: 5 * 1024 * 1024 // 限制5MB
}
}).array('images');

router.post('/upload-images', (req, res) => {
upload(req, res, async function(err) {
if (err instanceof multer.MulterError) {
console.error('Multer error:', err);
return res.status(400).json({ error: '文件上传错误: ' + err.message });
} else if (err) {
console.error('Unknown error:', err);
return res.status(500).json({ error: '未知错误: ' + err.message });
}

try {
if (!req.files || req.files.length === 0) {
return res.status(400).json({ error: '没有上传文件' });
}

// 验证OSS配置
if (!config.accessKeyId || !config.accessKeySecret || !config.bucket || !config.endpoint) {
console.error('OSS configuration missing:', config);
return res.status(500).json({ error: 'OSS配置错误' });
}

const client = new OSS({
accessKeyId: config.accessKeyId,
accessKeySecret: config.accessKeySecret,
bucket: config.bucket,
endpoint: config.endpoint,
secure: true
});

const uploadPromises = req.files.map(async file => {
try {
const fileExt = file.originalname.split('.').pop().toLowerCase();
// 验证文件类型
const allowedTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
if (!allowedTypes.includes(fileExt)) {
throw new Error(`不支持的文件类型: ${fileExt}`);
}

const uniqueId = Math.random().toString(36).substring(2) + Date.now();
const fileName = `ueditor/${uniqueId}.${fileExt}`;
console.log('Uploading file to OSS:', fileName);
const result = await client.put(fileName, file.buffer);
console.log('Upload result:', result);
return {
url: `https://${config.bucket}.${config.endpoint}/${fileName}`,
name: fileName.split('/').pop()
};
} catch (error) {
console.error('Individual file upload error:', error);
throw new Error(`文件 ${file.originalname} 上传失败: ${error.message}`);
}
});

console.log('Processing all uploads...');
const results = await Promise.all(uploadPromises);
console.log('Upload results:', results);
res.json(results);

} catch (error) {
console.error('Upload error:', error);
res.status(500).json({
error: error.message || '上传失败',
details: error.stack
});
}
});
});

module.exports = router;

binární
uploads/021026947b8e2765cff3c159b4575c8a Zobrazit soubor


binární
uploads/0f98de8a83aeb547d554dd69affd264c Zobrazit soubor


binární
uploads/1714d8f99fd82197eb3a6b3578c580fb Zobrazit soubor


binární
uploads/1c3bdaf4a6052c238f4c8f14a1481142 Zobrazit soubor


binární
uploads/2c3e49f94f0598133d8c28212c52c676 Zobrazit soubor


binární
uploads/32c0b3ed6261e53ebc9c3c655c6f0034 Zobrazit soubor


binární
uploads/398c8464f79caee71670907ab5b33363 Zobrazit soubor


binární
uploads/422ff7314608a05de33738bb25506186 Zobrazit soubor


binární
uploads/439f254fc7015b04c360a717ae8455f1 Zobrazit soubor


binární
uploads/44baab9e90d03f5b1bddb5a7fd6727b7 Zobrazit soubor


binární
uploads/47ce5f78e785832427a4cb820181a3d7 Zobrazit soubor


binární
uploads/4b5ec563e1da9169678c36ac3e6ef55f Zobrazit soubor


binární
uploads/568caef389174b75123b84f84bde41f7 Zobrazit soubor


binární
uploads/5bef95156eaeac6bf6a254e806006900 Zobrazit soubor


binární
uploads/5e05c813b3244bd4be8ffdd9d5dba2b5 Zobrazit soubor


binární
uploads/65aa04d414afa7992ab79937a7756951 Zobrazit soubor


binární
uploads/6b9401f11b16d109a9f4b3013814284e Zobrazit soubor


binární
uploads/70a35e1947a209ceac6a4c8a3211cb60 Zobrazit soubor


binární
uploads/7bcbe690fcd02456f6ca06f0122dd739 Zobrazit soubor


binární
uploads/7d3b961ebd1960e10aae86db9af91036 Zobrazit soubor


binární
uploads/7eff93203beecd1578548f2032b032dd Zobrazit soubor


binární
uploads/7f640b4e3250431d9d06ec4ddfdb049b Zobrazit soubor


binární
uploads/81eac194e46108db3a17ea0df63c7207 Zobrazit soubor


binární
uploads/82df0237f7f5685fbc7b21cd6af1b389 Zobrazit soubor


binární
uploads/843f27324cff13c805e593d3b2b49c5b Zobrazit soubor


binární
uploads/86848f33e40139c4f0e047ac2e0ef490 Zobrazit soubor


binární
uploads/880b956b7feef99f4fd4e9feeb2a1ba9 Zobrazit soubor


binární
uploads/8dae43608bf898c96a68bd6b219b3e88 Zobrazit soubor


binární
uploads/97be1001e3379decc8348023af08af88 Zobrazit soubor


binární
uploads/983344a3b07c6c038f889f803cfa2ce5 Zobrazit soubor


binární
uploads/a8d856d46190fd283a6b8ce8b30d593e Zobrazit soubor


binární
uploads/abd043c11fb2d7312ada0ce5420796fe Zobrazit soubor


binární
uploads/b2e8d9f5bf0fcf64c463816b447181e9 Zobrazit soubor


binární
uploads/be664b230914444e441c2e595db1607c Zobrazit soubor


binární
uploads/c19f72b1bd4675e99ecc54dece34b2ba Zobrazit soubor


binární
uploads/c260ce1326761d968d154419e11f3a67 Zobrazit soubor


binární
uploads/c7a2102d732a4ce2c0a6f9e725ba046e Zobrazit soubor


binární
uploads/d1be6b3a36d1c957813a6519f4cc13b9 Zobrazit soubor


binární
uploads/d29606a92390f4d15dc664c5de0c1709 Zobrazit soubor


binární
uploads/d6177affe2c9278cd8f025669515c2ea Zobrazit soubor


binární
uploads/da66242941096c2f7d770dddd55a4d00 Zobrazit soubor


binární
uploads/e3ab0b96c8f1c48d89fd7932f5064203 Zobrazit soubor


binární
uploads/f0d3e0272d5fa39becfd5a24ad1b369a Zobrazit soubor


binární
uploads/f6c5326074c6c84efefa1bd3b200abb2 Zobrazit soubor


Načítá se…
Zrušit
Uložit