Files
2026-05-23 14:05:22 +08:00

309 lines
12 KiB
HTML
Raw Permalink 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>步骤详解 - 创建 Vue 渲染组件</title>
<style>
:root {
--bg-primary: #0a0e1a;
--bg-secondary: #111827;
--bg-card: #1a2035;
--bg-card-hover: #1f2a45;
--border-color: #2d3a5c;
--text-primary: #e2e8f0;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--accent-green: #10b981;
--accent-green-light: #34d399;
--accent-cyan: #06b6d4;
--accent-purple: #8b5cf6;
--shadow-lg: 0 10px 40px rgba(0, 0, 0, 0.4);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.7;
padding: 2rem;
}
.container { max-width: 1000px; margin: 0 auto; }
.back-link {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: var(--accent-green-light);
text-decoration: none;
margin-bottom: 2rem;
padding: 0.5rem 1rem;
background: var(--bg-card);
border-radius: 8px;
border: 1px solid var(--border-color);
transition: all 0.2s ease;
}
.back-link:hover { background: var(--bg-card-hover); transform: translateX(-4px); }
.header {
background: var(--bg-card);
border-radius: 16px;
padding: 2rem;
margin-bottom: 2rem;
border: 1px solid var(--border-color);
box-shadow: var(--shadow-lg);
border-left: 4px solid var(--accent-green);
}
.step-badge {
display: inline-block;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
padding: 0.3rem 1rem;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
margin-bottom: 1rem;
}
.header h1 { font-size: 2rem; margin-bottom: 0.5rem; }
.header .desc { color: var(--text-secondary); font-size: 1rem; }
.section {
background: var(--bg-card);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid var(--border-color);
}
.section h2 {
font-size: 1.3rem;
margin-bottom: 1rem;
color: var(--accent-green-light);
display: flex;
align-items: center;
gap: 0.5rem;
}
.section p { color: var(--text-secondary); margin-bottom: 1rem; }
.code-block {
background: var(--bg-secondary);
padding: 1.25rem;
border-radius: 8px;
font-family: 'Fira Code', 'Cascadia Code', monospace;
font-size: 0.85rem;
color: var(--accent-cyan);
overflow-x: auto;
line-height: 1.6;
margin-bottom: 1rem;
}
.code-block .comment { color: var(--text-muted); }
.code-block .tag { color: var(--accent-green-light); }
.code-block .attr { color: var(--accent-purple); }
.code-block .string { color: #fbbf24; }
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.info-card {
background: var(--bg-secondary);
padding: 1rem;
border-radius: 8px;
border: 1px solid var(--border-color);
}
.info-card h4 { font-size: 0.9rem; margin-bottom: 0.5rem; color: var(--accent-green-light); }
.info-card p { font-size: 0.85rem; color: var(--text-secondary); margin: 0; }
.file-path {
font-family: 'Fira Code', monospace;
font-size: 0.8rem;
color: var(--accent-cyan);
background: rgba(6, 182, 212, 0.1);
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
ul { list-style: none; padding: 0; }
ul li { padding: 0.4rem 0; color: var(--text-secondary); display: flex; align-items: flex-start; gap: 0.5rem; }
ul li::before { content: "▸"; color: var(--accent-green); flex-shrink: 0; }
.warning {
background: rgba(245, 158, 11, 0.1);
border: 1px solid rgba(245, 158, 11, 0.3);
border-radius: 8px;
padding: 1rem;
margin-top: 1rem;
}
.warning h4 { color: #fbbf24; margin-bottom: 0.5rem; }
.warning p { color: var(--text-secondary); margin: 0; font-size: 0.9rem; }
.tip {
background: rgba(6, 182, 212, 0.1);
border: 1px solid rgba(6, 182, 212, 0.3);
border-radius: 8px;
padding: 1rem;
margin-top: 1rem;
}
.tip h4 { color: var(--accent-cyan); margin-bottom: 0.5rem; }
.tip p { color: var(--text-secondary); margin: 0; font-size: 0.9rem; }
</style>
</head>
<body>
<div class="container">
<a href="../../物料组件添加流程.html" class="back-link">← 返回主文档</a>
<div class="header">
<div class="step-badge">步骤 2</div>
<h1>创建 Vue 渲染组件</h1>
<p class="desc">定义物料组件的实际 UI 结构和渲染逻辑</p>
</div>
<div class="section">
<h2>📋 功能描述</h2>
<p>创建 Vue 单文件组件 (.vue),定义物料组件在画布和运行时的实际渲染内容。这是用户在页面设计器中拖拽后看到的视觉效果,也是最终用户在使用低代码页面时看到的 UI 元素。</p>
</div>
<div class="section">
<h2>🧩 模块描述</h2>
<p>该组件位于 form-designer 的 widget 目录下,主要职责:</p>
<ul>
<li><strong>画布渲染</strong> - 在页面设计器画布中显示组件预览</li>
<li><strong>运行时渲染</strong> - 在最终用户访问页面时渲染实际 UI</li>
<li><strong>属性绑定</strong> - 接收来自 Helper buildWidget 生成的 schema props</li>
<li><strong>事件处理</strong> - 处理用户交互和组件事件</li>
</ul>
</div>
<div class="section">
<h2>📁 文件信息</h2>
<div class="info-grid">
<div class="info-card">
<h4>文件路径</h4>
<p><span class="file-path">packages/form-designer/src/config/widget/&lt;组件名&gt;.vue</span></p>
</div>
<div class="info-card">
<h4>操作类型</h4>
<p><strong style="color: #34d399;">新建</strong> - 创建全新的 Vue 组件文件</p>
</div>
<div class="info-card">
<h4>组件模式</h4>
<p>简单组件直接建 .vue 文件,复杂组件可建目录</p>
</div>
<div class="info-card">
<h4>所属层级</h4>
<p>页面设计器层 (packages/form-designer)</p>
</div>
</div>
</div>
<div class="section">
<h2>💻 代码示例 - 简单组件</h2>
<p>适用于 CommonBtn、CommonTxt 等展示型组件:</p>
<div class="code-block">
<span class="comment">&lt;!-- packages/form-designer/src/config/widget/MyNewComponent.vue --&gt;</span>
&lt;<span class="tag">template</span>&gt;
&lt;<span class="tag">div</span> <span class="attr">class</span>=<span class="string">"my-new-component"</span>&gt;
{{ displayContent }}
&lt;/<span class="tag">div</span>&gt;
&lt;/<span class="tag">template</span>&gt;
&lt;<span class="tag">script</span> <span class="attr">setup</span> <span class="attr">lang</span>=<span class="string">"ts"</span>&gt;
import { computed } from 'vue'
const props = defineProps&lt;{
content?: string
fontSize?: string
color?: string
}&gt;()
const displayContent = computed(() =&gt; {
return props.content || '默认文本'
})
&lt;/<span class="tag">script</span>&gt;
&lt;<span class="tag">style</span> <span class="attr">scoped</span>&gt;
.my-new-component {
padding: 8px 16px;
font-size: v-bind(fontSize);
color: v-bind(color);
}
&lt;/<span class="tag">style</span>&gt;
</div>
</div>
<div class="section">
<h2>💻 代码示例 - 复杂组件(目录模式)</h2>
<p>适用于 Anchor、CommonTree 等包含子组件和工具函数的复杂组件:</p>
<div class="code-block">
<span class="comment">// 目录结构:</span>
<span class="comment">// packages/form-designer/src/config/widget/anchor/</span>
<span class="comment">// ├── index.vue # 主组件入口</span>
<span class="comment">// ├── AnchorIcon.vue # 子组件</span>
<span class="comment">// ├── Constant.ts # 常量/默认数据</span>
<span class="comment">// ├── hooks.ts # 组合式函数</span>
<span class="comment">// └── utils.ts # 工具函数</span>
<span class="comment">// packages/form-designer/src/config/widget/anchor/index.vue</span>
&lt;<span class="tag">template</span>&gt;
&lt;<span class="tag">div</span> <span class="attr">class</span>=<span class="string">"anchor-container"</span>&gt;
&lt;<span class="tag">AnchorIcon</span> /&gt;
&lt;<span class="tag">ul</span>&gt;
&lt;<span class="tag">li</span> <span class="attr">v-for</span>=<span class="string">"item in anchors"</span> <span class="attr">:key</span>=<span class="string">"item.id"</span>&gt;
{{ item.label }}
&lt;/<span class="tag">li</span>&gt;
&lt;/<span class="tag">ul</span>&gt;
&lt;/<span class="tag">div</span>&gt;
&lt;/<span class="tag">template</span>&gt;
&lt;<span class="tag">script</span> <span class="attr">setup</span> <span class="attr">lang</span>=<span class="string">"ts"</span>&gt;
import { useAnchorHooks } from './hooks'
import { DEFAULT_ANCHORS } from './Constant'
const props = defineProps&lt;{
anchors?: Array&lt;{ id: string; label: string }&gt;
}&gt;()
const { anchors } = useAnchorHooks(props.anchors ?? DEFAULT_ANCHORS)
&lt;/<span class="tag">script</span>&gt;
</div>
</div>
<div class="section">
<h2>📝 Props 来源</h2>
<p>组件接收的 props 来自 Helper 的 <code>buildWidget()</code> 方法返回的 schema</p>
<div class="code-block">
<span class="comment">// Helper 中的 buildWidget 返回:</span>
{
type: WidgetType.MyNewComponent,
props: {
content: <span class="string">'默认文本'</span>,
fontSize: <span class="string">'14px'</span>,
color: <span class="string">'#333'</span>
}
}
<span class="comment">// 这些 props 会自动传递给 Vue 组件</span>
</div>
</div>
<div class="section">
<h2>⚠️ 注意事项</h2>
<div class="warning">
<h4>样式隔离</h4>
<p>建议使用 <code>&lt;style scoped&gt;</code> 或 CSS Modules,避免全局样式污染。如确需全局样式,使用 <code>:global()</code> 明确声明。</p>
</div>
<div class="tip" style="margin-top: 1rem;">
<h4>组件设计建议</h4>
<p>保持组件简单专注,只负责自身渲染逻辑。复杂业务逻辑应抽取到 composables 或 utils 中。</p>
</div>
<ul style="margin-top: 1rem;">
<li>组件名必须与 WidgetType 枚举值保持一致</li>
<li>确保组件在没有 props 的情况下也能正常渲染(使用默认值)</li>
<li>复杂组件建议拆分到独立目录,便于维护</li>
</ul>
</div>
<div class="section" style="border-left: 4px solid var(--accent-green);">
<h2>➡️ 下一步</h2>
<p>完成 Vue 组件创建后,继续执行:</p>
<ul>
<li><strong>步骤 3</strong>:创建 Helper 配置文件(定义物料面板、属性面板、拖拽行为等)</li>
<li><strong>步骤 4</strong>:在 RegisterWidget.ts 中注册组件实例</li>
</ul>
</div>
</div>
</body>
</html>