Files
shanxia2.0/docs/demos/crm-product-selector-enhance-demo.html
T
2026-08-08 20:43:48 +08:00

927 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>选择产品弹窗 · 品牌/别名 + 新增编辑 Demo</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<style>
:root {
--primary: #10b981;
--primary-dark: #059669;
--primary-bg: #ecfdf5;
--text: #1e293b;
--text-muted: #64748b;
--border: #d9e8e2;
--bg-page: #f1f5f9;
--surface: #ffffff;
--table-head: #f8fafc;
--row-alt: #fafafa;
--row-hover: #f1f5f9;
--radius: 10px;
--radius-sm: 8px;
--shadow: 0 8px 30px rgba(15, 23, 42, 0.12);
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: Inter, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
font-size: 14px;
line-height: 1.5;
color: var(--text);
background:
radial-gradient(900px 420px at 12% -10%, rgba(16, 185, 129, 0.14), transparent 55%),
radial-gradient(700px 380px at 90% 0%, rgba(5, 150, 105, 0.08), transparent 50%),
var(--bg-page);
}
.page {
max-width: 1280px;
margin: 0 auto;
padding: 28px 20px 56px;
}
h1 {
margin: 0 0 6px;
font-size: 22px;
font-weight: 700;
}
.lead {
margin: 0 0 20px;
color: var(--text-muted);
font-size: 14px;
max-width: 820px;
}
.notes {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 22px;
}
.note {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 12px 14px;
}
.note strong {
display: block;
margin-bottom: 4px;
font-size: 13px;
}
.note p {
margin: 0;
font-size: 12px;
color: var(--text-muted);
line-height: 1.55;
}
.stage {
position: relative;
min-height: 720px;
border: 1px dashed #cbd5e1;
border-radius: 14px;
background: rgba(255, 255, 255, 0.45);
overflow: hidden;
}
.stage-bg {
padding: 24px;
opacity: 0.55;
pointer-events: none;
user-select: none;
}
.fake-page-title {
font-size: 16px;
font-weight: 700;
margin-bottom: 12px;
}
.fake-card {
height: 180px;
border-radius: var(--radius);
background: #fff;
border: 1px solid var(--border);
margin-bottom: 12px;
}
.mask {
position: absolute;
inset: 0;
background: rgba(15, 23, 42, 0.35);
display: flex;
align-items: center;
justify-content: center;
padding: 24px 16px;
}
.dialog {
width: min(1120px, 96vw);
max-height: calc(100vh - 80px);
background: var(--surface);
border-radius: 12px;
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
overflow: hidden;
}
.dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 20px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.dialog-header h2 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.dialog-close {
width: 28px;
height: 28px;
border: none;
background: transparent;
color: var(--text-muted);
font-size: 20px;
cursor: pointer;
border-radius: 6px;
}
.dialog-close:hover { background: var(--row-hover); }
.dialog-body {
padding: 16px 20px;
overflow: auto;
flex: 1;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 12px 20px;
border-top: 1px solid var(--border);
flex-shrink: 0;
}
.filter-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px 16px;
margin-bottom: 12px;
}
.field label {
display: block;
font-size: 11px;
color: var(--text-muted);
margin-bottom: 4px;
}
.control {
width: 100%;
height: 36px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 0 12px;
background: #fff;
color: var(--text);
font: inherit;
}
.filter-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-bottom: 14px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
height: 36px;
padding: 0 14px;
border-radius: var(--radius-sm);
border: 1px solid var(--border);
background: #fff;
color: var(--text);
font: inherit;
font-size: 13px;
font-weight: 500;
cursor: pointer;
white-space: nowrap;
}
.btn:hover { background: var(--row-hover); }
.btn-primary {
background: var(--primary);
border-color: var(--primary);
color: #fff;
}
.btn-primary:hover { background: var(--primary-dark); border-color: var(--primary-dark); }
.btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.btn svg {
width: 14px;
height: 14px;
flex-shrink: 0;
}
.list-panel {
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
background: #fff;
}
.list-toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
border-bottom: 1px solid var(--border);
background: #fff;
}
.selected-hint {
font-size: 13px;
color: var(--text-muted);
margin-right: 4px;
}
.selected-hint em {
font-style: normal;
color: var(--primary-dark);
font-weight: 600;
}
table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
thead th {
background: var(--table-head);
font-weight: 600;
font-size: 13px;
color: var(--text);
padding: 10px 12px;
border-bottom: 1px solid var(--border);
text-align: center;
white-space: nowrap;
}
tbody td {
padding: 10px 12px;
border-bottom: 1px solid #eef2f7;
font-size: 13px;
text-align: center;
vertical-align: middle;
}
tbody tr:nth-child(even) { background: var(--row-alt); }
tbody tr:hover { background: var(--row-hover); }
tbody tr.is-checked { background: var(--primary-bg); }
.col-check { width: 48px; }
.col-idx { width: 52px; }
.col-no { width: 120px; }
.col-name { width: 180px; text-align: left !important; }
.col-brand { width: 150px; }
.col-alias { width: 160px; }
.col-cat { width: 100px; }
.col-model { width: 100px; }
.col-unit { width: 80px; }
.col-tax { width: 72px; }
.col-status { width: 72px; }
.col-ops { width: 88px; }
.link-btn {
display: inline-flex;
align-items: center;
gap: 4px;
border: none;
background: transparent;
color: var(--primary-dark);
font: inherit;
font-size: 13px;
font-weight: 500;
cursor: pointer;
padding: 0;
}
.link-btn:hover { color: var(--primary); text-decoration: underline; }
.link-btn svg { width: 14px; height: 14px; }
.wrap-cell {
white-space: normal;
word-break: break-word;
line-height: 1.45;
text-align: left !important;
}
.name-cell {
text-align: left !important;
font-weight: 500;
}
.tag {
display: inline-flex;
align-items: center;
height: 22px;
padding: 0 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
background: #059669;
color: #fff;
}
.pager {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 8px;
padding: 10px 12px;
border-top: 1px solid var(--border);
font-size: 13px;
color: var(--text-muted);
}
.pager .page-btn {
min-width: 28px;
height: 28px;
border: 1px solid var(--border);
border-radius: 6px;
background: #fff;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: default;
}
.pager .page-btn.active {
background: var(--primary);
border-color: var(--primary);
color: #fff;
}
.divider {
display: flex;
align-items: center;
gap: 12px;
margin: 16px 0 10px;
color: var(--text-muted);
font-size: 13px;
}
.divider::before,
.divider::after {
content: "";
flex: 1;
height: 1px;
background: var(--border);
}
.selected-box {
min-height: 44px;
padding: 10px;
background: #f5f7fa;
border-radius: 6px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.chip {
display: inline-flex;
align-items: center;
gap: 6px;
height: 28px;
padding: 0 10px;
border-radius: 4px;
background: var(--primary-bg);
color: var(--primary-dark);
border: 1px solid #a7f3d0;
font-size: 12px;
}
.chip button {
border: none;
background: transparent;
color: inherit;
cursor: pointer;
font-size: 14px;
line-height: 1;
padding: 0;
}
.callout {
margin-top: 18px;
padding: 12px 14px;
border-left: 3px solid var(--primary);
background: var(--primary-bg);
border-radius: 0 8px 8px 0;
font-size: 13px;
color: var(--text);
}
.callout code {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 12px;
background: rgba(255, 255, 255, 0.7);
padding: 1px 5px;
border-radius: 4px;
}
/* nested form dialog peek */
.nested-mask {
position: absolute;
inset: 0;
background: rgba(15, 23, 42, 0.28);
display: none;
align-items: center;
justify-content: center;
z-index: 5;
padding: 24px;
}
.nested-mask.show { display: flex; }
.form-dialog {
width: min(960px, 96vw);
max-height: calc(100vh - 100px);
background: #fff;
border-radius: 12px;
box-shadow: var(--shadow);
overflow: hidden;
display: flex;
flex-direction: column;
}
.form-dialog .dialog-header h2 { font-size: 15px; }
.form-scroll {
overflow: auto;
flex: 1;
}
.form-body {
padding: 16px 20px 8px;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px 16px;
}
.form-body .span-2 { grid-column: span 2; }
.form-body .span-3 { grid-column: 1 / -1; }
.form-body label {
display: block;
font-size: 12px;
margin-bottom: 4px;
color: var(--text);
font-weight: 500;
}
.sub-card-section {
margin: 8px 20px 16px;
padding-top: 12px;
border-top: 1px solid var(--border);
}
.sub-block-title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
font-size: 13px;
font-weight: 600;
}
.mini-btn {
height: 28px;
padding: 0 10px;
font-size: 12px;
background: var(--primary);
border-color: var(--primary);
color: #fff;
}
.sub-card-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.sub-card {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: 8px;
background: #fafafa;
}
.sub-card .control { height: 32px; flex: 1; }
.sub-card .control.status { flex: 0 0 96px; }
.form-hint {
margin: 0 20px 12px;
font-size: 12px;
color: var(--text-muted);
}
@media (max-width: 900px) {
.notes { grid-template-columns: 1fr; }
.filter-grid { grid-template-columns: 1fr; }
.col-brand, .col-alias { width: 120px; }
}
</style>
</head>
<body>
<div class="page">
<h1>选择产品弹窗增强 Demo</h1>
<p class="lead">
用于报价单管理 / 合同管理中的「选择产品」弹窗。相对现状:在「产品名称」后增加「品牌」「别名」两列(多个值用「、」分隔,宽度不够自动换行);列表上方增加「新增」「编辑」按钮。
</p>
<div class="notes">
<div class="note">
<strong>① 列表列</strong>
<p>顺序:… → 产品名称 → <b>品牌</b> → <b>别名</b> → 产品分类 → …。单元格允许换行,不用单行截断。</p>
</div>
<div class="note">
<strong>② 工具栏 + 行内操作</strong>
<p>列表上方:已选 N 项 → 新增(主色)→ 编辑(勾选恰好 1 条可点)。表格右侧「操作」列也有「编辑」。</p>
</div>
<div class="note">
<strong>③ 产品表单</strong>
<p>新增/编辑弹窗字段与交互与「产品管理」页一致(含品牌、别名子卡片)。保存后刷新本列表。</p>
</div>
</div>
<div class="stage">
<div class="stage-bg">
<div class="fake-page-title">报价单管理 · 背景页(示意)</div>
<div class="fake-card"></div>
<div class="fake-card" style="height: 280px"></div>
</div>
<div class="mask">
<div class="dialog" id="selectorDialog">
<div class="dialog-header">
<h2>选择产品</h2>
<button class="dialog-close" type="button" aria-label="关闭">×</button>
</div>
<div class="dialog-body">
<div class="filter-grid">
<div class="field">
<label>产品名称</label>
<input class="control" placeholder="请输入产品名称" />
</div>
<div class="field">
<label>产品分类</label>
<select class="control"><option>请选择产品分类</option></select>
</div>
<div class="field">
<label>产品状态</label>
<select class="control"><option selected>上架</option><option>下架</option></select>
</div>
</div>
<div class="filter-actions">
<button class="btn btn-primary" type="button">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3-3"/></svg>
搜索
</button>
<button class="btn" type="button">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 4v5h5"/></svg>
重置
</button>
</div>
<div class="list-panel">
<!-- NEW: toolbar -->
<div class="list-toolbar">
<span class="selected-hint">已选 <em id="selCount">1</em> 项</span>
<button class="btn btn-primary" type="button" id="btnAdd">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>
新增
</button>
<button class="btn" type="button" id="btnEdit" disabled title="请先勾选恰好 1 条产品">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 20h4l10-10-4-4L4 16v4z"/><path d="M13 7l4 4"/></svg>
编辑
</button>
</div>
<div style="overflow-x: auto">
<table>
<thead>
<tr>
<th class="col-check"><input type="checkbox" aria-label="全选" /></th>
<th class="col-idx">序号</th>
<th class="col-no">产品编号</th>
<th class="col-name">产品名称</th>
<th class="col-brand">品牌</th>
<th class="col-alias">别名</th>
<th class="col-cat">产品分类</th>
<th class="col-model">规格型号</th>
<th class="col-unit">计量单位</th>
<th class="col-tax">税率(%)</th>
<th class="col-status">状态</th>
<th class="col-ops">操作</th>
</tr>
</thead>
<tbody id="tbody">
<tr class="is-checked" data-id="1">
<td><input type="checkbox" checked class="row-check" /></td>
<td>1</td>
<td>PRD-20260081</td>
<td class="name-cell">工业乙醇(无水)</td>
<td class="wrap-cell">中粮、壳牌、中石油</td>
<td class="wrap-cell">无水乙醇、工业酒精、乙醇 99.9%</td>
<td>溶剂类</td>
<td>99.9%</td>
<td>吨</td>
<td>13%</td>
<td><span class="tag">上架</span></td>
<td>
<button type="button" class="link-btn row-edit">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 20h4l10-10-4-4L4 16v4z"/><path d="M13 7l4 4"/></svg>
编辑
</button>
</td>
</tr>
<tr data-id="2">
<td><input type="checkbox" class="row-check" /></td>
<td>2</td>
<td>PRD-20260092</td>
<td class="name-cell">盐酸</td>
<td class="wrap-cell">金川、鲁西化工、东岳、华鲁恒升、滨化</td>
<td class="wrap-cell">工业盐酸、合成盐酸、HCl</td>
<td>酸类</td>
<td>31%</td>
<td>吨</td>
<td>13%</td>
<td><span class="tag">上架</span></td>
<td>
<button type="button" class="link-btn row-edit">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 20h4l10-10-4-4L4 16v4z"/><path d="M13 7l4 4"/></svg>
编辑
</button>
</td>
</tr>
<tr data-id="3">
<td><input type="checkbox" class="row-check" /></td>
<td>3</td>
<td>PRD-20260105</td>
<td class="name-cell">液碱</td>
<td class="wrap-cell">—</td>
<td class="wrap-cell">烧碱溶液、氢氧化钠溶液</td>
<td>碱类</td>
<td>32%</td>
<td>吨</td>
<td>13%</td>
<td><span class="tag">上架</span></td>
<td>
<button type="button" class="link-btn row-edit">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 20h4l10-10-4-4L4 16v4z"/><path d="M13 7l4 4"/></svg>
编辑
</button>
</td>
</tr>
<tr data-id="4">
<td><input type="checkbox" class="row-check" /></td>
<td>4</td>
<td>PRD-20260118</td>
<td class="name-cell">甲醇</td>
<td class="wrap-cell">兖矿、中海化学</td>
<td class="wrap-cell">—</td>
<td>醇类</td>
<td>优等品</td>
<td>吨</td>
<td>13%</td>
<td><span class="tag">上架</span></td>
<td>
<button type="button" class="link-btn row-edit">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 20h4l10-10-4-4L4 16v4z"/><path d="M13 7l4 4"/></svg>
编辑
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="pager">
<span>共 48 条</span>
<span class="page-btn">‹</span>
<span class="page-btn active">1</span>
<span class="page-btn">2</span>
<span class="page-btn">3</span>
<span class="page-btn">›</span>
</div>
</div>
<div class="divider">已选产品 (1)</div>
<div class="selected-box">
<span class="chip">工业乙醇(无水) <button type="button">×</button></span>
</div>
</div>
<div class="dialog-footer">
<button class="btn" type="button">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
取消
</button>
<button class="btn btn-primary" type="button">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12l5 5L20 7"/></svg>
确定 (1)
</button>
</div>
<!-- nested product form — aligned with 产品管理 -->
<div class="nested-mask" id="formMask">
<div class="form-dialog">
<div class="dialog-header">
<h2 id="formTitle">新增产品</h2>
<button class="dialog-close" type="button" id="formClose">×</button>
</div>
<div class="form-scroll">
<p class="form-hint">字段与布局对齐「产品管理」新增/编辑弹窗(Demo 示意,非完整交互)。</p>
<div class="form-body">
<div>
<label>产品编号</label>
<input class="control" placeholder="留空自动生成(如 SXWL000001)" />
</div>
<div>
<label>产品名称</label>
<input class="control" id="formName" value="" placeholder="请输入产品名称" />
</div>
<div>
<label>产品分类</label>
<select class="control"><option>请选择产品分类</option><option>溶剂类</option></select>
</div>
<div>
<label>规格型号</label>
<input class="control" placeholder="请输入规格型号" />
</div>
<div>
<label>条码</label>
<input class="control" placeholder="留空自动生成" />
</div>
<div>
<label>税率(%)</label>
<input class="control" value="13" />
</div>
<div>
<label>初始库存</label>
<input class="control" value="0" />
</div>
<div>
<label>库存预警值</label>
<input class="control" value="10" />
</div>
<div>
<label>状态</label>
<select class="control"><option selected>上架</option><option>下架</option></select>
</div>
<div class="span-2">
<label>产品描述</label>
<input class="control" placeholder="请输入产品描述" />
</div>
<div>
<label>备注</label>
<input class="control" placeholder="请输入备注" />
</div>
</div>
<div class="sub-card-section">
<div class="sub-block-title">
<span>品牌</span>
<button class="btn mini-btn" type="button">+ 新增品牌</button>
</div>
<div class="sub-card-grid">
<div class="sub-card">
<input class="control" value="中粮" />
<select class="control status"><option>上架</option></select>
</div>
<div class="sub-card">
<input class="control" value="壳牌" />
<select class="control status"><option>上架</option></select>
</div>
</div>
</div>
<div class="sub-card-section">
<div class="sub-block-title">
<span>别名</span>
<button class="btn mini-btn" type="button">+ 新增别名</button>
</div>
<div class="sub-card-grid">
<div class="sub-card"><input class="control" value="无水乙醇" /></div>
<div class="sub-card"><input class="control" value="工业酒精" /></div>
</div>
</div>
</div>
<div class="dialog-footer">
<button class="btn" type="button" id="formCancel">取消</button>
<button class="btn btn-primary" type="button" id="formSave">确定</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="callout">
本地预览:用浏览器打开
<code>docs/demos/crm-product-selector-enhance-demo.html</code>
。工具栏「新增 / 编辑」或行内「编辑」均打开与产品管理一致的表单示意层。
</div>
</div>
<script>
const tbody = document.getElementById('tbody')
const selCount = document.getElementById('selCount')
const btnEdit = document.getElementById('btnEdit')
const btnAdd = document.getElementById('btnAdd')
const formMask = document.getElementById('formMask')
const formTitle = document.getElementById('formTitle')
const formName = document.getElementById('formName')
function openForm(mode, name) {
formTitle.textContent = mode === 'edit' ? '编辑产品' : '新增产品'
formName.value = name || ''
formMask.classList.add('show')
}
function syncSelection() {
const checks = [...tbody.querySelectorAll('.row-check')]
const selected = checks.filter((c) => c.checked)
selCount.textContent = String(selected.length)
checks.forEach((c) => {
c.closest('tr').classList.toggle('is-checked', c.checked)
})
btnEdit.disabled = selected.length !== 1
btnEdit.title = selected.length === 1 ? '编辑所选产品' : '请先勾选恰好 1 条产品'
}
tbody.addEventListener('change', (e) => {
if (e.target.classList.contains('row-check')) syncSelection()
})
tbody.addEventListener('click', (e) => {
const btn = e.target.closest('.row-edit')
if (!btn) return
const row = btn.closest('tr')
openForm('edit', row?.querySelector('.name-cell')?.textContent?.trim() || '')
})
btnAdd.addEventListener('click', () => openForm('add', ''))
btnEdit.addEventListener('click', () => {
if (btnEdit.disabled) return
const row = tbody.querySelector('.row-check:checked')?.closest('tr')
openForm('edit', row?.querySelector('.name-cell')?.textContent?.trim() || '')
})
function closeForm() {
formMask.classList.remove('show')
}
document.getElementById('formClose').addEventListener('click', closeForm)
document.getElementById('formCancel').addEventListener('click', closeForm)
document.getElementById('formSave').addEventListener('click', closeForm)
formMask.addEventListener('click', (e) => {
if (e.target === formMask) closeForm()
})
syncSelection()
</script>
</body>
</html>