Polish static UI flows
This commit is contained in:
@@ -0,0 +1,414 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>新建商品 · Airshelf</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
|
||||
<style>
|
||||
/* ─── 主表单 ─── */
|
||||
.form-grid {
|
||||
display: grid; grid-template-columns: 1.05fr 1fr; gap: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.form-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
padding: 24px;
|
||||
}
|
||||
.form-card .card-h {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.form-card .card-h h3 {
|
||||
font-size: 14px; font-weight: 600; color: var(--accent-black);
|
||||
}
|
||||
.form-card .card-h .req-tag {
|
||||
font-family: var(--font-mono); font-size: 10px;
|
||||
padding: 2px 7px;
|
||||
background: var(--crimson-bg); color: var(--accent-crimson);
|
||||
border-radius: var(--r-sm); letter-spacing: .04em;
|
||||
border: 1px solid var(--red-bd);
|
||||
}
|
||||
.form-card .card-h .opt-tag {
|
||||
font-family: var(--font-mono); font-size: 10px;
|
||||
padding: 2px 7px;
|
||||
background: var(--background-lighter); color: var(--black-alpha-56);
|
||||
border-radius: var(--r-sm); letter-spacing: .04em;
|
||||
border: 1px solid var(--border-faint);
|
||||
}
|
||||
.form-card .card-sub {
|
||||
font-family: var(--font-mono); font-size: 11.5px;
|
||||
color: var(--black-alpha-48); margin: -10px 0 14px; letter-spacing: .02em;
|
||||
}
|
||||
|
||||
/* 原图槽位 */
|
||||
.photo-grid {
|
||||
display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.photo-slot {
|
||||
aspect-ratio: 1;
|
||||
border-radius: var(--r-md);
|
||||
border: 1px dashed var(--border-faint);
|
||||
background: var(--background-lighter);
|
||||
display: grid; place-items: center;
|
||||
color: var(--black-alpha-32);
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
font-size: 10px; font-family: var(--font-mono); letter-spacing: .04em;
|
||||
transition: all var(--t-base);
|
||||
}
|
||||
.photo-slot:hover { border-color: var(--heat); color: var(--heat); background: var(--heat-8); }
|
||||
.photo-slot.filled {
|
||||
border-style: solid;
|
||||
background-size: cover; background-position: center;
|
||||
cursor: default; color: transparent;
|
||||
}
|
||||
.photo-slot.filled:hover { border-color: var(--heat-40); }
|
||||
.photo-slot .slot-label {
|
||||
position: absolute; top: 5px; left: 5px;
|
||||
font-family: var(--font-mono); font-size: 9.5px; font-weight: 600;
|
||||
padding: 2px 6px;
|
||||
background: rgba(255,255,255,.92); color: var(--black-alpha-72);
|
||||
border-radius: var(--r-sm); letter-spacing: .04em;
|
||||
}
|
||||
.photo-slot .slot-main {
|
||||
position: absolute; top: 5px; right: 5px;
|
||||
font-family: var(--font-mono); font-size: 9.5px; font-weight: 600;
|
||||
padding: 2px 6px; background: var(--heat); color: #fff;
|
||||
border-radius: var(--r-sm); letter-spacing: .04em;
|
||||
}
|
||||
.photo-slot .slot-x {
|
||||
position: absolute; top: 4px; right: 4px;
|
||||
width: 18px; height: 18px;
|
||||
border-radius: 999px;
|
||||
background: rgba(21,20,15,.7); color: #fff;
|
||||
display: none; place-items: center; cursor: pointer; border: 0;
|
||||
}
|
||||
.photo-slot.filled:hover .slot-x { display: grid; }
|
||||
.photo-slot.filled:hover .slot-main { display: none; }
|
||||
.photo-slot .slot-x svg { width: 9px; height: 9px; }
|
||||
.photo-slot .plus { width: 22px; height: 22px; border: 1px solid currentColor; border-radius: var(--r-sm); display: grid; place-items: center; margin-bottom: 4px; }
|
||||
.photo-slot .plus svg { width: 12px; height: 12px; }
|
||||
|
||||
.upload-tip {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
margin-top: 14px; padding: 10px 12px;
|
||||
background: var(--heat-8); border: 1px dashed var(--heat-40);
|
||||
border-radius: var(--r-md);
|
||||
font-size: 12px; color: var(--accent-black); line-height: 1.5;
|
||||
}
|
||||
.upload-tip svg { width: 14px; height: 14px; color: var(--heat); flex-shrink: 0; }
|
||||
.upload-tip strong { color: var(--heat); font-weight: 600; }
|
||||
|
||||
/* AI 提示 banner(选填字段说明) */
|
||||
.ai-tip {
|
||||
margin-top: -6px; margin-bottom: 16px;
|
||||
padding: 10px 12px;
|
||||
background: var(--background-lighter);
|
||||
border-radius: var(--r-md);
|
||||
border: 1px dashed var(--border-faint);
|
||||
display: flex; align-items: flex-start; gap: 8px;
|
||||
font-size: 12px; color: var(--black-alpha-72); line-height: 1.55;
|
||||
}
|
||||
.ai-tip svg { width: 13px; height: 13px; color: var(--heat); flex-shrink: 0; margin-top: 2px; }
|
||||
.ai-tip strong { color: var(--accent-black); font-weight: 600; }
|
||||
|
||||
/* 卖点 bullet 输入 */
|
||||
.sell-list { list-style: none; margin: 0; padding: 0; }
|
||||
.sell-list li {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 8px 10px;
|
||||
background: var(--background-lighter);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
margin-bottom: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.sell-list li.add { background: var(--surface); border-style: dashed; }
|
||||
.sell-list .num {
|
||||
width: 18px; height: 18px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-sm);
|
||||
font-size: 10.5px; font-family: var(--font-mono);
|
||||
color: var(--black-alpha-56);
|
||||
display: grid; place-items: center; flex-shrink: 0;
|
||||
}
|
||||
.sell-list li.add .num { background: transparent; color: var(--heat); border-color: var(--heat-40); }
|
||||
.sell-list .txt { flex: 1; min-width: 0; }
|
||||
.sell-list .bl-input {
|
||||
flex: 1; border: 0; background: transparent;
|
||||
font-size: 13px; color: var(--accent-black); padding: 0;
|
||||
font-family: inherit;
|
||||
}
|
||||
.sell-list .bl-input::placeholder { color: var(--black-alpha-48); }
|
||||
.sell-list .bl-x {
|
||||
width: 20px; height: 20px;
|
||||
display: grid; place-items: center;
|
||||
color: var(--black-alpha-48); cursor: pointer;
|
||||
background: transparent; border: 0; opacity: 0;
|
||||
transition: opacity var(--t-base);
|
||||
}
|
||||
.sell-list li:hover .bl-x { opacity: 1; }
|
||||
.sell-list .bl-x:hover { color: var(--accent-crimson); }
|
||||
.sell-list .bl-x svg { width: 11px; height: 11px; }
|
||||
|
||||
/* 底部操作行 */
|
||||
.form-foot {
|
||||
position: sticky; bottom: 0;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
padding: 14px 22px;
|
||||
display: flex; align-items: center; gap: 14px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.form-foot .req-info {
|
||||
font-family: var(--font-mono); font-size: 11.5px;
|
||||
color: var(--black-alpha-48); letter-spacing: .02em;
|
||||
}
|
||||
.form-foot .req-info .ok { color: var(--accent-forest); }
|
||||
.form-foot .req-info .miss { color: var(--accent-crimson); }
|
||||
.form-foot .actions {
|
||||
margin-left: auto;
|
||||
display: flex; gap: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="page">
|
||||
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>新建商品</h1>
|
||||
<div class="sub"><span class="mono">// 上传原图 + 填写基本信息</span> · 保存后可在工作台逐步丰富素材</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ 表单 ============ -->
|
||||
<div class="form-grid">
|
||||
|
||||
<!-- 左:原图 -->
|
||||
<div class="form-card">
|
||||
<div class="card-h">
|
||||
<h3>商品原图</h3>
|
||||
<span class="req-tag">必填</span>
|
||||
</div>
|
||||
<div class="card-sub">// 1-5 张 · 这是后续所有 AI 生成的源材料</div>
|
||||
<input type="file" id="photo-input" accept="image/*" multiple hidden>
|
||||
<div class="photo-grid" id="photo-grid"></div>
|
||||
<div class="upload-tip">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="9"/><path d="M12 8v4M12 16h.01"/></svg>
|
||||
<span>建议上传 <strong>正面 / 侧面 / 细节 / 包装</strong> 4 张,后续在工作台生成的<strong>白底三视图</strong>更准确。</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右:基本信息 -->
|
||||
<div class="form-card">
|
||||
<div class="card-h">
|
||||
<h3>基本信息</h3>
|
||||
<span class="req-tag">必填</span>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label">商品名称<span class="req">*</span></label>
|
||||
<input class="input" id="p-name" placeholder="例: 透真玻尿酸补水面膜">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label">品类<span class="req">*</span></label>
|
||||
<select class="select" id="p-cat">
|
||||
<option value="">— 选择品类 —</option>
|
||||
<option>美妆个护</option>
|
||||
<option>服饰内衣</option>
|
||||
<option>食品饮料</option>
|
||||
<option>家居家电</option>
|
||||
<option>数码 3C</option>
|
||||
<option>个护清洁</option>
|
||||
<option>运动户外</option>
|
||||
<option>母婴亲子</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field" style="margin-bottom:0;">
|
||||
<label class="field-label">价格(元)</label>
|
||||
<input class="input" id="p-price" type="number" placeholder="选填 · 仅用于素材生成参考">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="form-card" style="margin-bottom: 24px;">
|
||||
<div class="card-h">
|
||||
<h3>卖点 & 人群</h3>
|
||||
<span class="opt-tag">选填 · 推荐</span>
|
||||
</div>
|
||||
<div class="ai-tip">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l1.5 4.5L18 8l-4.5 1.5L12 14l-1.5-4.5L6 8l4.5-1.5L12 2z"/></svg>
|
||||
<span>填上这两项,后续 AI 生脚本(<strong>痛点种草 / 剧情带货</strong> 等模板)质量明显更高 —— 系统会用卖点构造钩子,用人群定语气。现在不填也可以,做视频项目时仍可补。</span>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field-label">核心卖点</label>
|
||||
<div class="field-hint" style="margin: 4px 0 8px;">3-5 条要点,回车添加</div>
|
||||
<ul class="sell-list" id="sell-list">
|
||||
<li class="add"><span class="num">+</span><input class="bl-input" id="sell-input" placeholder="例: 玻尿酸双效保湿,4 小时持久水润"></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="field" style="margin-bottom:0;">
|
||||
<label class="field-label">目标人群</label>
|
||||
<input class="input" id="p-target" placeholder="例: 22-32 岁女性、敏感肌、办公室通勤">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ 底部操作 ============ -->
|
||||
<div class="form-foot">
|
||||
<span class="req-info" id="req-info">// 必填检查:<span class="miss">商品名 / 品类 / ≥1 张图</span> 未完成</span>
|
||||
<div class="actions">
|
||||
<a class="btn" href="products.html">取消</a>
|
||||
<button class="btn btn-primary" id="save-btn" disabled>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
|
||||
保存并进入工作台
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script src="assets/icons.js?v=2026052608"></script>
|
||||
<script src="assets/shell.js?v=2026052607"></script>
|
||||
<script>
|
||||
Shell.render({
|
||||
active: 'products',
|
||||
crumbs: [
|
||||
{ label: '工作台', href: 'index.html' },
|
||||
{ label: '商品库', href: 'products.html' },
|
||||
{ label: '新建' }
|
||||
]
|
||||
});
|
||||
|
||||
const MAX = 5;
|
||||
const photos = []; // { id, dataUrl }
|
||||
const SLOT_LABELS = ['主图', '细节 02', '细节 03', '细节 04', '细节 05'];
|
||||
|
||||
const $ = id => document.getElementById(id);
|
||||
|
||||
// 渲染槽位
|
||||
function renderPhotos() {
|
||||
const grid = $('photo-grid');
|
||||
grid.innerHTML = '';
|
||||
for (let i = 0; i < MAX; i++) {
|
||||
const slot = document.createElement('div');
|
||||
const p = photos[i];
|
||||
if (p) {
|
||||
slot.className = 'photo-slot filled';
|
||||
slot.style.backgroundImage = `url("${p.dataUrl}")`;
|
||||
slot.innerHTML = `
|
||||
<span class="slot-label">${SLOT_LABELS[i]}</span>
|
||||
${i === 0 ? '<span class="slot-main">MAIN</span>' : ''}
|
||||
<button class="slot-x" type="button" data-i="${i}" aria-label="移除">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>
|
||||
</button>
|
||||
`;
|
||||
} else if (i === photos.length) {
|
||||
slot.className = 'photo-slot empty';
|
||||
slot.innerHTML = `<div class="plus"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 5v14M5 12h14"/></svg></div><span>添加</span>`;
|
||||
slot.addEventListener('click', () => $('photo-input').click());
|
||||
} else {
|
||||
slot.className = 'photo-slot';
|
||||
slot.style.opacity = '.6';
|
||||
}
|
||||
grid.appendChild(slot);
|
||||
}
|
||||
// 绑定删除
|
||||
grid.querySelectorAll('.slot-x').forEach(b => {
|
||||
b.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const i = +b.dataset.i;
|
||||
photos.splice(i, 1);
|
||||
renderPhotos();
|
||||
syncSave();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 文件上传
|
||||
$('photo-input').addEventListener('change', e => {
|
||||
const files = [...e.target.files].filter(f => f.type.startsWith('image/'));
|
||||
const remain = MAX - photos.length;
|
||||
files.slice(0, remain).forEach(f => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = ev => {
|
||||
photos.push({
|
||||
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 5),
|
||||
dataUrl: ev.target.result,
|
||||
});
|
||||
renderPhotos();
|
||||
syncSave();
|
||||
};
|
||||
reader.readAsDataURL(f);
|
||||
});
|
||||
e.target.value = '';
|
||||
});
|
||||
|
||||
// 卖点
|
||||
const sellList = $('sell-list');
|
||||
const sellInput = $('sell-input');
|
||||
sellInput.addEventListener('keydown', e => {
|
||||
if (e.key !== 'Enter') return;
|
||||
e.preventDefault();
|
||||
const t = sellInput.value.trim();
|
||||
if (!t) return;
|
||||
const li = document.createElement('li');
|
||||
li.innerHTML = `<span class="num"></span><span class="txt"></span><button class="bl-x" type="button" aria-label="删除"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg></button>`;
|
||||
li.querySelector('.txt').textContent = t;
|
||||
sellList.querySelector('.add').before(li);
|
||||
sellInput.value = '';
|
||||
renumberSell();
|
||||
li.querySelector('.bl-x').addEventListener('click', () => {
|
||||
li.remove();
|
||||
renumberSell();
|
||||
});
|
||||
});
|
||||
function renumberSell() {
|
||||
sellList.querySelectorAll('li:not(.add) .num').forEach((n, i) => n.textContent = i + 1);
|
||||
}
|
||||
|
||||
// 必填检查
|
||||
function syncSave() {
|
||||
const hasName = $('p-name').value.trim().length > 0;
|
||||
const hasCat = $('p-cat').value.length > 0;
|
||||
const hasPhoto = photos.length > 0;
|
||||
const ok = hasName && hasCat && hasPhoto;
|
||||
$('save-btn').disabled = !ok;
|
||||
|
||||
const missing = [];
|
||||
if (!hasName) missing.push('商品名');
|
||||
if (!hasCat) missing.push('品类');
|
||||
if (!hasPhoto) missing.push('≥1 张图');
|
||||
|
||||
const info = $('req-info');
|
||||
if (ok) {
|
||||
info.innerHTML = '// 必填检查:<span class="ok">已全部完成 ✓</span> · 可进入工作台';
|
||||
} else {
|
||||
info.innerHTML = `// 必填检查:<span class="miss">${missing.join(' / ')}</span> 未完成`;
|
||||
}
|
||||
}
|
||||
$('p-name').addEventListener('input', syncSave);
|
||||
$('p-cat').addEventListener('change', syncSave);
|
||||
|
||||
// 保存 → 跳工作台
|
||||
$('save-btn').addEventListener('click', () => {
|
||||
if ($('save-btn').disabled) return;
|
||||
Shell.toast('商品已建档', `进入工作台`);
|
||||
setTimeout(() => {
|
||||
// 真实场景下会带商品 ID;demo 直接跳
|
||||
location.href = 'product-studio.html?from=new&onboard=1';
|
||||
}, 500);
|
||||
});
|
||||
|
||||
renderPhotos();
|
||||
syncSave();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user