Files
shanxia2.0/docs/fresh-mint-list-excel-select-demo.html
T
2026-10-21 05:04:56 +08:00

620 lines
21 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.0" />
<title>Fresh Mint · 列表 Excel 选择复制 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;
--surface: #ffffff;
--table-head: #f8fafc;
--bg-page: #f8fafc;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
--radius: 10px;
--select-fill: rgba(16, 185, 129, 0.16);
--select-border: #059669;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 14px;
line-height: 1.5;
color: var(--text);
background: var(--bg-page);
}
.demo-page { max-width: 1180px; margin: 0 auto; padding: 32px 24px 80px; }
.demo-head { margin-bottom: 20px; }
.demo-head h1 {
margin: 0 0 8px;
font-size: 24px;
font-weight: 700;
letter-spacing: -0.02em;
}
.demo-head h1 span { color: var(--primary); }
.demo-head p { margin: 0; color: var(--text-muted); max-width: 760px; }
.panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
}
.panel-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px 16px;
border-bottom: 1px solid var(--border);
}
.panel-top h3 {
margin: 0;
font-size: 15px;
font-weight: 700;
position: relative;
padding-left: 10px;
}
.panel-top h3::before {
content: "";
position: absolute;
left: 0;
top: 2px;
bottom: 2px;
width: 3px;
border-radius: 2px;
background: var(--primary);
}
.list-toolbar-row { width: 100%; }
.list-toolbar-end {
display: inline-flex;
align-items: center;
gap: 8px;
margin-left: auto;
flex-shrink: 0;
}
.icon-btn {
width: 36px;
height: 36px;
padding: 0;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--text-muted);
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.icon-btn:hover { color: var(--primary-dark); background: var(--primary-bg); }
.icon-btn.active {
color: var(--primary-dark);
background: var(--primary-bg);
}
.icon-btn svg { width: 16px; height: 16px; display: block; }
.list-toolbar { background: var(--table-head); border-bottom: 1px solid var(--border); }
.list-toolbar-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
padding: 8px 16px;
}
.list-toolbar-selected {
display: inline-flex;
align-items: center;
min-height: 36px;
padding-right: 10px;
margin-right: 4px;
border-right: 1px solid var(--border);
font-size: 12px;
font-weight: 600;
color: var(--text-muted);
white-space: nowrap;
}
.sel-num { color: var(--primary-dark); font-weight: 700; margin: 0 2px; }
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
height: 36px;
padding: 0 14px;
border-radius: 8px;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text);
font-size: 14px;
font-weight: 600;
font-family: inherit;
cursor: pointer;
white-space: nowrap;
}
.btn:hover { border-color: #b7d5cb; background: #f8fafc; }
.btn:disabled { opacity: .45; cursor: not-allowed; }
.btn-main {
background: var(--primary);
border-color: var(--primary);
color: #fff;
}
.btn-main:hover { background: var(--primary-dark); border-color: var(--primary-dark); }
.hint {
padding: 8px 16px 10px;
font-size: 12px;
color: var(--text-muted);
}
.hint strong { color: var(--text); font-weight: 600; }
.table-wrap { overflow: auto; }
table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
font-size: 14px;
}
th, td {
border-bottom: 1px solid #eef4f1;
padding: 0;
text-align: left;
vertical-align: middle;
background: #fff;
}
thead th {
background: var(--table-head);
color: #475569;
font-size: 13px;
font-weight: 600;
height: 42px;
}
tbody tr:nth-child(even) td { background: #fcfdfc; }
.cell-pad { padding: 10px 12px; }
.col-check { width: 52px; text-align: center; }
.col-index { width: 64px; text-align: center; color: var(--text-muted); }
.col-action { width: 88px; }
.link {
color: var(--primary-dark);
font-weight: 600;
cursor: pointer;
text-decoration: none;
}
.link:hover { text-decoration: underline; }
.tag {
display: inline-flex;
align-items: center;
height: 22px;
padding: 0 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
color: #fff;
}
.tag-success { background: #059669; }
.tag-warning { background: #d97706; }
.text-btn {
border: 0;
background: transparent;
color: var(--primary-dark);
font: inherit;
font-weight: 600;
cursor: pointer;
padding: 0;
}
body.mode-excel .col-head,
body.mode-excel .row-head {
cursor: pointer;
}
body.mode-excel table { user-select: none; }
.data-cell.selected {
background: var(--select-fill) !important;
box-shadow: inset 0 0 0 1px var(--select-border);
}
thead th.col-active { background: #d1fae5; color: var(--primary-dark); }
td.row-active { background: #d1fae5 !important; color: var(--primary-dark); font-weight: 700; }
body.mode-excel .link { color: var(--text); cursor: default; text-decoration: none; }
body.mode-excel .text-btn,
body.mode-excel .row-check { cursor: not-allowed; opacity: .4; }
.below {
display: grid;
grid-template-columns: 1.1fr .9fr;
gap: 16px;
margin-top: 16px;
}
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 14px 16px 16px;
}
.card h4 { margin: 0 0 8px; font-size: 14px; }
.card p, .card li { margin: 0; color: var(--text-muted); font-size: 13px; }
.card ol { margin: 0; padding-left: 18px; }
.card li + li { margin-top: 4px; }
textarea {
width: 100%;
min-height: 180px;
margin-top: 8px;
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: 8px;
resize: vertical;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 12px;
line-height: 1.6;
color: var(--text);
background: #f8fafc;
}
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translateX(-50%) translateY(12px);
opacity: 0;
pointer-events: none;
background: #1e293b;
color: #fff;
border-radius: 8px;
padding: 10px 14px;
font-size: 13px;
transition: .2s ease;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
@media (max-width: 860px) {
.below { grid-template-columns: 1fr; }
}
</style>
</head>
<body class="mode-list">
<div class="demo-page">
<header class="demo-head">
<h1>Fresh Mint · <span>列表 / Excel 切换</span></h1>
<p>Excel 入口在列表工具栏最右侧、刷新图标的左边。点一下进入选择复制,再点一下回到普通列表。</p>
</header>
<section class="panel">
<div class="panel-top">
<h3>客户列表</h3>
</div>
<div class="list-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-selected">已选 <span class="sel-num" id="checked-count">0</span> 项</div>
<button type="button" class="btn btn-main" id="btn-add">新增</button>
<button type="button" class="btn" id="btn-copy" disabled>复制选区</button>
<button type="button" class="btn" id="btn-clear" disabled>清空行列</button>
<span class="list-toolbar-selected" id="range-label" style="border: 0; margin-left: 8px;">未选择行列</span>
<div class="list-toolbar-end">
<button type="button" class="icon-btn" id="mode-excel" title="Excel 复制" aria-label="Excel 复制" aria-pressed="false">
<svg viewBox="0 0 16 16" aria-hidden="true">
<rect x="1.5" y="2" width="13" height="12" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.4"/>
<path d="M1.5 6.2h13M1.5 10h13M6.2 2v12M10.4 2v12" stroke="currentColor" stroke-width="1.1"/>
</svg>
</button>
<button type="button" class="icon-btn" id="btn-refresh" title="刷新列表" aria-label="刷新列表">
<svg viewBox="0 0 16 16" aria-hidden="true">
<path d="M13.2 8a5.2 5.2 0 1 1-1.4-3.5" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
<path d="M12.2 2.6v3.2H9" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<button type="button" class="icon-btn" id="btn-columns" title="列设置" aria-label="列设置">
<svg viewBox="0 0 16 16" aria-hidden="true">
<circle cx="8" cy="8" r="2" fill="none" stroke="currentColor" stroke-width="1.4"/>
<path d="M8 1.8v2.1M8 12.1v2.1M1.8 8h2.1M12.1 8h2.1M3.6 3.6l1.5 1.5M10.9 10.9l1.5 1.5M12.4 3.6l-1.5 1.5M5.1 10.9l-1.5 1.5" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
</svg>
</button>
</div>
</div>
<div class="hint" id="hint"></div>
</div>
<div class="table-wrap">
<table id="grid">
<thead>
<tr>
<th class="col-check"></th>
<th class="col-index">行号</th>
<th class="col-head" data-col="0">客户名称</th>
<th class="col-head" data-col="1">联系人</th>
<th class="col-head" data-col="2">电话</th>
<th class="col-head" data-col="3">地区</th>
<th class="col-head" data-col="4">状态</th>
<th class="col-action">操作</th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
</div>
</section>
<div class="below">
<div class="card">
<h4>怎么试</h4>
<ol>
<li>先停在「列表」:点客户名称会提示打开查看,点勾选框会计入「已选 N 项」,点编辑会提示编辑。</li>
<li>点刷新图标左侧的表格图标,进入 Excel 复制。图标变绿表示已打开,再点一次关闭。</li>
<li>行号同样:单击多选行,Shift 连选一段行。列和行可以一起选,复制的是它们的交叉区域。</li>
<li>只选了列时,复制这些列的全部行;只选了行时,复制这些行的全部数据列。然后 Ctrl/Cmd + C,或点「复制选区」。</li>
</ol>
</div>
<div class="card">
<h4>剪贴板预览</h4>
<p>这就是剪贴板里的文本:列与列之间是制表符,行与行之间是换行。在 Excel 里 Ctrl/Cmd + V,会按行列落入单元格。第一行是表头。</p>
<textarea id="preview" readonly placeholder="还没有复制"></textarea>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
const COLS = ['客户名称', '联系人', '电话', '地区', '状态']
const ROWS = [
['山峡化工', '张三', '13800001111', '重庆 / 长寿', '合作中'],
['华东贸易', '李四', '13900002222', '上海 / 浦东', '跟进中'],
['北港仓储', '王五', '13700003333', '天津 / 滨海', '合作中'],
['锦程物流', '赵六', '13600004444', '四川 / 成都', '跟进中'],
['清源新材', '陈七', '13500005555', '江苏 / 南京', '合作中'],
['泸州客户', '周八', '13400006666', '四川 / 泸州', '跟进中'],
['宜宾供应', '吴九', '13300007777', '四川 / 宜宾', '合作中'],
['万州门店', '郑十', '13200008888', '重庆 / 万州', '跟进中']
]
const tbody = document.getElementById('tbody')
const hint = document.getElementById('hint')
const preview = document.getElementById('preview')
const rangeLabel = document.getElementById('range-label')
const checkedCount = document.getElementById('checked-count')
const btnCopy = document.getElementById('btn-copy')
const btnClear = document.getElementById('btn-clear')
const toast = document.getElementById('toast')
let mode = 'list'
const selectedCols = new Set()
const selectedRows = new Set()
let lastCol = null
let lastRow = null
function showToast(text) {
toast.textContent = text
toast.classList.add('show')
clearTimeout(showToast._t)
showToast._t = setTimeout(() => toast.classList.remove('show'), 1600)
}
function statusTag(text) {
const kind = text === '合作中' ? 'tag-success' : 'tag-warning'
return '<span class="tag ' + kind + '">' + text + '</span>'
}
function renderRows() {
tbody.innerHTML = ROWS.map((row, r) => {
const cells = row.map((value, c) => {
const inner = c === 0
? '<a class="link" href="#" data-name="' + value + '">' + value + '</a>'
: c === 4
? statusTag(value)
: value
return '<td class="data-cell" data-row="' + r + '" data-col="' + c + '"><div class="cell-pad">' + inner + '</div></td>'
}).join('')
return '<tr>'
+ '<td class="col-check"><div class="cell-pad"><input class="row-check" type="checkbox" data-row="' + r + '" aria-label="选择第 ' + (r + 1) + ' 行"></div></td>'
+ '<td class="col-index row-head" data-row="' + r + '">' + (r + 1) + '</td>'
+ cells
+ '<td class="col-action"><div class="cell-pad"><button type="button" class="text-btn" data-edit="' + row[0] + '">编辑</button></div></td>'
+ '</tr>'
}).join('')
}
function sortedIndexes(set) {
return Array.from(set).sort((a, b) => a - b)
}
function copyAxes() {
if (!selectedCols.size && !selectedRows.size) return null
const cols = selectedCols.size ? sortedIndexes(selectedCols) : COLS.map((_, i) => i)
const rows = selectedRows.size ? sortedIndexes(selectedRows) : ROWS.map((_, i) => i)
return { cols, rows }
}
function toggleIndex(set, index, shift, last) {
if (shift && last !== null) {
const from = Math.min(last, index)
const to = Math.max(last, index)
for (let i = from; i <= to; i += 1) set.add(i)
return index
}
if (set.has(index)) set.delete(index)
else set.add(index)
return index
}
function paint() {
document.querySelectorAll('.data-cell').forEach((cell) => cell.classList.remove('selected'))
document.querySelectorAll('.col-head').forEach((el) => el.classList.remove('col-active'))
document.querySelectorAll('.row-head').forEach((el) => el.classList.remove('row-active'))
const axes = mode === 'excel' ? copyAxes() : null
const hasPick = selectedCols.size || selectedRows.size
btnClear.disabled = mode !== 'excel' || !hasPick
if (!axes) {
rangeLabel.textContent = '未选择行列'
btnCopy.disabled = true
return
}
rangeLabel.innerHTML = '将复制 <span class="sel-num">' + axes.rows.length + '</span> 行 × <span class="sel-num">' + axes.cols.length + '</span> 列'
btnCopy.disabled = false
selectedCols.forEach((c) => {
const head = document.querySelector('.col-head[data-col="' + c + '"]')
if (head) head.classList.add('col-active')
})
selectedRows.forEach((r) => {
const head = document.querySelector('.row-head[data-row="' + r + '"]')
if (head) head.classList.add('row-active')
})
const colSet = new Set(axes.cols)
const rowSet = new Set(axes.rows)
document.querySelectorAll('.data-cell').forEach((cell) => {
const r = Number(cell.dataset.row)
const c = Number(cell.dataset.col)
if (rowSet.has(r) && colSet.has(c)) cell.classList.add('selected')
})
}
function clearAxes() {
selectedCols.clear()
selectedRows.clear()
lastCol = null
lastRow = null
paint()
}
function setMode(next) {
mode = next
document.body.classList.toggle('mode-excel', mode === 'excel')
document.body.classList.toggle('mode-list', mode === 'list')
const excelBtn = document.getElementById('mode-excel')
excelBtn.classList.toggle('active', mode === 'excel')
excelBtn.setAttribute('aria-pressed', mode === 'excel' ? 'true' : 'false')
excelBtn.title = mode === 'excel' ? '退出 Excel 复制' : 'Excel 复制'
if (mode === 'list') {
clearAxes()
hint.innerHTML = '<strong>列表模式。</strong> 单击名称打开查看,勾选框用于批量操作,编辑按钮保持可用。'
} else {
hint.innerHTML = '<strong>Excel 模式。</strong> 点表头多选列,点行号多选行。Shift 可一次连选一段。只选列则复制这些列的全部行,只选行则复制这些行的全部列,两边都选则复制交叉区域。'
paint()
}
}
function excelCell(value) {
return String(value ?? '').replace(/\t/g, ' ').replace(/\r?\n/g, ' ')
}
function selectionText() {
const axes = copyAxes()
if (!axes) return ''
const lines = [axes.cols.map((c) => excelCell(COLS[c])).join('\t')]
axes.rows.forEach((r) => {
lines.push(axes.cols.map((c) => excelCell(ROWS[r][c])).join('\t'))
})
return lines.join('\r\n')
}
async function copySelection() {
const text = selectionText()
if (!text) return
preview.value = text
try {
await navigator.clipboard.write([
new ClipboardItem({
'text/plain': new Blob([text], { type: 'text/plain' })
})
])
showToast('已复制,到 Excel 中直接粘贴')
} catch (err) {
try {
await navigator.clipboard.writeText(text)
showToast('已复制,到 Excel 中直接粘贴')
return
} catch (err2) {
/* 预览区仍保留文本 */
}
preview.focus()
preview.select()
showToast('浏览器未授权剪贴板,文本已放在右侧预览')
}
}
function refreshChecked() {
const n = document.querySelectorAll('.row-check:checked').length
checkedCount.textContent = String(n)
}
renderRows()
setMode('list')
document.getElementById('mode-excel').addEventListener('click', () => {
setMode(mode === 'excel' ? 'list' : 'excel')
})
document.getElementById('btn-refresh').addEventListener('click', () => showToast('刷新列表'))
document.getElementById('btn-columns').addEventListener('click', () => showToast('列设置'))
document.getElementById('btn-add').addEventListener('click', () => showToast('这里只是演示,不会新增'))
btnCopy.addEventListener('click', copySelection)
btnClear.addEventListener('click', clearAxes)
document.getElementById('grid').addEventListener('click', (event) => {
const edit = event.target.closest('[data-edit]')
const link = event.target.closest('.link')
const check = event.target.closest('.row-check')
if (mode === 'list') {
if (link) {
event.preventDefault()
showToast('打开查看:' + link.dataset.name)
} else if (edit) {
showToast('编辑:' + edit.dataset.edit)
} else if (check) {
refreshChecked()
}
return
}
if (link) event.preventDefault()
if (check || edit) {
event.preventDefault()
if (check) check.checked = false
showToast('Excel 模式下勾选和编辑已暂停')
}
})
document.getElementById('grid').addEventListener('click', (event) => {
if (mode !== 'excel') return
const head = event.target.closest('.col-head')
const rowHead = event.target.closest('.row-head')
if (head) {
lastCol = toggleIndex(selectedCols, Number(head.dataset.col), event.shiftKey, lastCol)
paint()
return
}
if (rowHead) {
lastRow = toggleIndex(selectedRows, Number(rowHead.dataset.row), event.shiftKey, lastRow)
paint()
}
})
document.addEventListener('keydown', (event) => {
if (mode !== 'excel') return
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'c') {
if (!copyAxes()) return
event.preventDefault()
copySelection()
}
if (event.key === 'Escape') clearAxes()
})
</script>
</body>
</html>