Files
shanxia2.0/docs/fresh-mint-pl-vehicle-personnel-photo-demo.html
T
zlwuandCursor 9af37c3f56 docs: add vehicle/personnel photo field design and UI demo
Capture the approved photo_urls approach (list thumbnail A, optional upload) for pl_vehicle and pl_personnel before implementation.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-02 10:13:12 +08:00

760 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>Fresh Mint · 车辆/驾押人员照片字段 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;
--danger: #ef4444;
}
* { 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 96px; }
.demo-head { margin-bottom: 28px; }
.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: 820px; }
.toc {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 20px 0 0;
}
.toc a {
display: inline-flex;
align-items: center;
height: 32px;
padding: 0 12px;
border-radius: 8px;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text);
text-decoration: none;
font-size: 13px;
font-weight: 500;
}
.toc a:hover,
.toc a.rec {
border-color: var(--primary);
color: var(--primary-dark);
background: var(--primary-bg);
}
.section { margin-top: 40px; }
.section-head { margin-bottom: 14px; }
.section-head h2 {
margin: 0 0 4px;
font-size: 18px;
font-weight: 700;
}
.section-head p { margin: 0; color: var(--text-muted); font-size: 13px; max-width: 760px; }
.badge {
display: inline-flex;
align-items: center;
height: 24px;
padding: 0 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
background: var(--primary-bg);
color: var(--primary-dark);
margin-left: 8px;
vertical-align: middle;
}
.panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
}
.panel-top {
padding: 16px 20px 12px;
border-bottom: 1px solid var(--border);
}
.title-bar-heading {
margin: 0;
font-size: 16px;
font-weight: 700;
display: flex;
align-items: center;
gap: 8px;
}
.title-bar-heading::before {
content: "";
width: 3px;
height: 16px;
border-radius: 2px;
background: var(--primary);
}
.list-toolbar {
padding: 12px 20px;
display: flex;
gap: 8px;
align-items: center;
border-bottom: 1px solid var(--border);
}
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
height: 36px;
padding: 0 14px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
border: 1px solid var(--border);
background: #fff;
color: var(--text);
cursor: default;
}
.btn-main {
background: var(--primary);
border-color: var(--primary);
color: #fff;
}
.btn-ghost { background: #fff; }
table.list-table {
width: 100%;
border-collapse: collapse;
}
table.list-table th,
table.list-table td {
padding: 12px 14px;
border-bottom: 1px solid #eef2f7;
text-align: center;
font-size: 13px;
}
table.list-table th {
background: var(--table-head);
color: var(--text-muted);
font-weight: 600;
font-size: 12px;
}
table.list-table tr:nth-child(even) td { background: #fbfdff; }
.link { color: var(--primary-dark); font-weight: 600; text-decoration: none; }
.tag {
display: inline-flex;
align-items: center;
height: 22px;
padding: 0 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
background: var(--primary-bg);
color: var(--primary-dark);
}
.tag.muted { background: #f1f5f9; color: var(--text-muted); }
/* 方案 A:列表缩略图 */
.photo-thumb {
width: 48px;
height: 48px;
border-radius: 8px;
object-fit: cover;
border: 1px solid var(--border);
display: inline-block;
vertical-align: middle;
background: #f1f5f9;
}
.photo-thumb.placeholder {
display: inline-flex;
align-items: center;
justify-content: center;
color: #94a3b8;
font-size: 11px;
background: #f8fafc;
}
/* 方案 B:附件卡片紧凑态 */
.attach-compact {
width: 56px;
height: 40px;
border-radius: 6px;
object-fit: cover;
border: 1px solid var(--border);
display: inline-block;
vertical-align: middle;
}
.compare {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
@media (max-width: 900px) {
.compare { grid-template-columns: 1fr; }
}
.option-card {
border: 2px solid var(--border);
border-radius: var(--radius);
background: var(--surface);
overflow: hidden;
cursor: pointer;
transition: border-color 0.15s, box-shadow 0.15s;
}
.option-card:hover { border-color: #6ee7b7; }
.option-card.selected {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.15);
}
.option-card-head {
padding: 12px 16px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.option-card-head h3 { margin: 0; font-size: 15px; }
.option-card-head p { margin: 2px 0 0; font-size: 12px; color: var(--text-muted); }
.option-card-body { padding: 16px; background: #fbfdff; }
.pick {
width: 18px;
height: 18px;
border-radius: 50%;
border: 2px solid var(--border);
flex-shrink: 0;
position: relative;
}
.option-card.selected .pick {
border-color: var(--primary);
background: var(--primary);
}
.option-card.selected .pick::after {
content: "";
position: absolute;
left: 5px;
top: 2px;
width: 4px;
height: 8px;
border: solid #fff;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
/* 弹窗表单 */
.dialog-mask {
border: 1px solid var(--border);
border-radius: var(--radius);
background: rgba(15, 23, 42, 0.28);
padding: 28px 20px;
display: flex;
justify-content: center;
}
.dialog {
width: min(720px, 100%);
background: #fff;
border-radius: 10px;
box-shadow: 0 20px 40px rgba(15, 23, 42, 0.18);
overflow: hidden;
}
.dialog-header {
padding: 16px 20px;
border-bottom: 1px solid var(--border);
font-weight: 700;
font-size: 16px;
}
.dialog-body { padding: 20px; }
.form-grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.form-item label {
display: block;
font-size: 13px;
font-weight: 600;
margin-bottom: 6px;
color: var(--text);
}
.form-item .control {
height: 36px;
border: 1px solid var(--border);
border-radius: 8px;
background: #fff;
padding: 0 12px;
display: flex;
align-items: center;
color: var(--text-muted);
font-size: 13px;
}
.form-col-span-2 { grid-column: 1 / -1; }
.form-tip {
margin-top: 6px;
font-size: 12px;
color: var(--text-muted);
}
.upload-row {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: flex-start;
}
.upload-card {
width: 140px;
height: 96px;
border: 1px dashed #a7f3d0;
border-radius: 8px;
background: #f8fffb;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
color: var(--primary-dark);
font-size: 13px;
font-weight: 500;
}
.upload-card .plus {
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--primary-bg);
color: var(--primary);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
line-height: 1;
}
.attachment-item {
width: 140px;
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
background: #fff;
}
.attachment-preview {
height: 96px;
background: #f1f5f9;
position: relative;
}
.attachment-preview img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.attachment-footer {
padding: 6px 8px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px;
border-top: 1px solid var(--border);
font-size: 12px;
color: var(--text-muted);
}
.attachment-footer .name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.del {
color: var(--danger);
font-size: 12px;
cursor: default;
}
.dialog-footer {
padding: 12px 20px;
border-top: 1px solid var(--border);
display: flex;
justify-content: flex-end;
gap: 8px;
}
.view-block {
display: grid;
grid-template-columns: 120px 1fr;
gap: 16px;
align-items: start;
padding: 16px;
border: 1px solid var(--border);
border-radius: 8px;
background: #fff;
}
.view-photo {
width: 120px;
height: 120px;
border-radius: 10px;
object-fit: cover;
border: 1px solid var(--border);
background: #f1f5f9;
}
.kv { display: grid; grid-template-columns: 88px 1fr; gap: 8px 12px; font-size: 13px; }
.kv dt { color: var(--text-muted); }
.kv dd { margin: 0; font-weight: 500; }
.note {
margin-top: 12px;
padding: 12px 14px;
border-radius: 8px;
background: var(--primary-bg);
color: var(--primary-dark);
font-size: 13px;
}
.note strong { font-weight: 700; }
.scope-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
@media (max-width: 900px) {
.scope-grid { grid-template-columns: 1fr 1fr; }
.form-grid-2 { grid-template-columns: 1fr; }
}
.scope-item {
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px;
background: #fff;
text-align: center;
}
.scope-item .label { font-weight: 700; margin-bottom: 4px; }
.scope-item .field { font-size: 12px; color: var(--text-muted); }
.scope-item .field code {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 11px;
color: var(--primary-dark);
background: var(--primary-bg);
padding: 1px 6px;
border-radius: 4px;
}
.result-bar {
position: sticky;
bottom: 0;
margin-top: 32px;
padding: 14px 18px;
border: 1px solid var(--border);
border-radius: 10px;
background: #fff;
box-shadow: 0 -4px 20px rgba(15, 23, 42, 0.06);
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.result-bar .summary { font-size: 13px; color: var(--text-muted); }
.result-bar .summary b { color: var(--text); }
</style>
</head>
<body>
<div class="demo-page">
<header class="demo-head">
<h1><span>Fresh Mint</span> · 车辆 / 驾押人员照片字段</h1>
<p>
覆盖四个列表页:三方车辆、山峡自有车辆、三方驾押人员、山峡驾押人员。
车辆与人员各增加一个照片字段,默认只存 <strong>1 张</strong>图片(MinIO URL)。
请点击下方方案卡片选择偏好,便于后续落地实现。
</p>
<nav class="toc">
<a href="#scope">覆盖范围</a>
<a href="#list-style" class="rec">列表展示</a>
<a href="#form">新增/编辑表单</a>
<a href="#view">查看弹窗</a>
</nav>
</header>
<section class="section" id="scope">
<div class="section-head">
<h2>1. 覆盖范围 <span class="badge">两表四页</span></h2>
<p>底层两张表各加一字段;四个页面共用同一套上传控件(AttachmentUploadField,limit=1,accept=image/*)。</p>
</div>
<div class="scope-grid">
<div class="scope-item">
<div class="label">三方车辆</div>
<div class="field">表 <code>pl_vehicle</code><br />字段建议 <code>photo_urls</code></div>
</div>
<div class="scope-item">
<div class="label">山峡自有车辆</div>
<div class="field">同表 <code>pl_vehicle</code><br />车辆照片</div>
</div>
<div class="scope-item">
<div class="label">三方驾押人员</div>
<div class="field">表 <code>pl_personnel</code><br />字段建议 <code>photo_urls</code></div>
</div>
<div class="scope-item">
<div class="label">山峡驾押人员</div>
<div class="field">同表 <code>pl_personnel</code><br />人员照片</div>
</div>
</div>
<div class="note">
<strong>存储约定:</strong>与现有附件一致,库字段存逗号分隔 URL;前端 limit=1,实际默认只保留一张。上传 folder 建议:
<code>pl/vehicle-photo</code> / <code>pl/personnel-photo</code>。
</div>
</section>
<section class="section" id="list-style">
<div class="section-head">
<h2>2. 列表展示形态 <span class="badge">请点选</span></h2>
<p>照片列放在主标识列(车牌号 / 姓名)附近。请选择更偏好的列表展示方式。</p>
</div>
<div class="compare">
<div class="option-card selected" data-group="list" data-value="A" onclick="pick(this)">
<div class="option-card-head">
<div>
<h3>方案 A · 方形缩略图(推荐)</h3>
<p>48×48,object-fit: cover,无图显示「未上传」</p>
</div>
<div class="pick"></div>
</div>
<div class="option-card-body">
<div class="panel">
<div class="panel-top"><h2 class="title-bar-heading">三方车辆</h2></div>
<div class="list-toolbar">
<span class="btn btn-main">+ 新增车辆</span>
<span class="btn btn-ghost">编辑</span>
</div>
<table class="list-table">
<thead>
<tr>
<th style="width:72px">照片</th>
<th>车牌号</th>
<th>状态</th>
<th>承运商</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<img class="photo-thumb" alt="车辆照片"
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop stop-color='%2394a3b8'/%3E%3Cstop offset='1' stop-color='%23cbd5e1'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect fill='url(%23g)' width='96' height='96'/%3E%3Crect x='18' y='42' width='60' height='28' rx='4' fill='%23334155'/%3E%3Ccircle cx='32' cy='70' r='8' fill='%231e293b'/%3E%3Ccircle cx='64' cy='70' r='8' fill='%231e293b'/%3E%3C/svg%3E" />
</td>
<td><a class="link" href="#">皖H12345 / 皖H12346</a></td>
<td><span class="tag">可用</span></td>
<td>承运商1</td>
</tr>
<tr>
<td><span class="photo-thumb placeholder">未上传</span></td>
<td><a class="link" href="#">皖P09872 / 皖S90921</a></td>
<td><span class="tag">可用</span></td>
<td>隆鑫运输</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="option-card" data-group="list" data-value="B" onclick="pick(this)">
<div class="option-card-head">
<div>
<h3>方案 B · 附件卡片紧凑态</h3>
<p>对齐证书附件列样式,略扁的预览卡</p>
</div>
<div class="pick"></div>
</div>
<div class="option-card-body">
<div class="panel">
<div class="panel-top"><h2 class="title-bar-heading">山峡驾押人员</h2></div>
<div class="list-toolbar">
<span class="btn btn-main">+ 新增人员</span>
<span class="btn btn-ghost">编辑</span>
</div>
<table class="list-table">
<thead>
<tr>
<th style="width:80px">照片</th>
<th>姓名</th>
<th>类型</th>
<th>状态</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<img class="attach-compact" alt="人员照片"
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='112' height='80'%3E%3Crect fill='%23e2e8f0' width='112' height='80'/%3E%3Ccircle cx='56' cy='30' r='14' fill='%2394a3b8'/%3E%3Cpath d='M28 68c4-16 20-22 28-22s24 6 28 22' fill='%2394a3b8'/%3E%3C/svg%3E" />
</td>
<td><a class="link" href="#">王驾驶</a></td>
<td><span class="tag">驾驶员</span></td>
<td><span class="tag">在职</span></td>
</tr>
<tr>
<td><span class="tag muted">—</span></td>
<td><a class="link" href="#">李押运</a></td>
<td><span class="tag muted">押运员</span></td>
<td><span class="tag">在职</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</section>
<section class="section" id="form">
<div class="section-head">
<h2>3. 新增 / 编辑弹窗</h2>
<p>沿用协议附件卡片化布局:上传触发卡片 140×96,虚线边框;limit=1,仅图片;与业务字段同表单。</p>
</div>
<div class="dialog-mask">
<div class="dialog">
<div class="dialog-header">新增自有车辆</div>
<div class="dialog-body">
<div class="form-grid-2">
<div class="form-item">
<label>车牌号</label>
<div class="control">请输入车牌号</div>
</div>
<div class="form-item">
<label>挂车号</label>
<div class="control">请输入挂车号</div>
</div>
<div class="form-item">
<label>承运商</label>
<div class="control">请选择承运商</div>
</div>
<div class="form-item">
<label>载重(吨)</label>
<div class="control">如 32</div>
</div>
<div class="form-item form-col-span-2">
<label>车辆照片</label>
<div class="upload-row">
<div class="upload-card" title="已上传 1 张时隐藏此触发区">
<div class="plus">+</div>
<span>上传附件</span>
</div>
<!-- 演示:已上传一张时的状态(实际 limit=1 时上传卡可隐藏) -->
<div class="attachment-item" style="display:none" id="demo-filled">
<div class="attachment-preview">
<img alt="车辆照片"
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='96'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop stop-color='%2394a3b8'/%3E%3Cstop offset='1' stop-color='%23cbd5e1'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect fill='url(%23g)' width='140' height='96'/%3E%3Crect x='30' y='40' width='80' height='32' rx='4' fill='%23334155'/%3E%3Ccircle cx='50' cy='72' r='9' fill='%231e293b'/%3E%3Ccircle cx='90' cy='72' r='9' fill='%231e293b'/%3E%3C/svg%3E" />
</div>
<div class="attachment-footer">
<span class="name">vehicle-front.jpg</span>
<span class="del">删除</span>
</div>
</div>
</div>
<div class="form-tip">仅支持图片,最多 1 张;上传后立即保存到对象存储。</div>
</div>
</div>
</div>
<div class="dialog-footer">
<span class="btn btn-ghost">✕ 取消</span>
<span class="btn btn-main">✓ 确定</span>
</div>
</div>
</div>
<div class="compare" style="margin-top:16px">
<div class="option-card selected" data-group="form" data-value="optional" onclick="pick(this)">
<div class="option-card-head">
<div>
<h3>照片非必填(推荐)</h3>
<p>可先建档后补图,与当前证书附件策略一致</p>
</div>
<div class="pick"></div>
</div>
</div>
<div class="option-card" data-group="form" data-value="required" onclick="pick(this)">
<div class="option-card-head">
<div>
<h3>照片必填</h3>
<p>新增/编辑提交时校验必须有 1 张图</p>
</div>
<div class="pick"></div>
</div>
</div>
</div>
</section>
<section class="section" id="view">
<div class="section-head">
<h2>4. 查看弹窗中的照片</h2>
<p>只读预览:左侧大图,右侧基础信息;无图时显示占位。</p>
</div>
<div class="panel" style="padding:20px">
<div class="view-block">
<img class="view-photo" alt="人员照片"
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Crect fill='%23e2e8f0' width='120' height='120'/%3E%3Ccircle cx='60' cy='44' r='22' fill='%2394a3b8'/%3E%3Cpath d='M22 108c8-28 28-38 38-38s30 10 38 38' fill='%2394a3b8'/%3E%3C/svg%3E" />
<dl class="kv">
<dt>姓名</dt><dd>王驾驶</dd>
<dt>手机号</dt><dd>13089012212</dd>
<dt>人员类型</dt><dd><span class="tag">驾驶员</span></dd>
<dt>归属</dt><dd>山峡驾押人员</dd>
<dt>状态</dt><dd><span class="tag">在职</span></dd>
</dl>
</div>
</div>
</section>
<div class="result-bar">
<div class="summary">
当前选择:列表 <b id="sel-list">方案 A · 方形缩略图</b> · 校验 <b id="sel-form">非必填</b>
</div>
<div style="font-size:12px;color:var(--text-muted)">文件:docs/fresh-mint-pl-vehicle-personnel-photo-demo.html</div>
</div>
</div>
<script>
const labels = {
list: { A: '方案 A · 方形缩略图', B: '方案 B · 附件卡片紧凑态' },
form: { optional: '非必填', required: '必填' }
}
const state = { list: 'A', form: 'optional' }
function pick(el) {
const group = el.dataset.group
const value = el.dataset.value
document.querySelectorAll('.option-card[data-group="' + group + '"]').forEach(function (card) {
card.classList.toggle('selected', card === el)
})
state[group] = value
document.getElementById('sel-list').textContent = labels.list[state.list]
document.getElementById('sel-form').textContent = labels.form[state.form]
if (window.parent !== window) {
try {
window.parent.postMessage({ type: 'demo-select', group: group, value: value, state: state }, '*')
} catch (e) {}
}
}
</script>
</body>
</html>