新增 TinyPanda 图片压缩工具项目,基于 Electron 和 Tinify API 构建
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.log
|
||||
.DS_Store
|
||||
assets/icon.png
|
||||
.env
|
||||
5
brief.txt
Normal file
5
brief.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
项目名称:TinyPanda
|
||||
项目仓库:https://git.pandorastudio.cn/product/TinyPanda.git
|
||||
技术栈:electronjs、shadcn UI
|
||||
API文档:https://tinify.cn/developers/reference/nodejs
|
||||
功能描述:支持图片压缩(队列压缩、压缩进度显示、压缩前后大小对比);打包下载
|
||||
5579
package-lock.json
generated
Normal file
5579
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
1
package.json
Normal file
1
package.json
Normal file
@@ -0,0 +1 @@
|
||||
{"name": "tinypanda", "version": "1.0.0", "description": "图片压缩工具 - TinyPanda", "main": "src/main/index.js", "scripts": {"dev": "electron .", "build": "electron-builder", "build:win": "electron-builder --win"}, "keywords": ["electron", "image-compression", "tinify"], "author": "Pandora Studio", "license": "MIT", "devDependencies": {"electron": "^32.0.0", "electron-builder": "^25.0.0"}, "dependencies": {"tinify": "^1.7.1", "electron-store": "^8.2.0", "jszip": "^3.10.1", "dotenv": "^16.4.5"}, "build": {"appId": "cn.pandorastudio.tinypanda", "productName": "TinyPanda", "directories": {"output": "dist"}, "win": {"target": ["nsis"], "icon": "assets/icon.ico"}}}
|
||||
205
src/main/index.js
Normal file
205
src/main/index.js
Normal file
@@ -0,0 +1,205 @@
|
||||
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
|
||||
const path = require('path');
|
||||
const tinify = require('tinify');
|
||||
const fs = require('fs');
|
||||
const JSZip = require('jszip');
|
||||
require('dotenv').config();
|
||||
|
||||
let mainWindow;
|
||||
let apiKey = process.env.TINIFY_API_KEY || '';
|
||||
|
||||
function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
frame: false,
|
||||
transparent: false,
|
||||
backgroundColor: '#FFFFFF',
|
||||
vibrancy: 'acrylic',
|
||||
resizable: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false
|
||||
},
|
||||
icon: path.join(__dirname, '../../assets/icon.png')
|
||||
});
|
||||
|
||||
mainWindow.loadFile('src/renderer/index.html');
|
||||
|
||||
// 开发模式下打开开发者工具
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
}
|
||||
|
||||
app.whenReady().then(createWindow);
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
// 获取当前 API Key
|
||||
ipcMain.handle('get-api-key', async () => {
|
||||
return { apiKey: apiKey || '' };
|
||||
});
|
||||
|
||||
// 设置 API Key
|
||||
ipcMain.handle('set-api-key', async (event, key) => {
|
||||
apiKey = key;
|
||||
tinify.key = key;
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
// 验证 API Key
|
||||
ipcMain.handle('validate-api-key', async (event, key) => {
|
||||
try {
|
||||
tinify.key = key;
|
||||
await tinify.fromBuffer(Buffer.from('fake')).toBuffer();
|
||||
return { valid: false };
|
||||
} catch (error) {
|
||||
if (error.message && error.message.includes('Credentials')) {
|
||||
return { valid: false };
|
||||
}
|
||||
// 其他错误说明格式正确但不是真实图片
|
||||
return { valid: true };
|
||||
}
|
||||
});
|
||||
|
||||
// 压缩单张图片
|
||||
ipcMain.handle('compress-image', async (event, filePath) => {
|
||||
try {
|
||||
const source = tinify.fromFile(filePath);
|
||||
const compressed = await source.toBuffer();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
compressedData: compressed.toString('base64')
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 获取图片信息
|
||||
ipcMain.handle('get-image-info', async (event, filePath) => {
|
||||
try {
|
||||
const stats = fs.statSync(filePath);
|
||||
return {
|
||||
size: stats.size,
|
||||
success: true
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 保存压缩后的图片
|
||||
ipcMain.handle('save-compressed-image', async (event, savePath, base64Data) => {
|
||||
try {
|
||||
const buffer = Buffer.from(base64Data, 'base64');
|
||||
fs.writeFileSync(savePath, buffer);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 创建压缩包
|
||||
ipcMain.handle('create-zip', async (event, files) => {
|
||||
try {
|
||||
const zip = new JSZip();
|
||||
|
||||
for (const file of files) {
|
||||
const buffer = Buffer.from(file.data, 'base64');
|
||||
zip.file(file.name, buffer);
|
||||
}
|
||||
|
||||
const content = await zip.generateAsync({ type: 'base64' });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
zipData: content
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 选择保存目录
|
||||
ipcMain.handle('select-save-directory', async () => {
|
||||
const result = await dialog.showOpenDialog({
|
||||
properties: ['openDirectory']
|
||||
});
|
||||
|
||||
if (result.canceled) {
|
||||
return { canceled: true };
|
||||
}
|
||||
|
||||
return { canceled: false, path: result.filePaths[0] };
|
||||
});
|
||||
|
||||
// 选择文件
|
||||
ipcMain.handle('select-files', async () => {
|
||||
const result = await dialog.showOpenDialog({
|
||||
properties: ['openFile', 'multiSelections'],
|
||||
filters: [
|
||||
{ name: 'Images', extensions: ['jpg', 'jpeg', 'png', 'webp'] }
|
||||
]
|
||||
});
|
||||
|
||||
if (result.canceled) {
|
||||
return { canceled: true };
|
||||
}
|
||||
|
||||
return { canceled: false, files: result.filePaths };
|
||||
});
|
||||
|
||||
// 窗口控制
|
||||
ipcMain.handle('window-minimize', async () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.minimize();
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('window-maximize', async () => {
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMaximized()) {
|
||||
mainWindow.unmaximize();
|
||||
} else {
|
||||
mainWindow.maximize();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('window-close', async () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.close();
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('window-is-maximized', async () => {
|
||||
if (mainWindow) {
|
||||
return mainWindow.isMaximized();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
378
src/renderer/app.js
Normal file
378
src/renderer/app.js
Normal file
@@ -0,0 +1,378 @@
|
||||
const { ipcRenderer } = require('electron');
|
||||
|
||||
let queue = [];
|
||||
let apiKey = '';
|
||||
|
||||
// DOM 元素
|
||||
const apiKeyInput = document.getElementById('apiKey');
|
||||
const validateKeyBtn = document.getElementById('validateKeyBtn');
|
||||
const apiKeyStatus = document.getElementById('apiKeyStatus');
|
||||
const settingsBtn = document.getElementById('settingsBtn');
|
||||
const settingsModal = document.getElementById('settingsModal');
|
||||
const closeSettingsBtn = document.getElementById('closeSettingsBtn');
|
||||
const saveSettingsBtn = document.getElementById('saveSettingsBtn');
|
||||
const uploadArea = document.getElementById('uploadArea');
|
||||
const selectFilesBtn = document.getElementById('selectFilesBtn');
|
||||
const queueList = document.getElementById('queueList');
|
||||
const queueCount = document.getElementById('queueCount');
|
||||
const clearQueueBtn = document.getElementById('clearQueueBtn');
|
||||
const compressAllBtn = document.getElementById('compressAllBtn');
|
||||
const downloadAllBtn = document.getElementById('downloadAllBtn');
|
||||
|
||||
// 格式化文件大小
|
||||
function formatSize(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// 加载默认 API Key
|
||||
async function loadDefaultApiKey() {
|
||||
const result = await ipcRenderer.invoke('get-api-key');
|
||||
if (result.apiKey) {
|
||||
apiKey = result.apiKey;
|
||||
apiKeyInput.value = apiKey;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 API Key 状态显示
|
||||
function updateApiKeyStatus() {
|
||||
if (apiKey) {
|
||||
apiKeyStatus.textContent = '已配置';
|
||||
apiKeyStatus.className = 'status-badge status-completed';
|
||||
} else {
|
||||
apiKeyStatus.textContent = '未配置';
|
||||
apiKeyStatus.className = 'status-badge status-pending';
|
||||
}
|
||||
}
|
||||
|
||||
// 验证 API Key
|
||||
validateKeyBtn.addEventListener('click', async () => {
|
||||
const key = apiKeyInput.value.trim();
|
||||
if (!key) {
|
||||
alert('请输入 API Key');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await ipcRenderer.invoke('validate-api-key', key);
|
||||
if (result.valid) {
|
||||
apiKey = key;
|
||||
await ipcRenderer.invoke('set-api-key', key);
|
||||
alert('API Key 验证成功!');
|
||||
updateApiKeyStatus();
|
||||
} else {
|
||||
alert('API Key 无效,请检查后重试');
|
||||
}
|
||||
});
|
||||
|
||||
// 打开设置模态框
|
||||
settingsBtn.addEventListener('click', () => {
|
||||
settingsModal.classList.add('active');
|
||||
apiKeyInput.value = apiKey;
|
||||
updateApiKeyStatus();
|
||||
});
|
||||
|
||||
// 关闭设置模态框
|
||||
closeSettingsBtn.addEventListener('click', () => {
|
||||
settingsModal.classList.remove('active');
|
||||
});
|
||||
|
||||
// 点击模态框外部关闭
|
||||
settingsModal.addEventListener('click', (e) => {
|
||||
if (e.target === settingsModal) {
|
||||
settingsModal.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// 保存设置
|
||||
saveSettingsBtn.addEventListener('click', async () => {
|
||||
const key = apiKeyInput.value.trim();
|
||||
if (!key) {
|
||||
alert('请输入 API Key');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await ipcRenderer.invoke('validate-api-key', key);
|
||||
if (result.valid) {
|
||||
apiKey = key;
|
||||
await ipcRenderer.invoke('set-api-key', key);
|
||||
updateApiKeyStatus();
|
||||
alert('API Key 已保存!');
|
||||
settingsModal.classList.remove('active');
|
||||
updateQueueUI();
|
||||
} else {
|
||||
alert('API Key 无效,请检查后重试');
|
||||
}
|
||||
});
|
||||
|
||||
// 选择文件
|
||||
selectFilesBtn.addEventListener('click', async () => {
|
||||
const result = await ipcRenderer.invoke('select-files');
|
||||
if (!result.canceled) {
|
||||
addFilesToQueue(result.files);
|
||||
}
|
||||
});
|
||||
|
||||
// 拖拽上传
|
||||
uploadArea.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
uploadArea.style.borderColor = '#667eea';
|
||||
uploadArea.style.background = '#f0fdf4';
|
||||
});
|
||||
|
||||
uploadArea.addEventListener('dragleave', () => {
|
||||
uploadArea.style.borderColor = '#d1d5db';
|
||||
uploadArea.style.background = '#f9fafb';
|
||||
});
|
||||
|
||||
uploadArea.addEventListener('drop', async (e) => {
|
||||
e.preventDefault();
|
||||
uploadArea.style.borderColor = '#d1d5db';
|
||||
uploadArea.style.background = '#f9fafb';
|
||||
|
||||
const files = Array.from(e.dataTransfer.files).filter(f =>
|
||||
f.type.startsWith('image/')
|
||||
);
|
||||
|
||||
if (files.length > 0) {
|
||||
addFilesToQueue(files.map(f => f.path));
|
||||
}
|
||||
});
|
||||
|
||||
// 添加文件到队列
|
||||
async function addFilesToQueue(files) {
|
||||
if (!apiKey) {
|
||||
alert('请先验证 API Key');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const filePath of files) {
|
||||
const fileName = filePath.split('\\').pop();
|
||||
const info = await ipcRenderer.invoke('get-image-info', filePath);
|
||||
|
||||
if (info.success) {
|
||||
queue.push({
|
||||
id: Date.now() + Math.random(),
|
||||
name: fileName,
|
||||
path: filePath,
|
||||
originalSize: info.size,
|
||||
compressedSize: null,
|
||||
compressedData: null,
|
||||
status: 'pending',
|
||||
progress: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateQueueUI();
|
||||
}
|
||||
|
||||
// 更新队列 UI
|
||||
function updateQueueUI() {
|
||||
queueCount.textContent = `${queue.length} 张图片`;
|
||||
compressAllBtn.disabled = queue.length === 0 || !apiKey;
|
||||
downloadAllBtn.disabled = queue.filter(item => item.status === 'completed').length === 0;
|
||||
|
||||
if (queue.length === 0) {
|
||||
queueList.innerHTML = `
|
||||
<div class="empty-queue">
|
||||
<div class="empty-queue-icon">
|
||||
<tp-icon name="empty" size="64px"></tp-icon>
|
||||
</div>
|
||||
<p>队列为空,请添加图片</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
queueList.innerHTML = queue.map(item => `
|
||||
<div class="queue-item">
|
||||
<div class="queue-item-info">
|
||||
<div class="queue-item-name">${item.name}</div>
|
||||
<div class="queue-item-sizes">
|
||||
<span class="size-comparison">
|
||||
<span class="size-before">原始: ${formatSize(item.originalSize)}</span>
|
||||
${item.compressedSize ? `
|
||||
<span>→</span>
|
||||
<span class="size-after">压缩后: ${formatSize(item.compressedSize)}</span>
|
||||
<span class="saving-ratio">
|
||||
(节省 ${Math.round((1 - item.compressedSize / item.originalSize) * 100)}%)
|
||||
</span>
|
||||
` : ''}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="queue-item-progress">
|
||||
${item.status === 'processing' ? `
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: ${item.progress}%"></div>
|
||||
</div>
|
||||
<div class="progress-text">${item.progress}%</div>
|
||||
` : `
|
||||
<span class="status-badge status-${item.status}">
|
||||
${item.status === 'pending' ? '待处理' :
|
||||
item.status === 'completed' ? '已完成' : '错误'}
|
||||
</span>
|
||||
`}
|
||||
</div>
|
||||
<div class="queue-item-actions">
|
||||
${item.status === 'completed' ? `
|
||||
<button class="btn-download" onclick="downloadSingle('${item.id}')">下载</button>
|
||||
` : ''}
|
||||
<button class="btn-remove" onclick="removeFromQueue('${item.id}')">移除</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 从队列移除
|
||||
window.removeFromQueue = (id) => {
|
||||
queue = queue.filter(item => item.id != id);
|
||||
updateQueueUI();
|
||||
};
|
||||
|
||||
// 清空队列
|
||||
clearQueueBtn.addEventListener('click', () => {
|
||||
if (confirm('确定要清空队列吗?')) {
|
||||
queue = [];
|
||||
updateQueueUI();
|
||||
}
|
||||
});
|
||||
|
||||
// 压缩单张图片
|
||||
async function compressItem(item) {
|
||||
item.status = 'processing';
|
||||
item.progress = 0;
|
||||
updateQueueUI();
|
||||
|
||||
try {
|
||||
// 模拟进度
|
||||
const progressInterval = setInterval(() => {
|
||||
if (item.progress < 80) {
|
||||
item.progress += 10;
|
||||
updateQueueUI();
|
||||
}
|
||||
}, 200);
|
||||
|
||||
const result = await ipcRenderer.invoke('compress-image', item.path);
|
||||
|
||||
clearInterval(progressInterval);
|
||||
item.progress = 100;
|
||||
|
||||
if (result.success) {
|
||||
item.compressedData = result.compressedData;
|
||||
item.compressedSize = Math.round(result.compressedData.length * 0.75); // base64 解码后的大小
|
||||
item.status = 'completed';
|
||||
} else {
|
||||
item.status = 'error';
|
||||
console.error('压缩失败:', result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
item.status = 'error';
|
||||
console.error('压缩错误:', error);
|
||||
}
|
||||
|
||||
updateQueueUI();
|
||||
}
|
||||
|
||||
// 全部压缩
|
||||
compressAllBtn.addEventListener('click', async () => {
|
||||
const pendingItems = queue.filter(item => item.status === 'pending');
|
||||
|
||||
for (const item of pendingItems) {
|
||||
await compressItem(item);
|
||||
}
|
||||
});
|
||||
|
||||
// 下载单张图片
|
||||
window.downloadSingle = async (id) => {
|
||||
const item = queue.find(i => i.id == id);
|
||||
if (!item || !item.compressedData) return;
|
||||
|
||||
const result = await ipcRenderer.invoke('select-save-directory');
|
||||
if (!result.canceled) {
|
||||
const savePath = `${result.path}\\${item.name}`;
|
||||
const saveResult = await ipcRenderer.invoke('save-compressed-image', savePath, item.compressedData);
|
||||
|
||||
if (saveResult.success) {
|
||||
alert(`文件已保存到: ${savePath}`);
|
||||
} else {
|
||||
alert('保存失败: ' + saveResult.error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 打包下载
|
||||
downloadAllBtn.addEventListener('click', async () => {
|
||||
const completedItems = queue.filter(item => item.status === 'completed');
|
||||
if (completedItems.length === 0) {
|
||||
alert('没有可下载的文件');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await ipcRenderer.invoke('select-save-directory');
|
||||
if (!result.canceled) {
|
||||
const files = completedItems.map(item => ({
|
||||
name: item.name,
|
||||
data: item.compressedData
|
||||
}));
|
||||
|
||||
const zipResult = await ipcRenderer.invoke('create-zip', files);
|
||||
|
||||
if (zipResult.success) {
|
||||
const savePath = `${result.path}\\compressed_images.zip`;
|
||||
const saveResult = await ipcRenderer.invoke('save-compressed-image', savePath, zipResult.zipData);
|
||||
|
||||
if (saveResult.success) {
|
||||
alert(`压缩包已保存到: ${savePath}`);
|
||||
} else {
|
||||
alert('保存失败: ' + saveResult.error);
|
||||
}
|
||||
} else {
|
||||
alert('创建压缩包失败: ' + zipResult.error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化 UI
|
||||
loadDefaultApiKey();
|
||||
updateQueueUI();
|
||||
|
||||
// 窗口控制
|
||||
const minimizeBtn = document.getElementById('minimizeBtn');
|
||||
const maximizeBtn = document.getElementById('maximizeBtn');
|
||||
const closeBtn = document.getElementById('closeBtn');
|
||||
const titlebar = document.getElementById('titlebar');
|
||||
|
||||
// 最小化窗口
|
||||
minimizeBtn.addEventListener('click', async () => {
|
||||
await ipcRenderer.invoke('window-minimize');
|
||||
});
|
||||
|
||||
// 最大化/还原窗口
|
||||
maximizeBtn.addEventListener('click', async () => {
|
||||
await ipcRenderer.invoke('window-maximize');
|
||||
});
|
||||
|
||||
// 关闭窗口
|
||||
closeBtn.addEventListener('click', async () => {
|
||||
await ipcRenderer.invoke('window-close');
|
||||
});
|
||||
|
||||
// 更新最大化按钮图标
|
||||
async function updateMaximizeButton() {
|
||||
const isMaximized = await ipcRenderer.invoke('window-is-maximized');
|
||||
if (isMaximized) {
|
||||
maximizeBtn.innerHTML = '<tp-icon name="restore" size="10px"></tp-icon>';
|
||||
maximizeBtn.title = '还原';
|
||||
} else {
|
||||
maximizeBtn.innerHTML = '<tp-icon name="maximize" size="10px"></tp-icon>';
|
||||
maximizeBtn.title = '最大化';
|
||||
}
|
||||
}
|
||||
|
||||
// 监听窗口最大化状态变化
|
||||
window.addEventListener('resize', updateMaximizeButton);
|
||||
updateMaximizeButton();
|
||||
98
src/renderer/icons.js
Normal file
98
src/renderer/icons.js
Normal file
@@ -0,0 +1,98 @@
|
||||
// TinyPanda Icon Web Component
|
||||
class TPIcon extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
static get observedAttributes() {
|
||||
return ['name', 'size', 'color'];
|
||||
}
|
||||
|
||||
attributeChangedCallback() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.getAttribute('name') || 'default';
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this.getAttribute('size') || '1em';
|
||||
}
|
||||
|
||||
get color() {
|
||||
return this.getAttribute('color') || 'currentColor';
|
||||
}
|
||||
|
||||
getIcons() {
|
||||
return {
|
||||
// Logo - Panda
|
||||
'logo': `<svg viewBox="0 0 1024 1024" fill="currentColor"><path d="M725.333 896h-426.666c-70.613 0-128-57.387-128-128v-341.333c0-70.613 57.387-128 128-128h42.666v-85.334c0-47.147 38.187-85.333 85.334-85.333h170.666c47.147 0 85.334 38.187 85.334 85.333v85.334h42.666c70.613 0 128 57.387 128 128v341.333c0 70.613-57.387 128-128 128zM426.667 298.667v-85.334c0-23.573 19.093-42.666 42.666-42.666h85.334c23.573 0 42.666 19.093 42.666 42.666v85.334h-170.666z"/></svg>`,
|
||||
|
||||
// Settings
|
||||
'settings': `<svg viewBox="0 0 1024 1024" fill="currentColor"><path d="M512 0c-282.789 0-512 229.211-512 512s229.211 512 512 512 512-229.211 512-512-229.211-512-512-512zM512 938.667c-235.648 0-426.667-191.019-426.667-426.667s191.019-426.667 426.667-426.667 426.667 191.019 426.667 426.667-191.019 426.667-426.667 426.667z"/><path d="M832 469.333h-85.333c-14.72 0-28.16-8.107-34.987-21.12-6.827-13.013-5.547-28.8 3.413-40.533l53.76-71.68c9.387-12.373 8.32-30.080-2.56-41.173l-60.373-60.373c-10.667-10.667-28.16-11.733-40.533-2.56l-71.68 53.76c-11.733 8.96-27.52 10.24-40.533 3.413-13.013-6.827-21.12-20.267-21.12-34.987v-85.333c0-14.72-11.947-26.667-26.667-26.667h-85.333c-14.72 0-26.667 11.947-26.667 26.667v85.333c0 14.72-8.107 28.16-21.12 34.987-13.013 6.827-28.8 5.547-40.533-3.413l-71.68-53.76c-12.373-9.387-30.080-8.32-41.173 2.56l-60.373 60.373c-10.667 10.667-11.733 28.16-2.56 40.533l53.76 71.68c8.96 11.733 10.24 27.52 3.413 40.533-6.827 13.013-20.267 21.12-34.987 21.12h-85.333c-14.72 0-26.667 11.947-26.667 26.667v85.333c0 14.72 11.947 26.667 26.667 26.667h85.333c14.72 0 28.16 8.107 34.987 21.12 6.827 13.013 5.547 28.8-3.413 40.533l-53.76 71.68c-9.387 12.373-8.32 30.080 2.56 41.173l60.373 60.373c10.667 10.667 28.16 11.733 40.533 2.56l71.68-53.76c11.733-8.96 27.52-10.24 40.533-3.413 13.013 6.827 21.12 20.267 21.12 34.987v85.333c0 14.72 11.947 26.667 26.667 26.667h85.333c14.72 0 26.667-11.947 26.667-26.667v-85.333c0-14.72 8.107-28.16 21.12-34.987 13.013-6.827 28.8-5.547 40.533 3.413l71.68 53.76c12.373 9.387 30.080 8.32 41.173-2.56l60.373-60.373c10.667-10.667 11.733-28.16 2.56-40.533l-53.76-71.68c-8.96-11.733-10.24-27.52-3.413-40.533 6.827-13.013 20.267-21.12 34.987-21.12h85.333c14.72 0 26.667-11.947 26.667-26.667v-85.333c0-14.72-11.947-26.667-26.667-26.667zM512 597.333c-47.147 0-85.333-38.187-85.333-85.333s38.187-85.333 85.333-85.333 85.333 38.187 85.333 85.333-38.187 85.333-85.333 85.333z"/></svg>`,
|
||||
|
||||
// Upload
|
||||
'upload': `<svg viewBox="0 0 1024 1024" fill="currentColor"><path d="M896 298.667h-149.333c-14.72 0-26.667-11.947-26.667-26.667s11.947-26.667 26.667-26.667h149.333c23.573 0 42.667 19.093 42.667 42.667v469.333c0 23.573-19.093 42.667-42.667 42.667h-768c-23.573 0-42.667-19.093-42.667-42.667v-469.333c0-23.573 19.093-42.667 42.667-42.667h149.333c14.72 0 26.667 11.947 26.667 26.667s-11.947 26.667-26.667 26.667h-128v426.667h725.333v-426.667h-128zM512 213.333l-85.333 85.333c-10.667 10.667-27.733 10.667-38.4 0s-10.667-27.733 0-38.4l128-128c10.667-10.667 27.733-10.667 38.4 0l128 128c10.667 10.667 10.667 27.733 0 38.4s-27.733 10.667-38.4 0l-85.333-85.333v256c0 14.72-11.947 26.667-26.667 26.667s-26.667-11.947-26.667-26.667v-256z"/></svg>`,
|
||||
|
||||
// Close (X)
|
||||
'close': `<svg viewBox="0 0 1024 1024" fill="currentColor"><path d="M512 0c-282.789 0-512 229.211-512 512s229.211 512 512 512 512-229.211 512-512-229.211-512-512-512zM681.6 681.6c-10.667 10.667-27.733 10.667-38.4 0l-131.2-131.2-131.2 131.2c-10.667 10.667-27.733 10.667-38.4 0s-10.667-27.733 0-38.4l131.2-131.2-131.2-131.2c-10.667-10.667-10.667-27.733 0-38.4s27.733-10.667 38.4 0l131.2 131.2 131.2-131.2c10.667-10.667 27.733-10.667 38.4 0s10.667 27.733 0 38.4l-131.2 131.2 131.2 131.2c10.667 10.667 10.667 27.733 0 38.4z"/></svg>`,
|
||||
|
||||
// Empty - Inbox
|
||||
'empty': `<svg viewBox="0 0 1024 1024" fill="currentColor"><path d="M896 298.667h-149.333c-14.72 0-26.667-11.947-26.667-26.667s11.947-26.667 26.667-26.667h149.333c23.573 0 42.667 19.093 42.667 42.667v469.333c0 23.573-19.093 42.667-42.667 42.667h-768c-23.573 0-42.667-19.093-42.667-42.667v-469.333c0-23.573 19.093-42.667 42.667-42.667h149.333c14.72 0 26.667 11.947 26.667 26.667s-11.947 26.667-26.667 26.667h-128v426.667h725.333v-426.667h-128zM512 213.333l-85.333 85.333c-10.667 10.667-27.733 10.667-38.4 0s-10.667-27.733 0-38.4l128-128c10.667-10.667 27.733-10.667 38.4 0l128 128c10.667 10.667 10.667 27.733 0 38.4s-27.733 10.667-38.4 0l-85.333-85.333v256c0 14.72-11.947 26.667-26.667 26.667s-26.667-11.947-26.667-26.667v-256z"/></svg>`,
|
||||
|
||||
// Check
|
||||
'check': `<svg viewBox="0 0 1024 1024" fill="currentColor"><path d="M512 0c-282.789 0-512 229.211-512 512s229.211 512 512 512 512-229.211 512-512-229.211-512-512-512zM746.667 341.333l-341.333 341.333c-10.667 10.667-27.733 10.667-38.4 0l-170.667-170.667c-10.667-10.667-10.667-27.733 0-38.4s27.733-10.667 38.4 0l149.333 149.333 320-320c10.667-10.667 27.733-10.667 38.4 0s10.667 27.733 4.267 38.4z"/></svg>`,
|
||||
|
||||
// Delete
|
||||
'delete': `<svg viewBox="0 0 1024 1024" fill="currentColor"><path d="M341.333 213.333c0-47.147 38.187-85.333 85.333-85.333h170.667c47.147 0 85.333 38.187 85.333 85.333v42.667h170.667c23.573 0 42.667 19.093 42.667 42.667s-19.093 42.667-42.667 42.667h-21.333v469.333c0 47.147-38.187 85.333-85.333 85.333h-426.667c-47.147 0-85.333-38.187-85.333-85.333v-469.333h-21.333c-23.573 0-42.667-19.093-42.667-42.667s19.093-42.667 42.667-42.667h170.667v-42.667zM426.667 213.333v42.667h170.667v-42.667c0-23.573-19.093-42.667-42.667-42.667h-85.333c-23.573 0-42.667 19.093-42.667 42.667zM384 384v426.667c0 23.573 19.093 42.667 42.667 42.667h170.667c23.573 0 42.667-19.093 42.667-42.667v-426.667h-256z"/></svg>`,
|
||||
|
||||
// Download
|
||||
'download': `<svg viewBox="0 0 1024 1024" fill="currentColor"><path d="M512 0c-282.789 0-512 229.211-512 512s229.211 512 512 512 512-229.211 512-512-229.211-512-512-512zM725.333 554.667l-170.667 170.667c-10.667 10.667-27.733 10.667-38.4 0l-170.667-170.667c-10.667-10.667-10.667-27.733 0-38.4s27.733-10.667 38.4 0l123.733 123.733v-341.333c0-14.72 11.947-26.667 26.667-26.667s26.667 11.947 26.667 26.667v341.333l123.733-123.733c10.667-10.667 27.733-10.667 38.4 0s10.667 27.733 0 38.4zM768 768h-512c-14.72 0-26.667-11.947-26.667-26.667s11.947-26.667 26.667-26.667h512c14.72 0 26.667 11.947 26.667 26.667s-11.947 26.667-26.667 26.667z"/></svg>`,
|
||||
|
||||
// Compress
|
||||
'compress': `<svg viewBox="0 0 1024 1024" fill="currentColor"><path d="M512 0c-282.789 0-512 229.211-512 512s229.211 512 512 512 512-229.211 512-512-229.211-512-512-512zM768 384h-170.667c-14.72 0-26.667-11.947-26.667-26.667s11.947-26.667 26.667-26.667h149.333l-192-192-192 192h149.333c14.72 0 26.667 11.947 26.667 26.667s-11.947 26.667-26.667 26.667h-170.667c-14.72 0-26.667-11.947-26.667-26.667v-170.667c0-14.72 11.947-26.667 26.667-26.667s26.667 11.947 26.667 26.667v149.333l213.333-213.333c10.667-10.667 27.733-10.667 38.4 0l213.333 213.333v-149.333c0-14.72 11.947-26.667 26.667-26.667s26.667 11.947 26.667 26.667v170.667c0 14.72-11.947 26.667-26.667 26.667zM768 810.667h-170.667c-14.72 0-26.667-11.947-26.667-26.667s11.947-26.667 26.667-26.667h149.333l-192-192-192 192h149.333c14.72 0 26.667 11.947 26.667 26.667s-11.947 26.667-26.667 26.667h-170.667c-14.72 0-26.667-11.947-26.667-26.667v-170.667c0-14.72 11.947-26.667 26.667-26.667s26.667 11.947 26.667 26.667v149.333l213.333-213.333c10.667-10.667 27.733-10.667 38.4 0l213.333 213.333v-149.333c0-14.72 11.947-26.667 26.667-26.667s26.667 11.947 26.667 26.667v170.667c0 14.72-11.947 26.667-26.667 26.667z"/></svg>`,
|
||||
|
||||
// Minimize
|
||||
'minimize': `<svg viewBox="0 0 1024 1024" fill="currentColor"><path d="M853.333 682.667h-682.667c-23.573 0-42.667-19.093-42.667-42.667s19.093-42.667 42.667-42.667h682.667c23.573 0 42.667 19.093 42.667 42.667s-19.093 42.667-42.667 42.667z"/></svg>`,
|
||||
|
||||
// Maximize
|
||||
'maximize': `<svg viewBox="0 0 1024 1024" fill="currentColor"><path d="M853.333 170.667h-170.667c-23.573 0-42.667-19.093-42.667-42.667s19.093-42.667 42.667-42.667h170.667c23.573 0 42.667 19.093 42.667 42.667v170.667c0 23.573-19.093 42.667-42.667 42.667s-42.667-19.093-42.667-42.667v-128zM810.667 725.333c0 23.573-19.093 42.667-42.667 42.667h-170.667c-23.573 0-42.667-19.093-42.667-42.667s19.093-42.667 42.667-42.667h128v-128c0-23.573 19.093-42.667 42.667-42.667s42.667 19.093 42.667 42.667v128zM384 725.333c0 23.573-19.093 42.667-42.667 42.667h-170.667c-23.573 0-42.667-19.093-42.667-42.667v-170.667c0-23.573 19.093-42.667 42.667-42.667s42.667 19.093 42.667 42.667v128h128c23.573 0 42.667 19.093 42.667 42.667zM341.333 213.333c0-23.573 19.093-42.667 42.667-42.667h170.667c23.573 0 42.667 19.093 42.667 42.667v170.667c0 23.573-19.093 42.667-42.667 42.667s-42.667-19.093-42.667-42.667v-128h-128c-23.573 0-42.667-19.093-42.667-42.667z"/></svg>`,
|
||||
|
||||
// Restore
|
||||
'restore': `<svg viewBox="0 0 1024 1024" fill="currentColor"><path d="M725.333 170.667h-256c-23.573 0-42.667-19.093-42.667-42.667s19.093-42.667 42.667-42.667h341.333c23.573 0 42.667 19.093 42.667 42.667v341.333c0 23.573-19.093 42.667-42.667 42.667s-42.667-19.093-42.667-42.667v-213.333l-213.333 213.333c-10.667 10.667-27.733 10.667-38.4 0s-10.667-27.733 0-38.4l213.333-213.333zM170.667 298.667v512c0 23.573 19.093 42.667 42.667 42.667h512c23.573 0 42.667-19.093 42.667-42.667v-85.333c0-23.573 19.093-42.667 42.667-42.667s42.667 19.093 42.667 42.667v85.333c0 70.613-57.387 128-128 128h-512c-70.613 0-128-57.387-128-128v-512c0-70.613 57.387-128 128-128h85.333c23.573 0 42.667 19.093 42.667 42.667s-19.093 42.667-42.667 42.667h-85.333c-23.573 0-42.667 19.093-42.667 42.667z"/></svg>`
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const icons = this.getIcons();
|
||||
const iconSvg = icons[this.name] || icons['default'];
|
||||
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
:host {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: ${this.size};
|
||||
height: ${this.size};
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
fill: ${this.color};
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
${iconSvg}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Register the custom element
|
||||
customElements.define('tp-icon', TPIcon);
|
||||
101
src/renderer/index.html
Normal file
101
src/renderer/index.html
Normal file
@@ -0,0 +1,101 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TinyPanda - 图片压缩工具</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<script src="icons.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 自定义标题栏 -->
|
||||
<div class="titlebar" id="titlebar">
|
||||
<div class="titlebar-drag" id="titlebarDrag">
|
||||
<div class="titlebar-left">
|
||||
<tp-icon name="logo" size="16px"></tp-icon>
|
||||
<span class="titlebar-title">TinyPanda</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="window-controls">
|
||||
<button class="window-control" id="minimizeBtn" title="最小化">
|
||||
<tp-icon name="minimize" size="10px"></tp-icon>
|
||||
</button>
|
||||
<button class="window-control" id="maximizeBtn" title="最大化">
|
||||
<tp-icon name="maximize" size="10px"></tp-icon>
|
||||
</button>
|
||||
<button class="window-control close" id="closeBtn" title="关闭">
|
||||
<tp-icon name="close" size="10px"></tp-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<div class="header-logo">
|
||||
<tp-icon name="logo" size="48px"></tp-icon>
|
||||
<h1>TinyPanda</h1>
|
||||
</div>
|
||||
<p class="subtitle">专业的图片压缩工具</p>
|
||||
<button class="btn-icon" id="settingsBtn" title="设置">
|
||||
<tp-icon name="settings" size="20px"></tp-icon>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="upload-section">
|
||||
<div class="upload-area" id="uploadArea">
|
||||
<div class="upload-icon">
|
||||
<tp-icon name="upload" size="64px"></tp-icon>
|
||||
</div>
|
||||
<p>拖拽图片到此处或点击选择</p>
|
||||
<p class="hint">支持 JPG、PNG、WebP 格式</p>
|
||||
</div>
|
||||
<button class="btn-primary" id="selectFilesBtn">选择图片</button>
|
||||
</div>
|
||||
|
||||
<div class="queue-section">
|
||||
<div class="queue-header">
|
||||
<h2>压缩队列</h2>
|
||||
<div class="queue-actions">
|
||||
<span id="queueCount">0 张图片</span>
|
||||
<button class="btn-secondary" id="clearQueueBtn">清空队列</button>
|
||||
<button class="btn-success" id="compressAllBtn" disabled>全部压缩</button>
|
||||
<button class="btn-primary" id="downloadAllBtn" disabled>打包下载</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="queue-list" id="queueList"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 设置模态框 -->
|
||||
<div class="modal" id="settingsModal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">
|
||||
<tp-icon name="settings" size="24px"></tp-icon>
|
||||
<h2>设置</h2>
|
||||
</div>
|
||||
<button class="modal-close" id="closeSettingsBtn">
|
||||
<tp-icon name="close" size="20px"></tp-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="setting-item">
|
||||
<label for="apiKey">Tinify API Key:</label>
|
||||
<input type="text" id="apiKey" placeholder="请输入您的 API Key">
|
||||
<button class="btn-small" id="validateKeyBtn">验证</button>
|
||||
<p class="setting-hint">请访问 <a href="https://tinify.cn/" target="_blank">https://tinify.cn/</a> 获取您的 API Key</p>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label>当前状态:</label>
|
||||
<span id="apiKeyStatus" class="status-badge status-pending">未配置</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-primary" id="saveSettingsBtn">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
738
src/renderer/styles.css
Normal file
738
src/renderer/styles.css
Normal file
@@ -0,0 +1,738 @@
|
||||
/* Windows 11 Fluent Design 风格 */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--accent-color: #0078D4;
|
||||
--accent-hover: #106EBE;
|
||||
--accent-disabled: #C7E0F4;
|
||||
--background-primary: #F3F3F3;
|
||||
--background-secondary: #FFFFFF;
|
||||
--background-tertiary: #F9F9F9;
|
||||
--background-hover: #E5E5E5;
|
||||
--text-primary: #1A1A1A;
|
||||
--text-secondary: #616161;
|
||||
--text-disabled: #A19F9D;
|
||||
--border-color: #E5E5E5;
|
||||
--border-focus: #0078D4;
|
||||
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.08);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.16);
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-xl: 16px;
|
||||
}
|
||||
|
||||
/* 自定义标题栏 */
|
||||
.titlebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 32px;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
-webkit-app-region: no-drag;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.titlebar-drag {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
-webkit-app-region: drag;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.titlebar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.titlebar-left tp-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.titlebar-title {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.window-controls {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.window-control {
|
||||
width: 46px;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.1s ease;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.window-control:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.window-control.close:hover {
|
||||
background: #C42B1C;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.window-control tp-icon {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.window-control:hover tp-icon {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI Variable', 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
min-height: 100vh;
|
||||
padding: 56px 0 24px;
|
||||
margin: 0;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border-radius: var(--radius-xl);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 通用图标样式 */
|
||||
.icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
fill: currentColor;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* tp-icon Web Component 样式 */
|
||||
tp-icon {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* 头部 Logo */
|
||||
.header {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
color: var(--text-primary);
|
||||
padding: 48px 32px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.header-logo tp-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.25em;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1em;
|
||||
opacity: 0.9;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* 设置按钮 */
|
||||
.btn-icon {
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
right: 32px;
|
||||
background: var(--background-secondary);
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.btn-icon .icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background: var(--background-hover);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.btn-icon:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* 上传区域 */
|
||||
.upload-section {
|
||||
padding: 48px 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
border: 2px dashed var(--border-color);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 64px 32px;
|
||||
background: var(--background-tertiary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.upload-area:hover {
|
||||
border-color: var(--accent-color);
|
||||
background: var(--background-secondary);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.upload-area:active {
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.upload-icon tp-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.upload-area p {
|
||||
font-size: 1em;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 0.875em !important;
|
||||
color: var(--text-disabled) !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
/* 按钮样式 - Fluent Design */
|
||||
.btn-primary,
|
||||
.btn-secondary,
|
||||
.btn-success,
|
||||
.btn-small {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-family: inherit;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--accent-hover);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.btn-primary:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
background: var(--accent-disabled);
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--background-hover);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--border-color);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #107C10;
|
||||
color: white;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.btn-success:hover:not(:disabled) {
|
||||
background: #0B5A0B;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.btn-success:disabled {
|
||||
background: var(--background-hover);
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.btn-small:hover {
|
||||
background: var(--accent-hover);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* 队列区域 */
|
||||
.queue-section {
|
||||
padding: 0 40px 40px;
|
||||
}
|
||||
|
||||
.queue-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.queue-header h2 {
|
||||
font-size: 1.25em;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.queue-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
#queueCount {
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.queue-list {
|
||||
min-height: 200px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
/* 队列项卡片 */
|
||||
.queue-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background: var(--background-tertiary);
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: 12px;
|
||||
gap: 16px;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.queue-item:hover {
|
||||
background: var(--background-secondary);
|
||||
border-color: var(--border-color);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.queue-item-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.queue-item-name {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 6px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.queue-item-sizes {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.size-comparison {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.size-before {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.size-after {
|
||||
color: #107C10;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.saving-ratio {
|
||||
color: #107C10;
|
||||
font-weight: 700;
|
||||
background: rgba(16, 124, 16, 0.1);
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
/* 进度条 */
|
||||
.queue-item-progress {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 6px;
|
||||
background: var(--border-color);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: var(--accent-color);
|
||||
transition: width 0.3s ease;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 队列项操作按钮 */
|
||||
.queue-item-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.queue-item-actions button {
|
||||
padding: 8px 14px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.btn-download {
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-download:hover {
|
||||
background: var(--accent-hover);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.btn-remove {
|
||||
background: #C42B1C;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-remove:hover {
|
||||
background: #A8200F;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
/* 状态徽章 */
|
||||
.status-badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background: #FFF4CE;
|
||||
color: #797775;
|
||||
}
|
||||
|
||||
.status-processing {
|
||||
background: #DEECF9;
|
||||
color: #005A9E;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background: #DFF6DD;
|
||||
color: #107C10;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background: #FDE7E9;
|
||||
color: #C42B1C;
|
||||
}
|
||||
|
||||
/* 空队列状态 */
|
||||
.empty-queue {
|
||||
text-align: center;
|
||||
padding: 80px 32px;
|
||||
color: var(--text-disabled);
|
||||
}
|
||||
|
||||
.empty-queue-icon {
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-queue-icon tp-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
/* 模态框 - Fluent Design */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(8px);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.modal.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--background-secondary);
|
||||
border-radius: var(--radius-lg);
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: var(--shadow-lg);
|
||||
animation: modalIn 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes modalIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24px 28px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.modal-title tp-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.modal-title h2 {
|
||||
font-size: 1.25em;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--radius-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.modal-close tp-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background: var(--background-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.setting-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.setting-item label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.setting-item input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
transition: all 0.2s ease;
|
||||
background: var(--background-tertiary);
|
||||
}
|
||||
|
||||
.setting-item input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
background: var(--background-secondary);
|
||||
box-shadow: 0 0 0 2px rgba(0, 120, 212, 0.1);
|
||||
}
|
||||
|
||||
.setting-item .btn-small {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.setting-hint {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.setting-hint a {
|
||||
color: var(--accent-color);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.setting-hint a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 20px 28px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
background: var(--background-tertiary);
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.queue-list::-webkit-scrollbar,
|
||||
.modal-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.queue-list::-webkit-scrollbar-track,
|
||||
.modal-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.queue-list::-webkit-scrollbar-thumb,
|
||||
.modal-content::-webkit-scrollbar-thumb {
|
||||
background: var(--border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.queue-list::-webkit-scrollbar-thumb:hover,
|
||||
.modal-content::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-disabled);
|
||||
}-thumb {
|
||||
background: var(--border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.queue-list::-webkit-scrollbar-thumb:hover,
|
||||
.modal-content::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-disabled);
|
||||
}
|
||||
Reference in New Issue
Block a user