feat: 前端的细节修改
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
- [ ] 把多种图片合并成一个视频
|
||||||
@@ -218,8 +218,86 @@
|
|||||||
.ai-tag-chip-zh {
|
.ai-tag-chip-zh {
|
||||||
color: #9ca3af;
|
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 {
|
#historyTable:not(.show-optional-cols) .history-col-optional {
|
||||||
display: none;
|
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
@@ -11,7 +11,6 @@
|
|||||||
const bodyEl = document.getElementById('aiCopyBody');
|
const bodyEl = document.getElementById('aiCopyBody');
|
||||||
const statusEl = document.getElementById('aiCopyStatus');
|
const statusEl = document.getElementById('aiCopyStatus');
|
||||||
const modelSelectEl = document.getElementById('aiModelSelect');
|
const modelSelectEl = document.getElementById('aiModelSelect');
|
||||||
const contextProductNameEl = document.getElementById('aiContextProductName');
|
|
||||||
const contextModelCodeEl = document.getElementById('aiContextModelCode');
|
const contextModelCodeEl = document.getElementById('aiContextModelCode');
|
||||||
const titlesContainerEl = document.getElementById('aiTitlesContainer');
|
const titlesContainerEl = document.getElementById('aiTitlesContainer');
|
||||||
const tagsContainerEl = document.getElementById('aiTagsContainer');
|
const tagsContainerEl = document.getElementById('aiTagsContainer');
|
||||||
@@ -38,30 +37,64 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function syncContextHints() {
|
function syncContextHints() {
|
||||||
if (contextProductNameEl) {
|
// 注:生成文案不再带入「商品名」,故此处只同步型号提示
|
||||||
const name = (productNameInput && productNameInput.value || '').trim();
|
|
||||||
contextProductNameEl.textContent = name || '--';
|
|
||||||
}
|
|
||||||
if (contextModelCodeEl) {
|
if (contextModelCodeEl) {
|
||||||
const model = (modelCodeInput && modelCodeInput.value || '').trim();
|
const model = (modelCodeInput && modelCodeInput.value || '').trim();
|
||||||
contextModelCodeEl.textContent = model || '--';
|
contextModelCodeEl.textContent = model || '--';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyText(text) {
|
async function copyText(text, btn) {
|
||||||
const value = (text || '').trim();
|
const value = (text || '').trim();
|
||||||
if (!value) {
|
if (!value) {
|
||||||
setStatus('没有可复制的内容', true);
|
setStatus('没有可复制的内容', true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
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('已复制');
|
setStatus('已复制');
|
||||||
|
showCopyFeedback(btn);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setStatus('复制失败,请手动选择文本', true);
|
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) {
|
function toStringList(value) {
|
||||||
return Array.isArray(value)
|
return Array.isArray(value)
|
||||||
? value.map(function (item) { return String(item).trim(); })
|
? value.map(function (item) { return String(item).trim(); })
|
||||||
@@ -98,9 +131,9 @@
|
|||||||
|
|
||||||
const copyBtn = document.createElement('button');
|
const copyBtn = document.createElement('button');
|
||||||
copyBtn.type = '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.textContent = '复制俄文';
|
||||||
copyBtn.addEventListener('click', function () { copyText(ru); });
|
copyBtn.addEventListener('click', function () { copyText(ru, copyBtn); });
|
||||||
|
|
||||||
const fillBtn = document.createElement('button');
|
const fillBtn = document.createElement('button');
|
||||||
fillBtn.type = 'button';
|
fillBtn.type = 'button';
|
||||||
@@ -253,7 +286,6 @@
|
|||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
source_text: sourceText,
|
source_text: sourceText,
|
||||||
product_name: (productNameInput && productNameInput.value || '').trim(),
|
|
||||||
model_code: (modelCodeInput && modelCodeInput.value || '').trim(),
|
model_code: (modelCodeInput && modelCodeInput.value || '').trim(),
|
||||||
model: (modelSelectEl && modelSelectEl.value || '').trim(),
|
model: (modelSelectEl && modelSelectEl.value || '').trim(),
|
||||||
};
|
};
|
||||||
@@ -307,7 +339,7 @@
|
|||||||
btn.addEventListener('click', function () {
|
btn.addEventListener('click', function () {
|
||||||
const sourceId = btn.getAttribute('data-copy-source');
|
const sourceId = btn.getAttribute('data-copy-source');
|
||||||
const el = sourceId ? document.getElementById(sourceId) : null;
|
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) {
|
tagsContainerEl.addEventListener('click', function (event) {
|
||||||
const chip = event.target.closest('.ai-tag-chip');
|
const chip = event.target.closest('.ai-tag-chip');
|
||||||
if (!chip || !tagsContainerEl.contains(chip)) return;
|
if (!chip || !tagsContainerEl.contains(chip)) return;
|
||||||
copyText(chip.getAttribute('data-tag-ru') || '');
|
copyText(chip.getAttribute('data-tag-ru') || '', chip);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (copyAllTagsBtn) {
|
if (copyAllTagsBtn) {
|
||||||
copyAllTagsBtn.addEventListener('click', function () {
|
copyAllTagsBtn.addEventListener('click', function () {
|
||||||
copyText(lastTagsRu.join(', '));
|
copyText(lastTagsRu.join(', '), copyAllTagsBtn);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+296
-114
@@ -31,7 +31,13 @@
|
|||||||
const profitElement = document.getElementById('profitElement');
|
const profitElement = document.getElementById('profitElement');
|
||||||
const commission = document.getElementById('commission');
|
const commission = document.getElementById('commission');
|
||||||
const sellingPriceElement = document.getElementById('sellingPrice');
|
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 logisticsCard = document.getElementById('logisticsCard');
|
||||||
|
const totalCostCard = document.getElementById('totalCostCard');
|
||||||
const receivedCard = document.getElementById('receivedCard');
|
const receivedCard = document.getElementById('receivedCard');
|
||||||
const sellingCard = document.getElementById('sellingCard');
|
const sellingCard = document.getElementById('sellingCard');
|
||||||
const dimensionAlert = document.getElementById('dimensionAlert');
|
const dimensionAlert = document.getElementById('dimensionAlert');
|
||||||
@@ -71,6 +77,7 @@
|
|||||||
const previewContainer = document.getElementById('previewContainer');
|
const previewContainer = document.getElementById('previewContainer');
|
||||||
const addWatermarkBtn = document.getElementById('addWatermarkBtn');
|
const addWatermarkBtn = document.getElementById('addWatermarkBtn');
|
||||||
const exportAllBtn = document.getElementById('exportAllBtn');
|
const exportAllBtn = document.getElementById('exportAllBtn');
|
||||||
|
const clearAllImagesBtn = document.getElementById('clearAllImagesBtn');
|
||||||
const watermarkOpacityInput = document.getElementById('watermarkOpacity');
|
const watermarkOpacityInput = document.getElementById('watermarkOpacity');
|
||||||
const watermarkTextInput = document.getElementById('watermarkText');
|
const watermarkTextInput = document.getElementById('watermarkText');
|
||||||
const whiteBgToleranceInput = document.getElementById('whiteBgTolerance');
|
const whiteBgToleranceInput = document.getElementById('whiteBgTolerance');
|
||||||
@@ -84,11 +91,14 @@
|
|||||||
const WATERMARK_SCALE = 0.15; // 水印直径占原图宽度的比例
|
const WATERMARK_SCALE = 0.15; // 水印直径占原图宽度的比例
|
||||||
const WATERMARK_MARGIN = 10; // 默认位置距右下角的间距(原图像素)
|
const WATERMARK_MARGIN = 10; // 默认位置距右下角的间距(原图像素)
|
||||||
const WATERMARK_TEXT_SCALE = 0.06; // 文字水印字号占原图宽度的比例
|
const WATERMARK_TEXT_SCALE = 0.06; // 文字水印字号占原图宽度的比例
|
||||||
|
const WATERMARK_IMAGE_SHRINK = 0.8; // 加图片水印后,图片缩到当前尺寸的 80%
|
||||||
const DEFAULT_WHITE_BG_TOLERANCE = 60; // 白底颜色容差,越大清除得越狠
|
const DEFAULT_WHITE_BG_TOLERANCE = 60; // 白底颜色容差,越大清除得越狠
|
||||||
const PREVIEW_MAX_WIDTH = 400; // 预览画布宽度
|
const PREVIEW_MAX_WIDTH = 400; // 预览画布宽度
|
||||||
const DEFAULT_WATERMARK_OPACITY = 30;
|
const DEFAULT_WATERMARK_OPACITY = 30;
|
||||||
// 存储上传的图片信息(原图 + 水印状态)
|
// 存储上传的图片信息(原图 + 水印状态)
|
||||||
let imageList = [];
|
let imageList = [];
|
||||||
|
// 自增序号,保证每张图片拥有全局唯一的 index(多批追加时 data-index 不冲突)
|
||||||
|
let imageSeq = 0;
|
||||||
let watermarkImg = null;
|
let watermarkImg = null;
|
||||||
let watermarkImgPromise = null;
|
let watermarkImgPromise = null;
|
||||||
renderHistoryTable();
|
renderHistoryTable();
|
||||||
@@ -169,7 +179,7 @@
|
|||||||
toggleOptionalColsBtn.addEventListener('click', () => {
|
toggleOptionalColsBtn.addEventListener('click', () => {
|
||||||
const shown = historyTable.classList.toggle('show-optional-cols');
|
const shown = historyTable.classList.toggle('show-optional-cols');
|
||||||
toggleOptionalColsBtn.setAttribute('aria-pressed', shown ? 'true' : 'false');
|
toggleOptionalColsBtn.setAttribute('aria-pressed', shown ? 'true' : 'false');
|
||||||
toggleOptionalColsBtn.textContent = shown ? '隐藏型号/重量/尺寸' : '显示型号/重量/尺寸';
|
toggleOptionalColsBtn.textContent = shown ? '隐藏物流费/平台总抽成/重量/尺寸/状态' : '显示物流费/平台总抽成/重量/尺寸/状态';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
clearHistoryBtn.addEventListener('click', clearHistory);
|
clearHistoryBtn.addEventListener('click', clearHistory);
|
||||||
@@ -235,44 +245,79 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理上传的文件
|
// 处理上传的文件(追加模式:不清空已有图片)
|
||||||
function handleFiles(files) {
|
function handleFiles(files) {
|
||||||
imageList = [];
|
|
||||||
if (files.length === 0) return;
|
if (files.length === 0) return;
|
||||||
|
|
||||||
// 清空预览容器
|
|
||||||
previewContainer.innerHTML = '';
|
|
||||||
|
|
||||||
// 遍历文件并加载图片
|
// 遍历文件并加载图片
|
||||||
Array.from(files).forEach((file, index) => {
|
Array.from(files).forEach((file) => {
|
||||||
if (!file.type.startsWith('image/')) return;
|
if (!file.type.startsWith('image/')) return;
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
// 存储图片信息
|
// 存储图片信息(index 使用全局自增序号,保证唯一,避免多批追加时冲突)
|
||||||
const item = {
|
const item = {
|
||||||
index,
|
index: imageSeq++,
|
||||||
original: img,
|
original: img,
|
||||||
whiteBg: null, // 白底处理后的 Canvas(与原图同尺寸)
|
whiteBg: null, // 白底处理后的 Canvas(与原图同尺寸)
|
||||||
whiteBgRatio: 0, // 被判定为背景的像素占比
|
whiteBgRatio: 0, // 被判定为背景的像素占比
|
||||||
fileName: file.name,
|
fileName: file.name,
|
||||||
hasWatermark: false,
|
watermarkType: null, // 水印类型:'image' | 'text' | null(无)
|
||||||
pos: { x: 0, y: 0 } // 水印左上角在原图中的坐标
|
pos: { x: 0, y: 0 } // 水印左上角在原图中的坐标
|
||||||
};
|
};
|
||||||
imageList.push(item);
|
imageList.push(item);
|
||||||
|
// 上传后默认加上当前选择的水印(默认图片水印)
|
||||||
|
item.watermarkType = getWatermarkType();
|
||||||
|
item.pos = defaultWatermarkPos(item, item.watermarkType);
|
||||||
// 显示原始图片预览
|
// 显示原始图片预览
|
||||||
renderPreview(item);
|
renderPreview(item);
|
||||||
|
// 图片水印需确保水印图加载完成后再重绘一次,避免“只缩小没水印”
|
||||||
|
if (item.watermarkType === 'image') {
|
||||||
|
loadWatermarkImage().then(() => renderPreview(item)).catch(() => {});
|
||||||
|
}
|
||||||
|
// 每张图片加载完成后刷新按钮状态,确保有图即可操作
|
||||||
|
updateImageActionsState();
|
||||||
};
|
};
|
||||||
img.src = e.target.result;
|
img.src = e.target.result;
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 启用导出按钮(先显示原始图片,添加水印后更新)
|
function showEmptyPreviewPlaceholder() {
|
||||||
exportAllBtn.disabled = false;
|
previewContainer.innerHTML = '<p class="col-span-full text-gray-500 text-center py-4">暂无图片</p>';
|
||||||
exportAllBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
}
|
||||||
|
|
||||||
|
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');
|
const measureCtx = document.createElement('canvas').getContext('2d');
|
||||||
|
|
||||||
// 水印在原图坐标系中的尺寸
|
// 水印在原图坐标系中的尺寸(type 不传时取全局当前选择)
|
||||||
function getWatermarkSize(item) {
|
function getWatermarkSize(item, type = getWatermarkType()) {
|
||||||
if (getWatermarkType() === 'text') {
|
if (type === 'text') {
|
||||||
const fontSize = getWatermarkFontSize(item);
|
const fontSize = getWatermarkFontSize(item);
|
||||||
measureCtx.font = getWatermarkFont(fontSize);
|
measureCtx.font = getWatermarkFont(fontSize);
|
||||||
return {
|
return {
|
||||||
@@ -336,8 +381,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 限制水印不被拖出图片范围
|
// 限制水印不被拖出图片范围
|
||||||
function clampWatermarkPos(item, x, y) {
|
function clampWatermarkPos(item, x, y, type = getWatermarkType()) {
|
||||||
const size = getWatermarkSize(item);
|
const size = getWatermarkSize(item, type);
|
||||||
const maxX = Math.max(0, item.original.width - size.width);
|
const maxX = Math.max(0, item.original.width - size.width);
|
||||||
const maxY = Math.max(0, item.original.height - size.height);
|
const maxY = Math.max(0, item.original.height - size.height);
|
||||||
return {
|
return {
|
||||||
@@ -347,8 +392,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 默认位置:右下角
|
// 默认位置:右下角
|
||||||
function defaultWatermarkPos(item) {
|
function defaultWatermarkPos(item, type = getWatermarkType()) {
|
||||||
const size = getWatermarkSize(item);
|
const size = getWatermarkSize(item, type);
|
||||||
return clampWatermarkPos(
|
return clampWatermarkPos(
|
||||||
item,
|
item,
|
||||||
item.original.width - size.width - WATERMARK_MARGIN,
|
item.original.width - size.width - WATERMARK_MARGIN,
|
||||||
@@ -399,11 +444,6 @@
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 水印所需资源是否就绪
|
|
||||||
function isWatermarkReady() {
|
|
||||||
return getWatermarkType() === 'text' ? true : !!watermarkImg;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWhiteBgTolerance() {
|
function getWhiteBgTolerance() {
|
||||||
const value = parseFloat(whiteBgToleranceInput.value);
|
const value = parseFloat(whiteBgToleranceInput.value);
|
||||||
if (!isFinite(value)) return DEFAULT_WHITE_BG_TOLERANCE;
|
if (!isFinite(value)) return DEFAULT_WHITE_BG_TOLERANCE;
|
||||||
@@ -527,20 +567,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 按指定宽度把原图和水印合成到 Canvas
|
// 按指定宽度把原图和水印合成到 Canvas
|
||||||
|
// 图片水印会让整张图缩到当前的 80%(renderScale),文字水印保持原尺寸
|
||||||
function drawItemToCanvas(canvas, item, targetWidth) {
|
function drawItemToCanvas(canvas, item, targetWidth) {
|
||||||
|
const renderScale = item.watermarkType === 'image' ? WATERMARK_IMAGE_SHRINK : 1;
|
||||||
const scale = targetWidth / item.original.width;
|
const scale = targetWidth / item.original.width;
|
||||||
canvas.width = Math.max(1, Math.round(targetWidth));
|
canvas.width = Math.max(1, Math.round(targetWidth * renderScale));
|
||||||
canvas.height = Math.max(1, Math.round(item.original.height * scale));
|
canvas.height = Math.max(1, Math.round(item.original.height * scale * renderScale));
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
ctx.drawImage(item.whiteBg || item.original, 0, 0, canvas.width, canvas.height);
|
ctx.drawImage(item.whiteBg || item.original, 0, 0, canvas.width, canvas.height);
|
||||||
if (item.hasWatermark && isWatermarkReady()) {
|
if (item.watermarkType && (item.watermarkType === 'text' || !!watermarkImg)) {
|
||||||
const drawX = item.pos.x * scale;
|
const drawX = item.pos.x * scale * renderScale;
|
||||||
const drawY = item.pos.y * scale;
|
const drawY = item.pos.y * scale * renderScale;
|
||||||
if (getWatermarkType() === 'text') {
|
if (item.watermarkType === 'text') {
|
||||||
drawTextWatermark(ctx, drawX, drawY, getWatermarkFontSize(item) * scale);
|
drawTextWatermark(ctx, drawX, drawY, getWatermarkFontSize(item) * scale * renderScale);
|
||||||
} else {
|
} else {
|
||||||
drawImageWatermark(ctx, drawX, drawY, getWatermarkSize(item).width * scale);
|
drawImageWatermark(ctx, drawX, drawY, getWatermarkSize(item, 'image').width * scale * renderScale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return canvas;
|
return canvas;
|
||||||
@@ -560,9 +602,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
canvas.addEventListener('pointerdown', (e) => {
|
canvas.addEventListener('pointerdown', (e) => {
|
||||||
if (!item.hasWatermark || !isWatermarkReady()) return;
|
if (!item.watermarkType || (item.watermarkType === 'image' && !watermarkImg)) return;
|
||||||
const point = toImageCoords(e);
|
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
|
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;
|
&& point.y >= item.pos.y && point.y <= item.pos.y + size.height;
|
||||||
if (!inside) return;
|
if (!inside) return;
|
||||||
@@ -590,54 +632,25 @@
|
|||||||
canvas.addEventListener('pointercancel', stopDrag);
|
canvas.addEventListener('pointercancel', stopDrag);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 单张图片:切换是否贴水印
|
// 单张图片:切换是否贴水印(开启时用当前选择的水印类型)
|
||||||
function toggleItemWatermark(item) {
|
function toggleItemWatermark(item) {
|
||||||
if (item.hasWatermark) {
|
if (item.watermarkType) {
|
||||||
item.hasWatermark = false;
|
item.watermarkType = null;
|
||||||
} else {
|
} else {
|
||||||
item.hasWatermark = true;
|
item.watermarkType = getWatermarkType();
|
||||||
item.pos = defaultWatermarkPos(item);
|
item.pos = defaultWatermarkPos(item, item.watermarkType);
|
||||||
}
|
}
|
||||||
renderPreview(item);
|
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) {
|
function syncPreviewActions(previewItem, item) {
|
||||||
const watermarkBtn = previewItem.querySelector('[data-role="toggle-watermark"]');
|
const watermarkBtn = previewItem.querySelector('[data-role="toggle-watermark"]');
|
||||||
const whiteBgBtn = previewItem.querySelector('[data-role="toggle-white-bg"]');
|
const deleteBtn = previewItem.querySelector('[data-role="delete-item"]');
|
||||||
watermarkBtn.textContent = item.hasWatermark ? '清除水印' : '加水印';
|
watermarkBtn.textContent = item.watermarkType ? '清除水印' : '加水印';
|
||||||
watermarkBtn.title = item.hasWatermark ? '这张图不加水印' : '给这张图加上水印';
|
watermarkBtn.title = item.watermarkType ? '这张图不加水印' : '给这张图加上水印';
|
||||||
whiteBgBtn.disabled = false;
|
deleteBtn.textContent = '删除';
|
||||||
whiteBgBtn.textContent = item.whiteBg ? '还原底色' : '白底';
|
deleteBtn.title = '从列表中移除这张图片';
|
||||||
whiteBgBtn.title = item.whiteBg
|
|
||||||
? `已把 ${(item.whiteBgRatio * 100).toFixed(0)}% 的画面刷成白底,点击还原`
|
|
||||||
: '把这张图的背景刷成纯白';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染(或更新)单张图片的预览
|
// 渲染(或更新)单张图片的预览
|
||||||
@@ -666,12 +679,12 @@
|
|||||||
watermarkBtn.onclick = () => toggleItemWatermark(item);
|
watermarkBtn.onclick = () => toggleItemWatermark(item);
|
||||||
actions.appendChild(watermarkBtn);
|
actions.appendChild(watermarkBtn);
|
||||||
|
|
||||||
const whiteBgBtn = document.createElement('button');
|
const deleteBtn = document.createElement('button');
|
||||||
whiteBgBtn.type = 'button';
|
deleteBtn.type = 'button';
|
||||||
whiteBgBtn.className = 'preview-action-btn';
|
deleteBtn.className = 'preview-action-btn';
|
||||||
whiteBgBtn.dataset.role = 'toggle-white-bg';
|
deleteBtn.dataset.role = 'delete-item';
|
||||||
whiteBgBtn.onclick = () => toggleItemWhiteBackground(item, whiteBgBtn);
|
deleteBtn.onclick = () => deleteItem(item);
|
||||||
actions.appendChild(whiteBgBtn);
|
actions.appendChild(deleteBtn);
|
||||||
|
|
||||||
frame.appendChild(actions);
|
frame.appendChild(actions);
|
||||||
previewItem.appendChild(frame);
|
previewItem.appendChild(frame);
|
||||||
@@ -686,7 +699,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const canvas = previewItem.querySelector('canvas');
|
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);
|
drawItemToCanvas(canvas, item, PREVIEW_MAX_WIDTH);
|
||||||
syncPreviewActions(previewItem, item);
|
syncPreviewActions(previewItem, item);
|
||||||
}
|
}
|
||||||
@@ -719,21 +732,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
imageList.forEach(item => {
|
imageList.forEach(item => {
|
||||||
if (!item.hasWatermark) {
|
// 不论之前是什么水印,都用当前选择的水印类型重做:清除旧水印、加入新水印
|
||||||
item.hasWatermark = true;
|
item.watermarkType = getWatermarkType();
|
||||||
item.pos = defaultWatermarkPos(item);
|
item.pos = defaultWatermarkPos(item, item.watermarkType);
|
||||||
}
|
|
||||||
renderPreview(item);
|
renderPreview(item);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 切换水印类型:显示对应的表单项,并把已有水印重新约束回图片范围内
|
// 切换水印类型:只切换对应的表单项显示,不改动已有图片的水印
|
||||||
|
// (已有图片的水印类型只有在点击「添加水印」时才会被替换)
|
||||||
function syncWatermarkTypeUI() {
|
function syncWatermarkTypeUI() {
|
||||||
const isText = getWatermarkType() === 'text';
|
const isText = getWatermarkType() === 'text';
|
||||||
watermarkImageOptions.classList.toggle('hidden', isText);
|
watermarkImageOptions.classList.toggle('hidden', isText);
|
||||||
watermarkTextOptions.classList.toggle('hidden', !isText);
|
watermarkTextOptions.classList.toggle('hidden', !isText);
|
||||||
imageList.forEach(item => {
|
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();
|
renderAllPreviews();
|
||||||
}
|
}
|
||||||
@@ -743,7 +756,7 @@
|
|||||||
watermarkOpacityInput.addEventListener('input', renderAllPreviews);
|
watermarkOpacityInput.addEventListener('input', renderAllPreviews);
|
||||||
watermarkTextInput.addEventListener('input', () => {
|
watermarkTextInput.addEventListener('input', () => {
|
||||||
imageList.forEach(item => {
|
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();
|
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) {
|
function exportImage(index) {
|
||||||
const targetImage = imageList.find(item => item.index === index);
|
const targetImage = imageList.find(item => item.index === index);
|
||||||
if (!targetImage) return;
|
if (!targetImage) return;
|
||||||
|
|
||||||
// 按原图尺寸重新合成,保证导出的是全分辨率图片
|
// 按原图尺寸重新合成,保证导出的是全分辨率图片
|
||||||
const canvas = drawItemToCanvas(document.createElement('canvas'), targetImage, targetImage.original.width);
|
const canvas = getExportCanvas(targetImage);
|
||||||
const baseName = targetImage.fileName.replace(/\.\w+$/, '');
|
const baseName = targetImage.fileName.replace(/\.\w+$/, '');
|
||||||
const suffix = (targetImage.whiteBg ? '_white' : '') + (targetImage.hasWatermark ? '_watermark' : '');
|
const suffix = (targetImage.whiteBg ? '_white' : '') + (targetImage.watermarkType ? '_watermark' : '');
|
||||||
|
|
||||||
// 创建下载链接
|
// 创建下载链接
|
||||||
let dataUrl;
|
let dataUrl;
|
||||||
@@ -781,17 +820,83 @@
|
|||||||
link.click();
|
link.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出所有图片
|
// 导出所有图片:优先直接写出文件夹(File System Access API),不支持时回退 ZIP
|
||||||
exportAllBtn.addEventListener('click', () => {
|
async function exportAllImages() {
|
||||||
if (imageList.length === 0) {
|
if (imageList.length === 0) {
|
||||||
alert('暂无图片可导出');
|
alert('暂无图片可导出');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 批量导出(逐个触发下载)
|
|
||||||
imageList.forEach((item, i) => {
|
const folderName = getExportFolderName();
|
||||||
setTimeout(() => exportImage(item.index), i * 300); // 延迟避免浏览器拦截
|
|
||||||
});
|
// 方案 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) {
|
function validateDimensions(weight, length, width, height, level) {
|
||||||
const dimensions = [length, width, height].sort((a, b) => b - a);
|
const dimensions = [length, width, height].sort((a, b) => b - a);
|
||||||
@@ -854,27 +959,35 @@
|
|||||||
function calculateLogisticsFee(weight, length, width, height, level, TDPrice) {
|
function calculateLogisticsFee(weight, length, width, height, level, TDPrice) {
|
||||||
let logisticsFee = 0;
|
let logisticsFee = 0;
|
||||||
let usedWeight = weight;
|
let usedWeight = weight;
|
||||||
|
let rule = '';
|
||||||
if (level === 'low') {
|
if (level === 'low') {
|
||||||
if (weight <= 500) {
|
if (weight <= 500) {
|
||||||
logisticsFee = 3.12 + 0.026 * weight;
|
logisticsFee = 3.12 + 0.026 * weight;
|
||||||
|
rule = 'low:3.12 + 0.026×重量';
|
||||||
} else {
|
} else {
|
||||||
logisticsFee = 23.92 + 0.01768 * usedWeight;
|
logisticsFee = 23.92 + 0.01768 * usedWeight;
|
||||||
|
rule = 'low:23.92 + 0.01768×重量';
|
||||||
}
|
}
|
||||||
} else if (level === 'high2') {
|
} else if (level === 'high2') {
|
||||||
if (weight <= 5000) {
|
if (weight <= 5000) {
|
||||||
logisticsFee = 22.88 + 0.026 * weight;
|
logisticsFee = 22.88 + 0.026 * weight;
|
||||||
|
rule = '高2:22.88 + 0.026×重量';
|
||||||
} else {
|
} else {
|
||||||
logisticsFee = 64.48 + 0.024 * usedWeight;
|
logisticsFee = 64.48 + 0.024 * usedWeight;
|
||||||
|
rule = '高2:64.48 + 0.024×重量';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (weight <= 2000) {
|
if (weight <= 2000) {
|
||||||
logisticsFee = 16.64 + 0.026 * weight;
|
logisticsFee = 16.64 + 0.026 * weight;
|
||||||
|
rule = '普通:16.64 + 0.026×重量';
|
||||||
} else {
|
} else {
|
||||||
logisticsFee = 37.44 + 0.01768 * usedWeight;
|
logisticsFee = 37.44 + 0.01768 * usedWeight;
|
||||||
|
rule = '普通:37.44 + 0.01768×重量';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logisticsFee += TDPrice;
|
logisticsFee += TDPrice;
|
||||||
return { fee: logisticsFee };
|
if (TDPrice > 0) rule += ' + 通递价';
|
||||||
|
return { fee: logisticsFee, rule };
|
||||||
}
|
}
|
||||||
function calculateAndDisplay() {
|
function calculateAndDisplay() {
|
||||||
const purchasePrice = parseFloat(purchasePriceInput.value) || 0;
|
const purchasePrice = parseFloat(purchasePriceInput.value) || 0;
|
||||||
@@ -910,18 +1023,24 @@
|
|||||||
profitElement.textContent = '--'
|
profitElement.textContent = '--'
|
||||||
commission.textContent = '--';
|
commission.textContent = '--';
|
||||||
sellingPriceElement.textContent = '--';
|
sellingPriceElement.textContent = '--';
|
||||||
|
totalCostElement.textContent = '--';
|
||||||
|
fullCommissionElement.textContent = '--';
|
||||||
|
logisticsFeeRuleEl.textContent = '';
|
||||||
|
commissionRuleEl.textContent = '';
|
||||||
|
fullCommissionRuleEl.textContent = '';
|
||||||
logisticsLevelAlert.classList.add('hidden');
|
logisticsLevelAlert.classList.add('hidden');
|
||||||
priceRangeAlert.classList.add('hidden');
|
priceRangeAlert.classList.add('hidden');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const profitRateDecimal = profitRate / 100;
|
const profitRateDecimal = profitRate / 100;
|
||||||
const { fee: logisticsFee } = calculateLogisticsFee(
|
const { fee: logisticsFee, rule: logisticsFeeRule } = calculateLogisticsFee(
|
||||||
weight, length, width, height, logisticsLevel, TDPrice
|
weight, length, width, height, logisticsLevel, TDPrice
|
||||||
);
|
);
|
||||||
const receivedPrice = purchasePrice * (1 + profitRateDecimal);
|
const receivedPrice = purchasePrice * (1 + profitRateDecimal);
|
||||||
const profitPrice = purchasePrice * profitRateDecimal;
|
const profitPrice = purchasePrice * profitRateDecimal;
|
||||||
let commissionPrice
|
let commissionPrice
|
||||||
let sellingPrice;
|
let sellingPrice;
|
||||||
|
const netRate = (logisticsLevel === 'low') ? 0.845 : 0.785;
|
||||||
if (logisticsLevel === 'low') {
|
if (logisticsLevel === 'low') {
|
||||||
sellingPrice = (receivedPrice + logisticsFee) / 0.845;
|
sellingPrice = (receivedPrice + logisticsFee) / 0.845;
|
||||||
commissionPrice = sellingPrice * 0.12;
|
commissionPrice = sellingPrice * 0.12;
|
||||||
@@ -929,6 +1048,9 @@
|
|||||||
sellingPrice = (receivedPrice + logisticsFee) / 0.785;
|
sellingPrice = (receivedPrice + logisticsFee) / 0.785;
|
||||||
commissionPrice = sellingPrice * 0.18;
|
commissionPrice = sellingPrice * 0.18;
|
||||||
}
|
}
|
||||||
|
// 完全成本:进货价 + 物流费 + 平台真实总抽成(含佣金及约3.5%其它费)
|
||||||
|
const fullCommission = sellingPrice * (1 - netRate);
|
||||||
|
const totalCost = purchasePrice + logisticsFee + fullCommission;
|
||||||
const logisticsLevelMessage = validateLogisticsLevel(sellingPrice, logisticsLevel);
|
const logisticsLevelMessage = validateLogisticsLevel(sellingPrice, logisticsLevel);
|
||||||
const priceRangeMessage = validatePriceRange(sellingPrice);
|
const priceRangeMessage = validatePriceRange(sellingPrice);
|
||||||
if (logisticsLevelMessage) {
|
if (logisticsLevelMessage) {
|
||||||
@@ -951,7 +1073,12 @@
|
|||||||
sellingPriceElement.textContent = `¥ ${sellingPrice.toFixed(2)}`;
|
sellingPriceElement.textContent = `¥ ${sellingPrice.toFixed(2)}`;
|
||||||
profitElement.textContent = `¥ ${profitPrice.toFixed(2)}`
|
profitElement.textContent = `¥ ${profitPrice.toFixed(2)}`
|
||||||
commission.textContent = `¥ ${commissionPrice.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(() => {
|
setTimeout(() => {
|
||||||
card.classList.remove('opacity-0', 'translate-y-4');
|
card.classList.remove('opacity-0', 'translate-y-4');
|
||||||
}, index * 100);
|
}, index * 100);
|
||||||
@@ -986,7 +1113,7 @@
|
|||||||
function validateProductInfoForm() {
|
function validateProductInfoForm() {
|
||||||
const fields = [
|
const fields = [
|
||||||
{ label: '进货价', el: purchasePriceInput, value: purchasePriceInput && purchasePriceInput.value },
|
{ 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: weightInput, value: weightInput && weightInput.value },
|
||||||
{ label: '贴单费用', el: TDPriceInput, value: TDPriceInput && TDPriceInput.value },
|
{ label: '贴单费用', el: TDPriceInput, value: TDPriceInput && TDPriceInput.value },
|
||||||
{ label: '外包装长度', el: lengthInput, value: lengthInput && lengthInput.value },
|
{ label: '外包装长度', el: lengthInput, value: lengthInput && lengthInput.value },
|
||||||
@@ -1008,6 +1135,42 @@
|
|||||||
}
|
}
|
||||||
return false;
|
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() {
|
function recordData() {
|
||||||
if (!validateProductInfoForm()) {
|
if (!validateProductInfoForm()) {
|
||||||
return;
|
return;
|
||||||
@@ -1052,11 +1215,23 @@
|
|||||||
const profitRate = (!isNaN(purchaseNum) && purchaseNum > 0 && profit !== '')
|
const profitRate = (!isNaN(purchaseNum) && purchaseNum > 0 && profit !== '')
|
||||||
? ((parseFloat(profit) / purchaseNum) * 100).toFixed(0)
|
? ((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 = {
|
const record = {
|
||||||
sku,
|
sku,
|
||||||
modelCode: (modelCodeInput.value || '').trim(),
|
modelCode: (modelCodeInput.value || '').trim(),
|
||||||
skuSuffix: (skuSuffixInput.value || '').trim(),
|
skuSuffix: (skuSuffixInput.value || '').trim(),
|
||||||
productName,
|
productName,
|
||||||
|
status: '在售',
|
||||||
purchaseUrl: (purchaseUrlInput.value || '').trim(),
|
purchaseUrl: (purchaseUrlInput.value || '').trim(),
|
||||||
sellingPrice,
|
sellingPrice,
|
||||||
sellingPriceReserved: parseDisplayMoney(document.getElementById('sellingPriceReserved').textContent),
|
sellingPriceReserved: parseDisplayMoney(document.getElementById('sellingPriceReserved').textContent),
|
||||||
@@ -1069,6 +1244,8 @@
|
|||||||
purchasePrice,
|
purchasePrice,
|
||||||
profit,
|
profit,
|
||||||
profitRate,
|
profitRate,
|
||||||
|
fullCommission: fullCommission.toFixed(2),
|
||||||
|
totalCost: totalCost.toFixed(2),
|
||||||
weight,
|
weight,
|
||||||
dimensions: `${length}x${width}x${height}`,
|
dimensions: `${length}x${width}x${height}`,
|
||||||
logisticsLevel: logisticsLevel === 'high' ? '高' : (logisticsLevel === 'high2' ? 'Premium' : '低')
|
logisticsLevel: logisticsLevel === 'high' ? '高' : (logisticsLevel === 'high2' ? 'Premium' : '低')
|
||||||
@@ -1076,6 +1253,7 @@
|
|||||||
historyData.unshift(record);
|
historyData.unshift(record);
|
||||||
localStorage.setItem('priceCalculatorHistory', JSON.stringify(historyData));
|
localStorage.setItem('priceCalculatorHistory', JSON.stringify(historyData));
|
||||||
renderHistoryTable();
|
renderHistoryTable();
|
||||||
|
showToast(`商品「${productName || sku}」录入成功`, 'success');
|
||||||
}
|
}
|
||||||
function createLinkCell(url, label) {
|
function createLinkCell(url, label) {
|
||||||
const cell = document.createElement('td');
|
const cell = document.createElement('td');
|
||||||
@@ -1117,7 +1295,7 @@
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
function renderHistoryTable() {
|
function renderHistoryTable() {
|
||||||
const colCount = 15;
|
const colCount = 17;
|
||||||
if (historyData.length === 0) {
|
if (historyData.length === 0) {
|
||||||
historyTableBody.innerHTML = `<tr class="text-center"><td colspan="${colCount}" class="px-6 py-10 text-gray-500"></td></tr>`;
|
historyTableBody.innerHTML = `<tr class="text-center"><td colspan="${colCount}" class="px-6 py-10 text-gray-500"></td></tr>`;
|
||||||
return;
|
return;
|
||||||
@@ -1148,17 +1326,19 @@
|
|||||||
row.append(
|
row.append(
|
||||||
deleteCell,
|
deleteCell,
|
||||||
createCell(record.sku || '--'),
|
createCell(record.sku || '--'),
|
||||||
createCell(record.modelCode || '--', true),
|
|
||||||
createCell(record.productName || '--'),
|
createCell(record.productName || '--'),
|
||||||
createCell(formatCny(record.purchasePrice)),
|
createCell(formatCny(record.purchasePrice)),
|
||||||
createCell(formatCny(record.logisticsFee)),
|
createCell(formatCny(record.logisticsFee), true),
|
||||||
createCell(formatCny(record.sellingPrice)),
|
createCell(formatCny(record.fullCommission), true),
|
||||||
createCell(formatCny(record.receivedPrice)),
|
createCell(formatCny(record.receivedPrice)),
|
||||||
|
createCell(formatCny(record.totalCost)),
|
||||||
|
createCell(formatCny(record.sellingPrice)),
|
||||||
createCell(profit !== '' ? formatCny(profit) : '--'),
|
createCell(profit !== '' ? formatCny(profit) : '--'),
|
||||||
createCell(profitRate),
|
createCell(profitRate),
|
||||||
createCell(formatRub(record.sellingPriceRub)),
|
createCell(formatRub(record.sellingPriceRub)),
|
||||||
createCell(weightText, true),
|
createCell(weightText, true),
|
||||||
createCell(dimensionsText, true),
|
createCell(dimensionsText, true),
|
||||||
|
createCell(record.status || '在售', true),
|
||||||
createLinkCell(ozonUrl, ozonUrl),
|
createLinkCell(ozonUrl, ozonUrl),
|
||||||
createLinkCell(purchaseUrl, record.purchaseUrl || purchaseUrl)
|
createLinkCell(purchaseUrl, record.purchaseUrl || purchaseUrl)
|
||||||
);
|
);
|
||||||
@@ -1198,8 +1378,8 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const headers = [
|
const headers = [
|
||||||
'货号(sku)', '型号', '商品名', '进货价', '物流费', '销售价', '实收价',
|
'货号(sku)', '商品名', '进货价', '物流费', '平台总抽成', '实收价', '完全成本', '销售价',
|
||||||
'利润', '利润率', '卢布销价', '重量', '尺寸', 'Ozon地址', '采买地址'
|
'净利润', '净利率', '卢布销价', '重量', '尺寸', '状态', 'Ozon地址', '采买地址'
|
||||||
];
|
];
|
||||||
let csvContent = '\uFEFF' + headers.join(',') + '\n';
|
let csvContent = '\uFEFF' + headers.join(',') + '\n';
|
||||||
historyData.forEach(record => {
|
historyData.forEach(record => {
|
||||||
@@ -1207,17 +1387,19 @@
|
|||||||
const profitRate = getRecordProfitRate(record, profit);
|
const profitRate = getRecordProfitRate(record, profit);
|
||||||
const row = [
|
const row = [
|
||||||
record.sku || '',
|
record.sku || '',
|
||||||
record.modelCode || '',
|
|
||||||
record.productName || '',
|
record.productName || '',
|
||||||
record.purchasePrice || '',
|
record.purchasePrice || '',
|
||||||
record.logisticsFee || '',
|
record.logisticsFee || '',
|
||||||
record.sellingPrice || '',
|
record.fullCommission || '',
|
||||||
record.receivedPrice || '',
|
record.receivedPrice || '',
|
||||||
|
record.totalCost || '',
|
||||||
|
record.sellingPrice || '',
|
||||||
profit,
|
profit,
|
||||||
profitRate,
|
profitRate,
|
||||||
record.sellingPriceRub || '',
|
record.sellingPriceRub || '',
|
||||||
record.weight || '',
|
record.weight || '',
|
||||||
record.dimensions || '',
|
record.dimensions || '',
|
||||||
|
record.status || '在售',
|
||||||
record.sku ? `https://www.ozon.ru/product/${record.sku}` : '',
|
record.sku ? `https://www.ozon.ru/product/${record.sku}` : '',
|
||||||
record.purchaseUrl || ''
|
record.purchaseUrl || ''
|
||||||
].map(csvEscape);
|
].map(csvEscape);
|
||||||
@@ -1353,28 +1535,28 @@
|
|||||||
const cnyPrice = parseFloat(sellingPriceElement.textContent.replace(/[^\d.]/g, ''));
|
const cnyPrice = parseFloat(sellingPriceElement.textContent.replace(/[^\d.]/g, ''));
|
||||||
if (isNaN(cnyPrice) || cnyPrice <= 0) {
|
if (isNaN(cnyPrice) || cnyPrice <= 0) {
|
||||||
sellingPriceReserved.textContent = '--';
|
sellingPriceReserved.textContent = '--';
|
||||||
cnyReserveGap.textContent = '--';
|
cnyReserveGap.textContent = '';
|
||||||
sellingPriceRub.textContent = '--';
|
sellingPriceRub.textContent = '--';
|
||||||
sellingPriceRubReserved.textContent = '--';
|
sellingPriceRubReserved.textContent = '--';
|
||||||
rubReserveGap.textContent = '--';
|
rubReserveGap.textContent = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cnyReserved = cnyPrice / (1 - percent / 100);
|
const cnyReserved = cnyPrice / (1 - percent / 100);
|
||||||
sellingPriceReserved.textContent = `¥ ${cnyReserved.toFixed(2)}`;
|
sellingPriceReserved.textContent = `¥ ${cnyReserved.toFixed(2)}`;
|
||||||
cnyReserveGap.textContent = `¥ ${(cnyReserved - cnyPrice).toFixed(2)}`;
|
cnyReserveGap.textContent = `(折扣空间 ¥${(cnyReserved - cnyPrice).toFixed(2)})`;
|
||||||
|
|
||||||
if (!cnyToRubRate) {
|
if (!cnyToRubRate) {
|
||||||
sellingPriceRub.textContent = '--';
|
sellingPriceRub.textContent = '--';
|
||||||
sellingPriceRubReserved.textContent = '--';
|
sellingPriceRubReserved.textContent = '--';
|
||||||
rubReserveGap.textContent = '--';
|
rubReserveGap.textContent = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rubPrice = cnyPrice * cnyToRubRate;
|
const rubPrice = cnyPrice * cnyToRubRate;
|
||||||
const rubReserved = cnyReserved * cnyToRubRate;
|
const rubReserved = cnyReserved * cnyToRubRate;
|
||||||
sellingPriceRub.textContent = `₽ ${rubPrice.toFixed(2)}`;
|
sellingPriceRub.textContent = `₽ ${rubPrice.toFixed(2)}`;
|
||||||
sellingPriceRubReserved.textContent = `₽ ${rubReserved.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');
|
sellingCardRub.classList.remove('opacity-0', 'translate-y-4');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+53
-65
@@ -80,7 +80,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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"
|
<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">
|
class="w-full px-3 py-1.5 border border-gray-700 bg-dark-light rounded-lg input-focus text-gray-100">
|
||||||
</div>
|
</div>
|
||||||
@@ -202,17 +202,31 @@
|
|||||||
<div id="logisticsCard" class="flex result-card opacity-0 transform translate-y-4">
|
<div id="logisticsCard" class="flex result-card opacity-0 transform translate-y-4">
|
||||||
<div class="mr-20" style="width: 60%;">
|
<div class="mr-20" style="width: 60%;">
|
||||||
<h3 class="text-lg font-medium text-gray-100 mb-2 flex items-center text-glow">
|
<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>
|
</h3>
|
||||||
<p class="text-3xl font-bold text-gray-100" id="logisticsFee">--</p>
|
<p class="text-3xl font-bold text-gray-100" id="logisticsFee">--</p>
|
||||||
</div>
|
</div>
|
||||||
<div style="width: 40%;">
|
<div style="width: 40%;">
|
||||||
<h3 class="text-lg font-medium text-gray-100 mb-2 flex items-center text-glow">
|
<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>
|
</h3>
|
||||||
<p class="text-3xl font-bold text-gray-100" id="commission">--</p>
|
<p class="text-3xl font-bold text-gray-100" id="commission">--</p>
|
||||||
</div>
|
</div>
|
||||||
</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 id="receivedCard" class="flex result-card opacity-0 transform translate-y-4 transition-delay-100">
|
||||||
<div class="mr-20" style="width: 60%;">
|
<div class="mr-20" style="width: 60%;">
|
||||||
<h3 class="text-lg font-medium text-gray-100 mb-2 flex items-center text-glow">
|
<h3 class="text-lg font-medium text-gray-100 mb-2 flex items-center text-glow">
|
||||||
@@ -222,7 +236,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="width: 40%;">
|
<div style="width: 40%;">
|
||||||
<h3 class="text-lg font-medium text-gray-100 mb-2 flex items-center text-glow">
|
<h3 class="text-lg font-medium text-gray-100 mb-2 flex items-center text-glow">
|
||||||
利润
|
净利润
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-3xl font-bold text-gray-100" id="profitElement">--</p>
|
<p class="text-3xl font-bold text-gray-100" id="profitElement">--</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -242,12 +256,11 @@
|
|||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="mr-20" style="width: 60%;">
|
<div class="mr-20" style="width: 60%;">
|
||||||
<p class="text-sm text-gray-400 mb-1">现价</p>
|
<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>
|
||||||
<div style="width: 40%;">
|
<div style="width: 40%;">
|
||||||
<p class="text-sm text-gray-400 mb-1">预留 <span id="cnyReserveLabel">50%</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-gray-100" id="sellingPriceReserved">--</p>
|
<p class="text-3xl font-bold text-secondary" id="sellingPriceReserved">--</p>
|
||||||
<p class="text-xs text-gray-400 mt-1">折扣空间 <span id="cnyReserveGap">--</span></p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -262,13 +275,11 @@
|
|||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="mr-20" style="width: 60%;">
|
<div class="mr-20" style="width: 60%;">
|
||||||
<p class="text-sm text-gray-400 mb-1">现价</p>
|
<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>
|
||||||
<div style="width: 40%;">
|
<div style="width: 40%;">
|
||||||
<p class="text-sm text-gray-400 mb-1">预留 <span id="rubReserveLabel">50%</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-secondary" id="sellingPriceRubReserved">--</p>
|
<p class="text-3xl font-bold text-blue-400" id="sellingPriceRubReserved">--</p>
|
||||||
<p class="text-xs text-gray-400 mt-1">折扣空间 <span id="rubReserveGap"
|
|
||||||
class="text-secondary">--</span></p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
class="w-full px-3 py-2 border border-gray-700 bg-dark-light rounded-lg input-focus text-gray-100"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-500">
|
<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>
|
</p>
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
<label for="aiModelSelect" class="text-sm text-gray-400 whitespace-nowrap">模型</label>
|
<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">
|
<div class="flex items-center justify-between mb-1 gap-3">
|
||||||
<label class="block text-sm font-medium text-gray-300">描述</label>
|
<label class="block text-sm font-medium text-gray-300">描述</label>
|
||||||
<button type="button" data-copy-source="aiDescRu"
|
<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>
|
||||||
<div id="aiDescRu" class="ai-text-block ai-text-block-ru" data-placeholder="俄文描述将展示在这里"></div>
|
<div id="aiDescRu" class="ai-text-block ai-text-block-ru" data-placeholder="俄文描述将展示在这里"></div>
|
||||||
<div class="ai-text-divider"></div>
|
<div class="ai-text-divider"></div>
|
||||||
@@ -361,7 +372,7 @@
|
|||||||
<div class="flex items-center justify-between mb-1 gap-3">
|
<div class="flex items-center justify-between mb-1 gap-3">
|
||||||
<label class="block text-sm font-medium text-gray-300">标签</label>
|
<label class="block text-sm font-medium text-gray-300">标签</label>
|
||||||
<button type="button" id="aiCopyAllTagsBtn"
|
<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>
|
||||||
<div id="aiTagsContainer" class="flex flex-wrap gap-2 min-h-[2.5rem]">
|
<div id="aiTagsContainer" class="flex flex-wrap gap-2 min-h-[2.5rem]">
|
||||||
<p class="text-sm text-gray-500">生成后将在此展示标签,点击可复制俄文</p>
|
<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">
|
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>添加水印
|
<i class="fa fa-tint mr-2"></i>添加水印
|
||||||
</button>
|
</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">
|
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>导出所有图片
|
<i class="fa fa-download mr-2"></i>导出所有图片
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -478,7 +493,7 @@
|
|||||||
<button type="button" id="toggleOptionalColsBtn"
|
<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"
|
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">
|
aria-pressed="false">
|
||||||
显示型号/重量/尺寸
|
显示物流费/平台总抽成/重量/尺寸/状态
|
||||||
</button>
|
</button>
|
||||||
<button type="button" id="transferDataBtn"
|
<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">
|
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">
|
<table id="historyTable" class="min-w-full divide-y divide-gray-700">
|
||||||
<thead class="bg-dark-light">
|
<thead class="bg-dark-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col"
|
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">操作</th>
|
||||||
class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">操作
|
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">货号(sku)</th>
|
||||||
</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"
|
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">进货价</th>
|
||||||
class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">货号
|
<th scope="col" class="history-col-optional px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">物流费</th>
|
||||||
(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"
|
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">实收价</th>
|
||||||
class="history-col-optional px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">完全成本</th>
|
||||||
型号</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"
|
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">净利润</th>
|
||||||
class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">商品名
|
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">净利率</th>
|
||||||
</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"
|
<th scope="col" class="history-col-optional px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">重量</th>
|
||||||
class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">进货价
|
<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>
|
<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"
|
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">Ozon地址</th>
|
||||||
class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">物流费
|
<th scope="col" class="px-4 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">采买地址</th>
|
||||||
</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>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="bg-dark-card divide-y divide-gray-700" id="historyTableBody">
|
<tbody class="bg-dark-card divide-y divide-gray-700" id="historyTableBody">
|
||||||
<tr class="text-center">
|
<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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -568,6 +555,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</main>
|
</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/app.js"></script>
|
||||||
<script src="js/ai-copy.js"></script>
|
<script src="js/ai-copy.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user