feat: 工具板优化

This commit is contained in:
Joey
2026-08-09 22:21:56 +08:00
parent a925283bf7
commit 76be4a82dd
5 changed files with 473 additions and 192 deletions
+78
View File
@@ -218,8 +218,86 @@
.ai-tag-chip-zh {
color: #9ca3af;
}
.ai-copy-btn.is-copied,
.ai-copy-trigger.is-copied,
.ai-copy-all-tags-btn.is-copied {
background: rgba(16, 185, 129, 0.25) !important;
color: #34d399 !important;
}
.ai-tag-chip.is-copied {
border-color: #34d399;
background: rgba(16, 185, 129, 0.15);
transform: none;
}
/* 上品登记表:型号 / 重量 / 尺寸 默认隐藏 */
#historyTable:not(.show-optional-cols) .history-col-optional {
display: none;
}
/* ================================================================
Toast 轻提示
================================================================ */
.toast-container {
position: fixed;
top: 1.25rem;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.625rem;
pointer-events: none;
}
.toast {
pointer-events: auto;
min-width: 12rem;
max-width: 22rem;
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.75rem 1.125rem;
border-radius: 0.625rem;
background: var(--color-dark-light);
border: 1px solid var(--color-gray-700);
border-left: 4px solid var(--color-secondary);
color: #f3f4f6;
font-size: 0.9375rem;
line-height: 1.4;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.45);
opacity: 0;
transform: translateY(-12px);
transition: opacity 0.25s ease, transform 0.25s ease;
}
.toast.show {
opacity: 1;
transform: translateY(0);
}
.toast.hide {
opacity: 0;
transform: translateY(-12px);
}
.toast .toast-icon {
flex: 0 0 auto;
font-size: 1.125rem;
line-height: 1;
}
.toast.success {
border-left-color: var(--color-secondary);
}
.toast.success .toast-icon {
color: #34d399;
}
.toast.error {
border-left-color: var(--color-danger);
}
.toast.error .toast-icon {
color: #f87171;
}
.toast.info {
border-left-color: var(--color-primary);
}
.toast.info .toast-icon {
color: #60a5fa;
}
+45 -13
View File
@@ -11,7 +11,6 @@
const bodyEl = document.getElementById('aiCopyBody');
const statusEl = document.getElementById('aiCopyStatus');
const modelSelectEl = document.getElementById('aiModelSelect');
const contextProductNameEl = document.getElementById('aiContextProductName');
const contextModelCodeEl = document.getElementById('aiContextModelCode');
const titlesContainerEl = document.getElementById('aiTitlesContainer');
const tagsContainerEl = document.getElementById('aiTagsContainer');
@@ -38,30 +37,64 @@
}
function syncContextHints() {
if (contextProductNameEl) {
const name = (productNameInput && productNameInput.value || '').trim();
contextProductNameEl.textContent = name || '--';
}
// 注:生成文案不再带入「商品名」,故此处只同步型号提示
if (contextModelCodeEl) {
const model = (modelCodeInput && modelCodeInput.value || '').trim();
contextModelCodeEl.textContent = model || '--';
}
}
async function copyText(text) {
async function copyText(text, btn) {
const value = (text || '').trim();
if (!value) {
setStatus('没有可复制的内容', true);
return;
}
try {
await navigator.clipboard.writeText(value);
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(value);
} else {
const ta = document.createElement('textarea');
ta.value = value;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
setStatus('已复制');
showCopyFeedback(btn);
} catch (err) {
setStatus('复制失败,请手动选择文本', true);
}
}
function showCopyFeedback(btn) {
if (!btn || btn.dataset.copyFeedbackActive) return;
if (btn.classList.contains('ai-tag-chip')) {
btn.classList.add('is-copied');
setTimeout(function () {
btn.classList.remove('is-copied');
}, 1200);
return;
}
const originalText = btn.textContent;
btn.dataset.copyFeedbackActive = '1';
btn.disabled = true;
btn.textContent = '已复制';
btn.classList.add('is-copied');
setTimeout(function () {
btn.textContent = originalText;
btn.disabled = false;
btn.classList.remove('is-copied');
delete btn.dataset.copyFeedbackActive;
}, 1200);
}
function toStringList(value) {
return Array.isArray(value)
? value.map(function (item) { return String(item).trim(); })
@@ -98,9 +131,9 @@
const copyBtn = document.createElement('button');
copyBtn.type = 'button';
copyBtn.className = 'px-3 py-1.5 text-sm rounded-lg bg-primary/20 hover:bg-primary/30 text-primary transition-colors duration-200';
copyBtn.className = 'ai-copy-trigger px-3 py-1.5 text-sm rounded-lg bg-primary/20 hover:bg-primary/30 text-primary transition-colors duration-200';
copyBtn.textContent = '复制俄文';
copyBtn.addEventListener('click', function () { copyText(ru); });
copyBtn.addEventListener('click', function () { copyText(ru, copyBtn); });
const fillBtn = document.createElement('button');
fillBtn.type = 'button';
@@ -253,7 +286,6 @@
const payload = {
source_text: sourceText,
product_name: (productNameInput && productNameInput.value || '').trim(),
model_code: (modelCodeInput && modelCodeInput.value || '').trim(),
model: (modelSelectEl && modelSelectEl.value || '').trim(),
};
@@ -307,7 +339,7 @@
btn.addEventListener('click', function () {
const sourceId = btn.getAttribute('data-copy-source');
const el = sourceId ? document.getElementById(sourceId) : null;
copyText(el ? el.textContent : '');
copyText(el ? el.textContent : '', btn);
});
});
@@ -315,13 +347,13 @@
tagsContainerEl.addEventListener('click', function (event) {
const chip = event.target.closest('.ai-tag-chip');
if (!chip || !tagsContainerEl.contains(chip)) return;
copyText(chip.getAttribute('data-tag-ru') || '');
copyText(chip.getAttribute('data-tag-ru') || '', chip);
});
}
if (copyAllTagsBtn) {
copyAllTagsBtn.addEventListener('click', function () {
copyText(lastTagsRu.join(', '));
copyText(lastTagsRu.join(', '), copyAllTagsBtn);
});
}
+296 -114
View File
@@ -31,7 +31,13 @@
const profitElement = document.getElementById('profitElement');
const commission = document.getElementById('commission');
const sellingPriceElement = document.getElementById('sellingPrice');
const totalCostElement = document.getElementById('totalCost');
const fullCommissionElement = document.getElementById('fullCommission');
const logisticsFeeRuleEl = document.getElementById('logisticsFeeRule');
const commissionRuleEl = document.getElementById('commissionRule');
const fullCommissionRuleEl = document.getElementById('fullCommissionRule');
const logisticsCard = document.getElementById('logisticsCard');
const totalCostCard = document.getElementById('totalCostCard');
const receivedCard = document.getElementById('receivedCard');
const sellingCard = document.getElementById('sellingCard');
const dimensionAlert = document.getElementById('dimensionAlert');
@@ -71,6 +77,7 @@
const previewContainer = document.getElementById('previewContainer');
const addWatermarkBtn = document.getElementById('addWatermarkBtn');
const exportAllBtn = document.getElementById('exportAllBtn');
const clearAllImagesBtn = document.getElementById('clearAllImagesBtn');
const watermarkOpacityInput = document.getElementById('watermarkOpacity');
const watermarkTextInput = document.getElementById('watermarkText');
const whiteBgToleranceInput = document.getElementById('whiteBgTolerance');
@@ -84,11 +91,14 @@
const WATERMARK_SCALE = 0.15; // 水印直径占原图宽度的比例
const WATERMARK_MARGIN = 10; // 默认位置距右下角的间距(原图像素)
const WATERMARK_TEXT_SCALE = 0.06; // 文字水印字号占原图宽度的比例
const WATERMARK_IMAGE_SHRINK = 0.8; // 加图片水印后,图片缩到当前尺寸的 80%
const DEFAULT_WHITE_BG_TOLERANCE = 60; // 白底颜色容差,越大清除得越狠
const PREVIEW_MAX_WIDTH = 400; // 预览画布宽度
const DEFAULT_WATERMARK_OPACITY = 30;
// 存储上传的图片信息(原图 + 水印状态)
let imageList = [];
// 自增序号,保证每张图片拥有全局唯一的 index(多批追加时 data-index 不冲突)
let imageSeq = 0;
let watermarkImg = null;
let watermarkImgPromise = null;
renderHistoryTable();
@@ -169,7 +179,7 @@
toggleOptionalColsBtn.addEventListener('click', () => {
const shown = historyTable.classList.toggle('show-optional-cols');
toggleOptionalColsBtn.setAttribute('aria-pressed', shown ? 'true' : 'false');
toggleOptionalColsBtn.textContent = shown ? '隐藏型号/重量/尺寸' : '显示型号/重量/尺寸';
toggleOptionalColsBtn.textContent = shown ? '隐藏物流费/平台总抽成/重量/尺寸/状态' : '显示物流费/平台总抽成/重量/尺寸/状态';
});
}
clearHistoryBtn.addEventListener('click', clearHistory);
@@ -235,44 +245,79 @@
}
});
// 处理上传的文件
// 处理上传的文件(追加模式:不清空已有图片)
function handleFiles(files) {
imageList = [];
if (files.length === 0) return;
// 清空预览容器
previewContainer.innerHTML = '';
// 遍历文件并加载图片
Array.from(files).forEach((file, index) => {
Array.from(files).forEach((file) => {
if (!file.type.startsWith('image/')) return;
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
// 存储图片信息
// 存储图片信息(index 使用全局自增序号,保证唯一,避免多批追加时冲突)
const item = {
index,
index: imageSeq++,
original: img,
whiteBg: null, // 白底处理后的 Canvas(与原图同尺寸)
whiteBgRatio: 0, // 被判定为背景的像素占比
fileName: file.name,
hasWatermark: false,
watermarkType: null, // 水印类型:'image' | 'text' | null(无)
pos: { x: 0, y: 0 } // 水印左上角在原图中的坐标
};
imageList.push(item);
// 上传后默认加上当前选择的水印(默认图片水印)
item.watermarkType = getWatermarkType();
item.pos = defaultWatermarkPos(item, item.watermarkType);
// 显示原始图片预览
renderPreview(item);
// 图片水印需确保水印图加载完成后再重绘一次,避免“只缩小没水印”
if (item.watermarkType === 'image') {
loadWatermarkImage().then(() => renderPreview(item)).catch(() => {});
}
// 每张图片加载完成后刷新按钮状态,确保有图即可操作
updateImageActionsState();
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
});
}
// 启用导出按钮(先显示原始图片,添加水印后更新)
exportAllBtn.disabled = false;
exportAllBtn.classList.remove('opacity-50', 'cursor-not-allowed');
function showEmptyPreviewPlaceholder() {
previewContainer.innerHTML = '<p class="col-span-full text-gray-500 text-center py-4">暂无图片</p>';
}
function updateImageActionsState() {
const hasImages = imageList.length > 0;
exportAllBtn.disabled = !hasImages;
exportAllBtn.classList.toggle('opacity-50', !hasImages);
exportAllBtn.classList.toggle('cursor-not-allowed', !hasImages);
clearAllImagesBtn.disabled = !hasImages;
clearAllImagesBtn.classList.toggle('opacity-50', !hasImages);
clearAllImagesBtn.classList.toggle('cursor-not-allowed', !hasImages);
}
function deleteItem(item) {
const idx = imageList.indexOf(item);
if (idx === -1) return;
imageList.splice(idx, 1);
const previewItem = previewContainer.querySelector(`[data-index="${item.index}"]`);
if (previewItem) previewItem.remove();
if (imageList.length === 0) {
showEmptyPreviewPlaceholder();
}
updateImageActionsState();
}
function clearAllImages() {
if (imageList.length === 0) return;
imageList = [];
fileInput.value = '';
showEmptyPreviewPlaceholder();
updateImageActionsState();
}
// 预加载水印图片,避免拖动时才开始加载
@@ -320,9 +365,9 @@
// 量文字宽度用的离屏画布
const measureCtx = document.createElement('canvas').getContext('2d');
// 水印在原图坐标系中的尺寸
function getWatermarkSize(item) {
if (getWatermarkType() === 'text') {
// 水印在原图坐标系中的尺寸type 不传时取全局当前选择)
function getWatermarkSize(item, type = getWatermarkType()) {
if (type === 'text') {
const fontSize = getWatermarkFontSize(item);
measureCtx.font = getWatermarkFont(fontSize);
return {
@@ -336,8 +381,8 @@
}
// 限制水印不被拖出图片范围
function clampWatermarkPos(item, x, y) {
const size = getWatermarkSize(item);
function clampWatermarkPos(item, x, y, type = getWatermarkType()) {
const size = getWatermarkSize(item, type);
const maxX = Math.max(0, item.original.width - size.width);
const maxY = Math.max(0, item.original.height - size.height);
return {
@@ -347,8 +392,8 @@
}
// 默认位置:右下角
function defaultWatermarkPos(item) {
const size = getWatermarkSize(item);
function defaultWatermarkPos(item, type = getWatermarkType()) {
const size = getWatermarkSize(item, type);
return clampWatermarkPos(
item,
item.original.width - size.width - WATERMARK_MARGIN,
@@ -399,11 +444,6 @@
ctx.restore();
}
// 水印所需资源是否就绪
function isWatermarkReady() {
return getWatermarkType() === 'text' ? true : !!watermarkImg;
}
function getWhiteBgTolerance() {
const value = parseFloat(whiteBgToleranceInput.value);
if (!isFinite(value)) return DEFAULT_WHITE_BG_TOLERANCE;
@@ -527,20 +567,22 @@
}
// 按指定宽度把原图和水印合成到 Canvas
// 图片水印会让整张图缩到当前的 80%(renderScale),文字水印保持原尺寸
function drawItemToCanvas(canvas, item, targetWidth) {
const renderScale = item.watermarkType === 'image' ? WATERMARK_IMAGE_SHRINK : 1;
const scale = targetWidth / item.original.width;
canvas.width = Math.max(1, Math.round(targetWidth));
canvas.height = Math.max(1, Math.round(item.original.height * scale));
canvas.width = Math.max(1, Math.round(targetWidth * renderScale));
canvas.height = Math.max(1, Math.round(item.original.height * scale * renderScale));
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(item.whiteBg || item.original, 0, 0, canvas.width, canvas.height);
if (item.hasWatermark && isWatermarkReady()) {
const drawX = item.pos.x * scale;
const drawY = item.pos.y * scale;
if (getWatermarkType() === 'text') {
drawTextWatermark(ctx, drawX, drawY, getWatermarkFontSize(item) * scale);
if (item.watermarkType && (item.watermarkType === 'text' || !!watermarkImg)) {
const drawX = item.pos.x * scale * renderScale;
const drawY = item.pos.y * scale * renderScale;
if (item.watermarkType === 'text') {
drawTextWatermark(ctx, drawX, drawY, getWatermarkFontSize(item) * scale * renderScale);
} else {
drawImageWatermark(ctx, drawX, drawY, getWatermarkSize(item).width * scale);
drawImageWatermark(ctx, drawX, drawY, getWatermarkSize(item, 'image').width * scale * renderScale);
}
}
return canvas;
@@ -560,9 +602,9 @@
}
canvas.addEventListener('pointerdown', (e) => {
if (!item.hasWatermark || !isWatermarkReady()) return;
if (!item.watermarkType || (item.watermarkType === 'image' && !watermarkImg)) return;
const point = toImageCoords(e);
const size = getWatermarkSize(item);
const size = getWatermarkSize(item, item.watermarkType);
const inside = point.x >= item.pos.x && point.x <= item.pos.x + size.width
&& point.y >= item.pos.y && point.y <= item.pos.y + size.height;
if (!inside) return;
@@ -590,54 +632,25 @@
canvas.addEventListener('pointercancel', stopDrag);
}
// 单张图片:切换是否贴水印
// 单张图片:切换是否贴水印(开启时用当前选择的水印类型)
function toggleItemWatermark(item) {
if (item.hasWatermark) {
item.hasWatermark = false;
if (item.watermarkType) {
item.watermarkType = null;
} else {
item.hasWatermark = true;
item.pos = defaultWatermarkPos(item);
item.watermarkType = getWatermarkType();
item.pos = defaultWatermarkPos(item, item.watermarkType);
}
renderPreview(item);
}
// 单张图片:切换白底处理(再次点击还原原始底色)
function toggleItemWhiteBackground(item, btn) {
if (item.whiteBg) {
item.whiteBg = null;
renderPreview(item);
return;
}
// 大图区域生长是同步的,先让按钮进入处理中状态再开工
btn.disabled = true;
btn.textContent = '处理中…';
setTimeout(() => {
try {
const result = applyWhiteBackground(item.original, getWhiteBgTolerance());
item.whiteBg = result.canvas;
item.whiteBgRatio = result.ratio;
if (result.ratio < 0.02) {
alert('几乎没有识别到背景,这张图的背景可能不是纯色。可以把「白底容差」调大一些再试');
}
} catch (err) {
alert('白底处理失败,请重试或换一张图');
console.error('白底处理失败:', err);
}
renderPreview(item);
}, 0);
}
// 按当前状态刷新预览图右上角按钮的文案
function syncPreviewActions(previewItem, item) {
const watermarkBtn = previewItem.querySelector('[data-role="toggle-watermark"]');
const whiteBgBtn = previewItem.querySelector('[data-role="toggle-white-bg"]');
watermarkBtn.textContent = item.hasWatermark ? '清除水印' : '加水印';
watermarkBtn.title = item.hasWatermark ? '这张图不加水印' : '给这张图加上水印';
whiteBgBtn.disabled = false;
whiteBgBtn.textContent = item.whiteBg ? '还原底色' : '白底';
whiteBgBtn.title = item.whiteBg
? `已把 ${(item.whiteBgRatio * 100).toFixed(0)}% 的画面刷成白底,点击还原`
: '把这张图的背景刷成纯白';
const deleteBtn = previewItem.querySelector('[data-role="delete-item"]');
watermarkBtn.textContent = item.watermarkType ? '清除水印' : '加水印';
watermarkBtn.title = item.watermarkType ? '这张图不加水印' : '给这张图加上水印';
deleteBtn.textContent = '删除';
deleteBtn.title = '从列表中移除这张图片';
}
// 渲染(或更新)单张图片的预览
@@ -666,12 +679,12 @@
watermarkBtn.onclick = () => toggleItemWatermark(item);
actions.appendChild(watermarkBtn);
const whiteBgBtn = document.createElement('button');
whiteBgBtn.type = 'button';
whiteBgBtn.className = 'preview-action-btn';
whiteBgBtn.dataset.role = 'toggle-white-bg';
whiteBgBtn.onclick = () => toggleItemWhiteBackground(item, whiteBgBtn);
actions.appendChild(whiteBgBtn);
const deleteBtn = document.createElement('button');
deleteBtn.type = 'button';
deleteBtn.className = 'preview-action-btn';
deleteBtn.dataset.role = 'delete-item';
deleteBtn.onclick = () => deleteItem(item);
actions.appendChild(deleteBtn);
frame.appendChild(actions);
previewItem.appendChild(frame);
@@ -686,7 +699,7 @@
}
const canvas = previewItem.querySelector('canvas');
canvas.classList.toggle('preview-img-draggable', item.hasWatermark);
canvas.classList.toggle('preview-img-draggable', item.watermarkType);
drawItemToCanvas(canvas, item, PREVIEW_MAX_WIDTH);
syncPreviewActions(previewItem, item);
}
@@ -719,21 +732,21 @@
}
imageList.forEach(item => {
if (!item.hasWatermark) {
item.hasWatermark = true;
item.pos = defaultWatermarkPos(item);
}
// 不论之前是什么水印,都用当前选择的水印类型重做:清除旧水印、加入新水印
item.watermarkType = getWatermarkType();
item.pos = defaultWatermarkPos(item, item.watermarkType);
renderPreview(item);
});
});
// 切换水印类型:显示对应的表单项,并把已有水印重新约束回图片范围内
// 切换水印类型:只切换对应的表单项显示,不改动已有图片的水印
// (已有图片的水印类型只有在点击「添加水印」时才会被替换)
function syncWatermarkTypeUI() {
const isText = getWatermarkType() === 'text';
watermarkImageOptions.classList.toggle('hidden', isText);
watermarkTextOptions.classList.toggle('hidden', !isText);
imageList.forEach(item => {
if (item.hasWatermark) item.pos = clampWatermarkPos(item, item.pos.x, item.pos.y);
if (item.watermarkType) item.pos = clampWatermarkPos(item, item.pos.x, item.pos.y, item.watermarkType);
});
renderAllPreviews();
}
@@ -743,7 +756,7 @@
watermarkOpacityInput.addEventListener('input', renderAllPreviews);
watermarkTextInput.addEventListener('input', () => {
imageList.forEach(item => {
if (item.hasWatermark) item.pos = clampWatermarkPos(item, item.pos.x, item.pos.y);
if (item.watermarkType) item.pos = clampWatermarkPos(item, item.pos.x, item.pos.y, item.watermarkType);
});
renderAllPreviews();
});
@@ -756,14 +769,40 @@
});
// 导出单张图片
// 生成全分辨率导出画布
function getExportCanvas(item) {
return drawItemToCanvas(document.createElement('canvas'), item, item.original.width);
}
// canvas 转 PNG Blob
function canvasToPngBlob(canvas) {
return new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (blob) resolve(blob);
else reject(new Error('toBlob 返回空'));
}, 'image/png');
});
}
// 根据商品名生成导出文件夹名;未填写则用「未命名」+4位随机数
function getExportFolderName() {
const rand4 = String(Math.floor(Math.random() * 10000)).padStart(4, '0');
let raw = (productNameInput.value || '').trim();
let name = raw || ('未命名' + rand4);
// 过滤文件系统非法字符,并压缩多余空格
name = name.replace(/[\/\\:*?"<>|\r\n\t]/g, '_').replace(/\s+/g, ' ').trim();
if (!name) name = '未命名' + rand4;
return name;
}
function exportImage(index) {
const targetImage = imageList.find(item => item.index === index);
if (!targetImage) return;
// 按原图尺寸重新合成,保证导出的是全分辨率图片
const canvas = drawItemToCanvas(document.createElement('canvas'), targetImage, targetImage.original.width);
const canvas = getExportCanvas(targetImage);
const baseName = targetImage.fileName.replace(/\.\w+$/, '');
const suffix = (targetImage.whiteBg ? '_white' : '') + (targetImage.hasWatermark ? '_watermark' : '');
const suffix = (targetImage.whiteBg ? '_white' : '') + (targetImage.watermarkType ? '_watermark' : '');
// 创建下载链接
let dataUrl;
@@ -781,17 +820,83 @@
link.click();
}
// 导出所有图片
exportAllBtn.addEventListener('click', () => {
// 导出所有图片:优先直接写出文件夹(File System Access API),不支持时回退 ZIP
async function exportAllImages() {
if (imageList.length === 0) {
alert('暂无图片可导出');
return;
}
// 批量导出(逐个触发下载)
imageList.forEach((item, i) => {
setTimeout(() => exportImage(item.index), i * 300); // 延迟避免浏览器拦截
});
});
const folderName = getExportFolderName();
// 方案 A:浏览器支持文件系统访问 API(Chrome / Edge 等),直接写入用户选择的文件夹
if (typeof window.showDirectoryPicker === 'function') {
try {
const dirHandle = await window.showDirectoryPicker();
const targetDir = await dirHandle.getDirectoryHandle(folderName, { create: true });
for (const item of imageList) {
const canvas = getExportCanvas(item);
const blob = await canvasToPngBlob(canvas);
const baseName = item.fileName.replace(/\.\w+$/, '');
const suffix = (item.whiteBg ? '_white' : '') + (item.watermarkType ? '_watermark' : '');
const fileHandle = await targetDir.getFileHandle(baseName + suffix + '.png', { create: true });
const writable = await fileHandle.createWritable();
await writable.write(blob);
await writable.close();
}
alert(`已导出 ${imageList.length} 张图片到文件夹「${folderName}`);
return;
} catch (err) {
if (err && err.name === 'AbortError') return; // 用户取消选择目录
console.error('直接导出文件夹失败,回退到 ZIP:', err);
// 其它错误再走下面的 ZIP 回退
}
}
// 方案 B:不支持时回退为 ZIP 打包(需联网加载 JSZip)
if (typeof JSZip === 'undefined') {
alert('当前浏览器不支持直接导出文件夹,请使用 Chrome / Edge 浏览器,或在联网环境下使用 ZIP 打包导出');
return;
}
const zip = new JSZip();
const folder = zip.folder(folderName);
try {
for (const item of imageList) {
const canvas = getExportCanvas(item);
const blob = await canvasToPngBlob(canvas);
const baseName = item.fileName.replace(/\.\w+$/, '');
const suffix = (item.whiteBg ? '_white' : '') + (item.watermarkType ? '_watermark' : '');
folder.file(baseName + suffix + '.png', blob);
}
} catch (err) {
alert('导出失败,请通过本地服务访问页面(start.command)后重试');
console.error('导出失败:', err);
return;
}
let content;
try {
content = await zip.generateAsync({ type: 'blob' });
} catch (err) {
alert('打包失败,请重试');
console.error('打包失败:', err);
return;
}
const link = document.createElement('a');
link.href = URL.createObjectURL(content);
link.download = folderName + '.zip';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
}
exportAllBtn.addEventListener('click', exportAllImages);
clearAllImagesBtn.addEventListener('click', clearAllImages);
function validateDimensions(weight, length, width, height, level) {
const dimensions = [length, width, height].sort((a, b) => b - a);
@@ -854,27 +959,35 @@
function calculateLogisticsFee(weight, length, width, height, level, TDPrice) {
let logisticsFee = 0;
let usedWeight = weight;
let rule = '';
if (level === 'low') {
if (weight <= 500) {
logisticsFee = 3.12 + 0.026 * weight;
rule = 'low3.12 + 0.026×重量';
} else {
logisticsFee = 23.92 + 0.01768 * usedWeight;
rule = 'low23.92 + 0.01768×重量';
}
} else if (level === 'high2') {
if (weight <= 5000) {
logisticsFee = 22.88 + 0.026 * weight;
rule = '高222.88 + 0.026×重量';
} else {
logisticsFee = 64.48 + 0.024 * usedWeight;
rule = '高264.48 + 0.024×重量';
}
} else {
if (weight <= 2000) {
logisticsFee = 16.64 + 0.026 * weight;
rule = '普通:16.64 + 0.026×重量';
} else {
logisticsFee = 37.44 + 0.01768 * usedWeight;
rule = '普通:37.44 + 0.01768×重量';
}
}
logisticsFee += TDPrice;
return { fee: logisticsFee };
if (TDPrice > 0) rule += ' + 通递价';
return { fee: logisticsFee, rule };
}
function calculateAndDisplay() {
const purchasePrice = parseFloat(purchasePriceInput.value) || 0;
@@ -910,18 +1023,24 @@
profitElement.textContent = '--'
commission.textContent = '--';
sellingPriceElement.textContent = '--';
totalCostElement.textContent = '--';
fullCommissionElement.textContent = '--';
logisticsFeeRuleEl.textContent = '';
commissionRuleEl.textContent = '';
fullCommissionRuleEl.textContent = '';
logisticsLevelAlert.classList.add('hidden');
priceRangeAlert.classList.add('hidden');
return;
}
const profitRateDecimal = profitRate / 100;
const { fee: logisticsFee } = calculateLogisticsFee(
const { fee: logisticsFee, rule: logisticsFeeRule } = calculateLogisticsFee(
weight, length, width, height, logisticsLevel, TDPrice
);
const receivedPrice = purchasePrice * (1 + profitRateDecimal);
const profitPrice = purchasePrice * profitRateDecimal;
let commissionPrice
let sellingPrice;
const netRate = (logisticsLevel === 'low') ? 0.845 : 0.785;
if (logisticsLevel === 'low') {
sellingPrice = (receivedPrice + logisticsFee) / 0.845;
commissionPrice = sellingPrice * 0.12;
@@ -929,6 +1048,9 @@
sellingPrice = (receivedPrice + logisticsFee) / 0.785;
commissionPrice = sellingPrice * 0.18;
}
// 完全成本:进货价 + 物流费 + 平台真实总抽成(含佣金及约3.5%其它费)
const fullCommission = sellingPrice * (1 - netRate);
const totalCost = purchasePrice + logisticsFee + fullCommission;
const logisticsLevelMessage = validateLogisticsLevel(sellingPrice, logisticsLevel);
const priceRangeMessage = validatePriceRange(sellingPrice);
if (logisticsLevelMessage) {
@@ -951,7 +1073,12 @@
sellingPriceElement.textContent = `¥ ${sellingPrice.toFixed(2)}`;
profitElement.textContent = `¥ ${profitPrice.toFixed(2)}`
commission.textContent = `¥ ${commissionPrice.toFixed(2)}`;
[logisticsCard, receivedCard, sellingCard].forEach((card, index) => {
totalCostElement.textContent = `¥ ${totalCost.toFixed(2)}`;
fullCommissionElement.textContent = `¥ ${fullCommission.toFixed(2)}`;
logisticsFeeRuleEl.textContent = '(' + logisticsFeeRule + ')';
commissionRuleEl.textContent = (logisticsLevel === 'low') ? '(销售价 × 12%)' : '(销售价 × 18%)';
fullCommissionRuleEl.textContent = (logisticsLevel === 'low') ? '(销售价 × 15.5%)' : '(销售价 × 21.5%)';
[logisticsCard, totalCostCard, receivedCard, sellingCard].forEach((card, index) => {
setTimeout(() => {
card.classList.remove('opacity-0', 'translate-y-4');
}, index * 100);
@@ -986,7 +1113,7 @@
function validateProductInfoForm() {
const fields = [
{ label: '进货价', el: purchasePriceInput, value: purchasePriceInput && purchasePriceInput.value },
{ label: '利率', el: profitRateInput, value: profitRateInput && profitRateInput.value },
{ label: '利率', el: profitRateInput, value: profitRateInput && profitRateInput.value },
{ label: '商品重量', el: weightInput, value: weightInput && weightInput.value },
{ label: '贴单费用', el: TDPriceInput, value: TDPriceInput && TDPriceInput.value },
{ label: '外包装长度', el: lengthInput, value: lengthInput && lengthInput.value },
@@ -1008,6 +1135,42 @@
}
return false;
}
// 轻提示 Toasttype 可选 'success' | 'error' | 'info'
function showToast(message, type = 'info', duration = 2500) {
let container = document.getElementById('toastContainer');
if (!container) {
container = document.createElement('div');
container.id = 'toastContainer';
container.className = 'toast-container';
document.body.appendChild(container);
}
const toast = document.createElement('div');
toast.className = `toast ${type}`;
const icon = document.createElement('i');
const iconClass = type === 'success' ? 'fa fa-check-circle'
: type === 'error' ? 'fa fa-exclamation-circle'
: 'fa fa-info-circle';
icon.className = `toast-icon ${iconClass}`;
toast.appendChild(icon);
const text = document.createElement('span');
text.textContent = message;
toast.appendChild(text);
container.appendChild(toast);
// 触发进入动画
requestAnimationFrame(() => toast.classList.add('show'));
const remove = () => {
toast.classList.remove('show');
toast.classList.add('hide');
toast.addEventListener('transitionend', () => toast.remove(), { once: true });
};
setTimeout(remove, duration);
}
function recordData() {
if (!validateProductInfoForm()) {
return;
@@ -1052,11 +1215,23 @@
const profitRate = (!isNaN(purchaseNum) && purchaseNum > 0 && profit !== '')
? ((parseFloat(profit) / purchaseNum) * 100).toFixed(0)
: '';
const netRate = (logisticsLevel === 'low') ? 0.845 : 0.785;
// 完全成本 / 平台总抽成 直接取结果卡片(totalCostCard)显示值,保证与屏幕完全一致
const cardFullCommission = parseDisplayMoney(fullCommissionElement.textContent);
const cardTotalCost = parseDisplayMoney(totalCostElement.textContent);
const fullCommission = cardFullCommission !== ''
? Number(cardFullCommission)
: Number(sellingPrice) * (1 - netRate);
const totalCost = cardTotalCost !== ''
? Number(cardTotalCost)
: Number(purchasePrice) + Number(logisticsFee) + fullCommission;
const record = {
sku,
modelCode: (modelCodeInput.value || '').trim(),
skuSuffix: (skuSuffixInput.value || '').trim(),
productName,
status: '在售',
purchaseUrl: (purchaseUrlInput.value || '').trim(),
sellingPrice,
sellingPriceReserved: parseDisplayMoney(document.getElementById('sellingPriceReserved').textContent),
@@ -1069,6 +1244,8 @@
purchasePrice,
profit,
profitRate,
fullCommission: fullCommission.toFixed(2),
totalCost: totalCost.toFixed(2),
weight,
dimensions: `${length}x${width}x${height}`,
logisticsLevel: logisticsLevel === 'high' ? '高' : (logisticsLevel === 'high2' ? 'Premium' : '低')
@@ -1076,6 +1253,7 @@
historyData.unshift(record);
localStorage.setItem('priceCalculatorHistory', JSON.stringify(historyData));
renderHistoryTable();
showToast(`商品「${productName || sku}」录入成功`, 'success');
}
function createLinkCell(url, label) {
const cell = document.createElement('td');
@@ -1117,7 +1295,7 @@
return '';
}
function renderHistoryTable() {
const colCount = 15;
const colCount = 17;
if (historyData.length === 0) {
historyTableBody.innerHTML = `<tr class="text-center"><td colspan="${colCount}" class="px-6 py-10 text-gray-500"></td></tr>`;
return;
@@ -1148,17 +1326,19 @@
row.append(
deleteCell,
createCell(record.sku || '--'),
createCell(record.modelCode || '--', true),
createCell(record.productName || '--'),
createCell(formatCny(record.purchasePrice)),
createCell(formatCny(record.logisticsFee)),
createCell(formatCny(record.sellingPrice)),
createCell(formatCny(record.logisticsFee), true),
createCell(formatCny(record.fullCommission), true),
createCell(formatCny(record.receivedPrice)),
createCell(formatCny(record.totalCost)),
createCell(formatCny(record.sellingPrice)),
createCell(profit !== '' ? formatCny(profit) : '--'),
createCell(profitRate),
createCell(formatRub(record.sellingPriceRub)),
createCell(weightText, true),
createCell(dimensionsText, true),
createCell(record.status || '在售', true),
createLinkCell(ozonUrl, ozonUrl),
createLinkCell(purchaseUrl, record.purchaseUrl || purchaseUrl)
);
@@ -1198,8 +1378,8 @@
return;
}
const headers = [
'货号(sku)', '型号', '商品名', '进货价', '物流费', '销售价', '实收价',
'利润', '利率', '卢布销价', '重量', '尺寸', 'Ozon地址', '采买地址'
'货号(sku)', '商品名', '进货价', '物流费', '平台总抽成', '实收价', '完全成本', '销售价',
'利润', '利率', '卢布销价', '重量', '尺寸', '状态', 'Ozon地址', '采买地址'
];
let csvContent = '\uFEFF' + headers.join(',') + '\n';
historyData.forEach(record => {
@@ -1207,17 +1387,19 @@
const profitRate = getRecordProfitRate(record, profit);
const row = [
record.sku || '',
record.modelCode || '',
record.productName || '',
record.purchasePrice || '',
record.logisticsFee || '',
record.sellingPrice || '',
record.fullCommission || '',
record.receivedPrice || '',
record.totalCost || '',
record.sellingPrice || '',
profit,
profitRate,
record.sellingPriceRub || '',
record.weight || '',
record.dimensions || '',
record.status || '在售',
record.sku ? `https://www.ozon.ru/product/${record.sku}` : '',
record.purchaseUrl || ''
].map(csvEscape);
@@ -1353,28 +1535,28 @@
const cnyPrice = parseFloat(sellingPriceElement.textContent.replace(/[^\d.]/g, ''));
if (isNaN(cnyPrice) || cnyPrice <= 0) {
sellingPriceReserved.textContent = '--';
cnyReserveGap.textContent = '--';
cnyReserveGap.textContent = '';
sellingPriceRub.textContent = '--';
sellingPriceRubReserved.textContent = '--';
rubReserveGap.textContent = '--';
rubReserveGap.textContent = '';
return;
}
const cnyReserved = cnyPrice / (1 - percent / 100);
sellingPriceReserved.textContent = `¥ ${cnyReserved.toFixed(2)}`;
cnyReserveGap.textContent = `¥ ${(cnyReserved - cnyPrice).toFixed(2)}`;
cnyReserveGap.textContent = `(折扣空间 ¥${(cnyReserved - cnyPrice).toFixed(2)}`;
if (!cnyToRubRate) {
sellingPriceRub.textContent = '--';
sellingPriceRubReserved.textContent = '--';
rubReserveGap.textContent = '--';
rubReserveGap.textContent = '';
return;
}
const rubPrice = cnyPrice * cnyToRubRate;
const rubReserved = cnyReserved * cnyToRubRate;
sellingPriceRub.textContent = `${rubPrice.toFixed(2)}`;
sellingPriceRubReserved.textContent = `${rubReserved.toFixed(2)}`;
rubReserveGap.textContent = ` ${(rubReserved - rubPrice).toFixed(2)}`;
rubReserveGap.textContent = `(折扣空间 ${(rubReserved - rubPrice).toFixed(2)}`;
sellingCardRub.classList.remove('opacity-0', 'translate-y-4');
}
+53 -65
View File
@@ -80,7 +80,7 @@
</div>
</div>
<div>
<label for="profitRate" class="block text-sm font-medium text-gray-300 mb-1">率 (%)</label>
<label for="profitRate" class="block text-sm font-medium text-gray-300 mb-1">利率 (%)</label>
<input type="number" id="profitRate" step="1" min="0"
class="w-full px-3 py-1.5 border border-gray-700 bg-dark-light rounded-lg input-focus text-gray-100">
</div>
@@ -202,17 +202,31 @@
<div id="logisticsCard" class="flex result-card opacity-0 transform translate-y-4">
<div class="mr-20" style="width: 60%;">
<h3 class="text-lg font-medium text-gray-100 mb-2 flex items-center text-glow">
物流费
物流费<span id="logisticsFeeRule" class="text-xs font-normal text-gray-500 ml-1"></span>
</h3>
<p class="text-3xl font-bold text-gray-100" id="logisticsFee">--</p>
</div>
<div style="width: 40%;">
<h3 class="text-lg font-medium text-gray-100 mb-2 flex items-center text-glow">
平台佣金
平台佣金<span id="commissionRule" class="text-xs font-normal text-gray-500 ml-1"></span>
</h3>
<p class="text-3xl font-bold text-gray-100" id="commission">--</p>
</div>
</div>
<div id="totalCostCard" class="flex result-card opacity-0 transform translate-y-4 transition-delay-100">
<div class="mr-20" style="width: 60%;">
<h3 class="text-lg font-medium text-gray-100 mb-2 flex items-center text-glow">
完全成本<span class="text-xs font-normal text-gray-500 ml-1">(进货价+物流费+平台总抽成)</span>
</h3>
<p class="text-3xl font-bold text-red-400" id="totalCost">--</p>
</div>
<div style="width: 40%;">
<h3 class="text-lg font-medium text-gray-100 mb-2 flex items-center text-glow">
平台总抽成<span id="fullCommissionRule" class="text-xs font-normal text-gray-500 ml-1"></span>
</h3>
<p class="text-3xl font-bold text-red-400" id="fullCommission">--</p>
</div>
</div>
<div id="receivedCard" class="flex result-card opacity-0 transform translate-y-4 transition-delay-100">
<div class="mr-20" style="width: 60%;">
<h3 class="text-lg font-medium text-gray-100 mb-2 flex items-center text-glow">
@@ -222,7 +236,7 @@
</div>
<div style="width: 40%;">
<h3 class="text-lg font-medium text-gray-100 mb-2 flex items-center text-glow">
利润
利润
</h3>
<p class="text-3xl font-bold text-gray-100" id="profitElement">--</p>
</div>
@@ -242,12 +256,11 @@
<div class="flex">
<div class="mr-20" style="width: 60%;">
<p class="text-sm text-gray-400 mb-1">现价</p>
<p class="text-3xl font-bold text-gray-100" id="sellingPrice">--</p>
<p class="text-3xl font-bold text-secondary" id="sellingPrice">--</p>
</div>
<div style="width: 40%;">
<p class="text-sm text-gray-400 mb-1">预留 <span id="cnyReserveLabel">50%</span></p>
<p class="text-3xl font-bold text-gray-100" id="sellingPriceReserved">--</p>
<p class="text-xs text-gray-400 mt-1">折扣空间 <span id="cnyReserveGap">--</span></p>
<p class="text-sm text-gray-400 mb-1">预留 <span id="cnyReserveLabel">50%</span><span id="cnyReserveGap" class="text-xs text-gray-500 ml-1"></span></p>
<p class="text-3xl font-bold text-secondary" id="sellingPriceReserved">--</p>
</div>
</div>
</div>
@@ -262,13 +275,11 @@
<div class="flex">
<div class="mr-20" style="width: 60%;">
<p class="text-sm text-gray-400 mb-1">现价</p>
<p class="text-3xl font-bold text-secondary" id="sellingPriceRub">--</p>
<p class="text-3xl font-bold text-blue-400" id="sellingPriceRub">--</p>
</div>
<div style="width: 40%;">
<p class="text-sm text-gray-400 mb-1">预留 <span id="rubReserveLabel">50%</span></p>
<p class="text-3xl font-bold text-secondary" id="sellingPriceRubReserved">--</p>
<p class="text-xs text-gray-400 mt-1">折扣空间 <span id="rubReserveGap"
class="text-secondary">--</span></p>
<p class="text-sm text-gray-400 mb-1">预留 <span id="rubReserveLabel">50%</span><span id="rubReserveGap" class="text-xs text-gray-500 ml-1"></span></p>
<p class="text-3xl font-bold text-blue-400" id="sellingPriceRubReserved">--</p>
</div>
</div>
</div>
@@ -324,8 +335,8 @@
class="w-full px-3 py-2 border border-gray-700 bg-dark-light rounded-lg input-focus text-gray-100"></textarea>
</div>
<p class="text-xs text-gray-500">
将带入当前:商品名 <span id="aiContextProductName" class="text-gray-300">--</span>
型号 <span id="aiContextModelCode" class="text-gray-300">--</span>
将带入当前:型号 <span id="aiContextModelCode" class="text-gray-300">--</span>
<span class="text-gray-600">(生成文案不读取商品名)</span>
</p>
<div class="flex flex-wrap items-center gap-3">
<label for="aiModelSelect" class="text-sm text-gray-400 whitespace-nowrap">模型</label>
@@ -350,7 +361,7 @@
<div class="flex items-center justify-between mb-1 gap-3">
<label class="block text-sm font-medium text-gray-300">描述</label>
<button type="button" data-copy-source="aiDescRu"
class="ai-copy-btn shrink-0 px-3 py-1.5 text-sm rounded-lg bg-primary/20 hover:bg-primary/30 text-primary transition-colors duration-200">复制俄文</button>
class="ai-copy-btn ai-copy-trigger shrink-0 px-3 py-1.5 text-sm rounded-lg bg-primary/20 hover:bg-primary/30 text-primary transition-colors duration-200">复制俄文</button>
</div>
<div id="aiDescRu" class="ai-text-block ai-text-block-ru" data-placeholder="俄文描述将展示在这里"></div>
<div class="ai-text-divider"></div>
@@ -361,7 +372,7 @@
<div class="flex items-center justify-between mb-1 gap-3">
<label class="block text-sm font-medium text-gray-300">标签</label>
<button type="button" id="aiCopyAllTagsBtn"
class="ai-copy-all-tags-btn shrink-0 px-3 py-1.5 text-sm rounded-lg bg-primary/20 hover:bg-primary/30 text-primary transition-colors duration-200">复制全部俄文标签</button>
class="ai-copy-all-tags-btn ai-copy-trigger shrink-0 px-3 py-1.5 text-sm rounded-lg bg-primary/20 hover:bg-primary/30 text-primary transition-colors duration-200">复制全部俄文标签</button>
</div>
<div id="aiTagsContainer" class="flex flex-wrap gap-2 min-h-[2.5rem]">
<p class="text-sm text-gray-500">生成后将在此展示标签,点击可复制俄文</p>
@@ -455,10 +466,14 @@
class="flex-1 bg-primary hover:bg-primary/90 text-white py-3 rounded-lg flex items-center justify-center transition-all duration-200 transform hover:scale-[1.02] hover:shadow-lg hover:shadow-primary/20">
<i class="fa fa-tint mr-2"></i>添加水印
</button>
<button id="exportAllBtn" disabled
<button id="exportAllBtn" disabled title="导出为文件夹(Chrome/Edge 直接生成文件夹;其它浏览器打包为 ZIP)"
class="flex-1 bg-secondary hover:bg-secondary/90 text-white py-3 rounded-lg flex items-center justify-center transition-all duration-200 transform hover:scale-[1.02] hover:shadow-lg hover:shadow-secondary/20 opacity-50 cursor-not-allowed">
<i class="fa fa-download mr-2"></i>导出所有图片
</button>
<button id="clearAllImagesBtn" disabled
class="bg-danger/20 hover:bg-danger/30 text-danger px-5 py-3 rounded-lg flex items-center justify-center transition-all duration-200 whitespace-nowrap opacity-50 cursor-not-allowed">
<i class="fa fa-trash mr-2"></i>清空所有图片
</button>
</div>
</div>
</div>
@@ -478,7 +493,7 @@
<button type="button" id="toggleOptionalColsBtn"
class="bg-gray-800 hover:bg-gray-700 text-gray-300 px-3 py-1.5 rounded-lg text-sm flex items-center transition-colors duration-200"
aria-pressed="false">
显示型号/重量/尺寸
显示物流费/平台总抽成/重量/尺寸/状态
</button>
<button type="button" id="transferDataBtn"
class="bg-primary/20 hover:bg-primary/30 text-primary px-3 py-1.5 rounded-lg text-sm flex items-center transition-colors duration-200">
@@ -497,56 +512,28 @@
<table id="historyTable" class="min-w-full divide-y divide-gray-700">
<thead class="bg-dark-light">
<tr>
<th scope="col"
class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">操作
</th>
<th scope="col"
class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">货号
(sku)</th>
<th scope="col"
class="history-col-optional px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
型号</th>
<th scope="col"
class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">商品名
</th>
<th scope="col"
class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">进货价
</th>
<th scope="col"
class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">物流费
</th>
<th scope="col"
class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">销售价
</th>
<th scope="col"
class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">实收价
</th>
<th scope="col"
class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">利润
</th>
<th scope="col"
class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">利润率
</th>
<th scope="col"
class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">卢布销价
</th>
<th scope="col"
class="history-col-optional px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
重量</th>
<th scope="col"
class="history-col-optional px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
尺寸</th>
<th scope="col"
class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Ozon地址
</th>
<th scope="col"
class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">采买地址
</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">操作</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">货号(sku)</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">商品名</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">进货价</th>
<th scope="col" class="history-col-optional px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">物流费</th>
<th scope="col" class="history-col-optional px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">平台总抽成</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">实收价</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">完全成本</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">销售价</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">净利润</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">净利率</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">卢布销价</th>
<th scope="col" class="history-col-optional px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">重量</th>
<th scope="col" class="history-col-optional px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">尺寸</th>
<th scope="col" class="history-col-optional px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">状态</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Ozon地址</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">采买地址</th>
</tr>
</thead>
<tbody class="bg-dark-card divide-y divide-gray-700" id="historyTableBody">
<tr class="text-center">
<td colspan="15" class="px-6 py-10 text-gray-500"></td>
<td colspan="17" class="px-6 py-10 text-gray-500"></td>
</tr>
</tbody>
</table>
@@ -568,6 +555,7 @@
</div>
</main>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<script src="js/app.js"></script>
<script src="js/ai-copy.js"></script>
</body>