feat: 前端的细节修改
This commit is contained in:
@@ -0,0 +1 @@
|
||||
- [ ] 把多种图片合并成一个视频
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+44
-12
@@ -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 {
|
||||
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
@@ -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 = 'low:3.12 + 0.026×重量';
|
||||
} else {
|
||||
logisticsFee = 23.92 + 0.01768 * usedWeight;
|
||||
rule = 'low:23.92 + 0.01768×重量';
|
||||
}
|
||||
} else if (level === 'high2') {
|
||||
if (weight <= 5000) {
|
||||
logisticsFee = 22.88 + 0.026 * weight;
|
||||
rule = '高2:22.88 + 0.026×重量';
|
||||
} else {
|
||||
logisticsFee = 64.48 + 0.024 * usedWeight;
|
||||
rule = '高2:64.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;
|
||||
}
|
||||
// 轻提示 Toast:type 可选 '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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user