Files
shanxia2.0/docs/demo-pl-vehicle-inline-personnel-create.html
T
2026-08-20 11:17:41 +08:00

868 lines
25 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>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: #fefefe;
--surface: #ffffff;
--table-head: #f8fafc;
--row-alt: #fafafa;
--row-hover: #f1f5f9;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
--radius: 10px;
--content-padding: 24px;
--warning: #d97706;
--danger: #dc2626;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: Inter, -apple-system, sans-serif;
font-size: 14px;
line-height: 1.5;
color: var(--text);
background: var(--bg-page);
}
.demo-banner {
background: linear-gradient(90deg, #ecfdf5, #fff);
border-bottom: 1px solid var(--border);
padding: 12px 24px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.demo-banner h1 {
margin: 0;
font-size: 15px;
font-weight: 600;
}
.demo-banner p {
margin: 4px 0 0;
font-size: 12px;
color: var(--text-muted);
}
.demo-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.page-container {
padding: var(--content-padding);
max-width: 1440px;
margin: 0 auto;
}
.panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
}
.panel-top {
padding: 16px 20px;
border-bottom: 1px solid var(--border);
}
.title-bar-heading {
margin: 0;
font-size: 16px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.title-bar-heading::before {
content: '';
width: 4px;
height: 18px;
border-radius: 2px;
background: var(--primary);
}
.list-toolbar {
padding: 12px 20px;
background: var(--table-head);
border-bottom: 1px solid var(--border);
}
.list-toolbar-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.btn {
display: inline-flex;
align-items: 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-family: inherit;
cursor: pointer;
transition: all 0.15s ease;
white-space: nowrap;
}
.btn:hover { background: var(--row-hover); }
.btn-main {
background: var(--primary);
border-color: var(--primary);
color: #fff;
}
.btn-main:hover {
background: var(--primary-dark);
border-color: var(--primary-dark);
}
.btn-ghost {
background: var(--surface);
}
.btn-sm {
height: 32px;
padding: 0 12px;
font-size: 13px;
}
.btn-link {
border: none;
background: none;
color: var(--primary-dark);
padding: 0 4px;
height: auto;
}
.btn-link:hover { text-decoration: underline; background: none; }
.btn-danger {
color: var(--danger);
border-color: rgba(220, 38, 38, 0.25);
}
.list-body { padding: 0; }
table.list-table {
width: 100%;
border-collapse: collapse;
}
.list-table th,
.list-table td {
padding: 12px 16px;
text-align: center;
border-bottom: 1px solid var(--border);
}
.list-table th {
background: var(--table-head);
color: var(--text-muted);
font-size: 12px;
font-weight: 600;
}
.list-table tbody tr:nth-child(even) { background: var(--row-alt); }
.list-table tbody tr:hover { background: var(--row-hover); }
.link-primary {
color: var(--primary-dark);
text-decoration: none;
cursor: pointer;
font-weight: 500;
}
.link-primary:hover { text-decoration: underline; }
.tag {
display: inline-block;
padding: 2px 8px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
color: #fff;
}
.tag.success { background: var(--primary-dark); }
.tag.warning { background: var(--warning); }
/* Dialog */
.backdrop {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.35);
backdrop-filter: none;
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 24px;
}
.backdrop.is-open { display: flex; }
.dialog {
background: var(--surface);
border-radius: var(--radius);
box-shadow: 0 20px 60px rgba(15, 23, 42, 0.18);
width: min(1360px, 100%);
max-height: calc(100vh - 48px);
display: flex;
flex-direction: column;
overflow: hidden;
}
.dialog.dialog-sm { width: min(720px, 100%); }
.dialog-header {
padding: 16px 20px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
}
.dialog-header h2 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.dialog-close {
width: 32px;
height: 32px;
border: none;
background: none;
border-radius: 8px;
cursor: pointer;
color: var(--text-muted);
font-size: 20px;
line-height: 1;
}
.dialog-close:hover { background: var(--row-hover); color: var(--text); }
.dialog-body {
padding: 20px;
overflow-y: auto;
flex: 1;
}
.dialog-footer {
padding: 12px 20px;
border-top: 1px solid var(--border);
background: var(--table-head);
display: flex;
justify-content: flex-end;
gap: 8px;
}
.form-grid-3 {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px 20px;
}
.form-grid-2 {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px 20px;
}
@media (max-width: 900px) {
.form-grid-3, .form-grid-2 { grid-template-columns: 1fr; }
}
.form-item label {
display: block;
font-size: 13px;
font-weight: 500;
margin-bottom: 6px;
color: var(--text);
}
.form-item .form-label-tip {
font-size: 12px;
font-weight: 400;
color: var(--text-muted);
margin-left: 4px;
}
.form-item input,
.form-item select,
.form-item textarea {
width: 100%;
height: 36px;
padding: 0 12px;
border: 1px solid var(--border);
border-radius: 8px;
font-family: inherit;
font-size: 14px;
color: var(--text);
background: var(--surface);
}
.form-item textarea {
height: auto;
min-height: 72px;
padding: 8px 12px;
resize: vertical;
}
.form-item input:focus,
.form-item select:focus,
.form-item textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
}
.form-col-span-full { grid-column: 1 / -1; }
.sub-table-section {
margin-top: 20px;
padding-top: 16px;
border-top: 1px dashed var(--border);
}
.sub-table-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.sub-table-title {
font-size: 14px;
font-weight: 600;
}
.sub-table-actions {
display: flex;
gap: 8px;
}
.sub-table .list-table td { text-align: left; }
.sub-table .list-table th:first-child,
.sub-table .list-table td:first-child { text-align: center; }
.select-with-action {
display: flex;
gap: 6px;
align-items: center;
}
.select-with-action select { flex: 1; min-width: 0; }
.toast {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%) translateY(-20px);
background: var(--primary-dark);
color: #fff;
padding: 10px 18px;
border-radius: 8px;
font-size: 14px;
opacity: 0;
pointer-events: none;
transition: all 0.25s ease;
z-index: 2000;
box-shadow: 0 8px 24px rgba(5, 150, 105, 0.35);
}
.toast.is-show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
.hint-box {
margin-top: 16px;
padding: 12px 14px;
border-radius: 8px;
background: var(--primary-bg);
border: 1px solid rgba(16, 185, 129, 0.25);
font-size: 13px;
color: var(--text-muted);
}
.hint-box strong { color: var(--primary-dark); }
.flow-steps {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 8px;
}
.flow-step {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 999px;
background: var(--surface);
border: 1px solid var(--border);
font-size: 12px;
color: var(--text-muted);
}
.flow-step.active {
border-color: var(--primary);
background: var(--primary-bg);
color: var(--primary-dark);
font-weight: 600;
}
.flow-step .num {
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--border);
color: var(--text-muted);
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 700;
}
.flow-step.active .num {
background: var(--primary);
color: #fff;
}
.badge-new {
display: inline-block;
margin-left: 6px;
padding: 1px 6px;
border-radius: 4px;
background: var(--primary-bg);
color: var(--primary-dark);
font-size: 11px;
font-weight: 600;
}
.icon { width: 16px; height: 16px; display: inline-block; vertical-align: -3px; }
</style>
</head>
<body>
<div class="demo-banner">
<div>
<h1>Demo · 三方车辆 · 编辑时快速新建并绑定人员</h1>
<p>交互原型(Fresh Mint)— 点击下方按钮体验完整流程</p>
<div class="flow-steps" id="flowSteps">
<span class="flow-step active" data-step="1"><span class="num">1</span>编辑车辆</span>
<span class="flow-step" data-step="2"><span class="num">2</span>添加人员行</span>
<span class="flow-step" data-step="3"><span class="num">3</span>快速新建人员</span>
<span class="flow-step" data-step="4"><span class="num">4</span>自动选中并绑定</span>
</div>
</div>
<div class="demo-actions">
<button class="btn btn-ghost" type="button" onclick="openVehicleDialog()">打开「编辑车辆」弹窗</button>
<button class="btn btn-main" type="button" onclick="runFullDemo()">▶ 自动演示完整流程</button>
</div>
</div>
<div class="page-container">
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">三方车辆</h2>
</div>
<div class="list-toolbar">
<div class="list-toolbar-row">
<button class="btn btn-ghost" type="button">快速识别创建</button>
<button class="btn btn-main" type="button">新增车辆</button>
<button class="btn btn-ghost" type="button" onclick="openVehicleDialog()">编辑</button>
</div>
</div>
<div class="list-body">
<table class="list-table">
<thead>
<tr>
<th style="width:48px">序号</th>
<th>车牌号</th>
<th>车辆状态</th>
<th>承运商</th>
<th>关联人员</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td><a class="link-primary" href="#" onclick="openVehicleDialog(); return false;">皖A12345 / 皖A67890挂</a></td>
<td><span class="tag success">可用</span></td>
<td>某某物流</td>
<td id="listPersonnelSummary">—</td>
</tr>
<tr>
<td>2</td>
<td><a class="link-primary" href="#">苏B88888</a></td>
<td><span class="tag success">可用</span></td>
<td>华东运输</td>
<td>王师傅(司机)</td>
</tr>
</tbody>
</table>
</div>
</section>
<div class="hint-box">
<strong>设计要点</strong>:在「关联人员」子表每行的姓名下拉旁增加「新建」入口;新建成功后自动回填当前行并刷新下拉选项,无需切页到「三方驾押人员」。
</div>
</div>
<!-- 编辑车辆弹窗 -->
<div class="backdrop" id="vehicleBackdrop">
<div class="dialog" role="dialog" aria-labelledby="vehicleDialogTitle">
<div class="dialog-header">
<h2 id="vehicleDialogTitle">编辑车辆</h2>
<button class="dialog-close" type="button" aria-label="关闭" onclick="closeVehicleDialog()">×</button>
</div>
<div class="dialog-body">
<div class="form-grid-3">
<div class="form-item">
<label>车牌号</label>
<input type="text" value="皖A12345" readonly />
</div>
<div class="form-item">
<label>挂车号</label>
<input type="text" value="皖A67890挂" readonly />
</div>
<div class="form-item">
<label>载重(吨)</label>
<input type="text" value="34" readonly />
</div>
<div class="form-item">
<label>承运商</label>
<input type="text" value="某某物流" readonly />
</div>
<div class="form-item">
<label>车辆状态</label>
<input type="text" value="可用" readonly />
</div>
<div class="form-item">
<label>备注</label>
<input type="text" placeholder="请输入备注" />
</div>
</div>
<div class="sub-table-section">
<div class="sub-table-header">
<span class="sub-table-title">关联人员</span>
<div class="sub-table-actions">
<button class="btn btn-sm btn-ghost" type="button" id="btnAddPersonnel" onclick="addPersonnelRow()">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>
添加人员
</button>
</div>
</div>
<table class="list-table sub-table" id="personnelTable">
<thead>
<tr>
<th style="width:56px">序号</th>
<th style="min-width:220px">姓名</th>
<th style="width:120px">手机号</th>
<th style="width:80px">年龄</th>
<th style="width:110px">人员类型</th>
<th style="width:100px">备注</th>
<th style="width:60px">操作</th>
</tr>
</thead>
<tbody id="personnelTbody">
<!-- rows injected -->
</tbody>
</table>
</div>
</div>
<div class="dialog-footer">
<button class="btn btn-ghost" type="button" onclick="closeVehicleDialog()">取消</button>
<button class="btn btn-main" type="button" onclick="saveVehicle()">确定</button>
</div>
</div>
</div>
<!-- 快速新建人员弹窗 -->
<div class="backdrop" id="personnelBackdrop" style="z-index:1100">
<div class="dialog dialog-sm" role="dialog" aria-labelledby="personnelDialogTitle">
<div class="dialog-header">
<h2 id="personnelDialogTitle">快速新建三方人员</h2>
<button class="dialog-close" type="button" aria-label="关闭" onclick="closePersonnelDialog()">×</button>
</div>
<div class="dialog-body">
<div class="form-grid-2">
<div class="form-item">
<label>姓名 <span style="color:var(--danger)">*</span></label>
<input type="text" id="newName" placeholder="请输入姓名" maxlength="100" />
</div>
<div class="form-item">
<label>人员类型 <span style="color:var(--danger)">*</span></label>
<select id="newType">
<option value="driver">司机</option>
<option value="escort">押运员</option>
</select>
</div>
<div class="form-item">
<label>手机号</label>
<input type="text" id="newPhone" placeholder="13800000000" maxlength="32" />
</div>
<div class="form-item">
<label>身份证号</label>
<input type="text" id="newIdCard" placeholder="340000199001010000" maxlength="32" />
</div>
<div class="form-item form-col-span-full">
<label>备注</label>
<textarea id="newRemark" placeholder="可选"></textarea>
</div>
</div>
<div class="hint-box" style="margin-top:12px">
保存后将<strong>自动选中到当前关联行</strong>,并同步写入「三方驾押人员」主数据。
</div>
</div>
<div class="dialog-footer">
<button class="btn btn-ghost" type="button" onclick="closePersonnelDialog()">取消</button>
<button class="btn btn-main" type="button" id="btnSavePersonnel" onclick="saveNewPersonnel()">保存并关联</button>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
/** @type {{id:string,name:string,phone:string,age:number|null,type:string,remark:string,isNew?:boolean}[]} */
let personnelPool = [
{ id: 'p1', name: '王师傅', phone: '13800001111', age: 42, type: 'driver', remark: '' }
]
/** @type {{personnelId:string,role:string,remark:string}[]} */
let personnelRows = []
let activeRowIndex = -1
let rowKey = 0
function setFlowStep(step) {
document.querySelectorAll('.flow-step').forEach(el => {
el.classList.toggle('active', Number(el.dataset.step) <= step)
})
}
function showToast(msg) {
const t = document.getElementById('toast')
t.textContent = msg
t.classList.add('is-show')
setTimeout(() => t.classList.remove('is-show'), 2400)
}
function openVehicleDialog() {
document.getElementById('vehicleBackdrop').classList.add('is-open')
if (personnelRows.length === 0) {
addPersonnelRow()
}
renderPersonnelRows()
setFlowStep(1)
}
function closeVehicleDialog() {
document.getElementById('vehicleBackdrop').classList.remove('is-open')
}
function addPersonnelRow() {
personnelRows.push({ personnelId: '', role: 'driver', remark: '' })
renderPersonnelRows()
setFlowStep(2)
}
function removePersonnelRow(index) {
personnelRows.splice(index, 1)
renderPersonnelRows()
}
function getPersonnelLabel(p) {
return p.phone ? `${p.name}(${p.phone})` : p.name
}
function renderPersonnelRows() {
const tbody = document.getElementById('personnelTbody')
tbody.innerHTML = personnelRows.map((row, index) => {
const options = personnelPool.map(p =>
`<option value="${p.id}" ${row.personnelId === p.id ? 'selected' : ''}>${getPersonnelLabel(p)}${p.isNew ? ' ★' : ''}</option>`
).join('')
const selected = personnelPool.find(p => p.id === row.personnelId)
return `
<tr data-index="${index}">
<td>${index + 1}</td>
<td>
<div class="select-with-action">
<select onchange="onPersonnelChange(${index}, this.value)">
<option value="">请选择人员</option>
${options}
</select>
<button class="btn btn-sm btn-link" type="button" onclick="openPersonnelDialog(${index})">新建</button>
</div>
${selected?.isNew ? '<span class="badge-new">刚创建</span>' : ''}
</td>
<td>${selected?.phone || '—'}</td>
<td style="text-align:center">${selected?.age ?? '—'}</td>
<td>
<select onchange="onRoleChange(${index}, this.value)">
<option value="driver" ${row.role === 'driver' ? 'selected' : ''}>司机</option>
<option value="escort" ${row.role === 'escort' ? 'selected' : ''}>押运员</option>
</select>
</td>
<td><input type="text" value="${row.remark || ''}" placeholder="备注" oninput="personnelRows[${index}].remark=this.value" style="height:32px" /></td>
<td style="text-align:center">
<button class="btn btn-sm btn-danger" type="button" onclick="removePersonnelRow(${index})" title="删除">✕</button>
</td>
</tr>
`
}).join('')
}
function onPersonnelChange(index, id) {
personnelRows[index].personnelId = id
const p = personnelPool.find(x => x.id === id)
if (p) {
personnelRows[index].role = p.type === 'escort' ? 'escort' : 'driver'
}
renderPersonnelRows()
}
function onRoleChange(index, role) {
personnelRows[index].role = role
}
function openPersonnelDialog(rowIndex) {
activeRowIndex = rowIndex
document.getElementById('newName').value = ''
document.getElementById('newPhone').value = ''
document.getElementById('newIdCard').value = ''
document.getElementById('newRemark').value = ''
document.getElementById('newType').value = personnelRows[rowIndex]?.role || 'driver'
document.getElementById('personnelBackdrop').classList.add('is-open')
setFlowStep(3)
setTimeout(() => document.getElementById('newName').focus(), 200)
}
function closePersonnelDialog() {
document.getElementById('personnelBackdrop').classList.remove('is-open')
activeRowIndex = -1
}
function calcAgeFromIdCard(idCard) {
if (!idCard || idCard.length < 14) return null
const y = parseInt(idCard.slice(6, 10), 10)
if (!y) return null
return new Date().getFullYear() - y
}
function saveNewPersonnel() {
const name = document.getElementById('newName').value.trim()
const phone = document.getElementById('newPhone').value.trim()
const idCard = document.getElementById('newIdCard').value.trim()
const type = document.getElementById('newType').value
const remark = document.getElementById('newRemark').value.trim()
if (!name) {
showToast('请填写姓名')
document.getElementById('newName').focus()
return
}
rowKey += 1
const newPerson = {
id: `new-${rowKey}`,
name,
phone,
age: calcAgeFromIdCard(idCard),
type,
remark,
isNew: true
}
personnelPool.unshift(newPerson)
if (activeRowIndex >= 0) {
personnelRows[activeRowIndex].personnelId = newPerson.id
personnelRows[activeRowIndex].role = type
personnelRows[activeRowIndex].remark = remark
}
closePersonnelDialog()
renderPersonnelRows()
setFlowStep(4)
showToast(`已创建「${name}」并自动关联到当前行`)
}
function saveVehicle() {
const bound = personnelRows
.filter(r => r.personnelId)
.map(r => {
const p = personnelPool.find(x => x.id === r.personnelId)
const roleLabel = r.role === 'driver' ? '司机' : '押运员'
return `${p?.name || '未知'}(${roleLabel})`
})
const summary = bound.length ? bound.join('、') : '—'
document.getElementById('listPersonnelSummary').textContent = summary
closeVehicleDialog()
showToast('车辆保存成功,关联人员已更新')
}
async function runFullDemo() {
personnelRows = []
openVehicleDialog()
await wait(600)
addPersonnelRow()
await wait(500)
openPersonnelDialog(0)
await wait(400)
document.getElementById('newName').value = '张师傅'
document.getElementById('newPhone').value = '13812345678'
document.getElementById('newIdCard').value = '340000199001010000'
document.getElementById('newType').value = 'driver'
await wait(800)
saveNewPersonnel()
await wait(1000)
saveVehicle()
}
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
// init
renderPersonnelRows()
</script>
</body>
</html>