add word translate
This commit is contained in:
parent
d33e96e662
commit
3eeb9f66b9
91
README.md
91
README.md
@ -1,51 +1,88 @@
|
|||||||
# 丰链工具箱 Chrome 扩展
|
# 丰链工具箱 Chrome 扩展
|
||||||
|
|
||||||
一个实用的 Chrome 扩展工具集合,目前包含二维码生成器和 Word 转 HTML 功能。
|
一个集成多种实用工具的 Chrome 扩展,包括二维码生成、Word 转 HTML、Word 文档翻译等功能。
|
||||||
|
|
||||||
## 功能特性
|
## 功能特点
|
||||||
|
|
||||||
### 二维码生成器 🔲
|
### 1. 二维码生成
|
||||||
- 自动获取当前标签页 URL 并生成二维码
|
- 自动获取当前标签页 URL 并生成二维码
|
||||||
- 支持手动输入文本/链接生成二维码
|
- 支持手动输入文本或链接生成二维码
|
||||||
- 高清晰度二维码输出
|
- 高清晰度二维码输出
|
||||||
|
|
||||||
### Word 转 HTML 转换器 📄
|
### 2. Word 转 HTML
|
||||||
- 支持 .docx 文件转换为 HTML
|
- 支持 .docx 文件转换为 HTML
|
||||||
|
- 保持原文档格式和样式
|
||||||
- 实时预览转换结果
|
- 实时预览转换结果
|
||||||
- 保留文档格式和样式
|
- 一键下载 HTML 文件
|
||||||
- 支持下载转换后的 HTML 文件
|
|
||||||
|
|
||||||
## 安装方法
|
### 3. Word 文档翻译
|
||||||
|
- 支持中文文档翻译为英文、日文、繁体中文
|
||||||
|
- 完整保留原文档格式(包括字体、颜色、加粗等样式)
|
||||||
|
- 支持表格内容翻译
|
||||||
|
- 实时显示翻译进度
|
||||||
|
- 保持文档的换行格式
|
||||||
|
- 自动重试机制,提高翻译稳定性
|
||||||
|
|
||||||
|
## 安装说明
|
||||||
|
|
||||||
|
### 扩展安装
|
||||||
1. 下载本项目代码
|
1. 下载本项目代码
|
||||||
2. 打开 Chrome 浏览器,进入扩展程序页面 (chrome://extensions/)
|
2. 打开 Chrome 浏览器,进入扩展管理页面 (chrome://extensions/)
|
||||||
3. 开启"开发者模式"
|
3. 开启"开发者模式"
|
||||||
4. 点击"加载已解压的扩展程序"
|
4. 点击"加载已解压的扩展程序",选择项目目录
|
||||||
5. 选择项目文件夹即可完成安装
|
|
||||||
|
|
||||||
## 使用方法
|
### 后端服务安装
|
||||||
|
1. 确保已安装 Python 3.8+
|
||||||
|
2. 进入 backend 目录
|
||||||
|
3. 创建虚拟环境(可选):
|
||||||
|
```bash
|
||||||
|
python -m venv venv
|
||||||
|
# Windows
|
||||||
|
venv\Scripts\activate
|
||||||
|
# Mac/Linux
|
||||||
|
source venv/bin/activate
|
||||||
|
```
|
||||||
|
4. 安装依赖:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
5. 启动后端服务:
|
||||||
|
```bash
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
1. 点击 Chrome 工具栏中的扩展图标打开工具箱
|
## 使用说明
|
||||||
2. 选择需要使用的功能标签页
|
|
||||||
3. 根据需要使用相应功能
|
|
||||||
|
|
||||||
### 二维码生成
|
### 二维码生成
|
||||||
- 自动显示当前页面的二维码
|
1. 点击扩展图标打开工具箱
|
||||||
- 可以在输入框中输入其他文本/链接并点击"生成二维码"
|
2. 默认显示当前页面 URL 的二维码
|
||||||
|
3. 可在输入框中修改文本,点击"生成二维码"更新
|
||||||
|
|
||||||
### Word 转 HTML
|
### Word 转 HTML
|
||||||
- 点击选择文件上传 Word 文档
|
1. 选择"Word转HTML"选项卡
|
||||||
- 等待转换完成后预览效果
|
2. 点击上传区域选择 .docx 文件
|
||||||
- 点击"下载 HTML"保存转换结果
|
3. 等待转换完成后预览结果
|
||||||
|
4. 点击"下载HTML"保存文件
|
||||||
|
|
||||||
|
### Word 文档翻译
|
||||||
|
1. 选择"Word翻译"选项卡
|
||||||
|
2. 点击上传区域选择要翻译的 Word 文档
|
||||||
|
3. 从下拉菜单选择目标语言(英文/日文/繁体中文)
|
||||||
|
4. 点击"开始翻译"按钮
|
||||||
|
5. 等待翻译完成,自动下载翻译后的文档
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 使用翻译功能时需要确保后端服务正在运行
|
||||||
|
2. 翻译功能使用百度翻译 API,请确保网络连接正常
|
||||||
|
3. 大文件翻译可能需要较长时间,请耐心等待
|
||||||
|
4. 建议定期清理 temp 目录下的临时文件
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
- HTML5
|
- 前端:HTML, CSS, JavaScript
|
||||||
- CSS3
|
- 后端:Python, Flask
|
||||||
- JavaScript
|
- 依赖库:python-docx, mammoth.js, qrcode.js
|
||||||
- Chrome Extension API
|
|
||||||
- QRCode.js
|
|
||||||
- Mammoth.js
|
|
||||||
|
|
||||||
## 开发者
|
## 开发者
|
||||||
|
|
||||||
@ -53,4 +90,4 @@ Made by Scout
|
|||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
[添加许可证类型]
|
MIT License
|
85
backend/app.py
Normal file
85
backend/app.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
from flask import Flask, request, send_file
|
||||||
|
from flask_cors import CORS
|
||||||
|
import os
|
||||||
|
from utils.docx_translator import DocxTranslator
|
||||||
|
import shutil
|
||||||
|
import time
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
CORS(app, resources={
|
||||||
|
r"/*": {
|
||||||
|
"origins": ["chrome-extension://*"],
|
||||||
|
"methods": ["POST", "OPTIONS", "GET"],
|
||||||
|
"allow_headers": ["Content-Type"]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# 全局变量存储翻译进度
|
||||||
|
translation_progress = 0
|
||||||
|
|
||||||
|
UPLOAD_FOLDER = 'temp'
|
||||||
|
if not os.path.exists(UPLOAD_FOLDER):
|
||||||
|
os.makedirs(UPLOAD_FOLDER)
|
||||||
|
|
||||||
|
def update_progress(progress):
|
||||||
|
global translation_progress
|
||||||
|
translation_progress = progress
|
||||||
|
|
||||||
|
@app.route('/progress', methods=['GET'])
|
||||||
|
def get_progress():
|
||||||
|
return {'progress': translation_progress}
|
||||||
|
|
||||||
|
@app.route('/translate', methods=['POST'])
|
||||||
|
def translate_docx():
|
||||||
|
global translation_progress
|
||||||
|
translation_progress = 0
|
||||||
|
input_path = None
|
||||||
|
output_path = None
|
||||||
|
|
||||||
|
if 'file' not in request.files:
|
||||||
|
return {'error': 'No file provided'}, 400
|
||||||
|
|
||||||
|
file = request.files['file']
|
||||||
|
target_lang = request.form.get('target_lang', 'en')
|
||||||
|
|
||||||
|
if file.filename == '':
|
||||||
|
return {'error': 'No file selected'}, 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
timestamp = str(int(time.time()))
|
||||||
|
safe_filename = f"{timestamp}_{file.filename}"
|
||||||
|
input_path = os.path.join(UPLOAD_FOLDER, safe_filename)
|
||||||
|
file.save(input_path)
|
||||||
|
|
||||||
|
translator = DocxTranslator()
|
||||||
|
output_path = translator.translate_document(
|
||||||
|
input_path,
|
||||||
|
target_lang=target_lang,
|
||||||
|
progress_callback=update_progress
|
||||||
|
)
|
||||||
|
|
||||||
|
temp_output = output_path + '.tmp'
|
||||||
|
shutil.copy2(output_path, temp_output)
|
||||||
|
|
||||||
|
return send_file(
|
||||||
|
temp_output,
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=f"translated_{file.filename}"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {'error': str(e)}, 500
|
||||||
|
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
if input_path and os.path.exists(input_path):
|
||||||
|
os.remove(input_path)
|
||||||
|
if output_path and os.path.exists(output_path):
|
||||||
|
os.remove(output_path)
|
||||||
|
if 'temp_output' in locals() and os.path.exists(temp_output):
|
||||||
|
os.remove(temp_output)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error cleaning up files: {e}")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(debug=True, port=5000)
|
5
backend/requirements.txt
Normal file
5
backend/requirements.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
flask==2.3.3
|
||||||
|
werkzeug==2.3.7
|
||||||
|
flask-cors==4.0.0
|
||||||
|
python-docx==0.8.11
|
||||||
|
requests==2.31.0
|
BIN
backend/utils/__pycache__/docx_translator.cpython-310.pyc
Normal file
BIN
backend/utils/__pycache__/docx_translator.cpython-310.pyc
Normal file
Binary file not shown.
244
backend/utils/docx_translator.py
Normal file
244
backend/utils/docx_translator.py
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
from docx import Document
|
||||||
|
import hashlib
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
|
||||||
|
class DocxTranslator:
|
||||||
|
def __init__(self):
|
||||||
|
self.appid = "20241112002200806"
|
||||||
|
self.secret_key = "preM0becByYCdotRTP_a"
|
||||||
|
self.api_url = "https://api.fanyi.baidu.com/api/trans/vip/translate"
|
||||||
|
self.max_length = 2000
|
||||||
|
self.lang_map = {
|
||||||
|
'en': 'en', # English
|
||||||
|
'jp': 'jp', # Japanese
|
||||||
|
'cht': 'cht' # Traditional Chinese
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_sign(self, text, salt):
|
||||||
|
sign_str = f"{self.appid}{text}{salt}{self.secret_key}"
|
||||||
|
return hashlib.md5(sign_str.encode()).hexdigest()
|
||||||
|
|
||||||
|
def translate_text(self, text, to_lang):
|
||||||
|
target_lang = self.lang_map.get(to_lang, to_lang)
|
||||||
|
|
||||||
|
if len(text) > self.max_length:
|
||||||
|
segments = text.split('。')
|
||||||
|
current_segment = ''
|
||||||
|
translated_segments = []
|
||||||
|
|
||||||
|
for segment in segments:
|
||||||
|
if len(current_segment) + len(segment) < self.max_length:
|
||||||
|
current_segment += segment + '。'
|
||||||
|
else:
|
||||||
|
if current_segment:
|
||||||
|
translated_segments.append(self._translate_segment(current_segment, target_lang))
|
||||||
|
current_segment = segment + '。'
|
||||||
|
|
||||||
|
if current_segment:
|
||||||
|
translated_segments.append(self._translate_segment(current_segment, target_lang))
|
||||||
|
|
||||||
|
return ''.join(translated_segments)
|
||||||
|
else:
|
||||||
|
return self._translate_segment(text, target_lang)
|
||||||
|
|
||||||
|
def _translate_segment(self, text, to_lang):
|
||||||
|
if not text.strip():
|
||||||
|
return text
|
||||||
|
|
||||||
|
salt = str(int(time.time()))
|
||||||
|
sign = self._get_sign(text, salt)
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'q': text,
|
||||||
|
'from': 'zh',
|
||||||
|
'to': to_lang,
|
||||||
|
'appid': self.appid,
|
||||||
|
'salt': salt,
|
||||||
|
'sign': sign
|
||||||
|
}
|
||||||
|
|
||||||
|
max_retries = 3
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
try:
|
||||||
|
response = requests.get(self.api_url, params=params)
|
||||||
|
result = response.json()
|
||||||
|
|
||||||
|
if 'error_code' in result:
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
time.sleep(1)
|
||||||
|
continue
|
||||||
|
raise Exception(f"Translation error: {result['error_msg']}")
|
||||||
|
|
||||||
|
return result['trans_result'][0]['dst']
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
time.sleep(1)
|
||||||
|
continue
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def translate_document(self, input_path, target_lang='en', progress_callback=None):
|
||||||
|
doc = Document(input_path)
|
||||||
|
output_path = input_path.replace('.docx', f'_translated.docx')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 计算总翻译项
|
||||||
|
total_items = len(doc.paragraphs)
|
||||||
|
for table in doc.tables:
|
||||||
|
total_items += sum(len(row.cells) for row in table.rows)
|
||||||
|
|
||||||
|
current_item = 0
|
||||||
|
|
||||||
|
# 翻译段落
|
||||||
|
for paragraph in doc.paragraphs:
|
||||||
|
if paragraph.text.strip():
|
||||||
|
try:
|
||||||
|
# 保存原始的段落格式和换行
|
||||||
|
runs = paragraph.runs
|
||||||
|
original_runs = []
|
||||||
|
|
||||||
|
# 收集每个run的文本和格式信息,保留原始换行符
|
||||||
|
for run in runs:
|
||||||
|
text = run.text
|
||||||
|
if text: # 不去除空白字符,保留原始格式
|
||||||
|
original_runs.append({
|
||||||
|
'text': text,
|
||||||
|
'bold': run.bold,
|
||||||
|
'italic': run.italic,
|
||||||
|
'underline': run.underline,
|
||||||
|
'font_name': run.font.name,
|
||||||
|
'font_size': run.font.size,
|
||||||
|
'color_rgb': run.font.color.rgb if run.font.color else None,
|
||||||
|
'break_type': 'break' if any(text.endswith(x) for x in ['\n', '\v', '\r']) else None
|
||||||
|
})
|
||||||
|
|
||||||
|
# 清除原有内容但保持段落格式
|
||||||
|
paragraph.clear()
|
||||||
|
|
||||||
|
# 分别翻译和添加每个run的文本
|
||||||
|
for i, orig_run in enumerate(original_runs):
|
||||||
|
if orig_run['text']:
|
||||||
|
# 保留换行符
|
||||||
|
has_break = orig_run['break_type'] == 'break'
|
||||||
|
text_to_translate = orig_run['text'].rstrip('\n\r\v')
|
||||||
|
|
||||||
|
# 翻译非空文本
|
||||||
|
if text_to_translate.strip():
|
||||||
|
translated_text = self.translate_text(text_to_translate, target_lang)
|
||||||
|
else:
|
||||||
|
translated_text = text_to_translate
|
||||||
|
|
||||||
|
# 创建新的run并应用格式
|
||||||
|
new_run = paragraph.add_run()
|
||||||
|
new_run.bold = orig_run['bold']
|
||||||
|
new_run.italic = orig_run['italic']
|
||||||
|
new_run.underline = orig_run['underline']
|
||||||
|
new_run.font.name = orig_run['font_name']
|
||||||
|
if orig_run['font_size']:
|
||||||
|
new_run.font.size = orig_run['font_size']
|
||||||
|
if orig_run['color_rgb']:
|
||||||
|
new_run.font.color.rgb = orig_run['color_rgb']
|
||||||
|
|
||||||
|
# 添加翻译后的文本
|
||||||
|
new_run.text = translated_text
|
||||||
|
|
||||||
|
# 如果原文有换行,添加换行符
|
||||||
|
if has_break:
|
||||||
|
new_run.add_break() # 不指定类型,使用默认换行
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error translating paragraph: {e}")
|
||||||
|
# 保留原文和格式
|
||||||
|
paragraph.clear()
|
||||||
|
for run in runs:
|
||||||
|
new_run = paragraph.add_run(run.text)
|
||||||
|
new_run.bold = run.bold
|
||||||
|
new_run.italic = run.italic
|
||||||
|
new_run.underline = run.underline
|
||||||
|
new_run.font.name = run.font.name
|
||||||
|
if run.font.size:
|
||||||
|
new_run.font.size = run.font.size
|
||||||
|
if run.font.color and run.font.color.rgb:
|
||||||
|
new_run.font.color.rgb = run.font.color.rgb
|
||||||
|
|
||||||
|
current_item += 1
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(int((current_item / total_items) * 100))
|
||||||
|
|
||||||
|
# 翻译表格 (使用相同的逻辑处理表格单元格)
|
||||||
|
for table in doc.tables:
|
||||||
|
for row in table.rows:
|
||||||
|
for cell in row.cells:
|
||||||
|
if cell.text.strip():
|
||||||
|
try:
|
||||||
|
for paragraph in cell.paragraphs:
|
||||||
|
runs = paragraph.runs
|
||||||
|
original_runs = []
|
||||||
|
|
||||||
|
for run in runs:
|
||||||
|
text = run.text
|
||||||
|
if text:
|
||||||
|
original_runs.append({
|
||||||
|
'text': text,
|
||||||
|
'bold': run.bold,
|
||||||
|
'italic': run.italic,
|
||||||
|
'underline': run.underline,
|
||||||
|
'font_name': run.font.name,
|
||||||
|
'font_size': run.font.size,
|
||||||
|
'color_rgb': run.font.color.rgb if run.font.color else None,
|
||||||
|
'break_type': 'break' if any(text.endswith(x) for x in ['\n', '\v', '\r']) else None
|
||||||
|
})
|
||||||
|
|
||||||
|
paragraph.clear()
|
||||||
|
|
||||||
|
for i, orig_run in enumerate(original_runs):
|
||||||
|
if orig_run['text']:
|
||||||
|
has_break = orig_run['break_type'] == 'break'
|
||||||
|
text_to_translate = orig_run['text'].rstrip('\n\r\v')
|
||||||
|
|
||||||
|
if text_to_translate.strip():
|
||||||
|
translated_text = self.translate_text(text_to_translate, target_lang)
|
||||||
|
else:
|
||||||
|
translated_text = text_to_translate
|
||||||
|
|
||||||
|
new_run = paragraph.add_run()
|
||||||
|
new_run.bold = orig_run['bold']
|
||||||
|
new_run.italic = orig_run['italic']
|
||||||
|
new_run.underline = orig_run['underline']
|
||||||
|
new_run.font.name = orig_run['font_name']
|
||||||
|
if orig_run['font_size']:
|
||||||
|
new_run.font.size = orig_run['font_size']
|
||||||
|
if orig_run['color_rgb']:
|
||||||
|
new_run.font.color.rgb = orig_run['color_rgb']
|
||||||
|
|
||||||
|
new_run.text = translated_text
|
||||||
|
|
||||||
|
if has_break:
|
||||||
|
new_run.add_break()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error translating cell: {e}")
|
||||||
|
# 保留原文和格式
|
||||||
|
for paragraph in cell.paragraphs:
|
||||||
|
paragraph.clear()
|
||||||
|
for run in runs:
|
||||||
|
new_run = paragraph.add_run(run.text)
|
||||||
|
new_run.bold = run.bold
|
||||||
|
new_run.italic = run.italic
|
||||||
|
new_run.underline = run.underline
|
||||||
|
new_run.font.name = run.font.name
|
||||||
|
if run.font.size:
|
||||||
|
new_run.font.size = run.font.size
|
||||||
|
if run.font.color and run.font.color.rgb:
|
||||||
|
new_run.font.color.rgb = run.font.color.rgb
|
||||||
|
|
||||||
|
current_item += 1
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(int((current_item / total_items) * 100))
|
||||||
|
|
||||||
|
doc.save(output_path)
|
||||||
|
return output_path
|
||||||
|
finally:
|
||||||
|
del doc
|
@ -11,5 +11,10 @@
|
|||||||
"action": {
|
"action": {
|
||||||
"default_popup": "src/html/popup.html"
|
"default_popup": "src/html/popup.html"
|
||||||
},
|
},
|
||||||
"permissions": ["activeTab"]
|
"permissions": [
|
||||||
|
"activeTab"
|
||||||
|
],
|
||||||
|
"host_permissions": [
|
||||||
|
"http://localhost:5000/*"
|
||||||
|
]
|
||||||
}
|
}
|
@ -25,8 +25,9 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
display: flex;
|
display: grid;
|
||||||
gap: 10px;
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 5px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
@ -34,19 +35,24 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tab button {
|
.tab button {
|
||||||
flex: 1;
|
padding: 8px 4px;
|
||||||
padding: 12px;
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 0;
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #666;
|
color: #666;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
font-size: 14px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 8px;
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab button .icon {
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab button:hover {
|
.tab button:hover {
|
||||||
@ -203,11 +209,10 @@ button:hover {
|
|||||||
|
|
||||||
.status-message {
|
.status-message {
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
padding: 10px;
|
padding: 8px 12px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
display: none;
|
text-align: center;
|
||||||
animation: fadeIn 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-message.success {
|
.status-message.success {
|
||||||
@ -241,4 +246,82 @@ button:hover {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 添加新的样式 */
|
||||||
|
.language-select {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-select select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translate-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
background: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translate-btn:hover {
|
||||||
|
background: #45a049;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 添加进度条样式 */
|
||||||
|
.progress-container {
|
||||||
|
margin: 15px 0;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 4px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: #1a73e8;
|
||||||
|
width: 0;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 添加进度文本样式 */
|
||||||
|
.progress-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 5px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修改选项卡文字样式 */
|
||||||
|
.tab button[data-tab="qr"] {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab button[data-tab="qr"]::after {
|
||||||
|
content: "二维码生成";
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab button[data-tab="word"] {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab button[data-tab="word"]::after {
|
||||||
|
content: "Word转HTML";
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab button[data-tab="translate"] {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab button[data-tab="translate"]::after {
|
||||||
|
content: "Word翻译";
|
||||||
}
|
}
|
@ -12,11 +12,12 @@
|
|||||||
<div class="tab">
|
<div class="tab">
|
||||||
<button class="tablinks active" data-tab="qr">
|
<button class="tablinks active" data-tab="qr">
|
||||||
<span class="icon">🔲</span>
|
<span class="icon">🔲</span>
|
||||||
二维码生成
|
|
||||||
</button>
|
</button>
|
||||||
<button class="tablinks" data-tab="word">
|
<button class="tablinks" data-tab="word">
|
||||||
<span class="icon">📄</span>
|
<span class="icon">📄</span>
|
||||||
Word转HTML
|
</button>
|
||||||
|
<button class="tablinks" data-tab="translate">
|
||||||
|
<span class="icon">🔄</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -43,6 +44,32 @@
|
|||||||
下载HTML
|
下载HTML
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="translate" class="tabcontent" style="display:none;">
|
||||||
|
<div class="file-upload">
|
||||||
|
<label for="wordTranslateFile" class="file-label">
|
||||||
|
<span class="icon">📎</span>
|
||||||
|
<span id="translateFileNameDisplay">选择Word文件</span>
|
||||||
|
</label>
|
||||||
|
<input type="file" id="wordTranslateFile" accept=".docx" hidden>
|
||||||
|
</div>
|
||||||
|
<div class="language-select">
|
||||||
|
<select id="targetLang">
|
||||||
|
<option value="en">英文</option>
|
||||||
|
<option value="jp">日文</option>
|
||||||
|
<option value="cht">繁体中文</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="translateProgress" class="progress-container">
|
||||||
|
<div class="progress-bar"></div>
|
||||||
|
</div>
|
||||||
|
<div id="progressText" class="progress-text"></div>
|
||||||
|
<div id="translateStatus" class="status-message"></div>
|
||||||
|
<button id="translateBtn" class="translate-btn" style="display:none;">
|
||||||
|
<span class="icon">🔄</span>
|
||||||
|
开始翻译
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="../js/popup.js"></script>
|
<script src="../js/popup.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
162
src/js/popup.js
162
src/js/popup.js
@ -13,24 +13,41 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const tabs = document.querySelectorAll('.tablinks');
|
const tabs = document.querySelectorAll('.tablinks');
|
||||||
const tabContents = document.querySelectorAll('.tabcontent');
|
const tabContents = document.querySelectorAll('.tabcontent');
|
||||||
|
|
||||||
|
function switchTab(tabName) {
|
||||||
|
// 移除所有标签的激活状态
|
||||||
|
tabs.forEach(tab => tab.classList.remove('active'));
|
||||||
|
|
||||||
|
// 隐藏所有内容
|
||||||
|
tabContents.forEach(content => {
|
||||||
|
content.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 激活选中的标签
|
||||||
|
const selectedTab = document.querySelector(`[data-tab="${tabName}"]`);
|
||||||
|
if (selectedTab) {
|
||||||
|
selectedTab.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示对应内容
|
||||||
|
const selectedContent = document.getElementById(tabName);
|
||||||
|
if (selectedContent) {
|
||||||
|
selectedContent.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为每个标签添加点击事件
|
||||||
tabs.forEach(tab => {
|
tabs.forEach(tab => {
|
||||||
tab.addEventListener('click', (e) => {
|
tab.addEventListener('click', (e) => {
|
||||||
const tabName = e.target.getAttribute('data-tab');
|
const tabName = e.currentTarget.getAttribute('data-tab');
|
||||||
|
if (tabName) {
|
||||||
tabs.forEach(t => t.classList.remove('active'));
|
switchTab(tabName);
|
||||||
tab.classList.add('active');
|
|
||||||
|
|
||||||
tabContents.forEach(content => {
|
|
||||||
content.style.display = 'none';
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedContent = document.getElementById(tabName);
|
|
||||||
if (selectedContent) {
|
|
||||||
selectedContent.style.display = 'block';
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 初始化显示第一个标签页
|
||||||
|
switchTab('qr');
|
||||||
|
|
||||||
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
|
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
|
||||||
if (tabs[0] && tabs[0].url) {
|
if (tabs[0] && tabs[0].url) {
|
||||||
urlInput.value = tabs[0].url;
|
urlInput.value = tabs[0].url;
|
||||||
@ -131,4 +148,125 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
saveAs: true
|
saveAs: true
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 添加百度翻译API配置
|
||||||
|
const BAIDU_APPID = '20241112002200806';
|
||||||
|
const BAIDU_KEY = 'preM0becByYCdotRTP_a';
|
||||||
|
|
||||||
|
let wordTranslateFile = null;
|
||||||
|
|
||||||
|
document.getElementById('wordTranslateFile').addEventListener('change', async (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
wordTranslateFile = file;
|
||||||
|
document.getElementById('translateFileNameDisplay').textContent = file.name;
|
||||||
|
document.getElementById('translateBtn').style.display = 'block';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('translateBtn').addEventListener('click', async () => {
|
||||||
|
if (!wordTranslateFile) return;
|
||||||
|
|
||||||
|
const translateBtn = document.getElementById('translateBtn');
|
||||||
|
const translateStatus = document.getElementById('translateStatus');
|
||||||
|
const progressBar = document.querySelector('.progress-bar');
|
||||||
|
const progressContainer = document.querySelector('.progress-container');
|
||||||
|
const progressText = document.getElementById('progressText');
|
||||||
|
|
||||||
|
// 禁用翻译按钮
|
||||||
|
translateBtn.disabled = true;
|
||||||
|
translateBtn.style.opacity = '0.6';
|
||||||
|
translateBtn.textContent = '翻译中...';
|
||||||
|
|
||||||
|
translateStatus.textContent = '正在处理文档...';
|
||||||
|
progressContainer.style.display = 'block';
|
||||||
|
progressText.style.display = 'block';
|
||||||
|
progressBar.style.width = '0%';
|
||||||
|
|
||||||
|
// 进度检查变量
|
||||||
|
let lastProgress = 0;
|
||||||
|
let noProgressCount = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', wordTranslateFile);
|
||||||
|
formData.append('target_lang', document.getElementById('targetLang').value);
|
||||||
|
|
||||||
|
// 开始翻译请求
|
||||||
|
const response = await fetch('http://localhost:5000/translate', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
// 定时检查进度
|
||||||
|
const progressCheck = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const progressResponse = await fetch('http://localhost:5000/progress');
|
||||||
|
const progressData = await progressResponse.json();
|
||||||
|
|
||||||
|
// 检查进度是否停滞
|
||||||
|
if (progressData.progress === lastProgress) {
|
||||||
|
noProgressCount++;
|
||||||
|
if (noProgressCount > 10) { // 5秒无进度更新
|
||||||
|
clearInterval(progressCheck);
|
||||||
|
throw new Error('翻译进度停滞,请重试');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
noProgressCount = 0;
|
||||||
|
lastProgress = progressData.progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
progressBar.style.width = `${progressData.progress}%`;
|
||||||
|
progressText.textContent = `翻译进度: ${progressData.progress}%`;
|
||||||
|
} catch (error) {
|
||||||
|
clearInterval(progressCheck);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
clearInterval(progressCheck);
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.error || '翻译失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载翻译后的文件
|
||||||
|
const blob = await response.blob();
|
||||||
|
clearInterval(progressCheck);
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `translated_${wordTranslateFile.name}`;
|
||||||
|
a.click();
|
||||||
|
|
||||||
|
translateStatus.textContent = '翻译完成!';
|
||||||
|
progressBar.style.width = '100%';
|
||||||
|
|
||||||
|
// 重置按钮状态
|
||||||
|
setTimeout(() => {
|
||||||
|
translateBtn.disabled = false;
|
||||||
|
translateBtn.style.opacity = '1';
|
||||||
|
translateBtn.innerHTML = '<span class="icon">🔄</span>开始翻译';
|
||||||
|
progressContainer.style.display = 'none';
|
||||||
|
progressText.style.display = 'none';
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
translateStatus.textContent = '翻译失败:' + error.message;
|
||||||
|
progressContainer.style.display = 'none';
|
||||||
|
progressText.style.display = 'none';
|
||||||
|
|
||||||
|
// 重置按钮状态
|
||||||
|
translateBtn.disabled = false;
|
||||||
|
translateBtn.style.opacity = '1';
|
||||||
|
translateBtn.innerHTML = '<span class="icon">🔄</span>开始翻译';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// MD5函数实现
|
||||||
|
function MD5(string) {
|
||||||
|
// 实现MD5加密
|
||||||
|
// 可以使用第三方库如crypto-js
|
||||||
|
}
|
||||||
});
|
});
|
Loading…
Reference in New Issue
Block a user