Files
zkxw/docs/mockups/oa-approval-form-designer-demo.html
T
2026-07-15 16:20:48 +08:00

592 lines
16 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(Fresh Mint)</title>
<style>
:root {
--primary: #10b981;
--primary-dark: #059669;
--primary-bg: #ecfdf5;
--text: #0f172a;
--text-2: #334155;
--muted: #94a3b8;
--border: #e2e8f0;
--border-strong: #cbd5e1;
--bg: #f1f5f9;
--left-w: 280px;
--right-w: 300px;
--menu-w: 44px;
--top-h: 48px;
--radius: 8px;
}
* { box-sizing: border-box; }
html, body {
height: 100%;
margin: 0;
font: 14px/1.5 Inter, system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
color: var(--text);
background: var(--bg);
}
.page {
display: flex;
flex-direction: column;
height: 100%;
min-height: 560px;
padding: 12px 16px 16px;
}
.banner {
display: flex;
flex-wrap: wrap;
gap: 8px 16px;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
padding: 10px 14px;
background: #fff;
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: 13px;
}
.banner b { color: var(--primary-dark); }
.banner span { color: var(--muted); }
.tag {
padding: 2px 8px;
border-radius: 999px;
background: var(--primary-bg);
color: var(--primary-dark);
font-size: 12px;
font-weight: 600;
}
/* 顶部:返回 + Tab(示意,与设计器一致) */
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
height: 34px;
padding: 0 12px;
border: 1px solid var(--border);
border-radius: 8px;
background: #fff;
color: var(--text-2);
font-size: 13px;
cursor: pointer;
}
.btn.primary {
border-color: var(--primary);
background: var(--primary);
color: #fff;
}
.tabs {
display: flex;
justify-content: center;
gap: 8px;
margin: 4px 0 12px;
border-bottom: 1px solid var(--border);
}
.tab {
position: relative;
padding: 10px 18px;
border: 0;
background: none;
color: var(--text-2);
font-size: 15px;
font-weight: 600;
cursor: pointer;
}
.tab.active { color: var(--text); }
.tab.active::after {
content: "";
position: absolute;
right: 12px;
bottom: -1px;
left: 12px;
height: 3px;
background: var(--text);
border-radius: 2px;
}
.hint {
margin: 0 0 10px;
color: var(--muted);
font-size: 13px;
}
/* ===== 核心:左 / 中 / 右 三栏 ===== */
.designer {
display: flex;
flex: 1;
flex-direction: row;
flex-wrap: nowrap;
align-items: stretch;
min-height: 0;
overflow: hidden;
background: #fff;
border: 1px solid var(--border-strong);
border-radius: 10px;
box-shadow: 0 2px 8px rgb(15 23 42 / 8%);
}
/* 左侧极窄图标栏 */
.side-menu {
display: flex;
flex: 0 0 var(--menu-w);
flex-direction: column;
align-items: center;
gap: 4px;
padding-top: 8px;
background: #fff;
border-right: 1px solid var(--border);
}
.side-menu button {
display: grid;
place-items: center;
width: 36px;
height: 36px;
border: 0;
border-radius: 8px;
background: transparent;
color: #64748b;
font-size: 16px;
cursor: pointer;
}
.side-menu button:hover { background: #f1f5f9; }
.side-menu button.active {
color: var(--primary-dark);
background: var(--primary-bg);
}
/* 左:控件库 */
.pane-left {
display: flex;
flex: 0 0 var(--left-w);
flex-direction: column;
min-width: 0;
border-right: 1px solid var(--border);
background: #fff;
}
.pane-head {
display: flex;
flex-shrink: 0;
height: 44px;
border-bottom: 1px solid var(--border);
}
.pane-head button {
flex: 1;
border: 0;
background: none;
color: var(--muted);
font-size: 14px;
font-weight: 600;
cursor: pointer;
}
.pane-head button.active {
color: var(--text);
box-shadow: inset 0 -2px 0 var(--primary);
}
.pane-scroll {
flex: 1;
min-height: 0;
padding: 12px;
overflow: auto;
}
.group-title {
margin: 12px 0 8px;
color: var(--muted);
font-size: 12px;
font-weight: 600;
}
.group-title:first-child { margin-top: 0; }
.ctrl-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.ctrl {
display: flex;
gap: 8px;
align-items: center;
padding: 10px 10px;
cursor: grab;
user-select: none;
background: #f8fafc;
border: 1px solid var(--border);
border-radius: 8px;
font-size: 13px;
color: var(--text-2);
transition: border-color 0.15s, background 0.15s;
}
.ctrl:hover {
background: var(--primary-bg);
border-color: #a7f3d0;
color: var(--primary-dark);
}
.ctrl-ico {
display: grid;
place-items: center;
width: 28px;
height: 28px;
border-radius: 6px;
background: #fff;
border: 1px solid var(--border);
font-size: 14px;
}
/* 中:画布 */
.pane-mid {
display: flex;
flex: 1 1 auto;
flex-direction: column;
min-width: 0;
background: #f8fafc;
}
.mid-tools {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: space-between;
height: var(--top-h);
padding: 0 12px;
background: #fff;
border-bottom: 1px solid var(--border);
}
.mid-tools-l {
display: flex;
gap: 4px;
align-items: center;
}
.tool-ico {
display: grid;
place-items: center;
width: 32px;
height: 32px;
border: 0;
border-radius: 6px;
background: transparent;
color: #64748b;
cursor: pointer;
font-size: 15px;
}
.tool-ico:hover { background: #f1f5f9; }
.tool-ico.active {
color: var(--primary-dark);
background: var(--primary-bg);
}
.tool-line {
width: 1px;
height: 18px;
margin: 0 6px;
background: var(--border);
}
.canvas-area {
display: flex;
flex: 1;
justify-content: center;
align-items: flex-start;
min-height: 0;
padding: 20px;
overflow: auto;
}
.phone {
width: min(420px, 100%);
min-height: 520px;
padding: 16px;
background: #fff;
border: 1px solid var(--border-strong);
border-radius: 12px;
box-shadow: 0 4px 16px rgb(15 23 42 / 6%);
}
.phone.wide { width: min(720px, 100%); }
.field {
margin-bottom: 14px;
padding: 10px 12px;
border: 1px solid transparent;
border-radius: 8px;
cursor: pointer;
}
.field:hover { background: #f8fafc; }
.field.active {
background: var(--primary-bg);
border-color: #a7f3d0;
box-shadow: 0 0 0 2px rgb(16 185 129 / 12%);
}
.field label {
display: block;
margin-bottom: 6px;
color: var(--text-2);
font-size: 13px;
font-weight: 500;
}
.field .fake-input {
height: 36px;
padding: 0 12px;
color: var(--muted);
font-size: 13px;
line-height: 36px;
background: #fff;
border: 1px solid var(--border);
border-radius: 8px;
}
.drop-zone {
margin-top: 8px;
padding: 28px 16px;
color: var(--muted);
font-size: 13px;
text-align: center;
border: 1px dashed #a7f3d0;
border-radius: 8px;
background: #fafffe;
}
/* 右:属性设置 */
.pane-right {
display: flex;
flex: 0 0 var(--right-w);
flex-direction: column;
min-width: 0;
background: #fff;
border-left: 1px solid var(--border);
}
.prop-body {
flex: 1;
min-height: 0;
padding: 14px 16px;
overflow: auto;
}
.prop-body h4 {
margin: 0 0 14px;
font-size: 14px;
font-weight: 600;
}
.prop-item { margin-bottom: 14px; }
.prop-item label {
display: block;
margin-bottom: 6px;
color: var(--muted);
font-size: 12px;
}
.prop-item input,
.prop-item select,
.prop-item textarea {
width: 100%;
height: 36px;
padding: 0 10px;
border: 1px solid var(--border);
border-radius: 8px;
font: inherit;
color: var(--text);
background: #fff;
}
.prop-item textarea {
height: 72px;
padding: 8px 10px;
resize: vertical;
}
.prop-item .row {
display: flex;
gap: 8px;
align-items: center;
}
.switch {
width: 40px;
height: 22px;
border-radius: 999px;
background: var(--primary);
position: relative;
cursor: pointer;
flex-shrink: 0;
}
.switch::after {
content: "";
position: absolute;
top: 2px;
left: 20px;
width: 18px;
height: 18px;
background: #fff;
border-radius: 50%;
}
@media (max-width: 960px) {
:root {
--left-w: 220px;
--right-w: 240px;
}
}
</style>
</head>
<body>
<div class="page">
<div class="banner">
<div>
<b>表单设计 · 三栏布局 Demo</b>
<span>左:控件库 · 中:画布 · 右:选中控件属性(对照钉钉 / FormCreate)</span>
</div>
<span class="tag">Fresh Mint</span>
</div>
<div class="toolbar">
<button type="button" class="btn">← 返回</button>
<div style="display:flex;gap:8px">
<button type="button" class="btn">保存草稿</button>
<button type="button" class="btn primary">发布</button>
</div>
</div>
<div class="tabs">
<button type="button" class="tab">1 基础设置</button>
<button type="button" class="tab active">2 表单设计</button>
<button type="button" class="tab">3 流程设计</button>
<button type="button" class="tab">4 高级设置</button>
</div>
<p class="hint">拖拽左侧控件到中间画布;顶部可切换 PC / 手机预览。选中控件后在右侧配置属性。</p>
<div class="designer" id="designer">
<!-- 极窄模块图标 -->
<aside class="side-menu" aria-label="模块">
<button type="button" class="active" title="组件">▦</button>
<button type="button" title="多语言">🌐</button>
<button type="button" title="JSON">&lt;/&gt;</button>
<button type="button" title="AI" style="color:var(--primary-dark);font-weight:700;font-size:12px">AI</button>
</aside>
<!-- 左:控件 -->
<aside class="pane-left">
<div class="pane-head">
<button type="button" class="active">组件</button>
<button type="button">大纲</button>
</div>
<div class="pane-scroll">
<div class="group-title">布局</div>
<div class="ctrl-grid">
<div class="ctrl"><span class="ctrl-ico">▤</span>分栏</div>
<div class="ctrl"><span class="ctrl-ico">▭</span>分组</div>
</div>
<div class="group-title">基础</div>
<div class="ctrl-grid">
<div class="ctrl"><span class="ctrl-ico">Aa</span>单行</div>
<div class="ctrl"><span class="ctrl-ico"></span>多行</div>
<div class="ctrl"><span class="ctrl-ico">123</span>数字</div>
<div class="ctrl"><span class="ctrl-ico">◉</span>单选</div>
<div class="ctrl"><span class="ctrl-ico">☑</span>多选</div>
<div class="ctrl"><span class="ctrl-ico">📅</span>日期</div>
<div class="ctrl"><span class="ctrl-ico">☎</span>电话</div>
<div class="ctrl"><span class="ctrl-ico">¥</span>金额</div>
</div>
<div class="group-title">增强</div>
<div class="ctrl-grid">
<div class="ctrl"><span class="ctrl-ico">📎</span>附件</div>
<div class="ctrl"><span class="ctrl-ico">🏢</span>部门</div>
<div class="ctrl"><span class="ctrl-ico">📍</span>省市区</div>
<div class="ctrl"><span class="ctrl-ico">ƒ</span>公式</div>
</div>
</div>
</aside>
<!-- 中:画布 -->
<main class="pane-mid">
<div class="mid-tools">
<div class="mid-tools-l">
<button type="button" class="tool-ico" title="PC" data-device="pc">🖥</button>
<button type="button" class="tool-ico active" title="手机" data-device="mobile">📱</button>
<button type="button" class="tool-ico" title="平板" data-device="pad">▣</button>
<span class="tool-line"></span>
<button type="button" class="tool-ico" title="撤销">↶</button>
<button type="button" class="tool-ico" title="重做">↷</button>
</div>
<button type="button" class="btn">👁 预览</button>
</div>
<div class="canvas-area">
<div class="phone" id="phone">
<div class="field" data-name="请假类型">
<label>请假类型</label>
<div class="fake-input">请选择 ▾</div>
</div>
<div class="field active" data-name="开始日期">
<label>开始日期</label>
<div class="fake-input">2026-07-20</div>
</div>
<div class="field" data-name="事由">
<label>事由</label>
<div class="fake-input">请输入</div>
</div>
<div class="drop-zone">拖拽左侧控件到此处</div>
</div>
</div>
</main>
<!-- 右:属性 -->
<aside class="pane-right">
<div class="pane-head">
<button type="button" class="active">控件</button>
<button type="button">表单</button>
</div>
<div class="prop-body">
<h4 id="propTitle">开始日期</h4>
<div class="prop-item">
<label>标题</label>
<input id="propLabel" value="开始日期" />
</div>
<div class="prop-item">
<label>字段名</label>
<input value="startDate" />
</div>
<div class="prop-item">
<label>占位提示</label>
<input value="请选择日期" />
</div>
<div class="prop-item">
<label>必填</label>
<div class="row">
<span class="switch" title="开"></span>
<span style="color:var(--text-2);font-size:13px">是</span>
</div>
</div>
<div class="prop-item">
<label>说明</label>
<textarea>选中画布中的字段后,在此编辑属性(Demo)</textarea>
</div>
</div>
</aside>
</div>
</div>
<script>
const phone = document.getElementById('phone')
const propTitle = document.getElementById('propTitle')
const propLabel = document.getElementById('propLabel')
document.querySelectorAll('.field').forEach((el) => {
el.addEventListener('click', () => {
document.querySelectorAll('.field').forEach((f) => f.classList.remove('active'))
el.classList.add('active')
const name = el.dataset.name || '控件'
propTitle.textContent = name
propLabel.value = name
})
})
document.querySelectorAll('.tool-ico[data-device]').forEach((btn) => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tool-ico[data-device]').forEach((b) => b.classList.remove('active'))
btn.classList.add('active')
phone.classList.toggle('wide', btn.dataset.device === 'pc')
})
})
</script>
</body>
</html>